u-foo 2.5.8 → 2.5.9

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/README.md CHANGED
@@ -61,6 +61,7 @@ Installed binaries:
61
61
  | `uclaude` | Claude Code wrapper with ufoo bootstrap and bus identity. |
62
62
  | `ucodex` | Codex wrapper with ufoo bootstrap and bus identity. |
63
63
  | `uagy` | Antigravity wrapper with ufoo bootstrap and bus identity. |
64
+ | `ukimi` | Kimi Code wrapper with ufoo bootstrap and bus identity. |
64
65
  | `ucode` | Native ufoo coding-agent CLI/TUI. |
65
66
 
66
67
  ## Quick Start
@@ -88,6 +89,7 @@ Or launch wrappers directly inside a project:
88
89
  uclaude
89
90
  ucodex
90
91
  uagy
92
+ ukimi
91
93
  ucode
92
94
  ```
93
95
 
package/README.zh-CN.md CHANGED
@@ -59,6 +59,7 @@ npm link
59
59
  | `uclaude` | Claude Code 包装器,注入 ufoo bootstrap 和 bus 身份。 |
60
60
  | `ucodex` | Codex 包装器,注入 ufoo bootstrap 和 bus 身份。 |
61
61
  | `uagy` | Antigravity 包装器,注入 ufoo bootstrap 和 bus 身份。 |
62
+ | `ukimi` | Kimi Code 包装器,注入 ufoo bootstrap 和 bus 身份。 |
62
63
  | `ucode` | 原生 ufoo coding-agent CLI/TUI。 |
63
64
 
64
65
  ## 快速开始
@@ -86,6 +87,7 @@ ufoo
86
87
  uclaude
87
88
  ucodex
88
89
  uagy
90
+ ukimi
89
91
  ucode
90
92
  ```
91
93
 
@@ -155,7 +157,7 @@ ufoo -g
155
157
  /open /path/to/project
156
158
  ```
157
159
 
158
- `uclaude`、`ucodex`、`uagy`、`ucode` 这些直接包装器仍然可用,但 ufoo 的
160
+ `uclaude`、`ucodex`、`uagy`、`ukimi`、`ucode` 这些直接包装器仍然可用,但 ufoo 的
159
161
  主要工作流是在 chat 里完成。
160
162
 
161
163
  ### 初始化与维护
package/bin/ufoo.js CHANGED
@@ -143,6 +143,9 @@ async function main() {
143
143
  } else if (agentType === "codex") {
144
144
  scriptName = "ucodex.js";
145
145
  displayName = "ucodex";
146
+ } else if (agentType === "kimi") {
147
+ scriptName = "ukimi.js";
148
+ displayName = "ukimi";
146
149
  } else {
147
150
  console.error(`Error: Unable to determine agent type for ${subscriberId}`);
148
151
  process.exitCode = 1;
@@ -182,11 +185,13 @@ async function main() {
182
185
  scriptName = "uclaude.js";
183
186
  } else if (targetLower === "ucodex" || targetLower === "codex" || targetLower === "openai") {
184
187
  scriptName = "ucodex.js";
188
+ } else if (targetLower === "ukimi" || targetLower === "kimi" || targetLower === "kimi-cli" || targetLower === "kimi-code") {
189
+ scriptName = "ukimi.js";
185
190
  } else {
186
191
  // Not a valid agent type - might be an offline agent nickname
187
192
  console.error(`Error: Agent '${target}' is not online and is not a valid agent type`);
188
193
  console.error("");
189
- console.error("Valid agent types: ucode, uclaude, ucodex");
194
+ console.error("Valid agent types: ucode, uclaude, ucodex, ukimi");
190
195
  console.error("");
191
196
  console.error("To see online agents, run: ufoo bus status");
192
197
  process.exitCode = 1;
package/bin/ukimi.js ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ukimi: Launch Kimi Code CLI (kimi) and auto-join event bus
4
+ *
5
+ * Usage: ukimi [kimi args...]
6
+ *
7
+ * Differences vs uclaude:
8
+ * - kimi has no --append-system-prompt style flag, so the default
9
+ * bootstrap is delivered via post-launch PTY injection (same model as
10
+ * ucodex's UFOO_STARTUP_BOOTSTRAP_TEXT path).
11
+ * - Session resume uses kimi's own session ids via `--session <id>`.
12
+ */
13
+
14
+ const AgentLauncher = require("../src/agents/launch/launcher");
15
+ const { resolveDefaultManualBootstrap } = require("../src/agents/prompts/defaultBootstrap");
16
+
17
+ function extractUfooParamsFromArgs(args = []) {
18
+ const nextArgs = [];
19
+ let nickname = "";
20
+ let role = "";
21
+ for (let i = 0; i < args.length; i += 1) {
22
+ const arg = String(args[i] || "");
23
+ if (arg === "--nickname") {
24
+ if (i + 1 < args.length) {
25
+ nickname = String(args[i + 1]).trim();
26
+ i += 1;
27
+ }
28
+ continue;
29
+ }
30
+ if (arg.startsWith("--nickname=")) {
31
+ nickname = arg.slice("--nickname=".length).trim();
32
+ continue;
33
+ }
34
+ if (arg === "--role") {
35
+ if (i + 1 < args.length) {
36
+ role = String(args[i + 1]).trim();
37
+ i += 1;
38
+ }
39
+ continue;
40
+ }
41
+ if (arg.startsWith("--role=")) {
42
+ role = arg.slice("--role=".length).trim();
43
+ continue;
44
+ }
45
+ nextArgs.push(args[i]);
46
+ }
47
+ return { args: nextArgs, nickname, role };
48
+ }
49
+
50
+ const { args: cleanArgs, nickname, role } = extractUfooParamsFromArgs(process.argv.slice(2));
51
+ if (nickname) {
52
+ process.env.UFOO_NICKNAME = nickname;
53
+ }
54
+ if (role) {
55
+ process.env.UFOO_PROMPT_PROFILE = role;
56
+ }
57
+
58
+ const launcher = new AgentLauncher("kimi", "kimi");
59
+ const resolved = resolveDefaultManualBootstrap({
60
+ projectRoot: process.cwd(),
61
+ agentType: "kimi",
62
+ args: cleanArgs,
63
+ env: process.env,
64
+ });
65
+
66
+ for (const [key, value] of Object.entries(resolved.env || {})) {
67
+ process.env[key] = String(value);
68
+ }
69
+
70
+ launcher.launch(resolved.args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.5.8",
3
+ "version": "2.5.9",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
@@ -20,6 +20,7 @@
20
20
  "uclaude": "bin/uclaude.js",
21
21
  "ucodex": "bin/ucodex.js",
22
22
  "uagy": "bin/uagy.js",
23
+ "ukimi": "bin/ukimi.js",
23
24
  "ucode": "bin/ucode.js"
24
25
  },
25
26
  "files": [
@@ -53,6 +53,13 @@ const INPUT_PATTERNS = {
53
53
  /Yes, and run (?:in|without) sandbox/i, // Terminal command approval (sandbox toggle)
54
54
  /\bn\b\s*-\s*Don't run/i, // y/n/edit confirmation
55
55
  ],
56
+ // kimi (Kimi Code CLI) ink-style approval menus (verified against 0.27.0).
57
+ kimi: [
58
+ /Run this command\?/, // Bash command approval question
59
+ /Approve once[\s\S]*Approve for this session/, // Approval option list
60
+ /Reject with feedback/, // Approval option (reject path)
61
+ /↑\/↓ select · .*choose · ↵ confirm/, // Interactive menu navigation bar (option count varies)
62
+ ],
56
63
  };
57
64
 
58
65
  // Agent-specific FATAL patterns that immediately move the agent into BLOCKED
@@ -209,8 +209,9 @@ function computeInjectedSubmitDelayMs(agentType, text) {
209
209
  const normalizedAgent = String(agentType || "").trim().toLowerCase();
210
210
  const input = typeof text === "string" ? text : "";
211
211
  // Agy uses an ink-style TUI like claude-code, so it needs a similar grace
212
- // window before the input handler picks up injected text.
213
- const isInkStyle = normalizedAgent === "claude-code" || normalizedAgent === "agy";
212
+ // window before the input handler picks up injected text. Kimi Code is an
213
+ // Ink TUI as well and gets the same treatment.
214
+ const isInkStyle = normalizedAgent === "claude-code" || normalizedAgent === "agy" || normalizedAgent === "kimi";
214
215
  let delayMs = isInkStyle ? 350 : 200;
215
216
  if (input.includes("\n")) {
216
217
  delayMs += isInkStyle ? 250 : 120;
@@ -244,10 +245,10 @@ async function injectPtyCommand(wrapper, agentType, commandText, source = "injec
244
245
  const normalizedAgentType = String(agentType || "").trim().toLowerCase();
245
246
  const submitDelayMs = computeInjectedSubmitDelayMs(agentType, text);
246
247
  wrapper.write(text);
247
- // claude-code and agy both run ink-style TUIs that accept a bare CR to
248
- // submit. codex needs the Esc-prefix trick to flush its multi-byte input
248
+ // claude-code, agy and kimi all run ink-style TUIs that accept a bare CR
249
+ // to submit. codex needs the Esc-prefix trick to flush its multi-byte input
249
250
  // handler before the CR.
250
- const isInkStyle = normalizedAgentType === "claude-code" || normalizedAgentType === "agy";
251
+ const isInkStyle = normalizedAgentType === "claude-code" || normalizedAgentType === "agy" || normalizedAgentType === "kimi";
251
252
  if (isInkStyle) {
252
253
  await sleep(submitDelayMs);
253
254
  wrapper.write("\r");
@@ -108,6 +108,25 @@ class ReadyDetector {
108
108
  return false;
109
109
  }
110
110
 
111
+ /**
112
+ * Detect kimi (Kimi Code CLI) ready markers.
113
+ *
114
+ * kimi renders an ink-style TUI (verified against 0.27.0 via PTY capture):
115
+ * - A status footer "... @: mention files" that only appears once the
116
+ * prompt box is mounted (most reliable signal — post-boot only).
117
+ * - The welcome banner "Welcome to Kimi Code!" as fallback, in case the
118
+ * statusline render changes (e.g. narrow terminal).
119
+ */
120
+ _detectKimiReady(text) {
121
+ if (text.includes("@: mention files")) {
122
+ return true;
123
+ }
124
+ if (text.includes("Welcome to Kimi Code!")) {
125
+ return true;
126
+ }
127
+ return false;
128
+ }
129
+
111
130
  /**
112
131
  * 检测ufoo-code/ucode的ready标记
113
132
  */
@@ -158,6 +177,8 @@ class ReadyDetector {
158
177
  isReady = this._detectCodexReady(this.buffer);
159
178
  } else if (this.agentType === "agy") {
160
179
  isReady = this._detectAgyReady(this.buffer);
180
+ } else if (this.agentType === "kimi") {
181
+ isReady = this._detectKimiReady(this.buffer);
161
182
  } else if (this.agentType === "ufoo" || this.agentType === "ucode" || this.agentType === "ufoo-code") {
162
183
  isReady = this._detectUfooCodeReady(this.buffer);
163
184
  }
@@ -58,7 +58,9 @@ function buildDefaultStartupBootstrapPrompt({ agentType = "", projectRoot = "" }
58
58
  ? "Codex"
59
59
  : (normalizedAgent === "ufoo-code"
60
60
  ? "ucode"
61
- : (normalizedAgent === "agy" ? "Agy" : "agent")));
61
+ : (normalizedAgent === "agy"
62
+ ? "Agy"
63
+ : (normalizedAgent === "kimi" ? "Kimi" : "agent"))));
62
64
 
63
65
  const segments = [
64
66
  `Session bootstrap for ${displayAgent}.`,
@@ -253,11 +255,11 @@ function resolveDefaultManualBootstrap({
253
255
  const normalizedAgent = asTrimmedString(agentType).toLowerCase();
254
256
  const currentEnv = env && typeof env === "object" ? env : {};
255
257
  const currentArgs = Array.isArray(args) ? args.slice() : [];
256
- const hasCodexStartupBootstrap = normalizedAgent === "codex"
258
+ const hasStartupBootstrap = (normalizedAgent === "codex" || normalizedAgent === "kimi")
257
259
  && Boolean(currentEnv.UFOO_STARTUP_BOOTSTRAP_TEXT);
258
260
  if (
259
261
  currentEnv.UFOO_SKIP_DEFAULT_BOOTSTRAP === "1"
260
- || hasCodexStartupBootstrap
262
+ || hasStartupBootstrap
261
263
  || hasMetaCommandArgs(currentArgs)
262
264
  ) {
263
265
  return { args: currentArgs, env: {}, mode: "skip" };
@@ -328,6 +330,21 @@ function resolveDefaultManualBootstrap({
328
330
  };
329
331
  }
330
332
 
333
+ if (normalizedAgent === "kimi") {
334
+ // kimi has no --append-system-prompt or positional initial-prompt flag,
335
+ // so the bootstrap always goes out via post-launch PTY injection (the
336
+ // launcher reads UFOO_STARTUP_BOOTSTRAP_TEXT, same as the codex path).
337
+ const promptText = buildDefaultStartupBootstrapPrompt({ agentType: normalizedAgent, projectRoot });
338
+ return {
339
+ args: currentArgs,
340
+ env: {
341
+ UFOO_STARTUP_BOOTSTRAP_TEXT: promptText,
342
+ },
343
+ mode: "post-launch-inject",
344
+ promptText,
345
+ };
346
+ }
347
+
331
348
  if (normalizedAgent === "agy") {
332
349
  const promptText = buildDefaultStartupBootstrapPrompt({ agentType: normalizedAgent, projectRoot });
333
350
  // If the user passed -i / --prompt-interactive, fold the bootstrap into
@@ -104,3 +104,10 @@ module.exports = {
104
104
  toLegacyResolvedAuth,
105
105
  buildUpstreamAuthFromCredential,
106
106
  };
107
+
108
+ // Attached lazily after the shared helpers so the circular require from
109
+ // ./kimi (which pulls buildCredentialDescriptor from here) always resolves.
110
+ Object.defineProperty(module.exports, "kimi", {
111
+ enumerable: true,
112
+ get: () => require("./kimi"),
113
+ });
@@ -0,0 +1,342 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const path = require("path");
6
+ const {
7
+ buildCredentialDescriptor,
8
+ } = require("./index");
9
+
10
+ const KIMI_OAUTH_TOKEN_URL = "https://auth.kimi.com/api/oauth/token";
11
+ const KIMI_OAUTH_CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
12
+ const DEFAULT_REFRESH_WINDOW_MS = 300 * 1000;
13
+ const DEFAULT_LOCK_TIMEOUT_MS = 3000;
14
+ const DEFAULT_LOCK_RETRY_MS = 25;
15
+ const DEFAULT_STALE_LOCK_MS = 30 * 1000;
16
+
17
+ function sleep(ms) {
18
+ return new Promise((resolve) => setTimeout(resolve, ms));
19
+ }
20
+
21
+ function defaultKimiHome(env = process.env) {
22
+ const configured = String(env.KIMI_CODE_HOME || "").trim();
23
+ if (configured) return configured;
24
+ return path.join(os.homedir(), ".kimi-code");
25
+ }
26
+
27
+ function resolveKimiCredentialPaths(options = {}) {
28
+ const env = options.env || process.env;
29
+ const home = String(options.home || options.configDir || defaultKimiHome(env)).trim() || defaultKimiHome(env);
30
+ const explicitCredentialPath = String(options.credentialPath || "").trim();
31
+ const credentialPath = explicitCredentialPath || path.join(home, "credentials", "kimi-code.json");
32
+ return {
33
+ home,
34
+ credentialPath,
35
+ lockPath: `${credentialPath}.lock`,
36
+ };
37
+ }
38
+
39
+ function firstString(...values) {
40
+ for (const value of values) {
41
+ if (typeof value === "string" && value.trim()) return value.trim();
42
+ }
43
+ return "";
44
+ }
45
+
46
+ // kimi CLI stores expires_at as Unix epoch seconds (e.g. 1784486687); accept
47
+ // epoch millis and ISO strings as well so hand-rolled fixtures keep working.
48
+ function parseKimiExpiresAtMs(value) {
49
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
50
+ return value > 1e12 ? Math.floor(value) : Math.floor(value * 1000);
51
+ }
52
+ const text = String(value || "").trim();
53
+ if (!text) return NaN;
54
+ if (/^\d+(?:\.\d+)?$/.test(text)) {
55
+ const num = Number(text);
56
+ return num > 1e12 ? Math.floor(num) : Math.floor(num * 1000);
57
+ }
58
+ return Date.parse(text);
59
+ }
60
+
61
+ function parseKimiCredentialFile(raw = {}) {
62
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
63
+ const err = new Error("Kimi credential payload must be a JSON object");
64
+ err.code = "KIMI_AUTH_INVALID";
65
+ throw err;
66
+ }
67
+
68
+ const accessToken = firstString(raw.access_token, raw.accessToken);
69
+ const refreshToken = firstString(raw.refresh_token, raw.refreshToken);
70
+ if (!accessToken && !refreshToken) {
71
+ const err = new Error("Unsupported Kimi credential schema");
72
+ err.code = "KIMI_AUTH_SCHEMA_UNSUPPORTED";
73
+ throw err;
74
+ }
75
+
76
+ const expiresAtMs = parseKimiExpiresAtMs(
77
+ raw.expires_at !== undefined ? raw.expires_at : raw.expiresAt
78
+ );
79
+
80
+ return {
81
+ schemaVersion: "kimi-code-credentials-v1",
82
+ raw,
83
+ accessToken,
84
+ refreshToken,
85
+ tokenType: firstString(raw.token_type, raw.tokenType, "Bearer"),
86
+ scope: firstString(raw.scope),
87
+ expiresAt: Number.isFinite(expiresAtMs) ? new Date(expiresAtMs).toISOString() : "",
88
+ expiresAtMs,
89
+ };
90
+ }
91
+
92
+ async function withLockFile(lockPath, options = {}, fn) {
93
+ const fsModule = options.fsModule || fs;
94
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : DEFAULT_LOCK_TIMEOUT_MS;
95
+ const retryMs = Number.isFinite(options.retryMs) ? options.retryMs : DEFAULT_LOCK_RETRY_MS;
96
+ const staleMs = Number.isFinite(options.staleMs) ? options.staleMs : DEFAULT_STALE_LOCK_MS;
97
+ const sleepFn = typeof options.sleep === "function" ? options.sleep : sleep;
98
+ const startedAt = Date.now();
99
+
100
+ fsModule.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 0o700 });
101
+
102
+ while ((Date.now() - startedAt) <= timeoutMs) {
103
+ let fd = null;
104
+ try {
105
+ fd = fsModule.openSync(lockPath, "wx", 0o600);
106
+ try {
107
+ return await fn();
108
+ } finally {
109
+ try { fsModule.closeSync(fd); } catch {}
110
+ try { fsModule.unlinkSync(lockPath); } catch {}
111
+ }
112
+ } catch (err) {
113
+ if (!err || err.code !== "EEXIST") throw err;
114
+ try {
115
+ const stat = fsModule.statSync(lockPath);
116
+ if ((Date.now() - stat.mtimeMs) > staleMs) {
117
+ fsModule.unlinkSync(lockPath);
118
+ continue;
119
+ }
120
+ } catch {}
121
+ // eslint-disable-next-line no-await-in-loop
122
+ await sleepFn(retryMs);
123
+ }
124
+ }
125
+
126
+ const err = new Error(`Timed out waiting for Kimi OAuth lock: ${lockPath}`);
127
+ err.code = "KIMI_AUTH_LOCK_TIMEOUT";
128
+ throw err;
129
+ }
130
+
131
+ class KimiUpstreamCredentialResolver {
132
+ constructor(options = {}) {
133
+ this.fs = options.fsModule || fs;
134
+ this.env = options.env || process.env;
135
+ this.fetchImpl = options.fetchImpl || global.fetch;
136
+ this.now = typeof options.now === "function" ? options.now : () => Date.now();
137
+ this.paths = resolveKimiCredentialPaths(options);
138
+ this.refreshWindowMs = Number.isFinite(options.refreshWindowMs) ? options.refreshWindowMs : DEFAULT_REFRESH_WINDOW_MS;
139
+ this.autoRefresh = options.autoRefresh !== false;
140
+ this.refreshRetries = Number.isInteger(options.refreshRetries) && options.refreshRetries > 0
141
+ ? options.refreshRetries
142
+ : 2;
143
+ this.sleep = typeof options.sleep === "function" ? options.sleep : sleep;
144
+ this.lockTimeoutMs = Number.isFinite(options.lockTimeoutMs) ? options.lockTimeoutMs : DEFAULT_LOCK_TIMEOUT_MS;
145
+ this.lockRetryMs = Number.isFinite(options.lockRetryMs) ? options.lockRetryMs : DEFAULT_LOCK_RETRY_MS;
146
+ this.lockStaleMs = Number.isFinite(options.lockStaleMs) ? options.lockStaleMs : DEFAULT_STALE_LOCK_MS;
147
+ }
148
+
149
+ resolvePaths() {
150
+ return { ...this.paths };
151
+ }
152
+
153
+ readCredentialFile() {
154
+ const raw = JSON.parse(this.fs.readFileSync(this.paths.credentialPath, "utf8"));
155
+ return parseKimiCredentialFile(raw);
156
+ }
157
+
158
+ writeCredentialFile(raw) {
159
+ const text = `${JSON.stringify(raw, null, 2)}\n`;
160
+ const tmpPath = `${this.paths.credentialPath}.tmp-${process.pid}-${Date.now()}`;
161
+ this.fs.mkdirSync(path.dirname(this.paths.credentialPath), { recursive: true, mode: 0o700 });
162
+ this.fs.writeFileSync(tmpPath, text, { encoding: "utf8", mode: 0o600 });
163
+ this.fs.renameSync(tmpPath, this.paths.credentialPath);
164
+ try { this.fs.chmodSync(this.paths.credentialPath, 0o600); } catch {}
165
+ }
166
+
167
+ async refreshTokens(refreshToken) {
168
+ if (typeof this.fetchImpl !== "function") {
169
+ const err = new Error("fetch is unavailable for Kimi token refresh");
170
+ err.code = "KIMI_AUTH_REFRESH_UNAVAILABLE";
171
+ throw err;
172
+ }
173
+
174
+ let lastErr = null;
175
+ for (let attempt = 0; attempt < this.refreshRetries; attempt += 1) {
176
+ try {
177
+ const body = new URLSearchParams({
178
+ client_id: KIMI_OAUTH_CLIENT_ID,
179
+ grant_type: "refresh_token",
180
+ refresh_token: refreshToken,
181
+ });
182
+ const response = await this.fetchImpl(KIMI_OAUTH_TOKEN_URL, {
183
+ method: "POST",
184
+ headers: {
185
+ "content-type": "application/x-www-form-urlencoded",
186
+ accept: "application/json",
187
+ },
188
+ body: body.toString(),
189
+ });
190
+ const text = await response.text();
191
+ if (!response.ok) {
192
+ const err = new Error(`Kimi token refresh failed (${response.status}): ${text.slice(0, 500)}`);
193
+ err.code = "KIMI_AUTH_REFRESH_FAILED";
194
+ err.status = response.status;
195
+ throw err;
196
+ }
197
+ const payload = JSON.parse(text);
198
+ if (!payload || typeof payload !== "object" || !payload.access_token) {
199
+ const err = new Error("Kimi token refresh response did not include access_token");
200
+ err.code = "KIMI_AUTH_REFRESH_SCHEMA_UNSUPPORTED";
201
+ throw err;
202
+ }
203
+ const expiresIn = Number(payload.expires_in);
204
+ const expiresAtMs = Number.isFinite(expiresIn) && expiresIn > 0
205
+ ? this.now() + Math.floor(expiresIn * 1000)
206
+ : NaN;
207
+ return {
208
+ accessToken: firstString(payload.access_token),
209
+ refreshToken: firstString(payload.refresh_token, refreshToken),
210
+ tokenType: firstString(payload.token_type, "Bearer"),
211
+ scope: firstString(payload.scope),
212
+ expiresAt: Number.isFinite(expiresAtMs) ? new Date(expiresAtMs).toISOString() : "",
213
+ expiresAtMs,
214
+ };
215
+ } catch (err) {
216
+ lastErr = err;
217
+ if (err && err.code === "KIMI_AUTH_REFRESH_SCHEMA_UNSUPPORTED") {
218
+ throw err;
219
+ }
220
+ }
221
+ }
222
+
223
+ const err = new Error(lastErr && lastErr.message ? lastErr.message : "Kimi token refresh failed");
224
+ err.code = lastErr && lastErr.code ? lastErr.code : "KIMI_AUTH_REFRESH_FAILED";
225
+ err.cause = lastErr;
226
+ throw err;
227
+ }
228
+
229
+ async refreshCredentialRecord(record) {
230
+ if (!record || !record.refreshToken) {
231
+ const err = new Error("Kimi OAuth credential is expired and has no refresh token");
232
+ err.code = "KIMI_AUTH_REFRESH_UNAVAILABLE";
233
+ throw err;
234
+ }
235
+
236
+ return withLockFile(this.paths.lockPath, {
237
+ fsModule: this.fs,
238
+ timeoutMs: this.lockTimeoutMs,
239
+ retryMs: this.lockRetryMs,
240
+ staleMs: this.lockStaleMs,
241
+ sleep: this.sleep,
242
+ }, async () => {
243
+ const currentRecord = this.readCredentialFile();
244
+ const currentDescriptor = this.buildResolvedCredential(currentRecord);
245
+ if (currentDescriptor.state === "fresh") {
246
+ return currentRecord;
247
+ }
248
+ if (!currentRecord.refreshToken) {
249
+ const err = new Error("Kimi OAuth credential is expired and has no refresh token");
250
+ err.code = "KIMI_AUTH_REFRESH_UNAVAILABLE";
251
+ throw err;
252
+ }
253
+
254
+ const refreshed = await this.refreshTokens(currentRecord.refreshToken);
255
+ const nextRaw = currentRecord.raw && typeof currentRecord.raw === "object" && !Array.isArray(currentRecord.raw)
256
+ ? { ...currentRecord.raw }
257
+ : {};
258
+ nextRaw.access_token = refreshed.accessToken;
259
+ nextRaw.refresh_token = refreshed.refreshToken || currentRecord.refreshToken || "";
260
+ if (refreshed.tokenType) nextRaw.token_type = refreshed.tokenType;
261
+ if (refreshed.scope) nextRaw.scope = refreshed.scope;
262
+ if (Number.isFinite(refreshed.expiresAtMs)) {
263
+ nextRaw.expires_at = Math.floor(refreshed.expiresAtMs / 1000);
264
+ }
265
+ this.writeCredentialFile(nextRaw);
266
+ return this.readCredentialFile();
267
+ });
268
+ }
269
+
270
+ buildResolvedCredential(record) {
271
+ return buildCredentialDescriptor({
272
+ provider: "kimi",
273
+ credentialKind: "oauth",
274
+ source: "credential-file",
275
+ accessToken: record.accessToken,
276
+ refreshToken: record.refreshToken,
277
+ tokenType: record.tokenType || "Bearer",
278
+ expiresAt: record.expiresAt,
279
+ expiresAtMs: record.expiresAtMs,
280
+ refreshable: Boolean(record.refreshToken),
281
+ credentialPath: this.paths.credentialPath,
282
+ schemaVersion: record.schemaVersion,
283
+ nowMs: this.now(),
284
+ refreshWindowMs: this.refreshWindowMs,
285
+ metadata: {
286
+ scope: record.scope,
287
+ },
288
+ });
289
+ }
290
+
291
+ async resolveCredentials() {
292
+ let record;
293
+ try {
294
+ record = this.readCredentialFile();
295
+ } catch (err) {
296
+ if (err && err.code === "ENOENT") {
297
+ const missing = new Error("Kimi credential file not found; run `kimi` once to sign in");
298
+ missing.code = "KIMI_AUTH_UNAVAILABLE";
299
+ throw missing;
300
+ }
301
+ throw err;
302
+ }
303
+ const descriptor = this.buildResolvedCredential(record);
304
+ if (
305
+ this.autoRefresh
306
+ && descriptor.refreshable
307
+ && (descriptor.state === "expired" || descriptor.state === "near_expiry")
308
+ ) {
309
+ const refreshedRecord = await this.refreshCredentialRecord(record);
310
+ return this.buildResolvedCredential(refreshedRecord);
311
+ }
312
+ return descriptor;
313
+ }
314
+ }
315
+
316
+ async function resolveKimiUpstreamCredentials(options = {}) {
317
+ const resolver = new KimiUpstreamCredentialResolver(options);
318
+ return resolver.resolveCredentials();
319
+ }
320
+
321
+ // Synchronous, network-free read used by resolveRuntimeConfig: returns the
322
+ // credential descriptor (fresh or not) or null when the file is unusable.
323
+ function readKimiAccessToken(options = {}) {
324
+ try {
325
+ const resolver = new KimiUpstreamCredentialResolver({ ...options, autoRefresh: false });
326
+ const record = resolver.readCredentialFile();
327
+ return resolver.buildResolvedCredential(record);
328
+ } catch {
329
+ return null;
330
+ }
331
+ }
332
+
333
+ module.exports = {
334
+ KimiUpstreamCredentialResolver,
335
+ resolveKimiUpstreamCredentials,
336
+ resolveKimiCredentialPaths,
337
+ parseKimiCredentialFile,
338
+ parseKimiExpiresAtMs,
339
+ readKimiAccessToken,
340
+ KIMI_OAUTH_TOKEN_URL,
341
+ KIMI_OAUTH_CLIENT_ID,
342
+ };