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.

Potentially problematic release.


This version of uxnan-bridge might be problematic. Click here for more details.

Files changed (255) hide show
  1. package/README.md +150 -0
  2. package/dist/src/account-status.d.ts +13 -0
  3. package/dist/src/account-status.js +78 -0
  4. package/dist/src/account-status.js.map +1 -0
  5. package/dist/src/adapters/base-adapter.d.ts +18 -0
  6. package/dist/src/adapters/base-adapter.js +15 -0
  7. package/dist/src/adapters/base-adapter.js.map +1 -0
  8. package/dist/src/adapters/claude-adapter.d.ts +102 -0
  9. package/dist/src/adapters/claude-adapter.js +486 -0
  10. package/dist/src/adapters/claude-adapter.js.map +1 -0
  11. package/dist/src/adapters/claude-tools.d.ts +25 -0
  12. package/dist/src/adapters/claude-tools.js +146 -0
  13. package/dist/src/adapters/claude-tools.js.map +1 -0
  14. package/dist/src/adapters/codex-adapter.d.ts +116 -0
  15. package/dist/src/adapters/codex-adapter.js +912 -0
  16. package/dist/src/adapters/codex-adapter.js.map +1 -0
  17. package/dist/src/adapters/codex-app-server.d.ts +74 -0
  18. package/dist/src/adapters/codex-app-server.js +225 -0
  19. package/dist/src/adapters/codex-app-server.js.map +1 -0
  20. package/dist/src/adapters/codex-approval.d.ts +88 -0
  21. package/dist/src/adapters/codex-approval.js +160 -0
  22. package/dist/src/adapters/codex-approval.js.map +1 -0
  23. package/dist/src/adapters/codex-tools.d.ts +18 -0
  24. package/dist/src/adapters/codex-tools.js +106 -0
  25. package/dist/src/adapters/codex-tools.js.map +1 -0
  26. package/dist/src/adapters/content-blocks.d.ts +68 -0
  27. package/dist/src/adapters/content-blocks.js +205 -0
  28. package/dist/src/adapters/content-blocks.js.map +1 -0
  29. package/dist/src/adapters/echo-agent-adapter.d.ts +23 -0
  30. package/dist/src/adapters/echo-agent-adapter.js +72 -0
  31. package/dist/src/adapters/echo-agent-adapter.js.map +1 -0
  32. package/dist/src/adapters/gemini-adapter.d.ts +87 -0
  33. package/dist/src/adapters/gemini-adapter.js +594 -0
  34. package/dist/src/adapters/gemini-adapter.js.map +1 -0
  35. package/dist/src/adapters/gemini-tools.d.ts +4 -0
  36. package/dist/src/adapters/gemini-tools.js +48 -0
  37. package/dist/src/adapters/gemini-tools.js.map +1 -0
  38. package/dist/src/adapters/opencode-adapter.d.ts +74 -0
  39. package/dist/src/adapters/opencode-adapter.js +418 -0
  40. package/dist/src/adapters/opencode-adapter.js.map +1 -0
  41. package/dist/src/adapters/opencode-tools.d.ts +2 -0
  42. package/dist/src/adapters/opencode-tools.js +41 -0
  43. package/dist/src/adapters/opencode-tools.js.map +1 -0
  44. package/dist/src/adapters/pi-adapter.d.ts +92 -0
  45. package/dist/src/adapters/pi-adapter.js +467 -0
  46. package/dist/src/adapters/pi-adapter.js.map +1 -0
  47. package/dist/src/adapters/pi-tools.d.ts +10 -0
  48. package/dist/src/adapters/pi-tools.js +72 -0
  49. package/dist/src/adapters/pi-tools.js.map +1 -0
  50. package/dist/src/adapters/process-agent-adapter.d.ts +24 -0
  51. package/dist/src/adapters/process-agent-adapter.js +111 -0
  52. package/dist/src/adapters/process-agent-adapter.js.map +1 -0
  53. package/dist/src/adapters/resolve-claude.d.ts +13 -0
  54. package/dist/src/adapters/resolve-claude.js +57 -0
  55. package/dist/src/adapters/resolve-claude.js.map +1 -0
  56. package/dist/src/adapters/resolve-codex.d.ts +13 -0
  57. package/dist/src/adapters/resolve-codex.js +48 -0
  58. package/dist/src/adapters/resolve-codex.js.map +1 -0
  59. package/dist/src/adapters/resolve-gemini.d.ts +13 -0
  60. package/dist/src/adapters/resolve-gemini.js +47 -0
  61. package/dist/src/adapters/resolve-gemini.js.map +1 -0
  62. package/dist/src/adapters/resolve-opencode.d.ts +11 -0
  63. package/dist/src/adapters/resolve-opencode.js +49 -0
  64. package/dist/src/adapters/resolve-opencode.js.map +1 -0
  65. package/dist/src/adapters/resolve-pi.d.ts +13 -0
  66. package/dist/src/adapters/resolve-pi.js +46 -0
  67. package/dist/src/adapters/resolve-pi.js.map +1 -0
  68. package/dist/src/adapters/run-options.d.ts +22 -0
  69. package/dist/src/adapters/run-options.js +48 -0
  70. package/dist/src/adapters/run-options.js.map +1 -0
  71. package/dist/src/adapters/spawn.d.ts +20 -0
  72. package/dist/src/adapters/spawn.js +16 -0
  73. package/dist/src/adapters/spawn.js.map +1 -0
  74. package/dist/src/agents/agent-manager.d.ts +98 -0
  75. package/dist/src/agents/agent-manager.js +433 -0
  76. package/dist/src/agents/agent-manager.js.map +1 -0
  77. package/dist/src/agents/attachments.d.ts +28 -0
  78. package/dist/src/agents/attachments.js +121 -0
  79. package/dist/src/agents/attachments.js.map +1 -0
  80. package/dist/src/bridge-context.d.ts +45 -0
  81. package/dist/src/bridge-context.js +2 -0
  82. package/dist/src/bridge-context.js.map +1 -0
  83. package/dist/src/bridge-status.d.ts +12 -0
  84. package/dist/src/bridge-status.js +17 -0
  85. package/dist/src/bridge-status.js.map +1 -0
  86. package/dist/src/bridge.d.ts +37 -0
  87. package/dist/src/bridge.js +446 -0
  88. package/dist/src/bridge.js.map +1 -0
  89. package/dist/src/cli.d.ts +2 -0
  90. package/dist/src/cli.js +194 -0
  91. package/dist/src/cli.js.map +1 -0
  92. package/dist/src/conversation/session-history.d.ts +27 -0
  93. package/dist/src/conversation/session-history.js +1082 -0
  94. package/dist/src/conversation/session-history.js.map +1 -0
  95. package/dist/src/conversation/thread-store.d.ts +74 -0
  96. package/dist/src/conversation/thread-store.js +366 -0
  97. package/dist/src/conversation/thread-store.js.map +1 -0
  98. package/dist/src/daemon-config.d.ts +123 -0
  99. package/dist/src/daemon-config.js +64 -0
  100. package/dist/src/daemon-config.js.map +1 -0
  101. package/dist/src/daemon-state.d.ts +27 -0
  102. package/dist/src/daemon-state.js +76 -0
  103. package/dist/src/daemon-state.js.map +1 -0
  104. package/dist/src/git/git-runner.d.ts +24 -0
  105. package/dist/src/git/git-runner.js +63 -0
  106. package/dist/src/git/git-runner.js.map +1 -0
  107. package/dist/src/git/git-service.d.ts +76 -0
  108. package/dist/src/git/git-service.js +435 -0
  109. package/dist/src/git/git-service.js.map +1 -0
  110. package/dist/src/handler-router.d.ts +34 -0
  111. package/dist/src/handler-router.js +67 -0
  112. package/dist/src/handler-router.js.map +1 -0
  113. package/dist/src/handlers/account-handler.d.ts +4 -0
  114. package/dist/src/handlers/account-handler.js +27 -0
  115. package/dist/src/handlers/account-handler.js.map +1 -0
  116. package/dist/src/handlers/agent-handler.d.ts +2 -0
  117. package/dist/src/handlers/agent-handler.js +8 -0
  118. package/dist/src/handlers/agent-handler.js.map +1 -0
  119. package/dist/src/handlers/bridge-control-handler.d.ts +2 -0
  120. package/dist/src/handlers/bridge-control-handler.js +64 -0
  121. package/dist/src/handlers/bridge-control-handler.js.map +1 -0
  122. package/dist/src/handlers/desktop-handler.d.ts +12 -0
  123. package/dist/src/handlers/desktop-handler.js +5 -0
  124. package/dist/src/handlers/desktop-handler.js.map +1 -0
  125. package/dist/src/handlers/git-handler.d.ts +2 -0
  126. package/dist/src/handlers/git-handler.js +82 -0
  127. package/dist/src/handlers/git-handler.js.map +1 -0
  128. package/dist/src/handlers/index.d.ts +8 -0
  129. package/dist/src/handlers/index.js +22 -0
  130. package/dist/src/handlers/index.js.map +1 -0
  131. package/dist/src/handlers/not-implemented.d.ts +10 -0
  132. package/dist/src/handlers/not-implemented.js +21 -0
  133. package/dist/src/handlers/not-implemented.js.map +1 -0
  134. package/dist/src/handlers/notifications-handler.d.ts +2 -0
  135. package/dist/src/handlers/notifications-handler.js +62 -0
  136. package/dist/src/handlers/notifications-handler.js.map +1 -0
  137. package/dist/src/handlers/params.d.ts +11 -0
  138. package/dist/src/handlers/params.js +72 -0
  139. package/dist/src/handlers/params.js.map +1 -0
  140. package/dist/src/handlers/project-handler.d.ts +2 -0
  141. package/dist/src/handlers/project-handler.js +6 -0
  142. package/dist/src/handlers/project-handler.js.map +1 -0
  143. package/dist/src/handlers/thread-context-handler.d.ts +2 -0
  144. package/dist/src/handlers/thread-context-handler.js +211 -0
  145. package/dist/src/handlers/thread-context-handler.js.map +1 -0
  146. package/dist/src/handlers/workspace-handler.d.ts +2 -0
  147. package/dist/src/handlers/workspace-handler.js +101 -0
  148. package/dist/src/handlers/workspace-handler.js.map +1 -0
  149. package/dist/src/hooks/claude-approval-hook.d.ts +7 -0
  150. package/dist/src/hooks/claude-approval-hook.js +95 -0
  151. package/dist/src/hooks/claude-approval-hook.js.map +1 -0
  152. package/dist/src/hooks/gemini-approval-hook.d.ts +7 -0
  153. package/dist/src/hooks/gemini-approval-hook.js +113 -0
  154. package/dist/src/hooks/gemini-approval-hook.js.map +1 -0
  155. package/dist/src/index.d.ts +62 -0
  156. package/dist/src/index.js +65 -0
  157. package/dist/src/index.js.map +1 -0
  158. package/dist/src/keyring-secret-store.d.ts +36 -0
  159. package/dist/src/keyring-secret-store.js +70 -0
  160. package/dist/src/keyring-secret-store.js.map +1 -0
  161. package/dist/src/lock-file.d.ts +18 -0
  162. package/dist/src/lock-file.js +60 -0
  163. package/dist/src/lock-file.js.map +1 -0
  164. package/dist/src/logger.d.ts +28 -0
  165. package/dist/src/logger.js +99 -0
  166. package/dist/src/logger.js.map +1 -0
  167. package/dist/src/pairing/pairing-code-service.d.ts +45 -0
  168. package/dist/src/pairing/pairing-code-service.js +183 -0
  169. package/dist/src/pairing/pairing-code-service.js.map +1 -0
  170. package/dist/src/projects/project-registry.d.ts +14 -0
  171. package/dist/src/projects/project-registry.js +60 -0
  172. package/dist/src/projects/project-registry.js.map +1 -0
  173. package/dist/src/push/push-sender.d.ts +21 -0
  174. package/dist/src/push/push-sender.js +96 -0
  175. package/dist/src/push/push-sender.js.map +1 -0
  176. package/dist/src/push/push-service.d.ts +122 -0
  177. package/dist/src/push/push-service.js +260 -0
  178. package/dist/src/push/push-service.js.map +1 -0
  179. package/dist/src/qr.d.ts +17 -0
  180. package/dist/src/qr.js +31 -0
  181. package/dist/src/qr.js.map +1 -0
  182. package/dist/src/secret-store.d.ts +23 -0
  183. package/dist/src/secret-store.js +27 -0
  184. package/dist/src/secret-store.js.map +1 -0
  185. package/dist/src/secure-device-state.d.ts +16 -0
  186. package/dist/src/secure-device-state.js +63 -0
  187. package/dist/src/secure-device-state.js.map +1 -0
  188. package/dist/src/service-installer.d.ts +57 -0
  189. package/dist/src/service-installer.js +254 -0
  190. package/dist/src/service-installer.js.map +1 -0
  191. package/dist/src/session-state.d.ts +14 -0
  192. package/dist/src/session-state.js +19 -0
  193. package/dist/src/session-state.js.map +1 -0
  194. package/dist/src/transport/crypto.d.ts +43 -0
  195. package/dist/src/transport/crypto.js +78 -0
  196. package/dist/src/transport/crypto.js.map +1 -0
  197. package/dist/src/transport/lan-server.d.ts +33 -0
  198. package/dist/src/transport/lan-server.js +105 -0
  199. package/dist/src/transport/lan-server.js.map +1 -0
  200. package/dist/src/transport/local-hosts.d.ts +17 -0
  201. package/dist/src/transport/local-hosts.js +30 -0
  202. package/dist/src/transport/local-hosts.js.map +1 -0
  203. package/dist/src/transport/mdns-advertiser.d.ts +83 -0
  204. package/dist/src/transport/mdns-advertiser.js +282 -0
  205. package/dist/src/transport/mdns-advertiser.js.map +1 -0
  206. package/dist/src/transport/message-io.d.ts +33 -0
  207. package/dist/src/transport/message-io.js +87 -0
  208. package/dist/src/transport/message-io.js.map +1 -0
  209. package/dist/src/transport/outbound-log.d.ts +24 -0
  210. package/dist/src/transport/outbound-log.js +78 -0
  211. package/dist/src/transport/outbound-log.js.map +1 -0
  212. package/dist/src/transport/relay-client.d.ts +19 -0
  213. package/dist/src/transport/relay-client.js +27 -0
  214. package/dist/src/transport/relay-client.js.map +1 -0
  215. package/dist/src/transport/secure-channel.d.ts +33 -0
  216. package/dist/src/transport/secure-channel.js +81 -0
  217. package/dist/src/transport/secure-channel.js.map +1 -0
  218. package/dist/src/transport/server-handshake.d.ts +49 -0
  219. package/dist/src/transport/server-handshake.js +137 -0
  220. package/dist/src/transport/server-handshake.js.map +1 -0
  221. package/dist/src/transport/session-handler.d.ts +19 -0
  222. package/dist/src/transport/session-handler.js +134 -0
  223. package/dist/src/transport/session-handler.js.map +1 -0
  224. package/dist/src/transport/session-registry.d.ts +58 -0
  225. package/dist/src/transport/session-registry.js +91 -0
  226. package/dist/src/transport/session-registry.js.map +1 -0
  227. package/dist/src/transport/trust-store.d.ts +23 -0
  228. package/dist/src/transport/trust-store.js +33 -0
  229. package/dist/src/transport/trust-store.js.map +1 -0
  230. package/dist/src/transport/ws-adapter.d.ts +7 -0
  231. package/dist/src/transport/ws-adapter.js +16 -0
  232. package/dist/src/transport/ws-adapter.js.map +1 -0
  233. package/dist/src/version.d.ts +1 -0
  234. package/dist/src/version.js +13 -0
  235. package/dist/src/version.js.map +1 -0
  236. package/dist/src/workspace/browse-service.d.ts +10 -0
  237. package/dist/src/workspace/browse-service.js +97 -0
  238. package/dist/src/workspace/browse-service.js.map +1 -0
  239. package/dist/src/workspace/checkpoint-service.d.ts +21 -0
  240. package/dist/src/workspace/checkpoint-service.js +219 -0
  241. package/dist/src/workspace/checkpoint-service.js.map +1 -0
  242. package/dist/src/workspace/path-guard.d.ts +7 -0
  243. package/dist/src/workspace/path-guard.js +51 -0
  244. package/dist/src/workspace/path-guard.js.map +1 -0
  245. package/dist/src/workspace/workspace-service.d.ts +8 -0
  246. package/dist/src/workspace/workspace-service.js +111 -0
  247. package/dist/src/workspace/workspace-service.js.map +1 -0
  248. package/package.json +46 -0
  249. package/scripts/extract-gemini-hook.mjs +16 -0
  250. package/scripts/fake-approval-bridge.mjs +23 -0
  251. package/scripts/install-service-linux.sh +38 -0
  252. package/scripts/install-service-macos.sh +38 -0
  253. package/scripts/install-service-windows.ps1 +26 -0
  254. package/scripts/test-gemini-hook-e2e.mjs +168 -0
  255. package/scripts/write-gemini-settings.mjs +31 -0
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Drives a single mobile connection: runs the E2EE handshake, then decrypts
3
+ * inbound envelopes, dispatches the JSON-RPC request through the router, and
4
+ * returns the encrypted response.
5
+ *
6
+ * Transport-agnostic: works over any {@link MessageIO} (relay client or LAN
7
+ * server connection).
8
+ */
9
+ import { validateE2EEnvelope } from '@uxnan/shared';
10
+ import { queueFor } from './message-io.js';
11
+ import { performServerHandshake } from './server-handshake.js';
12
+ /**
13
+ * Handle a connection end-to-end. Resolves when the connection closes. Never
14
+ * throws — handshake/transport failures are logged and the channel is closed.
15
+ */
16
+ export async function handleSecureConnection(options) {
17
+ const { io, ctx, router, deviceState, trustStore, displayName } = options;
18
+ const queue = queueFor(io);
19
+ const send = (message) => io.send(Buffer.from(JSON.stringify(message), 'utf-8'));
20
+ let phoneDeviceId;
21
+ let sessionId;
22
+ let sink;
23
+ try {
24
+ const handshakeOptions = {
25
+ queue,
26
+ send,
27
+ deviceState,
28
+ trustStore,
29
+ displayName,
30
+ now: ctx.now,
31
+ // Build the channel over this phone's persistent outbound log so its seq
32
+ // continues across reconnects and outbound messages are retained for
33
+ // seq-based catch-up.
34
+ outboundLogFor: (id) => ctx.sessionRegistry.logFor(id),
35
+ ...(options.expectedSessionId !== undefined
36
+ ? { expectedSessionId: options.expectedSessionId }
37
+ : {}),
38
+ };
39
+ const result = await performServerHandshake(handshakeOptions);
40
+ phoneDeviceId = result.phoneDeviceId;
41
+ sessionId = result.sessionId;
42
+ // Mark this as the active session so a `notifications/register` from the phone
43
+ // (and later turn-end pushes) target the right relay session.
44
+ ctx.pushService.setActiveSession(result.sessionId);
45
+ // Catch-up (architecture/02a §5.9.2): replay every retained outbound message
46
+ // the phone hasn't applied yet (seq > resumeState.lastAppliedBridgeOutboundSeq),
47
+ // re-encrypted under the new session key, BEFORE registering the live sink so
48
+ // the replayed backlog precedes any new traffic and ordering is preserved.
49
+ const outboundLog = ctx.sessionRegistry.logFor(result.phoneDeviceId);
50
+ for (const entry of outboundLog.entriesAfter(result.lastAppliedBridgeOutboundSeq)) {
51
+ io.send(Buffer.from(JSON.stringify(result.channel.encryptReplay(entry.seq, entry.plaintext)), 'utf-8'));
52
+ }
53
+ // Register the encrypted sink synchronously (before any further await) so the
54
+ // bridge can push notifications immediately. Each live send is encrypted by
55
+ // the channel, which records it in the outbound log for future catch-up.
56
+ // Kept in a variable so the teardown below only fires when this connection
57
+ // is still the current one (a reconnecting phone may have superseded it).
58
+ sink = {
59
+ send: (message) => io.send(Buffer.from(JSON.stringify(result.channel.encrypt(toBytes(message))), 'utf-8')),
60
+ };
61
+ ctx.sessionRegistry.register(result.phoneDeviceId, sink);
62
+ const trusted = await trustStore.get(result.phoneDeviceId);
63
+ ctx.sessions.add({
64
+ deviceId: result.phoneDeviceId,
65
+ displayName: trusted?.displayName ?? result.phoneDeviceId,
66
+ connectedAt: ctx.now(),
67
+ lastSeen: ctx.now(),
68
+ });
69
+ ctx.logger.info(`phone session established (${result.mode}): ${result.phoneDeviceId}`);
70
+ for (;;) {
71
+ const raw = await queue.next();
72
+ const parsed = tryParse(raw);
73
+ const validation = validateE2EEnvelope(parsed);
74
+ if (!validation.valid)
75
+ continue;
76
+ let plaintext;
77
+ try {
78
+ plaintext = result.channel.decrypt(validation.data);
79
+ }
80
+ catch (err) {
81
+ ctx.logger.warn(`envelope rejected: ${errorMessage(err)}`);
82
+ continue;
83
+ }
84
+ const request = tryParse(plaintext);
85
+ // Tag the request with this connection's session identity so per-phone
86
+ // handlers (notifications/*) target the right session when several phones
87
+ // are connected concurrently.
88
+ const response = await router.dispatchRaw(request, {
89
+ sessionId: result.sessionId,
90
+ deviceId: result.phoneDeviceId,
91
+ });
92
+ send(result.channel.encrypt(Buffer.from(JSON.stringify(response), 'utf-8')));
93
+ }
94
+ }
95
+ catch (err) {
96
+ // queue closed (normal disconnect) or handshake failure.
97
+ if (phoneDeviceId === undefined) {
98
+ ctx.logger.warn(`handshake failed: ${errorMessage(err)}`);
99
+ }
100
+ }
101
+ finally {
102
+ if (phoneDeviceId !== undefined) {
103
+ // Only tear down the shared session state if THIS connection still owns
104
+ // it. A reconnecting phone (LAN/direct) may have opened a newer connection
105
+ // that already replaced our sink + active session while our old socket was
106
+ // still half-open; when our stale connection finally closes, clobbering
107
+ // the newer one's sink/session/active-id would silently kill its push
108
+ // delivery. `unregister` returns false when we were superseded.
109
+ const stillCurrent = sink === undefined || ctx.sessionRegistry.unregister(phoneDeviceId, sink);
110
+ if (stillCurrent) {
111
+ ctx.sessions.remove(phoneDeviceId);
112
+ if (sessionId !== undefined)
113
+ ctx.pushService.clearActiveSession(sessionId);
114
+ }
115
+ ctx.logger.info(`phone session closed: ${phoneDeviceId}`);
116
+ }
117
+ io.close();
118
+ }
119
+ }
120
+ function toBytes(message) {
121
+ return Buffer.from(JSON.stringify(message), 'utf-8');
122
+ }
123
+ function tryParse(bytes) {
124
+ try {
125
+ return JSON.parse(bytes.toString('utf-8'));
126
+ }
127
+ catch {
128
+ return null;
129
+ }
130
+ }
131
+ function errorMessage(err) {
132
+ return err instanceof Error ? err.message : String(err);
133
+ }
134
+ //# sourceMappingURL=session-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-handler.js","sourceRoot":"","sources":["../../../src/transport/session-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Tracks the live encrypted sink for each connected phone so the bridge can push
3
+ * JSON-RPC notifications (e.g. streamed agent events) to it, and owns each
4
+ * phone's persistent {@link OutboundLog} (seq counter + catch-up window).
5
+ *
6
+ * The log outlives any single connection: it is created on first use and kept
7
+ * across reconnects (so a returning phone can be caught up via seq-based
8
+ * replay), and is dropped only when the device is untrusted ({@link forget}).
9
+ * Messages sent to a device with no active sink are recorded in its log (and
10
+ * thus replayed on reconnect) instead of being lost.
11
+ */
12
+ import { OutboundLog } from './outbound-log.js';
13
+ /** Encrypts a JSON-RPC message and writes it to the active connection. */
14
+ export interface SessionSink {
15
+ send(message: unknown): void;
16
+ }
17
+ export declare class SessionRegistry {
18
+ #private;
19
+ /**
20
+ * The per-device outbound log, created on first use. The session handler
21
+ * builds the secure channel over this so its `seq` continues across
22
+ * reconnects and every outbound message is retained for catch-up.
23
+ */
24
+ logFor(deviceId: string): OutboundLog;
25
+ /** Register the active sink for a device. */
26
+ register(deviceId: string, sink: SessionSink): void;
27
+ /**
28
+ * Drop the live sink (e.g. on disconnect) but KEEP the outbound log so the
29
+ * phone can be caught up when it reconnects.
30
+ *
31
+ * Only removes [sink] if it is still the device's CURRENT sink. A returning
32
+ * phone (LAN/direct) can open a new connection — whose handshake calls
33
+ * {@link register} and overwrites the sink — while the old connection's socket
34
+ * is still half-open; when that stale connection finally tears down, it must
35
+ * not delete the newer connection's live sink. Returns `true` when this sink
36
+ * was the current one (i.e. the caller still owned the session), `false` when
37
+ * a newer connection had already superseded it.
38
+ */
39
+ unregister(deviceId: string, sink: SessionSink): boolean;
40
+ /**
41
+ * Forget a device entirely — drop its sink AND its outbound log. Used when the
42
+ * device is untrusted/unpaired: there is nothing to catch up anymore.
43
+ */
44
+ forget(deviceId: string): void;
45
+ /**
46
+ * Send a message to every known device: delivered live where a sink is
47
+ * connected, recorded in the outbound log otherwise (so it replays on
48
+ * reconnect). Each device keeps its own seq.
49
+ */
50
+ broadcast(message: unknown): void;
51
+ isActive(deviceId: string): boolean;
52
+ /**
53
+ * Deliver a message to a device. Returns `true` if sent live (the sink
54
+ * encrypts it, which records it in the log), `false` if the device is offline
55
+ * and the message was recorded in the log for later replay.
56
+ */
57
+ notify(deviceId: string, message: unknown): boolean;
58
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Tracks the live encrypted sink for each connected phone so the bridge can push
3
+ * JSON-RPC notifications (e.g. streamed agent events) to it, and owns each
4
+ * phone's persistent {@link OutboundLog} (seq counter + catch-up window).
5
+ *
6
+ * The log outlives any single connection: it is created on first use and kept
7
+ * across reconnects (so a returning phone can be caught up via seq-based
8
+ * replay), and is dropped only when the device is untrusted ({@link forget}).
9
+ * Messages sent to a device with no active sink are recorded in its log (and
10
+ * thus replayed on reconnect) instead of being lost.
11
+ */
12
+ import { OutboundLog } from './outbound-log.js';
13
+ export class SessionRegistry {
14
+ #sinks = new Map();
15
+ #logs = new Map();
16
+ /**
17
+ * The per-device outbound log, created on first use. The session handler
18
+ * builds the secure channel over this so its `seq` continues across
19
+ * reconnects and every outbound message is retained for catch-up.
20
+ */
21
+ logFor(deviceId) {
22
+ let log = this.#logs.get(deviceId);
23
+ if (!log) {
24
+ log = new OutboundLog();
25
+ this.#logs.set(deviceId, log);
26
+ }
27
+ return log;
28
+ }
29
+ /** Register the active sink for a device. */
30
+ register(deviceId, sink) {
31
+ this.#sinks.set(deviceId, sink);
32
+ // Ensure a log exists so subsequent broadcasts/notifies are retained even if
33
+ // the handshake path didn't pre-create one.
34
+ this.logFor(deviceId);
35
+ }
36
+ /**
37
+ * Drop the live sink (e.g. on disconnect) but KEEP the outbound log so the
38
+ * phone can be caught up when it reconnects.
39
+ *
40
+ * Only removes [sink] if it is still the device's CURRENT sink. A returning
41
+ * phone (LAN/direct) can open a new connection — whose handshake calls
42
+ * {@link register} and overwrites the sink — while the old connection's socket
43
+ * is still half-open; when that stale connection finally tears down, it must
44
+ * not delete the newer connection's live sink. Returns `true` when this sink
45
+ * was the current one (i.e. the caller still owned the session), `false` when
46
+ * a newer connection had already superseded it.
47
+ */
48
+ unregister(deviceId, sink) {
49
+ if (this.#sinks.get(deviceId) !== sink)
50
+ return false;
51
+ this.#sinks.delete(deviceId);
52
+ return true;
53
+ }
54
+ /**
55
+ * Forget a device entirely — drop its sink AND its outbound log. Used when the
56
+ * device is untrusted/unpaired: there is nothing to catch up anymore.
57
+ */
58
+ forget(deviceId) {
59
+ this.#sinks.delete(deviceId);
60
+ this.#logs.delete(deviceId);
61
+ }
62
+ /**
63
+ * Send a message to every known device: delivered live where a sink is
64
+ * connected, recorded in the outbound log otherwise (so it replays on
65
+ * reconnect). Each device keeps its own seq.
66
+ */
67
+ broadcast(message) {
68
+ for (const deviceId of this.#logs.keys()) {
69
+ this.notify(deviceId, message);
70
+ }
71
+ }
72
+ isActive(deviceId) {
73
+ return this.#sinks.has(deviceId);
74
+ }
75
+ /**
76
+ * Deliver a message to a device. Returns `true` if sent live (the sink
77
+ * encrypts it, which records it in the log), `false` if the device is offline
78
+ * and the message was recorded in the log for later replay.
79
+ */
80
+ notify(deviceId, message) {
81
+ const sink = this.#sinks.get(deviceId);
82
+ if (sink) {
83
+ sink.send(message);
84
+ return true;
85
+ }
86
+ // Offline: record the plaintext so a reconnecting phone replays it (seq > N).
87
+ this.logFor(deviceId).record(Buffer.from(JSON.stringify(message), 'utf-8'));
88
+ return false;
89
+ }
90
+ }
91
+ //# sourceMappingURL=session-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-registry.js","sourceRoot":"","sources":["../../../src/transport/session-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAOhD,MAAM,OAAO,eAAe;IACjB,MAAM,GAAG,IAAI,GAAG,EAAuB,CAAC;IACxC,KAAK,GAAG,IAAI,GAAG,EAAuB,CAAC;IAEhD;;;;OAIG;IACH,MAAM,CAAC,QAAgB;QACrB,IAAI,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,GAAG,GAAG,IAAI,WAAW,EAAE,CAAC;YACxB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,6CAA6C;IAC7C,QAAQ,CAAC,QAAgB,EAAE,IAAiB;QAC1C,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QAChC,6EAA6E;QAC7E,4CAA4C;QAC5C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACxB,CAAC;IAED;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,QAAgB,EAAE,IAAiB;QAC5C,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QACrD,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,QAAgB;QACrB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC7B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC9B,CAAC;IAED;;;;OAIG;IACH,SAAS,CAAC,OAAgB;QACxB,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;YACzC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IAED,QAAQ,CAAC,QAAgB;QACvB,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACnC,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,QAAgB,EAAE,OAAgB;QACvC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACvC,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACnB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,8EAA8E;QAC9E,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;QAC5E,OAAO,KAAK,CAAC;IACf,CAAC;CACF"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Persistence of trusted phones (`~/.uxnan/trusted-phones.json`).
3
+ *
4
+ * Stores only non-secret data: the phone's Ed25519 identity public key and
5
+ * metadata. Used to authenticate trusted reconnects and to back the
6
+ * `bridge/trustedDevices` handler.
7
+ */
8
+ import type { TrustedDevice } from '@uxnan/shared';
9
+ import { type DaemonState } from '../daemon-state.js';
10
+ export interface TrustStore {
11
+ list(): Promise<TrustedDevice[]>;
12
+ get(deviceId: string): Promise<TrustedDevice | null>;
13
+ upsert(device: TrustedDevice): Promise<void>;
14
+ remove(deviceId: string): Promise<boolean>;
15
+ }
16
+ export declare class FileTrustStore implements TrustStore {
17
+ #private;
18
+ constructor(state: DaemonState);
19
+ list(): Promise<TrustedDevice[]>;
20
+ get(deviceId: string): Promise<TrustedDevice | null>;
21
+ upsert(device: TrustedDevice): Promise<void>;
22
+ remove(deviceId: string): Promise<boolean>;
23
+ }
@@ -0,0 +1,33 @@
1
+ import { DAEMON_FILES } from '../daemon-state.js';
2
+ export class FileTrustStore {
3
+ #state;
4
+ constructor(state) {
5
+ this.#state = state;
6
+ }
7
+ async list() {
8
+ return (await this.#state.readJson(DAEMON_FILES.trustedPhones)) ?? [];
9
+ }
10
+ async get(deviceId) {
11
+ return (await this.list()).find((d) => d.deviceId === deviceId) ?? null;
12
+ }
13
+ async upsert(device) {
14
+ const all = await this.list();
15
+ const index = all.findIndex((d) => d.deviceId === device.deviceId);
16
+ if (index >= 0) {
17
+ all[index] = device;
18
+ }
19
+ else {
20
+ all.push(device);
21
+ }
22
+ await this.#state.writeJson(DAEMON_FILES.trustedPhones, all);
23
+ }
24
+ async remove(deviceId) {
25
+ const all = await this.list();
26
+ const next = all.filter((d) => d.deviceId !== deviceId);
27
+ if (next.length === all.length)
28
+ return false;
29
+ await this.#state.writeJson(DAEMON_FILES.trustedPhones, next);
30
+ return true;
31
+ }
32
+ }
33
+ //# sourceMappingURL=trust-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trust-store.js","sourceRoot":"","sources":["../../../src/transport/trust-store.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,YAAY,EAAoB,MAAM,oBAAoB,CAAC;AASpE,MAAM,OAAO,cAAc;IAChB,MAAM,CAAc;IAE7B,YAAY,KAAkB;QAC5B,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,IAAI;QACR,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAkB,YAAY,CAAC,aAAa,CAAC,CAAC,IAAI,EAAE,CAAC;IACzF,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,QAAgB;QACxB,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,IAAI,IAAI,CAAC;IAC1E,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,MAAqB;QAChC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ,CAAC,CAAC;QACnE,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;YACf,GAAG,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC;QACtB,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACnB,CAAC;QACD,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,YAAY,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;IAC/D,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,QAAgB;QAC3B,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC;QACxD,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAC7C,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,YAAY,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;QAC9D,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Adapts a `ws` WebSocket to the {@link MessageIO} interface.
3
+ */
4
+ import type { RawData, WebSocket } from 'ws';
5
+ import type { MessageIO } from './message-io.js';
6
+ export declare function rawDataToBuffer(data: RawData): Buffer;
7
+ export declare function wsToMessageIO(ws: WebSocket): MessageIO;
@@ -0,0 +1,16 @@
1
+ export function rawDataToBuffer(data) {
2
+ if (Buffer.isBuffer(data))
3
+ return data;
4
+ if (Array.isArray(data))
5
+ return Buffer.concat(data);
6
+ return Buffer.from(data);
7
+ }
8
+ export function wsToMessageIO(ws) {
9
+ return {
10
+ send: (bytes) => ws.send(bytes, { binary: true }),
11
+ onMessage: (listener) => ws.on('message', (data) => listener(rawDataToBuffer(data))),
12
+ onClose: (listener) => ws.on('close', () => listener()),
13
+ close: () => ws.close(),
14
+ };
15
+ }
16
+ //# sourceMappingURL=ws-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-adapter.js","sourceRoot":"","sources":["../../../src/transport/ws-adapter.ts"],"names":[],"mappings":"AAMA,MAAM,UAAU,eAAe,CAAC,IAAa;IAC3C,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACvC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACpD,OAAO,MAAM,CAAC,IAAI,CAAC,IAAmB,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAAa;IACzC,OAAO;QACL,IAAI,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;QACjD,SAAS,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,IAAa,EAAE,EAAE,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7F,OAAO,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;QACvD,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE;KACxB,CAAC;AACJ,CAAC"}
@@ -0,0 +1 @@
1
+ export declare const BRIDGE_VERSION: string;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Bridge package version, read from package.json at runtime so the CLI's
3
+ * reported version can never drift from the published package. Works both
4
+ * from the compiled tree (dist/src/version.js → ../../package.json) and from
5
+ * a published npm install (node_modules/uxnan-bridge/package.json).
6
+ */
7
+ import { readFileSync } from 'node:fs';
8
+ import { dirname, join } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json');
11
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
12
+ export const BRIDGE_VERSION = pkg.version;
13
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/version.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,oBAAoB,CAAC,CAAC;AACpF,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAwB,CAAC;AAE7E,MAAM,CAAC,MAAM,cAAc,GAAW,GAAG,CAAC,OAAO,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { type BrowseResult, type BrowseRoot } from '@uxnan/shared';
2
+ /** Stable id derived from the absolute path, so it survives restarts. */
3
+ export declare function browseRootIdFor(cwd: string): string;
4
+ export declare class BrowseService {
5
+ #private;
6
+ constructor(roots: string[], fallback?: string);
7
+ listRoots(): BrowseRoot[];
8
+ /** Browse `path` (relative to `rootId`, default the first root and its top). */
9
+ browse(rootId?: string, path?: string): Promise<BrowseResult>;
10
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Plug-and-play directory browsing: lets the phone navigate sub-directories under
3
+ * a configured base root (e.g. the user's `Documents`), mark which ones are git
4
+ * repos, and pick ANY directory as a thread's cwd — without per-project
5
+ * pre-configuration and without ever escaping above the root.
6
+ *
7
+ * The confinement here applies to the phone-facing browse/workspace API (it
8
+ * reuses {@link resolveWithinRoot}, which rejects `..`/absolute escapes). Note it
9
+ * does NOT sandbox the agent process itself: once a directory is chosen as a
10
+ * thread cwd, the agent CLI runs there and can act on that subtree (writes are
11
+ * bounded by each agent's sandbox posture). See bridge/FOR-HUMAN.md.
12
+ *
13
+ * Source: architecture/02a-system-architecture.md §5.8.5 (project resolution),
14
+ * extended for plug-and-play browsing.
15
+ */
16
+ import { existsSync } from 'node:fs';
17
+ import { readdir } from 'node:fs/promises';
18
+ import { createHash } from 'node:crypto';
19
+ import { basename, join, relative, resolve, sep } from 'node:path';
20
+ import { JsonRpcErrorCode, RpcError } from '@uxnan/shared';
21
+ import { isSensitiveName, resolveWithinRoot } from './path-guard.js';
22
+ /** Stable id derived from the absolute path, so it survives restarts. */
23
+ export function browseRootIdFor(cwd) {
24
+ return `root_${createHash('sha1').update(resolve(cwd)).digest('hex').slice(0, 12)}`;
25
+ }
26
+ export class BrowseService {
27
+ #roots;
28
+ // Defaults the root to the bridge's launch directory (`process.cwd()`) so a
29
+ // zero-config install is plug-and-play: start the bridge inside the folder you
30
+ // want the phone to reach, and that folder (plus its sub-directories) is the
31
+ // root — no `browseRoots`/`workspaceRoots` to configure. Matches ProjectRegistry.
32
+ constructor(roots, fallback = process.cwd()) {
33
+ const resolved = [...new Set(roots.map((r) => resolve(r)).filter((r) => r.length > 0))];
34
+ this.#roots = resolved.length > 0 ? resolved : [resolve(fallback)];
35
+ }
36
+ listRoots() {
37
+ return this.#roots.map((cwd) => ({
38
+ id: browseRootIdFor(cwd),
39
+ name: basename(cwd) || cwd,
40
+ cwd,
41
+ }));
42
+ }
43
+ /** Browse `path` (relative to `rootId`, default the first root and its top). */
44
+ async browse(rootId, path) {
45
+ const root = this.#resolveRoot(rootId);
46
+ const rel = (path ?? '').replace(/^[/\\]+/, '');
47
+ const abs = resolveWithinRoot(root, rel === '' ? '.' : rel);
48
+ let dirents;
49
+ try {
50
+ dirents = await readdir(abs, { withFileTypes: true });
51
+ }
52
+ catch {
53
+ throw new RpcError(JsonRpcErrorCode.WorkspaceAccessDenied, 'directory not accessible');
54
+ }
55
+ const dirs = dirents
56
+ .filter((d) => d.isDirectory() && d.name !== '.git' && !isSensitiveName(d.name))
57
+ .map((d) => {
58
+ const childAbs = join(abs, d.name);
59
+ return { name: d.name, path: toRel(root, childAbs), isGitRepo: isGitRepo(childAbs) };
60
+ })
61
+ .sort((a, b) => a.name.localeCompare(b.name));
62
+ const currentPath = toRel(root, abs);
63
+ return {
64
+ roots: this.listRoots(),
65
+ rootId: browseRootIdFor(root),
66
+ path: currentPath,
67
+ parent: parentOf(currentPath),
68
+ cwd: abs,
69
+ isGitRepo: isGitRepo(abs),
70
+ dirs,
71
+ };
72
+ }
73
+ #resolveRoot(rootId) {
74
+ if (!rootId)
75
+ return this.#roots[0];
76
+ const match = this.#roots.find((r) => browseRootIdFor(r) === rootId);
77
+ if (!match) {
78
+ throw new RpcError(JsonRpcErrorCode.ResourceNotFound, `unknown browse root: ${rootId}`);
79
+ }
80
+ return match;
81
+ }
82
+ }
83
+ function isGitRepo(dir) {
84
+ return existsSync(join(dir, '.git'));
85
+ }
86
+ /** Path of `abs` relative to `root`, POSIX separators (`''` when `abs` IS root). */
87
+ function toRel(root, abs) {
88
+ return relative(resolve(root), abs).split(sep).join('/');
89
+ }
90
+ /** Parent of a root-relative POSIX path, or null at the root. */
91
+ function parentOf(relPosix) {
92
+ if (relPosix === '')
93
+ return null;
94
+ const idx = relPosix.lastIndexOf('/');
95
+ return idx === -1 ? '' : relPosix.slice(0, idx);
96
+ }
97
+ //# sourceMappingURL=browse-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browse-service.js","sourceRoot":"","sources":["../../../src/workspace/browse-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { Checkpoint, CheckpointDiff } from '@uxnan/shared';
2
+ import { type DaemonState } from '../daemon-state.js';
3
+ export interface CaptureOptions {
4
+ threadId?: string;
5
+ label?: string;
6
+ now: number;
7
+ }
8
+ /** Bounds on how many checkpoints are retained (per project / by age). */
9
+ export interface CheckpointRetention {
10
+ /** Keep at most N newest checkpoints per project `cwd` (0 = unlimited). */
11
+ maxPerProject: number;
12
+ /** Delete checkpoints older than N days (0 = no TTL). */
13
+ ttlDays: number;
14
+ }
15
+ export declare class CheckpointService {
16
+ #private;
17
+ constructor(state: DaemonState, retention?: Partial<CheckpointRetention>);
18
+ capture(cwd: string, options: CaptureOptions): Promise<Checkpoint>;
19
+ diff(id: string): Promise<CheckpointDiff>;
20
+ apply(id: string): Promise<void>;
21
+ }