@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 +13 -0
- package/dist/manager/paths.d.ts +2 -0
- package/dist/manager/paths.js +3 -1
- package/dist/manager/quota/codex.d.ts +14 -6
- package/dist/manager/quota/codex.js +113 -105
- package/dist/manager/quota/kimi.js +22 -9
- package/dist/manager/types.d.ts +2 -3
- package/dist/providers/kimi-cli-mode.d.ts +1 -0
- package/dist/providers/kimi-cli-mode.js +115 -0
- package/dist/providers/kimi-print-session.d.ts +9 -1
- package/dist/providers/kimi-print-session.js +128 -16
- package/dist/resume/kimi.d.ts +1 -1
- package/dist/resume/kimi.js +112 -7
- package/dist/session-factory.js +9 -4
- package/package.json +3 -3
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
|
package/dist/manager/paths.d.ts
CHANGED
|
@@ -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;
|
package/dist/manager/paths.js
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
10
|
+
/** @deprecated Quota is account-scoped and no longer fetched through a model response. */
|
|
11
11
|
model?: string;
|
|
12
|
-
/**
|
|
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.
|
|
21
|
-
*
|
|
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 {
|
|
1
|
+
import { basename, dirname } from "node:path";
|
|
2
2
|
import { parseAuthFile } from "../auth-parser.js";
|
|
3
|
-
import { DEFAULT_CODEX_AUTH
|
|
4
|
-
import {
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
resetAt:
|
|
42
|
-
windowMinutes:
|
|
34
|
+
resetAt,
|
|
35
|
+
resetAfterSeconds: resetAt === undefined ? undefined : Math.max(0, resetAt - now),
|
|
36
|
+
windowMinutes: finiteNumber(value.windowDurationMins),
|
|
43
37
|
};
|
|
44
38
|
}
|
|
45
|
-
|
|
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:
|
|
48
|
-
activeLimit:
|
|
49
|
-
|
|
50
|
-
|
|
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:
|
|
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([
|
|
70
|
-
const file = cacheFile(
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 (
|
|
106
|
-
|
|
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 =
|
|
98
|
+
const parsed = parseCodexRateLimitsResponse(response, {
|
|
122
99
|
email: authInfo.email,
|
|
123
100
|
accountId: authInfo.accountId,
|
|
124
101
|
});
|
|
125
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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.
|
|
153
|
-
*
|
|
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([
|
|
163
|
-
const file = cacheFile(
|
|
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 {
|
|
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
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
23
|
-
return emptyQuota("unknown",
|
|
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);
|
package/dist/manager/types.d.ts
CHANGED
|
@@ -37,8 +37,7 @@ export interface CodexQuota {
|
|
|
37
37
|
tool: "codex";
|
|
38
38
|
plan?: string;
|
|
39
39
|
activeLimit?: string;
|
|
40
|
-
|
|
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,
|
|
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" ||
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
578
|
-
|
|
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) {
|
package/dist/resume/kimi.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function buildCliArgs(sessionId: any): string[];
|
|
2
|
-
export function findSessionPath(sessionId: any, options?: {}): Promise<
|
|
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;
|
package/dist/resume/kimi.js
CHANGED
|
@@ -15,20 +15,20 @@ export async function findSessionPath(sessionId, options = {}) {
|
|
|
15
15
|
if (!normalizedSessionId) {
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
18
|
-
const
|
|
19
|
-
|
|
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
|
|
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
|
|
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
|
|
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 {
|
package/dist/session-factory.js
CHANGED
|
@@ -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) =>
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
"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.
|
|
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": "
|
|
42
|
+
"gitCommitId": "702c7c4"
|
|
43
43
|
}
|