mitsupi 1.0.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.
Files changed (77) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +95 -0
  3. package/TODO.md +11 -0
  4. package/commands/handoff.md +100 -0
  5. package/commands/make-release.md +75 -0
  6. package/commands/pickup.md +30 -0
  7. package/commands/update-changelog.md +78 -0
  8. package/package.json +22 -0
  9. package/pi-extensions/answer.ts +527 -0
  10. package/pi-extensions/codex-tuning.ts +632 -0
  11. package/pi-extensions/commit.ts +248 -0
  12. package/pi-extensions/cwd-history.ts +237 -0
  13. package/pi-extensions/issues.ts +548 -0
  14. package/pi-extensions/loop.ts +446 -0
  15. package/pi-extensions/qna.ts +167 -0
  16. package/pi-extensions/reveal.ts +689 -0
  17. package/pi-extensions/review.ts +807 -0
  18. package/pi-themes/armin.json +81 -0
  19. package/pi-themes/nightowl.json +82 -0
  20. package/skills/anachb/SKILL.md +183 -0
  21. package/skills/anachb/departures.sh +79 -0
  22. package/skills/anachb/disruptions.sh +53 -0
  23. package/skills/anachb/route.sh +87 -0
  24. package/skills/anachb/search.sh +43 -0
  25. package/skills/ghidra/SKILL.md +254 -0
  26. package/skills/ghidra/scripts/find-ghidra.sh +54 -0
  27. package/skills/ghidra/scripts/ghidra-analyze.sh +239 -0
  28. package/skills/ghidra/scripts/ghidra_scripts/ExportAll.java +278 -0
  29. package/skills/ghidra/scripts/ghidra_scripts/ExportCalls.java +148 -0
  30. package/skills/ghidra/scripts/ghidra_scripts/ExportDecompiled.java +84 -0
  31. package/skills/ghidra/scripts/ghidra_scripts/ExportFunctions.java +114 -0
  32. package/skills/ghidra/scripts/ghidra_scripts/ExportStrings.java +123 -0
  33. package/skills/ghidra/scripts/ghidra_scripts/ExportSymbols.java +135 -0
  34. package/skills/github/SKILL.md +47 -0
  35. package/skills/improve-skill/SKILL.md +155 -0
  36. package/skills/improve-skill/scripts/extract-session.js +349 -0
  37. package/skills/oebb-scotty/SKILL.md +429 -0
  38. package/skills/oebb-scotty/arrivals.sh +83 -0
  39. package/skills/oebb-scotty/departures.sh +83 -0
  40. package/skills/oebb-scotty/disruptions.sh +33 -0
  41. package/skills/oebb-scotty/search-station.sh +36 -0
  42. package/skills/oebb-scotty/trip.sh +119 -0
  43. package/skills/openscad/SKILL.md +232 -0
  44. package/skills/openscad/examples/parametric_box.scad +92 -0
  45. package/skills/openscad/examples/phone_stand.scad +95 -0
  46. package/skills/openscad/tools/common.sh +50 -0
  47. package/skills/openscad/tools/export-stl.sh +56 -0
  48. package/skills/openscad/tools/extract-params.sh +147 -0
  49. package/skills/openscad/tools/multi-preview.sh +68 -0
  50. package/skills/openscad/tools/preview.sh +74 -0
  51. package/skills/openscad/tools/render-with-params.sh +91 -0
  52. package/skills/openscad/tools/validate.sh +46 -0
  53. package/skills/pi-share/SKILL.md +105 -0
  54. package/skills/pi-share/fetch-session.mjs +322 -0
  55. package/skills/sentry/SKILL.md +239 -0
  56. package/skills/sentry/lib/auth.js +99 -0
  57. package/skills/sentry/scripts/fetch-event.js +329 -0
  58. package/skills/sentry/scripts/fetch-issue.js +356 -0
  59. package/skills/sentry/scripts/list-issues.js +239 -0
  60. package/skills/sentry/scripts/search-events.js +291 -0
  61. package/skills/sentry/scripts/search-logs.js +240 -0
  62. package/skills/tmux/SKILL.md +105 -0
  63. package/skills/tmux/scripts/find-sessions.sh +112 -0
  64. package/skills/tmux/scripts/wait-for-text.sh +83 -0
  65. package/skills/web-browser/SKILL.md +91 -0
  66. package/skills/web-browser/scripts/cdp.js +210 -0
  67. package/skills/web-browser/scripts/dismiss-cookies.js +373 -0
  68. package/skills/web-browser/scripts/eval.js +68 -0
  69. package/skills/web-browser/scripts/logs-tail.js +69 -0
  70. package/skills/web-browser/scripts/nav.js +65 -0
  71. package/skills/web-browser/scripts/net-summary.js +94 -0
  72. package/skills/web-browser/scripts/package-lock.json +33 -0
  73. package/skills/web-browser/scripts/package.json +6 -0
  74. package/skills/web-browser/scripts/pick.js +165 -0
  75. package/skills/web-browser/scripts/screenshot.js +52 -0
  76. package/skills/web-browser/scripts/start.js +80 -0
  77. package/skills/web-browser/scripts/watch.js +266 -0
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readdirSync, readFileSync, statSync, watch } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ const LOG_ROOT = join(homedir(), ".cache/agent-web/logs");
8
+
9
+ function findLatestFile() {
10
+ if (!existsSync(LOG_ROOT)) return null;
11
+ const dirs = readdirSync(LOG_ROOT)
12
+ .filter((name) => /^\d{4}-\d{2}-\d{2}$/.test(name))
13
+ .map((name) => join(LOG_ROOT, name))
14
+ .filter((path) => statSafe(path)?.isDirectory())
15
+ .sort();
16
+ if (dirs.length === 0) return null;
17
+ const latestDir = dirs[dirs.length - 1];
18
+ const files = readdirSync(latestDir)
19
+ .filter((name) => name.endsWith(".jsonl"))
20
+ .map((name) => join(latestDir, name))
21
+ .map((path) => ({ path, mtime: statSafe(path)?.mtimeMs || 0 }))
22
+ .sort((a, b) => b.mtime - a.mtime);
23
+ return files[0]?.path || null;
24
+ }
25
+
26
+ function statSafe(path) {
27
+ try {
28
+ return statSync(path);
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ const argIndex = process.argv.indexOf("--file");
35
+ const filePath = argIndex !== -1 ? process.argv[argIndex + 1] : findLatestFile();
36
+ const follow = process.argv.includes("--follow");
37
+
38
+ if (!filePath) {
39
+ console.error("✗ No log file found");
40
+ process.exit(1);
41
+ }
42
+
43
+ function readAll() {
44
+ if (!existsSync(filePath)) return;
45
+ const data = readFileSync(filePath, "utf8");
46
+ if (data.length > 0) process.stdout.write(data);
47
+ }
48
+
49
+ let offset = 0;
50
+
51
+ function readNew() {
52
+ if (!existsSync(filePath)) return;
53
+ const data = readFileSync(filePath, "utf8");
54
+ if (data.length <= offset) return;
55
+ const chunk = data.slice(offset);
56
+ offset = data.length;
57
+ process.stdout.write(chunk);
58
+ }
59
+
60
+ try {
61
+ readAll();
62
+ if (!follow) process.exit(0);
63
+ offset = statSafe(filePath)?.size || 0;
64
+ watch(filePath, { persistent: true }, () => readNew());
65
+ console.log(`✓ tailing ${filePath}`);
66
+ } catch (e) {
67
+ console.error("✗ tail failed:", e.message);
68
+ process.exit(1);
69
+ }
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { connect } from "./cdp.js";
4
+
5
+ const DEBUG = process.env.DEBUG === "1";
6
+ const log = DEBUG ? (...args) => console.error("[debug]", ...args) : () => {};
7
+
8
+ const url = process.argv[2];
9
+ const newTab = process.argv[3] === "--new";
10
+
11
+ if (!url) {
12
+ console.log("Usage: nav.js <url> [--new]");
13
+ console.log("\nExamples:");
14
+ console.log(" nav.js https://example.com # Navigate current tab");
15
+ console.log(" nav.js https://example.com --new # Open in new tab");
16
+ process.exit(1);
17
+ }
18
+
19
+ // Global timeout
20
+ const globalTimeout = setTimeout(() => {
21
+ console.error("✗ Global timeout exceeded (45s)");
22
+ process.exit(1);
23
+ }, 45000);
24
+
25
+ try {
26
+ log("connecting...");
27
+ const cdp = await connect(5000);
28
+
29
+ log("getting pages...");
30
+ let targetId;
31
+
32
+ if (newTab) {
33
+ log("creating new tab...");
34
+ const { targetId: newTargetId } = await cdp.send("Target.createTarget", {
35
+ url: "about:blank",
36
+ });
37
+ targetId = newTargetId;
38
+ } else {
39
+ const pages = await cdp.getPages();
40
+ const page = pages.at(-1);
41
+ if (!page) {
42
+ console.error("✗ No active tab found");
43
+ process.exit(1);
44
+ }
45
+ targetId = page.targetId;
46
+ }
47
+
48
+ log("attaching to page...");
49
+ const sessionId = await cdp.attachToPage(targetId);
50
+
51
+ log("navigating...");
52
+ await cdp.navigate(sessionId, url);
53
+
54
+ console.log(newTab ? "✓ Opened:" : "✓ Navigated to:", url);
55
+
56
+ log("closing...");
57
+ cdp.close();
58
+ log("done");
59
+ } catch (e) {
60
+ console.error("✗", e.message);
61
+ process.exit(1);
62
+ } finally {
63
+ clearTimeout(globalTimeout);
64
+ setTimeout(() => process.exit(0), 100);
65
+ }
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ const LOG_ROOT = join(homedir(), ".cache/agent-web/logs");
8
+
9
+ function statSafe(path) {
10
+ try {
11
+ return statSync(path);
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ function findLatestFile() {
18
+ if (!existsSync(LOG_ROOT)) return null;
19
+ const dirs = readdirSync(LOG_ROOT)
20
+ .filter((name) => /^\d{4}-\d{2}-\d{2}$/.test(name))
21
+ .map((name) => join(LOG_ROOT, name))
22
+ .filter((path) => statSafe(path)?.isDirectory())
23
+ .sort();
24
+ if (dirs.length === 0) return null;
25
+ const latestDir = dirs[dirs.length - 1];
26
+ const files = readdirSync(latestDir)
27
+ .filter((name) => name.endsWith(".jsonl"))
28
+ .map((name) => join(latestDir, name))
29
+ .map((path) => ({ path, mtime: statSafe(path)?.mtimeMs || 0 }))
30
+ .sort((a, b) => b.mtime - a.mtime);
31
+ return files[0]?.path || null;
32
+ }
33
+
34
+ const argIndex = process.argv.indexOf("--file");
35
+ const filePath = argIndex !== -1 ? process.argv[argIndex + 1] : findLatestFile();
36
+
37
+ if (!filePath) {
38
+ console.error("✗ No log file found");
39
+ process.exit(1);
40
+ }
41
+
42
+ const statusCounts = new Map();
43
+ const failures = [];
44
+ let totalResponses = 0;
45
+ let totalRequests = 0;
46
+
47
+ try {
48
+ const data = readFileSync(filePath, "utf8");
49
+ const lines = data.split("\n").filter(Boolean);
50
+ for (const line of lines) {
51
+ let entry;
52
+ try {
53
+ entry = JSON.parse(line);
54
+ } catch {
55
+ continue;
56
+ }
57
+ if (entry.type === "network.request") {
58
+ totalRequests += 1;
59
+ } else if (entry.type === "network.response") {
60
+ totalResponses += 1;
61
+ const status = String(entry.status ?? "unknown");
62
+ statusCounts.set(status, (statusCounts.get(status) || 0) + 1);
63
+ } else if (entry.type === "network.failure") {
64
+ failures.push({
65
+ requestId: entry.requestId,
66
+ errorText: entry.errorText,
67
+ });
68
+ }
69
+ }
70
+ } catch (e) {
71
+ console.error("✗ summary failed:", e.message);
72
+ process.exit(1);
73
+ }
74
+
75
+ console.log(`file: ${filePath}`);
76
+ console.log(`requests: ${totalRequests}`);
77
+ console.log(`responses: ${totalResponses}`);
78
+
79
+ const statuses = Array.from(statusCounts.entries()).sort(
80
+ (a, b) => Number(a[0]) - Number(b[0]),
81
+ );
82
+ for (const [status, count] of statuses) {
83
+ console.log(`status ${status}: ${count}`);
84
+ }
85
+
86
+ if (failures.length > 0) {
87
+ console.log("failures:");
88
+ for (const failure of failures.slice(0, 10)) {
89
+ console.log(`- ${failure.errorText || "unknown"} (${failure.requestId})`);
90
+ }
91
+ if (failures.length > 10) {
92
+ console.log(`- ... ${failures.length - 10} more`);
93
+ }
94
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "scripts",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "dependencies": {
8
+ "ws": "^8.18.0"
9
+ }
10
+ },
11
+ "node_modules/ws": {
12
+ "version": "8.19.0",
13
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
14
+ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
15
+ "license": "MIT",
16
+ "engines": {
17
+ "node": ">=10.0.0"
18
+ },
19
+ "peerDependencies": {
20
+ "bufferutil": "^4.0.1",
21
+ "utf-8-validate": ">=5.0.2"
22
+ },
23
+ "peerDependenciesMeta": {
24
+ "bufferutil": {
25
+ "optional": true
26
+ },
27
+ "utf-8-validate": {
28
+ "optional": true
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "type": "module",
3
+ "dependencies": {
4
+ "ws": "^8.18.0"
5
+ }
6
+ }
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { connect } from "./cdp.js";
4
+
5
+ const DEBUG = process.env.DEBUG === "1";
6
+ const log = DEBUG ? (...args) => console.error("[debug]", ...args) : () => {};
7
+
8
+ const message = process.argv.slice(2).join(" ");
9
+ if (!message) {
10
+ console.log("Usage: pick.js 'message'");
11
+ console.log("\nExample:");
12
+ console.log(' pick.js "Click the submit button"');
13
+ process.exit(1);
14
+ }
15
+
16
+ // Global timeout - 5 minutes for interactive picking
17
+ const globalTimeout = setTimeout(() => {
18
+ console.error("✗ Global timeout exceeded (5m)");
19
+ process.exit(1);
20
+ }, 300000);
21
+
22
+ const PICK_SCRIPT = `(message) => {
23
+ if (!message) throw new Error("pick() requires a message parameter");
24
+ return new Promise((resolve) => {
25
+ const selections = [];
26
+ const selectedElements = new Set();
27
+
28
+ const overlay = document.createElement("div");
29
+ overlay.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;pointer-events:none";
30
+
31
+ const highlight = document.createElement("div");
32
+ highlight.style.cssText = "position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);transition:all 0.1s";
33
+ overlay.appendChild(highlight);
34
+
35
+ const banner = document.createElement("div");
36
+ banner.style.cssText = "position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#1f2937;color:white;padding:12px 24px;border-radius:8px;font:14px sans-serif;box-shadow:0 4px 12px rgba(0,0,0,0.3);pointer-events:auto;z-index:2147483647";
37
+
38
+ const updateBanner = () => {
39
+ banner.textContent = message + " (" + selections.length + " selected, Cmd/Ctrl+click to add, Enter to finish, ESC to cancel)";
40
+ };
41
+ updateBanner();
42
+
43
+ document.body.append(banner, overlay);
44
+
45
+ const cleanup = () => {
46
+ document.removeEventListener("mousemove", onMove, true);
47
+ document.removeEventListener("click", onClick, true);
48
+ document.removeEventListener("keydown", onKey, true);
49
+ overlay.remove();
50
+ banner.remove();
51
+ selectedElements.forEach((el) => { el.style.outline = ""; });
52
+ };
53
+
54
+ const onMove = (e) => {
55
+ const el = document.elementFromPoint(e.clientX, e.clientY);
56
+ if (!el || overlay.contains(el) || banner.contains(el)) return;
57
+ const r = el.getBoundingClientRect();
58
+ highlight.style.cssText = "position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);top:" + r.top + "px;left:" + r.left + "px;width:" + r.width + "px;height:" + r.height + "px";
59
+ };
60
+
61
+ const buildElementInfo = (el) => {
62
+ const parents = [];
63
+ let current = el.parentElement;
64
+ while (current && current !== document.body) {
65
+ const parentInfo = current.tagName.toLowerCase();
66
+ const id = current.id ? "#" + current.id : "";
67
+ const cls = current.className ? "." + current.className.trim().split(/\\s+/).join(".") : "";
68
+ parents.push(parentInfo + id + cls);
69
+ current = current.parentElement;
70
+ }
71
+ return {
72
+ tag: el.tagName.toLowerCase(),
73
+ id: el.id || null,
74
+ class: el.className || null,
75
+ text: (el.textContent || "").trim().slice(0, 200) || null,
76
+ html: el.outerHTML.slice(0, 500),
77
+ parents: parents.join(" > "),
78
+ };
79
+ };
80
+
81
+ const onClick = (e) => {
82
+ if (banner.contains(e.target)) return;
83
+ e.preventDefault();
84
+ e.stopPropagation();
85
+ const el = document.elementFromPoint(e.clientX, e.clientY);
86
+ if (!el || overlay.contains(el) || banner.contains(el)) return;
87
+
88
+ if (e.metaKey || e.ctrlKey) {
89
+ if (!selectedElements.has(el)) {
90
+ selectedElements.add(el);
91
+ el.style.outline = "3px solid #10b981";
92
+ selections.push(buildElementInfo(el));
93
+ updateBanner();
94
+ }
95
+ } else {
96
+ cleanup();
97
+ const info = buildElementInfo(el);
98
+ resolve(selections.length > 0 ? selections : info);
99
+ }
100
+ };
101
+
102
+ const onKey = (e) => {
103
+ if (e.key === "Escape") {
104
+ e.preventDefault();
105
+ cleanup();
106
+ resolve(null);
107
+ } else if (e.key === "Enter" && selections.length > 0) {
108
+ e.preventDefault();
109
+ cleanup();
110
+ resolve(selections);
111
+ }
112
+ };
113
+
114
+ document.addEventListener("mousemove", onMove, true);
115
+ document.addEventListener("click", onClick, true);
116
+ document.addEventListener("keydown", onKey, true);
117
+ });
118
+ }`;
119
+
120
+ try {
121
+ log("connecting...");
122
+ const cdp = await connect(5000);
123
+
124
+ log("getting pages...");
125
+ const pages = await cdp.getPages();
126
+ const page = pages.at(-1);
127
+
128
+ if (!page) {
129
+ console.error("✗ No active tab found");
130
+ process.exit(1);
131
+ }
132
+
133
+ log("attaching to page...");
134
+ const sessionId = await cdp.attachToPage(page.targetId);
135
+
136
+ log("waiting for user pick...");
137
+ const expression = `(${PICK_SCRIPT})(${JSON.stringify(message)})`;
138
+ const result = await cdp.evaluate(sessionId, expression, 300000);
139
+
140
+ log("formatting result...");
141
+ if (Array.isArray(result)) {
142
+ for (let i = 0; i < result.length; i++) {
143
+ if (i > 0) console.log("");
144
+ for (const [key, value] of Object.entries(result[i])) {
145
+ console.log(`${key}: ${value}`);
146
+ }
147
+ }
148
+ } else if (typeof result === "object" && result !== null) {
149
+ for (const [key, value] of Object.entries(result)) {
150
+ console.log(`${key}: ${value}`);
151
+ }
152
+ } else {
153
+ console.log(result);
154
+ }
155
+
156
+ log("closing...");
157
+ cdp.close();
158
+ log("done");
159
+ } catch (e) {
160
+ console.error("✗", e.message);
161
+ process.exit(1);
162
+ } finally {
163
+ clearTimeout(globalTimeout);
164
+ setTimeout(() => process.exit(0), 100);
165
+ }
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { writeFileSync } from "node:fs";
6
+ import { connect } from "./cdp.js";
7
+
8
+ const DEBUG = process.env.DEBUG === "1";
9
+ const log = DEBUG ? (...args) => console.error("[debug]", ...args) : () => {};
10
+
11
+ // Global timeout
12
+ const globalTimeout = setTimeout(() => {
13
+ console.error("✗ Global timeout exceeded (15s)");
14
+ process.exit(1);
15
+ }, 15000);
16
+
17
+ try {
18
+ log("connecting...");
19
+ const cdp = await connect(5000);
20
+
21
+ log("getting pages...");
22
+ const pages = await cdp.getPages();
23
+ const page = pages.at(-1);
24
+
25
+ if (!page) {
26
+ console.error("✗ No active tab found");
27
+ process.exit(1);
28
+ }
29
+
30
+ log("attaching to page...");
31
+ const sessionId = await cdp.attachToPage(page.targetId);
32
+
33
+ log("taking screenshot...");
34
+ const data = await cdp.screenshot(sessionId);
35
+
36
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
37
+ const filename = `screenshot-${timestamp}.png`;
38
+ const filepath = join(tmpdir(), filename);
39
+
40
+ writeFileSync(filepath, data);
41
+ console.log(filepath);
42
+
43
+ log("closing...");
44
+ cdp.close();
45
+ log("done");
46
+ } catch (e) {
47
+ console.error("✗", e.message);
48
+ process.exit(1);
49
+ } finally {
50
+ clearTimeout(globalTimeout);
51
+ setTimeout(() => process.exit(0), 100);
52
+ }
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn, execSync } from "node:child_process";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const useProfile = process.argv[2] === "--profile";
8
+
9
+ if (process.argv[2] && process.argv[2] !== "--profile") {
10
+ console.log("Usage: start.ts [--profile]");
11
+ console.log("\nOptions:");
12
+ console.log(
13
+ " --profile Copy your default Chrome profile (cookies, logins)",
14
+ );
15
+ console.log("\nExamples:");
16
+ console.log(" start.ts # Start with fresh profile");
17
+ console.log(" start.ts --profile # Start with your Chrome profile");
18
+ process.exit(1);
19
+ }
20
+
21
+ // Kill existing Chrome
22
+ try {
23
+ execSync("killall 'Google Chrome'", { stdio: "ignore" });
24
+ } catch {}
25
+
26
+ // Wait a bit for processes to fully die
27
+ await new Promise((r) => setTimeout(r, 1000));
28
+
29
+ // Setup profile directory
30
+ execSync("mkdir -p ~/.cache/scraping", { stdio: "ignore" });
31
+
32
+ if (useProfile) {
33
+ // Sync profile with rsync (much faster on subsequent runs)
34
+ execSync(
35
+ `rsync -a --delete "${process.env["HOME"]}/Library/Application Support/Google/Chrome/" ~/.cache/scraping/`,
36
+ { stdio: "pipe" },
37
+ );
38
+ }
39
+
40
+ // Start Chrome in background (detached so Node can exit)
41
+ spawn(
42
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
43
+ [
44
+ "--remote-debugging-port=9222",
45
+ `--user-data-dir=${process.env["HOME"]}/.cache/scraping`,
46
+ "--profile-directory=Default",
47
+ "--disable-search-engine-choice-screen",
48
+ "--no-first-run",
49
+ "--disable-features=ProfilePicker",
50
+ ],
51
+ { detached: true, stdio: "ignore" },
52
+ ).unref();
53
+
54
+ // Wait for Chrome to be ready by checking the debugging endpoint
55
+ let connected = false;
56
+ for (let i = 0; i < 30; i++) {
57
+ try {
58
+ const response = await fetch("http://localhost:9222/json/version");
59
+ if (response.ok) {
60
+ connected = true;
61
+ break;
62
+ }
63
+ } catch {
64
+ await new Promise((r) => setTimeout(r, 500));
65
+ }
66
+ }
67
+
68
+ if (!connected) {
69
+ console.error("✗ Failed to connect to Chrome");
70
+ process.exit(1);
71
+ }
72
+
73
+ // Start background watcher for logs/network (detached)
74
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
75
+ const watcherPath = join(scriptDir, "watch.js");
76
+ spawn(process.execPath, [watcherPath], { detached: true, stdio: "ignore" }).unref();
77
+
78
+ console.log(
79
+ `✓ Chrome started on :9222${useProfile ? " with your profile" : ""}`,
80
+ );