@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,632 @@
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import { mkdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import {
5
+ AgentKindSchema,
6
+ type AgentKind,
7
+ type ProviderAccount,
8
+ type ProviderAccountCreatePayload,
9
+ type ProviderAccountCreateResult,
10
+ type ProviderAccountMap,
11
+ type ProviderAccounts,
12
+ type ProviderAccountStatus,
13
+ type ProviderAccountVariable,
14
+ type ProviderInfo
15
+ } from '@adecore/agent-contracts';
16
+ import { ClientSinks } from '../../client-sinks.ts';
17
+ import { errorText } from '../../error-text.ts';
18
+ import type { AgentEvent, AgentSink } from '../../events.ts';
19
+ import type { ChatProvider } from '../provider.ts';
20
+ import { accountEnv, accountFolder, canContinue, defaultFolder, expandHome, isDefaultAccount, isKnownKind, transcriptFolder, type Env } from './accounts.ts';
21
+ import { AccountError, isDefaultAccountOf, type AccountLaunches } from './launch.ts';
22
+ import { prepareShadowHome, type ShadowHomeReport } from './shadow-home.ts';
23
+ import { askAccountAs, type AskAccount } from './status.ts';
24
+ import { acceptAccounts, accountsPath, InvalidAccountError, readAccounts, wireAccounts, writeAccounts, type StoredAccounts } from './store.ts';
25
+ import { DEFAULT_ACCOUNTS_HOST, platformSecrets, secretKey, SecretsUnavailableError, type AccountsHost, type SecretStore } from './variables.ts';
26
+ import { DEFAULT_CODEX_CLIENT, type CodexClientInfo } from '../../chat/codex-transport.ts';
27
+
28
+ /* Who is signed in changes by hand and rarely; often enough to notice, rarely enough not to start CLIs all day. */
29
+ const CHECK_INTERVAL_MS = 15 * 60_000;
30
+ const MAX_BACKOFF_MS = 60 * 60_000;
31
+ // A login in a terminal is a browser round trip: quick enough to notice at once, and given up on after a while.
32
+ const LOGIN_POLL_MS = 3_000;
33
+ const LOGIN_WATCH_MS = 5 * 60_000;
34
+
35
+ export interface ProviderAccountsOptions {
36
+ // The data folder: `providers.json`, and a folder per account the host makes.
37
+ home: string;
38
+ host?: AccountsHost;
39
+ // How the host names itself to an app-server it asks who is signed in.
40
+ client?: CodexClientInfo;
41
+ providers: { list(): Promise<ProviderInfo[]>; get(kind: AgentKind): ChatProvider; enabled?(kind: AgentKind): boolean };
42
+ env?: Env;
43
+ // How to ask a CLI who is signed in; a test answers without starting one.
44
+ ask?: AskAccount;
45
+ prepareShadow?: (home: string, shadow: string) => Promise<ShadowHomeReport>;
46
+ folderExists?: (path: string) => boolean;
47
+ // What the host puts in a config folder of an account (its hooks); absent puts nothing there.
48
+ install?: (kind: AgentKind, folder: string) => Promise<void>;
49
+ // Where sensitive values are kept; null on a machine without a keychain. Absent is the platform's own.
50
+ secrets?: SecretStore | null;
51
+ now?: () => number;
52
+ // How a login watch waits between two checks; a test moves its clock instead.
53
+ sleep?: (ms: number) => Promise<void>;
54
+ }
55
+
56
+ interface LoginWatch {
57
+ deadline: number;
58
+ done: Promise<void>;
59
+ }
60
+
61
+ interface AccountState {
62
+ status: ProviderAccountStatus;
63
+ // The account as it was when this status was drawn, so a save that changes it asks again.
64
+ drawnFor: string;
65
+ backoffMs: number;
66
+ nextAt: number;
67
+ }
68
+
69
+ // Asked on every start of a CLI under an account, which is one stat and never worth an await in the way of a spawn.
70
+ function isFolder(path: string): boolean {
71
+ return statSync(path, { throwIfNoEntry: false })?.isDirectory() === true;
72
+ }
73
+
74
+ function sleep(ms: number): Promise<void> {
75
+ return new Promise((resolve) => {
76
+ setTimeout(resolve, ms).unref();
77
+ });
78
+ }
79
+
80
+ /* The part of a label an id can hold: `Work (EU)` becomes `work-eu`. */
81
+ function slugOf(label: string): string {
82
+ return label
83
+ .normalize('NFKD')
84
+ .replace(/[\u0300-\u036f]/g, '')
85
+ .toLowerCase()
86
+ .replace(/[^a-z0-9]+/g, '-')
87
+ .replace(/^-+|-+$/g, '');
88
+ }
89
+
90
+ function sensitiveNames(entry: unknown): string[] {
91
+ const variables = typeof entry === 'object' && entry !== null ? (entry as { env?: unknown }).env : undefined;
92
+ if (!Array.isArray(variables)) {
93
+ return [];
94
+ }
95
+ return variables.flatMap((variable: unknown) => {
96
+ const { name, sensitive } = (variable ?? {}) as { name?: unknown; sensitive?: unknown };
97
+ return sensitive === true && typeof name === 'string' ? [name] : [];
98
+ });
99
+ }
100
+
101
+ /* A status without the moment it was drawn, to tell whether anything a person reads changed. */
102
+ function sameStatus(left: ProviderAccountStatus, right: ProviderAccountStatus): boolean {
103
+ return JSON.stringify({ ...left, checkedAt: 0 }) === JSON.stringify({ ...right, checkedAt: 0 });
104
+ }
105
+
106
+ /*
107
+ * The accounts of every agent CLI on this machine: the list a person keeps in `providers.json`, and
108
+ * what each CLI says about who is signed in there. The list is the person's; the statuses are only
109
+ * ever the CLI's answer, held in memory and asked again on a slow clock.
110
+ */
111
+ export class ProviderAccountsService implements AccountLaunches {
112
+ private readonly path: string;
113
+ private readonly home: string;
114
+ private readonly host: AccountsHost;
115
+ private readonly providers: ProviderAccountsOptions['providers'];
116
+ private readonly env: Env;
117
+ private readonly ask: AskAccount;
118
+ private readonly prepareShadow: NonNullable<ProviderAccountsOptions['prepareShadow']>;
119
+ private readonly folderExists: NonNullable<ProviderAccountsOptions['folderExists']>;
120
+ private readonly install: ProviderAccountsOptions['install'];
121
+ private readonly secrets: SecretStore | null;
122
+ // The sensitive values by `secretKey`, read from the keychain once, since a spawn asks for them without waiting.
123
+ private readonly secretValues = new Map<string, string>();
124
+ private readonly now: () => number;
125
+ private readonly sleep: (ms: number) => Promise<void>;
126
+ private readonly watches = new Map<string, LoginWatch>();
127
+ private writes: Promise<unknown> = Promise.resolve();
128
+ private readonly sinks = new ClientSinks<AgentEvent>();
129
+ private readonly listeners = new Set<() => void>();
130
+ // When a CLI last started under each account, by id; kept in memory, so a restart forgets it.
131
+ private readonly launches = new Map<string, number>();
132
+ private readonly states = new Map<string, AccountState>();
133
+ private stored: StoredAccounts = {};
134
+ private accounts: ProviderAccountMap = wireAccounts({});
135
+ private passes: Promise<void> = Promise.resolve();
136
+ private timer: ReturnType<typeof setInterval> | null = null;
137
+
138
+ constructor(options: ProviderAccountsOptions) {
139
+ this.path = accountsPath(options.home);
140
+ this.home = options.home;
141
+ this.host = options.host ?? DEFAULT_ACCOUNTS_HOST;
142
+ this.providers = options.providers;
143
+ this.env = options.env ?? process.env;
144
+ this.ask = options.ask ?? askAccountAs(options.client ?? DEFAULT_CODEX_CLIENT);
145
+ this.prepareShadow = options.prepareShadow ?? prepareShadowHome;
146
+ this.folderExists = options.folderExists ?? isFolder;
147
+ this.install = options.install;
148
+ this.secrets = options.secrets === undefined ? platformSecrets(this.host, options.home) : options.secrets;
149
+ this.now = options.now ?? Date.now;
150
+ this.sleep = options.sleep ?? sleep;
151
+ }
152
+
153
+ async load(): Promise<void> {
154
+ this.stored = await readAccounts(this.path, (kind) => this.providers.get(kind), this.env, this.host);
155
+ this.accounts = wireAccounts(this.stored);
156
+ await this.readSecrets();
157
+ }
158
+
159
+ subscribe(clientId: string, sink: AgentSink): () => void {
160
+ return this.sinks.subscribe(clientId, sink);
161
+ }
162
+
163
+ snapshot(): ProviderAccounts {
164
+ const loginCommands: Partial<Record<AgentKind, string>> = {};
165
+ for (const kind of AgentKindSchema.options) {
166
+ const command = this.providers.get(kind).home?.loginCommand;
167
+ if (command !== undefined) {
168
+ loginCommands[kind] = command;
169
+ }
170
+ }
171
+ return {
172
+ accounts: this.accounts,
173
+ statuses: Object.entries(this.accounts).map(([id, account]) => this.statusOf(id, account)),
174
+ secretsAvailable: this.secrets !== null,
175
+ loginCommands
176
+ };
177
+ }
178
+
179
+ /* The account under an id, for a launch; null for an id this machine does not have. */
180
+ get(id: string): ProviderAccount | null {
181
+ return this.accounts[id] ?? null;
182
+ }
183
+
184
+ /*
185
+ * The environment a CLI of this kind runs in under this account. An account that cannot be used
186
+ * refuses the start, since falling back on the default account would run it on someone else's costs.
187
+ */
188
+ envFor(kind: AgentKind, id: string | undefined, baseEnv: Env): Env {
189
+ const key = id ?? kind;
190
+ const refuse = (why: string): AccountError =>
191
+ new AccountError('account-unavailable', `The account '${this.labelOf(key)}' is not available on this machine: ${why}.`);
192
+ const provider = this.providers.get(kind);
193
+ const variables = (account: ProviderAccount): Record<string, string> => {
194
+ const { values, missing } = this.variablesOf(key, account);
195
+ if (missing !== null) {
196
+ throw refuse(`the keychain does not give the value of ${missing}`);
197
+ }
198
+ return values;
199
+ };
200
+ if (isDefaultAccountOf(kind, id)) {
201
+ const account = this.accounts[kind] ?? { kind };
202
+ return accountEnv(kind, account, provider, baseEnv, variables(account));
203
+ }
204
+ const account = this.accounts[key];
205
+ if (account === undefined) {
206
+ throw refuse('no account on it has that id');
207
+ }
208
+ if (account.kind !== kind) {
209
+ throw refuse(`it is an account of ${isKnownKind(account.kind) ? this.providers.get(account.kind).name : account.kind}, not of ${provider.name}`);
210
+ }
211
+ if (account.enabled === false) {
212
+ throw refuse('it is turned off');
213
+ }
214
+ if (provider.home === undefined || account.home === undefined) {
215
+ throw refuse(`${provider.name} has no setting for its config folder`);
216
+ }
217
+ for (const folder of [account.home, account.shadowHome]) {
218
+ if (folder !== undefined && !this.folderExists(expandHome(folder, this.env))) {
219
+ throw refuse(`its folder ${folder} is missing`);
220
+ }
221
+ }
222
+ return accountEnv(key, account, provider, baseEnv, variables(account));
223
+ }
224
+
225
+ transcriptFolder(kind: AgentKind, id: string | undefined): string | null {
226
+ const key = id ?? kind;
227
+ const account = this.accounts[key];
228
+ if (account === undefined || account.kind !== kind) {
229
+ return null;
230
+ }
231
+ return transcriptFolder(key, account, this.providers.get(kind), this.env);
232
+ }
233
+
234
+ homeFolder(kind: AgentKind, id: string | undefined): string | null {
235
+ const key = id ?? kind;
236
+ const account = this.accounts[key];
237
+ if (account === undefined || account.kind !== kind) {
238
+ return null;
239
+ }
240
+ return accountFolder(key, account, this.providers.get(kind), this.env);
241
+ }
242
+
243
+ canContinue(kind: AgentKind, from: string | undefined, to: string | undefined): boolean {
244
+ const fromId = from ?? kind;
245
+ const toId = to ?? kind;
246
+ const fromAccount = this.accounts[fromId];
247
+ const toAccount = this.accounts[toId];
248
+ if (fromId === toId) {
249
+ return true;
250
+ }
251
+ if (fromAccount === undefined || toAccount === undefined || fromAccount.kind !== kind) {
252
+ return false;
253
+ }
254
+ return canContinue({ id: fromId, account: fromAccount }, { id: toId, account: toAccount }, (each) => this.providers.get(each), this.env);
255
+ }
256
+
257
+ labelOf(id: string): string {
258
+ return this.accounts[id]?.label ?? id;
259
+ }
260
+
261
+ launched(kind: AgentKind, id: string | undefined): void {
262
+ this.launches.set(id ?? kind, this.now());
263
+ }
264
+
265
+ /* When a CLI last started under this account; null when none did since the host started. */
266
+ lastLaunchAt(id: string): number | null {
267
+ return this.launches.get(id) ?? null;
268
+ }
269
+
270
+ /* Told whenever the accounts or what their CLIs said changed, after the clients were. */
271
+ listen(listener: () => void): () => void {
272
+ this.listeners.add(listener);
273
+ return () => {
274
+ this.listeners.delete(listener);
275
+ };
276
+ }
277
+
278
+ /* Checks every account once at once, and the ones due on the clock after that. */
279
+ start(): void {
280
+ this.timer ??= setInterval(() => void this.queue(false), CHECK_INTERVAL_MS);
281
+ void this.queue(false);
282
+ }
283
+
284
+ stop(): void {
285
+ if (this.timer !== null) {
286
+ clearInterval(this.timer);
287
+ this.timer = null;
288
+ }
289
+ }
290
+
291
+ /* Answers at once; an account that changed reads `checking` until its CLI answered, which arrives as `accounts.changed`. */
292
+ save(accounts: ProviderAccountMap): Promise<ProviderAccounts> {
293
+ return this.serial(() => this.commit(accounts, this.folderExists));
294
+ }
295
+
296
+ /*
297
+ * An account in a folder of its own under `<home>/accounts`, signed in by nobody yet. A
298
+ * Codex account is a shadow home over the CLI's own folder, so its threads go on under the other.
299
+ */
300
+ create(payload: ProviderAccountCreatePayload): Promise<ProviderAccountCreateResult> {
301
+ return this.serial(async () => {
302
+ const { kind, label, color } = payload;
303
+ const provider = this.providers.get(kind);
304
+ if (provider.home === undefined) {
305
+ throw new InvalidAccountError(`${provider.name} has no setting for its config folder, so it only has its default account`);
306
+ }
307
+ const id = this.mintId(kind, label);
308
+ const folder = join(this.accountsFolder(), id);
309
+ await mkdir(this.accountsFolder(), { recursive: true, mode: 0o700 });
310
+ await mkdir(folder, { mode: 0o700 });
311
+ const folders = kind === 'codex' ? { home: defaultFolder(provider, this.env), shadowHome: folder } : { home: folder };
312
+ const account: ProviderAccount = { kind, label, ...(color === undefined ? {} : { color }), ...folders };
313
+ await this.prepare(id, kind, account);
314
+ const snapshot = await this.commit({ ...this.accounts, [id]: account }, (path) => path === folder || this.folderExists(path));
315
+ return { ...snapshot, id };
316
+ });
317
+ }
318
+
319
+ /*
320
+ * Asks the CLI of one account again every few seconds, for a login running in a terminal, until
321
+ * it is signed in or a few minutes passed. Resolves when the watch ends; a second call for the
322
+ * same account only moves the end.
323
+ */
324
+ watchLogin(id: string): Promise<void> {
325
+ if (this.accounts[id] === undefined) {
326
+ throw new AccountError('account-unavailable', `The account '${id}' is not available on this machine: no account on it has that id.`);
327
+ }
328
+ const deadline = this.now() + LOGIN_WATCH_MS;
329
+ const running = this.watches.get(id);
330
+ if (running !== undefined) {
331
+ running.deadline = deadline;
332
+ return running.done;
333
+ }
334
+ const watch: LoginWatch = { deadline, done: Promise.resolve() };
335
+ watch.done = this.runWatch(id, watch).finally(() => this.watches.delete(id));
336
+ this.watches.set(id, watch);
337
+ return watch.done;
338
+ }
339
+
340
+ /* Asks every CLI again, and answers once they all did. */
341
+ async refresh(): Promise<ProviderAccounts> {
342
+ await this.queue(true);
343
+ return this.snapshot();
344
+ }
345
+
346
+ private async runWatch(id: string, watch: LoginWatch): Promise<void> {
347
+ while (this.now() < watch.deadline) {
348
+ await this.sleep(LOGIN_POLL_MS);
349
+ if (this.accounts[id] === undefined) {
350
+ return;
351
+ }
352
+ await this.queue(true, id);
353
+ if (this.states.get(id)?.status.state === 'ready') {
354
+ return;
355
+ }
356
+ }
357
+ }
358
+
359
+ /* One change of the file at a time: a create and a save both read what the other just wrote. */
360
+ private serial<T>(work: () => Promise<T>): Promise<T> {
361
+ const next = this.writes.then(work);
362
+ this.writes = next.catch(() => undefined);
363
+ return next;
364
+ }
365
+
366
+ private async commit(accounts: ProviderAccountMap, isDirectory: (path: string) => boolean): Promise<ProviderAccounts> {
367
+ const stored = acceptAccounts(accounts, this.stored, (kind) => this.providers.get(kind), this.env, isDirectory, this.host);
368
+ const { writes, removals } = this.planSecrets(stored);
369
+ for (const [key, value] of writes) {
370
+ await this.requireSecrets().write(key, value);
371
+ this.secretValues.set(key, value);
372
+ }
373
+ await writeAccounts(this.path, stored);
374
+ this.stored = stored;
375
+ this.accounts = wireAccounts(stored);
376
+ for (const key of removals) {
377
+ this.secretValues.delete(key);
378
+ await this.secrets?.remove(key).catch((e: unknown) => console.error(`Could not remove ${key} from the keychain:`, errorText(e)));
379
+ }
380
+ const rewritten = new Set([...writes.keys()].map((key) => key.slice(0, key.indexOf('/'))));
381
+ for (const id of this.states.keys()) {
382
+ // A new secret can be a new login, which the account as the file holds it does not show.
383
+ if (this.accounts[id] === undefined || rewritten.has(id)) {
384
+ this.states.delete(id);
385
+ }
386
+ }
387
+ this.emit();
388
+ void this.queue(false);
389
+ return this.snapshot();
390
+ }
391
+
392
+ /*
393
+ * Takes every sensitive value out of the accounts about to be written, leaving the redacted form
394
+ * the file keeps. Answers which values go into the keychain and which keys no account has any more.
395
+ */
396
+ private planSecrets(stored: StoredAccounts): { writes: Map<string, string>; removals: string[] } {
397
+ const writes = new Map<string, string>();
398
+ const kept = new Set<string>();
399
+ for (const [id, entry] of Object.entries(stored)) {
400
+ const kind = (entry as { kind: string }).kind;
401
+ if (!isKnownKind(kind)) {
402
+ for (const name of sensitiveNames(entry)) {
403
+ kept.add(secretKey(id, name));
404
+ }
405
+ continue;
406
+ }
407
+ const account = entry as ProviderAccount;
408
+ if (account.env === undefined) {
409
+ continue;
410
+ }
411
+ const had = new Set(sensitiveNames(this.stored[id]));
412
+ const variables = account.env.map((variable): ProviderAccountVariable => {
413
+ if (!variable.sensitive) {
414
+ return { name: variable.name, value: variable.value, sensitive: false };
415
+ }
416
+ const key = secretKey(id, variable.name);
417
+ this.requireSecrets();
418
+ if (variable.value !== '') {
419
+ writes.set(key, variable.value);
420
+ } else if (variable.valueRedacted !== true || !had.has(variable.name)) {
421
+ throw new InvalidAccountError(`${id}: ${variable.name} has no value`);
422
+ }
423
+ kept.add(key);
424
+ return { name: variable.name, value: '', sensitive: true, valueRedacted: true };
425
+ });
426
+ const { env: _env, ...rest } = account;
427
+ stored[id] = variables.length === 0 ? rest : { ...rest, env: variables };
428
+ }
429
+ const removals = Object.entries(this.stored).flatMap(([id, entry]) =>
430
+ sensitiveNames(entry)
431
+ .map((name) => secretKey(id, name))
432
+ .filter((key) => !kept.has(key))
433
+ );
434
+ return { writes, removals };
435
+ }
436
+
437
+ private requireSecrets(): SecretStore {
438
+ if (this.secrets === null) {
439
+ throw new SecretsUnavailableError();
440
+ }
441
+ return this.secrets;
442
+ }
443
+
444
+ private async readSecrets(): Promise<void> {
445
+ if (this.secrets === null) {
446
+ return;
447
+ }
448
+ for (const [id, entry] of Object.entries(this.stored)) {
449
+ for (const name of sensitiveNames(entry)) {
450
+ const key = secretKey(id, name);
451
+ try {
452
+ const value = await this.secrets.read(key);
453
+ if (value !== null) {
454
+ this.secretValues.set(key, value);
455
+ }
456
+ } catch (e) {
457
+ console.error(`Could not read ${key} from the keychain:`, errorText(e));
458
+ }
459
+ }
460
+ }
461
+ }
462
+
463
+ /* The variables of an account as a CLI gets them, and the first sensitive one the keychain did not give. */
464
+ private variablesOf(id: string, account: ProviderAccount): { values: Record<string, string>; missing: string | null } {
465
+ const values: Record<string, string> = {};
466
+ for (const variable of account.env ?? []) {
467
+ const value = variable.sensitive ? this.secretValues.get(secretKey(id, variable.name)) : variable.value;
468
+ if (value === undefined) {
469
+ return { values, missing: variable.name };
470
+ }
471
+ values[variable.name] = value;
472
+ }
473
+ return { values, missing: null };
474
+ }
475
+
476
+ private accountsFolder(): string {
477
+ return join(this.home, 'accounts');
478
+ }
479
+
480
+ /* A free id for a new account; a folder a removed account left behind is never handed to another. */
481
+ private mintId(kind: AgentKind, label: string): string {
482
+ const base = `${kind}_${slugOf(label) || 'account'}`.slice(0, 60).replace(/-+$/, '');
483
+ const taken = (id: string): boolean => this.accounts[id] !== undefined || this.stored[id] !== undefined || existsSync(join(this.accountsFolder(), id));
484
+ let id = base;
485
+ for (let suffix = 2; taken(id); suffix += 1) {
486
+ id = `${base}-${suffix}`;
487
+ }
488
+ return id;
489
+ }
490
+
491
+ /* One pass at a time, in the order they were asked for, so a save during a pass is checked after it. */
492
+ private queue(force: boolean, only?: string): Promise<void> {
493
+ const pass = this.passes.then(() => this.runAll(force, only));
494
+ this.passes = pass.catch((e: unknown) => console.error('Could not check the provider accounts:', errorText(e)));
495
+ return this.passes;
496
+ }
497
+
498
+ private statusOf(id: string, account: ProviderAccount): ProviderAccountStatus {
499
+ const state = this.states.get(id);
500
+ if (state !== undefined && state.drawnFor === JSON.stringify(account)) {
501
+ return state.status;
502
+ }
503
+ return this.status(id, account, 'checking', { checkedAt: 0 });
504
+ }
505
+
506
+ private status(
507
+ id: string,
508
+ account: ProviderAccount,
509
+ state: ProviderAccountStatus['state'],
510
+ fields: Partial<Omit<ProviderAccountStatus, 'id' | 'kind' | 'state'>> = {}
511
+ ): ProviderAccountStatus {
512
+ const provider = isKnownKind(account.kind) ? this.providers.get(account.kind) : null;
513
+ const folder = provider === null ? '' : accountFolder(id, account, provider, this.env);
514
+ return {
515
+ id,
516
+ kind: account.kind,
517
+ state,
518
+ email: null,
519
+ plan: null,
520
+ organization: null,
521
+ home: folder,
522
+ ...(provider === null ? {} : { transcripts: transcriptFolder(id, account, provider, this.env) }),
523
+ message: null,
524
+ checkedAt: this.now(),
525
+ ...fields
526
+ };
527
+ }
528
+
529
+ private async runAll(force: boolean, only: string | undefined): Promise<void> {
530
+ const installed = await this.providers.list();
531
+ const due = Object.entries(this.accounts).filter(([id, account]) => {
532
+ if (only !== undefined && id !== only) {
533
+ return false;
534
+ }
535
+ const state = this.states.get(id);
536
+ return force || state === undefined || state.drawnFor !== JSON.stringify(account) || this.now() >= state.nextAt;
537
+ });
538
+ const results = await Promise.all(due.map(async ([id, account]) => ({ id, account, status: await this.check(id, account, installed) })));
539
+ let changed = false;
540
+ for (const { id, account, status } of results) {
541
+ if (this.accounts[id] === undefined) {
542
+ continue;
543
+ }
544
+ const before = this.states.get(id);
545
+ const drawnFor = JSON.stringify(account);
546
+ const failed = status.state === 'failed';
547
+ const backoffMs = failed ? Math.min(MAX_BACKOFF_MS, (before?.backoffMs ?? CHECK_INTERVAL_MS / 2) * 2) : CHECK_INTERVAL_MS;
548
+ this.states.set(id, { status, drawnFor, backoffMs, nextAt: this.now() + backoffMs });
549
+ // An account a save changed read `checking` since, whatever it read before.
550
+ changed ||= before === undefined || before.drawnFor !== drawnFor || !sameStatus(before.status, status);
551
+ }
552
+ if (changed) {
553
+ this.emit();
554
+ }
555
+ }
556
+
557
+ private async check(id: string, account: ProviderAccount, installed: ProviderInfo[]): Promise<ProviderAccountStatus> {
558
+ const kind = account.kind;
559
+ if (!isKnownKind(kind)) {
560
+ return this.status(id, account, 'unavailable', { message: `This version of ${this.host.name} does not know this CLI` });
561
+ }
562
+ const provider = this.providers.get(kind);
563
+ const isDefault = isDefaultAccount(id, account);
564
+ if (!isDefault && provider.home === undefined) {
565
+ return this.status(id, account, 'unavailable', {
566
+ message: `${provider.name} has no setting for its config folder, so it only has its default account`
567
+ });
568
+ }
569
+ if (account.enabled === false || this.providers.enabled?.(kind) === false) {
570
+ return this.status(id, account, 'disabled');
571
+ }
572
+ if (installed.find((info) => info.kind === kind)?.installed !== true) {
573
+ return this.status(id, account, 'not-found');
574
+ }
575
+ if (provider.home === undefined) {
576
+ return this.status(id, account, 'ready');
577
+ }
578
+ const folders = isDefault
579
+ ? []
580
+ : [account.home, account.shadowHome].filter((folder) => folder !== undefined).map((folder) => expandHome(folder, this.env));
581
+ for (const folder of folders) {
582
+ if (!this.folderExists(folder)) {
583
+ return this.status(id, account, 'folder-missing', { message: `${folder} does not exist` });
584
+ }
585
+ }
586
+ try {
587
+ const message = await this.prepare(id, kind, account);
588
+ const { values, missing } = this.variablesOf(id, account);
589
+ if (missing !== null) {
590
+ throw new Error(`The keychain does not give the value of ${missing}`);
591
+ }
592
+ const reading = await this.ask(kind, provider.command, accountEnv(id, account, provider, this.env, values));
593
+ return this.status(id, account, reading.signedIn ? 'ready' : 'signed-out', {
594
+ email: reading.email,
595
+ plan: reading.plan,
596
+ organization: reading.organization,
597
+ message
598
+ });
599
+ } catch (e) {
600
+ return this.status(id, account, 'failed', { message: errorText(e) });
601
+ }
602
+ }
603
+
604
+ /*
605
+ * Puts the hooks in the account's folder and links a shadow home to the home it shares, before the
606
+ * CLI is asked anything: Codex writes its own files into a fresh home the moment it starts, and a
607
+ * real file there is one the host will never replace by a link. Answers what a person should
608
+ * know about the shadow home, and throws when it cannot be used.
609
+ */
610
+ private async prepare(id: string, kind: AgentKind, account: ProviderAccount): Promise<string | null> {
611
+ if (isDefaultAccount(id, account) || account.home === undefined) {
612
+ return null;
613
+ }
614
+ const home = expandHome(account.home, this.env);
615
+ await this.install?.(kind, home).catch((e: unknown) => console.error(`Could not install hooks in ${home}:`, errorText(e)));
616
+ if (account.shadowHome === undefined) {
617
+ return null;
618
+ }
619
+ const report = await this.prepareShadow(home, expandHome(account.shadowHome, this.env));
620
+ if (report.sharedLogin) {
621
+ throw new Error('auth.json in the shadow home is a link, so this account would sign in as another one');
622
+ }
623
+ return report.unshared.length > 0 ? `Not shared with ${home}: ${report.unshared.join(', ')}` : null;
624
+ }
625
+
626
+ private emit(): void {
627
+ this.sinks.emit({ event: 'accounts.changed', payload: this.snapshot() });
628
+ for (const listener of this.listeners) {
629
+ listener();
630
+ }
631
+ }
632
+ }