@crouton-kit/crouter 0.3.27 → 0.3.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-root.js +4 -2
- package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
- package/dist/builtin-memory/crouter-development/personas.md +1 -1
- package/dist/builtin-memory/crouter-development/plugins.md +20 -18
- package/dist/builtin-memory/design.md +1 -1
- package/dist/builtin-memory/development.md +1 -1
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-memory/planning.md +1 -1
- package/dist/builtin-memory/spec.md +6 -4
- package/dist/builtin-personas/orchestration-kernel.md +4 -5
- package/dist/builtin-personas/runtime-base.md +8 -4
- package/dist/builtin-personas/spec/PERSONA.md +8 -4
- package/dist/builtin-personas/spec/orchestrator.md +5 -3
- package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
- package/dist/builtin-views/canvas/core.mjs +586 -0
- package/dist/builtin-views/canvas/text.mjs +58 -0
- package/dist/builtin-views/canvas/tui.mjs +165 -0
- package/dist/builtin-views/canvas/web.jsx +120 -0
- package/dist/builtin-views/git-pr/core.mjs +673 -0
- package/dist/builtin-views/git-pr/text.mjs +84 -0
- package/dist/builtin-views/git-pr/tui.mjs +302 -0
- package/dist/builtin-views/git-pr/web.jsx +216 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
- package/dist/builtin-views/inbox/core.mjs +1273 -0
- package/dist/builtin-views/inbox/text.mjs +73 -0
- package/dist/builtin-views/inbox/tui.mjs +312 -0
- package/dist/builtin-views/inbox/web.jsx +188 -0
- package/dist/builtin-views/linkedin/core.mjs +906 -0
- package/dist/builtin-views/linkedin/text.mjs +69 -0
- package/dist/builtin-views/linkedin/tui.mjs +427 -0
- package/dist/builtin-views/linkedin/web.jsx +206 -0
- package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
- package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
- package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
- package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
- package/dist/cli.js +3 -3
- package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
- package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
- package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
- package/dist/clients/attach/__tests__/git-info.test.js +31 -0
- package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
- package/dist/clients/attach/__tests__/reconnect-giveup.test.js +30 -0
- package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
- package/dist/clients/attach/__tests__/titled-editor.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
- package/dist/clients/attach/attach-cmd.js +445 -41
- package/dist/clients/attach/auth-pickers.d.ts +9 -0
- package/dist/clients/attach/auth-pickers.js +194 -0
- package/dist/clients/attach/canvas-panels.d.ts +12 -0
- package/dist/clients/attach/canvas-panels.js +78 -0
- package/dist/clients/attach/chat-view.d.ts +27 -2
- package/dist/clients/attach/chat-view.js +98 -9
- package/dist/clients/attach/clipboard-image.d.ts +47 -8
- package/dist/clients/attach/clipboard-image.js +150 -26
- package/dist/clients/attach/clipboard-text.d.ts +3 -0
- package/dist/clients/attach/clipboard-text.js +39 -0
- package/dist/clients/attach/config-load.d.ts +54 -7
- package/dist/clients/attach/config-load.js +99 -3
- package/dist/clients/attach/context-message.d.ts +12 -0
- package/dist/clients/attach/context-message.js +72 -0
- package/dist/clients/attach/git-info.d.ts +20 -0
- package/dist/clients/attach/git-info.js +44 -0
- package/dist/clients/attach/graph-overlay.d.ts +43 -0
- package/dist/clients/attach/graph-overlay.js +306 -0
- package/dist/clients/attach/input-controller.d.ts +81 -13
- package/dist/clients/attach/input-controller.js +307 -67
- package/dist/clients/attach/pickers.d.ts +58 -0
- package/dist/clients/attach/pickers.js +171 -0
- package/dist/clients/attach/slash-commands.d.ts +44 -1
- package/dist/clients/attach/slash-commands.js +250 -27
- package/dist/clients/attach/titled-editor.d.ts +33 -0
- package/dist/clients/attach/titled-editor.js +84 -0
- package/dist/clients/attach/view-socket.d.ts +37 -2
- package/dist/clients/attach/view-socket.js +131 -7
- package/dist/clients/web/dev-server.d.ts +7 -0
- package/dist/clients/web/dev-server.js +59 -0
- package/dist/clients/web/events.d.ts +14 -0
- package/dist/clients/web/events.js +151 -0
- package/dist/clients/web/server.d.ts +18 -0
- package/dist/clients/web/server.js +450 -0
- package/dist/clients/web/web-cmd.d.ts +2 -0
- package/dist/clients/web/web-cmd.js +120 -0
- package/dist/commands/canvas.js +1 -2
- package/dist/commands/chord.js +1 -1
- package/dist/commands/dashboard.js +5 -1
- package/dist/commands/human/prompts.js +3 -3
- package/dist/commands/human/queue.d.ts +17 -0
- package/dist/commands/human/queue.js +111 -4
- package/dist/commands/memory/__tests__/lint-schema.test.js +27 -4
- package/dist/commands/memory/lint.d.ts +5 -4
- package/dist/commands/memory/lint.js +23 -7
- package/dist/commands/memory/read.js +2 -2
- package/dist/commands/memory/shared.d.ts +7 -1
- package/dist/commands/memory/shared.js +18 -3
- package/dist/commands/memory/write.js +20 -7
- package/dist/commands/memory.js +4 -4
- package/dist/commands/node.d.ts +1 -1
- package/dist/commands/node.js +64 -65
- package/dist/commands/pkg.js +1 -2
- package/dist/commands/revive.js +11 -3
- package/dist/commands/sys/feedback.d.ts +1 -0
- package/dist/commands/sys/feedback.js +163 -0
- package/dist/commands/sys/sync.d.ts +1 -0
- package/dist/commands/sys/sync.js +187 -0
- package/dist/commands/sys.js +4 -2
- package/dist/commands/view-cycle.js +2 -2
- package/dist/commands/view-list.js +8 -8
- package/dist/commands/view-new.js +21 -17
- package/dist/commands/view-pick.js +1 -1
- package/dist/commands/view-run.js +35 -14
- package/dist/commands/view.js +8 -6
- package/dist/commands/workspace.d.ts +2 -0
- package/dist/commands/workspace.js +161 -0
- package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
- package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
- package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
- package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
- package/dist/core/__tests__/broker-snapshot-history.test.d.ts +1 -0
- package/dist/core/__tests__/broker-snapshot-history.test.js +105 -0
- package/dist/core/__tests__/child-death-wake.test.js +56 -111
- package/dist/core/__tests__/close.test.js +14 -20
- package/dist/core/__tests__/context-intro.test.js +19 -19
- package/dist/core/__tests__/daemon-boot.test.js +30 -23
- package/dist/core/__tests__/draw-style.test.js +1 -1
- package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -1
- package/dist/core/__tests__/fixtures/fake-engine.js +32 -2
- package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
- package/dist/core/__tests__/focuses.test.js +25 -13
- package/dist/core/__tests__/fork.test.js +22 -5
- package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
- package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
- package/dist/core/__tests__/full/cascade-close.test.js +22 -8
- package/dist/core/__tests__/full/detach-focus.test.js +66 -169
- package/dist/core/__tests__/helpers/harness.d.ts +6 -9
- package/dist/core/__tests__/helpers/harness.js +19 -36
- package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
- package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
- package/dist/core/__tests__/human-stranded-deliver.test.d.ts +1 -0
- package/dist/core/__tests__/human-stranded-deliver.test.js +108 -0
- package/dist/core/__tests__/lifecycle.test.js +1 -19
- package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
- package/dist/core/__tests__/memory-resolver.test.js +1 -1
- package/dist/core/__tests__/on-read-dedup-resume.test.d.ts +1 -0
- package/dist/core/__tests__/on-read-dedup-resume.test.js +81 -0
- package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
- package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch-root.test.js +114 -0
- package/dist/core/__tests__/reset.test.js +19 -79
- package/dist/core/__tests__/steer-note.test.js +9 -7
- package/dist/core/__tests__/tmux-surface.test.js +13 -7
- package/dist/core/bootstrap.d.ts +0 -3
- package/dist/core/bootstrap.js +1 -143
- package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
- package/dist/core/canvas/browse/app.js +131 -8
- package/dist/core/canvas/browse/model.d.ts +23 -5
- package/dist/core/canvas/browse/model.js +68 -13
- package/dist/core/canvas/browse/render.d.ts +7 -1
- package/dist/core/canvas/browse/render.js +178 -47
- package/dist/core/canvas/canvas.js +1 -1
- package/dist/core/canvas/focuses.d.ts +4 -4
- package/dist/core/canvas/focuses.js +7 -7
- package/dist/core/canvas/nav-model.d.ts +162 -0
- package/dist/core/canvas/nav-model.js +487 -0
- package/dist/core/canvas/paths.d.ts +7 -0
- package/dist/core/canvas/paths.js +9 -0
- package/dist/core/canvas/render.d.ts +68 -9
- package/dist/core/canvas/render.js +195 -61
- package/dist/core/canvas/types.d.ts +16 -23
- package/dist/core/config.js +1 -4
- package/dist/core/memory-resolver.js +43 -5
- package/dist/core/runtime/bearings.d.ts +1 -1
- package/dist/core/runtime/bearings.js +8 -8
- package/dist/core/runtime/branded-host.d.ts +16 -0
- package/dist/core/runtime/branded-host.js +127 -0
- package/dist/core/runtime/broker-protocol.d.ts +212 -6
- package/dist/core/runtime/broker-sdk.d.ts +0 -12
- package/dist/core/runtime/broker-sdk.js +77 -6
- package/dist/core/runtime/broker.d.ts +5 -1
- package/dist/core/runtime/broker.js +520 -59
- package/dist/core/runtime/close.d.ts +12 -2
- package/dist/core/runtime/close.js +37 -16
- package/dist/core/runtime/front-door.js +14 -7
- package/dist/core/runtime/host.d.ts +14 -34
- package/dist/core/runtime/host.js +14 -50
- package/dist/core/runtime/launch.d.ts +12 -9
- package/dist/core/runtime/launch.js +27 -15
- package/dist/core/runtime/lifecycle.d.ts +1 -1
- package/dist/core/runtime/lifecycle.js +15 -19
- package/dist/core/runtime/nodes.d.ts +0 -55
- package/dist/core/runtime/nodes.js +6 -74
- package/dist/core/runtime/placement.d.ts +91 -334
- package/dist/core/runtime/placement.js +262 -828
- package/dist/core/runtime/promote.d.ts +2 -0
- package/dist/core/runtime/promote.js +24 -6
- package/dist/core/runtime/recap.d.ts +8 -0
- package/dist/core/runtime/recap.js +107 -0
- package/dist/core/runtime/recycle.js +25 -61
- package/dist/core/runtime/reset.d.ts +43 -37
- package/dist/core/runtime/reset.js +131 -218
- package/dist/core/runtime/revive.d.ts +9 -29
- package/dist/core/runtime/revive.js +45 -135
- package/dist/core/runtime/spawn.d.ts +15 -11
- package/dist/core/runtime/spawn.js +168 -136
- package/dist/core/runtime/surface-bg.d.ts +11 -0
- package/dist/core/runtime/surface-bg.js +68 -0
- package/dist/core/runtime/tmux.d.ts +15 -43
- package/dist/core/runtime/tmux.js +38 -80
- package/dist/core/scope.d.ts +1 -2
- package/dist/core/scope.js +1 -5
- package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
- package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
- package/dist/core/skill-sync/builtins.d.ts +42 -0
- package/dist/core/skill-sync/builtins.js +112 -0
- package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
- package/dist/core/skill-sync/claude-plugins.js +71 -0
- package/dist/core/skill-sync/engine.d.ts +42 -0
- package/dist/core/skill-sync/engine.js +633 -0
- package/dist/core/skill-sync/export.d.ts +23 -0
- package/dist/core/skill-sync/export.js +86 -0
- package/dist/core/skill-sync/manifest.d.ts +64 -0
- package/dist/core/skill-sync/manifest.js +181 -0
- package/dist/core/skill-sync/profile.d.ts +76 -0
- package/dist/core/skill-sync/profile.js +173 -0
- package/dist/core/skill-sync/snapshot.d.ts +57 -0
- package/dist/core/skill-sync/snapshot.js +120 -0
- package/dist/core/spawn.d.ts +14 -0
- package/dist/core/spawn.js +29 -9
- package/dist/core/substrate/index.d.ts +2 -2
- package/dist/core/substrate/index.js +7 -7
- package/dist/core/substrate/injected-store.d.ts +10 -0
- package/dist/core/substrate/injected-store.js +55 -0
- package/dist/core/substrate/on-read.js +68 -6
- package/dist/core/substrate/render.d.ts +8 -11
- package/dist/core/substrate/render.js +29 -43
- package/dist/core/substrate/schema.d.ts +16 -11
- package/dist/core/substrate/schema.js +32 -31
- package/dist/core/tui/host.d.ts +12 -4
- package/dist/core/tui/host.js +280 -149
- package/dist/core/view/bridge.d.ts +10 -0
- package/dist/core/view/bridge.js +31 -0
- package/dist/core/view/chrome.d.ts +9 -0
- package/dist/core/view/chrome.js +22 -0
- package/dist/core/view/contract.d.ts +171 -0
- package/dist/core/view/contract.js +23 -0
- package/dist/core/view/loader.d.ts +31 -0
- package/dist/core/view/loader.js +188 -0
- package/dist/core/view/transport-local.d.ts +7 -0
- package/dist/core/view/transport-local.js +70 -0
- package/dist/core/view/transport.d.ts +4 -0
- package/dist/core/view/transport.js +15 -0
- package/dist/daemon/crtrd.d.ts +38 -27
- package/dist/daemon/crtrd.js +303 -376
- package/dist/daemon/manage.js +6 -1
- package/dist/index.js +1 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
- package/dist/pi-extensions/canvas-context-intro.js +4 -4
- package/dist/pi-extensions/canvas-doc-substrate.js +29 -20
- package/dist/pi-extensions/canvas-nav.js +35 -385
- package/dist/pi-extensions/canvas-recap.d.ts +37 -0
- package/dist/pi-extensions/canvas-recap.js +236 -0
- package/dist/pi-extensions/canvas-resume.js +2 -1
- package/dist/pi-extensions/canvas-stophook.d.ts +15 -13
- package/dist/pi-extensions/canvas-stophook.js +119 -103
- package/dist/pi-extensions/canvas-view.js +2 -1
- package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
- package/dist/pi-extensions/widget-order-bus.js +34 -0
- package/dist/prompts/view.d.ts +2 -2
- package/dist/prompts/view.js +145 -73
- package/dist/types.d.ts +1 -6
- package/dist/types.js +1 -3
- package/dist/web/ViewChrome.d.ts +7 -0
- package/dist/web/ViewChrome.js +28 -0
- package/dist/web/ViewPane.d.ts +39 -0
- package/dist/web/ViewPane.js +48 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.js +16 -0
- package/dist/web/runtime.d.ts +39 -0
- package/dist/web/runtime.js +133 -0
- package/dist/web/states.d.ts +24 -0
- package/dist/web/states.js +24 -0
- package/dist/web/transport-http.d.ts +5 -0
- package/dist/web/transport-http.js +28 -0
- package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
- package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
- package/dist/web-client/index.html +13 -0
- package/package.json +20 -6
- package/dist/builtin-views/canvas/client.mjs +0 -303
- package/dist/builtin-views/canvas/view.mjs +0 -576
- package/dist/builtin-views/git-pr/client.mjs +0 -440
- package/dist/builtin-views/git-pr/view.mjs +0 -675
- package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
- package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
- package/dist/builtin-views/inbox/view.mjs +0 -889
- package/dist/builtin-views/linkedin/client.mjs +0 -610
- package/dist/builtin-views/linkedin/view.mjs +0 -1171
- package/dist/commands/pkg/bridge.d.ts +0 -1
- package/dist/commands/pkg/bridge.js +0 -137
- package/dist/commands/skill/author.d.ts +0 -3
- package/dist/commands/skill/author.js +0 -140
- package/dist/commands/skill/shared.d.ts +0 -3
- package/dist/commands/skill/shared.js +0 -19
- package/dist/commands/skill.d.ts +0 -2
- package/dist/commands/skill.js +0 -21
- package/dist/commands/tmux-spread.d.ts +0 -2
- package/dist/commands/tmux-spread.js +0 -144
- package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
- package/dist/core/__tests__/full/placement-focus.test.js +0 -309
- package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
- package/dist/core/__tests__/full/placement-revive.test.js +0 -238
- package/dist/core/__tests__/full/placement-teardown.test.js +0 -221
- package/dist/core/__tests__/home-session.test.js +0 -186
- package/dist/core/__tests__/relaunch.test.js +0 -335
- package/dist/core/bridge-map.d.ts +0 -19
- package/dist/core/bridge-map.js +0 -73
- package/dist/core/tui/contract.d.ts +0 -83
- package/dist/core/tui/contract.js +0 -8
- package/dist/core/tui/loader.d.ts +0 -16
- package/dist/core/tui/loader.js +0 -94
- package/dist/prompts/skill.d.ts +0 -2
- package/dist/prompts/skill.js +0 -650
- /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
- /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/reconnect-giveup.test.d.ts} +0 -0
- /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/slash-quit-copy.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
|
-
}
|