@love-moon/ai-sdk 0.7.3 → 0.7.5

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,18 @@
1
1
  # @love-moon/ai-sdk
2
2
 
3
+ ## 0.7.5
4
+
5
+ ### Patch Changes
6
+
7
+ - f91a5df: Read Codex weekly quota from the official app-server account rate-limit RPC, ignore model-specific buckets, and invalidate legacy response-header caches.
8
+
9
+ ## 0.7.4
10
+
11
+ ### Patch Changes
12
+
13
+ - d5eca1c: Adapt Codex quota parsing to the 0.144 weekly-only limit while retaining compatibility with legacy header layouts and cached snapshots.
14
+ - d5eca1c: Support Kimi Code CLI prompt sessions, current session storage, and credential locations while retaining legacy Kimi wire compatibility.
15
+
3
16
  ## 0.7.3
4
17
 
5
18
  ### Patch Changes
@@ -2,5 +2,7 @@ export declare function expandHome(p: string): string;
2
2
  export declare const DEFAULT_CONDUCTOR_CONFIG: string;
3
3
  export declare const DEFAULT_CODEX_AUTH: string;
4
4
  export declare const DEFAULT_CODEX_CONFIG: string;
5
+ export declare const DEFAULT_KIMI_CODE_HOME: string;
5
6
  export declare const DEFAULT_KIMI_CREDENTIAL: string;
7
+ export declare const LEGACY_KIMI_CREDENTIAL: string;
6
8
  export declare const DEFAULT_QUOTA_CACHE_DIR: string;
@@ -12,5 +12,7 @@ export function expandHome(p) {
12
12
  export const DEFAULT_CONDUCTOR_CONFIG = join(homedir(), ".conductor", "config.yaml");
13
13
  export const DEFAULT_CODEX_AUTH = join(homedir(), ".codex", "auth.json");
14
14
  export const DEFAULT_CODEX_CONFIG = join(homedir(), ".codex", "config.toml");
15
- export const DEFAULT_KIMI_CREDENTIAL = join(homedir(), ".kimi", "credentials", "kimi-code.json");
15
+ export const DEFAULT_KIMI_CODE_HOME = process.env.KIMI_CODE_HOME?.trim() || join(homedir(), ".kimi-code");
16
+ export const DEFAULT_KIMI_CREDENTIAL = join(DEFAULT_KIMI_CODE_HOME, "credentials", "kimi-code.json");
17
+ export const LEGACY_KIMI_CREDENTIAL = join(homedir(), ".kimi", "credentials", "kimi-code.json");
16
18
  export const DEFAULT_QUOTA_CACHE_DIR = join(homedir(), ".conductor", "cache", "ai-manager");
@@ -7,20 +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
  }
22
+ /** Normalize Codex app-server `account/rateLimits/read` into the manager shape. */
23
+ export declare function parseCodexRateLimitsResponse(response: unknown, extras?: {
24
+ email?: string;
25
+ accountId?: string;
26
+ }): Omit<CodexQuota, "tool" | "fetchedAt" | "source">;
17
27
  export declare function getCodexQuota(opts?: GetCodexQuotaOptions): Promise<CodexQuota>;
18
28
  /**
19
29
  * Read the on-disk quota cache for a given codex auth.json without triggering
20
- * any network call. Returns whatever snapshot was last written (regardless of
21
- * age) or `null` if nothing has been cached for this identity yet. Used by the
22
- * daemon's `list_accounts` handler to surface "last known" quota for inactive
23
- * 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.
24
32
  */
25
33
  export declare function readCachedCodexQuota(codexAuthPath: string, opts?: {
26
34
  cacheDir?: string;
@@ -1,61 +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 usedPercent = num(map, `${prefix}-used-percent`) ?? 0;
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())) {
15
+ return undefined;
16
+ }
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);
37
31
  return {
38
32
  usedPercent,
39
33
  remainingPercent: Math.max(0, 100 - usedPercent),
40
- resetAfterSeconds: num(map, `${prefix}-reset-after-seconds`),
41
- resetAt: num(map, `${prefix}-reset-at`),
42
- windowMinutes: num(map, `${prefix}-window-minutes`),
34
+ resetAt,
35
+ resetAfterSeconds: resetAt === undefined ? undefined : Math.max(0, resetAt - now),
36
+ windowMinutes: finiteNumber(value.windowDurationMins),
43
37
  };
44
38
  }
45
- function parseCodexHeaders(map, extras) {
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);
46
61
  return {
47
- plan: str(map, "x-codex-plan-type"),
48
- activeLimit: str(map, "x-codex-active-limit"),
49
- fiveHour: windowFromHeaders(map, "x-codex-primary"),
50
- weekly: windowFromHeaders(map, "x-codex-secondary"),
51
- credits: {
52
- hasCredits: bool(map, "x-codex-credits-has-credits") ?? false,
53
- balance: str(map, "x-codex-credits-balance"),
54
- unlimited: bool(map, "x-codex-credits-unlimited") ?? false,
55
- },
62
+ plan: nonEmptyString(snapshot?.planType),
63
+ activeLimit: limitName ?? limitId,
64
+ weekly,
65
+ credits,
56
66
  email: extras.email,
57
67
  accountId: extras.accountId,
58
- 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" } : {}),
59
70
  };
60
71
  }
61
72
  export async function getCodexQuota(opts = {}) {
@@ -66,75 +77,69 @@ export async function getCodexQuota(opts = {}) {
66
77
  if (!authInfo.accessToken) {
67
78
  return emptyQuota("unknown", "codex auth.json missing access_token");
68
79
  }
69
- const fp = fingerprintKey(["codex", authInfo.identityFingerprint ?? "unknown"]);
70
- 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);
71
82
  if (!opts.forceRefresh && ttl > 0) {
72
83
  const cached = await readCache(file);
73
84
  if (isFresh(cached, ttl) && cached) {
74
85
  return { ...cached.value, source: "cached" };
75
86
  }
76
87
  }
77
- const model = opts.model ??
78
- (await readConfiguredModel(opts.codexConfigPath ?? DEFAULT_CODEX_CONFIG)) ??
79
- "gpt-5";
80
- const body = JSON.stringify({
81
- model,
82
- instructions: "Reply with exactly one character.",
83
- input: [
84
- { type: "message", role: "user", content: [{ type: "input_text", text: "." }] },
85
- ],
86
- stream: true,
87
- store: false,
88
- });
89
- const controller = new AbortController();
90
- const timer = setTimeout(() => controller.abort(), timeoutMs);
91
- let res;
88
+ let response;
92
89
  try {
93
- res = await fetch(RESPONSES_URL, {
94
- method: "POST",
95
- headers: {
96
- Authorization: `Bearer ${authInfo.accessToken}`,
97
- "Content-Type": "application/json",
98
- "OpenAI-Beta": "responses=experimental",
99
- ...(authInfo.accountId ? { "chatgpt-account-id": authInfo.accountId } : {}),
100
- },
101
- body,
102
- signal: controller.signal,
90
+ response = await (opts.rateLimitsReader ?? readCodexRateLimits)({
91
+ codexAuthPath,
92
+ timeoutMs,
103
93
  });
104
94
  }
105
- catch (err) {
106
- clearTimeout(timer);
107
- return await fallbackFromCache(file, ttl, err?.message ?? String(err));
108
- }
109
- // Abort the body stream immediately once headers are in; they're all we need.
110
- try {
111
- await res.body?.cancel();
112
- }
113
- catch {
114
- // ignore
115
- }
116
- clearTimeout(timer);
117
- const map = headersToMap(res.headers);
118
- if (res.status === 401 || res.status === 403) {
119
- return await fallbackFromCache(file, ttl, `auth failed: HTTP ${res.status}`);
95
+ catch (error) {
96
+ return fallbackFromCache(file, ttl, errorMessage(error));
120
97
  }
121
- const parsed = parseCodexHeaders(map, {
98
+ const parsed = parseCodexRateLimitsResponse(response, {
122
99
  email: authInfo.email,
123
100
  accountId: authInfo.accountId,
124
101
  });
125
- const now = Math.floor(Date.now() / 1000);
102
+ if (!parsed.weekly) {
103
+ return fallbackFromCache(file, ttl, parsed.error ?? "Codex weekly quota unavailable");
104
+ }
126
105
  const fresh = {
127
106
  tool: "codex",
128
- fetchedAt: now,
107
+ fetchedAt: Math.floor(Date.now() / 1000),
129
108
  source: "fresh",
130
109
  ...parsed,
131
110
  };
132
- // Only persist if we actually got rate-limit headers — otherwise the upstream was unhealthy.
133
- if (Object.keys(fresh.raw ?? {}).length > 0) {
134
- await writeCache(file, fresh);
135
- 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);
136
142
  }
137
- return await fallbackFromCache(file, ttl, `no x-codex-* headers on HTTP ${res.status}`);
138
143
  }
139
144
  async function fallbackFromCache(file, ttl, error) {
140
145
  const cached = await readCache(file);
@@ -149,34 +154,37 @@ async function fallbackFromCache(file, ttl, error) {
149
154
  }
150
155
  /**
151
156
  * Read the on-disk quota cache for a given codex auth.json without triggering
152
- * any network call. Returns whatever snapshot was last written (regardless of
153
- * age) or `null` if nothing has been cached for this identity yet. Used by the
154
- * daemon's `list_accounts` handler to surface "last known" quota for inactive
155
- * 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.
156
159
  */
157
160
  export async function readCachedCodexQuota(codexAuthPath, opts = {}) {
158
161
  try {
159
162
  const authInfo = await parseAuthFile(codexAuthPath);
160
163
  if (!authInfo.identityFingerprint)
161
164
  return null;
162
- const fp = fingerprintKey(["codex", authInfo.identityFingerprint]);
163
- const file = cacheFile("codex", fp, opts.cacheDir);
165
+ const fp = fingerprintKey([CACHE_TOOL, authInfo.identityFingerprint]);
166
+ const file = cacheFile(CACHE_TOOL, fp, opts.cacheDir);
164
167
  const entry = await readCache(file);
165
168
  if (!entry)
166
169
  return null;
167
- return { ...entry.value, source: "cached", fetchedAt: entry.fetchedAt };
170
+ return {
171
+ ...entry.value,
172
+ source: "cached",
173
+ fetchedAt: entry.fetchedAt,
174
+ };
168
175
  }
169
176
  catch {
170
177
  return null;
171
178
  }
172
179
  }
180
+ function errorMessage(error) {
181
+ return error instanceof Error ? error.message : String(error);
182
+ }
173
183
  function emptyQuota(source, error) {
174
184
  return {
175
185
  tool: "codex",
176
186
  source,
177
187
  error,
178
188
  fetchedAt: Math.floor(Date.now() / 1000),
179
- fiveHour: { usedPercent: 0, remainingPercent: 0 },
180
- weekly: { usedPercent: 0, remainingPercent: 0 },
181
189
  };
182
190
  }
@@ -1,5 +1,5 @@
1
1
  import { readFile, writeFile, rename } from "node:fs/promises";
2
- import { DEFAULT_KIMI_CREDENTIAL } from "../paths.js";
2
+ import { DEFAULT_KIMI_CREDENTIAL, LEGACY_KIMI_CREDENTIAL } from "../paths.js";
3
3
  import { cacheFile, fingerprintKey, isFresh, readCache, writeCache } from "./cache.js";
4
4
  const USAGE_URL = "https://api.kimi.com/coding/v1/usages";
5
5
  const OAUTH_TOKEN_URL = "https://auth.kimi.com/api/oauth/token";
@@ -8,19 +8,32 @@ const DEFAULT_TTL = 60;
8
8
  const DEFAULT_TIMEOUT_MS = 15_000;
9
9
  const REFRESH_LEEWAY_SECONDS = 60;
10
10
  export async function getKimiQuota(opts = {}) {
11
- const credentialPath = opts.credentialPath ?? DEFAULT_KIMI_CREDENTIAL;
11
+ const credentialPaths = opts.credentialPath
12
+ ? [opts.credentialPath]
13
+ : [DEFAULT_KIMI_CREDENTIAL, LEGACY_KIMI_CREDENTIAL];
12
14
  const ttl = opts.ttlSeconds ?? DEFAULT_TTL;
13
15
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
14
16
  const doFetch = opts.fetcher ?? fetch;
17
+ let credentialPath = credentialPaths[0];
15
18
  let cred;
16
- try {
17
- cred = JSON.parse(await readFile(credentialPath, "utf8"));
18
- }
19
- catch (err) {
20
- return emptyQuota("unknown", `kimi credential not readable: ${err?.message ?? err}`);
19
+ let credentialError = "credential missing access_token";
20
+ for (const candidatePath of credentialPaths) {
21
+ try {
22
+ const candidate = JSON.parse(await readFile(candidatePath, "utf8"));
23
+ if (!candidate.access_token) {
24
+ credentialError = "credential missing access_token";
25
+ continue;
26
+ }
27
+ credentialPath = candidatePath;
28
+ cred = candidate;
29
+ break;
30
+ }
31
+ catch (err) {
32
+ credentialError = `credential not readable: ${err?.message ?? err}`;
33
+ }
21
34
  }
22
- if (!cred.access_token) {
23
- return emptyQuota("unknown", "kimi credential missing access_token");
35
+ if (!cred) {
36
+ return emptyQuota("unknown", `kimi ${credentialError}`);
24
37
  }
25
38
  const fp = fingerprintKey(["kimi", cred.access_token.slice(-32)]);
26
39
  const file = cacheFile("kimi", fp, opts.cacheDir);
@@ -37,8 +37,7 @@ export interface CodexQuota {
37
37
  tool: "codex";
38
38
  plan?: string;
39
39
  activeLimit?: string;
40
- fiveHour: QuotaWindow;
41
- weekly: QuotaWindow;
40
+ weekly?: QuotaWindow;
42
41
  credits?: {
43
42
  hasCredits: boolean;
44
43
  balance?: string;
@@ -48,7 +47,7 @@ export interface CodexQuota {
48
47
  source: QuotaSource;
49
48
  email?: string;
50
49
  accountId?: string;
51
- raw?: Record<string, string>;
50
+ raw?: Record<string, unknown>;
52
51
  error?: string;
53
52
  }
54
53
  export interface ClaudeQuota {
@@ -0,0 +1 @@
1
+ export function resolveKimiCliMode(options?: {}): Promise<"prompt" | "wire">;
@@ -0,0 +1,115 @@
1
+ import { spawn } from "node:child_process";
2
+ import { emitLog, normalizeLogger, parseCommandParts, sanitizeForLog, } from "../shared.js";
3
+ const DEFAULT_KIMI_COMMAND = "kimi";
4
+ const DEFAULT_PROBE_TIMEOUT_MS = 5_000;
5
+ const MAX_PROBE_OUTPUT_CHARS = 128 * 1024;
6
+ function normalizeMode(value) {
7
+ const normalized = String(value || "").trim().toLowerCase();
8
+ if (normalized === "prompt" || normalized === "kimi-code" || normalized === "modern") {
9
+ return "prompt";
10
+ }
11
+ if (normalized === "wire" || normalized === "legacy") {
12
+ return "wire";
13
+ }
14
+ return "";
15
+ }
16
+ function resolveCommandLine(options = {}) {
17
+ return (process.env.CONDUCTOR_KIMI_COMMAND ||
18
+ options.commandLine ||
19
+ process.env.CONDUCTOR_CLI_COMMAND ||
20
+ DEFAULT_KIMI_COMMAND);
21
+ }
22
+ function appendProbeOutput(current, chunk) {
23
+ if (current.length >= MAX_PROBE_OUTPUT_CHARS) {
24
+ return current;
25
+ }
26
+ return `${current}${String(chunk || "")}`.slice(0, MAX_PROBE_OUTPUT_CHARS);
27
+ }
28
+ async function readKimiHelp(options = {}) {
29
+ const { command, args } = parseCommandParts(resolveCommandLine(options));
30
+ if (!command) {
31
+ throw new Error("Invalid kimi command");
32
+ }
33
+ const cwd = typeof options.cwd === "string" && options.cwd.trim()
34
+ ? options.cwd.trim()
35
+ : process.cwd();
36
+ const env = {
37
+ ...process.env,
38
+ ...(options.env && typeof options.env === "object" ? options.env : {}),
39
+ };
40
+ return await new Promise((resolve, reject) => {
41
+ const child = spawn(command, [...args, "--help"], {
42
+ cwd,
43
+ env,
44
+ stdio: ["ignore", "pipe", "pipe"],
45
+ });
46
+ let output = "";
47
+ let settled = false;
48
+ const timeoutMs = Number.isFinite(options.kimiCliProbeTimeoutMs)
49
+ ? Math.max(100, Number(options.kimiCliProbeTimeoutMs))
50
+ : DEFAULT_PROBE_TIMEOUT_MS;
51
+ const timeoutId = setTimeout(() => {
52
+ if (settled) {
53
+ return;
54
+ }
55
+ settled = true;
56
+ try {
57
+ child.kill("SIGTERM");
58
+ }
59
+ catch {
60
+ // best effort
61
+ }
62
+ reject(new Error(`Kimi CLI capability probe timed out after ${timeoutMs}ms`));
63
+ }, timeoutMs);
64
+ child.stdout.on("data", (chunk) => {
65
+ output = appendProbeOutput(output, chunk);
66
+ });
67
+ child.stderr.on("data", (chunk) => {
68
+ output = appendProbeOutput(output, chunk);
69
+ });
70
+ child.once("error", (error) => {
71
+ if (settled) {
72
+ return;
73
+ }
74
+ settled = true;
75
+ clearTimeout(timeoutId);
76
+ reject(error);
77
+ });
78
+ child.once("close", () => {
79
+ if (settled) {
80
+ return;
81
+ }
82
+ settled = true;
83
+ clearTimeout(timeoutId);
84
+ resolve(output);
85
+ });
86
+ });
87
+ }
88
+ export async function resolveKimiCliMode(options = {}) {
89
+ if (options.transport) {
90
+ return "wire";
91
+ }
92
+ const configuredMode = normalizeMode(options.kimiCliMode ||
93
+ options.env?.CONDUCTOR_KIMI_CLI_MODE ||
94
+ process.env.CONDUCTOR_KIMI_CLI_MODE);
95
+ if (configuredMode) {
96
+ return configuredMode;
97
+ }
98
+ const logger = normalizeLogger(options.logger);
99
+ try {
100
+ const helpText = await readKimiHelp(options);
101
+ if (/(^|\s)--wire(?:\s|$)/m.test(helpText)) {
102
+ emitLog(logger, "[kimi-detect] using legacy wire mode");
103
+ return "wire";
104
+ }
105
+ if (/(^|\s)--prompt(?:\s|,|$)/m.test(helpText) && helpText.includes("--output-format")) {
106
+ emitLog(logger, "[kimi-detect] using Kimi Code prompt mode");
107
+ return "prompt";
108
+ }
109
+ emitLog(logger, "[kimi-detect] CLI mode was not recognized; keeping legacy wire mode");
110
+ }
111
+ catch (error) {
112
+ emitLog(logger, `[kimi-detect] capability probe failed; keeping legacy wire mode: ${sanitizeForLog(error?.message || error, 180)}`);
113
+ }
114
+ return "wire";
115
+ }
@@ -4,11 +4,13 @@ export class KimiPrintSession extends EventEmitter<[never]> {
4
4
  options: {};
5
5
  logger: any;
6
6
  cwd: any;
7
+ cliMode: string;
7
8
  resumeSessionId: any;
8
9
  sessionId: any;
9
10
  sessionInfo: {
10
11
  backend: string;
11
12
  sessionId: any;
13
+ sessionIdDeferred: boolean;
12
14
  model: any;
13
15
  };
14
16
  history: any[];
@@ -21,6 +23,7 @@ export class KimiPrintSession extends EventEmitter<[never]> {
21
23
  items: never[];
22
24
  stderrTail: never[];
23
25
  settled: boolean;
26
+ interruptRequested: boolean;
24
27
  } | null;
25
28
  currentTurnStatus: any;
26
29
  sessionAnnounced: boolean;
@@ -47,6 +50,7 @@ export class KimiPrintSession extends EventEmitter<[never]> {
47
50
  sessionInfo: {
48
51
  backend: string;
49
52
  sessionId: any;
53
+ sessionIdDeferred: boolean;
50
54
  model: any;
51
55
  } | null;
52
56
  useSessionFileReplyStream: boolean;
@@ -61,12 +65,14 @@ export class KimiPrintSession extends EventEmitter<[never]> {
61
65
  getSessionInfo(): {
62
66
  backend: string;
63
67
  sessionId: any;
68
+ sessionIdDeferred: boolean;
64
69
  model: any;
65
70
  } | null;
66
71
  getCurrentTurnStatus(): any;
67
72
  ensureSessionInfo(): Promise<{
68
73
  backend: string;
69
74
  sessionId: any;
75
+ sessionIdDeferred: boolean;
70
76
  model: any;
71
77
  } | null>;
72
78
  getSessionUsageSummary(): Promise<{
@@ -87,12 +93,13 @@ export class KimiPrintSession extends EventEmitter<[never]> {
87
93
  setSessionReplyTarget(replyTo: any): void;
88
94
  getCurrentReplyTarget(): string | undefined;
89
95
  announceSession(): void;
96
+ adoptSessionId(sessionId: any): void;
90
97
  createSessionClosedError(): Error;
91
98
  updateCurrentTurnStatus(payload: any): void;
92
99
  emitWorkingStatus(payload: any, onProgress?: null): Promise<any>;
93
100
  emitAssistantMessage(text: any): Promise<void>;
94
101
  buildPrompt(promptText: any): string;
95
- buildPrintArgs(): string[];
102
+ buildPrintArgs(promptText?: string): string[];
96
103
  buildUserMessage(promptText: any, { useInitialImages }?: {
97
104
  useInitialImages?: boolean | undefined;
98
105
  }): {
@@ -100,6 +107,7 @@ export class KimiPrintSession extends EventEmitter<[never]> {
100
107
  content: any;
101
108
  };
102
109
  maybeEmitAuthRequired(stderrTail: any): void;
110
+ interruptCurrentTurn(): Promise<boolean>;
103
111
  runTurn(promptText: any, { useInitialImages, onProgress, jsonSchema }?: {
104
112
  useInitialImages?: boolean | undefined;
105
113
  onProgress?: null | undefined;
@@ -85,7 +85,7 @@ function statusLineForPhase(phase) {
85
85
  return "Kimi is working";
86
86
  }
87
87
  }
88
- function filterKimiPrintBaseArgs(args) {
88
+ function filterKimiPrintBaseArgs(args, cliMode = "legacy-print") {
89
89
  const filtered = [];
90
90
  let skipNext = false;
91
91
  for (const rawArg of Array.isArray(args) ? args : []) {
@@ -97,7 +97,18 @@ function filterKimiPrintBaseArgs(args) {
97
97
  skipNext = false;
98
98
  continue;
99
99
  }
100
- if (arg === "--wire" || arg === "--print" || arg === "--final-message-only") {
100
+ if (arg === "--wire" ||
101
+ arg === "--print" ||
102
+ arg === "--final-message-only" ||
103
+ arg === "--yolo" ||
104
+ arg === "--yes" ||
105
+ arg === "--auto-approve" ||
106
+ arg === "--auto" ||
107
+ arg === "--plan" ||
108
+ arg === "--continue" ||
109
+ arg === "-C" ||
110
+ arg === "-y" ||
111
+ (cliMode === "prompt" && arg === "-c")) {
101
112
  continue;
102
113
  }
103
114
  if (arg === "--input-format" ||
@@ -105,8 +116,15 @@ function filterKimiPrintBaseArgs(args) {
105
116
  arg === "--prompt" ||
106
117
  arg === "--command" ||
107
118
  arg === "--session" ||
119
+ arg === "--resume" ||
108
120
  arg === "--work-dir" ||
109
- arg === "--model") {
121
+ arg === "--model" ||
122
+ arg === "-S" ||
123
+ arg === "-r" ||
124
+ arg === "-m" ||
125
+ arg === "-p" ||
126
+ arg === "-w" ||
127
+ (cliMode === "legacy-print" && arg === "-c")) {
110
128
  skipNext = true;
111
129
  continue;
112
130
  }
@@ -115,14 +133,26 @@ function filterKimiPrintBaseArgs(args) {
115
133
  arg.startsWith("--prompt=") ||
116
134
  arg.startsWith("--command=") ||
117
135
  arg.startsWith("--session=") ||
136
+ arg.startsWith("--resume=") ||
118
137
  arg.startsWith("--work-dir=") ||
119
- arg.startsWith("--model=")) {
138
+ arg.startsWith("--model=") ||
139
+ ["-S", "-r", "-m", "-p", "-w", "-c"].some((prefix) => arg.startsWith(prefix) && arg.length > prefix.length)) {
120
140
  continue;
121
141
  }
122
142
  filtered.push(arg);
123
143
  }
124
144
  return filtered;
125
145
  }
146
+ function quoteShellArg(value) {
147
+ const normalized = String(value ?? "");
148
+ if (!normalized) {
149
+ return "''";
150
+ }
151
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(normalized)) {
152
+ return normalized;
153
+ }
154
+ return `'${normalized.replace(/'/g, `'\\''`)}'`;
155
+ }
126
156
  function normalizeTextContent(content) {
127
157
  if (typeof content === "string") {
128
158
  return content;
@@ -170,11 +200,13 @@ export class KimiPrintSession extends EventEmitter {
170
200
  typeof options.cwd === "string" && options.cwd.trim()
171
201
  ? options.cwd.trim()
172
202
  : process.cwd();
203
+ this.cliMode = options.kimiCliMode === "prompt" ? "prompt" : "legacy-print";
173
204
  this.resumeSessionId = typeof options.resumeSessionId === "string" ? options.resumeSessionId.trim() : "";
174
- this.sessionId = this.resumeSessionId || randomUUID();
205
+ this.sessionId = this.resumeSessionId || (this.cliMode === "prompt" ? "" : randomUUID());
175
206
  this.sessionInfo = {
176
207
  backend: this.backend,
177
- sessionId: this.sessionId,
208
+ sessionId: this.sessionId || undefined,
209
+ sessionIdDeferred: this.cliMode === "prompt" && !this.sessionId,
178
210
  model: typeof options.model === "string" && options.model.trim()
179
211
  ? options.model.trim()
180
212
  : this.backend,
@@ -207,7 +239,7 @@ export class KimiPrintSession extends EventEmitter {
207
239
  throw new Error("Invalid kimi print command");
208
240
  }
209
241
  this.command = command;
210
- this.baseArgs = filterKimiPrintBaseArgs(args);
242
+ this.baseArgs = filterKimiPrintBaseArgs(args, this.cliMode);
211
243
  }
212
244
  writeLog(message) {
213
245
  emitLog(this.logger, message);
@@ -227,7 +259,21 @@ export class KimiPrintSession extends EventEmitter {
227
259
  };
228
260
  }
229
261
  buildManualResumeCommand() {
230
- return `kimi --work-dir ${this.cwd} --session ${this.sessionId}`;
262
+ if (!this.sessionId) {
263
+ return "";
264
+ }
265
+ const parts = [...this.baseArgs];
266
+ if (this.cliMode === "legacy-print") {
267
+ parts.push("--work-dir", this.cwd);
268
+ }
269
+ parts.push("--session", this.sessionId);
270
+ if (typeof this.options.model === "string" && this.options.model.trim()) {
271
+ parts.push("--model", this.options.model.trim());
272
+ }
273
+ const command = [this.command, ...parts].map(quoteShellArg).join(" ");
274
+ return this.cliMode === "prompt"
275
+ ? `cd ${quoteShellArg(this.cwd)} && ${command}`
276
+ : command;
231
277
  }
232
278
  getSnapshot() {
233
279
  return {
@@ -255,7 +301,9 @@ export class KimiPrintSession extends EventEmitter {
255
301
  return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
256
302
  }
257
303
  async ensureSessionInfo() {
258
- this.announceSession();
304
+ if (!this.sessionInfo?.sessionIdDeferred) {
305
+ this.announceSession();
306
+ }
259
307
  return this.getSessionInfo();
260
308
  }
261
309
  async getSessionUsageSummary() {
@@ -294,12 +342,26 @@ export class KimiPrintSession extends EventEmitter {
294
342
  return this.activeReplyTarget || this.lastReplyTarget || undefined;
295
343
  }
296
344
  announceSession() {
297
- if (this.sessionAnnounced) {
345
+ if (this.sessionAnnounced || !this.sessionId) {
298
346
  return;
299
347
  }
300
348
  this.sessionAnnounced = true;
301
349
  this.emit("session", this.getSessionInfo());
302
350
  }
351
+ adoptSessionId(sessionId) {
352
+ const normalizedSessionId = typeof sessionId === "string" ? sessionId.trim() : "";
353
+ if (!normalizedSessionId || normalizedSessionId === this.sessionId) {
354
+ return;
355
+ }
356
+ this.sessionId = normalizedSessionId;
357
+ this.sessionInfo = {
358
+ ...this.sessionInfo,
359
+ sessionId: normalizedSessionId,
360
+ sessionIdDeferred: false,
361
+ };
362
+ this.trace(`session ready id=${sanitizeForLog(normalizedSessionId, 120)}`);
363
+ this.announceSession();
364
+ }
303
365
  createSessionClosedError() {
304
366
  return createTurnError("Kimi print session closed", {
305
367
  reason: "session_closed",
@@ -357,8 +419,19 @@ export class KimiPrintSession extends EventEmitter {
357
419
  this.pendingHistorySeed = false;
358
420
  return buildHistoryPrompt(this.history, normalizedPrompt);
359
421
  }
360
- buildPrintArgs() {
422
+ buildPrintArgs(promptText = "") {
361
423
  const args = [...this.baseArgs];
424
+ if (this.cliMode === "prompt") {
425
+ args.push("--output-format=stream-json");
426
+ if (this.sessionId) {
427
+ args.push(`--session=${this.sessionId}`);
428
+ }
429
+ if (typeof this.options.model === "string" && this.options.model.trim()) {
430
+ args.push(`--model=${this.options.model.trim()}`);
431
+ }
432
+ args.push("--prompt", promptText);
433
+ return args;
434
+ }
362
435
  args.push("--print");
363
436
  args.push("--input-format=stream-json");
364
437
  args.push("--output-format=stream-json");
@@ -413,6 +486,20 @@ export class KimiPrintSession extends EventEmitter {
413
486
  message: lastMessage || "Kimi authentication required",
414
487
  });
415
488
  }
489
+ async interruptCurrentTurn() {
490
+ const currentTurn = this.currentTurn;
491
+ if (!currentTurn?.child) {
492
+ return false;
493
+ }
494
+ currentTurn.interruptRequested = true;
495
+ try {
496
+ currentTurn.child.kill("SIGINT");
497
+ return true;
498
+ }
499
+ catch {
500
+ return false;
501
+ }
502
+ }
416
503
  async runTurn(promptText, { useInitialImages = false, onProgress = null, jsonSchema = null } = {}) {
417
504
  if (this.closeRequested || this.closed) {
418
505
  throw this.createSessionClosedError();
@@ -432,6 +519,10 @@ export class KimiPrintSession extends EventEmitter {
432
519
  effectivePrompt = injectJsonSchemaPrompt(promptWithSchema, jsonSchema);
433
520
  }
434
521
  }
522
+ if (this.cliMode === "prompt" && imagePaths.length > 0) {
523
+ const imageContext = imagePaths.map((item, index) => `${index + 1}. ${item}`).join("\n");
524
+ effectivePrompt = `${effectivePrompt || "Analyze the attached images."}\n\nAttached image files:\n${imageContext}`;
525
+ }
435
526
  if (!effectivePrompt && imagePaths.length === 0) {
436
527
  return buildEmptyTurnResult();
437
528
  }
@@ -443,6 +534,7 @@ export class KimiPrintSession extends EventEmitter {
443
534
  items: [],
444
535
  stderrTail: [],
445
536
  settled: false,
537
+ interruptRequested: false,
446
538
  };
447
539
  this.currentTurn = currentTurn;
448
540
  try {
@@ -451,8 +543,11 @@ export class KimiPrintSession extends EventEmitter {
451
543
  reply_in_progress: true,
452
544
  status_line: statusLineForPhase("turn_started"),
453
545
  }, onProgress);
454
- const args = this.buildPrintArgs();
455
- this.trace(`spawn ${[this.command, ...args].join(" ")}`);
546
+ const args = this.buildPrintArgs(effectivePrompt);
547
+ const argsForLog = this.cliMode === "prompt"
548
+ ? [...args.slice(0, -2), "--prompt", `<redacted:${effectivePrompt.length} chars>`]
549
+ : args;
550
+ this.trace(`spawn ${[this.command, ...argsForLog].join(" ")}`);
456
551
  const result = await new Promise((resolve, reject) => {
457
552
  const child = spawn(this.command, args, {
458
553
  cwd: this.cwd,
@@ -508,6 +603,13 @@ export class KimiPrintSession extends EventEmitter {
508
603
  }
509
604
  currentTurn.items.push(payload);
510
605
  const role = String(payload?.role || "").trim().toLowerCase();
606
+ if (role === "meta") {
607
+ const type = String(payload?.type || "").trim().toLowerCase();
608
+ if (type === "session.resume_hint") {
609
+ this.adoptSessionId(payload?.session_id);
610
+ }
611
+ return;
612
+ }
511
613
  if (role === "assistant") {
512
614
  const assistantText = normalizeTextContent(payload.content);
513
615
  if (Array.isArray(payload?.tool_calls) && payload.tool_calls.length > 0) {
@@ -552,11 +654,19 @@ export class KimiPrintSession extends EventEmitter {
552
654
  child.on("error", (error) => {
553
655
  settle(error);
554
656
  });
555
- child.on("exit", (code, signal) => {
657
+ child.on("close", (code, signal) => {
556
658
  if (this.closeRequested) {
557
659
  settle(this.createSessionClosedError());
558
660
  return;
559
661
  }
662
+ if (currentTurn.interruptRequested) {
663
+ settle(createTurnError("Kimi print turn interrupted", {
664
+ reason: "turn_interrupted",
665
+ code,
666
+ signal,
667
+ }));
668
+ return;
669
+ }
560
670
  if (code !== 0) {
561
671
  this.maybeEmitAuthRequired(currentTurn.stderrTail);
562
672
  const stderrSummary = sanitizeForLog(currentTurn.stderrTail.filter(Boolean).at(-1), 200);
@@ -574,8 +684,10 @@ export class KimiPrintSession extends EventEmitter {
574
684
  items: [...currentTurn.items],
575
685
  });
576
686
  });
577
- const userMessage = this.buildUserMessage(effectivePrompt, { useInitialImages });
578
- child.stdin.write(`${JSON.stringify(userMessage)}\n`);
687
+ if (this.cliMode === "legacy-print") {
688
+ const userMessage = this.buildUserMessage(effectivePrompt, { useInitialImages });
689
+ child.stdin.write(`${JSON.stringify(userMessage)}\n`);
690
+ }
579
691
  child.stdin.end();
580
692
  });
581
693
  if (result?.text) {
@@ -1,5 +1,5 @@
1
1
  export function buildCliArgs(sessionId: any): string[];
2
- export function findSessionPath(sessionId: any, options?: {}): Promise<string | null>;
2
+ export function findSessionPath(sessionId: any, options?: {}): Promise<any>;
3
3
  export function resolveResumeContext(sessionId: any, options?: {}): Promise<{
4
4
  provider: any;
5
5
  sessionId: any;
@@ -15,20 +15,20 @@ export async function findSessionPath(sessionId, options = {}) {
15
15
  if (!normalizedSessionId) {
16
16
  return null;
17
17
  }
18
- const homeDir = resolveHomeDir(options);
19
- const sessionsDir = options.kimiSessionsDir || path.join(homeDir, ".kimi", "sessions");
20
- return findKimiSessionDirectory(sessionsDir, normalizedSessionId);
18
+ const target = await findKimiSessionTarget(normalizedSessionId, options);
19
+ return target?.sessionPath || null;
21
20
  }
22
21
  export async function resolveResumeContext(sessionId, options = {}) {
23
22
  const normalizedSessionId = normalizeSessionId(sessionId);
24
23
  if (!normalizedSessionId) {
25
24
  throw new Error("--resume requires a session id");
26
25
  }
27
- const sessionPath = await findSessionPath(normalizedSessionId, options);
28
- if (!sessionPath) {
26
+ const target = await findKimiSessionTarget(normalizedSessionId, options);
27
+ if (!target?.sessionPath) {
29
28
  throw new Error(`Invalid --resume session id for kimi: ${normalizedSessionId}`);
30
29
  }
31
- const cwdFromSession = await resolveKimiResumeCwd(sessionPath, normalizedSessionId, options);
30
+ const sessionPath = target.sessionPath;
31
+ const cwdFromSession = target.cwd || (await resolveLegacyKimiResumeCwd(sessionPath, normalizedSessionId, options));
32
32
  if (!cwdFromSession) {
33
33
  throw new Error(`Could not resolve workspace for Kimi session ${normalizedSessionId}. Re-run from the original workspace or resume a session previously started by conductor fire.`);
34
34
  }
@@ -46,7 +46,7 @@ export async function resolveResumeContext(sessionId, options = {}) {
46
46
  export function md5Hex(value) {
47
47
  return crypto.createHash("md5").update(String(value ?? "")).digest("hex");
48
48
  }
49
- async function resolveKimiResumeCwd(sessionPath, _sessionId, options = {}) {
49
+ async function resolveLegacyKimiResumeCwd(sessionPath, _sessionId, options = {}) {
50
50
  const sessionDirectory = typeof sessionPath === "string" ? sessionPath.trim() : "";
51
51
  if (!sessionDirectory) {
52
52
  return null;
@@ -68,6 +68,111 @@ async function resolveKimiResumeCwd(sessionPath, _sessionId, options = {}) {
68
68
  }
69
69
  return null;
70
70
  }
71
+ async function findKimiSessionTarget(sessionId, options = {}) {
72
+ const homeDir = resolveHomeDir(options);
73
+ const kimiCodeHome = resolveKimiCodeHome(homeDir, options);
74
+ const kimiCodeTarget = await findKimiCodeSession(kimiCodeHome, sessionId);
75
+ if (kimiCodeTarget) {
76
+ return kimiCodeTarget;
77
+ }
78
+ const legacySessionsDir = options.kimiSessionsDir || path.join(homeDir, ".kimi", "sessions");
79
+ const legacySessionPath = await findKimiSessionDirectory(legacySessionsDir, sessionId);
80
+ return legacySessionPath
81
+ ? {
82
+ sessionPath: legacySessionPath,
83
+ cwd: null,
84
+ }
85
+ : null;
86
+ }
87
+ function resolveKimiCodeHome(homeDir, options = {}) {
88
+ const configured = options.kimiCodeHome ||
89
+ options.env?.KIMI_CODE_HOME ||
90
+ (!options.homeDir ? process.env.KIMI_CODE_HOME : "");
91
+ return typeof configured === "string" && configured.trim()
92
+ ? configured.trim()
93
+ : path.join(homeDir, ".kimi-code");
94
+ }
95
+ async function findKimiCodeSession(kimiCodeHome, sessionId) {
96
+ const sessionsDir = path.join(kimiCodeHome, "sessions");
97
+ const indexed = await findIndexedKimiCodeSession(kimiCodeHome, sessionsDir, sessionId);
98
+ if (indexed) {
99
+ return indexed;
100
+ }
101
+ const sessionPath = await findKimiSessionDirectory(sessionsDir, sessionId);
102
+ if (!sessionPath) {
103
+ return null;
104
+ }
105
+ return {
106
+ sessionPath,
107
+ cwd: await readKimiCodeSessionWorkDir(sessionPath),
108
+ };
109
+ }
110
+ async function findIndexedKimiCodeSession(kimiCodeHome, sessionsDir, sessionId) {
111
+ let raw = "";
112
+ try {
113
+ raw = await fsp.readFile(path.join(kimiCodeHome, "session_index.jsonl"), "utf8");
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ let matched = null;
119
+ for (const line of raw.split(/\r?\n/)) {
120
+ const trimmed = line.trim();
121
+ if (!trimmed) {
122
+ continue;
123
+ }
124
+ let record = null;
125
+ try {
126
+ record = JSON.parse(trimmed);
127
+ }
128
+ catch {
129
+ continue;
130
+ }
131
+ if (record?.sessionId !== sessionId) {
132
+ continue;
133
+ }
134
+ if (record?.deleted === true) {
135
+ matched = null;
136
+ continue;
137
+ }
138
+ const sessionDir = typeof record?.sessionDir === "string" ? record.sessionDir.trim() : "";
139
+ if (!sessionDir || !path.isAbsolute(sessionDir)) {
140
+ continue;
141
+ }
142
+ const relativePath = path.relative(path.resolve(sessionsDir), path.resolve(sessionDir));
143
+ if (!relativePath ||
144
+ relativePath === ".." ||
145
+ relativePath.startsWith(`..${path.sep}`) ||
146
+ path.isAbsolute(relativePath) ||
147
+ path.basename(sessionDir) !== sessionId) {
148
+ continue;
149
+ }
150
+ matched = {
151
+ sessionPath: sessionDir,
152
+ cwd: typeof record?.workDir === "string" ? record.workDir.trim() : "",
153
+ };
154
+ }
155
+ if (!matched || !(await pathExists(matched.sessionPath, "directory"))) {
156
+ return null;
157
+ }
158
+ return {
159
+ sessionPath: matched.sessionPath,
160
+ cwd: await readKimiCodeSessionWorkDir(matched.sessionPath, matched.cwd),
161
+ };
162
+ }
163
+ async function readKimiCodeSessionWorkDir(sessionPath, fallback = "") {
164
+ try {
165
+ const raw = await fsp.readFile(path.join(sessionPath, "state.json"), "utf8");
166
+ const state = JSON.parse(raw);
167
+ if (typeof state?.workDir === "string" && state.workDir.trim()) {
168
+ return state.workDir.trim();
169
+ }
170
+ }
171
+ catch {
172
+ // Fall back to the session index, which also records the working directory.
173
+ }
174
+ return typeof fallback === "string" && fallback.trim() ? fallback.trim() : null;
175
+ }
71
176
  async function findKimiSessionDirectory(rootDir, sessionId) {
72
177
  let hashDirs = [];
73
178
  try {
@@ -5,6 +5,7 @@ import { CodexExecSession } from "./providers/codex-exec-session.js";
5
5
  import { ClaudeAgentSdkSession } from "./providers/claude-agent-sdk-session.js";
6
6
  import { CopilotSdkSession } from "./providers/copilot-sdk-session.js";
7
7
  import { KimiCliSession } from "./providers/kimi-cli-session.js";
8
+ import { resolveKimiCliMode } from "./providers/kimi-cli-mode.js";
8
9
  import { KimiPrintSession } from "./providers/kimi-print-session.js";
9
10
  import { OpencodeSdkSession } from "./providers/opencode-sdk-session.js";
10
11
  import { getExternalProviderDescriptor, resolveExternalBackend, } from "./external-provider-registry.js";
@@ -36,9 +37,13 @@ const SESSION_FACTORIES_BY_BACKEND = new Map([
36
37
  ["copilot", (backend, options) => new CopilotSdkSession(backend, options)],
37
38
  [
38
39
  "kimi",
39
- (backend, options) => hasStructuredOutputPreference(options)
40
- ? new KimiPrintSession(backend, options)
41
- : new KimiCliSession(backend, options),
40
+ async (backend, options) => {
41
+ const kimiCliMode = await resolveKimiCliMode(options);
42
+ const resolvedOptions = { ...options, kimiCliMode };
43
+ return hasStructuredOutputPreference(options) || kimiCliMode === "prompt"
44
+ ? new KimiPrintSession(backend, resolvedOptions)
45
+ : new KimiCliSession(backend, resolvedOptions);
46
+ },
42
47
  ],
43
48
  ["opencode", (backend, options) => new OpencodeSdkSession(backend, options)],
44
49
  ["chat-web", (backend, options) => new ChatWebSession(backend, options)],
@@ -109,7 +114,7 @@ export async function createLocalAiSession(backend, options = {}) {
109
114
  const normalized = await assertSupportedBackend(backend, options);
110
115
  const factory = SESSION_FACTORIES_BY_BACKEND.get(normalized);
111
116
  if (factory) {
112
- return factory(normalized, options);
117
+ return await factory(normalized, options);
113
118
  }
114
119
  const descriptor = await getExternalProviderDescriptor(normalized, options);
115
120
  if (!descriptor) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/ai-sdk",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
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.3"
35
+ "@love-moon/chat-web": "^0.7.5"
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": "8081263"
42
+ "gitCommitId": "702c7c4"
43
43
  }