uxnan-bridge 0.0.1-alpha.20260627

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 +180 -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 +505 -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 +943 -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 +633 -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 +137 -0
  75. package/dist/src/agents/agent-manager.js +493 -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 +450 -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 +426 -0
  97. package/dist/src/conversation/thread-store.js.map +1 -0
  98. package/dist/src/daemon-config.d.ts +126 -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 +25 -0
  105. package/dist/src/git/git-runner.js +69 -0
  106. package/dist/src/git/git-runner.js.map +1 -0
  107. package/dist/src/git/git-service.d.ts +101 -0
  108. package/dist/src/git/git-service.js +852 -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 +103 -0
  127. package/dist/src/handlers/git-handler.js.map +1 -0
  128. package/dist/src/handlers/index.d.ts +9 -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 +217 -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 +23 -0
  201. package/dist/src/transport/local-hosts.js +64 -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 +144 -0
  223. package/dist/src/transport/session-handler.js.map +1 -0
  224. package/dist/src/transport/session-registry.d.ts +60 -0
  225. package/dist/src/transport/session-registry.js +95 -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 +150 -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,180 @@
1
+ # uxnan-bridge
2
+
3
+ ![Node.js](https://img.shields.io/badge/Node.js-%E2%89%A518-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)
4
+ ![TypeScript](https://img.shields.io/badge/TypeScript-ESM-3178C6?style=for-the-badge&logo=typescript&logoColor=white)
5
+ ![JSON RPC](https://img.shields.io/badge/JSON--RPC_2.0-60_methods-000000?style=for-the-badge&logo=json&logoColor=white)
6
+ ![E2EE](https://img.shields.io/badge/E2EE-AES--256--GCM-0a0a0a?style=for-the-badge&logo=letsencrypt&logoColor=white)
7
+ ![Platforms](https://img.shields.io/badge/Windows_%7C_macOS_%7C_Linux-lightgrey?style=for-the-badge)
8
+
9
+ The local control-plane daemon that connects the [Uxnan](../README.md) mobile app
10
+ to your PC over an end-to-end-encrypted channel. It is the **heart of the
11
+ product**: it holds the secure connection to your phone, runs Git and reads your
12
+ workspace on request, and drives the AI coding agents on your behalf, routing
13
+ JSON-RPC methods to per-domain handlers.
14
+
15
+ The product is **bridge-first**. The mobile app pairs with the bridge and tries
16
+ its direct LAN / Tailscale addresses first; the [relay](../relay/README.md) is an
17
+ optional, self-hosted off-LAN fallback. Background push notifications are sent
18
+ **by the bridge itself** (FCM HTTP v1) over any transport, so the phone keeps
19
+ receiving them whether it reached the bridge directly or through a relay.
20
+
21
+ > **Status:** alpha-functional on the primary path (LAN/Tailscale-direct,
22
+ > bridge-direct push), with **five real agents wired**. The detailed breakdown of
23
+ > what is built and what remains lives in [`FOR-DEV.md`](FOR-DEV.md); the release
24
+ > history is in [`CHANGELOG.md`](CHANGELOG.md).
25
+
26
+ ## Why the bridge matters
27
+
28
+ The bridge is small on purpose, but it is where the design decisions that make
29
+ Uxnan distinct actually live:
30
+
31
+ - **One bridge, many projects.** You start the bridge **once**, from a single
32
+ location of your choosing, and it gives the phone access to **all** the projects
33
+ underneath it — Git repositories or plain folders alike. There is no need to
34
+ launch a separate process per project: the phone browses the configured roots
35
+ (`workspace/browseDirs`, constrained by the `browseRoots` setting) and roots a
36
+ new conversation anywhere it is allowed to look.
37
+ - **Provider-agnostic, with no keys to hand over.** For each agent the bridge
38
+ spawns that agent's **official local CLI** and talks to it over stdio. It never
39
+ uses a provider HTTP API, API key, or language SDK. Each CLI runs under the
40
+ account or subscription you already authenticated on the machine, and the bridge
41
+ only orchestrates it.
42
+ - **Effortless discovery.** A freshly started bridge advertises itself on the
43
+ local network over mDNS (`_uxnan._tcp.local`), so the phone can find it without
44
+ typing an address. Pairing is by QR (which carries the bridge's direct
45
+ `host:port` list) or by a short manual code (`GET /pair/resolve?code=`) when a
46
+ camera is not convenient.
47
+ - **The transports it brings up.** On start, the bridge runs a direct LAN
48
+ `http + ws` server (which also serves Tailscale addresses transparently) and,
49
+ optionally, maintains a relay pairing session for off-LAN reach. The phone
50
+ chooses the best available path; you do not have to.
51
+ - **End-to-end encryption is not optional.** Every byte to and from the phone is
52
+ sealed with the documented E2EE protocol (X25519 + HKDF + Ed25519 +
53
+ AES-256-GCM). Responses are sanitized before they leave the machine — for
54
+ example, `auth/status` reports sign-in per agent and **never** returns a token.
55
+
56
+ <details>
57
+ <summary><b>Diagram — one bridge serving many projects over several transports</b></summary>
58
+
59
+ ```mermaid
60
+ flowchart LR
61
+ phone["📱 uxnanmobile"]
62
+
63
+ subgraph disc["Discovery & pairing"]
64
+ mdns["mDNS · _uxnan._tcp.local"]
65
+ qr["QR (direct hosts)"]
66
+ code["Manual code · /pair/resolve"]
67
+ end
68
+
69
+ subgraph pc["💻 your PC"]
70
+ bridge["uxnan-bridge<br/>(single instance)"]
71
+ subgraph roots["browseRoots"]
72
+ p1["project-a (git)"]
73
+ p2["project-b (git)"]
74
+ p3["scripts/ (plain folder)"]
75
+ end
76
+ clis["Official local CLIs<br/>opencode · claude · codex · pi · gemini"]
77
+ end
78
+
79
+ phone -- "E2EE" --> disc
80
+ disc --> bridge
81
+ phone -- "LAN / Tailscale (direct)" --> bridge
82
+ phone -- "relay (optional, off-LAN)" --> bridge
83
+ bridge --> p1
84
+ bridge --> p2
85
+ bridge --> p3
86
+ bridge --> clis
87
+ ```
88
+
89
+ </details>
90
+
91
+ ## How the bridge drives agents
92
+
93
+ This is the mechanism behind "provider-agnostic": the bridge spawns each agent's
94
+ official local CLI — `opencode`, `claude`, `codex`, `pi`, `gemini` — as a child
95
+ process and drives it over stdio, exactly as you would in a terminal. Prompts are
96
+ passed as `argv` elements with `shell:false` (no shell injection), in the thread's
97
+ working directory. The bridge parses each CLI's native stream and re-emits it as
98
+ structured events — `stream/content/block` (command / diff / tool) plus
99
+ `stream/thinking/delta` (reasoning) — so the phone renders the same shape no
100
+ matter which agent is running.
101
+
102
+ See [`FOR-HUMAN.md`](FOR-HUMAN.md) for the per-agent install / login
103
+ prerequisites, and [`docs/agents.md`](docs/agents.md) for the details.
104
+
105
+ ## Install
106
+
107
+ ```bash
108
+ npm install -g uxnan-bridge # (planned — published as a global package)
109
+ ```
110
+
111
+ ## CLI
112
+
113
+ ```bash
114
+ uxnan-bridge start # start the daemon: LAN server + (optional) relay pairing session
115
+ uxnan-bridge status # print current status as JSON
116
+ uxnan-bridge qr # print the pairing QR in the terminal (with the manual code)
117
+ uxnan-bridge code # print just the current pairing code
118
+ uxnan-bridge stop # stop the running daemon (via the lock file)
119
+ uxnan-bridge install-service # autostart at logon (Task Scheduler / LaunchAgent / systemd --user)
120
+ uxnan-bridge uninstall-service
121
+ ```
122
+
123
+ Logs are written to `~/.uxnan/logs/bridge-YYYY-MM-DD.log` (daily rotation, with a
124
+ secret-redaction pass) and to stderr. Autostart at login is configured by the
125
+ platform scripts under `scripts/`.
126
+
127
+ The Ed25519 identity is stored in the OS keychain (Windows Credential Manager /
128
+ macOS Keychain / Linux Secret Service) via `@napi-rs/keyring`. With no keychain
129
+ available, the bridge still runs with an in-memory identity (not persisted across
130
+ restarts).
131
+
132
+ ## Docs
133
+
134
+ Task-focused guides live in [`docs/`](docs/):
135
+ [installation & autostart](docs/installation.md) ·
136
+ [configuration](docs/configuration.md) ·
137
+ [connectivity (LAN / Tailscale / relay)](docs/connectivity.md) ·
138
+ [how agents are driven](docs/agents.md) ·
139
+ [testing](docs/testing.md) ·
140
+ [packaging & deploy](docs/deploy.md) ·
141
+ [push notifications](docs/push-notifications.md).
142
+
143
+ ## Architecture
144
+
145
+ - **Contracts.** Consumes [`@uxnan/shared`](../shared/README.md) for JSON-RPC and
146
+ E2EE types and runtime validators. The bridge exposes **60 JSON-RPC methods +
147
+ 8 streaming notifications** (see `shared/src/jsonrpc/`); the mobile app keeps
148
+ manually-synced Dart equivalents of the same shapes.
149
+ - **State.** Non-secret JSON under `~/.uxnan/` (atomic writes) —
150
+ `daemon-config.json`, `pairing-session.json`, `threads.json`,
151
+ `trusted-phones.json`, `push-state.json`, `agent-cache/`, `logs/`. The Ed25519
152
+ identity is a secret kept in a `SecretStore`, never written in plaintext.
153
+ - **Routing.** `HandlerRouter.dispatchRaw()` validates the envelope and routes to
154
+ registered handlers; errors map to JSON-RPC error codes (`-32000..-32008` +
155
+ standard).
156
+ - **Agents.** An `IAgentAdapter` per agent (OpenCode / Claude Code / Codex / pi /
157
+ Gemini CLI); `AgentManager` orchestrates streaming and broadcasts `stream/*`
158
+ notifications to connected phones.
159
+ - **Push.** `PushService` (persisted by relay `sessionId`) delivers FCM HTTP v1
160
+ directly via `createBridgePushSender` (lazy `firebase-admin`), with the relay
161
+ `/push/notify` as a fallback.
162
+
163
+ The cross-component specification is `architecture/02a-system-architecture.md`
164
+ §5.8 and
165
+ [`uxnandesktop/architecture/02e-bridge-integration.md`](../uxnandesktop/architecture/02e-bridge-integration.md).
166
+
167
+ ## Develop
168
+
169
+ ```bash
170
+ # from the repo root (npm workspaces):
171
+ npm run build # build @uxnan/shared then uxnan-bridge
172
+ npm test # build + run all node:test suites
173
+ npm run typecheck # tsc --noEmit across packages
174
+ npm run format # prettier --write
175
+ ```
176
+
177
+ Requires Node ≥ 18. ESM-only. The test runner uses `--test-concurrency=1` on
178
+ Windows (see [`CHANGELOG.md`](CHANGELOG.md) for why). What is implemented versus
179
+ still pending — including the recipe for wiring the next agent — is tracked in
180
+ [`FOR-DEV.md`](FOR-DEV.md).
@@ -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
+ }