@love-moon/ai-sdk 0.3.2 → 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.
- package/CHANGELOG.md +11 -0
- package/dist/built-in-backends.d.ts +1 -0
- package/dist/built-in-backends.js +6 -0
- package/dist/client.d.ts +15 -0
- package/dist/client.js +103 -1
- package/dist/external-provider-registry.js +4 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/manager/account.d.ts +6 -0
- package/dist/manager/account.js +121 -0
- package/dist/manager/auth-parser.d.ts +27 -0
- package/dist/manager/auth-parser.js +54 -0
- package/dist/manager/config.d.ts +6 -0
- package/dist/manager/config.js +32 -0
- package/dist/manager/index.d.ts +12 -0
- package/dist/manager/index.js +11 -0
- package/dist/manager/install.d.ts +9 -0
- package/dist/manager/install.js +117 -0
- package/dist/manager/manager.d.ts +51 -0
- package/dist/manager/manager.js +105 -0
- package/dist/manager/network.d.ts +8 -0
- package/dist/manager/network.js +46 -0
- package/dist/manager/paths.d.ts +6 -0
- package/dist/manager/paths.js +16 -0
- package/dist/manager/quota/cache.d.ts +9 -0
- package/dist/manager/quota/cache.js +33 -0
- package/dist/manager/quota/claude.d.ts +19 -0
- package/dist/manager/quota/claude.js +193 -0
- package/dist/manager/quota/codex.d.ts +27 -0
- package/dist/manager/quota/codex.js +182 -0
- package/dist/manager/quota/copilot.d.ts +64 -0
- package/dist/manager/quota/copilot.js +718 -0
- package/dist/manager/quota/external.d.ts +29 -0
- package/dist/manager/quota/external.js +176 -0
- package/dist/manager/quota/headers.d.ts +5 -0
- package/dist/manager/quota/headers.js +29 -0
- package/dist/manager/quota/kimi.d.ts +24 -0
- package/dist/manager/quota/kimi.js +230 -0
- package/dist/manager/types.d.ts +166 -0
- package/dist/manager/types.js +1 -0
- package/dist/providers/chat-web-session.d.ts +218 -0
- package/dist/providers/chat-web-session.js +584 -0
- package/dist/providers/claude-agent-sdk-session.d.ts +35 -1
- package/dist/providers/claude-agent-sdk-session.js +109 -1
- package/dist/providers/codex-app-server-session.d.ts +107 -0
- package/dist/providers/codex-app-server-session.js +479 -9
- package/dist/providers/copilot-sdk-session.d.ts +1 -1
- package/dist/resume/chat-web.d.ts +20 -0
- package/dist/resume/chat-web.js +44 -0
- package/dist/resume/index.js +2 -0
- package/dist/session-factory.d.ts +3 -1
- package/dist/session-factory.js +17 -4
- package/dist/shared.d.ts +159 -0
- package/dist/shared.js +111 -0
- package/dist/transports/codex-app-server-transport.d.ts +1 -0
- package/dist/transports/codex-app-server-transport.js +45 -1
- package/dist/worker.js +19 -5
- package/package.json +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
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
|
+
|
|
3
14
|
## 0.3.2
|
|
4
15
|
|
|
5
16
|
### 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>;
|