uxnan-bridge 0.0.1-alpha.20260621

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/README.md +150 -0
  2. package/dist/src/account-status.d.ts +13 -0
  3. package/dist/src/account-status.js +78 -0
  4. package/dist/src/account-status.js.map +1 -0
  5. package/dist/src/adapters/base-adapter.d.ts +18 -0
  6. package/dist/src/adapters/base-adapter.js +15 -0
  7. package/dist/src/adapters/base-adapter.js.map +1 -0
  8. package/dist/src/adapters/claude-adapter.d.ts +102 -0
  9. package/dist/src/adapters/claude-adapter.js +486 -0
  10. package/dist/src/adapters/claude-adapter.js.map +1 -0
  11. package/dist/src/adapters/claude-tools.d.ts +25 -0
  12. package/dist/src/adapters/claude-tools.js +146 -0
  13. package/dist/src/adapters/claude-tools.js.map +1 -0
  14. package/dist/src/adapters/codex-adapter.d.ts +116 -0
  15. package/dist/src/adapters/codex-adapter.js +912 -0
  16. package/dist/src/adapters/codex-adapter.js.map +1 -0
  17. package/dist/src/adapters/codex-app-server.d.ts +74 -0
  18. package/dist/src/adapters/codex-app-server.js +225 -0
  19. package/dist/src/adapters/codex-app-server.js.map +1 -0
  20. package/dist/src/adapters/codex-approval.d.ts +88 -0
  21. package/dist/src/adapters/codex-approval.js +160 -0
  22. package/dist/src/adapters/codex-approval.js.map +1 -0
  23. package/dist/src/adapters/codex-tools.d.ts +18 -0
  24. package/dist/src/adapters/codex-tools.js +106 -0
  25. package/dist/src/adapters/codex-tools.js.map +1 -0
  26. package/dist/src/adapters/content-blocks.d.ts +68 -0
  27. package/dist/src/adapters/content-blocks.js +205 -0
  28. package/dist/src/adapters/content-blocks.js.map +1 -0
  29. package/dist/src/adapters/echo-agent-adapter.d.ts +23 -0
  30. package/dist/src/adapters/echo-agent-adapter.js +72 -0
  31. package/dist/src/adapters/echo-agent-adapter.js.map +1 -0
  32. package/dist/src/adapters/gemini-adapter.d.ts +87 -0
  33. package/dist/src/adapters/gemini-adapter.js +594 -0
  34. package/dist/src/adapters/gemini-adapter.js.map +1 -0
  35. package/dist/src/adapters/gemini-tools.d.ts +4 -0
  36. package/dist/src/adapters/gemini-tools.js +48 -0
  37. package/dist/src/adapters/gemini-tools.js.map +1 -0
  38. package/dist/src/adapters/opencode-adapter.d.ts +74 -0
  39. package/dist/src/adapters/opencode-adapter.js +418 -0
  40. package/dist/src/adapters/opencode-adapter.js.map +1 -0
  41. package/dist/src/adapters/opencode-tools.d.ts +2 -0
  42. package/dist/src/adapters/opencode-tools.js +41 -0
  43. package/dist/src/adapters/opencode-tools.js.map +1 -0
  44. package/dist/src/adapters/pi-adapter.d.ts +92 -0
  45. package/dist/src/adapters/pi-adapter.js +467 -0
  46. package/dist/src/adapters/pi-adapter.js.map +1 -0
  47. package/dist/src/adapters/pi-tools.d.ts +10 -0
  48. package/dist/src/adapters/pi-tools.js +72 -0
  49. package/dist/src/adapters/pi-tools.js.map +1 -0
  50. package/dist/src/adapters/process-agent-adapter.d.ts +24 -0
  51. package/dist/src/adapters/process-agent-adapter.js +111 -0
  52. package/dist/src/adapters/process-agent-adapter.js.map +1 -0
  53. package/dist/src/adapters/resolve-claude.d.ts +13 -0
  54. package/dist/src/adapters/resolve-claude.js +57 -0
  55. package/dist/src/adapters/resolve-claude.js.map +1 -0
  56. package/dist/src/adapters/resolve-codex.d.ts +13 -0
  57. package/dist/src/adapters/resolve-codex.js +48 -0
  58. package/dist/src/adapters/resolve-codex.js.map +1 -0
  59. package/dist/src/adapters/resolve-gemini.d.ts +13 -0
  60. package/dist/src/adapters/resolve-gemini.js +47 -0
  61. package/dist/src/adapters/resolve-gemini.js.map +1 -0
  62. package/dist/src/adapters/resolve-opencode.d.ts +11 -0
  63. package/dist/src/adapters/resolve-opencode.js +49 -0
  64. package/dist/src/adapters/resolve-opencode.js.map +1 -0
  65. package/dist/src/adapters/resolve-pi.d.ts +13 -0
  66. package/dist/src/adapters/resolve-pi.js +46 -0
  67. package/dist/src/adapters/resolve-pi.js.map +1 -0
  68. package/dist/src/adapters/run-options.d.ts +22 -0
  69. package/dist/src/adapters/run-options.js +48 -0
  70. package/dist/src/adapters/run-options.js.map +1 -0
  71. package/dist/src/adapters/spawn.d.ts +20 -0
  72. package/dist/src/adapters/spawn.js +16 -0
  73. package/dist/src/adapters/spawn.js.map +1 -0
  74. package/dist/src/agents/agent-manager.d.ts +98 -0
  75. package/dist/src/agents/agent-manager.js +433 -0
  76. package/dist/src/agents/agent-manager.js.map +1 -0
  77. package/dist/src/agents/attachments.d.ts +28 -0
  78. package/dist/src/agents/attachments.js +121 -0
  79. package/dist/src/agents/attachments.js.map +1 -0
  80. package/dist/src/bridge-context.d.ts +45 -0
  81. package/dist/src/bridge-context.js +2 -0
  82. package/dist/src/bridge-context.js.map +1 -0
  83. package/dist/src/bridge-status.d.ts +12 -0
  84. package/dist/src/bridge-status.js +17 -0
  85. package/dist/src/bridge-status.js.map +1 -0
  86. package/dist/src/bridge.d.ts +37 -0
  87. package/dist/src/bridge.js +446 -0
  88. package/dist/src/bridge.js.map +1 -0
  89. package/dist/src/cli.d.ts +2 -0
  90. package/dist/src/cli.js +194 -0
  91. package/dist/src/cli.js.map +1 -0
  92. package/dist/src/conversation/session-history.d.ts +27 -0
  93. package/dist/src/conversation/session-history.js +1082 -0
  94. package/dist/src/conversation/session-history.js.map +1 -0
  95. package/dist/src/conversation/thread-store.d.ts +74 -0
  96. package/dist/src/conversation/thread-store.js +366 -0
  97. package/dist/src/conversation/thread-store.js.map +1 -0
  98. package/dist/src/daemon-config.d.ts +123 -0
  99. package/dist/src/daemon-config.js +64 -0
  100. package/dist/src/daemon-config.js.map +1 -0
  101. package/dist/src/daemon-state.d.ts +27 -0
  102. package/dist/src/daemon-state.js +76 -0
  103. package/dist/src/daemon-state.js.map +1 -0
  104. package/dist/src/git/git-runner.d.ts +24 -0
  105. package/dist/src/git/git-runner.js +63 -0
  106. package/dist/src/git/git-runner.js.map +1 -0
  107. package/dist/src/git/git-service.d.ts +76 -0
  108. package/dist/src/git/git-service.js +435 -0
  109. package/dist/src/git/git-service.js.map +1 -0
  110. package/dist/src/handler-router.d.ts +34 -0
  111. package/dist/src/handler-router.js +67 -0
  112. package/dist/src/handler-router.js.map +1 -0
  113. package/dist/src/handlers/account-handler.d.ts +4 -0
  114. package/dist/src/handlers/account-handler.js +27 -0
  115. package/dist/src/handlers/account-handler.js.map +1 -0
  116. package/dist/src/handlers/agent-handler.d.ts +2 -0
  117. package/dist/src/handlers/agent-handler.js +8 -0
  118. package/dist/src/handlers/agent-handler.js.map +1 -0
  119. package/dist/src/handlers/bridge-control-handler.d.ts +2 -0
  120. package/dist/src/handlers/bridge-control-handler.js +64 -0
  121. package/dist/src/handlers/bridge-control-handler.js.map +1 -0
  122. package/dist/src/handlers/desktop-handler.d.ts +12 -0
  123. package/dist/src/handlers/desktop-handler.js +5 -0
  124. package/dist/src/handlers/desktop-handler.js.map +1 -0
  125. package/dist/src/handlers/git-handler.d.ts +2 -0
  126. package/dist/src/handlers/git-handler.js +82 -0
  127. package/dist/src/handlers/git-handler.js.map +1 -0
  128. package/dist/src/handlers/index.d.ts +8 -0
  129. package/dist/src/handlers/index.js +22 -0
  130. package/dist/src/handlers/index.js.map +1 -0
  131. package/dist/src/handlers/not-implemented.d.ts +10 -0
  132. package/dist/src/handlers/not-implemented.js +21 -0
  133. package/dist/src/handlers/not-implemented.js.map +1 -0
  134. package/dist/src/handlers/notifications-handler.d.ts +2 -0
  135. package/dist/src/handlers/notifications-handler.js +62 -0
  136. package/dist/src/handlers/notifications-handler.js.map +1 -0
  137. package/dist/src/handlers/params.d.ts +11 -0
  138. package/dist/src/handlers/params.js +72 -0
  139. package/dist/src/handlers/params.js.map +1 -0
  140. package/dist/src/handlers/project-handler.d.ts +2 -0
  141. package/dist/src/handlers/project-handler.js +6 -0
  142. package/dist/src/handlers/project-handler.js.map +1 -0
  143. package/dist/src/handlers/thread-context-handler.d.ts +2 -0
  144. package/dist/src/handlers/thread-context-handler.js +211 -0
  145. package/dist/src/handlers/thread-context-handler.js.map +1 -0
  146. package/dist/src/handlers/workspace-handler.d.ts +2 -0
  147. package/dist/src/handlers/workspace-handler.js +101 -0
  148. package/dist/src/handlers/workspace-handler.js.map +1 -0
  149. package/dist/src/hooks/claude-approval-hook.d.ts +7 -0
  150. package/dist/src/hooks/claude-approval-hook.js +95 -0
  151. package/dist/src/hooks/claude-approval-hook.js.map +1 -0
  152. package/dist/src/hooks/gemini-approval-hook.d.ts +7 -0
  153. package/dist/src/hooks/gemini-approval-hook.js +113 -0
  154. package/dist/src/hooks/gemini-approval-hook.js.map +1 -0
  155. package/dist/src/index.d.ts +62 -0
  156. package/dist/src/index.js +65 -0
  157. package/dist/src/index.js.map +1 -0
  158. package/dist/src/keyring-secret-store.d.ts +36 -0
  159. package/dist/src/keyring-secret-store.js +70 -0
  160. package/dist/src/keyring-secret-store.js.map +1 -0
  161. package/dist/src/lock-file.d.ts +18 -0
  162. package/dist/src/lock-file.js +60 -0
  163. package/dist/src/lock-file.js.map +1 -0
  164. package/dist/src/logger.d.ts +28 -0
  165. package/dist/src/logger.js +99 -0
  166. package/dist/src/logger.js.map +1 -0
  167. package/dist/src/pairing/pairing-code-service.d.ts +45 -0
  168. package/dist/src/pairing/pairing-code-service.js +183 -0
  169. package/dist/src/pairing/pairing-code-service.js.map +1 -0
  170. package/dist/src/projects/project-registry.d.ts +14 -0
  171. package/dist/src/projects/project-registry.js +60 -0
  172. package/dist/src/projects/project-registry.js.map +1 -0
  173. package/dist/src/push/push-sender.d.ts +21 -0
  174. package/dist/src/push/push-sender.js +96 -0
  175. package/dist/src/push/push-sender.js.map +1 -0
  176. package/dist/src/push/push-service.d.ts +122 -0
  177. package/dist/src/push/push-service.js +260 -0
  178. package/dist/src/push/push-service.js.map +1 -0
  179. package/dist/src/qr.d.ts +17 -0
  180. package/dist/src/qr.js +31 -0
  181. package/dist/src/qr.js.map +1 -0
  182. package/dist/src/secret-store.d.ts +23 -0
  183. package/dist/src/secret-store.js +27 -0
  184. package/dist/src/secret-store.js.map +1 -0
  185. package/dist/src/secure-device-state.d.ts +16 -0
  186. package/dist/src/secure-device-state.js +63 -0
  187. package/dist/src/secure-device-state.js.map +1 -0
  188. package/dist/src/service-installer.d.ts +57 -0
  189. package/dist/src/service-installer.js +254 -0
  190. package/dist/src/service-installer.js.map +1 -0
  191. package/dist/src/session-state.d.ts +14 -0
  192. package/dist/src/session-state.js +19 -0
  193. package/dist/src/session-state.js.map +1 -0
  194. package/dist/src/transport/crypto.d.ts +43 -0
  195. package/dist/src/transport/crypto.js +78 -0
  196. package/dist/src/transport/crypto.js.map +1 -0
  197. package/dist/src/transport/lan-server.d.ts +33 -0
  198. package/dist/src/transport/lan-server.js +105 -0
  199. package/dist/src/transport/lan-server.js.map +1 -0
  200. package/dist/src/transport/local-hosts.d.ts +17 -0
  201. package/dist/src/transport/local-hosts.js +30 -0
  202. package/dist/src/transport/local-hosts.js.map +1 -0
  203. package/dist/src/transport/mdns-advertiser.d.ts +83 -0
  204. package/dist/src/transport/mdns-advertiser.js +282 -0
  205. package/dist/src/transport/mdns-advertiser.js.map +1 -0
  206. package/dist/src/transport/message-io.d.ts +33 -0
  207. package/dist/src/transport/message-io.js +87 -0
  208. package/dist/src/transport/message-io.js.map +1 -0
  209. package/dist/src/transport/outbound-log.d.ts +24 -0
  210. package/dist/src/transport/outbound-log.js +78 -0
  211. package/dist/src/transport/outbound-log.js.map +1 -0
  212. package/dist/src/transport/relay-client.d.ts +19 -0
  213. package/dist/src/transport/relay-client.js +27 -0
  214. package/dist/src/transport/relay-client.js.map +1 -0
  215. package/dist/src/transport/secure-channel.d.ts +33 -0
  216. package/dist/src/transport/secure-channel.js +81 -0
  217. package/dist/src/transport/secure-channel.js.map +1 -0
  218. package/dist/src/transport/server-handshake.d.ts +49 -0
  219. package/dist/src/transport/server-handshake.js +137 -0
  220. package/dist/src/transport/server-handshake.js.map +1 -0
  221. package/dist/src/transport/session-handler.d.ts +19 -0
  222. package/dist/src/transport/session-handler.js +134 -0
  223. package/dist/src/transport/session-handler.js.map +1 -0
  224. package/dist/src/transport/session-registry.d.ts +58 -0
  225. package/dist/src/transport/session-registry.js +91 -0
  226. package/dist/src/transport/session-registry.js.map +1 -0
  227. package/dist/src/transport/trust-store.d.ts +23 -0
  228. package/dist/src/transport/trust-store.js +33 -0
  229. package/dist/src/transport/trust-store.js.map +1 -0
  230. package/dist/src/transport/ws-adapter.d.ts +7 -0
  231. package/dist/src/transport/ws-adapter.js +16 -0
  232. package/dist/src/transport/ws-adapter.js.map +1 -0
  233. package/dist/src/version.d.ts +1 -0
  234. package/dist/src/version.js +13 -0
  235. package/dist/src/version.js.map +1 -0
  236. package/dist/src/workspace/browse-service.d.ts +10 -0
  237. package/dist/src/workspace/browse-service.js +97 -0
  238. package/dist/src/workspace/browse-service.js.map +1 -0
  239. package/dist/src/workspace/checkpoint-service.d.ts +21 -0
  240. package/dist/src/workspace/checkpoint-service.js +219 -0
  241. package/dist/src/workspace/checkpoint-service.js.map +1 -0
  242. package/dist/src/workspace/path-guard.d.ts +7 -0
  243. package/dist/src/workspace/path-guard.js +51 -0
  244. package/dist/src/workspace/path-guard.js.map +1 -0
  245. package/dist/src/workspace/workspace-service.d.ts +8 -0
  246. package/dist/src/workspace/workspace-service.js +111 -0
  247. package/dist/src/workspace/workspace-service.js.map +1 -0
  248. package/package.json +46 -0
  249. package/scripts/extract-gemini-hook.mjs +16 -0
  250. package/scripts/fake-approval-bridge.mjs +23 -0
  251. package/scripts/install-service-linux.sh +38 -0
  252. package/scripts/install-service-macos.sh +38 -0
  253. package/scripts/install-service-windows.ps1 +26 -0
  254. package/scripts/test-gemini-hook-e2e.mjs +168 -0
  255. package/scripts/write-gemini-settings.mjs +31 -0
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Workspace checkpoints: capture / diff / apply a snapshot of the project's
3
+ * working tree, backed by git.
4
+ *
5
+ * A checkpoint is a full snapshot of the working tree (tracked changes AND
6
+ * untracked files) taken WITHOUT touching the user's index: a temporary
7
+ * `GIT_INDEX_FILE` is seeded from HEAD, `git add -A` stages the current tree
8
+ * into it, and `commit-tree` records a snapshot commit parented on HEAD. The
9
+ * commit is anchored under `refs/uxnan/checkpoints/<id>` so git won't GC it, and
10
+ * metadata is persisted in `~/.uxnan/checkpoints.json`.
11
+ *
12
+ * Source: architecture/02a-system-architecture.md §5.8.7.
13
+ *
14
+ * `applyCheckpoint` performs a TRUE worktree restore: it restores the snapshot's
15
+ * file contents (recreating deleted files, overwriting modified ones) AND deletes
16
+ * files created after the checkpoint, so the working tree matches the snapshot
17
+ * exactly (parity with the mobile `AiChangeSet` revert). It is worktree-only —
18
+ * the user's index and HEAD are untouched — and never removes gitignored files
19
+ * (they were excluded from the snapshot).
20
+ *
21
+ * Retention: on each `capture` the service prunes old checkpoints beyond a
22
+ * per-project count cap and/or a TTL, deleting both their `refs/uxnan/checkpoints/*`
23
+ * anchors and their `checkpoints.json` entries, so the set never grows unbounded.
24
+ *
25
+ * Limitation: checkpoint commits use a fixed internal identity (never pushed).
26
+ */
27
+ import { tmpdir } from 'node:os';
28
+ import { join } from 'node:path';
29
+ import { randomUUID } from 'node:crypto';
30
+ import { rm } from 'node:fs/promises';
31
+ import { JsonRpcErrorCode, RpcError } from '@uxnan/shared';
32
+ import { DAEMON_FILES } from '../daemon-state.js';
33
+ import { runGit } from '../git/git-runner.js';
34
+ const SNAPSHOT_ENV = {
35
+ GIT_AUTHOR_NAME: 'uxnan-bridge',
36
+ GIT_AUTHOR_EMAIL: 'bridge@uxnan.local',
37
+ GIT_COMMITTER_NAME: 'uxnan-bridge',
38
+ GIT_COMMITTER_EMAIL: 'bridge@uxnan.local',
39
+ };
40
+ const DEFAULT_RETENTION = { maxPerProject: 25, ttlDays: 0 };
41
+ const MS_PER_DAY = 86_400_000;
42
+ export class CheckpointService {
43
+ #state;
44
+ #retention;
45
+ constructor(state, retention = {}) {
46
+ this.#state = state;
47
+ this.#retention = { ...DEFAULT_RETENTION, ...retention };
48
+ }
49
+ async capture(cwd, options) {
50
+ const baseSha = (await runGit(cwd, ['rev-parse', 'HEAD'])).stdout.trim();
51
+ const id = randomUUID();
52
+ const indexFile = join(tmpdir(), `uxnan-ckpt-${id}.index`);
53
+ const env = { ...process.env, ...SNAPSHOT_ENV, GIT_INDEX_FILE: indexFile };
54
+ let commitSha;
55
+ try {
56
+ await runGit(cwd, ['read-tree', baseSha], { env });
57
+ await runGit(cwd, ['add', '-A'], { env });
58
+ const treeSha = (await runGit(cwd, ['write-tree'], { env })).stdout.trim();
59
+ const label = options.label ?? 'checkpoint';
60
+ commitSha = (await runGit(cwd, ['commit-tree', treeSha, '-p', baseSha, '-m', `uxnan-checkpoint ${label}`], {
61
+ env,
62
+ })).stdout.trim();
63
+ }
64
+ finally {
65
+ await rm(indexFile, { force: true });
66
+ }
67
+ await runGit(cwd, ['update-ref', `refs/uxnan/checkpoints/${id}`, commitSha]);
68
+ const record = {
69
+ id,
70
+ cwd,
71
+ baseSha,
72
+ commitSha,
73
+ createdAt: options.now,
74
+ ...(options.label !== undefined ? { label: options.label } : {}),
75
+ ...(options.threadId !== undefined ? { threadId: options.threadId } : {}),
76
+ };
77
+ const records = await this.#list();
78
+ records.push(record);
79
+ const survivors = await this.#prune(records, options.now);
80
+ await this.#state.writeJson(DAEMON_FILES.checkpoints, survivors);
81
+ return toCheckpoint(record);
82
+ }
83
+ async diff(id) {
84
+ const record = await this.#require(id);
85
+ const range = [record.baseSha, record.commitSha];
86
+ const { stdout: diff } = await runGit(record.cwd, ['diff', ...range]);
87
+ const { stdout: nameStatus } = await runGit(record.cwd, ['diff', '--name-status', ...range]);
88
+ const files = nameStatus
89
+ .split('\n')
90
+ .map((line) => line.trim())
91
+ .filter((line) => line.length > 0)
92
+ .map((line) => {
93
+ const parts = line.split('\t');
94
+ const code = parts[0] ?? '';
95
+ const path = parts[parts.length - 1] ?? '';
96
+ return { path, status: mapFileStatus(code) };
97
+ });
98
+ return { diff, files };
99
+ }
100
+ async apply(id) {
101
+ const record = await this.#require(id);
102
+ // Files that exist now but NOT in the snapshot — created after the checkpoint.
103
+ // Compute these BEFORE restoring (which only touches snapshot paths).
104
+ const extras = await this.#extrasOf(record);
105
+ // Restore snapshot contents: recreates deleted files, overwrites modified ones.
106
+ await runGit(record.cwd, ['restore', `--source=${record.commitSha}`, '--', '.']);
107
+ // Delete the extras so the worktree matches the snapshot exactly.
108
+ for (const rel of extras) {
109
+ await rm(join(record.cwd, rel), { force: true });
110
+ }
111
+ }
112
+ /**
113
+ * Paths present in the current worktree but absent from the checkpoint snapshot
114
+ * (the files to delete on restore). Snapshots the current tree into a temp index
115
+ * exactly like {@link capture} (HEAD + `add -A`, so .gitignore is respected and
116
+ * the user's real index is untouched), then diffs snapshot → now; `A` entries
117
+ * are the extras.
118
+ */
119
+ async #extrasOf(record) {
120
+ const headSha = (await runGit(record.cwd, ['rev-parse', 'HEAD'])).stdout.trim();
121
+ const indexFile = join(tmpdir(), `uxnan-ckpt-apply-${randomUUID()}.index`);
122
+ const env = { ...process.env, ...SNAPSHOT_ENV, GIT_INDEX_FILE: indexFile };
123
+ try {
124
+ await runGit(record.cwd, ['read-tree', headSha], { env });
125
+ await runGit(record.cwd, ['add', '-A'], { env });
126
+ const nowTree = (await runGit(record.cwd, ['write-tree'], { env })).stdout.trim();
127
+ const { stdout } = await runGit(record.cwd, [
128
+ 'diff',
129
+ '--name-status',
130
+ '--no-renames',
131
+ record.commitSha,
132
+ nowTree,
133
+ ]);
134
+ const extras = [];
135
+ for (const line of stdout.split('\n')) {
136
+ const trimmed = line.trim();
137
+ if (trimmed.length === 0)
138
+ continue;
139
+ const parts = trimmed.split('\t');
140
+ if ((parts[0] ?? '')[0] !== 'A')
141
+ continue;
142
+ const path = parts[parts.length - 1];
143
+ if (path)
144
+ extras.push(path);
145
+ }
146
+ return extras;
147
+ }
148
+ finally {
149
+ await rm(indexFile, { force: true });
150
+ }
151
+ }
152
+ /**
153
+ * Drop checkpoints beyond the retention bounds: those older than the TTL, and
154
+ * the oldest-over-cap per project. Deletes each pruned checkpoint's git ref
155
+ * (best-effort — the repo/ref may be gone) and returns the survivors to persist.
156
+ */
157
+ async #prune(records, now) {
158
+ const { maxPerProject, ttlDays } = this.#retention;
159
+ const ttlCutoff = ttlDays > 0 ? now - ttlDays * MS_PER_DAY : undefined;
160
+ const removeIds = new Set();
161
+ const byCwd = new Map();
162
+ for (const record of records) {
163
+ if (ttlCutoff !== undefined && record.createdAt < ttlCutoff) {
164
+ removeIds.add(record.id);
165
+ continue;
166
+ }
167
+ const list = byCwd.get(record.cwd) ?? [];
168
+ list.push(record);
169
+ byCwd.set(record.cwd, list);
170
+ }
171
+ if (maxPerProject > 0) {
172
+ for (const list of byCwd.values()) {
173
+ list.sort((a, b) => b.createdAt - a.createdAt); // newest first
174
+ for (let i = maxPerProject; i < list.length; i += 1)
175
+ removeIds.add(list[i].id);
176
+ }
177
+ }
178
+ if (removeIds.size === 0)
179
+ return records;
180
+ for (const record of records) {
181
+ if (!removeIds.has(record.id))
182
+ continue;
183
+ try {
184
+ await runGit(record.cwd, ['update-ref', '-d', `refs/uxnan/checkpoints/${record.id}`]);
185
+ }
186
+ catch {
187
+ // The ref or its repo is already gone — nothing to clean up.
188
+ }
189
+ }
190
+ return records.filter((record) => !removeIds.has(record.id));
191
+ }
192
+ async #list() {
193
+ return (await this.#state.readJson(DAEMON_FILES.checkpoints)) ?? [];
194
+ }
195
+ async #require(id) {
196
+ const record = (await this.#list()).find((r) => r.id === id);
197
+ if (!record) {
198
+ throw new RpcError(JsonRpcErrorCode.ResourceNotFound, `checkpoint not found: ${id}`);
199
+ }
200
+ return record;
201
+ }
202
+ }
203
+ function toCheckpoint(record) {
204
+ const checkpoint = { id: record.id, createdAt: record.createdAt };
205
+ if (record.label !== undefined)
206
+ checkpoint.label = record.label;
207
+ if (record.threadId !== undefined)
208
+ checkpoint.threadId = record.threadId;
209
+ return checkpoint;
210
+ }
211
+ function mapFileStatus(code) {
212
+ const c = code[0];
213
+ if (c === 'A')
214
+ return 'added';
215
+ if (c === 'D')
216
+ return 'deleted';
217
+ return 'modified';
218
+ }
219
+ //# sourceMappingURL=checkpoint-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoint-service.js","sourceRoot":"","sources":["../../../src/workspace/checkpoint-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export declare function isSensitiveName(name: string): boolean;
2
+ /**
3
+ * Resolve `relPath` against `root` and ensure the result stays inside `root`.
4
+ * Rejects traversal (`..`), absolute escapes, the `.git` directory and sensitive
5
+ * file names. Returns the absolute path.
6
+ */
7
+ export declare function resolveWithinRoot(root: string, relPath: string): string;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Path-traversal protection and sensitive-file filtering for workspace access.
3
+ *
4
+ * Source: architecture/02a-system-architecture.md §5.8.9 (sanitization). The
5
+ * bridge never serves files outside the project root, nor secrets (.env, keys,
6
+ * credentials) or the .git internals.
7
+ */
8
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
9
+ import { JsonRpcErrorCode, RpcError } from '@uxnan/shared';
10
+ const SENSITIVE_PATTERNS = [
11
+ /^\.env(\..+)?$/i,
12
+ /\.pem$/i,
13
+ /\.key$/i,
14
+ /\.p12$/i,
15
+ /\.pfx$/i,
16
+ /\.keystore$/i,
17
+ /^id_rsa/i,
18
+ /^id_ed25519/i,
19
+ /^id_ecdsa/i,
20
+ /^credentials\.json$/i,
21
+ /^\.npmrc$/i,
22
+ ];
23
+ export function isSensitiveName(name) {
24
+ return SENSITIVE_PATTERNS.some((pattern) => pattern.test(name));
25
+ }
26
+ function denied(message) {
27
+ return new RpcError(JsonRpcErrorCode.WorkspaceAccessDenied, message);
28
+ }
29
+ /**
30
+ * Resolve `relPath` against `root` and ensure the result stays inside `root`.
31
+ * Rejects traversal (`..`), absolute escapes, the `.git` directory and sensitive
32
+ * file names. Returns the absolute path.
33
+ */
34
+ export function resolveWithinRoot(root, relPath) {
35
+ const resolvedRoot = resolve(root);
36
+ const target = resolve(resolvedRoot, relPath);
37
+ const rel = relative(resolvedRoot, target);
38
+ if (rel.startsWith('..') || isAbsolute(rel)) {
39
+ throw denied('path escapes the project root');
40
+ }
41
+ const segments = rel.split(sep);
42
+ if (segments.includes('.git')) {
43
+ throw denied('access to the .git directory is not allowed');
44
+ }
45
+ const name = segments[segments.length - 1] ?? '';
46
+ if (isSensitiveName(name)) {
47
+ throw denied('access to a sensitive file is not allowed');
48
+ }
49
+ return target;
50
+ }
51
+ //# sourceMappingURL=path-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-guard.js","sourceRoot":"","sources":["../../../src/workspace/path-guard.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE3D,MAAM,kBAAkB,GAAa;IACnC,iBAAiB;IACjB,SAAS;IACT,SAAS;IACT,SAAS;IACT,SAAS;IACT,cAAc;IACd,UAAU;IACV,cAAc;IACd,YAAY;IACZ,sBAAsB;IACtB,YAAY;CACb,CAAC;AAEF,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAClE,CAAC;AAED,SAAS,MAAM,CAAC,OAAe;IAC7B,OAAO,IAAI,QAAQ,CAAC,gBAAgB,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAC;AACvE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,OAAe;IAC7D,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IAC9C,MAAM,GAAG,GAAG,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IAE3C,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5C,MAAM,MAAM,CAAC,+BAA+B,CAAC,CAAC;IAChD,CAAC;IACD,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9B,MAAM,MAAM,CAAC,6CAA6C,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACjD,IAAI,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1B,MAAM,MAAM,CAAC,2CAA2C,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { ApplyResult, FileContent, ImageContent, PatchChange, WorkspaceListing } from '@uxnan/shared';
2
+ export declare class WorkspaceService {
3
+ #private;
4
+ readFile(root: string, relPath: string): Promise<FileContent>;
5
+ readImage(root: string, relPath: string): Promise<ImageContent>;
6
+ list(root: string): Promise<WorkspaceListing>;
7
+ applyPatch(root: string, changes: PatchChange[]): Promise<ApplyResult>;
8
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Workspace file operations, confined to the project root and stripped of
3
+ * sensitive files (see {@link resolveWithinRoot}). Paths returned to the phone
4
+ * are relative to the project root, never absolute.
5
+ *
6
+ * Source: architecture/02a-system-architecture.md §5.8.7 / §5.8.9.
7
+ */
8
+ import { readFile, readdir, stat, mkdir, writeFile, rm } from 'node:fs/promises';
9
+ import { dirname, extname, relative, resolve } from 'node:path';
10
+ import { JsonRpcErrorCode, RpcError } from '@uxnan/shared';
11
+ import { isSensitiveName, resolveWithinRoot } from './path-guard.js';
12
+ const MAX_FILE_BYTES = 5 * 1024 * 1024;
13
+ const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
14
+ const IMAGE_MIME = {
15
+ '.png': 'image/png',
16
+ '.jpg': 'image/jpeg',
17
+ '.jpeg': 'image/jpeg',
18
+ '.gif': 'image/gif',
19
+ '.webp': 'image/webp',
20
+ '.bmp': 'image/bmp',
21
+ '.svg': 'image/svg+xml',
22
+ };
23
+ export class WorkspaceService {
24
+ async readFile(root, relPath) {
25
+ const abs = resolveWithinRoot(root, relPath);
26
+ await this.#assertReadableFile(abs, MAX_FILE_BYTES);
27
+ const buffer = await readFile(abs);
28
+ const path = toRelative(root, abs);
29
+ if (isBinary(buffer)) {
30
+ return { path, content: buffer.toString('base64'), encoding: 'base64' };
31
+ }
32
+ return { path, content: buffer.toString('utf-8'), encoding: 'utf-8' };
33
+ }
34
+ async readImage(root, relPath) {
35
+ const abs = resolveWithinRoot(root, relPath);
36
+ const mimeType = IMAGE_MIME[extname(abs).toLowerCase()];
37
+ if (!mimeType) {
38
+ throw RpcError.invalidParams('not a supported image type');
39
+ }
40
+ await this.#assertReadableFile(abs, MAX_IMAGE_BYTES);
41
+ const buffer = await readFile(abs);
42
+ return { path: toRelative(root, abs), base64Data: buffer.toString('base64'), mimeType };
43
+ }
44
+ async list(root) {
45
+ const resolvedRoot = resolve(root);
46
+ let dirents;
47
+ try {
48
+ dirents = await readdir(resolvedRoot, { withFileTypes: true });
49
+ }
50
+ catch {
51
+ throw new RpcError(JsonRpcErrorCode.WorkspaceAccessDenied, 'directory not accessible');
52
+ }
53
+ const entries = [];
54
+ for (const dirent of dirents) {
55
+ if (dirent.name === '.git' || isSensitiveName(dirent.name))
56
+ continue;
57
+ const isDir = dirent.isDirectory();
58
+ const entry = { name: dirent.name, type: isDir ? 'dir' : 'file' };
59
+ if (!isDir) {
60
+ try {
61
+ entry.size = (await stat(resolve(resolvedRoot, dirent.name))).size;
62
+ }
63
+ catch {
64
+ // ignore unreadable entries' size
65
+ }
66
+ }
67
+ entries.push(entry);
68
+ }
69
+ entries.sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === 'dir' ? -1 : 1);
70
+ return { cwd: '.', entries };
71
+ }
72
+ async applyPatch(root, changes) {
73
+ let applied = 0;
74
+ for (const change of changes) {
75
+ const abs = resolveWithinRoot(root, change.path);
76
+ if (change.op === 'delete') {
77
+ await rm(abs, { force: true });
78
+ applied += 1;
79
+ continue;
80
+ }
81
+ // add | modify
82
+ await mkdir(dirname(abs), { recursive: true });
83
+ await writeFile(abs, change.content ?? '', 'utf-8');
84
+ applied += 1;
85
+ }
86
+ return { success: true, applied };
87
+ }
88
+ async #assertReadableFile(abs, maxBytes) {
89
+ let info;
90
+ try {
91
+ info = await stat(abs);
92
+ }
93
+ catch {
94
+ throw new RpcError(JsonRpcErrorCode.ResourceNotFound, 'file not found');
95
+ }
96
+ if (!info.isFile()) {
97
+ throw RpcError.invalidParams('path is not a file');
98
+ }
99
+ if (info.size > maxBytes) {
100
+ throw new RpcError(JsonRpcErrorCode.BridgeError, 'file is too large to read');
101
+ }
102
+ }
103
+ }
104
+ function toRelative(root, abs) {
105
+ return relative(resolve(root), abs).split('\\').join('/');
106
+ }
107
+ function isBinary(buffer) {
108
+ const sample = buffer.subarray(0, 8000);
109
+ return sample.includes(0);
110
+ }
111
+ //# sourceMappingURL=workspace-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-service.js","sourceRoot":"","sources":["../../../src/workspace/workspace-service.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "uxnan-bridge",
3
+ "version": "0.0.1-alpha.20260621",
4
+ "description": "Uxnan Bridge — local control-plane daemon connecting the Uxnan mobile app to the developer's PC over E2EE.",
5
+ "license": "MPL-2.0",
6
+ "author": "Luis Donaldo Gamas Vazquez",
7
+ "type": "module",
8
+ "main": "./dist/src/index.js",
9
+ "types": "./dist/src/index.d.ts",
10
+ "bin": {
11
+ "uxnan-bridge": "dist/src/cli.js"
12
+ },
13
+ "files": [
14
+ "dist/src",
15
+ "scripts"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/luisgamas/uxnan.git",
23
+ "directory": "bridge"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "typecheck": "tsc --noEmit",
28
+ "test": "tsc && node --test --test-concurrency=1 \"dist/test/**/*.test.js\"",
29
+ "start": "node dist/src/cli.js start",
30
+ "prepublishOnly": "tsc"
31
+ },
32
+ "dependencies": {
33
+ "@uxnan/shared": "0.0.1-alpha.20260621",
34
+ "ajv": "^8.17.1",
35
+ "qrcode-terminal": "^0.12.0",
36
+ "ws": "^8.18.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/ws": "^8.5.12",
40
+ "uxnan-relay": "*"
41
+ },
42
+ "optionalDependencies": {
43
+ "@napi-rs/keyring": "^1.3.0",
44
+ "firebase-admin": "^13.0.0"
45
+ }
46
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Writes the bridge's Gemini approval hook to a path passed on argv[2],
3
+ * then exits. Used by the end-to-end smoke test against the real `gemini`
4
+ * CLI to materialize the actual CJS script the bridge ships.
5
+ */
6
+ import { writeGeminiApprovalHook } from '../dist/src/hooks/gemini-approval-hook.js';
7
+ import { writeFileSync } from 'node:fs';
8
+
9
+ const path = process.argv[2];
10
+ if (!path) {
11
+ console.error('usage: extract-gemini-hook.mjs <output-path>');
12
+ process.exit(2);
13
+ }
14
+ const written = await writeGeminiApprovalHook(path);
15
+ writeFileSync(written + '.sha', 'sentinel', 'utf-8');
16
+ console.log(written);
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Spins up a fake bridge HTTP endpoint that ALWAYS returns `allow` after a
3
+ * short delay (so the gemini CLI's tool call gets a real round-trip), then
4
+ * prints the request it received and exits.
5
+ */
6
+ import { createServer } from 'node:http';
7
+
8
+ const server = createServer((req, res) => {
9
+ let body = '';
10
+ req.on('data', (c) => (body += c));
11
+ req.on('end', () => {
12
+ console.log(`[bridge] ${req.method} ${req.url} body=${body}`);
13
+ // Mimic the real bridge: hold the response for 250 ms (so the hook waits,
14
+ // exercising the same code path the real phone would) then return `allow`.
15
+ setTimeout(() => {
16
+ res.writeHead(200, { 'content-type': 'application/json' });
17
+ res.end(JSON.stringify({ decision: 'allow' }));
18
+ }, 250);
19
+ });
20
+ });
21
+ server.listen(19999, '127.0.0.1', () => {
22
+ console.log('[bridge] listening on http://127.0.0.1:19999/agent-hook/approval');
23
+ });
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # install-service-linux.sh
3
+ #
4
+ # Installs a systemd USER unit so the uxnan-bridge daemon starts at login.
5
+ # Requires the global CLI: npm install -g uxnan-bridge
6
+ #
7
+ # Remove: systemctl --user disable --now uxnan-bridge.service && \
8
+ # rm "${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/uxnan-bridge.service"
9
+ set -euo pipefail
10
+
11
+ BIN="$(command -v uxnan-bridge || true)"
12
+ if [ -z "$BIN" ]; then
13
+ echo "uxnan-bridge not found on PATH. Run: npm install -g uxnan-bridge" >&2
14
+ exit 1
15
+ fi
16
+
17
+ UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
18
+ mkdir -p "$UNIT_DIR"
19
+
20
+ cat > "$UNIT_DIR/uxnan-bridge.service" <<EOF
21
+ [Unit]
22
+ Description=Uxnan Bridge daemon
23
+ After=network-online.target
24
+ Wants=network-online.target
25
+
26
+ [Service]
27
+ ExecStart=$BIN start
28
+ Restart=on-failure
29
+ RestartSec=5
30
+
31
+ [Install]
32
+ WantedBy=default.target
33
+ EOF
34
+
35
+ systemctl --user daemon-reload
36
+ systemctl --user enable --now uxnan-bridge.service
37
+ echo "Enabled systemd user unit uxnan-bridge.service."
38
+ echo "Tip: 'loginctl enable-linger \$USER' keeps it running after logout."
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # install-service-macos.sh
3
+ #
4
+ # Installs a LaunchAgent so the uxnan-bridge daemon starts at login.
5
+ # Requires the global CLI: npm install -g uxnan-bridge
6
+ #
7
+ # Remove: launchctl unload ~/Library/LaunchAgents/dev.luisgamas.bridge.plist && \
8
+ # rm ~/Library/LaunchAgents/dev.luisgamas.bridge.plist
9
+ set -euo pipefail
10
+
11
+ BIN="$(command -v uxnan-bridge || true)"
12
+ if [ -z "$BIN" ]; then
13
+ echo "uxnan-bridge not found on PATH. Run: npm install -g uxnan-bridge" >&2
14
+ exit 1
15
+ fi
16
+
17
+ mkdir -p "$HOME/Library/LaunchAgents" "$HOME/.uxnan/logs"
18
+ PLIST="$HOME/Library/LaunchAgents/dev.luisgamas.bridge.plist"
19
+
20
+ cat > "$PLIST" <<EOF
21
+ <?xml version="1.0" encoding="UTF-8"?>
22
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
23
+ <plist version="1.0">
24
+ <dict>
25
+ <key>Label</key><string>dev.luisgamas.bridge</string>
26
+ <key>ProgramArguments</key>
27
+ <array><string>$BIN</string><string>start</string></array>
28
+ <key>RunAtLoad</key><true/>
29
+ <key>KeepAlive</key><true/>
30
+ <key>StandardOutPath</key><string>$HOME/.uxnan/logs/launchd.out.log</string>
31
+ <key>StandardErrorPath</key><string>$HOME/.uxnan/logs/launchd.err.log</string>
32
+ </dict>
33
+ </plist>
34
+ EOF
35
+
36
+ launchctl unload "$PLIST" 2>/dev/null || true
37
+ launchctl load "$PLIST"
38
+ echo "Loaded LaunchAgent dev.luisgamas.bridge (starts at login)."
@@ -0,0 +1,26 @@
1
+ # install-service-windows.ps1
2
+ #
3
+ # Registers the uxnan-bridge daemon to start at user logon via Task Scheduler.
4
+ # Requires the global CLI: npm install -g uxnan-bridge
5
+ #
6
+ # Usage: powershell -ExecutionPolicy Bypass -File install-service-windows.ps1
7
+ # Remove: Unregister-ScheduledTask -TaskName 'UxnanBridge' -Confirm:$false
8
+
9
+ $ErrorActionPreference = 'Stop'
10
+
11
+ $bin = (Get-Command uxnan-bridge -ErrorAction SilentlyContinue).Source
12
+ if (-not $bin) {
13
+ Write-Error "uxnan-bridge not found on PATH. Run: npm install -g uxnan-bridge"
14
+ exit 1
15
+ }
16
+
17
+ $action = New-ScheduledTaskAction -Execute $bin -Argument 'start'
18
+ $trigger = New-ScheduledTaskTrigger -AtLogOn
19
+ $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable `
20
+ -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero)
21
+
22
+ Register-ScheduledTask -TaskName 'UxnanBridge' -Action $action -Trigger $trigger `
23
+ -Settings $settings -Description 'Uxnan Bridge daemon' -Force | Out-Null
24
+
25
+ Write-Host "Registered scheduled task 'UxnanBridge' (starts at logon)."
26
+ Write-Host "Start now with: Start-ScheduledTask -TaskName 'UxnanBridge'"