@yuzc-001/grasp 0.6.6

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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +327 -0
  3. package/README.zh-CN.md +324 -0
  4. package/examples/README.md +31 -0
  5. package/examples/claude-desktop.json +8 -0
  6. package/examples/codex-config.toml +4 -0
  7. package/grasp.skill +0 -0
  8. package/index.js +87 -0
  9. package/package.json +48 -0
  10. package/scripts/grasp_openclaw_ctl.sh +122 -0
  11. package/scripts/run-search-benchmark.mjs +287 -0
  12. package/scripts/update-star-history.mjs +274 -0
  13. package/skill/SKILL.md +61 -0
  14. package/skill/references/tools.md +306 -0
  15. package/src/cli/auto-configure.js +116 -0
  16. package/src/cli/cmd-connect.js +148 -0
  17. package/src/cli/cmd-explain.js +42 -0
  18. package/src/cli/cmd-logs.js +55 -0
  19. package/src/cli/cmd-status.js +119 -0
  20. package/src/cli/config.js +27 -0
  21. package/src/cli/detect-chrome.js +58 -0
  22. package/src/grasp/handoff/events.js +67 -0
  23. package/src/grasp/handoff/persist.js +48 -0
  24. package/src/grasp/handoff/state.js +28 -0
  25. package/src/grasp/page/capture.js +34 -0
  26. package/src/grasp/page/state.js +273 -0
  27. package/src/grasp/verify/evidence.js +40 -0
  28. package/src/grasp/verify/pipeline.js +52 -0
  29. package/src/layer1-bridge/chrome.js +416 -0
  30. package/src/layer1-bridge/webmcp.js +143 -0
  31. package/src/layer2-perception/hints.js +284 -0
  32. package/src/layer3-action/actions.js +400 -0
  33. package/src/runtime/browser-instance.js +65 -0
  34. package/src/runtime/truth/model.js +94 -0
  35. package/src/runtime/truth/snapshot.js +51 -0
  36. package/src/server/affordances.js +47 -0
  37. package/src/server/audit.js +122 -0
  38. package/src/server/boss-fast-path.js +164 -0
  39. package/src/server/boundary-guard.js +53 -0
  40. package/src/server/content.js +97 -0
  41. package/src/server/continuity.js +256 -0
  42. package/src/server/engine-selection.js +29 -0
  43. package/src/server/entry-orchestrator.js +115 -0
  44. package/src/server/error-codes.js +7 -0
  45. package/src/server/explain-share-card.js +113 -0
  46. package/src/server/fast-path-router.js +134 -0
  47. package/src/server/form-runtime.js +602 -0
  48. package/src/server/form-tasks.js +254 -0
  49. package/src/server/gateway-response.js +62 -0
  50. package/src/server/index.js +22 -0
  51. package/src/server/observe.js +52 -0
  52. package/src/server/page-projection.js +31 -0
  53. package/src/server/page-state.js +27 -0
  54. package/src/server/postconditions.js +128 -0
  55. package/src/server/prompt-assembly.js +148 -0
  56. package/src/server/responses.js +44 -0
  57. package/src/server/route-boundary.js +174 -0
  58. package/src/server/route-policy.js +168 -0
  59. package/src/server/runtime-confirmation.js +87 -0
  60. package/src/server/runtime-status.js +7 -0
  61. package/src/server/share-artifacts.js +284 -0
  62. package/src/server/state.js +132 -0
  63. package/src/server/structured-extraction.js +131 -0
  64. package/src/server/surface-prompts.js +166 -0
  65. package/src/server/task-frame.js +11 -0
  66. package/src/server/tasks/search-task.js +321 -0
  67. package/src/server/tools.actions.js +1361 -0
  68. package/src/server/tools.form.js +526 -0
  69. package/src/server/tools.gateway.js +757 -0
  70. package/src/server/tools.handoff.js +210 -0
  71. package/src/server/tools.js +20 -0
  72. package/src/server/tools.legacy.js +983 -0
  73. package/src/server/tools.strategy.js +250 -0
  74. package/src/server/tools.task-surface.js +66 -0
  75. package/src/server/tools.workspace.js +873 -0
  76. package/src/server/workspace-runtime.js +1138 -0
  77. package/src/server/workspace-tasks.js +735 -0
  78. package/start-chrome.bat +84 -0
@@ -0,0 +1,416 @@
1
+ import { chromium } from 'playwright-core';
2
+ import { spawn } from 'child_process';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+ import { detectChromePath, startChromeHint } from '../cli/detect-chrome.js';
6
+ import { writeRuntimeStatus } from '../server/runtime-status.js';
7
+ import { getActiveTaskFrame, isSafeModeEnabled } from '../server/state.js';
8
+
9
+ const CDP_URL = process.env.CHROME_CDP_URL || 'http://localhost:9222';
10
+ const DEFAULT_RETRY_DELAYS = [0, 250, 1000];
11
+ const SAFE_MODE = isSafeModeEnabled();
12
+
13
+ async function autoLaunchChrome(cdpUrl) {
14
+ const chromePath = detectChromePath();
15
+ if (!chromePath) return false;
16
+
17
+ const parsedUrl = new URL(cdpUrl);
18
+ const localHosts = new Set(['localhost', '127.0.0.1', '::1']);
19
+ if (!localHosts.has(parsedUrl.hostname)) return false;
20
+
21
+ const port = parsedUrl.port || '9222';
22
+ const userDataDir = join(homedir(), 'chrome-grasp');
23
+
24
+ console.error(`[Grasp] Chrome not running — auto-launching: ${chromePath}`);
25
+ spawn(chromePath, [
26
+ `--remote-debugging-port=${port}`,
27
+ `--user-data-dir=${userDataDir}`,
28
+ '--no-first-run',
29
+ '--no-default-browser-check',
30
+ '--start-maximized',
31
+ ], { detached: true, stdio: 'ignore' }).unref();
32
+
33
+ for (let attempt = 0; attempt < 20; attempt += 1) {
34
+ await new Promise((resolve) => setTimeout(resolve, 500));
35
+ try {
36
+ const response = await fetch(`${cdpUrl}/json/version`, { signal: AbortSignal.timeout(1500) });
37
+ if (response.ok) {
38
+ console.error('[Grasp] Auto-launched Chrome is ready.');
39
+ return true;
40
+ }
41
+ } catch {
42
+ // not ready yet
43
+ }
44
+ }
45
+
46
+ console.error('[Grasp] Auto-launch: Chrome did not become reachable in time.');
47
+ return false;
48
+ }
49
+
50
+ async function defaultConnect() {
51
+ try {
52
+ const browser = await chromium.connectOverCDP(CDP_URL);
53
+ console.error('[Grasp] Connected to Chrome via CDP:', CDP_URL);
54
+ return browser;
55
+ } catch (err) {
56
+ throw new Error(
57
+ `Chrome not reachable at ${CDP_URL}.\n` +
58
+ `Start Chrome with remote debugging enabled:\n` +
59
+ ` ${startChromeHint(CDP_URL)}\n` +
60
+ `Or run: grasp status to diagnose the problem.\n` +
61
+ `(${err.message})`
62
+ );
63
+ }
64
+ }
65
+
66
+ const defaultPersistStatus = async (snapshot) => {
67
+ await writeRuntimeStatus(snapshot);
68
+ };
69
+
70
+ function runPersist(persistFn, snapshot) {
71
+ if (!persistFn) return;
72
+ try {
73
+ const result = persistFn(snapshot);
74
+ if (result && typeof result.catch === 'function') {
75
+ result.catch(() => {});
76
+ }
77
+ } catch {
78
+ // swallow
79
+ }
80
+ }
81
+
82
+ function clearTargetSession(state) {
83
+ if (state) {
84
+ const activeTaskFrame = getActiveTaskFrame(state);
85
+ if (activeTaskFrame) {
86
+ activeTaskFrame.pinnedTarget = null;
87
+ state.targetSession = null;
88
+ return;
89
+ }
90
+ if (state.targetSession) {
91
+ state.targetSession = null;
92
+ }
93
+ }
94
+ }
95
+
96
+ function getPinnedTargetSession(state) {
97
+ const activeTaskFrame = getActiveTaskFrame(state);
98
+ if (activeTaskFrame) {
99
+ return activeTaskFrame.pinnedTarget ?? null;
100
+ }
101
+ return state?.targetSession ?? null;
102
+ }
103
+
104
+ function isPinnedTargetAvailable(page, browser) {
105
+ if (!page) return false;
106
+ if (page.isClosed?.() === true) return false;
107
+
108
+ const contexts = browser?.contexts?.() ?? [];
109
+ for (const context of contexts) {
110
+ const pages = context?.pages?.() ?? [];
111
+ if (pages.includes(page)) {
112
+ return true;
113
+ }
114
+ }
115
+
116
+ return false;
117
+ }
118
+
119
+ function matchesPinnedTarget(page, targetSession) {
120
+ const currentUrl = page?.url?.() ?? '';
121
+ if (!currentUrl || !targetSession?.url) return false;
122
+ if (currentUrl !== targetSession.url) return false;
123
+ if (currentUrl.startsWith('chrome://')) return false;
124
+ if (currentUrl.startsWith('chrome-extension://')) return false;
125
+ if (currentUrl.startsWith('about:')) return false;
126
+ return true;
127
+ }
128
+
129
+ export async function pinTargetPage(page, state) {
130
+ const targetSession = {
131
+ page,
132
+ url: page.url(),
133
+ title: await page.title(),
134
+ };
135
+ if (state) {
136
+ const activeTaskFrame = getActiveTaskFrame(state);
137
+ if (activeTaskFrame) {
138
+ activeTaskFrame.pinnedTarget = targetSession;
139
+ state.targetSession = null;
140
+ } else {
141
+ state.targetSession = targetSession;
142
+ }
143
+ }
144
+ return targetSession;
145
+ }
146
+
147
+ export function createConnectionSupervisor({
148
+ connect = defaultConnect,
149
+ now = () => Date.now(),
150
+ retryDelays = DEFAULT_RETRY_DELAYS,
151
+ persistStatus = defaultPersistStatus,
152
+ safeMode = SAFE_MODE,
153
+ cdpUrl = CDP_URL,
154
+ autoLaunch = autoLaunchChrome,
155
+ } = {}) {
156
+ let browser = null;
157
+ let pending = null;
158
+ let status = {
159
+ state: 'idle',
160
+ retryCount: 0,
161
+ lastError: null,
162
+ lastAttemptAt: null,
163
+ connectedAt: null,
164
+ cdpUrl,
165
+ safeMode,
166
+ updatedAt: now(),
167
+ };
168
+
169
+ function updateStatus(updates) {
170
+ status = {
171
+ ...status,
172
+ ...updates,
173
+ cdpUrl,
174
+ safeMode,
175
+ updatedAt: now(),
176
+ };
177
+ runPersist(persistStatus, status);
178
+ return status;
179
+ }
180
+
181
+ function attachDisconnectListener(instance) {
182
+ if (!instance || typeof instance.once !== 'function') return;
183
+ instance.once('disconnected', () => {
184
+ browser = null;
185
+ updateStatus({ state: 'disconnected', lastError: 'browser disconnected' });
186
+ });
187
+ }
188
+
189
+ async function attemptConnect() {
190
+ updateStatus({ state: 'connecting', lastError: null });
191
+
192
+ for (let index = 0; index < retryDelays.length; index += 1) {
193
+ const delayMs = retryDelays[index];
194
+ if (delayMs) {
195
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
196
+ }
197
+
198
+ const attemptCount = index + 1;
199
+ updateStatus({
200
+ retryCount: attemptCount,
201
+ lastAttemptAt: now(),
202
+ lastError: null,
203
+ });
204
+
205
+ try {
206
+ const candidate = await connect();
207
+ browser = candidate;
208
+ attachDisconnectListener(candidate);
209
+ updateStatus({
210
+ state: 'connected',
211
+ connectedAt: now(),
212
+ lastError: null,
213
+ retryCount: attemptCount,
214
+ });
215
+ return candidate;
216
+ } catch (error) {
217
+ updateStatus({ lastError: error.message });
218
+ }
219
+ }
220
+
221
+ if (autoLaunch) {
222
+ updateStatus({ state: 'auto_launching', lastError: 'CDP unreachable, attempting auto-launch' });
223
+ const launched = await autoLaunch(cdpUrl);
224
+ if (launched) {
225
+ try {
226
+ const candidate = await connect();
227
+ browser = candidate;
228
+ attachDisconnectListener(candidate);
229
+ updateStatus({
230
+ state: 'connected',
231
+ connectedAt: now(),
232
+ lastError: null,
233
+ retryCount: retryDelays.length + 1,
234
+ });
235
+ return candidate;
236
+ } catch (error) {
237
+ updateStatus({ lastError: error.message });
238
+ }
239
+ }
240
+ }
241
+
242
+ updateStatus({ state: 'CDP_UNREACHABLE', retryCount: retryDelays.length, lastAttemptAt: now() });
243
+ throw new Error(status.lastError ?? 'CDP unreachable');
244
+ }
245
+
246
+ async function getBrowser() {
247
+ if (browser?.isConnected?.()) {
248
+ updateStatus({ state: 'connected', connectedAt: now(), lastError: null });
249
+ return browser;
250
+ }
251
+
252
+ if (pending) {
253
+ return pending;
254
+ }
255
+
256
+ pending = attemptConnect();
257
+ try {
258
+ return await pending;
259
+ } finally {
260
+ pending = null;
261
+ }
262
+ }
263
+
264
+ function getStatus() {
265
+ return status;
266
+ }
267
+
268
+ function reset() {
269
+ browser = null;
270
+ pending = null;
271
+ updateStatus({
272
+ cdp: { state: 'unknown', browserVersion: null, protocolVersion: null },
273
+ page: { state: 'unknown', title: null, url: null },
274
+ browser_process: { state: 'unknown', pid: null },
275
+ server: {
276
+ state: 'idle',
277
+ retryCount: 0,
278
+ lastError: null,
279
+ lastAttemptAt: null,
280
+ connectedAt: null,
281
+ },
282
+ });
283
+ }
284
+
285
+ return { getBrowser, getStatus, reset };
286
+ }
287
+
288
+ const supervisor = createConnectionSupervisor();
289
+
290
+ async function getBrowser() {
291
+ return supervisor.getBrowser();
292
+ }
293
+
294
+ async function getActivePage({ state = null, browser = null } = {}) {
295
+ const resolvedBrowser = browser ?? await getBrowser();
296
+ const pinnedTarget = getPinnedTargetSession(state);
297
+
298
+ if (pinnedTarget?.page) {
299
+ const pinnedPage = pinnedTarget.page;
300
+ if (isPinnedTargetAvailable(pinnedPage, resolvedBrowser) && matchesPinnedTarget(pinnedPage, pinnedTarget)) {
301
+ return pinnedPage;
302
+ }
303
+ clearTargetSession(state);
304
+ }
305
+
306
+ const context = resolvedBrowser.contexts()[0];
307
+ if (!context) throw new Error('No browser context available.');
308
+ const pages = context.pages();
309
+
310
+ const userPages = pages.filter((page) => {
311
+ const url = page.url();
312
+ if (!url) return false;
313
+ if (url.startsWith('chrome://')) return false;
314
+ if (url.startsWith('chrome-extension://')) return false;
315
+ if (url.startsWith('about:')) return false;
316
+ return true;
317
+ });
318
+
319
+ if (userPages.length === 0) {
320
+ if (pages.length > 0) return pages[pages.length - 1];
321
+ const newPage = await context.newPage();
322
+ console.error('[Grasp] No open tabs — created a blank tab.');
323
+ return newPage;
324
+ }
325
+
326
+ for (const page of userPages) {
327
+ try {
328
+ const isVisible = await page.evaluate(() => document.visibilityState === 'visible');
329
+ if (isVisible) return page;
330
+ } catch {
331
+ // Page still loading — skip
332
+ }
333
+ }
334
+
335
+ return userPages[userPages.length - 1];
336
+ }
337
+
338
+ async function navigateTo(url, { state = null, browser = null } = {}) {
339
+ const page = await getActivePage({ state, browser });
340
+
341
+ try {
342
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
343
+ } catch (err) {
344
+ if (err.name === 'TimeoutError' || err.message?.includes('timeout')) {
345
+ console.error(
346
+ `[Grasp] Navigation to ${url} timed out, continuing with partially loaded page.`
347
+ );
348
+ } else {
349
+ throw err;
350
+ }
351
+ }
352
+
353
+ return page;
354
+ }
355
+
356
+ async function getTabs() {
357
+ const browser = await getBrowser();
358
+ const context = browser.contexts()[0];
359
+ if (!context) throw new Error('No browser context available.');
360
+ const pages = context.pages();
361
+ return Promise.all(
362
+ pages.map(async (p, i) => ({
363
+ index: i,
364
+ title: await p.title().catch(() => ''),
365
+ url: p.url(),
366
+ isUser: p.url() && !p.url().startsWith('chrome://') && !p.url().startsWith('about:'),
367
+ }))
368
+ );
369
+ }
370
+
371
+ async function switchTab(index) {
372
+ const browser = await getBrowser();
373
+ const context = browser.contexts()[0];
374
+ if (!context) throw new Error('No browser context available.');
375
+ const pages = context.pages();
376
+ if (index < 0 || index >= pages.length) {
377
+ throw new Error(`Tab index ${index} out of range (0-${pages.length - 1})`);
378
+ }
379
+ await pages[index].bringToFront();
380
+ return pages[index];
381
+ }
382
+
383
+ async function newTab(url) {
384
+ const browser = await getBrowser();
385
+ const context = browser.contexts()[0];
386
+ if (!context) throw new Error('No browser context available.');
387
+ const page = await context.newPage();
388
+ try {
389
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
390
+ } catch (err) {
391
+ if (!err.message?.includes('timeout')) {
392
+ await page.close().catch(() => {});
393
+ throw err;
394
+ }
395
+ console.error(`[Grasp] newTab navigation timeout for ${url}, continuing.`);
396
+ }
397
+ await page.bringToFront();
398
+ return page;
399
+ }
400
+
401
+ async function trustedContextOpen(url) {
402
+ return newTab(url);
403
+ }
404
+
405
+ async function closeTab(index) {
406
+ const browser = await getBrowser();
407
+ const context = browser.contexts()[0];
408
+ if (!context) throw new Error('No browser context available.');
409
+ const pages = context.pages();
410
+ if (index < 0 || index >= pages.length) {
411
+ throw new Error(`Tab index ${index} out of range (0-${pages.length - 1})`);
412
+ }
413
+ await pages[index].close();
414
+ }
415
+
416
+ export { getBrowser, getActivePage, navigateTo, getTabs, switchTab, newTab, trustedContextOpen, closeTab };
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Layer 1 WebMCP 适配层
3
+ * 每次 navigate 后自动探测页面是否支持 WebMCP:
4
+ * - 命中 → 走原生工具通道(零 DOM 解析)
5
+ * - 未命中 → 静默降级到 CDP 模式
6
+ */
7
+
8
+ /**
9
+ * 探测页面是否支持 WebMCP。
10
+ * 两步探测,总开销 < 50ms。
11
+ *
12
+ * @param {import('playwright').Page} page
13
+ * @returns {Promise<WebMCPInfo>}
14
+ *
15
+ * @typedef {{ available: true, source: 'window' | 'well-known', endpoint: string, tools: any[] } | { available: false }} WebMCPInfo
16
+ */
17
+ export async function probe(page) {
18
+ // Step 1(~1ms):检测 window 对象上的 WebMCP 标记
19
+ try {
20
+ const windowResult = await page.evaluate(() => {
21
+ // 检测 window.__webmcp__
22
+ if (typeof window.__webmcp__ !== 'undefined') {
23
+ return {
24
+ found: true,
25
+ source: 'window',
26
+ endpoint: window.__webmcp__?.endpoint ?? window.location.origin,
27
+ };
28
+ }
29
+ // 检测 window.MCP
30
+ if (typeof window.MCP !== 'undefined') {
31
+ return {
32
+ found: true,
33
+ source: 'window',
34
+ endpoint: window.MCP?.endpoint ?? window.location.origin,
35
+ };
36
+ }
37
+ // 检测 <meta name="mcp-endpoint">
38
+ const meta = document.querySelector('meta[name="mcp-endpoint"]');
39
+ if (meta) {
40
+ return {
41
+ found: true,
42
+ source: 'window',
43
+ endpoint: meta.getAttribute('content') ?? window.location.origin,
44
+ };
45
+ }
46
+ return { found: false };
47
+ });
48
+
49
+ if (windowResult.found) {
50
+ return {
51
+ available: true,
52
+ source: 'window',
53
+ endpoint: windowResult.endpoint,
54
+ tools: [],
55
+ };
56
+ }
57
+ } catch {
58
+ // 静默捕获,继续 Step 2
59
+ }
60
+
61
+ // Step 2(~50ms 上限):尝试 well-known 端点
62
+ try {
63
+ const wellKnownResult = await page.evaluate(async () => {
64
+ const url = `${window.location.origin}/.well-known/mcp`;
65
+ const response = await fetch(url, {
66
+ signal: AbortSignal.timeout(3000),
67
+ });
68
+ if (!response.ok) return { found: false };
69
+ const data = await response.json();
70
+ return { found: true, url, tools: data.tools ?? [] };
71
+ });
72
+
73
+ if (wellKnownResult.found) {
74
+ return {
75
+ available: true,
76
+ source: 'well-known',
77
+ endpoint: wellKnownResult.url,
78
+ tools: wellKnownResult.tools,
79
+ };
80
+ }
81
+ } catch {
82
+ // 静默捕获
83
+ }
84
+
85
+ return { available: false };
86
+ }
87
+
88
+ /**
89
+ * 列出当前页面可用的 WebMCP 工具。
90
+ *
91
+ * @param {import('playwright').Page} page
92
+ * @param {WebMCPInfo} info
93
+ * @returns {Promise<any[]>}
94
+ */
95
+ export async function listTools(page, info) {
96
+ if (!info.available) return [];
97
+
98
+ try {
99
+ if (info.source === 'window') {
100
+ return await page.evaluate(() => {
101
+ return window.__webmcp__?.tools ?? window.MCP?.tools ?? [];
102
+ });
103
+ }
104
+
105
+ if (info.source === 'well-known') {
106
+ return info.tools ?? [];
107
+ }
108
+ } catch {
109
+ // 静默捕获
110
+ }
111
+
112
+ return [];
113
+ }
114
+
115
+ /**
116
+ * 调用指定的 WebMCP 工具。
117
+ *
118
+ * @param {import('playwright').Page} page
119
+ * @param {WebMCPInfo} info
120
+ * @param {string} toolName
121
+ * @param {Record<string, unknown>} args
122
+ * @returns {Promise<any>}
123
+ */
124
+ export async function callTool(page, info, toolName, args = {}) {
125
+ if (!info.available) {
126
+ throw new Error('WebMCP not available on this page');
127
+ }
128
+
129
+ if (info.source === 'well-known') {
130
+ throw new Error('WebMCP call is not supported for well-known source. The page does not expose a window call function.');
131
+ }
132
+
133
+ const result = await page.evaluate(
134
+ async ({ toolName, args }) => {
135
+ const callFn = window.__webmcp__?.call ?? window.MCP?.call;
136
+ if (!callFn) return { __error: 'WebMCP call function not found' };
137
+ return callFn(toolName, args);
138
+ },
139
+ { toolName, args },
140
+ );
141
+ if (result?.__error) throw new Error(result.__error);
142
+ return result;
143
+ }