@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
@@ -0,0 +1,85 @@
1
+ import { lstat, mkdir, readdir, readlink, symlink } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+ import { isNotFound } from '../../fs.ts';
4
+
5
+ /*
6
+ * A Codex shadow home: a folder of its own for one login, sharing everything else with a Codex home
7
+ * by symlink, so two accounts continue each other's threads. Codex needs its login in `auth.json`
8
+ * (`cli_auth_credentials_store = "file"`) for this; a login in the keychain has no folder to be in.
9
+ */
10
+
11
+ /* Made in the shared home before anything is linked, so the first thread of either account already lands there. */
12
+ const SHARED_FOLDERS = ['sessions', 'archived_sessions', 'skills', 'rules'];
13
+
14
+ /* The login, which is the whole point of the shadow home. */
15
+ const PRIVATE_ENTRIES = new Set(['auth.json']);
16
+
17
+ /*
18
+ * What one Codex process keeps for itself: what the models of this login are, caches, logs, and the
19
+ * sockets of an app-server that answers for the account that started it.
20
+ */
21
+ const LOCAL_ENTRIES = new Set(['models_cache.json', 'cache', 'log', 'logs', 'tmp', '.tmp', 'app-server-daemon', 'app-server-control', 'ipc']);
22
+
23
+ /* A SQLite journal comes and goes with its database, so a link to one would soon point at nothing. */
24
+ function isJournal(name: string): boolean {
25
+ return /-(wal|shm|journal)$/.test(name);
26
+ }
27
+
28
+ export interface ShadowHomeReport {
29
+ linked: string[];
30
+ // Entries of the shadow home in the way of a link: a real file or a link somewhere else. Left alone.
31
+ unshared: string[];
32
+ // A private entry that is a link, so this account would sign in as another one.
33
+ sharedLogin: boolean;
34
+ }
35
+
36
+ export class ShadowHomeError extends Error {}
37
+
38
+ type Existing = { kind: 'missing' } | { kind: 'link'; target: string } | { kind: 'real' };
39
+
40
+ async function existing(path: string): Promise<Existing> {
41
+ try {
42
+ const stats = await lstat(path);
43
+ return stats.isSymbolicLink() ? { kind: 'link', target: await readlink(path) } : { kind: 'real' };
44
+ } catch (e) {
45
+ if (isNotFound(e)) {
46
+ return { kind: 'missing' };
47
+ }
48
+ throw e;
49
+ }
50
+ }
51
+
52
+ /*
53
+ * Links every shared entry of `home` into `shadow` that is not there yet. Never overwrites and never
54
+ * removes anything, so it runs again whenever it likes and picks up what Codex added to `home` since.
55
+ * Both folders have to exist; the person makes them, the way they sign in.
56
+ */
57
+ export async function prepareShadowHome(home: string, shadow: string): Promise<ShadowHomeReport> {
58
+ const shared = resolve(home);
59
+ const own = resolve(shadow);
60
+ if (shared === own) {
61
+ throw new ShadowHomeError('The shadow home has to be another folder than the config folder');
62
+ }
63
+ for (const folder of SHARED_FOLDERS) {
64
+ await mkdir(join(shared, folder), { recursive: true });
65
+ }
66
+ const report: ShadowHomeReport = { linked: [], unshared: [], sharedLogin: false };
67
+ for (const name of PRIVATE_ENTRIES) {
68
+ if ((await existing(join(own, name))).kind === 'link') {
69
+ report.sharedLogin = true;
70
+ }
71
+ }
72
+ const names = (await readdir(shared)).filter((name) => !PRIVATE_ENTRIES.has(name) && !LOCAL_ENTRIES.has(name) && !isJournal(name));
73
+ for (const name of names.sort()) {
74
+ const target = join(shared, name);
75
+ const link = join(own, name);
76
+ const found = await existing(link);
77
+ if (found.kind === 'missing') {
78
+ await symlink(target, link);
79
+ report.linked.push(name);
80
+ } else if (found.kind === 'real' || resolve(own, found.target) !== target) {
81
+ report.unshared.push(name);
82
+ }
83
+ }
84
+ return report;
85
+ }
@@ -0,0 +1,106 @@
1
+ import type { AgentKind } from '@adecore/agent-contracts';
2
+ import { withTimeout } from '../../async.ts';
3
+ import { CodexTransport, type CodexClientInfo } from '../../chat/codex-transport.ts';
4
+ import { runProcess } from '../../run-process.ts';
5
+
6
+ /* A CLI that has not said who is signed in by now is not going to; the next pass asks again. */
7
+ const ASK_TIMEOUT_MS = 20_000;
8
+
9
+ /* Who a CLI says is signed in, from the CLI itself; the host never opens a credential. */
10
+ export interface AccountReading {
11
+ signedIn: boolean;
12
+ email: string | null;
13
+ plan: string | null;
14
+ organization: string | null;
15
+ }
16
+
17
+ export type AskAccount = (kind: AgentKind, command: readonly string[], env: Record<string, string | undefined>) => Promise<AccountReading>;
18
+
19
+ function text(value: unknown): string | null {
20
+ return typeof value === 'string' && value !== '' ? value : null;
21
+ }
22
+
23
+ function isRecord(value: unknown): value is Record<string, unknown> {
24
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
25
+ }
26
+
27
+ /* `claude auth status --json`, as Claude Code 2.1.282 prints it: `loggedIn`, `email`, `orgName`, `subscriptionType`. */
28
+ export function readClaudeAuthStatus(output: string): AccountReading {
29
+ let status: unknown;
30
+ try {
31
+ status = JSON.parse(output);
32
+ } catch {
33
+ throw new Error('Claude Code did not say who is signed in');
34
+ }
35
+ if (!isRecord(status) || typeof status.loggedIn !== 'boolean') {
36
+ throw new Error('Claude Code did not say who is signed in');
37
+ }
38
+ return {
39
+ signedIn: status.loggedIn,
40
+ email: text(status.email),
41
+ plan: text(status.subscriptionType),
42
+ organization: text(status.orgName)
43
+ };
44
+ }
45
+
46
+ /*
47
+ * The app-server's `account/read`, as Codex 0.157 answers it: `account` is null while nobody signed in,
48
+ * `{ type: 'chatgpt', email, planType }` for a plan and `{ type: 'apiKey' }` for a key. A model
49
+ * provider without OpenAI's login needs no account at all, which `requiresOpenaiAuth` says.
50
+ */
51
+ export function readCodexAccount(answer: unknown): AccountReading {
52
+ if (!isRecord(answer)) {
53
+ throw new Error('Codex did not say who is signed in');
54
+ }
55
+ const account = answer.account;
56
+ if (!isRecord(account)) {
57
+ return { signedIn: answer.requiresOpenaiAuth === false, email: null, plan: null, organization: null };
58
+ }
59
+ return { signedIn: true, email: text(account.email), plan: text(account.planType), organization: null };
60
+ }
61
+
62
+ async function askClaude(command: readonly string[], env: Record<string, string | undefined>): Promise<AccountReading> {
63
+ const { stdout } = await runProcess([...command, 'auth', 'status', '--json'], {
64
+ env,
65
+ timeoutMs: ASK_TIMEOUT_MS,
66
+ timeoutMessage: 'Claude Code did not answer in time'
67
+ });
68
+ // Signed out exits 1 and still prints the status, so the output decides and the exit code does not.
69
+ return readClaudeAuthStatus(stdout);
70
+ }
71
+
72
+ async function askCodex(command: readonly string[], env: Record<string, string | undefined>, client: CodexClientInfo): Promise<AccountReading> {
73
+ const transport = new CodexTransport({
74
+ command: [...command],
75
+ cwd: process.cwd(),
76
+ env: env as Record<string, string>,
77
+ onFrame: () => undefined,
78
+ onExit: () => undefined
79
+ });
80
+ try {
81
+ await withTimeout(
82
+ transport.request('initialize', { clientInfo: client, capabilities: { experimentalApi: true } }),
83
+ ASK_TIMEOUT_MS,
84
+ 'Codex did not answer in time'
85
+ );
86
+ transport.notify('initialized', {});
87
+ const answer = await withTimeout(transport.request('account/read', { refreshToken: false }), ASK_TIMEOUT_MS, 'Codex did not answer in time');
88
+ return readCodexAccount(answer);
89
+ } finally {
90
+ transport.end();
91
+ transport.kill();
92
+ }
93
+ }
94
+
95
+ /* Starts the CLI in the account's environment and asks it; only Claude Code and Codex can say. */
96
+ export function askAccountAs(client: CodexClientInfo): AskAccount {
97
+ return (kind, command, env) => {
98
+ if (kind === 'claude') {
99
+ return askClaude(command, env);
100
+ }
101
+ if (kind === 'codex') {
102
+ return askCodex(command, env, client);
103
+ }
104
+ return Promise.reject(new Error(`${kind} cannot say who is signed in`));
105
+ };
106
+ }
@@ -0,0 +1,158 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { ProviderAccountIdSchema, ProviderAccountSchema, type AgentKind, type ProviderAccount, type ProviderAccountMap } from '@adecore/agent-contracts';
4
+ import { CodedError } from '../../coded-error.ts';
5
+ import { isNotFound, writeAtomic } from '../../fs.ts';
6
+ import type { ChatProvider } from '../provider.ts';
7
+ import {
8
+ accountFolder,
9
+ accountProblem,
10
+ defaultFolder,
11
+ expandHome,
12
+ folderVariablesOf,
13
+ isDefaultAccount,
14
+ isKnownKind,
15
+ readAccount,
16
+ withDefaults,
17
+ type Env
18
+ } from './accounts.ts';
19
+ import { DEFAULT_ACCOUNTS_HOST, redactVariables, type AccountsHost } from './variables.ts';
20
+
21
+ const FILE_VERSION = 1;
22
+
23
+ export function accountsPath(home: string): string {
24
+ return join(home, 'providers.json');
25
+ }
26
+
27
+ /*
28
+ * The accounts as the file holds them. A known kind is a parsed account; an unknown one is the entry
29
+ * exactly as it was read, so a newer version's fields survive this one writing the file.
30
+ */
31
+ export type StoredAccounts = Record<string, unknown>;
32
+
33
+ /* Every entry that does not parse is dropped on its own; a file that is not JSON at all reads as none. */
34
+ export async function readAccounts(
35
+ path: string,
36
+ providerOf: (kind: AgentKind) => ChatProvider,
37
+ env: Env,
38
+ host: AccountsHost = DEFAULT_ACCOUNTS_HOST
39
+ ): Promise<StoredAccounts> {
40
+ let parsed: unknown;
41
+ try {
42
+ parsed = JSON.parse(await readFile(path, 'utf8'));
43
+ } catch (e) {
44
+ if (isNotFound(e) || e instanceof SyntaxError) {
45
+ return {};
46
+ }
47
+ throw e;
48
+ }
49
+ const entries = typeof parsed === 'object' && parsed !== null ? (parsed as { accounts?: unknown }).accounts : null;
50
+ if (typeof entries !== 'object' || entries === null || Array.isArray(entries)) {
51
+ return {};
52
+ }
53
+ const stored: StoredAccounts = {};
54
+ for (const [id, raw] of Object.entries(entries)) {
55
+ if (!ProviderAccountIdSchema.safeParse(id).success) {
56
+ continue;
57
+ }
58
+ const account = readAccount(id, raw, providerOf, env, host);
59
+ if (account !== null) {
60
+ stored[id] = account;
61
+ }
62
+ }
63
+ return stored;
64
+ }
65
+
66
+ export async function writeAccounts(path: string, stored: StoredAccounts): Promise<void> {
67
+ await writeAtomic(path, `${JSON.stringify({ version: FILE_VERSION, accounts: stored }, null, 2)}\n`, 0o600);
68
+ }
69
+
70
+ /* What a client reads of an entry of a kind it may not know either: the fields this version knows. */
71
+ function wireAccount(entry: unknown): ProviderAccount {
72
+ const parsed = ProviderAccountSchema.safeParse(entry);
73
+ if (!parsed.success) {
74
+ return { kind: String((entry as { kind?: unknown }).kind) };
75
+ }
76
+ return parsed.data.env === undefined ? parsed.data : { ...parsed.data, env: redactVariables(parsed.data.env) };
77
+ }
78
+
79
+ export function wireAccounts(stored: StoredAccounts): ProviderAccountMap {
80
+ return withDefaults(Object.fromEntries(Object.entries(stored).map(([id, entry]) => [id, wireAccount(entry)])));
81
+ }
82
+
83
+ export class InvalidAccountError extends CodedError<'invalid-account'> {
84
+ constructor(message: string) {
85
+ super('invalid-account', message);
86
+ }
87
+ }
88
+
89
+ function storedFolders(entry: unknown): Pick<ProviderAccount, 'home' | 'shadowHome'> {
90
+ const parsed = ProviderAccountSchema.safeParse(entry);
91
+ return parsed.success ? { home: parsed.data.home, shadowHome: parsed.data.shadowHome } : {};
92
+ }
93
+
94
+ /*
95
+ * What is wrong with the folders of an account a person points somewhere new, or null. An account
96
+ * whose folders did not change is left alone, so one whose folder went missing can still be renamed.
97
+ */
98
+ function linkProblem(
99
+ id: string,
100
+ account: ProviderAccount,
101
+ before: unknown,
102
+ provider: ChatProvider,
103
+ env: Env,
104
+ isDirectory: (path: string) => boolean
105
+ ): string | null {
106
+ if (isDefaultAccount(id, account) || provider.home === undefined || account.home === undefined) {
107
+ return null;
108
+ }
109
+ const was = storedFolders(before);
110
+ if (was.home === account.home && was.shadowHome === account.shadowHome) {
111
+ return null;
112
+ }
113
+ for (const folder of [account.home, account.shadowHome]) {
114
+ if (folder !== undefined && !isDirectory(expandHome(folder, env))) {
115
+ return `${folder} is not a folder on this machine`;
116
+ }
117
+ }
118
+ if (accountFolder(id, account, provider, env) === defaultFolder(provider, env)) {
119
+ return `${defaultFolder(provider, env)} is the folder of the default ${provider.name} account`;
120
+ }
121
+ return null;
122
+ }
123
+
124
+ /*
125
+ * The whole map a person saved, as the file will hold it. A mistake in an account of a known kind
126
+ * refuses the save; an account of an unknown kind keeps the entry the file had under its id, which a
127
+ * client that knows the kind no better than this host could only have cut short. Sensitive values
128
+ * are still in it: the caller puts them in the keychain before the file is written.
129
+ */
130
+ export function acceptAccounts(
131
+ saved: ProviderAccountMap,
132
+ before: StoredAccounts,
133
+ providerOf: (kind: AgentKind) => ChatProvider,
134
+ env: Env,
135
+ isDirectory: (path: string) => boolean,
136
+ host: AccountsHost = DEFAULT_ACCOUNTS_HOST
137
+ ): StoredAccounts {
138
+ const stored: StoredAccounts = {};
139
+ const reserved = { host, folderVariables: folderVariablesOf(providerOf) };
140
+ for (const [id, account] of Object.entries(withDefaults(saved))) {
141
+ if (!isKnownKind(account.kind)) {
142
+ if (isKnownKind(id)) {
143
+ throw new InvalidAccountError(`"${id}" is the id of the default ${id} account`);
144
+ }
145
+ const kept = before[id];
146
+ const keptKind = typeof kept === 'object' && kept !== null ? (kept as { kind?: unknown }).kind : undefined;
147
+ stored[id] = keptKind === account.kind ? kept : account;
148
+ continue;
149
+ }
150
+ const provider = providerOf(account.kind);
151
+ const problem = accountProblem(id, account, provider, env, reserved) ?? linkProblem(id, account, before[id], provider, env, isDirectory);
152
+ if (problem !== null) {
153
+ throw new InvalidAccountError(`${id}: ${problem}`);
154
+ }
155
+ stored[id] = account;
156
+ }
157
+ return stored;
158
+ }
@@ -0,0 +1,48 @@
1
+ import type { AgentKind, ProviderAccountMap, ProviderInfo } from '@adecore/agent-contracts';
2
+ import { providerFor } from '../registry.ts';
3
+ import { ProviderAccountsService } from './service.ts';
4
+ import { accountsPath, writeAccounts } from './store.ts';
5
+ import type { SecretStore } from './variables.ts';
6
+
7
+ /* A keychain in memory; `values` is what it holds, by key. */
8
+ export function memorySecrets(values = new Map<string, string>()): SecretStore & { values: Map<string, string> } {
9
+ return {
10
+ values,
11
+ read: async (key) => values.get(key) ?? null,
12
+ write: async (key, value) => {
13
+ values.set(key, value);
14
+ },
15
+ remove: async (key) => {
16
+ values.delete(key);
17
+ }
18
+ };
19
+ }
20
+
21
+ /*
22
+ * The accounts of a machine as a test wants them: written to `providers.json` under `home` as
23
+ * they are, with only the folders in `folders` on disk, and every CLI signed in without being asked.
24
+ */
25
+ export async function testAccounts(options: {
26
+ home: string;
27
+ env: Record<string, string | undefined>;
28
+ accounts: ProviderAccountMap;
29
+ // The folders that exist; a test takes one away to see what a missing folder does.
30
+ folders: Set<string>;
31
+ }): Promise<ProviderAccountsService> {
32
+ const service = new ProviderAccountsService({
33
+ home: options.home,
34
+ providers: {
35
+ list: async () => (['claude', 'codex'] as AgentKind[]).map((kind) => ({ kind, installed: true }) as ProviderInfo),
36
+ get: (kind) => providerFor(kind)
37
+ },
38
+ env: options.env,
39
+ ask: async () => ({ signedIn: true, email: null, plan: null, organization: null }),
40
+ folderExists: (path) => options.folders.has(path),
41
+ prepareShadow: async () => ({ linked: [], unshared: [], sharedLogin: false }),
42
+ secrets: memorySecrets()
43
+ });
44
+ // Written and not saved, so an account whose folder is already gone is there the way a person left it.
45
+ await writeAccounts(accountsPath(options.home), options.accounts);
46
+ await service.load();
47
+ return service;
48
+ }
@@ -0,0 +1,132 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { resolve } from 'node:path';
3
+ import type { ProviderAccountVariable } from '@adecore/agent-contracts';
4
+ import { CodedError } from '../../coded-error.ts';
5
+ import { runProcess } from '../../run-process.ts';
6
+
7
+ /*
8
+ * The variables of an account, and where a sensitive value is kept: the keychain of this machine,
9
+ * never `providers.json` and never the wire.
10
+ */
11
+
12
+ export class SecretsUnavailableError extends CodedError<'secrets-unavailable'> {
13
+ constructor() {
14
+ super('secrets-unavailable', 'This machine has no keychain to keep a sensitive value in.');
15
+ }
16
+ }
17
+
18
+ /* What keeps the sensitive values; a test keeps them in memory. */
19
+ export interface SecretStore {
20
+ /* Null for a key it has nothing under. */
21
+ read(key: string): Promise<string | null>;
22
+ write(key: string, value: string): Promise<void>;
23
+ /* Removing a key it has nothing under is not an error. */
24
+ remove(key: string): Promise<void>;
25
+ }
26
+
27
+ export function secretKey(accountId: string, name: string): string {
28
+ return `${accountId}/${name}`;
29
+ }
30
+
31
+ /* The names that decide which folder and which executable a CLI talks to. */
32
+ const RESERVED = new Set(['HOME', 'PATH']);
33
+
34
+ /* The app that runs the CLIs, as far as their accounts go: what a person calls it and which variables it sets itself. */
35
+ export interface AccountsHost {
36
+ name: string;
37
+ // No account may set a variable with one of these prefixes, since the app sets them for every CLI.
38
+ variablePrefixes: readonly string[];
39
+ // Keeps the app's keychain values apart from any other app's.
40
+ keychainPrefix: string;
41
+ }
42
+
43
+ export const DEFAULT_ACCOUNTS_HOST: AccountsHost = { name: 'the app', variablePrefixes: [], keychainPrefix: 'agents' };
44
+
45
+ /* The variables no account may set: the host's own and the ones that point a CLI at its folder. */
46
+ export interface ReservedVariables {
47
+ host: AccountsHost;
48
+ folderVariables: readonly string[];
49
+ }
50
+
51
+ export function isReservedName(name: string, reserved: ReservedVariables): boolean {
52
+ return RESERVED.has(name) || reserved.host.variablePrefixes.some((prefix) => name.startsWith(prefix)) || reserved.folderVariables.includes(name);
53
+ }
54
+
55
+ /* `security` prints anything else as hex, so a read could not tell such a value from its own encoding. */
56
+ function isPrintableAscii(value: string): boolean {
57
+ return /^[\x20-\x7e]*$/.test(value);
58
+ }
59
+
60
+ /* What is wrong with the variables of an account, or null when nothing is. A secret that is only redacted is judged by the service. */
61
+ export function variablesProblem(variables: readonly ProviderAccountVariable[], reserved: ReservedVariables): string | null {
62
+ const seen = new Set<string>();
63
+ for (const variable of variables) {
64
+ if (seen.has(variable.name)) {
65
+ return `${variable.name} is set twice`;
66
+ }
67
+ seen.add(variable.name);
68
+ if (isReservedName(variable.name, reserved)) {
69
+ return `${variable.name} is set by ${reserved.host.name} itself`;
70
+ }
71
+ if (variable.sensitive && !isPrintableAscii(variable.value)) {
72
+ return `The value of ${variable.name} can only hold printable ASCII characters`;
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+
78
+ /* The variables as a client may read them: a sensitive value never leaves the machine. */
79
+ export function redactVariables(variables: readonly ProviderAccountVariable[]): ProviderAccountVariable[] {
80
+ return variables.map((variable) => (variable.sensitive ? { name: variable.name, value: '', sensitive: true, valueRedacted: true } : variable));
81
+ }
82
+
83
+ /* A data folder of its own (a development one beside an installed one) keeps its values apart from the other's. */
84
+ export function keychainService(host: AccountsHost, home: string): string {
85
+ return `${host.keychainPrefix}-provider-env-${createHash('sha256').update(resolve(home)).digest('hex').slice(0, 12)}`;
86
+ }
87
+ // `security` exits with this when no item matches.
88
+ const NOT_FOUND = 44;
89
+
90
+ async function run(args: string[], stdin?: string): Promise<{ code: number; stdout: string; stderr: string }> {
91
+ const { exitCode, stdout, stderr } = await runProcess(['security', ...args], { ...(stdin === undefined ? {} : { stdin }) });
92
+ return { code: exitCode ?? 1, stdout, stderr };
93
+ }
94
+
95
+ /*
96
+ * Generic passwords in the login keychain, through the `security` CLI. A value goes in over stdin as
97
+ * hex in its interactive mode: on the command line any process could read it, and the password
98
+ * prompt cuts it at 128 characters.
99
+ */
100
+ export function keychainSecrets(service: string): SecretStore {
101
+ return {
102
+ read: async (key) => {
103
+ const { code, stdout, stderr } = await run(['find-generic-password', '-s', service, '-a', key, '-w']);
104
+ if (code === NOT_FOUND) {
105
+ return null;
106
+ }
107
+ if (code !== 0) {
108
+ throw new Error(`The keychain did not give ${key}: ${stderr.trim()}`);
109
+ }
110
+ return stdout.endsWith('\n') ? stdout.slice(0, -1) : stdout;
111
+ },
112
+ write: async (key, value) => {
113
+ const hex = Buffer.from(value, 'utf8').toString('hex');
114
+ const quoted = `"${key.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
115
+ const { code, stderr } = await run(['-i'], `add-generic-password -U -s ${service} -a ${quoted} -X ${hex}\n`);
116
+ if (code !== 0) {
117
+ throw new Error(`The keychain did not take ${key}: ${stderr.trim()}`);
118
+ }
119
+ },
120
+ remove: async (key) => {
121
+ const { code, stderr } = await run(['delete-generic-password', '-s', service, '-a', key]);
122
+ if (code !== 0 && code !== NOT_FOUND) {
123
+ throw new Error(`The keychain did not remove ${key}: ${stderr.trim()}`);
124
+ }
125
+ }
126
+ };
127
+ }
128
+
129
+ /* The keychain on macOS; elsewhere none, so a sensitive variable is refused. */
130
+ export function platformSecrets(host: AccountsHost, home: string): SecretStore | null {
131
+ return process.platform === 'darwin' ? keychainSecrets(keychainService(host, home)) : null;
132
+ }
@@ -0,0 +1,90 @@
1
+ import type { ModelCatalogData, ModelCatalogProfile, ModelInfo, ModelSelection } from '@adecore/agent-contracts';
2
+ import manifest from './claude-models.json' with { type: 'json' };
3
+
4
+ // What a host builds a catalog from; one that is never replaced needs no date.
5
+ export type ModelCatalogInput = Omit<ModelCatalogData, 'updatedAt'> & { updatedAt?: string };
6
+
7
+ function timeOf(data: ModelCatalogInput): number {
8
+ return data.updatedAt === undefined ? 0 : Date.parse(data.updatedAt);
9
+ }
10
+
11
+ /*
12
+ * The models a provider offers and what each one can be asked for. Bundled as JSON so a new
13
+ * model is a data change; a profile is only needed for a new combination of options. A host may
14
+ * swap in a newer copy while it runs, never one older than the copy it was built with.
15
+ */
16
+ export class ModelCatalog {
17
+ private readonly shippedAt: number;
18
+ private data: ModelCatalogInput;
19
+
20
+ constructor(data: ModelCatalogInput = manifest as never) {
21
+ this.shippedAt = timeOf(data);
22
+ this.data = data;
23
+ }
24
+
25
+ get defaultModel(): string {
26
+ return this.data.defaultModel;
27
+ }
28
+
29
+ /* Takes a validated copy unless it is older than the shipped one; answers whether the models changed. */
30
+ replace(data: ModelCatalogData): boolean {
31
+ if (timeOf(data) < this.shippedAt) {
32
+ return false;
33
+ }
34
+ const changed = JSON.stringify(data) !== JSON.stringify(this.data);
35
+ this.data = data;
36
+ return changed;
37
+ }
38
+
39
+ list(): ModelInfo[] {
40
+ return this.data.models.map((model) => ({
41
+ slug: model.slug,
42
+ name: model.name,
43
+ badge: model.badge,
44
+ legacy: model.legacy === true,
45
+ isDefault: model.slug === this.data.defaultModel,
46
+ options: this.profileOf(model.slug)?.options ?? []
47
+ }));
48
+ }
49
+
50
+ /* What a person reads for a model, so an error in a thread names it the way the picker does. */
51
+ nameOf(slug: string): string {
52
+ return this.data.models.find((entry) => entry.slug === slug)?.name ?? slug;
53
+ }
54
+
55
+ /* Accepts a slug or an alias; unknown models answer null so the caller can decide what to do. */
56
+ resolveModel(name: string): string | null {
57
+ const model = this.data.models.find((entry) => entry.slug === name || entry.aliases?.includes(name));
58
+ return model?.slug ?? null;
59
+ }
60
+
61
+ /* Fills in defaults and drops options the model does not have, so a selection is always complete. */
62
+ normalize(selection: Partial<ModelSelection> | undefined): ModelSelection {
63
+ const model = (selection?.model && this.resolveModel(selection.model)) || this.defaultModel;
64
+ const options: ModelSelection['options'] = {};
65
+ for (const descriptor of this.profileOf(model)?.options ?? []) {
66
+ const given = selection?.options?.[descriptor.id];
67
+ if (descriptor.type === 'select') {
68
+ options[descriptor.id] =
69
+ typeof given === 'string' && descriptor.choices.some((choice) => choice.id === given) ? given : descriptor.defaultChoice;
70
+ } else {
71
+ options[descriptor.id] = typeof given === 'boolean' ? given : descriptor.defaultValue;
72
+ }
73
+ }
74
+ return { model, options };
75
+ }
76
+
77
+ contextWindowFor(selection: ModelSelection): number | null {
78
+ const profile = this.profileOf(selection.model);
79
+ if (!profile) {
80
+ return null;
81
+ }
82
+ const chosen = selection.options.contextWindow;
83
+ return (typeof chosen === 'string' ? profile.contextWindowTokens[chosen] : undefined) ?? profile.contextWindowTokens['*'] ?? null;
84
+ }
85
+
86
+ private profileOf(slug: string): ModelCatalogProfile | undefined {
87
+ const model = this.data.models.find((entry) => entry.slug === slug);
88
+ return model ? this.data.profiles[model.profile] : undefined;
89
+ }
90
+ }