@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,873 @@
1
+ import { z } from 'zod';
2
+
3
+ import { buildGatewayResponse } from './gateway-response.js';
4
+ import { guardExpectedBoundary } from './boundary-guard.js';
5
+ import { getActivePage } from '../layer1-bridge/chrome.js';
6
+ import { clickByHintId } from '../layer3-action/actions.js';
7
+ import { syncPageState } from './state.js';
8
+ import { buildWorkspaceVerification, collectVisibleWorkspaceSnapshot, getWorkspaceContinuation, getWorkspaceStatus, summarizeWorkspaceSnapshot } from './workspace-tasks.js';
9
+ import { draftWorkspaceAction, executeWorkspaceAction, selectWorkspaceItem } from './workspace-runtime.js';
10
+ import { readBrowserInstance } from '../runtime/browser-instance.js';
11
+ import { requireConfirmedRuntimeInstance } from './runtime-confirmation.js';
12
+
13
+ const WORKSPACE_ITEM_SELECTOR = 'li, [role="option"], [role="row"], [role="treeitem"], [data-list-item], [data-thread-item], [data-conversation-item]';
14
+
15
+ function toGatewayPage(page, state) {
16
+ return {
17
+ title: page.title,
18
+ url: page.url,
19
+ page_role: state.pageState?.currentRole ?? 'unknown',
20
+ grasp_confidence: state.pageState?.graspConfidence ?? 'unknown',
21
+ risk_gate: state.pageState?.riskGateDetected ?? false,
22
+ };
23
+ }
24
+
25
+ function getWorkspaceBoundaryGuard(state, toolName, pageInfo) {
26
+ return guardExpectedBoundary({
27
+ toolName,
28
+ expectedBoundary: 'workspace_runtime',
29
+ status: getWorkspaceStatus(state),
30
+ page: toGatewayPage(pageInfo, state),
31
+ handoffState: state.handoff?.state ?? 'idle',
32
+ });
33
+ }
34
+
35
+ function pick(snapshot, camelKey, snakeKey, fallback = null) {
36
+ if (snapshot?.[camelKey] !== undefined) return snapshot[camelKey];
37
+ if (snapshot?.[snakeKey] !== undefined) return snapshot[snakeKey];
38
+ return fallback;
39
+ }
40
+
41
+ function getLiveItems(snapshot) {
42
+ const items = pick(snapshot, 'liveItems', 'live_items', []);
43
+ return Array.isArray(items) ? items : [];
44
+ }
45
+
46
+ function getComposer(snapshot) {
47
+ const composer = pick(snapshot, 'composer', 'composer', null);
48
+ return composer && typeof composer === 'object' ? composer : null;
49
+ }
50
+
51
+ function getActionControls(snapshot) {
52
+ const controls = pick(snapshot, 'actionControls', 'action_controls', []);
53
+ return Array.isArray(controls) ? controls : [];
54
+ }
55
+
56
+ function getBlockingModals(snapshot) {
57
+ const modals = pick(snapshot, 'blockingModals', 'blocking_modals', []);
58
+ return Array.isArray(modals) ? modals : [];
59
+ }
60
+
61
+ function getLoadingShell(snapshot) {
62
+ return pick(snapshot, 'loadingShell', 'loading_shell', false) === true;
63
+ }
64
+
65
+ function getActiveItem(snapshot) {
66
+ const activeItem = pick(snapshot, 'activeItem', 'active_item', null);
67
+ if (activeItem) {
68
+ return activeItem;
69
+ }
70
+
71
+ const summaryLabel = String(snapshot?.summary?.active_item_label ?? '').replace(/\s+/g, ' ').trim();
72
+ if (summaryLabel) {
73
+ return { label: summaryLabel };
74
+ }
75
+
76
+ const selectedLiveItem = getLiveItems(snapshot).find((item) => item?.selected === true);
77
+ if (selectedLiveItem?.label) {
78
+ return { label: String(selectedLiveItem.label).replace(/\s+/g, ' ').trim() };
79
+ }
80
+
81
+ return null;
82
+ }
83
+
84
+ function getSendLikeActionControls(snapshot) {
85
+ return getActionControls(snapshot).filter((control) => control?.action_kind === 'send');
86
+ }
87
+
88
+ function isActiveItemStable(snapshot, summary) {
89
+ if (summary?.outcome_signals?.active_item_stable === true) {
90
+ return true;
91
+ }
92
+
93
+ if (summary?.active_item_stable === true) {
94
+ return true;
95
+ }
96
+
97
+ return false;
98
+ }
99
+
100
+ function toPublicLiveItem(item) {
101
+ return {
102
+ label: String(item?.label ?? '').replace(/\s+/g, ' ').trim(),
103
+ selected: item?.selected === true,
104
+ };
105
+ }
106
+
107
+ function toPublicActiveItem(item) {
108
+ if (!item) return null;
109
+ return {
110
+ label: String(item?.label ?? '').replace(/\s+/g, ' ').trim(),
111
+ };
112
+ }
113
+
114
+ function toPublicComposer(composer) {
115
+ if (!composer) return null;
116
+ return {
117
+ kind: composer.kind ?? 'chat_composer',
118
+ draft_present: composer?.draft_present === true,
119
+ };
120
+ }
121
+
122
+ function toPublicActionControl(control) {
123
+ return {
124
+ label: String(control?.label ?? '').replace(/\s+/g, ' ').trim(),
125
+ action_kind: control?.action_kind ?? 'action',
126
+ };
127
+ }
128
+
129
+ function toPublicBlockingModal(modal) {
130
+ return {
131
+ label: String(modal?.label ?? '').replace(/\s+/g, ' ').trim(),
132
+ };
133
+ }
134
+
135
+ function toPublicSelectionItem(item, selected = item?.selected === true) {
136
+ if (!item) return null;
137
+ return {
138
+ label: String(item?.label ?? '').replace(/\s+/g, ' ').trim(),
139
+ selected: selected === true,
140
+ };
141
+ }
142
+
143
+ function toPublicSelectionEvidence(evidence, selected = false) {
144
+ if (!evidence) return null;
145
+
146
+ return {
147
+ requested_label: String(evidence?.requested_label ?? '').replace(/\s+/g, ' ').trim(),
148
+ selected_item: toPublicSelectionItem(evidence?.selected_item, selected),
149
+ active_item: toPublicActiveItem(evidence?.active_item),
150
+ detail_alignment: evidence?.detail_alignment ?? 'unknown',
151
+ selection_window: evidence?.selection_window ?? 'not_found',
152
+ recovery_hint: evidence?.recovery_hint ?? null,
153
+ match_count: evidence?.match_count ?? 0,
154
+ summary: evidence?.summary ?? 'unknown',
155
+ };
156
+ }
157
+
158
+ function toPublicSelectionUnresolved(unresolved) {
159
+ if (!unresolved) return null;
160
+
161
+ return {
162
+ reason: unresolved.reason ?? 'unknown',
163
+ requested_label: String(unresolved.requested_label ?? '').replace(/\s+/g, ' ').trim(),
164
+ recovery_hint: unresolved.recovery_hint ?? null,
165
+ };
166
+ }
167
+
168
+ function toPublicDraftEvidence(draftEvidence) {
169
+ if (!draftEvidence) return null;
170
+
171
+ return {
172
+ kind: draftEvidence.kind ?? 'draft_action',
173
+ target: draftEvidence.target ?? 'chat_composer',
174
+ autosave_possible: draftEvidence.autosave_possible === true,
175
+ write_side_effect: draftEvidence.write_side_effect ?? 'draft_mutation_possible',
176
+ draft_present: draftEvidence.draft_present === true,
177
+ summary: draftEvidence.summary ?? null,
178
+ };
179
+ }
180
+
181
+ function toPublicDraftUnresolved(unresolved) {
182
+ if (!unresolved) return null;
183
+
184
+ return {
185
+ reason: unresolved.reason ?? 'unknown',
186
+ requested_label: String(unresolved.requested_label ?? '').replace(/\s+/g, ' ').trim(),
187
+ recovery_hint: unresolved.recovery_hint ?? null,
188
+ };
189
+ }
190
+
191
+ function toPublicDraftFailure(result) {
192
+ const errorCode = result?.error_code ?? null;
193
+ const retryable = result?.retryable;
194
+ const suggestedNextStep = result?.suggested_next_step ?? null;
195
+
196
+ if (!errorCode && retryable === undefined && suggestedNextStep == null) {
197
+ return null;
198
+ }
199
+
200
+ return {
201
+ error_code: errorCode,
202
+ retryable: retryable ?? null,
203
+ suggested_next_step: suggestedNextStep,
204
+ };
205
+ }
206
+
207
+ function toPublicExecuteUnresolved(unresolved) {
208
+ if (!unresolved) return null;
209
+
210
+ return {
211
+ reason: unresolved.reason ?? 'unknown',
212
+ requested_label: String(unresolved.requested_label ?? '').replace(/\s+/g, ' ').trim(),
213
+ recovery_hint: unresolved.recovery_hint ?? null,
214
+ };
215
+ }
216
+
217
+ function toPublicExecuteFailure(failure) {
218
+ if (!failure) return null;
219
+
220
+ return {
221
+ error_code: failure.error_code ?? null,
222
+ retryable: failure.retryable ?? null,
223
+ suggested_next_step: failure.suggested_next_step ?? null,
224
+ };
225
+ }
226
+
227
+ function toPublicExecuteVerification(verification) {
228
+ if (!verification) return null;
229
+
230
+ return {
231
+ delivered: verification.delivered === true,
232
+ composer_cleared: verification.composer_cleared === true,
233
+ active_item_stable: verification.active_item_stable === true,
234
+ };
235
+ }
236
+
237
+ function toPublicWorkspaceSummary(summary, snapshot) {
238
+ const blockingModalLabels = Array.isArray(summary?.blocking_modal_labels)
239
+ ? summary.blocking_modal_labels
240
+ : getBlockingModals(snapshot)
241
+ .map((modal) => String(modal?.label ?? '').replace(/\s+/g, ' ').trim())
242
+ .filter(Boolean);
243
+
244
+ return {
245
+ active_item_label: summary?.active_item_label ?? null,
246
+ draft_present: summary?.draft_present === true,
247
+ loading_shell: summary?.loading_shell === true,
248
+ blocking_modal_count: summary?.blocking_modal_count ?? blockingModalLabels.length,
249
+ blocking_modal_labels: blockingModalLabels,
250
+ detail_alignment: summary?.detail_alignment ?? 'unknown',
251
+ selection_window: summary?.selection_window ?? 'not_found',
252
+ recovery_hint: summary?.recovery_hint ?? null,
253
+ summary: summary?.summary ?? 'unknown',
254
+ };
255
+ }
256
+
257
+ function formatWorkspaceSurfaceLabel(workspaceSurface) {
258
+ return String(workspaceSurface ?? 'unknown')
259
+ .replace(/_/g, ' ')
260
+ .trim();
261
+ }
262
+
263
+ function getWorkspaceSummaryLabel(workspace, workspaceSummary) {
264
+ const activeLabel = String(
265
+ workspaceSummary?.active_item_label
266
+ ?? workspace?.active_item?.label
267
+ ?? workspace?.live_items?.find((item) => item?.selected === true)?.label
268
+ ?? ''
269
+ ).replace(/\s+/g, ' ').trim();
270
+
271
+ return activeLabel || 'no active item';
272
+ }
273
+
274
+ function formatWorkspaceResultSummary(workspaceSurface, workspace, workspaceSummary) {
275
+ return `Workspace ${formatWorkspaceSurfaceLabel(workspaceSurface)} • ${getWorkspaceSummaryLabel(workspace, workspaceSummary)}`;
276
+ }
277
+
278
+ function getWorkspaceNextAction(snapshot) {
279
+ const summary = pick(snapshot, 'summary', 'summary', null);
280
+ const loadingShell = getLoadingShell(snapshot) || summary?.loading_shell === true;
281
+ if (loadingShell) {
282
+ return 'workspace_inspect';
283
+ }
284
+
285
+ const blockingModals = getBlockingModals(snapshot);
286
+ if (blockingModals.length > 0) {
287
+ return 'workspace_inspect';
288
+ }
289
+
290
+ const liveItems = getLiveItems(snapshot);
291
+ const activeItem = getActiveItem(snapshot);
292
+ const composer = getComposer(snapshot);
293
+ const activeItemStable = isActiveItemStable(snapshot, summary);
294
+ const draftPresent = composer?.draft_present === true || summary?.draft_present === true;
295
+ const sendLikeControls = getSendLikeActionControls(snapshot);
296
+
297
+ if (!activeItem && liveItems.length > 0) {
298
+ return 'select_live_item';
299
+ }
300
+
301
+ if (composer && activeItemStable && draftPresent && sendLikeControls.length > 0) {
302
+ return 'execute_action';
303
+ }
304
+
305
+ if (composer && activeItemStable) {
306
+ return 'draft_action';
307
+ }
308
+
309
+ if (liveItems.length > 0) {
310
+ return 'select_live_item';
311
+ }
312
+
313
+ return 'workspace_inspect';
314
+ }
315
+
316
+ function buildWorkspaceSnapshotView(snapshot) {
317
+ const workspaceSummary = summarizeWorkspaceSnapshot(snapshot);
318
+ const workspaceSurface = snapshot.workspace_surface ?? snapshot.workspaceSurface ?? workspaceSummary.workspace_surface;
319
+
320
+ return {
321
+ workspaceSummary: toPublicWorkspaceSummary(workspaceSummary, snapshot),
322
+ workspaceSurface,
323
+ workspace: {
324
+ workspace_surface: workspaceSurface,
325
+ live_items: getLiveItems(snapshot).map(toPublicLiveItem),
326
+ active_item: toPublicActiveItem(getActiveItem(snapshot)),
327
+ composer: toPublicComposer(getComposer(snapshot)),
328
+ action_controls: getActionControls(snapshot).map(toPublicActionControl),
329
+ blocking_modals: getBlockingModals(snapshot).map(toPublicBlockingModal),
330
+ loading_shell: getLoadingShell(snapshot),
331
+ summary: toPublicWorkspaceSummary(workspaceSummary, snapshot),
332
+ },
333
+ };
334
+ }
335
+
336
+ function createWorkspaceRebuildHints(page, state, syncState) {
337
+ return async () => {
338
+ await syncState(page, state, { force: true });
339
+ return null;
340
+ };
341
+ }
342
+
343
+ async function clickWorkspaceItemByLabel(page, requestedLabel) {
344
+ const point = await page.evaluate(({ selector, requestedLabel: label }) => {
345
+ function compactText(value) {
346
+ return String(value ?? '').replace(/\s+/g, ' ').trim();
347
+ }
348
+
349
+ function normalizeLabel(value) {
350
+ return compactText(value).toLowerCase();
351
+ }
352
+
353
+ function isVisible(el) {
354
+ const rect = el.getBoundingClientRect();
355
+ const style = window.getComputedStyle(el);
356
+ return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
357
+ }
358
+
359
+ function getText(el) {
360
+ return compactText(el.getAttribute('aria-label') || el.textContent || el.value || '');
361
+ }
362
+
363
+ const normalized = normalizeLabel(label);
364
+ if (!normalized) return null;
365
+
366
+ const target = [...document.querySelectorAll(selector)]
367
+ .find((el) => isVisible(el) && normalizeLabel(getText(el)) === normalized);
368
+
369
+ if (!target) return null;
370
+
371
+ const rect = target.getBoundingClientRect();
372
+ return {
373
+ x: rect.left + rect.width / 2,
374
+ y: rect.top + rect.height / 2,
375
+ };
376
+ }, {
377
+ selector: WORKSPACE_ITEM_SELECTOR,
378
+ requestedLabel,
379
+ });
380
+
381
+ if (!point) {
382
+ return false;
383
+ }
384
+
385
+ await page.mouse.click(point.x, point.y);
386
+ return true;
387
+ }
388
+
389
+ async function loadWorkspacePageContext(page, state, syncState, collectSnapshot) {
390
+ await syncState(page, state, { force: true });
391
+ const snapshot = await collectSnapshot(page, state);
392
+ const pageInfo = {
393
+ title: await page.title(),
394
+ url: page.url(),
395
+ };
396
+
397
+ return {
398
+ pageInfo,
399
+ snapshot,
400
+ ...buildWorkspaceSnapshotView(snapshot),
401
+ };
402
+ }
403
+
404
+ function registerWorkspaceActionTool(server, state, deps, toolName, actionKind) {
405
+ const getPage = deps.getActivePage ?? getActivePage;
406
+ const syncState = deps.syncPageState ?? syncPageState;
407
+ const collectSnapshot = deps.collectVisibleWorkspaceSnapshot ?? collectVisibleWorkspaceSnapshot;
408
+ const actionDependency = deps[toolName === 'select_live_item' ? 'selectLiveItem' : 'executeAction'];
409
+
410
+ server.registerTool(
411
+ toolName,
412
+ {
413
+ description: `Placeholder workspace action for ${actionKind}.`,
414
+ inputSchema: {},
415
+ },
416
+ async () => {
417
+ const page = await getPage();
418
+ const { pageInfo, snapshot, workspace, workspaceSummary, workspaceSurface } = await loadWorkspacePageContext(page, state, syncState, collectSnapshot);
419
+ const status = getWorkspaceStatus(state);
420
+ const continuationAction = status === 'direct' ? 'workspace_inspect' : 'request_handoff';
421
+ const delegated = status === 'direct' && typeof actionDependency === 'function';
422
+
423
+ if (delegated) {
424
+ await actionDependency({
425
+ page,
426
+ snapshot,
427
+ workspace,
428
+ workspaceSummary,
429
+ workspaceSurface,
430
+ });
431
+ }
432
+
433
+ return buildGatewayResponse({
434
+ status,
435
+ page: toGatewayPage(pageInfo, state),
436
+ result: {
437
+ task_kind: 'workspace',
438
+ action: {
439
+ kind: actionKind,
440
+ status: status === 'direct' ? (delegated ? 'delegated' : 'unimplemented') : 'blocked',
441
+ },
442
+ workspace,
443
+ summary: formatWorkspaceResultSummary(workspaceSurface, workspace, workspaceSummary),
444
+ },
445
+ continuation: getWorkspaceContinuation(state, continuationAction),
446
+ evidence: {
447
+ workspace_surface: workspaceSurface,
448
+ active_item_label: workspaceSummary.active_item_label ?? null,
449
+ loading_shell: workspaceSummary.loading_shell ?? false,
450
+ blocking_modal_count: workspaceSummary.blocking_modal_count ?? 0,
451
+ },
452
+ });
453
+ }
454
+ );
455
+ }
456
+
457
+ function registerWorkspaceDraftActionTool(server, state, deps) {
458
+ const getPage = deps.getActivePage ?? getActivePage;
459
+ const syncState = deps.syncPageState ?? syncPageState;
460
+ const collectSnapshot = deps.collectVisibleWorkspaceSnapshot ?? collectVisibleWorkspaceSnapshot;
461
+ const draftAction = deps.draftWorkspaceAction ?? draftWorkspaceAction;
462
+ const getBrowserInstance = deps.getBrowserInstance ?? (() => readBrowserInstance(process.env.CHROME_CDP_URL || 'http://localhost:9222'));
463
+
464
+ server.registerTool(
465
+ 'draft_action',
466
+ {
467
+ description: 'Draft text into the current workspace composer and return the refreshed workspace snapshot.',
468
+ inputSchema: {
469
+ text: z.string().describe('Draft text to write into the current workspace composer'),
470
+ },
471
+ },
472
+ async ({ text }) => {
473
+ const page = await getPage();
474
+ await syncState(page, state, { force: true });
475
+ const pageInfo = {
476
+ title: await page.title(),
477
+ url: page.url(),
478
+ };
479
+ const boundaryMismatch = getWorkspaceBoundaryGuard(state, 'draft_action', pageInfo);
480
+ if (boundaryMismatch) return boundaryMismatch;
481
+ const instance = await getBrowserInstance();
482
+ const confirmationError = requireConfirmedRuntimeInstance(state, instance, 'draft_action');
483
+ if (confirmationError) return confirmationError;
484
+ const snapshot = await collectSnapshot(page, state);
485
+ const { workspaceSummary, workspaceSurface, workspace } = buildWorkspaceSnapshotView(snapshot);
486
+ const status = getWorkspaceStatus(state);
487
+ const continuationAction = status === 'direct' ? 'workspace_inspect' : 'request_handoff';
488
+
489
+ if (status !== 'direct') {
490
+ return buildGatewayResponse({
491
+ status,
492
+ page: toGatewayPage(pageInfo, state),
493
+ result: {
494
+ task_kind: 'workspace',
495
+ status: 'blocked',
496
+ draft_evidence: null,
497
+ unresolved: null,
498
+ failure: null,
499
+ action: {
500
+ kind: 'draft_action',
501
+ status: 'blocked',
502
+ },
503
+ snapshot: workspace,
504
+ workspace,
505
+ summary: formatWorkspaceResultSummary(workspaceSurface, workspace, workspaceSummary),
506
+ },
507
+ continuation: getWorkspaceContinuation(state, continuationAction),
508
+ evidence: {
509
+ workspace_surface: workspaceSurface,
510
+ active_item_label: workspaceSummary.active_item_label ?? null,
511
+ loading_shell: workspaceSummary.loading_shell ?? false,
512
+ blocking_modal_count: workspaceSummary.blocking_modal_count ?? 0,
513
+ },
514
+ });
515
+ }
516
+
517
+ const draftResult = await draftAction({
518
+ state,
519
+ page,
520
+ snapshot,
521
+ refreshSnapshot: async () => {
522
+ await syncState(page, state, { force: true });
523
+ return collectSnapshot(page, state);
524
+ },
525
+ }, text);
526
+ const refreshedSnapshot = draftResult.snapshot ?? snapshot;
527
+ const refreshedView = buildWorkspaceSnapshotView(refreshedSnapshot);
528
+ const pageInfoAfter = {
529
+ title: await page.title(),
530
+ url: page.url(),
531
+ };
532
+ const publicDraftEvidence = toPublicDraftEvidence(draftResult.draft_evidence);
533
+ const publicUnresolved = toPublicDraftUnresolved(draftResult.unresolved);
534
+ const publicFailure = toPublicDraftFailure(draftResult);
535
+ const publicSnapshot = refreshedView.workspace;
536
+
537
+ return buildGatewayResponse({
538
+ status,
539
+ page: toGatewayPage(pageInfoAfter, state),
540
+ result: {
541
+ task_kind: 'workspace',
542
+ status: draftResult.status ?? 'unresolved',
543
+ draft_evidence: publicDraftEvidence,
544
+ unresolved: publicUnresolved,
545
+ failure: publicFailure,
546
+ action: {
547
+ kind: 'draft_action',
548
+ status: draftResult.status ?? 'unresolved',
549
+ },
550
+ snapshot: publicSnapshot,
551
+ workspace: publicSnapshot,
552
+ summary: formatWorkspaceResultSummary(refreshedView.workspaceSurface, publicSnapshot, refreshedView.workspaceSummary),
553
+ },
554
+ continuation: getWorkspaceContinuation(state, 'workspace_inspect'),
555
+ evidence: {
556
+ workspace_surface: refreshedView.workspaceSurface,
557
+ active_item_label: refreshedView.workspaceSummary.active_item_label ?? null,
558
+ loading_shell: refreshedView.workspaceSummary.loading_shell ?? false,
559
+ blocking_modal_count: refreshedView.workspaceSummary.blocking_modal_count ?? 0,
560
+ draft_evidence: publicDraftEvidence,
561
+ failure: publicFailure,
562
+ },
563
+ });
564
+ }
565
+ );
566
+ }
567
+
568
+ function registerWorkspaceExecuteActionTool(server, state, deps) {
569
+ const getPage = deps.getActivePage ?? getActivePage;
570
+ const syncState = deps.syncPageState ?? syncPageState;
571
+ const collectSnapshot = deps.collectVisibleWorkspaceSnapshot ?? collectVisibleWorkspaceSnapshot;
572
+ const actionExecutor = deps.executeWorkspaceAction ?? executeWorkspaceAction;
573
+ const getBrowserInstance = deps.getBrowserInstance ?? (() => readBrowserInstance(process.env.CHROME_CDP_URL || 'http://localhost:9222'));
574
+
575
+ server.registerTool(
576
+ 'execute_action',
577
+ {
578
+ description: 'Execute the current workspace send action after explicit confirmation and return the refreshed workspace snapshot.',
579
+ inputSchema: {
580
+ action: z.enum(['send']).default('send'),
581
+ mode: z.enum(['preview', 'confirm']).default('preview'),
582
+ confirmation: z.string().optional(),
583
+ },
584
+ },
585
+ async ({ action = 'send', mode = 'preview', confirmation } = {}) => {
586
+ const page = await getPage();
587
+ await syncState(page, state, { force: true });
588
+ const pageInfo = {
589
+ title: await page.title(),
590
+ url: page.url(),
591
+ };
592
+ const boundaryMismatch = getWorkspaceBoundaryGuard(state, 'execute_action', pageInfo);
593
+ if (boundaryMismatch) return boundaryMismatch;
594
+ const instance = await getBrowserInstance();
595
+ const confirmationError = requireConfirmedRuntimeInstance(state, instance, 'execute_action');
596
+ if (confirmationError) return confirmationError;
597
+ const snapshot = await collectSnapshot(page, state);
598
+ const executeResult = await actionExecutor({
599
+ state,
600
+ page,
601
+ snapshot,
602
+ clickByHintId: deps.clickByHintId ?? clickByHintId,
603
+ executeGuardedAction: deps.executeGuardedAction,
604
+ verifyActionOutcome: deps.verifyActionOutcome,
605
+ rebuildHints: deps.rebuildHints,
606
+ refreshSnapshot: async () => {
607
+ await syncState(page, state, { force: true });
608
+ return collectSnapshot(page, state);
609
+ },
610
+ }, {
611
+ action,
612
+ mode,
613
+ confirmation,
614
+ });
615
+ await syncState(page, state, { force: true });
616
+ const finalSnapshot = await collectSnapshot(page, state);
617
+ const finalView = buildWorkspaceSnapshotView(finalSnapshot);
618
+ const finalStatus = getWorkspaceStatus(state);
619
+ const continuationAction = finalStatus === 'direct' ? 'verify_outcome' : 'request_handoff';
620
+ const pageInfoAfter = {
621
+ title: await page.title(),
622
+ url: page.url(),
623
+ };
624
+
625
+ return buildGatewayResponse({
626
+ status: finalStatus,
627
+ page: toGatewayPage(pageInfoAfter, state),
628
+ result: {
629
+ task_kind: 'workspace',
630
+ status: executeResult.status ?? 'failed',
631
+ blocked: executeResult.blocked === true,
632
+ executed: executeResult.executed === true,
633
+ reason: executeResult.reason ?? null,
634
+ unresolved: toPublicExecuteUnresolved(executeResult.unresolved),
635
+ failure: toPublicExecuteFailure(executeResult.failure),
636
+ verification: toPublicExecuteVerification(executeResult.verification),
637
+ action: {
638
+ kind: 'execute_action',
639
+ status: executeResult.action?.status ?? (executeResult.status === 'success' ? 'executed' : executeResult.status ?? 'blocked'),
640
+ },
641
+ snapshot: finalView.workspace,
642
+ workspace: finalView.workspace,
643
+ summary: formatWorkspaceResultSummary(finalView.workspaceSurface, finalView.workspace, finalView.workspaceSummary),
644
+ },
645
+ continuation: getWorkspaceContinuation(state, continuationAction),
646
+ evidence: {
647
+ workspace_surface: finalView.workspaceSurface,
648
+ active_item_label: finalView.workspaceSummary.active_item_label ?? null,
649
+ loading_shell: finalView.workspaceSummary.loading_shell ?? false,
650
+ blocking_modal_count: finalView.workspaceSummary.blocking_modal_count ?? 0,
651
+ blocked: executeResult.blocked === true,
652
+ executed: executeResult.executed === true,
653
+ reason: executeResult.reason ?? null,
654
+ verification: toPublicExecuteVerification(executeResult.verification),
655
+ failure: toPublicExecuteFailure(executeResult.failure),
656
+ },
657
+ });
658
+ }
659
+ );
660
+ }
661
+
662
+ function registerWorkspaceVerifyOutcomeTool(server, state, deps) {
663
+ const getPage = deps.getActivePage ?? getActivePage;
664
+ const syncState = deps.syncPageState ?? syncPageState;
665
+ const collectSnapshot = deps.collectVisibleWorkspaceSnapshot ?? collectVisibleWorkspaceSnapshot;
666
+
667
+ server.registerTool(
668
+ 'verify_outcome',
669
+ {
670
+ description: 'Rebuild a fresh workspace snapshot, verify the current outcome, and suggest the next step.',
671
+ inputSchema: {},
672
+ },
673
+ async () => {
674
+ const page = await getPage();
675
+ await syncState(page, state, { force: true });
676
+ const pageInfo = {
677
+ title: await page.title(),
678
+ url: page.url(),
679
+ };
680
+ const boundaryMismatch = getWorkspaceBoundaryGuard(state, 'verify_outcome', pageInfo);
681
+ if (boundaryMismatch) return boundaryMismatch;
682
+ const snapshot = await collectSnapshot(page, state);
683
+ const workspaceSurface = snapshot.workspace_surface ?? snapshot.workspaceSurface ?? 'unknown';
684
+ const verification = buildWorkspaceVerification(snapshot);
685
+ const status = getWorkspaceStatus(state);
686
+ const suggestedNextAction = status === 'direct' ? verification.ready_for_next_action : 'request_handoff';
687
+ const publicVerification = status === 'direct'
688
+ ? verification
689
+ : {
690
+ ...verification,
691
+ ready_for_next_action: 'request_handoff',
692
+ };
693
+ return buildGatewayResponse({
694
+ status,
695
+ page: toGatewayPage(pageInfo, state),
696
+ result: {
697
+ task_kind: 'workspace',
698
+ verification: publicVerification,
699
+ suggested_next_action: suggestedNextAction,
700
+ summary: formatWorkspaceResultSummary(workspaceSurface, null, publicVerification),
701
+ },
702
+ continuation: getWorkspaceContinuation(state, suggestedNextAction),
703
+ evidence: {
704
+ workspace_surface: workspaceSurface,
705
+ active_item_label: publicVerification.active_item_label,
706
+ loading_shell: publicVerification.loading_shell,
707
+ blocking_modal_present: publicVerification.blocking_modal_present,
708
+ detail_alignment: publicVerification.detail_alignment,
709
+ ready_for_next_action: publicVerification.ready_for_next_action,
710
+ },
711
+ });
712
+ }
713
+ );
714
+ }
715
+
716
+ export function registerWorkspaceTools(server, state, deps = {}) {
717
+ const getPage = deps.getActivePage ?? getActivePage;
718
+ const syncState = deps.syncPageState ?? syncPageState;
719
+ const collectSnapshot = deps.collectVisibleWorkspaceSnapshot ?? collectVisibleWorkspaceSnapshot;
720
+ const getBrowserInstance = deps.getBrowserInstance ?? (() => readBrowserInstance(process.env.CHROME_CDP_URL || 'http://localhost:9222'));
721
+ const toolDeps = { ...deps, getBrowserInstance };
722
+
723
+ server.registerTool(
724
+ 'workspace_inspect',
725
+ {
726
+ description: 'Inspect the current workspace surface, live items, and composer state.',
727
+ inputSchema: {},
728
+ },
729
+ async () => {
730
+ const page = await getPage();
731
+ await syncState(page, state, { force: true });
732
+ const pageInfo = {
733
+ title: await page.title(),
734
+ url: page.url(),
735
+ };
736
+ const boundaryMismatch = getWorkspaceBoundaryGuard(state, 'workspace_inspect', pageInfo);
737
+ if (boundaryMismatch) return boundaryMismatch;
738
+ const snapshot = await collectSnapshot(page, state);
739
+ const { workspace, workspaceSummary, workspaceSurface } = buildWorkspaceSnapshotView(snapshot);
740
+
741
+ return buildGatewayResponse({
742
+ status: getWorkspaceStatus(state),
743
+ page: toGatewayPage(pageInfo, state),
744
+ result: {
745
+ task_kind: 'workspace',
746
+ workspace,
747
+ summary: formatWorkspaceResultSummary(workspaceSurface, workspace, workspaceSummary),
748
+ },
749
+ continuation: getWorkspaceContinuation(state, getWorkspaceNextAction(snapshot)),
750
+ evidence: {
751
+ workspace_surface: workspaceSurface,
752
+ active_item_label: workspaceSummary.active_item_label ?? null,
753
+ loading_shell: workspaceSummary.loading_shell ?? getLoadingShell(snapshot),
754
+ blocking_modal_count: workspaceSummary.blocking_modal_count ?? getBlockingModals(snapshot).length,
755
+ },
756
+ });
757
+ }
758
+ );
759
+
760
+ server.registerTool(
761
+ 'select_live_item',
762
+ {
763
+ description: 'Select a visible workspace item by label and return the refreshed workspace snapshot.',
764
+ inputSchema: {
765
+ item: z.string().describe('Visible item label to select in the current workspace'),
766
+ },
767
+ },
768
+ async ({ item }) => {
769
+ const page = await getPage();
770
+ await syncState(page, state, { force: true });
771
+ const pageInfo = {
772
+ title: await page.title(),
773
+ url: page.url(),
774
+ };
775
+ const boundaryMismatch = getWorkspaceBoundaryGuard(state, 'select_live_item', pageInfo);
776
+ if (boundaryMismatch) return boundaryMismatch;
777
+ const instance = await getBrowserInstance();
778
+ const confirmationError = requireConfirmedRuntimeInstance(state, instance, 'select_live_item');
779
+ if (confirmationError) return confirmationError;
780
+ const snapshot = await collectSnapshot(page, state);
781
+ const { workspace, workspaceSummary, workspaceSurface } = buildWorkspaceSnapshotView(snapshot);
782
+ const status = getWorkspaceStatus(state);
783
+ const rebuildHints = createWorkspaceRebuildHints(page, state, syncState);
784
+ const selection = await selectWorkspaceItem({
785
+ state,
786
+ page,
787
+ snapshot,
788
+ refreshSnapshot: async () => {
789
+ await syncState(page, state, { force: true });
790
+ return collectSnapshot(page, state);
791
+ },
792
+ selectItemByHint: async (candidate) => {
793
+ if (typeof deps.selectLiveItem === 'function') {
794
+ return deps.selectLiveItem({
795
+ page,
796
+ item: candidate,
797
+ snapshot,
798
+ workspace,
799
+ workspaceSummary,
800
+ workspaceSurface,
801
+ });
802
+ }
803
+
804
+ if (!candidate?.hint_id) {
805
+ const clicked = await clickWorkspaceItemByLabel(page, candidate?.label ?? item);
806
+ if (clicked) {
807
+ return { ok: true };
808
+ }
809
+
810
+ return {
811
+ ok: false,
812
+ unresolved: {
813
+ reason: 'no_live_target',
814
+ requested_label: item,
815
+ matches: [],
816
+ recovery_hint: 'retry_selection',
817
+ },
818
+ };
819
+ }
820
+
821
+ await clickByHintId(page, candidate.hint_id, { rebuildHints });
822
+ return { ok: true };
823
+ },
824
+ }, item);
825
+ const refreshedSnapshot = selection.snapshot ?? snapshot;
826
+ const refreshedView = buildWorkspaceSnapshotView(refreshedSnapshot);
827
+ const pageInfoAfter = {
828
+ title: await page.title(),
829
+ url: page.url(),
830
+ };
831
+ const publicSnapshot = refreshedView.workspace;
832
+ const publicSelectedItem = toPublicSelectionItem(selection.selected_item, selection.status === 'selected');
833
+ const publicActiveItem = toPublicActiveItem(selection.active_item);
834
+ const publicSelectionEvidence = toPublicSelectionEvidence(selection.selection_evidence, selection.status === 'selected');
835
+ const publicUnresolved = toPublicSelectionUnresolved(selection.unresolved);
836
+
837
+ return buildGatewayResponse({
838
+ status,
839
+ page: toGatewayPage(pageInfoAfter, state),
840
+ result: {
841
+ task_kind: 'workspace',
842
+ status: selection.status,
843
+ selected_item: publicSelectedItem,
844
+ active_item: publicActiveItem,
845
+ detail_alignment: selection.detail_alignment,
846
+ snapshot: publicSnapshot,
847
+ selection_evidence: publicSelectionEvidence,
848
+ unresolved: publicUnresolved,
849
+ action: {
850
+ kind: 'select_live_item',
851
+ status: selection.status,
852
+ },
853
+ workspace: publicSnapshot,
854
+ summary: formatWorkspaceResultSummary(refreshedView.workspaceSurface, publicSnapshot, {
855
+ ...refreshedView.workspaceSummary,
856
+ active_item_label: selection.active_item?.label ?? selection.selected_item?.label ?? refreshedView.workspaceSummary.active_item_label ?? null,
857
+ }),
858
+ },
859
+ continuation: getWorkspaceContinuation(state, 'workspace_inspect'),
860
+ evidence: {
861
+ workspace_surface: refreshedView.workspaceSurface,
862
+ active_item_label: selection.active_item?.label ?? refreshedView.workspaceSummary.active_item_label ?? null,
863
+ loading_shell: refreshedView.workspaceSummary.loading_shell ?? false,
864
+ blocking_modal_count: refreshedView.workspaceSummary.blocking_modal_count ?? 0,
865
+ selection_evidence: publicSelectionEvidence,
866
+ },
867
+ });
868
+ }
869
+ );
870
+ registerWorkspaceDraftActionTool(server, state, toolDeps);
871
+ registerWorkspaceExecuteActionTool(server, state, toolDeps);
872
+ registerWorkspaceVerifyOutcomeTool(server, state, toolDeps);
873
+ }