@crouton-kit/crouter 0.3.28 → 0.3.30

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 (324) hide show
  1. package/dist/build-root.d.ts +15 -7
  2. package/dist/build-root.js +48 -34
  3. package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
  4. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
  5. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
  6. package/dist/builtin-memory/crouter-development/personas.md +1 -1
  7. package/dist/builtin-memory/crouter-development/plugins.md +20 -18
  8. package/dist/builtin-memory/design.md +1 -1
  9. package/dist/builtin-memory/development.md +1 -1
  10. package/dist/builtin-memory/internal/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  12. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  13. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  14. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  15. package/dist/builtin-memory/planning.md +1 -1
  16. package/dist/builtin-memory/spec.md +6 -4
  17. package/dist/builtin-personas/orchestration-kernel.md +4 -5
  18. package/dist/builtin-personas/runtime-base.md +5 -4
  19. package/dist/builtin-personas/spec/PERSONA.md +8 -4
  20. package/dist/builtin-personas/spec/orchestrator.md +5 -3
  21. package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
  22. package/dist/builtin-views/canvas/core.mjs +586 -0
  23. package/dist/builtin-views/canvas/text.mjs +58 -0
  24. package/dist/builtin-views/canvas/tui.mjs +165 -0
  25. package/dist/builtin-views/canvas/web.jsx +120 -0
  26. package/dist/builtin-views/git-pr/core.mjs +673 -0
  27. package/dist/builtin-views/git-pr/text.mjs +84 -0
  28. package/dist/builtin-views/git-pr/tui.mjs +302 -0
  29. package/dist/builtin-views/git-pr/web.jsx +216 -0
  30. package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
  31. package/dist/builtin-views/inbox/core.mjs +1273 -0
  32. package/dist/builtin-views/inbox/text.mjs +73 -0
  33. package/dist/builtin-views/inbox/tui.mjs +312 -0
  34. package/dist/builtin-views/inbox/web.jsx +188 -0
  35. package/dist/builtin-views/linkedin/core.mjs +906 -0
  36. package/dist/builtin-views/linkedin/text.mjs +69 -0
  37. package/dist/builtin-views/linkedin/tui.mjs +427 -0
  38. package/dist/builtin-views/linkedin/web.jsx +206 -0
  39. package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
  40. package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
  41. package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
  42. package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
  43. package/dist/cli.js +22 -17
  44. package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
  45. package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
  46. package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
  47. package/dist/clients/attach/__tests__/git-info.test.js +31 -0
  48. package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
  49. package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
  50. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  51. package/dist/clients/attach/attach-cmd.js +282 -46
  52. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  53. package/dist/clients/attach/auth-pickers.js +194 -0
  54. package/dist/clients/attach/canvas-panels.d.ts +4 -2
  55. package/dist/clients/attach/canvas-panels.js +45 -17
  56. package/dist/clients/attach/chat-view.d.ts +27 -2
  57. package/dist/clients/attach/chat-view.js +98 -9
  58. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  59. package/dist/clients/attach/clipboard-image.js +150 -26
  60. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  61. package/dist/clients/attach/clipboard-text.js +39 -0
  62. package/dist/clients/attach/config-load.d.ts +54 -7
  63. package/dist/clients/attach/config-load.js +99 -3
  64. package/dist/clients/attach/context-message.d.ts +12 -0
  65. package/dist/clients/attach/context-message.js +72 -0
  66. package/dist/clients/attach/git-info.d.ts +20 -0
  67. package/dist/clients/attach/git-info.js +44 -0
  68. package/dist/clients/attach/graph-overlay.d.ts +13 -4
  69. package/dist/clients/attach/graph-overlay.js +59 -19
  70. package/dist/clients/attach/input-controller.d.ts +75 -13
  71. package/dist/clients/attach/input-controller.js +305 -67
  72. package/dist/clients/attach/pickers.d.ts +58 -0
  73. package/dist/clients/attach/pickers.js +171 -0
  74. package/dist/clients/attach/slash-commands.d.ts +22 -0
  75. package/dist/clients/attach/slash-commands.js +91 -25
  76. package/dist/clients/attach/titled-editor.d.ts +33 -0
  77. package/dist/clients/attach/titled-editor.js +84 -0
  78. package/dist/clients/attach/view-socket.d.ts +18 -1
  79. package/dist/clients/attach/view-socket.js +70 -1
  80. package/dist/clients/web/dev-server.d.ts +7 -0
  81. package/dist/clients/web/dev-server.js +59 -0
  82. package/dist/clients/web/events.d.ts +14 -0
  83. package/dist/clients/web/events.js +151 -0
  84. package/dist/clients/web/server.d.ts +18 -0
  85. package/dist/clients/web/server.js +450 -0
  86. package/dist/clients/web/web-cmd.d.ts +2 -0
  87. package/dist/clients/web/web-cmd.js +120 -0
  88. package/dist/commands/canvas.js +1 -2
  89. package/dist/commands/chord.js +4 -25
  90. package/dist/commands/dashboard.js +5 -1
  91. package/dist/commands/memory/__tests__/lint-schema.test.js +6 -6
  92. package/dist/commands/memory/lint.js +14 -2
  93. package/dist/commands/memory/read.js +2 -2
  94. package/dist/commands/memory/shared.d.ts +7 -1
  95. package/dist/commands/memory/shared.js +18 -3
  96. package/dist/commands/memory/write.js +8 -4
  97. package/dist/commands/memory.js +4 -4
  98. package/dist/commands/node.d.ts +1 -1
  99. package/dist/commands/node.js +64 -65
  100. package/dist/commands/pkg.js +1 -2
  101. package/dist/commands/revive.js +11 -3
  102. package/dist/commands/sys/sync.d.ts +1 -0
  103. package/dist/commands/sys/sync.js +187 -0
  104. package/dist/commands/sys.js +3 -2
  105. package/dist/commands/view-cycle.js +2 -2
  106. package/dist/commands/view-list.js +8 -8
  107. package/dist/commands/view-new.js +21 -17
  108. package/dist/commands/view-pick.js +1 -1
  109. package/dist/commands/view-run.js +35 -14
  110. package/dist/commands/view.js +8 -6
  111. package/dist/commands/workspace.d.ts +2 -0
  112. package/dist/commands/workspace.js +161 -0
  113. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  114. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  115. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  116. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  117. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  118. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  119. package/dist/core/__tests__/close.test.js +14 -20
  120. package/dist/core/__tests__/context-intro.test.js +19 -19
  121. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  122. package/dist/core/__tests__/draw-style.test.js +1 -1
  123. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  124. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  125. package/dist/core/__tests__/fixtures/fake-engine.d.ts +17 -1
  126. package/dist/core/__tests__/fixtures/fake-engine.js +22 -2
  127. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  128. package/dist/core/__tests__/focuses.test.js +25 -13
  129. package/dist/core/__tests__/fork.test.js +22 -5
  130. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  131. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  132. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  133. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  134. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  135. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  136. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  137. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  138. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  139. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  140. package/dist/core/__tests__/helpers/harness.js +19 -36
  141. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  142. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  143. package/dist/core/__tests__/lifecycle.test.js +1 -19
  144. package/dist/core/__tests__/listing-completeness.test.js +2 -2
  145. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  146. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  147. package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -2
  148. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  149. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  150. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  151. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  152. package/dist/core/__tests__/reset.test.js +19 -79
  153. package/dist/core/__tests__/steer-note.test.js +9 -7
  154. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  155. package/dist/core/bootstrap.d.ts +0 -3
  156. package/dist/core/bootstrap.js +1 -143
  157. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  158. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  159. package/dist/core/canvas/browse/app.js +131 -8
  160. package/dist/core/canvas/browse/model.d.ts +23 -5
  161. package/dist/core/canvas/browse/model.js +68 -13
  162. package/dist/core/canvas/browse/render.d.ts +7 -1
  163. package/dist/core/canvas/browse/render.js +178 -47
  164. package/dist/core/canvas/canvas.js +1 -1
  165. package/dist/core/canvas/focuses.d.ts +4 -4
  166. package/dist/core/canvas/focuses.js +7 -7
  167. package/dist/core/canvas/nav-model.js +8 -7
  168. package/dist/core/canvas/render.d.ts +68 -9
  169. package/dist/core/canvas/render.js +195 -61
  170. package/dist/core/canvas/types.d.ts +16 -23
  171. package/dist/core/config.js +1 -4
  172. package/dist/core/memory-resolver.js +43 -5
  173. package/dist/core/runtime/bearings.d.ts +1 -1
  174. package/dist/core/runtime/bearings.js +8 -8
  175. package/dist/core/runtime/branded-host.d.ts +16 -0
  176. package/dist/core/runtime/branded-host.js +127 -0
  177. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  178. package/dist/core/runtime/broker.d.ts +4 -1
  179. package/dist/core/runtime/broker.js +494 -58
  180. package/dist/core/runtime/close.d.ts +12 -2
  181. package/dist/core/runtime/close.js +37 -16
  182. package/dist/core/runtime/front-door.js +6 -14
  183. package/dist/core/runtime/host.d.ts +14 -34
  184. package/dist/core/runtime/host.js +14 -50
  185. package/dist/core/runtime/launch.d.ts +12 -9
  186. package/dist/core/runtime/launch.js +27 -15
  187. package/dist/core/runtime/lifecycle.d.ts +1 -1
  188. package/dist/core/runtime/lifecycle.js +15 -19
  189. package/dist/core/runtime/nodes.d.ts +0 -55
  190. package/dist/core/runtime/nodes.js +6 -74
  191. package/dist/core/runtime/placement.d.ts +91 -335
  192. package/dist/core/runtime/placement.js +291 -858
  193. package/dist/core/runtime/promote.d.ts +2 -0
  194. package/dist/core/runtime/promote.js +24 -6
  195. package/dist/core/runtime/recap.d.ts +8 -0
  196. package/dist/core/runtime/recap.js +107 -0
  197. package/dist/core/runtime/recycle.js +25 -61
  198. package/dist/core/runtime/reset.d.ts +43 -37
  199. package/dist/core/runtime/reset.js +131 -218
  200. package/dist/core/runtime/revive.d.ts +9 -29
  201. package/dist/core/runtime/revive.js +40 -139
  202. package/dist/core/runtime/spawn.d.ts +15 -16
  203. package/dist/core/runtime/spawn.js +165 -194
  204. package/dist/core/runtime/surface-bg.d.ts +11 -0
  205. package/dist/core/runtime/surface-bg.js +68 -0
  206. package/dist/core/runtime/tmux.d.ts +15 -52
  207. package/dist/core/runtime/tmux.js +35 -99
  208. package/dist/core/scope.d.ts +1 -2
  209. package/dist/core/scope.js +1 -5
  210. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  211. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  212. package/dist/core/skill-sync/builtins.d.ts +42 -0
  213. package/dist/core/skill-sync/builtins.js +112 -0
  214. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  215. package/dist/core/skill-sync/claude-plugins.js +71 -0
  216. package/dist/core/skill-sync/engine.d.ts +42 -0
  217. package/dist/core/skill-sync/engine.js +633 -0
  218. package/dist/core/skill-sync/export.d.ts +23 -0
  219. package/dist/core/skill-sync/export.js +86 -0
  220. package/dist/core/skill-sync/manifest.d.ts +64 -0
  221. package/dist/core/skill-sync/manifest.js +181 -0
  222. package/dist/core/skill-sync/profile.d.ts +76 -0
  223. package/dist/core/skill-sync/profile.js +173 -0
  224. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  225. package/dist/core/skill-sync/snapshot.js +120 -0
  226. package/dist/core/substrate/index.d.ts +1 -1
  227. package/dist/core/substrate/index.js +1 -1
  228. package/dist/core/substrate/injected-store.js +3 -3
  229. package/dist/core/substrate/on-read.js +68 -6
  230. package/dist/core/substrate/render.d.ts +8 -11
  231. package/dist/core/substrate/render.js +29 -43
  232. package/dist/core/substrate/schema.d.ts +10 -3
  233. package/dist/core/substrate/schema.js +6 -3
  234. package/dist/core/tui/host.d.ts +12 -4
  235. package/dist/core/tui/host.js +280 -149
  236. package/dist/core/view/bridge.d.ts +10 -0
  237. package/dist/core/view/bridge.js +31 -0
  238. package/dist/core/view/chrome.d.ts +9 -0
  239. package/dist/core/view/chrome.js +22 -0
  240. package/dist/core/view/contract.d.ts +171 -0
  241. package/dist/core/view/contract.js +23 -0
  242. package/dist/core/view/loader.d.ts +31 -0
  243. package/dist/core/view/loader.js +188 -0
  244. package/dist/core/view/transport-local.d.ts +7 -0
  245. package/dist/core/view/transport-local.js +70 -0
  246. package/dist/core/view/transport.d.ts +4 -0
  247. package/dist/core/view/transport.js +15 -0
  248. package/dist/daemon/crtrd.d.ts +38 -27
  249. package/dist/daemon/crtrd.js +303 -376
  250. package/dist/daemon/manage.js +6 -1
  251. package/dist/index.js +1 -1
  252. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  253. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  254. package/dist/pi-extensions/canvas-doc-substrate.js +16 -16
  255. package/dist/pi-extensions/canvas-nav.js +5 -0
  256. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  257. package/dist/pi-extensions/canvas-recap.js +236 -0
  258. package/dist/pi-extensions/canvas-resume.js +2 -1
  259. package/dist/pi-extensions/canvas-stophook.d.ts +14 -12
  260. package/dist/pi-extensions/canvas-stophook.js +87 -101
  261. package/dist/pi-extensions/canvas-view.js +2 -1
  262. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  263. package/dist/pi-extensions/widget-order-bus.js +34 -0
  264. package/dist/prompts/view.d.ts +2 -2
  265. package/dist/prompts/view.js +145 -73
  266. package/dist/types.d.ts +1 -6
  267. package/dist/types.js +1 -3
  268. package/dist/web/ViewChrome.d.ts +7 -0
  269. package/dist/web/ViewChrome.js +28 -0
  270. package/dist/web/ViewPane.d.ts +39 -0
  271. package/dist/web/ViewPane.js +48 -0
  272. package/dist/web/index.d.ts +6 -0
  273. package/dist/web/index.js +16 -0
  274. package/dist/web/runtime.d.ts +39 -0
  275. package/dist/web/runtime.js +133 -0
  276. package/dist/web/states.d.ts +24 -0
  277. package/dist/web/states.js +24 -0
  278. package/dist/web/transport-http.d.ts +5 -0
  279. package/dist/web/transport-http.js +28 -0
  280. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  281. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  282. package/dist/web-client/index.html +13 -0
  283. package/package.json +20 -6
  284. package/dist/builtin-views/canvas/client.mjs +0 -303
  285. package/dist/builtin-views/canvas/view.mjs +0 -576
  286. package/dist/builtin-views/git-pr/client.mjs +0 -440
  287. package/dist/builtin-views/git-pr/view.mjs +0 -675
  288. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  289. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  290. package/dist/builtin-views/inbox/view.mjs +0 -889
  291. package/dist/builtin-views/linkedin/client.mjs +0 -610
  292. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  293. package/dist/commands/pkg/bridge.d.ts +0 -1
  294. package/dist/commands/pkg/bridge.js +0 -137
  295. package/dist/commands/skill/author.d.ts +0 -3
  296. package/dist/commands/skill/author.js +0 -140
  297. package/dist/commands/skill/shared.d.ts +0 -3
  298. package/dist/commands/skill/shared.js +0 -19
  299. package/dist/commands/skill.d.ts +0 -2
  300. package/dist/commands/skill.js +0 -21
  301. package/dist/commands/tmux-spread.d.ts +0 -2
  302. package/dist/commands/tmux-spread.js +0 -144
  303. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  304. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  305. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  306. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  307. package/dist/core/__tests__/full/placement-teardown.test.js +0 -297
  308. package/dist/core/__tests__/home-session.test.js +0 -186
  309. package/dist/core/__tests__/relaunch.test.js +0 -335
  310. package/dist/core/bridge-map.d.ts +0 -19
  311. package/dist/core/bridge-map.js +0 -73
  312. package/dist/core/tui/contract.d.ts +0 -83
  313. package/dist/core/tui/contract.js +0 -8
  314. package/dist/core/tui/loader.d.ts +0 -16
  315. package/dist/core/tui/loader.js +0 -94
  316. package/dist/prompts/skill.d.ts +0 -2
  317. package/dist/prompts/skill.js +0 -650
  318. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  319. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  320. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  321. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  322. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  323. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
  324. /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/titled-editor.test.d.ts} +0 -0
@@ -1,965 +0,0 @@
1
- // @ts-check
2
- /**
3
- * Gmail data layer for the combined crtr `inbox` view.
4
- *
5
- * A self-contained `Source` adapter (default export) over the forked Gmail vault
6
- * libs (`libs/gmail`). The combined view merges N of these sources into one
7
- * ranked stream; this module owns Gmail's data fetching AND its own
8
- * discover→open→settle recovery state machine.
9
- *
10
- * Mirrors `../../linkedin/client.mjs` exactly in shape — the capture-exec
11
- * plumbing (buildExec / runCapture candidate-walk / classifyError), the
12
- * never-throw `Result` discipline, the getContext-cached-once pattern, and the
13
- * recovery flow (discoverTab via `capture list`, openMailTab via
14
- * `capture open <url>`, bounded settle-poll) — but Gmail-flavored:
15
- * • the tab is `mail.google.com` (no `--target`/`--port` is consumed from the
16
- * view; Gmail discovers its OWN tab),
17
- * • auth context is `{xsrf, account, globals}` read once via getContext() and
18
- * threaded into every later call,
19
- * • errors map to a `SourceError` whose `display` the VIEW renders verbatim
20
- * (the view does NOT interpret `kind`).
21
- *
22
- * Self-contained ESM, Node-builtins-only. Imports NOTHING from crtr so the build
23
- * can `cp -R src/builtin-views dist/builtin-views` verbatim and the view can
24
- * dynamically `import()` it at runtime. NOTHING here throws — every async
25
- * returns a `Result<T>`.
26
- *
27
- * @module inbox/sources/gmail
28
- */
29
-
30
- import { execFile } from 'node:child_process';
31
-
32
- // ── Shared data shapes (the merge currency; see inbox source contract) ───────
33
-
34
- /**
35
- * One left-pane row in the merged stream.
36
- * @typedef {Object} UnifiedRow
37
- * @property {'gmail'} sourceId
38
- * @property {string} key Globally-unique selection id (`gmail:<threadId>`).
39
- * @property {string} name Sender display name / address.
40
- * @property {string} snippet Last-message preview (single line; view truncates).
41
- * @property {boolean} unread
42
- * @property {number} ts Epoch ms (0 if unknown).
43
- * @property {GmailRef} ref Opaque source handle handed back to loadThread/reply.
44
- */
45
-
46
- /**
47
- * Opaque per-row handle. The view never inspects this; it round-trips it back.
48
- * @typedef {Object} GmailRef
49
- * @property {string} threadId
50
- * @property {string} messageId Latest message id at list time.
51
- * @property {string} subject
52
- * @property {string} fromEmail
53
- * @property {string} fromName
54
- */
55
-
56
- /**
57
- * One message in the right-pane thread.
58
- * @typedef {Object} UnifiedMessage
59
- * @property {string} sender
60
- * @property {boolean} fromMe
61
- * @property {string} text
62
- * @property {number} ts
63
- */
64
-
65
- /**
66
- * Right-pane thread payload.
67
- * @typedef {Object} UnifiedThread
68
- * @property {string} title
69
- * @property {string} [subtitle]
70
- * @property {UnifiedMessage[]} messages
71
- * @property {boolean} canReply
72
- * @property {boolean} canReact
73
- */
74
-
75
- /**
76
- * Gmail auth context, read ONCE via getContext() and cached on the sub.
77
- * @typedef {Object} GmailContext
78
- * @property {string} xsrf XSRF token for API requests.
79
- * @property {number} account Account number (0-indexed, from /u/{N}/).
80
- * @property {string} email Current account email.
81
- * @property {{g2:number,g3:string,g9:string,g10:string}} globals Internal Gmail values for the BTAI header.
82
- */
83
-
84
- /**
85
- * Typed failure. `kind` drives the recovery state machine internally; `display`
86
- * is what the view renders (it does NOT interpret `kind`).
87
- * @typedef {{kind:'no-cdp'}
88
- * | {kind:'no-tab'}
89
- * | {kind:'not-logged-in'}
90
- * | {kind:'rate-limited'}
91
- * | {kind:'capture-not-dev'}
92
- * | {kind:'still-loading'}
93
- * | {kind:'error', message:string}} ClientError
94
- */
95
-
96
- /**
97
- * Per-source down-state descriptor the view renders (banner + guided panel).
98
- * @typedef {Object} ErrorDisplay
99
- * @property {string} headline
100
- * @property {(string|string[])} [explanation]
101
- * @property {(string|null)} [nextStep]
102
- * @property {string} banner
103
- * @property {'info'|'action'|'error'} level
104
- * @property {boolean} blocking
105
- */
106
-
107
- /**
108
- * Per-source typed error surfaced to the view.
109
- * @typedef {Object} SourceError
110
- * @property {string} kind
111
- * @property {ErrorDisplay} display
112
- */
113
-
114
- /**
115
- * Never-throw return contract.
116
- * @template T
117
- * @typedef {{ok:true, data:T} | {ok:false, error:SourceError}} Result
118
- */
119
-
120
- /**
121
- * This source's private mutable substate (stored by the view at state.subs.gmail).
122
- * @typedef {Object} GmailSub
123
- * @property {string|null} target CDP tab id of the mail.google.com tab (discovered/opened).
124
- * @property {string|number|undefined} port CDP port — Gmail does NOT consume the view's port; stays undefined (capture auto-detects).
125
- * @property {GmailContext|null} ctx Cached auth context (read once).
126
- * @property {boolean} loginTabOpened True once the login tab was opened this not-logged-in episode; reset on ready.
127
- */
128
-
129
- /**
130
- * Common options threaded to capture. Gmail always discovers its own tab, so
131
- * `target` comes from {@link discoverTab}/{@link openMailTab}, never from the view.
132
- * @typedef {Object} BaseOpts
133
- * @property {string} [target]
134
- * @property {string|number} [port]
135
- */
136
-
137
- // ── Config ───────────────────────────────────────────────────────────────────
138
-
139
- /** The Gmail login / inbox URL the recovery flow opens / focuses. */
140
- const GMAIL_URL = 'https://mail.google.com';
141
-
142
- /**
143
- * Dev-checkout fallback. The Gmail vault libs are dev-checkout-only (the
144
- * published `capture` package throws `DEV_ONLY_MSG`), so this is the most likely
145
- * place a working binary lives if `capture` is not on PATH.
146
- */
147
- const CAPTURE_DEV_FALLBACK = '/Users/silasrhyneer/Code/cli/capture/bin/capture';
148
-
149
- /** The verbatim dev-only degradation message capture emits (mirrored for the panel). */
150
- const DEV_ONLY_MSG =
151
- 'This view needs a capture dev checkout (vault/ source + esbuild). ' +
152
- 'Not available in the published package.';
153
-
154
- /** Rows to request from the Gmail inbox per refresh. */
155
- const INBOX_COUNT = 25;
156
-
157
- /** Bounded settle-poll after an auto open (no infinite spin). */
158
- const SETTLE_MAX = 5;
159
- const SETTLE_INTERVAL_MS = 1200;
160
-
161
- /** Error kinds that waiting can't fix — stop the settle-poll and guide. */
162
- const HARD_STOP = new Set(['no-cdp', 'not-logged-in', 'capture-not-dev']);
163
-
164
- /**
165
- * Binary candidates, in order: explicit override, PATH `capture`, dev checkout.
166
- * We try the next only when a candidate fails to spawn (ENOENT) — a candidate
167
- * that spawns and exits nonzero is authoritative.
168
- * @returns {string[]}
169
- */
170
- function captureCandidates() {
171
- const out = [];
172
- if (process.env.CAPTURE_BIN) out.push(process.env.CAPTURE_BIN);
173
- out.push('capture');
174
- out.push(CAPTURE_DEV_FALLBACK);
175
- return out;
176
- }
177
-
178
- // ── Result helpers ───────────────────────────────────────────────────────────
179
-
180
- /** @template T @param {T} data @returns {Result<T>} */
181
- function ok(data) {
182
- return { ok: true, data };
183
- }
184
-
185
- /** @param {SourceError} error @returns {{ok:false, error:SourceError}} */
186
- function fail(error) {
187
- return { ok: false, error };
188
- }
189
-
190
- /** @param {string} s @param {number} n @returns {string} */
191
- function truncate(s, n) {
192
- const str = String(s);
193
- return str.length > n ? str.slice(0, n) + '…' : str;
194
- }
195
-
196
- /** @param {number} ms @returns {Promise<void>} */
197
- function sleep(ms) {
198
- return new Promise((r) => setTimeout(r, ms));
199
- }
200
-
201
- // ── capture stderr → ClientError ─────────────────────────────────────────────
202
-
203
- /**
204
- * Map a failed `capture` invocation's stderr to a typed {@link ClientError}.
205
- *
206
- * Mapping table:
207
- * No browser with CDP found | fetch failed | ECONNREFUSED ... no-cdp
208
- * No tab found ...................................... no-tab
209
- * DEV_ONLY_MSG / dev-only feature .................. capture-not-dev
210
- * RateLimited | 429 ............................... rate-limited
211
- * Unauthenticated | XSRF/GLOBALS not found |
212
- * not be logged in | Not on Gmail domain |
213
- * Account number not found ...................... not-logged-in
214
- * (anything else) ................................. error{message}
215
- *
216
- * Order matters: capture-not-dev and rate-limited are checked before the broad
217
- * not-logged-in patterns.
218
- * @param {string} stderr
219
- * @returns {ClientError}
220
- */
221
- function classifyError(stderr) {
222
- const s = String(stderr || '');
223
- if (/No browser with CDP found/i.test(s)) return { kind: 'no-cdp' };
224
- // A dead/unreachable debugger port surfaces as a raw connection failure: capture
225
- // probes http://localhost:<port>/json/version, so an unlistened port yields
226
- // `fetch failed` (Node fetch wrapping ECONNREFUSED). Treat it as no-cdp.
227
- if (/fetch failed/i.test(s) || /failed to fetch/i.test(s) || /ECONNREFUSED/i.test(s)) {
228
- return { kind: 'no-cdp' };
229
- }
230
- if (/No tab found/i.test(s)) return { kind: 'no-tab' };
231
- if (/DEV_ONLY/i.test(s) || /dev-only feature of capture/i.test(s) || /published package/i.test(s)) {
232
- return { kind: 'capture-not-dev' };
233
- }
234
- if (/RateLimited/i.test(s) || /\b429\b/.test(s)) return { kind: 'rate-limited' };
235
- if (
236
- /Unauthenticated/i.test(s) ||
237
- /XSRF token not found/i.test(s) ||
238
- /GLOBALS not found/i.test(s) ||
239
- /may not be logged in/i.test(s) ||
240
- /not be logged in/i.test(s) ||
241
- /Not on Gmail domain/i.test(s) ||
242
- /Account number not found/i.test(s) ||
243
- /Navigate to mail\.google\.com/i.test(s)
244
- ) {
245
- return { kind: 'not-logged-in' };
246
- }
247
- return { kind: 'error', message: extractErrorMessage(s) };
248
- }
249
-
250
- /**
251
- * Pull a human message out of capture stderr: prefer the last `ERROR:` line,
252
- * else the last non-empty line, else a generic fallback.
253
- * @param {string} stderr
254
- * @returns {string}
255
- */
256
- function extractErrorMessage(stderr) {
257
- const lines = String(stderr || '')
258
- .split(/\r?\n/)
259
- .map((l) => l.trim())
260
- .filter(Boolean);
261
- for (let i = lines.length - 1; i >= 0; i--) {
262
- if (/^ERROR:/i.test(lines[i])) return lines[i].replace(/^ERROR:\s*/i, '').trim();
263
- }
264
- if (lines.length) return lines[lines.length - 1];
265
- return 'capture exec failed';
266
- }
267
-
268
- // ── ClientError → SourceError (the view's render payload) ─────────────────────
269
-
270
- /**
271
- * Build the {@link ErrorDisplay} the view renders for a given error. The view
272
- * never inspects `kind`; it shows this `display` as the source's banner/panel.
273
- * @param {ClientError} error
274
- * @returns {ErrorDisplay}
275
- */
276
- function buildDisplay(error) {
277
- const kind = error && error.kind;
278
- switch (kind) {
279
- case 'no-cdp':
280
- return {
281
- headline: 'No debuggable browser',
282
- explanation: 'crtr drives a browser over CDP and none is running.',
283
- nextStep: 'Launch Chrome with --remote-debugging-port=9222 (or Arc), then press g',
284
- banner: 'Gmail: no debuggable browser — launch one, then g',
285
- level: 'error',
286
- blocking: true,
287
- };
288
- case 'no-tab':
289
- return {
290
- headline: 'No Gmail tab',
291
- explanation: 'Open mail.google.com in the debuggable browser.',
292
- nextStep: 'Press G to connect',
293
- banner: 'Gmail: open mail.google.com, then press g',
294
- level: 'action',
295
- blocking: true,
296
- };
297
- case 'not-logged-in':
298
- return {
299
- headline: 'Log in to Gmail',
300
- explanation: 'Gmail needs a sign-in in the browser.',
301
- nextStep: 'Log in in the opened tab, then press g',
302
- banner: 'Gmail: log in, then press g',
303
- level: 'action',
304
- blocking: true,
305
- };
306
- case 'rate-limited':
307
- return {
308
- headline: 'Gmail is throttling',
309
- explanation: 'Too many requests — wait a moment before retrying.',
310
- nextStep: 'Press g to retry',
311
- banner: 'Gmail: throttled — wait, then press g',
312
- level: 'info',
313
- blocking: false,
314
- };
315
- case 'capture-not-dev':
316
- return {
317
- headline: 'Browser bridge unavailable',
318
- explanation: DEV_ONLY_MSG,
319
- nextStep: null,
320
- banner: 'Gmail: browser bridge unavailable — capture dev checkout required',
321
- level: 'error',
322
- blocking: true,
323
- };
324
- case 'still-loading':
325
- return {
326
- headline: 'Still loading Gmail…',
327
- explanation: 'Gmail is taking a while to load.',
328
- nextStep: 'Press g to retry',
329
- banner: 'Gmail: still loading — press g to retry',
330
- level: 'action',
331
- blocking: false,
332
- };
333
- case 'error':
334
- default: {
335
- const msg = (error && /** @type {any} */ (error).message) || 'Unknown error.';
336
- return {
337
- headline: 'Something went wrong',
338
- explanation: msg,
339
- nextStep: 'Press g to retry',
340
- banner: `Gmail: ${truncate(msg, 80)}`,
341
- level: 'error',
342
- blocking: true,
343
- };
344
- }
345
- }
346
- }
347
-
348
- /** @param {ClientError} error @returns {SourceError} */
349
- function toSourceError(error) {
350
- return { kind: (error && error.kind) || 'error', display: buildDisplay(error) };
351
- }
352
-
353
- // ── capture command construction ─────────────────────────────────────────────
354
-
355
- /**
356
- * Serialize a JS arg object as a literal safe to splice into the exec code
357
- * string. JSON is a near-subset of JS object-literal syntax; additionally escape
358
- * U+2028/U+2029 (valid in JSON strings, historically illegal in JS) so a pasted
359
- * reply can't break the splice.
360
- * @param {Record<string, unknown>} obj
361
- * @returns {string}
362
- */
363
- function jsLiteral(obj) {
364
- return JSON.stringify(obj).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
365
- }
366
-
367
- /**
368
- * Build the argv + exec code string for a vault-lib call. The code string is a
369
- * SINGLE argv element (never a shell string) so there is no quoting to escape at
370
- * the process boundary. `libArgs === null` emits a no-arg call (getContext).
371
- * @param {string} fnName
372
- * @param {Record<string, unknown> | null} libArgs
373
- * @param {BaseOpts} [opts]
374
- * @returns {{argv:string[], code:string}}
375
- */
376
- function buildExec(fnName, libArgs, opts) {
377
- const call = libArgs === null ? `${fnName}()` : `${fnName}(${jsLiteral(libArgs)})`;
378
- const code = `import {${fnName}} from 'libs/gmail'; return await ${call}`;
379
- /** @type {string[]} */
380
- const argv = ['exec', code];
381
- if (opts && opts.target) argv.push('--target', String(opts.target));
382
- if (opts && opts.port != null && opts.port !== '') argv.push('--port', String(opts.port));
383
- return { argv, code };
384
- }
385
-
386
- /**
387
- * Build the `capture list` argv (tab discovery; no CDP target).
388
- * @param {BaseOpts} [opts]
389
- * @returns {string[]}
390
- */
391
- function buildListArgv(opts) {
392
- const argv = ['list'];
393
- if (opts && opts.port != null && opts.port !== '') argv.push('--port', String(opts.port));
394
- return argv;
395
- }
396
-
397
- /**
398
- * Build the `capture open <gmail url>` argv (open/reuse a mail.google.com tab).
399
- * @param {BaseOpts} [opts]
400
- * @returns {string[]}
401
- */
402
- function buildOpenArgv(opts) {
403
- const argv = ['open', GMAIL_URL];
404
- if (opts && opts.port != null && opts.port !== '') argv.push('--port', String(opts.port));
405
- return argv;
406
- }
407
-
408
- /**
409
- * Render an argv as the human-readable shell command (for logs/eyeballing). The
410
- * code element (which contains spaces) gets double-quoted; nothing is executed.
411
- * @param {string[]} argv
412
- * @returns {string}
413
- */
414
- function toDisplay(argv) {
415
- return (
416
- 'capture ' +
417
- argv.map((a) => (/[\s"]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a)).join(' ')
418
- );
419
- }
420
-
421
- // ── Process runner (never throws) ────────────────────────────────────────────
422
-
423
- /**
424
- * @typedef {Object} RunResult
425
- * @property {boolean} spawned False ⇒ no candidate binary could be spawned.
426
- * @property {number} exitCode Process exit code (0 on success; -1 if not spawned).
427
- * @property {string} stdout
428
- * @property {string} stderr
429
- * @property {string} [bin]
430
- */
431
-
432
- /**
433
- * Run one candidate binary. Resolves (never rejects). ENOENT is signalled by
434
- * `spawned:false` so the caller can try the next candidate.
435
- * @param {string} bin
436
- * @param {string[]} argv
437
- * @returns {Promise<RunResult>}
438
- */
439
- function runOnce(bin, argv) {
440
- return new Promise((resolve) => {
441
- execFile(
442
- bin,
443
- argv,
444
- { maxBuffer: 64 * 1024 * 1024, encoding: 'utf8' },
445
- (err, stdout, stderr) => {
446
- const out = typeof stdout === 'string' ? stdout : '';
447
- const errOut = typeof stderr === 'string' ? stderr : '';
448
- if (err && /** @type {any} */ (err).code === 'ENOENT') {
449
- resolve({ spawned: false, exitCode: -1, stdout: out, stderr: errOut, bin });
450
- return;
451
- }
452
- const code = err
453
- ? typeof /** @type {any} */ (err).code === 'number'
454
- ? /** @type {any} */ (err).code
455
- : 1
456
- : 0;
457
- resolve({ spawned: true, exitCode: code, stdout: out, stderr: errOut, bin });
458
- }
459
- );
460
- });
461
- }
462
-
463
- /**
464
- * Run `capture` with the given argv, walking the candidate list until one spawns.
465
- * @param {string[]} argv
466
- * @returns {Promise<RunResult>}
467
- */
468
- async function runCapture(argv) {
469
- let last = /** @type {RunResult} */ ({ spawned: false, exitCode: -1, stdout: '', stderr: '' });
470
- for (const bin of captureCandidates()) {
471
- last = await runOnce(bin, argv);
472
- if (last.spawned) return last;
473
- }
474
- return last; // never spawned
475
- }
476
-
477
- /** @returns {SourceError} */
478
- function captureMissingError() {
479
- return toSourceError({
480
- kind: 'error',
481
- message:
482
- `capture binary not found — tried CAPTURE_BIN, PATH 'capture', and dev fallback ` +
483
- `${CAPTURE_DEV_FALLBACK}. Install capture or set CAPTURE_BIN.`,
484
- });
485
- }
486
-
487
- /**
488
- * Run a vault-lib call and return the parsed JSON return value as a Result.
489
- * @param {string} fnName
490
- * @param {Record<string, unknown> | null} libArgs
491
- * @param {BaseOpts} opts
492
- * @returns {Promise<Result<any>>}
493
- */
494
- async function execLib(fnName, libArgs, opts) {
495
- const { argv } = buildExec(fnName, libArgs, opts);
496
- const r = await runCapture(argv);
497
- if (!r.spawned) return fail(captureMissingError());
498
- if (r.exitCode !== 0) return fail(toSourceError(classifyError(r.stderr)));
499
- const out = String(r.stdout || '').trim();
500
- if (out === '') return ok(null);
501
- try {
502
- return ok(JSON.parse(out));
503
- } catch {
504
- return fail(
505
- toSourceError({ kind: 'error', message: `could not parse capture output as JSON: ${truncate(out, 300)}` })
506
- );
507
- }
508
- }
509
-
510
- // ── Field mappers ────────────────────────────────────────────────────────────
511
-
512
- /** @param {unknown} v @returns {number} epoch ms (Gmail dates are already ms) */
513
- function asMs(v) {
514
- return typeof v === 'number' && Number.isFinite(v) ? v : 0;
515
- }
516
-
517
- /** @param {any} addr @returns {string} */
518
- function addrName(addr) {
519
- const a = addr || {};
520
- if (typeof a.name === 'string' && a.name.trim()) return a.name;
521
- if (typeof a.email === 'string' && a.email.trim()) return a.email;
522
- return 'Unknown';
523
- }
524
-
525
- /** @param {any} addr @returns {string} */
526
- function addrEmail(addr) {
527
- const a = addr || {};
528
- return typeof a.email === 'string' ? a.email : '';
529
- }
530
-
531
- /**
532
- * Map a raw Gmail `MessageSummary` (one per thread) to a {@link UnifiedRow}.
533
- * @param {any} m
534
- * @returns {UnifiedRow}
535
- */
536
- function toUnifiedRow(m) {
537
- const o = m || {};
538
- const threadId = typeof o.threadId === 'string' ? o.threadId : '';
539
- const from = o.from || {};
540
- return {
541
- sourceId: 'gmail',
542
- key: `gmail:${threadId}`,
543
- name: addrName(from),
544
- snippet: typeof o.snippet === 'string' && o.snippet ? o.snippet : (typeof o.subject === 'string' ? o.subject : ''),
545
- unread: o.unread === true,
546
- ts: asMs(o.date),
547
- ref: {
548
- threadId,
549
- messageId: typeof o.messageId === 'string' ? o.messageId : '',
550
- subject: typeof o.subject === 'string' ? o.subject : '',
551
- fromEmail: addrEmail(from),
552
- fromName: addrName(from),
553
- },
554
- };
555
- }
556
-
557
- /**
558
- * Map a raw Gmail `MessageContent` to a {@link UnifiedMessage}.
559
- * @param {any} m
560
- * @param {string} myEmail
561
- * @returns {UnifiedMessage}
562
- */
563
- function toUnifiedMessage(m, myEmail) {
564
- const o = m || {};
565
- const from = o.from || {};
566
- const fromEmail = addrEmail(from);
567
- return {
568
- sender: addrName(from),
569
- fromMe: !!myEmail && fromEmail.toLowerCase() === myEmail.toLowerCase(),
570
- text: typeof o.body === 'string' && o.body ? o.body : (typeof o.snippet === 'string' ? o.snippet : ''),
571
- ts: asMs(o.date),
572
- };
573
- }
574
-
575
- /** Strip leading `Re:` chains, then prefix a single `Re: `. @param {string} s @returns {string} */
576
- function reSubject(s) {
577
- const base = String(s || '').replace(/^\s*(re:\s*)+/i, '').trim();
578
- return base ? `Re: ${base}` : 'Re:';
579
- }
580
-
581
- // ── Recovery primitives (Gmail-flavored, mirroring linkedin/client.mjs) ──────
582
-
583
- /** @param {string} url @returns {boolean} */
584
- function isGmailUrl(url) {
585
- try {
586
- return new URL(url).hostname.toLowerCase() === 'mail.google.com';
587
- } catch {
588
- return false;
589
- }
590
- }
591
-
592
- /**
593
- * Find a usable mail.google.com tab id via `capture list`. No Gmail tab ⇒ no-tab.
594
- * @param {BaseOpts} [opts]
595
- * @returns {Promise<Result<string>>}
596
- */
597
- async function discoverTab(opts = {}) {
598
- const r = await runCapture(buildListArgv(opts));
599
- if (!r.spawned) return fail(captureMissingError());
600
- if (r.exitCode !== 0) return fail(toSourceError(classifyError(r.stderr)));
601
- let tabs;
602
- try {
603
- tabs = JSON.parse(String(r.stdout || '').trim() || '[]');
604
- } catch {
605
- return fail(
606
- toSourceError({ kind: 'error', message: `could not parse capture list output: ${truncate(r.stdout, 300)}` })
607
- );
608
- }
609
- if (!Array.isArray(tabs)) return fail(toSourceError({ kind: 'no-tab' }));
610
- const gmail = tabs.filter((t) => t && typeof t.url === 'string' && isGmailUrl(t.url));
611
- const chosen = gmail[0];
612
- if (chosen && chosen.id) return ok(String(chosen.id));
613
- return fail(toSourceError({ kind: 'no-tab' }));
614
- }
615
-
616
- /**
617
- * Open (or focus/reuse) the mail.google.com tab and return its CDP tab id. Shells
618
- * `capture open <url> [--port N]`, which finds an existing matching tab or opens
619
- * one and BLOCKS on `Page.loadEventFired` (~10s) — doing the first page settle.
620
- * Stdout is clean JSON `{id,title,url,port}`; we parse `.id`.
621
- * @param {BaseOpts} [opts]
622
- * @returns {Promise<Result<string>>}
623
- */
624
- async function openMailTab(opts = {}) {
625
- const r = await runCapture(buildOpenArgv(opts));
626
- if (!r.spawned) return fail(captureMissingError());
627
- if (r.exitCode !== 0) return fail(toSourceError(classifyError(r.stderr)));
628
- let body;
629
- try {
630
- body = JSON.parse(String(r.stdout || '').trim() || '{}');
631
- } catch {
632
- return fail(
633
- toSourceError({ kind: 'error', message: `could not parse capture open output: ${truncate(r.stdout, 300)}` })
634
- );
635
- }
636
- if (body && body.id) return ok(String(body.id));
637
- return fail(toSourceError({ kind: 'error', message: 'capture open returned no tab id' }));
638
- }
639
-
640
- /** @param {GmailSub} sub @returns {BaseOpts} */
641
- function baseOpts(sub) {
642
- /** @type {BaseOpts} */
643
- const o = {};
644
- if (sub.target) o.target = sub.target;
645
- if (sub.port != null && sub.port !== '') o.port = sub.port;
646
- return o;
647
- }
648
-
649
- /**
650
- * Read the page auth context. Call ONCE; cache `{xsrf, account, globals}` and
651
- * thread them into later calls.
652
- * @param {BaseOpts} opts
653
- * @returns {Promise<Result<GmailContext>>}
654
- */
655
- async function getContext(opts) {
656
- const r = await execLib('getContext', null, opts);
657
- if (!r.ok) return r;
658
- const d = r.data || {};
659
- if (!d.xsrf || d.account == null || !d.globals) {
660
- return fail(toSourceError({ kind: 'error', message: 'getContext() returned no xsrf/account/globals' }));
661
- }
662
- return ok({
663
- xsrf: String(d.xsrf),
664
- account: Number(d.account),
665
- email: typeof d.email === 'string' ? d.email : '',
666
- globals: d.globals,
667
- });
668
- }
669
-
670
- /**
671
- * One readiness attempt: discover the tab (unless known), then read auth context
672
- * once. Returns a Result-shaped value — never throws.
673
- * @param {GmailSub} sub
674
- * @returns {Promise<Result<void>>}
675
- */
676
- async function attemptReady(sub) {
677
- if (!sub.target) {
678
- const r = await discoverTab(baseOpts(sub));
679
- if (!r.ok) return r;
680
- sub.target = r.data;
681
- }
682
- if (!sub.ctx) {
683
- const r = await getContext(baseOpts(sub));
684
- if (!r.ok) return r;
685
- sub.ctx = r.data;
686
- }
687
- return ok(undefined);
688
- }
689
-
690
- /** Mark ready: reset the once-per-episode login-tab gate. @param {GmailSub} sub */
691
- function markReady(sub) {
692
- sub.loginTabOpened = false;
693
- }
694
-
695
- /**
696
- * Bounded settle-poll after an auto open: retry readiness up to SETTLE_MAX times
697
- * spaced SETTLE_INTERVAL_MS. On success → ready. On a hard-stop error → guide.
698
- * On exhaustion → a `still-loading` action display (no infinite spin).
699
- * @param {GmailSub} sub
700
- * @param {any} [host]
701
- * @returns {Promise<Result<void>>}
702
- */
703
- async function settlePoll(sub, host) {
704
- for (let i = 1; i <= SETTLE_MAX; i++) {
705
- await sleep(SETTLE_INTERVAL_MS);
706
- note(host, `Loading Gmail… (${i})`);
707
- const r = await attemptReady(sub);
708
- if (r.ok) {
709
- markReady(sub);
710
- return ok(undefined);
711
- }
712
- if (HARD_STOP.has(r.error.kind)) return r;
713
- // transient (no-tab / rate-limited / error) → keep polling
714
- }
715
- return fail(toSourceError({ kind: 'still-loading' }));
716
- }
717
-
718
- /**
719
- * Optional progress narration. The host may expose `setStatus`; guard it so this
720
- * module stays decoupled from any specific view.
721
- * @param {any} host @param {string} msg
722
- */
723
- function note(host, msg) {
724
- if (host && typeof host.setStatus === 'function') {
725
- try {
726
- host.setStatus(msg);
727
- } catch {
728
- /* never throw */
729
- }
730
- }
731
- }
732
-
733
- // ── Source object (default export) ───────────────────────────────────────────
734
-
735
- export default {
736
- id: 'gmail',
737
- label: 'Gmail',
738
- badge: { glyph: '@', fg: '31' },
739
- connectKey: 'G',
740
-
741
- /**
742
- * Build this source's private mutable substate. Cheap, sync. Gmail discovers
743
- * its OWN mail.google.com tab — it does NOT consume the view's `--target`/
744
- * `--port` (those apply to LinkedIn only), so `target`/`port` start unset.
745
- * @param {any} [_host]
746
- * @returns {GmailSub}
747
- */
748
- init(_host) {
749
- return { target: null, port: undefined, ctx: null, loginTabOpened: false };
750
- },
751
-
752
- /**
753
- * Discover tab → auth → settle, mutating `sub` in place. Owns the full recovery
754
- * state machine: a direct attempt, then on failure an error-kind-driven branch
755
- * (no-tab → open + bounded settle-poll; not-logged-in → open the login tab once
756
- * per episode then guide; hard stops / rate-limit / generic → guide). Returns
757
- * ok when ready; otherwise a {@link SourceError} whose `display` the view shows.
758
- * @param {GmailSub} sub
759
- * @param {any} [host]
760
- * @returns {Promise<Result<void>>}
761
- */
762
- async ensureReady(sub, host) {
763
- // Direct attempt first.
764
- let r = await attemptReady(sub);
765
- if (r.ok) {
766
- markReady(sub);
767
- return ok(undefined);
768
- }
769
- const kind = r.error.kind;
770
-
771
- // no-tab → open a Gmail tab, then bounded settle-poll.
772
- if (kind === 'no-tab') {
773
- note(host, 'Opening Gmail…');
774
- const o = await openMailTab(baseOpts(sub));
775
- if (!o.ok) return o;
776
- sub.target = o.data;
777
- sub.ctx = null; // re-read auth for the (possibly new) tab
778
- return await settlePoll(sub, host);
779
- }
780
-
781
- // not-logged-in → open the Gmail tab ONCE so the login page is visible, then
782
- // STOP and guide. Logged out, Gmail redirects away from mail.google.com, so
783
- // a NEW tab would otherwise be spawned on EVERY auto-refresh; gate to once
784
- // per episode (markReady resets the flag on a later success).
785
- if (kind === 'not-logged-in') {
786
- if (!sub.loginTabOpened) {
787
- const o = await openMailTab(baseOpts(sub));
788
- if (o.ok && o.data) sub.target = o.data;
789
- sub.loginTabOpened = true;
790
- }
791
- return r;
792
- }
793
-
794
- // hard stops (no-cdp, capture-not-dev), rate-limit, and generic → guide.
795
- return r;
796
- },
797
-
798
- /**
799
- * List inbox rows for the merged stream (one per thread, latest message).
800
- * Caller only invokes after ensureReady ok, so `sub.ctx` is set.
801
- * @param {GmailSub} sub
802
- * @returns {Promise<Result<UnifiedRow[]>>}
803
- */
804
- async listRows(sub) {
805
- if (!sub.ctx) return fail(toSourceError({ kind: 'error', message: 'listRows called before ready' }));
806
- const r = await execLib(
807
- 'listInbox',
808
- { xsrf: sub.ctx.xsrf, account: sub.ctx.account, globals: sub.ctx.globals, count: INBOX_COUNT },
809
- baseOpts(sub)
810
- );
811
- if (!r.ok) return r;
812
- const arr = r.data && Array.isArray(r.data.messages) ? r.data.messages : [];
813
- return ok(arr.map(toUnifiedRow));
814
- },
815
-
816
- /**
817
- * Load the full thread for a selected row's `ref`.
818
- * @param {GmailSub} sub
819
- * @param {GmailRef} ref
820
- * @returns {Promise<Result<UnifiedThread>>}
821
- */
822
- async loadThread(sub, ref) {
823
- if (!sub.ctx) return fail(toSourceError({ kind: 'error', message: 'loadThread called before ready' }));
824
- const threadId = ref && ref.threadId ? ref.threadId : '';
825
- if (!threadId) return fail(toSourceError({ kind: 'error', message: 'loadThread: missing threadId' }));
826
- const r = await execLib(
827
- 'readEmail',
828
- { xsrf: sub.ctx.xsrf, account: sub.ctx.account, globals: sub.ctx.globals, threadId },
829
- baseOpts(sub)
830
- );
831
- if (!r.ok) return r;
832
- const msgs = r.data && Array.isArray(r.data.messages) ? r.data.messages : [];
833
- const myEmail = sub.ctx.email || '';
834
- const first = msgs[0] || {};
835
- const last = msgs[msgs.length - 1] || {};
836
- const title =
837
- (typeof first.subject === 'string' && first.subject) || (ref && ref.subject) || '(no subject)';
838
- const fromAddr = addrEmail(last.from) || addrEmail(first.from);
839
- const toAddr = (last.to && last.to[0] && addrEmail(last.to[0])) || (first.to && first.to[0] && addrEmail(first.to[0])) || '';
840
- const subtitle = fromAddr || toAddr ? `from ${fromAddr || '—'} · to ${toAddr || '—'}` : undefined;
841
- /** @type {UnifiedThread} */
842
- const thread = {
843
- title,
844
- ...(subtitle ? { subtitle } : {}),
845
- messages: msgs.map((/** @type {any} */ m) => toUnifiedMessage(m, myEmail)),
846
- canReply: true,
847
- canReact: false,
848
- };
849
- return ok(thread);
850
- },
851
-
852
- /**
853
- * Send a reply in the thread identified by `ref`. Reads the thread first to
854
- * resolve the message being replied to + the counterpart address (a private
855
- * reply to the latest non-self sender; CC is not inherited).
856
- * @param {GmailSub} sub
857
- * @param {GmailRef} ref
858
- * @param {string} text
859
- * @returns {Promise<Result<void>>}
860
- */
861
- async reply(sub, ref, text) {
862
- if (!sub.ctx) return fail(toSourceError({ kind: 'error', message: 'reply called before ready' }));
863
- const threadId = ref && ref.threadId ? ref.threadId : '';
864
- if (!threadId) return fail(toSourceError({ kind: 'error', message: 'reply: missing threadId' }));
865
- const body = String(text == null ? '' : text);
866
- if (!body.trim()) return fail(toSourceError({ kind: 'error', message: 'reply: empty body' }));
867
-
868
- // Read the thread to resolve originalMsgId + recipient.
869
- const read = await execLib(
870
- 'readEmail',
871
- { xsrf: sub.ctx.xsrf, account: sub.ctx.account, globals: sub.ctx.globals, threadId },
872
- baseOpts(sub)
873
- );
874
- if (!read.ok) return read;
875
- const msgs = read.data && Array.isArray(read.data.messages) ? read.data.messages : [];
876
- if (msgs.length === 0) return fail(toSourceError({ kind: 'error', message: 'reply: thread has no messages' }));
877
- const myEmail = (sub.ctx.email || '').toLowerCase();
878
- const latest = msgs[msgs.length - 1] || {};
879
- const originalMsgId = typeof latest.messageId === 'string' ? latest.messageId : (ref && ref.messageId) || '';
880
- if (!originalMsgId) return fail(toSourceError({ kind: 'error', message: 'reply: could not resolve message id' }));
881
-
882
- // Reply to the latest message NOT sent by me; fall back to the latest message's
883
- // recipients, then to the row's cached sender.
884
- /** @type {any} */
885
- let target = null;
886
- for (let i = msgs.length - 1; i >= 0; i--) {
887
- const fe = addrEmail((msgs[i] || {}).from).toLowerCase();
888
- if (fe && fe !== myEmail) {
889
- target = msgs[i];
890
- break;
891
- }
892
- }
893
- let toEmail = target ? addrEmail(target.from) : '';
894
- if (!toEmail) toEmail = (latest.to && latest.to[0] && addrEmail(latest.to[0])) || '';
895
- if (!toEmail && ref && ref.fromEmail) toEmail = ref.fromEmail;
896
- if (!toEmail) return fail(toSourceError({ kind: 'error', message: 'reply: could not determine recipient' }));
897
-
898
- const subject = reSubject((target && target.subject) || latest.subject || (ref && ref.subject) || '');
899
-
900
- const send = await execLib(
901
- 'replyEmail',
902
- {
903
- xsrf: sub.ctx.xsrf,
904
- account: sub.ctx.account,
905
- globals: sub.ctx.globals,
906
- threadId,
907
- originalMsgId,
908
- to: toEmail,
909
- subject,
910
- body,
911
- },
912
- baseOpts(sub)
913
- );
914
- if (!send.ok) return send;
915
- const out = send.data || {};
916
- if (out.success === false) {
917
- return fail(toSourceError({ kind: 'error', message: 'Gmail rejected the reply' }));
918
- }
919
- return ok(undefined);
920
- },
921
-
922
- /**
923
- * Manual connect (bound to connectKey `G`): open/focus the Gmail tab; the next
924
- * refresh re-runs ensureReady. Clears the cached auth so it re-reads on the
925
- * (possibly switched) tab.
926
- * @param {GmailSub} sub
927
- * @param {any} [host]
928
- * @returns {Promise<Result<void>>}
929
- */
930
- async connect(sub, host) {
931
- note(host, 'Opening Gmail…');
932
- const o = await openMailTab(baseOpts(sub));
933
- if (!o.ok) return o;
934
- sub.target = o.data;
935
- sub.ctx = null;
936
- sub.loginTabOpened = true; // we just opened it; don't double-open on the next ensureReady
937
- return ok(undefined);
938
- },
939
- };
940
-
941
- /**
942
- * Introspection helper (not used at runtime): render the exact `capture` command
943
- * each call constructs, with placeholder args, so the command shape can be
944
- * eyeballed without a live browser.
945
- * @param {BaseOpts} [opts]
946
- * @returns {Record<string, string>}
947
- */
948
- export function describeCommands(opts = {}) {
949
- const o = { target: opts.target || '<tabId>', port: opts.port };
950
- const g = { g2: 0, g3: '<g3>', g9: '<g9>', g10: '<email>' };
951
- return {
952
- discoverTab: toDisplay(buildListArgv(o)),
953
- openMailTab: toDisplay(buildOpenArgv(o)),
954
- getContext: toDisplay(buildExec('getContext', null, o).argv),
955
- listInbox: toDisplay(buildExec('listInbox', { xsrf: '<xsrf>', account: 0, globals: g, count: INBOX_COUNT }, o).argv),
956
- readEmail: toDisplay(buildExec('readEmail', { xsrf: '<xsrf>', account: 0, globals: g, threadId: '<threadId>' }, o).argv),
957
- replyEmail: toDisplay(
958
- buildExec(
959
- 'replyEmail',
960
- { xsrf: '<xsrf>', account: 0, globals: g, threadId: '<threadId>', originalMsgId: '<msgId>', to: '<email>', subject: 'Re: …', body: 'hello' },
961
- o
962
- ).argv
963
- ),
964
- };
965
- }