diff --git a/.github/workflows/capability-map.yml b/.github/workflows/capability-map.yml new file mode 100644 index 000000000..635a89303 --- /dev/null +++ b/.github/workflows/capability-map.yml @@ -0,0 +1,28 @@ +name: capability-map +on: + pull_request: + branches: [develop, main] + paths: + - 'src/tools/**' + - 'src/types/mcp.ts' + - 'src/pilot/handoff/tool.ts' + - 'src/pilot/handoff/definitions.ts' + - 'scripts/gen-capability-map.ts' + - 'docs/agent/capability-map.md' +jobs: + drift-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci --prefer-offline --no-audit + - run: npm run gen:capability-map + - name: Verify no drift + run: | + if ! git diff --exit-code docs/agent/capability-map.md; then + echo "::error::capability-map drift — run 'npm run gen:capability-map' and commit the result" + exit 1 + fi diff --git a/docs/agent/README.md b/docs/agent/README.md new file mode 100644 index 000000000..a5bfcd57f --- /dev/null +++ b/docs/agent/README.md @@ -0,0 +1,47 @@ +# openchrome agent preamble + +`capability-map.md` is an auto-generated, drift-guarded summary of every MCP +tool exposed by openchrome. It is designed to be prepended to an agent's system +prompt so the agent knows what tools are available without calling `tools/list`. + +## Loading the capability map with the Anthropic SDK + +```typescript +import Anthropic from '@anthropic-ai/sdk'; +import * as fs from 'fs'; +import * as path from 'path'; + +const capabilityMap = fs.readFileSync( + path.join(__dirname, 'capability-map.md'), + 'utf8' +); + +const client = new Anthropic(); + +const response = await client.messages.create({ + model: 'claude-sonnet-4-5', + max_tokens: 4096, + system: `You are a browser-automation agent with access to the openchrome MCP server.\n\n${capabilityMap}`, + messages: [{ role: 'user', content: 'Navigate to https://example.com and return the page title.' }], +}); +``` + +## Keeping the map up to date + +The map is regenerated from the live tool registry: + +```bash +npm run gen:capability-map +``` + +A CI workflow (`.github/workflows/capability-map.yml`) fails any PR that +modifies tool files without regenerating the map, preventing drift between +source and documentation. + +## File constraints + +- Maximum size: **6 144 bytes** (fits comfortably in a system-prompt slot). +- If params lines push the file over the limit, the generator automatically + drops them and retains tool names + descriptions only. +- `expand_tools` is intentionally excluded — it is a server-injected + progressive-disclosure hint, not a stable registered tool. diff --git a/docs/agent/capability-map.md b/docs/agent/capability-map.md new file mode 100644 index 000000000..8a9204832 --- /dev/null +++ b/docs/agent/capability-map.md @@ -0,0 +1,134 @@ + +# openchrome MCP tools (auto-generated) + +## dom +- `find`: Find elements by query. Retur… +- `page_content`: Get HTML content from page or… +- `query_dom`: Query DOM elements via CSS se… +- `vision_find`: Find elements using vision-ba… + +## evidence +- `oc_assert`: Evaluate a single Outcome Con… +- `oc_evidence_bundle`: Capture a snapshot of the cur… +- `oc_skill_recall`: Retrieve skills from the JSON… +- `oc_skill_record`: Record a skill (domain, name,… + +## forms +- `file_upload`: Upload files to a file input … +- `fill_form`: Fill form fields and optional… +- `form_input`: Set one form element value by… + +## interact +- `act`: Execute multi-step browser ac… +- `computer`: Mouse, keyboard, and screensh… +- `drag_drop`: Drag and drop by selector or … +- `interact`: Find an element by natural la… +- `lightweight_scroll`: Scroll page via JS. Returns n… + +## js +- `javascript_tool`: Execute JavaScript in page co… + +## lifecycle +- `oc_checkpoint`: Save or load an automation ch… +- `oc_connection_health`: Get CDP connection health met… +- `oc_journal`: Query the tool call journal. … +- `oc_reap_orphans`: Manually sweep and terminate … +- `oc_session_resume`: Restore working context after… +- `oc_session_snapshot`: Save browser state snapshot f… +- `oc_stop`: Shut down OpenChrome and clos… +- `page_reload`: Reload the current page. +- `wait_for`: Wait for a condition. Strongl… + +## misc +- `batch_execute`: Execute JS across multiple ta… +- `batch_paginate`: Extract content from paginate… +- `crawl`: Recursively crawl a website v… +- `crawl_cancel`: Mark a crawl job as cancelled… +- `crawl_sitemap`: Crawl a website using its sit… +- `crawl_start`: Initialise a resumable crawl … +- `crawl_status`: Advance a crawl job by up to … +- `execute_plan`: Execute a cached plan by ID, … +- `extract_data`: Extract structured data from … +- `network_capture_full`: Capture network requests with… +- `network_capture_lite`: Capture network request metad… +- `oc_context_export`: Export the active tab's auth-… +- `oc_context_import`: Strict-replace import of a `C… +- `oc_copy_to_clipboard`: Copy text to the system clipb… +- `oc_devtools_url`: Get the Chrome DevTools inspe… +- `oc_doctor_report`: Read the most recent openchro… +- `oc_get_connection_info`: Get connection configuration … +- `oc_normalize_action`: Validate and normalize a near… +- `oc_observe`: Deterministic, numbered list … +- `oc_open_host_settings`: Open the MCP connector settin… +- `oc_performance_analyze`: Drill into one named insight … +- `oc_performance_insights`: Capture a CDP performance tra… +- `oc_progress_status`: Read-only diagnostics for whe… +- `oc_recording_status`: Report whether session record… +- `oc_reflect`: Create, get, or list structur… +- `oc_run_events`: Return recent events for an o… +- `oc_run_finish`: Finish an opt-in OpenChrome r… +- `oc_run_start`: Start an opt-in OpenChrome ru… +- `oc_run_status`: Return the current status and… +- `oc_task_cancel`: Request cancellation of a bac… +- `oc_task_get`: Fetch a single task by task_i… +- `oc_task_list`: List background tasks in the … +- `oc_task_run_checkpoint`: Write a compact caller-provid… +- `oc_task_run_complete`: Enter a terminal TaskRun stat… +- `oc_task_run_get`: Read a TaskRun meta record an… +- `oc_task_run_list`: List recent TaskRuns sorted b… +- `oc_task_run_needs_help`: Move a non-terminal TaskRun t… +- `oc_task_run_start`: Start an opt-in goal-level Ta… +- `oc_task_run_update`: Update a non-terminal TaskRun… +- `oc_task_start`: Launch a long-running tool as… +- `oc_task_wait`: Block until the task reaches … +- `oc_totp_generate`: Generate a current TOTP 2FA c… +- `read_page`: Get page as DOM, accessibilit… +- `worker`: Manage workers. Actions: "cre… +- `worker_complete`: Mark a worker as complete wit… +- `worker_update`: Report worker progress to the… +- `workflow_cleanup`: Clean up workflow resources (… +- `workflow_collect`: Collect and aggregate results… +- `workflow_collect_partial`: Collect results from complete… +- `workflow_init`: Initialize a workflow with mu… +- `workflow_status`: Get current workflow status a… + +## navigation +- `navigate`: Navigate to URL or go forward… + +## observability +- `console_capture`: Capture browser console outpu… +- `inspect`: Extract focused page state by… +- `network`: Simulate network conditions. +- `page_pdf`: Generate PDF from page. Saves… +- `page_screenshot`: Save page screenshot to file … +- `performance_metrics`: Get page performance metrics. +- `request_intercept`: Intercept network requests (l… +- `validate_page`: Composite health check: navig… + +## pilot +- `oc_pilot_handoff_create` — pilot: Pilot-tier: mint a single-use… +- `oc_pilot_handoff_redeem` — pilot: Pilot-tier: redeem a single-u… + +## profile +- `emulate_device`: Emulate device viewport and U… +- `geolocation`: Set or clear geolocation over… +- `http_auth`: Set or clear HTTP auth creden… +- `list_profiles`: List available Chrome profile… +- `oc_profile_status`: Check browser profile type an… +- `user_agent`: Set or reset browser user age… + +## recording +- `oc_recording_export`: Export a recording as JSON or… +- `oc_recording_list`: List available session record… +- `oc_recording_start`: Start a new session recording… +- `oc_recording_stop`: Stop the active session recor… + +## storage +- `cookies`: Manage browser cookies (get, … +- `memory`: Manage domain knowledge. Acti… +- `storage`: Manage browser localStorage a… + +## tabs +- `tabs_close`: Close one or more tabs by tab… +- `tabs_context`: Get session tab IDs grouped b… +- `tabs_create`: Create a new tab with URL. diff --git a/package.json b/package.json index d976a8e7f..286a3afa4 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "clean": "rimraf dist", "prepare": "npm run build", "lint:changed": "node scripts/lint-changed-src.js", + "gen:capability-map": "ts-node scripts/gen-capability-map.ts", "harness:parallel-smoke": "ts-node tests/harness/parallel-smoke.ts" }, "keywords": [ diff --git a/scripts/gen-capability-map.ts b/scripts/gen-capability-map.ts new file mode 100644 index 000000000..39cf5f596 --- /dev/null +++ b/scripts/gen-capability-map.ts @@ -0,0 +1,269 @@ +#!/usr/bin/env ts-node +/** + * Capability-map generator for openchrome MCP tools. + * + * Introspects every tool registered by registerAllTools() in src/tools/index.ts + * and emits docs/agent/capability-map.md — a compact, drift-guarded preamble + * (~2–6 KB) that MCP clients can prepend to their system prompt. + * + * Why expand_tools is excluded: + * expand_tools is a synthetic, server-injected tool defined inline in + * src/mcp-server.ts (see the handleToolsList method). It is NOT registered + * via registerTool() and therefore never appears in the tool registry that + * registerAllTools() populates. It is a progressive-disclosure hint, not a + * stable MCP tool, and would mislead agents if included in the preamble. + * + * Pilot-only tools (oc_pilot_handoff_create, oc_pilot_handoff_redeem) are + * registered by bootstrapPilot() via dynamic import — not by registerAllTools(). + * Their definitions are exported from src/pilot/handoff/tool.ts and imported + * here so the generated preamble has a single source of truth. + * + * Implementation note: + * src/mcp-server.ts imports many heavy runtime modules (puppeteer-core, + * Chrome launcher, dashboard, etc.) that are not available in the generator + * context. We inject a minimal stub for 'src/mcp-server' into the Node + * require cache before loading the tool modules, so registerAllTools() works + * without requiring a running Chrome instance or built artifacts. + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +// --------------------------------------------------------------------------- +// Minimal MCPToolDefinition type (mirrors src/types/mcp.ts). +// We define it inline so this script does not import from src/types/mcp.ts +// (which is safe, but keeps the dependency surface explicit). +// --------------------------------------------------------------------------- +interface InputSchema { + type: string; + properties?: Record; + required?: string[]; +} + +interface ToolDefinition { + name: string; + description: string; + category?: string; + inputSchema: InputSchema; +} + +// --------------------------------------------------------------------------- +// Stub MCPServer class — satisfies the interface expected by registerAllTools. +// Only registerTool and getToolNames are used; everything else is a no-op. +// --------------------------------------------------------------------------- +class StubMCPServer { + private _tools: Map = new Map(); + + registerTool(name: string, _handler: unknown, definition: ToolDefinition): void { + this._tools.set(name, definition); + } + + getToolNames(): string[] { + return Array.from(this._tools.keys()); + } + + getDefinitions(): ToolDefinition[] { + return Array.from(this._tools.values()); + } +} + +// --------------------------------------------------------------------------- +// Inject stub into require cache before tool modules are loaded. +// This prevents ts-node from trying to resolve src/mcp-server.ts and its +// heavy transitive dependencies (puppeteer-core, CDP, dashboard, etc.). +// --------------------------------------------------------------------------- +function injectMCPServerStub(): void { + const stubModule = { + id: 'stub:mcp-server', + filename: 'stub:mcp-server', + loaded: true, + parent: null, + children: [], + paths: [], + exports: { MCPServer: StubMCPServer }, + require: require, + // Node ≥18: Module._extensions needs .id on the object + }; + + // Resolve the real mcp-server path so we can key the cache correctly. + // ts-node resolves .ts files, so we look up the .ts path. + const serverTsPath = path.resolve(__dirname, '..', 'src', 'mcp-server.ts'); + const serverJsPath = path.resolve(__dirname, '..', 'src', 'mcp-server.js'); + + // Inject into require.cache under both possible keys + (require as NodeJS.Require & { cache: Record }).cache[serverTsPath] = + stubModule as unknown as NodeJS.Module; + (require as NodeJS.Require & { cache: Record }).cache[serverJsPath] = + stubModule as unknown as NodeJS.Module; +} + +// --------------------------------------------------------------------------- +// Tool collection +// --------------------------------------------------------------------------- +function collectStandardTools(): ToolDefinition[] { + injectMCPServerStub(); + + // Now safe to require tool index — it will use the stub MCPServer + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { registerAllTools } = require('../src/tools/index') as { + registerAllTools: (server: StubMCPServer) => void; + }; + + const server = new StubMCPServer(); + registerAllTools(server); + return server.getDefinitions(); +} + +/** + * Pilot-only tools registered by bootstrapPilot(), not registerAllTools(). + * Import their exported definitions directly to avoid metadata drift. + */ +function collectPilotTools(): ToolDefinition[] { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { PILOT_HANDOFF_TOOL_DEFINITIONS } = require('../src/pilot/handoff/definitions') as { + PILOT_HANDOFF_TOOL_DEFINITIONS: readonly ToolDefinition[]; + }; + return [...PILOT_HANDOFF_TOOL_DEFINITIONS]; +} + +// --------------------------------------------------------------------------- +// Markdown generation +// --------------------------------------------------------------------------- +function categoryOf(def: ToolDefinition): string { + return def.category ?? 'misc'; +} + +function formatParams(def: ToolDefinition): string { + const props = def.inputSchema?.properties ?? {}; + const required = new Set(def.inputSchema?.required ?? []); + return Object.entries(props) + .map(([name, schema]) => { + const s = schema as Record; + const type = typeof s.type === 'string' ? s.type : 'any'; + return required.has(name) ? `${name}:${type}` : `${name}?:${type}`; + }) + .join(', '); +} + +const CATEGORY_ORDER: string[] = [ + 'dom', + 'evidence', + 'forms', + 'interact', + 'js', + 'lifecycle', + 'misc', + 'navigation', + 'observability', + 'pilot', + 'profile', + 'recording', + 'storage', + 'tabs', +]; + +function truncateDesc(desc: string, maxLen: number): string { + const oneLine = desc.replace(/\s+/g, ' ').trim(); + if (oneLine.length <= maxLen) return oneLine; + return oneLine.slice(0, maxLen - 1) + '…'; +} + +function buildMarkdown( + tools: ToolDefinition[], + includeParams: boolean, + descMaxLen: number = Infinity +): string { + const sorted = [...tools].sort((a, b) => { + const ca = categoryOf(a); + const cb = categoryOf(b); + if (ca !== cb) return ca.localeCompare(cb); + return a.name.localeCompare(b.name); + }); + + const grouped = new Map(); + for (const def of sorted) { + const cat = categoryOf(def); + if (!grouped.has(cat)) grouped.set(cat, []); + grouped.get(cat)!.push(def); + } + + const categoryKeys = [ + ...CATEGORY_ORDER.filter(c => grouped.has(c)), + ...[...grouped.keys()].filter(c => !CATEGORY_ORDER.includes(c)).sort(), + ]; + + const lines: string[] = [ + '', + '# openchrome MCP tools (auto-generated)', + '', + ]; + + for (const cat of categoryKeys) { + const catTools = grouped.get(cat) ?? []; + lines.push(`## ${cat}`); + for (const def of catTools) { + const pilotMarker = categoryOf(def) === 'pilot' ? ' — pilot' : ''; + const desc = truncateDesc(def.description, descMaxLen); + lines.push(`- \`${def.name}\`${pilotMarker}: ${desc}`); + if (includeParams) { + const params = formatParams(def); + if (params) { + lines.push(` - params: \`${params}\``); + } + } + } + lines.push(''); + } + + return lines.join('\n').trimEnd() + '\n'; +} + +// --------------------------------------------------------------------------- +// Entry point +// --------------------------------------------------------------------------- +const OUTPUT_PATH = path.join(__dirname, '..', 'docs', 'agent', 'capability-map.md'); +const MAX_BYTES = 6144; + +function main(): void { + const standardTools = collectStandardTools(); + const pilotTools = collectPilotTools(); + const allTools = [...standardTools, ...pilotTools]; + + // Progressive fallback to stay within MAX_BYTES: + // 1. Full output with params sub-lines + // 2. Drop params, keep full descriptions + // 3. Drop params, truncate descriptions to 120 chars + // 4. Drop params, truncate descriptions to 80 chars + // 5. Drop params, truncate descriptions to 40 chars + // 6. Drop params, truncate descriptions to 30 chars + const attempts: Array<[boolean, number]> = [ + [true, Infinity], + [false, Infinity], + [false, 120], + [false, 80], + [false, 40], + [false, 30], + ]; + + let content = ''; + for (const [includeParams, descMaxLen] of attempts) { + content = buildMarkdown(allTools, includeParams, descMaxLen); + if (Buffer.byteLength(content, 'utf8') <= MAX_BYTES) break; + } + + const byteSize = Buffer.byteLength(content, 'utf8'); + if (byteSize > MAX_BYTES) { + throw new Error( + `capability-map.md exceeds ${MAX_BYTES} bytes (got ${byteSize}) even after truncation. ` + + `The tool count or description lengths are too large.` + ); + } + + fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true }); + fs.writeFileSync(OUTPUT_PATH, content, 'utf8'); + console.error( + `[gen-capability-map] Wrote ${byteSize} bytes to ${path.relative(process.cwd(), OUTPUT_PATH)}` + ); +} + +main(); diff --git a/src/dashboard/activity-tracker.ts b/src/dashboard/activity-tracker.ts index 96e87940d..e4b2c8ce5 100644 --- a/src/dashboard/activity-tracker.ts +++ b/src/dashboard/activity-tracker.ts @@ -5,7 +5,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { EventEmitter } from 'events'; -import type { ToolCallEvent, ToolCallResult } from './types.js'; +import type { ToolCallEvent, ToolCallResult } from './types'; export interface ActivityTrackerEvents { 'call:start': (event: ToolCallEvent) => void; diff --git a/src/dashboard/index.ts b/src/dashboard/index.ts index f8d4166bf..c5d22521e 100644 --- a/src/dashboard/index.ts +++ b/src/dashboard/index.ts @@ -5,17 +5,17 @@ */ import { EventEmitter } from 'events'; -import { getVersion } from '../version.js'; -import { Renderer, getRenderer } from './renderer.js'; -import { KeyboardHandler, getKeyboardHandler, KeyEvent } from './keyboard-handler.js'; -import { ActivityTracker, getActivityTracker } from './activity-tracker.js'; -import { OperationController, getOperationController } from './operation-controller.js'; -import { MainView, MainViewData } from './views/main-view.js'; -import { SessionsView, SessionsViewData } from './views/sessions-view.js'; -import { TabsView, TabsViewData } from './views/tabs-view.js'; -import { ConnectView, ConnectViewData } from './views/connect-view.js'; -import type { ViewMode, DashboardStats, SessionInfo, TabInfo } from './types.js'; -import type { SessionManager } from '../session-manager.js'; +import { getVersion } from '../version'; +import { Renderer, getRenderer } from './renderer'; +import { KeyboardHandler, getKeyboardHandler, KeyEvent } from './keyboard-handler'; +import { ActivityTracker, getActivityTracker } from './activity-tracker'; +import { OperationController, getOperationController } from './operation-controller'; +import { MainView, MainViewData } from './views/main-view'; +import { SessionsView, SessionsViewData } from './views/sessions-view'; +import { TabsView, TabsViewData } from './views/tabs-view'; +import { ConnectView, ConnectViewData } from './views/connect-view'; +import type { ViewMode, DashboardStats, SessionInfo, TabInfo } from './types'; +import type { SessionManager } from '../session-manager'; export interface DashboardOptions { enabled?: boolean; @@ -493,9 +493,9 @@ export function setDashboard(dashboard: Dashboard): void { } // Re-export components -export { ActivityTracker, getActivityTracker, setActivityTracker } from './activity-tracker.js'; -export { OperationController, getOperationController, setOperationController } from './operation-controller.js'; -export { KeyboardHandler, getKeyboardHandler } from './keyboard-handler.js'; -export { Renderer, getRenderer } from './renderer.js'; -export * from './types.js'; -export * from './ansi.js'; +export { ActivityTracker, getActivityTracker, setActivityTracker } from './activity-tracker'; +export { OperationController, getOperationController, setOperationController } from './operation-controller'; +export { KeyboardHandler, getKeyboardHandler } from './keyboard-handler'; +export { Renderer, getRenderer } from './renderer'; +export * from './types'; +export * from './ansi'; diff --git a/src/dashboard/renderer.ts b/src/dashboard/renderer.ts index 8f64d5790..b82ea7050 100644 --- a/src/dashboard/renderer.ts +++ b/src/dashboard/renderer.ts @@ -2,8 +2,8 @@ * Renderer - Screen rendering to stderr */ -import { ANSI, horizontalLine, BOX, stripAnsi } from './ansi.js'; -import type { ScreenSize } from './types.js'; +import { ANSI, horizontalLine, BOX, stripAnsi } from './ansi'; +import type { ScreenSize } from './types'; export class Renderer { private lastFrame: string = ''; diff --git a/src/dashboard/views/connect-view.ts b/src/dashboard/views/connect-view.ts index 2b59d381b..e6bf336a9 100644 --- a/src/dashboard/views/connect-view.ts +++ b/src/dashboard/views/connect-view.ts @@ -3,12 +3,12 @@ * Part of #523: Desktop App Web host connection guide. */ -import { ANSI, truncate, BOX, horizontalLine } from '../ansi.js'; -import type { ScreenSize } from '../types.js'; -import { Renderer } from '../renderer.js'; -import type { WebAIHostId, ConnectionInfo } from '../../connect/types.js'; -import { generateConnectionInfo, getHostIds } from '../../connect/index.js'; -import type { ServerConnectionState } from '../../connect/types.js'; +import { ANSI, truncate, BOX, horizontalLine } from '../ansi'; +import type { ScreenSize } from '../types'; +import { Renderer } from '../renderer'; +import type { WebAIHostId, ConnectionInfo } from '../../connect/types'; +import { generateConnectionInfo, getHostIds } from '../../connect/index'; +import type { ServerConnectionState } from '../../connect/types'; export interface ConnectViewData { /** Currently selected host index */ diff --git a/src/dashboard/views/main-view.ts b/src/dashboard/views/main-view.ts index 796b60869..45c059477 100644 --- a/src/dashboard/views/main-view.ts +++ b/src/dashboard/views/main-view.ts @@ -2,9 +2,9 @@ * Main View - Activity log and stats display */ -import { ANSI, formatTime, formatDuration, formatUptime, formatBytes, truncate, pad, BOX, horizontalLine } from '../ansi.js'; -import type { ToolCallEvent, DashboardStats, ScreenSize } from '../types.js'; -import { Renderer } from '../renderer.js'; +import { ANSI, formatTime, formatDuration, formatUptime, formatBytes, truncate, pad, BOX, horizontalLine } from '../ansi'; +import type { ToolCallEvent, DashboardStats, ScreenSize } from '../types'; +import { Renderer } from '../renderer'; export interface MainViewData { stats: DashboardStats; diff --git a/src/dashboard/views/sessions-view.ts b/src/dashboard/views/sessions-view.ts index 01abb7b4c..9679ec8c1 100644 --- a/src/dashboard/views/sessions-view.ts +++ b/src/dashboard/views/sessions-view.ts @@ -2,9 +2,9 @@ * Sessions View - Session list display */ -import { ANSI, formatTime, truncate, pad, horizontalLine, BOX } from '../ansi.js'; -import type { SessionInfo, ScreenSize } from '../types.js'; -import { Renderer } from '../renderer.js'; +import { ANSI, formatTime, truncate, pad, horizontalLine, BOX } from '../ansi'; +import type { SessionInfo, ScreenSize } from '../types'; +import { Renderer } from '../renderer'; export interface SessionsViewData { sessions: SessionInfo[]; diff --git a/src/dashboard/views/tabs-view.ts b/src/dashboard/views/tabs-view.ts index a74495382..381bca97a 100644 --- a/src/dashboard/views/tabs-view.ts +++ b/src/dashboard/views/tabs-view.ts @@ -2,9 +2,9 @@ * Tabs View - Tab list display */ -import { ANSI, truncate, pad, horizontalLine, BOX } from '../ansi.js'; -import type { TabInfo, ScreenSize } from '../types.js'; -import { Renderer } from '../renderer.js'; +import { ANSI, truncate, pad, horizontalLine, BOX } from '../ansi'; +import type { TabInfo, ScreenSize } from '../types'; +import { Renderer } from '../renderer'; export interface TabsViewData { tabs: TabInfo[]; diff --git a/src/failure/classifier.ts b/src/failure/classifier.ts index 9540381d0..17dbf8b61 100644 --- a/src/failure/classifier.ts +++ b/src/failure/classifier.ts @@ -1,4 +1,4 @@ -import type { FailureCategory, FailureClassification } from './categories.js'; +import type { FailureCategory, FailureClassification } from './categories'; export interface ClassifyFailureInput { /** Error object, string, or arbitrary thrown value. */ diff --git a/src/failure/index.ts b/src/failure/index.ts index c177ab681..23b5ae308 100644 --- a/src/failure/index.ts +++ b/src/failure/index.ts @@ -1,2 +1,2 @@ -export * from './categories.js'; -export * from './classifier.js'; +export * from './categories'; +export * from './classifier'; diff --git a/src/hints/progress-tracker.ts b/src/hints/progress-tracker.ts index 8d2eeb8d0..59b9bc507 100644 --- a/src/hints/progress-tracker.ts +++ b/src/hints/progress-tracker.ts @@ -6,7 +6,7 @@ * or spinning (auth redirects, stale refs, non-interactive clicks, timeouts). */ -import type { ToolCallEvent } from '../dashboard/types.js'; +import type { ToolCallEvent } from '../dashboard/types'; export type ProgressStatus = 'progressing' | 'stalling' | 'stuck'; diff --git a/src/pilot/curator/promote.ts b/src/pilot/curator/promote.ts index b464c8ce0..2d6295594 100644 --- a/src/pilot/curator/promote.ts +++ b/src/pilot/curator/promote.ts @@ -22,8 +22,8 @@ import * as fs from 'node:fs'; import * as crypto from 'node:crypto'; -import { SkillMemoryStore } from '../../core/skill-memory/store.js'; -import { listSkillsForDomain } from './extractor.js'; +import { SkillMemoryStore } from '../../core/skill-memory/store'; +import { listSkillsForDomain } from './extractor'; export interface PromoteOptions { rootDir?: string; diff --git a/src/pilot/curator/prune.ts b/src/pilot/curator/prune.ts index 873a6a2be..5e3e2f31f 100644 --- a/src/pilot/curator/prune.ts +++ b/src/pilot/curator/prune.ts @@ -28,13 +28,13 @@ import * as crypto from 'node:crypto'; import * as fs from 'node:fs'; import * as path from 'node:path'; -import { listSkillsForDomain } from './extractor.js'; -import { parseSkillMd, stringifySkillMd } from './skill-md.js'; +import { listSkillsForDomain } from './extractor'; +import { parseSkillMd, stringifySkillMd } from './skill-md'; import { SKILL_SCHEMA_VERSION, type SkillRecord, type SkillSidecar, -} from './types.js'; +} from './types'; export type PruneActionKind = | 'demote' diff --git a/src/pilot/curator/recall.ts b/src/pilot/curator/recall.ts index ebee1cde7..8d65fce83 100644 --- a/src/pilot/curator/recall.ts +++ b/src/pilot/curator/recall.ts @@ -30,9 +30,9 @@ * Gated by `isSkillCuratorEnabled()` — callers MUST check before use. */ -import { isSkillCuratorEnabled } from '../../harness/flags.js'; -import { SkillMemoryStore } from '../../core/skill-memory/store.js'; -import type { SkillRecord } from '../../core/skill-memory/types.js'; +import { isSkillCuratorEnabled } from '../../harness/flags'; +import { SkillMemoryStore } from '../../core/skill-memory/store'; +import type { SkillRecord } from '../../core/skill-memory/types'; export { isSkillCuratorEnabled }; diff --git a/src/pilot/curator/runner.ts b/src/pilot/curator/runner.ts index cce8e0971..882de3139 100644 --- a/src/pilot/curator/runner.ts +++ b/src/pilot/curator/runner.ts @@ -14,8 +14,8 @@ * * Example: * - * import { isSkillCuratorEnabled } from '../../harness/flags.js'; - * import { startCuratorRunner } from './runner.js'; + * import { isSkillCuratorEnabled } from '../../harness/flags'; + * import { startCuratorRunner } from './runner'; * * if (isSkillCuratorEnabled()) { * startCuratorRunner({ rootDir: defaultSkillRootDir() }); @@ -25,9 +25,9 @@ import * as os from 'node:os'; import * as path from 'node:path'; -import { CuratorLock, defaultCuratorLockDir, type CuratorLockOptions } from './lock.js'; -import { runPrune, type SkillStatsResolver } from './prune.js'; -import { runPromote } from './promote.js'; +import { CuratorLock, defaultCuratorLockDir, type CuratorLockOptions } from './lock'; +import { runPrune, type SkillStatsResolver } from './prune'; +import { runPromote } from './promote'; /** Default interval: 30 minutes. */ const DEFAULT_INTERVAL_MS = 30 * 60 * 1_000; diff --git a/src/pilot/dynamic-skills/attachment-defaults.ts b/src/pilot/dynamic-skills/attachment-defaults.ts index 0c5e9d2c0..bb3daaebb 100644 --- a/src/pilot/dynamic-skills/attachment-defaults.ts +++ b/src/pilot/dynamic-skills/attachment-defaults.ts @@ -26,16 +26,16 @@ import type { Page } from 'puppeteer-core'; -import { getSessionManager } from '../../session-manager.js'; -import { assertDomainAllowed } from '../../security/domain-guard.js'; -import type { SkillRecord } from '../../core/skill-memory/index.js'; +import { getSessionManager } from '../../session-manager'; +import { assertDomainAllowed } from '../../security/domain-guard'; +import type { SkillRecord } from '../../core/skill-memory/index'; import type { ActionStepResult, ContractAssertionVerdict, CurrentTabInfo, ReplayActionStep, -} from './replay.js'; +} from './replay'; const DEFAULT_WORKER_ID = 'default'; diff --git a/src/pilot/dynamic-skills/index.ts b/src/pilot/dynamic-skills/index.ts index 58f493723..8eda67ff6 100644 --- a/src/pilot/dynamic-skills/index.ts +++ b/src/pilot/dynamic-skills/index.ts @@ -46,22 +46,22 @@ * transformation of the recorded skill into a tool definition. */ -import { isDynamicSkillsEnabled } from '../../harness/flags.js'; +import { isDynamicSkillsEnabled } from '../../harness/flags'; import { defaultSkillMemoryRootDir, SkillMemoryStore, type SkillRecord, -} from '../../core/skill-memory/index.js'; -import type { MCPServer } from '../../mcp-server.js'; -import type { MCPResult, ToolContext, ToolHandler } from '../../types/mcp.js'; -import { logAuditEntry } from '../../security/audit-logger.js'; +} from '../../core/skill-memory/index'; +import type { MCPServer } from '../../mcp-server'; +import type { MCPResult, ToolContext, ToolHandler } from '../../types/mcp'; +import { logAuditEntry } from '../../security/audit-logger'; import { dynamicSkillEvents, type DomainEnteredEvent, type SkillRecordedEvent, -} from './events.js'; -import { getDynamicSkillsRegistry, type RegistryEntry } from './registry.js'; +} from './events'; +import { getDynamicSkillsRegistry, type RegistryEntry } from './registry'; import { runReplay, type ContractAssertionVerdict, @@ -69,8 +69,8 @@ import { type ReplayActionStep, type ReplayHandlerOpts, type ActionStepResult, -} from './replay.js'; -import { synthesizeToolDefinition } from './synthesizer.js'; +} from './replay'; +import { synthesizeToolDefinition } from './synthesizer'; /** * Glue object injected by the runtime. Tests substitute a diff --git a/src/pilot/dynamic-skills/synthesizer.ts b/src/pilot/dynamic-skills/synthesizer.ts index 2b3edea6c..9ddb37e0f 100644 --- a/src/pilot/dynamic-skills/synthesizer.ts +++ b/src/pilot/dynamic-skills/synthesizer.ts @@ -39,7 +39,7 @@ import type { SkillRecord } from '../../core/skill-memory'; import type { MCPToolDefinition } from '../../types/mcp'; -import { synthesizedToolName } from './name.js'; +import { synthesizedToolName } from './name'; export interface SkillParameter { readonly name: string; diff --git a/src/pilot/handoff/definitions.ts b/src/pilot/handoff/definitions.ts new file mode 100644 index 000000000..e641e3da3 --- /dev/null +++ b/src/pilot/handoff/definitions.ts @@ -0,0 +1,60 @@ +import type { MCPToolDefinition } from '../../types/mcp'; +import { TOOL_ANNOTATIONS } from '../../types/tool-annotations'; + +export const createDefinition: MCPToolDefinition = { + name: 'oc_pilot_handoff_create', + category: 'pilot', + annotations: TOOL_ANNOTATIONS.oc_pilot_handoff_create, + description: + 'Pilot-tier: mint a single-use handoff token that lets another agent ' + + 'inherit the named browser session. In-memory only; process restart ' + + 'drops every active handoff. Gated by --pilot + handoff_persist family.', + inputSchema: { + type: 'object', + properties: { + session_id: { + type: 'string', + description: 'Browser session being transferred. Required.', + }, + scope: { + type: 'string', + description: + 'Caller-defined scope label (e.g. "checkout", "read-only"). ' + + 'Surfaced back to the redeeming agent. Required.', + }, + ttl_ms: { + type: 'number', + description: + 'Optional explicit TTL in ms. Defaults to 300000ms (5 min). ' + + 'Non-finite, zero, or negative values fall back to the default.', + }, + }, + required: ['session_id', 'scope'], + }, +}; + +export const redeemDefinition: MCPToolDefinition = { + name: 'oc_pilot_handoff_redeem', + category: 'pilot', + annotations: TOOL_ANNOTATIONS.oc_pilot_handoff_redeem, + description: + 'Pilot-tier: redeem a single-use handoff token previously minted by ' + + 'oc_pilot_handoff_create. Consumes the record on success — subsequent ' + + 'calls with the same token return unknown_token. Gated by --pilot + ' + + 'handoff_persist family.', + inputSchema: { + type: 'object', + properties: { + token: { + type: 'string', + description: 'Token returned by oc_pilot_handoff_create.', + }, + }, + required: ['token'], + }, +}; + +export const PILOT_HANDOFF_TOOL_DEFINITIONS: readonly MCPToolDefinition[] = [ + createDefinition, + redeemDefinition, +] as const; diff --git a/src/pilot/handoff/index.ts b/src/pilot/handoff/index.ts index f8ff991cd..1de7a7d67 100644 --- a/src/pilot/handoff/index.ts +++ b/src/pilot/handoff/index.ts @@ -17,25 +17,25 @@ export { DEFAULT_TOKEN_TTL_MS, HANDOFF_TOKEN_BYTES, HANDOFF_TOKEN_LENGTH, -} from './token.js'; -export type { CreateHandoffTokenArgs, HandoffTokenResult } from './token.js'; +} from './token'; +export type { CreateHandoffTokenArgs, HandoffTokenResult } from './token'; -export { renderHandoffBanner } from './banner.js'; -export type { HandoffBannerPayload } from './banner.js'; +export { renderHandoffBanner } from './banner'; +export type { HandoffBannerPayload } from './banner'; -export { HandoffManager } from './manager.js'; +export { HandoffManager } from './manager'; export type { HandoffManagerOptions, HandoffPayload, HandoffRecord, HandoffRedemption, -} from './manager.js'; +} from './manager'; -export { registerOcPilotHandoffTool } from './tool.js'; +export { registerOcPilotHandoffTool } from './tool'; export { EphemeralEncryptedPersistence, FileBackedKeyEncryptedPersistence, autoSelectHandoffPersistence, -} from './persistence.js'; -export type { PersistenceAdapter, AutoSelectOptions } from './persistence.js'; +} from './persistence'; +export type { PersistenceAdapter, AutoSelectOptions } from './persistence'; diff --git a/src/pilot/handoff/manager.ts b/src/pilot/handoff/manager.ts index 95ff881de..f640885d3 100644 --- a/src/pilot/handoff/manager.ts +++ b/src/pilot/handoff/manager.ts @@ -28,8 +28,8 @@ * {@link HandoffManager.pruneExpired} manually. */ -import { createHandoffToken, type CreateHandoffTokenArgs, type HandoffTokenResult } from './token.js'; -import type { PersistenceAdapter } from './persistence.js'; +import { createHandoffToken, type CreateHandoffTokenArgs, type HandoffTokenResult } from './token'; +import type { PersistenceAdapter } from './persistence'; export interface HandoffPayload { /** Session being transferred. */ diff --git a/src/pilot/handoff/tool.ts b/src/pilot/handoff/tool.ts index 4e2a868df..ed1ffb03b 100644 --- a/src/pilot/handoff/tool.ts +++ b/src/pilot/handoff/tool.ts @@ -19,14 +19,14 @@ * {@link _resetHandoffManagerForTesting}. */ -import { MCPServer } from '../../mcp-server.js'; -import { MCPToolDefinition, MCPResult, ToolHandler } from '../../types/mcp.js'; -import { TOOL_ANNOTATIONS } from '../../types/tool-annotations.js'; -import { isHandoffPersistEnabled } from '../../harness/flags.js'; -import { logAuditEntry } from '../../security/audit-logger.js'; -import { HandoffManager } from './manager.js'; -import { renderHandoffBanner } from './banner.js'; -import { verifyHandoffToken, DEFAULT_TOKEN_TTL_MS } from './token.js'; +import { MCPServer } from '../../mcp-server'; +import { MCPResult, ToolHandler } from '../../types/mcp'; +import { isHandoffPersistEnabled } from '../../harness/flags'; +import { logAuditEntry } from '../../security/audit-logger'; +import { HandoffManager } from './manager'; +import { renderHandoffBanner } from './banner'; +import { verifyHandoffToken } from './token'; +import { createDefinition, redeemDefinition } from './definitions'; /** * Best-effort wrapper around {@link logAuditEntry}. Audit emission is @@ -96,58 +96,6 @@ interface RedeemOutput extends Record { error_message?: string; } -const createDefinition: MCPToolDefinition = { - name: 'oc_pilot_handoff_create', - description: - 'Pilot-tier: mint a single-use handoff token that lets another agent ' + - 'inherit the named browser session. In-memory only; process restart ' + - 'drops every active handoff. Gated by --pilot + handoff_persist family.', - inputSchema: { - type: 'object', - properties: { - session_id: { - type: 'string', - description: 'Browser session being transferred. Required.', - }, - scope: { - type: 'string', - description: - 'Caller-defined scope label (e.g. "checkout", "read-only"). ' + - 'Surfaced back to the redeeming agent. Required.', - }, - ttl_ms: { - type: 'number', - description: - 'Optional explicit TTL in ms. Defaults to ' + - `${DEFAULT_TOKEN_TTL_MS}ms (5 min). Non-finite, zero, or negative ` + - 'values fall back to the default.', - }, - }, - required: ['session_id', 'scope'], - }, - annotations: TOOL_ANNOTATIONS.oc_pilot_handoff_create, -}; - -const redeemDefinition: MCPToolDefinition = { - name: 'oc_pilot_handoff_redeem', - description: - 'Pilot-tier: redeem a single-use handoff token previously minted by ' + - 'oc_pilot_handoff_create. Consumes the record on success — subsequent ' + - 'calls with the same token return unknown_token. Gated by --pilot + ' + - 'handoff_persist family.', - inputSchema: { - type: 'object', - properties: { - token: { - type: 'string', - description: 'Token returned by oc_pilot_handoff_create.', - }, - }, - required: ['token'], - }, - annotations: TOOL_ANNOTATIONS.oc_pilot_handoff_redeem, -}; - const createHandler: ToolHandler = async ( _sessionId: string, args: Record, diff --git a/src/pilot/index.ts b/src/pilot/index.ts index fe3173c2e..351136023 100644 --- a/src/pilot/index.ts +++ b/src/pilot/index.ts @@ -27,32 +27,32 @@ // can resolve it without hard-coupling the harness to the runtime entry. // Keep this as a namespace export so adding sibling subdirs later does not // reorder the public surface and break consumer destructuring. -export * as runtime from './runtime/index.js'; +export * as runtime from './runtime/index'; // Phase 3 (issue #793): pilot-tier handoff token + manager. -export * as handoff from './handoff/index.js'; +export * as handoff from './handoff/index'; // Phase 4 (issue #759): voter-agnostic multi-model voting framework. // Gated by isPerceptionVotingEnabled() inside orchestrator.runVote(). // LLM-backed voter HTTP wrappers ship in openchrome-perception-voters (#775). -export * as voting from './voting/index.js'; +export * as voting from './voting/index'; // Phase 4 (issue #713): verified skill extractor — deterministic transform, // no LLM calls. Gate call sites on `isSkillCuratorEnabled()` from // `src/harness/flags.ts` before invoking any export from this namespace. -export * as curator from './curator/index.js'; +export * as curator from './curator/index'; // Phase 4 (issue #889): dynamic skill→tool synthesis. Gate call sites on // `isDynamicSkillsEnabled()` from `src/harness/flags.ts` before invoking. -export * as dynamicSkills from './dynamic-skills/index.js'; +export * as dynamicSkills from './dynamic-skills/index'; // Phase 5 (issue #874): user-supplied proxy lifecycle binding. Pilot-tier // MCP tool that lets the host declare origin→upstream rules without // openchrome ever contacting the upstream proxy. Gate call sites on // `isProxyHookEnabled()` from `src/harness/flags.ts` before invoking any // export from this namespace. -export * as proxy from './proxy/index.js'; +export * as proxy from './proxy/index'; // Phase 4 (issue #820, blocks #717): pilot-tier skill graph executor. // Pure `decide()` function — no I/O, no side effects. Gate call sites on // `isStateGraphEnabled()` from `src/harness/flags.ts` before invoking. -export * as skill from './skill/index.js'; +export * as skill from './skill/index'; diff --git a/src/pilot/runtime/before-irreversible.ts b/src/pilot/runtime/before-irreversible.ts index 7c2d42b56..0d461e6c2 100644 --- a/src/pilot/runtime/before-irreversible.ts +++ b/src/pilot/runtime/before-irreversible.ts @@ -52,7 +52,7 @@ * an irreversible action execute by accident. */ -import type { Evidence } from '../../contracts/types.js'; +import type { Evidence } from '../../contracts/types'; /** * Input passed to the hook on every fire. diff --git a/src/pilot/runtime/idempotency.ts b/src/pilot/runtime/idempotency.ts index 3b210a33f..2aff8147e 100644 --- a/src/pilot/runtime/idempotency.ts +++ b/src/pilot/runtime/idempotency.ts @@ -34,7 +34,7 @@ import * as crypto from 'node:crypto'; -import type { TransactionRecord } from './types.js'; +import type { TransactionRecord } from './types'; /** Default TTL for cached success verdicts (5 minutes). */ export const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000; diff --git a/src/pilot/runtime/index.ts b/src/pilot/runtime/index.ts index 03f2495a4..51bc0be51 100644 --- a/src/pilot/runtime/index.ts +++ b/src/pilot/runtime/index.ts @@ -14,22 +14,22 @@ export { defaultAuditEmitter, LogAuditEntryEmitter, runWithContract, -} from './runtime.js'; +} from './runtime'; export { defaultBeforeIrreversibleHook, getBeforeIrreversibleHook, registerBeforeIrreversibleHook, resetBeforeIrreversibleHookForTests, -} from './before-irreversible.js'; +} from './before-irreversible'; export { canonicalJson, DEFAULT_CACHE_TTL_MS, IdempotencyCache, -} from './idempotency.js'; +} from './idempotency'; -export type { IdempotencyCacheOptions } from './idempotency.js'; +export type { IdempotencyCacheOptions } from './idempotency'; export type { AuditEmitter, @@ -38,10 +38,10 @@ export type { SkillFn, TransactionRecord, Verdict, -} from './types.js'; +} from './types'; export type { BeforeIrreversibleDecision, BeforeIrreversibleHook, BeforeIrreversibleHookInput, -} from './before-irreversible.js'; +} from './before-irreversible'; diff --git a/src/pilot/runtime/runtime.ts b/src/pilot/runtime/runtime.ts index 56f451ebe..151d3f69c 100644 --- a/src/pilot/runtime/runtime.ts +++ b/src/pilot/runtime/runtime.ts @@ -38,21 +38,21 @@ import * as crypto from 'node:crypto'; -import { logAuditEntry } from '../../security/audit-logger.js'; -import type { EvalContext } from '../../contracts/eval-context.js'; -import { evaluate } from '../../contracts/evaluate.js'; -import { validateAssertion, type ValidationError } from '../../contracts/validator.js'; -import { isContractRuntimeEnabled } from '../../harness/flags.js'; -import { getLifecycleBus } from '../../core/lifecycle/index.js'; -import { getBeforeIrreversibleHook } from './before-irreversible.js'; -import { DEFAULT_CACHE_TTL_MS } from './idempotency.js'; +import { logAuditEntry } from '../../security/audit-logger'; +import type { EvalContext } from '../../contracts/eval-context'; +import { evaluate } from '../../contracts/evaluate'; +import { validateAssertion, type ValidationError } from '../../contracts/validator'; +import { isContractRuntimeEnabled } from '../../harness/flags'; +import { getLifecycleBus } from '../../core/lifecycle/index'; +import { getBeforeIrreversibleHook } from './before-irreversible'; +import { DEFAULT_CACHE_TTL_MS } from './idempotency'; import type { AuditEmitter, Contract, ContractRuntimeArgs, TransactionRecord, Verdict, -} from './types.js'; +} from './types'; const BACKOFF_BASE_MS = 500; const BACKOFF_FACTOR = 2; diff --git a/src/pilot/runtime/types.ts b/src/pilot/runtime/types.ts index c29434c30..195e84f90 100644 --- a/src/pilot/runtime/types.ts +++ b/src/pilot/runtime/types.ts @@ -10,8 +10,8 @@ * reference PR #749 for the original taxonomy this re-authors. */ -import type { Assertion, Evidence } from '../../contracts/types.js'; -import type { ValidationError } from '../../contracts/validator.js'; +import type { Assertion, Evidence } from '../../contracts/types'; +import type { ValidationError } from '../../contracts/validator'; /** Contract definition the runtime evaluates against. */ export interface Contract { diff --git a/src/pilot/skill/executor.ts b/src/pilot/skill/executor.ts index 91af29736..64bd6a8ae 100644 --- a/src/pilot/skill/executor.ts +++ b/src/pilot/skill/executor.ts @@ -21,13 +21,13 @@ * `--pilot` open for some families while leaving state-graph closed. */ -import { SkillGraphStorage } from '../../core/skill/index.js'; -import type { SkillEdge } from '../../core/skill/index.js'; +import { SkillGraphStorage } from '../../core/skill/index'; +import type { SkillEdge } from '../../core/skill/index'; import type { ExecutorAction, ExecutorDecision, ExecutorInput, -} from './types.js'; +} from './types'; /** * Top-of-distribution count must reach at least this share of the edge's diff --git a/src/pilot/skill/index.ts b/src/pilot/skill/index.ts index fb1d03af0..9ab5508b8 100644 --- a/src/pilot/skill/index.ts +++ b/src/pilot/skill/index.ts @@ -18,11 +18,11 @@ export { DISTRIBUTION_MATCH_THRESHOLD, RECOMMEND_RATE_FLOOR, SMALL_SAMPLE_TOTAL, -} from './executor.js'; +} from './executor'; export type { ExecutorAction, ExecutorDecision, ExecutorDecisionKind, ExecutorInput, -} from './types.js'; +} from './types'; diff --git a/src/pilot/voting/index.ts b/src/pilot/voting/index.ts index 5b0a52348..e495184a5 100644 --- a/src/pilot/voting/index.ts +++ b/src/pilot/voting/index.ts @@ -16,7 +16,7 @@ export { actionsEquivalent, type ActionInvocation, type EquivalenceContext, -} from './args-equivalence.js'; +} from './args-equivalence'; export { VotingOrchestrator, @@ -33,4 +33,4 @@ export { type VotingPolicy, type Voter, type VotingProvider, -} from './orchestrator.js'; +} from './orchestrator'; diff --git a/src/pilot/voting/orchestrator.ts b/src/pilot/voting/orchestrator.ts index 25243ede1..8e4c1adde 100644 --- a/src/pilot/voting/orchestrator.ts +++ b/src/pilot/voting/orchestrator.ts @@ -28,8 +28,8 @@ * Gated by `isPerceptionVotingEnabled()` from `src/harness/flags.ts`. */ -import { isPerceptionVotingEnabled } from '../../harness/flags.js'; -import { actionsEquivalent, type ActionInvocation, type EquivalenceContext } from './args-equivalence.js'; +import { isPerceptionVotingEnabled } from '../../harness/flags'; +import { actionsEquivalent, type ActionInvocation, type EquivalenceContext } from './args-equivalence'; export type VoterErrorKind = | 'timeout' diff --git a/src/run-harness/index.ts b/src/run-harness/index.ts index 34ccb2fa4..2fa707b8b 100644 --- a/src/run-harness/index.ts +++ b/src/run-harness/index.ts @@ -1,4 +1,4 @@ -export * from './types.js'; -export * from './store.js'; -export * from './tools.js'; -export * from './flags.js'; +export * from './types'; +export * from './store'; +export * from './tools'; +export * from './flags'; diff --git a/src/run-harness/store.ts b/src/run-harness/store.ts index 043d9542f..45bfe62db 100644 --- a/src/run-harness/store.ts +++ b/src/run-harness/store.ts @@ -3,8 +3,8 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; -import { redactValue } from '../core/trace/redactor.js'; -import { TERMINAL_RUN_STATUSES, type RunEvent, type RunRecord, type RunStatus } from './types.js'; +import { redactValue } from '../core/trace/redactor'; +import { TERMINAL_RUN_STATUSES, type RunEvent, type RunRecord, type RunStatus } from './types'; export interface RunStoreOptions { rootDir?: string; diff --git a/src/run-harness/tools.ts b/src/run-harness/tools.ts index 5c8e1adb5..3a3dfbf1d 100644 --- a/src/run-harness/tools.ts +++ b/src/run-harness/tools.ts @@ -1,8 +1,8 @@ -import type { MCPServer } from '../mcp-server.js'; -import type { MCPResult, MCPToolDefinition, ToolHandler } from '../types/mcp.js'; -import { TOOL_ANNOTATIONS } from '../types/tool-annotations.js'; -import { getRunStore } from './store.js'; -import { RUN_STATUSES, TERMINAL_RUN_STATUSES, type RunRecord, type RunStatus } from './types.js'; +import type { MCPServer } from '../mcp-server'; +import type { MCPResult, MCPToolDefinition, ToolHandler } from '../types/mcp'; +import { TOOL_ANNOTATIONS } from '../types/tool-annotations'; +import { getRunStore } from './store'; +import { RUN_STATUSES, TERMINAL_RUN_STATUSES, type RunRecord, type RunStatus } from './types'; const runIdProperty = { type: 'string', @@ -12,7 +12,6 @@ const runIdProperty = { const startDefinition: MCPToolDefinition = { name: 'oc_run_start', description: 'Start an opt-in OpenChrome run ledger. Returns {run_id,status,pathless metadata}.', - annotations: TOOL_ANNOTATIONS.oc_run_start, inputSchema: { type: 'object', properties: { @@ -23,19 +22,19 @@ const startDefinition: MCPToolDefinition = { }, required: [], }, + annotations: TOOL_ANNOTATIONS.oc_run_start, }; const statusDefinition: MCPToolDefinition = { name: 'oc_run_status', description: 'Return the current status and summary for an opt-in OpenChrome run ledger.', - annotations: TOOL_ANNOTATIONS.oc_run_status, inputSchema: { type: 'object', properties: { run_id: runIdProperty }, required: ['run_id'] }, + annotations: TOOL_ANNOTATIONS.oc_run_status, }; const eventsDefinition: MCPToolDefinition = { name: 'oc_run_events', description: 'Return recent events for an opt-in OpenChrome run ledger.', - annotations: TOOL_ANNOTATIONS.oc_run_events, inputSchema: { type: 'object', properties: { @@ -44,12 +43,12 @@ const eventsDefinition: MCPToolDefinition = { }, required: ['run_id'], }, + annotations: TOOL_ANNOTATIONS.oc_run_events, }; const finishDefinition: MCPToolDefinition = { name: 'oc_run_finish', description: 'Finish an opt-in OpenChrome run ledger with a terminal or needs_user_input status.', - annotations: TOOL_ANNOTATIONS.oc_run_finish, inputSchema: { type: 'object', properties: { @@ -60,6 +59,7 @@ const finishDefinition: MCPToolDefinition = { }, required: ['run_id', 'status'], }, + annotations: TOOL_ANNOTATIONS.oc_run_finish, }; const startHandler: ToolHandler = async (_sessionId, args): Promise => { diff --git a/src/tools/batch-execute.ts b/src/tools/batch-execute.ts index 2e5608e12..da48fd784 100644 --- a/src/tools/batch-execute.ts +++ b/src/tools/batch-execute.ts @@ -15,6 +15,7 @@ import { formatCDPResult, CDPEvalResult } from './javascript'; const definition: MCPToolDefinition = { name: 'batch_execute', + category: 'misc', description: 'Execute JS across multiple tabs in parallel.', inputSchema: { type: 'object', diff --git a/src/tools/batch-paginate.ts b/src/tools/batch-paginate.ts index 0abff5066..938c64c3b 100644 --- a/src/tools/batch-paginate.ts +++ b/src/tools/batch-paginate.ts @@ -18,6 +18,7 @@ import { withTimeout } from '../utils/with-timeout'; const definition: MCPToolDefinition = { name: 'batch_paginate', + category: 'misc', description: 'Extract content from paginated viewers in one call.', inputSchema: { type: 'object', diff --git a/src/tools/checkpoint.ts b/src/tools/checkpoint.ts index a6a2099f3..09e6d11fb 100644 --- a/src/tools/checkpoint.ts +++ b/src/tools/checkpoint.ts @@ -37,6 +37,7 @@ export const CHECKPOINT_FILE = 'current-checkpoint.json'; const definition: MCPToolDefinition = { name: 'oc_checkpoint', + category: 'lifecycle', description: 'Save or load an automation checkpoint for long-running session continuity. ' + 'Use "save" to persist current task state, "load" to restore after context compaction, ' + diff --git a/src/tools/computer.ts b/src/tools/computer.ts index 235e88fda..ecd032363 100644 --- a/src/tools/computer.ts +++ b/src/tools/computer.ts @@ -47,6 +47,7 @@ async function getPaginationGuidance(page: import('puppeteer-core').Page, tabId: const definition: MCPToolDefinition = { name: 'computer', + category: 'interact', description: 'Mouse, keyboard, and screenshot actions on a tab. Supports click, type, scroll, key, hover, and screenshot by pixel coordinate or element ref.\n\nWhen to use: Precise coordinate-based input, screenshots, or keyboard shortcuts.\nWhen NOT to use: Use interact for natural-language element actions, or act for multi-step sequences.', inputSchema: { type: 'object', diff --git a/src/tools/connect.ts b/src/tools/connect.ts index c80a52ed0..b4be98e33 100644 --- a/src/tools/connect.ts +++ b/src/tools/connect.ts @@ -68,6 +68,7 @@ export async function collectDevToolsInfo(): Promise< const getConnectionInfoDef: MCPToolDefinition = { name: 'oc_get_connection_info', + category: 'misc', description: 'Get connection configuration for a web AI host (Claude Web, ChatGPT, Gemini, or custom). ' + 'Returns the MCP server URL, bearer token, settings page URL, step-by-step instructions, and ' + @@ -161,6 +162,7 @@ const getConnectionInfoHandler: ToolHandler = async ( const copyToClipboardDef: MCPToolDefinition = { name: 'oc_copy_to_clipboard', + category: 'misc', description: 'Copy text to the system clipboard. Useful for copying MCP server URLs or config snippets.', inputSchema: { type: 'object', @@ -190,6 +192,7 @@ const copyToClipboardHandler: ToolHandler = async ( const openHostSettingsDef: MCPToolDefinition = { name: 'oc_open_host_settings', + category: 'misc', description: 'Open the MCP connector settings page for a web AI host in the default browser.', inputSchema: { type: 'object', diff --git a/src/tools/connection-health.ts b/src/tools/connection-health.ts index ad3ea0ba7..560c25add 100644 --- a/src/tools/connection-health.ts +++ b/src/tools/connection-health.ts @@ -10,6 +10,7 @@ import { getCDPClient } from '../cdp/client'; const definition: MCPToolDefinition = { name: 'oc_connection_health', + category: 'lifecycle', description: 'Get CDP connection health metrics including heartbeat mode, reconnect count, ping latency, connection state, and live reconnection progress. Use this to monitor connection stability during long-running sessions.', inputSchema: { diff --git a/src/tools/console-capture.ts b/src/tools/console-capture.ts index c3866b87b..59d95cbc0 100644 --- a/src/tools/console-capture.ts +++ b/src/tools/console-capture.ts @@ -178,6 +178,7 @@ function deduplicateLogs(logs: ConsoleLogEntry[]): DedupedLogEntry[] { const definition: MCPToolDefinition = { name: 'console_capture', + category: 'observability', description: 'Capture browser console output (start, stop, get, clear).', annotations: TOOL_ANNOTATIONS.console_capture, inputSchema: { diff --git a/src/tools/cookies.ts b/src/tools/cookies.ts index a3c9e59d8..7f57ca901 100644 --- a/src/tools/cookies.ts +++ b/src/tools/cookies.ts @@ -63,6 +63,7 @@ function formatCookiesCompact(cookies: any[]): string { const definition: MCPToolDefinition = { name: 'cookies', + category: 'storage', description: 'Manage browser cookies (get, set, delete, clear).', inputSchema: { type: 'object', diff --git a/src/tools/crawl-sitemap.ts b/src/tools/crawl-sitemap.ts index 253a60ebf..9a1537779 100644 --- a/src/tools/crawl-sitemap.ts +++ b/src/tools/crawl-sitemap.ts @@ -33,6 +33,7 @@ import { getGlobalConfig } from '../config/global'; const definition: MCPToolDefinition = { name: 'crawl_sitemap', + category: 'misc', description: 'Crawl a website using its sitemap.xml. Auto-discovers sitemaps from robots.txt or /sitemap.xml. Supports sitemap index files and URL filtering.\n\nWhen to use: Extracting content from many pages of a site that publishes a sitemap.xml.\nWhen NOT to use: Use crawl for BFS discovery when no sitemap exists, or navigate for a single page.', inputSchema: { diff --git a/src/tools/crawl.ts b/src/tools/crawl.ts index 7e4fbce56..ee4b80d80 100644 --- a/src/tools/crawl.ts +++ b/src/tools/crawl.ts @@ -37,6 +37,7 @@ import { AdaptiveCrawlDispatcher, DispatcherMode, parseAdaptiveDispatcherOptions const definition: MCPToolDefinition = { name: 'crawl', + category: 'misc', description: 'Recursively crawl a website via BFS. Opens pages in new tabs, extracts text and links, follows them up to max_depth. Respects robots.txt and scope constraints.\n\nWhen to use: Extracting content from multiple pages of a site when the URL structure is not known in advance.\nWhen NOT to use: Use crawl_sitemap when the site has a sitemap.xml, or navigate for a single page.', inputSchema: { diff --git a/src/tools/drag-drop.ts b/src/tools/drag-drop.ts index 78b56a227..1a2ef63fc 100644 --- a/src/tools/drag-drop.ts +++ b/src/tools/drag-drop.ts @@ -14,6 +14,7 @@ interface Position { const definition: MCPToolDefinition = { name: 'drag_drop', + category: 'interact', description: 'Drag and drop by selector or coordinates. Pass intent="..." (≤120 chars) to label this action in audit logs.', inputSchema: { type: 'object', diff --git a/src/tools/emulate-device.ts b/src/tools/emulate-device.ts index 701b469d7..0603ae7b7 100644 --- a/src/tools/emulate-device.ts +++ b/src/tools/emulate-device.ts @@ -84,6 +84,7 @@ const DEVICE_PRESETS: Record = { const definition: MCPToolDefinition = { name: 'emulate_device', + category: 'profile', description: 'Emulate device viewport and UA via preset or custom.', inputSchema: { type: 'object', diff --git a/src/tools/extract-data.ts b/src/tools/extract-data.ts index 02da4314d..5e1a7f298 100644 --- a/src/tools/extract-data.ts +++ b/src/tools/extract-data.ts @@ -22,6 +22,7 @@ import type { ExtractionSchema, SchemaProperty } from '../extraction'; const definition: MCPToolDefinition = { name: 'extract_data', + category: 'misc', description: 'Extract structured data from page using a JSON Schema. Tries JSON-LD, Microdata, OpenGraph, and CSS heuristics. Use multiple:true for listings.\n\nWhen to use: Extracting typed structured data (products, articles, prices) from a page into a schema.\nWhen NOT to use: Use javascript_tool for ad-hoc extraction, or read_page to read raw page content.', inputSchema: { diff --git a/src/tools/file-upload.ts b/src/tools/file-upload.ts index 0f5e3a117..ecc3b0238 100644 --- a/src/tools/file-upload.ts +++ b/src/tools/file-upload.ts @@ -60,6 +60,7 @@ export interface UploadPathValidationResult { const definition: MCPToolDefinition = { name: 'file_upload', + category: 'forms', description: 'Upload files to a file input element on the page. Pass intent="..." (≤120 chars) to label this action in audit logs.', inputSchema: { type: 'object', diff --git a/src/tools/fill-form.ts b/src/tools/fill-form.ts index 5397653cb..e2eacf394 100644 --- a/src/tools/fill-form.ts +++ b/src/tools/fill-form.ts @@ -23,6 +23,7 @@ import { coerceVerifyMode, runVerify, VERIFY_FIELD_SCHEMA } from '../core/percep const definition: MCPToolDefinition = { name: 'fill_form', + category: 'forms', description: 'Fill form fields and optionally submit. Pass intent="..." (≤120 chars) to label this action in audit logs.', inputSchema: { type: 'object', diff --git a/src/tools/find.ts b/src/tools/find.ts index 867eae3b4..e6268f086 100644 --- a/src/tools/find.ts +++ b/src/tools/find.ts @@ -18,6 +18,7 @@ import { detectVisionHints, formatVisionHints } from '../vision/auto-detect'; const definition: MCPToolDefinition = { name: 'find', + category: 'dom', description: 'Find elements by query. Returns up to 20 matches with refs.\n\nWhen to use: Locating elements by natural language when exact selectors are unknown.\nWhen NOT to use: Use query_dom when you have a CSS selector or XPath, or interact to find-and-click in one step.', inputSchema: { type: 'object', diff --git a/src/tools/form-input.ts b/src/tools/form-input.ts index a903973ec..f01757f55 100644 --- a/src/tools/form-input.ts +++ b/src/tools/form-input.ts @@ -11,6 +11,7 @@ import { withDomDelta } from '../utils/dom-delta'; const definition: MCPToolDefinition = { name: 'form_input', + category: 'forms', description: 'Set one form element value by ref. Pass intent="..." (≤120 chars) to label this action in audit logs.\n\nWhen to use: Filling a single known input, textarea, select, or checkbox by ref.\nWhen NOT to use: Use fill_form({fields:{...}}) for multiple fields or optional submit.', inputSchema: { type: 'object', diff --git a/src/tools/geolocation.ts b/src/tools/geolocation.ts index e970a4c2c..4af570cb4 100644 --- a/src/tools/geolocation.ts +++ b/src/tools/geolocation.ts @@ -26,6 +26,7 @@ const LOCATION_PRESETS: Record< const definition: MCPToolDefinition = { name: 'geolocation', + category: 'profile', description: 'Set or clear geolocation override.', inputSchema: { type: 'object', diff --git a/src/tools/http-auth.ts b/src/tools/http-auth.ts index ccd1a47d9..5ae95d378 100644 --- a/src/tools/http-auth.ts +++ b/src/tools/http-auth.ts @@ -9,6 +9,7 @@ import { getSessionManager } from '../session-manager'; const definition: MCPToolDefinition = { name: 'http_auth', + category: 'profile', description: 'Set or clear HTTP auth credentials.', inputSchema: { type: 'object', diff --git a/src/tools/inspect.ts b/src/tools/inspect.ts index 00ef92136..a4b793e87 100644 --- a/src/tools/inspect.ts +++ b/src/tools/inspect.ts @@ -22,6 +22,7 @@ import { const definition: MCPToolDefinition = { name: 'inspect', + category: 'observability', description: 'Extract focused page state by query. Returns headings, form fields, errors, tabs, and interactive counts scoped to the query intent.\n\nWhen to use: Checking focused aspects of page state (forms, errors, tabs) without loading the full DOM.\nWhen NOT to use: Use read_page for full DOM/AX tree, or find to locate a specific element.', inputSchema: { type: 'object', diff --git a/src/tools/javascript.ts b/src/tools/javascript.ts index 99f25174c..e88daa951 100644 --- a/src/tools/javascript.ts +++ b/src/tools/javascript.ts @@ -12,6 +12,7 @@ import { wrapMutatingHandler } from '../utils/snapshot-cache-helper'; const definition: MCPToolDefinition = { name: 'javascript_tool', + category: 'js', description: 'Execute JavaScript in page context. Supports await, async IIFE, and shadow-DOM helpers via __pierce.\n\nWhen to use: Custom DOM queries, data extraction, or triggering JS APIs not reachable via other tools.\nWhen NOT to use: Use interact or act for UI interactions, or extract_data for structured schema-based extraction.', inputSchema: { type: 'object', diff --git a/src/tools/journal.ts b/src/tools/journal.ts index 8e892173f..3085e04fd 100644 --- a/src/tools/journal.ts +++ b/src/tools/journal.ts @@ -12,6 +12,7 @@ import { readCurrentCheckpoint } from './checkpoint'; const definition: MCPToolDefinition = { name: 'oc_journal', + category: 'lifecycle', description: 'Query the tool call journal. Actions: "summary" (milestone overview), "recent" (last N entries), "handoff_summary" (compact JSON resume handoff).\nWhen to use: Reviewing session history, restoring context, or auditing past tool calls.\nWhen NOT to use: Use read_page or inspect to check the current live page state.', inputSchema: { diff --git a/src/tools/lightweight-scroll.ts b/src/tools/lightweight-scroll.ts index 3bcf97e57..692cca0f5 100644 --- a/src/tools/lightweight-scroll.ts +++ b/src/tools/lightweight-scroll.ts @@ -18,6 +18,7 @@ import { wrapMutatingHandler } from '../utils/snapshot-cache-helper'; const definition: MCPToolDefinition = { name: 'lightweight_scroll', + category: 'interact', description: 'Scroll page via JS. Returns new scroll position.', inputSchema: { type: 'object', diff --git a/src/tools/list-profiles.ts b/src/tools/list-profiles.ts index aae691319..f8b87bbe5 100644 --- a/src/tools/list-profiles.ts +++ b/src/tools/list-profiles.ts @@ -14,6 +14,7 @@ import { getChromeLauncher } from '../chrome/launcher'; const definition: MCPToolDefinition = { name: 'list_profiles', + category: 'profile', description: 'List available Chrome profiles with names and directory IDs.', inputSchema: { type: 'object', diff --git a/src/tools/memory.ts b/src/tools/memory.ts index bf6381027..f3238ba82 100644 --- a/src/tools/memory.ts +++ b/src/tools/memory.ts @@ -17,6 +17,7 @@ import { findLiteralSecret } from '../core/secrets'; const definition: MCPToolDefinition = { name: 'memory', + category: 'storage', description: 'Manage domain knowledge. Actions: "record" (store), "query" (retrieve by domain), "validate" (adjust confidence). Key prefixes: "selector:", "tip:", "avoid:".', inputSchema: { diff --git a/src/tools/navigate.ts b/src/tools/navigate.ts index b7052322d..df71bb993 100644 --- a/src/tools/navigate.ts +++ b/src/tools/navigate.ts @@ -437,6 +437,7 @@ async function withDomainSkillsResult( const definition: MCPToolDefinition = { name: 'navigate', + category: 'navigation', description: 'Navigate to URL or go forward/back. Omit tabId for new tab.', inputSchema: { type: 'object', diff --git a/src/tools/network.ts b/src/tools/network.ts index 4461c133b..065c574af 100644 --- a/src/tools/network.ts +++ b/src/tools/network.ts @@ -46,6 +46,7 @@ const NETWORK_PRESETS: Record< const definition: MCPToolDefinition = { name: 'network', + category: 'observability', description: 'Simulate network conditions.', inputSchema: { type: 'object', diff --git a/src/tools/oc-assert.ts b/src/tools/oc-assert.ts index 992a21b17..0ec485416 100644 --- a/src/tools/oc-assert.ts +++ b/src/tools/oc-assert.ts @@ -65,6 +65,7 @@ interface SnapshotInput { const definition: MCPToolDefinition = { name: 'oc_assert', + category: 'evidence', description: 'Evaluate a single Outcome Contract assertion against caller-supplied ' + 'evidence (snapshot). Returns verdict pass/fail/inconclusive plus the ' + diff --git a/src/tools/oc-evidence-bundle.ts b/src/tools/oc-evidence-bundle.ts index cc01e47ef..97be758df 100644 --- a/src/tools/oc-evidence-bundle.ts +++ b/src/tools/oc-evidence-bundle.ts @@ -55,6 +55,7 @@ const VALID_PARTS: readonly EvidenceBundlePart[] = [ const definition: MCPToolDefinition = { name: 'oc_evidence_bundle', + category: 'evidence', description: 'Capture a snapshot of the current page state (DOM, screenshot, network ' + 'slice, console, perceptual hash) and write it to a bundle directory. ' + diff --git a/src/tools/oc-skill-recall.ts b/src/tools/oc-skill-recall.ts index 039a621c5..7e1311c4e 100644 --- a/src/tools/oc-skill-recall.ts +++ b/src/tools/oc-skill-recall.ts @@ -25,6 +25,7 @@ const DEFAULT_LIMIT = 20; const definition: MCPToolDefinition = { name: 'oc_skill_recall', + category: 'evidence', description: 'Retrieve skills from the JSON skill memory store for a given domain. ' + 'Returns a recency-sorted list (last_used_at desc). Optionally filter by ' + diff --git a/src/tools/oc-skill-record.ts b/src/tools/oc-skill-record.ts index 24bcc6ffc..123f19e24 100644 --- a/src/tools/oc-skill-record.ts +++ b/src/tools/oc-skill-record.ts @@ -30,6 +30,7 @@ interface OcSkillRecordOutput { const definition: MCPToolDefinition = { name: 'oc_skill_record', + category: 'evidence', description: 'Record a skill (domain, name, steps, contract_id) into the JSON skill ' + 'memory store. Idempotent on (domain, name) — re-recording preserves the ' + diff --git a/src/tools/orchestration.ts b/src/tools/orchestration.ts index ab0b0bca9..088eb9a95 100644 --- a/src/tools/orchestration.ts +++ b/src/tools/orchestration.ts @@ -22,6 +22,7 @@ const dnsResolve = promisify(dns.resolve); const workflowInitDefinition: MCPToolDefinition = { name: 'workflow_init', + category: 'misc', description: 'Initialize a workflow with multiple isolated workers for parallel browser ops.', inputSchema: { type: 'object', @@ -216,6 +217,7 @@ const workflowInitHandler: ToolHandler = async ( const workflowStatusDefinition: MCPToolDefinition = { name: 'workflow_status', + category: 'misc', description: 'Get current workflow status and worker states.', inputSchema: { type: 'object', @@ -299,6 +301,7 @@ const workflowStatusHandler: ToolHandler = async ( const workflowCollectDefinition: MCPToolDefinition = { name: 'workflow_collect', + category: 'misc', description: 'Collect and aggregate results from all workers after completion.', inputSchema: { type: 'object', @@ -354,6 +357,7 @@ const workflowCollectHandler: ToolHandler = async ( const workflowCleanupDefinition: MCPToolDefinition = { name: 'workflow_cleanup', + category: 'misc', description: 'Clean up workflow resources (workers, tabs, scratchpads).', inputSchema: { type: 'object', @@ -402,6 +406,7 @@ const workflowCleanupHandler: ToolHandler = async ( const workerUpdateDefinition: MCPToolDefinition = { name: 'worker_update', + category: 'misc', description: 'Report worker progress to the orchestration scratchpad.', inputSchema: { type: 'object', @@ -492,6 +497,7 @@ const workerUpdateHandler: ToolHandler = async ( const workerCompleteDefinition: MCPToolDefinition = { name: 'worker_complete', + category: 'misc', description: 'Mark a worker as complete with final results.', inputSchema: { type: 'object', @@ -566,6 +572,7 @@ const workerCompleteHandler: ToolHandler = async ( const workflowCollectPartialDefinition: MCPToolDefinition = { name: 'workflow_collect_partial', + category: 'misc', description: 'Collect results from completed workers without waiting for all to finish.', inputSchema: { type: 'object', @@ -663,6 +670,7 @@ const workflowCollectPartialHandler: ToolHandler = async ( const executePlanDefinition: MCPToolDefinition = { name: 'execute_plan', + category: 'misc', description: 'Execute a cached plan by ID, bypassing per-step LLM calls. Falls back gracefully on failure for manual retry.', inputSchema: { type: 'object', diff --git a/src/tools/page-content.ts b/src/tools/page-content.ts index 06032b0f5..4bbeb9b2d 100644 --- a/src/tools/page-content.ts +++ b/src/tools/page-content.ts @@ -11,6 +11,7 @@ import { withTimeout } from '../utils/with-timeout'; const definition: MCPToolDefinition = { name: 'page_content', + category: 'dom', description: 'Get HTML content from page or element.', inputSchema: { type: 'object', diff --git a/src/tools/page-pdf.ts b/src/tools/page-pdf.ts index 1907309b0..b7919bbd9 100644 --- a/src/tools/page-pdf.ts +++ b/src/tools/page-pdf.ts @@ -12,6 +12,7 @@ import { getSessionManager } from '../session-manager'; const definition: MCPToolDefinition = { name: 'page_pdf', + category: 'observability', description: 'Generate PDF from page. Saves to path or returns base64.', inputSchema: { type: 'object', diff --git a/src/tools/page-reload.ts b/src/tools/page-reload.ts index 15331b8db..699952e29 100644 --- a/src/tools/page-reload.ts +++ b/src/tools/page-reload.ts @@ -12,6 +12,7 @@ import { wrapMutatingHandler } from '../utils/snapshot-cache-helper'; const definition: MCPToolDefinition = { name: 'page_reload', + category: 'lifecycle', description: 'Reload the current page.', inputSchema: { type: 'object', diff --git a/src/tools/page-screenshot.ts b/src/tools/page-screenshot.ts index 583925705..6144f5ed0 100644 --- a/src/tools/page-screenshot.ts +++ b/src/tools/page-screenshot.ts @@ -20,6 +20,7 @@ const FULL_PAGE_DIMENSION_TIMEOUT_MS = 5000; const definition: MCPToolDefinition = { name: 'page_screenshot', + category: 'observability', description: 'Save page screenshot to file or return as base64. Supports full-page capture, region clipping, and multiple image formats.\n\nWhen to use: Capturing a screenshot for saving to disk or when the full-page or clipped region is needed.\nWhen NOT to use: Use computer(action:"screenshot") for an inline viewport screenshot during interaction.', inputSchema: { type: 'object', diff --git a/src/tools/performance-metrics.ts b/src/tools/performance-metrics.ts index f165fd7fd..7863b6cd9 100644 --- a/src/tools/performance-metrics.ts +++ b/src/tools/performance-metrics.ts @@ -10,6 +10,7 @@ import { withTimeout } from '../utils/with-timeout'; const definition: MCPToolDefinition = { name: 'performance_metrics', + category: 'observability', description: 'Get page performance metrics.', inputSchema: { type: 'object', diff --git a/src/tools/profile-status.ts b/src/tools/profile-status.ts index a65625a48..b5d74c610 100644 --- a/src/tools/profile-status.ts +++ b/src/tools/profile-status.ts @@ -16,6 +16,7 @@ import { formatAge } from '../utils/format-age'; const definition: MCPToolDefinition = { name: 'oc_profile_status', + category: 'profile', description: 'Check browser profile type and capabilities.', inputSchema: { type: 'object', diff --git a/src/tools/query-dom.ts b/src/tools/query-dom.ts index 32d4fad0d..8dbe6998d 100644 --- a/src/tools/query-dom.ts +++ b/src/tools/query-dom.ts @@ -46,6 +46,7 @@ interface CSSElementInfo { const definition: MCPToolDefinition = { name: 'query_dom', + category: 'dom', description: 'Query DOM elements via CSS selector or XPath. Returns tag, attributes, text, position. CSS results include a ref field for use in subsequent calls.\n\nWhen to use: Precise element lookup by CSS selector or XPath when you know the exact selector.\nWhen NOT to use: Use find for natural-language element search or read_page for full DOM structure.', inputSchema: { diff --git a/src/tools/reap-orphans.ts b/src/tools/reap-orphans.ts index fda927d00..09d723fe4 100644 --- a/src/tools/reap-orphans.ts +++ b/src/tools/reap-orphans.ts @@ -6,6 +6,7 @@ import { cleanOrphanedChromeProcesses } from '../utils/pid-manager'; const definition: MCPToolDefinition = { name: 'oc_reap_orphans', + category: 'lifecycle', description: 'Manually sweep and terminate orphaned OpenChrome-managed Chrome processes. Never touches attach-mode or unmarked user Chrome.', inputSchema: { type: 'object', diff --git a/src/tools/recording.ts b/src/tools/recording.ts index 7de8d64bf..c9ccfd8a1 100644 --- a/src/tools/recording.ts +++ b/src/tools/recording.ts @@ -23,6 +23,7 @@ function isValidRecordingId(id: string): boolean { const startDefinition: MCPToolDefinition = { name: 'oc_recording_start', + category: 'recording', annotations: TOOL_ANNOTATIONS.oc_recording_start, description: 'Start a new session recording. All subsequent MCP tool calls will be recorded ' + @@ -96,6 +97,7 @@ const startHandler: ToolHandler = async ( const stopDefinition: MCPToolDefinition = { name: 'oc_recording_stop', + category: 'recording', annotations: TOOL_ANNOTATIONS.oc_recording_stop, description: 'Stop the active session recording and finalize it to disk. ' + @@ -159,6 +161,7 @@ const stopHandler: ToolHandler = async ( const statusDefinition: MCPToolDefinition = { name: 'oc_recording_status', + category: 'recording', annotations: TOOL_ANNOTATIONS.oc_recording_status, description: 'Report whether session recording is active, including trajectory bundle metadata when enabled.', inputSchema: { type: 'object', properties: {} }, @@ -185,6 +188,7 @@ const statusHandler: ToolHandler = async ( const listDefinition: MCPToolDefinition = { name: 'oc_recording_list', + category: 'recording', annotations: TOOL_ANNOTATIONS.oc_recording_list, description: 'List available session recordings, newest first.', inputSchema: { @@ -252,6 +256,7 @@ const listHandler: ToolHandler = async ( const exportDefinition: MCPToolDefinition = { name: 'oc_recording_export', + category: 'recording', annotations: TOOL_ANNOTATIONS.oc_recording_export, description: 'Export a recording as JSON or a self-contained HTML report. ' + diff --git a/src/tools/request-intercept.ts b/src/tools/request-intercept.ts index db6178515..e03393218 100644 --- a/src/tools/request-intercept.ts +++ b/src/tools/request-intercept.ts @@ -221,6 +221,7 @@ function estimatedStaticAssetResponseBytes(resourceType: string): number { const definition: MCPToolDefinition = { name: 'request_intercept', + category: 'observability', annotations: TOOL_ANNOTATIONS.request_intercept, description: 'Intercept network requests (log, block, modify). ' + diff --git a/src/tools/session-resume.ts b/src/tools/session-resume.ts index 69eacf2c8..18a32fed2 100644 --- a/src/tools/session-resume.ts +++ b/src/tools/session-resume.ts @@ -80,6 +80,7 @@ interface TabAnalysis { const definition: MCPToolDefinition = { name: 'oc_session_resume', + category: 'lifecycle', description: 'Restore working context after context compaction. ' + 'Reads the last oc_session_snapshot, checks which tabs are still alive, ' + diff --git a/src/tools/session-snapshot.ts b/src/tools/session-snapshot.ts index 36046cfa6..461ee6b9f 100644 --- a/src/tools/session-snapshot.ts +++ b/src/tools/session-snapshot.ts @@ -44,6 +44,7 @@ export interface SessionSnapshot { const definition: MCPToolDefinition = { name: 'oc_session_snapshot', + category: 'lifecycle', description: 'Save browser state snapshot for context recovery after compaction. ' + 'Captures open tabs, worker state, and your task memo. ' + diff --git a/src/tools/shutdown.ts b/src/tools/shutdown.ts index 67030c7be..e070bdcb7 100644 --- a/src/tools/shutdown.ts +++ b/src/tools/shutdown.ts @@ -19,6 +19,7 @@ import { shutdownHeadedFallback } from '../chrome/headed-fallback'; const definition: MCPToolDefinition = { name: 'oc_stop', + category: 'lifecycle', description: 'Shut down OpenChrome and close Chrome. Auto-relaunched on next tool call.', inputSchema: { type: 'object', diff --git a/src/tools/storage.ts b/src/tools/storage.ts index 1bea1bbe0..e0861befa 100644 --- a/src/tools/storage.ts +++ b/src/tools/storage.ts @@ -11,6 +11,7 @@ import { withTimeout } from '../utils/with-timeout'; const definition: MCPToolDefinition = { name: 'storage', + category: 'storage', description: 'Manage browser localStorage and sessionStorage.', inputSchema: { type: 'object', diff --git a/src/tools/tabs-close.ts b/src/tools/tabs-close.ts index 97883aef7..c282ff6c8 100644 --- a/src/tools/tabs-close.ts +++ b/src/tools/tabs-close.ts @@ -1,139 +1,140 @@ -/** - * Tabs Close Tool - Close tabs in the session - */ - -import { MCPServer } from '../mcp-server'; -import { MCPToolDefinition, MCPResult, ToolHandler } from '../types/mcp'; +/** + * Tabs Close Tool - Close tabs in the session + */ + +import { MCPServer } from '../mcp-server'; +import { MCPToolDefinition, MCPResult, ToolHandler } from '../types/mcp'; import { TOOL_ANNOTATIONS } from '../types/tool-annotations'; -import { getSessionManager } from '../session-manager'; - -const definition: MCPToolDefinition = { - name: 'tabs_close', - description: 'Close one or more tabs by tabId, tabIds, or workerId.', - inputSchema: { - type: 'object', - properties: { - tabId: { - type: 'string', - description: 'Specific tab ID to close', - }, - tabIds: { - type: 'array', - items: { type: 'string' }, - description: 'Tab IDs to batch close', - }, - workerId: { - type: 'string', - description: 'Close all tabs in this worker (worker preserved)', - }, - }, - }, +import { getSessionManager } from '../session-manager'; + +const definition: MCPToolDefinition = { + name: 'tabs_close', + category: 'tabs', + description: 'Close one or more tabs by tabId, tabIds, or workerId.', + inputSchema: { + type: 'object', + properties: { + tabId: { + type: 'string', + description: 'Specific tab ID to close', + }, + tabIds: { + type: 'array', + items: { type: 'string' }, + description: 'Tab IDs to batch close', + }, + workerId: { + type: 'string', + description: 'Close all tabs in this worker (worker preserved)', + }, + }, + }, annotations: TOOL_ANNOTATIONS.tabs_close, -}; - -const handler: ToolHandler = async ( - sessionId: string, - args: Record -): Promise => { - const sessionManager = getSessionManager(); - const tabId = args.tabId as string | undefined; - const tabIds = args.tabIds as string[] | undefined; - const workerId = args.workerId as string | undefined; - - // Validate at least one parameter is provided - if (!tabId && !tabIds && !workerId) { - return { - content: [ - { - type: 'text', - text: 'Error: Provide tabId, tabIds array, or workerId to close tabs', - }, - ], - isError: true, - }; - } - - try { - const results: { closed: string[]; failed: string[]; message: string } = { - closed: [], - failed: [], - message: '', - }; - - // Close specific tab - if (tabId) { - const success = await sessionManager.closeTarget(sessionId, tabId); - if (success) { - results.closed.push(tabId); - } else { - results.failed.push(tabId); - } - } - - // Close multiple tabs - if (tabIds && tabIds.length > 0) { - for (const id of tabIds) { - const success = await sessionManager.closeTarget(sessionId, id); - if (success) { - results.closed.push(id); - } else { - results.failed.push(id); - } - } - } - - // Close all tabs in a worker - if (workerId) { - const closedCount = await sessionManager.closeWorkerTabs(sessionId, workerId); - results.message = `Closed ${closedCount} tab(s) in worker "${workerId}"`; - } - - // Build response message - let responseMessage = ''; - if (results.closed.length > 0) { - responseMessage += `Closed ${results.closed.length} tab(s)`; - } - if (results.failed.length > 0) { - responseMessage += `${responseMessage ? '. ' : ''}Failed to close ${results.failed.length} tab(s): ${results.failed.join(', ')}`; - } - if (results.message) { - responseMessage += `${responseMessage ? '. ' : ''}${results.message}`; - } - if (!responseMessage) { - responseMessage = 'No tabs closed'; - } - - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { - success: results.failed.length === 0, - closedCount: results.closed.length, - closed: results.closed, - failed: results.failed, - message: responseMessage, - }, - null, - 2 - ), - }, - ], - }; - } catch (error) { - return { - content: [ - { - type: 'text', - text: `Error closing tabs: ${error instanceof Error ? error.message : String(error)}`, - }, - ], - isError: true, - }; - } -}; - -export function registerTabsCloseTool(server: MCPServer): void { - server.registerTool('tabs_close', handler, definition); -} +}; + +const handler: ToolHandler = async ( + sessionId: string, + args: Record +): Promise => { + const sessionManager = getSessionManager(); + const tabId = args.tabId as string | undefined; + const tabIds = args.tabIds as string[] | undefined; + const workerId = args.workerId as string | undefined; + + // Validate at least one parameter is provided + if (!tabId && !tabIds && !workerId) { + return { + content: [ + { + type: 'text', + text: 'Error: Provide tabId, tabIds array, or workerId to close tabs', + }, + ], + isError: true, + }; + } + + try { + const results: { closed: string[]; failed: string[]; message: string } = { + closed: [], + failed: [], + message: '', + }; + + // Close specific tab + if (tabId) { + const success = await sessionManager.closeTarget(sessionId, tabId); + if (success) { + results.closed.push(tabId); + } else { + results.failed.push(tabId); + } + } + + // Close multiple tabs + if (tabIds && tabIds.length > 0) { + for (const id of tabIds) { + const success = await sessionManager.closeTarget(sessionId, id); + if (success) { + results.closed.push(id); + } else { + results.failed.push(id); + } + } + } + + // Close all tabs in a worker + if (workerId) { + const closedCount = await sessionManager.closeWorkerTabs(sessionId, workerId); + results.message = `Closed ${closedCount} tab(s) in worker "${workerId}"`; + } + + // Build response message + let responseMessage = ''; + if (results.closed.length > 0) { + responseMessage += `Closed ${results.closed.length} tab(s)`; + } + if (results.failed.length > 0) { + responseMessage += `${responseMessage ? '. ' : ''}Failed to close ${results.failed.length} tab(s): ${results.failed.join(', ')}`; + } + if (results.message) { + responseMessage += `${responseMessage ? '. ' : ''}${results.message}`; + } + if (!responseMessage) { + responseMessage = 'No tabs closed'; + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: results.failed.length === 0, + closedCount: results.closed.length, + closed: results.closed, + failed: results.failed, + message: responseMessage, + }, + null, + 2 + ), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error closing tabs: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } +}; + +export function registerTabsCloseTool(server: MCPServer): void { + server.registerTool('tabs_close', handler, definition); +} diff --git a/src/tools/tabs-context.ts b/src/tools/tabs-context.ts index 2953acf36..e2a266066 100644 --- a/src/tools/tabs-context.ts +++ b/src/tools/tabs-context.ts @@ -10,6 +10,7 @@ import { safeTitle } from '../utils/safe-title'; const definition: MCPToolDefinition = { name: 'tabs_context', + category: 'tabs', description: 'Get session tab IDs grouped by worker.', annotations: TOOL_ANNOTATIONS.tabs_context, inputSchema: { diff --git a/src/tools/tabs-create.ts b/src/tools/tabs-create.ts index a808c5adb..e97636026 100644 --- a/src/tools/tabs-create.ts +++ b/src/tools/tabs-create.ts @@ -24,6 +24,7 @@ import { const definition: MCPToolDefinition = { name: 'tabs_create', + category: 'tabs', description: 'Create a new tab with URL.', inputSchema: { type: 'object', diff --git a/src/tools/totp-generate.ts b/src/tools/totp-generate.ts index fd2d51d82..78d11af25 100644 --- a/src/tools/totp-generate.ts +++ b/src/tools/totp-generate.ts @@ -9,6 +9,7 @@ import { TOOL_ANNOTATIONS } from '../types/tool-annotations'; const definition: MCPToolDefinition = { name: 'oc_totp_generate', + category: 'misc', description: 'Generate a current TOTP 2FA code for a domain. Requires TOTP secret to be configured.', inputSchema: { diff --git a/src/tools/user-agent.ts b/src/tools/user-agent.ts index cf4de6b68..20715a5c6 100644 --- a/src/tools/user-agent.ts +++ b/src/tools/user-agent.ts @@ -33,6 +33,7 @@ const USER_AGENT_PRESETS: Record = { const definition: MCPToolDefinition = { name: 'user_agent', + category: 'profile', description: 'Set or reset browser user agent.', inputSchema: { type: 'object', diff --git a/src/tools/validate-page.ts b/src/tools/validate-page.ts index b2b01ec9e..bb6a7a298 100644 --- a/src/tools/validate-page.ts +++ b/src/tools/validate-page.ts @@ -80,6 +80,7 @@ function argText(arg: { type: string; value?: unknown; description?: string }): const definition: MCPToolDefinition = { name: 'validate_page', + category: 'observability', description: 'Composite health check: navigate, wait, capture console errors, return structured summary (title, errors, interactive count, body sample).\n\nWhen to use: Verifying a page renders correctly without errors in a single call instead of chaining navigate + wait_for + console_capture + read_page.\nWhen NOT to use: Use navigate + read_page when you need full DOM content, not just a health summary.', inputSchema: { diff --git a/src/tools/vision-find.ts b/src/tools/vision-find.ts index ddf1ed552..ded5dff60 100644 --- a/src/tools/vision-find.ts +++ b/src/tools/vision-find.ts @@ -19,6 +19,7 @@ import { trackVisionUsage } from '../vision/config'; const definition: MCPToolDefinition = { name: 'vision_find', + category: 'dom', description: 'Find elements using vision-based screenshot analysis. Returns annotated screenshot with numbered elements.', inputSchema: { type: 'object', diff --git a/src/tools/wait-for.ts b/src/tools/wait-for.ts index 4f2f8e9a0..63e14926f 100644 --- a/src/tools/wait-for.ts +++ b/src/tools/wait-for.ts @@ -10,6 +10,7 @@ import { safeTitle } from '../utils/safe-title'; const definition: MCPToolDefinition = { name: 'wait_for', + category: 'lifecycle', description: "Wait for a condition. Strongly prefer 'function', 'selector', or 'url_match' — they return as soon as the condition is true (1 round-trip). Use 'timeout' only as a last resort: it blocks for a fixed duration and returns no information, forcing you to poll with another tool afterwards.", inputSchema: { type: 'object', diff --git a/src/tools/worker.ts b/src/tools/worker.ts index 3fbf8ada8..5f2dea0f0 100644 --- a/src/tools/worker.ts +++ b/src/tools/worker.ts @@ -11,6 +11,7 @@ import { getSessionManager } from '../session-manager'; const definition: MCPToolDefinition = { name: 'worker', + category: 'misc', description: 'Manage workers. Actions: "create" (isolated context), "list" (show all), "delete" (remove and close tabs).', inputSchema: { diff --git a/tests/scripts/gen-capability-map.test.ts b/tests/scripts/gen-capability-map.test.ts new file mode 100644 index 000000000..8493a6039 --- /dev/null +++ b/tests/scripts/gen-capability-map.test.ts @@ -0,0 +1,82 @@ +/** + * Tests for scripts/gen-capability-map.ts + * + * Verifies the generator produces deterministic, size-bounded output + * that does not include the synthetic expand_tools hint. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as crypto from 'crypto'; +import { execSync } from 'child_process'; + +const REPO_ROOT = path.resolve(__dirname, '..', '..'); +const OUTPUT_PATH = path.join(REPO_ROOT, 'docs', 'agent', 'capability-map.md'); +const MAX_BYTES = 6144; + +function runGenerator(): Buffer { + execSync('npm run gen:capability-map', { + cwd: REPO_ROOT, + stdio: 'pipe', + }); + return fs.readFileSync(OUTPUT_PATH); +} + +function sha256(buf: Buffer): string { + return crypto.createHash('sha256').update(buf).digest('hex'); +} + +describe('gen-capability-map', () => { + beforeAll(() => { + // Ensure the file exists before tests run (first run) + runGenerator(); + }); + + test('output is byte-identical on two consecutive runs', () => { + const first = runGenerator(); + const second = runGenerator(); + expect(sha256(first)).toBe(sha256(second)); + }); + + test('file size is at most 6144 bytes', () => { + const buf = fs.readFileSync(OUTPUT_PATH); + expect(buf.byteLength).toBeLessThanOrEqual(MAX_BYTES); + }); + + test('expand_tools is not present in output', () => { + const content = fs.readFileSync(OUTPUT_PATH, 'utf8'); + expect(content).not.toContain('expand_tools'); + }); + + test('generated file contains the do-not-edit comment', () => { + const content = fs.readFileSync(OUTPUT_PATH, 'utf8'); + expect(content).toContain('do not edit'); + }); + + test('all expected categories appear at least once', () => { + const content = fs.readFileSync(OUTPUT_PATH, 'utf8'); + const expectedCategories = [ + 'navigation', + 'dom', + 'interact', + 'forms', + 'lifecycle', + 'observability', + ]; + for (const cat of expectedCategories) { + expect(content).toContain(`## ${cat}`); + } + }); + + test('pilot tools are present with pilot marker', () => { + const content = fs.readFileSync(OUTPUT_PATH, 'utf8'); + expect(content).toContain('oc_pilot_handoff_create'); + expect(content).toContain('— pilot'); + }); + + test('file ends with exactly one newline', () => { + const content = fs.readFileSync(OUTPUT_PATH, 'utf8'); + expect(content.endsWith('\n')).toBe(true); + expect(content.endsWith('\n\n')).toBe(false); + }); +});