@adecore/agents 0.38.0 → 0.40.0

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 (170) hide show
  1. package/dist/async.d.ts +5 -0
  2. package/dist/async.js +13 -5
  3. package/dist/chat/acp-protocol.js +3 -5
  4. package/dist/chat/chat-core.d.ts +1 -0
  5. package/dist/chat/chat-core.js +9 -2
  6. package/dist/chat/chat-process.d.ts +1 -0
  7. package/dist/chat/chat-process.js +1 -1
  8. package/dist/chat/chat-session.d.ts +2 -0
  9. package/dist/chat/chat-session.js +9 -10
  10. package/dist/chat/claude-protocol.js +1 -6
  11. package/dist/chat/claude-transcript.js +1 -3
  12. package/dist/chat/codex-protocol.js +1 -6
  13. package/dist/chat/projector.js +1 -3
  14. package/dist/chat/request-summary.js +1 -3
  15. package/dist/coded-error.js +1 -5
  16. package/dist/context/argv.js +1 -2
  17. package/dist/context/verb.js +1 -3
  18. package/dist/error-text.d.ts +1 -0
  19. package/dist/error-text.js +4 -0
  20. package/dist/fields.d.ts +3 -0
  21. package/dist/fields.js +12 -0
  22. package/dist/fs.d.ts +1 -0
  23. package/dist/fs.js +8 -4
  24. package/dist/lineage.js +11 -5
  25. package/dist/messages/notice-store.js +3 -4
  26. package/dist/providers/accounts/acp-account.js +3 -20
  27. package/dist/providers/accounts/service.d.ts +1 -2
  28. package/dist/providers/accounts/service.js +5 -9
  29. package/dist/providers/accounts/status.js +1 -3
  30. package/dist/providers/acp-agent-state.js +1 -3
  31. package/dist/providers/claude.js +0 -1
  32. package/dist/providers/codex.js +0 -1
  33. package/dist/providers/detect.js +1 -1
  34. package/dist/providers/shell-commands.js +1 -2
  35. package/dist/run-process.js +1 -3
  36. package/dist/skills.d.ts +1 -1
  37. package/dist/skills.js +5 -8
  38. package/dist/tasks/background-limit.js +3 -2
  39. package/dist/tasks/task-coordinator.js +11 -17
  40. package/dist/testing/watch.js +0 -1
  41. package/dist/usage/limits/monitor.js +1 -2
  42. package/dist/usage/readers/claude.js +3 -10
  43. package/dist/usage/readers/codex.js +2 -9
  44. package/dist/usage/readers/grok.js +3 -10
  45. package/dist/usage/readers/host-log.js +3 -10
  46. package/dist/usage/record.d.ts +2 -0
  47. package/dist/usage/record.js +13 -0
  48. package/package.json +6 -6
  49. package/src/async.ts +23 -5
  50. package/src/chat/acp-backend.ts +121 -0
  51. package/src/chat/acp-protocol.ts +172 -6
  52. package/src/chat/attachment-store.ts +21 -0
  53. package/src/chat/backend.ts +0 -2
  54. package/src/chat/bookmark-store.ts +23 -0
  55. package/src/chat/chat-core.ts +218 -7
  56. package/src/chat/chat-log.ts +19 -0
  57. package/src/chat/chat-process.ts +52 -1
  58. package/src/chat/chat-session.ts +414 -13
  59. package/src/chat/chat-store.ts +50 -0
  60. package/src/chat/chat-title.ts +9 -0
  61. package/src/chat/claude-backend.ts +50 -0
  62. package/src/chat/claude-protocol.ts +155 -8
  63. package/src/chat/claude-title.ts +10 -0
  64. package/src/chat/claude-transcript.ts +47 -4
  65. package/src/chat/codex-backend.ts +131 -0
  66. package/src/chat/codex-protocol.ts +180 -8
  67. package/src/chat/codex-thread.ts +23 -0
  68. package/src/chat/composer-preferences.ts +6 -0
  69. package/src/chat/context-breakdown.ts +11 -0
  70. package/src/chat/delta-coalescer.ts +8 -0
  71. package/src/chat/external-sessions.ts +28 -0
  72. package/src/chat/generated-image.ts +25 -0
  73. package/src/chat/index.ts +5 -0
  74. package/src/chat/input.ts +13 -0
  75. package/src/chat/json-rpc-transport.ts +28 -0
  76. package/src/chat/limit-resume.ts +13 -0
  77. package/src/chat/projector.ts +160 -4
  78. package/src/chat/readable-text.ts +3 -0
  79. package/src/chat/request-summary.ts +18 -4
  80. package/src/chat/subagent-projection.ts +3 -0
  81. package/src/chat/subagent-reader.ts +76 -0
  82. package/src/chat/subagent-settlement.ts +24 -0
  83. package/src/chat/thread-search.ts +13 -0
  84. package/src/chat/thread.ts +31 -0
  85. package/src/chat/ui-queries.ts +122 -0
  86. package/src/chat/ui-replies.ts +34 -0
  87. package/src/chat/visual-store.ts +49 -0
  88. package/src/chat/wake-chat.ts +3 -0
  89. package/src/client-sinks.ts +2 -0
  90. package/src/coded-error.ts +1 -5
  91. package/src/context/argv.ts +18 -2
  92. package/src/context/index.ts +1 -0
  93. package/src/context/refusal.ts +3 -0
  94. package/src/context/verb.ts +32 -4
  95. package/src/coordination/index.ts +10 -0
  96. package/src/error-text.ts +7 -0
  97. package/src/fields.ts +14 -0
  98. package/src/fs.ts +20 -4
  99. package/src/host/agent-host.ts +20 -0
  100. package/src/host/environment.ts +6 -0
  101. package/src/host/handlers.ts +18 -0
  102. package/src/host/memory-port.ts +4 -0
  103. package/src/host/wiring.ts +10 -0
  104. package/src/index.ts +1 -0
  105. package/src/lineage.ts +24 -5
  106. package/src/messages/deliver-message.ts +6 -0
  107. package/src/messages/deliver-notice.ts +13 -0
  108. package/src/messages/notice-store.ts +16 -4
  109. package/src/modes.ts +1 -0
  110. package/src/outbox/outbox-worker.ts +31 -0
  111. package/src/outbox/outbox.ts +5 -0
  112. package/src/providers/accounts/accounts.ts +36 -0
  113. package/src/providers/accounts/acp-account.ts +22 -27
  114. package/src/providers/accounts/launch.ts +4 -0
  115. package/src/providers/accounts/service.ts +110 -12
  116. package/src/providers/accounts/shadow-home.ts +10 -0
  117. package/src/providers/accounts/status.ts +13 -4
  118. package/src/providers/accounts/store.ts +25 -0
  119. package/src/providers/accounts/variables.ts +11 -1
  120. package/src/providers/acp-agent-state.ts +24 -3
  121. package/src/providers/acp-provider.ts +5 -0
  122. package/src/providers/acp-usage-log.ts +2 -0
  123. package/src/providers/catalog.ts +17 -0
  124. package/src/providers/claude.ts +10 -1
  125. package/src/providers/codex-rules.ts +3 -0
  126. package/src/providers/codex.ts +2 -1
  127. package/src/providers/detect.ts +6 -1
  128. package/src/providers/provider.ts +1 -1
  129. package/src/providers/registry.ts +18 -2
  130. package/src/providers/shell-commands.ts +37 -2
  131. package/src/record-directory.ts +19 -0
  132. package/src/run-process.ts +24 -3
  133. package/src/serializer.ts +3 -0
  134. package/src/skills.ts +73 -10
  135. package/src/tasks/background-limit.ts +9 -2
  136. package/src/tasks/end-children.ts +9 -0
  137. package/src/tasks/give-task.ts +8 -0
  138. package/src/tasks/task-coordinator.ts +81 -17
  139. package/src/tasks/task-store.ts +28 -0
  140. package/src/tasks/waiting-child.ts +21 -0
  141. package/src/tasks/wake-parent.ts +26 -0
  142. package/src/tasks/wiring.ts +22 -0
  143. package/src/testing/accounts.ts +2 -0
  144. package/src/testing/fake-acp-capture.ts +16 -0
  145. package/src/testing/fake-acp.ts +66 -0
  146. package/src/testing/fake-claude.ts +177 -0
  147. package/src/testing/fake-cli.ts +29 -0
  148. package/src/testing/fake-codex.ts +122 -0
  149. package/src/testing/manual-clock.ts +2 -0
  150. package/src/testing/watch.ts +7 -1
  151. package/src/title-file.ts +12 -0
  152. package/src/usage/accounts.ts +6 -0
  153. package/src/usage/aggregate.ts +19 -0
  154. package/src/usage/exchange.ts +12 -0
  155. package/src/usage/index-file.ts +27 -0
  156. package/src/usage/limits/monitor.ts +31 -3
  157. package/src/usage/limits/normalize.ts +30 -0
  158. package/src/usage/limits/probe.ts +17 -0
  159. package/src/usage/pricing.ts +35 -0
  160. package/src/usage/projects.ts +8 -0
  161. package/src/usage/readers/claude.ts +12 -9
  162. package/src/usage/readers/codex.ts +40 -8
  163. package/src/usage/readers/grok.ts +17 -9
  164. package/src/usage/readers/host-log.ts +9 -9
  165. package/src/usage/record.ts +22 -0
  166. package/src/usage/roots.ts +6 -0
  167. package/src/usage/scanner.ts +61 -0
  168. package/src/usage/usage-service.ts +14 -0
  169. package/src/watch-seam.ts +8 -0
  170. package/src/windows-command.ts +12 -0
@@ -17,6 +17,7 @@ import { DEFAULT_CODEX_CLIENT, type CodexClientInfo } from '../../chat/codex-tra
17
17
  import { ClientSinks } from '../../client-sinks.ts';
18
18
  import { errorText } from '../../error-text.ts';
19
19
  import type { AgentEvent, AgentSink } from '../../events.ts';
20
+ import { Serializer } from '../../serializer.ts';
20
21
  import type { ChatProvider } from '../provider.ts';
21
22
  import {
22
23
  accountEnv,
@@ -52,7 +53,7 @@ export interface ProviderAccountsOptions {
52
53
  // `kinds` are the CLIs that get a default account; absent, every kind this version knows.
53
54
  providers: { list(): Promise<ProviderInfo[]>; get(kind: AgentKind): ChatProvider; enabled?(kind: AgentKind): boolean; kinds?(): readonly AgentKind[] };
54
55
  env?: Env;
55
- // How to ask a CLI who is signed in, for every kind whose provider has no reader of its own; a test answers without starting one.
56
+ // How to ask a CLI who is signed in, for every kind whose provider has no reader of its own.
56
57
  ask?: AskAccount;
57
58
  prepareShadow?: (home: string, shadow: string) => Promise<ShadowHomeReport>;
58
59
  folderExists?: (path: string) => boolean;
@@ -61,7 +62,7 @@ export interface ProviderAccountsOptions {
61
62
  // Where sensitive values are kept; null on a machine without a keychain. Absent is the platform's own.
62
63
  secrets?: SecretStore | null;
63
64
  now?: () => number;
64
- // How a login watch waits between two checks; a test moves its clock instead.
65
+ // How a login watch waits between two checks.
65
66
  sleep?: (ms: number) => Promise<void>;
66
67
  }
67
68
 
@@ -101,11 +102,14 @@ function slugOf(label: string): string {
101
102
 
102
103
  function sensitiveNames(entry: unknown): string[] {
103
104
  const variables = typeof entry === 'object' && entry !== null ? (entry as { env?: unknown }).env : undefined;
105
+
104
106
  if (!Array.isArray(variables)) {
105
107
  return [];
106
108
  }
109
+
107
110
  return variables.flatMap((variable: unknown) => {
108
111
  const { name, sensitive } = (variable ?? {}) as { name?: unknown; sensitive?: unknown };
112
+
109
113
  return sensitive === true && typeof name === 'string' ? [name] : [];
110
114
  });
111
115
  }
@@ -138,7 +142,8 @@ export class ProviderAccountsService implements AccountLaunches {
138
142
  private readonly now: () => number;
139
143
  private readonly sleep: (ms: number) => Promise<void>;
140
144
  private readonly watches = new Map<string, LoginWatch>();
141
- private writes: Promise<unknown> = Promise.resolve();
145
+ // One change of the file at a time: a create and a save both read what the other just wrote.
146
+ private readonly writes = new Serializer();
142
147
  private readonly sinks = new ClientSinks<AgentEvent>();
143
148
  private readonly listeners = new Set<() => void>();
144
149
  // When a CLI last started under each account, by id; kept in memory, so a restart forgets it.
@@ -180,11 +185,14 @@ export class ProviderAccountsService implements AccountLaunches {
180
185
  const loginCommands: Partial<Record<AgentKind, string>> = {};
181
186
  const chatLogins: AgentKind[] = [];
182
187
  const silentLogins: AgentKind[] = [];
188
+
183
189
  for (const kind of this.kinds()) {
184
190
  const provider = this.providers.get(kind);
185
191
  const home = provider.home;
192
+
186
193
  if (home?.loginCommand !== undefined) {
187
194
  loginCommands[kind] = home.loginCommand;
195
+
188
196
  if (provider.askAccount === undefined && !this.asksEveryKind && !ASKED_KINDS.includes(kind)) {
189
197
  silentLogins.push(kind);
190
198
  }
@@ -192,6 +200,7 @@ export class ProviderAccountsService implements AccountLaunches {
192
200
  chatLogins.push(kind);
193
201
  }
194
202
  }
203
+
195
204
  return {
196
205
  accounts: this.accounts,
197
206
  statuses: Object.entries(this.accounts).map(([id, account]) => this.statusOf(id, account)),
@@ -213,44 +222,59 @@ export class ProviderAccountsService implements AccountLaunches {
213
222
  */
214
223
  envFor(kind: AgentKind, id: string | undefined, baseEnv: Env): Env {
215
224
  const key = id ?? kind;
225
+
216
226
  const refuse = (why: string): AccountError =>
217
227
  new AccountError('account-unavailable', `The account '${this.labelOf(key)}' is not available on this machine: ${why}.`);
228
+
218
229
  const provider = this.providers.get(kind);
230
+
219
231
  const variables = (account: ProviderAccount): Record<string, string> => {
220
232
  const { values, missing } = this.variablesOf(key, account);
233
+
221
234
  if (missing !== null) {
222
235
  throw refuse(`the keychain does not give the value of ${missing}`);
223
236
  }
237
+
224
238
  return values;
225
239
  };
240
+
226
241
  if (isDefaultAccountOf(kind, id)) {
227
242
  const account = this.accounts[kind] ?? { kind };
243
+
228
244
  // Only a start that names it: every turn of a chat on the default account asks with none.
229
245
  if (id !== undefined && account.enabled === false) {
230
246
  throw refuse('it is turned off');
231
247
  }
248
+
232
249
  return accountEnv(kind, account, provider, baseEnv, variables(account));
233
250
  }
251
+
234
252
  const account = this.accounts[key];
253
+
235
254
  if (account === undefined) {
236
255
  throw refuse('no account on it has that id');
237
256
  }
257
+
238
258
  if (account.kind !== kind) {
239
259
  throw refuse(
240
260
  `it is an account of ${isOfferedKind(account.kind, this.kinds()) ? this.providers.get(account.kind).name : account.kind}, not of ${provider.name}`
241
261
  );
242
262
  }
263
+
243
264
  if (account.enabled === false) {
244
265
  throw refuse('it is turned off');
245
266
  }
267
+
246
268
  if (provider.home === undefined || account.home === undefined) {
247
269
  throw refuse(`${provider.name} has no setting for its config folder`);
248
270
  }
271
+
249
272
  for (const folder of [account.home, account.shadowHome]) {
250
273
  if (folder !== undefined && !this.folderExists(expandHome(folder, this.env))) {
251
274
  throw refuse(`its folder ${folder} is missing`);
252
275
  }
253
276
  }
277
+
254
278
  return accountEnv(key, account, provider, baseEnv, variables(account));
255
279
  }
256
280
 
@@ -267,12 +291,15 @@ export class ProviderAccountsService implements AccountLaunches {
267
291
  const toId = to ?? kind;
268
292
  const fromAccount = this.accounts[fromId];
269
293
  const toAccount = this.accounts[toId];
294
+
270
295
  if (fromId === toId) {
271
296
  return true;
272
297
  }
298
+
273
299
  if (fromAccount === undefined || toAccount === undefined || fromAccount.kind !== kind) {
274
300
  return false;
275
301
  }
302
+
276
303
  return canContinue({ id: fromId, account: fromAccount }, { id: toId, account: toAccount }, this.accountProviders(), this.env);
277
304
  }
278
305
 
@@ -292,6 +319,7 @@ export class ProviderAccountsService implements AccountLaunches {
292
319
  /* Told whenever the accounts or what their CLIs said changed, after the clients were. */
293
320
  listen(listener: () => void): () => void {
294
321
  this.listeners.add(listener);
322
+
295
323
  return () => {
296
324
  this.listeners.delete(listener);
297
325
  };
@@ -312,7 +340,7 @@ export class ProviderAccountsService implements AccountLaunches {
312
340
 
313
341
  /* Answers at once; an account that changed reads `checking` until its CLI answered, which arrives as `accounts.changed`. */
314
342
  save(accounts: ProviderAccountMap): Promise<ProviderAccounts> {
315
- return this.serial(() => this.commit(accounts, this.folderExists));
343
+ return this.writes.run(() => this.commit(accounts, this.folderExists));
316
344
  }
317
345
 
318
346
  /*
@@ -320,12 +348,14 @@ export class ProviderAccountsService implements AccountLaunches {
320
348
  * Codex account is a shadow home over the CLI's own folder, so its threads go on under the other.
321
349
  */
322
350
  create(payload: ProviderAccountCreatePayload): Promise<ProviderAccountCreateResult> {
323
- return this.serial(async () => {
351
+ return this.writes.run(async () => {
324
352
  const { kind, label, color } = payload;
325
353
  const provider = this.providers.get(kind);
354
+
326
355
  if (provider.home === undefined) {
327
356
  throw new InvalidAccountError(`${provider.name} has no setting for its config folder, so it only has its default account`);
328
357
  }
358
+
329
359
  const id = this.mintId(kind, label);
330
360
  const folder = join(this.accountsFolder(), id);
331
361
  await mkdir(this.accountsFolder(), { recursive: true, mode: 0o700 });
@@ -334,6 +364,7 @@ export class ProviderAccountsService implements AccountLaunches {
334
364
  const account: ProviderAccount = { kind, label, ...(color === undefined ? {} : { color }), ...folders };
335
365
  await this.prepare(id, kind, account);
336
366
  const snapshot = await this.commit({ ...this.accounts, [id]: account }, (path) => path === folder || this.folderExists(path));
367
+
337
368
  return { ...snapshot, id };
338
369
  });
339
370
  }
@@ -347,40 +378,51 @@ export class ProviderAccountsService implements AccountLaunches {
347
378
  if (this.accounts[id] === undefined) {
348
379
  throw new AccountError('account-unavailable', `The account '${id}' is not available on this machine: no account on it has that id.`);
349
380
  }
381
+
350
382
  const deadline = this.now() + LOGIN_WATCH_MS;
351
383
  const running = this.watches.get(id);
384
+
352
385
  if (running !== undefined) {
353
386
  running.deadline = deadline;
387
+
354
388
  return running.done;
355
389
  }
390
+
356
391
  const watch: LoginWatch = { deadline, done: Promise.resolve() };
357
392
  watch.done = this.runWatch(id, watch).finally(() => this.watches.delete(id));
358
393
  this.watches.set(id, watch);
394
+
359
395
  return watch.done;
360
396
  }
361
397
 
362
398
  /* Asks every CLI again, and answers once they all did. */
363
399
  async refresh(): Promise<ProviderAccounts> {
364
400
  await this.queue(true);
401
+
365
402
  return this.snapshot();
366
403
  }
367
404
 
368
405
  private folderOf(kind: AgentKind, id: string | undefined, folder: typeof accountFolder): string | null {
369
406
  const key = id ?? kind;
370
407
  const account = this.accounts[key];
408
+
371
409
  if (account === undefined || account.kind !== kind) {
372
410
  return null;
373
411
  }
412
+
374
413
  return folder(key, account, this.providers.get(kind), this.env);
375
414
  }
376
415
 
377
416
  private async runWatch(id: string, watch: LoginWatch): Promise<void> {
378
417
  while (this.now() < watch.deadline) {
379
418
  await this.sleep(LOGIN_POLL_MS);
419
+
380
420
  if (this.accounts[id] === undefined) {
381
421
  return;
382
422
  }
423
+
383
424
  await this.queue(true, id);
425
+
384
426
  if (this.states.get(id)?.status.state === 'ready') {
385
427
  return;
386
428
  }
@@ -395,36 +437,36 @@ export class ProviderAccountsService implements AccountLaunches {
395
437
  return { kinds: this.kinds(), get: (kind) => this.providers.get(kind) };
396
438
  }
397
439
 
398
- /* One change of the file at a time: a create and a save both read what the other just wrote. */
399
- private serial<T>(work: () => Promise<T>): Promise<T> {
400
- const next = this.writes.then(work);
401
- this.writes = next.catch(() => undefined);
402
- return next;
403
- }
404
-
405
440
  private async commit(accounts: ProviderAccountMap, isDirectory: (path: string) => boolean): Promise<ProviderAccounts> {
406
441
  const stored = acceptAccounts(accounts, this.stored, this.accountProviders(), this.env, isDirectory, this.host);
407
442
  const { writes, removals } = this.planSecrets(stored);
443
+
408
444
  for (const [key, value] of writes) {
409
445
  await this.requireSecrets().write(key, value);
410
446
  this.secretValues.set(key, value);
411
447
  }
448
+
412
449
  await writeAccounts(this.path, stored);
413
450
  this.stored = stored;
414
451
  this.accounts = wireAccounts(stored, this.kinds());
452
+
415
453
  for (const key of removals) {
416
454
  this.secretValues.delete(key);
417
455
  await this.secrets?.remove(key).catch((e: unknown) => console.error(`Could not remove ${key} from the keychain:`, errorText(e)));
418
456
  }
457
+
419
458
  const rewritten = new Set([...writes.keys()].map((key) => key.slice(0, key.indexOf('/'))));
459
+
420
460
  for (const id of this.states.keys()) {
421
461
  // A new secret can be a new login, which the account as the file holds it does not show.
422
462
  if (this.accounts[id] === undefined || rewritten.has(id)) {
423
463
  this.states.delete(id);
424
464
  }
425
465
  }
466
+
426
467
  this.emit();
427
468
  void this.queue(false);
469
+
428
470
  return this.snapshot();
429
471
  }
430
472
 
@@ -435,41 +477,55 @@ export class ProviderAccountsService implements AccountLaunches {
435
477
  private planSecrets(stored: StoredAccounts): { writes: Map<string, string>; removals: string[] } {
436
478
  const writes = new Map<string, string>();
437
479
  const kept = new Set<string>();
480
+
438
481
  for (const [id, entry] of Object.entries(stored)) {
439
482
  const kind = (entry as { kind: string }).kind;
483
+
440
484
  if (!isOfferedKind(kind, this.kinds())) {
441
485
  for (const name of sensitiveNames(entry)) {
442
486
  kept.add(secretKey(id, name));
443
487
  }
488
+
444
489
  continue;
445
490
  }
491
+
446
492
  const account = entry as ProviderAccount;
493
+
447
494
  if (account.env === undefined) {
448
495
  continue;
449
496
  }
497
+
450
498
  const had = new Set(sensitiveNames(this.stored[id]));
499
+
451
500
  const variables = account.env.map((variable): ProviderAccountVariable => {
452
501
  if (!variable.sensitive) {
453
502
  return { name: variable.name, value: variable.value, sensitive: false };
454
503
  }
504
+
455
505
  const key = secretKey(id, variable.name);
456
506
  this.requireSecrets();
507
+
457
508
  if (variable.value !== '') {
458
509
  writes.set(key, variable.value);
459
510
  } else if (variable.valueRedacted !== true || !had.has(variable.name)) {
460
511
  throw new InvalidAccountError(`${id}: ${variable.name} has no value`);
461
512
  }
513
+
462
514
  kept.add(key);
515
+
463
516
  return { name: variable.name, value: '', sensitive: true, valueRedacted: true };
464
517
  });
518
+
465
519
  const { env: _env, ...rest } = account;
466
520
  stored[id] = variables.length === 0 ? rest : { ...rest, env: variables };
467
521
  }
522
+
468
523
  const removals = Object.entries(this.stored).flatMap(([id, entry]) =>
469
524
  sensitiveNames(entry)
470
525
  .map((name) => secretKey(id, name))
471
526
  .filter((key) => !kept.has(key))
472
527
  );
528
+
473
529
  return { writes, removals };
474
530
  }
475
531
 
@@ -477,6 +533,7 @@ export class ProviderAccountsService implements AccountLaunches {
477
533
  if (this.secrets === null) {
478
534
  throw new SecretsUnavailableError();
479
535
  }
536
+
480
537
  return this.secrets;
481
538
  }
482
539
 
@@ -484,11 +541,14 @@ export class ProviderAccountsService implements AccountLaunches {
484
541
  if (this.secrets === null) {
485
542
  return;
486
543
  }
544
+
487
545
  for (const [id, entry] of Object.entries(this.stored)) {
488
546
  for (const name of sensitiveNames(entry)) {
489
547
  const key = secretKey(id, name);
548
+
490
549
  try {
491
550
  const value = await this.secrets.read(key);
551
+
492
552
  if (value !== null) {
493
553
  this.secretValues.set(key, value);
494
554
  }
@@ -502,13 +562,17 @@ export class ProviderAccountsService implements AccountLaunches {
502
562
  /* The variables of an account as a CLI gets them, and the first sensitive one the keychain did not give. */
503
563
  private variablesOf(id: string, account: ProviderAccount): { values: Record<string, string>; missing: string | null } {
504
564
  const values: Record<string, string> = {};
565
+
505
566
  for (const variable of account.env ?? []) {
506
567
  const value = variable.sensitive ? this.secretValues.get(secretKey(id, variable.name)) : variable.value;
568
+
507
569
  if (value === undefined) {
508
570
  return { values, missing: variable.name };
509
571
  }
572
+
510
573
  values[variable.name] = value;
511
574
  }
575
+
512
576
  return { values, missing: null };
513
577
  }
514
578
 
@@ -519,11 +583,15 @@ export class ProviderAccountsService implements AccountLaunches {
519
583
  /* A free id for a new account; a folder a removed account left behind is never handed to another. */
520
584
  private mintId(kind: AgentKind, label: string): string {
521
585
  const base = `${kind}_${slugOf(label) || 'account'}`.slice(0, 60).replace(/-+$/, '');
586
+
522
587
  const taken = (id: string): boolean => this.accounts[id] !== undefined || this.stored[id] !== undefined || existsSync(join(this.accountsFolder(), id));
588
+
523
589
  let id = base;
590
+
524
591
  for (let suffix = 2; taken(id); suffix += 1) {
525
592
  id = `${base}-${suffix}`;
526
593
  }
594
+
527
595
  return id;
528
596
  }
529
597
 
@@ -531,14 +599,17 @@ export class ProviderAccountsService implements AccountLaunches {
531
599
  private queue(force: boolean, only?: string): Promise<void> {
532
600
  const pass = this.passes.then(() => this.runAll(force, only));
533
601
  this.passes = pass.catch((e: unknown) => console.error('Could not check the provider accounts:', errorText(e)));
602
+
534
603
  return this.passes;
535
604
  }
536
605
 
537
606
  private statusOf(id: string, account: ProviderAccount): ProviderAccountStatus {
538
607
  const state = this.states.get(id);
608
+
539
609
  if (state !== undefined && state.drawnFor === JSON.stringify(account)) {
540
610
  return state.status;
541
611
  }
612
+
542
613
  return this.status(id, account, 'checking', { checkedAt: 0 });
543
614
  }
544
615
 
@@ -550,6 +621,7 @@ export class ProviderAccountsService implements AccountLaunches {
550
621
  ): ProviderAccountStatus {
551
622
  const provider = isOfferedKind(account.kind, this.kinds()) ? this.providers.get(account.kind) : null;
552
623
  const folder = provider === null ? '' : accountFolder(id, account, provider, this.env);
624
+
553
625
  return {
554
626
  id,
555
627
  kind: account.kind,
@@ -567,19 +639,25 @@ export class ProviderAccountsService implements AccountLaunches {
567
639
 
568
640
  private async runAll(force: boolean, only: string | undefined): Promise<void> {
569
641
  const installed = await this.providers.list();
642
+
570
643
  const due = Object.entries(this.accounts).filter(([id, account]) => {
571
644
  if (only !== undefined && id !== only) {
572
645
  return false;
573
646
  }
647
+
574
648
  const state = this.states.get(id);
649
+
575
650
  return force || state === undefined || state.drawnFor !== JSON.stringify(account) || this.now() >= state.nextAt;
576
651
  });
652
+
577
653
  const results = await Promise.all(due.map(async ([id, account]) => ({ id, account, status: await this.check(id, account, installed) })));
578
654
  let changed = false;
655
+
579
656
  for (const { id, account, status } of results) {
580
657
  if (this.accounts[id] === undefined) {
581
658
  continue;
582
659
  }
660
+
583
661
  const before = this.states.get(id);
584
662
  const drawnFor = JSON.stringify(account);
585
663
  const failed = status.state === 'failed';
@@ -588,6 +666,7 @@ export class ProviderAccountsService implements AccountLaunches {
588
666
  // An account a save changed read `checking` since, whatever it read before.
589
667
  changed ||= before === undefined || before.drawnFor !== drawnFor || !sameStatus(before.status, status);
590
668
  }
669
+
591
670
  if (changed) {
592
671
  this.emit();
593
672
  }
@@ -595,45 +674,58 @@ export class ProviderAccountsService implements AccountLaunches {
595
674
 
596
675
  private async check(id: string, account: ProviderAccount, installed: ProviderInfo[]): Promise<ProviderAccountStatus> {
597
676
  const kind = account.kind;
677
+
598
678
  if (!isOfferedKind(kind, this.kinds())) {
599
679
  return this.status(id, account, 'unavailable', { message: `This version of ${this.host.name} does not know this CLI` });
600
680
  }
681
+
601
682
  const provider = this.providers.get(kind);
602
683
  const isDefault = isDefaultAccount(id, account);
684
+
603
685
  if (!isDefault && provider.home === undefined) {
604
686
  return this.status(id, account, 'unavailable', {
605
687
  message: `${provider.name} has no setting for its config folder, so it only has its default account`
606
688
  });
607
689
  }
690
+
608
691
  if (account.enabled === false || this.providers.enabled?.(kind) === false) {
609
692
  return this.status(id, account, 'disabled');
610
693
  }
694
+
611
695
  if (installed.find((info) => info.kind === kind)?.installed !== true) {
612
696
  return this.status(id, account, 'not-found');
613
697
  }
698
+
614
699
  if (provider.home === undefined) {
615
700
  return this.status(id, account, 'ready');
616
701
  }
702
+
617
703
  const folders = isDefault
618
704
  ? []
619
705
  : [account.home, account.shadowHome].filter((folder) => folder !== undefined).map((folder) => expandHome(folder, this.env));
706
+
620
707
  for (const folder of folders) {
621
708
  if (!this.folderExists(folder)) {
622
709
  return this.status(id, account, 'folder-missing', { message: `${folder} does not exist` });
623
710
  }
624
711
  }
712
+
625
713
  try {
626
714
  const message = await this.prepare(id, kind, account);
627
715
  const { values, missing } = this.variablesOf(id, account);
716
+
628
717
  if (missing !== null) {
629
718
  throw new Error(`The keychain does not give the value of ${missing}`);
630
719
  }
720
+
631
721
  // A CLI that signs in from its chat has no command that says who is signed in; its chat asks when it must.
632
722
  if (provider.home.loginCommand === undefined) {
633
723
  return this.status(id, account, 'ready', { message });
634
724
  }
725
+
635
726
  const env = accountEnv(id, account, provider, this.env, values);
636
727
  const reading = await (provider.askAccount ? provider.askAccount(provider.command, env) : this.ask(kind, provider.command, env));
728
+
637
729
  return this.status(id, account, reading.signedIn ? 'ready' : 'signed-out', {
638
730
  email: reading.email,
639
731
  plan: reading.plan,
@@ -655,20 +747,26 @@ export class ProviderAccountsService implements AccountLaunches {
655
747
  if (isDefaultAccount(id, account) || account.home === undefined) {
656
748
  return null;
657
749
  }
750
+
658
751
  const home = expandHome(account.home, this.env);
659
752
  await this.install?.(kind, home).catch((e: unknown) => console.error(`Could not install hooks in ${home}:`, errorText(e)));
753
+
660
754
  if (account.shadowHome === undefined) {
661
755
  return null;
662
756
  }
757
+
663
758
  const report = await this.prepareShadow(home, expandHome(account.shadowHome, this.env));
759
+
664
760
  if (report.sharedLogin) {
665
761
  throw new Error('auth.json in the shadow home is a link, so this account would sign in as another one');
666
762
  }
763
+
667
764
  return report.unshared.length > 0 ? `Not shared with ${home}: ${report.unshared.join(', ')}` : null;
668
765
  }
669
766
 
670
767
  private emit(): void {
671
768
  this.sinks.emit({ event: 'accounts.changed', payload: this.snapshot() });
769
+
672
770
  for (const listener of this.listeners) {
673
771
  listener();
674
772
  }
@@ -40,11 +40,13 @@ type Existing = { kind: 'missing' } | { kind: 'link'; target: string } | { kind:
40
40
  async function existing(path: string): Promise<Existing> {
41
41
  try {
42
42
  const stats = await lstat(path);
43
+
43
44
  return stats.isSymbolicLink() ? { kind: 'link', target: await readlink(path) } : { kind: 'real' };
44
45
  } catch (e) {
45
46
  if (isNotFound(e)) {
46
47
  return { kind: 'missing' };
47
48
  }
49
+
48
50
  throw e;
49
51
  }
50
52
  }
@@ -57,23 +59,30 @@ async function existing(path: string): Promise<Existing> {
57
59
  export async function prepareShadowHome(home: string, shadow: string): Promise<ShadowHomeReport> {
58
60
  const shared = resolve(home);
59
61
  const own = resolve(shadow);
62
+
60
63
  if (shared === own) {
61
64
  throw new ShadowHomeError('The shadow home has to be another folder than the config folder');
62
65
  }
66
+
63
67
  for (const folder of SHARED_FOLDERS) {
64
68
  await mkdir(join(shared, folder), { recursive: true });
65
69
  }
70
+
66
71
  const report: ShadowHomeReport = { linked: [], unshared: [], sharedLogin: false };
72
+
67
73
  for (const name of PRIVATE_ENTRIES) {
68
74
  if ((await existing(join(own, name))).kind === 'link') {
69
75
  report.sharedLogin = true;
70
76
  }
71
77
  }
78
+
72
79
  const names = (await readdir(shared)).filter((name) => !PRIVATE_ENTRIES.has(name) && !LOCAL_ENTRIES.has(name) && !isJournal(name));
80
+
73
81
  for (const name of names.sort()) {
74
82
  const target = join(shared, name);
75
83
  const link = join(own, name);
76
84
  const found = await existing(link);
85
+
77
86
  if (found.kind === 'missing') {
78
87
  await symlink(target, link);
79
88
  report.linked.push(name);
@@ -81,5 +90,6 @@ export async function prepareShadowHome(home: string, shadow: string): Promise<S
81
90
  report.unshared.push(name);
82
91
  }
83
92
  }
93
+
84
94
  return report;
85
95
  }
@@ -1,6 +1,7 @@
1
1
  import type { AgentKind } from '@adecore/agent-contracts';
2
2
  import { isRecord } from '@adecore/agent-contracts';
3
3
  import { withTimeout } from '../../async.ts';
4
+ import { filledStr as text } from '../../fields.ts';
4
5
  import { CodexTransport, type CodexClientInfo } from '../../chat/codex-transport.ts';
5
6
  import { runProcess } from '../../run-process.ts';
6
7
 
@@ -17,21 +18,20 @@ export interface AccountReading {
17
18
 
18
19
  export type AskAccount = (kind: AgentKind, command: readonly string[], env: Record<string, string | undefined>) => Promise<AccountReading>;
19
20
 
20
- function text(value: unknown): string | null {
21
- return typeof value === 'string' && value !== '' ? value : null;
22
- }
23
-
24
21
  /* `claude auth status --json`, as Claude Code 2.1.282 prints it: `loggedIn`, `email`, `orgName`, `subscriptionType`. */
25
22
  export function readClaudeAuthStatus(output: string): AccountReading {
26
23
  let status: unknown;
24
+
27
25
  try {
28
26
  status = JSON.parse(output);
29
27
  } catch {
30
28
  throw new Error('Claude Code did not say who is signed in');
31
29
  }
30
+
32
31
  if (!isRecord(status) || typeof status.loggedIn !== 'boolean') {
33
32
  throw new Error('Claude Code did not say who is signed in');
34
33
  }
34
+
35
35
  return {
36
36
  signedIn: status.loggedIn,
37
37
  email: text(status.email),
@@ -49,10 +49,13 @@ export function readCodexAccount(answer: unknown): AccountReading {
49
49
  if (!isRecord(answer)) {
50
50
  throw new Error('Codex did not say who is signed in');
51
51
  }
52
+
52
53
  const account = answer.account;
54
+
53
55
  if (!isRecord(account)) {
54
56
  return { signedIn: answer.requiresOpenaiAuth === false, email: null, plan: null, organization: null };
55
57
  }
58
+
56
59
  return { signedIn: true, email: text(account.email), plan: text(account.planType), organization: null };
57
60
  }
58
61
 
@@ -62,6 +65,7 @@ async function askClaude(command: readonly string[], env: Record<string, string
62
65
  timeoutMs: ASK_TIMEOUT_MS,
63
66
  timeoutMessage: 'Claude Code did not answer in time'
64
67
  });
68
+
65
69
  // Signed out exits 1 and still prints the status, so the output decides and the exit code does not.
66
70
  return readClaudeAuthStatus(stdout);
67
71
  }
@@ -74,14 +78,17 @@ async function askCodex(command: readonly string[], env: Record<string, string |
74
78
  onFrame: () => undefined,
75
79
  onExit: () => undefined
76
80
  });
81
+
77
82
  try {
78
83
  await withTimeout(
79
84
  transport.request('initialize', { clientInfo: client, capabilities: { experimentalApi: true } }),
80
85
  ASK_TIMEOUT_MS,
81
86
  'Codex did not answer in time'
82
87
  );
88
+
83
89
  transport.notify('initialized', {});
84
90
  const answer = await withTimeout(transport.request('account/read', { refreshToken: false }), ASK_TIMEOUT_MS, 'Codex did not answer in time');
91
+
85
92
  return readCodexAccount(answer);
86
93
  } finally {
87
94
  transport.end();
@@ -98,9 +105,11 @@ export function askAccountAs(client: CodexClientInfo): AskAccount {
98
105
  if (kind === 'claude') {
99
106
  return askClaude(command, env);
100
107
  }
108
+
101
109
  if (kind === 'codex') {
102
110
  return askCodex(command, env, client);
103
111
  }
112
+
104
113
  return Promise.reject(new Error(`${kind} cannot say who is signed in`));
105
114
  };
106
115
  }