octocode-cli 1.3.1 → 1.5.1
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/README.md +193 -34
- package/out/chunks/chunk-7476PETK.js +309 -0
- package/out/chunks/chunk-CVNNNSMQ.js +26 -0
- package/out/chunks/chunk-OQBJTZWK.js +60 -0
- package/out/chunks/chunk-UCZCF3BQ.js +9 -0
- package/out/chunks/command-help-specs-JZXVSLZ5.js +8 -0
- package/out/chunks/commands-XBFPLHSQ.js +8 -0
- package/out/chunks/help-P7TCOYAJ.js +10 -0
- package/out/chunks/main-help-ULF5PAQY.js +10 -0
- package/out/chunks/prompts-5E6VKRX5.js +8 -0
- package/out/chunks/spinner-URV2OX6O.js +8 -0
- package/out/chunks/tool-command-M6VA7P2F.js +8 -0
- package/out/octocode-cli.js +1 -1
- package/package.json +5 -3
- package/skills/README.md +13 -1
- package/skills/agentic-flow-best-practices/SKILL.md +280 -0
- package/skills/agentic-flow-best-practices/references/agent-collaboration-patterns.md +75 -0
- package/skills/agentic-flow-best-practices/references/pr-review-agent-example.md +47 -0
- package/skills/agentic-flow-best-practices/references/resources.md +112 -0
- package/skills/octocode-chrome-devtools/README.md +541 -0
- package/skills/octocode-chrome-devtools/SKILL.md +197 -0
- package/skills/octocode-chrome-devtools/agents/openai.yaml +7 -0
- package/skills/octocode-chrome-devtools/references/CDP_AGENT_REFERENCE.md +401 -0
- package/skills/octocode-chrome-devtools/references/CHROME_FLAGS.md +234 -0
- package/skills/octocode-chrome-devtools/references/INTENTS.md +108 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_AUTH.md +179 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_AUTOMATION.md +214 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_DEBUG.md +329 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_ENVIRONMENT.md +237 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_INSPECT.md +263 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_STORAGE_CONSENT.md +214 -0
- package/skills/octocode-chrome-devtools/references/RECOVERY.md +39 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS.md +43 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_ASYNC_WORKERS.md +345 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_BROWSER.md +403 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_OBSERVE.md +275 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_SPECIAL.md +18 -0
- package/skills/octocode-chrome-devtools/scripts/cdp-runner.mjs +503 -0
- package/skills/octocode-chrome-devtools/scripts/cdp-sandbox.mjs +123 -0
- package/skills/octocode-chrome-devtools/scripts/cdp-template.mjs +81 -0
- package/skills/octocode-chrome-devtools/scripts/octocode-chrome-devtools.vpn.example.json +8 -0
- package/skills/octocode-chrome-devtools/scripts/open-browser.mjs +362 -0
- package/skills/octocode-chrome-devtools/scripts/sourcemap-resolver.mjs +193 -0
- package/skills/octocode-chrome-devtools/scripts/undercover.mjs +226 -0
- package/skills/octocode-design/README.md +2 -2
- package/skills/octocode-documentation-writer/README.md +1 -1
- package/skills/octocode-engineer/README.md +1 -1
- package/skills/octocode-engineer/SKILL.md +4 -2
- package/skills/octocode-engineer/references/output-files.md +3 -3
- package/skills/octocode-engineer/scripts/run.js +136 -136
- package/skills/octocode-engineer/src/reporting/summary-md.test.ts +48 -0
- package/skills/octocode-engineer/src/reporting/summary-md.ts +43 -2
- package/skills/octocode-install/SKILL.md +1 -1
- package/skills/octocode-pull-request-reviewer/README.md +5 -5
- package/skills/octocode-pull-request-reviewer/SKILL.md +1 -1
- package/skills/octocode-research/AGENTS.md +1 -1
- package/skills/octocode-research/README.md +2 -2
- package/skills/octocode-research/docs/ARCHITECTURE.md +1 -1
- package/skills/octocode-research/scripts/server.js +191 -246
- package/skills/octocode-research/src/routes/github.ts +4 -21
- package/skills/octocode-research/src/routes/local.ts +4 -21
- package/skills/octocode-research/src/utils/fileContentTransform.ts +44 -0
- package/skills/octocode-search-skill/SKILL.md +245 -229
- package/skills/octocode-search-skill/references/agent-skills-guide.md +177 -0
- package/skills/octocode-search-skill/references/discovery-surfaces.md +162 -0
- package/skills/octocode-search-skill/references/fetch-and-create-locally.md +57 -0
- package/skills/octocode-search-skill/references/install-reference.md +130 -0
- package/skills/octocode-search-skill/references/references-template.md +27 -0
- package/skills/octocode-search-skill/references/references.md +62 -0
- package/skills/octocode-slides/README.md +307 -0
- package/skills/octocode-slides/SKILL.md +410 -0
- package/skills/octocode-slides/references/01-brief.md +156 -0
- package/skills/octocode-slides/references/02-research.md +149 -0
- package/skills/octocode-slides/references/03-outline.md +172 -0
- package/skills/octocode-slides/references/04-design.md +301 -0
- package/skills/octocode-slides/references/05-implementation.md +213 -0
- package/skills/octocode-slides/references/06-review.md +258 -0
- package/skills/octocode-slides/references/animation.md +281 -0
- package/skills/octocode-slides/references/design-system.md +316 -0
- package/skills/octocode-slides/references/html-templates.md +673 -0
- package/skills/octocode-slides/references/image-generation.md +448 -0
- package/skills/octocode-slides/references/resources.md +840 -0
- package/skills/octocode-slides/references/slide-rules.md +541 -0
- package/skills/octocode-slides/references/wireframes.md +727 -0
- package/skills/octocode-slides/scripts/animation.js +182 -0
- package/skills/octocode-slides/scripts/base.css +353 -0
- package/skills/octocode-slides/scripts/base.html +655 -0
- package/skills/octocode-slides/scripts/generate_image.py +221 -0
- package/skills/octocode-slides/scripts/navbridge.js +79 -0
- package/skills/octocode-slides/scripts/presenter.js +316 -0
- package/skills/octocode-slides/scripts/slide.html +248 -0
- package/skills/octocode-stats/SKILL.md +73 -0
- package/skills/octocode-stats/assets/template.html +1332 -0
- package/skills/octocode-stats/scripts/build_dashboard.mjs +407 -0
- package/assets/example.png +0 -0
- package/out/chunks/chunk-QCY7Q7YW.js +0 -389
- package/out/chunks/command-help-specs-CQ3RBLP6.js +0 -8
- package/out/chunks/commands-OCTZP2TO.js +0 -51
- package/out/chunks/help-XPXP46ZT.js +0 -10
- package/out/chunks/main-help-35HX2UDQ.js +0 -10
- package/out/chunks/tool-command-HOSMVLNK.js +0 -8
- package/skills/octocode-search-skill/INSTALL_REFERENCE.md +0 -112
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# CDP Special Pattern Pointers
|
|
2
|
+
|
|
3
|
+
## Storage Audit
|
|
4
|
+
|
|
5
|
+
> **Detailed recipe in `INTENTS_STORAGE_CONSENT.md` -> `## storage`** — grep: `rg -n "^## storage" references/INTENTS_STORAGE_CONSENT.md`
|
|
6
|
+
|
|
7
|
+
Inventories cookies, localStorage, sessionStorage, IndexedDB, Cache Storage, Service Workers, and quota.
|
|
8
|
+
Also detects **cookie resurrection** (tracking IDs mirrored across storage to survive clearing).
|
|
9
|
+
|
|
10
|
+
## Consent Audit
|
|
11
|
+
|
|
12
|
+
> **Detailed recipe in `INTENTS_STORAGE_CONSENT.md` -> `## consent`** — grep: `rg -n "^## consent" references/INTENTS_STORAGE_CONSENT.md`
|
|
13
|
+
|
|
14
|
+
Detects CMP presence, pre-granted consent state, and tracker firing before user consent.
|
|
15
|
+
|
|
16
|
+
## Full Audit (combine all)
|
|
17
|
+
|
|
18
|
+
Combine the Network Console, Performance Audit, DOM Accessibility, and Security Audit `run()` functions above into a single script — enable all required domains at the top, attach all event listeners before navigating, run all sync checks after the page settles.
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Run a generated `run(cdp)` script against a Chrome CDP target.
|
|
3
|
+
|
|
4
|
+
import { resolve, join } from 'path';
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
6
|
+
import { pathToFileURL } from 'url';
|
|
7
|
+
import { tmpdir } from 'os';
|
|
8
|
+
|
|
9
|
+
const argv = process.argv.slice(2);
|
|
10
|
+
const scriptArg = argv.find(a => !a.startsWith('--') && (a.endsWith('.mjs') || a.endsWith('.js')));
|
|
11
|
+
const getArg = (flag, def) => { const i = argv.indexOf(flag); return i !== -1 && argv[i + 1] ? argv[i + 1] : def; };
|
|
12
|
+
const hasFlag = (flag) => argv.includes(flag);
|
|
13
|
+
|
|
14
|
+
const PORT = getArg('--port', '9222');
|
|
15
|
+
const NEW_TAB = getArg('--new-tab', '');
|
|
16
|
+
const TARGET_ID = getArg('--target', '');
|
|
17
|
+
const TARGET_URL = getArg('--target-url', '');
|
|
18
|
+
const TARGET_TYPE = getArg('--target-type', '');
|
|
19
|
+
const TIMEOUT = parseInt(getArg('--timeout', '60000'), 10);
|
|
20
|
+
const KEEP_TAB = hasFlag('--keep-tab');
|
|
21
|
+
const LIST_TARGETS = hasFlag('--list-targets');
|
|
22
|
+
|
|
23
|
+
if (!scriptArg && !LIST_TARGETS) {
|
|
24
|
+
console.error('[CDP_RUNNER] Usage: node cdp-runner.mjs <script.mjs> [--port 9222] [--new-tab <url>] [--target <id>] [--target-url <pattern>] [--target-type <type>] [--list-targets] [--keep-tab]');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const [nodeMajor] = process.versions.node.split('.').map(Number);
|
|
29
|
+
if (nodeMajor < 22) {
|
|
30
|
+
console.error(`[CDP_RUNNER] Node.js 22+ required (you have ${process.versions.node}). Native WebSocket is unavailable.`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const WS = globalThis.WebSocket;
|
|
34
|
+
|
|
35
|
+
function readJson(filePath, fallback = null) {
|
|
36
|
+
try { return JSON.parse(readFileSync(filePath, 'utf8')); } catch { return fallback; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeJson(filePath, value) {
|
|
40
|
+
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function cdpHttp(path, method = 'GET') {
|
|
44
|
+
const res = await fetch(`http://localhost:${PORT}${path}`, { method, signal: AbortSignal.timeout(5000) });
|
|
45
|
+
if (!res.ok) throw new Error(`CDP HTTP ${res.status} for ${path}`);
|
|
46
|
+
return res.json();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function getVersion() { return cdpHttp('/json/version'); }
|
|
50
|
+
async function getTargets() { return cdpHttp('/json'); }
|
|
51
|
+
async function openTab(url) { return cdpHttp(`/json/new?${encodeURIComponent(url)}`, 'PUT'); }
|
|
52
|
+
async function activateTarget(id) { return cdpHttp(`/json/activate/${id}`); }
|
|
53
|
+
async function closeTab(id) {
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch(`http://localhost:${PORT}/json/close/${id}`, { signal: AbortSignal.timeout(3000) });
|
|
56
|
+
return res.ok;
|
|
57
|
+
} catch { return false; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createSession(wsUrl, targetInfo) {
|
|
61
|
+
return new Promise((resolveSession, rejectSession) => {
|
|
62
|
+
const ws = new WS(wsUrl);
|
|
63
|
+
let msgId = 1;
|
|
64
|
+
const pending = new Map();
|
|
65
|
+
const handlers = new Map();
|
|
66
|
+
let closed = false;
|
|
67
|
+
|
|
68
|
+
function drainPending(reason) {
|
|
69
|
+
if (pending.size === 0) return;
|
|
70
|
+
const err = new Error(reason);
|
|
71
|
+
pending.forEach(({ rej, timer }) => { clearTimeout(timer); rej(err); });
|
|
72
|
+
pending.clear();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
ws.onopen = () => {
|
|
76
|
+
const session = {
|
|
77
|
+
targetInfo,
|
|
78
|
+
|
|
79
|
+
send(method, params = {}, sessionId = undefined) {
|
|
80
|
+
if (closed) return Promise.reject(new Error('Session already closed'));
|
|
81
|
+
return new Promise((res, rej) => {
|
|
82
|
+
const id = msgId++;
|
|
83
|
+
const timer = setTimeout(() => {
|
|
84
|
+
pending.delete(id);
|
|
85
|
+
rej(new Error(`CDP timeout (${TIMEOUT}ms) for: ${method}`));
|
|
86
|
+
}, TIMEOUT);
|
|
87
|
+
pending.set(id, { res, rej, timer });
|
|
88
|
+
const payload = { id, method, params };
|
|
89
|
+
if (sessionId) payload.sessionId = sessionId;
|
|
90
|
+
ws.send(JSON.stringify(payload));
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
on(event, handler) {
|
|
95
|
+
if (!handlers.has(event)) handlers.set(event, new Set());
|
|
96
|
+
handlers.get(event).add(handler);
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
off(event, handler) {
|
|
100
|
+
handlers.get(event)?.delete(handler);
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
log(...args) {
|
|
104
|
+
console.log('[BROWSER]', ...args);
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
outputDir: '',
|
|
108
|
+
|
|
109
|
+
close() {
|
|
110
|
+
if (closed) return;
|
|
111
|
+
closed = true;
|
|
112
|
+
drainPending('Session closed');
|
|
113
|
+
handlers.clear();
|
|
114
|
+
try { ws.close(); } catch {}
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
resolveSession(session);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
ws.onmessage = (evt) => {
|
|
122
|
+
let msg;
|
|
123
|
+
try { msg = JSON.parse(typeof evt === 'string' ? evt : evt.data); } catch { return; }
|
|
124
|
+
|
|
125
|
+
if (msg.id !== undefined && pending.has(msg.id)) {
|
|
126
|
+
const { res, rej, timer } = pending.get(msg.id);
|
|
127
|
+
pending.delete(msg.id);
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
if (msg.error) rej(new Error(`CDP error [${msg.error.code}]: ${msg.error.message}`));
|
|
130
|
+
else res(msg.result ?? {});
|
|
131
|
+
} else if (msg.method) {
|
|
132
|
+
const meta = msg.sessionId ? { sessionId: msg.sessionId } : {};
|
|
133
|
+
handlers.get(msg.method)?.forEach(h => {
|
|
134
|
+
try { h(msg.params ?? {}, meta); } catch (e) { console.error('[CDP_RUNNER] Handler error:', e.message); }
|
|
135
|
+
});
|
|
136
|
+
handlers.get('*')?.forEach(h => {
|
|
137
|
+
try { h(msg.method, msg.params ?? {}, meta); } catch {}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
ws.onerror = (e) => {
|
|
143
|
+
const msg = e?.message ?? String(e);
|
|
144
|
+
drainPending(`WebSocket error: ${msg}`);
|
|
145
|
+
if (!closed) rejectSession(new Error(`WebSocket error: ${msg}`));
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
ws.onclose = () => {
|
|
149
|
+
drainPending('WebSocket closed unexpectedly');
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let _cleanup = null;
|
|
155
|
+
function registerCleanup(fn) { _cleanup = fn; }
|
|
156
|
+
|
|
157
|
+
async function shutdown(signal) {
|
|
158
|
+
console.error(`[CDP_RUNNER] ${signal} received - cleaning up...`);
|
|
159
|
+
if (_cleanup) {
|
|
160
|
+
try { await _cleanup(); } catch {}
|
|
161
|
+
}
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
166
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
167
|
+
|
|
168
|
+
async function main() {
|
|
169
|
+
let version;
|
|
170
|
+
try {
|
|
171
|
+
version = await getVersion();
|
|
172
|
+
} catch {
|
|
173
|
+
console.error(`[CDP_RUNNER] Chrome not responding on port ${PORT}. Run open-browser.mjs first.`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
console.error(`[CDP_RUNNER] Chrome: ${version.Browser}`);
|
|
177
|
+
const sessionMetaDir = process.env.CDP_SESSION_META_DIR ?? (() => {
|
|
178
|
+
const dir = join(tmpdir(), '.octocode-chrome-devtools', 'session-meta', `port-${PORT}`);
|
|
179
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
180
|
+
return dir;
|
|
181
|
+
})();
|
|
182
|
+
mkdirSync(sessionMetaDir, { recursive: true, mode: 0o700 });
|
|
183
|
+
const sessionMetaFile = join(sessionMetaDir, 'session-metadata.json');
|
|
184
|
+
const targetSnapshotFile = join(sessionMetaDir, 'targets-latest.json');
|
|
185
|
+
|
|
186
|
+
if (LIST_TARGETS) {
|
|
187
|
+
const nowIso = new Date().toISOString();
|
|
188
|
+
const targets = await getTargets();
|
|
189
|
+
writeJson(targetSnapshotFile, {
|
|
190
|
+
capturedAt: nowIso,
|
|
191
|
+
port: PORT,
|
|
192
|
+
targets: targets.map(t => ({
|
|
193
|
+
id: t.id ?? null,
|
|
194
|
+
type: t.type ?? null,
|
|
195
|
+
url: t.url ?? null,
|
|
196
|
+
title: t.title ?? null,
|
|
197
|
+
})),
|
|
198
|
+
});
|
|
199
|
+
const existingMeta = readJson(sessionMetaFile, {}) ?? {};
|
|
200
|
+
writeJson(sessionMetaFile, {
|
|
201
|
+
...existingMeta,
|
|
202
|
+
port: PORT,
|
|
203
|
+
browser: version.Browser,
|
|
204
|
+
lastListedTargetsAt: nowIso,
|
|
205
|
+
updatedAt: nowIso,
|
|
206
|
+
});
|
|
207
|
+
console.log(JSON.stringify(targets.map(t => ({
|
|
208
|
+
id: t.id, type: t.type, url: t.url, title: t.title,
|
|
209
|
+
})), null, 2));
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let targetWsUrl, targetInfo, openedTabId;
|
|
214
|
+
|
|
215
|
+
if (NEW_TAB) {
|
|
216
|
+
const tab = await openTab(NEW_TAB);
|
|
217
|
+
openedTabId = tab.id;
|
|
218
|
+
targetWsUrl = tab.webSocketDebuggerUrl;
|
|
219
|
+
targetInfo = { id: tab.id, url: tab.url, title: tab.title, type: tab.type };
|
|
220
|
+
console.error(`[CDP_RUNNER] Opened new tab (${tab.id}) -> ${NEW_TAB}`);
|
|
221
|
+
await new Promise(r => setTimeout(r, 800));
|
|
222
|
+
|
|
223
|
+
} else if (TARGET_ID) {
|
|
224
|
+
const targets = await getTargets();
|
|
225
|
+
const t = targets.find(x => x.id === TARGET_ID);
|
|
226
|
+
if (!t) { console.error(`[CDP_RUNNER] Target ${TARGET_ID} not found`); process.exit(1); }
|
|
227
|
+
targetWsUrl = t.webSocketDebuggerUrl;
|
|
228
|
+
targetInfo = t;
|
|
229
|
+
await activateTarget(TARGET_ID).catch(() => {});
|
|
230
|
+
|
|
231
|
+
} else if (TARGET_URL) {
|
|
232
|
+
const targets = await getTargets();
|
|
233
|
+
const pool = TARGET_TYPE ? targets.filter(t => t.type === TARGET_TYPE) : targets;
|
|
234
|
+
const t = pool.find(x => x.url && x.url.includes(TARGET_URL));
|
|
235
|
+
if (!t) {
|
|
236
|
+
const available = targets.map(x => ` [${x.type}] ${x.url}`).join('\n');
|
|
237
|
+
console.error(`[CDP_RUNNER] No target URL matching "${TARGET_URL}". Available targets:\n${available}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
targetWsUrl = t.webSocketDebuggerUrl;
|
|
241
|
+
targetInfo = t;
|
|
242
|
+
console.error(`[CDP_RUNNER] Matched target [${t.type}]: ${t.url}`);
|
|
243
|
+
|
|
244
|
+
} else if (TARGET_TYPE) {
|
|
245
|
+
const targets = await getTargets();
|
|
246
|
+
const t = targets.find(x => x.type === TARGET_TYPE);
|
|
247
|
+
if (!t) {
|
|
248
|
+
const available = [...new Set(targets.map(x => x.type))].join(', ');
|
|
249
|
+
console.error(`[CDP_RUNNER] No target of type "${TARGET_TYPE}". Available types: ${available}`);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
targetWsUrl = t.webSocketDebuggerUrl;
|
|
253
|
+
targetInfo = t;
|
|
254
|
+
console.error(`[CDP_RUNNER] Matched target [${t.type}]: ${t.url}`);
|
|
255
|
+
|
|
256
|
+
} else {
|
|
257
|
+
const targets = await getTargets();
|
|
258
|
+
const pages = targets.filter(t => t.type === 'page');
|
|
259
|
+
if (pages.length === 0) {
|
|
260
|
+
console.error('[CDP_RUNNER] No page targets. Open a tab in Chrome first, or use --new-tab <url>');
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
const t = pages[0];
|
|
264
|
+
targetWsUrl = t.webSocketDebuggerUrl;
|
|
265
|
+
targetInfo = t;
|
|
266
|
+
console.error(`[CDP_RUNNER] Using tab: ${t.url}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!targetWsUrl) {
|
|
270
|
+
console.error('[CDP_RUNNER] Could not get WebSocket URL for target');
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const cdp = await createSession(targetWsUrl, targetInfo);
|
|
275
|
+
|
|
276
|
+
const outputDir = process.env.CDP_OUTPUT_DIR ?? (() => {
|
|
277
|
+
const ts = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
|
|
278
|
+
const dir = join(tmpdir(), '.octocode-chrome-devtools', ts);
|
|
279
|
+
mkdirSync(dir, { recursive: true });
|
|
280
|
+
return dir;
|
|
281
|
+
})();
|
|
282
|
+
const runLogFile = join(sessionMetaDir, 'run-history.json');
|
|
283
|
+
const existingMeta = readJson(sessionMetaFile, {}) ?? {};
|
|
284
|
+
const nowIso = new Date().toISOString();
|
|
285
|
+
const baseMeta = {
|
|
286
|
+
...existingMeta,
|
|
287
|
+
port: PORT,
|
|
288
|
+
browser: version.Browser,
|
|
289
|
+
lastConnectedAt: nowIso,
|
|
290
|
+
outputDir,
|
|
291
|
+
lastScript: scriptArg,
|
|
292
|
+
currentTarget: {
|
|
293
|
+
id: targetInfo.id ?? null,
|
|
294
|
+
type: targetInfo.type ?? null,
|
|
295
|
+
url: targetInfo.url ?? null,
|
|
296
|
+
title: targetInfo.title ?? null,
|
|
297
|
+
via: NEW_TAB ? 'new-tab' : TARGET_ID ? 'target' : TARGET_URL ? 'target-url' : TARGET_TYPE ? 'target-type' : 'first-page',
|
|
298
|
+
},
|
|
299
|
+
lastSelection: {
|
|
300
|
+
newTab: NEW_TAB || null,
|
|
301
|
+
targetId: TARGET_ID || null,
|
|
302
|
+
targetUrl: TARGET_URL || null,
|
|
303
|
+
targetType: TARGET_TYPE || null,
|
|
304
|
+
keepTab: KEEP_TAB,
|
|
305
|
+
},
|
|
306
|
+
updatedAt: nowIso,
|
|
307
|
+
};
|
|
308
|
+
writeJson(sessionMetaFile, baseMeta);
|
|
309
|
+
|
|
310
|
+
const currentTargets = await getTargets().catch(() => []);
|
|
311
|
+
writeJson(targetSnapshotFile, {
|
|
312
|
+
capturedAt: nowIso,
|
|
313
|
+
port: PORT,
|
|
314
|
+
targets: currentTargets.map(t => ({
|
|
315
|
+
id: t.id ?? null,
|
|
316
|
+
type: t.type ?? null,
|
|
317
|
+
url: t.url ?? null,
|
|
318
|
+
title: t.title ?? null,
|
|
319
|
+
attached: t.id === targetInfo.id,
|
|
320
|
+
})),
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const runHistory = readJson(runLogFile, { runs: [] }) ?? { runs: [] };
|
|
324
|
+
if (!Array.isArray(runHistory.runs)) runHistory.runs = [];
|
|
325
|
+
const runId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
326
|
+
runHistory.runs.push({
|
|
327
|
+
id: runId,
|
|
328
|
+
startedAt: nowIso,
|
|
329
|
+
script: scriptArg,
|
|
330
|
+
outputDir,
|
|
331
|
+
target: baseMeta.currentTarget,
|
|
332
|
+
status: 'running',
|
|
333
|
+
});
|
|
334
|
+
if (runHistory.runs.length > 100) runHistory.runs = runHistory.runs.slice(-100);
|
|
335
|
+
writeJson(runLogFile, runHistory);
|
|
336
|
+
const finalizeRun = (status, extra = {}) => {
|
|
337
|
+
const current = readJson(runLogFile, { runs: [] }) ?? { runs: [] };
|
|
338
|
+
if (!Array.isArray(current.runs)) current.runs = [];
|
|
339
|
+
const idx = current.runs.findIndex(r => r.id === runId);
|
|
340
|
+
if (idx !== -1) {
|
|
341
|
+
current.runs[idx] = {
|
|
342
|
+
...current.runs[idx],
|
|
343
|
+
status,
|
|
344
|
+
finishedAt: new Date().toISOString(),
|
|
345
|
+
...extra,
|
|
346
|
+
};
|
|
347
|
+
writeJson(runLogFile, current);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
cdp.outputDir = outputDir;
|
|
352
|
+
cdp.sessionMetaDir = sessionMetaDir;
|
|
353
|
+
cdp.sessionMetaFile = sessionMetaFile;
|
|
354
|
+
cdp.targetSnapshotFile = targetSnapshotFile;
|
|
355
|
+
cdp.resourcesFile = join(sessionMetaDir, 'resource-map.json');
|
|
356
|
+
cdp.reasoningFile = join(sessionMetaDir, 'reasoning-log.json');
|
|
357
|
+
cdp.addReasoningStep = (step) => {
|
|
358
|
+
const payload = readJson(cdp.reasoningFile, { steps: [] }) ?? { steps: [] };
|
|
359
|
+
if (!Array.isArray(payload.steps)) payload.steps = [];
|
|
360
|
+
payload.steps.push({
|
|
361
|
+
at: new Date().toISOString(),
|
|
362
|
+
...step,
|
|
363
|
+
});
|
|
364
|
+
if (payload.steps.length > 300) payload.steps = payload.steps.slice(-300);
|
|
365
|
+
writeJson(cdp.reasoningFile, payload);
|
|
366
|
+
return payload.steps.length;
|
|
367
|
+
};
|
|
368
|
+
cdp.upsertResourceMap = (resourceKey, details) => {
|
|
369
|
+
const payload = readJson(cdp.resourcesFile, { updatedAt: null, resources: {} }) ?? { updatedAt: null, resources: {} };
|
|
370
|
+
if (!payload.resources || typeof payload.resources !== 'object') payload.resources = {};
|
|
371
|
+
payload.resources[resourceKey] = {
|
|
372
|
+
...(payload.resources[resourceKey] ?? {}),
|
|
373
|
+
...details,
|
|
374
|
+
updatedAt: new Date().toISOString(),
|
|
375
|
+
};
|
|
376
|
+
payload.updatedAt = new Date().toISOString();
|
|
377
|
+
writeJson(cdp.resourcesFile, payload);
|
|
378
|
+
return payload.resources[resourceKey];
|
|
379
|
+
};
|
|
380
|
+
cdp.readSessionMetadata = () => readJson(cdp.sessionMetaFile, {});
|
|
381
|
+
cdp.writeSessionMetadata = (patch) => {
|
|
382
|
+
const current = readJson(cdp.sessionMetaFile, {}) ?? {};
|
|
383
|
+
const next = {
|
|
384
|
+
...current,
|
|
385
|
+
...patch,
|
|
386
|
+
updatedAt: new Date().toISOString(),
|
|
387
|
+
};
|
|
388
|
+
writeJson(cdp.sessionMetaFile, next);
|
|
389
|
+
return next;
|
|
390
|
+
};
|
|
391
|
+
console.error(`[CDP_RUNNER] Output dir: ${outputDir}`);
|
|
392
|
+
console.error(`[CDP_RUNNER] Session meta dir: ${sessionMetaDir}`);
|
|
393
|
+
console.error(`[CDP_RUNNER] Connected - running ${scriptArg}`);
|
|
394
|
+
|
|
395
|
+
// Node PM has no scoped networking; keep generated script fetch/WebSocket on localhost.
|
|
396
|
+
const _origFetch = globalThis.fetch;
|
|
397
|
+
const _OrigWS = globalThis.WebSocket;
|
|
398
|
+
function isLocalhost(url) {
|
|
399
|
+
try {
|
|
400
|
+
const h = new URL(String(url)).hostname;
|
|
401
|
+
return h === 'localhost' || h === '127.0.0.1' || h === '::1';
|
|
402
|
+
} catch { return false; }
|
|
403
|
+
}
|
|
404
|
+
globalThis.fetch = function restrictedFetch(input, init) {
|
|
405
|
+
const url = typeof input === 'string' ? input
|
|
406
|
+
: input instanceof URL ? input.href
|
|
407
|
+
: input?.url ?? '';
|
|
408
|
+
if (!isLocalhost(url)) {
|
|
409
|
+
throw new Error(`[SANDBOX] fetch blocked: only localhost allowed (attempted: ${url})`);
|
|
410
|
+
}
|
|
411
|
+
return _origFetch(input, init);
|
|
412
|
+
};
|
|
413
|
+
globalThis.WebSocket = class RestrictedWebSocket extends _OrigWS {
|
|
414
|
+
constructor(url, ...args) {
|
|
415
|
+
if (!isLocalhost(url)) {
|
|
416
|
+
throw new Error(`[SANDBOX] WebSocket blocked: only localhost allowed (attempted: ${url})`);
|
|
417
|
+
}
|
|
418
|
+
super(url, ...args);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
registerCleanup(async () => {
|
|
423
|
+
cdp.close();
|
|
424
|
+
if (openedTabId && !KEEP_TAB) {
|
|
425
|
+
const closed = await closeTab(openedTabId);
|
|
426
|
+
console.error(`[CDP_RUNNER] Tab ${openedTabId} ${closed ? 'closed' : 'already gone'}`);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const scriptPath = resolve(process.cwd(), scriptArg);
|
|
431
|
+
if (!existsSync(scriptPath)) {
|
|
432
|
+
console.error(`[CDP_RUNNER] Script not found: ${scriptPath}`);
|
|
433
|
+
cdp.writeSessionMetadata({ lastRunStatus: 'error', lastError: `Script not found: ${scriptPath}` });
|
|
434
|
+
finalizeRun('error', { error: `Script not found: ${scriptPath}` });
|
|
435
|
+
await _cleanup?.();
|
|
436
|
+
process.exit(1);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
let mod;
|
|
440
|
+
try {
|
|
441
|
+
mod = await import(pathToFileURL(scriptPath).href);
|
|
442
|
+
} catch (e) {
|
|
443
|
+
console.error(`[CDP_RUNNER] Failed to load script: ${e.message}`);
|
|
444
|
+
cdp.writeSessionMetadata({ lastRunStatus: 'error', lastError: e.message });
|
|
445
|
+
finalizeRun('error', { error: e.message });
|
|
446
|
+
await _cleanup?.();
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (typeof mod.run !== 'function') {
|
|
451
|
+
console.error('[CDP_RUNNER] Script must export: export async function run(cdp) { ... }');
|
|
452
|
+
cdp.writeSessionMetadata({
|
|
453
|
+
lastRunStatus: 'error',
|
|
454
|
+
lastError: 'Script must export: export async function run(cdp) { ... }',
|
|
455
|
+
});
|
|
456
|
+
finalizeRun('error', { error: 'missing run(cdp) export' });
|
|
457
|
+
await _cleanup?.();
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
let exitCode = 0;
|
|
462
|
+
try {
|
|
463
|
+
await mod.run(cdp);
|
|
464
|
+
cdp.writeSessionMetadata({ lastRunStatus: 'success' });
|
|
465
|
+
finalizeRun('success');
|
|
466
|
+
console.error('[CDP_RUNNER] Script completed successfully');
|
|
467
|
+
} catch (e) {
|
|
468
|
+
const isCdpError = /CDP error \[|CDP timeout/.test(e.message);
|
|
469
|
+
if (isCdpError) {
|
|
470
|
+
const methodMatch = e.message.match(/for:\s*(\S+)/) ?? e.message.match(/'([A-Z][a-zA-Z]+\.[a-zA-Z]+)'/);
|
|
471
|
+
const method = methodMatch ? methodMatch[1] : 'unknown';
|
|
472
|
+
console.log(`[CDP_RETRY_NEEDED] method=${method} error="${e.message}"`);
|
|
473
|
+
console.log(`[CDP_RETRY_NEEDED] Fix: ensure the domain for "${method}" is enabled before calling it, check parameter names, and re-run.`);
|
|
474
|
+
cdp.writeSessionMetadata({
|
|
475
|
+
lastRunStatus: 'retry-needed',
|
|
476
|
+
lastError: e.message,
|
|
477
|
+
lastErrorMethod: method,
|
|
478
|
+
});
|
|
479
|
+
finalizeRun('retry-needed', { error: e.message, errorMethod: method });
|
|
480
|
+
exitCode = 2;
|
|
481
|
+
} else {
|
|
482
|
+
console.error(`[CDP_RUNNER] Script error: ${e.message}`);
|
|
483
|
+
if (e.stack) console.error(e.stack);
|
|
484
|
+
cdp.writeSessionMetadata({
|
|
485
|
+
lastRunStatus: 'error',
|
|
486
|
+
lastError: e.message,
|
|
487
|
+
});
|
|
488
|
+
finalizeRun('error', { error: e.message });
|
|
489
|
+
exitCode = 1;
|
|
490
|
+
}
|
|
491
|
+
} finally {
|
|
492
|
+
await _cleanup?.();
|
|
493
|
+
_cleanup = null;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
process.exit(exitCode);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
main().catch(async e => {
|
|
500
|
+
console.error('[CDP_RUNNER_FATAL]', e.message);
|
|
501
|
+
if (_cleanup) { try { await _cleanup(); } catch {} }
|
|
502
|
+
process.exit(1);
|
|
503
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Run cdp-runner.mjs with Node permissions, output isolation, and a process timeout.
|
|
3
|
+
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import { resolve, dirname, join } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { existsSync, realpathSync,
|
|
8
|
+
mkdirSync, copyFileSync } from 'fs';
|
|
9
|
+
import { tmpdir } from 'os';
|
|
10
|
+
|
|
11
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const RUNNER = resolve(__dir, 'cdp-runner.mjs');
|
|
13
|
+
|
|
14
|
+
const argv = process.argv.slice(2);
|
|
15
|
+
const getArg = (flag, def) => { const i = argv.indexOf(flag); return i !== -1 && argv[i + 1] ? argv[i + 1] : def; };
|
|
16
|
+
const hasFlag = (flag) => argv.includes(flag);
|
|
17
|
+
|
|
18
|
+
const PORT = getArg('--port', '9222');
|
|
19
|
+
const LIST_TARGETS = hasFlag('--list-targets');
|
|
20
|
+
const scriptArg = argv.find(a => !a.startsWith('--') && (a.endsWith('.mjs') || a.endsWith('.js')));
|
|
21
|
+
const SCRIPT_TIMEOUT_MS = parseInt(getArg('--script-timeout', '300000'), 10);
|
|
22
|
+
|
|
23
|
+
if (!scriptArg && !LIST_TARGETS) {
|
|
24
|
+
console.error('[CDP_SANDBOX] Usage: node cdp-sandbox.mjs <script.mjs> [--port 9222] [options]');
|
|
25
|
+
console.error('[CDP_SANDBOX] Options are the same as cdp-runner.mjs');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
|
|
30
|
+
const OUTPUT_DIR = join(tmpdir(), '.octocode-chrome-devtools', timestamp);
|
|
31
|
+
mkdirSync(OUTPUT_DIR, { recursive: true, mode: 0o700 });
|
|
32
|
+
const SESSION_META_DIR = join(tmpdir(), '.octocode-chrome-devtools', 'session-meta', `port-${PORT}`);
|
|
33
|
+
mkdirSync(SESSION_META_DIR, { recursive: true, mode: 0o700 });
|
|
34
|
+
|
|
35
|
+
// Node 22 normalizes --allow-fs-* entries to realpaths; pass symlink and real forms.
|
|
36
|
+
const safePath = (p) => { try { return realpathSync(p); } catch { return p; } };
|
|
37
|
+
|
|
38
|
+
const TMPDIR_RAW = tmpdir();
|
|
39
|
+
const TMPDIR_REAL = safePath(TMPDIR_RAW);
|
|
40
|
+
const RUNNER_REAL = safePath(RUNNER);
|
|
41
|
+
const OUTPUT_REAL = safePath(OUTPUT_DIR);
|
|
42
|
+
const SESSION_META_REAL = safePath(SESSION_META_DIR);
|
|
43
|
+
|
|
44
|
+
// Stage helpers next to generated $TMPDIR scripts so relative imports work.
|
|
45
|
+
const HELPERS = ['sourcemap-resolver.mjs', 'undercover.mjs'];
|
|
46
|
+
for (const helper of HELPERS) {
|
|
47
|
+
const src = resolve(__dir, helper);
|
|
48
|
+
const dst = join(TMPDIR_RAW, helper);
|
|
49
|
+
if (existsSync(src)) {
|
|
50
|
+
try { copyFileSync(src, dst); }
|
|
51
|
+
catch (e) { console.error(`[CDP_SANDBOX] Warning: could not copy ${helper}: ${e.message}`); }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let scriptReal = null;
|
|
56
|
+
const allowReadExtra = [];
|
|
57
|
+
if (scriptArg) {
|
|
58
|
+
const scriptPath = resolve(process.cwd(), scriptArg);
|
|
59
|
+
if (!existsSync(scriptPath)) {
|
|
60
|
+
console.error(`[CDP_SANDBOX] Script not found: ${scriptPath}`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
scriptReal = safePath(scriptPath);
|
|
64
|
+
allowReadExtra.push(scriptPath, scriptReal);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const spawnArgv = argv.map(a => (a === scriptArg && scriptReal) ? scriptReal : a);
|
|
68
|
+
|
|
69
|
+
// Node PM has no stable scoped --allow-net; cdp-runner restricts fetch/WebSocket to localhost.
|
|
70
|
+
const readPaths = [...new Set([RUNNER, RUNNER_REAL, TMPDIR_RAW, TMPDIR_REAL, ...allowReadExtra])];
|
|
71
|
+
const writePaths = [...new Set([OUTPUT_DIR, OUTPUT_REAL, SESSION_META_DIR, SESSION_META_REAL])];
|
|
72
|
+
|
|
73
|
+
const permFlags = [
|
|
74
|
+
'--permission',
|
|
75
|
+
...readPaths.map(p => `--allow-fs-read=${p}`),
|
|
76
|
+
...writePaths.map(p => `--allow-fs-write=${p}`),
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const childEnv = {
|
|
80
|
+
CDP_OUTPUT_DIR: OUTPUT_DIR,
|
|
81
|
+
CDP_SESSION_META_DIR: SESSION_META_DIR,
|
|
82
|
+
...(process.env.TMPDIR ? { TMPDIR: process.env.TMPDIR } : {}),
|
|
83
|
+
...(process.env.TMP ? { TMP: process.env.TMP } : {}),
|
|
84
|
+
...(process.env.TEMP ? { TEMP: process.env.TEMP } : {}),
|
|
85
|
+
...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}),
|
|
86
|
+
...(process.env.WINDIR ? { WINDIR: process.env.WINDIR } : {}),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
console.error('[CDP_SANDBOX] Launching runner in sandbox (Node.js Permission Model)');
|
|
90
|
+
console.error(`[CDP_SANDBOX] Output dir: ${OUTPUT_DIR}`);
|
|
91
|
+
console.error(`[CDP_SANDBOX] Session meta: ${SESSION_META_DIR}`);
|
|
92
|
+
console.error(`[CDP_SANDBOX] FS write: output dir + session meta dir (mode 0700)`);
|
|
93
|
+
console.error(`[CDP_SANDBOX] FS read: $TMPDIR tree + runner`);
|
|
94
|
+
console.error(`[CDP_SANDBOX] child_process: blocked`);
|
|
95
|
+
console.error(`[CDP_SANDBOX] workers: blocked`);
|
|
96
|
+
console.error(`[CDP_SANDBOX] env: minimal allowlist (parent env not inherited)`);
|
|
97
|
+
console.error(`[CDP_SANDBOX] Network: localhost only (fetch+WebSocket patched in runner)`);
|
|
98
|
+
|
|
99
|
+
const child = spawn(process.execPath, [...permFlags, RUNNER_REAL, ...spawnArgv], {
|
|
100
|
+
stdio: 'inherit',
|
|
101
|
+
env: childEnv,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const scriptTimer = setTimeout(() => {
|
|
105
|
+
console.error(`[CDP_SANDBOX] Script timeout after ${SCRIPT_TIMEOUT_MS}ms - killing runner`);
|
|
106
|
+
child.kill('SIGTERM');
|
|
107
|
+
setTimeout(() => child.kill('SIGKILL'), 2000).unref();
|
|
108
|
+
}, SCRIPT_TIMEOUT_MS);
|
|
109
|
+
scriptTimer.unref(); // don't keep the process alive on its own
|
|
110
|
+
|
|
111
|
+
child.on('exit', (code, signal) => {
|
|
112
|
+
clearTimeout(scriptTimer);
|
|
113
|
+
if (signal) {
|
|
114
|
+
console.error(`[CDP_SANDBOX] Runner killed by signal: ${signal}`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
process.exit(code ?? 0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
child.on('error', (err) => {
|
|
121
|
+
console.error(`[CDP_SANDBOX] Failed to launch sandboxed runner: ${err.message}`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
});
|