From a36d3de7fca811b945acd28c646425479a19d9b5 Mon Sep 17 00:00:00 2001 From: Lakshman Turlapati Date: Wed, 21 Oct 2026 11:22:03 -0500 Subject: [PATCH 1/2] Remove dead MCP tools, pin the tool count to 69, and finish retiring background agents The server registered 72 tools while the READMEs said 69 and the website, llms files and onboarding said 68. start_visual_session and end_visual_session have only returned TOOL_REMOVED since 0.9.0, and back duplicated go_back, so all three are gone and the server lists exactly 69. mcp-version-parity now counts the built server's tools and fails when any published claim disagrees: both READMEs and their per-category tables, onboarding, the showcase pages and every translation, and the llms files. Background agents were retired in v0.9.45rc1 but their data and code stayed. On extension update, background.js now deletes bgAgents, fsb_sync_queue, the old notice flag and any fsb_agent_* alarms. The control-panel list of old agent names, extension/agents/, about 4,700 lines of commented-out agent code across the extension and both dashboards, and the unused executeAutomationTask are removed. The server drops /api/agents and the agent-only /api/stats; server-py drops its agents, stats and SSE routers. The agents and agent_runs tables stay. --- CHANGELOG.md | 10 + README.md | 8 +- extension/agents/agent-executor.js | 661 ------------ extension/agents/agent-manager.js | 533 ---------- extension/agents/agent-scheduler.js | 460 --------- extension/agents/server-sync.js | 242 ----- extension/background.js | 431 +------- extension/config/config.js | 2 +- extension/config/init-config.js | 2 +- extension/ui/control_panel.html | 17 - extension/ui/onboarding.js | 6 +- extension/ui/options.css | 25 +- extension/ui/options.js | 524 +--------- extension/ui/popup.js | 113 --- extension/ui/sidepanel.js | 113 --- extension/ws/mcp-bridge-client.js | 10 +- extension/ws/ws-client.js | 34 - mcp/CHANGELOG.md | 15 + mcp/README.md | 42 +- mcp/src/errors.ts | 16 +- mcp/src/runtime.ts | 4 - mcp/src/tools/agents.ts | 266 ----- mcp/src/tools/autopilot.ts | 2 +- mcp/src/tools/visual-session.ts | 60 -- server-py/app/main.py | 9 +- server-py/app/models.py | 82 +- server-py/app/routers/agents.py | 235 ----- server-py/app/routers/sse.py | 59 -- server-py/app/routers/stats.py | 69 -- showcase/angular/public/llms-full.txt | 6 +- showcase/angular/public/llms.txt | 6 +- showcase/angular/scripts/llms-full.source.md | 4 +- showcase/angular/scripts/llms.source.md | 4 +- .../app/pages/about/about-page.component.html | 2 +- .../pages/agents/agents-page.component.html | 4 +- .../app/pages/agents/agents-page.component.ts | 2 +- .../dashboard/dashboard-page.component.html | 1 - .../dashboard/dashboard-page.component.ts | 922 +---------------- .../app/pages/home/home-page.component.html | 4 +- .../pages/support/support-page.component.ts | 2 +- showcase/angular/src/locale/messages.de.xlf | 64 +- showcase/angular/src/locale/messages.es.xlf | 64 +- showcase/angular/src/locale/messages.ja.xlf | 64 +- showcase/angular/src/locale/messages.ko.xlf | 64 +- showcase/angular/src/locale/messages.xlf | 50 +- .../angular/src/locale/messages.zh-CN.xlf | 64 +- .../angular/src/locale/messages.zh-TW.xlf | 64 +- .../src/locale/same-source-allowlist.json | 4 +- showcase/dashboard.html | 1 - showcase/js/dashboard.js | 959 ------------------ showcase/server/server.js | 12 +- showcase/server/src/db/queries.js | 178 ---- showcase/server/src/routes/agents.js | 165 --- skills/fsb/SKILL.md | 6 +- skills/fsb/USAGE.md | 3 +- skills/fsb/references/multi-agent-contract.md | 14 +- skills/fsb/references/tool-decision-tree.md | 2 +- .../fsb/references/v0.9.62-contract-mirror.md | 2 +- .../references/visual-session-lifecycle.md | 5 +- tests/agent-manager-start-mode.test.js | 92 -- tests/agent-sunset-back-end.test.js | 228 +++-- tests/agent-sunset-control-panel.test.js | 143 +-- tests/back-tool.test.js | 170 ---- tests/dom-stream-perf.test.js | 2 +- tests/lattice-provider-bridge-smoke.test.js | 6 +- tests/lmstudio-startup-preflight.test.js | 2 +- tests/mcp-bridge-background-dispatch.test.js | 8 +- tests/mcp-numeric-param-coercion.test.js | 31 +- tests/mcp-tool-smoke.test.js | 86 +- tests/mcp-version-parity.test.js | 53 +- tests/mcp-visual-session-contract.test.js | 111 +- tests/remote-control-rebrand.test.js | 6 +- tests/server-public-stats-no-auth.test.js | 36 +- tests/skill-fsb-spec.test.js | 2 +- tests/test-agent-scheduler-cron.js | 124 --- 75 files changed, 611 insertions(+), 7281 deletions(-) delete mode 100644 extension/agents/agent-executor.js delete mode 100644 extension/agents/agent-manager.js delete mode 100644 extension/agents/agent-scheduler.js delete mode 100644 extension/agents/server-sync.js delete mode 100644 mcp/src/tools/agents.ts delete mode 100644 server-py/app/routers/agents.py delete mode 100644 server-py/app/routers/sse.py delete mode 100644 server-py/app/routers/stats.py delete mode 100644 showcase/server/src/routes/agents.js delete mode 100644 tests/agent-manager-start-mode.test.js delete mode 100644 tests/back-tool.test.js delete mode 100644 tests/test-agent-scheduler-cron.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 096eaf376..62f20ae1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The independently published `fsb-mcp-server` npm package keeps its own semver ch ## [Unreleased] +### Removed + +- **What background agents left behind.** Background agents were retired in v0.9.45rc1, but their saved definitions stayed in `chrome.storage.local['bgAgents']` and several thousand lines of commented-out agent code stayed in the extension and both dashboards. When the extension updates, it now deletes `bgAgents`, the `fsb_sync_queue` sync queue and any scheduled `fsb_agent_*` alarms. The control-panel list of old agent names, the commented-out code, and the unused `executeAutomationTask` entry point are gone. +- **Agent sync endpoints on the server.** `/api/agents` and the agent-only `/api/stats` route are removed from `showcase/server`, and the agents, stats and SSE routers from `server-py`. The `agents` and `agent_runs` tables are kept, so previously synced rows are untouched. +- **Three MCP tools.** `start_visual_session`, `end_visual_session` and `back` are no longer registered, leaving 69 tools; see [`mcp/CHANGELOG.md`](./mcp/CHANGELOG.md). + +### Changed + +- Every published MCP tool count now says 69 and is checked against the built server, so the README, onboarding, showcase and `llms` figures can no longer disagree. + ## v0.9.92 — Bridge Reliability and Editing Fixes — 2026-10-07 The extension, showcase, skill, documentation, and store metadata align at `0.9.92`. The independently versioned `fsb-mcp-server` advances to `0.11.1` with the accompanying patch; see [`mcp/CHANGELOG.md`](./mcp/CHANGELOG.md) for its package-specific release notes and compatibility requirements. diff --git a/README.md b/README.md index 4bf743172..0026cd7d9 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ Want to run FSB standalone from the extension popup/side panel? Open settings, p - Includes secure credential and payment vault flows for supervised autofill. - Exposes a local MCP server so Claude Code, Codex, Cursor, VS Code, Windsurf, and other MCP clients can drive the browser. -Background agents are retired. Existing `chrome.storage.local['bgAgents']` data is preserved for users who had old agents, but scheduled or recurring automation is no longer an active FSB feature. +Background agents were removed in v0.9.45rc1. When the extension updates, it deletes the saved agent definitions and alarms they left behind. For scheduled or recurring work, use [OpenClaw](https://github.com/openclaw/openclaw) or [Claude Routines](https://www.anthropic.com/claude/routines). ### Common Use Cases @@ -277,7 +277,7 @@ The extension reads bundled scripts directly from `extension/`. The MCP package ## MCP Server -FSB ships [`fsb-mcp-server`](https://www.npmjs.com/package/fsb-mcp-server), a local MCP server that lets external AI clients control the same browser extension. It exposes 69 registered tools across visual sessions, trigger watchers, manual browser control, Developer/UAT capture, capability search/invoke, read-only page inspection, autopilot, vault, and observability. +FSB ships [`fsb-mcp-server`](https://www.npmjs.com/package/fsb-mcp-server), a local MCP server that lets external AI clients control the same browser extension. It exposes 69 registered tools across autopilot, trigger watchers, manual browser control, read-only page inspection, task outcome reporting, Developer/UAT capture, capability search/invoke, observability, and vault. The extension connects to the MCP bridge on: @@ -426,11 +426,11 @@ The MCP server exposes a curated public surface around that registry plus direct | Surface | Count | Examples | |---------|-------|----------| -| Visual sessions | 2 | `start_visual_session`, `end_visual_session` | -| Autopilot and agent navigation | 4 | `run_task`, `stop_task`, `get_task_status`, `back` | +| Autopilot | 3 | `run_task`, `stop_task`, `get_task_status` | | Trigger watchers | 4 | `trigger`, `stop_trigger`, `get_trigger_status`, `list_triggers` | | Manual control | 37 | `execute_js`, `navigate`, `click`, `type_text`, `drag`, `upload_file` | | Read-only inspection | 8 | `read_page`, `get_dom_snapshot`, `get_site_guide`, `read_sheet` | +| Task outcome | 3 | `complete_task`, `partial_task`, `fail_task` | | Developer / UAT | 1 | `capture_screenshot` | | Capabilities | 2 | `search_capabilities`, `invoke_capability` | | Observability and replay | 7 | `list_sessions`, `get_session_replay`, `replay_session` | diff --git a/extension/agents/agent-executor.js b/extension/agents/agent-executor.js deleted file mode 100644 index 3fab7f0c9..000000000 --- a/extension/agents/agent-executor.js +++ /dev/null @@ -1,661 +0,0 @@ -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// /** -// * Background Agent Executor for FSB -// * Wraps existing automation infrastructure for headless background execution. -// * Creates background tabs, runs automation, captures results, closes tabs. -// * Supports session replay: after a successful AI run, records the action sequence -// * as a script. Subsequent runs replay the script directly (no AI, zero cost). -// * If replay fails, falls back to AI and updates the script on success. -// */ -// -// // Read-only tools that exist only for AI decision-making -- skip during replay -// const READONLY_TOOLS = new Set([ -// 'getText', 'getAttribute', 'waitForElement', 'listTabs', -// 'getCurrentTab', 'detectLoadingState', 'waitForDOMStable' -// ]); -// -// // Tools that trigger page navigation -- need extra wait after replay -// const NAVIGATION_TOOLS = new Set([ -// 'navigate', 'searchGoogle', 'goBack', 'goForward', 'refresh' -// ]); -// -// class AgentExecutor { -// constructor() { -// this.MAX_CONCURRENT = 3; -// this.EXECUTION_TIMEOUT = 4 * 60 * 1000; // 4 minutes (under MV3's 5-min limit) -// this._running = new Map(); // agentId -> { tabId, startTime, timeoutId } -// } -// -// /** -// * Get count of currently running agents -// * @returns {number} -// */ -// get runningCount() { -// return this._running.size; -// } -// -// /** -// * Check if an agent is currently running -// * @param {string} agentId -// * @returns {boolean} -// */ -// isRunning(agentId) { -// return this._running.has(agentId); -// } -// -// /** -// * Execute an agent's task in a background tab. -// * Decides between replay mode and AI mode based on agent configuration. -// * @param {Object} agent - Agent data from storage -// * @returns {Promise} Run result -// */ -// async execute(agent) { -// if (this._running.size >= this.MAX_CONCURRENT) { -// return { -// success: false, -// error: 'Max concurrent agents reached (' + this.MAX_CONCURRENT + '). Try again later.', -// duration: 0 -// }; -// } -// -// if (this._running.has(agent.agentId)) { -// return { -// success: false, -// error: 'Agent is already running', -// duration: 0 -// }; -// } -// -// const startTime = Date.now(); -// let backgroundTab = null; -// -// try { -// console.log('[FSB Executor] Starting agent:', agent.agentId, agent.name); -// const startMode = agent.startMode || (agent.targetUrl ? 'pinned' : 'ai_routed'); -// const pinnedTargetUrl = startMode === 'pinned' ? agent.targetUrl : ''; -// const replayTargetUrl = agent.recordedScript?.targetUrl || pinnedTargetUrl || ''; -// -// const hasScript = agent.recordedScript && -// agent.recordedScript.steps && -// agent.recordedScript.steps.length > 0; -// const replayEnabled = agent.replayEnabled !== false; -// const canReplay = hasScript && replayEnabled && !!replayTargetUrl; -// const initialUrl = canReplay -// ? replayTargetUrl -// : (pinnedTargetUrl || 'about:blank'); -// -// // Create background tab -// backgroundTab = await chrome.tabs.create({ -// url: initialUrl, -// active: false -// }); -// -// // Register running state -// const executionState = { -// tabId: backgroundTab.id, -// startTime, -// timeoutId: null -// }; -// this._running.set(agent.agentId, executionState); -// -// if (initialUrl !== 'about:blank') { -// await this._waitForTabLoad(backgroundTab.id, 15000); -// await this._ensureContentScript(backgroundTab.id); -// } -// -// if (canReplay) { -// // --- REPLAY PATH --- -// console.log('[FSB Executor] Attempting replay for agent:', agent.agentId, -// 'steps:', agent.recordedScript.steps.length); -// -// const replayResult = await this._executeWithTimeoutReplay( -// backgroundTab.id, agent.recordedScript, agent -// ); -// -// if (replayResult.success) { -// // Replay succeeded -- zero cost -// const duration = Date.now() - startTime; -// const costSaved = agent.recordedScript.estimatedCostPerRun || 0; -// console.log('[FSB Executor] Replay succeeded for agent:', agent.agentId, -// 'duration:', duration + 'ms', 'costSaved: $' + costSaved); -// -// return { -// success: true, -// sessionId: null, -// result: 'Task completed via replay (' + agent.recordedScript.totalSteps + ' steps)', -// error: null, -// duration, -// tokensUsed: 0, -// costUsd: 0, -// iterations: 0, -// executionMode: 'replay', -// costSaved, -// stepResults: replayResult.stepResults || [] -// }; -// } -// -// // --- Replay failed: close tab, reopen fresh, run AI fallback --- -// console.log('[FSB Executor] Replay failed at step', replayResult.failedAtStep, -// '- falling back to AI for agent:', agent.agentId, -// 'error:', replayResult.error); -// -// // Close the failed tab -// if (backgroundTab?.id) { -// await chrome.tabs.remove(backgroundTab.id).catch(() => {}); -// } -// -// // Open fresh tab for AI fallback -// backgroundTab = await chrome.tabs.create({ -// url: pinnedTargetUrl || 'about:blank', -// active: false -// }); -// -// // Update running state with new tab -// executionState.tabId = backgroundTab.id; -// -// if (pinnedTargetUrl) { -// await this._waitForTabLoad(backgroundTab.id, 15000); -// await this._ensureContentScript(backgroundTab.id); -// } -// -// // Run AI fallback -// const aiResult = await this._executeWithTimeout( -// backgroundTab.id, -// agent.task, -// { -// maxIterations: agent.maxIterations || 15, -// isBackgroundAgent: true, -// agentId: agent.agentId, -// triggerSource: 'background-agent', -// reuseMatchingTabs: false, -// activateTab: false -// } -// ); -// -// const duration = Date.now() - startTime; -// -// // If AI succeeds, update the recorded script -// if (aiResult.success && aiResult.actionHistory) { -// const newScript = this._extractRecordedScript( -// aiResult, aiResult.startUrl || pinnedTargetUrl || replayTargetUrl, aiResult.actionHistory -// ); -// if (newScript) { -// try { -// await agentManager.saveRecordedScript(agent.agentId, newScript); -// console.log('[FSB Executor] Updated recorded script after AI fallback for agent:', agent.agentId); -// // Clear needsReRecord flag since we have a fresh script -// if (agent.replayStats?.needsReRecord) { -// await agentManager.updateAgent(agent.agentId, { -// replayStats: { ...agent.replayStats, needsReRecord: false, stepSuccessRates: {} } -// }); -// } -// } catch (e) { -// console.error('[FSB Executor] Failed to save updated script:', e.message); -// } -// } -// } -// -// return { -// success: aiResult.success, -// sessionId: aiResult.sessionId || null, -// result: aiResult.result || null, -// error: aiResult.error || null, -// duration, -// tokensUsed: aiResult.tokensUsed || 0, -// costUsd: aiResult.costUsd || 0, -// iterations: aiResult.iterations || 0, -// executionMode: 'ai_fallback', -// replayFailedAtStep: replayResult.failedAtStep, -// stepResults: replayResult.stepResults || [], -// costSaved: 0 -// }; -// } -// -// // --- AI PATH (no script yet) --- -// const result = await this._executeWithTimeout( -// backgroundTab.id, -// agent.task, -// { -// maxIterations: agent.maxIterations || 15, -// isBackgroundAgent: true, -// agentId: agent.agentId, -// triggerSource: 'background-agent', -// reuseMatchingTabs: false, -// activateTab: false -// } -// ); -// -// const duration = Date.now() - startTime; -// console.log('[FSB Executor] Agent completed:', agent.agentId, -// 'success:', result.success, 'duration:', duration + 'ms'); -// -// // If AI succeeds, extract and save the script for future replays -// if (result.success && result.actionHistory) { -// const script = this._extractRecordedScript( -// result, result.startUrl || pinnedTargetUrl || replayTargetUrl, result.actionHistory -// ); -// if (script) { -// try { -// await agentManager.saveRecordedScript(agent.agentId, script); -// console.log('[FSB Executor] Saved initial recorded script for agent:', agent.agentId, -// 'steps:', script.totalSteps); -// // Clear needsReRecord flag since we have a fresh script -// if (agent.replayStats?.needsReRecord) { -// await agentManager.updateAgent(agent.agentId, { -// replayStats: { ...agent.replayStats, needsReRecord: false, stepSuccessRates: {} } -// }); -// } -// } catch (e) { -// console.error('[FSB Executor] Failed to save script:', e.message); -// } -// } -// } -// -// return { -// success: result.success, -// sessionId: result.sessionId || null, -// result: result.result || null, -// error: result.error || null, -// duration, -// tokensUsed: result.tokensUsed || 0, -// costUsd: result.costUsd || 0, -// iterations: result.iterations || 0, -// executionMode: 'ai_initial', -// costSaved: 0 -// }; -// -// } catch (error) { -// const duration = Date.now() - startTime; -// console.error('[FSB Executor] Agent failed:', agent.agentId, error.message); -// return { -// success: false, -// error: error.message, -// duration, -// executionMode: 'ai_initial', -// costSaved: 0 -// }; -// } finally { -// // Cleanup: close background tab and clear running state -// this._cleanup(agent.agentId, backgroundTab?.id); -// } -// } -// -// /** -// * Extract a recorded script from a successful AI run's action history. -// * Filters to only successful, meaningful actions and assigns delays. -// * @param {Object} result - The AI execution result -// * @param {string} targetUrl - Resolved start URL for replay -// * @param {Object[]} actionHistory - Array of action records from the session -// * @returns {Object|null} RecordedScript object, or null if no meaningful steps -// */ -// _extractRecordedScript(result, targetUrl, actionHistory) { -// if (!actionHistory || actionHistory.length === 0) return null; -// -// const steps = []; -// let stepNumber = 0; -// -// for (const action of actionHistory) { -// // Only include actions that succeeded -// if (action.result && action.result.success === false) continue; -// // Skip if no tool specified -// if (!action.tool) continue; -// -// const isReadOnly = READONLY_TOOLS.has(action.tool); -// const isNavigation = NAVIGATION_TOOLS.has(action.tool); -// -// // Determine delay after this step -// let delayAfter = 500; // default -// if (isNavigation) { -// delayAfter = 2000; -// } else if (action.tool === 'type' || action.tool === 'clearInput') { -// delayAfter = 300; -// } else if (action.tool === 'click') { -// delayAfter = 800; -// } -// -// steps.push({ -// stepNumber: stepNumber++, -// tool: action.tool, -// params: action.params || {}, -// isNavigation, -// isReadOnly, -// delayAfter, -// metadata: { -// originalResult: { -// success: action.result?.success !== false, -// hadEffect: action.result?.hadEffect || action.result?.success !== false -// }, -// originalDuration: action.duration || 0 -// } -// }); -// } -// -// // Filter out scripts with no meaningful (non-read-only) actions -// const meaningfulSteps = steps.filter(s => !s.isReadOnly); -// if (meaningfulSteps.length === 0) return null; -// -// const estimatedDuration = steps.reduce((sum, s) => sum + s.delayAfter + 200, 0); -// const estimatedCostPerRun = result.costUsd || 0; -// -// return { -// version: '1.0', -// recordedAt: Date.now(), -// sourceSessionId: result.sessionId || null, -// targetUrl, -// steps, -// totalSteps: steps.length, -// estimatedDuration, -// estimatedCostPerRun -// }; -// } -// -// /** -// * Execute a recorded script by replaying actions sequentially. -// * Skips read-only steps. Fails fast on first step failure. -// * @param {number} tabId - Tab to execute in -// * @param {Object} script - RecordedScript object -// * @param {Object} agent - Agent data -// * @returns {Promise} { success, failedAtStep, error, duration } -// */ -// async _executeReplayScript(tabId, script, agent) { -// const replayStart = Date.now(); -// const stepResults = []; -// -// for (const step of script.steps) { -// // Skip read-only steps -- they were for AI reasoning -// if (step.isReadOnly) continue; -// -// let stepSuccess = false; -// const MAX_STEP_RETRIES = 3; // initial + 2 retries -// let attempt = 0; -// let lastError = null; -// -// try { -// // Step-level retry loop: retry individual steps before full AI fallback -// for (attempt = 1; attempt <= MAX_STEP_RETRIES; attempt++) { -// const actionResult = await sendMessageWithRetry(tabId, { -// action: 'executeAction', -// tool: step.tool, -// params: step.params -// }); -// -// if (actionResult && actionResult.success !== false) { -// stepSuccess = true; -// break; -// } -// -// lastError = actionResult?.error || 'action returned failure'; -// -// // Don't retry on last attempt -// if (attempt < MAX_STEP_RETRIES) { -// await new Promise(resolve => setTimeout(resolve, 500 * attempt)); -// } -// } -// -// if (!stepSuccess) { -// stepResults.push({ -// stepNumber: step.stepNumber, -// tool: step.tool, -// success: false, -// attempts: attempt, -// retriesUsed: attempt - 1 -// }); -// return { -// success: false, -// failedAtStep: step.stepNumber, -// error: 'Step ' + step.stepNumber + ' (' + step.tool + ') failed after ' + attempt + ' attempts: ' + lastError, -// duration: Date.now() - replayStart, -// stepResults -// }; -// } -// -// // After navigation steps, wait for page to settle -// if (step.isNavigation) { -// try { -// await pageLoadWatcher.waitForPageReady(tabId, { maxWait: 8000 }); -// } catch (e) { -// // Page load timeout is not necessarily fatal -- continue -// console.log('[FSB Executor] Page load wait timed out after navigation replay step:', step.stepNumber); -// } -// -// // Re-check content script after navigation -// try { -// await waitForContentScriptReady(tabId, 5000); -// } catch (e) { -// await ensureContentScriptInjected(tabId); -// const ready = await waitForContentScriptReady(tabId, 3000); -// if (!ready) { -// stepResults.push({ -// stepNumber: step.stepNumber, -// tool: step.tool, -// success: false, -// attempts: attempt -// }); -// return { -// success: false, -// failedAtStep: step.stepNumber, -// error: 'Content script lost after navigation at step ' + step.stepNumber, -// duration: Date.now() - replayStart, -// stepResults -// }; -// } -// } -// } -// -// // Smart replay timing: use recorded original duration (clamped) or fall back to delayAfter -// const replayDelay = step.metadata?.originalDuration > 0 -// ? Math.max(200, Math.min(step.metadata.originalDuration, 5000)) -// : step.delayAfter; -// if (replayDelay > 0) { -// await new Promise(resolve => setTimeout(resolve, replayDelay)); -// } -// -// stepResults.push({ -// stepNumber: step.stepNumber, -// tool: step.tool, -// success: stepSuccess, -// attempts: attempt -// }); -// -// } catch (error) { -// stepResults.push({ -// stepNumber: step.stepNumber, -// tool: step.tool, -// success: false, -// attempts: attempt + 1 -// }); -// return { -// success: false, -// failedAtStep: step.stepNumber, -// error: 'Step ' + step.stepNumber + ' (' + step.tool + ') threw: ' + error.message, -// duration: Date.now() - replayStart, -// stepResults -// }; -// } -// } -// -// return { -// success: true, -// failedAtStep: null, -// error: null, -// duration: Date.now() - replayStart, -// stepResults -// }; -// } -// -// /** -// * Execute replay script with a timeout. -// * Timeout is based on the script's estimated duration with a 3x multiplier. -// * @param {number} tabId -// * @param {Object} script - RecordedScript -// * @param {Object} agent -// * @returns {Promise} -// */ -// _executeWithTimeoutReplay(tabId, script, agent) { -// const timeout = Math.min( -// (script.estimatedDuration || 30000) * 3, -// this.EXECUTION_TIMEOUT * 0.6 -// ); -// -// return new Promise((resolve) => { -// const timeoutId = setTimeout(() => { -// resolve({ -// success: false, -// failedAtStep: -1, -// error: 'Replay timed out after ' + Math.round(timeout / 1000) + 's', -// duration: timeout -// }); -// }, timeout); -// -// this._executeReplayScript(tabId, script, agent) -// .then(result => { -// clearTimeout(timeoutId); -// resolve(result); -// }) -// .catch(error => { -// clearTimeout(timeoutId); -// resolve({ -// success: false, -// failedAtStep: -1, -// error: 'Replay error: ' + error.message, -// duration: 0 -// }); -// }); -// }); -// } -// -// /** -// * Ensure content script is loaded and ready on a tab -// * @param {number} tabId -// */ -// async _ensureContentScript(tabId) { -// const isReady = await waitForContentScriptReady(tabId, 8000); -// if (!isReady) { -// await ensureContentScriptInjected(tabId); -// const retryReady = await waitForContentScriptReady(tabId, 5000); -// if (!retryReady) { -// throw new Error('Content script failed to initialize on target page'); -// } -// } -// } -// -// /** -// * Execute automation with a timeout wrapper -// * @param {number} tabId -// * @param {string} task -// * @param {Object} options -// * @returns {Promise} -// */ -// _executeWithTimeout(tabId, task, options) { -// return new Promise((resolve, reject) => { -// const timeoutId = setTimeout(() => { -// reject(new Error('Agent execution timed out after ' + (this.EXECUTION_TIMEOUT / 1000) + ' seconds')); -// }, this.EXECUTION_TIMEOUT); -// -// // Store timeout so we can clear it on cleanup -// const state = this._running.get(options.agentId); -// if (state) state.timeoutId = timeoutId; -// -// // Use the shared executeAutomationTask function from background.js -// executeAutomationTask(tabId, task, options) -// .then(result => { -// clearTimeout(timeoutId); -// resolve(result); -// }) -// .catch(error => { -// clearTimeout(timeoutId); -// reject(error); -// }); -// }); -// } -// -// /** -// * Wait for a tab to finish loading -// * @param {number} tabId -// * @param {number} timeout -// * @returns {Promise} -// */ -// _waitForTabLoad(tabId, timeout = 15000) { -// return new Promise((resolve, reject) => { -// const timer = setTimeout(() => { -// chrome.tabs.onUpdated.removeListener(listener); -// // Proceed even if timeout - page might be usable -// resolve(); -// }, timeout); -// -// const listener = (updatedTabId, changeInfo) => { -// if (updatedTabId === tabId && changeInfo.status === 'complete') { -// clearTimeout(timer); -// chrome.tabs.onUpdated.removeListener(listener); -// // Small delay for dynamic content -// setTimeout(resolve, 500); -// } -// }; -// -// // Check if already loaded -// chrome.tabs.get(tabId).then(tab => { -// if (tab.status === 'complete') { -// clearTimeout(timer); -// chrome.tabs.onUpdated.removeListener(listener); -// setTimeout(resolve, 500); -// } else { -// chrome.tabs.onUpdated.addListener(listener); -// } -// }).catch(() => { -// clearTimeout(timer); -// reject(new Error('Tab not accessible')); -// }); -// }); -// } -// -// /** -// * Cleanup after an agent execution -// * @param {string} agentId -// * @param {number} [tabId] -// */ -// _cleanup(agentId, tabId) { -// const state = this._running.get(agentId); -// if (state?.timeoutId) { -// clearTimeout(state.timeoutId); -// } -// this._running.delete(agentId); -// -// // Close background tab -// if (tabId) { -// chrome.tabs.remove(tabId).catch(() => { -// // Tab may already be closed -// }); -// } -// } -// -// /** -// * Force-stop a running agent -// * @param {string} agentId -// */ -// async forceStop(agentId) { -// const state = this._running.get(agentId); -// if (!state) return; -// -// console.log('[FSB Executor] Force stopping agent:', agentId); -// this._cleanup(agentId, state.tabId); -// } -// -// /** -// * Get status of all running agents -// * @returns {Object[]} -// */ -// getRunningAgents() { -// const result = []; -// for (const [agentId, state] of this._running) { -// result.push({ -// agentId, -// tabId: state.tabId, -// runningFor: Date.now() - state.startTime -// }); -// } -// return result; -// } -// } -// -// // Export for service worker importScripts -// const agentExecutor = new AgentExecutor(); diff --git a/extension/agents/agent-manager.js b/extension/agents/agent-manager.js deleted file mode 100644 index dce5ac684..000000000 --- a/extension/agents/agent-manager.js +++ /dev/null @@ -1,533 +0,0 @@ -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// /** -// * Background Agent Manager for FSB -// * Handles CRUD operations, run history, and storage coordination for background agents. -// */ -// -// class BackgroundAgentManager { -// constructor() { -// this.STORAGE_KEY = 'bgAgents'; -// this.MAX_HISTORY = 50; -// this._cache = null; -// this._cacheTime = 0; -// this.CACHE_TTL = 2000; // 2 seconds -// } -// -// /** -// * Generate a unique agent ID -// * @returns {string} -// */ -// _generateId() { -// return 'agent_' + Date.now().toString(36) + '_' + Math.random().toString(36).substring(2, 8); -// } -// -// _normalizeTargetUrl(targetUrl) { -// return typeof targetUrl === 'string' ? targetUrl.trim() : ''; -// } -// -// _normalizeStartMode(startMode, targetUrl) { -// if (startMode === 'pinned' || startMode === 'ai_routed') return startMode; -// return this._normalizeTargetUrl(targetUrl) ? 'pinned' : 'ai_routed'; -// } -// -// _validateStartConfig(startMode, targetUrl) { -// const normalizedTargetUrl = this._normalizeTargetUrl(targetUrl); -// const normalizedStartMode = this._normalizeStartMode(startMode, normalizedTargetUrl); -// -// if (normalizedStartMode === 'pinned') { -// if (!normalizedTargetUrl) { -// throw new Error('Pinned agents require a targetUrl'); -// } -// let parsed; -// try { -// parsed = new URL(normalizedTargetUrl); -// } catch { -// throw new Error('Pinned agents require a valid http/https targetUrl'); -// } -// if (!/^https?:$/i.test(parsed.protocol)) { -// throw new Error('Pinned agents require a valid http/https targetUrl'); -// } -// return { -// startMode: 'pinned', -// targetUrl: parsed.toString() -// }; -// } -// -// return { -// startMode: 'ai_routed', -// targetUrl: '' -// }; -// } -// -// _normalizeAgent(agent) { -// if (!agent || typeof agent !== 'object') return agent; -// const normalizedStartMode = this._normalizeStartMode(agent.startMode, agent.targetUrl); -// const normalizedTargetUrl = normalizedStartMode === 'pinned' -// ? this._normalizeTargetUrl(agent.targetUrl) -// : ''; -// -// return { -// ...agent, -// startMode: normalizedStartMode, -// targetUrl: normalizedTargetUrl, -// replayEnabled: agent.replayEnabled !== false -// }; -// } -// -// /** -// * Load all agents from storage -// * @returns {Promise} Map of agentId -> agent data -// */ -// async loadAgents() { -// if (this._cache && Date.now() - this._cacheTime < this.CACHE_TTL) { -// return this._cache; -// } -// try { -// const result = await chrome.storage.local.get(this.STORAGE_KEY); -// const storedAgents = result[this.STORAGE_KEY] || {}; -// const normalizedAgents = {}; -// for (const [agentId, agent] of Object.entries(storedAgents)) { -// normalizedAgents[agentId] = this._normalizeAgent(agent); -// } -// this._cache = normalizedAgents; -// this._cacheTime = Date.now(); -// return this._cache; -// } catch (error) { -// console.error('[FSB AgentManager] Failed to load agents:', error.message); -// return {}; -// } -// } -// -// /** -// * Save all agents to storage -// * @param {Object} agents - Map of agentId -> agent data -// */ -// async saveAgents(agents) { -// try { -// const normalizedAgents = {}; -// for (const [agentId, agent] of Object.entries(agents || {})) { -// normalizedAgents[agentId] = this._normalizeAgent(agent); -// } -// await chrome.storage.local.set({ [this.STORAGE_KEY]: normalizedAgents }); -// this._cache = normalizedAgents; -// this._cacheTime = Date.now(); -// } catch (error) { -// console.error('[FSB AgentManager] Failed to save agents:', error.message); -// throw error; -// } -// } -// -// /** -// * Create a new background agent -// * @param {Object} params - Agent configuration -// * @param {string} params.name - Agent display name -// * @param {string} params.task - Task description for the AI -// * @param {string} [params.startMode='pinned'] - 'pinned' or 'ai_routed' -// * @param {string} [params.targetUrl] - URL to navigate to before executing when pinned -// * @param {Object} params.schedule - Schedule configuration -// * @param {string} params.schedule.type - 'interval' | 'daily' | 'once' -// * @param {number} [params.schedule.intervalMinutes] - Minutes between runs (for interval type) -// * @param {string} [params.schedule.dailyTime] - HH:MM format (for daily type) -// * @param {number[]} [params.schedule.daysOfWeek] - 0-6, Sun-Sat (for daily type) -// * @param {number} [params.maxIterations=15] - Max automation iterations per run -// * @returns {Promise} The created agent -// */ -// async createAgent(params) { -// const { name, task, targetUrl, startMode, schedule, maxIterations = 15, replayEnabled = true } = params; -// -// if (!name || !task) { -// throw new Error('Agent requires name and task'); -// } -// if (!schedule || !schedule.type) { -// throw new Error('Agent requires a schedule with type'); -// } -// if (!['interval', 'daily', 'once', 'cron'].includes(schedule.type)) { -// throw new Error('Schedule type must be interval, daily, once, or cron'); -// } -// if (schedule.type === 'cron' && !schedule.cronExpression) { -// throw new Error('Cron schedule requires a cronExpression field'); -// } -// -// const agents = await this.loadAgents(); -// const agentId = this._generateId(); -// const startConfig = this._validateStartConfig(startMode, targetUrl); -// -// const agent = { -// agentId, -// name: name.trim(), -// task: task.trim(), -// startMode: startConfig.startMode, -// targetUrl: startConfig.targetUrl, -// schedule: { ...schedule }, -// enabled: true, -// createdAt: Date.now(), -// lastRunAt: null, -// lastRunStatus: null, -// lastRunResult: null, -// runCount: 0, -// totalTokensUsed: 0, -// totalCostUsd: 0, -// maxIterations: Math.min(Math.max(1, maxIterations), 50), -// serverHashKey: '', -// syncEnabled: false, -// replayEnabled: replayEnabled, -// recordedScript: null, -// replayStats: { -// totalReplays: 0, -// totalAISaves: 0, -// estimatedCostSaved: 0, -// stepSuccessRates: {}, -// needsReRecord: false -// }, -// runHistory: [], -// retryCount: 0, -// retryMaxAttempts: 3, -// lastRetryAt: null -// }; -// -// agents[agentId] = agent; -// await this.saveAgents(agents); -// -// console.log('[FSB AgentManager] Agent created:', agentId, name); -// return agent; -// } -// -// /** -// * Update an existing agent -// * @param {string} agentId - Agent to update -// * @param {Object} updates - Fields to update -// * @returns {Promise} Updated agent -// */ -// async updateAgent(agentId, updates) { -// const agents = await this.loadAgents(); -// const agent = agents[agentId]; -// if (!agent) { -// throw new Error('Agent not found: ' + agentId); -// } -// -// // Fields that can be updated -// const allowedFields = [ -// 'name', 'task', 'startMode', 'targetUrl', 'schedule', 'enabled', -// 'maxIterations', 'serverHashKey', 'syncEnabled', -// 'replayEnabled', 'recordedScript', 'replayStats' -// ]; -// -// const startConfig = this._validateStartConfig( -// updates.startMode !== undefined ? updates.startMode : agent.startMode, -// updates.targetUrl !== undefined ? updates.targetUrl : agent.targetUrl -// ); -// -// for (const field of allowedFields) { -// if (updates[field] !== undefined) { -// if (field === 'maxIterations') { -// agent[field] = Math.min(Math.max(1, updates[field]), 50); -// } else if (field === 'name' || field === 'task') { -// agent[field] = typeof updates[field] === 'string' ? updates[field].trim() : updates[field]; -// } else if (field === 'startMode' || field === 'targetUrl') { -// // Applied after validating the combined start config. -// } else { -// agent[field] = updates[field]; -// } -// } -// } -// -// agent.startMode = startConfig.startMode; -// agent.targetUrl = startConfig.targetUrl; -// -// agents[agentId] = agent; -// await this.saveAgents(agents); -// -// console.log('[FSB AgentManager] Agent updated:', agentId); -// return agent; -// } -// -// /** -// * Delete an agent -// * @param {string} agentId - Agent to delete -// * @returns {Promise} -// */ -// async deleteAgent(agentId) { -// const agents = await this.loadAgents(); -// if (!agents[agentId]) { -// return false; -// } -// delete agents[agentId]; -// await this.saveAgents(agents); -// console.log('[FSB AgentManager] Agent deleted:', agentId); -// return true; -// } -// -// /** -// * Get a single agent by ID -// * @param {string} agentId -// * @returns {Promise} -// */ -// async getAgent(agentId) { -// const agents = await this.loadAgents(); -// return agents[agentId] || null; -// } -// -// /** -// * List all agents -// * @returns {Promise} -// */ -// async listAgents() { -// const agents = await this.loadAgents(); -// return Object.values(agents).sort((a, b) => b.createdAt - a.createdAt); -// } -// -// /** -// * Toggle agent enabled/disabled state -// * @param {string} agentId -// * @returns {Promise} Updated agent -// */ -// async toggleAgent(agentId) { -// const agents = await this.loadAgents(); -// const agent = agents[agentId]; -// if (!agent) { -// throw new Error('Agent not found: ' + agentId); -// } -// agent.enabled = !agent.enabled; -// agents[agentId] = agent; -// await this.saveAgents(agents); -// console.log('[FSB AgentManager] Agent toggled:', agentId, 'enabled:', agent.enabled); -// return agent; -// } -// -// /** -// * Record a run result for an agent -// * @param {string} agentId -// * @param {Object} runResult -// * @param {boolean} runResult.success -// * @param {string} [runResult.sessionId] -// * @param {string} [runResult.result] -// * @param {string} [runResult.error] -// * @param {number} [runResult.duration] -// * @param {number} [runResult.tokensUsed] -// * @param {number} [runResult.costUsd] -// * @param {number} [runResult.iterations] -// * @returns {Promise} Updated agent -// */ -// async recordRun(agentId, runResult) { -// const agents = await this.loadAgents(); -// const agent = agents[agentId]; -// if (!agent) { -// throw new Error('Agent not found: ' + agentId); -// } -// -// const runEntry = { -// runId: 'run_' + Date.now().toString(36), -// timestamp: Date.now(), -// success: runResult.success, -// sessionId: runResult.sessionId || null, -// result: runResult.result ? runResult.result.substring(0, 500) : null, -// error: runResult.error ? runResult.error.substring(0, 500) : null, -// duration: runResult.duration || 0, -// tokensUsed: runResult.tokensUsed || 0, -// costUsd: runResult.costUsd || 0, -// iterations: runResult.iterations || 0, -// executionMode: runResult.executionMode || 'ai_initial', -// replayFailedAtStep: runResult.replayFailedAtStep || null, -// costSaved: runResult.costSaved || 0 -// }; -// -// // Update agent stats -// agent.lastRunAt = Date.now(); -// agent.lastRunStatus = runResult.success ? 'success' : 'failed'; -// agent.lastRunResult = runEntry.result || runEntry.error; -// agent.runCount++; -// agent.totalTokensUsed += runEntry.tokensUsed; -// agent.totalCostUsd += runEntry.costUsd; -// -// // Update replay stats -// if (!agent.replayStats) { -// agent.replayStats = { totalReplays: 0, totalAISaves: 0, estimatedCostSaved: 0, stepSuccessRates: {}, needsReRecord: false }; -// } -// if (!agent.replayStats.stepSuccessRates) { -// agent.replayStats.stepSuccessRates = {}; -// } -// if (runEntry.executionMode === 'replay') { -// agent.replayStats.totalReplays++; -// agent.replayStats.estimatedCostSaved += runEntry.costSaved || 0; -// -// // Track per-step success rates -// if (runResult.stepResults) { -// for (const sr of runResult.stepResults) { -// const key = String(sr.stepNumber); -// const existing = agent.replayStats.stepSuccessRates[key] || { successes: 0, total: 0 }; -// existing.total++; -// if (sr.success) existing.successes++; -// agent.replayStats.stepSuccessRates[key] = existing; -// } -// } -// -// // Check for unreliable steps (below 50% success rate with >= 4 data points) -// const unreliableSteps = Object.entries(agent.replayStats.stepSuccessRates) -// .filter(([_, s]) => s.total >= 4 && (s.successes / s.total) < 0.5); -// -// if (unreliableSteps.length > 0) { -// agent.replayStats.needsReRecord = true; -// console.log('[FSB Agent] Steps below 50% success rate:', unreliableSteps.map(([k]) => k).join(', '), -// '-- flagging for re-record'); -// } -// } else if (runEntry.executionMode === 'ai_fallback') { -// agent.replayStats.totalAISaves++; -// // If AI fallback succeeded with a new script, reset step tracking -// if (runResult.success) { -// agent.replayStats.stepSuccessRates = {}; -// agent.replayStats.needsReRecord = false; -// } -// } else if (runEntry.executionMode === 'ai_initial') { -// // Fresh AI run with new script -- reset step tracking -// if (runResult.success) { -// agent.replayStats.stepSuccessRates = {}; -// agent.replayStats.needsReRecord = false; -// } -// } -// -// // Add to history, cap at MAX_HISTORY -// if (!agent.runHistory) agent.runHistory = []; -// agent.runHistory.unshift(runEntry); -// if (agent.runHistory.length > this.MAX_HISTORY) { -// agent.runHistory = agent.runHistory.slice(0, this.MAX_HISTORY); -// } -// -// agents[agentId] = agent; -// await this.saveAgents(agents); -// -// console.log('[FSB AgentManager] Run recorded for agent:', agentId, 'success:', runResult.success); -// return agent; -// } -// -// /** -// * Reset retry counter for an agent (on success or max retries reached) -// * @param {string} agentId -// * @returns {Promise} -// */ -// async resetRetry(agentId) { -// const agents = await this.loadAgents(); -// if (!agents[agentId]) return null; -// agents[agentId].retryCount = 0; -// agents[agentId].lastRetryAt = null; -// await this.saveAgents(agents); -// return agents[agentId]; -// } -// -// /** -// * Increment retry counter for an agent -// * @param {string} agentId -// * @returns {Promise} -// */ -// async incrementRetry(agentId) { -// const agents = await this.loadAgents(); -// if (!agents[agentId]) return null; -// agents[agentId].retryCount = (agents[agentId].retryCount || 0) + 1; -// agents[agentId].lastRetryAt = Date.now(); -// await this.saveAgents(agents); -// return agents[agentId]; -// } -// -// /** -// * Get run history for an agent -// * @param {string} agentId -// * @param {number} [limit=10] -// * @returns {Promise} -// */ -// async getRunHistory(agentId, limit = 10) { -// const agent = await this.getAgent(agentId); -// if (!agent) return []; -// return (agent.runHistory || []).slice(0, limit); -// } -// -// /** -// * Save a recorded script to an agent for replay -// * @param {string} agentId -// * @param {Object} script - RecordedScript object -// * @returns {Promise} Updated agent -// */ -// async saveRecordedScript(agentId, script) { -// const agents = await this.loadAgents(); -// const agent = agents[agentId]; -// if (!agent) { -// throw new Error('Agent not found: ' + agentId); -// } -// -// agent.recordedScript = script; -// agents[agentId] = agent; -// await this.saveAgents(agents); -// -// console.log('[FSB AgentManager] Recorded script saved for agent:', agentId, 'steps:', script?.totalSteps || 0); -// return agent; -// } -// -// /** -// * Clear the recorded script for an agent (forces AI on next run) -// * @param {string} agentId -// * @returns {Promise} Updated agent -// */ -// async clearRecordedScript(agentId) { -// const agents = await this.loadAgents(); -// const agent = agents[agentId]; -// if (!agent) { -// throw new Error('Agent not found: ' + agentId); -// } -// -// agent.recordedScript = null; -// agents[agentId] = agent; -// await this.saveAgents(agents); -// -// console.log('[FSB AgentManager] Recorded script cleared for agent:', agentId); -// return agent; -// } -// -// /** -// * Get aggregate stats across all agents -// * @returns {Promise} -// */ -// async getStats() { -// const agents = await this.listAgents(); -// const now = Date.now(); -// const oneDayAgo = now - 24 * 60 * 60 * 1000; -// -// let totalRuns = 0; -// let runsToday = 0; -// let successfulRuns = 0; -// let totalTokens = 0; -// let totalCost = 0; -// let totalReplayRuns = 0; -// let totalCostSaved = 0; -// -// for (const agent of agents) { -// totalRuns += agent.runCount; -// totalTokens += agent.totalTokensUsed; -// totalCost += agent.totalCostUsd; -// -// if (agent.replayStats) { -// totalReplayRuns += agent.replayStats.totalReplays || 0; -// totalCostSaved += agent.replayStats.estimatedCostSaved || 0; -// } -// -// for (const run of (agent.runHistory || [])) { -// if (run.timestamp > oneDayAgo) { -// runsToday++; -// if (run.success) successfulRuns++; -// } -// } -// } -// -// return { -// totalAgents: agents.length, -// enabledAgents: agents.filter(a => a.enabled).length, -// totalRuns, -// runsToday, -// successRate: totalRuns > 0 ? Math.round((successfulRuns / Math.max(runsToday, 1)) * 100) : 0, -// totalTokens, -// totalCost: Math.round(totalCost * 10000) / 10000, -// totalReplayRuns, -// totalCostSaved: Math.round(totalCostSaved * 10000) / 10000 -// }; -// } -// } -// -// // Export for service worker importScripts -// const agentManager = new BackgroundAgentManager(); diff --git a/extension/agents/agent-scheduler.js b/extension/agents/agent-scheduler.js deleted file mode 100644 index b472d26d4..000000000 --- a/extension/agents/agent-scheduler.js +++ /dev/null @@ -1,460 +0,0 @@ -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// /** -// * Background Agent Scheduler for FSB -// * Manages chrome.alarms for scheduled agent execution. -// * Alarm names are prefixed with 'fsb_agent_' to avoid collisions. -// */ -// -// class AgentScheduler { -// constructor() { -// this.ALARM_PREFIX = 'fsb_agent_'; -// } -// -// /** -// * Get the alarm name for an agent -// * @param {string} agentId -// * @returns {string} -// */ -// getAlarmName(agentId) { -// return this.ALARM_PREFIX + agentId; -// } -// -// /** -// * Extract agentId from alarm name -// * @param {string} alarmName -// * @returns {string|null} -// */ -// getAgentIdFromAlarm(alarmName) { -// if (!alarmName || !alarmName.startsWith(this.ALARM_PREFIX)) return null; -// return alarmName.substring(this.ALARM_PREFIX.length); -// } -// -// /** -// * Schedule an alarm for an agent based on its schedule configuration -// * @param {Object} agent - Agent data with schedule field -// * @returns {Promise} Whether alarm was created -// */ -// async scheduleAgent(agent) { -// if (!agent || !agent.enabled || !agent.schedule) { -// return false; -// } -// -// const alarmName = this.getAlarmName(agent.agentId); -// -// // Clear any existing alarm first -// await this.clearAlarm(agent.agentId); -// -// const schedule = agent.schedule; -// -// switch (schedule.type) { -// case 'interval': { -// const minutes = Math.max(1, schedule.intervalMinutes || 1); -// await chrome.alarms.create(alarmName, { -// delayInMinutes: minutes, -// periodInMinutes: minutes -// }); -// console.log('[FSB Scheduler] Interval alarm created:', alarmName, 'every', minutes, 'min'); -// return true; -// } -// -// case 'daily': { -// const nextRun = this._calculateNextDailyRun(schedule.dailyTime, schedule.daysOfWeek); -// if (!nextRun) { -// console.warn('[FSB Scheduler] Could not calculate next daily run for:', agent.agentId); -// return false; -// } -// await chrome.alarms.create(alarmName, { when: nextRun }); -// console.log('[FSB Scheduler] Daily alarm created:', alarmName, 'at', new Date(nextRun).toLocaleString()); -// return true; -// } -// -// case 'cron': { -// const nextRun = this._calculateNextCronRun(schedule.cronExpression); -// if (!nextRun) { -// console.warn('[FSB Scheduler] Could not calculate next cron run for:', agent.agentId); -// return false; -// } -// await chrome.alarms.create(alarmName, { when: nextRun }); -// console.log('[FSB Scheduler] Cron alarm created:', alarmName, 'next at', new Date(nextRun).toLocaleString()); -// return true; -// } -// -// case 'once': { -// // For 'once' type, schedule immediately if never run, or skip if already run -// if (agent.runCount > 0) { -// console.log('[FSB Scheduler] Once agent already ran, skipping:', agent.agentId); -// return false; -// } -// // Run in 1 minute (minimum alarm delay) -// await chrome.alarms.create(alarmName, { delayInMinutes: 1 }); -// console.log('[FSB Scheduler] One-time alarm created:', alarmName); -// return true; -// } -// -// default: -// console.warn('[FSB Scheduler] Unknown schedule type:', schedule.type); -// return false; -// } -// } -// -// /** -// * Calculate the next occurrence timestamp for a daily schedule -// * @param {string} dailyTime - HH:MM format -// * @param {number[]} [daysOfWeek] - 0-6 (Sun-Sat), null/empty = every day -// * @returns {number|null} Timestamp of next run -// */ -// _calculateNextDailyRun(dailyTime, daysOfWeek) { -// if (!dailyTime) return null; -// -// const [hours, minutes] = dailyTime.split(':').map(Number); -// if (isNaN(hours) || isNaN(minutes)) return null; -// -// const now = new Date(); -// const target = new Date(); -// target.setHours(hours, minutes, 0, 0); -// -// // If target time already passed today, start from tomorrow -// if (target <= now) { -// target.setDate(target.getDate() + 1); -// } -// -// // If specific days are specified, find the next matching day -// if (daysOfWeek && daysOfWeek.length > 0) { -// const validDays = new Set(daysOfWeek); -// let attempts = 0; -// while (!validDays.has(target.getDay()) && attempts < 7) { -// target.setDate(target.getDate() + 1); -// attempts++; -// } -// if (attempts >= 7) return null; // No valid days -// } -// -// return target.getTime(); -// } -// -// /** -// * Clear the alarm for an agent -// * @param {string} agentId -// */ -// async clearAlarm(agentId) { -// const alarmName = this.getAlarmName(agentId); -// try { -// await chrome.alarms.clear(alarmName); -// } catch (error) { -// // Alarm may not exist, that's fine -// } -// } -// -// /** -// * Clear all FSB agent alarms -// */ -// async clearAllAlarms() { -// try { -// const allAlarms = await chrome.alarms.getAll(); -// for (const alarm of allAlarms) { -// if (alarm.name.startsWith(this.ALARM_PREFIX)) { -// await chrome.alarms.clear(alarm.name); -// } -// } -// console.log('[FSB Scheduler] All agent alarms cleared'); -// } catch (error) { -// console.error('[FSB Scheduler] Error clearing alarms:', error.message); -// } -// } -// -// /** -// * Reschedule all enabled agents. Called on SW startup/install. -// */ -// async rescheduleAllAgents() { -// try { -// const agents = await agentManager.listAgents(); -// let scheduled = 0; -// -// for (const agent of agents) { -// if (agent.enabled) { -// const success = await this.scheduleAgent(agent); -// if (success) scheduled++; -// } -// } -// -// console.log('[FSB Scheduler] Rescheduled', scheduled, 'of', agents.length, 'agents'); -// } catch (error) { -// console.error('[FSB Scheduler] Error rescheduling agents:', error.message); -// } -// } -// -// /** -// * Reschedule a daily agent for its next occurrence (called after each run) -// * @param {Object} agent -// */ -// async rescheduleDaily(agent) { -// if (agent.schedule.type !== 'daily') return; -// await this.scheduleAgent(agent); -// } -// -// /** -// * Reschedule a cron agent for its next occurrence (called after each run) -// * @param {Object} agent -// */ -// async rescheduleCron(agent) { -// if (agent.schedule.type !== 'cron') return; -// await this.scheduleAgent(agent); -// } -// -// /** -// * Parse a standard 5-field cron expression (minute hour dayOfMonth month dayOfWeek). -// * Supports: numeric values, wildcards (*), ranges (1-5), lists (1,3,5). -// * @param {string} expression - Cron expression string -// * @returns {Object|null} Parsed object or null if invalid -// */ -// parseCron(expression) { -// if (!expression || typeof expression !== 'string') return null; -// -// const parts = expression.trim().split(/\s+/); -// if (parts.length !== 5) return null; -// -// const limits = [ -// { min: 0, max: 59 }, // minute -// { min: 0, max: 23 }, // hour -// { min: 1, max: 31 }, // dayOfMonth -// { min: 1, max: 12 }, // month -// { min: 0, max: 6 } // dayOfWeek -// ]; -// -// const fieldNames = ['minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek']; -// const parsed = {}; -// -// for (let i = 0; i < 5; i++) { -// const field = parts[i]; -// const { min, max } = limits[i]; -// -// if (field === '*') { -// parsed[fieldNames[i]] = '*'; -// continue; -// } -// -// // Check for range (e.g., 1-5) -// if (field.includes('-') && !field.includes(',')) { -// const [startStr, endStr] = field.split('-'); -// const start = parseInt(startStr, 10); -// const end = parseInt(endStr, 10); -// if (isNaN(start) || isNaN(end) || start < min || end > max || start > end) return null; -// const arr = []; -// for (let v = start; v <= end; v++) arr.push(v); -// parsed[fieldNames[i]] = arr; -// continue; -// } -// -// // Check for list (e.g., 1,3,5) -// if (field.includes(',')) { -// const values = field.split(',').map(v => parseInt(v.trim(), 10)); -// if (values.some(v => isNaN(v) || v < min || v > max)) return null; -// parsed[fieldNames[i]] = values; -// continue; -// } -// -// // Single numeric value -// const val = parseInt(field, 10); -// if (isNaN(val) || val < min || val > max) return null; -// // Single values for minute/hour stored as number, day fields as array for consistency -// if (i <= 1) { -// parsed[fieldNames[i]] = val; -// } else { -// parsed[fieldNames[i]] = [val]; -// } -// } -// -// return parsed; -// } -// -// /** -// * Calculate the next matching timestamp for a cron expression from now. -// * @param {string} cronExpression - 5-field cron expression -// * @returns {number|null} Timestamp of next run, or null if invalid -// */ -// _calculateNextCronRun(cronExpression) { -// const cron = this.parseCron(cronExpression); -// if (!cron) return null; -// -// const now = new Date(); -// const candidate = new Date(now); -// // Round up to next minute -// candidate.setSeconds(0, 0); -// candidate.setMinutes(candidate.getMinutes() + 1); -// -// // Search up to 366 days ahead -// const maxTime = now.getTime() + 366 * 24 * 60 * 60 * 1000; -// -// while (candidate.getTime() < maxTime) { -// const matches = this._cronFieldMatches(cron, candidate); -// if (matches) return candidate.getTime(); -// -// // Optimization: skip ahead when possible -// if (cron.hour !== '*' && !this._valueInField(candidate.getHours(), cron.hour)) { -// // Skip to next hour -// candidate.setMinutes(0); -// candidate.setHours(candidate.getHours() + 1); -// continue; -// } -// -// // Advance by 1 minute -// candidate.setMinutes(candidate.getMinutes() + 1); -// } -// -// return null; -// } -// -// /** -// * Check if a date matches all cron fields -// * @param {Object} cron - Parsed cron object -// * @param {Date} date -// * @returns {boolean} -// */ -// _cronFieldMatches(cron, date) { -// if (!this._valueInField(date.getMinutes(), cron.minute)) return false; -// if (!this._valueInField(date.getHours(), cron.hour)) return false; -// if (!this._valueInField(date.getDate(), cron.dayOfMonth)) return false; -// if (!this._valueInField(date.getMonth() + 1, cron.month)) return false; -// if (!this._valueInField(date.getDay(), cron.dayOfWeek)) return false; -// return true; -// } -// -// /** -// * Check if a value matches a cron field (wildcard, number, or array) -// * @param {number} value -// * @param {string|number|number[]} field -// * @returns {boolean} -// */ -// _valueInField(value, field) { -// if (field === '*') return true; -// if (typeof field === 'number') return value === field; -// if (Array.isArray(field)) return field.includes(value); -// return false; -// } -// -// /** -// * Generate a human-readable description for any schedule type -// * @param {Object} schedule - Schedule configuration object -// * @returns {string} -// */ -// getScheduleDescription(schedule) { -// if (!schedule || !schedule.type) return 'Unknown schedule'; -// -// const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; -// -// switch (schedule.type) { -// case 'interval': { -// const mins = schedule.intervalMinutes || 1; -// if (mins >= 60 && mins % 60 === 0) { -// const hours = mins / 60; -// return hours === 1 ? 'Every 1 hour' : `Every ${hours} hours`; -// } -// return `Every ${mins} minutes`; -// } -// -// case 'daily': { -// const time = schedule.dailyTime || '00:00'; -// if (schedule.daysOfWeek && schedule.daysOfWeek.length > 0 && schedule.daysOfWeek.length < 7) { -// const names = schedule.daysOfWeek.map(d => dayNames[d]).join(', '); -// return `${names} at ${time}`; -// } -// return `Daily at ${time}`; -// } -// -// case 'once': -// return 'One-time execution'; -// -// case 'cron': { -// const expr = schedule.cronExpression; -// if (!expr) return 'Cron: (no expression)'; -// const cron = this.parseCron(expr); -// if (!cron) return `Cron: ${expr}`; -// -// // Try to produce natural language for common patterns -// const timeStr = String(typeof cron.hour === 'number' ? cron.hour : '').padStart(2, '0') + ':' + -// String(typeof cron.minute === 'number' ? cron.minute : '').padStart(2, '0'); -// const hasSpecificTime = typeof cron.hour === 'number' && typeof cron.minute === 'number'; -// -// // Weekday pattern: * * 1-5 or [1,2,3,4,5] -// if (hasSpecificTime && cron.dayOfMonth === '*' && cron.month === '*' && -// Array.isArray(cron.dayOfWeek) && cron.dayOfWeek.length === 5 && -// [1,2,3,4,5].every(d => cron.dayOfWeek.includes(d))) { -// return `Every weekday at ${timeStr}`; -// } -// -// // Specific days of week -// if (hasSpecificTime && cron.dayOfMonth === '*' && cron.month === '*' && -// Array.isArray(cron.dayOfWeek)) { -// const names = cron.dayOfWeek.map(d => dayNames[d]).join(', '); -// return `${names} at ${timeStr}`; -// } -// -// // Day of month pattern (e.g., 1st of every month) -// if (hasSpecificTime && Array.isArray(cron.dayOfMonth) && cron.month === '*' && cron.dayOfWeek === '*') { -// const dom = cron.dayOfMonth; -// if (dom.length === 1) { -// const suffix = dom[0] === 1 ? 'st' : dom[0] === 2 ? 'nd' : dom[0] === 3 ? 'rd' : 'th'; -// return `${dom[0]}${suffix} of every month at ${timeStr}`; -// } -// } -// -// // Every day at specific time -// if (hasSpecificTime && cron.dayOfMonth === '*' && cron.month === '*' && cron.dayOfWeek === '*') { -// return `Daily at ${timeStr}`; -// } -// -// // Fallback -// return `Cron: ${expr}`; -// } -// -// default: -// return 'Unknown schedule'; -// } -// } -// -// /** -// * Check if an alarm fire is valid (guard against double-runs) -// * @param {Object} agent -// * @returns {boolean} -// */ -// isValidAlarmFire(agent) { -// if (!agent.lastRunAt) return true; -// -// const timeSinceLastRun = Date.now() - agent.lastRunAt; -// -// if (agent.schedule.type === 'interval') { -// // Must have at least 80% of interval elapsed to prevent double-fire -// const intervalMs = (agent.schedule.intervalMinutes || 1) * 60 * 1000; -// return timeSinceLastRun >= intervalMs * 0.8; -// } -// -// if (agent.schedule.type === 'once') { -// // Once agents should only run once -// return agent.runCount === 0; -// } -// -// if (agent.schedule.type === 'cron') { -// // Cron intervals vary; require at least 50 seconds since last run to prevent double-fire -// return timeSinceLastRun >= 50 * 1000; -// } -// -// // Daily: must be at least 23 hours since last run -// return timeSinceLastRun >= 23 * 60 * 60 * 1000; -// } -// -// /** -// * Get all active alarms for FSB agents -// * @returns {Promise} -// */ -// async getActiveAlarms() { -// try { -// const allAlarms = await chrome.alarms.getAll(); -// return allAlarms.filter(a => a.name.startsWith(this.ALARM_PREFIX)); -// } catch (error) { -// return []; -// } -// } -// } -// -// // Export for service worker importScripts -// const agentScheduler = new AgentScheduler(); diff --git a/extension/agents/server-sync.js b/extension/agents/server-sync.js deleted file mode 100644 index 16be8c7c5..000000000 --- a/extension/agents/server-sync.js +++ /dev/null @@ -1,242 +0,0 @@ -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// /** -// * Server Sync for FSB Background Agents -// * Extension-side HTTP client for syncing agent results to the backend server. -// * Fully optional - extension works without server. -// * Queue persists to chrome.storage.local to survive service worker restarts. -// */ -// -// class ServerSync { -// constructor() { -// this.MAX_RETRIES = 3; -// this.RETRY_DELAYS = [1000, 3000, 8000]; // Exponential backoff -// this.QUEUE_STORAGE_KEY = 'fsb_sync_queue'; -// this._syncing = false; -// } -// -// /** -// * Get server config from storage -// * @returns {Promise<{url: string, hashKey: string, enabled: boolean}>} -// */ -// async getConfig() { -// try { -// const stored = await chrome.storage.local.get(['serverHashKey', 'serverSyncEnabled']); -// return { -// url: 'https://full-selfbrowsing.com', -// hashKey: stored.serverHashKey || '', -// enabled: stored.serverSyncEnabled || false -// }; -// } catch { -// return { url: '', hashKey: '', enabled: false }; -// } -// } -// -// /** -// * Load sync queue from persistent storage -// * @returns {Promise} -// */ -// async _loadQueue() { -// try { -// const stored = await chrome.storage.local.get([this.QUEUE_STORAGE_KEY]); -// return stored[this.QUEUE_STORAGE_KEY] || []; -// } catch { -// return []; -// } -// } -// -// /** -// * Save sync queue to persistent storage -// * @param {Array} queue -// */ -// async _saveQueue(queue) { -// try { -// await chrome.storage.local.set({ [this.QUEUE_STORAGE_KEY]: queue }); -// } catch (error) { -// console.error('[FSB Sync] Failed to persist queue:', error.message); -// } -// } -// -// /** -// * Sync a run result to the server -// * @param {Object} agent - Agent data -// * @param {Object} runResult - Run result to sync -// * @returns {Promise} -// */ -// async syncRun(agent, runResult) { -// const config = await this.getConfig(); -// if (!config.url || !config.hashKey) return false; -// -// const payload = { -// agentId: agent.agentId, -// name: agent.name, -// task: agent.task, -// startMode: agent.startMode || 'pinned', -// targetUrl: agent.targetUrl || '', -// scheduleType: agent.schedule?.type, -// scheduleConfig: JSON.stringify(agent.schedule), -// enabled: agent.enabled, -// run: { -// runId: runResult.runId || 'run_' + Date.now().toString(36), -// startedAt: new Date(Date.now() - (runResult.duration || 0)).toISOString(), -// completedAt: new Date().toISOString(), -// status: runResult.success ? 'success' : 'failed', -// result: runResult.result || null, -// error: runResult.error || null, -// iterations: runResult.iterations || 0, -// tokensUsed: runResult.tokensUsed || 0, -// costUsd: runResult.costUsd || 0, -// durationMs: runResult.duration || 0, -// executionMode: runResult.executionMode || 'ai_initial', -// costSaved: runResult.costSaved || 0 -// } -// }; -// -// for (let attempt = 0; attempt < this.MAX_RETRIES; attempt++) { -// try { -// const resp = await fetch(config.url + '/api/agents/' + agent.agentId + '/runs', { -// method: 'POST', -// headers: { -// 'Content-Type': 'application/json', -// 'X-FSB-Hash-Key': config.hashKey -// }, -// body: JSON.stringify(payload) -// }); -// -// if (resp.ok) { -// console.log('[FSB Sync] Run synced for agent:', agent.agentId); -// return true; -// } -// -// if (resp.status === 401) { -// console.warn('[FSB Sync] Invalid hash key, skipping sync'); -// return false; -// } -// -// // Retry on server errors -// if (resp.status >= 500) { -// await this._delay(this.RETRY_DELAYS[attempt] || 8000); -// continue; -// } -// -// console.warn('[FSB Sync] Sync failed with status:', resp.status); -// return false; -// -// } catch (error) { -// console.warn('[FSB Sync] Sync attempt', attempt + 1, 'failed:', error.message); -// if (attempt < this.MAX_RETRIES - 1) { -// await this._delay(this.RETRY_DELAYS[attempt] || 8000); -// } -// } -// } -// -// // All retries failed - queue for later (persistent) -// await this._queueSync(payload); -// return false; -// } -// -// /** -// * Sync agent definition to the server -// * @param {Object} agent - Agent data -// * @returns {Promise} -// */ -// async syncAgent(agent) { -// const config = await this.getConfig(); -// if (!config.url || !config.hashKey) return false; -// -// try { -// const resp = await fetch(config.url + '/api/agents', { -// method: 'POST', -// headers: { -// 'Content-Type': 'application/json', -// 'X-FSB-Hash-Key': config.hashKey -// }, -// body: JSON.stringify({ -// agentId: agent.agentId, -// name: agent.name, -// task: agent.task, -// startMode: agent.startMode || 'pinned', -// targetUrl: agent.targetUrl || '', -// scheduleType: agent.schedule?.type, -// scheduleConfig: JSON.stringify(agent.schedule), -// enabled: agent.enabled -// }) -// }); -// -// return resp.ok; -// } catch (error) { -// console.warn('[FSB Sync] Agent sync failed:', error.message); -// return false; -// } -// } -// -// /** -// * Queue a failed sync for later retry (persistent) -// * @param {Object} payload -// */ -// async _queueSync(payload) { -// const queue = await this._loadQueue(); -// queue.push({ payload, timestamp: Date.now() }); -// // Cap queue size -// const trimmed = queue.length > 100 ? queue.slice(-50) : queue; -// await this._saveQueue(trimmed); -// console.log('[FSB Sync] Queued sync, queue size:', trimmed.length); -// } -// -// /** -// * Process queued syncs (call periodically or when server becomes reachable) -// */ -// async processQueue() { -// if (this._syncing) return; -// const queue = await this._loadQueue(); -// if (queue.length === 0) return; -// this._syncing = true; -// -// const config = await this.getConfig(); -// if (!config.url || !config.hashKey) { -// this._syncing = false; -// return; -// } -// -// const failed = []; -// for (const item of queue) { -// try { -// const resp = await fetch(config.url + '/api/agents/' + item.payload.agentId + '/runs', { -// method: 'POST', -// headers: { -// 'Content-Type': 'application/json', -// 'X-FSB-Hash-Key': config.hashKey -// }, -// body: JSON.stringify(item.payload) -// }); -// -// if (!resp.ok && resp.status >= 500) { -// failed.push(item); -// } -// } catch { -// failed.push(item); -// } -// } -// -// await this._saveQueue(failed); -// this._syncing = false; -// if (failed.length > 0) { -// console.log('[FSB Sync] Queue processed, remaining:', failed.length); -// } -// } -// -// /** -// * Get current queue size for diagnostics -// * @returns {Promise} -// */ -// async getQueueSize() { -// const queue = await this._loadQueue(); -// return queue.length; -// } -// -// _delay(ms) { -// return new Promise(resolve => setTimeout(resolve, ms)); -// } -// } -// -// // Export for service worker importScripts -// const serverSync = new ServerSync(); diff --git a/extension/background.js b/extension/background.js index c1be193dd..e1f1cddc5 100644 --- a/extension/background.js +++ b/extension/background.js @@ -792,13 +792,6 @@ importScripts('site-guides/gaming/humble-bundle.js'); importScripts('site-guides/productivity/google-sheets.js'); importScripts('site-guides/productivity/google-docs.js'); -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// Background agent modules (commented out; modules retired in v0.9.45rc1) -// importScripts('agents/agent-manager.js'); -// importScripts('agents/agent-scheduler.js'); -// importScripts('agents/agent-executor.js'); -// importScripts('agents/server-sync.js'); - // Agent loop engine modules (Phase 181: re-enable modular agent loop per D-01, D-02) importScripts('ai/engine-config.js'); importScripts('ai/cost-tracker.js'); @@ -13028,176 +13021,6 @@ const fsbHandleRuntimeMessage = (request, sender, sendResponse) => { })(); return true; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// // --- Background Agent Management --- -// case 'createAgent': -// (async () => { -// try { -// const agent = await agentManager.createAgent(request.params); -// if (agent.enabled) { -// await agentScheduler.scheduleAgent(agent); -// } -// sendResponse({ success: true, agent }); -// } catch (error) { -// sendResponse({ success: false, error: error.message }); -// } -// })(); -// return true; -// -// case 'updateAgent': -// (async () => { -// try { -// const agent = await agentManager.updateAgent(request.agentId, request.updates); -// // Reschedule if schedule or enabled state changed -// if (agent.enabled) { -// await agentScheduler.scheduleAgent(agent); -// } else { -// await agentScheduler.clearAlarm(agent.agentId); -// } -// sendResponse({ success: true, agent }); -// } catch (error) { -// sendResponse({ success: false, error: error.message }); -// } -// })(); -// return true; -// -// case 'deleteAgent': -// (async () => { -// try { -// await agentScheduler.clearAlarm(request.agentId); -// await agentExecutor.forceStop(request.agentId); -// const deleted = await agentManager.deleteAgent(request.agentId); -// sendResponse({ success: deleted }); -// } catch (error) { -// sendResponse({ success: false, error: error.message }); -// } -// })(); -// return true; -// -// case 'listAgents': -// (async () => { -// try { -// const agents = await agentManager.listAgents(); -// sendResponse({ success: true, agents }); -// } catch (error) { -// sendResponse({ success: false, error: error.message }); -// } -// })(); -// return true; -// -// case 'toggleAgent': -// (async () => { -// try { -// const agent = await agentManager.toggleAgent(request.agentId); -// if (agent.enabled) { -// await agentScheduler.scheduleAgent(agent); -// } else { -// await agentScheduler.clearAlarm(agent.agentId); -// } -// sendResponse({ success: true, agent }); -// } catch (error) { -// sendResponse({ success: false, error: error.message }); -// } -// })(); -// return true; -// -// case 'runAgentNow': -// (async () => { -// try { -// const agent = await agentManager.getAgent(request.agentId); -// if (!agent) { -// sendResponse({ success: false, error: 'Agent not found' }); -// return; -// } -// // Execute immediately in background -// sendResponse({ success: true, message: 'Agent execution started' }); -// const result = await agentExecutor.execute(agent); -// await agentManager.recordRun(agent.agentId, result); -// chrome.runtime.sendMessage({ -// action: 'agentRunComplete', -// agentId: agent.agentId, -// result: { success: result.success, duration: result.duration, error: result.error } -// }).catch((err) => { -// console.warn('[FSB] agentRunComplete sendMessage delivery failed', { agentId: agent.agentId, error: err && err.message }); -// }); -// } catch (error) { -// sendResponse({ success: false, error: error.message }); -// } -// })(); -// return true; -// -// case 'getAgentStats': -// (async () => { -// try { -// const stats = await agentManager.getStats(); -// sendResponse({ success: true, stats }); -// } catch (error) { -// sendResponse({ success: false, error: error.message }); -// } -// })(); -// return true; -// -// case 'getAgentRunHistory': -// (async () => { -// try { -// const history = await agentManager.getRunHistory(request.agentId, request.limit || 10); -// sendResponse({ success: true, history }); -// } catch (error) { -// sendResponse({ success: false, error: error.message }); -// } -// })(); -// return true; -// -// case 'clearAgentScript': -// (async () => { -// try { -// await agentManager.clearRecordedScript(request.agentId); -// sendResponse({ success: true }); -// } catch (error) { -// sendResponse({ success: false, error: error.message }); -// } -// })(); -// return true; -// -// case 'getAgentReplayInfo': -// (async () => { -// try { -// const agent = await agentManager.getAgent(request.agentId); -// if (!agent) { -// sendResponse({ success: false, error: 'Agent not found' }); -// return; -// } -// sendResponse({ -// success: true, -// replayEnabled: agent.replayEnabled !== false, -// hasScript: !!(agent.recordedScript && agent.recordedScript.steps && agent.recordedScript.steps.length > 0), -// scriptSteps: agent.recordedScript?.totalSteps || 0, -// recordedAt: agent.recordedScript?.recordedAt || null, -// replayStats: agent.replayStats || { totalReplays: 0, totalAISaves: 0, estimatedCostSaved: 0 } -// }); -// } catch (error) { -// sendResponse({ success: false, error: error.message }); -// } -// })(); -// return true; -// -// case 'toggleAgentReplay': -// (async () => { -// try { -// const agent = await agentManager.getAgent(request.agentId); -// if (!agent) { -// sendResponse({ success: false, error: 'Agent not found' }); -// return; -// } -// const newValue = !(agent.replayEnabled !== false); -// await agentManager.updateAgent(request.agentId, { replayEnabled: newValue }); -// sendResponse({ success: true, replayEnabled: newValue }); -// } catch (error) { -// sendResponse({ success: false, error: error.message }); -// } -// })(); -// return true; - case 'prepareSessionReplay': handlePrepareSessionReplay(request, sender, sendResponse); return true; @@ -14275,165 +14098,6 @@ async function handleStartAutomation(request, sender, sendResponse) { } -/** - * Execute an automation task programmatically (used by background agents). - * Creates a session, runs the automation loop, and returns a Promise with the result. - * @param {number} tabId - Target tab ID (must already exist and be loaded) - * @param {string} task - Task description for the AI - * @param {Object} [options] - Execution options - * @param {number} [options.maxIterations=15] - Max automation iterations - * @param {boolean} [options.isBackgroundAgent=false] - If true, skip UI status messages - * @param {string} [options.agentId] - Agent ID for tracking - * @returns {Promise} { success, sessionId, result, error, duration, tokensUsed, costUsd, iterations } - */ -async function executeAutomationTask(tabId, task, options = {}) { - const { maxIterations = 15, isBackgroundAgent = false, agentId = null } = options; - - return new Promise(async (resolve) => { - try { - const sessionId = `session_${Date.now()}`; - const sessionData = { - task, - tabId: tabId, - originalTabId: tabId, - status: 'running', - startTime: Date.now(), - maxIterations: maxIterations, - actionHistory: [], - stateHistory: [], - failedAttempts: {}, - failedActionDetails: {}, - lastDOMHash: null, - stuckCounter: 0, - consecutiveNoProgressCount: 0, - iterationCount: 0, - urlHistory: [], - lastUrl: null, - actionSequences: [], - sequenceRepeatCount: {}, - isBackgroundAgent: isBackgroundAgent, - agentId: agentId, - animatedActionHighlights: false, // No highlights for background agents - _completionCallback: resolve, // Store callback for when automation finishes - // PERF: Cache DOM settings (use defaults for background agents) - domSettings: { - domOptimization: true, - maxDOMElements: 2000, - prioritizeViewport: true - }, - // Cost tracking fields - totalCost: 0, - totalInputTokens: 0, - totalOutputTokens: 0 - }; - - activeSessions.set(sessionId, sessionData); - persistSession(sessionId, sessionData); - - automationLogger.logSessionStart(sessionId, task, tabId); - initializeSessionMetrics(sessionId); - - startKeepAlive(); - - // Reset DOM state - try { - await chrome.tabs.sendMessage(tabId, { action: 'resetDOMState', sessionId }); - } catch (e) { - // Content script may not be ready yet, proceed anyway - } - - // Intercept completion messages for this session - const originalCleanup = cleanupSession; - - // Patch: listen for automationComplete for this session to capture result - const completionListener = (message) => { - if (message.action === 'automationComplete' && message.sessionId === sessionId) { - chrome.runtime.onMessage.removeListener(completionListener); - const session = activeSessions.get(sessionId) || sessionData; - const duration = Date.now() - sessionData.startTime; - const metrics = performanceMetrics.sessionStats.get(sessionId); - resolve({ - success: !message.partial && !message.error, - sessionId, - result: message.result || null, - error: message.error || (message.partial ? 'Task completed partially: ' + (message.reason || 'unknown') : null), - duration, - tokensUsed: metrics?.totalActions || 0, - costUsd: 0, - iterations: session.iterationCount || 0, - actionHistory: session.actionHistory || [] - }); - } - - if (message.action === 'automationError' && message.sessionId === sessionId) { - chrome.runtime.onMessage.removeListener(completionListener); - const duration = Date.now() - sessionData.startTime; - resolve({ - success: false, - sessionId, - result: null, - error: message.error || 'Automation error', - duration, - tokensUsed: 0, - costUsd: 0, - iterations: sessionData.iterationCount || 0 - }); - } - }; - chrome.runtime.onMessage.addListener(completionListener); - - // Safety timeout - resolve after maxIterations * 30s max - const safetyTimeout = setTimeout(() => { - chrome.runtime.onMessage.removeListener(completionListener); - const duration = Date.now() - sessionData.startTime; - resolve({ - success: false, - sessionId, - result: null, - error: 'Execution safety timeout reached', - duration, - tokensUsed: 0, - costUsd: 0, - iterations: sessionData.iterationCount || 0 - }); - }, Math.min(maxIterations * 30000, 4 * 60 * 1000)); - - // Store timeout for cleanup - sessionData._safetyTimeout = safetyTimeout; - - // Start the modular agent loop (Phase 181, D-01/D-02) - const agentTaskHooks = createSessionHooks(sessionId); - runAgentLoop(sessionId, { - activeSessions, - persistSession, - sendSessionStatus, - broadcastDashboardProgress: typeof broadcastDashboardProgress === 'function' ? broadcastDashboardProgress : function() {}, - endSessionOverlays, - cleanupSession, - startKeepAlive, - executeCDPToolDirect: executeCDPToolDirect, - handleDataTool: null, - resolveAuthWall: typeof resolveAuthWall === 'function' ? resolveAuthWall : null, - hooks: agentTaskHooks, - emitter: null - }); - - } catch (error) { - resolve({ - success: false, - sessionId: null, - result: null, - error: error.message, - duration: 0, - tokensUsed: 0, - costUsd: 0, - iterations: 0 - }); - } - }); -} - - // Handle automation stop async function handleStopAutomation(request, sender, sendResponse) { const { sessionId } = request; @@ -21205,72 +20869,6 @@ chrome.alarms.onAlarm.addListener(async (alarm) => { } return; } - - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// const agentId = agentScheduler.getAgentIdFromAlarm(alarm.name); -// if (!agentId) return; // Not an FSB agent alarm -// -// console.log('[FSB] Agent alarm fired:', alarm.name); -// -// try { -// const agent = await agentManager.getAgent(agentId); -// if (!agent) { -// console.warn('[FSB] Agent not found for alarm, clearing:', agentId); -// await agentScheduler.clearAlarm(agentId); -// return; -// } -// -// if (!agent.enabled) { -// console.log('[FSB] Agent disabled, skipping:', agentId); -// return; -// } -// -// // Guard against double-runs -// if (!agentScheduler.isValidAlarmFire(agent)) { -// console.log('[FSB] Agent alarm fired too soon, skipping:', agentId); -// return; -// } -// -// // Execute the agent -// const result = await agentExecutor.execute(agent); -// -// // Record the run -// const updatedAgent = await agentManager.recordRun(agentId, result); -// -// // Reschedule daily agents for their next occurrence -// if (agent.schedule.type === 'daily') { -// await agentScheduler.rescheduleDaily(updatedAgent); -// } -// -// // Disable once-type agents after execution -// if (agent.schedule.type === 'once') { -// await agentManager.updateAgent(agentId, { enabled: false }); -// await agentScheduler.clearAlarm(agentId); -// } -// -// // Notify any open UI about the run completion -// chrome.runtime.sendMessage({ -// action: 'agentRunComplete', -// agentId: agentId, -// result: { -// success: result.success, -// duration: result.duration, -// error: result.error -// } -// }).catch((err) => { -// console.warn('[FSB] agentRunComplete sendMessage delivery failed', { agentId, error: err && err.message }); -// }); -// -// // Sync to server if enabled -// if (updatedAgent.syncEnabled && typeof serverSync !== 'undefined') { -// serverSync.syncRun(updatedAgent, result).catch(err => { -// console.warn('[FSB] Server sync failed:', err.message); -// }); -// } -// -// } catch (error) { -// console.error('[FSB] Agent alarm handler error:', error.message); -// } }); // Phase 6 Plan 06-02 (FINT-07): SW-side startup wiring for the offscreen @@ -21301,8 +20899,26 @@ async function ensureLatticeOffscreen() { } globalThis.ensureLatticeOffscreen = ensureLatticeOffscreen; +// Background agents were removed in v0.9.45rc1. Every extension update +// deletes what they left behind: saved definitions, the server sync queue, +// the retired control-panel notice flag, and any scheduled fsb_agent_* alarms. +const RETIRED_AGENT_STORAGE_KEYS = ['bgAgents', 'fsb_sync_queue', 'fsb_sunset_notice_dismissed']; +const RETIRED_AGENT_ALARM_PREFIX = 'fsb_agent_'; + +async function clearRetiredBackgroundAgentData() { + try { + await chrome.storage.local.remove(RETIRED_AGENT_STORAGE_KEYS); + const alarms = await chrome.alarms.getAll(); + await Promise.all(alarms + .filter((alarm) => alarm.name.startsWith(RETIRED_AGENT_ALARM_PREFIX)) + .map((alarm) => chrome.alarms.clear(alarm.name))); + } catch (error) { + console.warn('[FSB BG] Retired background agent cleanup failed:', error && error.message); + } +} + // Set up side panel behavior -chrome.runtime.onInstalled.addListener(async () => { +chrome.runtime.onInstalled.addListener(async (details) => { automationLogger.logInit('extension', 'installed', { version: chrome.runtime.getManifest().version }); // Initialize analytics @@ -21370,9 +20986,9 @@ chrome.runtime.onInstalled.addListener(async () => { automationLogger.debug('Side panel API not available', { chromeVersion: 'below 114' }); } - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // Reschedule all background agents (retired in v0.9.45rc1) - // agentScheduler.rescheduleAllAgents(); + if (details && details.reason === 'update') { + await clearRetiredBackgroundAgentData(); + } // Connect to local MCP bridge (auto-reconnects if server not running yet) armMcpBridge('runtime.onInstalled'); @@ -21410,9 +21026,6 @@ chrome.runtime.onStartup.addListener(async () => { await loadDebugMode(); // Restore sessions from storage so stop button works after service worker restart await restoreSessionsFromStorage(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // Reschedule all background agents (retired in v0.9.45rc1) - // agentScheduler.rescheduleAllAgents(); // Connect to local MCP bridge (auto-reconnects if server not running yet) armMcpBridge('runtime.onStartup'); diff --git a/extension/config/config.js b/extension/config/config.js index 30ec38c96..566c0ed58 100644 --- a/extension/config/config.js +++ b/extension/config/config.js @@ -53,7 +53,7 @@ class Config { captchaSolverEnabled: false, captchaApiKey: '', - // Background Agents Server + // Remote Dashboard sync server serverUrl: 'https://fsb-server.fly.dev', serverHashKey: '', serverSyncEnabled: false diff --git a/extension/config/init-config.js b/extension/config/init-config.js index 8c077e3c1..09de234c8 100644 --- a/extension/config/init-config.js +++ b/extension/config/init-config.js @@ -42,7 +42,7 @@ const defaultConfig = { // Speech-to-Text provider ('browser' | 'whisper'); read by ui/speech-to-text.js sttProvider: 'browser', - // Background Agents Server + // Remote Dashboard sync server serverUrl: 'https://fsb-server.fly.dev', serverHashKey: '', serverSyncEnabled: false diff --git a/extension/ui/control_panel.html b/extension/ui/control_panel.html index bf08e0133..f42549069 100644 --- a/extension/ui/control_panel.html +++ b/extension/ui/control_panel.html @@ -938,23 +938,6 @@

Server Sync

Retired in v0.9.45rc1 · April 2026 - - - - diff --git a/extension/ui/onboarding.js b/extension/ui/onboarding.js index c0e48bf83..e37eadc3c 100644 --- a/extension/ui/onboarding.js +++ b/extension/ui/onboarding.js @@ -202,7 +202,7 @@
Direct APITalks to apps, not just clicks
130+ guidesAcross 17 categories
-
68 MCP toolsFrom your coding agent
+
69 MCP toolsFrom your coding agent
@@ -224,7 +224,7 @@
AgentRecommended
-
Use FSB as an MCP server in Claude Code, Cursor, VS Code, OpenClaw and more. 68 tools, no API key, nothing to configure in the browser.
+
Use FSB as an MCP server in Claude Code, Cursor, VS Code, OpenClaw and more. 69 tools, no API key, nothing to configure in the browser.
@@ -272,7 +272,7 @@
1Run the command in your terminal. Hover copy to pick a specific client.
-
2FSB writes itself into your agent's MCP config. 68 tools, zero manual JSON.
+
2FSB writes itself into your agent's MCP config. 69 tools, zero manual JSON.
3Ask your agent to browse. It drives FSB's DOM engine for you.
diff --git a/extension/ui/options.css b/extension/ui/options.css index de8f14133..ab8bb9093 100644 --- a/extension/ui/options.css +++ b/extension/ui/options.css @@ -5938,9 +5938,8 @@ textarea.form-input { color: var(--success, #22c55e); } -/* DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - Sync tab layout: key-generation row, action row, and the inline deprecation - notice for the retired remote-control sync feature. */ +/* Sync tab layout: key-generation row, action row, and the inline note about + retired background agents. */ .sync-key-row { display: flex; gap: 8px; @@ -6038,26 +6037,6 @@ textarea.form-input { color: var(--text-muted); } -.fsb-sunset-notice { - margin-top: 18px; -} - -.fsb-sunset-notice-list { - list-style: disc; - padding-left: 22px; - margin: 8px 0 14px 0; -} - -.fsb-sunset-notice-list li { - margin: 4px 0; - line-height: 1.5; - word-break: break-word; -} - -.fsb-sunset-notice-dismiss { - align-self: flex-start; -} - /* ============================================================================ Phase 213 - Sync tab section header + connection-status pill (SYNC-02) Pill lives in
header; reflects ext:remote-control-state. diff --git a/extension/ui/options.js b/extension/ui/options.js index 27a437471..11bc3b7a4 100644 --- a/extension/ui/options.js +++ b/extension/ui/options.js @@ -6456,72 +6456,9 @@ async function clearAllResearchResults() { } } -// ========================================== -// Background Agent Management -// ========================================== - -// Phase 212: Background Agents sunset -- replaces interactive agent UI with deprecation card + one-time names list. -// The function below is LIVE (not commented). The agent UI controllers below initializeAgentSection() are commented per-line. -function initializeBackgroundAgentsDeprecation() { - const dismissBtn = document.getElementById('fsbSunsetNoticeDismiss'); - const noticeEl = document.getElementById('fsbSunsetNotice'); - const namesList = document.getElementById('fsbSunsetNoticeNames'); - - if (!dismissBtn || !noticeEl || !namesList) { - // Scaffolding not present (older HTML cached?); silently skip. - return; - } - - // Wire the "Got it" button once; subsequent clicks are no-ops because the aside is hidden. - dismissBtn.addEventListener('click', function () { - try { - chrome.storage.local.set({ fsb_sunset_notice_dismissed: true }); - } catch (e) { - // chrome.storage may be unavailable in test environments; degrade gracefully. - } - noticeEl.hidden = true; - }); - - // Read bgAgents + dismiss flag in parallel; render only if (bgAgents non-empty AND not dismissed). - try { - chrome.storage.local.get(['bgAgents', 'fsb_sunset_notice_dismissed'], function (stored) { - const dismissed = stored && stored.fsb_sunset_notice_dismissed === true; - // Phase 212-04 (gap-closure / WR-01): bgAgents was historically written as an object map - // keyed by agentId (agents/agent-manager.js:86-91 -- the only writer that ever ran). - // Array.isArray({}) returns false, which suppressed the notice on the only profiles - // that matter. Accept both shapes defensively: array as-is, object map -> Object.values, - // anything else -> empty array. - const raw = stored && stored.bgAgents; - const agents = Array.isArray(raw) - ? raw - : (raw && typeof raw === 'object' ? Object.values(raw) : []); - if (dismissed) return; // D-09: never re-render after dismiss - if (agents.length === 0) return; // D-10: skip entirely if no agents ever existed - - // Defensive textContent rendering (T-01 mitigation -- agent names came from user input). - // NEVER use innerHTML for these strings. PurifyDOM is loaded but textContent is sufficient for plain text. - while (namesList.firstChild) { - namesList.removeChild(namesList.firstChild); - } - for (let i = 0; i < agents.length; i++) { - const a = agents[i] || {}; - const name = (typeof a.name === 'string' && a.name.length > 0) ? a.name : '(unnamed agent)'; - const li = document.createElement('li'); - li.textContent = name; // textContent ONLY; never innerHTML - namesList.appendChild(li); - } - noticeEl.hidden = false; - }); - } catch (e) { - // chrome.storage unavailable; deprecation card alone is sufficient. - } -} - function initializeSyncDeprecationAndButtons() { - // Sync consolidation: the deprecation card + sunset notice + Server Sync buttons all - // live in the Sync section now. Keep wiring idempotent so re-init from /clear is safe. - - initializeBackgroundAgentsDeprecation(); + // Sync consolidation: the deprecation card and Server Sync buttons both live in + // the Sync section. Keep wiring idempotent so re-init from /clear is safe. // Server sync buttons const btnGenerate = document.getElementById('btnGenerateHashKey'); @@ -6542,448 +6479,6 @@ function initializeSyncDeprecationAndButtons() { loadServerSettings(); } -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// function showAgentForm(editAgent) { -// const form = document.getElementById('agentFormCard'); -// const title = document.getElementById('agentFormTitle'); -// if (!form) return; -// -// if (editAgent && typeof editAgent === 'object' && editAgent.agentId) { -// // Edit mode -// title.textContent = 'Edit Agent'; -// document.getElementById('agentFormId').value = editAgent.agentId; -// document.getElementById('agentName').value = editAgent.name || ''; -// document.getElementById('agentTask').value = editAgent.task || ''; -// document.getElementById('agentTargetUrl').value = editAgent.targetUrl || ''; -// document.getElementById('agentScheduleType').value = editAgent.schedule?.type || 'interval'; -// document.getElementById('agentInterval').value = editAgent.schedule?.intervalMinutes || 30; -// document.getElementById('agentDailyTime').value = editAgent.schedule?.dailyTime || '09:00'; -// document.getElementById('agentMaxIterations').value = editAgent.maxIterations || 15; -// -// // Set replay toggle -// const replayToggle = document.getElementById('agentReplayEnabled'); -// if (replayToggle) replayToggle.checked = editAgent.replayEnabled !== false; -// -// // Set days of week checkboxes -// const daysContainer = document.getElementById('agentDaysOfWeek'); -// if (daysContainer) { -// const checkboxes = daysContainer.querySelectorAll('input[type="checkbox"]'); -// checkboxes.forEach(cb => { -// cb.checked = (editAgent.schedule?.daysOfWeek || []).includes(parseInt(cb.value)); -// }); -// } -// -// // Trigger schedule type change to show/hide fields -// document.getElementById('agentScheduleType').dispatchEvent(new Event('change')); -// } else { -// // Create mode -// title.textContent = 'Create New Agent'; -// document.getElementById('agentFormId').value = ''; -// document.getElementById('agentName').value = ''; -// document.getElementById('agentTask').value = ''; -// document.getElementById('agentTargetUrl').value = ''; -// document.getElementById('agentScheduleType').value = 'interval'; -// document.getElementById('agentInterval').value = 30; -// document.getElementById('agentDailyTime').value = '09:00'; -// document.getElementById('agentMaxIterations').value = 15; -// const replayToggleCreate = document.getElementById('agentReplayEnabled'); -// if (replayToggleCreate) replayToggleCreate.checked = true; -// document.getElementById('agentScheduleType').dispatchEvent(new Event('change')); -// } -// -// form.style.display = ''; -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// function hideAgentForm() { -// const form = document.getElementById('agentFormCard'); -// if (form) form.style.display = 'none'; -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// async function saveAgent() { -// const agentId = document.getElementById('agentFormId').value; -// const name = document.getElementById('agentName').value.trim(); -// const task = document.getElementById('agentTask').value.trim(); -// const targetUrl = document.getElementById('agentTargetUrl').value.trim(); -// const scheduleType = document.getElementById('agentScheduleType').value; -// const intervalMinutes = parseInt(document.getElementById('agentInterval').value) || 30; -// const dailyTime = document.getElementById('agentDailyTime').value; -// const maxIterations = parseInt(document.getElementById('agentMaxIterations').value) || 15; -// -// if (!name || !task || !targetUrl) { -// showToast('Please fill in all required fields', 'error'); -// return; -// } -// -// // Build schedule -// const schedule = { type: scheduleType }; -// if (scheduleType === 'interval') { -// schedule.intervalMinutes = Math.max(1, intervalMinutes); -// } else if (scheduleType === 'daily') { -// schedule.dailyTime = dailyTime; -// const daysContainer = document.getElementById('agentDaysOfWeek'); -// if (daysContainer) { -// const checked = Array.from(daysContainer.querySelectorAll('input:checked')).map(cb => parseInt(cb.value)); -// if (checked.length > 0) { -// schedule.daysOfWeek = checked; -// } -// } -// } -// -// const replayEnabled = document.getElementById('agentReplayEnabled')?.checked !== false; -// -// const action = agentId ? 'updateAgent' : 'createAgent'; -// const payload = agentId -// ? { action, agentId, updates: { name, task, targetUrl, schedule, maxIterations, replayEnabled } } -// : { action, params: { name, task, targetUrl, schedule, maxIterations, replayEnabled } }; -// -// try { -// const response = await new Promise((resolve) => { -// chrome.runtime.sendMessage(payload, resolve); -// }); -// -// if (response.success) { -// showToast(agentId ? 'Agent updated' : 'Agent created', 'success'); -// hideAgentForm(); -// loadAgentList(); -// loadAgentStats(); -// } else { -// showToast('Error: ' + (response.error || 'Unknown error'), 'error'); -// } -// } catch (error) { -// showToast('Error: ' + error.message, 'error'); -// } -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// async function loadAgentList() { -// try { -// const response = await new Promise((resolve) => { -// chrome.runtime.sendMessage({ action: 'listAgents' }, resolve); -// }); -// -// const list = document.getElementById('agentList'); -// const emptyState = document.getElementById('agentEmptyState'); -// if (!list) return; -// -// // Remove existing agent cards (keep empty state) -// list.querySelectorAll('.agent-card').forEach(card => card.remove()); -// -// const agents = response?.agents || []; -// -// if (agents.length === 0) { -// if (emptyState) emptyState.style.display = ''; -// return; -// } -// -// if (emptyState) emptyState.style.display = 'none'; -// -// for (const agent of agents) { -// const card = createAgentCard(agent); -// list.appendChild(card); -// } -// } catch (error) { -// console.error('Failed to load agents:', error); -// } -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// function createAgentCard(agent) { -// const card = document.createElement('div'); -// card.className = 'agent-card'; -// card.dataset.agentId = agent.agentId; -// -// const statusClass = agent.enabled ? 'active' : 'disabled'; -// const statusText = agent.enabled ? 'Active' : 'Disabled'; -// const lastRun = agent.lastRunAt ? formatTimeAgo(agent.lastRunAt) : 'Never'; -// const scheduleText = formatSchedule(agent.schedule); -// -// // Replay info -// const hasScript = agent.recordedScript && agent.recordedScript.steps && agent.recordedScript.steps.length > 0; -// const replayEnabled = agent.replayEnabled !== false; -// const replayBadgeClass = hasScript ? 'active' : 'inactive'; -// const replayBadgeText = hasScript -// ? 'Replay Ready - ' + agent.recordedScript.totalSteps + ' steps' -// : 'No Script'; -// const replayStats = agent.replayStats || { totalReplays: 0, estimatedCostSaved: 0 }; -// const costSavedText = replayStats.estimatedCostSaved > 0 -// ? '$' + replayStats.estimatedCostSaved.toFixed(4) -// : '$0'; -// -// card.innerHTML = ` -//
-//
-//

${escapeHtml(agent.name)}

-// ${statusText} -//
-// -//
-//
-//
-// Task: -// ${escapeHtml(agent.task.substring(0, 100))}${agent.task.length > 100 ? '...' : ''} -//
-//
-// URL: -// ${escapeHtml(agent.targetUrl)} -//
-//
-// Schedule: -// ${scheduleText} -//
-//
-// ${replayBadgeText} -// ${replayStats.totalReplays > 0 ? `${replayStats.totalReplays} replays` : ''} -// ${replayStats.estimatedCostSaved > 0 ? `${costSavedText} saved` : ''} -// ${hasScript ? `` : ''} -//
-//
-// Last run: ${lastRun} -// Runs: ${agent.runCount} -// ${agent.lastRunStatus ? `${agent.lastRunStatus}` : ''} -//
-//
-//
-// -// -// -// -//
-// -// `; -// -// // Toggle handler -// const toggle = card.querySelector('.agent-toggle'); -// if (toggle) { -// toggle.addEventListener('change', () => toggleAgent(agent.agentId)); -// } -// -// // Run now -// const runBtn = card.querySelector('.agent-run-btn'); -// if (runBtn) { -// runBtn.addEventListener('click', () => runAgentNow(agent.agentId)); -// } -// -// // Edit -// const editBtn = card.querySelector('.agent-edit-btn'); -// if (editBtn) { -// editBtn.addEventListener('click', async () => { -// const response = await new Promise(resolve => { -// chrome.runtime.sendMessage({ action: 'listAgents' }, resolve); -// }); -// const fresh = (response?.agents || []).find(a => a.agentId === agent.agentId); -// if (fresh) showAgentForm(fresh); -// }); -// } -// -// // Delete -// const deleteBtn = card.querySelector('.agent-delete-btn'); -// if (deleteBtn) { -// deleteBtn.addEventListener('click', () => deleteAgent(agent.agentId, agent.name)); -// } -// -// // History toggle -// const historyBtn = card.querySelector('.agent-history-btn'); -// if (historyBtn) { -// historyBtn.addEventListener('click', () => toggleAgentHistory(agent.agentId)); -// } -// -// // Re-record button (clear script) -// const rerecordBtn = card.querySelector('.agent-rerecord-btn'); -// if (rerecordBtn) { -// rerecordBtn.addEventListener('click', async (e) => { -// e.stopPropagation(); -// if (!confirm('Clear the recorded script for "' + agent.name + '"? The next run will use AI to re-record.')) return; -// try { -// const response = await new Promise(resolve => { -// chrome.runtime.sendMessage({ action: 'clearAgentScript', agentId: agent.agentId }, resolve); -// }); -// if (response.success) { -// showToast('Script cleared - next run will use AI', 'success'); -// loadAgentList(); -// } else { -// showToast('Error: ' + (response.error || 'Failed to clear script'), 'error'); -// } -// } catch (error) { -// showToast('Error: ' + error.message, 'error'); -// } -// }); -// } -// -// return card; -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// async function toggleAgent(agentId) { -// try { -// const response = await new Promise(resolve => { -// chrome.runtime.sendMessage({ action: 'toggleAgent', agentId }, resolve); -// }); -// if (response.success) { -// loadAgentList(); -// loadAgentStats(); -// } else { -// showToast('Error: ' + (response.error || 'Failed to toggle agent'), 'error'); -// } -// } catch (error) { -// showToast('Error: ' + error.message, 'error'); -// } -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// async function runAgentNow(agentId) { -// showToast('Starting agent run...', 'info'); -// try { -// const response = await new Promise(resolve => { -// chrome.runtime.sendMessage({ action: 'runAgentNow', agentId }, resolve); -// }); -// if (response.success) { -// showToast('Agent execution started', 'success'); -// } else { -// showToast('Error: ' + (response.error || 'Failed to start agent'), 'error'); -// } -// } catch (error) { -// showToast('Error: ' + error.message, 'error'); -// } -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// async function deleteAgent(agentId, name) { -// if (!confirm('Delete agent "' + name + '"? This cannot be undone.')) return; -// -// try { -// const response = await new Promise(resolve => { -// chrome.runtime.sendMessage({ action: 'deleteAgent', agentId }, resolve); -// }); -// if (response.success) { -// showToast('Agent deleted', 'success'); -// loadAgentList(); -// loadAgentStats(); -// } else { -// showToast('Error: ' + (response.error || 'Failed to delete agent'), 'error'); -// } -// } catch (error) { -// showToast('Error: ' + error.message, 'error'); -// } -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// async function toggleAgentHistory(agentId) { -// const container = document.getElementById('history-' + agentId); -// if (!container) return; -// -// if (container.style.display !== 'none') { -// container.style.display = 'none'; -// return; -// } -// -// try { -// const response = await new Promise(resolve => { -// chrome.runtime.sendMessage({ action: 'getAgentRunHistory', agentId, limit: 10 }, resolve); -// }); -// -// const history = response?.history || []; -// if (history.length === 0) { -// container.innerHTML = '
No run history yet.
'; -// } else { -// container.innerHTML = history.map(run => { -// const mode = run.executionMode || 'ai_initial'; -// let modeBadge = ''; -// if (mode === 'replay') { -// modeBadge = 'Replay'; -// } else if (mode === 'ai_fallback') { -// modeBadge = 'AI Fallback'; -// } else { -// modeBadge = 'AI'; -// } -// const costSavedInfo = run.costSaved > 0 -// ? '$' + run.costSaved.toFixed(4) + ' saved' -// : ''; -// return ` -//
-//
-// ${run.success ? 'Success' : 'Failed'} -// ${modeBadge} -// ${new Date(run.timestamp).toLocaleString()} -// ${formatDuration(run.duration)} -// ${costSavedInfo} -//
-//
-// ${run.result ? '

' + escapeHtml(run.result.substring(0, 200)) + '

' : ''} -// ${run.error ? '

' + escapeHtml(run.error.substring(0, 200)) + '

' : ''} -// ${run.iterations || 0} iterations -// ${run.replayFailedAtStep != null ? 'Replay failed at step ' + run.replayFailedAtStep + '' : ''} -//
-//
-// `; -// }).join(''); -// } -// -// container.style.display = ''; -// } catch (error) { -// container.innerHTML = '
Failed to load history.
'; -// container.style.display = ''; -// } -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// async function loadAgentStats() { -// try { -// const response = await new Promise(resolve => { -// chrome.runtime.sendMessage({ action: 'getAgentStats' }, resolve); -// }); -// -// const stats = response?.stats; -// if (!stats) return; -// -// const el = (id) => document.getElementById(id); -// if (el('statTotalAgents')) el('statTotalAgents').textContent = stats.totalAgents; -// if (el('statEnabledAgents')) el('statEnabledAgents').textContent = stats.enabledAgents; -// if (el('statRunsToday')) el('statRunsToday').textContent = stats.runsToday; -// if (el('statSuccessRate')) el('statSuccessRate').textContent = stats.totalRuns > 0 ? stats.successRate + '%' : '--'; -// if (el('statTotalCost')) el('statTotalCost').textContent = '$' + stats.totalCost.toFixed(4); -// if (el('statReplayRuns')) el('statReplayRuns').textContent = stats.totalReplayRuns || 0; -// if (el('statCostSaved')) el('statCostSaved').textContent = '$' + (stats.totalCostSaved || 0).toFixed(4); -// } catch (error) { -// console.error('Failed to load agent stats:', error); -// } -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// function formatSchedule(schedule) { -// if (!schedule) return 'Not set'; -// switch (schedule.type) { -// case 'interval': -// const mins = schedule.intervalMinutes || 1; -// if (mins >= 60) return 'Every ' + (mins / 60) + ' hour' + (mins >= 120 ? 's' : ''); -// return 'Every ' + mins + ' minute' + (mins > 1 ? 's' : ''); -// case 'daily': -// const days = schedule.daysOfWeek; -// const time = schedule.dailyTime || '09:00'; -// if (days && days.length > 0 && days.length < 7) { -// const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; -// return 'Daily at ' + time + ' (' + days.map(d => dayNames[d]).join(', ') + ')'; -// } -// return 'Daily at ' + time; -// case 'once': -// return 'Run once'; -// default: -// return schedule.type; -// } -// } - function formatTimeAgo(timestamp) { const diff = Date.now() - timestamp; if (diff < 60000) return 'Just now'; @@ -6992,21 +6487,6 @@ function formatTimeAgo(timestamp) { return Math.floor(diff / 86400000) + 'd ago'; } -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// function formatDuration(ms) { -// if (!ms) return '--'; -// if (ms < 1000) return ms + 'ms'; -// if (ms < 60000) return (ms / 1000).toFixed(1) + 's'; -// return Math.floor(ms / 60000) + 'm ' + Math.floor((ms % 60000) / 1000) + 's'; -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// function escapeHtml(text) { -// const div = document.createElement('div'); -// div.textContent = text; -// return div.innerHTML; -// } - // Server sync functions const FSB_DEFAULT_SERVER_URL = 'https://full-selfbrowsing.com'; diff --git a/extension/ui/popup.js b/extension/ui/popup.js index 98be3df8e..ac372d3b5 100644 --- a/extension/ui/popup.js +++ b/extension/ui/popup.js @@ -451,16 +451,6 @@ async function handleSendMessage() { return; } - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // Handle /agent slash commands - // if (message.startsWith('/agent')) { - // chatInput.textContent = ''; - // updateSendButtonState(); - // addMessage(message, 'user'); - // handleAgentCommand(message); - // return; - // } - try { // Add user message to chat addMessage(message, 'user'); @@ -1113,106 +1103,3 @@ console.log(`FSB v${chrome.runtime.getManifest().version} chat interface loaded` // ========================================== // /agent Slash Command Handler // ========================================== - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// function handleAgentCommand(message) { -// const parts = message.split(/\s+/); -// const subCommand = parts[1] || ''; -// -// if (subCommand === 'list') { -// showAgentList(); -// } else if (subCommand === 'stop') { -// const agentName = parts.slice(2).join(' '); -// stopAgentByName(agentName); -// } else { -// startAgentWizard(); -// } -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// async function showAgentList() { -// try { -// const response = await new Promise(resolve => { -// chrome.runtime.sendMessage({ action: 'listAgents' }, resolve); -// }); -// -// const agents = response?.agents || []; -// if (agents.length === 0) { -// addMessage('No background agents configured. Use /agent to create one.', 'system'); -// return; -// } -// -// let listText = 'Background Agents:\n'; -// for (const agent of agents) { -// const status = agent.enabled ? '[ON]' : '[OFF]'; -// const lastRun = agent.lastRunAt ? new Date(agent.lastRunAt).toLocaleString() : 'Never'; -// listText += `\n${status} ${agent.name} - ${formatScheduleShort(agent.schedule)} - Last: ${lastRun}`; -// } -// addMessage(listText, 'system'); -// } catch (error) { -// addMessage('Failed to load agents: ' + error.message, 'error'); -// } -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// async function stopAgentByName(name) { -// if (!name) { -// addMessage('Usage: /agent stop ', 'system'); -// return; -// } -// -// try { -// const response = await new Promise(resolve => { -// chrome.runtime.sendMessage({ action: 'listAgents' }, resolve); -// }); -// -// const agents = response?.agents || []; -// const agent = agents.find(a => a.name.toLowerCase().includes(name.toLowerCase())); -// -// if (!agent) { -// addMessage('Agent not found: "' + name + '"', 'error'); -// return; -// } -// -// if (!agent.enabled) { -// addMessage('Agent "' + agent.name + '" is already disabled.', 'system'); -// return; -// } -// -// const toggleResp = await new Promise(resolve => { -// chrome.runtime.sendMessage({ action: 'toggleAgent', agentId: agent.agentId }, resolve); -// }); -// -// if (toggleResp.success) { -// addMessage('Agent "' + agent.name + '" has been disabled.', 'system'); -// } else { -// addMessage('Failed to stop agent: ' + (toggleResp.error || 'Unknown error'), 'error'); -// } -// } catch (error) { -// addMessage('Error: ' + error.message, 'error'); -// } -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// function startAgentWizard() { -// chrome.runtime.openOptionsPage(); -// setTimeout(() => { -// chrome.runtime.sendMessage({ action: 'openAgentForm' }); -// }, 500); -// addMessage('Opening agent settings... Use the form in the options page to create your agent.', 'system'); -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// function formatScheduleShort(schedule) { -// if (!schedule) return 'Not set'; -// switch (schedule.type) { -// case 'interval': -// return 'Every ' + (schedule.intervalMinutes || 1) + ' min'; -// case 'daily': -// return 'Daily at ' + (schedule.dailyTime || '09:00'); -// case 'once': -// return 'Run once'; -// default: -// return schedule.type; -// } -// } diff --git a/extension/ui/sidepanel.js b/extension/ui/sidepanel.js index b3336e71e..8d551c353 100644 --- a/extension/ui/sidepanel.js +++ b/extension/ui/sidepanel.js @@ -4455,16 +4455,6 @@ async function _handleLegacySendMessage(message) { // the helper; never blocks the send path. try { conversationId = await ensureTabConversationForActiveTab(false); } catch (_e) { /* swallow */ } - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // Handle /agent slash commands - // if (message.startsWith('/agent')) { - // chatInput.textContent = ''; - // updateSendButtonState(); - // addMessage(message, 'user'); - // handleAgentCommand(message); - // return; - // } - try { // Add user message to chat addMessage(message, 'user'); @@ -6917,106 +6907,3 @@ console.log(`FSB v${chrome.runtime.getManifest().version} side panel script load // ========================================== // /agent Slash Command Handler // ========================================== - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// function handleAgentCommand(message) { -// const parts = message.split(/\s+/); -// const subCommand = parts[1] || ''; -// -// if (subCommand === 'list') { -// showAgentList(); -// } else if (subCommand === 'stop') { -// const agentName = parts.slice(2).join(' '); -// stopAgentByName(agentName); -// } else { -// startAgentWizard(); -// } -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// async function showAgentList() { -// try { -// const response = await new Promise(resolve => { -// chrome.runtime.sendMessage({ action: 'listAgents' }, resolve); -// }); -// -// const agents = response?.agents || []; -// if (agents.length === 0) { -// addMessage('No background agents configured. Use /agent to create one.', 'system'); -// return; -// } -// -// let listText = 'Background Agents:\n'; -// for (const agent of agents) { -// const status = agent.enabled ? '[ON]' : '[OFF]'; -// const lastRun = agent.lastRunAt ? new Date(agent.lastRunAt).toLocaleString() : 'Never'; -// listText += `\n${status} ${agent.name} - ${formatScheduleShort(agent.schedule)} - Last: ${lastRun}`; -// } -// addMessage(listText, 'system'); -// } catch (error) { -// addMessage('Failed to load agents: ' + error.message, 'error'); -// } -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// async function stopAgentByName(name) { -// if (!name) { -// addMessage('Usage: /agent stop ', 'system'); -// return; -// } -// -// try { -// const response = await new Promise(resolve => { -// chrome.runtime.sendMessage({ action: 'listAgents' }, resolve); -// }); -// -// const agents = response?.agents || []; -// const agent = agents.find(a => a.name.toLowerCase().includes(name.toLowerCase())); -// -// if (!agent) { -// addMessage('Agent not found: "' + name + '"', 'error'); -// return; -// } -// -// if (!agent.enabled) { -// addMessage('Agent "' + agent.name + '" is already disabled.', 'system'); -// return; -// } -// -// const toggleResp = await new Promise(resolve => { -// chrome.runtime.sendMessage({ action: 'toggleAgent', agentId: agent.agentId }, resolve); -// }); -// -// if (toggleResp.success) { -// addMessage('Agent "' + agent.name + '" has been disabled.', 'system'); -// } else { -// addMessage('Failed to stop agent: ' + (toggleResp.error || 'Unknown error'), 'error'); -// } -// } catch (error) { -// addMessage('Error: ' + error.message, 'error'); -// } -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// function startAgentWizard() { -// chrome.runtime.openOptionsPage(); -// setTimeout(() => { -// chrome.runtime.sendMessage({ action: 'openAgentForm' }); -// }, 500); -// addMessage('Opening agent settings... Use the form in the options page to create your agent.', 'system'); -// } - -// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// function formatScheduleShort(schedule) { -// if (!schedule) return 'Not set'; -// switch (schedule.type) { -// case 'interval': -// return 'Every ' + (schedule.intervalMinutes || 1) + ' min'; -// case 'daily': -// return 'Daily at ' + (schedule.dailyTime || '09:00'); -// case 'once': -// return 'Run once'; -// default: -// return schedule.type; -// } -// } diff --git a/extension/ws/mcp-bridge-client.js b/extension/ws/mcp-bridge-client.js index 8fe5d2b9a..4346cb849 100644 --- a/extension/ws/mcp-bridge-client.js +++ b/extension/ws/mcp-bridge-client.js @@ -3109,12 +3109,10 @@ class MCPBridgeClient { } async _handleAgentAction(action, payload) { - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // background.js's agent switch cases are commented out and - // mcp/src/tools/agents.ts registers no agent tools, so only a - // version-skewed MCP server can still send mcp:*-agent messages. - // Short-circuit with an explicit envelope instead of dispatching into - // the background switch's default branch. + // Background agents were removed in v0.9.45rc1, so only an MCP server + // from before then still sends mcp:*-agent messages. Answer with an + // explicit error instead of dispatching into the background switch's + // default branch. return { success: false, errorCode: 'agent_management_deprecated', diff --git a/extension/ws/ws-client.js b/extension/ws/ws-client.js index 91a47f2af..36ff4147e 100644 --- a/extension/ws/ws-client.js +++ b/extension/ws/ws-client.js @@ -1930,10 +1930,6 @@ class FSBWebSocket { this._sendStateSnapshot('dash:request-status'); try { _broadcastMetrics(this, this.serverHashKey); } catch (_e) { /* defensive */ } break; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // case 'dash:agent-run-now': - // this._handleAgentRunNow(msg.payload); - // break; case controlTypes.START: if (typeof _streamingActive !== 'undefined') _streamingActive = true; this._handleDashboardStreamStart(msg.payload); @@ -2224,36 +2220,6 @@ class FSBWebSocket { ); } - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// /** -// * Handle an immediate agent run request from the dashboard. -// * Validates the agent exists and is not already running, then triggers execution. -// * @param {Object} payload - { agentId: string } -// */ -// async _handleAgentRunNow(payload) { -// var agentId = payload && payload.agentId; -// if (!agentId) { -// this.send('ext:agent-run-complete', { agentId: null, success: false, error: 'No agentId provided' }); -// return; -// } -// -// // Check if another session is already running -// if (typeof activeSessions !== 'undefined') { -// var hasRunning = [...activeSessions.values()].some(function(s) { return s.status === 'running'; }); -// if (hasRunning) { -// this.send('ext:agent-run-complete', { agentId: agentId, success: false, error: 'Another task is already running' }); -// return; -// } -// } -// -// // Dispatch to background.js handler -// if (typeof startAgentRunNow === 'function') { -// startAgentRunNow(agentId); -// } else { -// this.send('ext:agent-run-complete', { agentId: agentId, success: false, error: 'Agent execution not available' }); -// } -// } - /** * Forward a message to the content script on the active tab. * Used for dashboard-to-content-script communication (DOM stream control). diff --git a/mcp/CHANGELOG.md b/mcp/CHANGELOG.md index 86c9b657b..0bf343419 100644 --- a/mcp/CHANGELOG.md +++ b/mcp/CHANGELOG.md @@ -2,6 +2,21 @@ All notable changes to `fsb-mcp-server` are documented in this file. Each entry corresponds to a published npm release; FSB extension milestones map to MCP package versions in the entry header. +## Unreleased + +### Removed + +- **`start_visual_session`, `end_visual_session` and `back` are no longer registered.** The two visual-session tools have only returned a `TOOL_REMOVED` error since 0.9.0, and `back` duplicated `go_back`. The server now registers 69 tools. A client that still calls one of these names gets its MCP host's unknown-tool error; use `go_back` for history navigation. +- The `TOOL_REMOVED` typed error is gone along with the stubs that returned it. + +### Changed + +- Every published tool count (both READMEs, extension onboarding, showcase pages and their translations, and the `llms` files) is now checked against the tools the built server actually registers. `tests/mcp-version-parity.test.js` fails when any of them disagrees, so the count can no longer drift. + +### Compatibility and update + +- Removing tools changes the advertised tool list, so release this as a minor version rather than a patch. +
## 0.11.2 (2026-10-14) diff --git a/mcp/README.md b/mcp/README.md index 23644cd94..86849231f 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -17,7 +17,7 @@ *Manual browser tools, capabilities, trigger watchers, visual sessions, autopilot, vault, and observability for the FSB Chrome Extension.* -[Quick Start](#quick-start) · [Trigger Watchers](#trigger-watchers) · [Tools](#tools-69-total) · [Diagnostics](#diagnostics) · [Architecture](#architecture) +[Quick Start](#quick-start) · [Trigger Watchers](#trigger-watchers) · [Tools](#tools) · [Diagnostics](#diagnostics) · [Architecture](#architecture) @@ -105,7 +105,7 @@ This is the FSB v0.9.60 milestone release. The headline change is the new multi- - Secure vault tools that avoid sending raw secrets over the bridge. - One-command installer coverage for 21 platforms. -Background-agent MCP tools were retired with the extension's background-agent sunset. The agent registrar now only exposes `back` for ownership-gated history navigation. +Background-agent MCP tools were retired with the extension's background-agent sunset. The last tool left in that module, `back`, duplicated `go_back` and has been removed. --- @@ -353,7 +353,7 @@ Move to a normal `http` or `https` page before calling DOM tools such as `read_p ## Visual Session Lifecycle -> **v0.9.0 breaking change** -- The explicit `start_visual_session` and `end_visual_session` tools were REMOVED in v0.9.0. Action tools now require `visual_reason` + `client` fields; the visual session is created implicitly on the first action call, refreshed on a sliding 60-second window, and cleared by `is_final: true` (immediate) or 60 seconds of silence (auto-clear). Calling the removed tools returns the typed `TOOL_REMOVED` error. See [CHANGELOG.md](./CHANGELOG.md#v0.9.0) for the migration recipe with concrete before/after code. +> **v0.9.0 breaking change** -- The explicit `start_visual_session` and `end_visual_session` tools were REMOVED in v0.9.0. Action tools now require `visual_reason` + `client` fields; the visual session is created implicitly on the first action call, refreshed on a sliding 60-second window, and cleared by `is_final: true` (immediate) or 60 seconds of silence (auto-clear). The two names are no longer registered at all, so they do not appear in `tools/list`. See [CHANGELOG.md](./CHANGELOG.md#v0.9.0) for the migration recipe with concrete before/after code. Use the implicit visual session when your MCP client controls the browser and you still want FSB's visible trusted overlay. The session is per-tab and per-agent; the v0.9.60 ownership gate (`TAB_NOT_OWNED`) fires before any session state is touched. @@ -371,7 +371,7 @@ type_text(selector="#email", text="user@example.com", visual_reason="Complete ch If the task ends without an explicit `is_final: true` signal, the overlay auto-clears after 60 seconds of no further carrying action calls. Read-only tools (`read_page`, `get_dom_snapshot`, `get_text`, ...) do NOT carry the field bundle and do NOT re-arm the sliding window -- reads stay silent by design. -Calling the removed `start_visual_session` or `end_visual_session` tools by name returns the typed `TOOL_REMOVED` error whose body names the new contract and points at this CHANGELOG entry. See `mcp/CHANGELOG.md` v0.9.0 for the full migration recipe and the typed-error catalogue (`VISUAL_FIELDS_REQUIRED`, `BADGE_NOT_ALLOWED`, `TOOL_REMOVED`). +A client that still calls `start_visual_session` or `end_visual_session` gets its MCP host's unknown-tool error. See `mcp/CHANGELOG.md` v0.9.0 for the full migration recipe and the typed-error catalogue (`VISUAL_FIELDS_REQUIRED`, `BADGE_NOT_ALLOWED`). Use `run_task` only when the user explicitly wants FSB autopilot to decide and execute the steps -- autopilot manages its own overlay lifecycle internally and is NOT affected by the v0.9.0 implicit-contract change. @@ -465,15 +465,9 @@ There is no idle timeout. An idle agent keeps its tab indefinitely until one of - If the bridge cannot recover after eviction, the server resolves `run_task` with a structured `partial_outcome` whose `disposition` is `'sw_evicted'`. The hint string is the literal `lifecycle event missing -- audit cleanup paths`. - The agent registry mirrors to `chrome.storage.session` write-through; on SW wake, `hydrate()` reconciles persisted records against `chrome.tabs.query()` and reaps ghost records before servicing any request. -### `back` tool +### Going back in history -The new ownership-gated `back` tool is the typed replacement for `execute_js("history.back()")`. It returns: - -```text -{ status, resultingUrl, historyDepth } -``` - -where `status` is one of `ok`, `no_history`, `cross_origin`, `bf_cache`, `fragment_only`. Settle verification uses `pageshow` with a 2s timeout; cross-origin transitions reuse the v0.9.11 BF-cache resilience path to re-inject the content script. The tool is background-tab compatible and does not steal focus. +Use `go_back` and `go_forward`. Both are ownership-gated, run on background tabs without stealing focus, and return the resulting URL. The separate `back` tool, which duplicated `go_back`, has been removed. ### Background-tab execution @@ -481,23 +475,17 @@ Most tools execute on background tabs without stealing focus. Tools that genuine --- -## Tools (69 Total) +## Tools -### Visual Sessions (2) +`fsb-mcp-server` registers 69 tools. -| Tool | Purpose | -|------|---------| -| `start_visual_session` | Removed in v0.9.0 -- see Visual Session Lifecycle section. Calling returns `TOOL_REMOVED`. | -| `end_visual_session` | Removed in v0.9.0 -- see Visual Session Lifecycle section. Calling returns `TOOL_REMOVED`. | - -### Autopilot And Agent Navigation (4) +### Autopilot (3) | Tool | Purpose | |------|---------| | `run_task` | Let FSB's AI perform a natural language browser task end to end. | | `stop_task` | Cancel the active automation task. | | `get_task_status` | Check task progress, phase, and ETA. | -| `back` | Single-step browser-history back, ownership-gated. Returns `{ status, resultingUrl, historyDepth }`. | ### Trigger Watchers (4) @@ -541,6 +529,14 @@ Notes: | `list_tabs` | List open tabs. | | `read_sheet` | Read spreadsheet ranges. | +### Task Outcome (3) + +| Tool | Purpose | +|------|---------| +| `complete_task` | Report that the task is fully done, with a summary. | +| `partial_task` | Report useful work that an external blocker stopped short of the final step. | +| `fail_task` | Report that the task cannot be completed, and why. | + ### Developer / UAT (1) | Tool | Purpose | @@ -585,7 +581,7 @@ Vault responses expose metadata only. Passwords, full card numbers, and CVV valu ### Tool Count Contract -The public count is intentionally based on registered MCP handlers, not every internal helper in the extension. The extension's canonical registry also contains task-lifecycle signals and internal context tools used by the browser runtime. The MCP README documents the tool surface an MCP client can call. +The public count is intentionally based on registered MCP handlers, not every internal helper in the extension. The extension's canonical registry also contains task-lifecycle signals and internal context tools used by the browser runtime. The MCP README documents the tool surface an MCP client can call. `tests/mcp-version-parity.test.js` counts the tools the built server registers and fails when any published count disagrees. --- @@ -676,7 +672,7 @@ Contract-sensitive changes should be covered by tests before publishing: - restricted-tab recovery messages - visual-session token lifecycle - vault redaction boundaries -- multi-agent contract: `agent_id` capture, ownership gate, configurable cap, ownership tokens, `back` tool +- multi-agent contract: `agent_id` capture, ownership gate, configurable cap, ownership tokens - trigger contract: `trigger`, `stop_trigger`, `get_trigger_status`, `list_triggers`, blocking/detached reporting, and local browser-open limits - terminal task-outcome contract: `mcp:task-status`, agent/ownership context, serialized lifecycle ordering, and `fail_task` acknowledgement semantics diff --git a/mcp/src/errors.ts b/mcp/src/errors.ts index a664674c3..a78abfda1 100644 --- a/mcp/src/errors.ts +++ b/mcp/src/errors.ts @@ -32,7 +32,7 @@ export const FSB_ERROR_MESSAGES: Record = { 'invalid_client_label': 'The MCP client label is not on the trusted visual-session allowlist. Use one of the approved client names exactly as documented.', 'visual_session_not_found': - 'The provided visual-session token does not match an active client-owned visual session. The token may be stale, already finalized, or already cleared. Start a new visual session or use the latest token returned by start_visual_session.', + 'The provided visual-session token does not match an active client-owned visual session. The token may be stale, already finalized, or already cleared. Omit session_token: the next action call that carries visual_reason and client starts a new visual session.', 'visual_surface_busy': 'The active visual surface is already owned by an FSB automation session. Wait for the current run to finish or stop it before starting a client-owned visual session.', 'session_not_found': @@ -69,8 +69,6 @@ const CODE_ONLY_ERROR_KEYS = new Set([ // v0.9.62 implicit visual session contract (Phase 255 Plan 02) 'VISUAL_FIELDS_REQUIRED', 'BADGE_NOT_ALLOWED', - // v0.9.62 removal of explicit visual-session tools (Phase 258 Plan 01) - 'TOOL_REMOVED', // v0.9.99 Native Capability Catalog (Phase 27 FETCH-04): mid-mutation SW-eviction // ambiguity. Surfaced verbatim so the MCP host can distinguish "ambiguous -- ask // the user" from a generic action_rejected. INV-01-safe (no MCP tool schema touched). @@ -222,8 +220,6 @@ function buildLayeredDetail( : []; const cap = typeof fsbResult?.cap === 'number' ? fsbResult.cap : undefined; const active = typeof fsbResult?.active === 'number' ? fsbResult.active : undefined; - const removedTool = typeof fsbResult?.removed_tool === 'string' ? fsbResult.removed_tool : ''; - const removedInVersion = typeof fsbResult?.removed_in_version === 'string' ? fsbResult.removed_in_version : '0.9.0'; switch (errorKey) { case 'package_version_mismatch': @@ -382,19 +378,11 @@ function buildLayeredDetail( ? `Retry with one of the approved client labels: ${allowedClients.join(', ')}.` : 'Retry with one of the approved client labels enumerated by getAllowedMcpVisualClientLabels() in mcp/src/tools/visual-session.ts.', }; - case 'TOOL_REMOVED': - return { - detected: LAYER_LABELS.visualSession, - why: removedTool - ? `The ${removedTool} tool was removed in v${removedInVersion} of fsb-mcp-server (v0.9.62 implicit visual-session contract).` - : `The explicit visual_session start/end tools were removed in v${removedInVersion} of fsb-mcp-server (v0.9.62 implicit visual-session contract).`, - nextAction: 'Use the implicit contract: every action tool (click, type_text, navigate, ...) now requires visual_reason (string) and client (allowlisted label) in its input, the visual session is created implicitly on the first action call and refreshed on a sliding 60-second window, and is_final: true on the last action of a task clears the overlay immediately. See CHANGELOG.md#v0.9.0 and the Visual Session Lifecycle section of mcp/README.md for the migration recipe.', - }; case 'visual_session_not_found': return { detected: LAYER_LABELS.visualSession, why: 'The provided visual-session token does not match an active client-owned visual session, which usually means it is stale, already finalized, or already cleared.', - nextAction: 'Start a new visual session or retry with the latest token returned by start_visual_session before sending more progress or final-status updates.', + nextAction: 'Omit session_token. The visual session is implicit: the next action call that carries visual_reason and client starts a new one.', }; case 'visual_surface_busy': return { diff --git a/mcp/src/runtime.ts b/mcp/src/runtime.ts index 07da714d9..72e276aaf 100644 --- a/mcp/src/runtime.ts +++ b/mcp/src/runtime.ts @@ -5,13 +5,11 @@ import { TaskQueue } from './queue.js'; import { AgentScope } from './agent-scope.js'; import { detectMcpClientInventory } from './client-inventory.js'; import { registerAutopilotTools } from './tools/autopilot.js'; -import { registerVisualSessionTools } from './tools/visual-session.js'; import { registerTriggerTools } from './tools/triggers.js'; import { registerManualTools } from './tools/manual.js'; import { registerReadOnlyTools } from './tools/read-only.js'; import { registerScreenshotTools } from './tools/screenshots.js'; import { registerObservabilityTools } from './tools/observability.js'; -import { registerAgentTools } from './tools/agents.js'; import { registerVaultTools } from './tools/vault.js'; import { registerCapabilityTools } from './tools/capabilities.js'; import { registerResources } from './resources/index.js'; @@ -43,13 +41,11 @@ export function createRuntime(options: RuntimeOptions = {}): FSBRuntime { agentScope.setClientInventorySupplier(() => detectMcpClientInventory()); } - registerVisualSessionTools(server, bridge, queue, agentScope); registerTriggerTools(server, bridge, queue, agentScope); registerManualTools(server, bridge, queue, agentScope); registerReadOnlyTools(server, bridge, queue, agentScope); registerScreenshotTools(server, bridge, queue, agentScope); registerObservabilityTools(server, bridge, queue, agentScope); - registerAgentTools(server, bridge, queue, agentScope); registerVaultTools(server, bridge, queue, agentScope); registerCapabilityTools(server, bridge, queue, agentScope); registerAutopilotTools(server, bridge, queue, agentScope); diff --git a/mcp/src/tools/agents.ts b/mcp/src/tools/agents.ts deleted file mode 100644 index 1edab7050..000000000 --- a/mcp/src/tools/agents.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { z } from 'zod'; -import type { WebSocketBridge } from '../bridge.js'; -import type { TaskQueue } from '../queue.js'; -import type { MCPResponse } from '../types.js'; -import { AgentScope } from '../agent-scope.js'; -import { sendAgentScopedBridgeMessage } from '../agent-bridge.js'; -import { mapFSBError } from '../errors.js'; - -/** - * Register agent management tools: create, list, run, stop, delete, toggle, - * stats, and history. These let MCP clients manage FSB background agents - * without the extension UI. - * - * Phase 245 D-04 / D-05: action tools registered here (currently `back`) - * return a `change_report` field describing what the action mutated -- URL - * changes, dialogs opened, nodes added/removed, attribute changes, focus - * shift -- so the agent learns the consequence without a follow-up - * read_page. Read-only tools registered here (none today) would not include - * this field. The contract is gated by the Action Change Reports toggle in - * the extension control panel (default on); when off, action tools revert - * to pre-Phase-245 response shape with zero observer overhead. - */ -export function registerAgentTools( - server: McpServer, - bridge: WebSocketBridge, - queue: TaskQueue, - agentScope: AgentScope, -): void { - // Phase 242 D-01: 'back' MCP tool -- single-step ownership-gated history back. - // Routes through extension dispatcher via the new 'mcp:go-back' bridge message. - server.tool( - 'back', - 'Navigate one step back in browser history on the agent\'s active tab. Single-step only (no back(n)); no companion forward tool in v0.9.60. Agent-scoped: targets the agent\'s active tab unless tab_id is supplied. Ownership-enforced via the Phase 240 dispatch gate -- cross-agent calls reject with TAB_NOT_OWNED. Returns a structured result of shape { status, resultingUrl, historyDepth } where status is one of: ok (back navigation settled cleanly), no_history (history depth <= 1, nothing to go back to), cross_origin (target page is on a different origin from the source), bf_cache (back was served from the back/forward cache and the pageshow listener observed event.persisted=true), fragment_only (only the URL fragment changed via history navigation). Background-tab compatible: does not steal focus or call chrome.tabs.update({active: true}). Multi-agent contract: agent_id is FSB-issued and required (the server captures it via agent:register on first tool call -- callers do not provide it). tab_id is agent-scoped: only tabs owned by the calling agent can be addressed. The concurrency cap is configurable (default 8, range 1-64); the (N+1)th agent claim is rejected with AGENT_CAP_REACHED { cap, active }. Ownership enforcement: cross-agent calls reject with TAB_NOT_OWNED; incognito tabs reject with TAB_INCOGNITO_NOT_SUPPORTED; cross-window tabs reject with TAB_OUT_OF_SCOPE. Tool-specific typed error codes returnable by this tool: NO_BACK_HISTORY, CROSS_ORIGIN, BF_CACHE, FRAGMENT_ONLY. RETURNS change_report: the response includes a `change_report` field with a compact diff of what the back navigation mutated (URL, dialogs_opened, nodes_added, nodes_removed, attrs_changed, inputs_changed, focus_shift). Cross-origin transitions emit a URL-only report with `cross_origin:true`. Use this to learn the consequence without calling read_page next; if `change_report.truncated:true` is set, call read_page for the full state.', - { tab_id: z.coerce.number().int().positive().finite().optional().describe('Target tab in agent pool. If omitted, defaults to the agent\'s active tab.') }, - async ({ tab_id }) => { - if (!bridge.isConnected) { - return mapFSBError({ success: false, error: 'extension_not_connected' }); - } - return queue.enqueue('back', async () => { - const targetTabId = typeof tab_id === 'number' ? tab_id : null; - const payload: Record = {}; - if (typeof tab_id === 'number') payload.tabId = tab_id; - const result = await sendAgentScopedBridgeMessage( - bridge, - agentScope, - 'mcp:go-back', - payload, - { timeout: 5_000, targetTabId }, // 2s settle + 3s headroom (RESEARCH Pattern 1) - ); - return mapFSBError(result); - }); - }, - ); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // // create_agent -- create a new background agent (mutation) -// server.tool( -// 'create_agent', -// 'Create a new background agent. The agent can either use a pinned URL or let AI choose its starting site at run time. Returns the created agent with its ID.', -// { -// name: z.string().describe('Agent display name'), -// task: z.string().describe('Natural language task description the agent will execute'), -// start_mode: z.enum(['pinned', 'ai_routed']).optional().describe('How the agent chooses its starting page. pinned uses target_url; ai_routed resolves from the task at run time.'), -// target_url: z.string().url().optional().describe('Pinned start URL. Required when start_mode is pinned; omit for ai_routed.'), -// schedule_type: z.enum(['interval', 'daily', 'once']).describe('Schedule type: interval (every N minutes), daily (at specific time), once (run once then disable)'), -// interval_minutes: z.number().optional().describe('Minutes between runs (required for interval schedule)'), -// daily_time: z.string().optional().describe('Time of day in HH:MM format (required for daily schedule)'), -// days_of_week: z.array(z.number().min(0).max(6)).optional().describe('Days of week 0=Sun..6=Sat (optional for daily schedule, defaults to every day)'), -// max_iterations: z.number().min(1).max(50).optional().describe('Max AI iterations per run (default: 15)'), -// }, -// async ({ name, task, start_mode, target_url, schedule_type, interval_minutes, daily_time, days_of_week, max_iterations }) => { -// if (!bridge.isConnected) { -// return mapFSBError({ success: false, error: 'extension_not_connected' }); -// } -// return queue.enqueue('create_agent', async () => { -// const startMode = start_mode ?? (target_url ? 'pinned' : 'ai_routed'); -// if (startMode === 'pinned' && !target_url) { -// return mapFSBError({ success: false, error: 'Pinned agents require target_url' }); -// } -// const schedule: Record = { type: schedule_type }; -// if (interval_minutes !== undefined) schedule.intervalMinutes = interval_minutes; -// if (daily_time !== undefined) schedule.dailyTime = daily_time; -// if (days_of_week !== undefined) schedule.daysOfWeek = days_of_week; - // -// const result = await bridge.sendAndWait( -// { -// type: 'mcp:create-agent', -// payload: { name, task, startMode, targetUrl: target_url || '', schedule, maxIterations: max_iterations }, -// }, -// { timeout: 10_000 }, -// ); -// return mapFSBError(result); -// }); -// }, -// ); - // -// // list_agents -- list all background agents (read-only, bypasses queue) -// server.tool( -// 'list_agents', -// 'List all background agents with status, schedule, and last run info. Returns agent summaries without full run history.', -// {}, -// async () => { -// if (!bridge.isConnected) { -// return mapFSBError({ success: false, error: 'extension_not_connected' }); -// } -// return queue.enqueue('list_agents', async () => { -// const result = await bridge.sendAndWait( -// { type: 'mcp:list-agents', payload: {} }, -// { timeout: 5_000 }, -// ); -// return mapFSBError(result); -// }); -// }, -// ); - // -// // run_agent -- trigger immediate agent execution (mutation, with progress) -// server.tool( -// 'run_agent', -// 'Trigger immediate execution of a background agent. The agent navigates to its target URL and runs its task. Returns execution result with status and summary.', -// { -// agent_id: z.string().describe('Agent ID to run'), -// }, -// async ({ agent_id }, extra) => { -// if (!bridge.isConnected) { -// return mapFSBError({ success: false, error: 'extension_not_connected' }); -// } -// return queue.enqueue('run_agent', async () => { -// const onProgress = (p: MCPResponse) => { -// const { progress, phase, eta, action, taskId } = p.payload as { -// taskId?: string; progress?: number; phase?: string; eta?: string; action?: string; -// }; -// const message = [phase && `[${phase}]`, action, eta && `(ETA: ${eta})`] -// .filter(Boolean).join(' '); - // -// if (extra._meta?.progressToken !== undefined) { -// extra.sendNotification({ -// method: 'notifications/progress', -// params: { -// progressToken: extra._meta.progressToken, -// progress: progress ?? 0, -// total: 100, -// message, -// }, -// }).catch(() => {}); -// } - // -// server.sendLoggingMessage({ -// level: 'info', -// logger: 'fsb-agent', -// data: { taskId, progress, phase, eta, action }, -// }); -// }; - // -// const result = await bridge.sendAndWait( -// { type: 'mcp:run-agent', payload: { agentId: agent_id } }, -// { timeout: 300_000, onProgress }, -// ); -// return mapFSBError(result); -// }); -// }, -// ); - // -// // stop_agent -- stop a running agent execution (mutation) -// server.tool( -// 'stop_agent', -// 'Stop a currently running agent execution. Use when an agent run is taking too long or needs to be cancelled.', -// { -// agent_id: z.string().describe('Agent ID to stop'), -// }, -// async ({ agent_id }) => { -// if (!bridge.isConnected) { -// return mapFSBError({ success: false, error: 'extension_not_connected' }); -// } -// return queue.enqueue('stop_agent', async () => { -// const result = await bridge.sendAndWait( -// { type: 'mcp:stop-agent', payload: { agentId: agent_id } }, -// { timeout: 10_000 }, -// ); -// return mapFSBError(result); -// }); -// }, -// ); - // -// // delete_agent -- permanently delete an agent (mutation) -// server.tool( -// 'delete_agent', -// 'Delete a background agent and remove its scheduled alarm. This action is permanent.', -// { -// agent_id: z.string().describe('Agent ID to delete'), -// }, -// async ({ agent_id }) => { -// if (!bridge.isConnected) { -// return mapFSBError({ success: false, error: 'extension_not_connected' }); -// } -// return queue.enqueue('delete_agent', async () => { -// const result = await bridge.sendAndWait( -// { type: 'mcp:delete-agent', payload: { agentId: agent_id } }, -// { timeout: 10_000 }, -// ); -// return mapFSBError(result); -// }); -// }, -// ); - // -// // toggle_agent -- enable/disable an agent (mutation) -// server.tool( -// 'toggle_agent', -// 'Enable or disable a background agent. Disabled agents keep their config but do not run on schedule.', -// { -// agent_id: z.string().describe('Agent ID to toggle'), -// }, -// async ({ agent_id }) => { -// if (!bridge.isConnected) { -// return mapFSBError({ success: false, error: 'extension_not_connected' }); -// } -// return queue.enqueue('toggle_agent', async () => { -// const result = await bridge.sendAndWait( -// { type: 'mcp:toggle-agent', payload: { agentId: agent_id } }, -// { timeout: 10_000 }, -// ); -// return mapFSBError(result); -// }); -// }, -// ); - // -// // get_agent_stats -- aggregate stats across all agents (read-only, bypasses queue) -// server.tool( -// 'get_agent_stats', -// 'Get aggregate statistics across all agents: total runs, success rate, total cost, active agent count.', -// {}, -// async () => { -// if (!bridge.isConnected) { -// return mapFSBError({ success: false, error: 'extension_not_connected' }); -// } -// return queue.enqueue('get_agent_stats', async () => { -// const result = await bridge.sendAndWait( -// { type: 'mcp:get-agent-stats', payload: {} }, -// { timeout: 5_000 }, -// ); -// return mapFSBError(result); -// }); -// }, -// ); - // -// // get_agent_history -- run history for a specific agent (read-only, bypasses queue) -// server.tool( -// 'get_agent_history', -// 'Get run history for a specific agent. Returns recent runs with status, duration, cost, and execution mode.', -// { -// agent_id: z.string().describe('Agent ID to get history for'), -// limit: z.number().min(1).max(100).optional().describe('Max runs to return (default: 10)'), -// }, -// async ({ agent_id, limit }) => { -// if (!bridge.isConnected) { -// return mapFSBError({ success: false, error: 'extension_not_connected' }); -// } -// return queue.enqueue('get_agent_history', async () => { -// const result = await bridge.sendAndWait( -// { type: 'mcp:get-agent-history', payload: { agentId: agent_id, limit } }, -// { timeout: 5_000 }, -// ); -// return mapFSBError(result); -// }); -// }, -// ); -} diff --git a/mcp/src/tools/autopilot.ts b/mcp/src/tools/autopilot.ts index a219cb5d0..748ad3f10 100644 --- a/mcp/src/tools/autopilot.ts +++ b/mcp/src/tools/autopilot.ts @@ -19,7 +19,7 @@ import { mapFSBError } from '../errors.js'; * tool the loop dispatches still emits change_report when the global toggle * is on). Read run_task's completion summary or call get_task_status for * autopilot-level state; the action-tool change_report contract documented - * on the manual tools (manual.ts) and on `back` (agents.ts) is the right + * on the manual tools (manual.ts) is the right * place to consume per-action consequences. */ export function registerAutopilotTools( diff --git a/mcp/src/tools/visual-session.ts b/mcp/src/tools/visual-session.ts index 786006018..dc20fcb92 100644 --- a/mcp/src/tools/visual-session.ts +++ b/mcp/src/tools/visual-session.ts @@ -1,10 +1,3 @@ -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { z } from 'zod'; -import type { WebSocketBridge } from '../bridge.js'; -import type { TaskQueue } from '../queue.js'; -import { AgentScope } from '../agent-scope.js'; -import { mapFSBError } from '../errors.js'; - // Approved visual-session client labels (must match the extension's allowlist) const MCP_VISUAL_CLIENT_LABELS: string[] = [ 'Claude', 'Codex', 'ChatGPT', 'Perplexity', 'Windsurf', @@ -36,56 +29,3 @@ export function isAllowedMcpVisualClientLabel(raw: unknown): boolean { export function getAllowedMcpVisualClientLabels(): string[] { return MCP_VISUAL_CLIENT_LABELS.slice(); } - -export function registerVisualSessionTools( - server: McpServer, - bridge: WebSocketBridge, - queue: TaskQueue, - agentScope: AgentScope, -): void { - server.tool( - 'start_visual_session', - '[REMOVED in v0.9.0] This tool was removed in fsb-mcp-server v0.9.0 (FSB v0.9.62 implicit visual-session contract). Calling it returns the typed TOOL_REMOVED error with a migration recipe pointer. The visual session is now implicit on every action tool call via the required visual_reason + client field bundle -- there is no longer a separate start call. See CHANGELOG.md#v0.9.0 and the Visual Session Lifecycle section of mcp/README.md for the migration recipe.', - { - client: z.string().describe('Trusted MCP client label, for example Codex, ChatGPT, Claude, or Gemini. Must be on the approved allowlist.'), - task: z.string().describe('Short task title shown in the visible automation surface.'), - detail: z.string().optional().describe('Optional initial detail line for the overlay, such as "Preparing checkout flow".'), - tab_id: z.coerce.number().int().positive().finite().optional().describe('Optional. Tab id the visual session attaches to. Omit when this agent owns exactly one tab; required to disambiguate when this agent owns multiple. Legacy popup/sidepanel/autopilot do not need to pass this.'), - }, - async () => { - // Phase 258 Plan 01 -- TOOL_REMOVED stub. Synchronous rejection - // BEFORE the task queue and BEFORE the bridge connectivity check: - // a caller of a removed tool gets the migration recipe even if - // the extension is offline. See .planning/v0.9.62-CONTRACT.md - // Typed Errors section (TOOL_REMOVED body intent). - return mapFSBError({ - success: false, - errorCode: 'TOOL_REMOVED', - removed_tool: 'start_visual_session', - removed_in_version: '0.9.0', - }); - }, - ); - - server.tool( - 'end_visual_session', - '[REMOVED in v0.9.0] This tool was removed in fsb-mcp-server v0.9.0 (FSB v0.9.62 implicit visual-session contract). Calling it returns the typed TOOL_REMOVED error with a migration recipe pointer. The visual session now clears either automatically after 60 seconds of silence (sliding window) or immediately when an action tool is called with is_final: true -- there is no longer a separate end call. See CHANGELOG.md#v0.9.0 and the Visual Session Lifecycle section of mcp/README.md for the migration recipe.', - { - session_token: z.string().describe('Session token returned by start_visual_session.'), - reason: z.enum(['cancelled', 'ended']).optional().describe('Optional end reason for analytics/debugging.'), - }, - async () => { - // Phase 258 Plan 01 -- TOOL_REMOVED stub. Synchronous rejection - // BEFORE the task queue and BEFORE the bridge connectivity check: - // a caller of a removed tool gets the migration recipe even if - // the extension is offline. See .planning/v0.9.62-CONTRACT.md - // Typed Errors section (TOOL_REMOVED body intent). - return mapFSBError({ - success: false, - errorCode: 'TOOL_REMOVED', - removed_tool: 'end_visual_session', - removed_in_version: '0.9.0', - }); - }, - ); -} diff --git a/server-py/app/main.py b/server-py/app/main.py index ddc081b1d..b8edf24cc 100644 --- a/server-py/app/main.py +++ b/server-py/app/main.py @@ -6,7 +6,7 @@ from .config import settings from .db import close_pool, init_pool -from .routers import agents, auth, sse, stats +from .routers import auth @asynccontextmanager @@ -41,17 +41,12 @@ async def log_requests(request: Request, call_next): start = time.time() response = await call_next(request) duration = int((time.time() - start) * 1000) - path = request.url.path - if path != "/api/sse": - print(f"{request.method} {path} {response.status_code} {duration}ms") + print(f"{request.method} {request.url.path} {response.status_code} {duration}ms") return response # Routers app.include_router(auth.router) -app.include_router(agents.router) -app.include_router(stats.router) -app.include_router(sse.router) @app.get("/") diff --git a/server-py/app/models.py b/server-py/app/models.py index d9197668a..b5ba51e08 100644 --- a/server-py/app/models.py +++ b/server-py/app/models.py @@ -1,6 +1,6 @@ from __future__ import annotations -from pydantic import BaseModel, Field +from pydantic import BaseModel # --- Auth --- @@ -16,86 +16,6 @@ class ValidateResponse(BaseModel): error: str | None = None -# --- Agents --- - -class AgentUpsert(BaseModel): - agentId: str - name: str - task: str - startMode: str = "pinned" - targetUrl: str = "" - scheduleType: str = "interval" - scheduleConfig: str = "{}" - enabled: bool = True - - -class AgentResponse(BaseModel): - success: bool = True - agent: dict | None = None - - -class AgentListResponse(BaseModel): - agents: list[dict] - - -class DeleteResponse(BaseModel): - success: bool = True - - -# --- Runs --- - -class RunPayloadRun(BaseModel): - runId: str | None = None - startedAt: str | None = None - completedAt: str | None = None - status: str = "unknown" - result: str | None = None - error: str | None = None - iterations: int = 0 - tokensUsed: int = 0 - costUsd: float = 0 - durationMs: int = 0 - executionMode: str = "ai_initial" - costSaved: float = 0 - - -class RunReportPayload(BaseModel): - """Body for POST /api/agents/{agentId}/runs -- includes agent upsert fields + run.""" - name: str | None = None - task: str | None = None - startMode: str = "pinned" - targetUrl: str | None = None - scheduleType: str = "interval" - scheduleConfig: str = "{}" - enabled: bool = True - run: RunPayloadRun = Field(default_factory=RunPayloadRun) - - -class RunListResponse(BaseModel): - runs: list[dict] - total: int - limit: int - offset: int - - -# --- Stats --- - -class StatsResponse(BaseModel): - totalAgents: int = 0 - enabledAgents: int = 0 - totalRuns: int = 0 - successfulRuns: int = 0 - runsToday: int = 0 - successfulToday: int = 0 - successRate: int = 0 - totalTokens: int = 0 - totalCost: float = 0 - totalDuration: int = 0 - totalCostSaved: float = 0 - replayRuns: int = 0 - aiFallbackRuns: int = 0 - - # --- Generic --- class ErrorResponse(BaseModel): diff --git a/server-py/app/routers/agents.py b/server-py/app/routers/agents.py deleted file mode 100644 index 28185df3a..000000000 --- a/server-py/app/routers/agents.py +++ /dev/null @@ -1,235 +0,0 @@ -import asyncio -import json -import time - -from fastapi import APIRouter, Depends, HTTPException, Query - -from ..auth import require_hash_key -from ..db import get_pool -from ..models import ( - AgentListResponse, - AgentResponse, - AgentUpsert, - DeleteResponse, - RunListResponse, - RunReportPayload, -) - -router = APIRouter(prefix="/api/agents", tags=["agents"]) - -# SSE clients registry: hash_key -> list[asyncio.Queue] -sse_clients: dict[str, list[asyncio.Queue]] = {} - - -def broadcast_sse(hash_key: str, data: dict) -> None: - """Push an event to all SSE clients subscribed to this hash_key.""" - clients = sse_clients.get(hash_key) - if not clients: - return - message = json.dumps(data) - dead = [] - for i, q in enumerate(clients): - try: - q.put_nowait(message) - except asyncio.QueueFull: - dead.append(i) - # Clean up dead queues - for i in reversed(dead): - clients.pop(i) - if not clients: - sse_clients.pop(hash_key, None) - - -def _row_to_dict(row) -> dict: - """Convert an asyncpg Record to a JSON-friendly dict.""" - d = dict(row) - for k, v in d.items(): - if hasattr(v, "isoformat"): - d[k] = v.isoformat() - return d - - -# --- GET /api/agents --- -@router.get("", response_model=AgentListResponse) -async def list_agents(hash_key: str = Depends(require_hash_key)): - pool = await get_pool() - async with pool.acquire() as conn: - rows = await conn.fetch( - "SELECT * FROM agents WHERE hash_key = $1 ORDER BY created_at DESC", - hash_key, - ) - return AgentListResponse(agents=[_row_to_dict(r) for r in rows]) - - -# --- POST /api/agents --- -@router.post("", response_model=AgentResponse) -async def upsert_agent(body: AgentUpsert, hash_key: str = Depends(require_hash_key)): - effective_start_mode = body.startMode or ("pinned" if body.targetUrl else "ai_routed") - - if not body.agentId or not body.name or not body.task: - raise HTTPException( - status_code=400, - detail="Missing required fields: agentId, name, task", - ) - if effective_start_mode == "pinned" and not body.targetUrl: - raise HTTPException(status_code=400, detail="Pinned agents require targetUrl") - - pool = await get_pool() - async with pool.acquire() as conn: - await conn.execute( - """ - INSERT INTO agents (hash_key, agent_id, name, task, start_mode, target_url, schedule_type, schedule_config, enabled) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9) - ON CONFLICT (hash_key, agent_id) DO UPDATE SET - name = EXCLUDED.name, - task = EXCLUDED.task, - start_mode = EXCLUDED.start_mode, - target_url = EXCLUDED.target_url, - schedule_type = EXCLUDED.schedule_type, - schedule_config = EXCLUDED.schedule_config, - enabled = EXCLUDED.enabled, - updated_at = NOW() - """, - hash_key, - body.agentId, - body.name, - body.task, - effective_start_mode, - body.targetUrl or "", - body.scheduleType, - body.scheduleConfig or "{}", - body.enabled, - ) - row = await conn.fetchrow( - "SELECT * FROM agents WHERE hash_key = $1 AND agent_id = $2", - hash_key, - body.agentId, - ) - - agent_dict = _row_to_dict(row) if row else None - broadcast_sse(hash_key, {"type": "agent_updated", "agent": agent_dict}) - return AgentResponse(success=True, agent=agent_dict) - - -# --- DELETE /api/agents/{agentId} --- -@router.delete("/{agentId}", response_model=DeleteResponse) -async def delete_agent(agentId: str, hash_key: str = Depends(require_hash_key)): - pool = await get_pool() - async with pool.acquire() as conn: - result = await conn.execute( - "DELETE FROM agents WHERE hash_key = $1 AND agent_id = $2", - hash_key, - agentId, - ) - # result is e.g. "DELETE 1" or "DELETE 0" - if result.endswith("0"): - raise HTTPException(status_code=404, detail="Agent not found") - - broadcast_sse(hash_key, {"type": "agent_deleted", "agentId": agentId}) - return DeleteResponse(success=True) - - -# --- POST /api/agents/{agentId}/runs --- -@router.post("/{agentId}/runs") -async def report_run( - agentId: str, body: RunReportPayload, hash_key: str = Depends(require_hash_key) -): - pool = await get_pool() - async with pool.acquire() as conn: - # Upsert agent data if provided - if body.name and body.task and body.targetUrl is not None: - effective_start_mode = body.startMode or ("pinned" if body.targetUrl else "ai_routed") - await conn.execute( - """ - INSERT INTO agents (hash_key, agent_id, name, task, start_mode, target_url, schedule_type, schedule_config, enabled) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9) - ON CONFLICT (hash_key, agent_id) DO UPDATE SET - name = EXCLUDED.name, - task = EXCLUDED.task, - start_mode = EXCLUDED.start_mode, - target_url = EXCLUDED.target_url, - schedule_type = EXCLUDED.schedule_type, - schedule_config = EXCLUDED.schedule_config, - enabled = EXCLUDED.enabled, - updated_at = NOW() - """, - hash_key, - agentId, - body.name, - body.task, - effective_start_mode, - body.targetUrl or "", - body.scheduleType, - body.scheduleConfig or "{}", - body.enabled, - ) - - # Record the run - run = body.run - run_id = run.runId or f"run_{int(time.time() * 1000):x}" - from datetime import datetime, timezone - - now_iso = datetime.now(timezone.utc).isoformat() - - await conn.execute( - """ - INSERT INTO agent_runs - (hash_key, agent_id, run_id, started_at, completed_at, status, - result, error, iterations, tokens_used, cost_usd, duration_ms, - execution_mode, cost_saved) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) - """, - hash_key, - agentId, - run_id, - run.startedAt or now_iso, - run.completedAt or now_iso, - run.status, - run.result, - run.error, - run.iterations, - run.tokensUsed, - run.costUsd, - run.durationMs, - run.executionMode, - run.costSaved, - ) - - broadcast_sse(hash_key, {"type": "run_completed", "agentId": agentId, "run": run.model_dump()}) - return {"success": True} - - -# --- GET /api/agents/{agentId}/runs --- -@router.get("/{agentId}/runs", response_model=RunListResponse) -async def list_runs( - agentId: str, - limit: int = Query(20, le=100), - offset: int = Query(0, ge=0), - hash_key: str = Depends(require_hash_key), -): - pool = await get_pool() - async with pool.acquire() as conn: - rows = await conn.fetch( - """ - SELECT * FROM agent_runs - WHERE hash_key = $1 AND agent_id = $2 - ORDER BY completed_at DESC - LIMIT $3 OFFSET $4 - """, - hash_key, - agentId, - limit, - offset, - ) - total_row = await conn.fetchrow( - "SELECT COUNT(*) AS count FROM agent_runs WHERE hash_key = $1 AND agent_id = $2", - hash_key, - agentId, - ) - - return RunListResponse( - runs=[_row_to_dict(r) for r in rows], - total=total_row["count"] if total_row else 0, - limit=limit, - offset=offset, - ) diff --git a/server-py/app/routers/sse.py b/server-py/app/routers/sse.py deleted file mode 100644 index ab9129d91..000000000 --- a/server-py/app/routers/sse.py +++ /dev/null @@ -1,59 +0,0 @@ -import asyncio -import json - -from fastapi import APIRouter, Header, HTTPException -from starlette.responses import StreamingResponse - -router = APIRouter(prefix="/api/sse", tags=["sse"]) - - -@router.get("") -async def sse_stream( - x_fsb_hash_key: str | None = Header(None, alias="X-FSB-Hash-Key"), -): - """Server-Sent Events stream for real-time updates.""" - if not x_fsb_hash_key: - raise HTTPException(status_code=401, detail="Missing X-FSB-Hash-Key header") - - # Import the shared client registry from agents router - from .agents import sse_clients - - queue: asyncio.Queue = asyncio.Queue(maxsize=256) - - # Register client - if x_fsb_hash_key not in sse_clients: - sse_clients[x_fsb_hash_key] = [] - sse_clients[x_fsb_hash_key].append(queue) - - async def event_generator(): - try: - # Send initial connected event - yield f"data: {json.dumps({'type': 'connected'})}\n\n" - - while True: - try: - # Wait for a message with a 30s timeout for keepalive - message = await asyncio.wait_for(queue.get(), timeout=30.0) - yield f"data: {message}\n\n" - except asyncio.TimeoutError: - # Send keepalive comment - yield ": keepalive\n\n" - except asyncio.CancelledError: - pass - finally: - # Cleanup on disconnect - clients = sse_clients.get(x_fsb_hash_key) - if clients and queue in clients: - clients.remove(queue) - if not clients: - sse_clients.pop(x_fsb_hash_key, None) - - return StreamingResponse( - event_generator(), - media_type="text/event-stream", - headers={ - "Cache-Control": "no-cache", - "Connection": "keep-alive", - "X-Accel-Buffering": "no", - }, - ) diff --git a/server-py/app/routers/stats.py b/server-py/app/routers/stats.py deleted file mode 100644 index 8b422d0e0..000000000 --- a/server-py/app/routers/stats.py +++ /dev/null @@ -1,69 +0,0 @@ -from fastapi import APIRouter, Depends - -from ..auth import require_hash_key -from ..db import get_pool -from ..models import StatsResponse - -router = APIRouter(prefix="/api/stats", tags=["stats"]) - - -@router.get("", response_model=StatsResponse) -async def get_stats(hash_key: str = Depends(require_hash_key)): - pool = await get_pool() - async with pool.acquire() as conn: - # All-time stats - all_time = await conn.fetchrow( - """ - SELECT - COUNT(DISTINCT agent_id) AS total_agents_from_runs, - COUNT(*) AS total_runs, - SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) AS successful_runs, - COALESCE(SUM(tokens_used), 0) AS total_tokens, - COALESCE(SUM(cost_usd), 0) AS total_cost, - COALESCE(SUM(duration_ms), 0) AS total_duration, - COALESCE(SUM(cost_saved), 0) AS total_cost_saved, - SUM(CASE WHEN execution_mode = 'replay' THEN 1 ELSE 0 END) AS replay_runs, - SUM(CASE WHEN execution_mode = 'ai_fallback' THEN 1 ELSE 0 END) AS ai_fallback_runs - FROM agent_runs - WHERE hash_key = $1 - """, - hash_key, - ) - - # Today stats - today = await conn.fetchrow( - """ - SELECT - COUNT(*) AS runs_today, - SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) AS successful_today - FROM agent_runs - WHERE hash_key = $1 AND completed_at >= NOW() - INTERVAL '1 day' - """, - hash_key, - ) - - # Agent counts - agents = await conn.fetch( - "SELECT enabled FROM agents WHERE hash_key = $1", hash_key - ) - - total_agents = len(agents) - enabled_agents = sum(1 for a in agents if a["enabled"]) - total_runs = all_time["total_runs"] or 0 - successful_runs = all_time["successful_runs"] or 0 - - return StatsResponse( - totalAgents=total_agents, - enabledAgents=enabled_agents, - totalRuns=total_runs, - successfulRuns=successful_runs, - runsToday=today["runs_today"] or 0, - successfulToday=today["successful_today"] or 0, - successRate=round((successful_runs / total_runs) * 100) if total_runs > 0 else 0, - totalTokens=int(all_time["total_tokens"]), - totalCost=round(float(all_time["total_cost"]), 4), - totalDuration=int(all_time["total_duration"]), - totalCostSaved=round(float(all_time["total_cost_saved"]), 4), - replayRuns=int(all_time["replay_runs"] or 0), - aiFallbackRuns=int(all_time["ai_fallback_runs"] or 0), - ) diff --git a/showcase/angular/public/llms-full.txt b/showcase/angular/public/llms-full.txt index 63bdfc1c2..29ab2d8e5 100644 --- a/showcase/angular/public/llms-full.txt +++ b/showcase/angular/public/llms-full.txt @@ -1,11 +1,11 @@ - + # FSB (Full Self-Browsing) The About page embeds the public 33-second FSB trailer (uploaded September 6, 2026), with a visible synopsis and YouTube link. FSB can run from natural-language instructions with a chosen model, through an agent CLI, or as an MCP browser connection for a calling agent. Chrome 116 or a compatible desktop Chromium browser is required; Firefox is not currently supported. The extension is free and MIT licensed; hosted model usage is billed by the chosen provider. MCP agents can work across tabs using agent identity and tab ownership. Setup, costs, privacy, and troubleshooting answers are at https://full-selfbrowsing.com/support. ## 1. Project Description -FSB (Full Self-Browsing) is an open-source Chrome extension that automates the browser through natural language. You describe a task in plain English; FSB plans the clicks, types, and navigation to complete it. The extension runs in your browser with your own API keys, encrypted local storage, and opt-out anonymous usage telemetry for aggregate public stats only. Multi-model AI (xAI Grok, OpenAI, Anthropic Claude, Google Gemini, OpenRouter, LM Studio, custom OpenAI-compatible endpoints), 56 canonical browser tools, a 68-tool MCP surface, 142+ site-specific guides, reactive trigger watchers, real file uploads, and a 128-app native capability catalog for verified first-party API execution. Current release: FSB v0.9.92 (Chrome extension) with fsb-mcp-server 0.11.2 (npm MCP package). +FSB (Full Self-Browsing) is an open-source Chrome extension that automates the browser through natural language. You describe a task in plain English; FSB plans the clicks, types, and navigation to complete it. The extension runs in your browser with your own API keys, encrypted local storage, and opt-out anonymous usage telemetry for aggregate public stats only. Multi-model AI (xAI Grok, OpenAI, Anthropic Claude, Google Gemini, OpenRouter, LM Studio, custom OpenAI-compatible endpoints), 56 canonical browser tools, a 69-tool MCP surface, 142+ site-specific guides, reactive trigger watchers, real file uploads, and a 128-app native capability catalog for verified first-party API execution. Current release: FSB v0.9.92 (Chrome extension) with fsb-mcp-server 0.11.2 (npm MCP package). License: [MIT License](https://github.com/fullselfbrowsing/FSB/blob/main/LICENSE). @@ -41,7 +41,7 @@ The bring-your-own-key model is deliberate. The user picks the model -- frontier - Operator loop: every workflow can act on the page, observe what changed, verify the outcome, report errors, and recover when the web app shifts. - Reactive monitoring: arm a trigger watcher on one page element and get notified on value changes, thresholds, deltas, or regex matches instead of polling manually. -### MCP tool surface (68 tools) +### MCP tool surface (69 tools) - Navigation and tabs: navigate, search, refresh, go_back, go_forward, back, open_tab, close_tab, switch_tab, list_tabs - Clicking and pointer: click, double_click, right_click, hover, click_at, double_click_at, click_and_hold, drag, drag_drop, drag_variable_speed - Text and keyboard: type_text, insert_text, clear_input, press_key, press_enter, select_text_range, focus diff --git a/showcase/angular/public/llms.txt b/showcase/angular/public/llms.txt index 5d2277cd6..a49c54789 100644 --- a/showcase/angular/public/llms.txt +++ b/showcase/angular/public/llms.txt @@ -1,7 +1,7 @@ - + # FSB (Full Self-Browsing) -FSB (Full Self-Browsing) is an open-source Chrome extension that automates the browser through natural language. You describe a task; FSB plans the clicks, types, and navigation to complete it. Multi-model AI (xAI, OpenAI, Anthropic, Gemini, OpenRouter, LM Studio, local), 60 browser actions, a 68-tool MCP surface, 142+ site guides, local-first execution, and BYO API keys. Current release: FSB v0.9.92 (extension) with fsb-mcp-server 0.11.2 (npm). +FSB (Full Self-Browsing) is an open-source Chrome extension that automates the browser through natural language. You describe a task; FSB plans the clicks, types, and navigation to complete it. Multi-model AI (xAI, OpenAI, Anthropic, Gemini, OpenRouter, LM Studio, local), 60 browser actions, a 69-tool MCP surface, 142+ site guides, local-first execution, and BYO API keys. Current release: FSB v0.9.92 (extension) with fsb-mcp-server 0.11.2 (npm). License: [MIT License](https://github.com/fullselfbrowsing/FSB/blob/main/LICENSE). @@ -30,7 +30,7 @@ The About page embeds the public 33-second FSB trailer (uploaded September 6, 20 ## Docs - [About](https://full-selfbrowsing.com/about): The official FSB trailer, an interactive knowledge graph of FSB's site intelligence, and a categorized library of the browser actions FSB exposes to agents. -- [Agents](https://full-selfbrowsing.com/agents): How always-on agents (Grok Bot, Muse, Dots) and MCP hosts such as OpenClaw, Hermes, Claude Code, Codex, and Cursor drive the user's real Chrome through FSB, the FSB MCP power story (68 MCP tools, multi-agent identity, vault boundary, real Chrome session), the OpenClaw and Hermes skill, ClawHub one-click install (`https://clawhub.ai/lakshmanturlapati/full-selfbrowsing`), a one-line `npx -y fsb-mcp-server@latest install` connect command, and grounded use cases. Autopilot (`run_task`) only fires on explicit user delegation; manual tool calls are default. +- [Agents](https://full-selfbrowsing.com/agents): How always-on agents (Grok Bot, Muse, Dots) and MCP hosts such as OpenClaw, Hermes, Claude Code, Codex, and Cursor drive the user's real Chrome through FSB, the FSB MCP power story (69 MCP tools, multi-agent identity, vault boundary, real Chrome session), the OpenClaw and Hermes skill, ClawHub one-click install (`https://clawhub.ai/lakshmanturlapati/full-selfbrowsing`), a one-line `npx -y fsb-mcp-server@latest install` connect command, and grounded use cases. Autopilot (`run_task`) only fires on explicit user delegation; manual tool calls are default. - [Support](https://full-selfbrowsing.com/support): Setup guides, MCP configuration, troubleshooting, GitHub issues, and direct contact. - [Release notes](https://full-selfbrowsing.com/release-notes): Every FSB release illustrated, from the 0.9.92 bridge recovery, at-most-once browser actions, and hung-page deadlines back to the first Chrome-extension prototype, with a sticky rail for switching between the fifteen published versions. - [Privacy](https://full-selfbrowsing.com/privacy): How FSB handles your data -- API keys encrypted in Chrome local storage, opt-out anonymous usage telemetry for aggregate public stats only, automation runs locally in your browser. diff --git a/showcase/angular/scripts/llms-full.source.md b/showcase/angular/scripts/llms-full.source.md index 22f116cb6..c1256e8be 100644 --- a/showcase/angular/scripts/llms-full.source.md +++ b/showcase/angular/scripts/llms-full.source.md @@ -4,7 +4,7 @@ The About page embeds the public 33-second FSB trailer (uploaded September 6, 20 ## 1. Project Description -FSB (Full Self-Browsing) is an open-source Chrome extension that automates the browser through natural language. You describe a task in plain English; FSB plans the clicks, types, and navigation to complete it. The extension runs in your browser with your own API keys, encrypted local storage, and opt-out anonymous usage telemetry for aggregate public stats only. Multi-model AI (xAI Grok, OpenAI, Anthropic Claude, Google Gemini, OpenRouter, LM Studio, custom OpenAI-compatible endpoints), 56 canonical browser tools, a 68-tool MCP surface, 142+ site-specific guides, reactive trigger watchers, real file uploads, and a 128-app native capability catalog for verified first-party API execution. Current release: FSB v0.9.92 (Chrome extension) with fsb-mcp-server 0.11.2 (npm MCP package). +FSB (Full Self-Browsing) is an open-source Chrome extension that automates the browser through natural language. You describe a task in plain English; FSB plans the clicks, types, and navigation to complete it. The extension runs in your browser with your own API keys, encrypted local storage, and opt-out anonymous usage telemetry for aggregate public stats only. Multi-model AI (xAI Grok, OpenAI, Anthropic Claude, Google Gemini, OpenRouter, LM Studio, custom OpenAI-compatible endpoints), 56 canonical browser tools, a 69-tool MCP surface, 142+ site-specific guides, reactive trigger watchers, real file uploads, and a 128-app native capability catalog for verified first-party API execution. Current release: FSB v0.9.92 (Chrome extension) with fsb-mcp-server 0.11.2 (npm MCP package). License: [MIT License](https://github.com/fullselfbrowsing/FSB/blob/main/LICENSE). @@ -40,7 +40,7 @@ The bring-your-own-key model is deliberate. The user picks the model -- frontier - Operator loop: every workflow can act on the page, observe what changed, verify the outcome, report errors, and recover when the web app shifts. - Reactive monitoring: arm a trigger watcher on one page element and get notified on value changes, thresholds, deltas, or regex matches instead of polling manually. -### MCP tool surface (68 tools) +### MCP tool surface (69 tools) - Navigation and tabs: navigate, search, refresh, go_back, go_forward, back, open_tab, close_tab, switch_tab, list_tabs - Clicking and pointer: click, double_click, right_click, hover, click_at, double_click_at, click_and_hold, drag, drag_drop, drag_variable_speed - Text and keyboard: type_text, insert_text, clear_input, press_key, press_enter, select_text_range, focus diff --git a/showcase/angular/scripts/llms.source.md b/showcase/angular/scripts/llms.source.md index 38016fd2c..46161b64a 100644 --- a/showcase/angular/scripts/llms.source.md +++ b/showcase/angular/scripts/llms.source.md @@ -1,6 +1,6 @@ # FSB (Full Self-Browsing) -FSB (Full Self-Browsing) is an open-source Chrome extension that automates the browser through natural language. You describe a task; FSB plans the clicks, types, and navigation to complete it. Multi-model AI (xAI, OpenAI, Anthropic, Gemini, OpenRouter, LM Studio, local), 60 browser actions, a 68-tool MCP surface, 142+ site guides, local-first execution, and BYO API keys. Current release: FSB v0.9.92 (extension) with fsb-mcp-server 0.11.2 (npm). +FSB (Full Self-Browsing) is an open-source Chrome extension that automates the browser through natural language. You describe a task; FSB plans the clicks, types, and navigation to complete it. Multi-model AI (xAI, OpenAI, Anthropic, Gemini, OpenRouter, LM Studio, local), 60 browser actions, a 69-tool MCP surface, 142+ site guides, local-first execution, and BYO API keys. Current release: FSB v0.9.92 (extension) with fsb-mcp-server 0.11.2 (npm). License: [MIT License](https://github.com/fullselfbrowsing/FSB/blob/main/LICENSE). @@ -29,7 +29,7 @@ The About page embeds the public 33-second FSB trailer (uploaded September 6, 20 ## Docs - [About](https://full-selfbrowsing.com/about): The official FSB trailer, an interactive knowledge graph of FSB's site intelligence, and a categorized library of the browser actions FSB exposes to agents. -- [Agents](https://full-selfbrowsing.com/agents): How always-on agents (Grok Bot, Muse, Dots) and MCP hosts such as OpenClaw, Hermes, Claude Code, Codex, and Cursor drive the user's real Chrome through FSB, the FSB MCP power story (68 MCP tools, multi-agent identity, vault boundary, real Chrome session), the OpenClaw and Hermes skill, ClawHub one-click install (`https://clawhub.ai/lakshmanturlapati/full-selfbrowsing`), a one-line `npx -y fsb-mcp-server@latest install` connect command, and grounded use cases. Autopilot (`run_task`) only fires on explicit user delegation; manual tool calls are default. +- [Agents](https://full-selfbrowsing.com/agents): How always-on agents (Grok Bot, Muse, Dots) and MCP hosts such as OpenClaw, Hermes, Claude Code, Codex, and Cursor drive the user's real Chrome through FSB, the FSB MCP power story (69 MCP tools, multi-agent identity, vault boundary, real Chrome session), the OpenClaw and Hermes skill, ClawHub one-click install (`https://clawhub.ai/lakshmanturlapati/full-selfbrowsing`), a one-line `npx -y fsb-mcp-server@latest install` connect command, and grounded use cases. Autopilot (`run_task`) only fires on explicit user delegation; manual tool calls are default. - [Support](https://full-selfbrowsing.com/support): Setup guides, MCP configuration, troubleshooting, GitHub issues, and direct contact. - [Release notes](https://full-selfbrowsing.com/release-notes): Every FSB release illustrated, from the 0.9.92 bridge recovery, at-most-once browser actions, and hung-page deadlines back to the first Chrome-extension prototype, with a sticky rail for switching between the fifteen published versions. - [Privacy](https://full-selfbrowsing.com/privacy): How FSB handles your data -- API keys encrypted in Chrome local storage, opt-out anonymous usage telemetry for aggregate public stats only, automation runs locally in your browser. diff --git a/showcase/angular/src/app/pages/about/about-page.component.html b/showcase/angular/src/app/pages/about/about-page.component.html index f50f4d9a0..6d403e4ce 100644 --- a/showcase/angular/src/app/pages/about/about-page.component.html +++ b/showcase/angular/src/app/pages/about/about-page.component.html @@ -99,7 +99,7 @@

Consolidated Memory

Action Library

-

60 browser actions organized by category, all executable through natural language, and surfaced to agents as part of the 68-tool MCP catalog.

+

60 browser actions organized by category, all executable through natural language, and surfaced to agents as part of the 69-tool MCP catalog.

diff --git a/showcase/angular/src/app/pages/agents/agents-page.component.html b/showcase/angular/src/app/pages/agents/agents-page.component.html index 8c09facda..2f54017be 100644 --- a/showcase/angular/src/app/pages/agents/agents-page.component.html +++ b/showcase/angular/src/app/pages/agents/agents-page.component.html @@ -89,7 +89,7 @@

{{ agent.name }}

}
- via MCP · 68 tools + via MCP · 69 tools
} @@ -119,7 +119,7 @@

Why FSB MCP

-
68 tools
+
69 tools

One surface, no glue code

Read, click, type, navigate, vault, observe. All typed. All agent-scoped.

diff --git a/showcase/angular/src/app/pages/agents/agents-page.component.ts b/showcase/angular/src/app/pages/agents/agents-page.component.ts index d9bd8e9ec..339c63d90 100644 --- a/showcase/angular/src/app/pages/agents/agents-page.component.ts +++ b/showcase/angular/src/app/pages/agents/agents-page.component.ts @@ -293,7 +293,7 @@ export class AgentsPageComponent implements OnInit, OnDestroy { url: `${HOST}/agents`, description: $localize`:@@agents.schema.software.description:Canonical OpenClaw and Hermes onboarding path for FSB. Doctor flow, stdio config printer, consent-gated multi-host installer, and 68 MCP browser tools for the FSB MCP server.`, featureList: [ - $localize`:@@agents.schema.software.feature.tools:68 MCP tools for browser action, observation, verification, and recovery`, + $localize`:@@agents.schema.software.feature.tools:69 MCP tools for browser action, observation, verification, and recovery`, $localize`:@@agents.schema.software.feature.manual:Manual mode by default; run_task autopilot only on explicit delegation`, $localize`:@@agents.schema.software.feature.vault:Vault tools fill credentials and payment methods without exposing raw secrets`, $localize`:@@agents.schema.software.feature.triggers:Trigger watcher tools for reactive page monitoring`, diff --git a/showcase/angular/src/app/pages/dashboard/dashboard-page.component.html b/showcase/angular/src/app/pages/dashboard/dashboard-page.component.html index 663bd1228..d2fe0d76b 100644 --- a/showcase/angular/src/app/pages/dashboard/dashboard-page.component.html +++ b/showcase/angular/src/app/pages/dashboard/dashboard-page.component.html @@ -273,7 +273,6 @@

FSB Dashboard

-

Background agents have moved.

diff --git a/showcase/angular/src/app/pages/dashboard/dashboard-page.component.ts b/showcase/angular/src/app/pages/dashboard/dashboard-page.component.ts index 931cba0d5..7a66dd027 100644 --- a/showcase/angular/src/app/pages/dashboard/dashboard-page.component.ts +++ b/showcase/angular/src/app/pages/dashboard/dashboard-page.component.ts @@ -413,14 +413,6 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy private lzLibraryPromise: Promise | null = null; private qrStartAttempt = 0; private qrStartPending = false; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // private agents: any[] = []; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // private stats: any = {}; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // private selectedAgentId: string | null = null; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // private pollTimer: any = null; private ws: WebSocket | null = null; private wsReconnectDelay = 0; private readonly wsMaxReconnectDelay = 30000; @@ -436,26 +428,6 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy private lastProgressAction = ''; private taskTimeoutTimer: any = null; - // ---- Agent management state ---- - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // private detailAgentId: string | null = null; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // private detailRunsOffset = 0; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // private readonly detailRunsLimit = 10; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // private modalMode: 'create' | 'edit' | null = null; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // private modalAgentId: string | null = null; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // private deleteAgentId: string | null = null; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // private deleteAgentName = ''; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // private saveAgentScheduleType = 'interval'; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // private agentRunningId: string | null = null; - // ---- DOM preview state ---- private previewState: PreviewState = 'hidden'; private previewLayoutMode: PreviewLayoutMode = 'inline'; @@ -522,14 +494,8 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy private keyInput!: HTMLInputElement | null; private connectBtn!: HTMLElement | null; private disconnectBtn!: HTMLElement | null; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // private agentCountEl!: HTMLElement | null; private sseStatusEl!: HTMLElement | null; private wakeBtn!: HTMLElement | null; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // private agentGrid!: HTMLElement | null; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // private emptyState!: HTMLElement | null; private tabScan!: HTMLElement | null; private tabPaste!: HTMLElement | null; private tabScanContent!: HTMLElement | null; @@ -611,60 +577,6 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy private actionFeed!: HTMLElement | null; private readonly ACTION_FEED_MAX = 15; - // Agent management DOM refs - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // private newAgentBtn!: HTMLElement | null; - // private agentContainer!: HTMLElement | null; - // private detailPanel!: HTMLElement | null; - // private detailClose!: HTMLElement | null; - // private detailRunNow!: HTMLElement | null; - // private detailEdit!: HTMLElement | null; - // private detailDelete!: HTMLElement | null; - // private detailName!: HTMLElement | null; - // private detailTask!: HTMLElement | null; - // private detailUrl!: HTMLElement | null; - // private detailSchedule!: HTMLElement | null; - // private detailReplayRuns!: HTMLElement | null; - // private detailAiFallback!: HTMLElement | null; - // private detailTokensSaved!: HTMLElement | null; - // private detailCostSaved!: HTMLElement | null; - // private detailRunProgress!: HTMLElement | null; - // private detailRunBar!: HTMLElement | null; - // private detailRunAction!: HTMLElement | null; - // private detailRunsList!: HTMLElement | null; - // private detailRunsPagination!: HTMLElement | null; - // private detailScriptToggle!: HTMLElement | null; - // private detailScriptContent!: HTMLElement | null; - // private detailScriptList!: HTMLElement | null; - // private detailScriptChevron!: HTMLElement | null; -// - // Modal DOM refs - // private modalOverlay!: HTMLElement | null; - // private modalTitle!: HTMLElement | null; - // private modalClose!: HTMLElement | null; - // private modalName!: HTMLInputElement | null; - // private modalTask!: HTMLTextAreaElement | null; - // private modalUrl!: HTMLInputElement | null; - // private modalScheduleType!: HTMLElement | null; - // private modalScheduleConfig!: HTMLElement | null; - // private modalDiscard!: HTMLElement | null; - // private modalSave!: HTMLElement | null; -// - // Delete dialog DOM refs - // private deleteOverlay!: HTMLElement | null; - // private deleteTitle!: HTMLElement | null; - // private deleteCancel!: HTMLElement | null; - // private deleteConfirm!: HTMLElement | null; -// - // Save-as-Agent DOM refs - // private saveAgentSection!: HTMLElement | null; - // private saveAgentTrigger!: HTMLElement | null; - // private saveAgentFields!: HTMLElement | null; - // private saveAgentNameEl!: HTMLInputElement | null; - // private saveAgentUrlEl!: HTMLInputElement | null; - // private saveAgentBtn!: HTMLElement | null; - // private saveAgentScheduleConfig!: HTMLElement | null; -// // Cleanup tracking private destroyed = false; private readonly boundHandlers: Array<{ el: EventTarget; event: string; handler: EventListener; options?: any }> = []; @@ -688,8 +600,6 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy this.disconnectWS(); // Stop polling - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.stopPolling(); // Stop QR scanner this.stopQRScanner(); @@ -732,14 +642,8 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy this.keyInput = this.el('dash-key-input') as HTMLInputElement | null; this.connectBtn = this.el('dash-connect-btn'); this.disconnectBtn = this.el('dash-disconnect-btn'); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.agentCountEl = this.el('dash-agent-count'); this.sseStatusEl = this.el('dash-sse-status'); this.wakeBtn = this.el('dash-wake-btn'); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.agentGrid = this.el('dash-agent-grid'); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.emptyState = this.el('dash-empty'); this.tabScan = this.el('dash-tab-scan'); this.tabPaste = this.el('dash-tab-paste'); this.tabScanContent = this.el('tab-scan'); @@ -815,55 +719,6 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy this.previewRestrictedUrl = this.el('dash-preview-restricted-url'); this.actionFeed = this.el('dash-action-feed'); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.newAgentBtn = this.el('dash-new-agent-btn'); - // this.agentContainer = this.el('dash-agent-container'); - // this.detailPanel = this.el('dash-agent-detail'); - // this.detailClose = this.el('dash-detail-close'); - // this.detailRunNow = this.el('dash-detail-run-now'); - // this.detailEdit = this.el('dash-detail-edit'); - // this.detailDelete = this.el('dash-detail-delete'); - // this.detailName = this.el('dash-detail-name'); - // this.detailTask = this.el('dash-detail-task'); - // this.detailUrl = this.el('dash-detail-url'); - // this.detailSchedule = this.el('dash-detail-schedule'); - // this.detailReplayRuns = this.el('dash-detail-replay-runs'); - // this.detailAiFallback = this.el('dash-detail-ai-fallback'); - // this.detailTokensSaved = this.el('dash-detail-tokens-saved'); - // this.detailCostSaved = this.el('dash-detail-cost-saved'); - // this.detailRunProgress = this.el('dash-detail-run-progress'); - // this.detailRunBar = this.el('dash-detail-run-bar'); - // this.detailRunAction = this.el('dash-detail-run-action'); - // this.detailRunsList = this.el('dash-detail-runs'); - // this.detailRunsPagination = this.el('dash-detail-runs-pagination'); - // this.detailScriptToggle = this.el('dash-detail-script-toggle'); - // this.detailScriptContent = this.el('dash-detail-script-content'); - // this.detailScriptList = this.el('dash-detail-script-list'); - // this.detailScriptChevron = this.el('dash-detail-script-chevron'); -// - // this.modalOverlay = this.el('dash-agent-modal-overlay'); - // this.modalTitle = this.el('dash-modal-title'); - // this.modalClose = this.el('dash-modal-close'); - // this.modalName = this.el('dash-modal-name') as HTMLInputElement | null; - // this.modalTask = this.el('dash-modal-task') as HTMLTextAreaElement | null; - // this.modalUrl = this.el('dash-modal-url') as HTMLInputElement | null; - // this.modalScheduleType = this.el('dash-modal-schedule-type'); - // this.modalScheduleConfig = this.el('dash-modal-schedule-config'); - // this.modalDiscard = this.el('dash-modal-discard'); - // this.modalSave = this.el('dash-modal-save'); -// - // this.deleteOverlay = this.el('dash-delete-overlay'); - // this.deleteTitle = this.el('dash-delete-title'); - // this.deleteCancel = this.el('dash-delete-cancel'); - // this.deleteConfirm = this.el('dash-delete-confirm'); -// - // this.saveAgentSection = this.el('dash-task-save-agent'); - // this.saveAgentTrigger = this.el('dash-save-agent-trigger'); - // this.saveAgentFields = this.el('dash-save-agent-fields'); - // this.saveAgentNameEl = this.el('dash-save-agent-name') as HTMLInputElement | null; - // this.saveAgentUrlEl = this.el('dash-save-agent-url') as HTMLInputElement | null; - // this.saveAgentBtn = this.el('dash-save-agent-btn'); - // this.saveAgentScheduleConfig = this.el('dash-save-agent-schedule-config'); } // ==================== EVENT LISTENERS ==================== @@ -919,99 +774,6 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy this.handleRemoteControlToggleClick(); }); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // Detail panel listeners - // this.listen(this.detailClose, 'click', () => this.closeDetailPanel()); - // this.listen(this.detailRunNow, 'click', () => { - // if (this.detailAgentId) this.runAgentNow(this.detailAgentId); - // }); - // this.listen(this.detailEdit, 'click', () => { - // if (this.detailAgentId) this.openAgentModal('edit', this.detailAgentId); - // }); - // this.listen(this.detailDelete, 'click', () => { - // if (this.detailAgentId) { - // const agent = this.agents.find(a => a.agent_id === this.detailAgentId); - // this.openDeleteDialog(this.detailAgentId, agent ? agent.name : this.detailAgentId); - // } - // }); -// - // Recorded script toggle - // this.listen(this.detailScriptToggle, 'click', () => { - // if (!this.detailScriptToggle) return; - // const isExpanded = this.detailScriptToggle.classList.contains('expanded'); - // this.detailScriptToggle.classList.toggle('expanded'); - // if (this.detailScriptContent) this.detailScriptContent.style.display = isExpanded ? 'none' : 'block'; - // }); - - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // New Agent button - // this.listen(this.newAgentBtn, 'click', () => this.openAgentModal('create')); - - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // Modal listeners - // this.listen(this.modalClose, 'click', () => this.closeAgentModal()); - // this.listen(this.modalDiscard, 'click', () => this.closeAgentModal()); - // this.listen(this.modalSave, 'click', () => this.saveAgentFromModal()); - // this.listen(this.modalOverlay, 'click', (e: Event) => { - // if (e.target === this.modalOverlay) this.closeAgentModal(); - // }); -// - // Escape key closes modal/delete dialog/maximized - // this.listen(document, 'keydown', (e: Event) => { - // const key = (e as KeyboardEvent).key; - // if (key === 'Escape') { - // if (this.modalOverlay && this.modalOverlay.style.display !== 'none') { - // this.closeAgentModal(); - // } else if (this.deleteOverlay && this.deleteOverlay.style.display !== 'none') { - // this.closeDeleteDialog(); - // } else if (this.previewLayoutMode === 'maximized') { - // this.setPreviewLayout('inline'); - // } - // } - // }); -// - // Schedule type pill handlers (modal) - // this.listen(this.modalScheduleType, 'click', (e: Event) => { - // const pill = (e.target as HTMLElement).closest('.dash-schedule-pill'); - // if (!pill) return; - // this.modalScheduleType?.querySelectorAll('.dash-schedule-pill').forEach(p => p.classList.remove('active')); - // pill.classList.add('active'); - // this.renderScheduleConfig(this.modalScheduleConfig, pill.getAttribute('data-type') || 'interval', '{}'); - // }); -// - // Delete dialog listeners - // this.listen(this.deleteCancel, 'click', () => this.closeDeleteDialog()); - // this.listen(this.deleteConfirm, 'click', () => this.confirmDeleteAgent()); - // this.listen(this.deleteOverlay, 'click', (e: Event) => { - // if (e.target === this.deleteOverlay) this.closeDeleteDialog(); - // }); -// - // Save-as-Agent listeners - // this.listen(this.saveAgentTrigger, 'click', () => { - // if (!this.saveAgentTrigger) return; - // const isExpanded = this.saveAgentTrigger.classList.contains('expanded'); - // this.saveAgentTrigger.classList.toggle('expanded'); - // if (this.saveAgentFields) { - // if (isExpanded) { - // this.saveAgentFields.classList.remove('dash-save-expanded'); - // this.saveAgentFields.style.display = 'none'; - // } else { - // this.saveAgentFields.style.display = 'flex'; - // this.saveAgentFields.classList.add('dash-save-expanded'); - // } - // } - // }); -// - // this.listen(this.saveAgentSection, 'click', (e: Event) => { - // const pill = (e.target as HTMLElement).closest('.dash-schedule-pill'); - // if (!pill) return; - // this.saveAgentSection?.querySelectorAll('.dash-schedule-pill').forEach(p => p.classList.remove('active')); - // pill.classList.add('active'); - // this.renderScheduleConfig(this.saveAgentScheduleConfig, pill.getAttribute('data-type') || 'interval', '{}'); - // }); -// - // this.listen(this.saveAgentBtn, 'click', () => this.submitSaveAsAgent()); -// // Wake Extension button this.listen(this.wakeBtn, 'click', () => { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return; @@ -1984,8 +1746,6 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy if (this.taskInput) { this.taskInput.value = ''; this.taskInput.disabled = false; } if (this.taskSubmitBtn) (this.taskSubmitBtn as HTMLButtonElement).disabled = false; if (this.taskBarFill) { this.taskBarFill.style.width = '0%'; this.taskBarFill.className = 'dash-task-bar-fill'; } - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.hideSaveAsAgent(); if (this.previewContainer) this.previewContainer.classList.remove('dash-preview-automating'); this.renderTaskRecoveryStatus('', ''); break; @@ -2012,8 +1772,6 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy } }, this.TASK_TIMEOUT_MS); this.disableAllTaskInputs(true); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.hideSaveAsAgent(); if (this.previewContainer) this.previewContainer.classList.add('dash-preview-automating'); this.renderTaskRecoveryStatus(this.activeTaskRunId || '', this.taskRecoverySource); break; @@ -2034,8 +1792,6 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy } this.disableAllTaskInputs(false); if (this.taskInputNext) this.taskInputNext.value = ''; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.showSaveAsAgent(); if (this.previewContainer) this.previewContainer.classList.remove('dash-preview-automating'); this.renderTaskRecoveryStatus('', ''); break; @@ -2287,11 +2043,7 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy localStorage.setItem(this.STORAGE_KEY, key); this.clearSessionStorage(); this.showDashboard(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.loadData(); this.connectWS(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.startPolling(); } else { this.showLoginError(this.dashboardCopy.invalidHashKey); } @@ -2308,21 +2060,13 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy this.validateKey(key).then(result => { if (result.valid) { this.showDashboard(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.loadData(); this.connectWS(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.startPolling(); } else { localStorage.removeItem(this.STORAGE_KEY); this.hashKey = ''; } }).catch(() => { this.showDashboard(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.loadData(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.startPolling(); }); } @@ -2336,11 +2080,6 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy this.hashKey = ''; localStorage.removeItem(this.STORAGE_KEY); this.clearSessionStorage(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.agents = []; - // this.stats = {}; - // this.selectedAgentId = null; - // this.stopPolling(); this.disconnectWS(); this.stopQRScanner(); this.showLogin(); @@ -2360,11 +2099,7 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy this.hashKey = result.hashKey; localStorage.setItem(this.STORAGE_KEY, this.hashKey); this.showDashboard(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.loadData(); this.connectWS(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.startPolling(); } else { this.clearSessionStorage(); if (result.reason === 'expired') { @@ -2376,11 +2111,7 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy }).catch(() => { if (this.hashKey) { this.showDashboard(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.loadData(); this.connectWS(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.startPolling(); } }); } @@ -2617,11 +2348,7 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy paired = true; this.storeSession(result.hashKey, result.sessionToken, result.expiresAt); this.showDashboard(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.loadData(); this.connectWS(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.startPolling(); }).catch((err: Error & { localizedMessage?: string }) => { if (paired) return; this.failScan(err?.localizedMessage || this.dashboardCopy.qrExchangeFailed); @@ -2657,562 +2384,7 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy this.showScanError(message); void this.startQRScanner(); } -// - // ==================== DATA LOADING ==================== -// - // private loadData(): void { - // this.fetchStats(); - // this.fetchAgents(); - // } -// - // private fetchStats(): void { - // this.apiFetch('/api/stats', { headers: { 'X-FSB-Hash-Key': this.hashKey } }) - // .then(data => { this.stats = data; this.renderStats(); }) - // .catch(() => {}); - // } -// - // private fetchAgents(): void { - // this.apiFetch('/api/agents', { headers: { 'X-FSB-Hash-Key': this.hashKey } }) - // .then(data => { this.agents = data.agents || []; this.renderAgents(); }) - // .catch(() => {}); - // } -// - // private fetchRuns(agentId: string, limit: number, offset: number): Promise { - // const url = '/api/agents/' + encodeURIComponent(agentId) + '/runs?limit=' + limit + '&offset=' + offset; - // return this.apiFetch(url, { headers: { 'X-FSB-Hash-Key': this.hashKey } }); - // } -// - // ==================== RENDERING ==================== -// - // private renderStats(): void { - // this.setTextById('stat-agents', String(this.stats.totalAgents || 0)); - // this.setTextById('stat-enabled', String(this.stats.enabledAgents || 0)); - // this.setTextById('stat-runs-today', String(this.stats.runsToday || 0)); - // this.setTextById('stat-success-rate', (this.stats.successRate || 0) + '%'); - // this.setTextById('stat-total-cost', '$' + (this.stats.totalCost || 0).toFixed(2)); - // this.setTextById('stat-cost-saved', '$' + (this.stats.totalCostSaved || 0).toFixed(2)); - // const countText = (this.stats.totalAgents || 0) + ' agent' + ((this.stats.totalAgents || 0) !== 1 ? 's' : ''); - // if (this.agentCountEl) this.agentCountEl.textContent = countText + (this.extensionOnline ? '' : ' - extension offline'); - // } -// - // private renderAgents(): void { - // if (!this.agentGrid) return; - // this.agentGrid.innerHTML = ''; -// - // if (this.agents.length === 0) { - // if (this.emptyState) this.emptyState.style.display = 'block'; - // this.agentGrid.style.display = 'none'; - // return; - // } -// - // if (this.emptyState) this.emptyState.style.display = 'none'; - // this.agentGrid.style.display = ''; -// - // this.agents.forEach(agent => { - // const card = document.createElement('div'); - // const isEnabled = agent.enabled === true || agent.enabled === 1; - // const isSelected = this.detailAgentId === agent.agent_id; - // card.className = 'dash-agent-card' + (isSelected ? ' selected' : '') + (!isEnabled ? ' dash-agent-disabled' : ''); - // card.setAttribute('data-agent-id', agent.agent_id); - // card.setAttribute('role', 'button'); - // card.setAttribute('aria-expanded', isSelected ? 'true' : 'false'); -// - // const scheduleLabel = this.formatScheduleLabel(agent.schedule_type, agent.schedule_config); - // const successCount = agent.successful_runs || 0; - // const totalRuns = agent.total_runs || 0; - // const successRateText = totalRuns > 0 ? successCount + '/' + totalRuns : '0/0'; - // const successPercent = totalRuns > 0 ? (successCount / totalRuns) * 100 : 100; - // const rateColor = successPercent > 80 ? '#22c55e' : successPercent >= 50 ? '#eab308' : '#ef4444'; - // const costSaved = agent.cost_saved || 0; - // const costText = '$' + costSaved.toFixed(2); - // const lastRunText = agent.last_run_at ? this.formatTimeAgo(agent.last_run_at) : 'Never'; - // const runningIcon = this.agentRunningId === agent.agent_id ? ' ' : ''; -// - // card.innerHTML = - // '
' + - // '
' + - // '' + - // this.escapeHtml(agent.name) + runningIcon + - // '
' + - // '' + - // '
' + - // '
' + this.escapeHtml(agent.task) + '
' + - // '
' + this.escapeHtml(agent.target_url || '') + '
' + - // '
' + - // '' + this.escapeHtml(scheduleLabel) + '' + - // '' + this.escapeHtml(lastRunText) + '' + - // '
' + - // '
' + - // '' + successRateText + '' + - // '' + costText + '' + - // '
'; -// - // card.addEventListener('click', (e: Event) => { - // if ((e.target as HTMLElement).closest('.dash-toggle')) return; - // this.openDetailPanel(agent.agent_id); - // }); -// - // const toggle = card.querySelector('.dash-toggle'); - // if (toggle) { - // toggle.addEventListener('click', (e: Event) => { - // e.stopPropagation(); - // this.toggleAgent(agent.agent_id, !isEnabled); - // }); - // } -// - // this.agentGrid!.appendChild(card); - // }); - // } -// - // ==================== TOGGLE AGENT ==================== - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// - // private toggleAgent(agentId: string, enabled: boolean): void { - // this.agents = this.agents.map(a => { - // if (a.agent_id === agentId) a.enabled = enabled ? 1 : 0; - // return a; - // }); - // this.renderAgents(); -// - // this.apiFetch('/api/agents/' + encodeURIComponent(agentId), { - // method: 'PATCH', - // headers: { 'Content-Type': 'application/json', 'X-FSB-Hash-Key': this.hashKey }, - // body: JSON.stringify({ enabled }), - // }).catch(() => { - // this.agents = this.agents.map(a => { - // if (a.agent_id === agentId) a.enabled = enabled ? 0 : 1; - // return a; - // }); - // this.renderAgents(); - // }); - // } -// - // ==================== DETAIL PANEL ==================== -// - // private openDetailPanel(agentId: string): void { - // const agent = this.agents.find(a => a.agent_id === agentId); - // if (!agent) return; -// - // this.detailAgentId = agentId; - // this.selectedAgentId = agentId; - // this.detailRunsOffset = 0; -// - // const cards = this.agentGrid?.querySelectorAll('.dash-agent-card'); - // cards?.forEach(c => { - // const isThis = c.getAttribute('data-agent-id') === agentId; - // c.classList.toggle('selected', isThis); - // c.setAttribute('aria-expanded', isThis ? 'true' : 'false'); - // }); -// - // if (this.detailName) this.detailName.textContent = agent.name; - // if (this.detailTask) this.detailTask.textContent = agent.task; - // if (this.detailUrl) this.detailUrl.textContent = agent.target_url || ''; - // if (this.detailSchedule) this.detailSchedule.textContent = this.formatScheduleLabel(agent.schedule_type, agent.schedule_config); -// - // if (this.detailPanel) this.detailPanel.style.display = 'block'; - // if (this.agentContainer) this.agentContainer.classList.add('dash-detail-open'); -// - // this.loadAgentStats(agentId); - // this.loadDetailRuns(agentId, 0); - // this.loadRecordedScript(agent); -// - // if (this.detailRunProgress) this.detailRunProgress.style.display = 'none'; - // } -// - // private closeDetailPanel(): void { - // this.detailAgentId = null; - // this.selectedAgentId = null; - // if (this.detailPanel) this.detailPanel.style.display = 'none'; - // if (this.agentContainer) this.agentContainer.classList.remove('dash-detail-open'); - // this.agentGrid?.querySelectorAll('.dash-agent-card').forEach(c => { - // c.classList.remove('selected'); - // c.setAttribute('aria-expanded', 'false'); - // }); - // } -// - // private loadAgentStats(agentId: string): void { - // this.apiFetch('/api/agents/' + encodeURIComponent(agentId) + '/stats', { - // headers: { 'X-FSB-Hash-Key': this.hashKey }, - // }).then(data => { - // if (this.detailReplayRuns) this.detailReplayRuns.textContent = String(data.replayRuns || 0); - // if (this.detailAiFallback) this.detailAiFallback.textContent = String(data.aiFallbackRuns || 0); - // if (this.detailTokensSaved) this.detailTokensSaved.textContent = this.formatNumber(data.tokensSaved || 0); - // if (this.detailCostSaved) this.detailCostSaved.textContent = '$' + (data.costSaved || 0).toFixed(2); - // }).catch(() => {}); - // } -// - // private loadDetailRuns(agentId: string, offset: number): void { - // if (!this.detailRunsList) return; - // this.detailRunsList.innerHTML = '
'; - // this.fetchRuns(agentId, this.detailRunsLimit, offset).then(data => { - // this.renderDetailRuns(data.runs || [], data.total || 0, data.limit || this.detailRunsLimit, data.offset || 0); - // }).catch(() => { - // if (this.detailRunsList) this.detailRunsList.innerHTML = '

Failed to load runs.

'; - // }); - // } -// - // private renderDetailRuns(runs: any[], total: number, limit: number, offset: number): void { - // if (!this.detailRunsList) return; - // this.detailRunsList.innerHTML = ''; -// - // if (runs.length === 0) { - // this.detailRunsList.innerHTML = '

No runs yet. Tap Run Now to test this agent.

'; - // if (this.detailRunsPagination) this.detailRunsPagination.innerHTML = ''; - // return; - // } -// - // runs.forEach(run => { - // const entry = document.createElement('div'); - // entry.className = 'dash-run-entry'; - // const time = this.formatTime(run.completed_at); - // const statusClass = run.status === 'success' ? 'dash-run-status-success' : - // run.status === 'failed' ? 'dash-run-status-failed' : 'dash-run-status-unknown'; - // const statusSr = run.status === 'success' ? 'Status: success' : run.status === 'failed' ? 'Status: failed' : 'Status: unknown'; - // const modeBadge = this.renderModeBadge(run.execution_mode); - // const resultText = run.error || run.result || '-'; - // const duration = run.duration_ms ? this.formatDuration(run.duration_ms) : '-'; - // const costStr = run.cost_saved && run.cost_saved > 0 ? '-$' + run.cost_saved.toFixed(4) : - // run.cost_usd ? '$' + run.cost_usd.toFixed(4) : '-'; - // entry.innerHTML = - // '
' + time + '
' + - // '
' + statusSr + '' + this.escapeHtml(run.status) + '
' + - // '
' + modeBadge + '
' + - // '
' + this.escapeHtml(resultText) + '
' + - // '
' + duration + '
' + - // '
' + costStr + '
'; - // this.detailRunsList!.appendChild(entry); - // }); -// - // if (this.detailRunsPagination) { - // this.detailRunsPagination.innerHTML = ''; - // if (total > limit) { - // const prevBtn = document.createElement('button'); - // prevBtn.textContent = 'Previous'; - // prevBtn.disabled = offset === 0; - // prevBtn.addEventListener('click', () => this.loadDetailRuns(this.detailAgentId!, Math.max(0, offset - limit))); - // const nextBtn = document.createElement('button'); - // nextBtn.textContent = 'Next'; - // nextBtn.disabled = (offset + limit) >= total; - // nextBtn.addEventListener('click', () => this.loadDetailRuns(this.detailAgentId!, offset + limit)); - // const info = document.createElement('span'); - // info.className = 'text-muted text-sm'; - // info.style.padding = '6px 8px'; - // info.textContent = (offset + 1) + '-' + Math.min(offset + limit, total) + ' of ' + total; - // this.detailRunsPagination.appendChild(prevBtn); - // this.detailRunsPagination.appendChild(info); - // this.detailRunsPagination.appendChild(nextBtn); - // } - // } - // } -// - // private loadRecordedScript(agent: any): void { - // if (!this.detailScriptList) return; - // this.detailScriptList.innerHTML = ''; - // if (this.detailScriptContent) this.detailScriptContent.style.display = 'none'; - // if (this.detailScriptToggle) this.detailScriptToggle.classList.remove('expanded'); -// - // const script = agent.recorded_script || agent.recordedScript; - // if (!script || !Array.isArray(script) || script.length === 0) { - // this.detailScriptList.innerHTML = '
  • No recorded script available
  • '; - // return; - // } - // script.forEach((step: any) => { - // const li = document.createElement('li'); - // li.textContent = typeof step === 'string' ? step : (step.action || step.description || JSON.stringify(step)); - // this.detailScriptList!.appendChild(li); - // }); - // } -// - // ==================== RUN NOW ==================== -// - // private runAgentNow(agentId: string): void { - // if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return; - // if (!this.extensionOnline) return; -// - // this.agentRunningId = agentId; - // this.renderAgents(); -// - // if (this.detailRunProgress) this.detailRunProgress.style.display = 'block'; - // if (this.detailRunBar) { this.detailRunBar.style.width = '0%'; this.detailRunBar.className = 'dash-task-bar-fill'; } - // if (this.detailRunAction) this.detailRunAction.textContent = 'Starting...'; - // if (this.detailRunNow) { (this.detailRunNow as HTMLButtonElement).disabled = true; this.detailRunNow.innerHTML = ' Running'; } -// - // this.ws.send(JSON.stringify({ - // type: 'dash:agent-run-now', - // payload: { agentId }, - // ts: Date.now(), - // })); - // } -// - // ==================== AGENT MODAL ==================== -// - // private openAgentModal(mode: 'create' | 'edit', agentId?: string): void { - // this.modalMode = mode; - // this.modalAgentId = agentId || null; - // if (this.modalTitle) this.modalTitle.textContent = mode === 'edit' ? 'Edit Agent' : 'New Agent'; - // if (this.modalSave) { this.modalSave.textContent = 'Save Agent'; (this.modalSave as HTMLButtonElement).disabled = false; } -// - // if (mode === 'edit' && agentId) { - // const agent = this.agents.find(a => a.agent_id === agentId); - // if (agent) { - // if (this.modalName) this.modalName.value = agent.name || ''; - // if (this.modalTask) this.modalTask.value = agent.task || ''; - // if (this.modalUrl) this.modalUrl.value = agent.target_url || ''; - // this.setModalScheduleType(agent.schedule_type || 'interval', agent.schedule_config); - // } - // } else { - // if (this.modalName) this.modalName.value = ''; - // if (this.modalTask) this.modalTask.value = ''; - // if (this.modalUrl) this.modalUrl.value = ''; - // this.setModalScheduleType('interval', '{}'); - // } -// - // if (this.modalOverlay) this.modalOverlay.style.display = 'flex'; - // if (this.modalName) this.modalName.focus(); - // } -// - // private closeAgentModal(): void { - // if (this.modalOverlay) this.modalOverlay.style.display = 'none'; - // this.modalMode = null; - // this.modalAgentId = null; - // this.clearModalErrors(); - // } -// - // private clearModalErrors(): void { - // const container = this.modalOverlay || this.host.nativeElement; - // container.querySelectorAll('.dash-field-error').forEach((e: Element) => e.remove()); - // container.querySelectorAll('.dash-input-error').forEach((e: Element) => e.classList.remove('dash-input-error')); - // } -// - // private saveAgentFromModal(): void { - // this.clearModalErrors(); - // const name = this.modalName?.value.trim() || ''; - // const task = this.modalTask?.value.trim() || ''; - // const url = this.modalUrl?.value.trim() || ''; -// - // let valid = true; - // if (!name) { this.showFieldError(this.modalName, 'Name is required'); valid = false; } - // if (!task) { this.showFieldError(this.modalTask, 'Task description is required'); valid = false; } - // if (!url) { this.showFieldError(this.modalUrl, 'Target URL is required'); valid = false; } - // if (!valid) return; -// - // const scheduleType = this.getActiveScheduleType(this.modalScheduleType); - // const scheduleConfig = this.getScheduleConfig(this.modalScheduleConfig, scheduleType); -// - // if (this.modalSave) { (this.modalSave as HTMLButtonElement).disabled = true; this.modalSave.innerHTML = ' Saving...'; } -// - // const agentId = this.modalMode === 'edit' ? this.modalAgentId! : - // 'agent_' + Date.now().toString(36) + '_' + Math.random().toString(36).substring(2, 8); -// - // this.apiFetch('/api/agents', { - // method: 'POST', - // headers: { 'Content-Type': 'application/json', 'X-FSB-Hash-Key': this.hashKey }, - // body: JSON.stringify({ - // agentId, name, task, targetUrl: url, - // scheduleType, scheduleConfig: JSON.stringify(scheduleConfig), enabled: true, - // }), - // }).then(() => { - // this.closeAgentModal(); - // this.loadData(); - // setTimeout(() => { - // const newCard = this.agentGrid?.querySelector('[data-agent-id="' + agentId + '"]'); - // if (newCard) { - // newCard.classList.add('dash-agent-card-highlight'); - // setTimeout(() => newCard.classList.remove('dash-agent-card-highlight'), 1100); - // } - // }, 200); - // }).catch(err => { - // if (this.modalSave) { (this.modalSave as HTMLButtonElement).disabled = false; this.modalSave.textContent = 'Save Agent'; } - // const msg = err?.error || 'Couldn\'t create agent. Check your connection and try again.'; - // this.showFieldError(this.modalUrl, msg); - // }); - // } -// - // private showFieldError(inputEl: HTMLElement | null, msg: string): void { - // if (!inputEl) return; - // inputEl.classList.add('dash-input-error'); - // const errEl = document.createElement('div'); - // errEl.className = 'dash-field-error'; - // errEl.textContent = msg; - // inputEl.parentNode?.appendChild(errEl); - // } -// - // ==================== SCHEDULE CONFIGURATION ==================== -// - // private setModalScheduleType(type: string, configStr: any): void { - // const pills = (this.modalScheduleType || this.host.nativeElement).querySelectorAll('.dash-schedule-pill'); - // pills.forEach((p: Element) => p.classList.toggle('active', p.getAttribute('data-type') === type)); - // this.renderScheduleConfig(this.modalScheduleConfig, type, configStr); - // } -// - // private renderScheduleConfig(container: HTMLElement | null, type: string, configStr: any): void { - // if (!container) return; - // let config: any = {}; - // try { config = typeof configStr === 'string' ? JSON.parse(configStr) : (configStr || {}); } catch (_) {} -// - // if (type === 'interval') { - // const mins = config.intervalMinutes || 60; - // container.innerHTML = - // '
    ' + - // 'Every' + - // '' + - // 'minutes' + - // '
    '; - // const input = container.querySelector('input'); - // if (input) { - // input.addEventListener('blur', () => { - // if (parseInt(input.value) < 5) { - // input.value = '5'; - // let msgEl = container.querySelector('.dash-schedule-snap-msg'); - // if (!msgEl) { - // msgEl = document.createElement('div'); - // msgEl.className = 'dash-schedule-snap-msg'; - // msgEl.textContent = 'Minimum 5 minutes'; - // container.appendChild(msgEl); - // setTimeout(() => { (msgEl as HTMLElement).style.opacity = '0'; }, 100); - // setTimeout(() => { if (msgEl?.parentNode) msgEl.remove(); }, 2100); - // } - // } - // }); - // } - // } else if (type === 'daily') { - // const time = config.dailyTime || '08:00'; - // const days = config.daysOfWeek || [0, 1, 2, 3, 4, 5, 6]; - // const dayLabels = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; - // const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; - // const pillsHtml = dayLabels.map((label, i) => { - // const checked = days.indexOf(i) >= 0; - // return ''; - // }).join(''); - // container.innerHTML = - // '' + - // '
    ' + pillsHtml + '
    '; - // container.querySelectorAll('.dash-day-pill').forEach(pill => { - // pill.addEventListener('click', () => { - // const isChecked = pill.getAttribute('aria-checked') === 'true'; - // pill.setAttribute('aria-checked', String(!isChecked)); - // }); - // }); - // } else if (type === 'once') { - // const dt = config.dateTime || ''; - // container.innerHTML = ''; - // } - // } -// - // private getActiveScheduleType(container: HTMLElement | null): string { - // if (!container) return 'interval'; - // const active = container.querySelector('.dash-schedule-pill.active'); - // return active ? (active.getAttribute('data-type') || 'interval') : 'interval'; - // } -// - // private getScheduleConfig(container: HTMLElement | null, type: string): any { - // if (!container) return {}; - // if (type === 'interval') { - // const input = container.querySelector('input[type="number"]') as HTMLInputElement | null; - // return { intervalMinutes: Math.max(5, parseInt(input?.value || '60') || 60) }; - // } - // if (type === 'daily') { - // const timeInput = container.querySelector('input[type="time"]') as HTMLInputElement | null; - // const daysChecked: number[] = []; - // container.querySelectorAll('.dash-day-pill[aria-checked="true"]').forEach(p => { - // daysChecked.push(parseInt(p.getAttribute('data-day') || '0')); - // }); - // return { dailyTime: timeInput?.value || '08:00', daysOfWeek: daysChecked }; - // } - // if (type === 'once') { - // const dtInput = container.querySelector('input[type="datetime-local"]') as HTMLInputElement | null; - // return { dateTime: dtInput?.value || '' }; - // } - // return {}; - // } -// - // ==================== DELETE AGENT ==================== -// - // private openDeleteDialog(agentId: string, agentName: string): void { - // this.deleteAgentId = agentId; - // this.deleteAgentName = agentName; - // if (this.deleteTitle) this.deleteTitle.textContent = 'Delete ' + agentName + '?'; - // if (this.deleteOverlay) this.deleteOverlay.style.display = 'flex'; - // if (this.deleteCancel) this.deleteCancel.focus(); - // } -// - // private closeDeleteDialog(): void { - // if (this.deleteOverlay) this.deleteOverlay.style.display = 'none'; - // this.deleteAgentId = null; - // this.deleteAgentName = ''; - // } -// - // private confirmDeleteAgent(): void { - // if (!this.deleteAgentId) return; - // this.apiFetch('/api/agents/' + encodeURIComponent(this.deleteAgentId), { - // method: 'DELETE', - // headers: { 'X-FSB-Hash-Key': this.hashKey }, - // }).then(() => { - // this.closeDeleteDialog(); - // this.closeDetailPanel(); - // this.loadData(); - // }).catch(() => { - // this.closeDeleteDialog(); - // }); - // } -// - // ==================== SAVE AS AGENT ==================== -// - // private showSaveAsAgent(): void { - // if (this.saveAgentSection) this.saveAgentSection.style.display = 'block'; - // if (this.saveAgentNameEl && this.taskText) { - // this.saveAgentNameEl.value = this.taskText.length > 50 ? this.taskText.substring(0, 50) + '...' : this.taskText; - // } - // if (this.saveAgentUrlEl) { - // const urlMatch = this.taskText.match(/https?:\/\/[^\s]+/); - // if (urlMatch) this.saveAgentUrlEl.value = urlMatch[0]; - // } - // this.renderScheduleConfig(this.saveAgentScheduleConfig, 'interval', '{"intervalMinutes": 60}'); - // } -// - // private hideSaveAsAgent(): void { - // if (this.saveAgentSection) this.saveAgentSection.style.display = 'none'; - // if (this.saveAgentFields) { this.saveAgentFields.style.display = 'none'; this.saveAgentFields.classList.remove('dash-save-expanded'); } - // if (this.saveAgentTrigger) this.saveAgentTrigger.classList.remove('expanded'); - // } -// - // private submitSaveAsAgent(): void { - // const name = this.saveAgentNameEl?.value.trim() || ''; - // const url = this.saveAgentUrlEl?.value.trim() || ''; - // if (!name || !url) return; -// - // let scheduleType = 'interval'; - // this.saveAgentSection?.querySelectorAll('.dash-schedule-pill').forEach(p => { - // if (p.classList.contains('active')) scheduleType = p.getAttribute('data-type') || 'interval'; - // }); - // const scheduleConfig = this.getScheduleConfig(this.saveAgentScheduleConfig, scheduleType); -// - // if (this.saveAgentBtn) { (this.saveAgentBtn as HTMLButtonElement).disabled = true; this.saveAgentBtn.innerHTML = ' Saving...'; } -// - // const agentId = 'agent_' + Date.now().toString(36) + '_' + Math.random().toString(36).substring(2, 8); -// - // this.apiFetch('/api/agents', { - // method: 'POST', - // headers: { 'Content-Type': 'application/json', 'X-FSB-Hash-Key': this.hashKey }, - // body: JSON.stringify({ - // agentId, name, task: this.taskText, targetUrl: url, - // scheduleType, scheduleConfig: JSON.stringify(scheduleConfig), enabled: true, - // }), - // }).then(() => { - // this.hideSaveAsAgent(); - // this.loadData(); - // if (this.saveAgentBtn) { (this.saveAgentBtn as HTMLButtonElement).disabled = false; this.saveAgentBtn.textContent = 'Save Agent'; } - // }).catch(() => { - // if (this.saveAgentBtn) { (this.saveAgentBtn as HTMLButtonElement).disabled = false; this.saveAgentBtn.textContent = 'Save Agent'; } - // }); - // } -// - // private renderModeBadge(mode: string): string { - // if (mode === 'replay') return 'Replay'; - // if (mode === 'ai_fallback') return 'AI Fallback'; - // return 'AI'; - // } -// + // ==================== DOM PREVIEW ==================== private setPreviewLoadingText(text: string): void { @@ -4177,11 +3349,6 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy this.setTaskRecoveryPending(true, 'extension-online'); } this.updateTaskOfflineState(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // if (this.agentCountEl) { - // const countText = (this.stats.totalAgents || 0) + ' agent' + ((this.stats.totalAgents || 0) !== 1 ? 's' : ''); - // this.agentCountEl.textContent = countText + (this.extensionOnline ? '' : ' - extension offline'); - // } if (!wasExtensionOnline && this.extensionOnline) this.scheduleStreamRecovery('extension-online'); if (!this.extensionOnline) this.pageReady = false; return; @@ -4191,8 +3358,6 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy const snapshot = msg.payload || {}; const snapshotIntentActive = snapshot.streamIntentActive !== false; this.extensionOnline = true; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // this.loadData(); this.recordTransportEvent('snapshot-recovered', { type: 'ext:snapshot' }); this.recordSnapshotRecovery({ type: 'ext:snapshot', status: snapshot.streamStatus || '' }); if (snapshot.remoteControl) this.renderRemoteControlState(snapshot.remoteControl); @@ -4227,38 +3392,6 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy return; } - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // if (msg.type === 'ext:agent-run-progress') { - // const rp = msg.payload || {}; - // if (rp.agentId === this.agentRunningId) { - // if (this.detailRunBar) this.detailRunBar.style.width = (rp.progress || 0) + '%'; - // if (this.detailRunAction) this.detailRunAction.textContent = rp.action || 'Working...'; - // } - // return; - // } - - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // if (msg.type === 'ext:agent-run-complete') { - // const rc = msg.payload || {}; - // this.agentRunningId = null; - // this.renderAgents(); - // if (rc.agentId === this.detailAgentId) { - // if (this.detailRunNow) { (this.detailRunNow as HTMLButtonElement).disabled = false; this.detailRunNow.textContent = 'Run Now'; } - // if (this.detailRunProgress) { - // if (this.detailRunBar) { - // this.detailRunBar.style.width = '100%'; - // this.detailRunBar.className = 'dash-task-bar-fill ' + (rc.success ? 'dash-task-bar-success' : 'dash-task-bar-failed'); - // } - // if (this.detailRunAction) this.detailRunAction.textContent = rc.success ? 'Complete' : (rc.error || 'Failed'); - // setTimeout(() => { if (this.detailRunProgress) this.detailRunProgress.style.display = 'none'; }, 3000); - // } - // this.loadAgentStats(this.detailAgentId!); - // this.loadDetailRuns(this.detailAgentId!, 0); - // } - // this.loadData(); - // return; - // } - if (msg.type === 'ext:dom-snapshot') { this.handleDOMSnapshot(msg.payload); return; } if (msg.type === 'ext:dom-mutations') { this.handleDOMMutations(msg.payload); return; } if (msg.type === 'ext:dom-scroll') { this.handleDOMScroll(msg.payload); return; } @@ -4306,26 +3439,7 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy return; } - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // if (msg.type === 'agent_updated' || msg.type === 'agent_deleted' || msg.type === 'run_completed') { - // this.loadData(); - // if (msg.agentId && msg.agentId === this.detailAgentId) { - // this.loadAgentStats(this.detailAgentId!); - // this.loadDetailRuns(this.detailAgentId!, 0); - // } - // } - } -// - // ==================== POLLING FALLBACK ==================== -// - // private startPolling(): void { - // this.stopPolling(); - // this.pollTimer = setInterval(() => { if (!this.destroyed) this.loadData(); }, this.POLL_INTERVAL); - // } -// - // private stopPolling(): void { - // if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; } - // } + } // ==================== API HELPERS ==================== @@ -4596,38 +3710,6 @@ export class DashboardPageComponent implements OnInit, AfterViewInit, OnDestroy return Math.floor(ms / 60000) + 'm ' + Math.round((ms % 60000) / 1000) + 's'; } - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // private formatTimeAgo(isoStr: string): string { - // if (!isoStr) return 'Never'; - // try { - // const diff = Date.now() - new Date(isoStr).getTime(); - // if (diff < 60000) return 'Just now'; - // if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago'; - // if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago'; - // return Math.floor(diff / 86400000) + 'd ago'; - // } catch (_) { return isoStr; } - // } -// - // private formatNumber(n: number): string { - // if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; - // if (n >= 1000) return (n / 1000).toFixed(1) + 'K'; - // return String(n); - // } -// - // private formatScheduleLabel(scheduleType: string, scheduleConfig: any): string { - // let config: any = {}; - // try { config = typeof scheduleConfig === 'string' ? JSON.parse(scheduleConfig) : (scheduleConfig || {}); } catch (_) {} - // if (scheduleType === 'interval') { - // const mins = config.intervalMinutes || 60; - // if (mins >= 1440) return 'Every ' + Math.round(mins / 1440) + 'd'; - // if (mins >= 60) return 'Every ' + Math.round(mins / 60) + 'h'; - // return 'Every ' + mins + 'min'; - // } - // if (scheduleType === 'daily') return 'Daily ' + (config.dailyTime || '08:00'); - // if (scheduleType === 'once') return 'Once'; - // return scheduleType || 'manual'; - // } - private escapeHtml(str: string): string { if (!str) return ''; return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); diff --git a/showcase/angular/src/app/pages/home/home-page.component.html b/showcase/angular/src/app/pages/home/home-page.component.html index f3b8f5782..794ebcd2c 100644 --- a/showcase/angular/src/app/pages/home/home-page.component.html +++ b/showcase/angular/src/app/pages/home/home-page.component.html @@ -65,7 +65,7 @@

    Site Intelligence

    MCP Integration

    -

    Use FSB as an MCP server from Claude Code, Codex, Cursor, OpenClaw, or any MCP client. 68 tools, with browser automation straight from your IDE.

    +

    Use FSB as an MCP server from Claude Code, Codex, Cursor, OpenClaw, or any MCP client. 69 tools, with browser automation straight from your IDE.

    @@ -273,7 +273,7 @@

    Any agent. Any model. One 01 {{ engMcpKind }}Recommended Your agent drives FSB - 68 tools · no API key + 69 tools · no API key

    -

    Background agents have moved.

    diff --git a/showcase/js/dashboard.js b/showcase/js/dashboard.js index ab625e2f7..34a25e5a3 100644 --- a/showcase/js/dashboard.js +++ b/showcase/js/dashboard.js @@ -17,11 +17,7 @@ var sessionToken = localStorage.getItem(SESSION_KEY) || ''; var sessionExpiresAt = localStorage.getItem(SESSION_EXPIRES_KEY) || ''; var qrScanner = null; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // var agents = []; var stats = {}; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // var selectedAgentId = null; var runsOffset = 0; var runsLimit = 20; var pollTimer = null; @@ -39,18 +35,6 @@ var taskElapsedTimer = null; var lastProgressAction = ''; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // Agent management state - // var detailAgentId = null; // Currently open detail panel agent - // var detailRunsOffset = 0; - // var detailRunsLimit = 10; - // var modalMode = null; // 'create' | 'edit' - // var modalAgentId = null; // agentId when editing - // var deleteAgentId = null; // agentId pending deletion - // var deleteAgentName = ''; // name for delete dialog - // var saveAgentScheduleType = 'interval'; // schedule type for inline save / modal - // var agentRunningId = null; // agentId currently running via Run Now - // DOM preview state var previewState = 'hidden'; // 'hidden' | 'loading' | 'streaming' | 'disconnected' | 'frozen-disconnect' | 'frozen-complete' | 'error' var previewLayoutMode = 'inline'; // 'inline' | 'maximized' | 'pip' | 'fullscreen' @@ -809,13 +793,8 @@ var keyInput = document.getElementById('dash-key-input'); var connectBtn = document.getElementById('dash-connect-btn'); var disconnectBtn = document.getElementById('dash-disconnect-btn'); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // var agentCountEl = document.getElementById('dash-agent-count'); var sseStatusEl = document.getElementById('dash-sse-status'); var wakeBtn = document.getElementById('dash-wake-btn'); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // var agentGrid = document.getElementById('dash-agent-grid'); - // var emptyState = document.getElementById('dash-empty'); var tabScan = document.getElementById('dash-tab-scan'); var tabPaste = document.getElementById('dash-tab-paste'); var tabScanContent = document.getElementById('tab-scan'); @@ -895,60 +874,6 @@ var previewRestrictedUrl = document.getElementById('dash-preview-restricted-url'); var lastKnownStreamUrl = ''; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // Agent management DOM refs - // var newAgentBtn = document.getElementById('dash-new-agent-btn'); - // var agentContainer = document.getElementById('dash-agent-container'); - // var detailPanel = document.getElementById('dash-agent-detail'); - // var detailClose = document.getElementById('dash-detail-close'); - // var detailRunNow = document.getElementById('dash-detail-run-now'); - // var detailEdit = document.getElementById('dash-detail-edit'); - // var detailDelete = document.getElementById('dash-detail-delete'); - // var detailName = document.getElementById('dash-detail-name'); - // var detailTask = document.getElementById('dash-detail-task'); - // var detailUrl = document.getElementById('dash-detail-url'); - // var detailSchedule = document.getElementById('dash-detail-schedule'); - // var detailReplayRuns = document.getElementById('dash-detail-replay-runs'); - // var detailAiFallback = document.getElementById('dash-detail-ai-fallback'); - // var detailTokensSaved = document.getElementById('dash-detail-tokens-saved'); - // var detailCostSaved = document.getElementById('dash-detail-cost-saved'); - // var detailRunProgress = document.getElementById('dash-detail-run-progress'); - // var detailRunBar = document.getElementById('dash-detail-run-bar'); - // var detailRunAction = document.getElementById('dash-detail-run-action'); - // var detailRunsList = document.getElementById('dash-detail-runs'); - // var detailRunsPagination = document.getElementById('dash-detail-runs-pagination'); - // var detailScriptToggle = document.getElementById('dash-detail-script-toggle'); - // var detailScriptContent = document.getElementById('dash-detail-script-content'); - // var detailScriptList = document.getElementById('dash-detail-script-list'); - // var detailScriptChevron = document.getElementById('dash-detail-script-chevron'); -// - // Modal DOM refs - // var modalOverlay = document.getElementById('dash-agent-modal-overlay'); - // var modalTitle = document.getElementById('dash-modal-title'); - // var modalClose = document.getElementById('dash-modal-close'); - // var modalName = document.getElementById('dash-modal-name'); - // var modalTask = document.getElementById('dash-modal-task'); - // var modalUrl = document.getElementById('dash-modal-url'); - // var modalScheduleType = document.getElementById('dash-modal-schedule-type'); - // var modalScheduleConfig = document.getElementById('dash-modal-schedule-config'); - // var modalDiscard = document.getElementById('dash-modal-discard'); - // var modalSave = document.getElementById('dash-modal-save'); -// - // Delete dialog DOM refs - // var deleteOverlay = document.getElementById('dash-delete-overlay'); - // var deleteTitle = document.getElementById('dash-delete-title'); - // var deleteCancel = document.getElementById('dash-delete-cancel'); - // var deleteConfirm = document.getElementById('dash-delete-confirm'); -// - // Save-as-Agent DOM refs - // var saveAgentSection = document.getElementById('dash-task-save-agent'); - // var saveAgentTrigger = document.getElementById('dash-save-agent-trigger'); - // var saveAgentFields = document.getElementById('dash-save-agent-fields'); - // var saveAgentName = document.getElementById('dash-save-agent-name'); - // var saveAgentUrl = document.getElementById('dash-save-agent-url'); - // var saveAgentBtn = document.getElementById('dash-save-agent-btn'); - // var saveAgentScheduleConfig = document.getElementById('dash-save-agent-schedule-config'); - // --- Init --- if (sessionToken && sessionExpiresAt) { // Check local expiry first (avoid server call if obviously expired) @@ -1031,100 +956,6 @@ }); } - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // Detail panel listeners - // if (detailClose) detailClose.addEventListener('click', closeDetailPanel); - // if (detailRunNow) detailRunNow.addEventListener('click', function () { - // if (detailAgentId) runAgentNow(detailAgentId); - // }); - // if (detailEdit) detailEdit.addEventListener('click', function () { - // if (detailAgentId) openAgentModal('edit', detailAgentId); - // }); - // if (detailDelete) detailDelete.addEventListener('click', function () { - // if (detailAgentId) { - // var agent = agents.find(function (a) { return a.agent_id === detailAgentId; }); - // openDeleteDialog(detailAgentId, agent ? agent.name : detailAgentId); - // } - // }); -// - // Recorded script toggle - // if (detailScriptToggle) { - // detailScriptToggle.addEventListener('click', function () { - // var isExpanded = detailScriptToggle.classList.contains('expanded'); - // detailScriptToggle.classList.toggle('expanded'); - // if (detailScriptContent) detailScriptContent.style.display = isExpanded ? 'none' : 'block'; - // }); - // } - - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // New Agent button - // if (newAgentBtn) newAgentBtn.addEventListener('click', function () { openAgentModal('create'); }); -// - // Modal listeners - // if (modalClose) modalClose.addEventListener('click', closeAgentModal); - // if (modalDiscard) modalDiscard.addEventListener('click', closeAgentModal); - // if (modalSave) modalSave.addEventListener('click', saveAgentFromModal); - // if (modalOverlay) { - // modalOverlay.addEventListener('click', function (e) { - // if (e.target === modalOverlay) closeAgentModal(); - // }); - // } - // Escape key closes modal - // document.addEventListener('keydown', function (e) { - // if (e.key === 'Escape') { - // if (modalOverlay && modalOverlay.style.display !== 'none') closeAgentModal(); - // else if (deleteOverlay && deleteOverlay.style.display !== 'none') closeDeleteDialog(); - // } - // }); -// - // Schedule type pill handlers (modal) - // if (modalScheduleType) { - // modalScheduleType.addEventListener('click', function (e) { - // var pill = e.target.closest('.dash-schedule-pill'); - // if (!pill) return; - // modalScheduleType.querySelectorAll('.dash-schedule-pill').forEach(function (p) { p.classList.remove('active'); }); - // pill.classList.add('active'); - // renderScheduleConfig(modalScheduleConfig, pill.getAttribute('data-type'), '{}'); - // }); - // } -// - // Delete dialog listeners - // if (deleteCancel) deleteCancel.addEventListener('click', closeDeleteDialog); - // if (deleteConfirm) deleteConfirm.addEventListener('click', confirmDelete); - // if (deleteOverlay) { - // deleteOverlay.addEventListener('click', function (e) { - // if (e.target === deleteOverlay) closeDeleteDialog(); - // }); - // } -// - // Save-as-Agent listeners - // if (saveAgentTrigger) { - // saveAgentTrigger.addEventListener('click', function () { - // var isExpanded = saveAgentTrigger.classList.contains('expanded'); - // saveAgentTrigger.classList.toggle('expanded'); - // if (saveAgentFields) { - // if (isExpanded) { - // saveAgentFields.classList.remove('dash-save-expanded'); - // saveAgentFields.style.display = 'none'; - // } else { - // saveAgentFields.style.display = 'flex'; - // saveAgentFields.classList.add('dash-save-expanded'); - // } - // } - // }); - // } - // Schedule pills for save-as-agent section - // if (saveAgentSection) { - // saveAgentSection.addEventListener('click', function (e) { - // var pill = e.target.closest('.dash-schedule-pill'); - // if (!pill) return; - // saveAgentSection.querySelectorAll('.dash-schedule-pill').forEach(function (p) { p.classList.remove('active'); }); - // pill.classList.add('active'); - // renderScheduleConfig(saveAgentScheduleConfig, pill.getAttribute('data-type'), '{}'); - // }); - // } - // if (saveAgentBtn) saveAgentBtn.addEventListener('click', submitSaveAsAgent); - // --- Task Control --- function submitTask(text) { @@ -1197,8 +1028,6 @@ // Reset progress bar if (taskBarFill) { taskBarFill.style.width = '0%'; taskBarFill.className = 'dash-task-bar-fill'; } if (actionFeed) actionFeed.innerHTML = ''; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // hideSaveAsAgent(); if (previewContainer) previewContainer.classList.remove('dash-preview-automating'); renderTaskRecoveryStatus('', ''); break; @@ -1229,8 +1058,6 @@ }, TASK_TIMEOUT_MS); // Disable all task inputs during run disableAllTaskInputs(true); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // hideSaveAsAgent(); if (previewContainer) previewContainer.classList.add('dash-preview-automating'); renderTaskRecoveryStatus(activeTaskRunId || '', taskRecoverySource); break; @@ -1252,9 +1079,6 @@ // Show next-task input disableAllTaskInputs(false); if (taskInputNext) { taskInputNext.value = ''; } - // Show save-as-agent option - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // showSaveAsAgent(); if (previewContainer) previewContainer.classList.remove('dash-preview-automating'); renderTaskRecoveryStatus('', ''); break; @@ -1648,11 +1472,7 @@ // For paste-key users, clear any stale session clearSession(); showDashboard(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // loadData(); connectWS(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // startPolling(); } else { showError(result.error || 'Invalid hash key. Check your key and try again.'); } @@ -1667,11 +1487,7 @@ validateKey(key).then(function (result) { if (result.valid) { showDashboard(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // loadData(); connectWS(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // startPolling(); } else { // Stored key is invalid, clear it localStorage.removeItem(STORAGE_KEY); @@ -1680,10 +1496,6 @@ }).catch(function () { // Server unreachable -- show dashboard anyway with cached key showDashboard(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // loadData(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // startPolling(); }); } @@ -1699,11 +1511,6 @@ hashKey = ''; localStorage.removeItem(STORAGE_KEY); clearSession(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // agents = []; - // stats = {}; - // selectedAgentId = null; - // stopPolling(); disconnectWS(); stopQRScanner(); showLogin(); @@ -1773,11 +1580,7 @@ hashKey = result.hashKey; localStorage.setItem(STORAGE_KEY, hashKey); showDashboard(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // loadData(); connectWS(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // startPolling(); } else { clearSession(); if (result.reason === 'expired') { @@ -1790,11 +1593,7 @@ // Server unreachable - try with stored hashKey if (hashKey) { showDashboard(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // loadData(); connectWS(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // startPolling(); } }); } @@ -1944,11 +1743,7 @@ // Store session and connect storeSession(result.hashKey, result.sessionToken, result.expiresAt); showDashboard(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // loadData(); connectWS(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // startPolling(); }).catch(function (err) { showScanError(err.message || 'Scan failed -- paste your key instead'); // Restore scan tab content @@ -1975,687 +1770,8 @@ } } - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // --- Data Loading --- -// - // function loadData() { - // fetchStats(); - // fetchAgents(); - // } -// - // function fetchStats() { - // apiFetch('/api/stats', { headers: { 'X-FSB-Hash-Key': hashKey } }) - // .then(function (data) { - // stats = data; - // renderStats(); - // }) - // .catch(function () {}); - // } -// - // function fetchAgents() { - // apiFetch('/api/agents', { headers: { 'X-FSB-Hash-Key': hashKey } }) - // .then(function (data) { - // agents = data.agents || []; - // renderAgents(); - // }) - // .catch(function () {}); - // } -// - // function fetchRuns(agentId, limit, offset) { - // var url = '/api/agents/' + encodeURIComponent(agentId) + '/runs?limit=' + limit + '&offset=' + offset; - // return apiFetch(url, { headers: { 'X-FSB-Hash-Key': hashKey } }); - // } -// - // --- Rendering --- -// - // function renderStats() { - // setTextById('stat-agents', stats.totalAgents || 0); - // setTextById('stat-enabled', stats.enabledAgents || 0); - // setTextById('stat-runs-today', stats.runsToday || 0); - // setTextById('stat-success-rate', (stats.successRate || 0) + '%'); - // setTextById('stat-total-cost', '$' + (stats.totalCost || 0).toFixed(2)); - // setTextById('stat-cost-saved', '$' + (stats.totalCostSaved || 0).toFixed(2)); - // var countText = (stats.totalAgents || 0) + ' agent' + ((stats.totalAgents || 0) !== 1 ? 's' : ''); - // agentCountEl.textContent = countText + (extensionOnline ? '' : ' - extension offline'); - // } - - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // function renderAgents() { - // agentGrid.innerHTML = ''; -// - // if (agents.length === 0) { - // emptyState.style.display = 'block'; - // agentGrid.style.display = 'none'; - // return; - // } -// - // emptyState.style.display = 'none'; - // agentGrid.style.display = ''; -// - // agents.forEach(function (agent) { - // var card = document.createElement('div'); - // var isEnabled = agent.enabled === true || agent.enabled === 1; - // var isSelected = detailAgentId === agent.agent_id; - // card.className = 'dash-agent-card' + (isSelected ? ' selected' : '') + (!isEnabled ? ' dash-agent-disabled' : ''); - // card.setAttribute('data-agent-id', agent.agent_id); - // card.setAttribute('role', 'button'); - // card.setAttribute('aria-expanded', isSelected ? 'true' : 'false'); -// - // Parse schedule for display - // var scheduleLabel = formatScheduleLabel(agent.schedule_type, agent.schedule_config); -// - // Calculate success rate from recent data - // var successCount = agent.successful_runs || 0; - // var totalRuns = agent.total_runs || 0; - // var successRateText = totalRuns > 0 ? successCount + '/' + totalRuns : '0/0'; - // var successPercent = totalRuns > 0 ? (successCount / totalRuns) * 100 : 100; - // var rateColor = successPercent > 80 ? '#22c55e' : successPercent >= 50 ? '#eab308' : '#ef4444'; -// - // Cost saved - // var costSaved = agent.cost_saved || 0; - // var costText = '$' + costSaved.toFixed(2); -// - // Last run time - // var lastRunText = agent.last_run_at ? formatTimeAgo(agent.last_run_at) : 'Never'; -// - // Running indicator - // var runningIcon = agentRunningId === agent.agent_id ? ' ' : ''; -// - // card.innerHTML = - // '
    ' + - // '
    ' + - // '' + - // escapeHtml(agent.name) + runningIcon + - // '
    ' + - // '' + - // '
    ' + - // '
    ' + escapeHtml(agent.task) + '
    ' + - // '
    ' + escapeHtml(agent.target_url || '') + '
    ' + - // '
    ' + - // '' + escapeHtml(scheduleLabel) + '' + - // '' + escapeHtml(lastRunText) + '' + - // '
    ' + - // '
    ' + - // '' + successRateText + '' + - // '' + costText + '' + - // '
    '; -// - // Card click opens detail panel (but not on toggle click) - // card.addEventListener('click', function (e) { - // if (e.target.closest('.dash-toggle')) return; // toggle has own handler - // openDetailPanel(agent.agent_id); - // }); -// - // Toggle click handler - // var toggle = card.querySelector('.dash-toggle'); - // if (toggle) { - // toggle.addEventListener('click', function (e) { - // e.stopPropagation(); - // toggleAgent(agent.agent_id, !isEnabled); - // }); - // } -// - // agentGrid.appendChild(card); - // }); - // } - // --- Helper Functions --- - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // function formatScheduleLabel(scheduleType, scheduleConfig) { - // var config = {}; - // try { config = typeof scheduleConfig === 'string' ? JSON.parse(scheduleConfig) : (scheduleConfig || {}); } catch (_) {} -// - // if (scheduleType === 'interval') { - // var mins = config.intervalMinutes || 60; - // if (mins >= 1440) return 'Every ' + Math.round(mins / 1440) + 'd'; - // if (mins >= 60) return 'Every ' + Math.round(mins / 60) + 'h'; - // return 'Every ' + mins + 'min'; - // } - // if (scheduleType === 'daily') { - // return 'Daily ' + (config.dailyTime || '08:00'); - // } - // if (scheduleType === 'once') { - // return 'Once'; - // } - // return scheduleType || 'manual'; - // } - - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // function formatTimeAgo(isoStr) { - // if (!isoStr) return 'Never'; - // try { - // var diff = Date.now() - new Date(isoStr).getTime(); - // if (diff < 60000) return 'Just now'; - // if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago'; - // if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago'; - // return Math.floor(diff / 86400000) + 'd ago'; - // } catch (_) { return isoStr; } - // } - - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // function formatNumber(n) { - // if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; - // if (n >= 1000) return (n / 1000).toFixed(1) + 'K'; - // return String(n); - // } - - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // --- Toggle Agent --- -// - // function toggleAgent(agentId, enabled) { - // Optimistic UI update - // agents = agents.map(function (a) { - // if (a.agent_id === agentId) { a.enabled = enabled ? 1 : 0; } - // return a; - // }); - // renderAgents(); -// - // API call - // apiFetch('/api/agents/' + encodeURIComponent(agentId), { - // method: 'PATCH', - // headers: { 'Content-Type': 'application/json', 'X-FSB-Hash-Key': hashKey }, - // body: JSON.stringify({ enabled: enabled }) - // }).catch(function () { - // Revert on failure - // agents = agents.map(function (a) { - // if (a.agent_id === agentId) { a.enabled = enabled ? 0 : 1; } - // return a; - // }); - // renderAgents(); - // }); - // } - - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // --- Detail Panel --- -// - // function openDetailPanel(agentId) { - // var agent = agents.find(function (a) { return a.agent_id === agentId; }); - // if (!agent) return; -// - // detailAgentId = agentId; - // selectedAgentId = agentId; // Keep selected for card styling - // detailRunsOffset = 0; -// - // Update card selection - // var cards = agentGrid.querySelectorAll('.dash-agent-card'); - // cards.forEach(function (c) { - // var isThis = c.getAttribute('data-agent-id') === agentId; - // c.classList.toggle('selected', isThis); - // c.setAttribute('aria-expanded', isThis ? 'true' : 'false'); - // }); -// - // Fill detail panel - // if (detailName) detailName.textContent = agent.name; - // if (detailTask) detailTask.textContent = agent.task; - // if (detailUrl) detailUrl.textContent = agent.target_url || ''; - // if (detailSchedule) detailSchedule.textContent = formatScheduleLabel(agent.schedule_type, agent.schedule_config); -// - // Show panel - // if (detailPanel) detailPanel.style.display = 'block'; - // if (agentContainer) agentContainer.classList.add('dash-detail-open'); -// - // Load cost savings - // loadAgentStats(agentId); -// - // Load run history - // loadDetailRuns(agentId, 0); -// - // Load recorded script - // loadRecordedScript(agent); -// - // Reset run progress - // if (detailRunProgress) detailRunProgress.style.display = 'none'; - // } -// - // function closeDetailPanel() { - // detailAgentId = null; - // selectedAgentId = null; - // if (detailPanel) detailPanel.style.display = 'none'; - // if (agentContainer) agentContainer.classList.remove('dash-detail-open'); - // var cards = agentGrid.querySelectorAll('.dash-agent-card'); - // cards.forEach(function (c) { - // c.classList.remove('selected'); - // c.setAttribute('aria-expanded', 'false'); - // }); - // } -// - // function loadAgentStats(agentId) { - // apiFetch('/api/agents/' + encodeURIComponent(agentId) + '/stats', { - // headers: { 'X-FSB-Hash-Key': hashKey } - // }).then(function (data) { - // if (detailReplayRuns) detailReplayRuns.textContent = data.replayRuns || 0; - // if (detailAiFallback) detailAiFallback.textContent = data.aiFallbackRuns || 0; - // if (detailTokensSaved) detailTokensSaved.textContent = formatNumber(data.tokensSaved || 0); - // if (detailCostSaved) detailCostSaved.textContent = '$' + (data.costSaved || 0).toFixed(2); - // }).catch(function () {}); - // } -// - // function loadDetailRuns(agentId, offset) { - // if (!detailRunsList) return; - // detailRunsList.innerHTML = '
    '; -// - // fetchRuns(agentId, detailRunsLimit, offset).then(function (data) { - // renderDetailRuns(data.runs || [], data.total || 0, data.limit || detailRunsLimit, data.offset || 0); - // }).catch(function () { - // detailRunsList.innerHTML = '

    Failed to load runs.

    '; - // }); - // } -// - // function renderDetailRuns(runs, total, limit, offset) { - // if (!detailRunsList) return; - // detailRunsList.innerHTML = ''; -// - // if (runs.length === 0) { - // detailRunsList.innerHTML = '

    No runs yet. Tap Run Now to test this agent.

    '; - // if (detailRunsPagination) detailRunsPagination.innerHTML = ''; - // return; - // } -// - // runs.forEach(function (run) { - // var entry = document.createElement('div'); - // entry.className = 'dash-run-entry'; -// - // var time = formatTime(run.completed_at); - // var statusClass = run.status === 'success' ? 'dash-run-status-success' : - // run.status === 'failed' ? 'dash-run-status-failed' : - // 'dash-run-status-unknown'; - // var statusSr = run.status === 'success' ? 'Status: success' : run.status === 'failed' ? 'Status: failed' : 'Status: unknown'; -// - // var modeBadge = renderModeBadge(run.execution_mode); - // var resultText = run.error || run.result || '-'; - // var duration = run.duration_ms ? formatDuration(run.duration_ms) : '-'; - // var costStr = run.cost_saved && run.cost_saved > 0 ? '-$' + run.cost_saved.toFixed(4) : - // run.cost_usd ? '$' + run.cost_usd.toFixed(4) : '-'; -// - // entry.innerHTML = - // '
    ' + time + '
    ' + - // '
    ' + statusSr + '' + escapeHtml(run.status) + '
    ' + - // '
    ' + modeBadge + '
    ' + - // '
    ' + escapeHtml(resultText) + '
    ' + - // '
    ' + duration + '
    ' + - // '
    ' + costStr + '
    '; -// - // detailRunsList.appendChild(entry); - // }); -// - // Pagination (reuse existing pattern) - // if (detailRunsPagination) { - // detailRunsPagination.innerHTML = ''; - // if (total > limit) { - // var prevBtn = document.createElement('button'); - // prevBtn.textContent = 'Previous'; - // prevBtn.disabled = offset === 0; - // prevBtn.addEventListener('click', function () { loadDetailRuns(detailAgentId, Math.max(0, offset - limit)); }); -// - // var nextBtn = document.createElement('button'); - // nextBtn.textContent = 'Next'; - // nextBtn.disabled = (offset + limit) >= total; - // nextBtn.addEventListener('click', function () { loadDetailRuns(detailAgentId, offset + limit); }); -// - // var info = document.createElement('span'); - // info.className = 'text-muted text-sm'; - // info.style.padding = '6px 8px'; - // info.textContent = (offset + 1) + '-' + Math.min(offset + limit, total) + ' of ' + total; -// - // detailRunsPagination.appendChild(prevBtn); - // detailRunsPagination.appendChild(info); - // detailRunsPagination.appendChild(nextBtn); - // } - // } - // } -// - // function loadRecordedScript(agent) { - // if (!detailScriptList) return; - // detailScriptList.innerHTML = ''; - // Collapse script section by default - // if (detailScriptContent) detailScriptContent.style.display = 'none'; - // if (detailScriptToggle) detailScriptToggle.classList.remove('expanded'); -// - // Agent may have recordedScript from extension sync - // var script = agent.recorded_script || agent.recordedScript; - // if (!script || !Array.isArray(script) || script.length === 0) { - // detailScriptList.innerHTML = '
  • No recorded script available
  • '; - // return; - // } -// - // script.forEach(function (step) { - // var li = document.createElement('li'); - // li.textContent = typeof step === 'string' ? step : (step.action || step.description || JSON.stringify(step)); - // detailScriptList.appendChild(li); - // }); - // } - - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // --- Run Now --- -// - // function runAgentNow(agentId) { - // if (!ws || ws.readyState !== WebSocket.OPEN) return; - // if (!extensionOnline) return; -// - // agentRunningId = agentId; - // renderAgents(); // Show spinner on card -// - // Show progress bar in detail panel - // if (detailRunProgress) detailRunProgress.style.display = 'block'; - // if (detailRunBar) { detailRunBar.style.width = '0%'; detailRunBar.className = 'dash-task-bar-fill'; } - // if (detailRunAction) detailRunAction.textContent = 'Starting...'; - // if (detailRunNow) { detailRunNow.disabled = true; detailRunNow.innerHTML = ' Running'; } -// - // ws.send(JSON.stringify({ - // type: 'dash:agent-run-now', - // payload: { agentId: agentId }, - // ts: Date.now() - // })); - // } - - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // --- Agent Modal --- -// - // function openAgentModal(mode, agentId) { - // modalMode = mode; - // modalAgentId = agentId || null; -// - // if (modalTitle) modalTitle.textContent = mode === 'edit' ? 'Edit Agent' : 'New Agent'; - // if (modalSave) { modalSave.textContent = 'Save Agent'; modalSave.disabled = false; } -// - // Clear or pre-fill fields - // if (mode === 'edit' && agentId) { - // var agent = agents.find(function (a) { return a.agent_id === agentId; }); - // if (agent) { - // if (modalName) modalName.value = agent.name || ''; - // if (modalTask) modalTask.value = agent.task || ''; - // if (modalUrl) modalUrl.value = agent.target_url || ''; - // setModalScheduleType(agent.schedule_type || 'interval', agent.schedule_config); - // } - // } else { - // if (modalName) modalName.value = ''; - // if (modalTask) modalTask.value = ''; - // if (modalUrl) modalUrl.value = ''; - // setModalScheduleType('interval', '{}'); - // } -// - // Show modal - // if (modalOverlay) modalOverlay.style.display = 'flex'; - // if (modalName) modalName.focus(); - // } -// - // function closeAgentModal() { - // if (modalOverlay) modalOverlay.style.display = 'none'; - // modalMode = null; - // modalAgentId = null; - // clearModalErrors(); - // } -// - // function clearModalErrors() { - // var errors = (modalOverlay || document).querySelectorAll('.dash-field-error'); - // errors.forEach(function (e) { e.remove(); }); - // var errorInputs = (modalOverlay || document).querySelectorAll('.dash-input-error'); - // errorInputs.forEach(function (e) { e.classList.remove('dash-input-error'); }); - // } -// - // function saveAgentFromModal() { - // clearModalErrors(); -// - // var name = modalName ? modalName.value.trim() : ''; - // var task = modalTask ? modalTask.value.trim() : ''; - // var url = modalUrl ? modalUrl.value.trim() : ''; -// - // Validate - // var valid = true; - // if (!name) { showFieldError(modalName, 'Name is required'); valid = false; } - // if (!task) { showFieldError(modalTask, 'Task description is required'); valid = false; } - // if (!url) { showFieldError(modalUrl, 'Target URL is required'); valid = false; } - // if (!valid) return; -// - // Gather schedule - // var scheduleType = getActiveScheduleType(modalScheduleType); - // var scheduleConfig = getScheduleConfig(modalScheduleConfig, scheduleType); -// - // Disable save button - // if (modalSave) { modalSave.disabled = true; modalSave.innerHTML = ' Saving...'; } -// - // var agentId = modalMode === 'edit' ? modalAgentId : 'agent_' + Date.now().toString(36) + '_' + Math.random().toString(36).substring(2, 8); -// - // apiFetch('/api/agents', { - // method: 'POST', - // headers: { 'Content-Type': 'application/json', 'X-FSB-Hash-Key': hashKey }, - // body: JSON.stringify({ - // agentId: agentId, - // name: name, - // task: task, - // targetUrl: url, - // scheduleType: scheduleType, - // scheduleConfig: JSON.stringify(scheduleConfig), - // enabled: true - // }) - // }).then(function () { - // closeAgentModal(); - // loadData(); - // if (window.showToast) showToast('Agent ' + (modalMode === 'edit' ? 'updated' : 'created')); - // Highlight new card briefly - // setTimeout(function () { - // var newCard = agentGrid.querySelector('[data-agent-id="' + agentId + '"]'); - // if (newCard) { - // newCard.classList.add('dash-agent-card-highlight'); - // setTimeout(function () { newCard.classList.remove('dash-agent-card-highlight'); }, 1100); - // } - // }, 200); - // }).catch(function (err) { - // if (modalSave) { modalSave.disabled = false; modalSave.textContent = 'Save Agent'; } - // var msg = (err && err.error) || 'Couldn\'t create agent. Check your connection and try again.'; - // showFieldError(modalUrl, msg); - // }); - // } -// - // function showFieldError(inputEl, msg) { - // if (!inputEl) return; - // inputEl.classList.add('dash-input-error'); - // var errEl = document.createElement('div'); - // errEl.className = 'dash-field-error'; - // errEl.textContent = msg; - // inputEl.parentNode.appendChild(errEl); - // } - - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // --- Schedule Configuration --- -// - // function setModalScheduleType(type, configStr) { - // var pills = (modalScheduleType || document).querySelectorAll('.dash-schedule-pill'); - // pills.forEach(function (p) { p.classList.toggle('active', p.getAttribute('data-type') === type); }); - // renderScheduleConfig(modalScheduleConfig, type, configStr); - // } -// - // function renderScheduleConfig(container, type, configStr) { - // if (!container) return; - // var config = {}; - // try { config = typeof configStr === 'string' ? JSON.parse(configStr) : (configStr || {}); } catch (_) {} -// - // if (type === 'interval') { - // var mins = config.intervalMinutes || 60; - // container.innerHTML = - // '
    ' + - // 'Every' + - // '' + - // 'minutes' + - // '
    '; - // Snap to minimum - // var input = container.querySelector('input'); - // if (input) { - // input.addEventListener('blur', function () { - // if (parseInt(input.value) < 5) { - // input.value = 5; - // var msgEl = container.querySelector('.dash-schedule-snap-msg'); - // if (!msgEl) { - // msgEl = document.createElement('div'); - // msgEl.className = 'dash-schedule-snap-msg'; - // msgEl.textContent = 'Minimum 5 minutes'; - // container.appendChild(msgEl); - // setTimeout(function () { msgEl.style.opacity = '0'; }, 100); - // setTimeout(function () { if (msgEl.parentNode) msgEl.remove(); }, 2100); - // } - // } - // }); - // } - // } else if (type === 'daily') { - // var time = config.dailyTime || '08:00'; - // var days = config.daysOfWeek || [0, 1, 2, 3, 4, 5, 6]; - // var dayLabels = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; - // var dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; -// - // var pillsHtml = dayLabels.map(function (label, i) { - // var checked = days.indexOf(i) >= 0; - // return ''; - // }).join(''); -// - // container.innerHTML = - // '' + - // '
    ' + pillsHtml + '
    '; -// - // Day pill toggle handlers - // container.querySelectorAll('.dash-day-pill').forEach(function (pill) { - // pill.addEventListener('click', function () { - // var isChecked = pill.getAttribute('aria-checked') === 'true'; - // pill.setAttribute('aria-checked', !isChecked); - // }); - // }); - // } else if (type === 'once') { - // var dt = config.dateTime || ''; - // container.innerHTML = ''; - // } - // } -// - // function getActiveScheduleType(container) { - // if (!container) return 'interval'; - // var active = container.querySelector('.dash-schedule-pill.active'); - // return active ? active.getAttribute('data-type') : 'interval'; - // } -// - // function getScheduleConfig(container, type) { - // if (!container) return {}; - // if (type === 'interval') { - // var input = container.querySelector('input[type="number"]'); - // return { intervalMinutes: Math.max(5, parseInt(input ? input.value : '60') || 60) }; - // } - // if (type === 'daily') { - // var timeInput = container.querySelector('input[type="time"]'); - // var daysChecked = []; - // container.querySelectorAll('.dash-day-pill[aria-checked="true"]').forEach(function (p) { - // daysChecked.push(parseInt(p.getAttribute('data-day'))); - // }); - // return { dailyTime: timeInput ? timeInput.value : '08:00', daysOfWeek: daysChecked }; - // } - // if (type === 'once') { - // var dtInput = container.querySelector('input[type="datetime-local"]'); - // return { dateTime: dtInput ? dtInput.value : '' }; - // } - // return {}; - // } - - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // --- Delete Agent --- -// - // function openDeleteDialog(agentId, agentName) { - // deleteAgentId = agentId; - // deleteAgentName = agentName; - // if (deleteTitle) deleteTitle.textContent = 'Delete ' + agentName + '?'; - // if (deleteOverlay) deleteOverlay.style.display = 'flex'; - // if (deleteCancel) deleteCancel.focus(); - // } -// - // function closeDeleteDialog() { - // if (deleteOverlay) deleteOverlay.style.display = 'none'; - // deleteAgentId = null; - // deleteAgentName = ''; - // } -// - // function confirmDelete() { - // if (!deleteAgentId) return; - // apiFetch('/api/agents/' + encodeURIComponent(deleteAgentId), { - // method: 'DELETE', - // headers: { 'X-FSB-Hash-Key': hashKey } - // }).then(function () { - // closeDeleteDialog(); - // closeDetailPanel(); - // loadData(); - // if (window.showToast) showToast('Agent deleted'); - // }).catch(function () { - // closeDeleteDialog(); - // }); - // } - - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // --- Post-Task Save as Agent --- -// - // function showSaveAsAgent() { - // if (saveAgentSection) saveAgentSection.style.display = 'block'; - // Pre-fill from completed task context - // if (saveAgentName && taskText) { - // Use first ~50 chars of task as agent name - // saveAgentName.value = taskText.length > 50 ? taskText.substring(0, 50) + '...' : taskText; - // } - // if (saveAgentUrl) { - // URL can be populated from task text if it contains a URL - // var urlMatch = taskText.match(/https?:\/\/[^\s]+/); - // if (urlMatch) saveAgentUrl.value = urlMatch[0]; - // } - // Render default schedule config - // renderScheduleConfig(saveAgentScheduleConfig, 'interval', '{"intervalMinutes": 60}'); - // } -// - // function hideSaveAsAgent() { - // if (saveAgentSection) saveAgentSection.style.display = 'none'; - // if (saveAgentFields) { saveAgentFields.style.display = 'none'; saveAgentFields.classList.remove('dash-save-expanded'); } - // if (saveAgentTrigger) saveAgentTrigger.classList.remove('expanded'); - // } -// - // function submitSaveAsAgent() { - // var name = saveAgentName ? saveAgentName.value.trim() : ''; - // var url = saveAgentUrl ? saveAgentUrl.value.trim() : ''; - // if (!name || !url) return; -// - // var scheduleType = 'interval'; - // var saveSchedulePills = saveAgentSection ? saveAgentSection.querySelectorAll('.dash-schedule-pill') : []; - // saveSchedulePills.forEach(function (p) { if (p.classList.contains('active')) scheduleType = p.getAttribute('data-type'); }); - // var scheduleConfig = getScheduleConfig(saveAgentScheduleConfig, scheduleType); -// - // if (saveAgentBtn) { saveAgentBtn.disabled = true; saveAgentBtn.innerHTML = ' Saving...'; } -// - // var agentId = 'agent_' + Date.now().toString(36) + '_' + Math.random().toString(36).substring(2, 8); -// - // apiFetch('/api/agents', { - // method: 'POST', - // headers: { 'Content-Type': 'application/json', 'X-FSB-Hash-Key': hashKey }, - // body: JSON.stringify({ - // agentId: agentId, - // name: name, - // task: taskText, - // targetUrl: url, - // scheduleType: scheduleType, - // scheduleConfig: JSON.stringify(scheduleConfig), - // enabled: true - // }) - // }).then(function () { - // hideSaveAsAgent(); - // loadData(); - // if (saveAgentBtn) { saveAgentBtn.disabled = false; saveAgentBtn.textContent = 'Save Agent'; } - // if (window.showToast) showToast('Agent created'); - // }).catch(function () { - // if (saveAgentBtn) { saveAgentBtn.disabled = false; saveAgentBtn.textContent = 'Save Agent'; } - // }); - // } - - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // function renderModeBadge(mode) { - // if (mode === 'replay') { - // return 'Replay'; - // } - // if (mode === 'ai_fallback') { - // return 'AI Fallback'; - // } - // return 'AI'; - // } -// // --- DOM Preview --- function setPreviewLoadingText(text) { @@ -4030,14 +3146,6 @@ setTaskRecoveryPending(true, 'extension-online'); } updateTaskOfflineState(); - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // Update agent count area to show extension status - // if (agentCountEl) { - // var countText = (stats.totalAgents || 0) + ' agent' + - // ((stats.totalAgents || 0) !== 1 ? 's' : ''); - // agentCountEl.textContent = countText + - // (extensionOnline ? '' : ' - extension offline'); - // } if (!wasExtensionOnline && extensionOnline) { scheduleStreamRecovery('extension-online'); } @@ -4051,8 +3159,6 @@ var snapshot = msg.payload || {}; var snapshotIntentActive = snapshot.streamIntentActive !== false; extensionOnline = true; - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // loadData(); // Refresh dashboard data on extension reconnect recordDashboardTransportEvent('snapshot-recovered', { type: 'ext:snapshot', status: snapshot.streamStatus || '', @@ -4108,47 +3214,6 @@ return; } - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // Agent run progress from extension - // if (msg.type === 'ext:agent-run-progress') { - // var rp = msg.payload || {}; - // if (rp.agentId === agentRunningId) { - // if (detailRunBar) detailRunBar.style.width = (rp.progress || 0) + '%'; - // if (detailRunAction) detailRunAction.textContent = rp.action || 'Working...'; - // } - // return; - // } -// - // Agent run complete from extension - // if (msg.type === 'ext:agent-run-complete') { - // var rc = msg.payload || {}; - // agentRunningId = null; - // renderAgents(); // Remove spinner -// - // Update detail panel if showing this agent - // if (rc.agentId === detailAgentId) { - // if (detailRunNow) { detailRunNow.disabled = false; detailRunNow.textContent = 'Run Now'; } - // if (detailRunProgress) { - // if (detailRunBar) { - // detailRunBar.style.width = '100%'; - // detailRunBar.className = 'dash-task-bar-fill ' + (rc.success ? 'dash-task-bar-success' : 'dash-task-bar-failed'); - // } - // if (detailRunAction) detailRunAction.textContent = rc.success ? 'Complete' : (rc.error || 'Failed'); - // Auto-hide progress bar after 3 seconds - // setTimeout(function () { - // if (detailRunProgress) detailRunProgress.style.display = 'none'; - // }, 3000); - // } - // Reload detail panel data - // loadAgentStats(detailAgentId); - // loadDetailRuns(detailAgentId, 0); - // } -// - // Refresh grid data - // loadData(); - // return; - // } - if (msg.type === 'ext:dom-snapshot') { handleDOMSnapshot(msg.payload); return; @@ -4246,33 +3311,9 @@ return; } - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md - // Agent/run events from REST API broadcasts - // if (msg.type === 'agent_updated' || msg.type === 'agent_deleted' || msg.type === 'run_completed') { - // loadData(); - // if (msg.agentId && msg.agentId === detailAgentId) { - // loadAgentStats(detailAgentId); - // loadDetailRuns(detailAgentId, 0); - // } - // } } // --- Polling Fallback --- - // DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md -// - // function startPolling() { - // stopPolling(); - // pollTimer = setInterval(function () { - // loadData(); - // }, POLL_INTERVAL); - // } -// - // function stopPolling() { - // if (pollTimer) { - // clearInterval(pollTimer); - // pollTimer = null; - // } - // } // --- API Helpers --- diff --git a/showcase/server/server.js b/showcase/server/server.js index 0b6d63627..3316eae50 100644 --- a/showcase/server/server.js +++ b/showcase/server/server.js @@ -13,7 +13,6 @@ const { initializeDatabase } = require('./src/db/schema'); const Queries = require('./src/db/queries'); const authMiddleware = require('./src/middleware/auth'); const createAuthRouter = require('./src/routes/auth'); -const createAgentsRouter = require('./src/routes/agents'); const createPairRouter = require('./src/routes/pair'); const { setupWSHandler } = require('./src/ws/handler'); const { RELAY_PER_MESSAGE_LIMIT_BYTES } = require('./src/ws/phantomstream-relay-compat'); @@ -117,20 +116,11 @@ app.use('/api/auth', createAuthRouter(queries)); // Routes - protected (require hash key) const auth = authMiddleware(queries); -app.use('/api/agents', auth, createAgentsRouter(queries)); app.use('/api/pair', createPairRouter(queries, auth)); -app.use('/api/stats', auth, (req, res) => { - try { - const stats = queries.getAgentStats(req.hashKey); - res.json(stats); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); // Phase 273 / INGEST-01..13 -- anonymous telemetry ingest (PUBLIC, no auth). // Mount order: AFTER trust-proxy line (top of file), AFTER existing routes -// (no path overlap with /api/auth /api/agents /api/pair /api/stats), +// (no path overlap with /api/auth /api/pair), // BEFORE static-file middleware so the static handler doesn't shadow /api/telemetry/*. const createTelemetryRouter = require('./src/routes/telemetry'); const { hashIp } = require('./src/utils/telemetry-hash'); diff --git a/showcase/server/src/db/queries.js b/showcase/server/src/db/queries.js index c2bd72012..e8ae011b5 100644 --- a/showcase/server/src/db/queries.js +++ b/showcase/server/src/db/queries.js @@ -31,79 +31,6 @@ class Queries { "UPDATE hash_keys SET last_seen_at = datetime('now') WHERE hash_key = ?" ); - // Agents - this.upsertAgent = this.db.prepare(` - INSERT INTO agents (hash_key, agent_id, name, task, start_mode, target_url, schedule_type, schedule_config, enabled) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(hash_key, agent_id) DO UPDATE SET - name = excluded.name, - task = excluded.task, - start_mode = excluded.start_mode, - target_url = excluded.target_url, - schedule_type = excluded.schedule_type, - schedule_config = excluded.schedule_config, - enabled = excluded.enabled, - updated_at = datetime('now') - `); - - this.getAgents = this.db.prepare( - 'SELECT * FROM agents WHERE hash_key = ? ORDER BY created_at DESC' - ); - - this.getAgent = this.db.prepare( - 'SELECT * FROM agents WHERE hash_key = ? AND agent_id = ?' - ); - - this.deleteAgent = this.db.prepare( - 'DELETE FROM agents WHERE hash_key = ? AND agent_id = ?' - ); - - // Agent runs - this.insertRun = this.db.prepare(` - INSERT INTO agent_runs (hash_key, agent_id, run_id, started_at, completed_at, status, result, error, iterations, tokens_used, cost_usd, duration_ms, execution_mode, cost_saved) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - - this.getRuns = this.db.prepare(` - SELECT * FROM agent_runs - WHERE hash_key = ? AND agent_id = ? - ORDER BY completed_at DESC - LIMIT ? OFFSET ? - `); - - this.getRunCount = this.db.prepare( - 'SELECT COUNT(*) as count FROM agent_runs WHERE hash_key = ? AND agent_id = ?' - ); - - this.getRecentRuns = this.db.prepare(` - SELECT * FROM agent_runs - WHERE hash_key = ? - ORDER BY completed_at DESC - LIMIT ? - `); - - // Stats - this.getStats = this.db.prepare(` - SELECT - COUNT(DISTINCT agent_id) as total_agents, - COUNT(*) as total_runs, - SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successful_runs, - SUM(tokens_used) as total_tokens, - SUM(cost_usd) as total_cost, - SUM(cost_saved) as total_cost_saved, - SUM(duration_ms) as total_duration - FROM agent_runs - WHERE hash_key = ? - `); - - this.getTodayStats = this.db.prepare(` - SELECT - COUNT(*) as runs_today, - SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successful_today - FROM agent_runs - WHERE hash_key = ? AND completed_at >= datetime('now', '-1 day') - `); - // Pairing tokens this.insertPairingToken = this.db.prepare( 'INSERT INTO pairing_tokens (token, hash_key, expires_at) VALUES (?, ?, ?)' @@ -127,24 +54,6 @@ class Queries { 'DELETE FROM pairing_tokens WHERE session_token = ?' ); - // Agent toggle - this.updateAgentEnabled = this.db.prepare( - "UPDATE agents SET enabled = ?, updated_at = datetime('now') WHERE hash_key = ? AND agent_id = ?" - ); - - // Per-agent stats - this.getAgentPerStats = this.db.prepare(` - SELECT - COUNT(*) as total_runs, - SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successful_runs, - SUM(CASE WHEN execution_mode = 'replay' THEN 1 ELSE 0 END) as replay_runs, - SUM(CASE WHEN execution_mode = 'ai_fallback' THEN 1 ELSE 0 END) as ai_fallback_runs, - SUM(tokens_used) as tokens_saved, - SUM(cost_saved) as cost_saved - FROM agent_runs - WHERE hash_key = ? AND agent_id = ? - `); - // ----------------------------------------------------------------------- // Phase 273 / INGEST-07/08/11/12 -- anonymous telemetry pipeline statements. // Additive; existing statements untouched. Uses INSERT OR IGNORE on event_id @@ -567,70 +476,6 @@ class Queries { return row || null; } - // Agent operations - upsertAgentData(hashKey, data) { - return this.upsertAgent.run( - hashKey, data.agentId, data.name, data.task, - data.startMode || 'pinned', - data.targetUrl || '', - data.scheduleType, data.scheduleConfig || '{}', - data.enabled ? 1 : 0 - ); - } - - listAgents(hashKey) { - return this.getAgents.all(hashKey); - } - - getAgentData(hashKey, agentId) { - return this.getAgent.get(hashKey, agentId); - } - - removeAgent(hashKey, agentId) { - return this.deleteAgent.run(hashKey, agentId); - } - - toggleAgentEnabled(hashKey, agentId, enabled) { - return this.updateAgentEnabled.run(enabled ? 1 : 0, hashKey, agentId); - } - - getPerAgentStats(hashKey, agentId) { - const row = this.getAgentPerStats.get(hashKey, agentId); - return { - totalRuns: row.total_runs || 0, - successfulRuns: row.successful_runs || 0, - replayRuns: row.replay_runs || 0, - aiFallbackRuns: row.ai_fallback_runs || 0, - tokensSaved: row.tokens_saved || 0, - costSaved: Math.round((row.cost_saved || 0) * 10000) / 10000 - }; - } - - // Run operations - recordRun(hashKey, agentId, run) { - return this.insertRun.run( - hashKey, agentId, run.runId, - run.startedAt, run.completedAt, - run.status, run.result, run.error, - run.iterations || 0, run.tokensUsed || 0, - run.costUsd || 0, run.durationMs || 0, - run.executionMode || 'ai_initial', - run.costSaved || 0 - ); - } - - listRuns(hashKey, agentId, limit = 20, offset = 0) { - return this.getRuns.all(hashKey, agentId, limit, offset); - } - - countRuns(hashKey, agentId) { - return this.getRunCount.get(hashKey, agentId).count; - } - - listRecentRuns(hashKey, limit = 50) { - return this.getRecentRuns.all(hashKey, limit); - } - // Pairing token operations createPairingToken(token, hashKey, expiresAt) { return this.insertPairingToken.run(token, hashKey, expiresAt); @@ -660,29 +505,6 @@ class Queries { return this.deleteSessionByToken.run(sessionToken); } - // Stats - getAgentStats(hashKey) { - const allTime = this.getStats.get(hashKey); - const today = this.getTodayStats.get(hashKey); - const agents = this.getAgents.all(hashKey); - - return { - totalAgents: agents.length, - enabledAgents: agents.filter(a => a.enabled).length, - totalRuns: allTime.total_runs || 0, - successfulRuns: allTime.successful_runs || 0, - runsToday: today.runs_today || 0, - successfulToday: today.successful_today || 0, - successRate: allTime.total_runs > 0 - ? Math.round((allTime.successful_runs / allTime.total_runs) * 100) - : 0, - totalTokens: allTime.total_tokens || 0, - totalCost: Math.round((allTime.total_cost || 0) * 10000) / 10000, - totalCostSaved: Math.round((allTime.total_cost_saved || 0) * 10000) / 10000, - totalDuration: allTime.total_duration || 0 - }; - } - // ----------------------------------------------------------------------- // Phase 273 / INGEST-07/08/11/12 -- anonymous telemetry pipeline helpers. // Each method is a small wrapper around the prepared statement; business diff --git a/showcase/server/src/routes/agents.js b/showcase/server/src/routes/agents.js deleted file mode 100644 index 099f6e1c0..000000000 --- a/showcase/server/src/routes/agents.js +++ /dev/null @@ -1,165 +0,0 @@ -const express = require('express'); -const { broadcastToRoom } = require('../ws/handler'); - -function createAgentsRouter(queries) { - const router = express.Router(); - - // GET /api/agents - List all agents - router.get('/', (req, res) => { - try { - const agents = queries.listAgents(req.hashKey); - res.json({ agents }); - } catch (error) { - res.status(500).json({ error: error.message }); - } - }); - - // POST /api/agents - Upsert agent definition - router.post('/', (req, res) => { - try { - const { agentId, name, task, startMode, targetUrl, scheduleType, scheduleConfig, enabled } = req.body; - const effectiveStartMode = startMode || (targetUrl ? 'pinned' : 'ai_routed'); - - if (!agentId || !name || !task) { - return res.status(400).json({ error: 'Missing required fields: agentId, name, task' }); - } - if (effectiveStartMode === 'pinned' && !targetUrl) { - return res.status(400).json({ error: 'Pinned agents require targetUrl' }); - } - - queries.upsertAgentData(req.hashKey, { - agentId, name, task, - startMode: effectiveStartMode, - targetUrl: targetUrl || '', - scheduleType: scheduleType || 'interval', - scheduleConfig: scheduleConfig || '{}', - enabled: enabled !== false - }); - - const agent = queries.getAgentData(req.hashKey, agentId); - broadcastToRoom(req.hashKey, { type: 'agent_updated', agent }); - res.json({ success: true, agent }); - } catch (error) { - res.status(500).json({ error: error.message }); - } - }); - - // DELETE /api/agents/:agentId - Delete agent - router.delete('/:agentId', (req, res) => { - try { - const result = queries.removeAgent(req.hashKey, req.params.agentId); - if (result.changes === 0) { - return res.status(404).json({ error: 'Agent not found' }); - } - broadcastToRoom(req.hashKey, { type: 'agent_deleted', agentId: req.params.agentId }); - res.json({ success: true }); - } catch (error) { - res.status(500).json({ error: error.message }); - } - }); - - // PATCH /api/agents/:agentId - Toggle agent enabled/disabled - router.patch('/:agentId', (req, res) => { - try { - const { enabled } = req.body; - if (typeof enabled !== 'boolean') { - return res.status(400).json({ error: 'Missing required field: enabled (boolean)' }); - } - const result = queries.toggleAgentEnabled(req.hashKey, req.params.agentId, enabled); - if (result.changes === 0) { - return res.status(404).json({ error: 'Agent not found' }); - } - const agent = queries.getAgentData(req.hashKey, req.params.agentId); - broadcastToRoom(req.hashKey, { type: 'agent_updated', agent }); - res.json({ success: true, agent }); - } catch (error) { - res.status(500).json({ error: error.message }); - } - }); - - // POST /api/agents/:agentId/runs - Report run result - router.post('/:agentId/runs', (req, res) => { - try { - const { agentId } = req.params; - const body = req.body; - - // Upsert agent data if provided - if (body.name && body.task && body.targetUrl !== undefined) { - queries.upsertAgentData(req.hashKey, { - agentId, - name: body.name, - task: body.task, - startMode: body.startMode || (body.targetUrl ? 'pinned' : 'ai_routed'), - targetUrl: body.targetUrl || '', - scheduleType: body.scheduleType || 'interval', - scheduleConfig: body.scheduleConfig || '{}', - enabled: body.enabled !== false - }); - } - - // Record the run - const run = body.run || {}; - queries.recordRun(req.hashKey, agentId, { - runId: run.runId || 'run_' + Date.now().toString(36), - startedAt: run.startedAt || new Date().toISOString(), - completedAt: run.completedAt || new Date().toISOString(), - status: run.status || 'unknown', - result: run.result || null, - error: run.error || null, - iterations: run.iterations || 0, - tokensUsed: run.tokensUsed || 0, - costUsd: run.costUsd || 0, - durationMs: run.durationMs || 0, - executionMode: run.executionMode || 'ai_initial', - costSaved: run.costSaved || 0 - }); - - broadcastToRoom(req.hashKey, { - type: 'run_completed', - agentId, - run: run - }); - - res.json({ success: true }); - } catch (error) { - res.status(500).json({ error: error.message }); - } - }); - - // GET /api/agents/:agentId/stats - Per-agent cost savings stats - router.get('/:agentId/stats', (req, res) => { - try { - const stats = queries.getPerAgentStats(req.hashKey, req.params.agentId); - res.json(stats); - } catch (error) { - res.status(500).json({ error: error.message }); - } - }); - - // GET /api/agents/:agentId/runs - Get run history (paginated) - router.get('/:agentId/runs', (req, res) => { - try { - const limit = Math.min(parseInt(req.query.limit) || 20, 100); - const offset = parseInt(req.query.offset) || 0; - const runs = queries.listRuns(req.hashKey, req.params.agentId, limit, offset); - const total = queries.countRuns(req.hashKey, req.params.agentId); - res.json({ runs, total, limit, offset }); - } catch (error) { - res.status(500).json({ error: error.message }); - } - }); - - // GET /api/agents/stats - Aggregate stats - router.get('/stats', (req, res) => { - try { - const stats = queries.getAgentStats(req.hashKey); - res.json(stats); - } catch (error) { - res.status(500).json({ error: error.message }); - } - }); - - return router; -} - -module.exports = createAgentsRouter; diff --git a/skills/fsb/SKILL.md b/skills/fsb/SKILL.md index 9f189c88d..8686be7c2 100644 --- a/skills/fsb/SKILL.md +++ b/skills/fsb/SKILL.md @@ -46,15 +46,15 @@ As of fsb-mcp-server v0.9.0 (FSB milestone v0.9.62) the visual session is IMPLIC - `client` (required, allowlisted) -- badge label such as `OpenClaw`, `Claude`, `Codex`. Freeform strings reject with `BADGE_NOT_ALLOWED`. - `is_final` (optional boolean) -- set `true` on the LAST action of a task to clear the overlay immediately. -The first action call brings up the overlay; each subsequent action call re-arms a 60-second sliding window; `is_final: true` clears immediately; 60 seconds of silence auto-clears. No `start_visual_session` / `end_visual_session` calls are needed -- those tools were REMOVED in v0.9.0 and now return the typed `TOOL_REMOVED` error. Read-only tools (`read_page`, `get_dom_snapshot`, `list_tabs`, ...) do NOT carry the bundle and do NOT re-arm the sliding window. If the first action call hits `NO_OWNED_TAB`, call `open_tab({ url, active: false })` first, then retry. Lifecycle details, the read-tool vs action-tool split, and the typed-error catalogue live in `references/visual-session-lifecycle.md`. +The first action call brings up the overlay; each subsequent action call re-arms a 60-second sliding window; `is_final: true` clears immediately; 60 seconds of silence auto-clears. No `start_visual_session` / `end_visual_session` calls are needed -- those tools were removed in v0.9.0 and are no longer registered. Read-only tools (`read_page`, `get_dom_snapshot`, `list_tabs`, ...) do NOT carry the bundle and do NOT re-arm the sliding window. If the first action call hits `NO_OWNED_TAB`, call `open_tab({ url, active: false })` first, then retry. Lifecycle details, the read-tool vs action-tool split, and the typed-error catalogue live in `references/visual-session-lifecycle.md`. -The canonical 36-tool action list, the 15-tool read-only list, and the three typed-error names (`VISUAL_FIELDS_REQUIRED`, `BADGE_NOT_ALLOWED`, `TOOL_REMOVED`) are pinned in `.planning/v0.9.62-CONTRACT.md` -- that artifact is the single source of truth. Use it to answer "does this tool require the field bundle?" by lookup; do not re-derive the lists from memory. +The canonical 36-tool action list, the 15-tool read-only list, and the typed-error names (`VISUAL_FIELDS_REQUIRED`, `BADGE_NOT_ALLOWED`) are pinned in `.planning/v0.9.62-CONTRACT.md` -- that artifact is the single source of truth. Use it to answer "does this tool require the field bundle?" by lookup; do not re-derive the lists from memory. On Hermes: as of fsb-mcp-server v0.9.2 / FSB v0.9.69 (PR #49), the badge label `Hermes` is on the v0.9.36 shared client allowlist, so action calls passing `client: "Hermes"` accept normally. The field bundle (`visual_reason`, `client`, optional `is_final`) works identically; no Hermes-specific schema fork exists. ## Multi-agent contract -Never pass `agent_id` (the server mints it). Use the `back` tool instead of `execute_js("history.back()")`. Each agent owns its own tabs; do not foreground or close tabs you do not own, and do not introduce a global browser lock -- per-agent ownership is the only concurrency mechanism. Typed errors (`NO_OWNED_TAB`, `AMBIGUOUS_TAB`, `TAB_NOT_OWNED`, `AGENT_CAP_REACHED`, `TAB_INCOGNITO_NOT_SUPPORTED`, `TAB_OUT_OF_SCOPE`), the bootstrap recovery ladder, and the default recovery flow: see `references/multi-agent-contract.md`. +Never pass `agent_id` (the server mints it). Use `go_back` instead of `execute_js("history.back()")`. Each agent owns its own tabs; do not foreground or close tabs you do not own, and do not introduce a global browser lock -- per-agent ownership is the only concurrency mechanism. Typed errors (`NO_OWNED_TAB`, `AMBIGUOUS_TAB`, `TAB_NOT_OWNED`, `AGENT_CAP_REACHED`, `TAB_INCOGNITO_NOT_SUPPORTED`, `TAB_OUT_OF_SCOPE`), the bootstrap recovery ladder, and the default recovery flow: see `references/multi-agent-contract.md`. ## Vault and credentials diff --git a/skills/fsb/USAGE.md b/skills/fsb/USAGE.md index 3e61d3f57..56f22c2cf 100644 --- a/skills/fsb/USAGE.md +++ b/skills/fsb/USAGE.md @@ -1,6 +1,6 @@ # FSB Skill -- Usage -> **v0.9.0 breaking change** -- The explicit `start_visual_session` / `end_visual_session` tools were REMOVED in fsb-mcp-server v0.9.0 (FSB milestone v0.9.62). Action tools now require `visual_reason` + `client` fields in every call (validated against the v0.9.36 badge allowlist). The visual session is created implicitly on the first action call, refreshed on a sliding 60-second window, and cleared by `is_final: true` (immediate) or 60 seconds of silence (auto-clear). Calling the removed tools returns the typed `TOOL_REMOVED` error. See [`mcp/CHANGELOG.md`](../../mcp/CHANGELOG.md#v0.9.0) and [`mcp/README.md`](../../mcp/README.md#visual-session-lifecycle) for the migration recipe with concrete before/after code. +> **v0.9.0 breaking change** -- The explicit `start_visual_session` / `end_visual_session` tools were REMOVED in fsb-mcp-server v0.9.0 (FSB milestone v0.9.62). Action tools now require `visual_reason` + `client` fields in every call (validated against the v0.9.36 badge allowlist). The visual session is created implicitly on the first action call, refreshed on a sliding 60-second window, and cleared by `is_final: true` (immediate) or 60 seconds of silence (auto-clear). The removed names are no longer registered at all. See [`mcp/CHANGELOG.md`](../../mcp/CHANGELOG.md#v0.9.0) and [`mcp/README.md`](../../mcp/README.md#visual-session-lifecycle) for the migration recipe with concrete before/after code. FSB drives the user's real Chrome via the FSB extension and a local MCP bridge so OpenClaw can run live web tasks (clicks, typing, multi-tab flows, auth-gated reads). @@ -121,7 +121,6 @@ Three typed errors guard the contract; match on the name, not the human-readable - **`VISUAL_FIELDS_REQUIRED`** -- raised when an action call is missing `visual_reason` or `client`. Schema-layer reject; no DOM mutation occurs. - **`BADGE_NOT_ALLOWED`** -- raised when `client` is not on the v0.9.36 allowlist. Use one of the labels above. -- **`TOOL_REMOVED`** -- raised when a caller invokes `start_visual_session` or `end_visual_session` by name. The body names the new contract; do not retry with the removed tool names. ### Autopilot exception diff --git a/skills/fsb/references/multi-agent-contract.md b/skills/fsb/references/multi-agent-contract.md index 5adc7174e..bea770d9a 100644 --- a/skills/fsb/references/multi-agent-contract.md +++ b/skills/fsb/references/multi-agent-contract.md @@ -1,6 +1,6 @@ # FSB multi-agent contract (current as of v0.9.92) -This file documents the rules that make FSB's per-agent tab ownership work. Anyone calling FSB tools through MCP MUST follow these rules; breaking them produces typed errors that recover cleanly only when the caller knows the contract. The contract is small (six error names + the `agent_id` rule + the `back` tool), but every rule matters: bypassing one corrupts the ownership graph and the only recovery is closing the affected tab. +This file documents the rules that make FSB's per-agent tab ownership work. Anyone calling FSB tools through MCP MUST follow these rules; breaking them produces typed errors that recover cleanly only when the caller knows the contract. The contract is small (six error names + the `agent_id` rule), but every rule matters: bypassing one corrupts the ownership graph and the only recovery is closing the affected tab. ## Tab ownership behavior (v0.8.0+) @@ -54,22 +54,22 @@ When an FSB tool call hits one of the dispatcher boundaries, the server returns - What it means: The targeted tab is in a different Chrome window than the agent's session window. Each agent only sees tabs in the window where its session began. - What to do: Use `list_tabs` to enumerate this agent's owned tabs and pick one; if the work belongs in a different window, open a new tab in the current window with `open_tab`. -## Use back instead of execute_js('history.back()') +## Use go_back instead of execute_js('history.back()') -Use the typed `back` tool (FSB v0.8.0+) for one-step history navigation. Do NOT call `execute_js("history.back()")`. +Use `go_back` for one-step history navigation, and `go_forward` to undo it. Do NOT call `execute_js("history.back()")`. -`back` is typed, ownership-gated, BF-cache aware, and history-depth-tracking. It returns a structured result with status fields the caller can branch on. `execute_js("history.back()")` is none of those: it bypasses ownership, lies about success on BF-cached pages, and returns whatever the JS evaluator coerced last. +`go_back` is ownership-gated, carries the `visual_reason`/`client` field bundle like other action tools, and returns the resulting URL. `execute_js("history.back()")` bypasses ownership, lies about success on BF-cached pages, and returns whatever the JS evaluator coerced last. ``` [BAD] execute_js("history.back()") -[GOOD] back({}) +[GOOD] go_back({ visual_reason: "Return to results", client: "OpenClaw" }) ``` -Note: `go_back` (`mcp/ai/tool-definitions.cjs`, part of the 37-tool action-tool field-bundle contract) and `back` (`mcp/src/tools/agents.ts`, outside that contract) are two distinct tools that coexist in the current build, not version alternates for the same surface. `go_back` carries the `visual_reason`/`client` field bundle like other action tools; `back` does not, but adds a richer `change_report` (URL, dialogs, node/attribute/input changes, focus shift) plus explicit `status` values (`ok`, `no_history`, `cross_origin`, `bf_cache`, `fragment_only`). Prefer `back` for its structured result. Both are typed, ownership-gated, and BF-cache aware; never fall back to `execute_js`. +The separate `back` tool that older builds registered was removed; it duplicated `go_back`. ## Why the contract matters -FSB's per-agent ownership is what lets multiple AI sessions share one Chrome instance without fighting. The contract is small (six error names plus `agent_id` plus `back`), but every rule matters -- bypassing one rule corrupts the ownership graph and the only recovery is closing the affected tab. +FSB's per-agent ownership is what lets multiple AI sessions share one Chrome instance without fighting. The contract is small (six error names plus `agent_id`), but every rule matters -- bypassing one rule corrupts the ownership graph and the only recovery is closing the affected tab. ## Default recovery ladder diff --git a/skills/fsb/references/tool-decision-tree.md b/skills/fsb/references/tool-decision-tree.md index 97a1280e5..136e35d0e 100644 --- a/skills/fsb/references/tool-decision-tree.md +++ b/skills/fsb/references/tool-decision-tree.md @@ -107,7 +107,7 @@ Use autopilot ONLY when the user explicitly delegates the whole task ("use FSB a ## See also - `references/visual-session-lifecycle.md` -- v0.9.62 implicit contract, field bundle, sliding window, typed errors. -- `references/multi-agent-contract.md` -- typed errors and the `back` tool. +- `references/multi-agent-contract.md` -- typed errors and history navigation. - `references/restricted-tab-recovery.md` -- DOM tools fail on `chrome://`, `edge://`, and the Web Store. - `references/default-to-fsb.md` -- when to prefer FSB versus WebFetch. - `references/vault-boundary.md` -- credential-routed tools. diff --git a/skills/fsb/references/v0.9.62-contract-mirror.md b/skills/fsb/references/v0.9.62-contract-mirror.md index 452ed93e7..df889f1bf 100644 --- a/skills/fsb/references/v0.9.62-contract-mirror.md +++ b/skills/fsb/references/v0.9.62-contract-mirror.md @@ -74,4 +74,4 @@ Count: 15. These tools never carry the field bundle and never re-arm the 60-seco - `VISUAL_FIELDS_REQUIRED` -- raised when an action tool is missing `visual_reason` or `client`. - `BADGE_NOT_ALLOWED` -- raised when `client` is not on the v0.9.36 shared allowlist. Allowed values: Claude, Codex, ChatGPT, Perplexity, Windsurf, Cursor, Antigravity, OpenCode, OpenClaw, Grok, Gemini, Hermes (added v0.9.69 / PR #49). -- `TOOL_REMOVED` -- raised when callers invoke the removed `start_visual_session` / `end_visual_session` tools. The contract is now implicit. +- `TOOL_REMOVED` -- retired together with the error-only `start_visual_session` / `end_visual_session` stubs; those names are no longer registered. The contract is implicit. diff --git a/skills/fsb/references/visual-session-lifecycle.md b/skills/fsb/references/visual-session-lifecycle.md index fae6001fd..d80c838aa 100644 --- a/skills/fsb/references/visual-session-lifecycle.md +++ b/skills/fsb/references/visual-session-lifecycle.md @@ -2,7 +2,7 @@ As of fsb-mcp-server v0.9.0 (FSB milestone v0.9.62) the visual session is IMPLICIT. There is no `start_visual_session` / `end_visual_session` pairing. Every action tool call carries a field bundle, and the lifecycle is driven by those fields alone. This file documents the bundle, the sliding 60-second window, the read-tool vs action-tool split, and the bootstrap path. -If you arrived here from v0.8.0 / v0.9.36 docs that documented an explicit start/end pairing, that pairing was removed in v0.9.0. Calling the removed tools returns the typed `TOOL_REMOVED` error -- the body names the new contract and points at the migration recipe in `mcp/CHANGELOG.md` v0.9.0. +If you arrived here from v0.8.0 / v0.9.36 docs that documented an explicit start/end pairing, that pairing was removed in v0.9.0. The removed names are no longer registered, so calling them fails with the MCP host's unknown-tool error. The migration recipe is in `mcp/CHANGELOG.md` v0.9.0. ## The field bundle @@ -85,9 +85,8 @@ Three typed errors guard the contract. Match on the name, not the human-readable |---------------------------|---------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------| | `VISUAL_FIELDS_REQUIRED` | Action call missing `visual_reason` or `client`. | Add the missing field(s) and retry. Schema-layer reject; no DOM mutation occurs. | | `BADGE_NOT_ALLOWED` | `client` value not on the v0.9.36 allowlist. | Use one of the allowlisted labels listed above (`OpenClaw` for this skill) and retry. | -| `TOOL_REMOVED` | Caller invoked `start_visual_session` or `end_visual_session` by name. | Stop calling the removed tools. Migrate to the field bundle on action calls. See migration recipe. | -The three error names are pinned by `.planning/v0.9.62-CONTRACT.md` and stay verbatim across surfaces. +The two error names are pinned by `.planning/v0.9.62-CONTRACT.md` and stay verbatim across surfaces. ## Autopilot exception diff --git a/tests/agent-manager-start-mode.test.js b/tests/agent-manager-start-mode.test.js deleted file mode 100644 index 100794561..000000000 --- a/tests/agent-manager-start-mode.test.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Tests for BackgroundAgentManager start-mode handling. - * Run: node tests/agent-manager-start-mode.test.js - */ - -const fs = require('fs'); -const vm = require('vm'); - -const storage = {}; -globalThis.chrome = { - storage: { - local: { - async get(key) { - if (typeof key === 'string') { - return { [key]: storage[key] }; - } - return {}; - }, - async set(update) { - Object.assign(storage, update); - } - } - } -}; - -const src = fs.readFileSync('extension/agents/agent-manager.js', 'utf8'); -vm.runInThisContext(src); - -let passed = 0; -let failed = 0; - -function assert(cond, msg) { - if (cond) { passed++; console.log(' PASS:', msg); } - else { failed++; console.error(' FAIL:', msg); } -} - -(async () => { - console.log('\n--- createAgent tests ---'); - - const pinnedAgent = await agentManager.createAgent({ - name: 'Pinned Agent', - task: 'Check the inbox', - startMode: 'pinned', - targetUrl: 'https://mail.google.com', - schedule: { type: 'interval', intervalMinutes: 30 } - }); - - assert(pinnedAgent.startMode === 'pinned', 'createAgent stores pinned start mode'); - assert(pinnedAgent.targetUrl === 'https://mail.google.com/', 'createAgent normalizes pinned URL'); - - const aiRoutedAgent = await agentManager.createAgent({ - name: 'AI Routed Agent', - task: 'Compare the best flight options to Austin', - startMode: 'ai_routed', - schedule: { type: 'daily', dailyTime: '09:00' } - }); - - assert(aiRoutedAgent.startMode === 'ai_routed', 'createAgent stores ai_routed start mode'); - assert(aiRoutedAgent.targetUrl === '', 'ai_routed agent clears targetUrl'); - - let pinnedError = null; - try { - await agentManager.createAgent({ - name: 'Broken Agent', - task: 'This should fail', - startMode: 'pinned', - schedule: { type: 'once' } - }); - } catch (error) { - pinnedError = error; - } - assert(!!pinnedError, 'createAgent rejects pinned agents without targetUrl'); - - console.log('\n--- updateAgent tests ---'); - - const updated = await agentManager.updateAgent(pinnedAgent.agentId, { - startMode: 'ai_routed' - }); - - assert(updated.startMode === 'ai_routed', 'updateAgent switches start mode to ai_routed'); - assert(updated.targetUrl === '', 'updateAgent clears targetUrl when switching to ai_routed'); - - const normalizedAgents = await agentManager.listAgents(); - const reloadedPinned = normalizedAgents.find(agent => agent.agentId === pinnedAgent.agentId); - assert(reloadedPinned && reloadedPinned.startMode === 'ai_routed', 'listAgents returns normalized startMode'); - - console.log('\n=== Results: ' + passed + ' passed, ' + failed + ' failed ==='); - process.exit(failed > 0 ? 1 : 0); -})().catch((error) => { - console.error('Test harness failed:', error); - process.exit(1); -}); diff --git a/tests/agent-sunset-back-end.test.js b/tests/agent-sunset-back-end.test.js index 9aae8efdb..251595501 100644 --- a/tests/agent-sunset-back-end.test.js +++ b/tests/agent-sunset-back-end.test.js @@ -1,40 +1,33 @@ /** - * Phase 212-01 regression tests + * Background-agents retirement: back-end checks. * - * Verifies the preservation invariants of the background-agents sunset: - * - AGENTS-02: every agent code path carries the canonical deprecation annotation; - * MCP registerAgentTools shell preserved with zero registered tools. - * - AGENTS-05: chrome.storage.local['bgAgents'] is NOT proactively cleaned by Phase 212. - * - AGENTS-06: chrome.alarms.onAlarm listener preserves the MCP_RECONNECT_ALARM early-return - * BYTE-FOR-BYTE, and the Phase 211-02 dom-stream watchdog branch is untouched. + * Background agents were removed in v0.9.45rc1. These checks keep the + * leftovers from coming back and prove the update-time cleanup works: + * - The agent modules, the MCP agent-tools module and every commented-out + * agent code path are gone. + * - On extension update, background.js deletes the saved agent data + * (bgAgents, fsb_sync_queue, the old notice flag) and clears every + * fsb_agent_* alarm, leaving other storage keys and alarms alone. + * - The alarm listener still returns early for the MCP reconnect alarm + * and keeps the dom-stream watchdog branch live. * - * Plain Node + assert. No jsdom, no chrome stubs. Static-analysis on source files. + * Plain Node + assert. Static checks plus one sandboxed run of the cleanup. */ 'use strict'; const fs = require('fs'); const path = require('path'); +const vm = require('vm'); const assert = require('assert'); const ROOT = path.resolve(__dirname, '..'); -const ANNOTATION = '// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md'; +const RETIRED_ANNOTATION = 'DEPRECATED v0.9.45rc1'; function read(rel) { return fs.readFileSync(path.join(ROOT, rel), 'utf8'); } -function countSubstring(haystack, needle) { - if (!needle) return 0; - let count = 0; - let idx = 0; - while ((idx = haystack.indexOf(needle, idx)) !== -1) { - count += 1; - idx += needle.length; - } - return count; -} - let failures = 0; function pass(label) { console.log('PASS - ' + label); } function fail(label, detail) { @@ -43,123 +36,152 @@ function fail(label, detail) { if (detail) console.log(' ' + detail); } -// ---- Section 1: AGENTS-02 canonical annotation present in every modified back-end file ---- +// ---- Section 1: the retired modules are deleted ---- -const annotationFiles = [ +for (const rel of [ 'extension/agents/agent-manager.js', 'extension/agents/agent-scheduler.js', 'extension/agents/agent-executor.js', 'extension/agents/server-sync.js', 'mcp/src/tools/agents.ts', +]) { + if (!fs.existsSync(path.join(ROOT, rel))) pass(rel + ' is deleted'); + else fail(rel + ' still exists'); +} + +{ + const src = read('mcp/src/runtime.ts'); + if (!/registerAgentTools|tools\/agents\.js/.test(src)) pass('runtime.ts no longer imports or calls registerAgentTools'); + else fail('runtime.ts still references the agent tools module'); +} + +// ---- Section 2: no commented-out agent code is left behind ---- + +const sourceFiles = [ 'extension/background.js', 'extension/ws/ws-client.js', + 'extension/ws/mcp-bridge-client.js', + 'extension/ui/popup.js', + 'extension/ui/sidepanel.js', + 'extension/ui/options.js', + 'extension/ui/options.css', + 'extension/ui/control_panel.html', ]; -for (const f of annotationFiles) { - const src = read(f); - const n = countSubstring(src, ANNOTATION); - if (n >= 1) pass('AGENTS-02 annotation present in ' + f + ' (count=' + n + ')'); - else fail('AGENTS-02 annotation MISSING in ' + f); +for (const rel of sourceFiles) { + const src = read(rel); + if (!src.includes(RETIRED_ANNOTATION)) pass('no retired-agent annotation left in ' + rel); + else fail('retired-agent annotation still present in ' + rel); } -// ---- Section 2: AGENTS-02 only `back` tool is LIVE in mcp/src/tools/agents.ts ---- -// Phase 242 (v0.9.60) added the `back` browser-history tool to this file. It is NOT a -// background-agent tool; the AGENTS-02 invariant is "no background-agent tool registrations", -// not "no tool registrations at all". Allow exactly one live registration AND assert it's `back`. +{ + const src = read('extension/background.js'); + const leftovers = ['agentScheduler', 'agentManager', 'agentExecutor', 'serverSync.', 'executeAutomationTask'] + .filter((token) => src.includes(token)); + if (leftovers.length === 0) pass('background.js has no background-agent code paths'); + else fail('background.js still mentions retired agent code', leftovers.join(', ')); +} { - const src = read('mcp/src/tools/agents.ts'); - const lines = src.split('\n'); - const liveServerTool = lines.filter(line => /^[\s]*server\.tool\(/.test(line)); - const allowedSingle = liveServerTool.length === 1; - // Confirm the single live registration is for `back` by scanning the next ~3 lines after the call. - let isBackTool = false; - if (allowedSingle) { - const idx = lines.findIndex(line => /^[\s]*server\.tool\(/.test(line)); - const window = lines.slice(idx, idx + 4).join('\n'); - isBackTool = /['"]back['"]/.test(window); - } - if (allowedSingle && isBackTool) pass('AGENTS-02 only `back` tool live in agents.ts (Phase 242 carve-out)'); - else fail('AGENTS-02 found ' + liveServerTool.length + ' LIVE server.tool() calls in agents.ts (only `back` permitted)', JSON.stringify(liveServerTool.slice(0, 3))); + const html = read('extension/ui/control_panel.html'); + const js = read('extension/ui/options.js'); + const gone = !html.includes('fsbSunsetNotice') && !js.includes('fsbSunsetNotice') && !js.includes('bgAgents'); + if (gone) pass('control panel no longer renders the old agent-names notice'); + else fail('control panel still carries the agent-names notice or reads bgAgents'); } -// ---- Section 3: AGENTS-02 registerAgentTools function shell preserved in runtime.ts (D-16) ---- +// ---- Section 3: the update-time cleanup ---- + +const bgSource = read('extension/background.js'); { - const src = read('mcp/src/runtime.ts'); - const hasImport = /^\s*import\s*\{\s*registerAgentTools\s*\}\s*from\s*['"]\.\/tools\/agents\.js['"]/m.test(src); - // Phase 238 added agentScope as a 4th argument; signature is now (server, bridge, queue, agentScope). - const hasCall = /^\s*registerAgentTools\(server,\s*bridge,\s*queue(?:,\s*agentScope)?\)\s*;/m.test(src); - if (hasImport && hasCall) pass('AGENTS-02 registerAgentTools import + call preserved in runtime.ts'); - else fail('AGENTS-02 registerAgentTools shell broken (import=' + hasImport + ', call=' + hasCall + ')'); + const start = bgSource.indexOf('const RETIRED_AGENT_STORAGE_KEYS'); + const end = bgSource.indexOf('chrome.runtime.onInstalled.addListener(', start); + if (start === -1 || end === -1) { + fail('background.js defines the retired-agent cleanup before the onInstalled listener'); + } else { + pass('background.js defines the retired-agent cleanup before the onInstalled listener'); + const cleanupSource = bgSource.slice(start, end); + const removedKeys = []; + const clearedAlarms = []; + const sandbox = { + console, + chrome: { + storage: { + local: { + remove: async (keys) => { removedKeys.push(...[].concat(keys)); }, + }, + }, + alarms: { + getAll: async () => [ + { name: 'fsb_agent_daily-digest' }, + { name: 'fsb_agent_price-watch' }, + { name: 'fsb-mcp-bridge-reconnect' }, + { name: 'fsb-telemetry-beat' }, + { name: 'fsbTrigger:abc' }, + ], + clear: async (name) => { clearedAlarms.push(name); return true; }, + }, + }, + }; + vm.createContext(sandbox); + vm.runInContext(cleanupSource + '\nthis.__cleanup = clearRetiredBackgroundAgentData;', sandbox); + + sandbox.__cleanup().then(() => { + assert.deepStrictEqual(removedKeys.slice().sort(), + ['bgAgents', 'fsb_sunset_notice_dismissed', 'fsb_sync_queue']); + pass('cleanup removes bgAgents, fsb_sync_queue and the old notice flag'); + assert.deepStrictEqual(clearedAlarms.slice().sort(), + ['fsb_agent_daily-digest', 'fsb_agent_price-watch']); + pass('cleanup clears only fsb_agent_* alarms'); + }).catch((error) => { + fail('cleanup run', error && error.message); + }).finally(finish); + } } -// ---- Section 4: AGENTS-06 MCP_RECONNECT_ALARM early-return preserved BYTE-FOR-BYTE ---- - { - const src = read('extension/background.js'); - // The exact byte-for-byte substring (matches lines 12579-12582 indentation in current file) - const expected = ' if (isMcpReconnectAlarm) {\n armMcpBridge(\'alarm:\' + MCP_RECONNECT_ALARM);\n return;\n }'; - if (src.indexOf(expected) !== -1) pass('AGENTS-06 MCP_RECONNECT_ALARM early-return byte-for-byte preserved'); - else fail('AGENTS-06 MCP_RECONNECT_ALARM early-return path mutated or missing', 'Expected substring not found'); + const listenerStart = bgSource.indexOf('chrome.runtime.onInstalled.addListener(async (details) => {'); + const listenerEnd = bgSource.indexOf('\n});', listenerStart); + const listener = listenerStart === -1 ? '' : bgSource.slice(listenerStart, listenerEnd); + if (/if \(details && details\.reason === 'update'\) \{\s*await clearRetiredBackgroundAgentData\(\);/.test(listener)) { + pass('onInstalled runs the cleanup on extension update only'); + } else { + fail('onInstalled does not gate the cleanup on reason === "update"'); + } } -// ---- Section 5: AGENTS-06 Phase 211-02 dom-stream watchdog branch preserved on a LIVE line ---- +// ---- Section 4: MCP_RECONNECT_ALARM early-return preserved byte-for-byte ---- { - const src = read('extension/background.js'); - const lines = src.split('\n'); - const hits = lines.filter(line => /if\s*\(alarm\.name\s*===\s*'fsb-domstream-watchdog'\)/.test(line)); - const liveHits = hits.filter(line => !/^\s*\/\//.test(line)); - if (liveHits.length === 1) pass('AGENTS-06 Phase 211-02 dom-stream watchdog branch is LIVE (and unique)'); - else fail('AGENTS-06 expected exactly 1 LIVE dom-stream watchdog hit, found ' + liveHits.length); + const expected = ' if (isMcpReconnectAlarm) {\n armMcpBridge(\'alarm:\' + MCP_RECONNECT_ALARM);\n return;\n }'; + if (bgSource.indexOf(expected) !== -1) pass('MCP_RECONNECT_ALARM early-return byte-for-byte preserved'); + else fail('MCP_RECONNECT_ALARM early-return path mutated or missing', 'Expected substring not found'); } -// ---- Section 6: AGENTS-05 no chrome.storage cleanup of bgAgents in modified files ---- - -const cleanupTargets = [ - 'extension/background.js', - 'extension/agents/agent-manager.js', - 'extension/agents/agent-scheduler.js', - 'extension/agents/agent-executor.js', - 'extension/agents/server-sync.js', - 'extension/ws/ws-client.js', -]; +// ---- Section 5: dom-stream watchdog branch preserved on a live line ---- { - let violations = []; - for (const f of cleanupTargets) { - const src = read(f); - const lines = src.split('\n'); - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (/^\s*\/\//.test(line)) continue; // skip commented lines - - if (/chrome\.storage\.local\.remove\([^)]*bgAgents/.test(line)) { - violations.push(f + ':' + (i + 1) + ' chrome.storage.local.remove(bgAgents)'); - } - if (/chrome\.storage\.local\.set\(\s*\{[^}]*bgAgents\s*:/.test(line)) { - violations.push(f + ':' + (i + 1) + ' chrome.storage.local.set({ bgAgents: ... })'); - } - if (/chrome\.alarms\.clear\([^)]*['"]fsb_agent_/.test(line)) { - violations.push(f + ':' + (i + 1) + ' chrome.alarms.clear with fsb_agent_ prefix'); - } - } - } - if (violations.length === 0) pass('AGENTS-05 no LIVE bgAgents cleanup or fsb_agent_ alarm clear in modified files'); - else fail('AGENTS-05 found ' + violations.length + ' violation(s)', violations.slice(0, 5).join('; ')); + const lines = bgSource.split('\n'); + const hits = lines.filter(line => /if\s*\(alarm\.name\s*===\s*'fsb-domstream-watchdog'\)/.test(line)); + const liveHits = hits.filter(line => !/^\s*\/\//.test(line)); + if (liveHits.length === 1) pass('dom-stream watchdog branch is live (and unique)'); + else fail('expected exactly 1 live dom-stream watchdog hit, found ' + liveHits.length); } // ---- Result ---- -assert.strictEqual(typeof failures, 'number'); - -if (failures === 0) { - console.log(''); - console.log('All Phase 212-01 regression checks PASSED'); - process.exit(0); -} else { +let finished = false; +function finish() { + if (finished) return; + finished = true; console.log(''); + if (failures === 0) { + console.log('All background-agent retirement checks PASSED'); + process.exit(0); + } console.log(failures + ' check(s) FAILED'); process.exit(1); } + +if (bgSource.indexOf('const RETIRED_AGENT_STORAGE_KEYS') === -1) finish(); diff --git a/tests/agent-sunset-control-panel.test.js b/tests/agent-sunset-control-panel.test.js index e9f3a84a8..f2272b549 100644 --- a/tests/agent-sunset-control-panel.test.js +++ b/tests/agent-sunset-control-panel.test.js @@ -1,14 +1,13 @@ /** - * Phase 212-02 regression tests + * Background-agents retirement: control panel and chat UI checks. * - * Verifies the deprecation notice, sunset notice, and slash-command commenting: - * - AGENTS-01: deprecation notice with both CTAs (target=_blank rel=noopener noreferrer) - * and a footer naming v0.9.45rc1 plus April 2026. - * - AGENTS-02 (UI portion): canonical annotations on every commented agent code path; - * Server Sync + pairing wiring at ui/options.js:4189-4205 stays LIVE - * because Phase 213 relocates it. - * - AGENTS-03: names list scaffolding present and rendering function defined; - * names rendered via textContent only (T-01 mitigation against XSS). + * - The Sync tab keeps its note about the retired agents, with both + * links (target=_blank rel=noopener noreferrer) and a footer naming + * v0.9.45rc1 and April 2026. + * - The commented-out agent UI (agent forms, /agent slash commands) and + * the old "Your previous agents" notice are gone; nothing in the UI + * reads bgAgents any more. + * - Server Sync and pairing wiring stays live. * * Plain Node + assert + fs. No browser, no chrome stubs. */ @@ -19,7 +18,7 @@ const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); -const ANNOTATION = '// DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md'; +const RETIRED_ANNOTATION = 'DEPRECATED v0.9.45rc1'; function read(rel) { return fs.readFileSync(path.join(ROOT, rel), 'utf8'); @@ -46,23 +45,6 @@ function extractDeprecationFooterTexts(source) { return footers; } -/** Returns the substring of source between the line that opens `signature` and its matching closing brace. */ -function extractFunctionBody(source, signature) { - const startIdx = source.indexOf(signature); - if (startIdx === -1) return null; - const openIdx = source.indexOf('{', startIdx); - if (openIdx === -1) return null; - let depth = 1; - let i = openIdx + 1; - while (i < source.length && depth > 0) { - const ch = source[i]; - if (ch === '{') depth += 1; - else if (ch === '}') depth -= 1; - i += 1; - } - return source.slice(openIdx, i); -} - let failures = 0; function pass(label) { console.log('PASS - ' + label); } function fail(label, detail) { @@ -115,32 +97,23 @@ const emojiTargets = [ else fail('Emojis detected in modified UI files', violations.join('; ')); } -// ---- Section 3: AGENTS-02 (UI portion) annotation counts ---- +// ---- Section 3: the commented-out agent UI is gone ---- { - const opts = read('extension/ui/options.js'); - const sp = read('extension/ui/sidepanel.js'); - const pop = read('extension/ui/popup.js'); - const css = read('extension/ui/options.css'); - - const optsCount = countSubstring(opts, ANNOTATION); - const spCount = countSubstring(sp, ANNOTATION); - const popCount = countSubstring(pop, ANNOTATION); - // CSS uses block-comment syntax `/* ... */`, so match on the annotation text without the JS `//` prefix. - const ANNOTATION_TEXT = 'DEPRECATED v0.9.45rc1: superseded by OpenClaw / Claude Routines -- see PROJECT.md'; - const cssCount = countSubstring(css, ANNOTATION_TEXT); - - if (optsCount >= 11) pass('AGENTS-02 ui/options.js annotation count >= 11 (got ' + optsCount + ')'); - else fail('AGENTS-02 ui/options.js annotation count too low (got ' + optsCount + ', expected >= 11)'); - - if (spCount >= 2) pass('AGENTS-02 ui/sidepanel.js annotation count >= 2 (got ' + spCount + ')'); - else fail('AGENTS-02 ui/sidepanel.js annotation count too low (got ' + spCount + ', expected >= 2)'); - - if (popCount >= 2) pass('AGENTS-02 ui/popup.js annotation count >= 2 (got ' + popCount + ')'); - else fail('AGENTS-02 ui/popup.js annotation count too low (got ' + popCount + ', expected >= 2)'); - - if (cssCount >= 1) pass('AGENTS-02 ui/options.css annotation count >= 1 (got ' + cssCount + ')'); - else fail('AGENTS-02 ui/options.css annotation missing (got ' + cssCount + ')'); + const files = { + 'extension/ui/options.js': read('extension/ui/options.js'), + 'extension/ui/sidepanel.js': read('extension/ui/sidepanel.js'), + 'extension/ui/popup.js': read('extension/ui/popup.js'), + 'extension/ui/options.css': read('extension/ui/options.css'), + }; + for (const [rel, src] of Object.entries(files)) { + if (countSubstring(src, RETIRED_ANNOTATION) === 0) pass('no retired-agent annotation left in ' + rel); + else fail('retired-agent annotation still present in ' + rel); + } + for (const rel of ['extension/ui/sidepanel.js', 'extension/ui/popup.js']) { + if (!/handleAgentCommand|startsWith\('\/agent'\)/.test(files[rel])) pass(rel + ' has no /agent slash-command code'); + else fail(rel + ' still carries /agent slash-command code'); + } } // ---- Section 4: D-15 -- Server Sync + pairing wiring at ui/options.js:4189-4205 stays LIVE ---- @@ -165,75 +138,27 @@ const emojiTargets = [ else fail('D-15 Server Sync + pairing wiring broken', violations.join('; ')); } -// ---- Section 5: AGENTS-03 names list scaffolding + rendering function present ---- +// ---- Section 5: the old agent-names notice is gone ---- { const html = read('extension/ui/control_panel.html'); const opts = read('extension/ui/options.js'); - const ids = ['fsbSunsetNotice', 'fsbSunsetNoticeNames', 'fsbSunsetNoticeDismiss']; - let missing = []; - for (const id of ids) { - if (html.indexOf('id="' + id + '"') === -1) missing.push('control_panel.html missing id="' + id + '"'); - } - const fnDef = /function initializeBackgroundAgentsDeprecation\s*\(/.test(opts); - const fnCall = /initializeBackgroundAgentsDeprecation\s*\(\)\s*;/.test(opts); - if (missing.length === 0 && fnDef && fnCall) { - pass('AGENTS-03 names list scaffolding + rendering function present and called'); - } else { - fail('AGENTS-03 scaffolding incomplete', 'missing=' + JSON.stringify(missing) + ' fnDef=' + fnDef + ' fnCall=' + fnCall); - } -} - -// ---- Section 6: T-01 -- XSS mitigation: function uses textContent, NOT innerHTML, for agent names ---- - -{ - const opts = read('extension/ui/options.js'); - const body = extractFunctionBody(opts, 'function initializeBackgroundAgentsDeprecation'); - if (!body) { - fail('T-01 could not extract initializeBackgroundAgentsDeprecation body'); - } else { - const usesTextContent = body.indexOf('.textContent =') !== -1; - const usesInnerHTML = body.indexOf('.innerHTML =') !== -1 || body.indexOf('.innerHTML=') !== -1; - if (usesTextContent && !usesInnerHTML) { - pass('T-01 agent names rendered via textContent; no innerHTML assignment in initializeBackgroundAgentsDeprecation'); - } else { - fail('T-01 XSS mitigation broken', 'usesTextContent=' + usesTextContent + ' usesInnerHTML=' + usesInnerHTML); - } - } -} - -// ---- Section 7: Gap 1 / WR-01 -- bgAgents storage-shape coercion handles BOTH legacy object-map and defensive array shapes ---- - -{ - const opts = read('extension/ui/options.js'); - const body = extractFunctionBody(opts, 'function initializeBackgroundAgentsDeprecation'); - if (!body) { - fail('Gap1 could not extract initializeBackgroundAgentsDeprecation body for storage-shape check'); - } else { - // Canonical legacy shape (the only writer that ever ran wrote { agentId: agent } map): - // the renderer MUST handle it via Object.values. - const handlesObjectMap = body.indexOf('Object.values(') !== -1; - // Defensive: the renderer MUST also handle a plain array shape. - const handlesArray = body.indexOf('Array.isArray(') !== -1; - // Regression guard: the buggy single-branch Array.isArray(stored.bgAgents) literal MUST be gone. - const hasBuggyOldBranch = body.indexOf('Array.isArray(stored.bgAgents)') !== -1; - - if (handlesObjectMap && handlesArray && !hasBuggyOldBranch) { - pass('Gap1 / WR-01 storage-shape coercion handles object-map (Object.values) AND array (Array.isArray) shapes; old single-branch literal removed'); - } else { - fail( - 'Gap1 / WR-01 storage-shape coercion incomplete', - 'handlesObjectMap=' + handlesObjectMap + ' handlesArray=' + handlesArray + ' hasBuggyOldBranch=' + hasBuggyOldBranch - ); - } + const leftovers = []; + for (const id of ['fsbSunsetNotice', 'fsbSunsetNoticeNames', 'fsbSunsetNoticeDismiss']) { + if (html.includes('id="' + id + '"')) leftovers.push('control_panel.html id="' + id + '"'); } + if (opts.includes('initializeBackgroundAgentsDeprecation')) leftovers.push('options.js initializeBackgroundAgentsDeprecation'); + if (opts.includes('bgAgents')) leftovers.push('options.js reads bgAgents'); + if (opts.includes('fsb_sunset_notice_dismissed')) leftovers.push('options.js writes fsb_sunset_notice_dismissed'); + if (leftovers.length === 0) pass('agent-names notice, its renderer and its storage reads are removed'); + else fail('agent-names notice leftovers', leftovers.join('; ')); } // ---- Result ---- if (failures === 0) { console.log(''); - console.log('All Phase 212-02 regression checks PASSED'); + console.log('All background-agent UI retirement checks PASSED'); process.exit(0); } else { console.log(''); diff --git a/tests/back-tool.test.js b/tests/back-tool.test.js deleted file mode 100644 index 6b4255e46..000000000 --- a/tests/back-tool.test.js +++ /dev/null @@ -1,170 +0,0 @@ -'use strict'; - -/** - * Phase 242 plan 01 -- BACK-01 server-side smoke - * - * Verifies the new server.tool('back', ...) registration in - * mcp/src/tools/agents.ts: - * - The handler resolves to a callable function via the smoke harness. - * - Invoking back({}) emits a single 'mcp:go-back' bridge envelope - * carrying agentId / ownershipToken / connectionId (the canonical - * Phase 238/240/241 triple). - * - Invoking back({ tabId: N }) threads N into payload.tabId. - * - All 5 D-05 status codes (ok | no_history | cross_origin | bf_cache | - * fragment_only) flow back to the MCP host unchanged through - * mapFSBError's success-text JSON serialization. - * - A bridge reply of { success:false, error:'TAB_NOT_OWNED', ... } - * surfaces 'TAB_NOT_OWNED' in the mapped error response (proves the - * Phase 240 gate's reject reaches the MCP host; the actual gate - * check runs on the extension side in Plan 02). - * - * Plain Node + assert; no jest, no mocha. Mirrors the structure of - * tests/mcp-tool-smoke.test.js (assert helper, agent:register lazy-mint - * skip pattern at lines 49-58). - */ - -const { - createToolHarness, - loadAgentScope, - loadBuildModule, -} = require('./mcp-smoke-harness.js'); - -let passed = 0; -let failed = 0; - -function assert(cond, msg) { - if (cond) { - passed++; - console.log(' PASS:', msg); - } else { - failed++; - console.error(' FAIL:', msg); - } -} - -async function buildHarnessWithBackReply(reply) { - const agentsModule = await loadBuildModule('tools/agents.js'); - const harness = createToolHarness({ - bridgeResponses: { - 'mcp:go-back': reply, - }, - }); - const agentScope = await loadAgentScope(); - agentsModule.registerAgentTools(harness.server, harness.bridge, harness.queue, agentScope); - return harness; -} - -function findToolCall(harness, beforeIndex, expectedType) { - for (let i = beforeIndex; i < harness.bridgeCalls.length; i++) { - const entry = harness.bridgeCalls[i]; - if (entry && entry.message && entry.message.type === 'agent:register') continue; - if (entry && entry.message && entry.message.type === expectedType) return entry; - } - return null; -} - -async function runStatusSubTest(reply, expectedStatusInResponse) { - const harness = await buildHarnessWithBackReply(reply); - const handler = harness.getHandler('back'); - assert(typeof handler === 'function', 'back resolves to a callable handler (status=' + expectedStatusInResponse + ')'); - if (typeof handler !== 'function') return; - - const before = harness.bridgeCalls.length; - const result = await handler({}, harness.createExtra()); - - const call = findToolCall(harness, before, 'mcp:go-back'); - assert(!!call, 'back emits mcp:go-back bridge envelope (status=' + expectedStatusInResponse + ')'); - if (!call) return; - - const payload = call.message && call.message.payload; - assert(payload && typeof payload.agentId === 'string' && payload.agentId.length > 0, 'payload.agentId is non-empty (status=' + expectedStatusInResponse + ')'); - - // Status preservation: mapFSBError stringifies the success result; assert - // the textual content surfaces the status verbatim. - assert(result && Array.isArray(result.content) && result.content.length > 0, 'back returns MCP content output (status=' + expectedStatusInResponse + ')'); - const responseText = result && result.content && result.content[0] && result.content[0].text ? String(result.content[0].text) : JSON.stringify(result); - assert(responseText.indexOf(expectedStatusInResponse) !== -1, 'response surfaces status verbatim: ' + expectedStatusInResponse); -} - -async function run() { - console.log('\n--- Phase 242-01 server-side back tool smoke ---'); - - // 5 status codes through the bridge. - await runStatusSubTest( - { success: true, status: 'ok', resultingUrl: 'https://example.com/a', historyDepth: 3 }, - 'ok', - ); - await runStatusSubTest( - { success: true, status: 'no_history', resultingUrl: 'https://example.com/curr', historyDepth: 1 }, - 'no_history', - ); - await runStatusSubTest( - { success: true, status: 'cross_origin', resultingUrl: 'https://other.example.org/p', historyDepth: 5 }, - 'cross_origin', - ); - await runStatusSubTest( - { success: true, status: 'bf_cache', resultingUrl: 'https://cached.example.com/x', historyDepth: 4 }, - 'bf_cache', - ); - await runStatusSubTest( - { success: true, status: 'fragment_only', resultingUrl: 'https://example.com/p#frag', historyDepth: 2 }, - 'fragment_only', - ); - - // Optional tabId pass-through. - console.log('\n--- optional tabId pass-through ---'); - { - const harness = await buildHarnessWithBackReply({ - success: true, status: 'ok', resultingUrl: 'https://example.com/p', historyDepth: 2, - }); - const handler = harness.getHandler('back'); - assert(typeof handler === 'function', 'back resolves to a callable handler (tabId pass-through)'); - if (typeof handler === 'function') { - const before = harness.bridgeCalls.length; - await handler({ tabId: 7 }, harness.createExtra()); - const call = findToolCall(harness, before, 'mcp:go-back'); - assert(!!call, 'back emits mcp:go-back when tabId supplied'); - const payload = call && call.message && call.message.payload; - assert(payload && payload.tabId === 7, 'optional tabId threads into payload (got ' + (payload && payload.tabId) + ')'); - } - } - - // tabId omitted: payload should NOT carry a tabId field. - console.log('\n--- omitted tabId stays absent ---'); - { - const harness = await buildHarnessWithBackReply({ - success: true, status: 'ok', resultingUrl: 'https://example.com/p', historyDepth: 2, - }); - const handler = harness.getHandler('back'); - if (typeof handler === 'function') { - const before = harness.bridgeCalls.length; - await handler({}, harness.createExtra()); - const call = findToolCall(harness, before, 'mcp:go-back'); - const payload = call && call.message && call.message.payload; - assert(payload && !Object.prototype.hasOwnProperty.call(payload, 'tabId'), 'payload omits tabId when caller did not supply one'); - } - } - - // TAB_NOT_OWNED reject surface (Phase 240 gate proxy). - console.log('\n--- TAB_NOT_OWNED reject surfacing ---'); - { - const harness = await buildHarnessWithBackReply({ - success: false, error: 'TAB_NOT_OWNED', ownerAgentId: 'agent_other', - }); - const handler = harness.getHandler('back'); - if (typeof handler === 'function') { - const result = await handler({}, harness.createExtra()); - const responseText = result && result.content && result.content[0] && result.content[0].text ? String(result.content[0].text) : JSON.stringify(result); - assert(responseText.indexOf('TAB_NOT_OWNED') !== -1, 'TAB_NOT_OWNED reject surfaces to MCP host (text=' + responseText.slice(0, 120) + ')'); - } - } - - console.log('\n=== Results: ' + passed + ' passed, ' + failed + ' failed ==='); - process.exit(failed === 0 ? 0 : 1); -} - -run().catch((err) => { - console.error('FATAL', err && err.stack ? err.stack : err); - console.log('\n=== Results: ' + passed + ' passed, ' + (failed + 1) + ' failed ==='); - process.exit(2); -}); diff --git a/tests/dom-stream-perf.test.js b/tests/dom-stream-perf.test.js index 3d0d8bd30..922def53d 100644 --- a/tests/dom-stream-perf.test.js +++ b/tests/dom-stream-perf.test.js @@ -58,7 +58,7 @@ assert(bgSource.includes("alarm.name === 'fsb-domstream-watchdog'"), 'fsb-domstr assert(bgSource.includes("'fsb-domstream-watchdog', { periodInMinutes: 1 }"), 'chrome.alarms.create call present'); assert(bgSource.includes('if (isMcpReconnectAlarm) {'), 'MCP_RECONNECT_ALARM early-return preserved'); assert(bgSource.includes("armMcpBridge('alarm:' + MCP_RECONNECT_ALARM)"), 'MCP arm call preserved'); -assert(bgSource.includes('agentScheduler.getAgentIdFromAlarm(alarm.name)'), 'agent alarm branch preserved'); +assert(!bgSource.includes('agentScheduler'), 'retired background-agent alarm branch is gone'); assert(bgSource.includes('_lastDomStreamStaleFlushCount'), 'SW-side staleFlushCount cache var present'); console.log(' PASS: SW alarm branch invariants present'); diff --git a/tests/lattice-provider-bridge-smoke.test.js b/tests/lattice-provider-bridge-smoke.test.js index fdc7bb914..06f115d35 100644 --- a/tests/lattice-provider-bridge-smoke.test.js +++ b/tests/lattice-provider-bridge-smoke.test.js @@ -632,8 +632,9 @@ async function loadOffscreenHandlerSource(chromeMock) { // discovery add two more classic-SW loads -> 332. The delegation tab seed // (side-panel active tab handed to the delegated agent) adds one -> 333. // The retained debugger session manager adds one shared load -> 334. + // Deleting the four commented-out background-agent loads drops it to 330. const importScriptsCount = (bgSource.match(/importScripts/g) || []).length; - passAssertEqual(importScriptsCount, 334, 'background.js importScripts count includes the retained debugger session manager'); + passAssertEqual(importScriptsCount, 330, 'background.js importScripts count includes the retained debugger session manager'); // Companion call-site-only count (regex requires open paren): Phase 5 baseline // was 150 actual importScripts() calls; Phase 6 adds 1 -> 151; Phase 8 adds 1 -> 152; // Phase 14 adds 2 (trigger-store + trigger-lifecycle) -> 154; Phase 15 adds 2 @@ -680,8 +681,9 @@ async function loadOffscreenHandlerSource(chromeMock) { // capture adds three call sites -> 326. Agent tab-spawn provenance and // shared model discovery add two more call sites -> 328. The delegation tab // seed is call site 329. The debugger session manager is call site 330. + // Deleting the four commented-out background-agent loads drops it to 326. const importScriptsCallSites = (bgSource.match(/importScripts\(/g) || []).length; - passAssertEqual(importScriptsCallSites, 330, 'background.js importScripts() call sites include the retained debugger session manager'); + passAssertEqual(importScriptsCallSites, 326, 'background.js importScripts() call sites include the retained debugger session manager'); passAssertEqual((bgSource.match(/importScripts\(['"]utils\/debugger-sessions\.js['"]\)/g) || []).length, 1, 'debugger session manager is loaded exactly once'); const lineLease = bgLines.findIndex(line => /importScripts\(['"]utils\/cdp-lease\.js['"]\)/.test(line)); diff --git a/tests/lmstudio-startup-preflight.test.js b/tests/lmstudio-startup-preflight.test.js index 4ba4fe245..5b5414e79 100644 --- a/tests/lmstudio-startup-preflight.test.js +++ b/tests/lmstudio-startup-preflight.test.js @@ -103,7 +103,7 @@ function makePreflightHarness(result) { assert.strictEqual(configured.discoveryCalls, 0, 'configured model does not trigger startup discovery'); const handlerStart = backgroundSource.indexOf('async function handleStartAutomation(request, sender, sendResponse) {'); - const handlerEnd = backgroundSource.indexOf('\nasync function executeAutomationTask', handlerStart); + const handlerEnd = backgroundSource.indexOf('\n// Handle automation stop\nasync function handleStopAutomation', handlerStart); const handler = backgroundSource.slice(handlerStart, handlerEnd); assert( handler.indexOf('fsbResolveLmStudioModelForStart(authoritativeProvider)') diff --git a/tests/mcp-bridge-background-dispatch.test.js b/tests/mcp-bridge-background-dispatch.test.js index 1e3bf0dcb..97a6e2285 100644 --- a/tests/mcp-bridge-background-dispatch.test.js +++ b/tests/mcp-bridge-background-dispatch.test.js @@ -3269,10 +3269,10 @@ function runSourceContractCase() { assert(!compatibilityComposition.includes(forbidden), `compatibility refresh orchestration does not touch ${forbidden}`); } - const sunsetListAgents = backgroundSource.indexOf("// case 'listAgents':"); - assert(sunsetListAgents >= 0, 'sunset listAgents runtime case remains commented out'); - assert(backgroundSource.indexOf("case 'getMcpClients'") < sunsetListAgents, - 'new inventory action is independent from the later sunset listAgents block'); + assert(!/case 'listAgents':/.test(backgroundSource), + 'retired listAgents runtime case is gone, commented-out copy included'); + assert(backgroundSource.indexOf("case 'getMcpClients'") >= 0, + 'MCP client inventory action stays live'); const bridgeSource = fs.readFileSync(path.join(__dirname, '..', 'extension', 'ws', 'mcp-bridge-client.js'), 'utf8'); assert(bridgeSource.includes('globalThis.fsbDispatchInternalMessage'), 'mcp-bridge-client.js prefers globalThis.fsbDispatchInternalMessage'); diff --git a/tests/mcp-numeric-param-coercion.test.js b/tests/mcp-numeric-param-coercion.test.js index 360334cd6..e9c95ed43 100644 --- a/tests/mcp-numeric-param-coercion.test.js +++ b/tests/mcp-numeric-param-coercion.test.js @@ -5,17 +5,15 @@ * * Context: Claude Code's current MCP tool-call transport stringifies JSON * numbers, so the FSB MCP server's Zod gate rejects every numeric tool call - * (switch_tab tabId, click_at x/y, agents back tab_id, vault tab_id, - * visual-session tab_id, observability limit/count/topN, ...). The bug lives + * (switch_tab tabId, click_at x/y, vault tab_id, observability + * limit/count/topN, ...). The bug lives * in two places: * * 1. The JSON Schema -> Zod translator at mcp/src/tools/schema-bridge.ts:95 * (covers ~35 numeric fields across the TOOL_REGISTRY). - * 2. Seven hand-rolled Zod sites that bypass the translator: - * - mcp/src/tools/agents.ts:35 (back tab_id) + * 2. Five hand-rolled Zod sites that bypass the translator: * - mcp/src/tools/vault.ts:60 (fill_credential tab_id) * - mcp/src/tools/vault.ts:110 (use_payment_method tab_id) - * - mcp/src/tools/visual-session.ts:50 (start_visual_session tab_id) * - mcp/src/tools/observability.ts:27 (list_sessions limit) * - mcp/src/tools/observability.ts:68 (get_logs count) * - mcp/src/tools/observability.ts:91 (search_memory topN) @@ -176,8 +174,8 @@ async function main() { // guard (A11) is what enforces the actual source matches this shape. function postFixTabIdPositive() { - // Shape used at agents.ts:35, vault.ts:60, vault.ts:110, - // visual-session.ts:50: tab ids MUST be positive integers. + // Shape used at vault.ts:60 and vault.ts:110: tab ids MUST be positive + // integers. return z.coerce.number().int().positive().finite().optional(); } function postFixCountNonnegative() { @@ -190,14 +188,6 @@ async function main() { return z.coerce.number().int().positive().finite().optional(); } - // A10.1: agents.ts:35 back tool tab_id - { - const s = postFixTabIdPositive(); - const r = s.safeParse('999'); - check('A10.1: agents.ts:35 back.tab_id accepts string "999"', - r.success && r.data === 999 && typeof r.data === 'number', JSON.stringify(r)); - } - // A10.2: vault.ts:60 fill_credential tab_id { const s = postFixTabIdPositive(); @@ -214,14 +204,6 @@ async function main() { r.success && r.data === 999, JSON.stringify(r)); } - // A10.4: visual-session.ts:50 start_visual_session tab_id - { - const s = postFixTabIdPositive(); - const r = s.safeParse('999'); - check('A10.4: visual-session.ts:50 start_visual_session.tab_id accepts string "999"', - r.success && r.data === 999, JSON.stringify(r)); - } - // A10.5: observability.ts:27 list_sessions.limit { const s = postFixCountNonnegative(); @@ -249,8 +231,7 @@ async function main() { // -------- A11: source-grep regression guard -------- // Scan every .ts file under mcp/src/tools/ for a literal `z.number(`. // ALLOW: `z.coerce.number(` (the post-fix shape). - // ALLOW: lines whose trimmed start is `//` (commented-out code, e.g. - // the deprecated agent block in agents.ts kept as historical refs). + // ALLOW: lines whose trimmed start is `//` (commented-out code). // ALLOW: a line that contains the marker `internal-only` (escape valve // per research; today there should be ZERO exemptions of this kind). // FAIL: any other line containing `z.number(` is a regression. diff --git a/tests/mcp-tool-smoke.test.js b/tests/mcp-tool-smoke.test.js index ac422fa46..212eda963 100644 --- a/tests/mcp-tool-smoke.test.js +++ b/tests/mcp-tool-smoke.test.js @@ -33,8 +33,6 @@ const requiredSmokeTools = [ 'get_site_guide', 'click', 'close_tab', - 'start_visual_session', - 'end_visual_session', 'run_task', 'stop_task', 'complete_task', @@ -47,9 +45,12 @@ const requiredSmokeTools = [ 'get_logs', 'get_session_replay', 'replay_session', - 'back', ]; +// Removed from the MCP surface: the two visual-session stubs that only +// returned an error, and `back`, which duplicated go_back. +const retiredTools = ['start_visual_session', 'end_visual_session', 'back']; + // Session journals (ac377038) stamp every agent-scoped bridge payload with // correlation fields in buildAgentPayload() -- mcp/src/agent-bridge.ts. Two of // them are random UUIDs, so a whole-payload deepEqual can never match. Assert @@ -125,26 +126,22 @@ async function run() { const readOnlyModule = await loadBuildModule(pathJoin('tools', 'read-only.js')); const screenshotsModule = await loadBuildModule(pathJoin('tools', 'screenshots.js')); const manualModule = await loadBuildModule(pathJoin('tools', 'manual.js')); - const visualSessionModule = await loadBuildModule(pathJoin('tools', 'visual-session.js')); const autopilotModule = await loadBuildModule(pathJoin('tools', 'autopilot.js')); const triggersModule = await loadBuildModule(pathJoin('tools', 'triggers.js')); const observabilityModule = await loadBuildModule(pathJoin('tools', 'observability.js')); - // Phase 242 plan 02: agents.ts now exposes the 'back' tool (Plan 01 D-01). - // Loading the module here lets the smoke test register the back handler - // alongside the other surfaces; without this, 'back' stays in - // requiredSmokeTools but its handler never lands in harness.handlers. - const agentsModule = await loadBuildModule(pathJoin('tools', 'agents.js')); console.log('\n--- packaged runtime surface ---'); assert(typeof runtimeModule.createRuntime === 'function', 'build/runtime.js exports createRuntime'); assert(typeof readOnlyModule.registerReadOnlyTools === 'function', 'build/tools/read-only.js exports registerReadOnlyTools'); assert(typeof screenshotsModule.registerScreenshotTools === 'function', 'build/tools/screenshots.js exports registerScreenshotTools'); assert(typeof manualModule.registerManualTools === 'function', 'build/tools/manual.js exports registerManualTools'); - assert(typeof visualSessionModule.registerVisualSessionTools === 'function', 'build/tools/visual-session.js exports registerVisualSessionTools'); assert(typeof autopilotModule.registerAutopilotTools === 'function', 'build/tools/autopilot.js exports registerAutopilotTools'); assert(typeof triggersModule.registerTriggerTools === 'function', 'build/tools/triggers.js exports registerTriggerTools'); assert(typeof observabilityModule.registerObservabilityTools === 'function', 'build/tools/observability.js exports registerObservabilityTools'); - assert(typeof agentsModule.registerAgentTools === 'function', 'build/tools/agents.js exports registerAgentTools'); + const runtimeTools = Object.keys(runtimeModule.createRuntime().server._registeredTools || {}); + for (const toolName of retiredTools) { + assert(!runtimeTools.includes(toolName), `createRuntime does not register the retired ${toolName} tool`); + } const harness = createToolHarness({ bridgeResponses: { @@ -163,17 +160,6 @@ async function run() { status: 'approval_required', requestId: 'smoke-replay-approval' }, - 'mcp:start-visual-session': ({ payload }) => ({ - success: true, - sessionToken: 'visual_token_123', - clientLabel: payload.clientLabel, - tabId: 7, - }), - 'mcp:end-visual-session': ({ payload }) => ({ - success: true, - sessionToken: payload.sessionToken, - cleared: true, - }), 'mcp:start-automation': { success: true, sessionId: 'smoke-session', status: 'started' }, 'mcp:stop-automation': { success: true, stopped: true }, 'mcp:task-status': ({ payload }) => { @@ -207,13 +193,9 @@ async function run() { readOnlyModule.registerReadOnlyTools(harness.server, harness.bridge, harness.queue, agentScope); screenshotsModule.registerScreenshotTools(harness.server, harness.bridge, harness.queue, agentScope); manualModule.registerManualTools(harness.server, harness.bridge, harness.queue, agentScope); - visualSessionModule.registerVisualSessionTools(harness.server, harness.bridge, harness.queue, agentScope); triggersModule.registerTriggerTools(harness.server, harness.bridge, harness.queue, agentScope); autopilotModule.registerAutopilotTools(harness.server, harness.bridge, harness.queue, agentScope); observabilityModule.registerObservabilityTools(harness.server, harness.bridge, harness.queue, agentScope); - // Phase 242 D-01: registerAgentTools registers the 'back' MCP tool. Loaded - // last so the smoke test's registered-handlers assertion (line 118) sees it. - agentsModule.registerAgentTools(harness.server, harness.bridge, harness.queue, agentScope); console.log('\n--- registered smoke tools ---'); for (const toolName of requiredSmokeTools) { @@ -300,30 +282,6 @@ async function run() { 'click routes through mcp:execute-action with click payload + v0.9.62 visualSession sidecar', ); - // v0.9.62 (fsb-mcp-server 0.9.0): start_visual_session + end_visual_session - // are TOOL_REMOVED stubs. Handlers stay registered (for the migration recipe - // surface) but they short-circuit BEFORE the task queue and BEFORE the bridge - // connectivity check -- no bridge dispatch happens. See - // mcp/src/tools/visual-session.ts and .planning/v0.9.62-CONTRACT.md. - const startVisualSessionCall = await invokeTool(harness, 'start_visual_session', { - client: ' codex ', - task: 'Smoke test the visual lifecycle', - detail: 'Preparing overlay', - }); - assert( - startVisualSessionCall === null, - 'start_visual_session is a TOOL_REMOVED stub and does not dispatch a bridge message', - ); - - const endVisualSessionCall = await invokeTool(harness, 'end_visual_session', { - session_token: 'visual_token_123', - reason: 'ended', - }); - assert( - endVisualSessionCall === null, - 'end_visual_session is a TOOL_REMOVED stub and does not dispatch a bridge message', - ); - const runTaskCall = await invokeTool(harness, 'run_task', { task: 'Smoke test the browser bridge' }, harness.createExtra({ progressToken: 'smoke-progress' })); assertDeepEqual( runTaskCall && runTaskCall.message, @@ -442,16 +400,6 @@ async function run() { 'replay_session requests side-panel consent without target-page input', ); - // Phase 242 plan 02: 'back' routes through mcp:go-back. Bridge response - // surfaces the canonical 5-status envelope; agentScope captures the - // (optional) ownershipToken via captureOwnershipToken on success. - const backCall = await invokeTool(harness, 'back'); - assertDeepEqual( - backCall && backCall.message, - { type: 'mcp:go-back', payload: { agentId: 'agent_test_smoke', ownershipToken: 'token_test_smoke' } }, - 'back routes through mcp:go-back with agentId + ownershipToken (Phase 242 D-01)', - ); - console.log('\n--- explicit tab_id uses tab-specific ownership token ---'); agentScope.captureOwnershipToken(77, 'token_tab_77'); const triggerCall = await invokeTool(harness, 'trigger', { @@ -588,13 +536,6 @@ async function run() { 'read_page with tab_id uses ownershipTokenFor(tab_id)', ); - const explicitBackCall = await invokeTool(harness, 'back', { tab_id: 77 }); - assertDeepEqual( - explicitBackCall && explicitBackCall.message, - { type: 'mcp:go-back', payload: { agentId: 'agent_test_smoke', ownershipToken: 'token_tab_77', tabId: 77 } }, - 'back with tab_id uses ownershipTokenFor(tab_id)', - ); - const explicitCloseTabCall = await invokeTool(harness, 'close_tab', { tab_id: 77, visual_reason: 'smoke test', @@ -620,12 +561,9 @@ async function run() { assert(registerCalls.length === 1, 'agent:register must fire exactly once across all tool invocations; saw ' + registerCalls.length); console.log('\n--- queue coverage ---'); - // v0.9.62 (fsb-mcp-server 0.9.0): start_visual_session + end_visual_session - // are TOOL_REMOVED stubs that short-circuit BEFORE queue.enqueue (see - // mcp/src/tools/visual-session.ts). They are excluded from queue coverage - // alongside stop_task (which stays direct so cancellation does not wait - // behind queued work). - const DIRECT_TOOLS = new Set(['stop_task', 'start_visual_session', 'end_visual_session', 'trigger', 'stop_trigger', 'get_trigger_status', 'list_triggers']); + // stop_task stays direct so cancellation does not wait behind queued work; + // the trigger tools dispatch directly from their registrar. + const DIRECT_TOOLS = new Set(['stop_task', 'trigger', 'stop_trigger', 'get_trigger_status', 'list_triggers']); for (const toolName of requiredSmokeTools.filter((name) => !DIRECT_TOOLS.has(name))) { assert(harness.queueCalls.includes(toolName), `${toolName} passes through the shared queue surface`); } @@ -634,8 +572,6 @@ async function run() { assert(!harness.queueCalls.includes('stop_trigger'), 'stop_trigger dispatches directly so cancellation does not wait behind queued work'); assert(!harness.queueCalls.includes('get_trigger_status'), 'get_trigger_status dispatches directly so status does not wait behind queued work'); assert(!harness.queueCalls.includes('list_triggers'), 'list_triggers dispatches directly so listing does not wait behind queued work'); - assert(!harness.queueCalls.includes('start_visual_session'), 'start_visual_session is a TOOL_REMOVED stub and does not enqueue'); - assert(!harness.queueCalls.includes('end_visual_session'), 'end_visual_session is a TOOL_REMOVED stub and does not enqueue'); console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`); process.exit(failed > 0 ? 1 : 0); diff --git a/tests/mcp-version-parity.test.js b/tests/mcp-version-parity.test.js index 07c6e83cc..1e0e2de8b 100644 --- a/tests/mcp-version-parity.test.js +++ b/tests/mcp-version-parity.test.js @@ -4,6 +4,7 @@ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const { execSync } = require('child_process'); +const { pathToFileURL } = require('url'); let passed = 0; let failed = 0; @@ -268,6 +269,8 @@ async function run() { ['MCP README', packageReadme], ['extension onboarding', readText('extension/ui/onboarding.js')], ['showcase about page', readText('showcase/angular/src/app/pages/about/about-page.component.html')], + ['showcase about metadata', readText('showcase/angular/src/app/pages/about/about-page.component.ts')], + ['showcase home page', readText('showcase/angular/src/app/pages/home/home-page.component.html')], ['showcase agents page', readText('showcase/angular/src/app/pages/agents/agents-page.component.html')], ['showcase agents metadata', readText('showcase/angular/src/app/pages/agents/agents-page.component.ts')], ['showcase support page', readText('showcase/angular/src/app/pages/support/support-page.component.html')], @@ -320,18 +323,54 @@ async function run() { ]) { assert(content.includes(`fsb-mcp-server ${canonicalVersion}`), `${name} advertises the canonical MCP release`); } + console.log('\n--- published tool counts match the built server ---'); + const runtimeModule = await import(pathToFileURL(path.join(repoRoot, 'mcp', 'build', 'runtime.js')).href); + const registeredToolNames = Object.keys(runtimeModule.createRuntime().server._registeredTools || {}); + const toolCount = registeredToolNames.length; + assert(toolCount > 0, `built runtime registers MCP tools (got ${toolCount})`); + for (const retired of ['start_visual_session', 'end_visual_session', 'back']) { + assert(!registeredToolNames.includes(retired), `retired ${retired} tool stays off the MCP surface`); + } + // A count claim is "N tools", "N MCP tools", "N registered (MCP) tools" or "N-tool". + const TOOL_COUNT_CLAIM = /(? Array.from(text.matchAll(TOOL_COUNT_CLAIM), (match) => Number(match[1])); + const statesToolCount = new RegExp(`(? count === toolCount), + `${name} states the ${toolCount}-tool registered surface (claims found: ${counts.join(', ') || 'none'})`); + continue; + } + // Catalogs: every unit whose English source states a count must state the + // live count, and so must its translation. Release notes are history. + for (const unit of content.matchAll(//gu)) { + if (unit[1].startsWith('releaseNotes.')) continue; + const source = (unit[0].match(/([\s\S]*?)<\/source>/u) || [])[1] || ''; + const counts = claimedCounts(source); + if (counts.length === 0) continue; + assert(counts.every((count) => count === toolCount), + `${name} ${unit[1]} source states ${toolCount} tools (got ${counts.join(', ')})`); + const target = (unit[0].match(/]*>([\s\S]*?)<\/target>/u) || [])[1]; + if (target !== undefined) { + assert(statesToolCount.test(target), `${name} ${unit[1]} translation states ${toolCount} tools`); + } + } } - assert(rootReadme.includes('69 registered MCP tools') && packageReadme.includes('## Tools (69 Total)'), - 'primary README authorities advertise the 69-tool registered surface'); - const rootSurfaceTable = rootReadme.match(/\| Visual sessions \|[\s\S]*?\n\nRead-only tools bypass/u); + assert(rootReadme.includes(`${toolCount} registered MCP tools`) && packageReadme.includes(`\`fsb-mcp-server\` registers ${toolCount} tools.`), + `primary README authorities advertise the ${toolCount}-tool registered surface`); + const rootSurfaceTable = rootReadme.match(/\| Surface \| Count \| Examples \|[\s\S]*?\n\nRead-only tools bypass/u); const rootSurfaceCounts = rootSurfaceTable ? Array.from(rootSurfaceTable[0].matchAll(/^\|[^|]+\|\s*(\d+)\s*\|/gmu), (match) => Number(match[1])) : []; - assert(rootSurfaceCounts.length === 9 && rootSurfaceCounts.reduce((sum, count) => sum + count, 0) === 69, - 'root README category table contains nine surfaces totaling 69 tools'); + assert(rootSurfaceCounts.length > 0 && rootSurfaceCounts.reduce((sum, count) => sum + count, 0) === toolCount, + `root README category table totals ${toolCount} tools (got ${rootSurfaceCounts.join(' + ')})`); + const packageToolsSection = packageReadme.match(/\n## Tools\n[\s\S]*?\n### Tool Count Contract\n/u); + const packageSectionCounts = packageToolsSection + ? Array.from(packageToolsSection[0].matchAll(/^### [^\n]*\((\d+)\)$/gmu), (match) => Number(match[1])) + : []; + assert(packageSectionCounts.length > 0 && packageSectionCounts.reduce((sum, count) => sum + count, 0) === toolCount, + `MCP README tool sections total ${toolCount} tools (got ${packageSectionCounts.join(' + ')})`); assert(promptSource.includes('FSB Tool Reference (49 tools)') && promptSource.includes('## Developer / UAT (1 tool)') && promptSource.includes('## Observability (7 tools)') && diff --git a/tests/mcp-visual-session-contract.test.js b/tests/mcp-visual-session-contract.test.js index 1e550db7c..dbdceec10 100644 --- a/tests/mcp-visual-session-contract.test.js +++ b/tests/mcp-visual-session-contract.test.js @@ -52,13 +52,11 @@ * resolveAgentTabOrError, dispatchMcpToolRoute, wrapWithChangeReport, * and the agent-registry instance, which is out of scope here.) * - * 5. runToolRemovedCase - * - Calling either removed tool name (start_visual_session, - * end_visual_session) through the MCP server's registerVisualSessionTools - * dispatcher (mcp/build/tools/visual-session.js) returns the typed - * TOOL_REMOVED error envelope. The response body references the - * CHANGELOG anchor and the README migration recipe. No bridge call, - * no queue call. + * 5. runRetiredToolsCase + * - The built runtime no longer registers start_visual_session or + * end_visual_session (their error-only stubs were removed), and + * mcp/build/tools/visual-session.js still exports the client-label + * helpers that the action tools use. * * Run: node tests/mcp-visual-session-contract.test.js */ @@ -637,89 +635,34 @@ async function runOwnershipPrecedenceCase() { } // ------------------------------------------------------------------ -// Case 5: TOOL_REMOVED -- both removed tool names return the typed -// error envelope with the migration recipe pointer (CHANGELOG anchor -// + README anchor); no bridge call, no queue call. +// Case 5: the removed visual-session tools stay off the MCP surface, +// while the shared client-label helpers keep working. // ------------------------------------------------------------------ -async function runToolRemovedCase() { - console.log('\n--- Case 5: TOOL_REMOVED for start_visual_session and end_visual_session ---'); +async function runRetiredToolsCase() { + console.log('\n--- Case 5: start_visual_session and end_visual_session are not registered ---'); - const BUILD_PATH = path.resolve(__dirname, '..', 'mcp', 'build', 'tools', 'visual-session.js'); - if (!fs.existsSync(BUILD_PATH)) { - console.error(' FAIL: mcp/build/tools/visual-session.js missing -- run `cd mcp && npm run build` before this test'); + const BUILD_DIR = path.resolve(__dirname, '..', 'mcp', 'build'); + const runtimePath = path.join(BUILD_DIR, 'runtime.js'); + const visualSessionPath = path.join(BUILD_DIR, 'tools', 'visual-session.js'); + if (!fs.existsSync(runtimePath) || !fs.existsSync(visualSessionPath)) { + console.error(' FAIL: mcp/build is missing -- run `cd mcp && npm run build` before this test'); failed++; return; } - const visualSessionModule = await import(BUILD_PATH); - const recordingServer = makeRecordingServer(); - const mockBridge = makeRecordingBridge(); - const mockQueue = makeRecordingQueue(); - const mockAgentScope = makeStubAgentScope(); - - visualSessionModule.registerVisualSessionTools( - recordingServer.server, - mockBridge.bridge, - mockQueue.queue, - mockAgentScope, - ); - - // Both removed tools must be registered (so the MCP tools/list surface - // still advertises them with the [REMOVED in v0.9.0] banner). - assert(recordingServer.tools.has('start_visual_session'), '5.0a start_visual_session handler registered (stub for tools/list visibility)'); - assert(recordingServer.tools.has('end_visual_session'), '5.0b end_visual_session handler registered (stub for tools/list visibility)'); - - // Sub-case A: start_visual_session returns TOOL_REMOVED with migration recipe. - mockBridge.calls.length = 0; - mockQueue.calls.length = 0; - const startHandler = recordingServer.tools.get('start_visual_session'); - const startRes = await startHandler({ - client: 'Claude', - task: 'Drive checkout', - detail: 'should be ignored', - }); - assertEqual(startRes.isError, true, '5.A start_visual_session returns isError: true'); - assertEqual(mockBridge.calls.length, 0, '5.A no bridge.sendAndWait call on removed tool'); - assertEqual(mockQueue.calls.length, 0, '5.A no queue.enqueue call on removed tool'); - assert(typeof startRes.content[0].text === 'string', '5.A response has a text body'); - - const startText = startRes.content[0].text; - assert(startText.includes('start_visual_session'), '5.A response body names the removed tool start_visual_session'); - assert(startText.includes('0.9.0'), '5.A response body cites the removed_in_version 0.9.0'); - assert(startText.includes('visual_reason'), '5.A migration recipe references visual_reason field'); - assert(startText.includes('client'), '5.A migration recipe references client field'); - assert(startText.includes('is_final'), '5.A migration recipe references is_final field'); - // The TOOL_REMOVED migration recipe in mcp/src/errors.ts cites CHANGELOG.md - // and mcp/README.md as the migration recipe anchors. - assert(startText.includes('CHANGELOG'), '5.A response references CHANGELOG anchor'); - assert(startText.includes('README'), '5.A response references README anchor'); - - // Sub-case B: end_visual_session returns TOOL_REMOVED with migration recipe. - mockBridge.calls.length = 0; - mockQueue.calls.length = 0; - const endHandler = recordingServer.tools.get('end_visual_session'); - const endRes = await endHandler({ - session_token: 'visual_token_irrelevant', - reason: 'ended', - }); - assertEqual(endRes.isError, true, '5.B end_visual_session returns isError: true'); - assertEqual(mockBridge.calls.length, 0, '5.B no bridge.sendAndWait call on removed tool'); - assertEqual(mockQueue.calls.length, 0, '5.B no queue.enqueue call on removed tool'); - const endText = endRes.content[0].text; - assert(endText.includes('end_visual_session'), '5.B response body names the removed tool end_visual_session'); - assert(endText.includes('0.9.0'), '5.B response body cites the removed_in_version 0.9.0'); - assert(endText.includes('visual_reason'), '5.B migration recipe references visual_reason field'); - assert(endText.includes('client'), '5.B migration recipe references client field'); - assert(endText.includes('is_final'), '5.B migration recipe references is_final field'); - assert(endText.includes('CHANGELOG'), '5.B response references CHANGELOG anchor'); - assert(endText.includes('README'), '5.B response references README anchor'); - - // Both responses must route through the same TOOL_REMOVED layered detail - // (rather than a generic "tool not found" surface). Loose match: the - // response carries the "Visual session contract" layer label. - assert(startText.includes('Visual session contract'), '5.A response detected layer is Visual session contract'); - assert(endText.includes('Visual session contract'), '5.B response detected layer is Visual session contract'); + const { createRuntime } = await import(runtimePath); + const registered = Object.keys(createRuntime().server._registeredTools || {}); + assert(!registered.includes('start_visual_session'), '5.1 start_visual_session is not registered'); + assert(!registered.includes('end_visual_session'), '5.2 end_visual_session is not registered'); + + const visualSessionModule = await import(visualSessionPath); + assertEqual(typeof visualSessionModule.registerVisualSessionTools, 'undefined', + '5.3 visual-session.js no longer exports a tool registrar'); + assertEqual(visualSessionModule.normalizeMcpVisualClientLabel(' codex '), 'Codex', + '5.4 client labels still normalize for the action tools'); + assertEqual(visualSessionModule.isAllowedMcpVisualClientLabel('Not A Client'), false, + '5.5 unknown client labels are still rejected'); } // ------------------------------------------------------------------ @@ -731,7 +674,7 @@ async function runToolRemovedCase() { await runPersistenceReplayCase(); await runImplicitDispatcherValidationCase(); await runOwnershipPrecedenceCase(); - await runToolRemovedCase(); + await runRetiredToolsCase(); console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`); process.exit(failed === 0 ? 0 : 1); diff --git a/tests/remote-control-rebrand.test.js b/tests/remote-control-rebrand.test.js index b11bdb01e..46c28002d 100644 --- a/tests/remote-control-rebrand.test.js +++ b/tests/remote-control-rebrand.test.js @@ -104,10 +104,8 @@ assert( '[SYNC-04] deprecation footer names v0.9.45rc1 plus April 2026 inside #sync' ); assert( - /id="fsbSunsetNotice"/.test(syncBody) && - /id="fsbSunsetNoticeNames"/.test(syncBody) && - /id="fsbSunsetNoticeDismiss"/.test(syncBody), - '[SYNC-04] sunset-notice scaffolding (fsbSunsetNotice / Names / Dismiss) lives inside #sync' + !/id="fsbSunsetNotice/.test(syncBody), + '[SYNC-04] the retired agent-names notice is gone from #sync (its data is cleared on update)' ); console.log('\n--- Showcase mirror reflects Sync consolidation ---'); diff --git a/tests/server-public-stats-no-auth.test.js b/tests/server-public-stats-no-auth.test.js index 5fc7ba0ee..593741d38 100644 --- a/tests/server-public-stats-no-auth.test.js +++ b/tests/server-public-stats-no-auth.test.js @@ -1,13 +1,11 @@ /** * Phase 274 / STATS-04 + T-274-09 -- public-stats no-auth + no-cookie + no-PII. * - * Mounts BOTH the auth-gated /api/stats handler AND the public /api/public-stats - * router in the same Express app, then asserts: + * Mounts the public /api/public-stats router next to /api/auth in the same + * Express app, then asserts: * - GET /api/public-stats/global with NO auth headers returns 200. * - No Set-Cookie or WWW-Authenticate header on the public response. - * - GET /api/stats with NO auth header returns 401 (proves path-shadow safety: - * /api/public-stats and /api/stats are distinct namespaces). - * - Both endpoints' bodies (where applicable) carry no PII signatures. + * - The public bodies carry no PII signatures. * * Run: node tests/server-public-stats-no-auth.test.js */ @@ -24,7 +22,6 @@ const cors = require(require.resolve('cors', { paths: [SERVER_NM] })); const { initializeDatabase } = require(path.join(__dirname, '..', 'showcase', 'server', 'src', 'db', 'schema')); const Queries = require(path.join(__dirname, '..', 'showcase', 'server', 'src', 'db', 'queries')); -const authMiddleware = require(path.join(__dirname, '..', 'showcase', 'server', 'src', 'middleware', 'auth')); const createAuthRouter = require(path.join(__dirname, '..', 'showcase', 'server', 'src', 'routes', 'auth')); const createPublicStatsRouter = require(path.join(__dirname, '..', 'showcase', 'server', 'src', 'routes', 'public-stats')); const activeTracker = require(path.join(__dirname, '..', 'showcase', 'server', 'src', 'telemetry', 'active-tracker')); @@ -41,13 +38,8 @@ const app = express(); app.use(cors({ origin: true, credentials: true, exposedHeaders: ['X-FSB-Hash-Key'] })); app.use(express.json()); -// Mount /api/auth + auth-gated /api/stats + public /api/public-stats in the same -// app so we can verify they don't shadow each other. +// Mount /api/auth next to the public /api/public-stats router, as server.js does. app.use('/api/auth', createAuthRouter(queries)); -const auth = authMiddleware(queries); -app.use('/api/stats', auth, (req, res) => { - res.json(queries.getAgentStats(req.hashKey)); -}); app.use('/api/public-stats', createPublicStatsRouter(db, queries)); const server = http.createServer(app).listen(0); @@ -89,16 +81,8 @@ function rawGet(path_, extraHeaders = {}) { check('Public response has NO WWW-Authenticate header', pub.headers['www-authenticate'] === undefined, `got ${pub.headers['www-authenticate']}`); - // --- 2. AUTH-GATED endpoint with NO auth header -> 401. --- - const auth401 = await rawGet('/api/stats'); - check('GET /api/stats (no auth) -> 401 (path-shadow safety)', auth401.statusCode === 401, - `got ${auth401.statusCode} body=${auth401.body}`); - - // --- 3. Confirm /api/public-stats does NOT accidentally satisfy the auth gate either. - // Path shape: /api/public-stats/global vs /api/stats. Exact-prefix routing - // means /api/stats does NOT match /api/public-stats/global. Repeat the - // public GET with the same headers a typical browser would carry to ensure - // no quirk pops up. --- + // --- 3. Repeat the public GET with the headers a typical browser would carry + // to make sure no quirk pops up. --- const pub2 = await rawGet('/api/public-stats/global', { 'Accept': 'application/json', 'User-Agent': 'Mozilla/5.0 (test)', @@ -136,14 +120,6 @@ function rawGet(path_, extraHeaders = {}) { check('Public route does NOT leak Authorization back as Set-Cookie', pubAdvers.headers['set-cookie'] === undefined, `got ${JSON.stringify(pubAdvers.headers['set-cookie'])}`); - // --- 7. Verify request path /api/public-stats does NOT cross-match /api/stats. --- - // Try a deliberate prefix-walk: ensure /api/public-stats does NOT match the auth-gated handler. - const crossPrefix = await rawGet('/api/public-stats'); - // The router has no root handler so Express returns 404. Anything other than - // 401 means the auth gate didn't fire on /api/public-stats. - check('GET /api/public-stats (no auth, no inner route) -> NOT 401', - crossPrefix.statusCode !== 401, `got ${crossPrefix.statusCode}`); - server.close(); db.close(); diff --git a/tests/skill-fsb-spec.test.js b/tests/skill-fsb-spec.test.js index 33b42ee6f..ee2439ff7 100644 --- a/tests/skill-fsb-spec.test.js +++ b/tests/skill-fsb-spec.test.js @@ -132,7 +132,7 @@ const typedErrors = ['TAB_NOT_OWNED', 'AGENT_CAP_REACHED', 'TAB_INCOGNITO_NOT_SU for (const err of typedErrors) { check(macSrc.includes(err), `multi-agent-contract.md mentions ${err}`); } -check(/\bback\b/.test(macSrc), 'multi-agent-contract.md mentions back tool'); +check(/\[GOOD\] go_back\(/.test(macSrc) && !/\[GOOD\] back\(|Prefer `back`/.test(macSrc), 'multi-agent-contract.md points history navigation at go_back, not the removed back tool'); // tool-decision-tree.md tool token cross-reference const tdtSrc = fs.existsSync(refs['tool-decision-tree.md']) diff --git a/tests/test-agent-scheduler-cron.js b/tests/test-agent-scheduler-cron.js deleted file mode 100644 index d6a0a57a1..000000000 --- a/tests/test-agent-scheduler-cron.js +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Tests for AgentScheduler cron support and schedule descriptions. - * Run: node tests/test-agent-scheduler-cron.js - */ - -// Minimal chrome.alarms mock -const alarms = {}; -globalThis.chrome = { - alarms: { - create: async (name, opts) => { alarms[name] = opts; }, - clear: async (name) => { delete alarms[name]; }, - getAll: async () => Object.entries(alarms).map(([name, opts]) => ({ name, ...opts })) - } -}; - -// Mock agentManager for rescheduleAllAgents -globalThis.agentManager = { listAgents: async () => [] }; - -// Load the scheduler -const fs = require('fs'); -const vm = require('vm'); -const src = fs.readFileSync('extension/agents/agent-scheduler.js', 'utf8'); -vm.runInThisContext(src); - -let passed = 0; -let failed = 0; - -function assert(cond, msg) { - if (cond) { passed++; console.log(' PASS:', msg); } - else { failed++; console.error(' FAIL:', msg); } -} - -function deepEqual(a, b) { - return JSON.stringify(a) === JSON.stringify(b); -} - -console.log('\n--- parseCron tests ---'); - -const p1 = agentScheduler.parseCron('0 9 * * 1-5'); -assert(p1 !== null, 'parseCron("0 9 * * 1-5") returns non-null'); -assert(p1 && p1.minute === 0, 'minute = 0'); -assert(p1 && p1.hour === 9, 'hour = 9'); -assert(p1 && p1.dayOfMonth === '*', 'dayOfMonth = *'); -assert(p1 && p1.month === '*', 'month = *'); -assert(p1 && deepEqual(p1.dayOfWeek, [1,2,3,4,5]), 'dayOfWeek = [1,2,3,4,5]'); - -const p2 = agentScheduler.parseCron('30 14 * * 1,4'); -assert(p2 !== null, 'parseCron("30 14 * * 1,4") returns non-null'); -assert(p2 && p2.minute === 30, 'minute = 30'); -assert(p2 && p2.hour === 14, 'hour = 14'); -assert(p2 && deepEqual(p2.dayOfWeek, [1,4]), 'dayOfWeek = [1,4]'); - -const p3 = agentScheduler.parseCron('0 0 1 * *'); -assert(p3 !== null, 'parseCron("0 0 1 * *") returns non-null'); -assert(p3 && p3.minute === 0, 'minute = 0'); -assert(p3 && p3.hour === 0, 'hour = 0'); -assert(p3 && deepEqual(p3.dayOfMonth, [1]), 'dayOfMonth = [1]'); -assert(p3 && p3.dayOfWeek === '*', 'dayOfWeek = *'); - -const p4 = agentScheduler.parseCron('invalid'); -assert(p4 === null, 'parseCron("invalid") returns null'); - -const p5 = agentScheduler.parseCron('0 25 * * *'); -assert(p5 === null, 'parseCron("0 25 * * *") returns null (invalid hour)'); - -console.log('\n--- _calculateNextCronRun tests ---'); - -const nextWeekday = agentScheduler._calculateNextCronRun('0 9 * * 1-5'); -assert(typeof nextWeekday === 'number' && nextWeekday > Date.now(), '_calculateNextCronRun returns future timestamp'); -if (nextWeekday) { - const d = new Date(nextWeekday); - assert(d.getHours() === 9 && d.getMinutes() === 0, 'next run is at 09:00'); - assert(d.getDay() >= 1 && d.getDay() <= 5, 'next run is on a weekday'); -} - -const nextInvalid = agentScheduler._calculateNextCronRun('invalid'); -assert(nextInvalid === null, '_calculateNextCronRun("invalid") returns null'); - -console.log('\n--- getScheduleDescription tests ---'); - -assert(agentScheduler.getScheduleDescription({ type: 'cron', cronExpression: '0 9 * * 1-5' }) === 'Every weekday at 09:00', - 'cron weekday description'); - -assert(agentScheduler.getScheduleDescription({ type: 'interval', intervalMinutes: 30 }) === 'Every 30 minutes', - 'interval 30 min description'); - -assert(agentScheduler.getScheduleDescription({ type: 'daily', dailyTime: '14:00' }) === 'Daily at 14:00', - 'daily description'); - -assert(agentScheduler.getScheduleDescription({ type: 'daily', dailyTime: '09:00', daysOfWeek: [1,3,5] }) === 'Mon, Wed, Fri at 09:00', - 'daily with days description'); - -assert(agentScheduler.getScheduleDescription({ type: 'once' }) === 'One-time execution', - 'once description'); - -console.log('\n--- scheduleAgent cron case ---'); - -(async () => { - const cronAgent = { - agentId: 'test_cron_1', - enabled: true, - schedule: { type: 'cron', cronExpression: '0 9 * * 1-5' }, - runCount: 0 - }; - const result = await agentScheduler.scheduleAgent(cronAgent); - assert(result === true, 'scheduleAgent with cron returns true'); - const alarmName = agentScheduler.getAlarmName('test_cron_1'); - assert(alarms[alarmName] && alarms[alarmName].when > Date.now(), 'cron alarm has future "when" timestamp'); - - console.log('\n--- isValidAlarmFire cron case ---'); - - const recentAgent = { lastRunAt: Date.now() - 10000, schedule: { type: 'cron' } }; - assert(agentScheduler.isValidAlarmFire(recentAgent) === false, 'cron: too recent (10s) returns false'); - - const oldAgent = { lastRunAt: Date.now() - 60000, schedule: { type: 'cron' } }; - assert(agentScheduler.isValidAlarmFire(oldAgent) === true, 'cron: 60s elapsed returns true'); - - console.log('\n--- rescheduleCron ---'); - - assert(typeof agentScheduler.rescheduleCron === 'function', 'rescheduleCron method exists'); - - console.log('\n=== Results: ' + passed + ' passed, ' + failed + ' failed ==='); - process.exit(failed > 0 ? 1 : 0); -})(); From 006bbeb8a2282c4d5bab77dcc9bc5b856441a907 Mon Sep 17 00:00:00 2001 From: Lakshman Turlapati Date: Wed, 21 Oct 2026 12:00:43 -0500 Subject: [PATCH 2/2] Catch tool counts the parity check missed and fix the last 68s The count check only read "N tools", "N MCP tools", "N registered (MCP) tools" and "N-tool" in raw text, and a file with no matches passed. Five live strings still said 68: the onboarding "68 available" row, "68 MCP tools" on the home and agents pages (with MCP wrapped in a span), "68 MCP browser tools" in the agents schema, and "68 granular tools" in the support FAQ, plus their translations in every catalog. The check now strips markup before matching, also reads "granular", "MCP browser" and "Tools ... N available" claims, and fails when a source that should state the count no longer contains one it can read. Before the strings were fixed it failed on exactly those five sources and four catalog units; now all of them say 69. --- extension/ui/onboarding.js | 2 +- .../pages/agents/agents-page.component.html | 2 +- .../app/pages/agents/agents-page.component.ts | 2 +- .../app/pages/home/home-page.component.html | 2 +- .../pages/support/support-page.component.ts | 2 +- showcase/angular/src/locale/messages.de.xlf | 16 +++++++-------- showcase/angular/src/locale/messages.es.xlf | 16 +++++++-------- showcase/angular/src/locale/messages.ja.xlf | 16 +++++++-------- showcase/angular/src/locale/messages.ko.xlf | 16 +++++++-------- showcase/angular/src/locale/messages.xlf | 8 ++++---- .../angular/src/locale/messages.zh-CN.xlf | 16 +++++++-------- .../angular/src/locale/messages.zh-TW.xlf | 16 +++++++-------- .../src/locale/same-source-allowlist.json | 4 ++-- tests/mcp-version-parity.test.js | 20 +++++++++++++++---- 14 files changed, 75 insertions(+), 63 deletions(-) diff --git a/extension/ui/onboarding.js b/extension/ui/onboarding.js index e37eadc3c..d7db0cf6c 100644 --- a/extension/ui/onboarding.js +++ b/extension/ui/onboarding.js @@ -879,7 +879,7 @@ return [ { k: 'Setup', v: 'MCP server' }, { k: 'Command', v: 'fsb-mcp-server', mono: true }, - { k: 'Tools', v: '68 available' }, + { k: 'Tools', v: '69 available' }, { k: 'Access', v: 'Granted on install', ok: true }, { k: 'Version', v: `v${VERSION}`, mono: true } ]; diff --git a/showcase/angular/src/app/pages/agents/agents-page.component.html b/showcase/angular/src/app/pages/agents/agents-page.component.html index 2f54017be..c0f7616d4 100644 --- a/showcase/angular/src/app/pages/agents/agents-page.component.html +++ b/showcase/angular/src/app/pages/agents/agents-page.component.html @@ -12,7 +12,7 @@

    Give your agent a real browser.

    - Always-on agents like Grok Bot, Muse, and Dots ship with a cloud computer. FSB hands them yours: the Chrome you are already signed into, 68 MCP tools, and a verified result after every action. + Always-on agents like Grok Bot, Muse, and Dots ship with a cloud computer. FSB hands them yours: the Chrome you are already signed into, 69 MCP tools, and a verified result after every action.

    diff --git a/showcase/angular/src/app/pages/agents/agents-page.component.ts b/showcase/angular/src/app/pages/agents/agents-page.component.ts index 339c63d90..f85c24077 100644 --- a/showcase/angular/src/app/pages/agents/agents-page.component.ts +++ b/showcase/angular/src/app/pages/agents/agents-page.component.ts @@ -291,7 +291,7 @@ export class AgentsPageComponent implements OnInit, OnDestroy { applicationCategory: 'DeveloperApplication', operatingSystem: 'macOS, Linux, Windows (Node.js 18+)', url: `${HOST}/agents`, - description: $localize`:@@agents.schema.software.description:Canonical OpenClaw and Hermes onboarding path for FSB. Doctor flow, stdio config printer, consent-gated multi-host installer, and 68 MCP browser tools for the FSB MCP server.`, + description: $localize`:@@agents.schema.software.description:Canonical OpenClaw and Hermes onboarding path for FSB. Doctor flow, stdio config printer, consent-gated multi-host installer, and 69 MCP browser tools for the FSB MCP server.`, featureList: [ $localize`:@@agents.schema.software.feature.tools:69 MCP tools for browser action, observation, verification, and recovery`, $localize`:@@agents.schema.software.feature.manual:Manual mode by default; run_task autopilot only on explicit delegation`, diff --git a/showcase/angular/src/app/pages/home/home-page.component.html b/showcase/angular/src/app/pages/home/home-page.component.html index 794ebcd2c..c9ba8f2cc 100644 --- a/showcase/angular/src/app/pages/home/home-page.component.html +++ b/showcase/angular/src/app/pages/home/home-page.component.html @@ -324,7 +324,7 @@

    Any agent. Any model. One

    Your agent drives FSB

    Coding agents and MCP clients call FSB's tools directly. Your agent's own model does the reasoning, so there is no FSB key to add and no second bill.

    - 68 MCP tools + 69 MCP tools One-command install No API key
    diff --git a/showcase/angular/src/app/pages/support/support-page.component.ts b/showcase/angular/src/app/pages/support/support-page.component.ts index 799ec8fb0..c9a41de1c 100644 --- a/showcase/angular/src/app/pages/support/support-page.component.ts +++ b/showcase/angular/src/app/pages/support/support-page.component.ts @@ -58,7 +58,7 @@ const SUPPORT_FAQ = { }, tools: { q: $localize`:@@support.schema.faq.tools.q:What can I do with FSB through MCP tools?`, - a: $localize`:@@support.schema.faq.tools.a:FSB exposes two modes through MCP. Manual mode is the default and gives the calling agent 68 granular tools: navigate, click, type, scroll, read page state, take screenshots, upload real local files with upload_file, arm trigger watchers with trigger / get_trigger_status / list_triggers, and search or invoke guarded first-party API capabilities with search_capabilities and invoke_capability. Vault and payment helpers fill inside the encrypted extension store, so raw secrets do not cross the MCP bridge. Autopilot mode (run_task) only fires when the user explicitly delegates the whole task. For everything else the calling agent stays in the loop on every action.`, + a: $localize`:@@support.schema.faq.tools.a:FSB exposes two modes through MCP. Manual mode is the default and gives the calling agent 69 granular tools: navigate, click, type, scroll, read page state, take screenshots, upload real local files with upload_file, arm trigger watchers with trigger / get_trigger_status / list_triggers, and search or invoke guarded first-party API capabilities with search_capabilities and invoke_capability. Vault and payment helpers fill inside the encrypted extension store, so raw secrets do not cross the MCP bridge. Autopilot mode (run_task) only fires when the user explicitly delegates the whole task. For everything else the calling agent stays in the loop on every action.`, }, openclawInstall: { q: $localize`:@@support.schema.faq.openclawInstall.q:How do I install FSB on OpenClaw?`, diff --git a/showcase/angular/src/locale/messages.de.xlf b/showcase/angular/src/locale/messages.de.xlf index e62aee833..c7c6fb41c 100644 --- a/showcase/angular/src/locale/messages.de.xlf +++ b/showcase/angular/src/locale/messages.de.xlf @@ -1735,8 +1735,8 @@ - Always-on agents like Grok Bot, Muse, and Dots ship with a cloud computer. FSB hands them yours: the Chrome you are already signed into, 68 MCP tools, and a verified result after every action. - Ständig aktive Agenten wie Grok Bot, Muse und Dots bringen einen Cloud-Computer mit. FSB gibt ihnen Zugang zu deinem: deinem bereits angemeldeten Chrome, 68 MCP-Tools und einem überprüften Ergebnis nach jeder Aktion. + Always-on agents like Grok Bot, Muse, and Dots ship with a cloud computer. FSB hands them yours: the Chrome you are already signed into, 69 MCP tools, and a verified result after every action. + Ständig aktive Agenten wie Grok Bot, Muse und Dots bringen einen Cloud-Computer mit. FSB gibt ihnen Zugang zu deinem: deinem bereits angemeldeten Chrome, 69 MCP-Tools und einem überprüften Ergebnis nach jeder Aktion. src/app/pages/agents/agents-page.component.html 15,17 @@ -2271,8 +2271,8 @@ - Canonical OpenClaw and Hermes onboarding path for FSB. Doctor flow, stdio config printer, consent-gated multi-host installer, and 68 MCP browser tools for the FSB MCP server. - Kanonischer OpenClaw- und Hermes-Onboarding-Pfad für FSB. Doctor-Flow, stdio-Config-Drucker, einwilligungsbasierter Multi-Host-Installer und 68 MCP-Browser-Tools für den FSB-MCP-Server. + Canonical OpenClaw and Hermes onboarding path for FSB. Doctor flow, stdio config printer, consent-gated multi-host installer, and 69 MCP browser tools for the FSB MCP server. + Kanonischer OpenClaw- und Hermes-Onboarding-Pfad für FSB. Doctor-Flow, stdio-Config-Drucker, einwilligungsbasierter Multi-Host-Installer und 69 MCP-Browser-Tools für den FSB-MCP-Server. src/app/pages/agents/agents-page.component.ts 294 @@ -6239,8 +6239,8 @@ - 68 MCP tools - 68 MCP-Tools + 69 MCP tools + 69 MCP-Tools src/app/pages/home/home-page.component.html 327,328 @@ -19515,8 +19515,8 @@ https://full-selfbrowsing.com/api/telemetry/forget - FSB exposes two modes through MCP. Manual mode is the default and gives the calling agent 68 granular tools: navigate, click, type, scroll, read page state, take screenshots, upload real local files with upload_file, arm trigger watchers with trigger / get_trigger_status / list_triggers, and search or invoke guarded first-party API capabilities with search_capabilities and invoke_capability. Vault and payment helpers fill inside the encrypted extension store, so raw secrets do not cross the MCP bridge. Autopilot mode (run_task) only fires when the user explicitly delegates the whole task. For everything else the calling agent stays in the loop on every action. - FSB stellt zwei Modi über MCP bereit. Der Manual-Modus ist Standard und gibt dem aufrufenden Agenten 68 granulare Tools: navigieren, klicken, tippen, scrollen, Seitenstatus lesen, Screenshots machen, echte lokale Dateien mit upload_file hochladen, Trigger-Watcher mit trigger / get_trigger_status / list_triggers scharf schalten und geschützte First-Party-API-Capabilities mit search_capabilities und invoke_capability suchen oder aufrufen. Vault- und Zahlungshelfer füllen innerhalb des verschlüsselten Extension-Stores, sodass Rohgeheimnisse die MCP-Brücke nicht überqueren. Der Autopilot-Modus (run_task) startet nur, wenn der Nutzer die gesamte Aufgabe ausdrücklich delegiert. Bei allem anderen bleibt der aufrufende Agent bei jeder Aktion in der Schleife. + FSB exposes two modes through MCP. Manual mode is the default and gives the calling agent 69 granular tools: navigate, click, type, scroll, read page state, take screenshots, upload real local files with upload_file, arm trigger watchers with trigger / get_trigger_status / list_triggers, and search or invoke guarded first-party API capabilities with search_capabilities and invoke_capability. Vault and payment helpers fill inside the encrypted extension store, so raw secrets do not cross the MCP bridge. Autopilot mode (run_task) only fires when the user explicitly delegates the whole task. For everything else the calling agent stays in the loop on every action. + FSB stellt zwei Modi über MCP bereit. Der Manual-Modus ist Standard und gibt dem aufrufenden Agenten 69 granulare Tools: navigieren, klicken, tippen, scrollen, Seitenstatus lesen, Screenshots machen, echte lokale Dateien mit upload_file hochladen, Trigger-Watcher mit trigger / get_trigger_status / list_triggers scharf schalten und geschützte First-Party-API-Capabilities mit search_capabilities und invoke_capability suchen oder aufrufen. Vault- und Zahlungshelfer füllen innerhalb des verschlüsselten Extension-Stores, sodass Rohgeheimnisse die MCP-Brücke nicht überqueren. Der Autopilot-Modus (run_task) startet nur, wenn der Nutzer die gesamte Aufgabe ausdrücklich delegiert. Bei allem anderen bleibt der aufrufende Agent bei jeder Aktion in der Schleife. src/app/pages/support/support-page.component.ts 61 diff --git a/showcase/angular/src/locale/messages.es.xlf b/showcase/angular/src/locale/messages.es.xlf index f79737ec2..531d05c38 100644 --- a/showcase/angular/src/locale/messages.es.xlf +++ b/showcase/angular/src/locale/messages.es.xlf @@ -1735,8 +1735,8 @@ - Always-on agents like Grok Bot, Muse, and Dots ship with a cloud computer. FSB hands them yours: the Chrome you are already signed into, 68 MCP tools, and a verified result after every action. - Agentes siempre activos como Grok Bot, Muse y Dots incluyen un ordenador en la nube. FSB les da acceso al tuyo: el Chrome donde ya has iniciado sesión, 68 herramientas MCP y un resultado verificado después de cada acción. + Always-on agents like Grok Bot, Muse, and Dots ship with a cloud computer. FSB hands them yours: the Chrome you are already signed into, 69 MCP tools, and a verified result after every action. + Agentes siempre activos como Grok Bot, Muse y Dots incluyen un ordenador en la nube. FSB les da acceso al tuyo: el Chrome donde ya has iniciado sesión, 69 herramientas MCP y un resultado verificado después de cada acción. src/app/pages/agents/agents-page.component.html 15,17 @@ -2271,8 +2271,8 @@ - Canonical OpenClaw and Hermes onboarding path for FSB. Doctor flow, stdio config printer, consent-gated multi-host installer, and 68 MCP browser tools for the FSB MCP server. - Ruta canónica de onboarding OpenClaw y Hermes para FSB. Flujo Doctor, impresora de config stdio, instalador multi-host con consentimiento y 68 herramientas de navegador MCP para el servidor MCP de FSB. + Canonical OpenClaw and Hermes onboarding path for FSB. Doctor flow, stdio config printer, consent-gated multi-host installer, and 69 MCP browser tools for the FSB MCP server. + Ruta canónica de onboarding OpenClaw y Hermes para FSB. Flujo Doctor, impresora de config stdio, instalador multi-host con consentimiento y 69 herramientas de navegador MCP para el servidor MCP de FSB. src/app/pages/agents/agents-page.component.ts 294 @@ -6239,8 +6239,8 @@ - 68 MCP tools - 68 herramientas MCP + 69 MCP tools + 69 herramientas MCP src/app/pages/home/home-page.component.html 327,328 @@ -19515,8 +19515,8 @@ https://full-selfbrowsing.com/api/telemetry/forget - FSB exposes two modes through MCP. Manual mode is the default and gives the calling agent 68 granular tools: navigate, click, type, scroll, read page state, take screenshots, upload real local files with upload_file, arm trigger watchers with trigger / get_trigger_status / list_triggers, and search or invoke guarded first-party API capabilities with search_capabilities and invoke_capability. Vault and payment helpers fill inside the encrypted extension store, so raw secrets do not cross the MCP bridge. Autopilot mode (run_task) only fires when the user explicitly delegates the whole task. For everything else the calling agent stays in the loop on every action. - FSB expone dos modos a través de MCP. El modo Manual es el predeterminado y da al agente llamante 68 herramientas granulares: navegar, hacer clic, escribir, desplazarse, leer el estado de la página, capturar pantallas, subir archivos locales reales con upload_file, armar vigilantes de triggers con trigger / get_trigger_status / list_triggers, y buscar o invocar capacidades API de primera parte protegidas con search_capabilities e invoke_capability. Los ayudantes de vault y pagos rellenan dentro del almacén cifrado de la extensión, así que los secretos en bruto no cruzan el puente MCP. El modo Autopilot (run_task) solo se activa cuando el usuario delega explícitamente toda la tarea. En todo lo demás, el agente llamante permanece en el bucle en cada acción. + FSB exposes two modes through MCP. Manual mode is the default and gives the calling agent 69 granular tools: navigate, click, type, scroll, read page state, take screenshots, upload real local files with upload_file, arm trigger watchers with trigger / get_trigger_status / list_triggers, and search or invoke guarded first-party API capabilities with search_capabilities and invoke_capability. Vault and payment helpers fill inside the encrypted extension store, so raw secrets do not cross the MCP bridge. Autopilot mode (run_task) only fires when the user explicitly delegates the whole task. For everything else the calling agent stays in the loop on every action. + FSB expone dos modos a través de MCP. El modo Manual es el predeterminado y da al agente llamante 69 herramientas granulares: navegar, hacer clic, escribir, desplazarse, leer el estado de la página, capturar pantallas, subir archivos locales reales con upload_file, armar vigilantes de triggers con trigger / get_trigger_status / list_triggers, y buscar o invocar capacidades API de primera parte protegidas con search_capabilities e invoke_capability. Los ayudantes de vault y pagos rellenan dentro del almacén cifrado de la extensión, así que los secretos en bruto no cruzan el puente MCP. El modo Autopilot (run_task) solo se activa cuando el usuario delega explícitamente toda la tarea. En todo lo demás, el agente llamante permanece en el bucle en cada acción. src/app/pages/support/support-page.component.ts 61 diff --git a/showcase/angular/src/locale/messages.ja.xlf b/showcase/angular/src/locale/messages.ja.xlf index b6088bced..ab16ac0a6 100644 --- a/showcase/angular/src/locale/messages.ja.xlf +++ b/showcase/angular/src/locale/messages.ja.xlf @@ -1735,8 +1735,8 @@ - Always-on agents like Grok Bot, Muse, and Dots ship with a cloud computer. FSB hands them yours: the Chrome you are already signed into, 68 MCP tools, and a verified result after every action. - 常時稼働するエージェントの Grok Bot、Muse、Dots にはクラウドコンピュータが付属しています。FSB は、あなたの環境を使えるようにします。ログイン済みの Chrome、68 個の MCP ツール、そして操作ごとに検証された結果を提供します。 + Always-on agents like Grok Bot, Muse, and Dots ship with a cloud computer. FSB hands them yours: the Chrome you are already signed into, 69 MCP tools, and a verified result after every action. + 常時稼働するエージェントの Grok Bot、Muse、Dots にはクラウドコンピュータが付属しています。FSB は、あなたの環境を使えるようにします。ログイン済みの Chrome、69 個の MCP ツール、そして操作ごとに検証された結果を提供します。 src/app/pages/agents/agents-page.component.html 15,17 @@ -2271,8 +2271,8 @@ - Canonical OpenClaw and Hermes onboarding path for FSB. Doctor flow, stdio config printer, consent-gated multi-host installer, and 68 MCP browser tools for the FSB MCP server. - FSB 向けの正規 OpenClaw / Hermes オンボーディング経路。Doctor フロー、stdio 設定プリンタ、同意ゲート付きマルチホストインストーラ、および FSB MCP サーバー用の 68 の MCP ブラウザツール。 + Canonical OpenClaw and Hermes onboarding path for FSB. Doctor flow, stdio config printer, consent-gated multi-host installer, and 69 MCP browser tools for the FSB MCP server. + FSB 向けの正規 OpenClaw / Hermes オンボーディング経路。Doctor フロー、stdio 設定プリンタ、同意ゲート付きマルチホストインストーラ、および FSB MCP サーバー用の 69 の MCP ブラウザツール。 src/app/pages/agents/agents-page.component.ts 294 @@ -6239,8 +6239,8 @@ - 68 MCP tools - 68 個の MCP ツール + 69 MCP tools + 69 個の MCP ツール src/app/pages/home/home-page.component.html 327,328 @@ -19515,8 +19515,8 @@ https://full-selfbrowsing.com/api/telemetry/forget - FSB exposes two modes through MCP. Manual mode is the default and gives the calling agent 68 granular tools: navigate, click, type, scroll, read page state, take screenshots, upload real local files with upload_file, arm trigger watchers with trigger / get_trigger_status / list_triggers, and search or invoke guarded first-party API capabilities with search_capabilities and invoke_capability. Vault and payment helpers fill inside the encrypted extension store, so raw secrets do not cross the MCP bridge. Autopilot mode (run_task) only fires when the user explicitly delegates the whole task. For everything else the calling agent stays in the loop on every action. - FSB は MCP 経由で 2 つのモードを公開します。デフォルトの Manual モードでは、呼び出し側エージェントに 68 の粒度の高いツールを提供します: ナビゲート、クリック、入力、スクロール、ページ状態の読み取り、スクリーンショット、upload_file による実ローカルファイルのアップロード、trigger / get_trigger_status / list_triggers によるトリガーウォッチャーのアーム、search_capabilities と invoke_capability によるガード付きファーストパーティ API 能力の検索・呼び出し。Vault と支払いヘルパーは暗号化された拡張機能ストア内で入力されるため、生のシークレットは MCP ブリッジを越えません。Autopilot モード (run_task) は、ユーザーがタスク全体を明示的に委任したときだけ起動します。それ以外は、呼び出し側エージェントがすべてのアクションでループに留まります。 + FSB exposes two modes through MCP. Manual mode is the default and gives the calling agent 69 granular tools: navigate, click, type, scroll, read page state, take screenshots, upload real local files with upload_file, arm trigger watchers with trigger / get_trigger_status / list_triggers, and search or invoke guarded first-party API capabilities with search_capabilities and invoke_capability. Vault and payment helpers fill inside the encrypted extension store, so raw secrets do not cross the MCP bridge. Autopilot mode (run_task) only fires when the user explicitly delegates the whole task. For everything else the calling agent stays in the loop on every action. + FSB は MCP 経由で 2 つのモードを公開します。デフォルトの Manual モードでは、呼び出し側エージェントに 69 の粒度の高いツールを提供します: ナビゲート、クリック、入力、スクロール、ページ状態の読み取り、スクリーンショット、upload_file による実ローカルファイルのアップロード、trigger / get_trigger_status / list_triggers によるトリガーウォッチャーのアーム、search_capabilities と invoke_capability によるガード付きファーストパーティ API 能力の検索・呼び出し。Vault と支払いヘルパーは暗号化された拡張機能ストア内で入力されるため、生のシークレットは MCP ブリッジを越えません。Autopilot モード (run_task) は、ユーザーがタスク全体を明示的に委任したときだけ起動します。それ以外は、呼び出し側エージェントがすべてのアクションでループに留まります。 src/app/pages/support/support-page.component.ts 61 diff --git a/showcase/angular/src/locale/messages.ko.xlf b/showcase/angular/src/locale/messages.ko.xlf index 9cb4d03e2..aebe61ed7 100644 --- a/showcase/angular/src/locale/messages.ko.xlf +++ b/showcase/angular/src/locale/messages.ko.xlf @@ -1735,8 +1735,8 @@ - Always-on agents like Grok Bot, Muse, and Dots ship with a cloud computer. FSB hands them yours: the Chrome you are already signed into, 68 MCP tools, and a verified result after every action. - 항상 실행되는 에이전트인 Grok Bot, Muse, Dots에는 클라우드 컴퓨터가 함께 제공됩니다. FSB는 여러분의 컴퓨터를 연결해 줍니다. 이미 로그인한 Chrome, MCP 도구 68개, 그리고 동작마다 검증된 결과를 이용할 수 있습니다. + Always-on agents like Grok Bot, Muse, and Dots ship with a cloud computer. FSB hands them yours: the Chrome you are already signed into, 69 MCP tools, and a verified result after every action. + 항상 실행되는 에이전트인 Grok Bot, Muse, Dots에는 클라우드 컴퓨터가 함께 제공됩니다. FSB는 여러분의 컴퓨터를 연결해 줍니다. 이미 로그인한 Chrome, MCP 도구 69개, 그리고 동작마다 검증된 결과를 이용할 수 있습니다. src/app/pages/agents/agents-page.component.html 15,17 @@ -2271,8 +2271,8 @@ - Canonical OpenClaw and Hermes onboarding path for FSB. Doctor flow, stdio config printer, consent-gated multi-host installer, and 68 MCP browser tools for the FSB MCP server. - FSB를 위한 표준 OpenClaw 및 Hermes 온보딩 경로입니다. 진단 흐름, stdio 구성 출력기, 동의를 거친 다중 호스트 설치 프로그램, 그리고 FSB MCP 서버를 위한 68개의 MCP 브라우저 도구를 제공합니다. + Canonical OpenClaw and Hermes onboarding path for FSB. Doctor flow, stdio config printer, consent-gated multi-host installer, and 69 MCP browser tools for the FSB MCP server. + FSB를 위한 표준 OpenClaw 및 Hermes 온보딩 경로입니다. 진단 흐름, stdio 구성 출력기, 동의를 거친 다중 호스트 설치 프로그램, 그리고 FSB MCP 서버를 위한 69개의 MCP 브라우저 도구를 제공합니다. src/app/pages/agents/agents-page.component.ts 294 @@ -6239,8 +6239,8 @@ - 68 MCP tools - MCP 도구 68개 + 69 MCP tools + MCP 도구 69개 src/app/pages/home/home-page.component.html 327,328 @@ -19515,8 +19515,8 @@ https://full-selfbrowsing.com/api/telemetry/forget - FSB exposes two modes through MCP. Manual mode is the default and gives the calling agent 68 granular tools: navigate, click, type, scroll, read page state, take screenshots, upload real local files with upload_file, arm trigger watchers with trigger / get_trigger_status / list_triggers, and search or invoke guarded first-party API capabilities with search_capabilities and invoke_capability. Vault and payment helpers fill inside the encrypted extension store, so raw secrets do not cross the MCP bridge. Autopilot mode (run_task) only fires when the user explicitly delegates the whole task. For everything else the calling agent stays in the loop on every action. - FSB는 MCP를 통해 두 가지 모드를 제공합니다. 수동 모드가 기본이며, 호출하는 에이전트에 68개의 세분화된 도구를 제공합니다. 이동, 클릭, 입력, 스크롤, 페이지 상태 읽기, 스크린샷 촬영, upload_file을 통한 실제 로컬 파일 업로드, trigger / get_trigger_status / list_triggers로 트리거 감시자 설정, search_capabilities와 invoke_capability로 보호된 1자 API 기능 검색 및 호출이 가능합니다. 볼트와 결제 보조 기능은 암호화된 확장 프로그램 저장소 안에서 입력하므로 원본 비밀 정보가 MCP 브리지를 넘지 않습니다. 오토파일럿 모드(run_task)는 사용자가 작업 전체를 명시적으로 위임할 때만 동작합니다. 그 외의 모든 경우에는 호출하는 에이전트가 모든 동작에 관여합니다. + FSB exposes two modes through MCP. Manual mode is the default and gives the calling agent 69 granular tools: navigate, click, type, scroll, read page state, take screenshots, upload real local files with upload_file, arm trigger watchers with trigger / get_trigger_status / list_triggers, and search or invoke guarded first-party API capabilities with search_capabilities and invoke_capability. Vault and payment helpers fill inside the encrypted extension store, so raw secrets do not cross the MCP bridge. Autopilot mode (run_task) only fires when the user explicitly delegates the whole task. For everything else the calling agent stays in the loop on every action. + FSB는 MCP를 통해 두 가지 모드를 제공합니다. 수동 모드가 기본이며, 호출하는 에이전트에 69개의 세분화된 도구를 제공합니다. 이동, 클릭, 입력, 스크롤, 페이지 상태 읽기, 스크린샷 촬영, upload_file을 통한 실제 로컬 파일 업로드, trigger / get_trigger_status / list_triggers로 트리거 감시자 설정, search_capabilities와 invoke_capability로 보호된 1자 API 기능 검색 및 호출이 가능합니다. 볼트와 결제 보조 기능은 암호화된 확장 프로그램 저장소 안에서 입력하므로 원본 비밀 정보가 MCP 브리지를 넘지 않습니다. 오토파일럿 모드(run_task)는 사용자가 작업 전체를 명시적으로 위임할 때만 동작합니다. 그 외의 모든 경우에는 호출하는 에이전트가 모든 동작에 관여합니다. src/app/pages/support/support-page.component.ts 61 diff --git a/showcase/angular/src/locale/messages.xlf b/showcase/angular/src/locale/messages.xlf index a9a0fa1fe..73176e075 100644 --- a/showcase/angular/src/locale/messages.xlf +++ b/showcase/angular/src/locale/messages.xlf @@ -1524,7 +1524,7 @@ - Always-on agents like Grok Bot, Muse, and Dots ship with a cloud computer. FSB hands them yours: the Chrome you are already signed into, 68 MCP tools, and a verified result after every action. + Always-on agents like Grok Bot, Muse, and Dots ship with a cloud computer. FSB hands them yours: the Chrome you are already signed into, 69 MCP tools, and a verified result after every action. src/app/pages/agents/agents-page.component.html 15,17 @@ -1993,7 +1993,7 @@ - Canonical OpenClaw and Hermes onboarding path for FSB. Doctor flow, stdio config printer, consent-gated multi-host installer, and 68 MCP browser tools for the FSB MCP server. + Canonical OpenClaw and Hermes onboarding path for FSB. Doctor flow, stdio config printer, consent-gated multi-host installer, and 69 MCP browser tools for the FSB MCP server. src/app/pages/agents/agents-page.component.ts 294 @@ -5472,7 +5472,7 @@ - 68 MCP tools + 69 MCP tools src/app/pages/home/home-page.component.html 327,328 @@ -17083,7 +17083,7 @@ https://full-selfbrowsing.com/api/telemetry/forget - FSB exposes two modes through MCP. Manual mode is the default and gives the calling agent 68 granular tools: navigate, click, type, scroll, read page state, take screenshots, upload real local files with upload_file, arm trigger watchers with trigger / get_trigger_status / list_triggers, and search or invoke guarded first-party API capabilities with search_capabilities and invoke_capability. Vault and payment helpers fill inside the encrypted extension store, so raw secrets do not cross the MCP bridge. Autopilot mode (run_task) only fires when the user explicitly delegates the whole task. For everything else the calling agent stays in the loop on every action. + FSB exposes two modes through MCP. Manual mode is the default and gives the calling agent 69 granular tools: navigate, click, type, scroll, read page state, take screenshots, upload real local files with upload_file, arm trigger watchers with trigger / get_trigger_status / list_triggers, and search or invoke guarded first-party API capabilities with search_capabilities and invoke_capability. Vault and payment helpers fill inside the encrypted extension store, so raw secrets do not cross the MCP bridge. Autopilot mode (run_task) only fires when the user explicitly delegates the whole task. For everything else the calling agent stays in the loop on every action. src/app/pages/support/support-page.component.ts 61 diff --git a/showcase/angular/src/locale/messages.zh-CN.xlf b/showcase/angular/src/locale/messages.zh-CN.xlf index b3570cac3..d6152f86a 100644 --- a/showcase/angular/src/locale/messages.zh-CN.xlf +++ b/showcase/angular/src/locale/messages.zh-CN.xlf @@ -1735,8 +1735,8 @@ - Always-on agents like Grok Bot, Muse, and Dots ship with a cloud computer. FSB hands them yours: the Chrome you are already signed into, 68 MCP tools, and a verified result after every action. - Grok Bot、Muse 和 Dots 等全天候智能体自带云端计算机。FSB 让它们使用你的计算机:已登录的 Chrome、68 个 MCP 工具,以及每次操作后经过验证的结果。 + Always-on agents like Grok Bot, Muse, and Dots ship with a cloud computer. FSB hands them yours: the Chrome you are already signed into, 69 MCP tools, and a verified result after every action. + Grok Bot、Muse 和 Dots 等全天候智能体自带云端计算机。FSB 让它们使用你的计算机:已登录的 Chrome、69 个 MCP 工具,以及每次操作后经过验证的结果。 src/app/pages/agents/agents-page.component.html 15,17 @@ -2271,8 +2271,8 @@ - Canonical OpenClaw and Hermes onboarding path for FSB. Doctor flow, stdio config printer, consent-gated multi-host installer, and 68 MCP browser tools for the FSB MCP server. - FSB 的标准 OpenClaw 与 Hermes 接入路径。Doctor 流程、stdio 配置打印、经同意门控的多主机安装程序,以及面向 FSB MCP 服务器的 68 个 MCP 浏览器工具。 + Canonical OpenClaw and Hermes onboarding path for FSB. Doctor flow, stdio config printer, consent-gated multi-host installer, and 69 MCP browser tools for the FSB MCP server. + FSB 的标准 OpenClaw 与 Hermes 接入路径。Doctor 流程、stdio 配置打印、经同意门控的多主机安装程序,以及面向 FSB MCP 服务器的 69 个 MCP 浏览器工具。 src/app/pages/agents/agents-page.component.ts 294 @@ -6239,8 +6239,8 @@ - 68 MCP tools - 68 个 MCP 工具 + 69 MCP tools + 69 个 MCP 工具 src/app/pages/home/home-page.component.html 327,328 @@ -19515,8 +19515,8 @@ https://full-selfbrowsing.com/api/telemetry/forget - FSB exposes two modes through MCP. Manual mode is the default and gives the calling agent 68 granular tools: navigate, click, type, scroll, read page state, take screenshots, upload real local files with upload_file, arm trigger watchers with trigger / get_trigger_status / list_triggers, and search or invoke guarded first-party API capabilities with search_capabilities and invoke_capability. Vault and payment helpers fill inside the encrypted extension store, so raw secrets do not cross the MCP bridge. Autopilot mode (run_task) only fires when the user explicitly delegates the whole task. For everything else the calling agent stays in the loop on every action. - FSB 通过 MCP 提供两种模式。默认的 Manual 模式为调用方智能体提供 68 个细粒度工具:导航、点击、输入、滚动、读取页面状态、截图、用 upload_file 上传真实本地文件、用 trigger / get_trigger_status / list_triggers 部署触发监视器,以及用 search_capabilities 与 invoke_capability 搜索或调用受保护的第一方 API 能力。保险库与支付助手在加密的扩展存储内填写,因此原始密钥不会越过 MCP 桥接。Autopilot 模式(run_task)仅在用户明确委派整项任务时才会启动。其余情况下,调用方智能体在每个操作上都保持在环路中。 + FSB exposes two modes through MCP. Manual mode is the default and gives the calling agent 69 granular tools: navigate, click, type, scroll, read page state, take screenshots, upload real local files with upload_file, arm trigger watchers with trigger / get_trigger_status / list_triggers, and search or invoke guarded first-party API capabilities with search_capabilities and invoke_capability. Vault and payment helpers fill inside the encrypted extension store, so raw secrets do not cross the MCP bridge. Autopilot mode (run_task) only fires when the user explicitly delegates the whole task. For everything else the calling agent stays in the loop on every action. + FSB 通过 MCP 提供两种模式。默认的 Manual 模式为调用方智能体提供 69 个细粒度工具:导航、点击、输入、滚动、读取页面状态、截图、用 upload_file 上传真实本地文件、用 trigger / get_trigger_status / list_triggers 部署触发监视器,以及用 search_capabilities 与 invoke_capability 搜索或调用受保护的第一方 API 能力。保险库与支付助手在加密的扩展存储内填写,因此原始密钥不会越过 MCP 桥接。Autopilot 模式(run_task)仅在用户明确委派整项任务时才会启动。其余情况下,调用方智能体在每个操作上都保持在环路中。 src/app/pages/support/support-page.component.ts 61 diff --git a/showcase/angular/src/locale/messages.zh-TW.xlf b/showcase/angular/src/locale/messages.zh-TW.xlf index 01164e2f9..41ea9d3bb 100644 --- a/showcase/angular/src/locale/messages.zh-TW.xlf +++ b/showcase/angular/src/locale/messages.zh-TW.xlf @@ -1735,8 +1735,8 @@ - Always-on agents like Grok Bot, Muse, and Dots ship with a cloud computer. FSB hands them yours: the Chrome you are already signed into, 68 MCP tools, and a verified result after every action. - Grok Bot、Muse 和 Dots 等全天候代理都附有雲端電腦。FSB 讓它們使用你的電腦:已登入的 Chrome、68 個 MCP 工具,以及每次操作後經過驗證的結果。 + Always-on agents like Grok Bot, Muse, and Dots ship with a cloud computer. FSB hands them yours: the Chrome you are already signed into, 69 MCP tools, and a verified result after every action. + Grok Bot、Muse 和 Dots 等全天候代理都附有雲端電腦。FSB 讓它們使用你的電腦:已登入的 Chrome、69 個 MCP 工具,以及每次操作後經過驗證的結果。 src/app/pages/agents/agents-page.component.html 15,17 @@ -2271,8 +2271,8 @@ - Canonical OpenClaw and Hermes onboarding path for FSB. Doctor flow, stdio config printer, consent-gated multi-host installer, and 68 MCP browser tools for the FSB MCP server. - FSB 的標準 OpenClaw 與 Hermes 接入路徑。Doctor 流程、stdio 設定列印、經同意閘控的多主機安裝程式,以及面向 FSB MCP 伺服器的 68 個 MCP 瀏覽器工具。 + Canonical OpenClaw and Hermes onboarding path for FSB. Doctor flow, stdio config printer, consent-gated multi-host installer, and 69 MCP browser tools for the FSB MCP server. + FSB 的標準 OpenClaw 與 Hermes 接入路徑。Doctor 流程、stdio 設定列印、經同意閘控的多主機安裝程式,以及面向 FSB MCP 伺服器的 69 個 MCP 瀏覽器工具。 src/app/pages/agents/agents-page.component.ts 294 @@ -6239,8 +6239,8 @@ - 68 MCP tools - 68 個 MCP 工具 + 69 MCP tools + 69 個 MCP 工具 src/app/pages/home/home-page.component.html 327,328 @@ -19515,8 +19515,8 @@ https://full-selfbrowsing.com/api/telemetry/forget - FSB exposes two modes through MCP. Manual mode is the default and gives the calling agent 68 granular tools: navigate, click, type, scroll, read page state, take screenshots, upload real local files with upload_file, arm trigger watchers with trigger / get_trigger_status / list_triggers, and search or invoke guarded first-party API capabilities with search_capabilities and invoke_capability. Vault and payment helpers fill inside the encrypted extension store, so raw secrets do not cross the MCP bridge. Autopilot mode (run_task) only fires when the user explicitly delegates the whole task. For everything else the calling agent stays in the loop on every action. - FSB 透過 MCP 提供兩種模式。預設的 Manual 模式為呼叫端代理提供 68 個細粒度工具:導覽、點擊、輸入、捲動、讀取頁面狀態、截圖、用 upload_file 上傳真實本機檔案、用 trigger / get_trigger_status / list_triggers 部署觸發監視器,以及用 search_capabilities 與 invoke_capability 搜尋或呼叫受保護的第一方 API 能力。保險庫與付款助手在加密的擴充功能儲存內填寫,因此原始金鑰不會越過 MCP 橋接。Autopilot 模式(run_task)僅在使用者明確委派整項任務時才會啟動。其餘情況下,呼叫端代理在每個操作上都保持在迴圈中。 + FSB exposes two modes through MCP. Manual mode is the default and gives the calling agent 69 granular tools: navigate, click, type, scroll, read page state, take screenshots, upload real local files with upload_file, arm trigger watchers with trigger / get_trigger_status / list_triggers, and search or invoke guarded first-party API capabilities with search_capabilities and invoke_capability. Vault and payment helpers fill inside the encrypted extension store, so raw secrets do not cross the MCP bridge. Autopilot mode (run_task) only fires when the user explicitly delegates the whole task. For everything else the calling agent stays in the loop on every action. + FSB 透過 MCP 提供兩種模式。預設的 Manual 模式為呼叫端代理提供 69 個細粒度工具:導覽、點擊、輸入、捲動、讀取頁面狀態、截圖、用 upload_file 上傳真實本機檔案、用 trigger / get_trigger_status / list_triggers 部署觸發監視器,以及用 search_capabilities 與 invoke_capability 搜尋或呼叫受保護的第一方 API 能力。保險庫與付款助手在加密的擴充功能儲存內填寫,因此原始金鑰不會越過 MCP 橋接。Autopilot 模式(run_task)僅在使用者明確委派整項任務時才會啟動。其餘情況下,呼叫端代理在每個操作上都保持在迴圈中。 src/app/pages/support/support-page.component.ts 61 diff --git a/showcase/angular/src/locale/same-source-allowlist.json b/showcase/angular/src/locale/same-source-allowlist.json index 36005f8cc..260da84fc 100644 --- a/showcase/angular/src/locale/same-source-allowlist.json +++ b/showcase/angular/src/locale/same-source-allowlist.json @@ -1621,8 +1621,8 @@ }, { "id": "home.engine.mcp.fact.tools", - "sourceSha256": "be82197e9662c8e2c9788aea0a27c4dc9e51db4ae7e456eefb39ab88c021de34", - "targetSha256": "04d914741631f87377073bf5fa253d9b4a9d90379573b16c970f16d2afaa886f", + "sourceSha256": "485c669be39838cb5f5e9593d6393b2eeb78676f283835af645bb7f1625946e6", + "targetSha256": "dedc78f12f58994c4f436de0107e23cc658ee138e559e5b14b9ab9c1f1a0027e", "reason": "Tools is the standard German technical loanword, capitalized as a noun; the MCP compound uses a German hyphen." } ] diff --git a/tests/mcp-version-parity.test.js b/tests/mcp-version-parity.test.js index 1e0e2de8b..b5b30ead1 100644 --- a/tests/mcp-version-parity.test.js +++ b/tests/mcp-version-parity.test.js @@ -331,13 +331,25 @@ async function run() { for (const retired of ['start_visual_session', 'end_visual_session', 'back']) { assert(!registeredToolNames.includes(retired), `retired ${retired} tool stays off the MCP surface`); } - // A count claim is "N tools", "N MCP tools", "N registered (MCP) tools" or "N-tool". - const TOOL_COUNT_CLAIM = /(? Array.from(text.matchAll(TOOL_COUNT_CLAIM), (match) => Number(match[1])); + // A count claim is "N-tool", "N [registered|granular] [MCP [browser]] tools", + // or a "Tools ... N available" row. Markup is stripped first, so + // "68 MCP tools" reads as "68 MCP tools". "53 browser tools" and + // "4 trigger tools" count the extension's own registry, not MCP, and stay out. + const TOOL_COUNT_CLAIM = /(? text + .replace(/]*\/>/gu, '') + .replace(/<[^>]+>/gu, ' ') + .replace(/\s+/gu, ' '); + const claimedCounts = (text) => Array.from(withoutMarkup(text).matchAll(TOOL_COUNT_CLAIM), + (match) => Number(match[1] ?? match[2])); const statesToolCount = new RegExp(`(? 0 || sourcesWithoutClaims.has(name), + `${name} still states a tool count the check can read`); assert(counts.every((count) => count === toolCount), `${name} states the ${toolCount}-tool registered surface (claims found: ${counts.join(', ') || 'none'})`); continue; @@ -353,7 +365,7 @@ async function run() { `${name} ${unit[1]} source states ${toolCount} tools (got ${counts.join(', ')})`); const target = (unit[0].match(/]*>([\s\S]*?)<\/target>/u) || [])[1]; if (target !== undefined) { - assert(statesToolCount.test(target), `${name} ${unit[1]} translation states ${toolCount} tools`); + assert(statesToolCount.test(withoutMarkup(target)), `${name} ${unit[1]} translation states ${toolCount} tools`); } } }