@adecore/agents 0.0.1 → 0.17.0-beta.2

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 (348) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +48 -0
  3. package/dist/async.d.ts +2 -0
  4. package/dist/async.js +31 -0
  5. package/dist/chat/attachment-store.d.ts +10 -0
  6. package/dist/chat/attachment-store.js +93 -0
  7. package/dist/chat/backend.d.ts +214 -0
  8. package/dist/chat/backend.js +1 -0
  9. package/dist/chat/background-work.d.ts +8 -0
  10. package/dist/chat/background-work.js +33 -0
  11. package/dist/chat/bookmark-store.d.ts +25 -0
  12. package/dist/chat/bookmark-store.js +141 -0
  13. package/dist/chat/chat-core.d.ts +311 -0
  14. package/dist/chat/chat-core.js +1025 -0
  15. package/dist/chat/chat-log.d.ts +33 -0
  16. package/dist/chat/chat-log.js +163 -0
  17. package/dist/chat/chat-process.d.ts +46 -0
  18. package/dist/chat/chat-process.js +190 -0
  19. package/dist/chat/chat-session.d.ts +209 -0
  20. package/dist/chat/chat-session.js +1511 -0
  21. package/dist/chat/chat-store.d.ts +37 -0
  22. package/dist/chat/chat-store.js +202 -0
  23. package/dist/chat/chat-title.d.ts +10 -0
  24. package/dist/chat/chat-title.js +74 -0
  25. package/dist/chat/claude-backend.d.ts +35 -0
  26. package/dist/chat/claude-backend.js +214 -0
  27. package/dist/chat/claude-protocol.d.ts +41 -0
  28. package/dist/chat/claude-protocol.js +642 -0
  29. package/dist/chat/claude-title.d.ts +11 -0
  30. package/dist/chat/claude-title.js +97 -0
  31. package/dist/chat/claude-transcript.d.ts +28 -0
  32. package/dist/chat/claude-transcript.js +211 -0
  33. package/dist/chat/codex-backend.d.ts +51 -0
  34. package/dist/chat/codex-backend.js +364 -0
  35. package/dist/chat/codex-protocol.d.ts +56 -0
  36. package/dist/chat/codex-protocol.js +767 -0
  37. package/dist/chat/codex-thread.d.ts +32 -0
  38. package/dist/chat/codex-thread.js +131 -0
  39. package/dist/chat/codex-transport.d.ts +42 -0
  40. package/dist/chat/codex-transport.js +188 -0
  41. package/dist/chat/composer-preferences.d.ts +15 -0
  42. package/dist/chat/composer-preferences.js +46 -0
  43. package/dist/chat/context-breakdown.d.ts +2 -0
  44. package/dist/chat/context-breakdown.js +74 -0
  45. package/dist/chat/delta-coalescer.d.ts +12 -0
  46. package/dist/chat/delta-coalescer.js +55 -0
  47. package/dist/chat/errors.d.ts +4 -0
  48. package/dist/chat/errors.js +4 -0
  49. package/dist/chat/fake-claude.d.ts +3 -0
  50. package/dist/chat/fake-claude.js +982 -0
  51. package/dist/chat/fake-cli.d.ts +25 -0
  52. package/dist/chat/fake-cli.js +157 -0
  53. package/dist/chat/fake-codex.d.ts +10 -0
  54. package/dist/chat/fake-codex.js +502 -0
  55. package/dist/chat/fixtures/claude-2.1.285-auto-fallback.json +24 -0
  56. package/dist/chat/fixtures/claude-2.1.285-native-children.json +6600 -0
  57. package/dist/chat/fixtures/claude-2.1.285-resume-direct-compact.json +202 -0
  58. package/dist/chat/fixtures/claude-2.1.285-resume-task-notification.json +57 -0
  59. package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.jsonl +10 -0
  60. package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.meta.json +1 -0
  61. package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.jsonl +2 -0
  62. package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.meta.json +1 -0
  63. package/dist/chat/fixtures/codex-0.159.2-native-approval.json +119 -0
  64. package/dist/chat/fixtures/codex-0.159.2-native-file-approval.json +136 -0
  65. package/dist/chat/fixtures/codex-0.159.2-native-interacted.json +220 -0
  66. package/dist/chat/fixtures/codex-0.159.2-usage-windows.json +74 -0
  67. package/dist/chat/input.d.ts +19 -0
  68. package/dist/chat/input.js +71 -0
  69. package/dist/chat/limit-resume.d.ts +11 -0
  70. package/dist/chat/limit-resume.js +68 -0
  71. package/dist/chat/projector.d.ts +60 -0
  72. package/dist/chat/projector.js +860 -0
  73. package/dist/chat/request-summary.d.ts +3 -0
  74. package/dist/chat/request-summary.js +143 -0
  75. package/dist/chat/subagent-projection.d.ts +8 -0
  76. package/dist/chat/subagent-projection.js +40 -0
  77. package/dist/chat/subagent-reader.d.ts +66 -0
  78. package/dist/chat/subagent-reader.js +396 -0
  79. package/dist/chat/subagent-settlement.d.ts +6 -0
  80. package/dist/chat/subagent-settlement.js +103 -0
  81. package/dist/chat/thread.d.ts +30 -0
  82. package/dist/chat/thread.js +147 -0
  83. package/dist/chat/wake-chat.d.ts +20 -0
  84. package/dist/chat/wake-chat.js +27 -0
  85. package/dist/client-sinks.d.ts +9 -0
  86. package/dist/client-sinks.js +34 -0
  87. package/dist/coded-error.d.ts +5 -0
  88. package/dist/coded-error.js +17 -0
  89. package/dist/context/argv.d.ts +11 -0
  90. package/dist/context/argv.js +58 -0
  91. package/dist/context/refusal.d.ts +9 -0
  92. package/dist/context/refusal.js +38 -0
  93. package/dist/context/verb.d.ts +85 -0
  94. package/dist/context/verb.js +233 -0
  95. package/dist/error-text.d.ts +5 -0
  96. package/dist/error-text.js +24 -0
  97. package/dist/events.d.ts +12 -0
  98. package/dist/events.js +1 -0
  99. package/dist/fs.d.ts +9 -0
  100. package/dist/fs.js +107 -0
  101. package/dist/host/agent-host.d.ts +28 -0
  102. package/dist/host/agent-host.js +105 -0
  103. package/dist/host/environment.d.ts +7 -0
  104. package/dist/host/environment.js +22 -0
  105. package/dist/host/handlers.d.ts +30 -0
  106. package/dist/host/handlers.js +111 -0
  107. package/dist/host/memory-port.d.ts +2 -0
  108. package/dist/host/memory-port.js +25 -0
  109. package/dist/host/wiring.d.ts +40 -0
  110. package/dist/host/wiring.js +120 -0
  111. package/dist/lineage.d.ts +44 -0
  112. package/dist/lineage.js +134 -0
  113. package/dist/messages/deliver-message.d.ts +21 -0
  114. package/dist/messages/deliver-message.js +42 -0
  115. package/dist/messages/deliver-notice.d.ts +25 -0
  116. package/dist/messages/deliver-notice.js +75 -0
  117. package/dist/messages/notice-notes.d.ts +15 -0
  118. package/dist/messages/notice-notes.js +26 -0
  119. package/dist/messages/notice-store.d.ts +30 -0
  120. package/dist/messages/notice-store.js +120 -0
  121. package/dist/modes.d.ts +11 -0
  122. package/dist/modes.js +28 -0
  123. package/dist/outbox/manual-clock.d.ts +10 -0
  124. package/dist/outbox/manual-clock.js +25 -0
  125. package/dist/outbox/outbox-worker.d.ts +44 -0
  126. package/dist/outbox/outbox-worker.js +173 -0
  127. package/dist/outbox/outbox.d.ts +37 -0
  128. package/dist/outbox/outbox.js +76 -0
  129. package/dist/providers/accounts/accounts.d.ts +20 -0
  130. package/dist/providers/accounts/accounts.js +158 -0
  131. package/dist/providers/accounts/launch.d.ts +17 -0
  132. package/dist/providers/accounts/launch.js +30 -0
  133. package/dist/providers/accounts/service.d.ts +87 -0
  134. package/dist/providers/accounts/service.js +538 -0
  135. package/dist/providers/accounts/shadow-home.d.ts +8 -0
  136. package/dist/providers/accounts/shadow-home.js +70 -0
  137. package/dist/providers/accounts/status.d.ts +12 -0
  138. package/dist/providers/accounts/status.js +85 -0
  139. package/dist/providers/accounts/store.d.ts +14 -0
  140. package/dist/providers/accounts/store.js +112 -0
  141. package/dist/providers/accounts/test-accounts.d.ts +12 -0
  142. package/dist/providers/accounts/test-accounts.js +38 -0
  143. package/dist/providers/accounts/variables.d.ts +27 -0
  144. package/dist/providers/accounts/variables.js +94 -0
  145. package/dist/providers/catalog.d.ts +17 -0
  146. package/dist/providers/catalog.js +76 -0
  147. package/dist/providers/claude-models.json +114 -0
  148. package/dist/providers/claude-provider.d.ts +4 -0
  149. package/dist/providers/claude-provider.js +29 -0
  150. package/dist/providers/claude.d.ts +15 -0
  151. package/dist/providers/claude.js +105 -0
  152. package/dist/providers/codex-models.json +135 -0
  153. package/dist/providers/codex-provider.d.ts +4 -0
  154. package/dist/providers/codex-provider.js +27 -0
  155. package/dist/providers/codex-rules.d.ts +9 -0
  156. package/dist/providers/codex-rules.js +35 -0
  157. package/dist/providers/codex.d.ts +14 -0
  158. package/dist/providers/codex.js +52 -0
  159. package/dist/providers/detect.d.ts +6 -0
  160. package/dist/providers/detect.js +30 -0
  161. package/dist/providers/provider.d.ts +25 -0
  162. package/dist/providers/provider.js +1 -0
  163. package/dist/providers/registry.d.ts +28 -0
  164. package/dist/providers/registry.js +118 -0
  165. package/dist/record-directory.d.ts +27 -0
  166. package/dist/record-directory.js +114 -0
  167. package/dist/run-process.d.ts +13 -0
  168. package/dist/run-process.js +42 -0
  169. package/dist/serializer.d.ts +9 -0
  170. package/dist/serializer.js +34 -0
  171. package/dist/skills.d.ts +21 -0
  172. package/dist/skills.js +174 -0
  173. package/dist/tasks/background-limit.d.ts +16 -0
  174. package/dist/tasks/background-limit.js +55 -0
  175. package/dist/tasks/end-children.d.ts +34 -0
  176. package/dist/tasks/end-children.js +53 -0
  177. package/dist/tasks/give-task.d.ts +14 -0
  178. package/dist/tasks/give-task.js +33 -0
  179. package/dist/tasks/task-coordinator.d.ts +52 -0
  180. package/dist/tasks/task-coordinator.js +323 -0
  181. package/dist/tasks/task-store.d.ts +49 -0
  182. package/dist/tasks/task-store.js +197 -0
  183. package/dist/tasks/task-work.d.ts +47 -0
  184. package/dist/tasks/task-work.js +28 -0
  185. package/dist/tasks/waiting-child.d.ts +51 -0
  186. package/dist/tasks/waiting-child.js +143 -0
  187. package/dist/tasks/wake-parent.d.ts +19 -0
  188. package/dist/tasks/wake-parent.js +109 -0
  189. package/dist/tasks/wiring.d.ts +51 -0
  190. package/dist/tasks/wiring.js +151 -0
  191. package/dist/title-file.d.ts +2 -0
  192. package/dist/title-file.js +55 -0
  193. package/dist/usage/accounts.d.ts +15 -0
  194. package/dist/usage/accounts.js +64 -0
  195. package/dist/usage/aggregate.d.ts +12 -0
  196. package/dist/usage/aggregate.js +154 -0
  197. package/dist/usage/exchange.d.ts +16 -0
  198. package/dist/usage/exchange.js +84 -0
  199. package/dist/usage/index-file.d.ts +15 -0
  200. package/dist/usage/index-file.js +123 -0
  201. package/dist/usage/limits/monitor.d.ts +50 -0
  202. package/dist/usage/limits/monitor.js +179 -0
  203. package/dist/usage/limits/normalize.d.ts +21 -0
  204. package/dist/usage/limits/normalize.js +160 -0
  205. package/dist/usage/limits/probe.d.ts +9 -0
  206. package/dist/usage/limits/probe.js +113 -0
  207. package/dist/usage/prices-snapshot.json +1071 -0
  208. package/dist/usage/pricing.d.ts +34 -0
  209. package/dist/usage/pricing.js +186 -0
  210. package/dist/usage/projects.d.ts +17 -0
  211. package/dist/usage/projects.js +49 -0
  212. package/dist/usage/readers/claude.d.ts +3 -0
  213. package/dist/usage/readers/claude.js +59 -0
  214. package/dist/usage/readers/codex.d.ts +23 -0
  215. package/dist/usage/readers/codex.js +155 -0
  216. package/dist/usage/record.d.ts +15 -0
  217. package/dist/usage/record.js +39 -0
  218. package/dist/usage/roots.d.ts +12 -0
  219. package/dist/usage/roots.js +29 -0
  220. package/dist/usage/scanner.d.ts +30 -0
  221. package/dist/usage/scanner.js +234 -0
  222. package/dist/usage/usage-service.d.ts +35 -0
  223. package/dist/usage/usage-service.js +130 -0
  224. package/dist/watch-seam.d.ts +29 -0
  225. package/dist/watch-seam.js +78 -0
  226. package/dist/watch-test-helpers.d.ts +28 -0
  227. package/dist/watch-test-helpers.js +86 -0
  228. package/package.json +42 -3
  229. package/src/async.ts +40 -0
  230. package/src/chat/attachment-store.ts +108 -0
  231. package/src/chat/backend.ts +199 -0
  232. package/src/chat/background-work.ts +42 -0
  233. package/src/chat/bookmark-store.ts +166 -0
  234. package/src/chat/chat-core.ts +1228 -0
  235. package/src/chat/chat-log.ts +187 -0
  236. package/src/chat/chat-process.ts +237 -0
  237. package/src/chat/chat-session.ts +1723 -0
  238. package/src/chat/chat-store.ts +261 -0
  239. package/src/chat/chat-title.ts +90 -0
  240. package/src/chat/claude-backend.ts +235 -0
  241. package/src/chat/claude-protocol.ts +684 -0
  242. package/src/chat/claude-title.ts +106 -0
  243. package/src/chat/claude-transcript.ts +233 -0
  244. package/src/chat/codex-backend.ts +397 -0
  245. package/src/chat/codex-protocol.ts +872 -0
  246. package/src/chat/codex-thread.ts +162 -0
  247. package/src/chat/codex-transport.ts +224 -0
  248. package/src/chat/composer-preferences.ts +67 -0
  249. package/src/chat/context-breakdown.ts +80 -0
  250. package/src/chat/delta-coalescer.ts +62 -0
  251. package/src/chat/errors.ts +25 -0
  252. package/src/chat/fake-claude.ts +1006 -0
  253. package/src/chat/fake-cli.ts +199 -0
  254. package/src/chat/fake-codex.ts +534 -0
  255. package/src/chat/fixtures/claude-2.1.285-auto-fallback.json +24 -0
  256. package/src/chat/fixtures/claude-2.1.285-native-children.json +6600 -0
  257. package/src/chat/fixtures/claude-2.1.285-resume-direct-compact.json +202 -0
  258. package/src/chat/fixtures/claude-2.1.285-resume-task-notification.json +57 -0
  259. package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.jsonl +10 -0
  260. package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.meta.json +1 -0
  261. package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.jsonl +2 -0
  262. package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.meta.json +1 -0
  263. package/src/chat/fixtures/codex-0.159.2-native-approval.json +119 -0
  264. package/src/chat/fixtures/codex-0.159.2-native-file-approval.json +136 -0
  265. package/src/chat/fixtures/codex-0.159.2-native-interacted.json +220 -0
  266. package/src/chat/fixtures/codex-0.159.2-usage-windows.json +74 -0
  267. package/src/chat/input.ts +89 -0
  268. package/src/chat/limit-resume.ts +74 -0
  269. package/src/chat/projector.ts +969 -0
  270. package/src/chat/request-summary.ts +168 -0
  271. package/src/chat/subagent-projection.ts +45 -0
  272. package/src/chat/subagent-reader.ts +469 -0
  273. package/src/chat/subagent-settlement.ts +111 -0
  274. package/src/chat/thread.ts +167 -0
  275. package/src/chat/wake-chat.ts +45 -0
  276. package/src/client-sinks.ts +39 -0
  277. package/src/coded-error.ts +18 -0
  278. package/src/context/argv.ts +61 -0
  279. package/src/context/refusal.ts +50 -0
  280. package/src/context/verb.ts +392 -0
  281. package/src/error-text.ts +28 -0
  282. package/src/events.ts +9 -0
  283. package/src/fs.ts +118 -0
  284. package/src/host/agent-host.ts +124 -0
  285. package/src/host/environment.ts +31 -0
  286. package/src/host/handlers.ts +142 -0
  287. package/src/host/memory-port.ts +27 -0
  288. package/src/host/wiring.ts +164 -0
  289. package/src/lineage.ts +159 -0
  290. package/src/messages/deliver-message.ts +61 -0
  291. package/src/messages/deliver-notice.ts +120 -0
  292. package/src/messages/notice-notes.ts +34 -0
  293. package/src/messages/notice-store.ts +142 -0
  294. package/src/modes.ts +38 -0
  295. package/src/outbox/manual-clock.ts +31 -0
  296. package/src/outbox/outbox-worker.ts +214 -0
  297. package/src/outbox/outbox.ts +106 -0
  298. package/src/providers/accounts/accounts.ts +192 -0
  299. package/src/providers/accounts/launch.ts +55 -0
  300. package/src/providers/accounts/service.ts +632 -0
  301. package/src/providers/accounts/shadow-home.ts +85 -0
  302. package/src/providers/accounts/status.ts +106 -0
  303. package/src/providers/accounts/store.ts +158 -0
  304. package/src/providers/accounts/test-accounts.ts +48 -0
  305. package/src/providers/accounts/variables.ts +132 -0
  306. package/src/providers/catalog.ts +90 -0
  307. package/src/providers/claude-models.json +114 -0
  308. package/src/providers/claude-provider.ts +32 -0
  309. package/src/providers/claude.ts +123 -0
  310. package/src/providers/codex-models.json +135 -0
  311. package/src/providers/codex-provider.ts +30 -0
  312. package/src/providers/codex-rules.ts +54 -0
  313. package/src/providers/codex.ts +69 -0
  314. package/src/providers/detect.ts +40 -0
  315. package/src/providers/provider.ts +46 -0
  316. package/src/providers/registry.ts +150 -0
  317. package/src/record-directory.ts +136 -0
  318. package/src/run-process.ts +61 -0
  319. package/src/serializer.ts +41 -0
  320. package/src/skills.ts +199 -0
  321. package/src/tasks/background-limit.ts +72 -0
  322. package/src/tasks/end-children.ts +100 -0
  323. package/src/tasks/give-task.ts +52 -0
  324. package/src/tasks/task-coordinator.ts +384 -0
  325. package/src/tasks/task-store.ts +236 -0
  326. package/src/tasks/task-work.ts +62 -0
  327. package/src/tasks/waiting-child.ts +203 -0
  328. package/src/tasks/wake-parent.ts +142 -0
  329. package/src/tasks/wiring.ts +231 -0
  330. package/src/title-file.ts +57 -0
  331. package/src/usage/accounts.ts +86 -0
  332. package/src/usage/aggregate.ts +220 -0
  333. package/src/usage/exchange.ts +93 -0
  334. package/src/usage/index-file.ts +178 -0
  335. package/src/usage/limits/monitor.ts +239 -0
  336. package/src/usage/limits/normalize.ts +188 -0
  337. package/src/usage/limits/probe.ts +120 -0
  338. package/src/usage/prices-snapshot.json +1071 -0
  339. package/src/usage/pricing.ts +236 -0
  340. package/src/usage/projects.ts +65 -0
  341. package/src/usage/readers/claude.ts +60 -0
  342. package/src/usage/readers/codex.ts +196 -0
  343. package/src/usage/record.ts +60 -0
  344. package/src/usage/roots.ts +39 -0
  345. package/src/usage/scanner.ts +265 -0
  346. package/src/usage/usage-service.ts +161 -0
  347. package/src/watch-seam.ts +111 -0
  348. package/src/watch-test-helpers.ts +102 -0
package/src/async.ts ADDED
@@ -0,0 +1,40 @@
1
+ /* The two waits the host does: a pause it can be woken from, and a deadline on somebody else's promise. */
2
+
3
+ /*
4
+ * Resolves after `ms`, or at once when the signal is raised. An abort ends the wait rather than
5
+ * failing it: every caller here is a loop that checks the signal itself on the next turn, and a
6
+ * rejection would only be caught and thrown away.
7
+ */
8
+ export function wait(ms: number, signal?: AbortSignal): Promise<void> {
9
+ return new Promise((resolve) => {
10
+ const timer = setTimeout(resolve, ms);
11
+ signal?.addEventListener(
12
+ 'abort',
13
+ () => {
14
+ clearTimeout(timer);
15
+ resolve();
16
+ },
17
+ { once: true }
18
+ );
19
+ });
20
+ }
21
+
22
+ /*
23
+ * The promise, or `message` when it takes longer than `ms`. The timer is cleared either way, so a
24
+ * call that answers at once does not hold the process for the rest of its deadline.
25
+ */
26
+ export function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
27
+ return new Promise((resolve, reject) => {
28
+ const timer = setTimeout(() => reject(new Error(message)), ms);
29
+ promise.then(
30
+ (value) => {
31
+ clearTimeout(timer);
32
+ resolve(value);
33
+ },
34
+ (e: unknown) => {
35
+ clearTimeout(timer);
36
+ reject(e instanceof Error ? e : new Error(String(e)));
37
+ }
38
+ );
39
+ });
40
+ }
@@ -0,0 +1,108 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
3
+ import { extname, join } from 'node:path';
4
+ import { attachmentImageMime, type ChatAttachment, type ChatAttachmentUpload } from '@adecore/agent-contracts';
5
+
6
+ // The extension the file gets on disk when its name does not carry a usable one.
7
+ const EXTENSIONS: Record<string, string> = {
8
+ 'image/png': 'png',
9
+ 'image/jpeg': 'jpg',
10
+ 'image/gif': 'gif',
11
+ 'image/webp': 'webp',
12
+ 'image/svg+xml': 'svg',
13
+ 'application/pdf': 'pdf',
14
+ 'application/json': 'json',
15
+ 'text/csv': 'csv',
16
+ 'text/markdown': 'md',
17
+ 'text/plain': 'txt'
18
+ };
19
+
20
+ const SAFE_EXTENSION = /^[A-Za-z0-9]{1,8}$/;
21
+
22
+ /* What the file is called on disk: the id, plus an extension the agent's tools can recognize. */
23
+ export function extensionFor(name: string, mime: string): string {
24
+ const own = extname(name).slice(1).toLowerCase();
25
+ if (SAFE_EXTENSION.test(own)) {
26
+ return own;
27
+ }
28
+ const known = EXTENSIONS[mime.toLowerCase()];
29
+ if (known) {
30
+ return known;
31
+ }
32
+ const subtype = mime.split('/')[1]?.replace(/\+.*$/, '') ?? '';
33
+ return SAFE_EXTENSION.test(subtype) ? subtype.toLowerCase() : 'bin';
34
+ }
35
+
36
+ /*
37
+ * The files people attach to a message, under `<home>/attachments/<chatId>`. The thread keeps
38
+ * only what a row needs (name, mime, size, path); the bytes stay on disk, so a thread with a video
39
+ * in it is still a small JSON file.
40
+ */
41
+ export class AttachmentStore {
42
+ readonly dir: string;
43
+
44
+ constructor(home: string) {
45
+ this.dir = join(home, 'attachments');
46
+ }
47
+
48
+ /* Writes one upload and answers the metadata the thread stores. */
49
+ async save(chatId: string, upload: ChatAttachmentUpload): Promise<ChatAttachment> {
50
+ const bytes = Buffer.from(upload.data, 'base64');
51
+ const mime = attachmentImageMime(upload) ?? upload.mime;
52
+ const id = randomBytes(8).toString('hex');
53
+ const folder = this.folder(chatId);
54
+ await mkdir(folder, { recursive: true, mode: 0o700 });
55
+ const path = join(folder, `${id}.${extensionFor(upload.name, mime)}`);
56
+ await writeFile(path, bytes, { mode: 0o600 });
57
+ return { id, name: upload.name, mime, size: bytes.byteLength, path };
58
+ }
59
+
60
+ /* Everything a chat attached; used when the chat itself is deleted. */
61
+ async removeAll(chatId: string): Promise<void> {
62
+ await rm(this.folder(chatId), { recursive: true, force: true });
63
+ }
64
+
65
+ private folder(chatId: string): string {
66
+ return join(this.dir, encodeURIComponent(chatId));
67
+ }
68
+ }
69
+
70
+ function isRecord(value: unknown): value is Record<string, unknown> {
71
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
72
+ }
73
+
74
+ // What an attachment looked like before the files moved out of the thread: the bytes inline.
75
+ function isInline(value: unknown): value is { name: string; mediaType: string; data: string } {
76
+ return isRecord(value) && typeof value.name === 'string' && typeof value.mediaType === 'string' && typeof value.data === 'string';
77
+ }
78
+
79
+ /*
80
+ * Writes out the images a thread still carries inline and answers the record with metadata in their
81
+ * place, or null when there was nothing to do. Old records are migrated on the read that opens the
82
+ * chat, before the schema sees them, because the schema no longer knows the inline shape.
83
+ */
84
+ export async function migrateInlineAttachments(chatId: string, record: unknown, store: AttachmentStore): Promise<unknown | null> {
85
+ const items = isRecord(record) && Array.isArray(record.items) ? record.items : null;
86
+ if (!items) {
87
+ return null;
88
+ }
89
+ let migrated = false;
90
+ const next = await Promise.all(
91
+ items.map(async (item) => {
92
+ if (!isRecord(item) || !Array.isArray(item.attachments) || !item.attachments.some(isInline)) {
93
+ return item;
94
+ }
95
+ const attachments = await Promise.all(
96
+ item.attachments.map(async (attachment) => {
97
+ if (!isInline(attachment)) {
98
+ return attachment;
99
+ }
100
+ migrated = true;
101
+ return await store.save(chatId, { name: attachment.name, mime: attachment.mediaType, data: attachment.data });
102
+ })
103
+ );
104
+ return { ...item, attachments };
105
+ })
106
+ );
107
+ return migrated ? { ...(record as Record<string, unknown>), items: next } : null;
108
+ }
@@ -0,0 +1,199 @@
1
+ import type {
2
+ ChatAttachment,
3
+ ChatFileChange,
4
+ ChatQuestion,
5
+ ChatSkill,
6
+ ChatSubagentUsage,
7
+ ChatTurnLimit,
8
+ ChatWorkflow,
9
+ ModelSelection,
10
+ RuntimeMode
11
+ } from '@adecore/agent-contracts';
12
+ import type { LimitsUpdate } from '../usage/limits/normalize.ts';
13
+ import type { SpawnChatProcess } from './chat-process.ts';
14
+
15
+ /*
16
+ * The seam between one chat and one CLI. A backend owns a process and the protocol it speaks; it
17
+ * reports what happened as `BackendEvent`s and never touches the thread, its item ids or its turns.
18
+ * Everything a provider does differently lives behind this interface.
19
+ */
20
+
21
+ export interface BackendLaunch {
22
+ // A test points this at a fake CLI.
23
+ command: string[];
24
+ cwd: string;
25
+ env: Record<string, string>;
26
+ selection: ModelSelection;
27
+ // The selected model as a person reads it, for the errors a backend puts in the thread.
28
+ modelName: string;
29
+ runtimeMode: RuntimeMode;
30
+ // The CLI's own session or thread id to continue, if the chat has one.
31
+ resume: string | null;
32
+ // How often this chat started a CLI; a protocol that numbers its own requests from zero
33
+ // needs it to keep the ids of one process apart from those of the one before.
34
+ generation: number;
35
+ // What the agent is told once, at the start of a process: a system prompt, or a new thread's developer instructions.
36
+ instructions: string | null;
37
+ // The part of it a resumed thread has to hear again in front of its next prompt, for a CLI that ignores new
38
+ // instructions on a resume; null when there is nothing to repeat.
39
+ resumeNote: string | null;
40
+ // Folders outside `cwd` the agent may reach without asking. Claude Code takes them as `--add-dir`; Codex's
41
+ // sandbox reads the whole disk already and keeps writes to the workspace, so it needs nothing.
42
+ folders?: readonly string[];
43
+ // How the CLI is started; a test runs a fake in the same process, everything else spawns it.
44
+ spawn?: SpawnChatProcess;
45
+ }
46
+
47
+ export interface TurnInput {
48
+ promptId?: string;
49
+ text: string;
50
+ // What the session wants in front of the text (a context change); the backend places it after its own prefix.
51
+ preamble: string | null;
52
+ attachments: ChatAttachment[];
53
+ // The paths the person picked with `@`; they also sit in the text, for a CLI that expands them itself.
54
+ mentions: string[];
55
+ // The skills the person picked with `$`; they also sit in the text, for a CLI that expands them itself.
56
+ skills: string[];
57
+ }
58
+
59
+ export type ApprovalDecision = 'allow' | 'allow-always' | 'deny';
60
+
61
+ /*
62
+ * What a CLI told us, in one vocabulary. `ref` is the backend's own key for an item (a tool use id,
63
+ * a message id plus block ordinal, a native item id); the projector turns it into a thread item id.
64
+ */
65
+ export type BackendEvent =
66
+ | { type: 'turn.accepted'; promptId: string }
67
+ // `title` is the name the CLI already has for the thread, as a resumed Codex thread carries it.
68
+ | {
69
+ type: 'session';
70
+ agentSessionId: string | null;
71
+ model: string | null;
72
+ slashCommands?: string[];
73
+ skills?: string[];
74
+ title?: string | null;
75
+ effectiveRuntimeMode?: RuntimeMode;
76
+ permissionMode?: string;
77
+ }
78
+ // The CLI renamed its thread, for a protocol that says so.
79
+ | { type: 'title'; title: string }
80
+ | { type: 'permissions'; permissionMode: string; effectiveRuntimeMode?: RuntimeMode }
81
+ | { type: 'text.delta'; ref: string; text: string }
82
+ // `parentRef` is set for text a subagent wrote; it belongs to that agent's row, not to the thread.
83
+ | { type: 'text.done'; ref: string; text: string; parentRef?: string | null }
84
+ // What the model thought before it answered; consecutive blocks become one thinking item.
85
+ | { type: 'thinking.delta'; ref: string; text: string }
86
+ | { type: 'thinking.done'; ref: string; text: string }
87
+ | { type: 'tool.started'; ref: string; name: string; input: unknown; parentRef: string | null; changes?: ChatFileChange[] }
88
+ | { type: 'tool.progress'; ref: string; startedAt: number | null; description: string | null }
89
+ | { type: 'tool.output'; ref: string; text: string }
90
+ | { type: 'tool.done'; ref: string; output: string | null; state: 'done' | 'error'; changes?: ChatFileChange[] }
91
+ | {
92
+ type: 'approval.requested';
93
+ requestId: string;
94
+ // The tool call the request is about, when the CLI names one.
95
+ ref: string | null;
96
+ toolName: string;
97
+ input: unknown;
98
+ description: string | null;
99
+ canAllowAlways: boolean;
100
+ // A child request may arrive after the main turn ended, without starting another one.
101
+ background?: boolean;
102
+ allowAlways?: { label: string; description: string };
103
+ }
104
+ // `async` marks a question the CLI goes on past, which is the only kind the person may dismiss.
105
+ | { type: 'question.requested'; requestId: string; questions: ChatQuestion[]; async?: boolean; background?: boolean }
106
+ // The CLI took an approval or a question back; the person no longer has to answer it.
107
+ | { type: 'request.withdrawn'; requestId: string }
108
+ // An agent the agent delegated to, keyed by the call that spawned it. `background` says whether
109
+ // it runs beside the turn; a foreground one settles with the call's own result instead.
110
+ | {
111
+ type: 'task.started';
112
+ ref: string;
113
+ description: string | null;
114
+ subagentType: string | null;
115
+ prompt: string | null;
116
+ background: boolean;
117
+ // The CLI's own id for the agent, which stays the same when a message wakes it again under another call.
118
+ taskId?: string | null;
119
+ // The thread a CLI that keeps one per agent opened for it, which is where its whole conversation is read.
120
+ threadId?: string | null;
121
+ // How many agents deep it runs: 1 for one the chat's own agent opened, 2 for one a subagent opened.
122
+ depth?: number;
123
+ resumed?: boolean;
124
+ }
125
+ | { type: 'task.progress'; ref: string; taskId?: string | null; summary: string | null; lastTool: string | null; usage: ChatSubagentUsage | null }
126
+ // The model a subagent's own answers came from, which its call may not have named.
127
+ | { type: 'task.model'; ref: string; model: string }
128
+ // A task the CLI runs beside the turn (a background subagent, a backgrounded command) settled.
129
+ // Its summary is what the CLI says came of it, and what a turn the CLI opens on its own is about.
130
+ | {
131
+ type: 'task.done';
132
+ ref: string | null;
133
+ taskId?: string | null;
134
+ summary: string | null;
135
+ ok: boolean;
136
+ usage?: ChatSubagentUsage | null;
137
+ outputFile?: string | null;
138
+ }
139
+ // What the workflow a Workflow call launched runs now, whole; a null name leaves the one said before.
140
+ | { type: 'workflow.progress'; ref: string; workflow: ChatWorkflow }
141
+ // A command or a monitor the CLI keeps running beside its turns. `ref` is the call that started it, when a
142
+ // call did; `monitor` is set when the CLI's own frame already says it is one.
143
+ | { type: 'background.started'; taskId: string; ref: string | null; monitor: boolean; description: string | null }
144
+ | { type: 'background.ended'; taskId: string }
145
+ | { type: 'usage'; contextTokens?: number; contextWindow?: number; costUsd?: number }
146
+ // What the CLI said in passing about the plan it runs on; it belongs to the machine, not the chat.
147
+ | { type: 'limits'; update: LimitsUpdate }
148
+ | { type: 'compaction'; preTokens: number | null }
149
+ | { type: 'model'; model: string }
150
+ | { type: 'note'; level: 'info' | 'warning' | 'error'; text: string }
151
+ // `native` is the CLI's own name for where the turn ended, which a fork of the chat is cut at.
152
+ | {
153
+ type: 'turn.done';
154
+ promptId?: string;
155
+ state: 'done' | 'aborted' | 'error';
156
+ costUsd: number;
157
+ error?: string;
158
+ native?: { turnId?: string; lastUuid?: string };
159
+ limit?: ChatTurnLimit;
160
+ }
161
+ // The CLI could not be reached or refused the request; the turn ends and the chat needs a new one.
162
+ | { type: 'failed'; message: string; processAlive?: boolean }
163
+ // `stderr` is the last of what the CLI wrote there, for an exit with an error code.
164
+ | { type: 'exit'; exitCode: number | null; stderr?: string };
165
+
166
+ export interface BackendHost {
167
+ onEvent(event: BackendEvent): void;
168
+ }
169
+
170
+ export interface ChatBackend {
171
+ readonly acknowledgesTurns?: boolean;
172
+ readonly running: boolean;
173
+ // The CLI's process while it runs, so the process panel can put its tree under the node.
174
+ readonly pid: number | null;
175
+ // Starts the process if it has not started; resolves once the CLI can take a turn.
176
+ start(): Promise<void>;
177
+ sendTurn(input: TurnInput): void;
178
+ compact(): void;
179
+ interrupt(): void;
180
+ // False when nothing waits under that id, so the caller can answer the client with an error.
181
+ respondApproval(requestId: string, decision: ApprovalDecision, message?: string): boolean;
182
+ respondQuestion(requestId: string, answers: Record<string, string>): boolean;
183
+ // Ends one command or monitor the CLI runs beside its turns, for a protocol that can name one.
184
+ stopTask?(taskId: string): void;
185
+ // Turns down a request the CLI still holds after a person stopped the turn, for a protocol that leaves one open past it.
186
+ declineRequest?(requestId: string, message: string): boolean;
187
+ // Drops an asynchronous question without telling the CLI, for a protocol that keeps one waiting.
188
+ dismissRequest?(requestId: string): boolean;
189
+ // Gives the CLI's own thread the name the chat got, for a protocol whose thread list and resume carry one.
190
+ setTitle?(title: string): void;
191
+ // What this CLI would run right now, for a protocol that answers the question itself.
192
+ listSkills?(): Promise<ChatSkill[]>;
193
+ // One page of a thread the CLI keeps, asked of this running process rather than of a new one.
194
+ listThreadItems?(params: { threadId: string; cursor?: string; limit: number; sortDirection: 'asc' | 'desc' }): Promise<unknown>;
195
+ // Closes the input and lets the CLI leave on its own; `dispose` ends it.
196
+ stop(): void;
197
+ // Ends the CLI and everything it started; settles once it exited or was sent the SIGKILL.
198
+ dispose(): Promise<void>;
199
+ }
@@ -0,0 +1,42 @@
1
+ import type { AgentKind, ChatBackgroundTask, ChatItem, ChatSubagentItem, ChatToolItem } from '@adecore/agent-contracts';
2
+
3
+ export type BackgroundWork = ChatSubagentItem | ChatToolItem;
4
+
5
+ /*
6
+ * Work a chat's CLI goes on with after the turn that started it ended: a subagent it runs in the
7
+ * background (Claude's `run_in_background`, every agent Codex spawns) and a Claude workflow, whose call
8
+ * answers at launch. A command or a monitor sent to the background is not work of this kind but a
9
+ * `ChatBackgroundTask` in the chat's info, held to `BACKGROUND_COMMAND_LIMIT_MS`; a row the host drew for
10
+ * work it delegated itself (origin `ruimte` on the wire) is that work's own.
11
+ */
12
+ export function isBackgroundWork(item: ChatItem): item is BackgroundWork {
13
+ return (item.kind === 'subagent' && item.background && item.origin !== 'ruimte') || (item.kind === 'tool' && item.name === 'Workflow');
14
+ }
15
+
16
+ export function runsInBackground(item: BackgroundWork): boolean {
17
+ return item.kind === 'subagent' ? item.status === 'running' : item.state === 'running';
18
+ }
19
+
20
+ export function runningInBackground(items: readonly ChatItem[]): BackgroundWork[] {
21
+ return items.filter((item): item is BackgroundWork => isBackgroundWork(item) && runsInBackground(item));
22
+ }
23
+
24
+ /*
25
+ * How long a command or a monitor still running in the background holds a child's task once nothing
26
+ * else does, since a dev server never ends. Subagents and workflows have no such limit.
27
+ */
28
+ export const BACKGROUND_COMMAND_LIMIT_MS = 30 * 60_000;
29
+
30
+ export function commandLabel(task: ChatBackgroundTask): string {
31
+ return task.command ?? (task.description === '' ? task.id : task.description);
32
+ }
33
+
34
+ /*
35
+ * Whether the CLI opens a turn of its own once its background work ended, to tell what came of it.
36
+ * Claude Code 2.1.282 does after every task notification, a command's included; a command a subagent
37
+ * started first takes that subagent up again, which then notifies the main agent. Codex 0.156.1
38
+ * reports a spawned agent completed and opens nothing.
39
+ */
40
+ export function reportsOnBackgroundWork(provider: AgentKind): boolean {
41
+ return provider === 'claude';
42
+ }
@@ -0,0 +1,166 @@
1
+ import { mkdir, readFile, rm } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { CHAT_BOOKMARK_LIMITS, ChatBookmarksSchema, clipText, type ChatBookmark } from '@adecore/agent-contracts';
4
+ import { z } from 'zod';
5
+ import { isNotFound, writeAtomic } from '../fs.ts';
6
+ import { KeyedSerializer } from '../serializer.ts';
7
+ import { ChatError } from './errors.ts';
8
+
9
+ const SUFFIX = '.bookmarks.json';
10
+
11
+ const BookmarkFileSchema = z.object({ version: z.literal(1), bookmarks: ChatBookmarksSchema });
12
+
13
+ export function bookmarkFileName(chatId: string): string {
14
+ return `${encodeURIComponent(chatId)}${SUFFIX}`;
15
+ }
16
+
17
+ /* Whether a name in the chats folder is a bookmarks file, which the chat records beside it must not count as a chat. */
18
+ export function isBookmarkFileName(name: string): boolean {
19
+ return name.endsWith(SUFFIX);
20
+ }
21
+
22
+ export type BookmarkListener = (chatId: string, bookmarks: ChatBookmark[]) => void;
23
+
24
+ /* A name as it is kept: trimmed, and none at all when nothing is left. */
25
+ function cleanName(name: string | undefined): string | undefined {
26
+ const trimmed = name === undefined ? undefined : clipText(name.trim(), CHAT_BOOKMARK_LIMITS.name);
27
+ return trimmed === undefined || trimmed === '' ? undefined : trimmed;
28
+ }
29
+
30
+ function named(bookmark: ChatBookmark, name: string | undefined): ChatBookmark {
31
+ const { name: _previous, ...rest } = bookmark;
32
+ return name === undefined ? rest : { ...rest, name };
33
+ }
34
+
35
+ /*
36
+ * The bookmarks of every chat, one file per chat beside its record under `<home>/chats`, apart
37
+ * from the record, which is rewritten on every turn. One chain per chat puts two clients' clicks
38
+ * after each other, and every change is told to the listeners from inside that chain, so the lists
39
+ * go out in the order they were written.
40
+ */
41
+ export class BookmarkStore {
42
+ readonly dir: string;
43
+ private readonly writes = new KeyedSerializer();
44
+ private readonly listeners = new Set<BookmarkListener>();
45
+
46
+ constructor(home: string) {
47
+ this.dir = join(home, 'chats');
48
+ }
49
+
50
+ listen(listener: BookmarkListener): () => void {
51
+ this.listeners.add(listener);
52
+ return () => {
53
+ this.listeners.delete(listener);
54
+ };
55
+ }
56
+
57
+ /* The bookmarks of one chat in the order they were made, after any write still on its way. */
58
+ read(chatId: string): Promise<ChatBookmark[]> {
59
+ return this.writes.run(chatId, () => this.readFile(chatId));
60
+ }
61
+
62
+ /* Marks a message; one that already has a bookmark keeps it, and takes the name when one is given. */
63
+ add(chatId: string, input: { itemId: string; excerpt: string; name?: string }, now: number): Promise<ChatBookmark[]> {
64
+ return this.change(chatId, (bookmarks) => {
65
+ const name = cleanName(input.name);
66
+ const existing = bookmarks.find((bookmark) => bookmark.itemId === input.itemId);
67
+ if (existing) {
68
+ return name === undefined ? null : bookmarks.map((bookmark) => (bookmark === existing ? named(bookmark, name) : bookmark));
69
+ }
70
+ if (bookmarks.length >= CHAT_BOOKMARK_LIMITS.perChat) {
71
+ throw new ChatError('too-many-bookmarks', `This chat already keeps ${CHAT_BOOKMARK_LIMITS.perChat} bookmarks; remove one first`);
72
+ }
73
+ const excerpt = clipText(input.excerpt, CHAT_BOOKMARK_LIMITS.excerpt);
74
+ return [...bookmarks, named({ itemId: input.itemId, excerpt, createdAt: now }, name)];
75
+ });
76
+ }
77
+
78
+ rename(chatId: string, itemId: string, name: string): Promise<ChatBookmark[]> {
79
+ return this.change(chatId, (bookmarks) => {
80
+ if (!bookmarks.some((bookmark) => bookmark.itemId === itemId)) {
81
+ throw new ChatError('bookmark-not-found', `This chat has no bookmark on ${itemId}`);
82
+ }
83
+ return bookmarks.map((bookmark) => (bookmark.itemId === itemId ? named(bookmark, cleanName(name)) : bookmark));
84
+ });
85
+ }
86
+
87
+ remove(chatId: string, itemId: string): Promise<ChatBookmark[]> {
88
+ return this.change(chatId, (bookmarks) =>
89
+ bookmarks.some((bookmark) => bookmark.itemId === itemId) ? bookmarks.filter((bookmark) => bookmark.itemId !== itemId) : null
90
+ );
91
+ }
92
+
93
+ /* The bookmarks go with the chat or with a clear of it. A file that no longer parses goes too, since nothing could show it again. */
94
+ removeChat(chatId: string): Promise<void> {
95
+ return this.writes.run(chatId, async () => {
96
+ const had = await this.readFile(chatId).then(
97
+ (bookmarks) => bookmarks.length > 0,
98
+ () => true
99
+ );
100
+ await rm(join(this.dir, bookmarkFileName(chatId)), { force: true });
101
+ if (had) {
102
+ this.tell(chatId, []);
103
+ }
104
+ });
105
+ }
106
+
107
+ /* A fork keeps the bookmarks on the messages it copied, which are the ones up to where it was cut. */
108
+ async copyChat(fromChatId: string, toChatId: string, keep: (itemId: string) => boolean): Promise<void> {
109
+ const kept = (await this.read(fromChatId)).filter((bookmark) => keep(bookmark.itemId));
110
+ if (kept.length === 0) {
111
+ return;
112
+ }
113
+ await this.writes.run(toChatId, async () => {
114
+ await this.writeFile(toChatId, kept);
115
+ this.tell(toChatId, kept);
116
+ });
117
+ }
118
+
119
+ /* One read, one decision and one write in the chat's chain; a change that returns null changes nothing and tells nobody. */
120
+ private change(chatId: string, decide: (bookmarks: ChatBookmark[]) => ChatBookmark[] | null): Promise<ChatBookmark[]> {
121
+ return this.writes.run(chatId, async () => {
122
+ const bookmarks = await this.readFile(chatId);
123
+ const next = decide(bookmarks);
124
+ if (next === null) {
125
+ return bookmarks;
126
+ }
127
+ await this.writeFile(chatId, next);
128
+ this.tell(chatId, next);
129
+ return next;
130
+ });
131
+ }
132
+
133
+ private tell(chatId: string, bookmarks: ChatBookmark[]): void {
134
+ for (const listener of this.listeners) {
135
+ listener(chatId, bookmarks);
136
+ }
137
+ }
138
+
139
+ private async readFile(chatId: string): Promise<ChatBookmark[]> {
140
+ let raw: string;
141
+ try {
142
+ raw = await readFile(join(this.dir, bookmarkFileName(chatId)), 'utf8');
143
+ } catch (e) {
144
+ if (isNotFound(e)) {
145
+ return [];
146
+ }
147
+ throw e;
148
+ }
149
+ // A file that does not parse is refused rather than read as empty, which the next write would make true.
150
+ const parsed = BookmarkFileSchema.safeParse(JSON.parse(raw));
151
+ if (!parsed.success) {
152
+ throw new Error(`The bookmarks file of chat ${chatId} is not valid: ${parsed.error.issues[0]?.message ?? 'unknown problem'}`);
153
+ }
154
+ return parsed.data.bookmarks;
155
+ }
156
+
157
+ private async writeFile(chatId: string, bookmarks: readonly ChatBookmark[]): Promise<void> {
158
+ const path = join(this.dir, bookmarkFileName(chatId));
159
+ if (bookmarks.length === 0) {
160
+ await rm(path, { force: true });
161
+ return;
162
+ }
163
+ await mkdir(this.dir, { recursive: true, mode: 0o700 });
164
+ await writeAtomic(path, JSON.stringify({ version: 1, bookmarks }));
165
+ }
166
+ }