@love-moon/ai-sdk 0.7.4 → 0.7.6

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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # @love-moon/ai-sdk
2
2
 
3
+ ## 0.7.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 7bbb412: Add `CONDUCTOR_HOME` support for relocating user-level configuration, logs,
8
+ Fire locks, sessions, update metadata, and AI manager caches while leaving
9
+ project-scoped `.conductor` directories and Fire task markers in place.
10
+
11
+ Migrate device authorization to `conductor.conductor-ai.top` while preserving
12
+ compatibility with the legacy official endpoint and self-hosted backends.
13
+
14
+ ## 0.7.5
15
+
16
+ ### Patch Changes
17
+
18
+ - f91a5df: Read Codex weekly quota from the official app-server account rate-limit RPC, ignore model-specific buckets, and invalidate legacy response-header caches.
19
+
3
20
  ## 0.7.4
4
21
 
5
22
  ### Patch Changes
@@ -1,12 +1,12 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { parse as parseYaml } from "yaml";
3
- import { DEFAULT_CONDUCTOR_CONFIG, expandHome } from "./paths.js";
3
+ import { expandHome, resolveDefaultConductorConfig } from "./paths.js";
4
4
  const EMPTY = { codex: { authJson: [] } };
5
5
  /**
6
6
  * Load the `ai_manager` section from a conductor config file.
7
7
  * Returns an empty config if the file does not exist or the section is missing.
8
8
  */
9
- export async function loadAiManagerConfig(configPath = DEFAULT_CONDUCTOR_CONFIG) {
9
+ export async function loadAiManagerConfig(configPath = resolveDefaultConductorConfig()) {
10
10
  let raw;
11
11
  try {
12
12
  raw = await readFile(configPath, "utf8");
@@ -5,7 +5,7 @@ import { type GetCopilotQuotaOptions } from "./quota/copilot.js";
5
5
  import { type GetExternalQuotaListOptions, type GetExternalQuotaOptions } from "./quota/external.js";
6
6
  import type { AiManagerConfig, CodexAccount, CodexQuota, ClaudeQuota, ExternalQuota, ExternalQuotaList, KimiQuota, CopilotQuota, InstallStatus, NetworkStatus, SwitchResult, Tool } from "./types.js";
7
7
  export interface AiManagerOptions {
8
- /** Path to conductor config.yaml. Default: ~/.conductor/config.yaml */
8
+ /** Path to conductor config.yaml. Default: $CONDUCTOR_HOME/config.yaml. */
9
9
  configPath?: string;
10
10
  /** Path to the active codex auth.json. Default: ~/.codex/auth.json */
11
11
  codexAuthPath?: string;
@@ -20,7 +20,7 @@ export declare class AiManager {
20
20
  constructor(opts?: AiManagerOptions);
21
21
  /**
22
22
  * Read the latest ai_manager config. Cached against the config file's mtime,
23
- * so edits to ~/.conductor/config.yaml are picked up immediately without
23
+ * so edits to the resolved Conductor config.yaml are picked up immediately without
24
24
  * restarting the daemon, while a 30s polling cycle does not re-parse YAML
25
25
  * on every action. Falls back to always-reload if stat() fails.
26
26
  * The `config` constructor option short-circuits disk reads (used by tests).
@@ -8,20 +8,20 @@ import { getClaudeQuota } from "./quota/claude.js";
8
8
  import { getKimiQuota } from "./quota/kimi.js";
9
9
  import { getCopilotQuota } from "./quota/copilot.js";
10
10
  import { getExternalQuota, getExternalQuotaList, } from "./quota/external.js";
11
- import { DEFAULT_CODEX_AUTH, DEFAULT_CONDUCTOR_CONFIG } from "./paths.js";
11
+ import { DEFAULT_CODEX_AUTH, resolveDefaultConductorConfig } from "./paths.js";
12
12
  export class AiManager {
13
13
  configPath;
14
14
  codexAuthPath;
15
15
  configOverride;
16
16
  cached;
17
17
  constructor(opts = {}) {
18
- this.configPath = opts.configPath ?? DEFAULT_CONDUCTOR_CONFIG;
18
+ this.configPath = opts.configPath ?? resolveDefaultConductorConfig();
19
19
  this.codexAuthPath = opts.codexAuthPath ?? DEFAULT_CODEX_AUTH;
20
20
  this.configOverride = opts.config;
21
21
  }
22
22
  /**
23
23
  * Read the latest ai_manager config. Cached against the config file's mtime,
24
- * so edits to ~/.conductor/config.yaml are picked up immediately without
24
+ * so edits to the resolved Conductor config.yaml are picked up immediately without
25
25
  * restarting the daemon, while a 30s polling cycle does not re-parse YAML
26
26
  * on every action. Falls back to always-reload if stat() fails.
27
27
  * The `config` constructor option short-circuits disk reads (used by tests).
@@ -1,4 +1,8 @@
1
+ type PathEnv = Record<string, string | undefined>;
1
2
  export declare function expandHome(p: string): string;
3
+ export declare function resolveConductorHome(env?: PathEnv): string;
4
+ export declare function resolveDefaultConductorConfig(env?: PathEnv): string;
5
+ export declare function resolveDefaultQuotaCacheDir(env?: PathEnv): string;
2
6
  export declare const DEFAULT_CONDUCTOR_CONFIG: string;
3
7
  export declare const DEFAULT_CODEX_AUTH: string;
4
8
  export declare const DEFAULT_CODEX_CONFIG: string;
@@ -6,3 +10,4 @@ export declare const DEFAULT_KIMI_CODE_HOME: string;
6
10
  export declare const DEFAULT_KIMI_CREDENTIAL: string;
7
11
  export declare const LEGACY_KIMI_CREDENTIAL: string;
8
12
  export declare const DEFAULT_QUOTA_CACHE_DIR: string;
13
+ export {};
@@ -1,18 +1,46 @@
1
1
  import { homedir } from "node:os";
2
- import { join } from "node:path";
3
- export function expandHome(p) {
2
+ import { join, resolve } from "node:path";
3
+ function optionalString(value) {
4
+ return typeof value === "string" && value.trim() ? value.trim() : "";
5
+ }
6
+ function userHome(env = process.env) {
7
+ return optionalString(env.HOME) || optionalString(env.USERPROFILE) || homedir();
8
+ }
9
+ function expandHomeWithEnv(p, env = process.env) {
4
10
  if (!p)
5
11
  return p;
12
+ const home = userHome(env);
6
13
  if (p === "~")
7
- return homedir();
8
- if (p.startsWith("~/"))
9
- return join(homedir(), p.slice(2));
14
+ return home;
15
+ if (p.startsWith("~/") || p.startsWith("~\\"))
16
+ return join(home, p.slice(2));
10
17
  return p;
11
18
  }
12
- export const DEFAULT_CONDUCTOR_CONFIG = join(homedir(), ".conductor", "config.yaml");
19
+ export function expandHome(p) {
20
+ return expandHomeWithEnv(p);
21
+ }
22
+ export function resolveConductorHome(env = process.env) {
23
+ const configured = optionalString(env.CONDUCTOR_HOME);
24
+ if (configured) {
25
+ const expanded = expandHomeWithEnv(configured, env);
26
+ return resolve(expanded);
27
+ }
28
+ return join(resolve(userHome(env)), ".conductor");
29
+ }
30
+ export function resolveDefaultConductorConfig(env = process.env) {
31
+ const configured = optionalString(env.CONDUCTOR_CONFIG);
32
+ if (configured) {
33
+ return resolve(expandHomeWithEnv(configured, env));
34
+ }
35
+ return join(resolveConductorHome(env), "config.yaml");
36
+ }
37
+ export function resolveDefaultQuotaCacheDir(env = process.env) {
38
+ return join(resolveConductorHome(env), "cache", "ai-manager");
39
+ }
40
+ export const DEFAULT_CONDUCTOR_CONFIG = resolveDefaultConductorConfig();
13
41
  export const DEFAULT_CODEX_AUTH = join(homedir(), ".codex", "auth.json");
14
42
  export const DEFAULT_CODEX_CONFIG = join(homedir(), ".codex", "config.toml");
15
43
  export const DEFAULT_KIMI_CODE_HOME = process.env.KIMI_CODE_HOME?.trim() || join(homedir(), ".kimi-code");
16
44
  export const DEFAULT_KIMI_CREDENTIAL = join(DEFAULT_KIMI_CODE_HOME, "credentials", "kimi-code.json");
17
45
  export const LEGACY_KIMI_CREDENTIAL = join(homedir(), ".kimi", "credentials", "kimi-code.json");
18
- export const DEFAULT_QUOTA_CACHE_DIR = join(homedir(), ".conductor", "cache", "ai-manager");
46
+ export const DEFAULT_QUOTA_CACHE_DIR = resolveDefaultQuotaCacheDir();
@@ -1,7 +1,7 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { createHash } from "node:crypto";
4
- import { DEFAULT_QUOTA_CACHE_DIR } from "../paths.js";
4
+ import { resolveDefaultQuotaCacheDir } from "../paths.js";
5
5
  export function fingerprintKey(parts) {
6
6
  const h = createHash("sha1");
7
7
  for (const p of parts)
@@ -23,7 +23,7 @@ export async function writeCache(file, value) {
23
23
  await writeFile(file, JSON.stringify(entry), "utf8");
24
24
  return entry;
25
25
  }
26
- export function cacheFile(tool, fingerprint, dir = DEFAULT_QUOTA_CACHE_DIR) {
26
+ export function cacheFile(tool, fingerprint, dir = resolveDefaultQuotaCacheDir()) {
27
27
  return join(dir, `quota-${tool}-${fingerprint}.json`);
28
28
  }
29
29
  export function isFresh(entry, ttlSeconds) {
@@ -7,25 +7,28 @@ export interface GetCodexQuotaOptions {
7
7
  forceRefresh?: boolean;
8
8
  /** Request timeout in ms. */
9
9
  timeoutMs?: number;
10
- /** Model name; defaults to the one in ~/.codex/config.toml, then `gpt-5`. */
10
+ /** @deprecated Quota is account-scoped and no longer fetched through a model response. */
11
11
  model?: string;
12
- /** Override path to codex config.toml (to detect model). */
12
+ /** @deprecated The Codex app-server reads its own config from CODEX_HOME. */
13
13
  codexConfigPath?: string;
14
14
  /** Override cache directory (mainly for tests). */
15
15
  cacheDir?: string;
16
+ /** Override the app-server reader (tests only). */
17
+ rateLimitsReader?: (options: {
18
+ codexAuthPath: string;
19
+ timeoutMs: number;
20
+ }) => Promise<unknown>;
16
21
  }
17
- /** Exported for protocol compatibility tests. */
18
- export declare function parseCodexHeaders(map: Record<string, string>, extras: {
22
+ /** Normalize Codex app-server `account/rateLimits/read` into the manager shape. */
23
+ export declare function parseCodexRateLimitsResponse(response: unknown, extras?: {
19
24
  email?: string;
20
25
  accountId?: string;
21
26
  }): Omit<CodexQuota, "tool" | "fetchedAt" | "source">;
22
27
  export declare function getCodexQuota(opts?: GetCodexQuotaOptions): Promise<CodexQuota>;
23
28
  /**
24
29
  * Read the on-disk quota cache for a given codex auth.json without triggering
25
- * any network call. Returns whatever snapshot was last written (regardless of
26
- * age) or `null` if nothing has been cached for this identity yet. Used by the
27
- * daemon's `list_accounts` handler to surface "last known" quota for inactive
28
- * accounts so the web UI can restore them across page refreshes.
30
+ * any network call. Only app-server snapshots are considered; legacy caches
31
+ * from response headers are intentionally ignored.
29
32
  */
30
33
  export declare function readCachedCodexQuota(codexAuthPath: string, opts?: {
31
34
  cacheDir?: string;
@@ -1,77 +1,72 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { basename, dirname } from "node:path";
2
2
  import { parseAuthFile } from "../auth-parser.js";
3
- import { DEFAULT_CODEX_AUTH, DEFAULT_CODEX_CONFIG } from "../paths.js";
4
- import { bool, headersToMap, num, str } from "./headers.js";
3
+ import { DEFAULT_CODEX_AUTH } from "../paths.js";
4
+ import { CodexAppServerTransport } from "../../transports/codex-app-server-transport.js";
5
5
  import { cacheFile, fingerprintKey, isFresh, readCache, writeCache } from "./cache.js";
6
- /** Best-effort: read the configured model from ~/.codex/config.toml. */
7
- async function readConfiguredModel(path) {
8
- try {
9
- const raw = await readFile(path, "utf8");
10
- // Minimal top-level key scan: `model = "..."` (skip lines inside [tables]).
11
- let inRootTable = true;
12
- for (const line of raw.split(/\r?\n/)) {
13
- const trimmed = line.trim();
14
- if (trimmed.startsWith("#") || trimmed === "")
15
- continue;
16
- if (/^\[/.test(trimmed)) {
17
- inRootTable = false;
18
- continue;
19
- }
20
- if (!inRootTable)
21
- continue;
22
- const m = /^model\s*=\s*"([^"]+)"/.exec(trimmed);
23
- if (m)
24
- return m[1];
25
- }
26
- }
27
- catch {
28
- // ignore
29
- }
30
- return undefined;
31
- }
32
- const RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
33
6
  const DEFAULT_TTL = 60; // seconds
34
7
  const DEFAULT_TIMEOUT_MS = 15000;
35
- function windowFromHeaders(map, prefix) {
36
- const suffixes = [
37
- "used-percent",
38
- "reset-after-seconds",
39
- "reset-at",
40
- "window-minutes",
41
- ];
42
- if (!suffixes.some((suffix) => map[`${prefix}-${suffix}`] !== undefined)) {
8
+ const WEEKLY_WINDOW_MINUTES = 7 * 24 * 60;
9
+ // The old cache was populated from /backend-api/codex/responses headers. Keep
10
+ // the new app-server snapshots in a separate namespace so an upgrade cannot
11
+ // surface a model-specific response limit as the account's weekly limit.
12
+ const CACHE_TOOL = "codex-app-server";
13
+ function finiteNumber(value) {
14
+ if (typeof value !== "number" && (typeof value !== "string" || !value.trim())) {
43
15
  return undefined;
44
16
  }
45
- const usedPercent = num(map, `${prefix}-used-percent`) ?? 0;
17
+ const parsed = typeof value === "number" ? value : Number(value);
18
+ return Number.isFinite(parsed) ? parsed : undefined;
19
+ }
20
+ function nonEmptyString(value) {
21
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
22
+ }
23
+ function quotaWindow(value) {
24
+ if (!value || typeof value !== "object")
25
+ return undefined;
26
+ const usedPercent = finiteNumber(value.usedPercent);
27
+ if (usedPercent === undefined)
28
+ return undefined;
29
+ const resetAt = finiteNumber(value.resetsAt);
30
+ const now = Math.floor(Date.now() / 1000);
46
31
  return {
47
32
  usedPercent,
48
33
  remainingPercent: Math.max(0, 100 - usedPercent),
49
- resetAfterSeconds: num(map, `${prefix}-reset-after-seconds`),
50
- resetAt: num(map, `${prefix}-reset-at`),
51
- windowMinutes: num(map, `${prefix}-window-minutes`),
34
+ resetAt,
35
+ resetAfterSeconds: resetAt === undefined ? undefined : Math.max(0, resetAt - now),
36
+ windowMinutes: finiteNumber(value.windowDurationMins),
52
37
  };
53
38
  }
54
- /** Exported for protocol compatibility tests. */
55
- export function parseCodexHeaders(map, extras) {
56
- const primary = windowFromHeaders(map, "x-codex-primary");
57
- const secondary = windowFromHeaders(map, "x-codex-secondary");
58
- // Codex CLI 0.144 removed the 5h limit. Its only Codex window is now the
59
- // 10080-minute weekly window in `primary`, with `secondary` absent. Older
60
- // responses used primary=5h and secondary=weekly, so prefer an explicitly
61
- // weekly-sized window and then fall back to the legacy secondary position.
62
- const weekly = [primary, secondary].find((window) => (window?.windowMinutes ?? 0) >= 7 * 24 * 60) ?? secondary ?? primary ?? { usedPercent: 0, remainingPercent: 0 };
39
+ /** Normalize Codex app-server `account/rateLimits/read` into the manager shape. */
40
+ export function parseCodexRateLimitsResponse(response, extras = {}) {
41
+ const payload = response && typeof response === "object"
42
+ ? response
43
+ : {};
44
+ // `rateLimits` is the official backward-compatible account bucket. Do not
45
+ // pick a model-specific entry from rateLimitsByLimitId: e.g. Spark can have
46
+ // a different weekly percentage from the main Codex account limit.
47
+ const snapshot = payload.rateLimits && typeof payload.rateLimits === "object"
48
+ ? payload.rateLimits
49
+ : undefined;
50
+ const windows = [quotaWindow(snapshot?.primary), quotaWindow(snapshot?.secondary)];
51
+ const weekly = windows.find((window) => (window?.windowMinutes ?? 0) >= WEEKLY_WINDOW_MINUTES);
52
+ const credits = snapshot?.credits && typeof snapshot.credits === "object"
53
+ ? {
54
+ hasCredits: snapshot.credits.hasCredits === true,
55
+ balance: nonEmptyString(snapshot.credits.balance),
56
+ unlimited: snapshot.credits.unlimited === true,
57
+ }
58
+ : undefined;
59
+ const limitId = nonEmptyString(snapshot?.limitId);
60
+ const limitName = nonEmptyString(snapshot?.limitName);
63
61
  return {
64
- plan: str(map, "x-codex-plan-type"),
65
- activeLimit: str(map, "x-codex-active-limit"),
62
+ plan: nonEmptyString(snapshot?.planType),
63
+ activeLimit: limitName ?? limitId,
66
64
  weekly,
67
- credits: {
68
- hasCredits: bool(map, "x-codex-credits-has-credits") ?? false,
69
- balance: str(map, "x-codex-credits-balance"),
70
- unlimited: bool(map, "x-codex-credits-unlimited") ?? false,
71
- },
65
+ credits,
72
66
  email: extras.email,
73
67
  accountId: extras.accountId,
74
- raw: Object.fromEntries(Object.entries(map).filter(([k]) => k.startsWith("x-codex-"))),
68
+ raw: payload,
69
+ ...(!weekly ? { error: "Codex rate-limit response has no weekly window" } : {}),
75
70
  };
76
71
  }
77
72
  export async function getCodexQuota(opts = {}) {
@@ -82,81 +77,75 @@ export async function getCodexQuota(opts = {}) {
82
77
  if (!authInfo.accessToken) {
83
78
  return emptyQuota("unknown", "codex auth.json missing access_token");
84
79
  }
85
- const fp = fingerprintKey(["codex", authInfo.identityFingerprint ?? "unknown"]);
86
- const file = cacheFile("codex", fp, opts.cacheDir);
80
+ const fp = fingerprintKey([CACHE_TOOL, authInfo.identityFingerprint ?? "unknown"]);
81
+ const file = cacheFile(CACHE_TOOL, fp, opts.cacheDir);
87
82
  if (!opts.forceRefresh && ttl > 0) {
88
83
  const cached = await readCache(file);
89
84
  if (isFresh(cached, ttl) && cached) {
90
- return { ...withoutLegacyFiveHour(cached.value), source: "cached" };
85
+ return { ...cached.value, source: "cached" };
91
86
  }
92
87
  }
93
- const model = opts.model ??
94
- (await readConfiguredModel(opts.codexConfigPath ?? DEFAULT_CODEX_CONFIG)) ??
95
- "gpt-5";
96
- const body = JSON.stringify({
97
- model,
98
- instructions: "Reply with exactly one character.",
99
- input: [
100
- { type: "message", role: "user", content: [{ type: "input_text", text: "." }] },
101
- ],
102
- stream: true,
103
- store: false,
104
- });
105
- const controller = new AbortController();
106
- const timer = setTimeout(() => controller.abort(), timeoutMs);
107
- let res;
88
+ let response;
108
89
  try {
109
- res = await fetch(RESPONSES_URL, {
110
- method: "POST",
111
- headers: {
112
- Authorization: `Bearer ${authInfo.accessToken}`,
113
- "Content-Type": "application/json",
114
- "OpenAI-Beta": "responses=experimental",
115
- ...(authInfo.accountId ? { "chatgpt-account-id": authInfo.accountId } : {}),
116
- },
117
- body,
118
- signal: controller.signal,
90
+ response = await (opts.rateLimitsReader ?? readCodexRateLimits)({
91
+ codexAuthPath,
92
+ timeoutMs,
119
93
  });
120
94
  }
121
- catch (err) {
122
- clearTimeout(timer);
123
- return await fallbackFromCache(file, ttl, err?.message ?? String(err));
124
- }
125
- // Abort the body stream immediately once headers are in; they're all we need.
126
- try {
127
- await res.body?.cancel();
128
- }
129
- catch {
130
- // ignore
131
- }
132
- clearTimeout(timer);
133
- const map = headersToMap(res.headers);
134
- if (res.status === 401 || res.status === 403) {
135
- return await fallbackFromCache(file, ttl, `auth failed: HTTP ${res.status}`);
95
+ catch (error) {
96
+ return fallbackFromCache(file, ttl, errorMessage(error));
136
97
  }
137
- const parsed = parseCodexHeaders(map, {
98
+ const parsed = parseCodexRateLimitsResponse(response, {
138
99
  email: authInfo.email,
139
100
  accountId: authInfo.accountId,
140
101
  });
141
- const now = Math.floor(Date.now() / 1000);
102
+ if (!parsed.weekly) {
103
+ return fallbackFromCache(file, ttl, parsed.error ?? "Codex weekly quota unavailable");
104
+ }
142
105
  const fresh = {
143
106
  tool: "codex",
144
- fetchedAt: now,
107
+ fetchedAt: Math.floor(Date.now() / 1000),
145
108
  source: "fresh",
146
109
  ...parsed,
147
110
  };
148
- // Only persist if we actually got rate-limit headers — otherwise the upstream was unhealthy.
149
- if (Object.keys(fresh.raw ?? {}).length > 0) {
150
- await writeCache(file, fresh);
151
- return fresh;
111
+ await writeCache(file, fresh);
112
+ return fresh;
113
+ }
114
+ async function readCodexRateLimits(options) {
115
+ if (basename(options.codexAuthPath) !== "auth.json") {
116
+ throw new Error(`Codex app-server quota requires an active auth.json path; got ${options.codexAuthPath}`);
117
+ }
118
+ const transport = new CodexAppServerTransport({
119
+ env: { CODEX_HOME: dirname(options.codexAuthPath) },
120
+ ignoreCodexApiKey: true,
121
+ });
122
+ try {
123
+ return await withTimeout(transport.request("account/rateLimits/read"), options.timeoutMs, "Codex account/rateLimits/read timed out");
124
+ }
125
+ finally {
126
+ await transport.close();
127
+ }
128
+ }
129
+ async function withTimeout(promise, timeoutMs, message) {
130
+ let timer;
131
+ try {
132
+ return await Promise.race([
133
+ promise,
134
+ new Promise((_resolve, reject) => {
135
+ timer = setTimeout(() => reject(new Error(message)), timeoutMs);
136
+ }),
137
+ ]);
138
+ }
139
+ finally {
140
+ if (timer)
141
+ clearTimeout(timer);
152
142
  }
153
- return await fallbackFromCache(file, ttl, `no x-codex-* headers on HTTP ${res.status}`);
154
143
  }
155
144
  async function fallbackFromCache(file, ttl, error) {
156
145
  const cached = await readCache(file);
157
146
  if (cached) {
158
147
  return {
159
- ...withoutLegacyFiveHour(cached.value),
148
+ ...cached.value,
160
149
  source: isFresh(cached, ttl) ? "cached" : "stale",
161
150
  error,
162
151
  };
@@ -165,23 +154,21 @@ async function fallbackFromCache(file, ttl, error) {
165
154
  }
166
155
  /**
167
156
  * Read the on-disk quota cache for a given codex auth.json without triggering
168
- * any network call. Returns whatever snapshot was last written (regardless of
169
- * age) or `null` if nothing has been cached for this identity yet. Used by the
170
- * daemon's `list_accounts` handler to surface "last known" quota for inactive
171
- * accounts so the web UI can restore them across page refreshes.
157
+ * any network call. Only app-server snapshots are considered; legacy caches
158
+ * from response headers are intentionally ignored.
172
159
  */
173
160
  export async function readCachedCodexQuota(codexAuthPath, opts = {}) {
174
161
  try {
175
162
  const authInfo = await parseAuthFile(codexAuthPath);
176
163
  if (!authInfo.identityFingerprint)
177
164
  return null;
178
- const fp = fingerprintKey(["codex", authInfo.identityFingerprint]);
179
- const file = cacheFile("codex", fp, opts.cacheDir);
165
+ const fp = fingerprintKey([CACHE_TOOL, authInfo.identityFingerprint]);
166
+ const file = cacheFile(CACHE_TOOL, fp, opts.cacheDir);
180
167
  const entry = await readCache(file);
181
168
  if (!entry)
182
169
  return null;
183
170
  return {
184
- ...withoutLegacyFiveHour(entry.value),
171
+ ...entry.value,
185
172
  source: "cached",
186
173
  fetchedAt: entry.fetchedAt,
187
174
  };
@@ -190,9 +177,8 @@ export async function readCachedCodexQuota(codexAuthPath, opts = {}) {
190
177
  return null;
191
178
  }
192
179
  }
193
- function withoutLegacyFiveHour(quota) {
194
- const { fiveHour: _legacyFiveHour, ...current } = quota;
195
- return current;
180
+ function errorMessage(error) {
181
+ return error instanceof Error ? error.message : String(error);
196
182
  }
197
183
  function emptyQuota(source, error) {
198
184
  return {
@@ -200,6 +186,5 @@ function emptyQuota(source, error) {
200
186
  source,
201
187
  error,
202
188
  fetchedAt: Math.floor(Date.now() / 1000),
203
- weekly: { usedPercent: 0, remainingPercent: 0 },
204
189
  };
205
190
  }
@@ -37,9 +37,7 @@ export interface CodexQuota {
37
37
  tool: "codex";
38
38
  plan?: string;
39
39
  activeLimit?: string;
40
- /** @deprecated Codex CLI 0.144+ no longer exposes a 5h limit. */
41
- fiveHour?: QuotaWindow;
42
- weekly: QuotaWindow;
40
+ weekly?: QuotaWindow;
43
41
  credits?: {
44
42
  hasCredits: boolean;
45
43
  balance?: string;
@@ -49,7 +47,7 @@ export interface CodexQuota {
49
47
  source: QuotaSource;
50
48
  email?: string;
51
49
  accountId?: string;
52
- raw?: Record<string, string>;
50
+ raw?: Record<string, unknown>;
53
51
  error?: string;
54
52
  }
55
53
  export interface ClaudeQuota {
package/dist/shared.js CHANGED
@@ -280,8 +280,29 @@ export function extractLongFlagFromCommandLine(commandLine, flag) {
280
280
  return "";
281
281
  }
282
282
  export function resolveConductorConfigPath(configFilePath) {
283
- const home = os.homedir();
284
- return configFilePath || process.env.CONDUCTOR_CONFIG || path.join(home, ".conductor", "config.yaml");
283
+ if (configFilePath) {
284
+ return configFilePath;
285
+ }
286
+ const userHome = process.env.HOME || process.env.USERPROFILE || os.homedir();
287
+ if (typeof process.env.CONDUCTOR_CONFIG === "string" && process.env.CONDUCTOR_CONFIG.trim()) {
288
+ const configuredPath = process.env.CONDUCTOR_CONFIG.trim();
289
+ const expandedPath = configuredPath === "~"
290
+ ? userHome
291
+ : configuredPath.startsWith("~/") || configuredPath.startsWith("~\\")
292
+ ? path.join(userHome, configuredPath.slice(2))
293
+ : configuredPath;
294
+ return path.resolve(expandedPath);
295
+ }
296
+ const configuredHome = typeof process.env.CONDUCTOR_HOME === "string" ? process.env.CONDUCTOR_HOME.trim() : "";
297
+ if (configuredHome) {
298
+ const expandedHome = configuredHome === "~"
299
+ ? userHome
300
+ : configuredHome.startsWith("~/") || configuredHome.startsWith("~\\")
301
+ ? path.join(userHome, configuredHome.slice(2))
302
+ : configuredHome;
303
+ return path.join(path.resolve(expandedHome), "config.yaml");
304
+ }
305
+ return path.join(userHome, ".conductor", "config.yaml");
285
306
  }
286
307
  export function loadYamlConfig(configFilePath) {
287
308
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/ai-sdk",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/lovemoon-ai/conductor.git"
@@ -32,12 +32,12 @@
32
32
  "zod": "^4.1.5"
33
33
  },
34
34
  "optionalDependencies": {
35
- "@love-moon/chat-web": "^0.7.4"
35
+ "@love-moon/chat-web": "^0.7.6"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^22.10.2",
39
39
  "tsx": "^4.20.6",
40
40
  "typescript": "^5.6.3"
41
41
  },
42
- "gitCommitId": "b9a27b4"
42
+ "gitCommitId": "c939df2"
43
43
  }