claude-flow 3.21.1 → 3.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/helpers/.helpers-version +1 -0
- package/.claude/helpers/auto-memory-hook.mjs +59 -274
- package/.claude/helpers/helpers.manifest.json +12 -0
- package/.claude/helpers/hook-handler.cjs +132 -112
- package/.claude/helpers/intelligence.cjs +992 -196
- package/package.json +1 -1
- package/v3/@claude-flow/cli/dist/src/commands/daemon.js +23 -2
- package/v3/@claude-flow/cli/dist/src/commands/memory-backup.d.ts +11 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory-backup.js +46 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory-distill.d.ts +27 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory-distill.js +374 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +5 -2
- package/v3/@claude-flow/cli/dist/src/index.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/index.js +22 -1
- package/v3/@claude-flow/cli/dist/src/init/executor.js +20 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +19 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +175 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-signing.d.ts +30 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-signing.js +60 -0
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.d.ts +16 -0
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +63 -6
- package/v3/@claude-flow/cli/dist/src/init/statusline-generator.js +18 -7
- package/v3/@claude-flow/cli/dist/src/mcp-client.js +13 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-speculate-tools.js +9 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.d.ts +162 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.js +548 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.d.ts +9 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.js +4 -1
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +67 -47
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +71 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +330 -2
- package/v3/@claude-flow/cli/dist/src/services/distill-tuning.d.ts +111 -0
- package/v3/@claude-flow/cli/dist/src/services/distill-tuning.js +510 -0
- package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +24 -0
- package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +94 -0
- package/v3/@claude-flow/cli/dist/src/services/memory-distillation.d.ts +41 -0
- package/v3/@claude-flow/cli/dist/src/services/memory-distillation.js +277 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +31 -2
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +159 -6
- package/v3/@claude-flow/cli/package.json +5 -2
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Intent MCP Tools — ADR-175 page-agent integration.
|
|
3
|
+
*
|
|
4
|
+
* Adds a natural-language INTENT layer on top of the low-level selector-based
|
|
5
|
+
* `browser_*` tools (browser-tools.ts, driven by the `agent-browser` CLI).
|
|
6
|
+
* `browser_act({ task })` lets a caller say "Click the login button" instead
|
|
7
|
+
* of chaining browser_snapshot + browser_click.
|
|
8
|
+
*
|
|
9
|
+
* Backing library: `page-agent` (npm, MIT, https://github.com/alibaba/page-agent)
|
|
10
|
+
* — in-page injected JS that turns the DOM into text and lets an LLM execute
|
|
11
|
+
* natural-language intents via `agent.execute('Click login')`.
|
|
12
|
+
*
|
|
13
|
+
* ============================================================================
|
|
14
|
+
* VERIFIED API (recorded 2026-07-04 against page-agent@1.11.0 — `npm view
|
|
15
|
+
* page-agent`, the published README, and the actual npm tarball contents).
|
|
16
|
+
* Deviations from the original integration brief are called out inline.
|
|
17
|
+
* ============================================================================
|
|
18
|
+
*
|
|
19
|
+
* - Package layout changed since the brief was written: `page-agent` is now a
|
|
20
|
+
* thin composition of `@page-agent/core` + `@page-agent/llms` +
|
|
21
|
+
* `@page-agent/page-controller` + `@page-agent/ui`. The npm package is
|
|
22
|
+
* ESM-only (`"type":"module"`) and ships two builds:
|
|
23
|
+
* - `dist/esm/page-agent.js` — the Node/bundler ESM entry.
|
|
24
|
+
* - `dist/iife/page-agent.demo.js` — a self-contained browser IIFE that
|
|
25
|
+
* sets `window.PageAgent = <class>` (this is what we inject).
|
|
26
|
+
*
|
|
27
|
+
* - DEVIATION #1 (load-bearing): `await import('page-agent')` is NOT a safe
|
|
28
|
+
* availability probe in Node. Empirically, importing the ESM entry throws
|
|
29
|
+
* `ReferenceError: window is not defined` — even when the package IS
|
|
30
|
+
* correctly installed — because `@page-agent/core`'s module graph touches
|
|
31
|
+
* DOM globals at import time (it's designed to run in a browser, not Node).
|
|
32
|
+
* This differs from the agenticow-loader.ts pattern used elsewhere in this
|
|
33
|
+
* repo (dynamic `import()` + catch `ERR_MODULE_NOT_FOUND`), which only
|
|
34
|
+
* works for pure-Node optional deps. Instead we detect availability via
|
|
35
|
+
* `require.resolve('page-agent')` — path resolution only, never executes
|
|
36
|
+
* module code — the same technique `browser-session-tools.ts` already uses
|
|
37
|
+
* to resolve the local `ruvector` CLI bin without invoking it.
|
|
38
|
+
*
|
|
39
|
+
* - DEVIATION #2: the IIFE bundle unconditionally (also) auto-constructs a
|
|
40
|
+
* DEMO `PageAgent` instance via `setTimeout`, pointed at Alibaba's public
|
|
41
|
+
* sandbox endpoint (`model: 'qwen3.5-plus'`, a `page-ag-testing-*.run`
|
|
42
|
+
* baseURL, `apiKey: 'NA'`) UNLESS the bundle is loaded via a real
|
|
43
|
+
* `<script src="...?autoInit=false">` tag — the guard reads
|
|
44
|
+
* `document.currentScript.src`, which is `null` when the bundle runs via
|
|
45
|
+
* `eval`/CDP (our only injection path through the `agent-browser`
|
|
46
|
+
* primitive set). Left alone, every `browser_act` call would leak page
|
|
47
|
+
* content to Alibaba's test endpoint using a placeholder key. We strip the
|
|
48
|
+
* demo auto-init tail (`stripDemoAutoInit`) before injecting rather than
|
|
49
|
+
* relying on the query-param guard.
|
|
50
|
+
*
|
|
51
|
+
* - Constructor: `new PageAgent({ model, baseURL, apiKey, language })` —
|
|
52
|
+
* `apiKey` is OPTIONAL on the type (`LLMConfig.apiKey?: string`), confirmed
|
|
53
|
+
* in `@page-agent/llms`' `LLMConfig` interface.
|
|
54
|
+
*
|
|
55
|
+
* - `execute(task: string): Promise<ExecutionResult>` where
|
|
56
|
+
* `ExecutionResult = { success: boolean; data: string; history: HistoricalEvent[] }`
|
|
57
|
+
* — DEVIATION #3: NOT `{ success, result, steps }` as sketched in the
|
|
58
|
+
* original brief. We map `data` → `result` and `history.length` → `steps`
|
|
59
|
+
* in the tool's own return shape, and also pass `history` through for
|
|
60
|
+
* callers that want the full step trace.
|
|
61
|
+
*
|
|
62
|
+
* - LLM transport: confirmed via `@page-agent/llms`' `OpenAIClient` — a
|
|
63
|
+
* single non-streaming `POST ${baseURL}/chat/completions` with
|
|
64
|
+
* `Authorization: Bearer ${apiKey}` and OpenAI tool-calling JSON. This is
|
|
65
|
+
* why a bare `ANTHROPIC_API_KEY` cannot be handed to page-agent directly:
|
|
66
|
+
* Anthropic's native API is a different shape (`/v1/messages`, different
|
|
67
|
+
* tool-call format). We require an OpenAI-*compatible* endpoint — reusing
|
|
68
|
+
* this CLI's own Tier-3 fallback ladder from `agent-execute-core.ts`
|
|
69
|
+
* (OpenRouter, then Ollama Cloud), or an explicit override.
|
|
70
|
+
*
|
|
71
|
+
* ARCHITECTURAL CONSTRAINT (mirrors metaharness-tools.ts / agenticow-tools.ts)
|
|
72
|
+
* `page-agent` lives in `optionalDependencies`. Every code path degrades to
|
|
73
|
+
* `{ degraded: true }` — never throws — when the package or an LLM provider
|
|
74
|
+
* isn't available.
|
|
75
|
+
*
|
|
76
|
+
* KEY-SAFETY CONSTRAINT (load-bearing): page-agent's LLM calls happen FROM
|
|
77
|
+
* the browser page context (it's client-side injected JS), so its `apiKey`
|
|
78
|
+
* config field is necessarily visible to whatever we inject into the page.
|
|
79
|
+
* To avoid ever placing a real provider key into a page-context string, we
|
|
80
|
+
* start a short-lived local HTTP proxy (`startLocalLLMProxy`) bound to
|
|
81
|
+
* 127.0.0.1 that holds the REAL key server-side and injects the real
|
|
82
|
+
* `Authorization` header itself; the page only ever sees the proxy's
|
|
83
|
+
* loopback URL plus a constant placeholder string (`PLACEHOLDER_API_KEY`).
|
|
84
|
+
*
|
|
85
|
+
* @module @claude-flow/cli/mcp-tools/browser-intent
|
|
86
|
+
*/
|
|
87
|
+
import { createRequire } from 'node:module';
|
|
88
|
+
import { createServer } from 'node:http';
|
|
89
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
90
|
+
import { dirname, join } from 'node:path';
|
|
91
|
+
import { validateText, validateIdentifier } from './validate-input.js';
|
|
92
|
+
import { execBrowserCommand } from './browser-tools.js';
|
|
93
|
+
import { storeEntry } from '../memory/memory-initializer.js';
|
|
94
|
+
/**
|
|
95
|
+
* Locate the installed `page-agent` package's browser bundle without ever
|
|
96
|
+
* executing its module code. `resolvePkg` is injectable for tests (DI over
|
|
97
|
+
* module mocking, since `vi.mock('page-agent', ...)` cannot simulate absence
|
|
98
|
+
* for a package we deliberately never `import()`).
|
|
99
|
+
*/
|
|
100
|
+
export function locatePageAgentBundle(resolvePkg = (id) => createRequire(import.meta.url).resolve(id)) {
|
|
101
|
+
let entry;
|
|
102
|
+
try {
|
|
103
|
+
entry = resolvePkg('page-agent');
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const e = err;
|
|
107
|
+
if (e?.code === 'MODULE_NOT_FOUND' ||
|
|
108
|
+
e?.code === 'ERR_MODULE_NOT_FOUND' ||
|
|
109
|
+
/Cannot find (module|package)/i.test(String(e?.message))) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
// entry resolves to <root>/dist/esm/page-agent.js — walk up looking for the
|
|
115
|
+
// package's own package.json (mirrors resolveLocalRuvectorCli's technique
|
|
116
|
+
// in browser-session-tools.ts).
|
|
117
|
+
let dir = dirname(entry);
|
|
118
|
+
for (let i = 0; i < 6; i++) {
|
|
119
|
+
try {
|
|
120
|
+
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));
|
|
121
|
+
if (pkg.name === 'page-agent') {
|
|
122
|
+
const iifePath = join(dir, 'dist', 'iife', 'page-agent.demo.js');
|
|
123
|
+
if (!existsSync(iifePath))
|
|
124
|
+
return null;
|
|
125
|
+
return { root: dir, iifePath, version: pkg.version ?? 'unknown' };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// no package.json at this level — keep walking up
|
|
130
|
+
}
|
|
131
|
+
const parent = dirname(dir);
|
|
132
|
+
if (parent === dir)
|
|
133
|
+
break;
|
|
134
|
+
dir = parent;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
export function resolvePageAgentLLMConfig(env = process.env) {
|
|
139
|
+
// 1. Explicit override — a caller-configured OpenAI-compatible endpoint
|
|
140
|
+
// dedicated to page-agent (private proxy, Azure OpenAI, vLLM, etc).
|
|
141
|
+
if (env.CLAUDE_FLOW_PAGE_AGENT_BASE_URL && env.CLAUDE_FLOW_PAGE_AGENT_API_KEY) {
|
|
142
|
+
return {
|
|
143
|
+
baseURL: env.CLAUDE_FLOW_PAGE_AGENT_BASE_URL,
|
|
144
|
+
apiKey: env.CLAUDE_FLOW_PAGE_AGENT_API_KEY,
|
|
145
|
+
model: env.CLAUDE_FLOW_PAGE_AGENT_MODEL || 'gpt-4o-mini',
|
|
146
|
+
source: 'explicit',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// 2. OpenRouter — this CLI's own Tier-3 OpenAI-compatible fallback.
|
|
150
|
+
if (env.OPENROUTER_API_KEY) {
|
|
151
|
+
const base = (env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api').replace(/\/+$/, '');
|
|
152
|
+
return {
|
|
153
|
+
baseURL: base.endsWith('/v1') ? base : `${base}/v1`,
|
|
154
|
+
apiKey: env.OPENROUTER_API_KEY,
|
|
155
|
+
model: env.OPENROUTER_DEFAULT_MODEL || 'anthropic/claude-sonnet-4-6',
|
|
156
|
+
source: 'openrouter',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// 3. Ollama Cloud — OpenAI-compatible per #1725.
|
|
160
|
+
if (env.OLLAMA_API_KEY) {
|
|
161
|
+
const base = (env.OLLAMA_BASE_URL || 'https://ollama.com').replace(/\/+$/, '');
|
|
162
|
+
return {
|
|
163
|
+
baseURL: base.endsWith('/v1') ? base : `${base}/v1`,
|
|
164
|
+
apiKey: env.OLLAMA_API_KEY,
|
|
165
|
+
model: env.CLAUDE_FLOW_PAGE_AGENT_OLLAMA_MODEL || 'qwen2.5-coder:32b-cloud',
|
|
166
|
+
source: 'ollama',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// Local loopback LLM proxy — keeps the real provider key server-side. Only
|
|
173
|
+
// the proxy's 127.0.0.1 URL + a placeholder string ever reach page context.
|
|
174
|
+
// ============================================================================
|
|
175
|
+
export const PLACEHOLDER_API_KEY = 'ruflo-proxied-key';
|
|
176
|
+
export function startLocalLLMProxy(real) {
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
const server = createServer((req, res) => {
|
|
179
|
+
const target = `${real.baseURL.replace(/\/+$/, '')}${req.url ?? ''}`;
|
|
180
|
+
const chunks = [];
|
|
181
|
+
req.on('data', (c) => chunks.push(c));
|
|
182
|
+
req.on('end', () => {
|
|
183
|
+
const body = Buffer.concat(chunks);
|
|
184
|
+
fetch(target, {
|
|
185
|
+
method: req.method,
|
|
186
|
+
headers: {
|
|
187
|
+
'content-type': 'application/json',
|
|
188
|
+
// The real key is injected here, server-side, and nowhere else —
|
|
189
|
+
// whatever Authorization header the page-context request carried
|
|
190
|
+
// (the placeholder) is discarded.
|
|
191
|
+
authorization: `Bearer ${real.apiKey}`,
|
|
192
|
+
},
|
|
193
|
+
body: body.length > 0 ? body : undefined,
|
|
194
|
+
})
|
|
195
|
+
.then(async (upstream) => {
|
|
196
|
+
const text = await upstream.text();
|
|
197
|
+
res.writeHead(upstream.status, {
|
|
198
|
+
'content-type': upstream.headers.get('content-type') || 'application/json',
|
|
199
|
+
});
|
|
200
|
+
res.end(text);
|
|
201
|
+
})
|
|
202
|
+
.catch((err) => {
|
|
203
|
+
res.writeHead(502, { 'content-type': 'application/json' });
|
|
204
|
+
res.end(JSON.stringify({ error: { message: String(err instanceof Error ? err.message : err) } }));
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
server.on('error', reject);
|
|
209
|
+
server.listen(0, '127.0.0.1', () => {
|
|
210
|
+
const addr = server.address();
|
|
211
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
212
|
+
resolve({
|
|
213
|
+
url: `http://127.0.0.1:${port}`,
|
|
214
|
+
close: () => {
|
|
215
|
+
try {
|
|
216
|
+
server.close();
|
|
217
|
+
}
|
|
218
|
+
catch { /* best-effort */ }
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
// ============================================================================
|
|
225
|
+
// Injection script assembly
|
|
226
|
+
// ============================================================================
|
|
227
|
+
/** Marker at which the demo bundle's always-on auto-init tail begins (see DEVIATION #2). */
|
|
228
|
+
const DEMO_TAIL_MARKER = 'var DEMO_MODEL=';
|
|
229
|
+
/**
|
|
230
|
+
* Fail-CLOSED firewall signatures. page-agent's shipped IIFE (`page-agent.demo.js`)
|
|
231
|
+
* auto-POSTs page content to Alibaba's public sandbox on inject
|
|
232
|
+
* (`https://page-ag-testing-*.<region>.fcapp.run`, model `DEMO_MODEL`). We strip
|
|
233
|
+
* that tail, but the strip is only best-effort text matching. These signatures
|
|
234
|
+
* are the load-bearing guarantee: if ANY survive the strip, we REFUSE to inject.
|
|
235
|
+
* Version-independent — an upstream bundle change that moves/renames the marker
|
|
236
|
+
* can never silently re-enable the leak (the previous strip fell OPEN and
|
|
237
|
+
* injected as-is when the marker moved; this cannot).
|
|
238
|
+
*/
|
|
239
|
+
const DEMO_LEAK_SIGNATURES = [
|
|
240
|
+
/page-ag-testing/i, // the demo endpoint subdomain prefix
|
|
241
|
+
/\.fcapp\.run/i, // Alibaba Function Compute host (the demo backend)
|
|
242
|
+
/\bDEMO_MODEL\b/, // the demo auto-init marker/model
|
|
243
|
+
];
|
|
244
|
+
/** Thrown when a bundle still contains a demo/sandbox endpoint after stripping. */
|
|
245
|
+
export class DemoLeakError extends Error {
|
|
246
|
+
signature;
|
|
247
|
+
constructor(signature) {
|
|
248
|
+
super(`page-agent bundle still contains a demo/sandbox endpoint (matched /${signature}/) ` +
|
|
249
|
+
'after stripping — refusing to inject to avoid leaking page content to Alibaba\'s sandbox');
|
|
250
|
+
this.signature = signature;
|
|
251
|
+
this.name = 'DemoLeakError';
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** Returns the first demo-leak signature found in `source`, or null if clean. */
|
|
255
|
+
export function findDemoLeak(source) {
|
|
256
|
+
for (const re of DEMO_LEAK_SIGNATURES) {
|
|
257
|
+
if (re.test(source))
|
|
258
|
+
return re.source;
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Strip the demo bundle's auto-init tail so injecting it via eval doesn't spin
|
|
264
|
+
* up a second `window.pageAgent` pointed at Alibaba's sandbox endpoint. This is
|
|
265
|
+
* best-effort at the text level; the real guarantee is the fail-closed
|
|
266
|
+
* `findDemoLeak` firewall enforced in `buildPageAgentInjection`, NOT this strip.
|
|
267
|
+
*/
|
|
268
|
+
export function stripDemoAutoInit(iifeSource) {
|
|
269
|
+
const idx = iifeSource.indexOf(DEMO_TAIL_MARKER);
|
|
270
|
+
return idx === -1 ? iifeSource : iifeSource.slice(0, idx);
|
|
271
|
+
}
|
|
272
|
+
export const BROWSER_ACT_RESULT_GLOBAL = '__ruflo_browser_act_result__';
|
|
273
|
+
/**
|
|
274
|
+
* Build the full script to hand to `browser_eval` / `agent-browser eval`:
|
|
275
|
+
* the (demo-stripped) IIFE bundle, followed by our own controlled
|
|
276
|
+
* construction + `execute()` call whose settled result lands on a window
|
|
277
|
+
* global we can poll for.
|
|
278
|
+
*
|
|
279
|
+
* KEY-SAFETY: `pageConfig.apiKey` MUST be `PLACEHOLDER_API_KEY` (or another
|
|
280
|
+
* non-secret placeholder) — callers are responsible for routing the real key
|
|
281
|
+
* through `startLocalLLMProxy` first. This function only assembles strings;
|
|
282
|
+
* it does not itself guarantee key safety, so callers MUST NOT pass a real
|
|
283
|
+
* key here (see `browserIntentTools` handler for the enforced call site).
|
|
284
|
+
*/
|
|
285
|
+
export function buildPageAgentInjection(iifeSource, pageConfig, task) {
|
|
286
|
+
const safeIife = stripDemoAutoInit(iifeSource);
|
|
287
|
+
// Fail-closed: never inject a bundle that still carries the demo endpoint.
|
|
288
|
+
const leak = findDemoLeak(safeIife);
|
|
289
|
+
if (leak)
|
|
290
|
+
throw new DemoLeakError(leak);
|
|
291
|
+
const cfgJson = JSON.stringify(pageConfig);
|
|
292
|
+
const taskJson = JSON.stringify(task);
|
|
293
|
+
return `${safeIife}
|
|
294
|
+
;(function(){
|
|
295
|
+
window.${BROWSER_ACT_RESULT_GLOBAL} = null;
|
|
296
|
+
try {
|
|
297
|
+
if (!window.PageAgent) {
|
|
298
|
+
window.${BROWSER_ACT_RESULT_GLOBAL} = { success: false, error: 'PageAgent not defined after injection' };
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
var agent = new window.PageAgent(${cfgJson});
|
|
302
|
+
window.__ruflo_pageAgent__ = agent;
|
|
303
|
+
agent.execute(${taskJson}).then(function(r){
|
|
304
|
+
window.${BROWSER_ACT_RESULT_GLOBAL} = { success: true, result: r };
|
|
305
|
+
}).catch(function(e){
|
|
306
|
+
window.${BROWSER_ACT_RESULT_GLOBAL} = { success: false, error: String((e && e.message) || e) };
|
|
307
|
+
});
|
|
308
|
+
} catch (e) {
|
|
309
|
+
window.${BROWSER_ACT_RESULT_GLOBAL} = { success: false, error: String((e && e.message) || e) };
|
|
310
|
+
}
|
|
311
|
+
})();
|
|
312
|
+
'ruflo-browser-act-injected'`;
|
|
313
|
+
}
|
|
314
|
+
// ============================================================================
|
|
315
|
+
// AIDefence content gate (best-effort — @claude-flow/aidefence is itself an
|
|
316
|
+
// optional dep; mirrors the lazy-load pattern in security-tools.ts)
|
|
317
|
+
// ============================================================================
|
|
318
|
+
async function gateWithAIDefence(text) {
|
|
319
|
+
if (!text)
|
|
320
|
+
return { text, flagged: false, gated: false };
|
|
321
|
+
try {
|
|
322
|
+
const mod = await import('@claude-flow/aidefence');
|
|
323
|
+
const defender = mod.createAIDefence({ enableLearning: false });
|
|
324
|
+
let flagged = false;
|
|
325
|
+
try {
|
|
326
|
+
const quick = defender.quickScan(text);
|
|
327
|
+
flagged = !!quick?.threat;
|
|
328
|
+
}
|
|
329
|
+
catch { /* quickScan unavailable on this build */ }
|
|
330
|
+
try {
|
|
331
|
+
if (!flagged && defender.hasPII(text))
|
|
332
|
+
flagged = true;
|
|
333
|
+
}
|
|
334
|
+
catch { /* hasPII unavailable */ }
|
|
335
|
+
if (flagged) {
|
|
336
|
+
return { text: '[content redacted by AIDefence — threat or PII detected]', flagged: true, gated: true };
|
|
337
|
+
}
|
|
338
|
+
return { text, flagged: false, gated: true };
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
// @claude-flow/aidefence is itself optional — pass through rather than
|
|
342
|
+
// block the caller when it isn't installed.
|
|
343
|
+
return { text, flagged: false, gated: false };
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// ============================================================================
|
|
347
|
+
// ADR-174 distillation — best-effort trajectory recording into the `browser`
|
|
348
|
+
// memory namespace. Never fatal.
|
|
349
|
+
// ============================================================================
|
|
350
|
+
export async function recordBrowserTrajectory(entry) {
|
|
351
|
+
try {
|
|
352
|
+
const slug = entry.task.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40) || 'task';
|
|
353
|
+
const key = `browser-act-${Date.now()}-${slug}`;
|
|
354
|
+
const result = await storeEntry({
|
|
355
|
+
key,
|
|
356
|
+
namespace: 'browser',
|
|
357
|
+
value: JSON.stringify(entry),
|
|
358
|
+
generateEmbeddingFlag: true,
|
|
359
|
+
});
|
|
360
|
+
return !!result?.success;
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// ============================================================================
|
|
367
|
+
// Result polling — `agent-browser eval` doesn't itself await in-page
|
|
368
|
+
// promises, so we kick off `execute()` in a fire-and-forget IIFE and poll a
|
|
369
|
+
// window global for the settled result.
|
|
370
|
+
// ============================================================================
|
|
371
|
+
async function pollForResult(session, timeoutMs) {
|
|
372
|
+
const start = Date.now();
|
|
373
|
+
const pollScript = `JSON.stringify(window.${BROWSER_ACT_RESULT_GLOBAL} || null)`;
|
|
374
|
+
while (Date.now() - start < timeoutMs) {
|
|
375
|
+
const r = await execBrowserCommand(['eval', pollScript], session);
|
|
376
|
+
const text = r.content[0]?.text;
|
|
377
|
+
if (text) {
|
|
378
|
+
let outer;
|
|
379
|
+
try {
|
|
380
|
+
outer = JSON.parse(text);
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
outer = null;
|
|
384
|
+
}
|
|
385
|
+
const raw = outer && typeof outer === 'object' ? (outer.result ?? outer.data ?? outer) : outer;
|
|
386
|
+
let parsed = null;
|
|
387
|
+
if (typeof raw === 'string') {
|
|
388
|
+
try {
|
|
389
|
+
parsed = JSON.parse(raw);
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
parsed = null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
parsed = raw;
|
|
397
|
+
}
|
|
398
|
+
if (parsed && parsed !== 'null')
|
|
399
|
+
return parsed;
|
|
400
|
+
}
|
|
401
|
+
await new Promise((res) => setTimeout(res, 500));
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
function fail(error, extra = {}) {
|
|
406
|
+
return {
|
|
407
|
+
content: [{ type: 'text', text: JSON.stringify({ success: false, error, ...extra }, null, 2) }],
|
|
408
|
+
isError: true,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function ok(payload) {
|
|
412
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true, ...payload }, null, 2) }] };
|
|
413
|
+
}
|
|
414
|
+
function degraded(reason, hint) {
|
|
415
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true, degraded: true, reason, hint }, null, 2) }] };
|
|
416
|
+
}
|
|
417
|
+
// ============================================================================
|
|
418
|
+
// Tool definition
|
|
419
|
+
// ============================================================================
|
|
420
|
+
export const browserIntentTools = [
|
|
421
|
+
{
|
|
422
|
+
name: 'browser_act',
|
|
423
|
+
description: 'Use when a target element is easier to describe than to select, or when an intent spans several steps: executes a natural-language instruction on the current page via page-agent (e.g. "Click the login button", "Fill the search box with cats and submit"). ' +
|
|
424
|
+
'Prefer this over chaining browser_snapshot + browser_click/fill for such multi-step intents; pair with browser_open/browser_screenshot for navigation and visual verification (page-agent is text-DOM only, so it is blind to canvas/visual-only UIs — keep the selector + screenshot tools for those). ' +
|
|
425
|
+
'Falls back to {degraded:true} when page-agent is not installed or no OpenAI-compatible LLM provider is configured ' +
|
|
426
|
+
'(set OPENROUTER_API_KEY or OLLAMA_API_KEY; a bare ANTHROPIC_API_KEY is not sufficient — page-agent requires a /chat/completions-shaped endpoint).',
|
|
427
|
+
category: 'browser',
|
|
428
|
+
tags: ['browser', 'intent', 'nl', 'page-agent'],
|
|
429
|
+
inputSchema: {
|
|
430
|
+
type: 'object',
|
|
431
|
+
properties: {
|
|
432
|
+
task: { type: 'string', description: 'Natural-language instruction, e.g. "Click the login button"' },
|
|
433
|
+
session: { type: 'string', description: 'Session ID (default: "default")' },
|
|
434
|
+
url: { type: 'string', description: 'Optional URL to navigate to before executing the intent' },
|
|
435
|
+
timeoutMs: { type: 'number', description: 'Max time to wait for execute() to settle (default 120000)' },
|
|
436
|
+
},
|
|
437
|
+
required: ['task'],
|
|
438
|
+
},
|
|
439
|
+
handler: async (input) => {
|
|
440
|
+
const vTask = validateText(input.task, 'task');
|
|
441
|
+
if (!vTask.valid)
|
|
442
|
+
return fail(vTask.error || 'invalid task');
|
|
443
|
+
if (input.session) {
|
|
444
|
+
const vS = validateIdentifier(input.session, 'session');
|
|
445
|
+
if (!vS.valid)
|
|
446
|
+
return fail(vS.error || 'invalid session');
|
|
447
|
+
}
|
|
448
|
+
if (input.url) {
|
|
449
|
+
const vU = validateText(input.url, 'url');
|
|
450
|
+
if (!vU.valid)
|
|
451
|
+
return fail(vU.error || 'invalid url');
|
|
452
|
+
}
|
|
453
|
+
const task = input.task;
|
|
454
|
+
const session = input.session || 'default';
|
|
455
|
+
const url = input.url;
|
|
456
|
+
const timeoutMs = typeof input.timeoutMs === 'number' && input.timeoutMs > 0 ? input.timeoutMs : 120_000;
|
|
457
|
+
// 1. page-agent must be installed.
|
|
458
|
+
const bundle = locatePageAgentBundle();
|
|
459
|
+
if (!bundle) {
|
|
460
|
+
return degraded('page-agent not installed', 'npm i page-agent');
|
|
461
|
+
}
|
|
462
|
+
// 2. An OpenAI-compatible LLM provider must be configured.
|
|
463
|
+
const llmConfig = resolvePageAgentLLMConfig();
|
|
464
|
+
if (!llmConfig) {
|
|
465
|
+
return degraded('no OpenAI-compatible LLM provider configured', 'Set OPENROUTER_API_KEY or OLLAMA_API_KEY (both OpenAI-compatible), or CLAUDE_FLOW_PAGE_AGENT_BASE_URL + CLAUDE_FLOW_PAGE_AGENT_API_KEY for a custom endpoint. ANTHROPIC_API_KEY alone is not sufficient.');
|
|
466
|
+
}
|
|
467
|
+
let iifeSource;
|
|
468
|
+
try {
|
|
469
|
+
iifeSource = readFileSync(bundle.iifePath, 'utf-8');
|
|
470
|
+
}
|
|
471
|
+
catch (err) {
|
|
472
|
+
return degraded('page-agent bundle unreadable', String(err instanceof Error ? err.message : err));
|
|
473
|
+
}
|
|
474
|
+
// 3. Start the loopback proxy — the real key stays server-side.
|
|
475
|
+
let proxy;
|
|
476
|
+
try {
|
|
477
|
+
proxy = await startLocalLLMProxy({ baseURL: llmConfig.baseURL, apiKey: llmConfig.apiKey });
|
|
478
|
+
}
|
|
479
|
+
catch (err) {
|
|
480
|
+
return degraded('local LLM proxy failed to start', String(err instanceof Error ? err.message : err));
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
// 4. Navigate first, if requested.
|
|
484
|
+
if (url) {
|
|
485
|
+
const nav = await execBrowserCommand(['open', url], session);
|
|
486
|
+
if (nav.isError)
|
|
487
|
+
return fail('browser open failed', { detail: nav.content[0]?.text });
|
|
488
|
+
}
|
|
489
|
+
// 5. Inject the (demo-stripped) bundle + our controlled execute() call.
|
|
490
|
+
// apiKey here is ALWAYS the placeholder — the real key lives only
|
|
491
|
+
// in `llmConfig`, used above to start the proxy.
|
|
492
|
+
const pageConfig = {
|
|
493
|
+
baseURL: proxy.url,
|
|
494
|
+
apiKey: PLACEHOLDER_API_KEY,
|
|
495
|
+
model: llmConfig.model,
|
|
496
|
+
language: 'en-US',
|
|
497
|
+
};
|
|
498
|
+
let script;
|
|
499
|
+
try {
|
|
500
|
+
script = buildPageAgentInjection(iifeSource, pageConfig, task);
|
|
501
|
+
}
|
|
502
|
+
catch (err) {
|
|
503
|
+
if (err instanceof DemoLeakError) {
|
|
504
|
+
// Refuse to inject rather than leak page content to Alibaba's sandbox.
|
|
505
|
+
return degraded('page-agent bundle contains a demo endpoint we could not neutralize', err.message);
|
|
506
|
+
}
|
|
507
|
+
throw err;
|
|
508
|
+
}
|
|
509
|
+
const inject = await execBrowserCommand(['eval', script], session);
|
|
510
|
+
if (inject.isError)
|
|
511
|
+
return fail('page-agent injection failed', { detail: inject.content[0]?.text });
|
|
512
|
+
// 6. Poll for the settled execute() result.
|
|
513
|
+
const settled = await pollForResult(session, timeoutMs);
|
|
514
|
+
if (!settled) {
|
|
515
|
+
return fail('timed out waiting for page-agent execution', { timeoutMs });
|
|
516
|
+
}
|
|
517
|
+
if (!settled.success) {
|
|
518
|
+
return fail(settled.error || 'page-agent execution failed', { task, url });
|
|
519
|
+
}
|
|
520
|
+
const execResult = settled.result;
|
|
521
|
+
const rawData = execResult?.data ?? '';
|
|
522
|
+
const gate = await gateWithAIDefence(String(rawData));
|
|
523
|
+
// 7. Best-effort ADR-174 trajectory recording.
|
|
524
|
+
await recordBrowserTrajectory({
|
|
525
|
+
task,
|
|
526
|
+
url,
|
|
527
|
+
success: !!execResult?.success,
|
|
528
|
+
dataPreview: gate.text.slice(0, 500),
|
|
529
|
+
steps: execResult?.history?.length ?? 0,
|
|
530
|
+
source: llmConfig.source,
|
|
531
|
+
});
|
|
532
|
+
return ok({
|
|
533
|
+
result: gate.text,
|
|
534
|
+
steps: execResult?.history?.length ?? 0,
|
|
535
|
+
history: execResult?.history ?? [],
|
|
536
|
+
contentFlagged: gate.flagged,
|
|
537
|
+
pageAgentVersion: bundle.version,
|
|
538
|
+
llmSource: llmConfig.source,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
finally {
|
|
542
|
+
proxy.close();
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
];
|
|
547
|
+
export default browserIntentTools;
|
|
548
|
+
//# sourceMappingURL=browser-intent-tools.js.map
|
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
* CLI integration for @claude-flow/browser package.
|
|
5
5
|
* Provides browser automation tools for web navigation, interaction, and data extraction.
|
|
6
6
|
*/
|
|
7
|
-
import type { MCPTool } from './types.js';
|
|
7
|
+
import type { MCPTool, MCPToolResult } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Execute agent-browser CLI command.
|
|
10
|
+
* Tries global agent-browser first, falls back to npx if ENOENT.
|
|
11
|
+
*
|
|
12
|
+
* Exported so browser-intent-tools.ts (ADR-175 `browser_act`) can reuse the same
|
|
13
|
+
* exec + fallback + session-plumbing instead of duplicating it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function execBrowserCommand(args: string[], session?: string): Promise<MCPToolResult>;
|
|
8
16
|
/**
|
|
9
17
|
* Browser MCP Tools
|
|
10
18
|
*/
|
|
@@ -11,8 +11,11 @@ const browserSessions = new Map();
|
|
|
11
11
|
/**
|
|
12
12
|
* Execute agent-browser CLI command.
|
|
13
13
|
* Tries global agent-browser first, falls back to npx if ENOENT.
|
|
14
|
+
*
|
|
15
|
+
* Exported so browser-intent-tools.ts (ADR-175 `browser_act`) can reuse the same
|
|
16
|
+
* exec + fallback + session-plumbing instead of duplicating it.
|
|
14
17
|
*/
|
|
15
|
-
async function execBrowserCommand(args, session = 'default') {
|
|
18
|
+
export async function execBrowserCommand(args, session = 'default') {
|
|
16
19
|
const { execFileSync } = await import('child_process');
|
|
17
20
|
const fullArgs = ['--session', session, '--json', ...args];
|
|
18
21
|
let result;
|