uxnan-bridge 0.0.1-alpha.20260621

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 (255) hide show
  1. package/README.md +150 -0
  2. package/dist/src/account-status.d.ts +13 -0
  3. package/dist/src/account-status.js +78 -0
  4. package/dist/src/account-status.js.map +1 -0
  5. package/dist/src/adapters/base-adapter.d.ts +18 -0
  6. package/dist/src/adapters/base-adapter.js +15 -0
  7. package/dist/src/adapters/base-adapter.js.map +1 -0
  8. package/dist/src/adapters/claude-adapter.d.ts +102 -0
  9. package/dist/src/adapters/claude-adapter.js +486 -0
  10. package/dist/src/adapters/claude-adapter.js.map +1 -0
  11. package/dist/src/adapters/claude-tools.d.ts +25 -0
  12. package/dist/src/adapters/claude-tools.js +146 -0
  13. package/dist/src/adapters/claude-tools.js.map +1 -0
  14. package/dist/src/adapters/codex-adapter.d.ts +116 -0
  15. package/dist/src/adapters/codex-adapter.js +912 -0
  16. package/dist/src/adapters/codex-adapter.js.map +1 -0
  17. package/dist/src/adapters/codex-app-server.d.ts +74 -0
  18. package/dist/src/adapters/codex-app-server.js +225 -0
  19. package/dist/src/adapters/codex-app-server.js.map +1 -0
  20. package/dist/src/adapters/codex-approval.d.ts +88 -0
  21. package/dist/src/adapters/codex-approval.js +160 -0
  22. package/dist/src/adapters/codex-approval.js.map +1 -0
  23. package/dist/src/adapters/codex-tools.d.ts +18 -0
  24. package/dist/src/adapters/codex-tools.js +106 -0
  25. package/dist/src/adapters/codex-tools.js.map +1 -0
  26. package/dist/src/adapters/content-blocks.d.ts +68 -0
  27. package/dist/src/adapters/content-blocks.js +205 -0
  28. package/dist/src/adapters/content-blocks.js.map +1 -0
  29. package/dist/src/adapters/echo-agent-adapter.d.ts +23 -0
  30. package/dist/src/adapters/echo-agent-adapter.js +72 -0
  31. package/dist/src/adapters/echo-agent-adapter.js.map +1 -0
  32. package/dist/src/adapters/gemini-adapter.d.ts +87 -0
  33. package/dist/src/adapters/gemini-adapter.js +594 -0
  34. package/dist/src/adapters/gemini-adapter.js.map +1 -0
  35. package/dist/src/adapters/gemini-tools.d.ts +4 -0
  36. package/dist/src/adapters/gemini-tools.js +48 -0
  37. package/dist/src/adapters/gemini-tools.js.map +1 -0
  38. package/dist/src/adapters/opencode-adapter.d.ts +74 -0
  39. package/dist/src/adapters/opencode-adapter.js +418 -0
  40. package/dist/src/adapters/opencode-adapter.js.map +1 -0
  41. package/dist/src/adapters/opencode-tools.d.ts +2 -0
  42. package/dist/src/adapters/opencode-tools.js +41 -0
  43. package/dist/src/adapters/opencode-tools.js.map +1 -0
  44. package/dist/src/adapters/pi-adapter.d.ts +92 -0
  45. package/dist/src/adapters/pi-adapter.js +467 -0
  46. package/dist/src/adapters/pi-adapter.js.map +1 -0
  47. package/dist/src/adapters/pi-tools.d.ts +10 -0
  48. package/dist/src/adapters/pi-tools.js +72 -0
  49. package/dist/src/adapters/pi-tools.js.map +1 -0
  50. package/dist/src/adapters/process-agent-adapter.d.ts +24 -0
  51. package/dist/src/adapters/process-agent-adapter.js +111 -0
  52. package/dist/src/adapters/process-agent-adapter.js.map +1 -0
  53. package/dist/src/adapters/resolve-claude.d.ts +13 -0
  54. package/dist/src/adapters/resolve-claude.js +57 -0
  55. package/dist/src/adapters/resolve-claude.js.map +1 -0
  56. package/dist/src/adapters/resolve-codex.d.ts +13 -0
  57. package/dist/src/adapters/resolve-codex.js +48 -0
  58. package/dist/src/adapters/resolve-codex.js.map +1 -0
  59. package/dist/src/adapters/resolve-gemini.d.ts +13 -0
  60. package/dist/src/adapters/resolve-gemini.js +47 -0
  61. package/dist/src/adapters/resolve-gemini.js.map +1 -0
  62. package/dist/src/adapters/resolve-opencode.d.ts +11 -0
  63. package/dist/src/adapters/resolve-opencode.js +49 -0
  64. package/dist/src/adapters/resolve-opencode.js.map +1 -0
  65. package/dist/src/adapters/resolve-pi.d.ts +13 -0
  66. package/dist/src/adapters/resolve-pi.js +46 -0
  67. package/dist/src/adapters/resolve-pi.js.map +1 -0
  68. package/dist/src/adapters/run-options.d.ts +22 -0
  69. package/dist/src/adapters/run-options.js +48 -0
  70. package/dist/src/adapters/run-options.js.map +1 -0
  71. package/dist/src/adapters/spawn.d.ts +20 -0
  72. package/dist/src/adapters/spawn.js +16 -0
  73. package/dist/src/adapters/spawn.js.map +1 -0
  74. package/dist/src/agents/agent-manager.d.ts +98 -0
  75. package/dist/src/agents/agent-manager.js +433 -0
  76. package/dist/src/agents/agent-manager.js.map +1 -0
  77. package/dist/src/agents/attachments.d.ts +28 -0
  78. package/dist/src/agents/attachments.js +121 -0
  79. package/dist/src/agents/attachments.js.map +1 -0
  80. package/dist/src/bridge-context.d.ts +45 -0
  81. package/dist/src/bridge-context.js +2 -0
  82. package/dist/src/bridge-context.js.map +1 -0
  83. package/dist/src/bridge-status.d.ts +12 -0
  84. package/dist/src/bridge-status.js +17 -0
  85. package/dist/src/bridge-status.js.map +1 -0
  86. package/dist/src/bridge.d.ts +37 -0
  87. package/dist/src/bridge.js +446 -0
  88. package/dist/src/bridge.js.map +1 -0
  89. package/dist/src/cli.d.ts +2 -0
  90. package/dist/src/cli.js +194 -0
  91. package/dist/src/cli.js.map +1 -0
  92. package/dist/src/conversation/session-history.d.ts +27 -0
  93. package/dist/src/conversation/session-history.js +1082 -0
  94. package/dist/src/conversation/session-history.js.map +1 -0
  95. package/dist/src/conversation/thread-store.d.ts +74 -0
  96. package/dist/src/conversation/thread-store.js +366 -0
  97. package/dist/src/conversation/thread-store.js.map +1 -0
  98. package/dist/src/daemon-config.d.ts +123 -0
  99. package/dist/src/daemon-config.js +64 -0
  100. package/dist/src/daemon-config.js.map +1 -0
  101. package/dist/src/daemon-state.d.ts +27 -0
  102. package/dist/src/daemon-state.js +76 -0
  103. package/dist/src/daemon-state.js.map +1 -0
  104. package/dist/src/git/git-runner.d.ts +24 -0
  105. package/dist/src/git/git-runner.js +63 -0
  106. package/dist/src/git/git-runner.js.map +1 -0
  107. package/dist/src/git/git-service.d.ts +76 -0
  108. package/dist/src/git/git-service.js +435 -0
  109. package/dist/src/git/git-service.js.map +1 -0
  110. package/dist/src/handler-router.d.ts +34 -0
  111. package/dist/src/handler-router.js +67 -0
  112. package/dist/src/handler-router.js.map +1 -0
  113. package/dist/src/handlers/account-handler.d.ts +4 -0
  114. package/dist/src/handlers/account-handler.js +27 -0
  115. package/dist/src/handlers/account-handler.js.map +1 -0
  116. package/dist/src/handlers/agent-handler.d.ts +2 -0
  117. package/dist/src/handlers/agent-handler.js +8 -0
  118. package/dist/src/handlers/agent-handler.js.map +1 -0
  119. package/dist/src/handlers/bridge-control-handler.d.ts +2 -0
  120. package/dist/src/handlers/bridge-control-handler.js +64 -0
  121. package/dist/src/handlers/bridge-control-handler.js.map +1 -0
  122. package/dist/src/handlers/desktop-handler.d.ts +12 -0
  123. package/dist/src/handlers/desktop-handler.js +5 -0
  124. package/dist/src/handlers/desktop-handler.js.map +1 -0
  125. package/dist/src/handlers/git-handler.d.ts +2 -0
  126. package/dist/src/handlers/git-handler.js +82 -0
  127. package/dist/src/handlers/git-handler.js.map +1 -0
  128. package/dist/src/handlers/index.d.ts +8 -0
  129. package/dist/src/handlers/index.js +22 -0
  130. package/dist/src/handlers/index.js.map +1 -0
  131. package/dist/src/handlers/not-implemented.d.ts +10 -0
  132. package/dist/src/handlers/not-implemented.js +21 -0
  133. package/dist/src/handlers/not-implemented.js.map +1 -0
  134. package/dist/src/handlers/notifications-handler.d.ts +2 -0
  135. package/dist/src/handlers/notifications-handler.js +62 -0
  136. package/dist/src/handlers/notifications-handler.js.map +1 -0
  137. package/dist/src/handlers/params.d.ts +11 -0
  138. package/dist/src/handlers/params.js +72 -0
  139. package/dist/src/handlers/params.js.map +1 -0
  140. package/dist/src/handlers/project-handler.d.ts +2 -0
  141. package/dist/src/handlers/project-handler.js +6 -0
  142. package/dist/src/handlers/project-handler.js.map +1 -0
  143. package/dist/src/handlers/thread-context-handler.d.ts +2 -0
  144. package/dist/src/handlers/thread-context-handler.js +211 -0
  145. package/dist/src/handlers/thread-context-handler.js.map +1 -0
  146. package/dist/src/handlers/workspace-handler.d.ts +2 -0
  147. package/dist/src/handlers/workspace-handler.js +101 -0
  148. package/dist/src/handlers/workspace-handler.js.map +1 -0
  149. package/dist/src/hooks/claude-approval-hook.d.ts +7 -0
  150. package/dist/src/hooks/claude-approval-hook.js +95 -0
  151. package/dist/src/hooks/claude-approval-hook.js.map +1 -0
  152. package/dist/src/hooks/gemini-approval-hook.d.ts +7 -0
  153. package/dist/src/hooks/gemini-approval-hook.js +113 -0
  154. package/dist/src/hooks/gemini-approval-hook.js.map +1 -0
  155. package/dist/src/index.d.ts +62 -0
  156. package/dist/src/index.js +65 -0
  157. package/dist/src/index.js.map +1 -0
  158. package/dist/src/keyring-secret-store.d.ts +36 -0
  159. package/dist/src/keyring-secret-store.js +70 -0
  160. package/dist/src/keyring-secret-store.js.map +1 -0
  161. package/dist/src/lock-file.d.ts +18 -0
  162. package/dist/src/lock-file.js +60 -0
  163. package/dist/src/lock-file.js.map +1 -0
  164. package/dist/src/logger.d.ts +28 -0
  165. package/dist/src/logger.js +99 -0
  166. package/dist/src/logger.js.map +1 -0
  167. package/dist/src/pairing/pairing-code-service.d.ts +45 -0
  168. package/dist/src/pairing/pairing-code-service.js +183 -0
  169. package/dist/src/pairing/pairing-code-service.js.map +1 -0
  170. package/dist/src/projects/project-registry.d.ts +14 -0
  171. package/dist/src/projects/project-registry.js +60 -0
  172. package/dist/src/projects/project-registry.js.map +1 -0
  173. package/dist/src/push/push-sender.d.ts +21 -0
  174. package/dist/src/push/push-sender.js +96 -0
  175. package/dist/src/push/push-sender.js.map +1 -0
  176. package/dist/src/push/push-service.d.ts +122 -0
  177. package/dist/src/push/push-service.js +260 -0
  178. package/dist/src/push/push-service.js.map +1 -0
  179. package/dist/src/qr.d.ts +17 -0
  180. package/dist/src/qr.js +31 -0
  181. package/dist/src/qr.js.map +1 -0
  182. package/dist/src/secret-store.d.ts +23 -0
  183. package/dist/src/secret-store.js +27 -0
  184. package/dist/src/secret-store.js.map +1 -0
  185. package/dist/src/secure-device-state.d.ts +16 -0
  186. package/dist/src/secure-device-state.js +63 -0
  187. package/dist/src/secure-device-state.js.map +1 -0
  188. package/dist/src/service-installer.d.ts +57 -0
  189. package/dist/src/service-installer.js +254 -0
  190. package/dist/src/service-installer.js.map +1 -0
  191. package/dist/src/session-state.d.ts +14 -0
  192. package/dist/src/session-state.js +19 -0
  193. package/dist/src/session-state.js.map +1 -0
  194. package/dist/src/transport/crypto.d.ts +43 -0
  195. package/dist/src/transport/crypto.js +78 -0
  196. package/dist/src/transport/crypto.js.map +1 -0
  197. package/dist/src/transport/lan-server.d.ts +33 -0
  198. package/dist/src/transport/lan-server.js +105 -0
  199. package/dist/src/transport/lan-server.js.map +1 -0
  200. package/dist/src/transport/local-hosts.d.ts +17 -0
  201. package/dist/src/transport/local-hosts.js +30 -0
  202. package/dist/src/transport/local-hosts.js.map +1 -0
  203. package/dist/src/transport/mdns-advertiser.d.ts +83 -0
  204. package/dist/src/transport/mdns-advertiser.js +282 -0
  205. package/dist/src/transport/mdns-advertiser.js.map +1 -0
  206. package/dist/src/transport/message-io.d.ts +33 -0
  207. package/dist/src/transport/message-io.js +87 -0
  208. package/dist/src/transport/message-io.js.map +1 -0
  209. package/dist/src/transport/outbound-log.d.ts +24 -0
  210. package/dist/src/transport/outbound-log.js +78 -0
  211. package/dist/src/transport/outbound-log.js.map +1 -0
  212. package/dist/src/transport/relay-client.d.ts +19 -0
  213. package/dist/src/transport/relay-client.js +27 -0
  214. package/dist/src/transport/relay-client.js.map +1 -0
  215. package/dist/src/transport/secure-channel.d.ts +33 -0
  216. package/dist/src/transport/secure-channel.js +81 -0
  217. package/dist/src/transport/secure-channel.js.map +1 -0
  218. package/dist/src/transport/server-handshake.d.ts +49 -0
  219. package/dist/src/transport/server-handshake.js +137 -0
  220. package/dist/src/transport/server-handshake.js.map +1 -0
  221. package/dist/src/transport/session-handler.d.ts +19 -0
  222. package/dist/src/transport/session-handler.js +134 -0
  223. package/dist/src/transport/session-handler.js.map +1 -0
  224. package/dist/src/transport/session-registry.d.ts +58 -0
  225. package/dist/src/transport/session-registry.js +91 -0
  226. package/dist/src/transport/session-registry.js.map +1 -0
  227. package/dist/src/transport/trust-store.d.ts +23 -0
  228. package/dist/src/transport/trust-store.js +33 -0
  229. package/dist/src/transport/trust-store.js.map +1 -0
  230. package/dist/src/transport/ws-adapter.d.ts +7 -0
  231. package/dist/src/transport/ws-adapter.js +16 -0
  232. package/dist/src/transport/ws-adapter.js.map +1 -0
  233. package/dist/src/version.d.ts +1 -0
  234. package/dist/src/version.js +13 -0
  235. package/dist/src/version.js.map +1 -0
  236. package/dist/src/workspace/browse-service.d.ts +10 -0
  237. package/dist/src/workspace/browse-service.js +97 -0
  238. package/dist/src/workspace/browse-service.js.map +1 -0
  239. package/dist/src/workspace/checkpoint-service.d.ts +21 -0
  240. package/dist/src/workspace/checkpoint-service.js +219 -0
  241. package/dist/src/workspace/checkpoint-service.js.map +1 -0
  242. package/dist/src/workspace/path-guard.d.ts +7 -0
  243. package/dist/src/workspace/path-guard.js +51 -0
  244. package/dist/src/workspace/path-guard.js.map +1 -0
  245. package/dist/src/workspace/workspace-service.d.ts +8 -0
  246. package/dist/src/workspace/workspace-service.js +111 -0
  247. package/dist/src/workspace/workspace-service.js.map +1 -0
  248. package/package.json +46 -0
  249. package/scripts/extract-gemini-hook.mjs +16 -0
  250. package/scripts/fake-approval-bridge.mjs +23 -0
  251. package/scripts/install-service-linux.sh +38 -0
  252. package/scripts/install-service-macos.sh +38 -0
  253. package/scripts/install-service-windows.ps1 +26 -0
  254. package/scripts/test-gemini-hook-e2e.mjs +168 -0
  255. package/scripts/write-gemini-settings.mjs +31 -0
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # uxnan-bridge
2
+
3
+ Local control-plane daemon that connects the Uxnan mobile app to the developer's
4
+ PC over an end-to-end-encrypted channel. It runs Git, reads the workspace, and
5
+ drives AI coding agents on behalf of the phone, routing JSON-RPC methods to
6
+ per-domain handlers.
7
+
8
+ > **Status: ALPHA-FUNCTIONAL on the primary path (LAN/Tailscale-direct, bridge-
9
+ > direct push).** The bridge is the **heart of the product** — the product is
10
+ > bridge-first: the mobile app pairs with the bridge and tries direct LAN/
11
+ > Tailscale addresses first; the relay is an optional, self-hosted off-LAN
12
+ > fallback. Background push is sent **by the bridge** (FCM HTTP v1) over any
13
+ > transport, so the phone keeps getting notifications whether the bridge is
14
+ > reached via direct LAN, Tailscale, or relay.
15
+ >
16
+ > **DONE:**
17
+ > - **E2EE transport** (relay `mac` client + direct-LAN `http+ws` server,
18
+ > handshake, AES-256-GCM channel, byte-for-byte compatible with the mobile
19
+ > app; background reconnect loop; stable pairing session; mDNS discovery
20
+ > `_uxnan._tcp.local`; manual-code pairing `GET /pair/resolve?code=`).
21
+ > - **OS-keychain identity persistence** + single-instance lock.
22
+ > - **Real Git + Workspace handlers** (path-traversal-safe, working-tree
23
+ > checkpoints with **true restore** + retention pruning, `git/revert`,
24
+ > `git/deleteBranch`, `git/removeWorktree`, `workspace/exists`,
25
+ > `workspace/browseDirs`).
26
+ > - **Conversation engine** (threads/turns + streaming, per-thread
27
+ > `Message.blocks`/`Message.thinking`/`Message.usage`).
28
+ > - **5 real agents wired:** OpenCode (default), Claude Code, Codex, pi, and
29
+ > **Gemini CLI**. Each spawns its **official local CLI** over stdio with
30
+ > `shell:false`, parses the native stream, and emits structured
31
+ > `stream/content/block` events (command / diff / tool) plus
32
+ > `stream/thinking/delta` (reasoning). **Aider** is the only remaining
33
+ > agent (recipe in [`FOR-DEV.md`](FOR-DEV.md)).
34
+ > - **Per-thread agent/project selection** + per-project agent/model pins
35
+ > (`projectAgents` config); per-model run-option knobs advertised on
36
+ > `agent/models`; per-turn token usage on `stream/turn/completed`.
37
+ > - **Full thread lifecycle** (`thread/rename|archive|unarchive|delete`).
38
+ > - **Plug-and-play folder browsing** (`workspace/browseDirs`) with
39
+ > `browseRoots` config.
40
+ > - **Direct FCM push from the bridge** (primary path, persisted across
41
+ > restarts, per-phone target, prune-on-untrust). `firebase-admin` is an
42
+ > `optionalDependency` — no creds = silent no-op, foreground local
43
+ > notifications still work.
44
+ > - **Sanitized per-agent `auth/status`** (never tokens, login detected by
45
+ > auth-file existence only).
46
+ > - **Interactive approval intake** (Echo demo + Claude Code opt-in
47
+ > `PreToolUse` hook + Codex via the `codex app-server` turn protocol
48
+ > — `applyPatchApproval` / `execCommandApproval` elicitations routed
49
+ > through the same `requestApproval` round-trip; all validated
50
+ > end-to-end).
51
+ > - **Image attachments** (CLI-agnostic file-path, sandbox-safe).
52
+ > - **On-disk `turn/list` history fallback** for Claude/Codex/OpenCode/pi/Gemini
53
+ > JSONL/JSON stores.
54
+ > - **Bridge control:** `bridge/status` (real `relayConnected`),
55
+ > `bridge/removeTrustedDevice` (revokes + drops session + prunes push
56
+ > registration), `bridge/trustedDevices`, `bridge/connectedPhones`,
57
+ > `bridge/generatePairingQr`.
58
+ > - **Autostart** (`install-service`/`uninstall-service` per platform,
59
+ > never elevated), file logging with secret redaction, CLI
60
+ > (`start`/`stop`/`status`/`qr`/`code`/`install-service`).
61
+ >
62
+ > **PENDING that matters for a public release (not LAN alpha):** Aider
63
+ > adapter, packaging + `npm publish` (pin `@uxnan/shared`), real-device push
64
+ > validation. **Optional / blocked-on-mobile:** seq catch-up + key rotation
65
+ > (await a mobile trigger), desktop embedded IPC (desktop Phase 6), Aider in
66
+ > the history reader (no per-session log shipped), log size-rotation. See
67
+ > [`FOR-DEV.md`](./FOR-DEV.md).
68
+ >
69
+ > **How the bridge talks to agents:** it spawns each agent's **official local
70
+ > CLI** (`opencode`, `claude`, `codex`, `pi`, `gemini`) as a child process
71
+ > and drives it over stdio — exactly as you would in a terminal. It does
72
+ > **not** use any provider HTTP API, API key, or language SDK; each CLI runs
73
+ > under the account/subscription you already authenticated it with. Prompts
74
+ > are passed as argv elements with `shell:false` (no shell injection). See
75
+ > [`FOR-HUMAN.md`](./FOR-HUMAN.md) for the per-agent install/login
76
+ > prerequisites.
77
+
78
+ ## Docs
79
+
80
+ Detailed docs live in [`docs/`](./docs/):
81
+ [installation & autostart](./docs/installation.md) ·
82
+ [configuration](./docs/configuration.md) ·
83
+ [connectivity (LAN/Tailscale/relay)](./docs/connectivity.md) ·
84
+ [how agents are driven](./docs/agents.md) ·
85
+ [testing](./docs/testing.md) ·
86
+ [packaging & deploy](./docs/deploy.md) ·
87
+ [push notifications](./docs/push-notifications.md).
88
+
89
+ ## Install (later, as a global package)
90
+
91
+ ```bash
92
+ npm install -g uxnan-bridge
93
+ ```
94
+
95
+ ## CLI
96
+
97
+ ```bash
98
+ uxnan-bridge start # start the daemon: LAN server + (optional) relay pairing session
99
+ uxnan-bridge status # print current status as JSON
100
+ uxnan-bridge qr # print the pairing QR in the terminal (with the manual code)
101
+ uxnan-bridge code # print just the current pairing code
102
+ uxnan-bridge stop # stop the running daemon (via the lock file)
103
+ uxnan-bridge install-service # autostart at logon (Task Scheduler / LaunchAgent / systemd --user)
104
+ uxnan-bridge uninstall-service
105
+ ```
106
+
107
+ Logs are written to `~/.uxnan/logs/bridge-YYYY-MM-DD.log` (daily rotation, with a
108
+ secret-redaction pass) and to stderr. Autostart at login is configured by the
109
+ platform scripts under `scripts/`.
110
+
111
+ The Ed25519 identity is stored in the OS keychain (Windows Credential Manager /
112
+ macOS Keychain / Linux Secret Service) via `@napi-rs/keyring`. If no keychain
113
+ is available the bridge still runs with an in-memory identity (not persisted
114
+ across restarts).
115
+
116
+ ## Architecture
117
+
118
+ - **Contracts:** consumes [`@uxnan/shared`](../shared) for JSON-RPC and E2EE
119
+ types and runtime validators. The bridge exposes **59 JSON-RPC methods +
120
+ 8 streaming notifications** (see `shared/src/jsonrpc/`). The mobile app
121
+ keeps manually-synced Dart equivalents of the same shapes.
122
+ - **State:** non-secret JSON under `~/.uxnan/` (atomic writes): `daemon-config.json`,
123
+ `pairing-session.json`, `threads.json`, `trusted-phones.json`,
124
+ `push-state.json`, `agent-cache/`, `logs/`. The Ed25519 identity is a
125
+ secret and is kept in a `SecretStore`, never written in plaintext.
126
+ - **Routing:** `HandlerRouter.dispatchRaw()` validates the envelope and
127
+ routes to registered handlers; errors map to JSON-RPC error codes
128
+ (`-32000..-32008` + standard).
129
+ - **Agents:** `IAgentAdapter` per agent (OpenCode / Claude Code / Codex /
130
+ pi / Gemini CLI); `AgentManager` orchestrates streaming and broadcasts
131
+ `stream/*` notifications to connected phones.
132
+ - **Push:** `PushService` (persisted by relay `sessionId`) delivers FCM
133
+ HTTP v1 directly via `createBridgePushSender` (lazy `firebase-admin`),
134
+ with the relay `/push/notify` as a fallback.
135
+
136
+ See `architecture/02a-system-architecture.md` §5.8 and
137
+ `uxnandesktop/architecture/02e-bridge-integration.md`.
138
+
139
+ ## Develop
140
+
141
+ ```bash
142
+ # from the repo root (workspaces):
143
+ npm run build # build @uxnan/shared then uxnan-bridge
144
+ npm test # build + run all node:test suites (263 bridge + 29 shared + 9 relay)
145
+ npm run typecheck # tsc --noEmit across packages
146
+ npm run format # prettier --write
147
+ ```
148
+
149
+ Requires Node ≥18. ESM-only. Test runner uses `--test-concurrency=1` on
150
+ Windows (see CHANGELOG for why).
@@ -0,0 +1,13 @@
1
+ import type { AgentId, AuthStatus } from '@uxnan/shared';
2
+ export interface AccountStatusDeps {
3
+ /** Whether the agent's binary resolved (from the AgentManager). */
4
+ isAvailable(agentId: AgentId): boolean;
5
+ /** Injectable home dir (defaults to the OS home) — for tests. */
6
+ homeDir?: string;
7
+ /** Injectable existence check (defaults to fs `access`) — for tests. */
8
+ fileExists?: (path: string) => Promise<boolean>;
9
+ /** Injectable platform (defaults to `process.platform`) — for tests. */
10
+ platform?: NodeJS.Platform | string;
11
+ }
12
+ /** Build the sanitized {@link AuthStatus} for one agent. */
13
+ export declare function getAuthStatus(agentId: AgentId, deps: AccountStatusDeps): Promise<AuthStatus>;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Sanitized account/auth status per agent (architecture/02a §5.8.9).
3
+ *
4
+ * Reports whether an agent's CLI looks logged in on this PC WITHOUT ever reading
5
+ * or exposing tokens/keys: detection is by **existence only** of each agent's
6
+ * well-known auth file (never its contents), and the response carries only the
7
+ * agent id, booleans, the public provider name, the transport mode and the OS
8
+ * platform. There is intentionally no `displayName` — deriving it would mean
9
+ * reading an account/credentials file, which may hold secrets.
10
+ *
11
+ * Heuristic, not authoritative: an installed-but-logged-out CLI whose auth file
12
+ * is absent reports `requiresLogin: true`; an agent we have no auth-file mapping
13
+ * for falls back to its binary availability. FOR-DEV: for an authoritative
14
+ * answer, run the agent CLI's own auth/whoami command (out of MVP scope — it is
15
+ * slower and per-CLI).
16
+ */
17
+ import { homedir } from 'node:os';
18
+ import { join } from 'node:path';
19
+ import { access } from 'node:fs/promises';
20
+ /** Public provider name surfaced per agent (never a secret). */
21
+ const PROVIDER_BY_AGENT = {
22
+ codex: 'openai',
23
+ 'claude-code': 'anthropic',
24
+ opencode: 'opencode',
25
+ };
26
+ /**
27
+ * Auth-presence files per agent, relative to the user's home. EXISTENCE is the
28
+ * only signal read — contents (which hold the token) are never opened. Multiple
29
+ * candidates cover platform/version differences; the first that exists wins.
30
+ */
31
+ const AUTH_FILES = {
32
+ codex: ['.codex/auth.json'],
33
+ 'claude-code': ['.claude/.credentials.json', '.claude.json'],
34
+ opencode: ['.local/share/opencode/auth.json', '.config/opencode/auth.json'],
35
+ // pi stores per-provider credentials (OAuth/API keys) in one auth file; its
36
+ // existence means at least one provider is logged in. pi is multi-provider, so
37
+ // no single public provider name is surfaced.
38
+ 'pi-agent': ['.pi/agent/auth.json'],
39
+ };
40
+ async function defaultExists(path) {
41
+ try {
42
+ await access(path);
43
+ return true;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ /** Build the sanitized {@link AuthStatus} for one agent. */
50
+ export async function getAuthStatus(agentId, deps) {
51
+ const available = deps.isAvailable(agentId);
52
+ const provider = PROVIDER_BY_AGENT[agentId];
53
+ const authFiles = AUTH_FILES[agentId] ?? [];
54
+ let authenticated = false;
55
+ if (available && authFiles.length > 0) {
56
+ const home = deps.homeDir ?? homedir();
57
+ const exists = deps.fileExists ?? defaultExists;
58
+ for (const relative of authFiles) {
59
+ if (await exists(join(home, relative))) {
60
+ authenticated = true;
61
+ break;
62
+ }
63
+ }
64
+ }
65
+ // No binary → must be set up on the PC. Binary present with an auth-file
66
+ // mapping → trust the existence check. Binary present without a mapping →
67
+ // assume usable (we can't cheaply tell, and blocking would be wrong).
68
+ const requiresLogin = available ? (authFiles.length > 0 ? !authenticated : false) : true;
69
+ return {
70
+ agentId,
71
+ requiresLogin,
72
+ loginInProgress: false,
73
+ ...(provider !== undefined && authenticated ? { authenticatedProvider: provider } : {}),
74
+ transportMode: 'local',
75
+ platform: deps.platform ?? process.platform,
76
+ };
77
+ }
78
+ //# sourceMappingURL=account-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account-status.js","sourceRoot":"","sources":["../../src/account-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAG1C,gEAAgE;AAChE,MAAM,iBAAiB,GAAqC;IAC1D,KAAK,EAAE,QAAQ;IACf,aAAa,EAAE,WAAW;IAC1B,QAAQ,EAAE,UAAU;CACrB,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,GAAuC;IACrD,KAAK,EAAE,CAAC,kBAAkB,CAAC;IAC3B,aAAa,EAAE,CAAC,2BAA2B,EAAE,cAAc,CAAC;IAC5D,QAAQ,EAAE,CAAC,iCAAiC,EAAE,4BAA4B,CAAC;IAC3E,4EAA4E;IAC5E,+EAA+E;IAC/E,8CAA8C;IAC9C,UAAU,EAAE,CAAC,qBAAqB,CAAC;CACpC,CAAC;AAaF,KAAK,UAAU,aAAa,CAAC,IAAY;IACvC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,4DAA4D;AAC5D,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAgB,EAChB,IAAuB;IAEvB,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAC5C,MAAM,QAAQ,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;IAE5C,IAAI,aAAa,GAAG,KAAK,CAAC;IAC1B,IAAI,SAAS,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,IAAI,OAAO,EAAE,CAAC;QACvC,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,IAAI,aAAa,CAAC;QAChD,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;YACjC,IAAI,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC;gBACvC,aAAa,GAAG,IAAI,CAAC;gBACrB,MAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAED,yEAAyE;IACzE,0EAA0E;IAC1E,sEAAsE;IACtE,MAAM,aAAa,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAEzF,OAAO;QACL,OAAO;QACP,aAAa;QACb,eAAe,EAAE,KAAK;QACtB,GAAG,CAAC,QAAQ,KAAK,SAAS,IAAI,aAAa,CAAC,CAAC,CAAC,EAAE,qBAAqB,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvF,aAAa,EAAE,OAAO;QACtB,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ;KAC5C,CAAC;AACJ,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Base class for agent CLI adapters: event fan-out plus the {@link IAgentAdapter}
3
+ * surface. Concrete adapters implement the lifecycle and turn methods.
4
+ *
5
+ * Source: architecture/02a-system-architecture.md §5.8.2 (adapters/base-adapter).
6
+ */
7
+ import type { AgentCapabilities, AgentConfig, AgentId, AgentStreamEvent, IAgentAdapter, SendTurnOptions } from '@uxnan/shared';
8
+ export declare abstract class BaseAgentAdapter implements IAgentAdapter {
9
+ #private;
10
+ abstract readonly agentId: AgentId;
11
+ abstract readonly capabilities: AgentCapabilities;
12
+ onEvent(listener: (event: AgentStreamEvent) => void): () => void;
13
+ protected emit(event: AgentStreamEvent): void;
14
+ abstract start(config: AgentConfig): Promise<void>;
15
+ abstract stop(): Promise<void>;
16
+ abstract sendTurn(options: SendTurnOptions): Promise<void>;
17
+ abstract cancelTurn(threadId: string, turnId: string): Promise<void>;
18
+ }
@@ -0,0 +1,15 @@
1
+ export class BaseAgentAdapter {
2
+ #listeners = new Set();
3
+ onEvent(listener) {
4
+ this.#listeners.add(listener);
5
+ return () => {
6
+ this.#listeners.delete(listener);
7
+ };
8
+ }
9
+ emit(event) {
10
+ for (const listener of this.#listeners) {
11
+ listener(event);
12
+ }
13
+ }
14
+ }
15
+ //# sourceMappingURL=base-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-adapter.js","sourceRoot":"","sources":["../../../src/adapters/base-adapter.ts"],"names":[],"mappings":"AAeA,MAAM,OAAgB,gBAAgB;IAI3B,UAAU,GAAG,IAAI,GAAG,EAAqC,CAAC;IAEnE,OAAO,CAAC,QAA2C;QACjD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC9B,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACnC,CAAC,CAAC;IACJ,CAAC;IAES,IAAI,CAAC,KAAuB;QACpC,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACvC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;CAMF"}
@@ -0,0 +1,102 @@
1
+ import type { AgentCapabilities, AgentConfig, AgentId, AgentModel, SendTurnOptions } from '@uxnan/shared';
2
+ import { BaseAgentAdapter } from './base-adapter.js';
3
+ import { type ClaudeToolResult, type ClaudeToolUse } from './claude-tools.js';
4
+ import { type SpawnFn } from './spawn.js';
5
+ /**
6
+ * Headless permission posture passed to the CLI:
7
+ * - `default` → no flag (tools needing approval are auto-denied headless);
8
+ * - `acceptEdits` → `--permission-mode acceptEdits` (file edits auto-apply);
9
+ * - `bypassPermissions` → `--dangerously-skip-permissions` (all tools run).
10
+ */
11
+ export type ClaudePermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions';
12
+ /** An explicit, concrete model to add to the picker beyond the stable aliases. */
13
+ export interface ClaudeModelSpec {
14
+ /** Exact model id passed to `--model` (e.g. `claude-opus-4-8`). */
15
+ id: string;
16
+ /** Human-facing label (defaults to `id`). */
17
+ displayName?: string;
18
+ /** Optional one-line description. */
19
+ description?: string;
20
+ }
21
+ export interface ClaudeCodeAdapterOptions {
22
+ /** Executable to spawn (resolved path; see resolve-claude.ts). */
23
+ binaryPath?: string;
24
+ /** Args prepended before the adapter args (e.g. `[cli.js]` when running via node). */
25
+ prependArgs?: string[];
26
+ /** Default model (`alias` or full id) when the thread/turn doesn't pick one. */
27
+ defaultModel?: string;
28
+ /**
29
+ * Concrete, versioned models to surface in the picker **in addition** to the
30
+ * stable `opus`/`sonnet`/`haiku` aliases — declared in daemon config
31
+ * (`agents.claude-code.models`). Lets users pick an exact/older version while
32
+ * the aliases keep tracking "latest". Deduplicated against the aliases by id.
33
+ */
34
+ pinnedModels?: ClaudeModelSpec[];
35
+ /** Headless permission posture (default `acceptEdits`). */
36
+ permissionMode?: ClaudePermissionMode;
37
+ /**
38
+ * Opt-in interactive approvals: inject a `PreToolUse` hook (via `--settings`)
39
+ * so every tool round-trips to the bridge for the user's approval. Requires
40
+ * {@link approvalHook} to be set and resolvable (the bridge's local endpoint).
41
+ */
42
+ interactiveApprovals?: boolean;
43
+ /**
44
+ * The local approval-hook endpoint + token + the path to the shipped hook
45
+ * script. `url()` is lazy because the LAN port is known only after the server
46
+ * starts; it returns `undefined` until then (the turn runs without the hook).
47
+ */
48
+ approvalHook?: {
49
+ token: string;
50
+ scriptPath: string;
51
+ url: () => string | undefined;
52
+ };
53
+ /** Injected spawn function for the one-shot path (tests). */
54
+ spawnFn?: SpawnFn;
55
+ }
56
+ /** A normalized Claude Code event extracted from one stream-json line. */
57
+ export interface ClaudeEvent {
58
+ kind: 'init' | 'delta' | 'thinking' | 'assistant_text' | 'tool_result' | 'result' | 'other';
59
+ sessionId?: string;
60
+ text?: string;
61
+ /** Only set for `init`: the concrete model id the run resolved the alias to. */
62
+ model?: string;
63
+ /** Only set for `result`: whether the turn ended in error. */
64
+ isError?: boolean;
65
+ /** Only set for `result`: the raw `usage` object (token counts), if present. */
66
+ usage?: unknown;
67
+ /** Only set for `assistant_text`: any tool invocations in the message. */
68
+ toolUses?: ClaudeToolUse[];
69
+ /** Only set for `tool_result`: results the agent fed back from its tools. */
70
+ toolResults?: ClaudeToolResult[];
71
+ }
72
+ /**
73
+ * Context-window size (tokens) for a Claude model id or alias, so the phone can
74
+ * show context usage as a percentage. Fable/Opus/Sonnet are 1M, Haiku is 200K
75
+ * (matches the current model catalog); unknown ids return undefined.
76
+ */
77
+ export declare function claudeContextWindow(model: string | undefined): number | undefined;
78
+ /** Sum the context-occupying token counts from a Claude `result.usage` object. */
79
+ export declare function claudeUsageTokens(usage: unknown): number | undefined;
80
+ /** Parse one `claude … --output-format stream-json` line, or null if it isn't JSON. */
81
+ export declare function parseClaudeLine(line: string): ClaudeEvent | null;
82
+ export declare class ClaudeCodeAdapter extends BaseAgentAdapter {
83
+ #private;
84
+ readonly agentId: AgentId;
85
+ readonly capabilities: AgentCapabilities;
86
+ /** Native Claude session id for a thread (on-disk history-fallback locator). */
87
+ nativeSessionId(threadId: string): string | undefined;
88
+ constructor(options?: ClaudeCodeAdapterOptions);
89
+ get defaultModel(): string | undefined;
90
+ start(config: AgentConfig): Promise<void>;
91
+ stop(): Promise<void>;
92
+ sendTurn(options: SendTurnOptions): Promise<void>;
93
+ cancelTurn(threadId: string, turnId: string): Promise<void>;
94
+ /**
95
+ * Claude Code has no model-list command. Expose the stable `--model` aliases
96
+ * (each tracks the latest model of its tier the account can use — the concrete
97
+ * version is reported per-run via the `model_resolved` event), followed by any
98
+ * concrete versions pinned in config. Pinned ids that collide with an alias
99
+ * are dropped so the alias (the "latest" entry) wins.
100
+ */
101
+ listModels(): Promise<AgentModel[]>;
102
+ }