@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.
- package/LICENSE +110 -0
- package/README.md +48 -0
- package/dist/async.d.ts +2 -0
- package/dist/async.js +31 -0
- package/dist/chat/attachment-store.d.ts +10 -0
- package/dist/chat/attachment-store.js +93 -0
- package/dist/chat/backend.d.ts +214 -0
- package/dist/chat/backend.js +1 -0
- package/dist/chat/background-work.d.ts +8 -0
- package/dist/chat/background-work.js +33 -0
- package/dist/chat/bookmark-store.d.ts +25 -0
- package/dist/chat/bookmark-store.js +141 -0
- package/dist/chat/chat-core.d.ts +311 -0
- package/dist/chat/chat-core.js +1025 -0
- package/dist/chat/chat-log.d.ts +33 -0
- package/dist/chat/chat-log.js +163 -0
- package/dist/chat/chat-process.d.ts +46 -0
- package/dist/chat/chat-process.js +190 -0
- package/dist/chat/chat-session.d.ts +209 -0
- package/dist/chat/chat-session.js +1511 -0
- package/dist/chat/chat-store.d.ts +37 -0
- package/dist/chat/chat-store.js +202 -0
- package/dist/chat/chat-title.d.ts +10 -0
- package/dist/chat/chat-title.js +74 -0
- package/dist/chat/claude-backend.d.ts +35 -0
- package/dist/chat/claude-backend.js +214 -0
- package/dist/chat/claude-protocol.d.ts +41 -0
- package/dist/chat/claude-protocol.js +642 -0
- package/dist/chat/claude-title.d.ts +11 -0
- package/dist/chat/claude-title.js +97 -0
- package/dist/chat/claude-transcript.d.ts +28 -0
- package/dist/chat/claude-transcript.js +211 -0
- package/dist/chat/codex-backend.d.ts +51 -0
- package/dist/chat/codex-backend.js +364 -0
- package/dist/chat/codex-protocol.d.ts +56 -0
- package/dist/chat/codex-protocol.js +767 -0
- package/dist/chat/codex-thread.d.ts +32 -0
- package/dist/chat/codex-thread.js +131 -0
- package/dist/chat/codex-transport.d.ts +42 -0
- package/dist/chat/codex-transport.js +188 -0
- package/dist/chat/composer-preferences.d.ts +15 -0
- package/dist/chat/composer-preferences.js +46 -0
- package/dist/chat/context-breakdown.d.ts +2 -0
- package/dist/chat/context-breakdown.js +74 -0
- package/dist/chat/delta-coalescer.d.ts +12 -0
- package/dist/chat/delta-coalescer.js +55 -0
- package/dist/chat/errors.d.ts +4 -0
- package/dist/chat/errors.js +4 -0
- package/dist/chat/fake-claude.d.ts +3 -0
- package/dist/chat/fake-claude.js +982 -0
- package/dist/chat/fake-cli.d.ts +25 -0
- package/dist/chat/fake-cli.js +157 -0
- package/dist/chat/fake-codex.d.ts +10 -0
- package/dist/chat/fake-codex.js +502 -0
- package/dist/chat/fixtures/claude-2.1.285-auto-fallback.json +24 -0
- package/dist/chat/fixtures/claude-2.1.285-native-children.json +6600 -0
- package/dist/chat/fixtures/claude-2.1.285-resume-direct-compact.json +202 -0
- package/dist/chat/fixtures/claude-2.1.285-resume-task-notification.json +57 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.jsonl +10 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.meta.json +1 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.jsonl +2 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.meta.json +1 -0
- package/dist/chat/fixtures/codex-0.159.2-native-approval.json +119 -0
- package/dist/chat/fixtures/codex-0.159.2-native-file-approval.json +136 -0
- package/dist/chat/fixtures/codex-0.159.2-native-interacted.json +220 -0
- package/dist/chat/fixtures/codex-0.159.2-usage-windows.json +74 -0
- package/dist/chat/input.d.ts +19 -0
- package/dist/chat/input.js +71 -0
- package/dist/chat/limit-resume.d.ts +11 -0
- package/dist/chat/limit-resume.js +68 -0
- package/dist/chat/projector.d.ts +60 -0
- package/dist/chat/projector.js +860 -0
- package/dist/chat/request-summary.d.ts +3 -0
- package/dist/chat/request-summary.js +143 -0
- package/dist/chat/subagent-projection.d.ts +8 -0
- package/dist/chat/subagent-projection.js +40 -0
- package/dist/chat/subagent-reader.d.ts +66 -0
- package/dist/chat/subagent-reader.js +396 -0
- package/dist/chat/subagent-settlement.d.ts +6 -0
- package/dist/chat/subagent-settlement.js +103 -0
- package/dist/chat/thread.d.ts +30 -0
- package/dist/chat/thread.js +147 -0
- package/dist/chat/wake-chat.d.ts +20 -0
- package/dist/chat/wake-chat.js +27 -0
- package/dist/client-sinks.d.ts +9 -0
- package/dist/client-sinks.js +34 -0
- package/dist/coded-error.d.ts +5 -0
- package/dist/coded-error.js +17 -0
- package/dist/context/argv.d.ts +11 -0
- package/dist/context/argv.js +58 -0
- package/dist/context/refusal.d.ts +9 -0
- package/dist/context/refusal.js +38 -0
- package/dist/context/verb.d.ts +85 -0
- package/dist/context/verb.js +233 -0
- package/dist/error-text.d.ts +5 -0
- package/dist/error-text.js +24 -0
- package/dist/events.d.ts +12 -0
- package/dist/events.js +1 -0
- package/dist/fs.d.ts +9 -0
- package/dist/fs.js +107 -0
- package/dist/host/agent-host.d.ts +28 -0
- package/dist/host/agent-host.js +105 -0
- package/dist/host/environment.d.ts +7 -0
- package/dist/host/environment.js +22 -0
- package/dist/host/handlers.d.ts +30 -0
- package/dist/host/handlers.js +111 -0
- package/dist/host/memory-port.d.ts +2 -0
- package/dist/host/memory-port.js +25 -0
- package/dist/host/wiring.d.ts +40 -0
- package/dist/host/wiring.js +120 -0
- package/dist/lineage.d.ts +44 -0
- package/dist/lineage.js +134 -0
- package/dist/messages/deliver-message.d.ts +21 -0
- package/dist/messages/deliver-message.js +42 -0
- package/dist/messages/deliver-notice.d.ts +25 -0
- package/dist/messages/deliver-notice.js +75 -0
- package/dist/messages/notice-notes.d.ts +15 -0
- package/dist/messages/notice-notes.js +26 -0
- package/dist/messages/notice-store.d.ts +30 -0
- package/dist/messages/notice-store.js +120 -0
- package/dist/modes.d.ts +11 -0
- package/dist/modes.js +28 -0
- package/dist/outbox/manual-clock.d.ts +10 -0
- package/dist/outbox/manual-clock.js +25 -0
- package/dist/outbox/outbox-worker.d.ts +44 -0
- package/dist/outbox/outbox-worker.js +173 -0
- package/dist/outbox/outbox.d.ts +37 -0
- package/dist/outbox/outbox.js +76 -0
- package/dist/providers/accounts/accounts.d.ts +20 -0
- package/dist/providers/accounts/accounts.js +158 -0
- package/dist/providers/accounts/launch.d.ts +17 -0
- package/dist/providers/accounts/launch.js +30 -0
- package/dist/providers/accounts/service.d.ts +87 -0
- package/dist/providers/accounts/service.js +538 -0
- package/dist/providers/accounts/shadow-home.d.ts +8 -0
- package/dist/providers/accounts/shadow-home.js +70 -0
- package/dist/providers/accounts/status.d.ts +12 -0
- package/dist/providers/accounts/status.js +85 -0
- package/dist/providers/accounts/store.d.ts +14 -0
- package/dist/providers/accounts/store.js +112 -0
- package/dist/providers/accounts/test-accounts.d.ts +12 -0
- package/dist/providers/accounts/test-accounts.js +38 -0
- package/dist/providers/accounts/variables.d.ts +27 -0
- package/dist/providers/accounts/variables.js +94 -0
- package/dist/providers/catalog.d.ts +17 -0
- package/dist/providers/catalog.js +76 -0
- package/dist/providers/claude-models.json +114 -0
- package/dist/providers/claude-provider.d.ts +4 -0
- package/dist/providers/claude-provider.js +29 -0
- package/dist/providers/claude.d.ts +15 -0
- package/dist/providers/claude.js +105 -0
- package/dist/providers/codex-models.json +135 -0
- package/dist/providers/codex-provider.d.ts +4 -0
- package/dist/providers/codex-provider.js +27 -0
- package/dist/providers/codex-rules.d.ts +9 -0
- package/dist/providers/codex-rules.js +35 -0
- package/dist/providers/codex.d.ts +14 -0
- package/dist/providers/codex.js +52 -0
- package/dist/providers/detect.d.ts +6 -0
- package/dist/providers/detect.js +30 -0
- package/dist/providers/provider.d.ts +25 -0
- package/dist/providers/provider.js +1 -0
- package/dist/providers/registry.d.ts +28 -0
- package/dist/providers/registry.js +118 -0
- package/dist/record-directory.d.ts +27 -0
- package/dist/record-directory.js +114 -0
- package/dist/run-process.d.ts +13 -0
- package/dist/run-process.js +42 -0
- package/dist/serializer.d.ts +9 -0
- package/dist/serializer.js +34 -0
- package/dist/skills.d.ts +21 -0
- package/dist/skills.js +174 -0
- package/dist/tasks/background-limit.d.ts +16 -0
- package/dist/tasks/background-limit.js +55 -0
- package/dist/tasks/end-children.d.ts +34 -0
- package/dist/tasks/end-children.js +53 -0
- package/dist/tasks/give-task.d.ts +14 -0
- package/dist/tasks/give-task.js +33 -0
- package/dist/tasks/task-coordinator.d.ts +52 -0
- package/dist/tasks/task-coordinator.js +323 -0
- package/dist/tasks/task-store.d.ts +49 -0
- package/dist/tasks/task-store.js +197 -0
- package/dist/tasks/task-work.d.ts +47 -0
- package/dist/tasks/task-work.js +28 -0
- package/dist/tasks/waiting-child.d.ts +51 -0
- package/dist/tasks/waiting-child.js +143 -0
- package/dist/tasks/wake-parent.d.ts +19 -0
- package/dist/tasks/wake-parent.js +109 -0
- package/dist/tasks/wiring.d.ts +51 -0
- package/dist/tasks/wiring.js +151 -0
- package/dist/title-file.d.ts +2 -0
- package/dist/title-file.js +55 -0
- package/dist/usage/accounts.d.ts +15 -0
- package/dist/usage/accounts.js +64 -0
- package/dist/usage/aggregate.d.ts +12 -0
- package/dist/usage/aggregate.js +154 -0
- package/dist/usage/exchange.d.ts +16 -0
- package/dist/usage/exchange.js +84 -0
- package/dist/usage/index-file.d.ts +15 -0
- package/dist/usage/index-file.js +123 -0
- package/dist/usage/limits/monitor.d.ts +50 -0
- package/dist/usage/limits/monitor.js +179 -0
- package/dist/usage/limits/normalize.d.ts +21 -0
- package/dist/usage/limits/normalize.js +160 -0
- package/dist/usage/limits/probe.d.ts +9 -0
- package/dist/usage/limits/probe.js +113 -0
- package/dist/usage/prices-snapshot.json +1071 -0
- package/dist/usage/pricing.d.ts +34 -0
- package/dist/usage/pricing.js +186 -0
- package/dist/usage/projects.d.ts +17 -0
- package/dist/usage/projects.js +49 -0
- package/dist/usage/readers/claude.d.ts +3 -0
- package/dist/usage/readers/claude.js +59 -0
- package/dist/usage/readers/codex.d.ts +23 -0
- package/dist/usage/readers/codex.js +155 -0
- package/dist/usage/record.d.ts +15 -0
- package/dist/usage/record.js +39 -0
- package/dist/usage/roots.d.ts +12 -0
- package/dist/usage/roots.js +29 -0
- package/dist/usage/scanner.d.ts +30 -0
- package/dist/usage/scanner.js +234 -0
- package/dist/usage/usage-service.d.ts +35 -0
- package/dist/usage/usage-service.js +130 -0
- package/dist/watch-seam.d.ts +29 -0
- package/dist/watch-seam.js +78 -0
- package/dist/watch-test-helpers.d.ts +28 -0
- package/dist/watch-test-helpers.js +86 -0
- package/package.json +42 -3
- package/src/async.ts +40 -0
- package/src/chat/attachment-store.ts +108 -0
- package/src/chat/backend.ts +199 -0
- package/src/chat/background-work.ts +42 -0
- package/src/chat/bookmark-store.ts +166 -0
- package/src/chat/chat-core.ts +1228 -0
- package/src/chat/chat-log.ts +187 -0
- package/src/chat/chat-process.ts +237 -0
- package/src/chat/chat-session.ts +1723 -0
- package/src/chat/chat-store.ts +261 -0
- package/src/chat/chat-title.ts +90 -0
- package/src/chat/claude-backend.ts +235 -0
- package/src/chat/claude-protocol.ts +684 -0
- package/src/chat/claude-title.ts +106 -0
- package/src/chat/claude-transcript.ts +233 -0
- package/src/chat/codex-backend.ts +397 -0
- package/src/chat/codex-protocol.ts +872 -0
- package/src/chat/codex-thread.ts +162 -0
- package/src/chat/codex-transport.ts +224 -0
- package/src/chat/composer-preferences.ts +67 -0
- package/src/chat/context-breakdown.ts +80 -0
- package/src/chat/delta-coalescer.ts +62 -0
- package/src/chat/errors.ts +25 -0
- package/src/chat/fake-claude.ts +1006 -0
- package/src/chat/fake-cli.ts +199 -0
- package/src/chat/fake-codex.ts +534 -0
- package/src/chat/fixtures/claude-2.1.285-auto-fallback.json +24 -0
- package/src/chat/fixtures/claude-2.1.285-native-children.json +6600 -0
- package/src/chat/fixtures/claude-2.1.285-resume-direct-compact.json +202 -0
- package/src/chat/fixtures/claude-2.1.285-resume-task-notification.json +57 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.jsonl +10 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.meta.json +1 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.jsonl +2 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.meta.json +1 -0
- package/src/chat/fixtures/codex-0.159.2-native-approval.json +119 -0
- package/src/chat/fixtures/codex-0.159.2-native-file-approval.json +136 -0
- package/src/chat/fixtures/codex-0.159.2-native-interacted.json +220 -0
- package/src/chat/fixtures/codex-0.159.2-usage-windows.json +74 -0
- package/src/chat/input.ts +89 -0
- package/src/chat/limit-resume.ts +74 -0
- package/src/chat/projector.ts +969 -0
- package/src/chat/request-summary.ts +168 -0
- package/src/chat/subagent-projection.ts +45 -0
- package/src/chat/subagent-reader.ts +469 -0
- package/src/chat/subagent-settlement.ts +111 -0
- package/src/chat/thread.ts +167 -0
- package/src/chat/wake-chat.ts +45 -0
- package/src/client-sinks.ts +39 -0
- package/src/coded-error.ts +18 -0
- package/src/context/argv.ts +61 -0
- package/src/context/refusal.ts +50 -0
- package/src/context/verb.ts +392 -0
- package/src/error-text.ts +28 -0
- package/src/events.ts +9 -0
- package/src/fs.ts +118 -0
- package/src/host/agent-host.ts +124 -0
- package/src/host/environment.ts +31 -0
- package/src/host/handlers.ts +142 -0
- package/src/host/memory-port.ts +27 -0
- package/src/host/wiring.ts +164 -0
- package/src/lineage.ts +159 -0
- package/src/messages/deliver-message.ts +61 -0
- package/src/messages/deliver-notice.ts +120 -0
- package/src/messages/notice-notes.ts +34 -0
- package/src/messages/notice-store.ts +142 -0
- package/src/modes.ts +38 -0
- package/src/outbox/manual-clock.ts +31 -0
- package/src/outbox/outbox-worker.ts +214 -0
- package/src/outbox/outbox.ts +106 -0
- package/src/providers/accounts/accounts.ts +192 -0
- package/src/providers/accounts/launch.ts +55 -0
- package/src/providers/accounts/service.ts +632 -0
- package/src/providers/accounts/shadow-home.ts +85 -0
- package/src/providers/accounts/status.ts +106 -0
- package/src/providers/accounts/store.ts +158 -0
- package/src/providers/accounts/test-accounts.ts +48 -0
- package/src/providers/accounts/variables.ts +132 -0
- package/src/providers/catalog.ts +90 -0
- package/src/providers/claude-models.json +114 -0
- package/src/providers/claude-provider.ts +32 -0
- package/src/providers/claude.ts +123 -0
- package/src/providers/codex-models.json +135 -0
- package/src/providers/codex-provider.ts +30 -0
- package/src/providers/codex-rules.ts +54 -0
- package/src/providers/codex.ts +69 -0
- package/src/providers/detect.ts +40 -0
- package/src/providers/provider.ts +46 -0
- package/src/providers/registry.ts +150 -0
- package/src/record-directory.ts +136 -0
- package/src/run-process.ts +61 -0
- package/src/serializer.ts +41 -0
- package/src/skills.ts +199 -0
- package/src/tasks/background-limit.ts +72 -0
- package/src/tasks/end-children.ts +100 -0
- package/src/tasks/give-task.ts +52 -0
- package/src/tasks/task-coordinator.ts +384 -0
- package/src/tasks/task-store.ts +236 -0
- package/src/tasks/task-work.ts +62 -0
- package/src/tasks/waiting-child.ts +203 -0
- package/src/tasks/wake-parent.ts +142 -0
- package/src/tasks/wiring.ts +231 -0
- package/src/title-file.ts +57 -0
- package/src/usage/accounts.ts +86 -0
- package/src/usage/aggregate.ts +220 -0
- package/src/usage/exchange.ts +93 -0
- package/src/usage/index-file.ts +178 -0
- package/src/usage/limits/monitor.ts +239 -0
- package/src/usage/limits/normalize.ts +188 -0
- package/src/usage/limits/probe.ts +120 -0
- package/src/usage/prices-snapshot.json +1071 -0
- package/src/usage/pricing.ts +236 -0
- package/src/usage/projects.ts +65 -0
- package/src/usage/readers/claude.ts +60 -0
- package/src/usage/readers/codex.ts +196 -0
- package/src/usage/record.ts +60 -0
- package/src/usage/roots.ts +39 -0
- package/src/usage/scanner.ts +265 -0
- package/src/usage/usage-service.ts +161 -0
- package/src/watch-seam.ts +111 -0
- package/src/watch-test-helpers.ts +102 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { AgentKindSchema } from '@adecore/agent-contracts';
|
|
5
|
+
import { ClientSinks } from '../../client-sinks.js';
|
|
6
|
+
import { errorText } from '../../error-text.js';
|
|
7
|
+
import { accountEnv, accountFolder, canContinue, defaultFolder, expandHome, isDefaultAccount, isKnownKind, transcriptFolder } from './accounts.js';
|
|
8
|
+
import { AccountError, isDefaultAccountOf } from './launch.js';
|
|
9
|
+
import { prepareShadowHome } from './shadow-home.js';
|
|
10
|
+
import { askAccountAs } from './status.js';
|
|
11
|
+
import { acceptAccounts, accountsPath, InvalidAccountError, readAccounts, wireAccounts, writeAccounts } from './store.js';
|
|
12
|
+
import { DEFAULT_ACCOUNTS_HOST, platformSecrets, secretKey, SecretsUnavailableError } from './variables.js';
|
|
13
|
+
import { DEFAULT_CODEX_CLIENT } from '../../chat/codex-transport.js';
|
|
14
|
+
/* Who is signed in changes by hand and rarely; often enough to notice, rarely enough not to start CLIs all day. */
|
|
15
|
+
const CHECK_INTERVAL_MS = 15 * 60_000;
|
|
16
|
+
const MAX_BACKOFF_MS = 60 * 60_000;
|
|
17
|
+
// A login in a terminal is a browser round trip: quick enough to notice at once, and given up on after a while.
|
|
18
|
+
const LOGIN_POLL_MS = 3_000;
|
|
19
|
+
const LOGIN_WATCH_MS = 5 * 60_000;
|
|
20
|
+
// 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.
|
|
21
|
+
function isFolder(path) {
|
|
22
|
+
return statSync(path, { throwIfNoEntry: false })?.isDirectory() === true;
|
|
23
|
+
}
|
|
24
|
+
function sleep(ms) {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
setTimeout(resolve, ms).unref();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/* The part of a label an id can hold: `Work (EU)` becomes `work-eu`. */
|
|
30
|
+
function slugOf(label) {
|
|
31
|
+
return label
|
|
32
|
+
.normalize('NFKD')
|
|
33
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
36
|
+
.replace(/^-+|-+$/g, '');
|
|
37
|
+
}
|
|
38
|
+
function sensitiveNames(entry) {
|
|
39
|
+
const variables = typeof entry === 'object' && entry !== null ? entry.env : undefined;
|
|
40
|
+
if (!Array.isArray(variables)) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
return variables.flatMap((variable) => {
|
|
44
|
+
const { name, sensitive } = (variable ?? {});
|
|
45
|
+
return sensitive === true && typeof name === 'string' ? [name] : [];
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/* A status without the moment it was drawn, to tell whether anything a person reads changed. */
|
|
49
|
+
function sameStatus(left, right) {
|
|
50
|
+
return JSON.stringify({ ...left, checkedAt: 0 }) === JSON.stringify({ ...right, checkedAt: 0 });
|
|
51
|
+
}
|
|
52
|
+
/*
|
|
53
|
+
* The accounts of every agent CLI on this machine: the list a person keeps in `providers.json`, and
|
|
54
|
+
* what each CLI says about who is signed in there. The list is the person's; the statuses are only
|
|
55
|
+
* ever the CLI's answer, held in memory and asked again on a slow clock.
|
|
56
|
+
*/
|
|
57
|
+
export class ProviderAccountsService {
|
|
58
|
+
path;
|
|
59
|
+
home;
|
|
60
|
+
host;
|
|
61
|
+
providers;
|
|
62
|
+
env;
|
|
63
|
+
ask;
|
|
64
|
+
prepareShadow;
|
|
65
|
+
folderExists;
|
|
66
|
+
install;
|
|
67
|
+
secrets;
|
|
68
|
+
// The sensitive values by `secretKey`, read from the keychain once, since a spawn asks for them without waiting.
|
|
69
|
+
secretValues = new Map();
|
|
70
|
+
now;
|
|
71
|
+
sleep;
|
|
72
|
+
watches = new Map();
|
|
73
|
+
writes = Promise.resolve();
|
|
74
|
+
sinks = new ClientSinks();
|
|
75
|
+
listeners = new Set();
|
|
76
|
+
// When a CLI last started under each account, by id; kept in memory, so a restart forgets it.
|
|
77
|
+
launches = new Map();
|
|
78
|
+
states = new Map();
|
|
79
|
+
stored = {};
|
|
80
|
+
accounts = wireAccounts({});
|
|
81
|
+
passes = Promise.resolve();
|
|
82
|
+
timer = null;
|
|
83
|
+
constructor(options) {
|
|
84
|
+
this.path = accountsPath(options.home);
|
|
85
|
+
this.home = options.home;
|
|
86
|
+
this.host = options.host ?? DEFAULT_ACCOUNTS_HOST;
|
|
87
|
+
this.providers = options.providers;
|
|
88
|
+
this.env = options.env ?? process.env;
|
|
89
|
+
this.ask = options.ask ?? askAccountAs(options.client ?? DEFAULT_CODEX_CLIENT);
|
|
90
|
+
this.prepareShadow = options.prepareShadow ?? prepareShadowHome;
|
|
91
|
+
this.folderExists = options.folderExists ?? isFolder;
|
|
92
|
+
this.install = options.install;
|
|
93
|
+
this.secrets = options.secrets === undefined ? platformSecrets(this.host, options.home) : options.secrets;
|
|
94
|
+
this.now = options.now ?? Date.now;
|
|
95
|
+
this.sleep = options.sleep ?? sleep;
|
|
96
|
+
}
|
|
97
|
+
async load() {
|
|
98
|
+
this.stored = await readAccounts(this.path, (kind) => this.providers.get(kind), this.env, this.host);
|
|
99
|
+
this.accounts = wireAccounts(this.stored);
|
|
100
|
+
await this.readSecrets();
|
|
101
|
+
}
|
|
102
|
+
subscribe(clientId, sink) {
|
|
103
|
+
return this.sinks.subscribe(clientId, sink);
|
|
104
|
+
}
|
|
105
|
+
snapshot() {
|
|
106
|
+
const loginCommands = {};
|
|
107
|
+
for (const kind of AgentKindSchema.options) {
|
|
108
|
+
const command = this.providers.get(kind).home?.loginCommand;
|
|
109
|
+
if (command !== undefined) {
|
|
110
|
+
loginCommands[kind] = command;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
accounts: this.accounts,
|
|
115
|
+
statuses: Object.entries(this.accounts).map(([id, account]) => this.statusOf(id, account)),
|
|
116
|
+
secretsAvailable: this.secrets !== null,
|
|
117
|
+
loginCommands
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/* The account under an id, for a launch; null for an id this machine does not have. */
|
|
121
|
+
get(id) {
|
|
122
|
+
return this.accounts[id] ?? null;
|
|
123
|
+
}
|
|
124
|
+
/*
|
|
125
|
+
* The environment a CLI of this kind runs in under this account. An account that cannot be used
|
|
126
|
+
* refuses the start, since falling back on the default account would run it on someone else's costs.
|
|
127
|
+
*/
|
|
128
|
+
envFor(kind, id, baseEnv) {
|
|
129
|
+
const key = id ?? kind;
|
|
130
|
+
const refuse = (why) => new AccountError('account-unavailable', `The account '${this.labelOf(key)}' is not available on this machine: ${why}.`);
|
|
131
|
+
const provider = this.providers.get(kind);
|
|
132
|
+
const variables = (account) => {
|
|
133
|
+
const { values, missing } = this.variablesOf(key, account);
|
|
134
|
+
if (missing !== null) {
|
|
135
|
+
throw refuse(`the keychain does not give the value of ${missing}`);
|
|
136
|
+
}
|
|
137
|
+
return values;
|
|
138
|
+
};
|
|
139
|
+
if (isDefaultAccountOf(kind, id)) {
|
|
140
|
+
const account = this.accounts[kind] ?? { kind };
|
|
141
|
+
return accountEnv(kind, account, provider, baseEnv, variables(account));
|
|
142
|
+
}
|
|
143
|
+
const account = this.accounts[key];
|
|
144
|
+
if (account === undefined) {
|
|
145
|
+
throw refuse('no account on it has that id');
|
|
146
|
+
}
|
|
147
|
+
if (account.kind !== kind) {
|
|
148
|
+
throw refuse(`it is an account of ${isKnownKind(account.kind) ? this.providers.get(account.kind).name : account.kind}, not of ${provider.name}`);
|
|
149
|
+
}
|
|
150
|
+
if (account.enabled === false) {
|
|
151
|
+
throw refuse('it is turned off');
|
|
152
|
+
}
|
|
153
|
+
if (provider.home === undefined || account.home === undefined) {
|
|
154
|
+
throw refuse(`${provider.name} has no setting for its config folder`);
|
|
155
|
+
}
|
|
156
|
+
for (const folder of [account.home, account.shadowHome]) {
|
|
157
|
+
if (folder !== undefined && !this.folderExists(expandHome(folder, this.env))) {
|
|
158
|
+
throw refuse(`its folder ${folder} is missing`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return accountEnv(key, account, provider, baseEnv, variables(account));
|
|
162
|
+
}
|
|
163
|
+
transcriptFolder(kind, id) {
|
|
164
|
+
const key = id ?? kind;
|
|
165
|
+
const account = this.accounts[key];
|
|
166
|
+
if (account === undefined || account.kind !== kind) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return transcriptFolder(key, account, this.providers.get(kind), this.env);
|
|
170
|
+
}
|
|
171
|
+
homeFolder(kind, id) {
|
|
172
|
+
const key = id ?? kind;
|
|
173
|
+
const account = this.accounts[key];
|
|
174
|
+
if (account === undefined || account.kind !== kind) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
return accountFolder(key, account, this.providers.get(kind), this.env);
|
|
178
|
+
}
|
|
179
|
+
canContinue(kind, from, to) {
|
|
180
|
+
const fromId = from ?? kind;
|
|
181
|
+
const toId = to ?? kind;
|
|
182
|
+
const fromAccount = this.accounts[fromId];
|
|
183
|
+
const toAccount = this.accounts[toId];
|
|
184
|
+
if (fromId === toId) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
if (fromAccount === undefined || toAccount === undefined || fromAccount.kind !== kind) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
return canContinue({ id: fromId, account: fromAccount }, { id: toId, account: toAccount }, (each) => this.providers.get(each), this.env);
|
|
191
|
+
}
|
|
192
|
+
labelOf(id) {
|
|
193
|
+
return this.accounts[id]?.label ?? id;
|
|
194
|
+
}
|
|
195
|
+
launched(kind, id) {
|
|
196
|
+
this.launches.set(id ?? kind, this.now());
|
|
197
|
+
}
|
|
198
|
+
/* When a CLI last started under this account; null when none did since the host started. */
|
|
199
|
+
lastLaunchAt(id) {
|
|
200
|
+
return this.launches.get(id) ?? null;
|
|
201
|
+
}
|
|
202
|
+
/* Told whenever the accounts or what their CLIs said changed, after the clients were. */
|
|
203
|
+
listen(listener) {
|
|
204
|
+
this.listeners.add(listener);
|
|
205
|
+
return () => {
|
|
206
|
+
this.listeners.delete(listener);
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/* Checks every account once at once, and the ones due on the clock after that. */
|
|
210
|
+
start() {
|
|
211
|
+
this.timer ??= setInterval(() => void this.queue(false), CHECK_INTERVAL_MS);
|
|
212
|
+
void this.queue(false);
|
|
213
|
+
}
|
|
214
|
+
stop() {
|
|
215
|
+
if (this.timer !== null) {
|
|
216
|
+
clearInterval(this.timer);
|
|
217
|
+
this.timer = null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/* Answers at once; an account that changed reads `checking` until its CLI answered, which arrives as `accounts.changed`. */
|
|
221
|
+
save(accounts) {
|
|
222
|
+
return this.serial(() => this.commit(accounts, this.folderExists));
|
|
223
|
+
}
|
|
224
|
+
/*
|
|
225
|
+
* An account in a folder of its own under `<home>/accounts`, signed in by nobody yet. A
|
|
226
|
+
* Codex account is a shadow home over the CLI's own folder, so its threads go on under the other.
|
|
227
|
+
*/
|
|
228
|
+
create(payload) {
|
|
229
|
+
return this.serial(async () => {
|
|
230
|
+
const { kind, label, color } = payload;
|
|
231
|
+
const provider = this.providers.get(kind);
|
|
232
|
+
if (provider.home === undefined) {
|
|
233
|
+
throw new InvalidAccountError(`${provider.name} has no setting for its config folder, so it only has its default account`);
|
|
234
|
+
}
|
|
235
|
+
const id = this.mintId(kind, label);
|
|
236
|
+
const folder = join(this.accountsFolder(), id);
|
|
237
|
+
await mkdir(this.accountsFolder(), { recursive: true, mode: 0o700 });
|
|
238
|
+
await mkdir(folder, { mode: 0o700 });
|
|
239
|
+
const folders = kind === 'codex' ? { home: defaultFolder(provider, this.env), shadowHome: folder } : { home: folder };
|
|
240
|
+
const account = { kind, label, ...(color === undefined ? {} : { color }), ...folders };
|
|
241
|
+
await this.prepare(id, kind, account);
|
|
242
|
+
const snapshot = await this.commit({ ...this.accounts, [id]: account }, (path) => path === folder || this.folderExists(path));
|
|
243
|
+
return { ...snapshot, id };
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/*
|
|
247
|
+
* Asks the CLI of one account again every few seconds, for a login running in a terminal, until
|
|
248
|
+
* it is signed in or a few minutes passed. Resolves when the watch ends; a second call for the
|
|
249
|
+
* same account only moves the end.
|
|
250
|
+
*/
|
|
251
|
+
watchLogin(id) {
|
|
252
|
+
if (this.accounts[id] === undefined) {
|
|
253
|
+
throw new AccountError('account-unavailable', `The account '${id}' is not available on this machine: no account on it has that id.`);
|
|
254
|
+
}
|
|
255
|
+
const deadline = this.now() + LOGIN_WATCH_MS;
|
|
256
|
+
const running = this.watches.get(id);
|
|
257
|
+
if (running !== undefined) {
|
|
258
|
+
running.deadline = deadline;
|
|
259
|
+
return running.done;
|
|
260
|
+
}
|
|
261
|
+
const watch = { deadline, done: Promise.resolve() };
|
|
262
|
+
watch.done = this.runWatch(id, watch).finally(() => this.watches.delete(id));
|
|
263
|
+
this.watches.set(id, watch);
|
|
264
|
+
return watch.done;
|
|
265
|
+
}
|
|
266
|
+
/* Asks every CLI again, and answers once they all did. */
|
|
267
|
+
async refresh() {
|
|
268
|
+
await this.queue(true);
|
|
269
|
+
return this.snapshot();
|
|
270
|
+
}
|
|
271
|
+
async runWatch(id, watch) {
|
|
272
|
+
while (this.now() < watch.deadline) {
|
|
273
|
+
await this.sleep(LOGIN_POLL_MS);
|
|
274
|
+
if (this.accounts[id] === undefined) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
await this.queue(true, id);
|
|
278
|
+
if (this.states.get(id)?.status.state === 'ready') {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/* One change of the file at a time: a create and a save both read what the other just wrote. */
|
|
284
|
+
serial(work) {
|
|
285
|
+
const next = this.writes.then(work);
|
|
286
|
+
this.writes = next.catch(() => undefined);
|
|
287
|
+
return next;
|
|
288
|
+
}
|
|
289
|
+
async commit(accounts, isDirectory) {
|
|
290
|
+
const stored = acceptAccounts(accounts, this.stored, (kind) => this.providers.get(kind), this.env, isDirectory, this.host);
|
|
291
|
+
const { writes, removals } = this.planSecrets(stored);
|
|
292
|
+
for (const [key, value] of writes) {
|
|
293
|
+
await this.requireSecrets().write(key, value);
|
|
294
|
+
this.secretValues.set(key, value);
|
|
295
|
+
}
|
|
296
|
+
await writeAccounts(this.path, stored);
|
|
297
|
+
this.stored = stored;
|
|
298
|
+
this.accounts = wireAccounts(stored);
|
|
299
|
+
for (const key of removals) {
|
|
300
|
+
this.secretValues.delete(key);
|
|
301
|
+
await this.secrets?.remove(key).catch((e) => console.error(`Could not remove ${key} from the keychain:`, errorText(e)));
|
|
302
|
+
}
|
|
303
|
+
const rewritten = new Set([...writes.keys()].map((key) => key.slice(0, key.indexOf('/'))));
|
|
304
|
+
for (const id of this.states.keys()) {
|
|
305
|
+
// A new secret can be a new login, which the account as the file holds it does not show.
|
|
306
|
+
if (this.accounts[id] === undefined || rewritten.has(id)) {
|
|
307
|
+
this.states.delete(id);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
this.emit();
|
|
311
|
+
void this.queue(false);
|
|
312
|
+
return this.snapshot();
|
|
313
|
+
}
|
|
314
|
+
/*
|
|
315
|
+
* Takes every sensitive value out of the accounts about to be written, leaving the redacted form
|
|
316
|
+
* the file keeps. Answers which values go into the keychain and which keys no account has any more.
|
|
317
|
+
*/
|
|
318
|
+
planSecrets(stored) {
|
|
319
|
+
const writes = new Map();
|
|
320
|
+
const kept = new Set();
|
|
321
|
+
for (const [id, entry] of Object.entries(stored)) {
|
|
322
|
+
const kind = entry.kind;
|
|
323
|
+
if (!isKnownKind(kind)) {
|
|
324
|
+
for (const name of sensitiveNames(entry)) {
|
|
325
|
+
kept.add(secretKey(id, name));
|
|
326
|
+
}
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const account = entry;
|
|
330
|
+
if (account.env === undefined) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const had = new Set(sensitiveNames(this.stored[id]));
|
|
334
|
+
const variables = account.env.map((variable) => {
|
|
335
|
+
if (!variable.sensitive) {
|
|
336
|
+
return { name: variable.name, value: variable.value, sensitive: false };
|
|
337
|
+
}
|
|
338
|
+
const key = secretKey(id, variable.name);
|
|
339
|
+
this.requireSecrets();
|
|
340
|
+
if (variable.value !== '') {
|
|
341
|
+
writes.set(key, variable.value);
|
|
342
|
+
}
|
|
343
|
+
else if (variable.valueRedacted !== true || !had.has(variable.name)) {
|
|
344
|
+
throw new InvalidAccountError(`${id}: ${variable.name} has no value`);
|
|
345
|
+
}
|
|
346
|
+
kept.add(key);
|
|
347
|
+
return { name: variable.name, value: '', sensitive: true, valueRedacted: true };
|
|
348
|
+
});
|
|
349
|
+
const { env: _env, ...rest } = account;
|
|
350
|
+
stored[id] = variables.length === 0 ? rest : { ...rest, env: variables };
|
|
351
|
+
}
|
|
352
|
+
const removals = Object.entries(this.stored).flatMap(([id, entry]) => sensitiveNames(entry)
|
|
353
|
+
.map((name) => secretKey(id, name))
|
|
354
|
+
.filter((key) => !kept.has(key)));
|
|
355
|
+
return { writes, removals };
|
|
356
|
+
}
|
|
357
|
+
requireSecrets() {
|
|
358
|
+
if (this.secrets === null) {
|
|
359
|
+
throw new SecretsUnavailableError();
|
|
360
|
+
}
|
|
361
|
+
return this.secrets;
|
|
362
|
+
}
|
|
363
|
+
async readSecrets() {
|
|
364
|
+
if (this.secrets === null) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
for (const [id, entry] of Object.entries(this.stored)) {
|
|
368
|
+
for (const name of sensitiveNames(entry)) {
|
|
369
|
+
const key = secretKey(id, name);
|
|
370
|
+
try {
|
|
371
|
+
const value = await this.secrets.read(key);
|
|
372
|
+
if (value !== null) {
|
|
373
|
+
this.secretValues.set(key, value);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch (e) {
|
|
377
|
+
console.error(`Could not read ${key} from the keychain:`, errorText(e));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/* The variables of an account as a CLI gets them, and the first sensitive one the keychain did not give. */
|
|
383
|
+
variablesOf(id, account) {
|
|
384
|
+
const values = {};
|
|
385
|
+
for (const variable of account.env ?? []) {
|
|
386
|
+
const value = variable.sensitive ? this.secretValues.get(secretKey(id, variable.name)) : variable.value;
|
|
387
|
+
if (value === undefined) {
|
|
388
|
+
return { values, missing: variable.name };
|
|
389
|
+
}
|
|
390
|
+
values[variable.name] = value;
|
|
391
|
+
}
|
|
392
|
+
return { values, missing: null };
|
|
393
|
+
}
|
|
394
|
+
accountsFolder() {
|
|
395
|
+
return join(this.home, 'accounts');
|
|
396
|
+
}
|
|
397
|
+
/* A free id for a new account; a folder a removed account left behind is never handed to another. */
|
|
398
|
+
mintId(kind, label) {
|
|
399
|
+
const base = `${kind}_${slugOf(label) || 'account'}`.slice(0, 60).replace(/-+$/, '');
|
|
400
|
+
const taken = (id) => this.accounts[id] !== undefined || this.stored[id] !== undefined || existsSync(join(this.accountsFolder(), id));
|
|
401
|
+
let id = base;
|
|
402
|
+
for (let suffix = 2; taken(id); suffix += 1) {
|
|
403
|
+
id = `${base}-${suffix}`;
|
|
404
|
+
}
|
|
405
|
+
return id;
|
|
406
|
+
}
|
|
407
|
+
/* One pass at a time, in the order they were asked for, so a save during a pass is checked after it. */
|
|
408
|
+
queue(force, only) {
|
|
409
|
+
const pass = this.passes.then(() => this.runAll(force, only));
|
|
410
|
+
this.passes = pass.catch((e) => console.error('Could not check the provider accounts:', errorText(e)));
|
|
411
|
+
return this.passes;
|
|
412
|
+
}
|
|
413
|
+
statusOf(id, account) {
|
|
414
|
+
const state = this.states.get(id);
|
|
415
|
+
if (state !== undefined && state.drawnFor === JSON.stringify(account)) {
|
|
416
|
+
return state.status;
|
|
417
|
+
}
|
|
418
|
+
return this.status(id, account, 'checking', { checkedAt: 0 });
|
|
419
|
+
}
|
|
420
|
+
status(id, account, state, fields = {}) {
|
|
421
|
+
const provider = isKnownKind(account.kind) ? this.providers.get(account.kind) : null;
|
|
422
|
+
const folder = provider === null ? '' : accountFolder(id, account, provider, this.env);
|
|
423
|
+
return {
|
|
424
|
+
id,
|
|
425
|
+
kind: account.kind,
|
|
426
|
+
state,
|
|
427
|
+
email: null,
|
|
428
|
+
plan: null,
|
|
429
|
+
organization: null,
|
|
430
|
+
home: folder,
|
|
431
|
+
...(provider === null ? {} : { transcripts: transcriptFolder(id, account, provider, this.env) }),
|
|
432
|
+
message: null,
|
|
433
|
+
checkedAt: this.now(),
|
|
434
|
+
...fields
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
async runAll(force, only) {
|
|
438
|
+
const installed = await this.providers.list();
|
|
439
|
+
const due = Object.entries(this.accounts).filter(([id, account]) => {
|
|
440
|
+
if (only !== undefined && id !== only) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
const state = this.states.get(id);
|
|
444
|
+
return force || state === undefined || state.drawnFor !== JSON.stringify(account) || this.now() >= state.nextAt;
|
|
445
|
+
});
|
|
446
|
+
const results = await Promise.all(due.map(async ([id, account]) => ({ id, account, status: await this.check(id, account, installed) })));
|
|
447
|
+
let changed = false;
|
|
448
|
+
for (const { id, account, status } of results) {
|
|
449
|
+
if (this.accounts[id] === undefined) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
const before = this.states.get(id);
|
|
453
|
+
const drawnFor = JSON.stringify(account);
|
|
454
|
+
const failed = status.state === 'failed';
|
|
455
|
+
const backoffMs = failed ? Math.min(MAX_BACKOFF_MS, (before?.backoffMs ?? CHECK_INTERVAL_MS / 2) * 2) : CHECK_INTERVAL_MS;
|
|
456
|
+
this.states.set(id, { status, drawnFor, backoffMs, nextAt: this.now() + backoffMs });
|
|
457
|
+
// An account a save changed read `checking` since, whatever it read before.
|
|
458
|
+
changed ||= before === undefined || before.drawnFor !== drawnFor || !sameStatus(before.status, status);
|
|
459
|
+
}
|
|
460
|
+
if (changed) {
|
|
461
|
+
this.emit();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async check(id, account, installed) {
|
|
465
|
+
const kind = account.kind;
|
|
466
|
+
if (!isKnownKind(kind)) {
|
|
467
|
+
return this.status(id, account, 'unavailable', { message: `This version of ${this.host.name} does not know this CLI` });
|
|
468
|
+
}
|
|
469
|
+
const provider = this.providers.get(kind);
|
|
470
|
+
const isDefault = isDefaultAccount(id, account);
|
|
471
|
+
if (!isDefault && provider.home === undefined) {
|
|
472
|
+
return this.status(id, account, 'unavailable', {
|
|
473
|
+
message: `${provider.name} has no setting for its config folder, so it only has its default account`
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
if (account.enabled === false || this.providers.enabled?.(kind) === false) {
|
|
477
|
+
return this.status(id, account, 'disabled');
|
|
478
|
+
}
|
|
479
|
+
if (installed.find((info) => info.kind === kind)?.installed !== true) {
|
|
480
|
+
return this.status(id, account, 'not-found');
|
|
481
|
+
}
|
|
482
|
+
if (provider.home === undefined) {
|
|
483
|
+
return this.status(id, account, 'ready');
|
|
484
|
+
}
|
|
485
|
+
const folders = isDefault
|
|
486
|
+
? []
|
|
487
|
+
: [account.home, account.shadowHome].filter((folder) => folder !== undefined).map((folder) => expandHome(folder, this.env));
|
|
488
|
+
for (const folder of folders) {
|
|
489
|
+
if (!this.folderExists(folder)) {
|
|
490
|
+
return this.status(id, account, 'folder-missing', { message: `${folder} does not exist` });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
const message = await this.prepare(id, kind, account);
|
|
495
|
+
const { values, missing } = this.variablesOf(id, account);
|
|
496
|
+
if (missing !== null) {
|
|
497
|
+
throw new Error(`The keychain does not give the value of ${missing}`);
|
|
498
|
+
}
|
|
499
|
+
const reading = await this.ask(kind, provider.command, accountEnv(id, account, provider, this.env, values));
|
|
500
|
+
return this.status(id, account, reading.signedIn ? 'ready' : 'signed-out', {
|
|
501
|
+
email: reading.email,
|
|
502
|
+
plan: reading.plan,
|
|
503
|
+
organization: reading.organization,
|
|
504
|
+
message
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
catch (e) {
|
|
508
|
+
return this.status(id, account, 'failed', { message: errorText(e) });
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/*
|
|
512
|
+
* Puts the hooks in the account's folder and links a shadow home to the home it shares, before the
|
|
513
|
+
* CLI is asked anything: Codex writes its own files into a fresh home the moment it starts, and a
|
|
514
|
+
* real file there is one the host will never replace by a link. Answers what a person should
|
|
515
|
+
* know about the shadow home, and throws when it cannot be used.
|
|
516
|
+
*/
|
|
517
|
+
async prepare(id, kind, account) {
|
|
518
|
+
if (isDefaultAccount(id, account) || account.home === undefined) {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
const home = expandHome(account.home, this.env);
|
|
522
|
+
await this.install?.(kind, home).catch((e) => console.error(`Could not install hooks in ${home}:`, errorText(e)));
|
|
523
|
+
if (account.shadowHome === undefined) {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
const report = await this.prepareShadow(home, expandHome(account.shadowHome, this.env));
|
|
527
|
+
if (report.sharedLogin) {
|
|
528
|
+
throw new Error('auth.json in the shadow home is a link, so this account would sign in as another one');
|
|
529
|
+
}
|
|
530
|
+
return report.unshared.length > 0 ? `Not shared with ${home}: ${report.unshared.join(', ')}` : null;
|
|
531
|
+
}
|
|
532
|
+
emit() {
|
|
533
|
+
this.sinks.emit({ event: 'accounts.changed', payload: this.snapshot() });
|
|
534
|
+
for (const listener of this.listeners) {
|
|
535
|
+
listener();
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ShadowHomeReport {
|
|
2
|
+
linked: string[];
|
|
3
|
+
unshared: string[];
|
|
4
|
+
sharedLogin: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare class ShadowHomeError extends Error {
|
|
7
|
+
}
|
|
8
|
+
export declare function prepareShadowHome(home: string, shadow: string): Promise<ShadowHomeReport>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { lstat, mkdir, readdir, readlink, symlink } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { isNotFound } from '../../fs.js';
|
|
4
|
+
/*
|
|
5
|
+
* A Codex shadow home: a folder of its own for one login, sharing everything else with a Codex home
|
|
6
|
+
* by symlink, so two accounts continue each other's threads. Codex needs its login in `auth.json`
|
|
7
|
+
* (`cli_auth_credentials_store = "file"`) for this; a login in the keychain has no folder to be in.
|
|
8
|
+
*/
|
|
9
|
+
/* Made in the shared home before anything is linked, so the first thread of either account already lands there. */
|
|
10
|
+
const SHARED_FOLDERS = ['sessions', 'archived_sessions', 'skills', 'rules'];
|
|
11
|
+
/* The login, which is the whole point of the shadow home. */
|
|
12
|
+
const PRIVATE_ENTRIES = new Set(['auth.json']);
|
|
13
|
+
/*
|
|
14
|
+
* What one Codex process keeps for itself: what the models of this login are, caches, logs, and the
|
|
15
|
+
* sockets of an app-server that answers for the account that started it.
|
|
16
|
+
*/
|
|
17
|
+
const LOCAL_ENTRIES = new Set(['models_cache.json', 'cache', 'log', 'logs', 'tmp', '.tmp', 'app-server-daemon', 'app-server-control', 'ipc']);
|
|
18
|
+
/* A SQLite journal comes and goes with its database, so a link to one would soon point at nothing. */
|
|
19
|
+
function isJournal(name) {
|
|
20
|
+
return /-(wal|shm|journal)$/.test(name);
|
|
21
|
+
}
|
|
22
|
+
export class ShadowHomeError extends Error {
|
|
23
|
+
}
|
|
24
|
+
async function existing(path) {
|
|
25
|
+
try {
|
|
26
|
+
const stats = await lstat(path);
|
|
27
|
+
return stats.isSymbolicLink() ? { kind: 'link', target: await readlink(path) } : { kind: 'real' };
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
if (isNotFound(e)) {
|
|
31
|
+
return { kind: 'missing' };
|
|
32
|
+
}
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/*
|
|
37
|
+
* Links every shared entry of `home` into `shadow` that is not there yet. Never overwrites and never
|
|
38
|
+
* removes anything, so it runs again whenever it likes and picks up what Codex added to `home` since.
|
|
39
|
+
* Both folders have to exist; the person makes them, the way they sign in.
|
|
40
|
+
*/
|
|
41
|
+
export async function prepareShadowHome(home, shadow) {
|
|
42
|
+
const shared = resolve(home);
|
|
43
|
+
const own = resolve(shadow);
|
|
44
|
+
if (shared === own) {
|
|
45
|
+
throw new ShadowHomeError('The shadow home has to be another folder than the config folder');
|
|
46
|
+
}
|
|
47
|
+
for (const folder of SHARED_FOLDERS) {
|
|
48
|
+
await mkdir(join(shared, folder), { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
const report = { linked: [], unshared: [], sharedLogin: false };
|
|
51
|
+
for (const name of PRIVATE_ENTRIES) {
|
|
52
|
+
if ((await existing(join(own, name))).kind === 'link') {
|
|
53
|
+
report.sharedLogin = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const names = (await readdir(shared)).filter((name) => !PRIVATE_ENTRIES.has(name) && !LOCAL_ENTRIES.has(name) && !isJournal(name));
|
|
57
|
+
for (const name of names.sort()) {
|
|
58
|
+
const target = join(shared, name);
|
|
59
|
+
const link = join(own, name);
|
|
60
|
+
const found = await existing(link);
|
|
61
|
+
if (found.kind === 'missing') {
|
|
62
|
+
await symlink(target, link);
|
|
63
|
+
report.linked.push(name);
|
|
64
|
+
}
|
|
65
|
+
else if (found.kind === 'real' || resolve(own, found.target) !== target) {
|
|
66
|
+
report.unshared.push(name);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return report;
|
|
70
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AgentKind } from '@adecore/agent-contracts';
|
|
2
|
+
import { type CodexClientInfo } from '../../chat/codex-transport.ts';
|
|
3
|
+
export interface AccountReading {
|
|
4
|
+
signedIn: boolean;
|
|
5
|
+
email: string | null;
|
|
6
|
+
plan: string | null;
|
|
7
|
+
organization: string | null;
|
|
8
|
+
}
|
|
9
|
+
export type AskAccount = (kind: AgentKind, command: readonly string[], env: Record<string, string | undefined>) => Promise<AccountReading>;
|
|
10
|
+
export declare function readClaudeAuthStatus(output: string): AccountReading;
|
|
11
|
+
export declare function readCodexAccount(answer: unknown): AccountReading;
|
|
12
|
+
export declare function askAccountAs(client: CodexClientInfo): AskAccount;
|