@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.
- package/LICENSE +21 -0
- package/README.md +327 -0
- package/README.zh-CN.md +324 -0
- package/examples/README.md +31 -0
- package/examples/claude-desktop.json +8 -0
- package/examples/codex-config.toml +4 -0
- package/grasp.skill +0 -0
- package/index.js +87 -0
- package/package.json +48 -0
- package/scripts/grasp_openclaw_ctl.sh +122 -0
- package/scripts/run-search-benchmark.mjs +287 -0
- package/scripts/update-star-history.mjs +274 -0
- package/skill/SKILL.md +61 -0
- package/skill/references/tools.md +306 -0
- package/src/cli/auto-configure.js +116 -0
- package/src/cli/cmd-connect.js +148 -0
- package/src/cli/cmd-explain.js +42 -0
- package/src/cli/cmd-logs.js +55 -0
- package/src/cli/cmd-status.js +119 -0
- package/src/cli/config.js +27 -0
- package/src/cli/detect-chrome.js +58 -0
- package/src/grasp/handoff/events.js +67 -0
- package/src/grasp/handoff/persist.js +48 -0
- package/src/grasp/handoff/state.js +28 -0
- package/src/grasp/page/capture.js +34 -0
- package/src/grasp/page/state.js +273 -0
- package/src/grasp/verify/evidence.js +40 -0
- package/src/grasp/verify/pipeline.js +52 -0
- package/src/layer1-bridge/chrome.js +416 -0
- package/src/layer1-bridge/webmcp.js +143 -0
- package/src/layer2-perception/hints.js +284 -0
- package/src/layer3-action/actions.js +400 -0
- package/src/runtime/browser-instance.js +65 -0
- package/src/runtime/truth/model.js +94 -0
- package/src/runtime/truth/snapshot.js +51 -0
- package/src/server/affordances.js +47 -0
- package/src/server/audit.js +122 -0
- package/src/server/boss-fast-path.js +164 -0
- package/src/server/boundary-guard.js +53 -0
- package/src/server/content.js +97 -0
- package/src/server/continuity.js +256 -0
- package/src/server/engine-selection.js +29 -0
- package/src/server/entry-orchestrator.js +115 -0
- package/src/server/error-codes.js +7 -0
- package/src/server/explain-share-card.js +113 -0
- package/src/server/fast-path-router.js +134 -0
- package/src/server/form-runtime.js +602 -0
- package/src/server/form-tasks.js +254 -0
- package/src/server/gateway-response.js +62 -0
- package/src/server/index.js +22 -0
- package/src/server/observe.js +52 -0
- package/src/server/page-projection.js +31 -0
- package/src/server/page-state.js +27 -0
- package/src/server/postconditions.js +128 -0
- package/src/server/prompt-assembly.js +148 -0
- package/src/server/responses.js +44 -0
- package/src/server/route-boundary.js +174 -0
- package/src/server/route-policy.js +168 -0
- package/src/server/runtime-confirmation.js +87 -0
- package/src/server/runtime-status.js +7 -0
- package/src/server/share-artifacts.js +284 -0
- package/src/server/state.js +132 -0
- package/src/server/structured-extraction.js +131 -0
- package/src/server/surface-prompts.js +166 -0
- package/src/server/task-frame.js +11 -0
- package/src/server/tasks/search-task.js +321 -0
- package/src/server/tools.actions.js +1361 -0
- package/src/server/tools.form.js +526 -0
- package/src/server/tools.gateway.js +757 -0
- package/src/server/tools.handoff.js +210 -0
- package/src/server/tools.js +20 -0
- package/src/server/tools.legacy.js +983 -0
- package/src/server/tools.strategy.js +250 -0
- package/src/server/tools.task-surface.js +66 -0
- package/src/server/tools.workspace.js +873 -0
- package/src/server/workspace-runtime.js +1138 -0
- package/src/server/workspace-tasks.js +735 -0
- package/start-chrome.bat +84 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { getActivePage } from '../layer1-bridge/chrome.js';
|
|
4
|
+
import { typeByHintId } from '../layer3-action/actions.js';
|
|
5
|
+
import { buildGatewayResponse } from './gateway-response.js';
|
|
6
|
+
import { guardExpectedBoundary } from './boundary-guard.js';
|
|
7
|
+
import { syncPageState } from './state.js';
|
|
8
|
+
import { collectVisibleFormSnapshot } from './form-tasks.js';
|
|
9
|
+
import {
|
|
10
|
+
fillSafeFields,
|
|
11
|
+
writeTextField as writeTextFieldBridge,
|
|
12
|
+
setControlValue as setControlValueBridge,
|
|
13
|
+
setDateValue as setDateValueBridge,
|
|
14
|
+
applyReviewedControl,
|
|
15
|
+
applyReviewedDate,
|
|
16
|
+
previewSubmit,
|
|
17
|
+
} from './form-runtime.js';
|
|
18
|
+
import { readBrowserInstance } from '../runtime/browser-instance.js';
|
|
19
|
+
import { requireConfirmedRuntimeInstance } from './runtime-confirmation.js';
|
|
20
|
+
|
|
21
|
+
function toGatewayPage(page, state) {
|
|
22
|
+
return {
|
|
23
|
+
title: page.title,
|
|
24
|
+
url: page.url,
|
|
25
|
+
page_role: state.pageState?.currentRole ?? 'unknown',
|
|
26
|
+
grasp_confidence: state.pageState?.graspConfidence ?? 'unknown',
|
|
27
|
+
risk_gate: state.pageState?.riskGateDetected ?? false,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getFormStatus(state) {
|
|
32
|
+
const handoffState = state.handoff?.state ?? 'idle';
|
|
33
|
+
if (handoffState === 'handoff_required' || handoffState === 'handoff_in_progress' || handoffState === 'awaiting_reacquisition') {
|
|
34
|
+
return 'handoff_required';
|
|
35
|
+
}
|
|
36
|
+
return state.pageState?.riskGateDetected ? 'gated' : 'direct';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getFormContinuation(state) {
|
|
40
|
+
const handoffState = state.handoff?.state ?? 'idle';
|
|
41
|
+
const status = getFormStatus(state);
|
|
42
|
+
if (status !== 'direct') {
|
|
43
|
+
return {
|
|
44
|
+
can_continue: false,
|
|
45
|
+
suggested_next_action: 'request_handoff',
|
|
46
|
+
handoff_state: handoffState,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
can_continue: true,
|
|
52
|
+
suggested_next_action: 'verify_form',
|
|
53
|
+
handoff_state: handoffState,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getFormBoundaryGuard(state, toolName, pageInfo) {
|
|
58
|
+
return guardExpectedBoundary({
|
|
59
|
+
toolName,
|
|
60
|
+
expectedBoundary: 'form_runtime',
|
|
61
|
+
status: getFormStatus(state),
|
|
62
|
+
page: toGatewayPage(pageInfo, state),
|
|
63
|
+
handoffState: state.handoff?.state ?? 'idle',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isTextLikeField(field) {
|
|
68
|
+
const type = String(field?.type ?? '').toLowerCase();
|
|
69
|
+
return type === 'textarea' || !['checkbox', 'radio', 'select', 'date', 'datetime-local', 'month', 'week', 'time', 'file', 'submit', 'button'].includes(type);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isDateLikeField(field) {
|
|
73
|
+
const type = String(field?.type ?? '').toLowerCase();
|
|
74
|
+
return ['date', 'datetime-local', 'month', 'week', 'time'].includes(type);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isEditableField(field) {
|
|
78
|
+
return field?.disabled !== true && field?.readOnly !== true && field?.readonly !== true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getNextVerifyAction(snapshot) {
|
|
82
|
+
const fields = Array.isArray(snapshot?.fields) ? snapshot.fields : [];
|
|
83
|
+
if (fields.some((field) => field.risk_level === 'safe' && field.current_state !== 'filled' && isTextLikeField(field) && isEditableField(field))) {
|
|
84
|
+
return 'fill_form';
|
|
85
|
+
}
|
|
86
|
+
if (fields.some((field) => field.risk_level === 'review' && field.current_state !== 'filled' && isDateLikeField(field) && isEditableField(field))) {
|
|
87
|
+
return 'set_date';
|
|
88
|
+
}
|
|
89
|
+
if (fields.some((field) => field.risk_level === 'review' && field.current_state !== 'filled' && !isDateLikeField(field) && isEditableField(field))) {
|
|
90
|
+
return 'set_option';
|
|
91
|
+
}
|
|
92
|
+
return 'safe_submit';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function createRebuildHints(page, state, syncState) {
|
|
96
|
+
return async () => {
|
|
97
|
+
await syncState(page, state, { force: true });
|
|
98
|
+
return null;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function setControlByField(page, field, value) {
|
|
103
|
+
const result = await page.evaluate(({ hintId, id, name, value: nextValue }) => {
|
|
104
|
+
const target = (
|
|
105
|
+
(hintId && document.querySelector(`[data-grasp-id="${hintId}"]`))
|
|
106
|
+
|| (id && document.getElementById(id))
|
|
107
|
+
|| (name && document.querySelector(`[name="${name}"]`))
|
|
108
|
+
);
|
|
109
|
+
if (!target) return { ok: false, reason: 'no_live_target' };
|
|
110
|
+
if (target.tagName.toLowerCase() !== 'select') return { ok: false, reason: 'unsupported_widget' };
|
|
111
|
+
if (target.disabled || target.readOnly) return { ok: false, reason: 'field_not_editable' };
|
|
112
|
+
|
|
113
|
+
const option = [...target.options].find((item) => item.value === nextValue || item.textContent?.trim() === nextValue);
|
|
114
|
+
if (!option) return { ok: false, reason: 'unsupported_widget' };
|
|
115
|
+
if (target.value === option.value) return { ok: false, reason: 'no_effect' };
|
|
116
|
+
|
|
117
|
+
target.value = option.value;
|
|
118
|
+
target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
119
|
+
target.dispatchEvent(new Event('change', { bubbles: true }));
|
|
120
|
+
return { ok: true };
|
|
121
|
+
}, {
|
|
122
|
+
hintId: field.hint_id ?? null,
|
|
123
|
+
id: field.id ?? null,
|
|
124
|
+
name: field.name ?? null,
|
|
125
|
+
value,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!result.ok) {
|
|
129
|
+
throw new Error(result.reason);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function setDateByField(page, field, value) {
|
|
134
|
+
const result = await page.evaluate(({ hintId, id, name, value: nextValue }) => {
|
|
135
|
+
const target = (
|
|
136
|
+
(hintId && document.querySelector(`[data-grasp-id="${hintId}"]`))
|
|
137
|
+
|| (id && document.getElementById(id))
|
|
138
|
+
|| (name && document.querySelector(`[name="${name}"]`))
|
|
139
|
+
);
|
|
140
|
+
if (!target) return { ok: false, reason: 'no_live_target' };
|
|
141
|
+
const type = target.getAttribute('type') || '';
|
|
142
|
+
if (!['date', 'datetime-local', 'month', 'week', 'time'].includes(type)) {
|
|
143
|
+
return { ok: false, reason: 'unsupported_widget' };
|
|
144
|
+
}
|
|
145
|
+
if (target.disabled || target.readOnly) return { ok: false, reason: 'field_not_editable' };
|
|
146
|
+
if (target.value === nextValue) return { ok: false, reason: 'no_effect' };
|
|
147
|
+
|
|
148
|
+
target.value = nextValue;
|
|
149
|
+
target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
150
|
+
target.dispatchEvent(new Event('change', { bubbles: true }));
|
|
151
|
+
return target.value === nextValue
|
|
152
|
+
? { ok: true }
|
|
153
|
+
: { ok: false, reason: 'no_effect' };
|
|
154
|
+
}, {
|
|
155
|
+
hintId: field.hint_id ?? null,
|
|
156
|
+
id: field.id ?? null,
|
|
157
|
+
name: field.name ?? null,
|
|
158
|
+
value,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (!result.ok) {
|
|
162
|
+
throw new Error(result.reason);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function clickSubmitControl(page, control) {
|
|
167
|
+
const result = await page.evaluate(({ hintId, label }) => {
|
|
168
|
+
const target = (
|
|
169
|
+
(hintId && document.querySelector(`[data-grasp-id="${hintId}"]`))
|
|
170
|
+
|| [...document.querySelectorAll('button, input[type="submit"], input[type="button"], input[type="image"]')]
|
|
171
|
+
.find((item) => {
|
|
172
|
+
const text = (item.getAttribute('aria-label') || item.textContent || item.getAttribute('value') || '').trim();
|
|
173
|
+
return text === label;
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (!target) return { ok: false };
|
|
178
|
+
target.click();
|
|
179
|
+
return { ok: true };
|
|
180
|
+
}, {
|
|
181
|
+
hintId: control?.hint_id ?? null,
|
|
182
|
+
label: control?.label ?? '',
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (!result.ok) {
|
|
186
|
+
throw new Error('no_submit_control');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function registerFormTools(server, state, deps = {}) {
|
|
191
|
+
const getPage = deps.getActivePage ?? getActivePage;
|
|
192
|
+
const syncState = deps.syncPageState ?? syncPageState;
|
|
193
|
+
const getBrowserInstance = deps.getBrowserInstance ?? (() => readBrowserInstance(process.env.CHROME_CDP_URL || 'http://localhost:9222'));
|
|
194
|
+
const collectSnapshot = deps.collectVisibleFormSnapshot ?? collectVisibleFormSnapshot;
|
|
195
|
+
const fillFields = deps.fillSafeFields ?? fillSafeFields;
|
|
196
|
+
const typeField = deps.typeByHintId ?? typeByHintId;
|
|
197
|
+
const applyControl = deps.applyReviewedControl ?? applyReviewedControl;
|
|
198
|
+
const applyDate = deps.applyReviewedDate ?? applyReviewedDate;
|
|
199
|
+
const previewSubmission = deps.previewSubmit ?? previewSubmit;
|
|
200
|
+
|
|
201
|
+
server.registerTool(
|
|
202
|
+
'form_inspect',
|
|
203
|
+
{
|
|
204
|
+
description: 'Inspect the visible form, its fields, and current completion state.',
|
|
205
|
+
inputSchema: {},
|
|
206
|
+
},
|
|
207
|
+
async () => {
|
|
208
|
+
const page = await getPage();
|
|
209
|
+
await syncState(page, state, { force: true });
|
|
210
|
+
const pageInfo = {
|
|
211
|
+
title: await page.title(),
|
|
212
|
+
url: page.url(),
|
|
213
|
+
};
|
|
214
|
+
const boundaryMismatch = getFormBoundaryGuard(state, 'form_inspect', pageInfo);
|
|
215
|
+
if (boundaryMismatch) return boundaryMismatch;
|
|
216
|
+
const snapshot = await collectSnapshot(page);
|
|
217
|
+
|
|
218
|
+
return buildGatewayResponse({
|
|
219
|
+
status: getFormStatus(state),
|
|
220
|
+
page: toGatewayPage(pageInfo, state),
|
|
221
|
+
result: {
|
|
222
|
+
task_kind: 'form',
|
|
223
|
+
form: {
|
|
224
|
+
completion_status: snapshot.completion_status,
|
|
225
|
+
sections: snapshot.sections,
|
|
226
|
+
fields: snapshot.fields,
|
|
227
|
+
submit_controls: snapshot.submit_controls,
|
|
228
|
+
summary: snapshot.summary,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
continuation: getFormContinuation(state),
|
|
232
|
+
evidence: {
|
|
233
|
+
ambiguous_labels: snapshot.ambiguous_labels,
|
|
234
|
+
autosave_possible: true,
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
server.registerTool(
|
|
241
|
+
'fill_form',
|
|
242
|
+
{
|
|
243
|
+
description: 'Fill safe text-like fields on the current form and return refreshed form state.',
|
|
244
|
+
inputSchema: {
|
|
245
|
+
values: z.record(z.string(), z.string()).describe('Map of field labels to desired values'),
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
async ({ values }) => {
|
|
249
|
+
const page = await getPage();
|
|
250
|
+
await syncState(page, state, { force: true });
|
|
251
|
+
const pageInfo = {
|
|
252
|
+
title: await page.title(),
|
|
253
|
+
url: page.url(),
|
|
254
|
+
};
|
|
255
|
+
const boundaryMismatch = getFormBoundaryGuard(state, 'fill_form', pageInfo);
|
|
256
|
+
if (boundaryMismatch) return boundaryMismatch;
|
|
257
|
+
const instance = await getBrowserInstance();
|
|
258
|
+
const confirmationError = requireConfirmedRuntimeInstance(state, instance, 'fill_form');
|
|
259
|
+
if (confirmationError) return confirmationError;
|
|
260
|
+
const rebuildHints = createRebuildHints(page, state, syncState);
|
|
261
|
+
const snapshot = await collectSnapshot(page);
|
|
262
|
+
const operation = await fillFields(
|
|
263
|
+
{
|
|
264
|
+
snapshot,
|
|
265
|
+
writeTextField: async (field, value) => writeTextFieldBridge({
|
|
266
|
+
snapshot,
|
|
267
|
+
typeByHintId: async (resolvedField, text) => typeField(page, resolvedField.hint_id, text, false, { rebuildHints }),
|
|
268
|
+
refreshSnapshot: async () => {
|
|
269
|
+
await syncState(page, state, { force: true });
|
|
270
|
+
return collectSnapshot(page);
|
|
271
|
+
},
|
|
272
|
+
}, field.label, value),
|
|
273
|
+
},
|
|
274
|
+
values,
|
|
275
|
+
);
|
|
276
|
+
const refreshed = operation.snapshot ?? snapshot;
|
|
277
|
+
return buildGatewayResponse({
|
|
278
|
+
status: getFormStatus(state),
|
|
279
|
+
page: toGatewayPage(pageInfo, state),
|
|
280
|
+
result: {
|
|
281
|
+
task_kind: 'form',
|
|
282
|
+
written: operation.written,
|
|
283
|
+
skipped: operation.skipped,
|
|
284
|
+
unresolved: operation.unresolved,
|
|
285
|
+
write_evidence: operation.evidence,
|
|
286
|
+
operation: {
|
|
287
|
+
written: operation.written,
|
|
288
|
+
skipped: operation.skipped,
|
|
289
|
+
unresolved: operation.unresolved,
|
|
290
|
+
},
|
|
291
|
+
form: {
|
|
292
|
+
completion_status: refreshed.completion_status,
|
|
293
|
+
sections: refreshed.sections,
|
|
294
|
+
fields: refreshed.fields,
|
|
295
|
+
submit_controls: refreshed.submit_controls,
|
|
296
|
+
summary: refreshed.summary,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
continuation: {
|
|
300
|
+
...getFormContinuation(state),
|
|
301
|
+
suggested_next_action: 'verify_form',
|
|
302
|
+
},
|
|
303
|
+
evidence: {
|
|
304
|
+
ambiguous_labels: refreshed.ambiguous_labels ?? [],
|
|
305
|
+
writes: operation.evidence,
|
|
306
|
+
autosave_possible: operation.evidence.some((item) => item.autosave_possible),
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
server.registerTool(
|
|
313
|
+
'set_option',
|
|
314
|
+
{
|
|
315
|
+
description: 'Set a review-tier option field on the current form.',
|
|
316
|
+
inputSchema: {
|
|
317
|
+
field: z.string().describe('Field label to update'),
|
|
318
|
+
value: z.string().describe('Desired option label or value'),
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
async ({ field, value }) => {
|
|
322
|
+
const page = await getPage();
|
|
323
|
+
await syncState(page, state, { force: true });
|
|
324
|
+
const pageInfo = {
|
|
325
|
+
title: await page.title(),
|
|
326
|
+
url: page.url(),
|
|
327
|
+
};
|
|
328
|
+
const boundaryMismatch = getFormBoundaryGuard(state, 'set_option', pageInfo);
|
|
329
|
+
if (boundaryMismatch) return boundaryMismatch;
|
|
330
|
+
const instance = await getBrowserInstance();
|
|
331
|
+
const confirmationError = requireConfirmedRuntimeInstance(state, instance, 'set_option');
|
|
332
|
+
if (confirmationError) return confirmationError;
|
|
333
|
+
const snapshot = await collectSnapshot(page);
|
|
334
|
+
const operation = await applyControl({
|
|
335
|
+
snapshot,
|
|
336
|
+
setControlValue: async (resolvedField, nextValue) => setControlValueBridge({
|
|
337
|
+
snapshot,
|
|
338
|
+
setControlByField: async (fallbackField, fallbackValue) => setControlByField(page, fallbackField, fallbackValue),
|
|
339
|
+
refreshSnapshot: async () => {
|
|
340
|
+
await syncState(page, state, { force: true });
|
|
341
|
+
return collectSnapshot(page);
|
|
342
|
+
},
|
|
343
|
+
}, resolvedField.label, nextValue),
|
|
344
|
+
}, field, value);
|
|
345
|
+
const refreshed = operation.snapshot ?? snapshot;
|
|
346
|
+
return buildGatewayResponse({
|
|
347
|
+
status: getFormStatus(state),
|
|
348
|
+
page: toGatewayPage(pageInfo, state),
|
|
349
|
+
result: {
|
|
350
|
+
task_kind: 'form',
|
|
351
|
+
operation,
|
|
352
|
+
form: {
|
|
353
|
+
completion_status: refreshed.completion_status,
|
|
354
|
+
sections: refreshed.sections,
|
|
355
|
+
fields: refreshed.fields,
|
|
356
|
+
submit_controls: refreshed.submit_controls,
|
|
357
|
+
summary: refreshed.summary,
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
continuation: {
|
|
361
|
+
...getFormContinuation(state),
|
|
362
|
+
suggested_next_action: 'verify_form',
|
|
363
|
+
},
|
|
364
|
+
evidence: {
|
|
365
|
+
ambiguous_labels: refreshed.ambiguous_labels ?? [],
|
|
366
|
+
write: operation.evidence ?? null,
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
server.registerTool(
|
|
373
|
+
'set_date',
|
|
374
|
+
{
|
|
375
|
+
description: 'Set a review-tier date field on the current form.',
|
|
376
|
+
inputSchema: {
|
|
377
|
+
field: z.string().describe('Field label to update'),
|
|
378
|
+
value: z.string().describe('Desired ISO-like date value'),
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
async ({ field, value }) => {
|
|
382
|
+
const page = await getPage();
|
|
383
|
+
await syncState(page, state, { force: true });
|
|
384
|
+
const pageInfo = {
|
|
385
|
+
title: await page.title(),
|
|
386
|
+
url: page.url(),
|
|
387
|
+
};
|
|
388
|
+
const boundaryMismatch = getFormBoundaryGuard(state, 'set_date', pageInfo);
|
|
389
|
+
if (boundaryMismatch) return boundaryMismatch;
|
|
390
|
+
const instance = await getBrowserInstance();
|
|
391
|
+
const confirmationError = requireConfirmedRuntimeInstance(state, instance, 'set_date');
|
|
392
|
+
if (confirmationError) return confirmationError;
|
|
393
|
+
const snapshot = await collectSnapshot(page);
|
|
394
|
+
const operation = await applyDate({
|
|
395
|
+
snapshot,
|
|
396
|
+
setDateValue: async (resolvedField, nextValue) => setDateValueBridge({
|
|
397
|
+
snapshot,
|
|
398
|
+
setDateByField: async (fallbackField, fallbackValue) => setDateByField(page, fallbackField, fallbackValue),
|
|
399
|
+
refreshSnapshot: async () => {
|
|
400
|
+
await syncState(page, state, { force: true });
|
|
401
|
+
return collectSnapshot(page);
|
|
402
|
+
},
|
|
403
|
+
}, resolvedField.label, nextValue),
|
|
404
|
+
}, field, value);
|
|
405
|
+
const refreshed = operation.snapshot ?? snapshot;
|
|
406
|
+
return buildGatewayResponse({
|
|
407
|
+
status: getFormStatus(state),
|
|
408
|
+
page: toGatewayPage(pageInfo, state),
|
|
409
|
+
result: {
|
|
410
|
+
task_kind: 'form',
|
|
411
|
+
operation,
|
|
412
|
+
form: {
|
|
413
|
+
completion_status: refreshed.completion_status,
|
|
414
|
+
sections: refreshed.sections,
|
|
415
|
+
fields: refreshed.fields,
|
|
416
|
+
submit_controls: refreshed.submit_controls,
|
|
417
|
+
summary: refreshed.summary,
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
continuation: {
|
|
421
|
+
...getFormContinuation(state),
|
|
422
|
+
suggested_next_action: 'verify_form',
|
|
423
|
+
},
|
|
424
|
+
evidence: {
|
|
425
|
+
ambiguous_labels: refreshed.ambiguous_labels ?? [],
|
|
426
|
+
write: operation.evidence ?? null,
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
server.registerTool(
|
|
433
|
+
'verify_form',
|
|
434
|
+
{
|
|
435
|
+
description: 'Re-read the visible form and report missing, risky, and unresolved fields.',
|
|
436
|
+
inputSchema: {},
|
|
437
|
+
},
|
|
438
|
+
async () => {
|
|
439
|
+
const page = await getPage();
|
|
440
|
+
await syncState(page, state, { force: true });
|
|
441
|
+
const pageInfo = {
|
|
442
|
+
title: await page.title(),
|
|
443
|
+
url: page.url(),
|
|
444
|
+
};
|
|
445
|
+
const boundaryMismatch = getFormBoundaryGuard(state, 'verify_form', pageInfo);
|
|
446
|
+
if (boundaryMismatch) return boundaryMismatch;
|
|
447
|
+
const snapshot = await collectSnapshot(page);
|
|
448
|
+
|
|
449
|
+
return buildGatewayResponse({
|
|
450
|
+
status: getFormStatus(state),
|
|
451
|
+
page: toGatewayPage(pageInfo, state),
|
|
452
|
+
result: {
|
|
453
|
+
task_kind: 'form',
|
|
454
|
+
form: {
|
|
455
|
+
completion_status: snapshot.completion_status,
|
|
456
|
+
verification: snapshot.verification,
|
|
457
|
+
sections: snapshot.sections,
|
|
458
|
+
fields: snapshot.fields,
|
|
459
|
+
submit_controls: snapshot.submit_controls,
|
|
460
|
+
summary: snapshot.summary,
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
continuation: {
|
|
464
|
+
...getFormContinuation(state),
|
|
465
|
+
suggested_next_action: getNextVerifyAction(snapshot),
|
|
466
|
+
},
|
|
467
|
+
evidence: {
|
|
468
|
+
ambiguous_labels: snapshot.ambiguous_labels ?? [],
|
|
469
|
+
autosave_possible: true,
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
server.registerTool(
|
|
476
|
+
'safe_submit',
|
|
477
|
+
{
|
|
478
|
+
description: 'Preview or confirm form submission with blocker reporting.',
|
|
479
|
+
inputSchema: {
|
|
480
|
+
mode: z.enum(['preview', 'confirm']).default('preview'),
|
|
481
|
+
confirmation: z.string().optional(),
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
async ({ mode = 'preview', confirmation } = {}) => {
|
|
485
|
+
const page = await getPage();
|
|
486
|
+
await syncState(page, state, { force: true });
|
|
487
|
+
const pageInfo = {
|
|
488
|
+
title: await page.title(),
|
|
489
|
+
url: page.url(),
|
|
490
|
+
};
|
|
491
|
+
const boundaryMismatch = getFormBoundaryGuard(state, 'safe_submit', pageInfo);
|
|
492
|
+
if (boundaryMismatch) return boundaryMismatch;
|
|
493
|
+
const instance = await getBrowserInstance();
|
|
494
|
+
const confirmationError = requireConfirmedRuntimeInstance(state, instance, 'safe_submit');
|
|
495
|
+
if (confirmationError) return confirmationError;
|
|
496
|
+
const snapshot = await collectSnapshot(page);
|
|
497
|
+
const submit = await previewSubmission({
|
|
498
|
+
clickSubmit: async (control) => clickSubmitControl(page, control),
|
|
499
|
+
}, snapshot, { mode, confirmation });
|
|
500
|
+
|
|
501
|
+
return buildGatewayResponse({
|
|
502
|
+
status: getFormStatus(state),
|
|
503
|
+
page: toGatewayPage(pageInfo, state),
|
|
504
|
+
result: {
|
|
505
|
+
task_kind: 'form',
|
|
506
|
+
submit,
|
|
507
|
+
form: {
|
|
508
|
+
completion_status: snapshot.completion_status,
|
|
509
|
+
sections: snapshot.sections,
|
|
510
|
+
fields: snapshot.fields,
|
|
511
|
+
submit_controls: snapshot.submit_controls,
|
|
512
|
+
summary: snapshot.summary,
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
continuation: {
|
|
516
|
+
...getFormContinuation(state),
|
|
517
|
+
suggested_next_action: submit.blocked ? getNextVerifyAction(snapshot) : 'form_inspect',
|
|
518
|
+
},
|
|
519
|
+
evidence: {
|
|
520
|
+
ambiguous_labels: snapshot.ambiguous_labels ?? [],
|
|
521
|
+
submit: submit.evidence ?? null,
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
);
|
|
526
|
+
}
|