@blxzer/cursor-trellis 0.4.2 → 0.4.3

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 (315) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +23 -14
  3. package/README.zh-CN.md +14 -10
  4. package/bin/smart-search.js +10 -2
  5. package/dist/cli/index.d.ts.map +1 -1
  6. package/dist/cli/index.js +36 -15
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/commands/init.d.ts +1 -1
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +42 -153
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/commands/migrate.d.ts +10 -0
  13. package/dist/commands/migrate.d.ts.map +1 -0
  14. package/dist/commands/migrate.js +38 -0
  15. package/dist/commands/migrate.js.map +1 -0
  16. package/dist/commands/update.d.ts +5 -0
  17. package/dist/commands/update.d.ts.map +1 -1
  18. package/dist/commands/update.js +137 -40
  19. package/dist/commands/update.js.map +1 -1
  20. package/dist/configurators/cursor.d.ts +2 -1
  21. package/dist/configurators/cursor.d.ts.map +1 -1
  22. package/dist/configurators/cursor.js +4 -2
  23. package/dist/configurators/cursor.js.map +1 -1
  24. package/dist/configurators/index.js +4 -4
  25. package/dist/configurators/index.js.map +1 -1
  26. package/dist/configurators/shared.d.ts +8 -0
  27. package/dist/configurators/shared.d.ts.map +1 -1
  28. package/dist/configurators/shared.js +27 -1
  29. package/dist/configurators/shared.js.map +1 -1
  30. package/dist/configurators/workflow.d.ts +0 -9
  31. package/dist/configurators/workflow.d.ts.map +1 -1
  32. package/dist/configurators/workflow.js +2 -4
  33. package/dist/configurators/workflow.js.map +1 -1
  34. package/dist/constants/paths.d.ts +9 -0
  35. package/dist/constants/paths.d.ts.map +1 -1
  36. package/dist/constants/paths.js +13 -0
  37. package/dist/constants/paths.js.map +1 -1
  38. package/dist/migrations/manifests/0.3.0.json +1 -1
  39. package/dist/templates/common/bundled-skills/cstl-meta/references/platform-files/skills-and-commands.md +2 -4
  40. package/dist/templates/common/bundled-skills/cstl-skill-creator/SKILL.md +10 -7
  41. package/dist/templates/common/bundled-skills/cstl-skill-creator/references/authoring-rules.md +93 -19
  42. package/dist/templates/common/bundled-skills/cstl-skill-creator/references/general-authoring.md +189 -0
  43. package/dist/templates/common/bundled-skills/cstl-skill-creator/references/review-checklist.md +10 -5
  44. package/dist/templates/common/commands/continue.md +5 -4
  45. package/dist/templates/common/index.d.ts +20 -0
  46. package/dist/templates/common/index.d.ts.map +1 -1
  47. package/dist/templates/common/index.js +21 -2
  48. package/dist/templates/common/index.js.map +1 -1
  49. package/dist/templates/common/optional-skills/chrome-cdp/SKILL.md +179 -0
  50. package/dist/templates/common/optional-skills/chrome-cdp/examples/fetch-hook-api-capture.md +149 -0
  51. package/dist/templates/common/optional-skills/chrome-cdp/scripts/cdp.mjs +903 -0
  52. package/dist/templates/cursor/agents/cstl-research.md +1 -1
  53. package/dist/templates/cursor/fixtures/expected-rules.d.ts +1 -1
  54. package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -1
  55. package/dist/templates/cursor/fixtures/expected-rules.js +7 -24
  56. package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -1
  57. package/dist/templates/cursor/hooks.json +4 -0
  58. package/dist/templates/cursor/index.d.ts +1 -1
  59. package/dist/templates/cursor/index.js +1 -1
  60. package/dist/templates/cursor/rules/cstl-bootstrap.mdc +17 -0
  61. package/dist/templates/markdown/agents.md +15 -10
  62. package/dist/templates/markdown/framework/cursor-native-modes-guide.md.txt +12 -9
  63. package/dist/templates/markdown/framework/cursor-subagent-policy.md.txt +62 -126
  64. package/dist/templates/markdown/framework/dogfood-only-surfaces.md.txt +4 -6
  65. package/dist/templates/markdown/framework/execution-strategy.md.txt +2 -0
  66. package/dist/templates/markdown/framework/index.md.txt +4 -0
  67. package/dist/templates/markdown/framework/internal-skills-cursor-reachability.md.txt +4 -3
  68. package/dist/templates/markdown/framework/middleware-protocol.md.txt +83 -0
  69. package/dist/templates/markdown/framework/parallel-first-execution.md.txt +78 -0
  70. package/dist/templates/markdown/framework/release-boundary.md.txt +30 -0
  71. package/dist/templates/markdown/framework/retrieval-daily-guide.md.txt +1 -1
  72. package/dist/templates/markdown/framework/upgrade.md.txt +15 -0
  73. package/dist/templates/markdown/index.d.ts +4 -0
  74. package/dist/templates/markdown/index.d.ts.map +1 -1
  75. package/dist/templates/markdown/index.js +17 -0
  76. package/dist/templates/markdown/index.js.map +1 -1
  77. package/dist/templates/markdown/spec/guides/index.md.txt +0 -6
  78. package/dist/templates/shared-hooks/event-bridge.py +60 -0
  79. package/dist/templates/shared-hooks/index.d.ts +1 -1
  80. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  81. package/dist/templates/shared-hooks/index.js +1 -0
  82. package/dist/templates/shared-hooks/index.js.map +1 -1
  83. package/dist/templates/shared-hooks/session-start.py +18 -1
  84. package/dist/templates/trellis/CONTEXT.md +63 -3
  85. package/dist/templates/trellis/gitignore.txt +39 -39
  86. package/dist/templates/trellis/index.d.ts +12 -0
  87. package/dist/templates/trellis/index.d.ts.map +1 -1
  88. package/dist/templates/trellis/index.js +73 -15
  89. package/dist/templates/trellis/index.js.map +1 -1
  90. package/dist/templates/trellis/pool/README.md +12 -3
  91. package/dist/templates/trellis/pool/plan.md +24 -5
  92. package/dist/templates/trellis/scripts/common/adapter_middleware.py +158 -0
  93. package/dist/templates/trellis/scripts/common/artifact_locale.py +12 -3
  94. package/dist/templates/trellis/scripts/common/cursor_retrieval_env.py +5 -2
  95. package/dist/templates/trellis/scripts/common/full_quality.py +237 -0
  96. package/dist/templates/trellis/scripts/common/git.py +6 -0
  97. package/dist/templates/trellis/scripts/common/git_context.py +17 -5
  98. package/dist/templates/trellis/scripts/common/kernel_command.py +367 -0
  99. package/dist/templates/trellis/scripts/common/lite_context.py +280 -0
  100. package/dist/templates/trellis/scripts/common/ondemand_topology.py +225 -0
  101. package/dist/templates/trellis/scripts/common/parent_orchestration.py +1 -1
  102. package/dist/templates/trellis/scripts/common/pool_store.py +16 -4
  103. package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +2 -2
  104. package/dist/templates/trellis/scripts/common/session_context.py +28 -2
  105. package/dist/templates/trellis/scripts/common/task_gates.py +73 -12
  106. package/dist/templates/trellis/scripts/common/task_map.py +42 -9
  107. package/dist/templates/trellis/scripts/common/task_store.py +286 -56
  108. package/dist/templates/trellis/scripts/common/test_adapter_middleware.py +56 -0
  109. package/dist/templates/trellis/scripts/common/test_depends_mode_block.py +9 -0
  110. package/dist/templates/trellis/scripts/common/test_full_quality.py +197 -0
  111. package/dist/templates/trellis/scripts/common/test_kernel_command.py +326 -0
  112. package/dist/templates/trellis/scripts/common/test_lite_path.py +190 -0
  113. package/dist/templates/trellis/scripts/common/test_ondemand_topology.py +122 -0
  114. package/dist/templates/trellis/scripts/common/test_pool_store.py +17 -0
  115. package/dist/templates/trellis/scripts/common/test_task_store_kernel_patch.py +163 -0
  116. package/dist/templates/trellis/scripts/common/workflow_phase.py +3 -0
  117. package/dist/templates/trellis/scripts/get_context.py +1 -0
  118. package/dist/templates/trellis/scripts/pool.py +5 -0
  119. package/dist/templates/trellis/scripts/task.py +83 -7
  120. package/dist/templates/trellis/workflow.md +100 -18
  121. package/dist/utils/cursor-retrieval-env.d.ts +2 -1
  122. package/dist/utils/cursor-retrieval-env.d.ts.map +1 -1
  123. package/dist/utils/cursor-retrieval-env.js +3 -2
  124. package/dist/utils/cursor-retrieval-env.js.map +1 -1
  125. package/dist/utils/cursor2plus-residue-cleanup.d.ts +38 -0
  126. package/dist/utils/cursor2plus-residue-cleanup.d.ts.map +1 -0
  127. package/dist/utils/cursor2plus-residue-cleanup.js +145 -0
  128. package/dist/utils/cursor2plus-residue-cleanup.js.map +1 -0
  129. package/dist/utils/manifest-prune.d.ts.map +1 -1
  130. package/dist/utils/manifest-prune.js +7 -2
  131. package/dist/utils/manifest-prune.js.map +1 -1
  132. package/dist/utils/p36-upgrade.d.ts +62 -0
  133. package/dist/utils/p36-upgrade.d.ts.map +1 -0
  134. package/dist/utils/p36-upgrade.js +160 -0
  135. package/dist/utils/p36-upgrade.js.map +1 -0
  136. package/dist/utils/project-capabilities.d.ts +1 -12
  137. package/dist/utils/project-capabilities.d.ts.map +1 -1
  138. package/dist/utils/project-capabilities.js +3 -100
  139. package/dist/utils/project-capabilities.js.map +1 -1
  140. package/dist/utils/readiness.d.ts +10 -0
  141. package/dist/utils/readiness.d.ts.map +1 -1
  142. package/dist/utils/readiness.js +58 -26
  143. package/dist/utils/readiness.js.map +1 -1
  144. package/dist/utils/retrieval-agent-instructions.js +1 -1
  145. package/dist/utils/retrieval-agent-instructions.js.map +1 -1
  146. package/dist/utils/template-hash.d.ts.map +1 -1
  147. package/dist/utils/template-hash.js +11 -1
  148. package/dist/utils/template-hash.js.map +1 -1
  149. package/dist/utils/uninstall-scrubbers.d.ts.map +1 -1
  150. package/dist/utils/uninstall-scrubbers.js +52 -14
  151. package/dist/utils/uninstall-scrubbers.js.map +1 -1
  152. package/dist/utils/update-rollout-report.d.ts +14 -0
  153. package/dist/utils/update-rollout-report.d.ts.map +1 -1
  154. package/dist/utils/update-rollout-report.js +1 -0
  155. package/dist/utils/update-rollout-report.js.map +1 -1
  156. package/dist/utils/workflow-ownership.d.ts +1 -1
  157. package/dist/utils/workflow-ownership.js +4 -4
  158. package/dist/utils/workflow-ownership.js.map +1 -1
  159. package/package.json +5 -4
  160. package/scripts/postinstall.js +6 -7
  161. package/dist/commands/campaign/canvas-render.d.ts +0 -68
  162. package/dist/commands/campaign/canvas-render.d.ts.map +0 -1
  163. package/dist/commands/campaign/canvas-render.js +0 -414
  164. package/dist/commands/campaign/canvas-render.js.map +0 -1
  165. package/dist/commands/campaign/compose.d.ts +0 -15
  166. package/dist/commands/campaign/compose.d.ts.map +0 -1
  167. package/dist/commands/campaign/compose.js +0 -110
  168. package/dist/commands/campaign/compose.js.map +0 -1
  169. package/dist/commands/campaign/index.d.ts +0 -13
  170. package/dist/commands/campaign/index.d.ts.map +0 -1
  171. package/dist/commands/campaign/index.js +0 -134
  172. package/dist/commands/campaign/index.js.map +0 -1
  173. package/dist/commands/campaign/kind-map.d.ts +0 -6
  174. package/dist/commands/campaign/kind-map.d.ts.map +0 -1
  175. package/dist/commands/campaign/kind-map.js +0 -21
  176. package/dist/commands/campaign/kind-map.js.map +0 -1
  177. package/dist/commands/campaign/mcp-server.d.ts +0 -6
  178. package/dist/commands/campaign/mcp-server.d.ts.map +0 -1
  179. package/dist/commands/campaign/mcp-server.js +0 -167
  180. package/dist/commands/campaign/mcp-server.js.map +0 -1
  181. package/dist/commands/campaign/render.d.ts +0 -5
  182. package/dist/commands/campaign/render.d.ts.map +0 -1
  183. package/dist/commands/campaign/render.js +0 -103
  184. package/dist/commands/campaign/render.js.map +0 -1
  185. package/dist/commands/campaign/trellis-load.d.ts +0 -13
  186. package/dist/commands/campaign/trellis-load.d.ts.map +0 -1
  187. package/dist/commands/campaign/trellis-load.js +0 -122
  188. package/dist/commands/campaign/trellis-load.js.map +0 -1
  189. package/dist/commands/campaign/types.d.ts +0 -61
  190. package/dist/commands/campaign/types.d.ts.map +0 -1
  191. package/dist/commands/campaign/types.js +0 -3
  192. package/dist/commands/campaign/types.js.map +0 -1
  193. package/dist/commands/goal/index.d.ts +0 -3
  194. package/dist/commands/goal/index.d.ts.map +0 -1
  195. package/dist/commands/goal/index.js +0 -159
  196. package/dist/commands/goal/index.js.map +0 -1
  197. package/dist/commands/goal/review.d.ts +0 -6
  198. package/dist/commands/goal/review.d.ts.map +0 -1
  199. package/dist/commands/goal/review.js +0 -28
  200. package/dist/commands/goal/review.js.map +0 -1
  201. package/dist/commands/rpc/client.d.ts +0 -51
  202. package/dist/commands/rpc/client.d.ts.map +0 -1
  203. package/dist/commands/rpc/client.js +0 -147
  204. package/dist/commands/rpc/client.js.map +0 -1
  205. package/dist/commands/rpc/index.d.ts +0 -10
  206. package/dist/commands/rpc/index.d.ts.map +0 -1
  207. package/dist/commands/rpc/index.js +0 -157
  208. package/dist/commands/rpc/index.js.map +0 -1
  209. package/dist/commands/rpc/serve.d.ts +0 -23
  210. package/dist/commands/rpc/serve.d.ts.map +0 -1
  211. package/dist/commands/rpc/serve.js +0 -60
  212. package/dist/commands/rpc/serve.js.map +0 -1
  213. package/dist/commands/rpc/smoke.d.ts +0 -16
  214. package/dist/commands/rpc/smoke.d.ts.map +0 -1
  215. package/dist/commands/rpc/smoke.js +0 -92
  216. package/dist/commands/rpc/smoke.js.map +0 -1
  217. package/dist/commands/sdk/index.d.ts +0 -9
  218. package/dist/commands/sdk/index.d.ts.map +0 -1
  219. package/dist/commands/sdk/index.js +0 -58
  220. package/dist/commands/sdk/index.js.map +0 -1
  221. package/dist/commands/sdk/run.d.ts +0 -42
  222. package/dist/commands/sdk/run.d.ts.map +0 -1
  223. package/dist/commands/sdk/run.js +0 -220
  224. package/dist/commands/sdk/run.js.map +0 -1
  225. package/dist/commands/sdk/status.d.ts +0 -14
  226. package/dist/commands/sdk/status.d.ts.map +0 -1
  227. package/dist/commands/sdk/status.js +0 -69
  228. package/dist/commands/sdk/status.js.map +0 -1
  229. package/dist/configurators/cursor2plus-local.d.ts +0 -6
  230. package/dist/configurators/cursor2plus-local.d.ts.map +0 -1
  231. package/dist/configurators/cursor2plus-local.js +0 -21
  232. package/dist/configurators/cursor2plus-local.js.map +0 -1
  233. package/dist/goal/action-packet.d.ts +0 -89
  234. package/dist/goal/action-packet.d.ts.map +0 -1
  235. package/dist/goal/action-packet.js +0 -113
  236. package/dist/goal/action-packet.js.map +0 -1
  237. package/dist/goal/audit.d.ts +0 -14
  238. package/dist/goal/audit.d.ts.map +0 -1
  239. package/dist/goal/audit.js +0 -12
  240. package/dist/goal/audit.js.map +0 -1
  241. package/dist/goal/constants.d.ts +0 -26
  242. package/dist/goal/constants.d.ts.map +0 -1
  243. package/dist/goal/constants.js +0 -24
  244. package/dist/goal/constants.js.map +0 -1
  245. package/dist/goal/paths.d.ts +0 -8
  246. package/dist/goal/paths.d.ts.map +0 -1
  247. package/dist/goal/paths.js +0 -36
  248. package/dist/goal/paths.js.map +0 -1
  249. package/dist/goal/preflight.d.ts +0 -21
  250. package/dist/goal/preflight.d.ts.map +0 -1
  251. package/dist/goal/preflight.js +0 -97
  252. package/dist/goal/preflight.js.map +0 -1
  253. package/dist/goal/review-seam.d.ts +0 -44
  254. package/dist/goal/review-seam.d.ts.map +0 -1
  255. package/dist/goal/review-seam.js +0 -153
  256. package/dist/goal/review-seam.js.map +0 -1
  257. package/dist/goal/reviewer-rules.d.ts +0 -13
  258. package/dist/goal/reviewer-rules.d.ts.map +0 -1
  259. package/dist/goal/reviewer-rules.js +0 -153
  260. package/dist/goal/reviewer-rules.js.map +0 -1
  261. package/dist/goal/reviewer.d.ts +0 -9
  262. package/dist/goal/reviewer.d.ts.map +0 -1
  263. package/dist/goal/reviewer.js +0 -54
  264. package/dist/goal/reviewer.js.map +0 -1
  265. package/dist/goal/runtime.d.ts +0 -32
  266. package/dist/goal/runtime.d.ts.map +0 -1
  267. package/dist/goal/runtime.js +0 -242
  268. package/dist/goal/runtime.js.map +0 -1
  269. package/dist/goal/sdk-client.d.ts +0 -10
  270. package/dist/goal/sdk-client.d.ts.map +0 -1
  271. package/dist/goal/sdk-client.js +0 -34
  272. package/dist/goal/sdk-client.js.map +0 -1
  273. package/dist/goal/state.d.ts +0 -40
  274. package/dist/goal/state.d.ts.map +0 -1
  275. package/dist/goal/state.js +0 -37
  276. package/dist/goal/state.js.map +0 -1
  277. package/dist/goal/task-triggers.d.ts +0 -15
  278. package/dist/goal/task-triggers.d.ts.map +0 -1
  279. package/dist/goal/task-triggers.js +0 -23
  280. package/dist/goal/task-triggers.js.map +0 -1
  281. package/dist/goal/walls.d.ts +0 -12
  282. package/dist/goal/walls.d.ts.map +0 -1
  283. package/dist/goal/walls.js +0 -65
  284. package/dist/goal/walls.js.map +0 -1
  285. package/dist/goal/worker-prompt.d.ts +0 -13
  286. package/dist/goal/worker-prompt.d.ts.map +0 -1
  287. package/dist/goal/worker-prompt.js +0 -41
  288. package/dist/goal/worker-prompt.js.map +0 -1
  289. package/dist/goal/worker.d.ts +0 -42
  290. package/dist/goal/worker.d.ts.map +0 -1
  291. package/dist/goal/worker.js +0 -180
  292. package/dist/goal/worker.js.map +0 -1
  293. package/dist/templates/common/bundled-skills/cstl-cursor2plus-setup/SKILL.md +0 -63
  294. package/dist/templates/cursor/commands/cursor2plus-setup.md +0 -82
  295. package/dist/templates/cursor/rules/cstl-cursor-modes.mdc +0 -32
  296. package/dist/templates/cursor/rules/cstl-session-rename.mdc +0 -31
  297. package/dist/templates/cursor/rules/cstl-subagent-dispatch.mdc +0 -34
  298. package/dist/templates/cursor/rules/cstl-triage.mdc +0 -36
  299. package/dist/templates/cursor/rules/retrieval-routing.mdc +0 -78
  300. package/dist/templates/trellis/local/README.md +0 -85
  301. package/dist/templates/trellis/local/config.local.json.example +0 -6
  302. package/dist/templates/trellis/local/index.d.ts +0 -8
  303. package/dist/templates/trellis/local/index.d.ts.map +0 -1
  304. package/dist/templates/trellis/local/index.js +0 -15
  305. package/dist/templates/trellis/local/index.js.map +0 -1
  306. package/dist/templates/trellis/local/patch_wpelc8.py +0 -421
  307. package/dist/templates/trellis/local/smoke.py +0 -94
  308. package/dist/templates/trellis/local/subagent-models.json.example +0 -4
  309. package/dist/templates/trellis/local/trellis-task-models.json.example +0 -10
  310. package/dist/templates/trellis/local/trellis-task-models.json5.example +0 -45
  311. package/dist/templates/trellis/local/trellis_task_models_config.py +0 -200
  312. package/dist/utils/cursor-sdk-gate.d.ts +0 -10
  313. package/dist/utils/cursor-sdk-gate.d.ts.map +0 -1
  314. package/dist/utils/cursor-sdk-gate.js +0 -32
  315. package/dist/utils/cursor-sdk-gate.js.map +0 -1
@@ -0,0 +1,903 @@
1
+ #!/usr/bin/env node
2
+ // cdp - lightweight Chrome DevTools Protocol CLI
3
+ // Uses raw CDP over WebSocket, no Puppeteer dependency.
4
+ // Requires Node 22+ (built-in WebSocket).
5
+ //
6
+ // Per-tab persistent daemon: page commands go through a daemon that holds
7
+ // the CDP session open. Chrome's "Allow debugging" modal fires once per
8
+ // daemon (= once per tab). Daemons auto-exit after 20min idle.
9
+
10
+ import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
11
+ import { homedir } from 'os';
12
+ import { resolve } from 'path';
13
+ import { spawn } from 'child_process';
14
+ import net from 'net';
15
+
16
+ const TIMEOUT = 15000;
17
+ const NAVIGATION_TIMEOUT = 30000;
18
+ const IDLE_TIMEOUT = 20 * 60 * 1000;
19
+ const DAEMON_CONNECT_RETRIES = 20;
20
+ const DAEMON_CONNECT_DELAY = 300;
21
+ const MIN_TARGET_PREFIX_LEN = 8;
22
+ const IS_WINDOWS = process.platform === 'win32';
23
+ if (!IS_WINDOWS) process.umask(0o077);
24
+ const RUNTIME_DIR = IS_WINDOWS
25
+ ? resolve(process.env.LOCALAPPDATA || resolve(homedir(), 'AppData', 'Local'), 'cdp')
26
+ : process.env.XDG_RUNTIME_DIR
27
+ ? resolve(process.env.XDG_RUNTIME_DIR, 'cdp')
28
+ : resolve(homedir(), '.cache', 'cdp');
29
+ try { mkdirSync(RUNTIME_DIR, { recursive: true, mode: 0o700 }); } catch {}
30
+ const PAGES_CACHE = resolve(RUNTIME_DIR, 'pages.json');
31
+
32
+ function sockPath(targetId) {
33
+ return IS_WINDOWS
34
+ ? `\\\\.\\pipe\\cdp-${targetId}`
35
+ : resolve(RUNTIME_DIR, `cdp-${targetId}.sock`);
36
+ }
37
+
38
+ function getWsUrl() {
39
+ const home = homedir();
40
+ // macOS: ~/Library/Application Support/<name>/DevToolsActivePort
41
+ const macBrowsers = [
42
+ 'Google/Chrome', 'Google/Chrome Beta', 'Google/Chrome for Testing',
43
+ 'Chromium', 'BraveSoftware/Brave-Browser', 'Microsoft Edge',
44
+ ];
45
+ // Linux: ~/.config/<name>/DevToolsActivePort
46
+ const linuxBrowsers = [
47
+ 'google-chrome', 'google-chrome-beta', 'chromium',
48
+ 'vivaldi', 'vivaldi-snapshot',
49
+ 'BraveSoftware/Brave-Browser', 'microsoft-edge',
50
+ ];
51
+ // Linux Flatpak: ~/.var/app/<app-id>/config/<name>/DevToolsActivePort
52
+ const flatpakBrowsers = [
53
+ ['org.chromium.Chromium', 'chromium'],
54
+ ['com.google.Chrome', 'google-chrome'],
55
+ ['com.brave.Browser', 'BraveSoftware/Brave-Browser'],
56
+ ['com.microsoft.Edge', 'microsoft-edge'],
57
+ ['com.vivaldi.Vivaldi', 'vivaldi'],
58
+ ];
59
+ const candidates = [
60
+ process.env.CDP_PORT_FILE,
61
+ ...macBrowsers.flatMap(b => [
62
+ resolve(home, 'Library/Application Support', b, 'DevToolsActivePort'),
63
+ resolve(home, 'Library/Application Support', b, 'Default/DevToolsActivePort'),
64
+ ]),
65
+ ...linuxBrowsers.flatMap(b => [
66
+ resolve(home, '.config', b, 'DevToolsActivePort'),
67
+ resolve(home, '.config', b, 'Default/DevToolsActivePort'),
68
+ ]),
69
+ ...flatpakBrowsers.flatMap(([appId, name]) => [
70
+ resolve(home, '.var/app', appId, 'config', name, 'DevToolsActivePort'),
71
+ resolve(home, '.var/app', appId, 'config', name, 'Default/DevToolsActivePort'),
72
+ ]),
73
+ // Windows: %LOCALAPPDATA%/<name>/User Data/DevToolsActivePort
74
+ ...(IS_WINDOWS ? ['Google/Chrome', 'BraveSoftware/Brave-Browser', 'Microsoft/Edge'].flatMap(b => {
75
+ const base = process.env.LOCALAPPDATA || resolve(home, 'AppData/Local');
76
+ return [
77
+ resolve(base, b, 'User Data/DevToolsActivePort'),
78
+ resolve(base, b, 'User Data/Default/DevToolsActivePort'),
79
+ ];
80
+ }) : []),
81
+ ].filter(Boolean);
82
+ const portFile = candidates.find(p => existsSync(p));
83
+ if (!portFile) throw new Error('No DevToolsActivePort found. Enable remote debugging at chrome://inspect/#remote-debugging');
84
+ const lines = readFileSync(portFile, 'utf8').trim().split('\n');
85
+ if (lines.length < 2 || !lines[0] || !lines[1]) throw new Error(`Invalid DevToolsActivePort file: ${portFile}`);
86
+ const host = process.env.CDP_HOST || '127.0.0.1';
87
+ return `ws://${host}:${lines[0]}${lines[1]}`;
88
+ }
89
+
90
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
91
+
92
+
93
+ function resolvePrefix(prefix, candidates, noun = 'target', missingHint = '') {
94
+ const upper = prefix.toUpperCase();
95
+ const matches = candidates.filter(candidate => candidate.toUpperCase().startsWith(upper));
96
+ if (matches.length === 0) {
97
+ const hint = missingHint ? ` ${missingHint}` : '';
98
+ throw new Error(`No ${noun} matching prefix "${prefix}".${hint}`);
99
+ }
100
+ if (matches.length > 1) {
101
+ throw new Error(`Ambiguous prefix "${prefix}" — matches ${matches.length} ${noun}s. Use more characters.`);
102
+ }
103
+ return matches[0];
104
+ }
105
+
106
+ function getDisplayPrefixLength(targetIds) {
107
+ if (targetIds.length === 0) return MIN_TARGET_PREFIX_LEN;
108
+ const maxLen = Math.max(...targetIds.map(id => id.length));
109
+ for (let len = MIN_TARGET_PREFIX_LEN; len <= maxLen; len++) {
110
+ const prefixes = new Set(targetIds.map(id => id.slice(0, len).toUpperCase()));
111
+ if (prefixes.size === targetIds.length) return len;
112
+ }
113
+ return maxLen;
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // CDP WebSocket client
118
+ // ---------------------------------------------------------------------------
119
+
120
+ class CDP {
121
+ #ws; #id = 0; #pending = new Map(); #eventHandlers = new Map(); #closeHandlers = [];
122
+
123
+ async connect(wsUrl) {
124
+ return new Promise((res, rej) => {
125
+ this.#ws = new WebSocket(wsUrl);
126
+ this.#ws.onopen = () => res();
127
+ this.#ws.onerror = (e) => rej(new Error('WebSocket error: ' + (e.message || e.type)));
128
+ this.#ws.onclose = () => this.#closeHandlers.forEach(h => h());
129
+ this.#ws.onmessage = (ev) => {
130
+ const msg = JSON.parse(ev.data);
131
+ if (msg.id && this.#pending.has(msg.id)) {
132
+ const { resolve, reject } = this.#pending.get(msg.id);
133
+ this.#pending.delete(msg.id);
134
+ if (msg.error) reject(new Error(msg.error.message));
135
+ else resolve(msg.result);
136
+ } else if (msg.method && this.#eventHandlers.has(msg.method)) {
137
+ for (const handler of [...this.#eventHandlers.get(msg.method)]) {
138
+ handler(msg.params || {}, msg);
139
+ }
140
+ }
141
+ };
142
+ });
143
+ }
144
+
145
+ send(method, params = {}, sessionId) {
146
+ const id = ++this.#id;
147
+ return new Promise((resolve, reject) => {
148
+ this.#pending.set(id, { resolve, reject });
149
+ const msg = { id, method, params };
150
+ if (sessionId) msg.sessionId = sessionId;
151
+ this.#ws.send(JSON.stringify(msg));
152
+ setTimeout(() => {
153
+ if (this.#pending.has(id)) {
154
+ this.#pending.delete(id);
155
+ reject(new Error(`Timeout: ${method}`));
156
+ }
157
+ }, TIMEOUT);
158
+ });
159
+ }
160
+
161
+ onEvent(method, handler) {
162
+ if (!this.#eventHandlers.has(method)) this.#eventHandlers.set(method, new Set());
163
+ const handlers = this.#eventHandlers.get(method);
164
+ handlers.add(handler);
165
+ return () => {
166
+ handlers.delete(handler);
167
+ if (handlers.size === 0) this.#eventHandlers.delete(method);
168
+ };
169
+ }
170
+
171
+ waitForEvent(method, timeout = TIMEOUT) {
172
+ let settled = false;
173
+ let off;
174
+ let timer;
175
+ const promise = new Promise((resolve, reject) => {
176
+ off = this.onEvent(method, (params) => {
177
+ if (settled) return;
178
+ settled = true;
179
+ clearTimeout(timer);
180
+ off();
181
+ resolve(params);
182
+ });
183
+ timer = setTimeout(() => {
184
+ if (settled) return;
185
+ settled = true;
186
+ off();
187
+ reject(new Error(`Timeout waiting for event: ${method}`));
188
+ }, timeout);
189
+ });
190
+ return {
191
+ promise,
192
+ cancel() {
193
+ if (settled) return;
194
+ settled = true;
195
+ clearTimeout(timer);
196
+ off?.();
197
+ },
198
+ };
199
+ }
200
+
201
+ onClose(handler) { this.#closeHandlers.push(handler); }
202
+ close() { this.#ws.close(); }
203
+ }
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // Command implementations — return strings, take (cdp, sessionId)
207
+ // ---------------------------------------------------------------------------
208
+
209
+ async function getPages(cdp) {
210
+ const { targetInfos } = await cdp.send('Target.getTargets');
211
+ return targetInfos.filter(t => t.type === 'page' && !t.url.startsWith('chrome://'));
212
+ }
213
+
214
+ function formatPageList(pages, activeUrl = null) {
215
+ const prefixLen = getDisplayPrefixLength(pages.map(p => p.targetId));
216
+ return pages.map(p => {
217
+ const id = p.targetId.slice(0, prefixLen).padEnd(prefixLen);
218
+ const title = p.title.substring(0, 54).padEnd(54);
219
+ const marker = activeUrl && p.url === activeUrl ? '* ' : ' ';
220
+ return `${marker}${id} ${title} ${p.url}`;
221
+ }).join('\n');
222
+ }
223
+
224
+ async function getActiveChromeUrl() {
225
+ if (process.platform !== 'darwin') return null;
226
+ const { execFile } = await import('child_process');
227
+ const { promisify } = await import('util');
228
+ const execFileAsync = promisify(execFile);
229
+ const browsers = ['Google Chrome', 'Brave Browser', 'Microsoft Edge', 'Chromium'];
230
+ for (const browser of browsers) {
231
+ try {
232
+ const script = `tell application "${browser}" to get URL of active tab of front window`;
233
+ const { stdout } = await execFileAsync('osascript', ['-e', script], { timeout: 3000 });
234
+ return stdout.trim();
235
+ } catch {}
236
+ }
237
+ return null;
238
+ }
239
+
240
+ function shouldShowAxNode(node, compact = false) {
241
+ const role = node.role?.value || '';
242
+ const name = node.name?.value ?? '';
243
+ const value = node.value?.value;
244
+ if (compact && role === 'InlineTextBox') return false;
245
+ return role !== 'none' && role !== 'generic' && !(name === '' && (value === '' || value == null));
246
+ }
247
+
248
+ function formatAxNode(node, depth) {
249
+ const role = node.role?.value || '';
250
+ const name = node.name?.value ?? '';
251
+ const value = node.value?.value;
252
+ const indent = ' '.repeat(Math.min(depth, 10));
253
+ let line = `${indent}[${role}]`;
254
+ if (name !== '') line += ` ${name}`;
255
+ if (!(value === '' || value == null)) line += ` = ${JSON.stringify(value)}`;
256
+ return line;
257
+ }
258
+
259
+ function orderedAxChildren(node, nodesById, childrenByParent) {
260
+ const children = [];
261
+ const seen = new Set();
262
+ for (const childId of node.childIds || []) {
263
+ const child = nodesById.get(childId);
264
+ if (child && !seen.has(child.nodeId)) {
265
+ seen.add(child.nodeId);
266
+ children.push(child);
267
+ }
268
+ }
269
+ for (const child of childrenByParent.get(node.nodeId) || []) {
270
+ if (!seen.has(child.nodeId)) {
271
+ seen.add(child.nodeId);
272
+ children.push(child);
273
+ }
274
+ }
275
+ return children;
276
+ }
277
+
278
+ async function snapshotStr(cdp, sid, compact = false) {
279
+ const { nodes } = await cdp.send('Accessibility.getFullAXTree', {}, sid);
280
+ const nodesById = new Map(nodes.map(node => [node.nodeId, node]));
281
+ const childrenByParent = new Map();
282
+ for (const node of nodes) {
283
+ if (!node.parentId) continue;
284
+ if (!childrenByParent.has(node.parentId)) childrenByParent.set(node.parentId, []);
285
+ childrenByParent.get(node.parentId).push(node);
286
+ }
287
+
288
+ const lines = [];
289
+ const visited = new Set();
290
+ function visit(node, depth) {
291
+ if (!node || visited.has(node.nodeId)) return;
292
+ visited.add(node.nodeId);
293
+ if (shouldShowAxNode(node, compact)) lines.push(formatAxNode(node, depth));
294
+ for (const child of orderedAxChildren(node, nodesById, childrenByParent)) {
295
+ visit(child, depth + 1);
296
+ }
297
+ }
298
+
299
+ const roots = nodes.filter(node => !node.parentId || !nodesById.has(node.parentId));
300
+ for (const root of roots) visit(root, 0);
301
+ for (const node of nodes) visit(node, 0);
302
+
303
+ return lines.join('\n');
304
+ }
305
+
306
+ async function evalStr(cdp, sid, expression) {
307
+ await cdp.send('Runtime.enable', {}, sid);
308
+ const result = await cdp.send('Runtime.evaluate', {
309
+ expression, returnByValue: true, awaitPromise: true,
310
+ }, sid);
311
+ if (result.exceptionDetails) {
312
+ throw new Error(result.exceptionDetails.text || result.exceptionDetails.exception?.description);
313
+ }
314
+ const val = result.result.value;
315
+ return typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val ?? '');
316
+ }
317
+
318
+ async function shotStr(cdp, sid, filePath, targetId) {
319
+ // Get device scale factor so we can report coordinate mapping
320
+ let dpr = 1;
321
+ try {
322
+ const metrics = await cdp.send('Page.getLayoutMetrics', {}, sid);
323
+ dpr = metrics.visualViewport?.clientWidth
324
+ ? metrics.cssVisualViewport?.clientWidth
325
+ ? Math.round((metrics.visualViewport.clientWidth / metrics.cssVisualViewport.clientWidth) * 100) / 100
326
+ : 1
327
+ : 1;
328
+ // Simpler: deviceScaleFactor is on the root Page metrics
329
+ const { deviceScaleFactor } = await cdp.send('Emulation.getDeviceMetricsOverride', {}, sid).catch(() => ({}));
330
+ if (deviceScaleFactor) dpr = deviceScaleFactor;
331
+ } catch {}
332
+ // Fallback: try to get DPR from JS
333
+ if (dpr === 1) {
334
+ try {
335
+ const raw = await evalStr(cdp, sid, 'window.devicePixelRatio');
336
+ const parsed = parseFloat(raw);
337
+ if (parsed > 0) dpr = parsed;
338
+ } catch {}
339
+ }
340
+
341
+ const { data } = await cdp.send('Page.captureScreenshot', { format: 'png' }, sid);
342
+ const out = filePath || resolve(RUNTIME_DIR, `screenshot-${(targetId || 'unknown').slice(0, 8)}.png`);
343
+ writeFileSync(out, Buffer.from(data, 'base64'));
344
+
345
+ const lines = [out];
346
+ lines.push(`Screenshot saved. Device pixel ratio (DPR): ${dpr}`);
347
+ lines.push(`Coordinate mapping:`);
348
+ lines.push(` Screenshot pixels → CSS pixels (for CDP Input events): divide by ${dpr}`);
349
+ lines.push(` e.g. screenshot point (${Math.round(100 * dpr)}, ${Math.round(200 * dpr)}) → CSS (100, 200) → use clickxy <target> 100 200`);
350
+ if (dpr !== 1) {
351
+ lines.push(` On this ${dpr}x display: CSS px = screenshot px / ${dpr} ≈ screenshot px × ${Math.round(100/dpr)/100}`);
352
+ }
353
+ return lines.join('\n');
354
+ }
355
+
356
+ async function htmlStr(cdp, sid, selector) {
357
+ const expr = selector
358
+ ? `document.querySelector(${JSON.stringify(selector)})?.outerHTML || 'Element not found'`
359
+ : `document.documentElement.outerHTML`;
360
+ return evalStr(cdp, sid, expr);
361
+ }
362
+
363
+ async function waitForDocumentReady(cdp, sid, timeoutMs = NAVIGATION_TIMEOUT) {
364
+ const deadline = Date.now() + timeoutMs;
365
+ let lastState = '';
366
+ let lastError;
367
+ while (Date.now() < deadline) {
368
+ try {
369
+ const state = await evalStr(cdp, sid, 'document.readyState');
370
+ lastState = state;
371
+ if (state === 'complete') return;
372
+ } catch (e) {
373
+ lastError = e;
374
+ }
375
+ await sleep(200);
376
+ }
377
+
378
+ if (lastState) {
379
+ throw new Error(`Timed out waiting for navigation to finish (last readyState: ${lastState})`);
380
+ }
381
+ if (lastError) {
382
+ throw new Error(`Timed out waiting for navigation to finish (${lastError.message})`);
383
+ }
384
+ throw new Error('Timed out waiting for navigation to finish');
385
+ }
386
+
387
+ async function navStr(cdp, sid, url) {
388
+ try {
389
+ const parsed = new URL(url);
390
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
391
+ throw new Error(`Only http/https URLs allowed, got: ${url}`);
392
+ } catch (e) {
393
+ if (e.message.startsWith('Only')) throw e;
394
+ throw new Error(`Invalid URL: ${url}`);
395
+ }
396
+ await cdp.send('Page.enable', {}, sid);
397
+ const loadEvent = cdp.waitForEvent('Page.loadEventFired', NAVIGATION_TIMEOUT);
398
+ const result = await cdp.send('Page.navigate', { url }, sid);
399
+ if (result.errorText) {
400
+ loadEvent.cancel();
401
+ throw new Error(result.errorText);
402
+ }
403
+ if (result.loaderId) {
404
+ await loadEvent.promise;
405
+ } else {
406
+ loadEvent.cancel();
407
+ }
408
+ await waitForDocumentReady(cdp, sid, 5000);
409
+ return `Navigated to ${url}`;
410
+ }
411
+
412
+ async function netStr(cdp, sid) {
413
+ const raw = await evalStr(cdp, sid, `JSON.stringify(performance.getEntriesByType('resource').map(e => ({
414
+ name: e.name.substring(0, 120), type: e.initiatorType,
415
+ duration: Math.round(e.duration), size: e.transferSize
416
+ })))`);
417
+ return JSON.parse(raw).map(e =>
418
+ `${String(e.duration).padStart(5)}ms ${String(e.size || '?').padStart(8)}B ${e.type.padEnd(8)} ${e.name}`
419
+ ).join('\n');
420
+ }
421
+
422
+ // Click element by CSS selector
423
+ async function clickStr(cdp, sid, selector) {
424
+ if (!selector) throw new Error('CSS selector required');
425
+ const expr = `
426
+ (function() {
427
+ const el = document.querySelector(${JSON.stringify(selector)});
428
+ if (!el) return { ok: false, error: 'Element not found: ' + ${JSON.stringify(selector)} };
429
+ el.scrollIntoView({ block: 'center' });
430
+ el.click();
431
+ return { ok: true, tag: el.tagName, text: el.textContent.trim().substring(0, 80) };
432
+ })()
433
+ `;
434
+ const result = await evalStr(cdp, sid, expr);
435
+ const r = JSON.parse(result);
436
+ if (!r.ok) throw new Error(r.error);
437
+ return `Clicked <${r.tag}> "${r.text}"`;
438
+ }
439
+
440
+ // Click at CSS pixel coordinates using Input.dispatchMouseEvent
441
+ async function clickXyStr(cdp, sid, x, y) {
442
+ const cx = parseFloat(x);
443
+ const cy = parseFloat(y);
444
+ if (isNaN(cx) || isNaN(cy)) throw new Error('x and y must be numbers (CSS pixels)');
445
+ const base = { x: cx, y: cy, button: 'left', clickCount: 1, modifiers: 0 };
446
+ await cdp.send('Input.dispatchMouseEvent', { ...base, type: 'mouseMoved' }, sid);
447
+ await cdp.send('Input.dispatchMouseEvent', { ...base, type: 'mousePressed' }, sid);
448
+ await sleep(50);
449
+ await cdp.send('Input.dispatchMouseEvent', { ...base, type: 'mouseReleased' }, sid);
450
+ return `Clicked at CSS (${cx}, ${cy})`;
451
+ }
452
+
453
+ // Type text using Input.insertText (works in cross-origin iframes, unlike eval)
454
+ async function typeStr(cdp, sid, text) {
455
+ if (text == null || text === '') throw new Error('text required');
456
+ await cdp.send('Input.insertText', { text }, sid);
457
+ return `Typed ${text.length} characters`;
458
+ }
459
+
460
+ // Load-more: repeatedly click a button/selector until it disappears
461
+ async function loadAllStr(cdp, sid, selector, intervalMs = 1500) {
462
+ if (!selector) throw new Error('CSS selector required');
463
+ let clicks = 0;
464
+ const deadline = Date.now() + 5 * 60 * 1000; // 5-minute hard cap
465
+ while (Date.now() < deadline) {
466
+ const exists = await evalStr(cdp, sid,
467
+ `!!document.querySelector(${JSON.stringify(selector)})`
468
+ );
469
+ if (exists !== 'true') break;
470
+ const clickExpr = `
471
+ (function() {
472
+ const el = document.querySelector(${JSON.stringify(selector)});
473
+ if (!el) return false;
474
+ el.scrollIntoView({ block: 'center' });
475
+ el.click();
476
+ return true;
477
+ })()
478
+ `;
479
+ const clicked = await evalStr(cdp, sid, clickExpr);
480
+ if (clicked !== 'true') break;
481
+ clicks++;
482
+ await sleep(intervalMs);
483
+ }
484
+ return `Clicked "${selector}" ${clicks} time(s) until it disappeared`;
485
+ }
486
+
487
+ // Send a raw CDP command and return the result as JSON
488
+ async function evalRawStr(cdp, sid, method, paramsJson) {
489
+ if (!method) throw new Error('CDP method required (e.g. "DOM.getDocument")');
490
+ let params = {};
491
+ if (paramsJson) {
492
+ try { params = JSON.parse(paramsJson); }
493
+ catch { throw new Error(`Invalid JSON params: ${paramsJson}`); }
494
+ }
495
+ const result = await cdp.send(method, params, sid);
496
+ return JSON.stringify(result, null, 2);
497
+ }
498
+
499
+ // ---------------------------------------------------------------------------
500
+ // Per-tab daemon
501
+ // ---------------------------------------------------------------------------
502
+
503
+ async function runDaemon(targetId) {
504
+ const sp = sockPath(targetId);
505
+
506
+ const cdp = new CDP();
507
+ try {
508
+ await cdp.connect(getWsUrl());
509
+ } catch (e) {
510
+ process.stderr.write(`Daemon: cannot connect to Chrome: ${e.message}\n`);
511
+ process.exit(1);
512
+ }
513
+
514
+ let sessionId;
515
+ try {
516
+ const res = await cdp.send('Target.attachToTarget', { targetId, flatten: true });
517
+ sessionId = res.sessionId;
518
+ } catch (e) {
519
+ process.stderr.write(`Daemon: attach failed: ${e.message}\n`);
520
+ cdp.close();
521
+ process.exit(1);
522
+ }
523
+
524
+ // Shutdown helpers
525
+ let alive = true;
526
+ function shutdown() {
527
+ if (!alive) return;
528
+ alive = false;
529
+ server.close();
530
+ if (!IS_WINDOWS) try { unlinkSync(sp); } catch {}
531
+ cdp.close();
532
+ process.exit(0);
533
+ }
534
+
535
+ // Exit if target goes away or Chrome disconnects
536
+ cdp.onEvent('Target.targetDestroyed', (params) => {
537
+ if (params.targetId === targetId) shutdown();
538
+ });
539
+ cdp.onEvent('Target.detachedFromTarget', (params) => {
540
+ if (params.sessionId === sessionId) shutdown();
541
+ });
542
+ cdp.onClose(() => shutdown());
543
+ process.on('SIGTERM', shutdown);
544
+ process.on('SIGINT', shutdown);
545
+
546
+ // Idle timer
547
+ let idleTimer = setTimeout(shutdown, IDLE_TIMEOUT);
548
+ function resetIdle() {
549
+ clearTimeout(idleTimer);
550
+ idleTimer = setTimeout(shutdown, IDLE_TIMEOUT);
551
+ }
552
+
553
+ // Handle a command
554
+ async function handleCommand({ cmd, args }) {
555
+ resetIdle();
556
+ try {
557
+ let result;
558
+ switch (cmd) {
559
+ case 'list': {
560
+ const pages = await getPages(cdp);
561
+ result = formatPageList(pages);
562
+ break;
563
+ }
564
+ case 'list_raw': {
565
+ const pages = await getPages(cdp);
566
+ result = JSON.stringify(pages);
567
+ break;
568
+ }
569
+ case 'snap': case 'snapshot': result = await snapshotStr(cdp, sessionId, true); break;
570
+ case 'eval': result = await evalStr(cdp, sessionId, args[0]); break;
571
+ case 'shot': case 'screenshot': result = await shotStr(cdp, sessionId, args[0], targetId); break;
572
+ case 'html': result = await htmlStr(cdp, sessionId, args[0]); break;
573
+ case 'nav': case 'navigate': result = await navStr(cdp, sessionId, args[0]); break;
574
+ case 'net': case 'network': result = await netStr(cdp, sessionId); break;
575
+ case 'click': result = await clickStr(cdp, sessionId, args[0]); break;
576
+ case 'clickxy': result = await clickXyStr(cdp, sessionId, args[0], args[1]); break;
577
+ case 'type': result = await typeStr(cdp, sessionId, args[0]); break;
578
+ case 'loadall': result = await loadAllStr(cdp, sessionId, args[0], args[1] ? parseInt(args[1]) : 1500); break;
579
+ case 'evalraw': result = await evalRawStr(cdp, sessionId, args[0], args[1]); break;
580
+ case 'stop': return { ok: true, result: '', stopAfter: true };
581
+ default: return { ok: false, error: `Unknown command: ${cmd}` };
582
+ }
583
+ return { ok: true, result: result ?? '' };
584
+ } catch (e) {
585
+ return { ok: false, error: e.message };
586
+ }
587
+ }
588
+
589
+ // Unix socket server — NDJSON protocol
590
+ // Wire format: each message is one JSON object followed by \n (newline-delimited JSON).
591
+ // Request: { "id": <number>, "cmd": "<command>", "args": ["arg1", "arg2", ...] }
592
+ // Response: { "id": <number>, "ok": <boolean>, "result": "<string>" }
593
+ // or { "id": <number>, "ok": false, "error": "<message>" }
594
+ const server = net.createServer((conn) => {
595
+ let buf = '';
596
+ conn.on('data', (chunk) => {
597
+ buf += chunk.toString();
598
+ const lines = buf.split('\n');
599
+ buf = lines.pop(); // keep incomplete last line
600
+ for (const line of lines) {
601
+ if (!line.trim()) continue;
602
+ let req;
603
+ try {
604
+ req = JSON.parse(line);
605
+ } catch {
606
+ conn.write(JSON.stringify({ ok: false, error: 'Invalid JSON request', id: null }) + '\n');
607
+ continue;
608
+ }
609
+ handleCommand(req).then((res) => {
610
+ const payload = JSON.stringify({ ...res, id: req.id }) + '\n';
611
+ if (res.stopAfter) conn.end(payload, shutdown);
612
+ else conn.write(payload);
613
+ });
614
+ }
615
+ });
616
+ });
617
+
618
+ server.on('error', (e) => {
619
+ process.stderr.write(`Daemon server listen failed: ${e.message}\n`);
620
+ process.exit(1);
621
+ });
622
+
623
+ if (!IS_WINDOWS) try { unlinkSync(sp); } catch {}
624
+ server.listen(sp);
625
+ }
626
+
627
+ // ---------------------------------------------------------------------------
628
+ // CLI ↔ daemon communication
629
+ // ---------------------------------------------------------------------------
630
+
631
+ function connectToSocket(sp) {
632
+ return new Promise((resolve, reject) => {
633
+ const conn = net.connect(sp);
634
+ conn.on('connect', () => resolve(conn));
635
+ conn.on('error', reject);
636
+ });
637
+ }
638
+
639
+ async function getOrStartTabDaemon(targetId) {
640
+ const sp = sockPath(targetId);
641
+ // Try existing daemon
642
+ try { return await connectToSocket(sp); } catch {}
643
+
644
+ // Clean stale socket
645
+ if (!IS_WINDOWS) try { unlinkSync(sp); } catch {}
646
+
647
+ // Spawn daemon
648
+ const child = spawn(process.execPath, [process.argv[1], '_daemon', targetId], {
649
+ detached: true,
650
+ stdio: 'ignore',
651
+ });
652
+ child.unref();
653
+
654
+ // Wait for socket (includes time for user to click Allow)
655
+ for (let i = 0; i < DAEMON_CONNECT_RETRIES; i++) {
656
+ await sleep(DAEMON_CONNECT_DELAY);
657
+ try { return await connectToSocket(sp); } catch {}
658
+ }
659
+ throw new Error('Daemon failed to start — did you click Allow in Chrome?');
660
+ }
661
+
662
+ function sendCommand(conn, req) {
663
+ return new Promise((resolve, reject) => {
664
+ let buf = '';
665
+ let settled = false;
666
+
667
+ const cleanup = () => {
668
+ conn.off('data', onData);
669
+ conn.off('error', onError);
670
+ conn.off('end', onEnd);
671
+ conn.off('close', onClose);
672
+ };
673
+
674
+ const onData = (chunk) => {
675
+ buf += chunk.toString();
676
+ const idx = buf.indexOf('\n');
677
+ if (idx === -1) return;
678
+ settled = true;
679
+ cleanup();
680
+ resolve(JSON.parse(buf.slice(0, idx)));
681
+ conn.end();
682
+ };
683
+
684
+ const onError = (error) => {
685
+ if (settled) return;
686
+ settled = true;
687
+ cleanup();
688
+ reject(error);
689
+ };
690
+
691
+ const onEnd = () => {
692
+ if (settled) return;
693
+ settled = true;
694
+ cleanup();
695
+ reject(new Error('Connection closed before response'));
696
+ };
697
+
698
+ const onClose = () => {
699
+ if (settled) return;
700
+ settled = true;
701
+ cleanup();
702
+ reject(new Error('Connection closed before response'));
703
+ };
704
+
705
+ conn.on('data', onData);
706
+ conn.on('error', onError);
707
+ conn.on('end', onEnd);
708
+ conn.on('close', onClose);
709
+ req.id = 1;
710
+ conn.write(JSON.stringify(req) + '\n');
711
+ });
712
+ }
713
+
714
+ // ---------------------------------------------------------------------------
715
+ // Stop daemons
716
+ // ---------------------------------------------------------------------------
717
+
718
+ async function stopDaemons(targetPrefix) {
719
+ if (!existsSync(PAGES_CACHE)) return;
720
+ const pages = JSON.parse(readFileSync(PAGES_CACHE, 'utf8'));
721
+ const targets = targetPrefix
722
+ ? [resolvePrefix(targetPrefix, pages.map(p => p.targetId), 'target')]
723
+ : pages.map(p => p.targetId);
724
+
725
+ for (const targetId of targets) {
726
+ const sp = sockPath(targetId);
727
+ try {
728
+ const conn = await connectToSocket(sp);
729
+ await sendCommand(conn, { cmd: 'stop' });
730
+ } catch {
731
+ if (!IS_WINDOWS) try { unlinkSync(sp); } catch {}
732
+ }
733
+ }
734
+ }
735
+
736
+ // ---------------------------------------------------------------------------
737
+ // Main
738
+ // ---------------------------------------------------------------------------
739
+
740
+ const USAGE = `cdp - lightweight Chrome DevTools Protocol CLI (no Puppeteer)
741
+
742
+ Usage: cdp <command> [args]
743
+
744
+ list List open pages (shows unique target prefixes)
745
+ snap <target> Accessibility tree snapshot
746
+ eval <target> <expr> Evaluate JS expression
747
+ shot <target> [file] Screenshot (default: screenshot-<target>.png in runtime dir); prints coordinate mapping
748
+ html <target> [selector] Get HTML (full page or CSS selector)
749
+ nav <target> <url> Navigate to URL and wait for load completion
750
+ net <target> Network performance entries
751
+ click <target> <selector> Click an element by CSS selector
752
+ clickxy <target> <x> <y> Click at CSS pixel coordinates (see coordinate note below)
753
+ type <target> <text> Type text at current focus via Input.insertText
754
+ Works in cross-origin iframes unlike eval-based approaches
755
+ loadall <target> <selector> [ms] Repeatedly click a "load more" button until it disappears
756
+ Optional interval in ms between clicks (default 1500)
757
+ evalraw <target> <method> [json] Send a raw CDP command; returns JSON result
758
+ e.g. evalraw <t> "DOM.getDocument" '{}'
759
+ open [url] Open a new tab (default: about:blank)
760
+ Note: each new tab triggers a fresh "Allow debugging?" prompt
761
+ stop [target] Stop daemon(s)
762
+
763
+ <target> is a unique targetId prefix from "cdp list". If a prefix is ambiguous,
764
+ use more characters.
765
+
766
+ COORDINATE SYSTEM
767
+ shot captures the viewport at the device's native resolution.
768
+ The screenshot image size = CSS pixels × DPR (device pixel ratio).
769
+ For CDP Input events (clickxy, etc.) you need CSS pixels, not image pixels.
770
+
771
+ CSS pixels = screenshot image pixels / DPR
772
+
773
+ shot prints the DPR and an example conversion for the current page.
774
+ Typical Retina (DPR=2): CSS px ≈ screenshot px × 0.5
775
+ If your viewer rescales the image further, account for that scaling too.
776
+
777
+ EVAL SAFETY NOTE
778
+ Avoid index-based DOM selection (querySelectorAll(...)[i]) across multiple
779
+ eval calls when the list can change between calls (e.g. after clicking
780
+ "Ignore" buttons on a feed — indices shift). Prefer stable selectors or
781
+ collect all data in a single eval.
782
+
783
+ DAEMON IPC (for advanced use / scripting)
784
+ Each tab runs a persistent daemon at Unix socket in the runtime dir (see below).
785
+ Protocol: newline-delimited JSON (one JSON object per line, UTF-8).
786
+ Request: {"id":<number>, "cmd":"<command>", "args":["arg1","arg2",...]}
787
+ Response: {"id":<number>, "ok":true, "result":"<string>"}
788
+ or {"id":<number>, "ok":false, "error":"<message>"}
789
+ Commands mirror the CLI: snap, eval, shot, html, nav, net, click, clickxy,
790
+ type, loadall, evalraw, stop. Use evalraw to send arbitrary CDP methods.
791
+ The socket disappears after 20 min of inactivity or when the tab closes.
792
+ `;
793
+
794
+ const NEEDS_TARGET = new Set([
795
+ 'snap','snapshot','eval','shot','screenshot','html','nav','navigate',
796
+ 'net','network','click','clickxy','type','loadall','evalraw',
797
+ ]);
798
+
799
+ async function main() {
800
+ const [cmd, ...args] = process.argv.slice(2);
801
+
802
+ // Daemon mode (internal)
803
+ if (cmd === '_daemon') { await runDaemon(args[0]); return; }
804
+
805
+ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
806
+ console.log(USAGE); process.exit(0);
807
+ }
808
+
809
+ if (cmd === 'list' || cmd === 'ls') {
810
+ const cdp = new CDP();
811
+ await cdp.connect(getWsUrl());
812
+ const pages = await getPages(cdp);
813
+ cdp.close();
814
+ writeFileSync(PAGES_CACHE, JSON.stringify(pages), { mode: 0o600 });
815
+ const activeUrl = await getActiveChromeUrl();
816
+ if (activeUrl) {
817
+ console.log('(* = current active tab in Chrome)');
818
+ }
819
+ console.log(formatPageList(pages, activeUrl));
820
+ setTimeout(() => process.exit(0), 100);
821
+ return;
822
+ }
823
+
824
+ // Open new tab
825
+ if (cmd === 'open') {
826
+ const url = args[0] || 'about:blank';
827
+ const cdp = new CDP();
828
+ await cdp.connect(getWsUrl());
829
+ const { targetId } = await cdp.send('Target.createTarget', { url });
830
+ // Refresh cache; new tab may not appear in getTargets immediately, so add it manually
831
+ const pages = await getPages(cdp);
832
+ if (!pages.some(p => p.targetId === targetId)) {
833
+ pages.push({ targetId, title: url, url });
834
+ }
835
+ cdp.close();
836
+ writeFileSync(PAGES_CACHE, JSON.stringify(pages), { mode: 0o600 });
837
+ console.log(`Opened new tab: ${targetId.slice(0, 8)} ${url}`);
838
+ console.log('Note: this tab will need "Allow debugging?" approval on first access.');
839
+ return;
840
+ }
841
+
842
+ // Stop
843
+ if (cmd === 'stop') {
844
+ await stopDaemons(args[0]);
845
+ return;
846
+ }
847
+
848
+ // Page commands — need target prefix
849
+ if (!NEEDS_TARGET.has(cmd)) {
850
+ console.error(`Unknown command: ${cmd}\n`);
851
+ console.log(USAGE);
852
+ process.exit(1);
853
+ }
854
+
855
+ const targetPrefix = args[0];
856
+ if (!targetPrefix) {
857
+ console.error('Error: target ID required. Run "cdp list" first.');
858
+ process.exit(1);
859
+ }
860
+
861
+ // Resolve prefix → full targetId from pages cache
862
+ if (!existsSync(PAGES_CACHE)) {
863
+ console.error('No page list cached. Run "cdp list" first.');
864
+ process.exit(1);
865
+ }
866
+ const pages = JSON.parse(readFileSync(PAGES_CACHE, 'utf8'));
867
+ const targetId = resolvePrefix(targetPrefix, pages.map(p => p.targetId), 'target', 'Run "cdp list".');
868
+
869
+ const conn = await getOrStartTabDaemon(targetId);
870
+
871
+ const cmdArgs = args.slice(1);
872
+
873
+ if (cmd === 'eval') {
874
+ const expr = cmdArgs.join(' ');
875
+ if (!expr) { console.error('Error: expression required'); process.exit(1); }
876
+ cmdArgs[0] = expr;
877
+ } else if (cmd === 'type') {
878
+ // Join all remaining args as text (allows spaces)
879
+ const text = cmdArgs.join(' ');
880
+ if (!text) { console.error('Error: text required'); process.exit(1); }
881
+ cmdArgs[0] = text;
882
+ } else if (cmd === 'evalraw') {
883
+ // args: [method, ...jsonParts] — join json parts in case of spaces
884
+ if (!cmdArgs[0]) { console.error('Error: CDP method required'); process.exit(1); }
885
+ if (cmdArgs.length > 2) cmdArgs[1] = cmdArgs.slice(1).join(' ');
886
+ }
887
+
888
+ if ((cmd === 'nav' || cmd === 'navigate') && !cmdArgs[0]) {
889
+ console.error('Error: URL required');
890
+ process.exit(1);
891
+ }
892
+
893
+ const response = await sendCommand(conn, { cmd, args: cmdArgs });
894
+
895
+ if (response.ok) {
896
+ if (response.result) console.log(response.result);
897
+ } else {
898
+ console.error('Error:', response.error);
899
+ process.exitCode = 1;
900
+ }
901
+ }
902
+
903
+ main().catch(e => { console.error(e.message); process.exit(1); });