@love-moon/ai-sdk 0.3.1 → 0.4.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 (59) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/built-in-backends.d.ts +1 -0
  3. package/dist/built-in-backends.js +6 -0
  4. package/dist/client.d.ts +15 -0
  5. package/dist/client.js +103 -1
  6. package/dist/external-provider-registry.js +4 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.js +3 -0
  9. package/dist/manager/account.d.ts +6 -0
  10. package/dist/manager/account.js +121 -0
  11. package/dist/manager/auth-parser.d.ts +27 -0
  12. package/dist/manager/auth-parser.js +54 -0
  13. package/dist/manager/config.d.ts +6 -0
  14. package/dist/manager/config.js +32 -0
  15. package/dist/manager/index.d.ts +12 -0
  16. package/dist/manager/index.js +11 -0
  17. package/dist/manager/install.d.ts +9 -0
  18. package/dist/manager/install.js +117 -0
  19. package/dist/manager/manager.d.ts +51 -0
  20. package/dist/manager/manager.js +105 -0
  21. package/dist/manager/network.d.ts +8 -0
  22. package/dist/manager/network.js +46 -0
  23. package/dist/manager/paths.d.ts +6 -0
  24. package/dist/manager/paths.js +16 -0
  25. package/dist/manager/quota/cache.d.ts +9 -0
  26. package/dist/manager/quota/cache.js +33 -0
  27. package/dist/manager/quota/claude.d.ts +19 -0
  28. package/dist/manager/quota/claude.js +193 -0
  29. package/dist/manager/quota/codex.d.ts +27 -0
  30. package/dist/manager/quota/codex.js +182 -0
  31. package/dist/manager/quota/copilot.d.ts +64 -0
  32. package/dist/manager/quota/copilot.js +718 -0
  33. package/dist/manager/quota/external.d.ts +29 -0
  34. package/dist/manager/quota/external.js +176 -0
  35. package/dist/manager/quota/headers.d.ts +5 -0
  36. package/dist/manager/quota/headers.js +29 -0
  37. package/dist/manager/quota/kimi.d.ts +24 -0
  38. package/dist/manager/quota/kimi.js +230 -0
  39. package/dist/manager/types.d.ts +166 -0
  40. package/dist/manager/types.js +1 -0
  41. package/dist/providers/chat-web-session.d.ts +218 -0
  42. package/dist/providers/chat-web-session.js +584 -0
  43. package/dist/providers/claude-agent-sdk-session.d.ts +35 -1
  44. package/dist/providers/claude-agent-sdk-session.js +109 -1
  45. package/dist/providers/codex-app-server-session.d.ts +107 -0
  46. package/dist/providers/codex-app-server-session.js +479 -9
  47. package/dist/providers/copilot-sdk-session.d.ts +9 -1
  48. package/dist/providers/copilot-sdk-session.js +48 -0
  49. package/dist/resume/chat-web.d.ts +20 -0
  50. package/dist/resume/chat-web.js +44 -0
  51. package/dist/resume/index.js +2 -0
  52. package/dist/session-factory.d.ts +3 -1
  53. package/dist/session-factory.js +17 -4
  54. package/dist/shared.d.ts +159 -0
  55. package/dist/shared.js +111 -0
  56. package/dist/transports/codex-app-server-transport.d.ts +1 -0
  57. package/dist/transports/codex-app-server-transport.js +45 -1
  58. package/dist/worker.js +19 -5
  59. package/package.json +10 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @love-moon/ai-sdk
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4ecc359: Publish the chat-web browser runtime and wire it into the CLI and AI SDK for
8
+ ChatGPT and Gemini web sessions, including provider error handling and local
9
+ development installation support.
10
+
11
+ Ship app SDK realtime history catch-up and the CLI/AI SDK goal-mode and custom
12
+ command runtime updates included in this release.
13
+
14
+ ## 0.3.2
15
+
16
+ ### Patch Changes
17
+
18
+ - 8e1d4a8: Prefer the bundled Copilot platform executable before the JS entrypoint so Node
19
+ 20 installs do not fail with `ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite`.
20
+
3
21
  ## 0.3.1
4
22
 
5
23
  ### Patch Changes
@@ -22,6 +22,7 @@ export const COPILOT_SDK_VARIANT: "copilot-sdk";
22
22
  export const KIMI_CLI_WIRE_VARIANT: "kimi-cli-wire";
23
23
  export const KIMI_CLI_PRINT_VARIANT: "kimi-cli-print";
24
24
  export const OPENCODE_SDK_VARIANT: "opencode-sdk";
25
+ export const CHAT_WEB_SESSION_VARIANT: "chat-web-session";
25
26
  /**
26
27
  * @typedef {Object} BuiltInBackendEntry
27
28
  * @property {string} backend Canonical backend name.
@@ -18,6 +18,7 @@ export const COPILOT_SDK_VARIANT = "copilot-sdk";
18
18
  export const KIMI_CLI_WIRE_VARIANT = "kimi-cli-wire";
19
19
  export const KIMI_CLI_PRINT_VARIANT = "kimi-cli-print";
20
20
  export const OPENCODE_SDK_VARIANT = "opencode-sdk";
21
+ export const CHAT_WEB_SESSION_VARIANT = "chat-web-session";
21
22
  /**
22
23
  * @typedef {Object} BuiltInBackendEntry
23
24
  * @property {string} backend Canonical backend name.
@@ -54,6 +55,11 @@ export const BUILT_IN_BACKENDS = [
54
55
  aliases: ["opencode", "open-code", "open_code"],
55
56
  defaultVariant: OPENCODE_SDK_VARIANT,
56
57
  },
58
+ {
59
+ backend: "chat-web",
60
+ aliases: ["chat-web", "chatweb", "chat_web", "web-chat"],
61
+ defaultVariant: CHAT_WEB_SESSION_VARIANT,
62
+ },
57
63
  ];
58
64
  const ALIAS_TO_BACKEND = new Map();
59
65
  for (const entry of BUILT_IN_BACKENDS) {
package/dist/client.d.ts CHANGED
@@ -16,6 +16,9 @@ export class RemoteAiSession extends EventEmitter<[never]> {
16
16
  sessionId: any;
17
17
  useSessionFileReplyStream: boolean;
18
18
  workerReady: boolean;
19
+ capabilities: {
20
+ goal?: boolean | undefined;
21
+ };
19
22
  };
20
23
  sessionMessageHandler: any;
21
24
  workingStatusHandler: any;
@@ -46,6 +49,9 @@ export class RemoteAiSession extends EventEmitter<[never]> {
46
49
  sessionId: any;
47
50
  useSessionFileReplyStream: boolean;
48
51
  workerReady: boolean;
52
+ capabilities: {
53
+ goal?: boolean | undefined;
54
+ };
49
55
  };
50
56
  usesSessionFileReplyStream(): boolean;
51
57
  getSessionInfo(): any;
@@ -57,6 +63,9 @@ export class RemoteAiSession extends EventEmitter<[never]> {
57
63
  getSessionUsageSummary(): Promise<any>;
58
64
  interruptCurrentTurn(): Promise<any>;
59
65
  runTurn(promptText: any, options?: {}): Promise<any>;
66
+ runGoal(goal: any, options?: {}): Promise<any>;
67
+ getGoal(): Promise<any>;
68
+ clearGoal(): Promise<any>;
60
69
  close(): Promise<void>;
61
70
  callWorker(method: any, args?: any[], { progressHandler, messageType }?: {
62
71
  progressHandler?: null | undefined;
@@ -92,6 +101,9 @@ declare class LocalAiSessionProxy extends EventEmitter<[never]> {
92
101
  sessionId: any;
93
102
  useSessionFileReplyStream: boolean;
94
103
  workerReady: boolean;
104
+ capabilities: {
105
+ goal?: boolean | undefined;
106
+ };
95
107
  };
96
108
  readyPromise: Promise<any>;
97
109
  initializeSession(backend: any, options: any): Promise<any>;
@@ -108,6 +120,9 @@ declare class LocalAiSessionProxy extends EventEmitter<[never]> {
108
120
  getSessionUsageSummary(): Promise<any>;
109
121
  interruptCurrentTurn(): Promise<any>;
110
122
  runTurn(promptText: any, options?: {}): Promise<any>;
123
+ runGoal(goal: any, options?: {}): Promise<any>;
124
+ getGoal(): Promise<any>;
125
+ clearGoal(): Promise<any>;
111
126
  close(): Promise<void>;
112
127
  }
113
128
  import { EventEmitter } from "node:events";
package/dist/client.js CHANGED
@@ -3,7 +3,12 @@ import { spawn } from "node:child_process";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import readline from "node:readline";
5
5
  import { assertSupportedBackend, createLocalAiSession, providerVariantForBackend, } from "./session-factory.js";
6
- import { normalizeLogger, reviveError } from "./shared.js";
6
+ import { DEFAULT_SESSION_CAPABILITIES, normalizeLogger, resolveSessionCapabilities, reviveError, } from "./shared.js";
7
+ function createUnsupportedGoalError(name) {
8
+ const err = new Error(`runGoal() is not supported by backend session (${name || "unknown"}).`);
9
+ err.reason = "unsupported_goal";
10
+ return err;
11
+ }
7
12
  const WORKER_PATH = fileURLToPath(new URL("./worker.js", import.meta.url));
8
13
  function sanitizeOptionsForWorker(options = {}) {
9
14
  const sanitized = {};
@@ -46,6 +51,7 @@ export class RemoteAiSession extends EventEmitter {
46
51
  sessionId: this.threadIdValue || undefined,
47
52
  useSessionFileReplyStream: this.useSessionFileReplyStreamValue,
48
53
  workerReady: false,
54
+ capabilities: { ...DEFAULT_SESSION_CAPABILITIES },
49
55
  };
50
56
  this.sessionMessageHandler = null;
51
57
  this.workingStatusHandler = null;
@@ -179,6 +185,43 @@ export class RemoteAiSession extends EventEmitter {
179
185
  progressHandler: typeof onProgress === "function" ? onProgress : null,
180
186
  });
181
187
  }
188
+ async runGoal(goal, options = {}) {
189
+ // Wait for the worker bootstrap to finish so we can consult the snapshot's
190
+ // capability flags. Critically, we must NOT swallow a `readyPromise`
191
+ // rejection: when the worker fails to come up (binary missing, config
192
+ // error, transport crash on init), the real error has to surface — masking
193
+ // it as `unsupported_goal` would mislead operators about what's wrong.
194
+ // See N3 in the review.
195
+ await this.readyPromise;
196
+ // Cheap capability gate: when the worker has reported a snapshot with
197
+ // `capabilities.goal === false`, short-circuit instead of paying for an
198
+ // IPC round-trip just to surface `unsupported_goal`.
199
+ if (this.snapshot?.capabilities && this.snapshot.capabilities.goal === false) {
200
+ throw createUnsupportedGoalError(this.snapshot.provider || this.snapshot.backend);
201
+ }
202
+ const { onProgress, ...restOptions } = options || {};
203
+ return this.callWorker("runGoal", [goal, restOptions], {
204
+ progressHandler: typeof onProgress === "function" ? onProgress : null,
205
+ });
206
+ }
207
+ async getGoal() {
208
+ // Propagate worker-creation errors instead of masking them as "no goal".
209
+ // See N3 in the review.
210
+ await this.readyPromise;
211
+ if (this.snapshot?.capabilities && this.snapshot.capabilities.goal === false) {
212
+ return null;
213
+ }
214
+ return this.callWorker("getGoal", []);
215
+ }
216
+ async clearGoal() {
217
+ // Propagate worker-creation errors instead of masking them as "nothing
218
+ // to clear". See N3 in the review.
219
+ await this.readyPromise;
220
+ if (this.snapshot?.capabilities && this.snapshot.capabilities.goal === false) {
221
+ return false;
222
+ }
223
+ return this.callWorker("clearGoal", []);
224
+ }
182
225
  async close() {
183
226
  if (this.closed) {
184
227
  return;
@@ -298,9 +341,13 @@ export class RemoteAiSession extends EventEmitter {
298
341
  }
299
342
  applyReadyPayload(payload) {
300
343
  const snapshot = payload?.snapshot && typeof payload.snapshot === "object" ? payload.snapshot : {};
344
+ const capabilities = snapshot && typeof snapshot.capabilities === "object" && snapshot.capabilities !== null
345
+ ? { ...DEFAULT_SESSION_CAPABILITIES, ...snapshot.capabilities }
346
+ : { ...DEFAULT_SESSION_CAPABILITIES };
301
347
  this.snapshot = {
302
348
  ...this.snapshot,
303
349
  ...snapshot,
350
+ capabilities,
304
351
  workerReady: true,
305
352
  workerPid: payload?.workerPid || undefined,
306
353
  workerProcessPid: payload?.workerProcessPid || undefined,
@@ -428,6 +475,7 @@ class LocalAiSessionProxy extends EventEmitter {
428
475
  sessionId: this.threadIdValue || undefined,
429
476
  useSessionFileReplyStream: this.useSessionFileReplyStreamValue,
430
477
  workerReady: false,
478
+ capabilities: { ...DEFAULT_SESSION_CAPABILITIES },
431
479
  };
432
480
  this.readyPromise = this.initializeSession(backend, options);
433
481
  this.readyPromise.catch(() => {
@@ -482,6 +530,12 @@ class LocalAiSessionProxy extends EventEmitter {
482
530
  : snapshot.sessionInfo && typeof snapshot.sessionInfo === "object"
483
531
  ? { ...snapshot.sessionInfo }
484
532
  : this.sessionInfo;
533
+ // Capability resolution: prefer the snapshot's `capabilities`, then fall
534
+ // back to a runtime resolver that checks `session.getCapabilities()` and
535
+ // `session.constructor.capabilities`.
536
+ const capabilities = snapshot && typeof snapshot.capabilities === "object" && snapshot.capabilities !== null
537
+ ? { ...DEFAULT_SESSION_CAPABILITIES, ...snapshot.capabilities }
538
+ : resolveSessionCapabilities(session);
485
539
  this.snapshot = {
486
540
  ...this.snapshot,
487
541
  ...snapshot,
@@ -491,6 +545,7 @@ class LocalAiSessionProxy extends EventEmitter {
491
545
  currentTurnStatus: typeof session.getCurrentTurnStatus === "function" ? session.getCurrentTurnStatus() : this.currentTurnStatus,
492
546
  useSessionFileReplyStream: this.useSessionFileReplyStreamValue,
493
547
  workerReady: true,
548
+ capabilities,
494
549
  };
495
550
  return session;
496
551
  }
@@ -503,6 +558,16 @@ class LocalAiSessionProxy extends EventEmitter {
503
558
  getSnapshot() {
504
559
  const sessionSnapshot = typeof this.session?.getSnapshot === "function" ? this.session.getSnapshot() : null;
505
560
  if (sessionSnapshot) {
561
+ // N5: capabilities were resolved once during `initializeSession` and are
562
+ // immutable for the lifetime of the underlying session. Reuse the cached
563
+ // value instead of re-resolving on every `getSnapshot()` call — this
564
+ // avoids per-call object identity flicker that breaks callers that
565
+ // memoize on the snapshot reference (e.g. React selectors).
566
+ const capabilities = this.snapshot?.capabilities
567
+ ? this.snapshot.capabilities
568
+ : sessionSnapshot && typeof sessionSnapshot.capabilities === "object" && sessionSnapshot.capabilities !== null
569
+ ? { ...DEFAULT_SESSION_CAPABILITIES, ...sessionSnapshot.capabilities }
570
+ : resolveSessionCapabilities(this.session);
506
571
  return {
507
572
  ...this.snapshot,
508
573
  ...sessionSnapshot,
@@ -514,6 +579,7 @@ class LocalAiSessionProxy extends EventEmitter {
514
579
  sessionInfo: typeof this.session?.getSessionInfo === "function"
515
580
  ? this.session.getSessionInfo()
516
581
  : sessionSnapshot.sessionInfo || this.sessionInfo || null,
582
+ capabilities,
517
583
  };
518
584
  }
519
585
  return {
@@ -582,6 +648,42 @@ class LocalAiSessionProxy extends EventEmitter {
582
648
  const session = await this.readyPromise;
583
649
  return await session.runTurn(promptText, options);
584
650
  }
651
+ async runGoal(goal, options = {}) {
652
+ const session = await this.readyPromise;
653
+ // Capability check: declarative `capabilities.goal === false` wins over
654
+ // method presence. This matters because a proxy may unconditionally
655
+ // forward runGoal and the per-method check would mis-report support.
656
+ const capabilities = resolveSessionCapabilities(session);
657
+ if (capabilities.goal === false) {
658
+ throw createUnsupportedGoalError(session?.constructor?.name);
659
+ }
660
+ if (typeof session.runGoal !== "function") {
661
+ throw createUnsupportedGoalError(session?.constructor?.name);
662
+ }
663
+ return await session.runGoal(goal, options);
664
+ }
665
+ async getGoal() {
666
+ const session = await this.readyPromise;
667
+ const capabilities = resolveSessionCapabilities(session);
668
+ if (capabilities.goal === false) {
669
+ return null;
670
+ }
671
+ if (typeof session.getGoal !== "function") {
672
+ return null;
673
+ }
674
+ return await session.getGoal();
675
+ }
676
+ async clearGoal() {
677
+ const session = await this.readyPromise;
678
+ const capabilities = resolveSessionCapabilities(session);
679
+ if (capabilities.goal === false) {
680
+ return false;
681
+ }
682
+ if (typeof session.clearGoal !== "function") {
683
+ return false;
684
+ }
685
+ return await session.clearGoal();
686
+ }
585
687
  async close() {
586
688
  if (this.closed) {
587
689
  return;
@@ -133,6 +133,10 @@ function validateDescriptor(descriptor, sourcePath) {
133
133
  resolveResumeContext: optionalFn("resolveResumeContext"),
134
134
  buildResumeArgs: optionalFn("buildResumeArgs"),
135
135
  findSessionPath: optionalFn("findSessionPath"),
136
+ getQuota: optionalFn("getQuota"),
137
+ getQuotaList: optionalFn("getQuotaList"),
138
+ readCachedQuota: optionalFn("readCachedQuota"),
139
+ readCachedQuotaList: optionalFn("readCachedQuotaList"),
136
140
  sourcePath,
137
141
  };
138
142
  }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  export { BUILT_IN_BACKENDS } from "./built-in-backends.js";
2
2
  export { createAiSession, RemoteAiSession } from "./client.js";
3
+ export { GOAL_STATUSES, TERMINAL_GOAL_STATUSES, isGoalStatus, isTerminalGoalStatus } from "./shared.js";
4
+ export { AiManager, accountNameFromPath, checkInstall, checkInstallAll, checkNetwork, checkNetworkAll, getClaudeQuota, getCodexQuota, getCopilotQuota, getCurrentCodexAccount, getExternalQuota, getExternalQuotaList, getKimiQuota, listCodexAccounts, loadAiManagerConfig, normalizeExternalQuota, normalizeExternalQuotaList, parseAuthFile, parseAuthFileContents, parseCopilotQuotaSnapshots, readCachedCodexQuota, resolveClaudeCredential, switchCodexAccount } from "./manager/index.js";
5
+ export { getExternalProviderDescriptor, getExternalProviderRegistry, resetExternalProviderRegistryForTests, resolveExternalBackend } from "./external-provider-registry.js";
3
6
  export { resolveResumeContext, buildResumeArgsForBackend, resumeProviderForBackend, findSessionPath, resolveSessionRunDirectory, inspectResumeTarget } from "./resume/index.js";
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
1
  export { createAiSession, RemoteAiSession } from "./client.js";
2
2
  export { BUILT_IN_BACKENDS } from "./built-in-backends.js";
3
+ export { GOAL_STATUSES, TERMINAL_GOAL_STATUSES, isGoalStatus, isTerminalGoalStatus, } from "./shared.js";
4
+ export { AiManager, accountNameFromPath, checkInstall, checkInstallAll, checkNetwork, checkNetworkAll, getClaudeQuota, getCodexQuota, getCopilotQuota, getCurrentCodexAccount, getExternalQuota, getExternalQuotaList, getKimiQuota, listCodexAccounts, loadAiManagerConfig, normalizeExternalQuota, normalizeExternalQuotaList, parseAuthFile, parseAuthFileContents, parseCopilotQuotaSnapshots, readCachedCodexQuota, resolveClaudeCredential, switchCodexAccount, } from "./manager/index.js";
5
+ export { getExternalProviderDescriptor, getExternalProviderRegistry, resetExternalProviderRegistryForTests, resolveExternalBackend, } from "./external-provider-registry.js";
3
6
  export { resolveResumeContext, buildResumeArgsForBackend, resumeProviderForBackend, findSessionPath, resolveSessionRunDirectory, inspectResumeTarget, } from "./resume/index.js";
@@ -0,0 +1,6 @@
1
+ import type { AiManagerConfig, CodexAccount, SwitchResult } from "./types.js";
2
+ /** Derive the short account name from the file path. */
3
+ export declare function accountNameFromPath(path: string): string;
4
+ export declare function listCodexAccounts(config: AiManagerConfig, codexAuthPath?: string): Promise<CodexAccount[]>;
5
+ export declare function getCurrentCodexAccount(config: AiManagerConfig, codexAuthPath?: string): Promise<CodexAccount | null>;
6
+ export declare function switchCodexAccount(config: AiManagerConfig, name: string, codexAuthPath?: string): Promise<SwitchResult>;
@@ -0,0 +1,121 @@
1
+ import { readFile, writeFile, copyFile, chmod, rename, stat } from "node:fs/promises";
2
+ import { readFileSync } from "node:fs";
3
+ import { basename } from "node:path";
4
+ import { parseAuthFile, parseAuthFileContents, } from "./auth-parser.js";
5
+ import { DEFAULT_CODEX_AUTH } from "./paths.js";
6
+ /** Derive the short account name from the file path. */
7
+ export function accountNameFromPath(path) {
8
+ const base = basename(path);
9
+ return base.replace(/^auth_/, "").replace(/\.json$/, "");
10
+ }
11
+ async function fileExists(p) {
12
+ try {
13
+ await stat(p);
14
+ return true;
15
+ }
16
+ catch (err) {
17
+ if (err?.code === "ENOENT")
18
+ return false;
19
+ throw err;
20
+ }
21
+ }
22
+ function fingerprintOfPath(p) {
23
+ try {
24
+ const data = JSON.parse(readFileSync(p, "utf8"));
25
+ return parseAuthFileContents(data).identityFingerprint;
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }
31
+ async function readCurrentFingerprint(codexAuthPath) {
32
+ if (!(await fileExists(codexAuthPath)))
33
+ return undefined;
34
+ const info = await parseAuthFile(codexAuthPath);
35
+ return info.identityFingerprint;
36
+ }
37
+ export async function listCodexAccounts(config, codexAuthPath = DEFAULT_CODEX_AUTH) {
38
+ const currentFp = await readCurrentFingerprint(codexAuthPath);
39
+ const out = [];
40
+ for (const path of config.codex.authJson) {
41
+ const entry = {
42
+ name: accountNameFromPath(path),
43
+ path,
44
+ isCurrent: false,
45
+ };
46
+ try {
47
+ const info = await parseAuthFile(path);
48
+ entry.email = info.email;
49
+ entry.accountId = info.accountId;
50
+ entry.planType = info.planType;
51
+ entry.lastRefresh = info.lastRefresh;
52
+ entry.isCurrent = Boolean(currentFp && info.identityFingerprint && currentFp === info.identityFingerprint);
53
+ }
54
+ catch {
55
+ // leave entry minimal; caller can see that name/path exist but details missing
56
+ }
57
+ out.push(entry);
58
+ }
59
+ return out;
60
+ }
61
+ export async function getCurrentCodexAccount(config, codexAuthPath = DEFAULT_CODEX_AUTH) {
62
+ const accounts = await listCodexAccounts(config, codexAuthPath);
63
+ return accounts.find((a) => a.isCurrent) ?? null;
64
+ }
65
+ function assertValidAuthJson(data) {
66
+ if (!data || typeof data !== "object") {
67
+ throw new Error("auth.json is not a JSON object");
68
+ }
69
+ const tokens = data.tokens;
70
+ if (!tokens || typeof tokens.access_token !== "string" || tokens.access_token.length < 20) {
71
+ throw new Error("auth.json is missing a valid tokens.access_token");
72
+ }
73
+ }
74
+ function nameForFingerprint(config, fingerprint) {
75
+ if (!fingerprint)
76
+ return undefined;
77
+ for (const p of config.codex.authJson) {
78
+ if (fingerprintOfPath(p) === fingerprint)
79
+ return accountNameFromPath(p);
80
+ }
81
+ return undefined;
82
+ }
83
+ export async function switchCodexAccount(config, name, codexAuthPath = DEFAULT_CODEX_AUTH) {
84
+ const match = config.codex.authJson.find((p) => accountNameFromPath(p) === name);
85
+ if (!match) {
86
+ const available = config.codex.authJson.map(accountNameFromPath).join(", ") || "<none>";
87
+ throw new Error(`codex account "${name}" not found in config (available: ${available})`);
88
+ }
89
+ const raw = await readFile(match, "utf8");
90
+ let data;
91
+ try {
92
+ data = JSON.parse(raw);
93
+ }
94
+ catch (err) {
95
+ throw new Error(`target auth.json is not valid JSON: ${err.message}`);
96
+ }
97
+ assertValidAuthJson(data);
98
+ const targetInfo = parseAuthFileContents(data);
99
+ const backupPath = `${codexAuthPath}.bak`;
100
+ let previousName;
101
+ if (await fileExists(codexAuthPath)) {
102
+ try {
103
+ const prevInfo = await parseAuthFile(codexAuthPath);
104
+ previousName = nameForFingerprint(config, prevInfo.identityFingerprint);
105
+ if (prevInfo.identityFingerprint &&
106
+ targetInfo.identityFingerprint &&
107
+ prevInfo.identityFingerprint === targetInfo.identityFingerprint) {
108
+ return { previousName, newName: name, backupPath };
109
+ }
110
+ }
111
+ catch {
112
+ // Best-effort; still back up raw contents below
113
+ }
114
+ await copyFile(codexAuthPath, backupPath);
115
+ }
116
+ const tmp = `${codexAuthPath}.tmp`;
117
+ await writeFile(tmp, raw, { mode: 0o600 });
118
+ await rename(tmp, codexAuthPath);
119
+ await chmod(codexAuthPath, 0o600).catch(() => { });
120
+ return { previousName, newName: name, backupPath };
121
+ }
@@ -0,0 +1,27 @@
1
+ export interface CodexAuthFile {
2
+ auth_mode?: string;
3
+ OPENAI_API_KEY?: string | null;
4
+ tokens?: {
5
+ id_token?: string;
6
+ access_token?: string;
7
+ refresh_token?: string;
8
+ account_id?: string;
9
+ };
10
+ last_refresh?: string;
11
+ }
12
+ export interface CodexAuthInfo {
13
+ email?: string;
14
+ accountId?: string;
15
+ planType?: string;
16
+ userId?: string;
17
+ name?: string;
18
+ lastRefresh?: string;
19
+ accessToken?: string;
20
+ /**
21
+ * Stable identity fingerprint across auth files.
22
+ * Prefers `email|account_id` from the id_token; falls back to a hash of access_token.
23
+ */
24
+ identityFingerprint?: string;
25
+ }
26
+ export declare function parseAuthFileContents(data: CodexAuthFile): CodexAuthInfo;
27
+ export declare function parseAuthFile(path: string): Promise<CodexAuthInfo>;
@@ -0,0 +1,54 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
3
+ /** Decode the JWT payload segment (no signature verification). */
4
+ function decodeJwtPayload(token) {
5
+ const parts = token.split(".");
6
+ if (parts.length !== 3)
7
+ throw new Error("Invalid JWT");
8
+ const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
9
+ const padded = b64 + "===".slice((b64.length + 3) % 4);
10
+ const json = Buffer.from(padded, "base64").toString("utf8");
11
+ return JSON.parse(json);
12
+ }
13
+ export function parseAuthFileContents(data) {
14
+ const info = {};
15
+ const tokens = data.tokens;
16
+ if (!tokens)
17
+ return info;
18
+ info.accountId = tokens.account_id;
19
+ info.accessToken = tokens.access_token;
20
+ info.lastRefresh = data.last_refresh;
21
+ if (tokens.id_token) {
22
+ try {
23
+ const payload = decodeJwtPayload(tokens.id_token);
24
+ info.email = payload.email;
25
+ info.name = payload.name;
26
+ info.userId = payload.user_id ?? payload.sub;
27
+ const ext = payload["https://api.openai.com/auth"];
28
+ if (ext && typeof ext === "object") {
29
+ info.planType = ext.chatgpt_plan_type;
30
+ if (!info.accountId)
31
+ info.accountId = ext.chatgpt_account_id;
32
+ }
33
+ }
34
+ catch {
35
+ // ignore malformed id_token; caller may still have an access_token
36
+ }
37
+ }
38
+ info.identityFingerprint = computeIdentityFingerprint(info);
39
+ return info;
40
+ }
41
+ function computeIdentityFingerprint(info) {
42
+ if (info.email || info.accountId) {
43
+ return `${(info.email ?? "").toLowerCase()}|${info.accountId ?? ""}`;
44
+ }
45
+ if (info.accessToken) {
46
+ return "tok:" + createHash("sha1").update(info.accessToken).digest("hex").slice(0, 16);
47
+ }
48
+ return undefined;
49
+ }
50
+ export async function parseAuthFile(path) {
51
+ const raw = await readFile(path, "utf8");
52
+ const data = JSON.parse(raw);
53
+ return parseAuthFileContents(data);
54
+ }
@@ -0,0 +1,6 @@
1
+ import type { AiManagerConfig } from "./types.js";
2
+ /**
3
+ * Load the `ai_manager` section from a conductor config file.
4
+ * Returns an empty config if the file does not exist or the section is missing.
5
+ */
6
+ export declare function loadAiManagerConfig(configPath?: string): Promise<AiManagerConfig>;
@@ -0,0 +1,32 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { parse as parseYaml } from "yaml";
3
+ import { DEFAULT_CONDUCTOR_CONFIG, expandHome } from "./paths.js";
4
+ const EMPTY = { codex: { authJson: [] } };
5
+ /**
6
+ * Load the `ai_manager` section from a conductor config file.
7
+ * Returns an empty config if the file does not exist or the section is missing.
8
+ */
9
+ export async function loadAiManagerConfig(configPath = DEFAULT_CONDUCTOR_CONFIG) {
10
+ let raw;
11
+ try {
12
+ raw = await readFile(configPath, "utf8");
13
+ }
14
+ catch (err) {
15
+ if (err?.code === "ENOENT")
16
+ return EMPTY;
17
+ throw err;
18
+ }
19
+ const doc = parseYaml(raw);
20
+ const section = doc?.ai_manager;
21
+ if (!section || typeof section !== "object")
22
+ return EMPTY;
23
+ const codexSection = section.codex ?? {};
24
+ const rawList = codexSection.auth_json ?? [];
25
+ if (!Array.isArray(rawList)) {
26
+ throw new Error(`ai_manager.codex.auth_json must be a list of paths, got ${typeof rawList}`);
27
+ }
28
+ const authJson = rawList
29
+ .filter((x) => typeof x === "string" && x.length > 0)
30
+ .map(expandHome);
31
+ return { codex: { authJson } };
32
+ }
@@ -0,0 +1,12 @@
1
+ export { AiManager, type AiManagerOptions } from "./manager.js";
2
+ export { loadAiManagerConfig } from "./config.js";
3
+ export { parseAuthFile, parseAuthFileContents, type CodexAuthFile, type CodexAuthInfo, } from "./auth-parser.js";
4
+ export { accountNameFromPath, listCodexAccounts, getCurrentCodexAccount, switchCodexAccount, } from "./account.js";
5
+ export { checkInstall, checkInstallAll } from "./install.js";
6
+ export { checkNetwork, checkNetworkAll } from "./network.js";
7
+ export { getCodexQuota, readCachedCodexQuota, type GetCodexQuotaOptions, } from "./quota/codex.js";
8
+ export { getClaudeQuota, resolveClaudeCredential, type GetClaudeQuotaOptions, type ClaudeCredential, } from "./quota/claude.js";
9
+ export { getKimiQuota, type GetKimiQuotaOptions } from "./quota/kimi.js";
10
+ export { getCopilotQuota, parseCopilotQuotaSnapshots, type GetCopilotQuotaOptions, } from "./quota/copilot.js";
11
+ export { getExternalQuota, getExternalQuotaList, normalizeExternalQuota, normalizeExternalQuotaList, type GetExternalQuotaListOptions, type GetExternalQuotaOptions, } from "./quota/external.js";
12
+ export type { AiManagerConfig, CodexAccount, CodexQuota, ClaudeQuota, ExternalQuota, ExternalQuotaList, KimiQuota, CopilotQuota, CopilotQuotaSnapshot, InstallStatus, NetworkStatus, QuotaSource, QuotaWindow, SwitchResult, Tool, } from "./types.js";
@@ -0,0 +1,11 @@
1
+ export { AiManager } from "./manager.js";
2
+ export { loadAiManagerConfig } from "./config.js";
3
+ export { parseAuthFile, parseAuthFileContents, } from "./auth-parser.js";
4
+ export { accountNameFromPath, listCodexAccounts, getCurrentCodexAccount, switchCodexAccount, } from "./account.js";
5
+ export { checkInstall, checkInstallAll } from "./install.js";
6
+ export { checkNetwork, checkNetworkAll } from "./network.js";
7
+ export { getCodexQuota, readCachedCodexQuota, } from "./quota/codex.js";
8
+ export { getClaudeQuota, resolveClaudeCredential, } from "./quota/claude.js";
9
+ export { getKimiQuota } from "./quota/kimi.js";
10
+ export { getCopilotQuota, parseCopilotQuotaSnapshots, } from "./quota/copilot.js";
11
+ export { getExternalQuota, getExternalQuotaList, normalizeExternalQuota, normalizeExternalQuotaList, } from "./quota/external.js";
@@ -0,0 +1,9 @@
1
+ import type { InstallStatus, Tool } from "./types.js";
2
+ export declare function checkInstall(tool: Tool, opts?: {
3
+ timeoutMs?: number;
4
+ }): Promise<InstallStatus>;
5
+ export declare function checkInstallAll(opts?: {
6
+ timeoutMs?: number;
7
+ }): Promise<Record<Tool, InstallStatus>>;
8
+ /** Exported for tests. */
9
+ export declare function findPackageVersionForEntry(entryPath: string, packageName: string): Promise<string | undefined>;