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.
- package/LICENSE +201 -0
- package/README.md +95 -0
- package/TODO.md +11 -0
- package/commands/handoff.md +100 -0
- package/commands/make-release.md +75 -0
- package/commands/pickup.md +30 -0
- package/commands/update-changelog.md +78 -0
- package/package.json +22 -0
- package/pi-extensions/answer.ts +527 -0
- package/pi-extensions/codex-tuning.ts +632 -0
- package/pi-extensions/commit.ts +248 -0
- package/pi-extensions/cwd-history.ts +237 -0
- package/pi-extensions/issues.ts +548 -0
- package/pi-extensions/loop.ts +446 -0
- package/pi-extensions/qna.ts +167 -0
- package/pi-extensions/reveal.ts +689 -0
- package/pi-extensions/review.ts +807 -0
- package/pi-themes/armin.json +81 -0
- package/pi-themes/nightowl.json +82 -0
- package/skills/anachb/SKILL.md +183 -0
- package/skills/anachb/departures.sh +79 -0
- package/skills/anachb/disruptions.sh +53 -0
- package/skills/anachb/route.sh +87 -0
- package/skills/anachb/search.sh +43 -0
- package/skills/ghidra/SKILL.md +254 -0
- package/skills/ghidra/scripts/find-ghidra.sh +54 -0
- package/skills/ghidra/scripts/ghidra-analyze.sh +239 -0
- package/skills/ghidra/scripts/ghidra_scripts/ExportAll.java +278 -0
- package/skills/ghidra/scripts/ghidra_scripts/ExportCalls.java +148 -0
- package/skills/ghidra/scripts/ghidra_scripts/ExportDecompiled.java +84 -0
- package/skills/ghidra/scripts/ghidra_scripts/ExportFunctions.java +114 -0
- package/skills/ghidra/scripts/ghidra_scripts/ExportStrings.java +123 -0
- package/skills/ghidra/scripts/ghidra_scripts/ExportSymbols.java +135 -0
- package/skills/github/SKILL.md +47 -0
- package/skills/improve-skill/SKILL.md +155 -0
- package/skills/improve-skill/scripts/extract-session.js +349 -0
- package/skills/oebb-scotty/SKILL.md +429 -0
- package/skills/oebb-scotty/arrivals.sh +83 -0
- package/skills/oebb-scotty/departures.sh +83 -0
- package/skills/oebb-scotty/disruptions.sh +33 -0
- package/skills/oebb-scotty/search-station.sh +36 -0
- package/skills/oebb-scotty/trip.sh +119 -0
- package/skills/openscad/SKILL.md +232 -0
- package/skills/openscad/examples/parametric_box.scad +92 -0
- package/skills/openscad/examples/phone_stand.scad +95 -0
- package/skills/openscad/tools/common.sh +50 -0
- package/skills/openscad/tools/export-stl.sh +56 -0
- package/skills/openscad/tools/extract-params.sh +147 -0
- package/skills/openscad/tools/multi-preview.sh +68 -0
- package/skills/openscad/tools/preview.sh +74 -0
- package/skills/openscad/tools/render-with-params.sh +91 -0
- package/skills/openscad/tools/validate.sh +46 -0
- package/skills/pi-share/SKILL.md +105 -0
- package/skills/pi-share/fetch-session.mjs +322 -0
- package/skills/sentry/SKILL.md +239 -0
- package/skills/sentry/lib/auth.js +99 -0
- package/skills/sentry/scripts/fetch-event.js +329 -0
- package/skills/sentry/scripts/fetch-issue.js +356 -0
- package/skills/sentry/scripts/list-issues.js +239 -0
- package/skills/sentry/scripts/search-events.js +291 -0
- package/skills/sentry/scripts/search-logs.js +240 -0
- package/skills/tmux/SKILL.md +105 -0
- package/skills/tmux/scripts/find-sessions.sh +112 -0
- package/skills/tmux/scripts/wait-for-text.sh +83 -0
- package/skills/web-browser/SKILL.md +91 -0
- package/skills/web-browser/scripts/cdp.js +210 -0
- package/skills/web-browser/scripts/dismiss-cookies.js +373 -0
- package/skills/web-browser/scripts/eval.js +68 -0
- package/skills/web-browser/scripts/logs-tail.js +69 -0
- package/skills/web-browser/scripts/nav.js +65 -0
- package/skills/web-browser/scripts/net-summary.js +94 -0
- package/skills/web-browser/scripts/package-lock.json +33 -0
- package/skills/web-browser/scripts/package.json +6 -0
- package/skills/web-browser/scripts/pick.js +165 -0
- package/skills/web-browser/scripts/screenshot.js +52 -0
- package/skills/web-browser/scripts/start.js +80 -0
- 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,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
|
+
);
|