octocode-cli 1.3.0 → 1.5.0
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 +129 -28
- 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 +60 -58
- 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-brainstorming/.env.example +11 -0
- package/skills/octocode-brainstorming/SKILL.md +262 -0
- package/skills/octocode-brainstorming/scripts/tavily-search.mjs +138 -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 +137 -306
- package/skills/octocode-engineer/references/cli-reference.md +13 -0
- package/skills/octocode-engineer/references/output-files.md +3 -3
- package/skills/octocode-engineer/scripts/run.js +146 -146
- package/skills/octocode-engineer/src/pipeline/main.ts +1 -17
- package/skills/octocode-engineer/src/pipeline/progress.ts +4 -0
- 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 +184 -239
- 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 +337 -0
- 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/out/chunks/chunk-LH4AZJPA.js +0 -389
- package/out/chunks/command-help-specs-CQ3RBLP6.js +0 -8
- package/out/chunks/commands-M3QTWKWE.js +0 -51
- package/out/chunks/help-XPXP46ZT.js +0 -10
- package/out/chunks/main-help-HXFAFHPG.js +0 -10
- package/out/chunks/tool-command-VHFLPIHY.js +0 -8
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Base run(cdp) template. Save task copies in $TMPDIR and run with cdp-sandbox.mjs.
|
|
2
|
+
|
|
3
|
+
export async function run(cdp) {
|
|
4
|
+
// Optional: source maps must be registered before navigation.
|
|
5
|
+
// const { createSourceMapResolver } = await import(new URL('./sourcemap-resolver.mjs', import.meta.url).href);
|
|
6
|
+
// const resolver = await createSourceMapResolver(cdp);
|
|
7
|
+
|
|
8
|
+
await cdp.send('Runtime.enable', {});
|
|
9
|
+
await cdp.send('Network.enable', {});
|
|
10
|
+
await cdp.send('Log.enable', {});
|
|
11
|
+
// await cdp.send('Page.enable', {});
|
|
12
|
+
// await cdp.send('DOM.enable', {});
|
|
13
|
+
// await cdp.send('CSS.enable', {});
|
|
14
|
+
// await cdp.send('Performance.enable', {});
|
|
15
|
+
|
|
16
|
+
console.log(`[METRIC] Inspecting: ${cdp.targetInfo.url}`);
|
|
17
|
+
cdp.addReasoningStep?.({
|
|
18
|
+
step: 'init',
|
|
19
|
+
hypothesis: 'Target page has useful runtime/network evidence',
|
|
20
|
+
action: 'Enabled Runtime/Network/Log and started listeners',
|
|
21
|
+
result: `Attached to ${cdp.targetInfo.url}`,
|
|
22
|
+
nextAction: 'Collect signals and decide if targeted follow-up tabs are needed',
|
|
23
|
+
});
|
|
24
|
+
cdp.upsertResourceMap?.(`tab:${cdp.targetInfo.id ?? 'unknown'}`, {
|
|
25
|
+
type: 'tab',
|
|
26
|
+
url: cdp.targetInfo.url ?? null,
|
|
27
|
+
title: cdp.targetInfo.title ?? null,
|
|
28
|
+
tabId: cdp.targetInfo.id ?? null,
|
|
29
|
+
notes: 'Primary analysis target',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
cdp.on('Runtime.consoleAPICalled', ({ type, args }) => {
|
|
33
|
+
const msg = args.map(a => a.value ?? a.description ?? '[object]').join(' ');
|
|
34
|
+
console.log(`[CONSOLE:${type.toUpperCase()}] ${msg}`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
cdp.on('Runtime.exceptionThrown', ({ exceptionDetails }) => {
|
|
38
|
+
const desc = exceptionDetails.exception?.description ?? exceptionDetails.text;
|
|
39
|
+
console.log(`[EXCEPTION] ${desc}`);
|
|
40
|
+
const frame = exceptionDetails.stackTrace?.callFrames?.[0];
|
|
41
|
+
if (frame)
|
|
42
|
+
console.log(`[EXCEPTION_LOCATION] ${frame.url}:${frame.lineNumber}:${frame.columnNumber} in ${frame.functionName || '(anonymous)'}`);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const requests = new Map();
|
|
46
|
+
cdp.on('Network.requestWillBeSent', ({ requestId, request }) => {
|
|
47
|
+
requests.set(requestId, { url: request.url, method: request.method, start: Date.now() });
|
|
48
|
+
});
|
|
49
|
+
cdp.on('Network.responseReceived', ({ requestId, response }) => {
|
|
50
|
+
const r = requests.get(requestId);
|
|
51
|
+
if (!r) return;
|
|
52
|
+
const duration = Date.now() - r.start;
|
|
53
|
+
console.log(`[NETWORK] ${response.status} ${r.method} ${r.url} (${duration}ms)`);
|
|
54
|
+
if (response.status >= 400) console.log(`[NETWORK_ERROR] HTTP ${response.status} -> ${r.url}`);
|
|
55
|
+
if (duration > 3000) console.log(`[FINDING] SLOW_REQUEST: ${r.url} took ${duration}ms`);
|
|
56
|
+
});
|
|
57
|
+
cdp.on('Network.loadingFailed', ({ requestId, errorText, blockedReason }) => {
|
|
58
|
+
const r = requests.get(requestId);
|
|
59
|
+
console.log(`[NETWORK_FAILED] ${r?.url ?? 'unknown'}: ${blockedReason ? `blocked:${blockedReason}` : errorText}`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
cdp.on('Log.entryAdded', ({ entry }) => {
|
|
63
|
+
if (entry.level === 'error' || entry.level === 'warning')
|
|
64
|
+
console.log(`[LOG:${entry.level.toUpperCase()}] [${entry.source}] ${entry.text}${entry.url ? ` @ ${entry.url}:${entry.lineNumber}` : ''}`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Add task-specific cdp.send() calls here. See SCRIPT_PATTERNS.md.
|
|
68
|
+
|
|
69
|
+
const MONITOR_MS = 10000; // 3s static, 10s dynamic, 30s long network check
|
|
70
|
+
console.log(`[METRIC] Monitoring for ${MONITOR_MS / 1000}s...`);
|
|
71
|
+
await new Promise(r => setTimeout(r, MONITOR_MS));
|
|
72
|
+
|
|
73
|
+
console.log(`[METRIC] Total requests: ${requests.size}`);
|
|
74
|
+
cdp.addReasoningStep?.({
|
|
75
|
+
step: 'wrap-up',
|
|
76
|
+
hypothesis: 'Collected enough baseline signal for this run',
|
|
77
|
+
action: `Observed requests and exceptions for ${MONITOR_MS}ms`,
|
|
78
|
+
result: `requests=${requests.size}`,
|
|
79
|
+
nextAction: requests.size === 0 ? 'Consider navigation or explicit trigger on same tab' : 'Drill into failing or slow requests',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Launch Chrome with CDP enabled; tracks isolated sessions for cleanup.
|
|
3
|
+
|
|
4
|
+
import { spawn, execSync, execFileSync } from 'child_process';
|
|
5
|
+
import { platform, tmpdir } from 'os';
|
|
6
|
+
import { existsSync, writeFileSync, readFileSync, rmSync, mkdirSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
|
|
9
|
+
const argv = process.argv.slice(2);
|
|
10
|
+
const getArg = (flag, def) => { const i = argv.indexOf(flag); return i !== -1 && argv[i + 1] ? argv[i + 1] : def; };
|
|
11
|
+
const hasFlag = (flag) => argv.includes(flag);
|
|
12
|
+
|
|
13
|
+
const PORT = getArg('--port', '9222');
|
|
14
|
+
const PROFILE = getArg('--profile', 'Default');
|
|
15
|
+
const URL_ARG = getArg('--url', '');
|
|
16
|
+
const HEADLESS = hasFlag('--headless');
|
|
17
|
+
const CLEANUP = hasFlag('--cleanup');
|
|
18
|
+
const DRY_RUN = hasFlag('--dry-run');
|
|
19
|
+
const CHROME_PATH = getArg('--chromePath', '');
|
|
20
|
+
const WINDOW_SIZE = getArg('--windowSize', '');
|
|
21
|
+
const USER_AGENT = getArg('--userAgent', '');
|
|
22
|
+
const PROXY_SERVER = getArg('--proxyServer', '');
|
|
23
|
+
const PROXY_BYPASS_LIST = getArg('--proxyBypassList', '');
|
|
24
|
+
const PROXY_PAC_URL = getArg('--proxyPacUrl', '');
|
|
25
|
+
const CONFIG_PATH = getArg('--config', '');
|
|
26
|
+
|
|
27
|
+
const TMP = tmpdir();
|
|
28
|
+
const SESSION_FILE = join(TMP, `cdp-session-${PORT}.json`);
|
|
29
|
+
// Headless always uses an isolated temp profile.
|
|
30
|
+
const HEADLESS_PROFILE_DIR = join(TMP, `cdp-chrome-profile-${PORT}`);
|
|
31
|
+
|
|
32
|
+
function ok(payload) { console.log(JSON.stringify(payload)); }
|
|
33
|
+
function err(message) { console.log(JSON.stringify({ status: 'ERROR', message })); process.exit(1); }
|
|
34
|
+
|
|
35
|
+
function readJsonFile(filePath, { strict = false } = {}) {
|
|
36
|
+
if (!filePath || !existsSync(filePath)) return null;
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
39
|
+
} catch (jsonErr) {
|
|
40
|
+
if (strict) err(`Invalid JSON in config file: ${filePath} (${jsonErr.message})`);
|
|
41
|
+
console.error(`[BROWSER] Warning: ignoring invalid JSON config ${filePath}: ${jsonErr.message}`);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeProxyConfig(raw) {
|
|
47
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
48
|
+
const enabled = raw.enabled;
|
|
49
|
+
const server = typeof raw.server === 'string' ? raw.server.trim() : '';
|
|
50
|
+
const bypassList = typeof raw.bypassList === 'string' ? raw.bypassList.trim() : '';
|
|
51
|
+
const pacUrl = typeof raw.pacUrl === 'string' ? raw.pacUrl.trim() : '';
|
|
52
|
+
return {
|
|
53
|
+
enabled: enabled === undefined ? true : Boolean(enabled),
|
|
54
|
+
server,
|
|
55
|
+
bypassList,
|
|
56
|
+
pacUrl,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function loadProxyConfig() {
|
|
61
|
+
const cwdConfig = join(process.cwd(), '.octocode', 'chrome-devtools.json');
|
|
62
|
+
const homeConfig = join(process.env.HOME ?? process.env.USERPROFILE ?? '', '.octocode', 'config.json');
|
|
63
|
+
if (CONFIG_PATH && !existsSync(CONFIG_PATH)) err(`Config file not found: ${CONFIG_PATH}`);
|
|
64
|
+
const candidateFiles = CONFIG_PATH ? [CONFIG_PATH] : [cwdConfig, homeConfig];
|
|
65
|
+
|
|
66
|
+
for (const configFile of candidateFiles) {
|
|
67
|
+
const json = readJsonFile(configFile, { strict: Boolean(CONFIG_PATH) });
|
|
68
|
+
if (!json || typeof json !== 'object') continue;
|
|
69
|
+
|
|
70
|
+
const candidates = [
|
|
71
|
+
json?.proxy,
|
|
72
|
+
json?.chromeDevtools?.proxy,
|
|
73
|
+
json?.skills?.['octocode-chrome-devtools']?.proxy,
|
|
74
|
+
];
|
|
75
|
+
for (const candidate of candidates) {
|
|
76
|
+
const normalized = normalizeProxyConfig(candidate);
|
|
77
|
+
if (normalized) return normalized;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function findChrome() {
|
|
84
|
+
if (platform() === 'darwin') {
|
|
85
|
+
const candidates = [
|
|
86
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
87
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
88
|
+
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
|
89
|
+
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
90
|
+
];
|
|
91
|
+
return candidates.find(p => existsSync(p)) ?? null;
|
|
92
|
+
}
|
|
93
|
+
if (platform() === 'linux') {
|
|
94
|
+
for (const bin of ['google-chrome', 'google-chrome-stable', 'chromium-browser', 'chromium']) {
|
|
95
|
+
try { execSync(`which ${bin}`, { stdio: 'ignore' }); return bin; } catch {}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (platform() === 'win32') {
|
|
99
|
+
const candidates = [
|
|
100
|
+
`${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
101
|
+
`${process.env.PROGRAMFILES}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
102
|
+
`${process.env['PROGRAMFILES(X86)']}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
103
|
+
`${process.env.LOCALAPPDATA}\\Chromium\\Application\\chrome.exe`,
|
|
104
|
+
];
|
|
105
|
+
return candidates.find(p => p && existsSync(p)) ?? null;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function checkRunning() {
|
|
111
|
+
try {
|
|
112
|
+
const res = await fetch(`http://localhost:${PORT}/json/version`, { signal: AbortSignal.timeout(2000) });
|
|
113
|
+
if (res.ok) return await res.json();
|
|
114
|
+
} catch {}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function readSession() {
|
|
119
|
+
try { return JSON.parse(readFileSync(SESSION_FILE, 'utf8')); } catch { return null; }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function writeSession(pid) {
|
|
123
|
+
writeFileSync(SESSION_FILE, JSON.stringify({
|
|
124
|
+
pid,
|
|
125
|
+
port: PORT,
|
|
126
|
+
profileDir: HEADLESS_PROFILE_DIR,
|
|
127
|
+
headless: HEADLESS,
|
|
128
|
+
isolated: HEADLESS || usingIsolatedProfile,
|
|
129
|
+
startedAt: Date.now(),
|
|
130
|
+
chromePath,
|
|
131
|
+
}), { mode: 0o600 });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getProcessCommand(pid) {
|
|
135
|
+
const pidText = String(pid);
|
|
136
|
+
if (!/^\d+$/.test(pidText)) return '';
|
|
137
|
+
try {
|
|
138
|
+
if (platform() === 'darwin' || platform() === 'linux') {
|
|
139
|
+
return execFileSync('ps', ['-p', pidText, '-o', 'command='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
140
|
+
}
|
|
141
|
+
if (platform() === 'win32') {
|
|
142
|
+
return execFileSync('powershell.exe', [
|
|
143
|
+
'-NoProfile',
|
|
144
|
+
'-Command',
|
|
145
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId=${pidText}").CommandLine`,
|
|
146
|
+
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
147
|
+
}
|
|
148
|
+
} catch {}
|
|
149
|
+
return '';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function processMatchesTrackedSession(session) {
|
|
153
|
+
const command = getProcessCommand(session.pid);
|
|
154
|
+
if (!command) return false;
|
|
155
|
+
|
|
156
|
+
const isChrome = /Chrome|Chromium|Brave Browser|chrome|chromium|chrome\.exe/i.test(command);
|
|
157
|
+
const hasPort = command.includes(`--remote-debugging-port=${session.port}`);
|
|
158
|
+
const hasProfile = session.profileDir && command.includes(session.profileDir);
|
|
159
|
+
|
|
160
|
+
return isChrome && hasPort && hasProfile;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function waitForExit(pid, timeoutMs = 3000) {
|
|
164
|
+
const start = Date.now();
|
|
165
|
+
while (Date.now() - start < timeoutMs) {
|
|
166
|
+
try {
|
|
167
|
+
process.kill(pid, 0);
|
|
168
|
+
await new Promise(r => setTimeout(r, 100));
|
|
169
|
+
} catch {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function removeDirWithRetry(dir) {
|
|
177
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
178
|
+
try {
|
|
179
|
+
rmSync(dir, { recursive: true, force: true });
|
|
180
|
+
if (!existsSync(dir)) return true;
|
|
181
|
+
} catch (e) {
|
|
182
|
+
if (attempt === 4) throw e;
|
|
183
|
+
}
|
|
184
|
+
await new Promise(r => setTimeout(r, 200));
|
|
185
|
+
}
|
|
186
|
+
return !existsSync(dir);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function cleanupSession() {
|
|
190
|
+
const session = readSession();
|
|
191
|
+
if (!session) {
|
|
192
|
+
console.error('[BROWSER] No tracked session found for port', PORT);
|
|
193
|
+
ok({ status: 'NO_TRACKED_SESSION', port: PORT });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (String(session.port) !== String(PORT)) {
|
|
197
|
+
err(`Tracked session port mismatch: expected ${PORT}, found ${session.port}`);
|
|
198
|
+
}
|
|
199
|
+
if (!processMatchesTrackedSession(session)) {
|
|
200
|
+
console.error(`[BROWSER] Refusing to kill pid=${session.pid}; it does not match the tracked CDP port/profile`);
|
|
201
|
+
if (DRY_RUN) {
|
|
202
|
+
ok({ status: 'CLEANUP_DRY_RUN', port: PORT, pid: session.pid, matchesTrackedSession: false, wouldKill: false });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
try { rmSync(SESSION_FILE, { force: true }); } catch {}
|
|
206
|
+
ok({ status: 'STALE_SESSION_REMOVED', port: PORT });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (DRY_RUN) {
|
|
211
|
+
ok({ status: 'CLEANUP_DRY_RUN', port: PORT, pid: session.pid, matchesTrackedSession: true, wouldKill: true, profileDir: session.profileDir });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try { process.kill(session.pid, 'SIGTERM'); console.error(`[BROWSER] Sent SIGTERM to Chrome pid=${session.pid}`); }
|
|
216
|
+
catch { console.error(`[BROWSER] Process pid=${session.pid} already gone`); }
|
|
217
|
+
const exited = await waitForExit(session.pid);
|
|
218
|
+
if (!exited) {
|
|
219
|
+
try { process.kill(session.pid, 'SIGKILL'); console.error(`[BROWSER] Sent SIGKILL to Chrome pid=${session.pid}`); }
|
|
220
|
+
catch {}
|
|
221
|
+
await waitForExit(session.pid, 1000);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (session.profileDir && existsSync(session.profileDir)) {
|
|
225
|
+
try { await removeDirWithRetry(session.profileDir); console.error(`[BROWSER] Removed profile: ${session.profileDir}`); }
|
|
226
|
+
catch (e) { console.error(`[BROWSER] Could not remove profile: ${e.message}`); }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
try { rmSync(SESSION_FILE, { force: true }); } catch {}
|
|
230
|
+
console.error('[BROWSER] Session cleaned up');
|
|
231
|
+
ok({ status: 'CLEANED_UP', port: PORT });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (CLEANUP) { await cleanupSession(); process.exit(0); }
|
|
235
|
+
|
|
236
|
+
const proxyConfig = loadProxyConfig();
|
|
237
|
+
const effectiveProxyServer =
|
|
238
|
+
PROXY_SERVER || (proxyConfig?.enabled ? proxyConfig.server : '');
|
|
239
|
+
const effectiveProxyBypassList =
|
|
240
|
+
PROXY_BYPASS_LIST || (proxyConfig?.enabled ? proxyConfig.bypassList : '');
|
|
241
|
+
const effectiveProxyPacUrl =
|
|
242
|
+
PROXY_PAC_URL || (proxyConfig?.enabled ? proxyConfig.pacUrl : '');
|
|
243
|
+
const proxyRequested = Boolean(effectiveProxyServer || effectiveProxyPacUrl);
|
|
244
|
+
|
|
245
|
+
const existing = await checkRunning();
|
|
246
|
+
if (existing) {
|
|
247
|
+
ok({
|
|
248
|
+
status: 'BROWSER_READY',
|
|
249
|
+
wsUrl: existing.webSocketDebuggerUrl,
|
|
250
|
+
port: PORT,
|
|
251
|
+
reused: true,
|
|
252
|
+
browser: existing.Browser,
|
|
253
|
+
proxyConfigured: false,
|
|
254
|
+
proxyRequested,
|
|
255
|
+
warning: proxyRequested
|
|
256
|
+
? 'Existing Chrome was reused; proxy settings cannot be applied to an already-running CDP session. Run cleanup or use another port for a fresh proxied session.'
|
|
257
|
+
: undefined,
|
|
258
|
+
});
|
|
259
|
+
process.exit(0);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const chromePath = CHROME_PATH || findChrome();
|
|
263
|
+
if (!chromePath) err('Chrome not found. Install Google Chrome from https://www.google.com/chrome/ or pass --chromePath <path>');
|
|
264
|
+
if (CHROME_PATH && !existsSync(CHROME_PATH)) err(`Chrome not found at --chromePath: ${CHROME_PATH}`);
|
|
265
|
+
|
|
266
|
+
const HOME = process.env.HOME ?? process.env.USERPROFILE;
|
|
267
|
+
|
|
268
|
+
// If Chrome is already running without CDP, real-profile launches may hand off to it.
|
|
269
|
+
function isChromeRunning() {
|
|
270
|
+
if (platform() === 'darwin') {
|
|
271
|
+
try { execSync('pgrep -x "Google Chrome" > /dev/null 2>&1'); return true; } catch { return false; }
|
|
272
|
+
}
|
|
273
|
+
if (platform() === 'linux') {
|
|
274
|
+
for (const name of ['chrome', 'google-chrome', 'chromium', 'chromium-browser']) {
|
|
275
|
+
try { execSync(`pgrep -x "${name}" > /dev/null 2>&1`); return true; } catch {}
|
|
276
|
+
}
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
if (platform() === 'win32') {
|
|
280
|
+
try {
|
|
281
|
+
const out = execSync('tasklist /FI "IMAGENAME eq chrome.exe" /NH', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
282
|
+
return out.includes('chrome.exe');
|
|
283
|
+
} catch { return false; }
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let userDataDir;
|
|
289
|
+
let usingIsolatedProfile = false;
|
|
290
|
+
|
|
291
|
+
if (HEADLESS) {
|
|
292
|
+
mkdirSync(HEADLESS_PROFILE_DIR, { recursive: true });
|
|
293
|
+
userDataDir = HEADLESS_PROFILE_DIR;
|
|
294
|
+
} else if (isChromeRunning()) {
|
|
295
|
+
usingIsolatedProfile = true;
|
|
296
|
+
mkdirSync(HEADLESS_PROFILE_DIR, { recursive: true });
|
|
297
|
+
userDataDir = HEADLESS_PROFILE_DIR;
|
|
298
|
+
console.error('[BROWSER] Chrome already running without CDP - launching isolated CDP session');
|
|
299
|
+
} else {
|
|
300
|
+
userDataDir = platform() === 'darwin'
|
|
301
|
+
? `${HOME}/Library/Application Support/Google/Chrome`
|
|
302
|
+
: platform() === 'win32'
|
|
303
|
+
? `${process.env.LOCALAPPDATA}\\Google\\Chrome\\User Data`
|
|
304
|
+
: `${HOME}/.config/google-chrome`;
|
|
305
|
+
console.error('[BROWSER] WARNING: Using real user Chrome profile. CDP scripts will have access');
|
|
306
|
+
console.error('[BROWSER] WARNING: to all cookies, auth tokens, and sessions in this profile.');
|
|
307
|
+
console.error('[BROWSER] WARNING: Use --headless for isolated inspection without profile access.');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const chromeArgs = [
|
|
311
|
+
`--remote-debugging-port=${PORT}`,
|
|
312
|
+
`--user-data-dir=${userDataDir}`,
|
|
313
|
+
'--no-first-run',
|
|
314
|
+
'--no-default-browser-check',
|
|
315
|
+
'--disable-background-mode',
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
if (!HEADLESS && !usingIsolatedProfile) chromeArgs.push(`--profile-directory=${PROFILE}`, '--restore-last-session');
|
|
319
|
+
if (HEADLESS) chromeArgs.push('--headless=new', '--disable-gpu', '--disable-dev-shm-usage');
|
|
320
|
+
if (HEADLESS && platform() === 'linux') chromeArgs.push('--no-sandbox', '--disable-setuid-sandbox');
|
|
321
|
+
if (USER_AGENT) chromeArgs.push(`--user-agent=${USER_AGENT}`);
|
|
322
|
+
if (WINDOW_SIZE) {
|
|
323
|
+
chromeArgs.push(`--window-size=${WINDOW_SIZE.replace('x', ',')}`);
|
|
324
|
+
}
|
|
325
|
+
if (effectiveProxyServer) chromeArgs.push(`--proxy-server=${effectiveProxyServer}`);
|
|
326
|
+
if (effectiveProxyBypassList) chromeArgs.push(`--proxy-bypass-list=${effectiveProxyBypassList}`);
|
|
327
|
+
if (effectiveProxyPacUrl && !effectiveProxyServer) chromeArgs.push(`--proxy-pac-url=${effectiveProxyPacUrl}`);
|
|
328
|
+
if (URL_ARG) chromeArgs.push(URL_ARG);
|
|
329
|
+
|
|
330
|
+
const profileLabel = HEADLESS ? 'headless' : usingIsolatedProfile ? 'isolated-cdp' : PROFILE;
|
|
331
|
+
console.error(`[BROWSER] Launching Chrome: headless=${HEADLESS} port=${PORT} profile=${profileLabel}`);
|
|
332
|
+
|
|
333
|
+
const child = spawn(chromePath, chromeArgs, { detached: true, stdio: 'ignore' });
|
|
334
|
+
child.unref();
|
|
335
|
+
|
|
336
|
+
if (HEADLESS || usingIsolatedProfile) writeSession(child.pid);
|
|
337
|
+
|
|
338
|
+
let attempts = 0;
|
|
339
|
+
while (attempts < 40) {
|
|
340
|
+
await new Promise(r => setTimeout(r, 500));
|
|
341
|
+
const info = await checkRunning();
|
|
342
|
+
if (info) {
|
|
343
|
+
ok({
|
|
344
|
+
status: 'BROWSER_READY',
|
|
345
|
+
wsUrl: info.webSocketDebuggerUrl,
|
|
346
|
+
port: PORT,
|
|
347
|
+
reused: false,
|
|
348
|
+
browser: info.Browser,
|
|
349
|
+
isolated: HEADLESS || usingIsolatedProfile,
|
|
350
|
+
sessionFile: (HEADLESS || usingIsolatedProfile) ? SESSION_FILE : null,
|
|
351
|
+
proxyConfigured: proxyRequested,
|
|
352
|
+
});
|
|
353
|
+
process.exit(0);
|
|
354
|
+
}
|
|
355
|
+
attempts++;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if ((HEADLESS || usingIsolatedProfile) && existsSync(HEADLESS_PROFILE_DIR)) {
|
|
359
|
+
rmSync(HEADLESS_PROFILE_DIR, { recursive: true, force: true });
|
|
360
|
+
rmSync(SESSION_FILE, { force: true });
|
|
361
|
+
}
|
|
362
|
+
err(`Chrome did not respond on port ${PORT} after 20s. Try launching manually with --remote-debugging-port=${PORT}`);
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Minimal source-map resolver for CDP scripts; strips sourcesContent.
|
|
2
|
+
|
|
3
|
+
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
4
|
+
const B64_MAP = new Uint8Array(256).fill(255);
|
|
5
|
+
for (let i = 0; i < B64.length; i++) B64_MAP[B64.charCodeAt(i)] = i;
|
|
6
|
+
|
|
7
|
+
function decodeVLQList(str) {
|
|
8
|
+
const result = [];
|
|
9
|
+
let i = 0;
|
|
10
|
+
while (i < str.length) {
|
|
11
|
+
let value = 0, shift = 0, digit;
|
|
12
|
+
do {
|
|
13
|
+
digit = B64_MAP[str.charCodeAt(i++)];
|
|
14
|
+
value |= (digit & 31) << shift;
|
|
15
|
+
shift += 5;
|
|
16
|
+
} while (digit & 32);
|
|
17
|
+
result.push(value & 1 ? -(value >> 1) : value >> 1);
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseMap(mapJson) {
|
|
23
|
+
// Never retain original source bodies.
|
|
24
|
+
delete mapJson.sourcesContent;
|
|
25
|
+
|
|
26
|
+
const sources = mapJson.sources ?? [];
|
|
27
|
+
const names = mapJson.names ?? [];
|
|
28
|
+
const segments = [];
|
|
29
|
+
|
|
30
|
+
let genLine = 0;
|
|
31
|
+
let srcIdx = 0, srcLine = 0, srcCol = 0, nameIdx = 0;
|
|
32
|
+
|
|
33
|
+
for (const lineStr of mapJson.mappings.split(';')) {
|
|
34
|
+
let genCol = 0;
|
|
35
|
+
if (lineStr) {
|
|
36
|
+
for (const segStr of lineStr.split(',')) {
|
|
37
|
+
if (!segStr) continue;
|
|
38
|
+
const fields = decodeVLQList(segStr);
|
|
39
|
+
genCol += fields[0];
|
|
40
|
+
if (fields.length >= 4) {
|
|
41
|
+
srcIdx += fields[1];
|
|
42
|
+
srcLine += fields[2];
|
|
43
|
+
srcCol += fields[3];
|
|
44
|
+
const ni = fields.length >= 5 ? nameIdx + fields[4] : -1;
|
|
45
|
+
if (fields.length >= 5) nameIdx += fields[4];
|
|
46
|
+
segments.push({
|
|
47
|
+
gl: genLine, gc: genCol,
|
|
48
|
+
si: srcIdx, sl: srcLine, sc: srcCol,
|
|
49
|
+
ni,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
genLine++;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
segments.sort((a, b) => a.gl !== b.gl ? a.gl - b.gl : a.gc - b.gc);
|
|
58
|
+
|
|
59
|
+
return { segments, sources, names };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function originalPositionFor(parsed, genLine, genCol) {
|
|
63
|
+
const { segments, sources, names } = parsed;
|
|
64
|
+
if (!segments.length) return null;
|
|
65
|
+
|
|
66
|
+
// Same-line lookup only; previous-line matches look plausible but are wrong.
|
|
67
|
+
let lo = 0, hi = segments.length - 1, best = -1;
|
|
68
|
+
|
|
69
|
+
while (lo <= hi) {
|
|
70
|
+
const mid = (lo + hi) >>> 1;
|
|
71
|
+
const s = segments[mid];
|
|
72
|
+
if (s.gl < genLine || (s.gl === genLine && s.gc <= genCol)) {
|
|
73
|
+
if (s.gl === genLine) best = mid;
|
|
74
|
+
lo = mid + 1;
|
|
75
|
+
} else {
|
|
76
|
+
hi = mid - 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (best === -1) return null;
|
|
81
|
+
const seg = segments[best];
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
source: sources[seg.si] ?? null,
|
|
85
|
+
line: seg.sl + 1,
|
|
86
|
+
col: seg.sc,
|
|
87
|
+
name: seg.ni >= 0 ? (names[seg.ni] ?? null) : null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function fetchText(url, timeoutMs = 4000) {
|
|
92
|
+
const { get } = url.startsWith('https') ? await import('https') : await import('http');
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const timer = setTimeout(() => reject(new Error('timeout')), timeoutMs);
|
|
95
|
+
get(url, (res) => {
|
|
96
|
+
if (res.statusCode !== 200) {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
res.resume();
|
|
99
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
100
|
+
}
|
|
101
|
+
const chunks = [];
|
|
102
|
+
res.on('data', c => chunks.push(c));
|
|
103
|
+
res.on('end', () => { clearTimeout(timer); resolve(Buffer.concat(chunks).toString('utf8')); });
|
|
104
|
+
res.on('error', e => { clearTimeout(timer); reject(e); });
|
|
105
|
+
}).on('error', e => { clearTimeout(timer); reject(e); });
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function createSourceMapResolver(cdp) {
|
|
110
|
+
await cdp.send('Debugger.enable', {});
|
|
111
|
+
await cdp.send('Debugger.setSkipAllPauses', { skip: true });
|
|
112
|
+
|
|
113
|
+
const parsedMaps = new Map();
|
|
114
|
+
const scriptUrls = new Map();
|
|
115
|
+
|
|
116
|
+
const stats = { withMap: 0, withoutMap: 0, loaded: 0, failed: 0 };
|
|
117
|
+
|
|
118
|
+
const loadPromises = [];
|
|
119
|
+
|
|
120
|
+
cdp.on('Debugger.scriptParsed', ({ scriptId, url, sourceMapURL }) => {
|
|
121
|
+
scriptUrls.set(scriptId, url);
|
|
122
|
+
|
|
123
|
+
if (!sourceMapURL) {
|
|
124
|
+
stats.withoutMap++;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
stats.withMap++;
|
|
129
|
+
|
|
130
|
+
const loadMap = async () => {
|
|
131
|
+
try {
|
|
132
|
+
let mapText;
|
|
133
|
+
|
|
134
|
+
if (sourceMapURL.startsWith('data:')) {
|
|
135
|
+
const b64start = sourceMapURL.indexOf('base64,');
|
|
136
|
+
if (b64start === -1) return;
|
|
137
|
+
mapText = Buffer.from(sourceMapURL.slice(b64start + 7), 'base64').toString('utf8');
|
|
138
|
+
} else {
|
|
139
|
+
const mapUrl = url ? new URL(sourceMapURL, url).href : sourceMapURL;
|
|
140
|
+
mapText = await fetchText(mapUrl);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const mapJson = JSON.parse(mapText);
|
|
144
|
+
const parsed = parseMap(mapJson); // strips sourcesContent internally
|
|
145
|
+
parsedMaps.set(scriptId, parsed);
|
|
146
|
+
stats.loaded++;
|
|
147
|
+
} catch {
|
|
148
|
+
stats.failed++;
|
|
149
|
+
parsedMaps.set(scriptId, null);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
loadPromises.push(loadMap());
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
async settle(timeoutMs = 8000) {
|
|
158
|
+
let timer;
|
|
159
|
+
try {
|
|
160
|
+
await Promise.race([
|
|
161
|
+
Promise.allSettled(loadPromises),
|
|
162
|
+
new Promise(r => { timer = setTimeout(r, timeoutMs); }),
|
|
163
|
+
]);
|
|
164
|
+
} finally {
|
|
165
|
+
if (timer) clearTimeout(timer);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
resolve(scriptId, line, col) {
|
|
170
|
+
const parsed = parsedMaps.get(scriptId);
|
|
171
|
+
if (!parsed) return null;
|
|
172
|
+
return originalPositionFor(parsed, line, col);
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
hasMap(scriptId) {
|
|
176
|
+
const m = parsedMaps.get(scriptId);
|
|
177
|
+
return m !== null && m !== undefined;
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
printSummary() {
|
|
181
|
+
const total = stats.withMap + stats.withoutMap;
|
|
182
|
+
const failNote = stats.failed > 0
|
|
183
|
+
? ` (failed maps are likely on internal servers or require auth - expected for production sites)`
|
|
184
|
+
: '';
|
|
185
|
+
console.log(
|
|
186
|
+
`[SOURCEMAP] ${total} scripts: ` +
|
|
187
|
+
`${stats.loaded} maps loaded, ` +
|
|
188
|
+
`${stats.failed} failed${failNote}, ` +
|
|
189
|
+
`${stats.withoutMap} had no map`
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|