@love-moon/ai-sdk 0.7.3 → 0.7.4

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,12 @@
1
1
  # @love-moon/ai-sdk
2
2
 
3
+ ## 0.7.4
4
+
5
+ ### Patch Changes
6
+
7
+ - d5eca1c: Adapt Codex quota parsing to the 0.144 weekly-only limit while retaining compatibility with legacy header layouts and cached snapshots.
8
+ - d5eca1c: Support Kimi Code CLI prompt sessions, current session storage, and credential locations while retaining legacy Kimi wire compatibility.
9
+
3
10
  ## 0.7.3
4
11
 
5
12
  ### 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");
@@ -14,6 +14,11 @@ export interface GetCodexQuotaOptions {
14
14
  /** Override cache directory (mainly for tests). */
15
15
  cacheDir?: string;
16
16
  }
17
+ /** Exported for protocol compatibility tests. */
18
+ export declare function parseCodexHeaders(map: Record<string, string>, extras: {
19
+ email?: string;
20
+ accountId?: string;
21
+ }): Omit<CodexQuota, "tool" | "fetchedAt" | "source">;
17
22
  export declare function getCodexQuota(opts?: GetCodexQuotaOptions): Promise<CodexQuota>;
18
23
  /**
19
24
  * Read the on-disk quota cache for a given codex auth.json without triggering
@@ -33,6 +33,15 @@ const RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
33
33
  const DEFAULT_TTL = 60; // seconds
34
34
  const DEFAULT_TIMEOUT_MS = 15000;
35
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)) {
43
+ return undefined;
44
+ }
36
45
  const usedPercent = num(map, `${prefix}-used-percent`) ?? 0;
37
46
  return {
38
47
  usedPercent,
@@ -42,12 +51,19 @@ function windowFromHeaders(map, prefix) {
42
51
  windowMinutes: num(map, `${prefix}-window-minutes`),
43
52
  };
44
53
  }
45
- function parseCodexHeaders(map, extras) {
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 };
46
63
  return {
47
64
  plan: str(map, "x-codex-plan-type"),
48
65
  activeLimit: str(map, "x-codex-active-limit"),
49
- fiveHour: windowFromHeaders(map, "x-codex-primary"),
50
- weekly: windowFromHeaders(map, "x-codex-secondary"),
66
+ weekly,
51
67
  credits: {
52
68
  hasCredits: bool(map, "x-codex-credits-has-credits") ?? false,
53
69
  balance: str(map, "x-codex-credits-balance"),
@@ -71,7 +87,7 @@ export async function getCodexQuota(opts = {}) {
71
87
  if (!opts.forceRefresh && ttl > 0) {
72
88
  const cached = await readCache(file);
73
89
  if (isFresh(cached, ttl) && cached) {
74
- return { ...cached.value, source: "cached" };
90
+ return { ...withoutLegacyFiveHour(cached.value), source: "cached" };
75
91
  }
76
92
  }
77
93
  const model = opts.model ??
@@ -140,7 +156,7 @@ async function fallbackFromCache(file, ttl, error) {
140
156
  const cached = await readCache(file);
141
157
  if (cached) {
142
158
  return {
143
- ...cached.value,
159
+ ...withoutLegacyFiveHour(cached.value),
144
160
  source: isFresh(cached, ttl) ? "cached" : "stale",
145
161
  error,
146
162
  };
@@ -164,19 +180,26 @@ export async function readCachedCodexQuota(codexAuthPath, opts = {}) {
164
180
  const entry = await readCache(file);
165
181
  if (!entry)
166
182
  return null;
167
- return { ...entry.value, source: "cached", fetchedAt: entry.fetchedAt };
183
+ return {
184
+ ...withoutLegacyFiveHour(entry.value),
185
+ source: "cached",
186
+ fetchedAt: entry.fetchedAt,
187
+ };
168
188
  }
169
189
  catch {
170
190
  return null;
171
191
  }
172
192
  }
193
+ function withoutLegacyFiveHour(quota) {
194
+ const { fiveHour: _legacyFiveHour, ...current } = quota;
195
+ return current;
196
+ }
173
197
  function emptyQuota(source, error) {
174
198
  return {
175
199
  tool: "codex",
176
200
  source,
177
201
  error,
178
202
  fetchedAt: Math.floor(Date.now() / 1000),
179
- fiveHour: { usedPercent: 0, remainingPercent: 0 },
180
203
  weekly: { usedPercent: 0, remainingPercent: 0 },
181
204
  };
182
205
  }
@@ -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,7 +37,8 @@ export interface CodexQuota {
37
37
  tool: "codex";
38
38
  plan?: string;
39
39
  activeLimit?: string;
40
- fiveHour: QuotaWindow;
40
+ /** @deprecated Codex CLI 0.144+ no longer exposes a 5h limit. */
41
+ fiveHour?: QuotaWindow;
41
42
  weekly: QuotaWindow;
42
43
  credits?: {
43
44
  hasCredits: boolean;
@@ -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.4",
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.4"
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": "b9a27b4"
43
43
  }