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.
- package/README.md +150 -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 +486 -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 +912 -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 +594 -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 +98 -0
- package/dist/src/agents/agent-manager.js +433 -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 +446 -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 +366 -0
- package/dist/src/conversation/thread-store.js.map +1 -0
- package/dist/src/daemon-config.d.ts +123 -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 +24 -0
- package/dist/src/git/git-runner.js +63 -0
- package/dist/src/git/git-runner.js.map +1 -0
- package/dist/src/git/git-service.d.ts +76 -0
- package/dist/src/git/git-service.js +435 -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 +82 -0
- package/dist/src/handlers/git-handler.js.map +1 -0
- package/dist/src/handlers/index.d.ts +8 -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 +211 -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 +17 -0
- package/dist/src/transport/local-hosts.js +30 -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 +134 -0
- package/dist/src/transport/session-handler.js.map +1 -0
- package/dist/src/transport/session-registry.d.ts +58 -0
- package/dist/src/transport/session-registry.js +91 -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 +111 -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,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
|
+
}
|