@maxiaochao/pi-toolkit 0.1.9 → 0.2.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.
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn, execFileSync, execSync } from "node:child_process";
4
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const DEBUG_HOST = process.env.BROWSER_DEBUG_HOST || "localhost";
10
+ const DEBUG_PORT = Number(process.env.BROWSER_DEBUG_PORT || 9222);
11
+
12
+ if (!Number.isInteger(DEBUG_PORT) || DEBUG_PORT < 1 || DEBUG_PORT > 65535) {
13
+ console.error("✗ Invalid BROWSER_DEBUG_PORT (expected 1-65535)");
14
+ process.exit(1);
15
+ }
16
+
17
+ const args = new Set(process.argv.slice(2));
18
+ const headless = args.has("--headless");
19
+ const useProfile = args.has("--profile");
20
+ const resetProfile = args.has("--reset-profile");
21
+
22
+ const unknownArgs = [...args].filter(
23
+ (arg) =>
24
+ arg !== "--headless" && arg !== "--profile" && arg !== "--reset-profile",
25
+ );
26
+ if (unknownArgs.length > 0) {
27
+ console.log("Usage: start.js [--headless] [--profile] [--reset-profile]");
28
+ console.log("\nOptions:");
29
+ console.log(" --headless Run Chrome without opening a visible window");
30
+ console.log(
31
+ " --profile Copy your default Chrome profile into an isolated cache",
32
+ );
33
+ console.log(" --reset-profile Clear the selected cached profile before launch");
34
+ console.log("\nExamples:");
35
+ console.log(" start.js --headless");
36
+ console.log(" start.js");
37
+ console.log(" start.js --headless --profile");
38
+ console.log(" start.js --headless --reset-profile");
39
+ process.exit(1);
40
+ }
41
+
42
+ const HOME = process.env["HOME"] || homedir();
43
+ const CACHE_ROOT = join(HOME, ".cache", "agent-web");
44
+ const BROWSER_ROOT = join(CACHE_ROOT, "browser");
45
+ const FRESH_PROFILE_DIR = join(BROWSER_ROOT, "fresh-profile");
46
+ const PROFILE_COPY_DIR = join(BROWSER_ROOT, "profile-copy");
47
+ const STATE_FILE = join(BROWSER_ROOT, "state.json");
48
+ const IS_WSL = Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP);
49
+
50
+ function commandOutput(command, args) {
51
+ return execFileSync(command, args, { encoding: "utf8" }).trim();
52
+ }
53
+
54
+ function windowsPath(path) {
55
+ return commandOutput("wslpath", ["-w", path]);
56
+ }
57
+
58
+ function linuxPath(path) {
59
+ return commandOutput("wslpath", ["-u", path]);
60
+ }
61
+
62
+ function powershellQuote(value) {
63
+ return `'${String(value).replaceAll("'", "''")}'`;
64
+ }
65
+
66
+ function resolveChromeBinary() {
67
+ if (process.env.BROWSER_BIN && existsSync(process.env.BROWSER_BIN)) {
68
+ return process.env.BROWSER_BIN;
69
+ }
70
+
71
+ const candidates = [
72
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
73
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
74
+ "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
75
+ "/usr/bin/google-chrome",
76
+ "/usr/bin/google-chrome-stable",
77
+ "/usr/bin/chromium",
78
+ "/usr/bin/chromium-browser",
79
+ "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
80
+ "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
81
+ ];
82
+
83
+ return candidates.find((path) => existsSync(path)) || null;
84
+ }
85
+
86
+ const chromeBinary = resolveChromeBinary();
87
+
88
+ if (!chromeBinary) {
89
+ console.error("✗ Could not find Chrome/Chromium binary");
90
+ console.error(" Set BROWSER_BIN=/path/to/chrome and retry");
91
+ process.exit(1);
92
+ }
93
+
94
+ const usesWindowsChrome =
95
+ IS_WSL &&
96
+ (/^\/mnt\/[a-z]\//i.test(chromeBinary) || /\.exe$/i.test(chromeBinary));
97
+ const windowsLocalAppData = usesWindowsChrome
98
+ ? commandOutput("powershell.exe", [
99
+ "-NoProfile",
100
+ "-NonInteractive",
101
+ "-Command",
102
+ "[Environment]::GetFolderPath('LocalApplicationData')",
103
+ ])
104
+ : null;
105
+ const mode = useProfile ? "profile-copy" : "fresh";
106
+ const profileDirectoryName = useProfile ? "profile-copy" : "fresh-profile";
107
+ const userDataDir = usesWindowsChrome
108
+ ? `${windowsLocalAppData}\\agent-web\\browser\\${profileDirectoryName}`
109
+ : useProfile
110
+ ? PROFILE_COPY_DIR
111
+ : FRESH_PROFILE_DIR;
112
+ const userDataFsDir = usesWindowsChrome ? linuxPath(userDataDir) : userDataDir;
113
+
114
+ function ensureDir(path) {
115
+ if (!existsSync(path)) {
116
+ mkdirSync(path, { recursive: true });
117
+ }
118
+ }
119
+
120
+ function isProcessAlive(pid) {
121
+ if (!pid || typeof pid !== "number") return false;
122
+ try {
123
+ process.kill(pid, 0);
124
+ return true;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ function readState() {
131
+ if (!existsSync(STATE_FILE)) return null;
132
+ try {
133
+ return JSON.parse(readFileSync(STATE_FILE, "utf8"));
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+
139
+ function writeState(state) {
140
+ ensureDir(BROWSER_ROOT);
141
+ writeFileSync(STATE_FILE, `${JSON.stringify(state, null, 2)}\n`);
142
+ }
143
+
144
+ function clearState() {
145
+ try {
146
+ rmSync(STATE_FILE, { force: true });
147
+ } catch {
148
+ // ignore
149
+ }
150
+ }
151
+
152
+ async function isDebugEndpointUp() {
153
+ try {
154
+ const response = await fetch(
155
+ `http://${DEBUG_HOST}:${DEBUG_PORT}/json/version`,
156
+ );
157
+ return response.ok;
158
+ } catch {
159
+ return false;
160
+ }
161
+ }
162
+
163
+ ensureDir(BROWSER_ROOT);
164
+
165
+ if (resetProfile) {
166
+ rmSync(userDataFsDir, { recursive: true, force: true });
167
+ }
168
+
169
+ const state = readState();
170
+ if (state?.pid && !isProcessAlive(state.pid)) {
171
+ clearState();
172
+ }
173
+
174
+ if (await isDebugEndpointUp()) {
175
+ const runningState = readState();
176
+
177
+ if (
178
+ runningState?.port === DEBUG_PORT &&
179
+ ((runningState.external === true && usesWindowsChrome) ||
180
+ (runningState.pid && isProcessAlive(runningState.pid)))
181
+ ) {
182
+ const runningHeadless = runningState.headless === true;
183
+ const runningExtensionsDisabled =
184
+ runningState.extensionsDisabled === true;
185
+ if (
186
+ runningState.mode === mode &&
187
+ runningState.userDataDir === userDataDir &&
188
+ runningHeadless === headless &&
189
+ runningExtensionsDisabled === headless
190
+ ) {
191
+ console.log(
192
+ `✓ Chrome already running on :${DEBUG_PORT} (${headless ? "headless" : "headed"}, ${mode} profile)`,
193
+ );
194
+ process.exit(0);
195
+ }
196
+
197
+ console.error(
198
+ `✗ Chrome already running on :${DEBUG_PORT} (${runningHeadless ? "headless" : "headed"}, ${runningState.mode} profile)`,
199
+ );
200
+ console.error(" Close it first before switching launch or profile modes.");
201
+ process.exit(1);
202
+ }
203
+
204
+ console.error(`✗ Debugging endpoint :${DEBUG_PORT} is already in use`);
205
+ console.error(
206
+ " Refusing to reuse unknown instance to avoid attaching to your regular profile.",
207
+ );
208
+ console.error(
209
+ ` Close the process using :${DEBUG_PORT} or set BROWSER_DEBUG_PORT to a different port.`,
210
+ );
211
+ process.exit(1);
212
+ }
213
+
214
+ ensureDir(userDataFsDir);
215
+
216
+ if (useProfile) {
217
+ const sourceProfileDir = usesWindowsChrome
218
+ ? linuxPath(`${windowsLocalAppData}\\Google\\Chrome\\User Data`)
219
+ : process.platform === "darwin"
220
+ ? join(HOME, "Library", "Application Support", "Google", "Chrome")
221
+ : join(HOME, ".config", "google-chrome");
222
+
223
+ if (!existsSync(sourceProfileDir)) {
224
+ console.error("✗ Could not find your local Chrome profile directory");
225
+ console.error(` Expected: ${sourceProfileDir}`);
226
+ process.exit(1);
227
+ }
228
+
229
+ execSync(
230
+ `rsync -a --delete --exclude 'Singleton*' --exclude 'DevToolsActivePort*' "${sourceProfileDir}/" "${userDataFsDir}/"`,
231
+ { stdio: "pipe" },
232
+ );
233
+ }
234
+
235
+ for (const staleFile of [
236
+ "SingletonCookie",
237
+ "SingletonLock",
238
+ "SingletonSocket",
239
+ "DevToolsActivePort",
240
+ "DevToolsActivePort.lock",
241
+ ]) {
242
+ try {
243
+ rmSync(join(userDataFsDir, staleFile), { force: true });
244
+ } catch {
245
+ // ignore
246
+ }
247
+ }
248
+
249
+ const chromeArgs = [
250
+ `--remote-debugging-port=${DEBUG_PORT}`,
251
+ `--user-data-dir=${userDataDir}`,
252
+ "--profile-directory=Default",
253
+ "--disable-search-engine-choice-screen",
254
+ "--no-first-run",
255
+ "--no-default-browser-check",
256
+ "--disable-features=ProfilePicker",
257
+ "--enable-automation",
258
+ ];
259
+
260
+ if (headless) {
261
+ // Profile copies may contain extensions that depend on headed-only native
262
+ // integrations. Keep their data in the copy, but do not load them in
263
+ // headless automation.
264
+ chromeArgs.push("--headless", "--disable-extensions");
265
+ }
266
+
267
+ let chromePid = null;
268
+ let external = false;
269
+
270
+ if (usesWindowsChrome) {
271
+ const chromeWindowsPath = windowsPath(chromeBinary);
272
+ const argumentList = chromeArgs.map(powershellQuote).join(", ");
273
+ const command = `Start-Process -FilePath ${powershellQuote(chromeWindowsPath)} -ArgumentList @(${argumentList})`;
274
+ execFileSync(
275
+ "powershell.exe",
276
+ ["-NoProfile", "-NonInteractive", "-Command", command],
277
+ { stdio: "ignore" },
278
+ );
279
+ external = true;
280
+ } else {
281
+ const chromeProc = spawn(chromeBinary, chromeArgs, {
282
+ detached: true,
283
+ stdio: "ignore",
284
+ });
285
+ chromeProc.unref();
286
+ chromePid = chromeProc.pid;
287
+ }
288
+
289
+ let connected = false;
290
+ for (let i = 0; i < 30; i++) {
291
+ if (await isDebugEndpointUp()) {
292
+ connected = true;
293
+ break;
294
+ }
295
+ await new Promise((r) => setTimeout(r, 500));
296
+ }
297
+
298
+ if (!connected) {
299
+ console.error(`✗ Failed to connect to Chrome on :${DEBUG_PORT}`);
300
+ console.error(` Attempted binary: ${chromeBinary}`);
301
+ process.exit(1);
302
+ }
303
+
304
+ writeState({
305
+ pid: chromePid,
306
+ external,
307
+ chromeBinary,
308
+ mode,
309
+ headless,
310
+ extensionsDisabled: headless,
311
+ userDataDir,
312
+ port: DEBUG_PORT,
313
+ startedAt: new Date().toISOString(),
314
+ });
315
+
316
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
317
+ const watcherPath = join(scriptDir, "watch.js");
318
+ spawn(process.execPath, [watcherPath], { detached: true, stdio: "ignore" }).unref();
319
+
320
+ console.log(
321
+ `✓ Chrome started on :${DEBUG_PORT} in ${headless ? "headless" : "headed"} mode with ${useProfile ? "profile-copy" : "fresh"} profile`,
322
+ );
323
+ if (!useProfile) {
324
+ console.log(` profile dir: ${userDataDir}`);
325
+ }
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { connect } from "./cdp.js";
7
+
8
+ const LOG_ROOT = join(homedir(), ".cache/agent-web/logs");
9
+ const PID_FILE = join(LOG_ROOT, ".pid");
10
+
11
+ function ensureDir(dir) {
12
+ if (!existsSync(dir)) {
13
+ mkdirSync(dir, { recursive: true });
14
+ }
15
+ }
16
+
17
+ function isProcessAlive(pid) {
18
+ try {
19
+ process.kill(pid, 0);
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ function getDateDir() {
27
+ const now = new Date();
28
+ const yyyy = String(now.getFullYear());
29
+ const mm = String(now.getMonth() + 1).padStart(2, "0");
30
+ const dd = String(now.getDate()).padStart(2, "0");
31
+ return join(LOG_ROOT, `${yyyy}-${mm}-${dd}`);
32
+ }
33
+
34
+ function safeFileName(value) {
35
+ return value.replace(/[^a-zA-Z0-9._-]/g, "_");
36
+ }
37
+
38
+ function compactStack(stackTrace) {
39
+ if (!stackTrace || !Array.isArray(stackTrace.callFrames)) return null;
40
+ return stackTrace.callFrames.slice(0, 8).map((frame) => ({
41
+ functionName: frame.functionName || null,
42
+ url: frame.url || null,
43
+ lineNumber: frame.lineNumber,
44
+ columnNumber: frame.columnNumber,
45
+ }));
46
+ }
47
+
48
+ function serializeRemoteObject(obj) {
49
+ if (!obj || typeof obj !== "object") return obj;
50
+ const value =
51
+ Object.prototype.hasOwnProperty.call(obj, "value")
52
+ ? obj.value
53
+ : obj.unserializableValue || obj.description || null;
54
+ return {
55
+ type: obj.type || null,
56
+ subtype: obj.subtype || null,
57
+ value,
58
+ description: obj.description || null,
59
+ };
60
+ }
61
+
62
+ ensureDir(LOG_ROOT);
63
+
64
+ if (existsSync(PID_FILE)) {
65
+ try {
66
+ const existing = Number(readFileSync(PID_FILE, "utf8").trim());
67
+ if (existing && isProcessAlive(existing)) {
68
+ console.log("✓ watch already running");
69
+ process.exit(0);
70
+ }
71
+ } catch {
72
+ // Ignore and overwrite stale pid.
73
+ }
74
+ }
75
+
76
+ writeFileSync(PID_FILE, String(process.pid));
77
+
78
+ const dateDir = getDateDir();
79
+ ensureDir(dateDir);
80
+
81
+ const targetState = new Map();
82
+ const sessionToTarget = new Map();
83
+
84
+ function getStreamForTarget(targetId) {
85
+ const state = targetState.get(targetId);
86
+ if (state?.stream) return state.stream;
87
+ const filename = `${safeFileName(targetId)}.jsonl`;
88
+ const filepath = join(dateDir, filename);
89
+ const stream = createWriteStream(filepath, { flags: "a" });
90
+ if (state) state.stream = stream;
91
+ return stream;
92
+ }
93
+
94
+ function writeLog(targetId, payload) {
95
+ const stream = getStreamForTarget(targetId);
96
+ const record = {
97
+ ts: new Date().toISOString(),
98
+ targetId,
99
+ ...payload,
100
+ };
101
+ stream.write(`${JSON.stringify(record)}\n`);
102
+ }
103
+
104
+ async function enableSession(cdp, sessionId) {
105
+ await cdp.send("Runtime.enable", {}, sessionId);
106
+ await cdp.send("Log.enable", {}, sessionId);
107
+ await cdp.send("Network.enable", {}, sessionId);
108
+ await cdp.send("Page.enable", {}, sessionId);
109
+ }
110
+
111
+ async function attachToTarget(cdp, targetInfo) {
112
+ if (targetInfo.type !== "page") return;
113
+ if (targetState.has(targetInfo.targetId)) return;
114
+
115
+ const { sessionId } = await cdp.send("Target.attachToTarget", {
116
+ targetId: targetInfo.targetId,
117
+ flatten: true,
118
+ });
119
+
120
+ targetState.set(targetInfo.targetId, {
121
+ sessionId,
122
+ url: targetInfo.url || null,
123
+ title: targetInfo.title || null,
124
+ stream: null,
125
+ });
126
+ sessionToTarget.set(sessionId, targetInfo.targetId);
127
+
128
+ await enableSession(cdp, sessionId);
129
+ writeLog(targetInfo.targetId, {
130
+ type: "target.attached",
131
+ url: targetInfo.url || null,
132
+ title: targetInfo.title || null,
133
+ });
134
+ }
135
+
136
+ async function main() {
137
+ const cdp = await connect(5000);
138
+
139
+ cdp.on("Target.targetCreated", async (params) => {
140
+ try {
141
+ await attachToTarget(cdp, params.targetInfo);
142
+ } catch (e) {
143
+ console.error("watch: attach error:", e.message);
144
+ }
145
+ });
146
+
147
+ cdp.on("Target.targetDestroyed", (params) => {
148
+ const targetId = params.targetId;
149
+ const state = targetState.get(targetId);
150
+ if (state?.stream) state.stream.end();
151
+ targetState.delete(targetId);
152
+ if (state?.sessionId) sessionToTarget.delete(state.sessionId);
153
+ });
154
+
155
+ cdp.on("Target.targetInfoChanged", (params) => {
156
+ const info = params.targetInfo;
157
+ const state = targetState.get(info.targetId);
158
+ if (state) {
159
+ state.url = info.url || state.url;
160
+ state.title = info.title || state.title;
161
+ writeLog(info.targetId, {
162
+ type: "target.info",
163
+ url: info.url || null,
164
+ title: info.title || null,
165
+ });
166
+ }
167
+ });
168
+
169
+ cdp.on("Runtime.consoleAPICalled", (params, sessionId) => {
170
+ const targetId = sessionToTarget.get(sessionId);
171
+ if (!targetId) return;
172
+ writeLog(targetId, {
173
+ type: "console",
174
+ level: params.type || null,
175
+ args: (params.args || []).map(serializeRemoteObject),
176
+ stack: compactStack(params.stackTrace),
177
+ });
178
+ });
179
+
180
+ cdp.on("Runtime.exceptionThrown", (params, sessionId) => {
181
+ const targetId = sessionToTarget.get(sessionId);
182
+ if (!targetId) return;
183
+ const details = params.exceptionDetails || {};
184
+ writeLog(targetId, {
185
+ type: "exception",
186
+ text: details.text || null,
187
+ description: details.exception?.description || null,
188
+ lineNumber: details.lineNumber,
189
+ columnNumber: details.columnNumber,
190
+ url: details.url || null,
191
+ stack: compactStack(details.stackTrace),
192
+ });
193
+ });
194
+
195
+ cdp.on("Log.entryAdded", (params, sessionId) => {
196
+ const targetId = sessionToTarget.get(sessionId);
197
+ if (!targetId) return;
198
+ const entry = params.entry || {};
199
+ writeLog(targetId, {
200
+ type: "log",
201
+ level: entry.level || null,
202
+ source: entry.source || null,
203
+ text: entry.text || null,
204
+ url: entry.url || null,
205
+ lineNumber: entry.lineNumber ?? null,
206
+ });
207
+ });
208
+
209
+ cdp.on("Network.requestWillBeSent", (params, sessionId) => {
210
+ const targetId = sessionToTarget.get(sessionId);
211
+ if (!targetId) return;
212
+ const request = params.request || {};
213
+ writeLog(targetId, {
214
+ type: "network.request",
215
+ requestId: params.requestId,
216
+ method: request.method || null,
217
+ url: request.url || null,
218
+ documentURL: params.documentURL || null,
219
+ initiator: params.initiator?.type || null,
220
+ hasPostData: !!request.hasPostData,
221
+ });
222
+ });
223
+
224
+ cdp.on("Network.responseReceived", (params, sessionId) => {
225
+ const targetId = sessionToTarget.get(sessionId);
226
+ if (!targetId) return;
227
+ const response = params.response || {};
228
+ writeLog(targetId, {
229
+ type: "network.response",
230
+ requestId: params.requestId,
231
+ url: response.url || null,
232
+ status: response.status,
233
+ statusText: response.statusText || null,
234
+ mimeType: response.mimeType || null,
235
+ fromDiskCache: !!response.fromDiskCache,
236
+ fromServiceWorker: !!response.fromServiceWorker,
237
+ });
238
+ });
239
+
240
+ cdp.on("Network.loadingFailed", (params, sessionId) => {
241
+ const targetId = sessionToTarget.get(sessionId);
242
+ if (!targetId) return;
243
+ writeLog(targetId, {
244
+ type: "network.failure",
245
+ requestId: params.requestId,
246
+ errorText: params.errorText || null,
247
+ canceled: !!params.canceled,
248
+ });
249
+ });
250
+
251
+ await cdp.send("Target.setDiscoverTargets", { discover: true });
252
+
253
+ const pages = await cdp.getPages();
254
+ for (const page of pages) {
255
+ await attachToTarget(cdp, page);
256
+ }
257
+
258
+ console.log("✓ watch started");
259
+ }
260
+
261
+ try {
262
+ await main();
263
+ } catch (e) {
264
+ console.error("✗ watch failed:", e.message);
265
+ process.exit(1);
266
+ }