@adecore/agents 0.39.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.
- package/dist/async.d.ts +5 -0
- package/dist/async.js +13 -5
- package/dist/chat/acp-protocol.js +3 -5
- package/dist/chat/chat-core.js +0 -2
- package/dist/chat/chat-process.d.ts +1 -0
- package/dist/chat/chat-process.js +1 -1
- package/dist/chat/chat-session.js +6 -8
- package/dist/chat/claude-protocol.js +1 -6
- package/dist/chat/claude-transcript.js +1 -3
- package/dist/chat/codex-protocol.js +1 -6
- package/dist/chat/projector.js +1 -3
- package/dist/chat/request-summary.js +1 -3
- package/dist/coded-error.js +1 -5
- package/dist/context/argv.js +1 -2
- package/dist/context/verb.js +1 -3
- package/dist/error-text.d.ts +1 -0
- package/dist/error-text.js +4 -0
- package/dist/fields.d.ts +3 -0
- package/dist/fields.js +12 -0
- package/dist/fs.d.ts +1 -0
- package/dist/fs.js +8 -4
- package/dist/lineage.js +11 -5
- package/dist/messages/notice-store.js +3 -4
- package/dist/providers/accounts/acp-account.js +3 -20
- package/dist/providers/accounts/service.d.ts +1 -2
- package/dist/providers/accounts/service.js +5 -9
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/acp-agent-state.js +1 -3
- package/dist/providers/claude.js +0 -1
- package/dist/providers/codex.js +0 -1
- package/dist/providers/detect.js +1 -1
- package/dist/providers/shell-commands.js +1 -2
- package/dist/run-process.js +1 -3
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +5 -8
- package/dist/tasks/background-limit.js +3 -2
- package/dist/tasks/task-coordinator.js +11 -17
- package/dist/testing/watch.js +0 -1
- package/dist/usage/limits/monitor.js +1 -2
- package/dist/usage/readers/claude.js +3 -10
- package/dist/usage/readers/codex.js +2 -9
- package/dist/usage/readers/grok.js +3 -10
- package/dist/usage/readers/host-log.js +3 -10
- package/dist/usage/record.d.ts +2 -0
- package/dist/usage/record.js +13 -0
- package/package.json +6 -6
- package/src/async.ts +23 -5
- package/src/chat/acp-backend.ts +121 -0
- package/src/chat/acp-protocol.ts +172 -6
- package/src/chat/attachment-store.ts +21 -0
- package/src/chat/backend.ts +0 -2
- package/src/chat/bookmark-store.ts +23 -0
- package/src/chat/chat-core.ts +208 -7
- package/src/chat/chat-log.ts +19 -0
- package/src/chat/chat-process.ts +52 -1
- package/src/chat/chat-session.ts +401 -10
- package/src/chat/chat-store.ts +50 -0
- package/src/chat/chat-title.ts +9 -0
- package/src/chat/claude-backend.ts +50 -0
- package/src/chat/claude-protocol.ts +155 -8
- package/src/chat/claude-title.ts +10 -0
- package/src/chat/claude-transcript.ts +47 -4
- package/src/chat/codex-backend.ts +131 -0
- package/src/chat/codex-protocol.ts +180 -8
- package/src/chat/codex-thread.ts +23 -0
- package/src/chat/composer-preferences.ts +6 -0
- package/src/chat/context-breakdown.ts +11 -0
- package/src/chat/delta-coalescer.ts +8 -0
- package/src/chat/external-sessions.ts +28 -0
- package/src/chat/generated-image.ts +25 -0
- package/src/chat/index.ts +5 -0
- package/src/chat/input.ts +13 -0
- package/src/chat/json-rpc-transport.ts +28 -0
- package/src/chat/limit-resume.ts +13 -0
- package/src/chat/projector.ts +160 -4
- package/src/chat/readable-text.ts +3 -0
- package/src/chat/request-summary.ts +18 -4
- package/src/chat/subagent-projection.ts +3 -0
- package/src/chat/subagent-reader.ts +76 -0
- package/src/chat/subagent-settlement.ts +24 -0
- package/src/chat/thread-search.ts +13 -0
- package/src/chat/thread.ts +31 -0
- package/src/chat/ui-queries.ts +122 -0
- package/src/chat/ui-replies.ts +34 -0
- package/src/chat/visual-store.ts +49 -0
- package/src/chat/wake-chat.ts +3 -0
- package/src/client-sinks.ts +2 -0
- package/src/coded-error.ts +1 -5
- package/src/context/argv.ts +18 -2
- package/src/context/index.ts +1 -0
- package/src/context/refusal.ts +3 -0
- package/src/context/verb.ts +32 -4
- package/src/coordination/index.ts +10 -0
- package/src/error-text.ts +7 -0
- package/src/fields.ts +14 -0
- package/src/fs.ts +20 -4
- package/src/host/agent-host.ts +20 -0
- package/src/host/environment.ts +6 -0
- package/src/host/handlers.ts +18 -0
- package/src/host/memory-port.ts +4 -0
- package/src/host/wiring.ts +10 -0
- package/src/index.ts +1 -0
- package/src/lineage.ts +24 -5
- package/src/messages/deliver-message.ts +6 -0
- package/src/messages/deliver-notice.ts +13 -0
- package/src/messages/notice-store.ts +16 -4
- package/src/modes.ts +1 -0
- package/src/outbox/outbox-worker.ts +31 -0
- package/src/outbox/outbox.ts +5 -0
- package/src/providers/accounts/accounts.ts +36 -0
- package/src/providers/accounts/acp-account.ts +22 -27
- package/src/providers/accounts/launch.ts +4 -0
- package/src/providers/accounts/service.ts +110 -12
- package/src/providers/accounts/shadow-home.ts +10 -0
- package/src/providers/accounts/status.ts +13 -4
- package/src/providers/accounts/store.ts +25 -0
- package/src/providers/accounts/variables.ts +11 -1
- package/src/providers/acp-agent-state.ts +24 -3
- package/src/providers/acp-provider.ts +5 -0
- package/src/providers/acp-usage-log.ts +2 -0
- package/src/providers/catalog.ts +17 -0
- package/src/providers/claude.ts +10 -1
- package/src/providers/codex-rules.ts +3 -0
- package/src/providers/codex.ts +2 -1
- package/src/providers/detect.ts +6 -1
- package/src/providers/provider.ts +1 -1
- package/src/providers/registry.ts +18 -2
- package/src/providers/shell-commands.ts +37 -2
- package/src/record-directory.ts +19 -0
- package/src/run-process.ts +24 -3
- package/src/serializer.ts +3 -0
- package/src/skills.ts +73 -10
- package/src/tasks/background-limit.ts +9 -2
- package/src/tasks/end-children.ts +9 -0
- package/src/tasks/give-task.ts +8 -0
- package/src/tasks/task-coordinator.ts +81 -17
- package/src/tasks/task-store.ts +28 -0
- package/src/tasks/waiting-child.ts +21 -0
- package/src/tasks/wake-parent.ts +26 -0
- package/src/tasks/wiring.ts +22 -0
- package/src/testing/accounts.ts +2 -0
- package/src/testing/fake-acp-capture.ts +16 -0
- package/src/testing/fake-acp.ts +66 -0
- package/src/testing/fake-claude.ts +177 -0
- package/src/testing/fake-cli.ts +29 -0
- package/src/testing/fake-codex.ts +122 -0
- package/src/testing/manual-clock.ts +2 -0
- package/src/testing/watch.ts +7 -1
- package/src/title-file.ts +12 -0
- package/src/usage/accounts.ts +6 -0
- package/src/usage/aggregate.ts +19 -0
- package/src/usage/exchange.ts +12 -0
- package/src/usage/index-file.ts +27 -0
- package/src/usage/limits/monitor.ts +31 -3
- package/src/usage/limits/normalize.ts +30 -0
- package/src/usage/limits/probe.ts +17 -0
- package/src/usage/pricing.ts +35 -0
- package/src/usage/projects.ts +8 -0
- package/src/usage/readers/claude.ts +12 -9
- package/src/usage/readers/codex.ts +40 -8
- package/src/usage/readers/grok.ts +17 -9
- package/src/usage/readers/host-log.ts +9 -9
- package/src/usage/record.ts +22 -0
- package/src/usage/roots.ts +6 -0
- package/src/usage/scanner.ts +61 -0
- package/src/usage/usage-service.ts +14 -0
- package/src/watch-seam.ts +8 -0
- 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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|