kimiflare 0.13.0 → 0.13.2

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/dist/index.js CHANGED
@@ -230,57 +230,87 @@ async function* parseStream(body, signal) {
230
230
  const toolCalls = /* @__PURE__ */ new Map();
231
231
  let lastUsage = null;
232
232
  let finishReason = null;
233
- for await (const dataStr of readSSE(body, signal)) {
234
- if (dataStr === "[DONE]") break;
235
- let chunk = null;
236
- try {
237
- chunk = JSON.parse(dataStr);
238
- } catch {
239
- continue;
240
- }
241
- if (!chunk) continue;
242
- if (chunk.usage) {
243
- lastUsage = chunk.usage;
244
- yield { type: "usage", usage: chunk.usage };
245
- }
246
- const choice = chunk.choices?.[0];
247
- if (!choice) continue;
248
- const d = choice.delta;
249
- if (d) {
250
- if (typeof d.reasoning_content === "string" && d.reasoning_content.length) {
251
- yield { type: "reasoning", delta: d.reasoning_content };
233
+ let timeoutId = null;
234
+ let timedOut = false;
235
+ const resetTimeout = () => {
236
+ if (timeoutId) clearTimeout(timeoutId);
237
+ timeoutId = setTimeout(() => {
238
+ timedOut = true;
239
+ }, STREAM_TIMEOUT_MS);
240
+ };
241
+ try {
242
+ for await (const dataStr of readSSE(body, signal)) {
243
+ if (timedOut) {
244
+ throw new KimiApiError(
245
+ `kimiflare: stream timed out (no data for ${STREAM_TIMEOUT_MS / 1e3}s)`,
246
+ void 0,
247
+ void 0
248
+ );
249
+ }
250
+ resetTimeout();
251
+ if (dataStr === "[DONE]") break;
252
+ let chunk = null;
253
+ try {
254
+ chunk = JSON.parse(dataStr);
255
+ } catch {
256
+ continue;
252
257
  }
253
- if (typeof d.content === "string" && d.content.length) {
254
- yield { type: "text", delta: d.content };
258
+ if (!chunk) continue;
259
+ if (chunk.usage) {
260
+ lastUsage = chunk.usage;
261
+ yield { type: "usage", usage: chunk.usage };
255
262
  }
256
- if (Array.isArray(d.tool_calls)) {
257
- for (const tc of d.tool_calls) {
258
- const idx = typeof tc.index === "number" ? tc.index : 0;
259
- let buf = toolCalls.get(idx);
260
- const incomingName = tc.function?.name ?? null;
261
- const incomingId = tc.id ?? null;
262
- if (!buf) {
263
- buf = { id: incomingId ?? `tc_${idx}`, name: incomingName ?? "", args: "" };
264
- toolCalls.set(idx, buf);
265
- if (buf.name) {
266
- yield { type: "tool_call_start", index: idx, id: buf.id, name: buf.name };
263
+ const choice = chunk.choices?.[0];
264
+ if (!choice) continue;
265
+ const d = choice.delta;
266
+ if (d) {
267
+ if (typeof d.reasoning_content === "string" && d.reasoning_content.length) {
268
+ yield { type: "reasoning", delta: d.reasoning_content };
269
+ }
270
+ if (typeof d.content === "string" && d.content.length) {
271
+ yield { type: "text", delta: d.content };
272
+ }
273
+ if (Array.isArray(d.tool_calls)) {
274
+ for (const tc of d.tool_calls) {
275
+ const idx = typeof tc.index === "number" ? tc.index : 0;
276
+ let buf = toolCalls.get(idx);
277
+ const incomingName = tc.function?.name ?? null;
278
+ const incomingId = tc.id ?? null;
279
+ if (!buf) {
280
+ buf = { id: incomingId ?? `tc_${idx}`, name: incomingName ?? "", args: "" };
281
+ toolCalls.set(idx, buf);
282
+ if (buf.name) {
283
+ yield { type: "tool_call_start", index: idx, id: buf.id, name: buf.name };
284
+ }
285
+ } else {
286
+ if (!buf.name && incomingName) {
287
+ buf.name = incomingName;
288
+ yield { type: "tool_call_start", index: idx, id: buf.id, name: buf.name };
289
+ }
290
+ if (buf.id.startsWith("tc_") && incomingId) buf.id = incomingId;
267
291
  }
268
- } else {
269
- if (!buf.name && incomingName) {
270
- buf.name = incomingName;
271
- yield { type: "tool_call_start", index: idx, id: buf.id, name: buf.name };
292
+ const argDelta = tc.function?.arguments;
293
+ if (typeof argDelta === "string" && argDelta.length) {
294
+ buf.args += argDelta;
295
+ yield { type: "tool_call_args", index: idx, argsDelta: argDelta };
272
296
  }
273
- if (buf.id.startsWith("tc_") && incomingId) buf.id = incomingId;
274
- }
275
- const argDelta = tc.function?.arguments;
276
- if (typeof argDelta === "string" && argDelta.length) {
277
- buf.args += argDelta;
278
- yield { type: "tool_call_args", index: idx, argsDelta: argDelta };
279
297
  }
280
298
  }
281
299
  }
300
+ if (choice.finish_reason) finishReason = choice.finish_reason;
301
+ }
302
+ for (const [idx, buf] of [...toolCalls.entries()].sort((a, b) => a[0] - b[0])) {
303
+ if (!buf.name) continue;
304
+ yield {
305
+ type: "tool_call_complete",
306
+ index: idx,
307
+ id: buf.id,
308
+ name: buf.name,
309
+ arguments: buf.args
310
+ };
282
311
  }
283
- if (choice.finish_reason) finishReason = choice.finish_reason;
312
+ } finally {
313
+ if (timeoutId) clearTimeout(timeoutId);
284
314
  }
285
315
  for (const [idx, buf] of [...toolCalls.entries()].sort((a, b) => a[0] - b[0])) {
286
316
  if (!buf.name) continue;
@@ -354,7 +384,7 @@ function sleep(ms, signal) {
354
384
  signal?.addEventListener("abort", onAbort, { once: true });
355
385
  });
356
386
  }
357
- var RETRYABLE_CODES, MAX_ATTEMPTS;
387
+ var RETRYABLE_CODES, MAX_ATTEMPTS, STREAM_TIMEOUT_MS;
358
388
  var init_client = __esm({
359
389
  "src/agent/client.ts"() {
360
390
  "use strict";
@@ -363,6 +393,7 @@ var init_client = __esm({
363
393
  init_messages();
364
394
  RETRYABLE_CODES = /* @__PURE__ */ new Set([3040]);
365
395
  MAX_ATTEMPTS = 5;
396
+ STREAM_TIMEOUT_MS = 6e4;
366
397
  }
367
398
  });
368
399
 
@@ -751,7 +782,7 @@ How to work:
751
782
  - You have a 262k-token context window. Read as much of a file as needed rather than guessing.
752
783
  - If a request is ambiguous, ask one focused question instead of making large assumptions.
753
784
  - When you finish a task, stop. Do not add a closing summary.
754
- - When creating git commits, you may include \`Co-authored-by: kimiflare <kimiflare@proton.me>\` in the commit message so kimiflare is credited as a contributor. The bash tool will also auto-append this trailer when it detects \`git commit\` commands.`;
785
+ - When creating git commits, you must include \`Co-authored-by: kimiflare <kimiflare@proton.me>\` in the commit message so kimiflare is credited as a contributor. The bash tool will also auto-append this trailer when it detects git commit-creating commands.`;
755
786
  const ctx = loadContextFile(opts2.cwd);
756
787
  const contextBlock = ctx ? `
757
788
 
@@ -946,6 +977,8 @@ var init_edit = __esm({
946
977
 
947
978
  // src/tools/bash.ts
948
979
  import { spawn } from "child_process";
980
+ import { tmpdir } from "os";
981
+ import { join as join3 } from "path";
949
982
  function formatBashTitle(raw) {
950
983
  let cmd = (raw ?? "").trim();
951
984
  const m = cmd.match(/^cd\s+([^\s&;]+)\s*(?:&&|;)\s*(.*)$/);
@@ -956,13 +989,28 @@ function injectCoauthor(command, coauthor) {
956
989
  if (!coauthor) return command;
957
990
  const trailer = `Co-authored-by: ${coauthor.name} <${coauthor.email}>`;
958
991
  const trimmed = command.trim();
959
- if (!/\bgit\s+commit\b/.test(trimmed)) return command;
960
992
  if (command.includes(trailer)) return command;
961
- if (/\b(--dry-run|-n)\b/.test(trimmed) && !/-m\b|--message\b/.test(trimmed)) return command;
962
- const tmpFile = `/tmp/kf-coauthor-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
963
- const check = `! git log -1 --pretty=%B 2>/dev/null | grep -qF "${trailer}"`;
964
- const append = `git log -1 --pretty=%B | git interpret-trailers --trailer "${trailer}" > "${tmpFile}" && git commit --amend -F "${tmpFile}" --no-edit && rm -f "${tmpFile}"`;
965
- return `(${command}) && ${check} && ${append}`;
993
+ const createsCommit = /\bgit\s+(commit|merge|revert|cherry-pick)\b/.test(trimmed);
994
+ const isRebaseContinue = /\bgit\s+rebase\b/.test(trimmed) && !/\b--abort\b|\b--skip\b/.test(trimmed);
995
+ const mentionsGit = /\bgit\b/.test(trimmed);
996
+ if (!createsCommit && !isRebaseContinue && !mentionsGit) return command;
997
+ const tmpFile = join3(tmpdir(), `kf-coauthor-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
998
+ const amendBlock = `
999
+ if ! git log -1 --pretty=%B 2>/dev/null | grep -qF "${trailer}"; then
1000
+ git log -1 --pretty=%B | git interpret-trailers --trailer "${trailer}" > "${tmpFile}" && git commit --amend -F "${tmpFile}" --no-edit && rm -f "${tmpFile}"
1001
+ fi
1002
+ `.trim();
1003
+ if (createsCommit || isRebaseContinue) {
1004
+ return `(${command}) && { ${amendBlock}; }`;
1005
+ }
1006
+ const beforeHead = `git rev-parse HEAD 2>/dev/null || echo "NO_HEAD"`;
1007
+ const afterCheck = `
1008
+ _KF_AFTER_HEAD=$(git rev-parse HEAD 2>/dev/null || echo "NO_HEAD")
1009
+ if [ "$_KF_BEFORE_HEAD" != "$_KF_AFTER_HEAD" ] && [ "$_KF_AFTER_HEAD" != "NO_HEAD" ]; then
1010
+ ${amendBlock}
1011
+ fi
1012
+ `.trim();
1013
+ return `_KF_BEFORE_HEAD=$(${beforeHead}); (${command}); _KF_EXIT=$?; [ $_KF_EXIT -eq 0 ] && { ${afterCheck}; }; exit $_KF_EXIT`;
966
1014
  }
967
1015
  function runBash(args, ctx) {
968
1016
  const timeout = Math.min(Math.max(1e3, args.timeout_ms ?? DEFAULT_TIMEOUT), MAX_TIMEOUT);
@@ -1406,16 +1454,16 @@ var init_executor = __esm({
1406
1454
  // src/util/update-check.ts
1407
1455
  import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir3, access } from "fs/promises";
1408
1456
  import { homedir as homedir4 } from "os";
1409
- import { join as join3, dirname as dirname2 } from "path";
1457
+ import { join as join4, dirname as dirname2 } from "path";
1410
1458
  import { fileURLToPath } from "url";
1411
1459
  function cachePath() {
1412
- const xdg = process.env.XDG_CONFIG_HOME || join3(homedir4(), ".config");
1413
- return join3(xdg, "kimiflare", "update-check.json");
1460
+ const xdg = process.env.XDG_CONFIG_HOME || join4(homedir4(), ".config");
1461
+ return join4(xdg, "kimiflare", "update-check.json");
1414
1462
  }
1415
1463
  async function findPackageJson(startDir) {
1416
1464
  let dir = startDir;
1417
1465
  while (true) {
1418
- const candidate = join3(dir, "package.json");
1466
+ const candidate = join4(dir, "package.json");
1419
1467
  try {
1420
1468
  const raw = await readFile6(candidate, "utf8");
1421
1469
  const parsed = JSON.parse(raw);
@@ -1499,7 +1547,7 @@ async function isGitRepo() {
1499
1547
  let dir = dirname2(fileURLToPath(import.meta.url));
1500
1548
  while (true) {
1501
1549
  try {
1502
- await access(join3(dir, ".git"));
1550
+ await access(join4(dir, ".git"));
1503
1551
  return true;
1504
1552
  } catch {
1505
1553
  }
@@ -3450,10 +3498,10 @@ var init_theme = __esm({
3450
3498
  // src/sessions.ts
3451
3499
  import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir4, readdir, stat as stat2 } from "fs/promises";
3452
3500
  import { homedir as homedir5 } from "os";
3453
- import { join as join4 } from "path";
3501
+ import { join as join5 } from "path";
3454
3502
  function sessionsDir() {
3455
- const xdg = process.env.XDG_DATA_HOME || join4(homedir5(), ".local", "share");
3456
- return join4(xdg, "kimiflare", "sessions");
3503
+ const xdg = process.env.XDG_DATA_HOME || join5(homedir5(), ".local", "share");
3504
+ return join5(xdg, "kimiflare", "sessions");
3457
3505
  }
3458
3506
  function sanitize(text) {
3459
3507
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
@@ -3466,7 +3514,7 @@ function makeSessionId(firstPrompt) {
3466
3514
  async function saveSession(file) {
3467
3515
  const dir = sessionsDir();
3468
3516
  await mkdir4(dir, { recursive: true });
3469
- const path = join4(dir, `${file.id}.json`);
3517
+ const path = join5(dir, `${file.id}.json`);
3470
3518
  await writeFile5(path, JSON.stringify(file, null, 2), "utf8");
3471
3519
  return path;
3472
3520
  }
@@ -3481,7 +3529,7 @@ async function listSessions(limit = 30) {
3481
3529
  const summaries = [];
3482
3530
  for (const name of entries) {
3483
3531
  if (!name.endsWith(".json")) continue;
3484
- const path = join4(dir, name);
3532
+ const path = join5(dir, name);
3485
3533
  try {
3486
3534
  const [s, raw] = await Promise.all([stat2(path), readFile7(path, "utf8")]);
3487
3535
  const parsed = JSON.parse(raw);
@@ -3558,7 +3606,7 @@ __export(app_exports, {
3558
3606
  import { useState as useState6, useRef as useRef3, useEffect as useEffect4, useCallback } from "react";
3559
3607
  import { Box as Box12, Text as Text13, useApp, useInput as useInput2, render } from "ink";
3560
3608
  import { existsSync } from "fs";
3561
- import { join as join5 } from "path";
3609
+ import { join as join6 } from "path";
3562
3610
  import { unlink } from "fs/promises";
3563
3611
  import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
3564
3612
  function capEvents(prev) {
@@ -3575,6 +3623,60 @@ function findImagePaths(text) {
3575
3623
  }
3576
3624
  return [...new Set(paths)];
3577
3625
  }
3626
+ function stripImagesFromHistory(messages) {
3627
+ for (const m of messages) {
3628
+ if (!Array.isArray(m.content)) continue;
3629
+ let changed = false;
3630
+ const next = [];
3631
+ for (const part of m.content) {
3632
+ if (part.type === "image_url") {
3633
+ changed = true;
3634
+ next.push({ type: "text", text: "[image]" });
3635
+ } else {
3636
+ next.push(part);
3637
+ }
3638
+ }
3639
+ if (changed) {
3640
+ m.content = next;
3641
+ }
3642
+ }
3643
+ }
3644
+ function truncateOldToolResults(messages, keepRecent) {
3645
+ const toolIndices = [];
3646
+ for (let i = 0; i < messages.length; i++) {
3647
+ if (messages[i].role === "tool") toolIndices.push(i);
3648
+ }
3649
+ const cutoff = toolIndices.length - keepRecent;
3650
+ for (let i = 0; i < cutoff; i++) {
3651
+ const idx = toolIndices[i];
3652
+ const m = messages[idx];
3653
+ const text = typeof m.content === "string" ? m.content : "";
3654
+ if (text.length > 500) {
3655
+ m.content = text.slice(0, 500) + "\n\u2026 [truncated]";
3656
+ }
3657
+ }
3658
+ }
3659
+ async function maybeAutoCompact(messages, cfg, onInfo) {
3660
+ const heapUsed = process.memoryUsage().heapUsed;
3661
+ if (heapUsed < HEAP_AUTO_COMPACT_THRESHOLD || messages.length <= MSG_AUTO_COMPACT_THRESHOLD) {
3662
+ return false;
3663
+ }
3664
+ onInfo("auto-compacting to reduce memory usage");
3665
+ const result = await compactMessages({
3666
+ accountId: cfg.accountId,
3667
+ apiToken: cfg.apiToken,
3668
+ model: cfg.model,
3669
+ messages,
3670
+ keepLastTurns: 2
3671
+ });
3672
+ if (result.replacedCount > 0) {
3673
+ messages.length = 0;
3674
+ messages.push(...result.newMessages);
3675
+ onInfo(`compacted ${result.replacedCount} messages into a summary`);
3676
+ return true;
3677
+ }
3678
+ return false;
3679
+ }
3578
3680
  function App({ initialCfg, initialUpdateResult }) {
3579
3681
  const { exit } = useApp();
3580
3682
  const [cfg, setCfg] = useState6(initialCfg);
@@ -3878,13 +3980,13 @@ function App({ initialCfg, initialUpdateResult }) {
3878
3980
  }
3879
3981
  const cwd = process.cwd();
3880
3982
  for (const name of ["KIMI.md", "KIMIFLARE.md", "AGENT.md"]) {
3881
- if (existsSync(join5(cwd, name))) {
3983
+ if (existsSync(join6(cwd, name))) {
3882
3984
  setEvents((e) => [
3883
3985
  ...e,
3884
3986
  {
3885
3987
  kind: "info",
3886
3988
  key: mkKey(),
3887
- text: `${name} already exists at ${join5(cwd, name)} \u2014 delete it first if you want to regenerate`
3989
+ text: `${name} already exists at ${join6(cwd, name)} \u2014 delete it first if you want to regenerate`
3888
3990
  }
3889
3991
  ]);
3890
3992
  return;
@@ -4002,7 +4104,14 @@ function App({ initialCfg, initialUpdateResult }) {
4002
4104
  })
4003
4105
  }
4004
4106
  });
4005
- if (existsSync(join5(cwd, "KIMI.md"))) {
4107
+ stripImagesFromHistory(messagesRef.current);
4108
+ truncateOldToolResults(messagesRef.current, 8);
4109
+ await maybeAutoCompact(
4110
+ messagesRef.current,
4111
+ cfg,
4112
+ (text) => setEvents((es) => [...es, { kind: "info", key: mkKey(), text }])
4113
+ );
4114
+ if (existsSync(join6(cwd, "KIMI.md"))) {
4006
4115
  messagesRef.current[0] = {
4007
4116
  role: "system",
4008
4117
  content: buildSystemPrompt({
@@ -4456,7 +4565,14 @@ use: /thinking low | medium | high`
4456
4565
  })
4457
4566
  }
4458
4567
  });
4459
- await saveSessionSafe();
4568
+ stripImagesFromHistory(messagesRef.current);
4569
+ truncateOldToolResults(messagesRef.current, 8);
4570
+ await maybeAutoCompact(
4571
+ messagesRef.current,
4572
+ cfg,
4573
+ (text2) => setEvents((es) => [...es, { kind: "info", key: mkKey(), text: text2 }])
4574
+ );
4575
+ void saveSessionSafe();
4460
4576
  } catch (e) {
4461
4577
  if (e.name === "AbortError") {
4462
4578
  setEvents((es) => [...es, { kind: "info", key: mkKey(), text: "(interrupted)" }]);
@@ -4652,7 +4768,7 @@ async function renderApp(cfg, updateResult) {
4652
4768
  const instance = render(/* @__PURE__ */ jsx13(App, { initialCfg: cfg, initialUpdateResult: updateResult }));
4653
4769
  await instance.waitUntilExit();
4654
4770
  }
4655
- var CONTEXT_LIMIT, AUTO_COMPACT_SUGGEST_PCT, MAX_EVENTS, nextAssistantId, nextKey, mkKey, MAX_IMAGES_PER_MESSAGE, EFFORT_DESCRIPTIONS;
4771
+ var CONTEXT_LIMIT, AUTO_COMPACT_SUGGEST_PCT, MAX_EVENTS, nextAssistantId, nextKey, mkKey, MAX_IMAGES_PER_MESSAGE, EFFORT_DESCRIPTIONS, HEAP_AUTO_COMPACT_THRESHOLD, MSG_AUTO_COMPACT_THRESHOLD;
4656
4772
  var init_app = __esm({
4657
4773
  "src/app.tsx"() {
4658
4774
  "use strict";
@@ -4689,6 +4805,8 @@ var init_app = __esm({
4689
4805
  medium: "medium \u2014 balanced (default). Solid quality on most edits, fast on trivial prompts.",
4690
4806
  high: "high \u2014 deepest reasoning; slowest. Best for complex debugging, architecture, multi-file refactors."
4691
4807
  };
4808
+ HEAP_AUTO_COMPACT_THRESHOLD = 2e9;
4809
+ MSG_AUTO_COMPACT_THRESHOLD = 50;
4692
4810
  }
4693
4811
  });
4694
4812
 
@@ -4701,11 +4819,11 @@ init_update_check();
4701
4819
  import { Command } from "commander";
4702
4820
  import { readFileSync as readFileSync2 } from "fs";
4703
4821
  import { fileURLToPath as fileURLToPath2 } from "url";
4704
- import { dirname as dirname3, join as join6 } from "path";
4822
+ import { dirname as dirname3, join as join7 } from "path";
4705
4823
  function readPackageVersion() {
4706
4824
  try {
4707
4825
  const here = dirname3(fileURLToPath2(import.meta.url));
4708
- const pkg = JSON.parse(readFileSync2(join6(here, "..", "package.json"), "utf8"));
4826
+ const pkg = JSON.parse(readFileSync2(join7(here, "..", "package.json"), "utf8"));
4709
4827
  return pkg.version ?? "0.0.0";
4710
4828
  } catch {
4711
4829
  return "0.0.0";