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.
@@ -1,5 +1,9 @@
1
1
  const { randomUUID } = require("crypto");
2
2
  const { loadConfig, defaultAgentModelForProvider, sameModelProvider } = require("../config");
3
+ const {
4
+ readKimiAccessToken,
5
+ resolveKimiUpstreamCredentials,
6
+ } = require("../agents/providers/credentials/kimi");
3
7
  const { runToolCall } = require("./dispatch");
4
8
  const { getReadToolDescription } = require("../agents/prompts/native/toolDescriptions/read");
5
9
  const { getWriteToolDescription } = require("../agents/prompts/native/toolDescriptions/write");
@@ -9,6 +13,8 @@ const { getBashToolDescription } = require("../agents/prompts/native/toolDescrip
9
13
  const CORE_TOOL_NAMES = new Set(["read", "write", "edit", "bash"]);
10
14
  const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
11
15
  const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
16
+ const DEFAULT_KIMI_BASE_URL = "https://api.kimi.com/coding/v1";
17
+ const DEFAULT_KIMI_MODEL = "k3";
12
18
  // Claude Code SDK defaults to no turn limit; built-in agents cap at 30 (DreamTask)
13
19
  // to 200 (fork). We count individual tool calls (not turns), so 100 leaves headroom
14
20
  // for non-trivial tasks while still catching runaway loops. Override via env.
@@ -107,6 +113,7 @@ function normalizeProvider(value = "") {
107
113
  if (!text) return "";
108
114
  if (text === "codex" || text === "codex-cli" || text === "codex-code") return "openai";
109
115
  if (text === "claude" || text === "claude-cli" || text === "claude-code") return "anthropic";
116
+ if (text === "kimi" || text === "kimi-code" || text === "moonshot") return "kimi";
110
117
  if (text === "openai" || text === "anthropic") return text;
111
118
  return text;
112
119
  }
@@ -116,6 +123,7 @@ function resolveTransport({ provider = "", baseUrl = "" } = {}) {
116
123
  const url = String(baseUrl || "").trim().toLowerCase();
117
124
 
118
125
  if (normalizedProvider === "anthropic") return "anthropic-messages";
126
+ if (normalizedProvider === "kimi") return "openai-chat";
119
127
  if (url.includes("anthropic.com")) return "anthropic-messages";
120
128
  if (/\/messages(?:$|[/?#])/.test(url) && !/\/chat\/completions(?:$|[/?#])/.test(url)) {
121
129
  return "anthropic-messages";
@@ -141,12 +149,14 @@ function resolveRuntimeConfig({ workspaceRoot = process.cwd(), provider = "", mo
141
149
  model
142
150
  || process.env.UFOO_UCODE_MODEL
143
151
  || configuredModel
144
- || defaultAgentModelForProvider(selectedProvider)
152
+ || (selectedProvider === "kimi" ? DEFAULT_KIMI_MODEL : defaultAgentModelForProvider(selectedProvider))
145
153
  ).trim();
146
154
 
147
155
  const defaultBaseUrl = selectedProvider === "anthropic"
148
156
  ? String(process.env.ANTHROPIC_BASE_URL || DEFAULT_ANTHROPIC_BASE_URL)
149
- : String(process.env.OPENAI_BASE_URL || DEFAULT_OPENAI_BASE_URL);
157
+ : selectedProvider === "kimi"
158
+ ? DEFAULT_KIMI_BASE_URL
159
+ : String(process.env.OPENAI_BASE_URL || DEFAULT_OPENAI_BASE_URL);
150
160
 
151
161
  const baseUrl = String(
152
162
  process.env.UFOO_UCODE_BASE_URL
@@ -154,19 +164,38 @@ function resolveRuntimeConfig({ workspaceRoot = process.cwd(), provider = "", mo
154
164
  || defaultBaseUrl
155
165
  ).trim();
156
166
 
157
- const apiKey = String(
167
+ const explicitApiKey = String(
158
168
  process.env.UFOO_UCODE_API_KEY
159
169
  || config.ucodeApiKey
160
- || (selectedProvider === "openai" ? process.env.OPENAI_API_KEY : "")
161
- || (selectedProvider === "anthropic" ? process.env.ANTHROPIC_API_KEY : "")
162
170
  || ""
163
171
  ).trim();
172
+ let apiKey = explicitApiKey;
173
+ let apiKeySource = explicitApiKey ? "explicit" : "";
174
+ let kimiCredentialState = "";
175
+ if (!apiKey && selectedProvider === "kimi") {
176
+ const credential = readKimiAccessToken({ env: process.env });
177
+ if (credential && credential.accessToken) {
178
+ apiKey = String(credential.accessToken).trim();
179
+ apiKeySource = "kimi-credential";
180
+ kimiCredentialState = String(credential.state || "");
181
+ }
182
+ }
183
+ if (!apiKey) {
184
+ apiKey = String(
185
+ (selectedProvider === "openai" ? process.env.OPENAI_API_KEY : "")
186
+ || (selectedProvider === "anthropic" ? process.env.ANTHROPIC_API_KEY : "")
187
+ || ""
188
+ ).trim();
189
+ if (apiKey) apiKeySource = "env";
190
+ }
164
191
 
165
192
  return {
166
193
  provider: selectedProvider,
167
194
  model: selectedModel,
168
195
  baseUrl,
169
196
  apiKey,
197
+ apiKeySource,
198
+ kimiCredentialState,
170
199
  transport: resolveTransport({ provider: selectedProvider, baseUrl }),
171
200
  };
172
201
  }
@@ -532,6 +561,7 @@ async function runOpenAiLikeTurn({
532
561
  url = "",
533
562
  apiKey = "",
534
563
  model = "",
564
+ provider = "",
535
565
  messages = [],
536
566
  onTextDelta = null,
537
567
  onThinkingDelta = null,
@@ -546,7 +576,8 @@ async function runOpenAiLikeTurn({
546
576
  tools: buildCoreToolSpecs(),
547
577
  tool_choice: "auto",
548
578
  stream: true,
549
- temperature: 0,
579
+ // Kimi k3 rejects any temperature other than 1.
580
+ temperature: normalizeProvider(provider) === "kimi" ? 1 : 0,
550
581
  };
551
582
 
552
583
  const headers = {
@@ -1075,6 +1106,7 @@ async function runNativeLoop({
1075
1106
  model = "",
1076
1107
  baseUrl = "",
1077
1108
  apiKey = "",
1109
+ provider = "",
1078
1110
  timeoutMs = 300000,
1079
1111
  onStreamDelta = null,
1080
1112
  onThinkingDelta = null,
@@ -1109,6 +1141,7 @@ async function runNativeLoop({
1109
1141
  url: requestUrl,
1110
1142
  apiKey,
1111
1143
  model: requestModel,
1144
+ provider,
1112
1145
  systemPrompt,
1113
1146
  messages,
1114
1147
  signal,
@@ -1235,6 +1268,23 @@ async function runNativeAgentTask({
1235
1268
  model,
1236
1269
  });
1237
1270
 
1271
+ // Kimi tokens expire; resolveRuntimeConfig reads the credential file
1272
+ // synchronously, so refresh it here (async) when the key came from that
1273
+ // file and the token is outside the fresh window.
1274
+ if (
1275
+ runtime.provider === "kimi"
1276
+ && runtime.apiKeySource === "kimi-credential"
1277
+ && runtime.kimiCredentialState !== "fresh"
1278
+ ) {
1279
+ try {
1280
+ const credential = await resolveKimiUpstreamCredentials({ env: process.env });
1281
+ const token = String(credential && credential.accessToken || "").trim();
1282
+ if (token) runtime.apiKey = token;
1283
+ } catch {
1284
+ // Keep the file token; the request itself will surface auth failures.
1285
+ }
1286
+ }
1287
+
1238
1288
  const transport = TRANSPORTS[runtime.transport] || TRANSPORTS["openai-chat"];
1239
1289
 
1240
1290
  const runResult = await runNativeLoop({
@@ -1246,6 +1296,7 @@ async function runNativeAgentTask({
1246
1296
  model: runtime.model,
1247
1297
  baseUrl: runtime.baseUrl,
1248
1298
  apiKey: runtime.apiKey,
1299
+ provider: runtime.provider,
1249
1300
  timeoutMs,
1250
1301
  onStreamDelta: trackingStreamDelta,
1251
1302
  onThinkingDelta,
package/src/config.js CHANGED
@@ -13,11 +13,15 @@ const SETTINGS_MODEL_DEFAULTS = Object.freeze({
13
13
  // command-line flag for model, so the value here is a placeholder for
14
14
  // display; we never pass it on the agy command line.
15
15
  agy: "",
16
+ // kimi reads its model from its own config.toml (default_model); ufoo
17
+ // never injects -m/--model, so keep the placeholder empty like agy.
18
+ kimi: "",
16
19
  }),
17
20
  router: Object.freeze({
18
21
  codex: "gpt-5.3-codex-spark",
19
22
  claude: "sonnet-4.7",
20
23
  agy: "",
24
+ kimi: "",
21
25
  }),
22
26
  });
23
27
 
@@ -57,6 +61,7 @@ function normalizeLaunchMode(value) {
57
61
  function normalizeAgentProvider(value) {
58
62
  if (value === "claude-cli") return "claude-cli";
59
63
  if (value === "agy-cli" || value === "agy" || value === "antigravity") return "agy-cli";
64
+ if (value === "kimi-cli" || value === "kimi") return "kimi-cli";
60
65
  return "codex-cli";
61
66
  }
62
67
 
@@ -64,6 +69,7 @@ function providerKey(value = "") {
64
69
  const text = String(value || "").trim().toLowerCase();
65
70
  if (text === "claude" || text === "claude-cli" || text === "claude-code" || text === "anthropic") return "claude";
66
71
  if (text === "agy" || text === "agy-cli" || text === "antigravity") return "agy";
72
+ if (text === "kimi" || text === "kimi-cli" || text === "kimi-code") return "kimi";
67
73
  return "codex";
68
74
  }
69
75
 
@@ -81,6 +87,8 @@ function defaultRouterProviderForAgentProvider(value = "") {
81
87
  // agy has no router-model API; fall back to codex (the controller still
82
88
  // routes via codex/claude regardless of which agent provider runs).
83
89
  if (key === "agy") return "codex";
90
+ // kimi likewise manages its own models; router falls back to codex.
91
+ if (key === "kimi") return "codex";
84
92
  return "codex";
85
93
  }
86
94
 
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- const ALLOWED_AGENT_TYPES = new Set(["auto", "codex", "claude", "ucode", "agy"]);
3
+ const ALLOWED_AGENT_TYPES = new Set(["auto", "codex", "claude", "ucode", "agy", "kimi"]);
4
4
  const { resolvePromptProfileReference } = require("./promptProfiles");
5
5
 
6
6
  function isPlainObject(value) {
@@ -310,6 +310,7 @@ function resolveAutoAgentType(projectRoot, requestedType) {
310
310
  if (provider === "claude-cli") return "claude";
311
311
  if (provider === "ucode") return "ucode";
312
312
  if (provider === "agy-cli") return "agy";
313
+ if (provider === "kimi-cli") return "kimi";
313
314
  return "codex";
314
315
  }
315
316
 
@@ -397,6 +398,8 @@ function buildExecutionPlan({
397
398
  // not via post-launch injection.
398
399
  : (resolvedType === "agy"
399
400
  ? "initial-prompt-arg"
401
+ // Kimi has no initial-prompt flag, so it falls through to
402
+ // post-launch PTY injection like any other future type.
400
403
  : "post-launch-inject"))));
401
404
  const bootstrapPrompt = bootstrapRequired
402
405
  ? composeGroupBootstrapPrompt({
@@ -923,6 +926,9 @@ function createGroupOrchestrator(options = {}) {
923
926
  // Agy uses the same "send bootstrap as a launch flag" model, but its
924
927
  // flag is `-i <text>` (alias for --prompt-interactive). Same strategy
925
928
  // key, different arg shape.
929
+ //
930
+ // Kimi never enters this branch: it has no initial-prompt flag, so its
931
+ // bootstrap strategy is post-launch-inject (handled further below).
926
932
  if (item.bootstrap_strategy === "initial-prompt-arg") {
927
933
  member.bootstrap_attempted_at = nowIso();
928
934
  member.bootstrap_error = "";
@@ -62,6 +62,7 @@ function normalizeBusAgentType(agentType = "") {
62
62
  if (value === "codex") return "codex";
63
63
  if (value === "claude" || value === "claude-code") return "claude-code";
64
64
  if (value === "agy" || value === "antigravity") return "agy";
65
+ if (value === "kimi" || value === "kimi-cli" || value === "kimi-code") return "kimi";
65
66
  if (value === "ufoo" || value === "ucode" || value === "ufoo-code") return "ufoo-code";
66
67
  return value;
67
68
  }
@@ -71,6 +72,7 @@ function normalizeLaunchAgent(agent = "") {
71
72
  if (value === "codex") return "codex";
72
73
  if (value === "claude" || value === "claude-code") return "claude";
73
74
  if (value === "agy" || value === "antigravity") return "agy";
75
+ if (value === "kimi" || value === "kimi-cli" || value === "kimi-code") return "kimi";
74
76
  if (value === "ufoo" || value === "ucode" || value === "ufoo-code") return "ufoo";
75
77
  return "";
76
78
  }
@@ -1685,9 +1687,9 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1685
1687
  : null,
1686
1688
  };
1687
1689
  let soloLaunchBootstrap = null;
1688
- if (requestedProfile && (normalizedAgent === "ufoo" || normalizedAgent === "claude" || normalizedAgent === "codex" || normalizedAgent === "agy")) {
1689
- const agentTypeMap = { ufoo: "ufoo-code", claude: "claude-code", codex: "codex", agy: "agy" };
1690
- const defaultNickMap = { ufoo: "ucode", claude: "claude", codex: "codex", agy: "agy" };
1690
+ if (requestedProfile && (normalizedAgent === "ufoo" || normalizedAgent === "claude" || normalizedAgent === "codex" || normalizedAgent === "agy" || normalizedAgent === "kimi")) {
1691
+ const agentTypeMap = { ufoo: "ufoo-code", claude: "claude-code", codex: "codex", agy: "agy", kimi: "kimi" };
1692
+ const defaultNickMap = { ufoo: "ucode", claude: "claude", codex: "codex", agy: "agy", kimi: "kimi" };
1691
1693
  const agentTypeForBootstrap = agentTypeMap[normalizedAgent];
1692
1694
  const soloNickname = explicitNickname || defaultNickMap[normalizedAgent];
1693
1695
  const profileResult = resolveSoloPromptProfile(projectRoot, requestedProfile);
@@ -1738,6 +1740,13 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1738
1740
  ...(Array.isArray(op.extra_args) ? op.extra_args : []),
1739
1741
  "-i", built.promptText,
1740
1742
  ];
1743
+ } else if (normalizedAgent === "kimi") {
1744
+ // kimi: no initial-prompt flag — deliver the bootstrap through
1745
+ // the launcher's post-launch PTY injection instead.
1746
+ op.extra_env = {
1747
+ ...(op.extra_env && typeof op.extra_env === "object" ? op.extra_env : {}),
1748
+ UFOO_STARTUP_BOOTSTRAP_TEXT: built.promptText,
1749
+ };
1741
1750
  }
1742
1751
  soloLaunchBootstrap = {
1743
1752
  requested_profile: profileResult.requested_profile,
@@ -22,6 +22,7 @@ function normalizeLaunchAgent(agent = "") {
22
22
  if (value === "codex") return "codex";
23
23
  if (value === "claude" || value === "claude-code") return "claude";
24
24
  if (value === "agy" || value === "antigravity") return "agy";
25
+ if (value === "kimi" || value === "kimi-cli" || value === "kimi-code") return "kimi";
25
26
  if (value === "ufoo" || value === "ucode" || value === "ufoo-code") return "ufoo";
26
27
  return "";
27
28
  }
@@ -30,6 +31,7 @@ function toBusAgentType(agent = "") {
30
31
  if (agent === "codex") return "codex";
31
32
  if (agent === "claude") return "claude-code";
32
33
  if (agent === "agy") return "agy";
34
+ if (agent === "kimi") return "kimi";
33
35
  if (agent === "ufoo") return "ufoo-code";
34
36
  return "";
35
37
  }
@@ -38,6 +40,7 @@ function toTerminalBinary(agent = "") {
38
40
  if (agent === "codex") return "ucodex";
39
41
  if (agent === "claude") return "uclaude";
40
42
  if (agent === "agy") return "uagy";
43
+ if (agent === "kimi") return "ukimi";
41
44
  if (agent === "ufoo") return "ucode";
42
45
  return "";
43
46
  }
@@ -46,6 +49,7 @@ function toTmuxBinary(agent = "") {
46
49
  if (agent === "codex") return "ucodex";
47
50
  if (agent === "claude") return "uclaude";
48
51
  if (agent === "agy") return "uagy";
52
+ if (agent === "kimi") return "ukimi";
49
53
  if (agent === "ufoo") return "ucode";
50
54
  return "";
51
55
  }
@@ -1100,6 +1104,8 @@ function buildResumeArgs(agent, sessionId) {
1100
1104
  // Agy resumes by conversation UUID; bin/uagy.js de-duplicates if the same
1101
1105
  // flag is already injected via provider_session_id readback.
1102
1106
  if (agent === "agy") return [`--conversation=${sessionId}`];
1107
+ // kimi resumes a session by id via `-S, --session <id>`.
1108
+ if (agent === "kimi") return ["--session", sessionId];
1103
1109
  return [];
1104
1110
  }
1105
1111
 
@@ -1152,7 +1158,7 @@ function collectRecoverableAgents(projectRoot, target = "") {
1152
1158
  continue;
1153
1159
  }
1154
1160
  const agent = normalizeAgentType(meta.agent_type);
1155
- if (agent !== "codex" && agent !== "claude" && agent !== "agy") {
1161
+ if (agent !== "codex" && agent !== "claude" && agent !== "agy" && agent !== "kimi") {
1156
1162
  skipped.push({ id, reason: "unsupported agent type" });
1157
1163
  continue;
1158
1164
  }
@@ -113,9 +113,44 @@ function resolveCodexSessionFromFile(cwd) {
113
113
  }
114
114
  }
115
115
 
116
+ /**
117
+ * Resolve Kimi Code session ID from the session index.
118
+ * Kimi writes $KIMI_CODE_HOME/session_index.jsonl (default ~/.kimi-code/)
119
+ * with one JSON line per session: { sessionId, sessionDir, workDir }.
120
+ * The file is append-only, so the last line matching the cwd is the most
121
+ * recent session for that directory.
122
+ */
123
+ function resolveKimiSessionFromIndex(cwd) {
124
+ if (!cwd) return null;
125
+ try {
126
+ const kimiHome = String(process.env.KIMI_CODE_HOME || "").trim()
127
+ || path.join(os.homedir(), ".kimi-code");
128
+ const indexPath = path.join(kimiHome, "session_index.jsonl");
129
+ if (!fs.existsSync(indexPath)) return null;
130
+ const lines = fs.readFileSync(indexPath, "utf8").split("\n");
131
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
132
+ const line = lines[i].trim();
133
+ if (!line) continue;
134
+ try {
135
+ const record = JSON.parse(line);
136
+ const sessionId = record.sessionId || record.session_id || "";
137
+ const workDir = record.workDir || record.work_dir || record.cwd || "";
138
+ if (sessionId && workDir === cwd) {
139
+ return { sessionId, source: indexPath };
140
+ }
141
+ } catch {
142
+ continue;
143
+ }
144
+ }
145
+ return null;
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+
116
151
  /**
117
152
  * Resolve provider session ID directly from session files.
118
- * @param {string} agentType - "claude-code" or "codex"
153
+ * @param {string} agentType - "claude-code", "codex" or "kimi"
119
154
  * @param {object} opts - { pid, cwd }
120
155
  */
121
156
  function resolveSessionFromFile(agentType, opts = {}) {
@@ -125,6 +160,9 @@ function resolveSessionFromFile(agentType, opts = {}) {
125
160
  if (agentType === "codex") {
126
161
  return resolveCodexSessionFromFile(opts.cwd);
127
162
  }
163
+ if (agentType === "kimi") {
164
+ return resolveKimiSessionFromIndex(opts.cwd);
165
+ }
128
166
  return null;
129
167
  }
130
168
 
@@ -169,7 +207,7 @@ function scheduleProviderSessionResolve({
169
207
  onResolved = null,
170
208
  }) {
171
209
  if (!subscriberId || !agentType) return null;
172
- if (agentType !== "codex" && agentType !== "claude-code") return null;
210
+ if (agentType !== "codex" && agentType !== "claude-code" && agentType !== "kimi") return null;
173
211
 
174
212
  let executed = false;
175
213
  let cancelled = false;
@@ -226,5 +264,6 @@ module.exports = {
226
264
  __private: {
227
265
  resolveClaudeSessionFromFile,
228
266
  resolveCodexSessionFromFile,
267
+ resolveKimiSessionFromIndex,
229
268
  },
230
269
  };
@@ -456,7 +456,7 @@ const LAUNCH_AGENT_SCHEMA = Object.freeze({
456
456
  properties: Object.freeze({
457
457
  agent: Object.freeze({
458
458
  type: "string",
459
- enum: Object.freeze(["codex", "claude", "ucode", "agy"]),
459
+ enum: Object.freeze(["codex", "claude", "ucode", "agy", "kimi"]),
460
460
  }),
461
461
  count: Object.freeze({ type: "integer", minimum: 1 }),
462
462
  nickname: Object.freeze({ type: "string" }),
@@ -16,9 +16,21 @@ const UCODE_BANNER_LINES = [
16
16
 
17
17
  const UCODE_VERSION = String((pkg && pkg.version) || "dev");
18
18
 
19
+ // Flying-saucer patrol for busy/loading states: the 🛸 drifts left-right
20
+ // inside a fixed 6-cell field (three emoji slots), so the status text after
21
+ // it stays anchored instead of shifting with the saucer.
22
+ const UFO_FIELD_CELLS = 6;
23
+ const UFO_FRAMES = [];
24
+ for (let i = 0; i <= UFO_FIELD_CELLS - 2; i += 1) {
25
+ UFO_FRAMES.push(`${" ".repeat(i)}🛸${" ".repeat(UFO_FIELD_CELLS - 2 - i)}`);
26
+ }
27
+ for (let i = UFO_FIELD_CELLS - 3; i > 0; i -= 1) {
28
+ UFO_FRAMES.push(`${" ".repeat(i)}🛸${" ".repeat(UFO_FIELD_CELLS - 2 - i)}`);
29
+ }
30
+
19
31
  const STATUS_INDICATORS = {
20
- thinking: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
21
- typing: ["◐", "◓", "◑", "◒"],
32
+ thinking: UFO_FRAMES,
33
+ typing: UFO_FRAMES,
22
34
  waiting: ["∙", "∙∙", "∙∙∙", "∙∙", "∙"],
23
35
  };
24
36