@crouton-kit/crouter 0.3.18 → 0.3.20

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 (289) hide show
  1. package/dist/build-root.js +6 -0
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
  5. package/dist/builtin-memory/crouter-development/personas.md +117 -0
  6. package/dist/builtin-memory/crouter-development/plugins.md +163 -0
  7. package/dist/builtin-memory/design.md +60 -0
  8. package/dist/builtin-memory/development.md +116 -0
  9. package/dist/builtin-memory/planning.md +66 -0
  10. package/dist/builtin-memory/spec.md +99 -0
  11. package/dist/builtin-personas/design/PERSONA.md +1 -0
  12. package/dist/builtin-personas/design/orchestrator.md +2 -1
  13. package/dist/builtin-personas/developer/PERSONA.md +1 -0
  14. package/dist/builtin-personas/developer/orchestrator.md +2 -1
  15. package/dist/builtin-personas/explore/PERSONA.md +1 -0
  16. package/dist/builtin-personas/explore/orchestrator.md +4 -0
  17. package/dist/builtin-personas/general/PERSONA.md +1 -0
  18. package/dist/builtin-personas/general/orchestrator.md +4 -0
  19. package/dist/builtin-personas/lifecycle/resident.md +1 -1
  20. package/dist/builtin-personas/lifecycle/terminal.md +5 -2
  21. package/dist/builtin-personas/orchestration-kernel.md +15 -24
  22. package/dist/builtin-personas/plan/PERSONA.md +1 -0
  23. package/dist/builtin-personas/plan/orchestrator.md +2 -1
  24. package/dist/builtin-personas/plan/reviewers/architecture-fit/PERSONA.md +1 -0
  25. package/dist/builtin-personas/plan/reviewers/code-smells/PERSONA.md +1 -0
  26. package/dist/builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md +1 -0
  27. package/dist/builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md +1 -0
  28. package/dist/builtin-personas/plan/reviewers/security/PERSONA.md +1 -0
  29. package/dist/builtin-personas/review/PERSONA.md +1 -0
  30. package/dist/builtin-personas/review/orchestrator.md +4 -0
  31. package/dist/builtin-personas/runtime-base.md +6 -3
  32. package/dist/builtin-personas/spec/PERSONA.md +1 -0
  33. package/dist/builtin-personas/spec/orchestrator.md +2 -1
  34. package/dist/builtin-personas/waiting.md +8 -0
  35. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
  36. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
  37. package/dist/builtin-views/_lib/states.mjs +161 -0
  38. package/dist/builtin-views/canvas/client.mjs +303 -0
  39. package/dist/builtin-views/canvas/view.mjs +576 -0
  40. package/dist/builtin-views/git-pr/client.mjs +440 -0
  41. package/dist/builtin-views/git-pr/view.mjs +675 -0
  42. package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
  43. package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
  44. package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
  45. package/dist/builtin-views/inbox/view.mjs +889 -0
  46. package/dist/builtin-views/linkedin/client.mjs +610 -0
  47. package/dist/builtin-views/linkedin/view.mjs +1171 -0
  48. package/dist/clients/attach/attach-cmd.d.ts +2 -0
  49. package/dist/clients/attach/attach-cmd.js +354 -0
  50. package/dist/clients/attach/chat-view.d.ts +77 -0
  51. package/dist/clients/attach/chat-view.js +450 -0
  52. package/dist/clients/attach/clipboard-image.d.ts +16 -0
  53. package/dist/clients/attach/clipboard-image.js +113 -0
  54. package/dist/clients/attach/config-load.d.ts +31 -0
  55. package/dist/clients/attach/config-load.js +113 -0
  56. package/dist/clients/attach/extension-dialogs.d.ts +29 -0
  57. package/dist/clients/attach/extension-dialogs.js +101 -0
  58. package/dist/clients/attach/input-controller.d.ts +54 -0
  59. package/dist/clients/attach/input-controller.js +204 -0
  60. package/dist/clients/attach/slash-commands.d.ts +36 -0
  61. package/dist/clients/attach/slash-commands.js +200 -0
  62. package/dist/clients/attach/view-socket.d.ts +48 -0
  63. package/dist/clients/attach/view-socket.js +126 -0
  64. package/dist/commands/attention.js +3 -3
  65. package/dist/commands/canvas-prune.js +1 -1
  66. package/dist/commands/daemon.js +3 -2
  67. package/dist/commands/human/prompts.js +1 -1
  68. package/dist/commands/human/queue.js +54 -8
  69. package/dist/commands/human/shared.d.ts +2 -0
  70. package/dist/commands/memory/find.d.ts +1 -0
  71. package/dist/commands/memory/find.js +180 -0
  72. package/dist/commands/memory/lint.d.ts +1 -0
  73. package/dist/commands/memory/lint.js +140 -0
  74. package/dist/commands/memory/list.d.ts +1 -0
  75. package/dist/commands/memory/list.js +79 -0
  76. package/dist/commands/memory/read.js +103 -0
  77. package/dist/commands/memory/shared.d.ts +30 -0
  78. package/dist/commands/memory/shared.js +122 -0
  79. package/dist/commands/memory/write.d.ts +1 -0
  80. package/dist/commands/memory/write.js +85 -0
  81. package/dist/commands/memory.d.ts +2 -0
  82. package/dist/commands/memory.js +27 -0
  83. package/dist/commands/node.d.ts +3 -2
  84. package/dist/commands/node.js +610 -61
  85. package/dist/commands/pkg/market-manage.js +1 -1
  86. package/dist/commands/push.js +6 -6
  87. package/dist/commands/revive.js +1 -1
  88. package/dist/commands/skill/author.js +1 -1
  89. package/dist/commands/skill/shared.d.ts +1 -8
  90. package/dist/commands/skill/shared.js +2 -55
  91. package/dist/commands/skill.js +9 -14
  92. package/dist/commands/sys/doctor.js +1 -1
  93. package/dist/commands/sys/update.js +1 -1
  94. package/dist/commands/view-cycle.d.ts +2 -0
  95. package/dist/commands/view-cycle.js +125 -0
  96. package/dist/commands/view-list.d.ts +2 -0
  97. package/dist/commands/view-list.js +66 -0
  98. package/dist/commands/view-new.d.ts +2 -0
  99. package/dist/commands/view-new.js +70 -0
  100. package/dist/commands/view-pick.d.ts +2 -0
  101. package/dist/commands/view-pick.js +119 -0
  102. package/dist/commands/view-run.d.ts +2 -0
  103. package/dist/commands/view-run.js +191 -0
  104. package/dist/commands/view.d.ts +2 -0
  105. package/dist/commands/view.js +29 -0
  106. package/dist/core/__tests__/broker-attach-limits.test.d.ts +1 -0
  107. package/dist/core/__tests__/broker-attach-limits.test.js +157 -0
  108. package/dist/core/__tests__/broker-attach-stream.test.d.ts +1 -0
  109. package/dist/core/__tests__/broker-attach-stream.test.js +125 -0
  110. package/dist/core/__tests__/broker-crash-teardown.test.d.ts +1 -0
  111. package/dist/core/__tests__/broker-crash-teardown.test.js +116 -0
  112. package/dist/core/__tests__/broker-dialogs.test.d.ts +1 -0
  113. package/dist/core/__tests__/broker-dialogs.test.js +126 -0
  114. package/dist/core/__tests__/broker-dormant-wake.test.d.ts +1 -0
  115. package/dist/core/__tests__/broker-dormant-wake.test.js +51 -0
  116. package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
  117. package/dist/core/__tests__/broker-lifecycle.test.js +87 -0
  118. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  119. package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
  120. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.d.ts +1 -0
  121. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +102 -0
  122. package/dist/core/__tests__/canvas-inbox-watcher.test.js +2 -36
  123. package/dist/core/__tests__/cascade-close.test.js +12 -2
  124. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  125. package/dist/core/__tests__/child-death-wake.test.js +245 -0
  126. package/dist/core/__tests__/context-intro.test.js +76 -62
  127. package/dist/core/__tests__/daemon-boot.test.js +14 -5
  128. package/dist/core/__tests__/daemon-liveness.test.js +34 -9
  129. package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
  130. package/dist/core/__tests__/detach-focus.test.js +206 -0
  131. package/dist/core/__tests__/draw-style.test.d.ts +1 -0
  132. package/dist/core/__tests__/draw-style.test.js +258 -0
  133. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  134. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
  135. package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
  136. package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
  137. package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
  138. package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
  139. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
  140. package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
  141. package/dist/core/__tests__/helpers/broker-clients.d.ts +43 -0
  142. package/dist/core/__tests__/helpers/broker-clients.js +178 -0
  143. package/dist/core/__tests__/helpers/harness.d.ts +9 -0
  144. package/dist/core/__tests__/helpers/harness.js +111 -1
  145. package/dist/core/__tests__/home-session.test.js +41 -8
  146. package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
  147. package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
  148. package/dist/core/__tests__/kickoff.test.js +37 -3
  149. package/dist/core/__tests__/live-mutation-verbs.test.d.ts +1 -0
  150. package/dist/core/__tests__/live-mutation-verbs.test.js +175 -0
  151. package/dist/core/__tests__/live-mutation.test.js +6 -132
  152. package/dist/core/__tests__/memory.test.js +23 -115
  153. package/dist/core/__tests__/placement-focus.test.js +5 -0
  154. package/dist/core/__tests__/placement-teardown.test.js +54 -11
  155. package/dist/core/__tests__/relaunch.test.js +4 -3
  156. package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
  157. package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
  158. package/dist/core/__tests__/spawn-root.test.js +10 -0
  159. package/dist/core/__tests__/spike-harness.test.js +1 -0
  160. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  161. package/dist/core/__tests__/wake-bearings.test.js +156 -0
  162. package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
  163. package/dist/core/__tests__/wake-origin.test.js +110 -0
  164. package/dist/core/bootstrap.js +1 -1
  165. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  166. package/dist/core/canvas/browse/app.js +24 -2
  167. package/dist/core/canvas/browse/model.d.ts +38 -2
  168. package/dist/core/canvas/browse/model.js +134 -10
  169. package/dist/core/canvas/browse/render.d.ts +6 -12
  170. package/dist/core/canvas/browse/render.js +72 -104
  171. package/dist/core/canvas/canvas.js +16 -23
  172. package/dist/core/canvas/db.js +46 -0
  173. package/dist/core/canvas/index.d.ts +1 -0
  174. package/dist/core/canvas/index.js +1 -0
  175. package/dist/core/canvas/pid.d.ts +4 -0
  176. package/dist/core/canvas/pid.js +23 -0
  177. package/dist/core/canvas/render.d.ts +11 -2
  178. package/dist/core/canvas/render.js +69 -0
  179. package/dist/core/canvas/types.d.ts +91 -3
  180. package/dist/core/canvas/wakeups.d.ts +76 -0
  181. package/dist/core/canvas/wakeups.js +185 -0
  182. package/dist/core/config.js +4 -1
  183. package/dist/core/frontmatter.js +37 -124
  184. package/dist/core/memory-resolver.d.ts +49 -0
  185. package/dist/core/memory-resolver.js +141 -0
  186. package/dist/core/personas/loader.d.ts +7 -0
  187. package/dist/core/personas/loader.js +36 -5
  188. package/dist/core/personas/resolve.js +13 -3
  189. package/dist/core/predicate.d.ts +63 -0
  190. package/dist/core/predicate.js +189 -0
  191. package/dist/core/resolver.js +26 -5
  192. package/dist/core/runtime/bearings.d.ts +53 -12
  193. package/dist/core/runtime/bearings.js +132 -59
  194. package/dist/core/runtime/broker-cli.d.ts +1 -0
  195. package/dist/core/runtime/broker-cli.js +46 -0
  196. package/dist/core/runtime/broker-protocol.d.ts +332 -0
  197. package/dist/core/runtime/broker-protocol.js +153 -0
  198. package/dist/core/runtime/broker-sdk.d.ts +48 -0
  199. package/dist/core/runtime/broker-sdk.js +72 -0
  200. package/dist/core/runtime/broker.d.ts +55 -0
  201. package/dist/core/runtime/broker.js +1128 -0
  202. package/dist/core/runtime/close.js +35 -6
  203. package/dist/core/runtime/host.d.ts +53 -0
  204. package/dist/core/runtime/host.js +186 -0
  205. package/dist/core/runtime/kickoff.d.ts +2 -1
  206. package/dist/core/runtime/kickoff.js +91 -5
  207. package/dist/core/runtime/launch.d.ts +54 -4
  208. package/dist/core/runtime/launch.js +85 -5
  209. package/dist/core/runtime/lifecycle.js +23 -6
  210. package/dist/core/runtime/memory.d.ts +2 -42
  211. package/dist/core/runtime/memory.js +11 -162
  212. package/dist/core/runtime/nodes.d.ts +37 -0
  213. package/dist/core/runtime/nodes.js +60 -1
  214. package/dist/core/runtime/persona.js +21 -11
  215. package/dist/core/runtime/pi-vendored.d.ts +18 -0
  216. package/dist/core/runtime/pi-vendored.js +49 -0
  217. package/dist/core/runtime/placement.d.ts +32 -14
  218. package/dist/core/runtime/placement.js +206 -52
  219. package/dist/core/runtime/promote.d.ts +0 -6
  220. package/dist/core/runtime/promote.js +4 -12
  221. package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
  222. package/dist/core/runtime/{demote.js → recycle.js} +27 -11
  223. package/dist/core/runtime/reset.js +11 -8
  224. package/dist/core/runtime/revive.d.ts +2 -0
  225. package/dist/core/runtime/revive.js +34 -34
  226. package/dist/core/runtime/spawn.d.ts +23 -0
  227. package/dist/core/runtime/spawn.js +77 -23
  228. package/dist/core/runtime/stop-guard.d.ts +1 -1
  229. package/dist/core/runtime/stop-guard.js +6 -1
  230. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  231. package/dist/core/runtime/tmux-chrome.js +1 -1
  232. package/dist/core/runtime/tmux.d.ts +28 -0
  233. package/dist/core/runtime/tmux.js +80 -6
  234. package/dist/core/scope.d.ts +11 -0
  235. package/dist/core/scope.js +39 -0
  236. package/dist/core/spawn.d.ts +5 -0
  237. package/dist/core/spawn.js +20 -2
  238. package/dist/core/substrate/gate.d.ts +13 -0
  239. package/dist/core/substrate/gate.js +21 -0
  240. package/dist/core/substrate/index.d.ts +7 -0
  241. package/dist/core/substrate/index.js +18 -0
  242. package/dist/core/substrate/on-read.d.ts +14 -0
  243. package/dist/core/substrate/on-read.js +292 -0
  244. package/dist/core/substrate/render.d.ts +25 -0
  245. package/dist/core/substrate/render.js +256 -0
  246. package/dist/core/substrate/schema.d.ts +76 -0
  247. package/dist/core/substrate/schema.js +124 -0
  248. package/dist/core/substrate/session-cache.d.ts +30 -0
  249. package/dist/core/substrate/session-cache.js +77 -0
  250. package/dist/core/substrate/subject.d.ts +41 -0
  251. package/dist/core/substrate/subject.js +54 -0
  252. package/dist/core/tui/contract.d.ts +83 -0
  253. package/dist/core/tui/contract.js +8 -0
  254. package/dist/core/tui/draw.d.ts +96 -0
  255. package/dist/core/tui/draw.js +339 -0
  256. package/dist/core/tui/host.d.ts +29 -0
  257. package/dist/core/tui/host.js +379 -0
  258. package/dist/core/tui/loader.d.ts +16 -0
  259. package/dist/core/tui/loader.js +94 -0
  260. package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
  261. package/dist/core/wake.d.ts +86 -0
  262. package/dist/core/wake.js +308 -0
  263. package/dist/daemon/crtrd.d.ts +29 -4
  264. package/dist/daemon/crtrd.js +662 -46
  265. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  266. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
  267. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
  268. package/dist/pi-extensions/canvas-commands.d.ts +3 -0
  269. package/dist/pi-extensions/canvas-commands.js +10 -0
  270. package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
  271. package/dist/pi-extensions/canvas-context-intro.js +55 -15
  272. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  273. package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
  274. package/dist/pi-extensions/canvas-nav.d.ts +3 -0
  275. package/dist/pi-extensions/canvas-nav.js +87 -23
  276. package/dist/pi-extensions/canvas-stophook.js +17 -8
  277. package/dist/pi-extensions/canvas-view.d.ts +21 -0
  278. package/dist/pi-extensions/canvas-view.js +75 -0
  279. package/dist/prompts/skill.js +19 -26
  280. package/dist/prompts/view.d.ts +7 -0
  281. package/dist/prompts/view.js +101 -0
  282. package/dist/types.d.ts +4 -0
  283. package/dist/types.js +1 -0
  284. package/package.json +9 -4
  285. package/dist/commands/skill/find.d.ts +0 -4
  286. package/dist/commands/skill/find.js +0 -257
  287. package/dist/commands/skill/read.js +0 -91
  288. /package/dist/commands/{skill → memory}/read.d.ts +0 -0
  289. /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
@@ -0,0 +1,610 @@
1
+ // @ts-check
2
+ /**
3
+ * LinkedIn data layer for the crtr `linkedin` view (Phase A).
4
+ *
5
+ * Self-contained ESM, Node-builtins-only. Imports NOTHING from crtr so it can be
6
+ * shipped verbatim (`cp -R src/builtin-views dist/builtin-views`) and dynamically
7
+ * `import()`ed by the view at runtime where there is no TS toolchain.
8
+ *
9
+ * It wraps the `capture` CLI (`capture exec "<js>" --target <tabId> [--port N]`),
10
+ * which esbuild-bundles the forked LinkedIn vault libs and `Runtime.evaluate`s
11
+ * them inside the logged-in LinkedIn browser page over CDP. Each call is a fresh
12
+ * `capture` process (~1–3s); the view calls `getContext()` ONCE and threads
13
+ * `csrf`/`memberId` explicitly on every later call to skip re-auth round-trips.
14
+ *
15
+ * NOTHING here throws. Every exported function returns a `Result<T>` discriminated
16
+ * union; failures surface as a typed `ClientError` so the view can render guidance
17
+ * instead of crashing.
18
+ *
19
+ * @module linkedin/client
20
+ */
21
+
22
+ import { execFile } from 'node:child_process';
23
+
24
+ // ── Types ───────────────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Auth context read once from the page session via `getContext()`.
28
+ * @typedef {Object} LiContext
29
+ * @property {string} csrf CSRF token (form `"ajax:<digits>"`), from the JSESSIONID cookie.
30
+ * @property {string} memberId Current user member ID (`ACo…`).
31
+ */
32
+
33
+ /**
34
+ * One inbox conversation, flattened from the vault `listConversations` shape.
35
+ * @typedef {Object} Conversation
36
+ * @property {string} urn Conversation URN (`urn:li:msg_conversation:(…)`).
37
+ * @property {string} name Display name: group title, else first participant's name.
38
+ * @property {string} lastMessage Text of the most recent message.
39
+ * @property {boolean} unread True when `unreadCount > 0`.
40
+ * @property {number} ts Last-activity time as epoch ms (0 if unknown).
41
+ * @property {string} recipientId `participants[0].memberId` (the 1:1 counterpart; '' for empty/group).
42
+ */
43
+
44
+ /**
45
+ * One message in a thread, flattened from the vault `viewConversation` shape.
46
+ * @typedef {Object} Message
47
+ * @property {string} urn Message URN (`urn:li:msg_message:(…)`).
48
+ * @property {string} sender Sender full name.
49
+ * @property {string} text Message body text.
50
+ * @property {number} ts Delivery time as epoch ms (0 if unknown).
51
+ * @property {boolean} fromMe True when the sender's member ID equals the caller's `myMemberId`.
52
+ */
53
+
54
+ /**
55
+ * Typed failure. `kind` drives the guidance banner the view shows; `error` only
56
+ * for the `error` catch-all carries a raw message.
57
+ * @typedef {{kind:'no-cdp'}
58
+ * | {kind:'no-tab'}
59
+ * | {kind:'not-logged-in'}
60
+ * | {kind:'not-messaging'}
61
+ * | {kind:'rate-limited'}
62
+ * | {kind:'not-connection'}
63
+ * | {kind:'capture-not-dev'}
64
+ * | {kind:'error', message:string}} ClientError
65
+ */
66
+
67
+ /**
68
+ * Never-throw return contract. `ok:true` carries data; `ok:false` carries a typed error.
69
+ * @template T
70
+ * @typedef {{ok:true, data:T} | {ok:false, error:ClientError}} Result
71
+ */
72
+
73
+ /**
74
+ * Common options every function accepts. `target` is the CDP tab id from
75
+ * {@link discoverTab}; `port` (string|number) skips capture's port auto-detection.
76
+ * @typedef {Object} BaseOpts
77
+ * @property {string} [target] CDP tab id (`--target`). Omit ⇒ capture falls back to $CDP_TARGET/session.
78
+ * @property {string|number} [port] CDP port (`--port`); optional speed-up.
79
+ */
80
+
81
+ // ── Config: locating the `capture` binary ────────────────────────────────────
82
+
83
+ /**
84
+ * Dev-checkout fallback. The LinkedIn vault libs are dev-checkout-only (the
85
+ * published `capture` package throws `DEV_ONLY_MSG`), so this path is the most
86
+ * likely place a working binary lives if `capture` is not on PATH.
87
+ */
88
+ const CAPTURE_DEV_FALLBACK = '/Users/silasrhyneer/Code/cli/capture/bin/capture';
89
+
90
+ /**
91
+ * Binary candidates, in order. Prefer an explicit override, then PATH `capture`,
92
+ * then the dev checkout. We try the next only when a candidate fails to spawn
93
+ * (ENOENT) — a candidate that spawns and exits nonzero is authoritative.
94
+ * @returns {string[]}
95
+ */
96
+ function captureCandidates() {
97
+ const out = [];
98
+ if (process.env.CAPTURE_BIN) out.push(process.env.CAPTURE_BIN);
99
+ out.push('capture');
100
+ out.push(CAPTURE_DEV_FALLBACK);
101
+ return out;
102
+ }
103
+
104
+ // ── Result helpers ───────────────────────────────────────────────────────────
105
+
106
+ /** @template T @param {T} data @returns {Result<T>} */
107
+ function ok(data) {
108
+ return { ok: true, data };
109
+ }
110
+
111
+ /** @param {ClientError} error @returns {{ok:false, error:ClientError}} */
112
+ function fail(error) {
113
+ return { ok: false, error };
114
+ }
115
+
116
+ // ── capture stderr → ClientError mapping ─────────────────────────────────────
117
+
118
+ /**
119
+ * Map a failed `capture` invocation's stderr to a typed {@link ClientError}.
120
+ *
121
+ * Mapping table (kept in lockstep with the crtr-views spec "Data model"):
122
+ * No browser with CDP found ............................. no-cdp
123
+ * No tab found ......................................... no-tab
124
+ * Unauthenticated ..................................... not-logged-in
125
+ * Messaging queryId not found | Navigate to /messaging/ not-messaging
126
+ * RateLimited | 429 ................................... rate-limited
127
+ * must be a 1st-degree connection .................... not-connection
128
+ * DEV_ONLY_MSG ....................................... capture-not-dev
129
+ * (anything else) .................................... error{message}
130
+ *
131
+ * @param {string} stderr
132
+ * @returns {ClientError}
133
+ */
134
+ function classifyError(stderr) {
135
+ const s = String(stderr || '');
136
+ if (/No browser with CDP found/i.test(s)) return { kind: 'no-cdp' };
137
+ // A dead/unreachable debugger port surfaces as a raw connection failure: capture
138
+ // probes http://localhost:<port>/json/version, so an unlistened port yields
139
+ // `fetch failed` (Node fetch wrapping ECONNREFUSED). Classify it as no-cdp so the
140
+ // view shows the §5 "launch a debuggable browser" panel, not the generic error.
141
+ if (/fetch failed/i.test(s) || /failed to fetch/i.test(s) || /ECONNREFUSED/i.test(s)) {
142
+ return { kind: 'no-cdp' };
143
+ }
144
+ if (/No tab found/i.test(s)) return { kind: 'no-tab' };
145
+ if (/Unauthenticated/i.test(s)) return { kind: 'not-logged-in' };
146
+ if (/Messaging queryId not found/i.test(s) || /Navigate to \/messaging\//i.test(s)) {
147
+ return { kind: 'not-messaging' };
148
+ }
149
+ if (/RateLimited/i.test(s) || /\b429\b/.test(s)) return { kind: 'rate-limited' };
150
+ if (/must be a 1st-degree connection/i.test(s)) return { kind: 'not-connection' };
151
+ if (/DEV_ONLY_MSG/i.test(s)) return { kind: 'capture-not-dev' };
152
+ return { kind: 'error', message: extractErrorMessage(s) };
153
+ }
154
+
155
+ /**
156
+ * Pull a human message out of capture stderr: prefer the last `ERROR:` line,
157
+ * else the last non-empty line, else a generic fallback.
158
+ * @param {string} stderr
159
+ * @returns {string}
160
+ */
161
+ function extractErrorMessage(stderr) {
162
+ const lines = String(stderr || '')
163
+ .split(/\r?\n/)
164
+ .map((l) => l.trim())
165
+ .filter(Boolean);
166
+ for (let i = lines.length - 1; i >= 0; i--) {
167
+ if (/^ERROR:/i.test(lines[i])) return lines[i].replace(/^ERROR:\s*/i, '').trim();
168
+ }
169
+ if (lines.length) return lines[lines.length - 1];
170
+ return 'capture exec failed';
171
+ }
172
+
173
+ // ── capture command construction ─────────────────────────────────────────────
174
+
175
+ /**
176
+ * Serialize a JS arg object as a literal safe to splice into the exec code
177
+ * string. JSON is a near-subset of JS object-literal syntax; we additionally
178
+ * escape U+2028/U+2029 (valid in JSON strings, historically illegal in JS) so a
179
+ * pasted reply can't break the splice.
180
+ * @param {Record<string, unknown>} obj
181
+ * @returns {string}
182
+ */
183
+ function jsLiteral(obj) {
184
+ return JSON.stringify(obj).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
185
+ }
186
+
187
+ /**
188
+ * Build the argv (for `execFile`) and the exec code string for a vault-lib call.
189
+ * The code string is a SINGLE argv element — passed as an array element, never a
190
+ * shell string — so there is no quoting to escape at the process boundary.
191
+ * `libArgs === null` emits a no-arg call (used by getContext).
192
+ *
193
+ * @param {string} fnName
194
+ * @param {Record<string, unknown> | null} libArgs
195
+ * @param {BaseOpts} [opts]
196
+ * @returns {{argv:string[], code:string}}
197
+ */
198
+ function buildExec(fnName, libArgs, opts) {
199
+ const call = libArgs === null ? `${fnName}()` : `${fnName}(${jsLiteral(libArgs)})`;
200
+ const code = `import {${fnName}} from 'libs/linkedin'; return await ${call}`;
201
+ /** @type {string[]} */
202
+ const argv = ['exec', code];
203
+ if (opts && opts.target) argv.push('--target', String(opts.target));
204
+ if (opts && opts.port != null && opts.port !== '') argv.push('--port', String(opts.port));
205
+ return { argv, code };
206
+ }
207
+
208
+ /**
209
+ * Build the `capture list` argv (tab discovery; no CDP, no target).
210
+ * @param {BaseOpts} [opts]
211
+ * @returns {string[]}
212
+ */
213
+ function buildListArgv(opts) {
214
+ const argv = ['list'];
215
+ if (opts && opts.port != null && opts.port !== '') argv.push('--port', String(opts.port));
216
+ return argv;
217
+ }
218
+
219
+ /** The LinkedIn inbox URL the recovery flow opens / navigates to. */
220
+ const MESSAGING_URL = 'https://www.linkedin.com/messaging/';
221
+
222
+ /**
223
+ * Build the `capture open <url>` argv (open/reuse a tab; no --target).
224
+ * @param {BaseOpts} [opts]
225
+ * @returns {string[]}
226
+ */
227
+ function buildOpenArgv(opts) {
228
+ const argv = ['open', MESSAGING_URL];
229
+ if (opts && opts.port != null && opts.port !== '') argv.push('--port', String(opts.port));
230
+ return argv;
231
+ }
232
+
233
+ /**
234
+ * Build the `capture navigate <url> --target <id>` argv (drive an existing tab).
235
+ * @param {BaseOpts} [opts]
236
+ * @returns {string[]}
237
+ */
238
+ function buildNavigateArgv(opts) {
239
+ const argv = ['navigate', MESSAGING_URL];
240
+ if (opts && opts.target) argv.push('--target', String(opts.target));
241
+ if (opts && opts.port != null && opts.port !== '') argv.push('--port', String(opts.port));
242
+ return argv;
243
+ }
244
+
245
+ /**
246
+ * Render an argv as the human-readable shell command (for logs/eyeballing). The
247
+ * code element (which contains spaces) gets double-quoted; nothing is executed.
248
+ * @param {string[]} argv
249
+ * @returns {string}
250
+ */
251
+ function toDisplay(argv) {
252
+ return (
253
+ 'capture ' +
254
+ argv
255
+ .map((a) => (/[\s"]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a))
256
+ .join(' ')
257
+ );
258
+ }
259
+
260
+ // ── Process runner (never throws) ────────────────────────────────────────────
261
+
262
+ /**
263
+ * @typedef {Object} RunResult
264
+ * @property {boolean} spawned False ⇒ no candidate binary could be spawned.
265
+ * @property {number} exitCode Process exit code (0 on success; -1 if not spawned).
266
+ * @property {string} stdout
267
+ * @property {string} stderr
268
+ * @property {string} [bin] Which candidate actually ran.
269
+ */
270
+
271
+ /**
272
+ * Run one candidate binary. Resolves (never rejects). `spawnError` (ENOENT) is
273
+ * signalled by `spawned:false` so the caller can try the next candidate.
274
+ * @param {string} bin
275
+ * @param {string[]} argv
276
+ * @returns {Promise<RunResult>}
277
+ */
278
+ function runOnce(bin, argv) {
279
+ return new Promise((resolve) => {
280
+ execFile(
281
+ bin,
282
+ argv,
283
+ { maxBuffer: 64 * 1024 * 1024, encoding: 'utf8' },
284
+ (err, stdout, stderr) => {
285
+ const out = typeof stdout === 'string' ? stdout : '';
286
+ const errOut = typeof stderr === 'string' ? stderr : '';
287
+ if (err && /** @type {any} */ (err).code === 'ENOENT') {
288
+ resolve({ spawned: false, exitCode: -1, stdout: out, stderr: errOut, bin });
289
+ return;
290
+ }
291
+ const code = err
292
+ ? typeof /** @type {any} */ (err).code === 'number'
293
+ ? /** @type {any} */ (err).code
294
+ : 1
295
+ : 0;
296
+ resolve({ spawned: true, exitCode: code, stdout: out, stderr: errOut, bin });
297
+ }
298
+ );
299
+ });
300
+ }
301
+
302
+ /**
303
+ * Run `capture` with the given argv, walking the candidate list until one spawns.
304
+ * @param {string[]} argv
305
+ * @returns {Promise<RunResult>}
306
+ */
307
+ async function runCapture(argv) {
308
+ let last = /** @type {RunResult} */ ({ spawned: false, exitCode: -1, stdout: '', stderr: '' });
309
+ for (const bin of captureCandidates()) {
310
+ last = await runOnce(bin, argv);
311
+ if (last.spawned) return last;
312
+ }
313
+ return last; // never spawned
314
+ }
315
+
316
+ /** @returns {ClientError} */
317
+ function captureMissingError() {
318
+ return {
319
+ kind: 'error',
320
+ message:
321
+ `capture binary not found — tried CAPTURE_BIN, PATH 'capture', and dev fallback ` +
322
+ `${CAPTURE_DEV_FALLBACK}. Install capture or set CAPTURE_BIN.`,
323
+ };
324
+ }
325
+
326
+ /**
327
+ * Run a vault-lib call and return the parsed JSON return value as a Result. The
328
+ * generic path; per-function wrappers map the raw value into the view's types.
329
+ * @param {string} fnName
330
+ * @param {Record<string, unknown> | null} libArgs
331
+ * @param {BaseOpts} opts
332
+ * @returns {Promise<Result<any>>}
333
+ */
334
+ async function execLib(fnName, libArgs, opts) {
335
+ const { argv } = buildExec(fnName, libArgs, opts);
336
+ const r = await runCapture(argv);
337
+ if (!r.spawned) return fail(captureMissingError());
338
+ if (r.exitCode !== 0) return fail(classifyError(r.stderr));
339
+ const out = String(r.stdout || '').trim();
340
+ if (out === '') return ok(null);
341
+ try {
342
+ return ok(JSON.parse(out));
343
+ } catch {
344
+ return fail({ kind: 'error', message: `could not parse capture output as JSON: ${truncate(out, 300)}` });
345
+ }
346
+ }
347
+
348
+ /** @param {string} s @param {number} n @returns {string} */
349
+ function truncate(s, n) {
350
+ const str = String(s);
351
+ return str.length > n ? str.slice(0, n) + '…' : str;
352
+ }
353
+
354
+ // ── Field mappers ────────────────────────────────────────────────────────────
355
+
356
+ /** @param {unknown} iso @returns {number} */
357
+ function parseTs(iso) {
358
+ if (typeof iso !== 'string' || iso === '') return 0;
359
+ const t = Date.parse(iso);
360
+ return Number.isNaN(t) ? 0 : t;
361
+ }
362
+
363
+ /**
364
+ * @param {any} c raw vault conversation
365
+ * @returns {Conversation}
366
+ */
367
+ function toConversation(c) {
368
+ const o = c || {};
369
+ const participants = Array.isArray(o.participants) ? o.participants : [];
370
+ const p0 = participants[0] || {};
371
+ const names = participants.map((/** @type {any} */ p) => p && p.name).filter(Boolean);
372
+ const name = o.title || p0.name || (names.length ? names.join(', ') : 'Unknown');
373
+ return {
374
+ urn: typeof o.conversationUrn === 'string' ? o.conversationUrn : '',
375
+ name,
376
+ lastMessage: typeof o.lastMessage === 'string' ? o.lastMessage : '',
377
+ unread: typeof o.unreadCount === 'number' ? o.unreadCount > 0 : false,
378
+ ts: parseTs(o.lastActivityAt),
379
+ recipientId: typeof p0.memberId === 'string' ? p0.memberId : '',
380
+ };
381
+ }
382
+
383
+ /**
384
+ * @param {any} m raw vault message
385
+ * @param {string} myMemberId
386
+ * @returns {Message}
387
+ */
388
+ function toMessage(m, myMemberId) {
389
+ const o = m || {};
390
+ return {
391
+ urn: typeof o.messageUrn === 'string' ? o.messageUrn : '',
392
+ sender: typeof o.fromName === 'string' ? o.fromName : '',
393
+ text: typeof o.text === 'string' ? o.text : '',
394
+ ts: parseTs(o.sentAt),
395
+ fromMe: !!myMemberId && o.fromMemberId === myMemberId,
396
+ };
397
+ }
398
+
399
+ /** @param {string} url @returns {boolean} */
400
+ function isLinkedInUrl(url) {
401
+ try {
402
+ const h = new URL(url).hostname.toLowerCase();
403
+ return h === 'linkedin.com' || h.endsWith('.linkedin.com');
404
+ } catch {
405
+ return false;
406
+ }
407
+ }
408
+
409
+ // ── Public API ───────────────────────────────────────────────────────────────
410
+
411
+ /**
412
+ * Find a usable CDP tab id. Runs `capture list`, parses the JSON, and returns the
413
+ * first LinkedIn `/messaging/` tab (preferred), else any LinkedIn tab. No
414
+ * LinkedIn tab ⇒ `no-tab`.
415
+ * @param {BaseOpts} [opts]
416
+ * @returns {Promise<Result<string>>}
417
+ */
418
+ export async function discoverTab(opts = {}) {
419
+ const r = await runCapture(buildListArgv(opts));
420
+ if (!r.spawned) return fail(captureMissingError());
421
+ if (r.exitCode !== 0) return fail(classifyError(r.stderr));
422
+ let tabs;
423
+ try {
424
+ tabs = JSON.parse(String(r.stdout || '').trim() || '[]');
425
+ } catch {
426
+ return fail({ kind: 'error', message: `could not parse capture list output: ${truncate(r.stdout, 300)}` });
427
+ }
428
+ if (!Array.isArray(tabs)) return fail({ kind: 'no-tab' });
429
+ const linkedin = tabs.filter((t) => t && typeof t.url === 'string' && isLinkedInUrl(t.url));
430
+ const messaging = linkedin.find((t) => /\/messaging\//i.test(t.url));
431
+ const chosen = messaging || linkedin[0];
432
+ if (chosen && chosen.id) return ok(String(chosen.id));
433
+ return fail({ kind: 'no-tab' });
434
+ }
435
+
436
+ /**
437
+ * Open (or reuse) a LinkedIn `/messaging/` tab and return its CDP tab id. Shells
438
+ * `capture open <url> [--port N]`, which finds an existing matching tab or opens
439
+ * one and BLOCKS on `Page.loadEventFired` (~10s timeout) — so it does the first
440
+ * page settle for us. Stdout is clean JSON `{id,title,url,port}` (the next-step
441
+ * hints go to stderr); we parse `.id`. Part of the not-ready recovery flow: the
442
+ * view drives the browser to the inbox instead of dead-ending on `no-tab`.
443
+ * @param {BaseOpts} [opts]
444
+ * @returns {Promise<Result<string>>}
445
+ */
446
+ export async function openMessagingTab(opts = {}) {
447
+ const r = await runCapture(buildOpenArgv(opts));
448
+ if (!r.spawned) return fail(captureMissingError());
449
+ if (r.exitCode !== 0) return fail(classifyError(r.stderr));
450
+ let body;
451
+ try {
452
+ body = JSON.parse(String(r.stdout || '').trim() || '{}');
453
+ } catch {
454
+ return fail({ kind: 'error', message: `could not parse capture open output: ${truncate(r.stdout, 300)}` });
455
+ }
456
+ if (body && body.id) return ok(String(body.id));
457
+ return fail({ kind: 'error', message: 'capture open returned no tab id' });
458
+ }
459
+
460
+ /**
461
+ * Drive an EXISTING LinkedIn tab to the `/messaging/` inbox. Shells
462
+ * `capture navigate <url> --target <tabId> [--port N]`. Used for the
463
+ * `not-messaging` recovery branch (a LinkedIn tab parked on the wrong page).
464
+ * Exit 0 ⇒ ok; we don't need the HAR body it returns on stdout.
465
+ * @param {BaseOpts} [opts]
466
+ * @returns {Promise<Result<void>>}
467
+ */
468
+ export async function navigateToMessaging(opts = {}) {
469
+ const r = await runCapture(buildNavigateArgv(opts));
470
+ if (!r.spawned) return fail(captureMissingError());
471
+ if (r.exitCode !== 0) return fail(classifyError(r.stderr));
472
+ return ok(undefined);
473
+ }
474
+
475
+ /**
476
+ * Read the page auth context. Call ONCE; cache `csrf`+`memberId` and thread them
477
+ * into later calls.
478
+ * @param {BaseOpts} [opts]
479
+ * @returns {Promise<Result<LiContext>>}
480
+ */
481
+ export async function getContext(opts = {}) {
482
+ const r = await execLib('getContext', null, opts);
483
+ if (!r.ok) return r;
484
+ const d = r.data || {};
485
+ if (!d.csrf || !d.memberId) {
486
+ return fail({ kind: 'error', message: 'getContext() returned no csrf/memberId' });
487
+ }
488
+ return ok({ csrf: String(d.csrf), memberId: String(d.memberId) });
489
+ }
490
+
491
+ /**
492
+ * List recent inbox conversations (newest-first from LinkedIn). Sorting
493
+ * unread-first is the VIEW's job; here we only map fields.
494
+ * @param {BaseOpts & {csrf?:string, memberId?:string, count?:number}} opts
495
+ * @returns {Promise<Result<Conversation[]>>}
496
+ */
497
+ export async function listConversations(opts) {
498
+ const count = opts && typeof opts.count === 'number' ? opts.count : 20;
499
+ /** @type {Record<string, unknown>} */
500
+ const libArgs = { count };
501
+ if (opts && opts.csrf) libArgs.csrf = opts.csrf;
502
+ if (opts && opts.memberId) libArgs.memberId = opts.memberId;
503
+ const r = await execLib('listConversations', libArgs, opts);
504
+ if (!r.ok) return r;
505
+ const arr = r.data && Array.isArray(r.data.conversations) ? r.data.conversations : [];
506
+ return ok(arr.map(toConversation));
507
+ }
508
+
509
+ /**
510
+ * Load a conversation's messages (oldest-first, up to ~40). `fromMe` is set by
511
+ * comparing each sender's member ID to `opts.myMemberId`.
512
+ * @param {BaseOpts & {csrf:string, conversationUrn:string, myMemberId?:string}} opts
513
+ * @returns {Promise<Result<Message[]>>}
514
+ */
515
+ export async function viewConversation(opts) {
516
+ const libArgs = { csrf: opts.csrf, conversationUrn: opts.conversationUrn };
517
+ const r = await execLib('viewConversation', libArgs, opts);
518
+ if (!r.ok) return r;
519
+ const arr = r.data && Array.isArray(r.data.messages) ? r.data.messages : [];
520
+ const myId = (opts && opts.myMemberId) || '';
521
+ return ok(arr.map((/** @type {any} */ m) => toMessage(m, myId)));
522
+ }
523
+
524
+ /**
525
+ * Send a message to a 1st-degree connection (optionally replying within
526
+ * `conversationUrn`). The 422 "must be a 1st-degree connection" maps to
527
+ * `not-connection` whether it arrives as exit-1 stderr OR as a `{success:false}`
528
+ * body.
529
+ * @param {BaseOpts & {csrf:string, myMemberId:string, recipient:string, text:string, conversationUrn?:string}} opts
530
+ * @returns {Promise<Result<void>>}
531
+ */
532
+ export async function sendMessage(opts) {
533
+ /** @type {Record<string, unknown>} */
534
+ const libArgs = {
535
+ csrf: opts.csrf,
536
+ myMemberId: opts.myMemberId,
537
+ recipient: opts.recipient,
538
+ text: opts.text,
539
+ };
540
+ if (opts.conversationUrn) libArgs.conversationUrn = opts.conversationUrn;
541
+ const r = await execLib('sendMessage', libArgs, opts);
542
+ if (!r.ok) return r; // stderr-mapped errors (incl. 422 → not-connection)
543
+ const body = r.data || {};
544
+ if (body.success === false) {
545
+ const msg = typeof body.error === 'string' ? body.error : 'send failed';
546
+ if (/1st-degree connection/i.test(msg)) return fail({ kind: 'not-connection' });
547
+ return fail({ kind: 'error', message: msg });
548
+ }
549
+ return ok(undefined);
550
+ }
551
+
552
+ /**
553
+ * Mark a conversation as read (acknowledges its latest message).
554
+ * @param {BaseOpts & {csrf:string, conversationUrn:string}} opts
555
+ * @returns {Promise<Result<void>>}
556
+ */
557
+ export async function markConversationAsRead(opts) {
558
+ const r = await execLib('markConversationAsRead', { csrf: opts.csrf, conversationUrn: opts.conversationUrn }, opts);
559
+ if (!r.ok) return r;
560
+ return ok(undefined);
561
+ }
562
+
563
+ /**
564
+ * Add an emoji reaction to a message.
565
+ * @param {BaseOpts & {csrf:string, messageUrn:string, emoji:string}} opts
566
+ * @returns {Promise<Result<void>>}
567
+ */
568
+ export async function reactToMessage(opts) {
569
+ const r = await execLib('reactToMessage', { csrf: opts.csrf, messageUrn: opts.messageUrn, emoji: opts.emoji }, opts);
570
+ if (!r.ok) return r;
571
+ const body = r.data || {};
572
+ if (body.success === false) return fail({ kind: 'error', message: 'LinkedIn rejected the reaction' });
573
+ return ok(undefined);
574
+ }
575
+
576
+ /**
577
+ * Introspection helper (not used at runtime): render the exact `capture` command
578
+ * each function constructs, with placeholder args, so the command shape can be
579
+ * eyeballed without a live browser.
580
+ * @param {BaseOpts} [opts]
581
+ * @returns {Record<string, string>}
582
+ */
583
+ export function describeCommands(opts = {}) {
584
+ const o = { target: opts.target || '<tabId>', port: opts.port };
585
+ return {
586
+ discoverTab: toDisplay(buildListArgv(o)),
587
+ openMessagingTab: toDisplay(buildOpenArgv(o)),
588
+ navigateToMessaging: toDisplay(buildNavigateArgv(o)),
589
+ getContext: toDisplay(buildExec('getContext', null, o).argv),
590
+ listConversations: toDisplay(
591
+ buildExec('listConversations', { count: 20, csrf: '<csrf>', memberId: '<memberId>' }, o).argv
592
+ ),
593
+ viewConversation: toDisplay(
594
+ buildExec('viewConversation', { csrf: '<csrf>', conversationUrn: '<conversationUrn>' }, o).argv
595
+ ),
596
+ sendMessage: toDisplay(
597
+ buildExec(
598
+ 'sendMessage',
599
+ { csrf: '<csrf>', myMemberId: '<memberId>', recipient: '<recipientId>', text: 'hello' },
600
+ o
601
+ ).argv
602
+ ),
603
+ markConversationAsRead: toDisplay(
604
+ buildExec('markConversationAsRead', { csrf: '<csrf>', conversationUrn: '<conversationUrn>' }, o).argv
605
+ ),
606
+ reactToMessage: toDisplay(
607
+ buildExec('reactToMessage', { csrf: '<csrf>', messageUrn: '<messageUrn>', emoji: '👍' }, o).argv
608
+ ),
609
+ };
610
+ }