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.
- package/README.md +180 -0
- package/dist/src/account-status.d.ts +13 -0
- package/dist/src/account-status.js +78 -0
- package/dist/src/account-status.js.map +1 -0
- package/dist/src/adapters/base-adapter.d.ts +18 -0
- package/dist/src/adapters/base-adapter.js +15 -0
- package/dist/src/adapters/base-adapter.js.map +1 -0
- package/dist/src/adapters/claude-adapter.d.ts +102 -0
- package/dist/src/adapters/claude-adapter.js +505 -0
- package/dist/src/adapters/claude-adapter.js.map +1 -0
- package/dist/src/adapters/claude-tools.d.ts +25 -0
- package/dist/src/adapters/claude-tools.js +146 -0
- package/dist/src/adapters/claude-tools.js.map +1 -0
- package/dist/src/adapters/codex-adapter.d.ts +116 -0
- package/dist/src/adapters/codex-adapter.js +943 -0
- package/dist/src/adapters/codex-adapter.js.map +1 -0
- package/dist/src/adapters/codex-app-server.d.ts +74 -0
- package/dist/src/adapters/codex-app-server.js +225 -0
- package/dist/src/adapters/codex-app-server.js.map +1 -0
- package/dist/src/adapters/codex-approval.d.ts +88 -0
- package/dist/src/adapters/codex-approval.js +160 -0
- package/dist/src/adapters/codex-approval.js.map +1 -0
- package/dist/src/adapters/codex-tools.d.ts +18 -0
- package/dist/src/adapters/codex-tools.js +106 -0
- package/dist/src/adapters/codex-tools.js.map +1 -0
- package/dist/src/adapters/content-blocks.d.ts +68 -0
- package/dist/src/adapters/content-blocks.js +205 -0
- package/dist/src/adapters/content-blocks.js.map +1 -0
- package/dist/src/adapters/echo-agent-adapter.d.ts +23 -0
- package/dist/src/adapters/echo-agent-adapter.js +72 -0
- package/dist/src/adapters/echo-agent-adapter.js.map +1 -0
- package/dist/src/adapters/gemini-adapter.d.ts +87 -0
- package/dist/src/adapters/gemini-adapter.js +633 -0
- package/dist/src/adapters/gemini-adapter.js.map +1 -0
- package/dist/src/adapters/gemini-tools.d.ts +4 -0
- package/dist/src/adapters/gemini-tools.js +48 -0
- package/dist/src/adapters/gemini-tools.js.map +1 -0
- package/dist/src/adapters/opencode-adapter.d.ts +74 -0
- package/dist/src/adapters/opencode-adapter.js +418 -0
- package/dist/src/adapters/opencode-adapter.js.map +1 -0
- package/dist/src/adapters/opencode-tools.d.ts +2 -0
- package/dist/src/adapters/opencode-tools.js +41 -0
- package/dist/src/adapters/opencode-tools.js.map +1 -0
- package/dist/src/adapters/pi-adapter.d.ts +92 -0
- package/dist/src/adapters/pi-adapter.js +467 -0
- package/dist/src/adapters/pi-adapter.js.map +1 -0
- package/dist/src/adapters/pi-tools.d.ts +10 -0
- package/dist/src/adapters/pi-tools.js +72 -0
- package/dist/src/adapters/pi-tools.js.map +1 -0
- package/dist/src/adapters/process-agent-adapter.d.ts +24 -0
- package/dist/src/adapters/process-agent-adapter.js +111 -0
- package/dist/src/adapters/process-agent-adapter.js.map +1 -0
- package/dist/src/adapters/resolve-claude.d.ts +13 -0
- package/dist/src/adapters/resolve-claude.js +57 -0
- package/dist/src/adapters/resolve-claude.js.map +1 -0
- package/dist/src/adapters/resolve-codex.d.ts +13 -0
- package/dist/src/adapters/resolve-codex.js +48 -0
- package/dist/src/adapters/resolve-codex.js.map +1 -0
- package/dist/src/adapters/resolve-gemini.d.ts +13 -0
- package/dist/src/adapters/resolve-gemini.js +47 -0
- package/dist/src/adapters/resolve-gemini.js.map +1 -0
- package/dist/src/adapters/resolve-opencode.d.ts +11 -0
- package/dist/src/adapters/resolve-opencode.js +49 -0
- package/dist/src/adapters/resolve-opencode.js.map +1 -0
- package/dist/src/adapters/resolve-pi.d.ts +13 -0
- package/dist/src/adapters/resolve-pi.js +46 -0
- package/dist/src/adapters/resolve-pi.js.map +1 -0
- package/dist/src/adapters/run-options.d.ts +22 -0
- package/dist/src/adapters/run-options.js +48 -0
- package/dist/src/adapters/run-options.js.map +1 -0
- package/dist/src/adapters/spawn.d.ts +20 -0
- package/dist/src/adapters/spawn.js +16 -0
- package/dist/src/adapters/spawn.js.map +1 -0
- package/dist/src/agents/agent-manager.d.ts +137 -0
- package/dist/src/agents/agent-manager.js +493 -0
- package/dist/src/agents/agent-manager.js.map +1 -0
- package/dist/src/agents/attachments.d.ts +28 -0
- package/dist/src/agents/attachments.js +121 -0
- package/dist/src/agents/attachments.js.map +1 -0
- package/dist/src/bridge-context.d.ts +45 -0
- package/dist/src/bridge-context.js +2 -0
- package/dist/src/bridge-context.js.map +1 -0
- package/dist/src/bridge-status.d.ts +12 -0
- package/dist/src/bridge-status.js +17 -0
- package/dist/src/bridge-status.js.map +1 -0
- package/dist/src/bridge.d.ts +37 -0
- package/dist/src/bridge.js +450 -0
- package/dist/src/bridge.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +194 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/conversation/session-history.d.ts +27 -0
- package/dist/src/conversation/session-history.js +1082 -0
- package/dist/src/conversation/session-history.js.map +1 -0
- package/dist/src/conversation/thread-store.d.ts +74 -0
- package/dist/src/conversation/thread-store.js +426 -0
- package/dist/src/conversation/thread-store.js.map +1 -0
- package/dist/src/daemon-config.d.ts +126 -0
- package/dist/src/daemon-config.js +64 -0
- package/dist/src/daemon-config.js.map +1 -0
- package/dist/src/daemon-state.d.ts +27 -0
- package/dist/src/daemon-state.js +76 -0
- package/dist/src/daemon-state.js.map +1 -0
- package/dist/src/git/git-runner.d.ts +25 -0
- package/dist/src/git/git-runner.js +69 -0
- package/dist/src/git/git-runner.js.map +1 -0
- package/dist/src/git/git-service.d.ts +101 -0
- package/dist/src/git/git-service.js +852 -0
- package/dist/src/git/git-service.js.map +1 -0
- package/dist/src/handler-router.d.ts +34 -0
- package/dist/src/handler-router.js +67 -0
- package/dist/src/handler-router.js.map +1 -0
- package/dist/src/handlers/account-handler.d.ts +4 -0
- package/dist/src/handlers/account-handler.js +27 -0
- package/dist/src/handlers/account-handler.js.map +1 -0
- package/dist/src/handlers/agent-handler.d.ts +2 -0
- package/dist/src/handlers/agent-handler.js +8 -0
- package/dist/src/handlers/agent-handler.js.map +1 -0
- package/dist/src/handlers/bridge-control-handler.d.ts +2 -0
- package/dist/src/handlers/bridge-control-handler.js +64 -0
- package/dist/src/handlers/bridge-control-handler.js.map +1 -0
- package/dist/src/handlers/desktop-handler.d.ts +12 -0
- package/dist/src/handlers/desktop-handler.js +5 -0
- package/dist/src/handlers/desktop-handler.js.map +1 -0
- package/dist/src/handlers/git-handler.d.ts +2 -0
- package/dist/src/handlers/git-handler.js +103 -0
- package/dist/src/handlers/git-handler.js.map +1 -0
- package/dist/src/handlers/index.d.ts +9 -0
- package/dist/src/handlers/index.js +22 -0
- package/dist/src/handlers/index.js.map +1 -0
- package/dist/src/handlers/not-implemented.d.ts +10 -0
- package/dist/src/handlers/not-implemented.js +21 -0
- package/dist/src/handlers/not-implemented.js.map +1 -0
- package/dist/src/handlers/notifications-handler.d.ts +2 -0
- package/dist/src/handlers/notifications-handler.js +62 -0
- package/dist/src/handlers/notifications-handler.js.map +1 -0
- package/dist/src/handlers/params.d.ts +11 -0
- package/dist/src/handlers/params.js +72 -0
- package/dist/src/handlers/params.js.map +1 -0
- package/dist/src/handlers/project-handler.d.ts +2 -0
- package/dist/src/handlers/project-handler.js +6 -0
- package/dist/src/handlers/project-handler.js.map +1 -0
- package/dist/src/handlers/thread-context-handler.d.ts +2 -0
- package/dist/src/handlers/thread-context-handler.js +217 -0
- package/dist/src/handlers/thread-context-handler.js.map +1 -0
- package/dist/src/handlers/workspace-handler.d.ts +2 -0
- package/dist/src/handlers/workspace-handler.js +101 -0
- package/dist/src/handlers/workspace-handler.js.map +1 -0
- package/dist/src/hooks/claude-approval-hook.d.ts +7 -0
- package/dist/src/hooks/claude-approval-hook.js +95 -0
- package/dist/src/hooks/claude-approval-hook.js.map +1 -0
- package/dist/src/hooks/gemini-approval-hook.d.ts +7 -0
- package/dist/src/hooks/gemini-approval-hook.js +113 -0
- package/dist/src/hooks/gemini-approval-hook.js.map +1 -0
- package/dist/src/index.d.ts +62 -0
- package/dist/src/index.js +65 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/keyring-secret-store.d.ts +36 -0
- package/dist/src/keyring-secret-store.js +70 -0
- package/dist/src/keyring-secret-store.js.map +1 -0
- package/dist/src/lock-file.d.ts +18 -0
- package/dist/src/lock-file.js +60 -0
- package/dist/src/lock-file.js.map +1 -0
- package/dist/src/logger.d.ts +28 -0
- package/dist/src/logger.js +99 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/pairing/pairing-code-service.d.ts +45 -0
- package/dist/src/pairing/pairing-code-service.js +183 -0
- package/dist/src/pairing/pairing-code-service.js.map +1 -0
- package/dist/src/projects/project-registry.d.ts +14 -0
- package/dist/src/projects/project-registry.js +60 -0
- package/dist/src/projects/project-registry.js.map +1 -0
- package/dist/src/push/push-sender.d.ts +21 -0
- package/dist/src/push/push-sender.js +96 -0
- package/dist/src/push/push-sender.js.map +1 -0
- package/dist/src/push/push-service.d.ts +122 -0
- package/dist/src/push/push-service.js +260 -0
- package/dist/src/push/push-service.js.map +1 -0
- package/dist/src/qr.d.ts +17 -0
- package/dist/src/qr.js +31 -0
- package/dist/src/qr.js.map +1 -0
- package/dist/src/secret-store.d.ts +23 -0
- package/dist/src/secret-store.js +27 -0
- package/dist/src/secret-store.js.map +1 -0
- package/dist/src/secure-device-state.d.ts +16 -0
- package/dist/src/secure-device-state.js +63 -0
- package/dist/src/secure-device-state.js.map +1 -0
- package/dist/src/service-installer.d.ts +57 -0
- package/dist/src/service-installer.js +254 -0
- package/dist/src/service-installer.js.map +1 -0
- package/dist/src/session-state.d.ts +14 -0
- package/dist/src/session-state.js +19 -0
- package/dist/src/session-state.js.map +1 -0
- package/dist/src/transport/crypto.d.ts +43 -0
- package/dist/src/transport/crypto.js +78 -0
- package/dist/src/transport/crypto.js.map +1 -0
- package/dist/src/transport/lan-server.d.ts +33 -0
- package/dist/src/transport/lan-server.js +105 -0
- package/dist/src/transport/lan-server.js.map +1 -0
- package/dist/src/transport/local-hosts.d.ts +23 -0
- package/dist/src/transport/local-hosts.js +64 -0
- package/dist/src/transport/local-hosts.js.map +1 -0
- package/dist/src/transport/mdns-advertiser.d.ts +83 -0
- package/dist/src/transport/mdns-advertiser.js +282 -0
- package/dist/src/transport/mdns-advertiser.js.map +1 -0
- package/dist/src/transport/message-io.d.ts +33 -0
- package/dist/src/transport/message-io.js +87 -0
- package/dist/src/transport/message-io.js.map +1 -0
- package/dist/src/transport/outbound-log.d.ts +24 -0
- package/dist/src/transport/outbound-log.js +78 -0
- package/dist/src/transport/outbound-log.js.map +1 -0
- package/dist/src/transport/relay-client.d.ts +19 -0
- package/dist/src/transport/relay-client.js +27 -0
- package/dist/src/transport/relay-client.js.map +1 -0
- package/dist/src/transport/secure-channel.d.ts +33 -0
- package/dist/src/transport/secure-channel.js +81 -0
- package/dist/src/transport/secure-channel.js.map +1 -0
- package/dist/src/transport/server-handshake.d.ts +49 -0
- package/dist/src/transport/server-handshake.js +137 -0
- package/dist/src/transport/server-handshake.js.map +1 -0
- package/dist/src/transport/session-handler.d.ts +19 -0
- package/dist/src/transport/session-handler.js +144 -0
- package/dist/src/transport/session-handler.js.map +1 -0
- package/dist/src/transport/session-registry.d.ts +60 -0
- package/dist/src/transport/session-registry.js +95 -0
- package/dist/src/transport/session-registry.js.map +1 -0
- package/dist/src/transport/trust-store.d.ts +23 -0
- package/dist/src/transport/trust-store.js +33 -0
- package/dist/src/transport/trust-store.js.map +1 -0
- package/dist/src/transport/ws-adapter.d.ts +7 -0
- package/dist/src/transport/ws-adapter.js +16 -0
- package/dist/src/transport/ws-adapter.js.map +1 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +13 -0
- package/dist/src/version.js.map +1 -0
- package/dist/src/workspace/browse-service.d.ts +10 -0
- package/dist/src/workspace/browse-service.js +97 -0
- package/dist/src/workspace/browse-service.js.map +1 -0
- package/dist/src/workspace/checkpoint-service.d.ts +21 -0
- package/dist/src/workspace/checkpoint-service.js +219 -0
- package/dist/src/workspace/checkpoint-service.js.map +1 -0
- package/dist/src/workspace/path-guard.d.ts +7 -0
- package/dist/src/workspace/path-guard.js +51 -0
- package/dist/src/workspace/path-guard.js.map +1 -0
- package/dist/src/workspace/workspace-service.d.ts +8 -0
- package/dist/src/workspace/workspace-service.js +150 -0
- package/dist/src/workspace/workspace-service.js.map +1 -0
- package/package.json +46 -0
- package/scripts/extract-gemini-hook.mjs +16 -0
- package/scripts/fake-approval-bridge.mjs +23 -0
- package/scripts/install-service-linux.sh +38 -0
- package/scripts/install-service-macos.sh +38 -0
- package/scripts/install-service-windows.ps1 +26 -0
- package/scripts/test-gemini-hook-e2e.mjs +168 -0
- package/scripts/write-gemini-settings.mjs +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# uxnan-bridge
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
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
|
+
}
|