@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,116 @@
1
+ /**
2
+ * Auto-configure Grasp into detected AI clients.
3
+ * Supported: Claude Code, Codex CLI, Cursor
4
+ *
5
+ * OpenClaw is an agent gateway (not an MCP client) — not applicable here.
6
+ */
7
+
8
+ import { readFile, writeFile, mkdir } from 'fs/promises';
9
+ import { join } from 'path';
10
+ import { homedir, platform } from 'os';
11
+ import { existsSync } from 'fs';
12
+ import { execSync } from 'child_process';
13
+
14
+ const GRASP_MCP_JSON = { command: 'npx', args: ['-y', '@yuzc-001/grasp'] };
15
+ const env = process.env;
16
+
17
+ // ─── Helpers ────────────────────────────────────────────────────────────────
18
+
19
+ function hasCommand(cmd) {
20
+ try {
21
+ execSync(platform() === 'win32' ? `where ${cmd}` : `which ${cmd}`, { stdio: 'ignore' });
22
+ return true;
23
+ } catch { return false; }
24
+ }
25
+
26
+ async function writeMcpJson(filePath) {
27
+ await mkdir(join(filePath, '..'), { recursive: true });
28
+ let config = {};
29
+ try { config = JSON.parse(await readFile(filePath, 'utf8')); } catch { /* new file */ }
30
+ config.mcpServers ??= {};
31
+ if (JSON.stringify(config.mcpServers.grasp) === JSON.stringify(GRASP_MCP_JSON)) {
32
+ return 'already-configured';
33
+ }
34
+ config.mcpServers.grasp = GRASP_MCP_JSON;
35
+ await writeFile(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
36
+ return 'written';
37
+ }
38
+
39
+ async function writeCodexToml(filePath) {
40
+ await mkdir(join(filePath, '..'), { recursive: true });
41
+ let content = '';
42
+ try { content = await readFile(filePath, 'utf8'); } catch { /* new file */ }
43
+ if (content.includes('[mcp_servers.grasp]')) return 'already-configured';
44
+ const entry = '\n[mcp_servers.grasp]\ntype = "stdio"\ncommand = "npx"\nargs = ["-y", "@yuzc-001/grasp"]\n';
45
+ await writeFile(filePath, content + entry, 'utf8');
46
+ return 'written';
47
+ }
48
+
49
+ // ─── Client definitions ──────────────────────────────────────────────────────
50
+
51
+ const CLIENTS = [
52
+
53
+ // Claude Code CLI
54
+ {
55
+ id: 'claude-code',
56
+ label: 'Claude Code',
57
+ installed: () => hasCommand('claude'),
58
+ configure: async () => {
59
+ try {
60
+ const out = execSync('claude mcp list', {
61
+ encoding: 'utf8',
62
+ stdio: ['ignore', 'pipe', 'ignore'],
63
+ });
64
+ if (out.includes('grasp:')) return 'already-configured';
65
+ } catch { /* no entries yet */ }
66
+ try {
67
+ execSync('claude mcp add grasp -- npx -y @yuzc-001/grasp', { stdio: 'ignore' });
68
+ return 'written';
69
+ } catch { return 'failed'; }
70
+ },
71
+ },
72
+
73
+ // Codex CLI
74
+ {
75
+ id: 'codex',
76
+ label: 'Codex CLI',
77
+ installed: () => hasCommand('codex'),
78
+ configure: async () => writeCodexToml(join(homedir(), '.codex', 'config.toml')),
79
+ },
80
+
81
+ // Cursor
82
+ {
83
+ id: 'cursor',
84
+ label: 'Cursor',
85
+ installed: () => {
86
+ if (hasCommand('cursor')) return true;
87
+ if (platform() === 'win32')
88
+ return existsSync(join(env.LOCALAPPDATA ?? '', 'Programs', 'cursor', 'Cursor.exe'));
89
+ if (platform() === 'darwin') return existsSync('/Applications/Cursor.app');
90
+ return false;
91
+ },
92
+ configure: async () => writeMcpJson(join(homedir(), '.cursor', 'mcp.json')),
93
+ },
94
+
95
+ ];
96
+
97
+ // ─── Public API ──────────────────────────────────────────────────────────────
98
+
99
+ export function detectClients() {
100
+ return CLIENTS.filter(c => c.installed()).map(c => c.id);
101
+ }
102
+
103
+ export async function autoConfigureAll(clientIds) {
104
+ const results = [];
105
+ for (const id of clientIds) {
106
+ const client = CLIENTS.find(c => c.id === id);
107
+ if (!client) continue;
108
+ try {
109
+ const result = await client.configure();
110
+ results.push({ id, label: client.label, result });
111
+ } catch (err) {
112
+ results.push({ id, label: client.label, result: 'failed', error: err.message });
113
+ }
114
+ }
115
+ return results;
116
+ }
@@ -0,0 +1,148 @@
1
+ import { spawn } from 'child_process';
2
+ import { join } from 'path';
3
+ import { homedir, platform } from 'os';
4
+ import { readConfig, writeConfig } from './config.js';
5
+ import { detectChromePath } from './detect-chrome.js';
6
+ import { detectClients, autoConfigureAll } from './auto-configure.js';
7
+ import { readBrowserInstance } from '../runtime/browser-instance.js';
8
+
9
+ const STEP_OK = '[ok]';
10
+ const STEP_WAIT = '[..]';
11
+ const STEP_FAIL = '[!!]';
12
+
13
+ async function launchChrome(chromePath, cdpUrl) {
14
+ const port = new URL(cdpUrl).port || '9222';
15
+ const userDataDir = join(homedir(), 'chrome-grasp');
16
+
17
+ spawn(chromePath, [
18
+ `--remote-debugging-port=${port}`,
19
+ `--user-data-dir=${userDataDir}`,
20
+ '--no-first-run',
21
+ '--no-default-browser-check',
22
+ '--start-maximized',
23
+ ], { detached: true, stdio: 'ignore' }).unref();
24
+
25
+ // Wait up to 8s
26
+ for (let i = 0; i < 16; i++) {
27
+ await new Promise(r => setTimeout(r, 500));
28
+ const info = await readBrowserInstance(cdpUrl, { timeout: 800 });
29
+ if (info) return info;
30
+ }
31
+ return null;
32
+ }
33
+
34
+ export async function runConnect() {
35
+ const config = await readConfig();
36
+ const cdpUrl = process.env.CHROME_CDP_URL || config.cdpUrl;
37
+ const userDataDir = join(homedir(), 'chrome-grasp');
38
+
39
+ console.log('');
40
+ console.log(' Grasp Runtime Setup');
41
+ console.log(' ' + '─'.repeat(44));
42
+ console.log(' Bring a Chrome CDP session into the runtime.');
43
+ console.log(' One URL, one best path.');
44
+ console.log('');
45
+
46
+ // Step 1: detect Chrome
47
+ const chromePath = detectChromePath();
48
+ if (chromePath) {
49
+ console.log(` ${STEP_OK} Chrome found`);
50
+ console.log(` ${chromePath}`);
51
+ } else {
52
+ console.log(` ${STEP_FAIL} Chrome not found`);
53
+ console.log('');
54
+ console.log(' Install Google Chrome and try again:');
55
+ console.log(' https://www.google.com/chrome/');
56
+ console.log('');
57
+ process.exit(1);
58
+ }
59
+
60
+ // Step 2: check or launch Chrome with CDP
61
+ console.log('');
62
+ console.log(` ${STEP_WAIT} Ensuring browser runtime at ${cdpUrl} ...`);
63
+ let chromeInfo = await readBrowserInstance(cdpUrl);
64
+ let launchedDedicatedProfile = false;
65
+
66
+ if (!chromeInfo) {
67
+ console.log(` ${STEP_WAIT} Chrome not running, launching dedicated profile...`);
68
+ chromeInfo = await launchChrome(chromePath, cdpUrl);
69
+ launchedDedicatedProfile = chromeInfo !== null;
70
+ }
71
+
72
+ if (!chromeInfo) {
73
+ console.log(` ${STEP_FAIL} Failed to bring the browser runtime online`);
74
+ console.log('');
75
+ console.log(' Try running manually:');
76
+ if (platform() === 'win32') {
77
+ console.log(' start-chrome.bat');
78
+ } else {
79
+ console.log(` "${chromePath}" --remote-debugging-port=9222 --user-data-dir=$HOME/chrome-grasp`);
80
+ }
81
+ console.log('');
82
+ process.exit(1);
83
+ }
84
+
85
+ console.log(` ${STEP_OK} Browser runtime ready (${chromeInfo.browser ?? 'unknown'})`);
86
+ console.log(` Profile: ${userDataDir}`);
87
+ console.log(` Instance: ${chromeInfo.display === 'headless' ? 'headless browser' : chromeInfo.display === 'windowed' ? 'windowed browser' : 'unknown browser mode'}`);
88
+ if (launchedDedicatedProfile) {
89
+ console.log(' Scope: dedicated chrome-grasp profile, not an arbitrary existing browser session');
90
+ }
91
+ if (chromeInfo.warning) {
92
+ console.log(` Warning: ${chromeInfo.warning}`);
93
+ }
94
+
95
+ // Step 3: save cdpUrl to config
96
+ await writeConfig({ cdpUrl });
97
+
98
+ // Step 4: get active tab
99
+ try {
100
+ const tabsRes = await fetch(`${cdpUrl}/json`, { signal: AbortSignal.timeout(1500) });
101
+ const tabs = await tabsRes.json();
102
+ const tab = tabs.find(t => t.type === 'page' && t.url && !t.url.startsWith('chrome://'));
103
+ if (tab) {
104
+ console.log(` ${STEP_OK} Current page: ${tab.title?.slice(0, 50) || tab.url}`);
105
+ }
106
+ } catch { /* ignore */ }
107
+
108
+ // Step 5: auto-configure AI clients
109
+ console.log('');
110
+ console.log(` ${STEP_WAIT} Connecting AI clients...`);
111
+ const clients = detectClients();
112
+
113
+ if (clients.length === 0) {
114
+ console.log(` ${STEP_FAIL} No AI clients found`);
115
+ console.log('');
116
+ console.log(' Add Grasp manually to your AI client config:');
117
+ console.log(' { "mcpServers": { "grasp": { "command": "npx", "args": ["-y", "@yuzc-001/grasp"] } } }');
118
+ } else {
119
+ const results = await autoConfigureAll(clients);
120
+ for (const { label, result } of results) {
121
+ if (result === 'written') {
122
+ console.log(` ${STEP_OK} ${label} — config written`);
123
+ } else if (result === 'already-configured') {
124
+ console.log(` ${STEP_OK} ${label} — already configured`);
125
+ } else {
126
+ console.log(` ${STEP_FAIL} ${label} — failed, add manually`);
127
+ }
128
+ }
129
+
130
+ const needsRestart = results
131
+ .filter(r => r.result === 'written')
132
+ .map(r => r.label);
133
+
134
+ if (needsRestart.length > 0) {
135
+ console.log('');
136
+ console.log(` Restart ${needsRestart.join(', ')} to apply changes.`);
137
+ }
138
+ }
139
+
140
+ console.log('');
141
+ console.log(' ' + '─'.repeat(44));
142
+ console.log(' Runtime ready. First win:');
143
+ console.log(' 1. Tell your AI: "call get_status"');
144
+ console.log(' 2. Then: "use entry(url, intent) on a real page"');
145
+ console.log(' 3. Then: "inspect, then extract or continue"');
146
+ console.log(' 4. Then: "explain_route or grasp explain"');
147
+ console.log('');
148
+ }
@@ -0,0 +1,42 @@
1
+ import { readLatestRouteDecision } from '../server/audit.js';
2
+
3
+ export function formatRouteExplanation(route) {
4
+ if (!route) {
5
+ return [
6
+ '',
7
+ ' No route decision recorded yet.',
8
+ ' Run entry(url, intent) first, then ask Grasp to explain the route.',
9
+ '',
10
+ ];
11
+ }
12
+
13
+ const fallback = route.fallback_chain?.length
14
+ ? route.fallback_chain.join(' -> ')
15
+ : 'none';
16
+ const triggers = route.evidence?.triggers?.length
17
+ ? route.evidence.triggers.join(', ')
18
+ : 'none';
19
+
20
+ return [
21
+ '',
22
+ ' Grasp Route Explanation',
23
+ ` ${'─'.repeat(44)}`,
24
+ ` URL ${route.url ?? 'unknown'}`,
25
+ ` Intent ${route.intent ?? 'unknown'}`,
26
+ ` Status ${route.status ?? 'unknown'}`,
27
+ ` Mode ${route.selected_mode ?? 'unknown'}`,
28
+ ` Confidence ${route.confidence ?? 'unknown'}`,
29
+ ` Risk ${route.risk_level ?? 'unknown'}`,
30
+ ` Human ${route.requires_human ? 'yes' : 'no'}`,
31
+ ` Next ${route.next_step ?? 'unknown'}`,
32
+ ` Fallback ${fallback}`,
33
+ ` Failure ${route.failure_type ?? 'none'}`,
34
+ ` Because ${triggers}`,
35
+ '',
36
+ ];
37
+ }
38
+
39
+ export async function runExplain() {
40
+ const route = await readLatestRouteDecision();
41
+ formatRouteExplanation(route).forEach((line) => console.log(line));
42
+ }
@@ -0,0 +1,55 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ const LOG_PATH = join(homedir(), '.grasp', 'audit.log');
6
+
7
+ async function readLines(n) {
8
+ try {
9
+ const content = await readFile(LOG_PATH, 'utf8');
10
+ const lines = content.split('\n').filter(Boolean);
11
+ return lines.slice(-n);
12
+ } catch {
13
+ return [];
14
+ }
15
+ }
16
+
17
+ export async function runLogs(args) {
18
+ let lines = 50;
19
+ let follow = false;
20
+
21
+ for (let i = 0; i < args.length; i++) {
22
+ if ((args[i] === '--lines' || args[i] === '-n') && args[i + 1]) {
23
+ lines = parseInt(args[++i], 10) || 50;
24
+ } else if (args[i] === '--follow' || args[i] === '-f') {
25
+ follow = true;
26
+ }
27
+ }
28
+
29
+ const entries = await readLines(lines);
30
+ if (entries.length === 0) {
31
+ console.log('No log entries yet.');
32
+ if (!follow) return;
33
+ } else {
34
+ entries.forEach(l => console.log(l));
35
+ }
36
+
37
+ if (!follow) return;
38
+
39
+ // Follow mode: poll every 500ms, print new lines
40
+ console.log('--- following (Ctrl+C to stop) ---');
41
+ let lastCount = entries.length;
42
+
43
+ setInterval(async () => {
44
+ try {
45
+ const content = await readFile(LOG_PATH, 'utf8');
46
+ const all = content.split('\n').filter(Boolean);
47
+ if (all.length > lastCount) {
48
+ all.slice(lastCount).forEach(l => console.log(l));
49
+ lastCount = all.length;
50
+ }
51
+ } catch {
52
+ // Log file removed or inaccessible — keep waiting
53
+ }
54
+ }, 500);
55
+ }
@@ -0,0 +1,119 @@
1
+ import { readConfig } from './config.js';
2
+ import { detectChromePath, startChromeHint } from './detect-chrome.js';
3
+ import { readRuntimeTruth } from '../server/runtime-status.js';
4
+ import { readLatestRouteDecision, readLogs } from '../server/audit.js';
5
+ import { isSafeModeEnabled } from '../server/state.js';
6
+ import { readBrowserInstance } from '../runtime/browser-instance.js';
7
+
8
+ async function getActiveChromeTab(cdpUrl) {
9
+ try {
10
+ const res = await fetch(`${cdpUrl}/json`, { signal: AbortSignal.timeout(1500) });
11
+ const tabs = await res.json();
12
+ return tabs.find(t =>
13
+ t.type === 'page' &&
14
+ t.url &&
15
+ !t.url.startsWith('chrome://') &&
16
+ !t.url.startsWith('about:')
17
+ ) ?? null;
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ export function formatConnectionLabel(connected, runtimeTruth) {
24
+ if (connected) return 'ready';
25
+
26
+ // Backward compatibility for legacy runtime-status snapshots/tests.
27
+ if (runtimeTruth?.state === 'CDP_UNREACHABLE') return 'browser unreachable';
28
+ if (runtimeTruth?.state) return 'disconnected';
29
+
30
+ if (runtimeTruth?.cdp?.state === 'unreachable') return 'browser unreachable';
31
+ if (runtimeTruth?.server?.state && runtimeTruth.server.state !== 'idle') return 'disconnected';
32
+ return 'browser unreachable';
33
+ }
34
+
35
+ export function formatLatestRouteSummary(route) {
36
+ if (!route?.selected_mode) return null;
37
+ const nextStep = route.next_step ?? 'unknown';
38
+ const intent = route.intent ?? 'unknown';
39
+ return `${route.selected_mode} -> ${nextStep} (intent: ${intent})`;
40
+ }
41
+
42
+ export function formatInstanceLabel(instance) {
43
+ if (instance?.display === 'headless') return 'headless browser';
44
+ if (instance?.display === 'windowed') return 'windowed browser';
45
+ return 'unknown browser mode';
46
+ }
47
+
48
+ export async function runStatus() {
49
+ const config = await readConfig();
50
+ const cdpUrl = process.env.CHROME_CDP_URL || config.cdpUrl;
51
+ const runtimeTruth = await readRuntimeTruth();
52
+ const safeMode = isSafeModeEnabled();
53
+ const safeModeNote = safeMode === config.safeMode ? '' : ` (config: ${config.safeMode ? 'on' : 'off'})`;
54
+
55
+ const sep = '─'.repeat(44);
56
+ console.log('');
57
+ console.log(' Grasp Runtime Status');
58
+ console.log(` ${sep}`);
59
+
60
+ const instance = await readBrowserInstance(cdpUrl);
61
+ const connected = instance !== null;
62
+
63
+ const statusLabel = formatConnectionLabel(connected, runtimeTruth);
64
+ console.log(` Runtime ${statusLabel}`);
65
+ console.log(` Endpoint ${cdpUrl}`);
66
+ if (!connected && runtimeTruth?.server?.lastError) {
67
+ console.log(` Last error: ${runtimeTruth.server.lastError}`);
68
+ }
69
+ if (runtimeTruth?.updatedAt) {
70
+ const updatedAt = new Date(runtimeTruth.updatedAt).toLocaleString();
71
+ console.log(` Last seen: ${updatedAt}`);
72
+ }
73
+ console.log(` Browser ${connected ? 'running ' + (instance.browser ?? 'unknown') : 'not reachable'}`);
74
+ if (connected) {
75
+ console.log(` Instance ${formatInstanceLabel(instance)}`);
76
+ if (instance.warning) {
77
+ console.log(` Warning: ${instance.warning}`);
78
+ }
79
+ }
80
+ console.log(' Profile chrome-grasp');
81
+ console.log(` Safe mode ${safeMode ? 'on' : 'off'}${safeModeNote}`);
82
+ const latestRoute = await readLatestRouteDecision();
83
+ const latestRouteSummary = formatLatestRouteSummary(latestRoute);
84
+ if (latestRouteSummary) {
85
+ console.log(` Last route ${latestRouteSummary}`);
86
+ }
87
+
88
+ if (connected) {
89
+ const tab = await getActiveChromeTab(cdpUrl);
90
+ if (tab) {
91
+ const title = tab.title?.slice(0, 50) || '(no title)';
92
+ const url = tab.url?.slice(0, 70) || '';
93
+ console.log(` Current page ${title}`);
94
+ console.log(` ${url}`);
95
+ }
96
+ } else {
97
+ const chromePath = detectChromePath();
98
+ console.log('');
99
+ if (chromePath) {
100
+ console.log(' Chrome found at:');
101
+ console.log(` ${chromePath}`);
102
+ console.log('');
103
+ console.log(' Bring the runtime back:');
104
+ console.log(` ${startChromeHint(cdpUrl)}`);
105
+ } else {
106
+ console.log(' Chrome not found. Install Google Chrome, then run:');
107
+ console.log(` ${startChromeHint(cdpUrl)}`);
108
+ }
109
+ }
110
+
111
+ const logs = await readLogs(3);
112
+ if (logs.length > 0) {
113
+ console.log('');
114
+ console.log(' Recent activity');
115
+ logs.forEach((l) => console.log(` ${l}`));
116
+ }
117
+
118
+ console.log('');
119
+ }
@@ -0,0 +1,27 @@
1
+ import { readFile, writeFile, mkdir } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ export const CONFIG_DIR = join(homedir(), '.grasp');
6
+ export const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
7
+
8
+ const DEFAULTS = {
9
+ cdpUrl: 'http://localhost:9222',
10
+ safeMode: true,
11
+ };
12
+
13
+ export async function readConfig() {
14
+ try {
15
+ const raw = await readFile(CONFIG_PATH, 'utf8');
16
+ return { ...DEFAULTS, ...JSON.parse(raw) };
17
+ } catch {
18
+ return { ...DEFAULTS };
19
+ }
20
+ }
21
+
22
+ export async function writeConfig(updates) {
23
+ await mkdir(CONFIG_DIR, { recursive: true });
24
+ const current = await readConfig();
25
+ const merged = { ...current, ...updates };
26
+ await writeFile(CONFIG_PATH, JSON.stringify(merged, null, 2) + '\n', 'utf8');
27
+ }
@@ -0,0 +1,58 @@
1
+ import { existsSync } from 'fs';
2
+ import { platform } from 'process';
3
+ import { join } from 'path';
4
+
5
+ const WIN_PATHS = [
6
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
7
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
8
+ join(process.env.LOCALAPPDATA ?? '', 'Google\\Chrome\\Application\\chrome.exe'),
9
+ join(process.env.LOCALAPPDATA ?? '', 'Google\\Chrome Beta\\Application\\chrome.exe'),
10
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
11
+ 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
12
+ ];
13
+
14
+ const MAC_PATHS = [
15
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
16
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
17
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
18
+ ];
19
+
20
+ const LINUX_PATHS = [
21
+ '/usr/bin/google-chrome',
22
+ '/usr/bin/google-chrome-stable',
23
+ '/usr/bin/chromium',
24
+ '/usr/bin/chromium-browser',
25
+ '/snap/bin/chromium',
26
+ ];
27
+
28
+ /**
29
+ * Returns the path to the first found Chrome/Chromium executable, or null.
30
+ */
31
+ export function detectChromePath() {
32
+ const paths = platform === 'win32' ? WIN_PATHS
33
+ : platform === 'darwin' ? MAC_PATHS
34
+ : LINUX_PATHS;
35
+ return paths.find(p => p && existsSync(p)) ?? null;
36
+ }
37
+
38
+ /**
39
+ * Returns a platform-appropriate command to start Chrome with CDP enabled.
40
+ */
41
+ export function startChromeHint(cdpUrl = 'http://localhost:9222') {
42
+ const port = new URL(cdpUrl).port || '9222';
43
+ const chromePath = detectChromePath();
44
+
45
+ if (platform === 'win32') {
46
+ return chromePath
47
+ ? `"${chromePath}" --remote-debugging-port=${port} --user-data-dir="%USERPROFILE%\\chrome-grasp"`
48
+ : `start-chrome.bat`;
49
+ }
50
+ if (platform === 'darwin') {
51
+ return chromePath
52
+ ? `"${chromePath}" --remote-debugging-port=${port} --user-data-dir=/tmp/chrome-grasp`
53
+ : `open -a "Google Chrome" --args --remote-debugging-port=${port}`;
54
+ }
55
+ return chromePath
56
+ ? `"${chromePath}" --remote-debugging-port=${port} --user-data-dir=/tmp/chrome-grasp`
57
+ : `google-chrome --remote-debugging-port=${port}`;
58
+ }
@@ -0,0 +1,67 @@
1
+ import { createHandoffState, mergeHandoffState } from './state.js';
2
+
3
+ export function requestHandoff(current, reason, note = null, anchors = {}) {
4
+ return mergeHandoffState(current ?? createHandoffState(), {
5
+ state: 'handoff_required',
6
+ reason,
7
+ note,
8
+ requestedAt: Date.now(),
9
+ evidence: null,
10
+ expected_url_contains: anchors.expected_url_contains ?? null,
11
+ expected_page_role: anchors.expected_page_role ?? null,
12
+ expected_selector: anchors.expected_selector ?? null,
13
+ continuation_goal: anchors.continuation_goal ?? null,
14
+ expected_hint_label: anchors.expected_hint_label ?? null,
15
+ });
16
+ }
17
+
18
+ export function markHandoffInProgress(current, note = null) {
19
+ return mergeHandoffState(current ?? createHandoffState(), {
20
+ state: 'handoff_in_progress',
21
+ note,
22
+ });
23
+ }
24
+
25
+ export function markAwaitingReacquisition(current, note = null) {
26
+ return mergeHandoffState(current ?? createHandoffState(), {
27
+ state: 'awaiting_reacquisition',
28
+ note,
29
+ });
30
+ }
31
+
32
+ export function markResumedUnverified(current, evidence = null, note = null) {
33
+ return mergeHandoffState(current ?? createHandoffState(), {
34
+ state: 'resumed_unverified',
35
+ evidence,
36
+ note,
37
+ });
38
+ }
39
+
40
+ export function markResumeVerified(current, evidence = null, note = null) {
41
+ return mergeHandoffState(current ?? createHandoffState(), {
42
+ state: 'resumed_verified',
43
+ evidence,
44
+ note,
45
+ verifiedAt: Date.now(),
46
+ });
47
+ }
48
+
49
+ export function clearHandoff(current) {
50
+ return mergeHandoffState(current ?? createHandoffState(), {
51
+ state: 'idle',
52
+ reason: null,
53
+ note: null,
54
+ requestedAt: null,
55
+ verifiedAt: null,
56
+ evidence: null,
57
+ expected_url_contains: null,
58
+ expected_page_role: null,
59
+ expected_selector: null,
60
+ continuation_goal: null,
61
+ expected_hint_label: null,
62
+ taskId: null,
63
+ siteKey: null,
64
+ sessionKey: null,
65
+ lastUrl: null,
66
+ });
67
+ }
@@ -0,0 +1,48 @@
1
+ import { mkdir, readFile, writeFile } from 'fs/promises';
2
+ import { dirname, join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { createHandoffState, mergeHandoffState } from './state.js';
5
+
6
+ export const HANDOFF_STATE_PATH =
7
+ process.env.GRASP_HANDOFF_STATE_PATH ??
8
+ join(homedir(), '.grasp', 'handoff-state.json');
9
+
10
+ export function attachHandoffTaskMetadata(handoff = {}, source = {}) {
11
+ return {
12
+ ...handoff,
13
+ taskId: source.taskId ?? handoff.taskId ?? null,
14
+ siteKey: source.siteKey ?? handoff.siteKey ?? null,
15
+ sessionKey: source.sessionKey ?? handoff.sessionKey ?? null,
16
+ lastUrl: source.lastUrl ?? handoff.lastUrl ?? null,
17
+ };
18
+ }
19
+
20
+ async function ensureDir() {
21
+ await mkdir(dirname(HANDOFF_STATE_PATH), { recursive: true });
22
+ }
23
+
24
+ export async function writeHandoffState(snapshot) {
25
+ try {
26
+ await ensureDir();
27
+ const state = attachHandoffTaskMetadata(
28
+ mergeHandoffState(createHandoffState(), snapshot),
29
+ snapshot
30
+ );
31
+ await writeFile(HANDOFF_STATE_PATH, JSON.stringify(state, null, 2) + '\n', 'utf8');
32
+ } catch {
33
+ // best effort
34
+ }
35
+ }
36
+
37
+ export async function readHandoffState() {
38
+ try {
39
+ const raw = await readFile(HANDOFF_STATE_PATH, 'utf8');
40
+ const parsed = JSON.parse(raw);
41
+ return attachHandoffTaskMetadata(
42
+ mergeHandoffState(createHandoffState(), parsed),
43
+ parsed
44
+ );
45
+ } catch {
46
+ return createHandoffState();
47
+ }
48
+ }