kimiflare 0.15.0 → 0.16.0

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
@@ -37,6 +37,8 @@ async function loadConfig() {
37
37
  const envCoauthor = readCoauthorEnv();
38
38
  const envCacheStable = process.env.KIMIFLARE_CACHE_STABLE_PROMPTS;
39
39
  const cacheStablePrompts = envCacheStable === "0" || envCacheStable === "false" ? false : true;
40
+ const envCompiled = process.env.KIMIFLARE_COMPILED_CONTEXT;
41
+ const compiledContext = envCompiled === "1" || envCompiled === "true" ? true : false;
40
42
  if (envAccount && envToken) {
41
43
  return {
42
44
  accountId: envAccount,
@@ -47,7 +49,8 @@ async function loadConfig() {
47
49
  coauthor: envCoauthor?.enabled ?? true,
48
50
  coauthorName: envCoauthor?.name,
49
51
  coauthorEmail: envCoauthor?.email,
50
- cacheStablePrompts
52
+ cacheStablePrompts,
53
+ compiledContext
51
54
  };
52
55
  }
53
56
  try {
@@ -64,7 +67,8 @@ async function loadConfig() {
64
67
  coauthorName: envCoauthor?.name ?? parsed.coauthorName,
65
68
  coauthorEmail: envCoauthor?.email ?? parsed.coauthorEmail,
66
69
  mcpServers: parsed.mcpServers,
67
- cacheStablePrompts: parsed.cacheStablePrompts ?? cacheStablePrompts
70
+ cacheStablePrompts: parsed.cacheStablePrompts ?? cacheStablePrompts,
71
+ compiledContext: parsed.compiledContext ?? compiledContext
68
72
  };
69
73
  }
70
74
  } catch {
@@ -406,16 +410,107 @@ var init_registry = __esm({
406
410
  }
407
411
  });
408
412
 
413
+ // src/storage-limits.ts
414
+ import { readdir, stat, unlink } from "fs/promises";
415
+ import { join as join2 } from "path";
416
+ async function listFilesByMtime(dir, pattern = /.*/) {
417
+ let entries;
418
+ try {
419
+ entries = await readdir(dir);
420
+ } catch {
421
+ return [];
422
+ }
423
+ const files = [];
424
+ for (const name of entries) {
425
+ if (!pattern.test(name)) continue;
426
+ const p = join2(dir, name);
427
+ try {
428
+ const s = await stat(p);
429
+ if (s.isFile()) files.push({ path: p, mtime: s.mtime });
430
+ } catch {
431
+ }
432
+ }
433
+ files.sort((a, b) => b.mtime < a.mtime ? -1 : 1);
434
+ return files;
435
+ }
436
+ async function pruneFiles(files, maxAgeDays, maxCount) {
437
+ const cutoff = new Date(Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3);
438
+ let removed = 0;
439
+ for (const f of files) {
440
+ if (f.mtime < cutoff) {
441
+ try {
442
+ await unlink(f.path);
443
+ removed++;
444
+ } catch {
445
+ }
446
+ }
447
+ }
448
+ const remaining = files.filter((f) => {
449
+ return f.mtime >= cutoff;
450
+ });
451
+ if (remaining.length > maxCount) {
452
+ const toDelete = remaining.slice(maxCount);
453
+ for (const f of toDelete) {
454
+ try {
455
+ await unlink(f.path);
456
+ removed++;
457
+ } catch {
458
+ }
459
+ }
460
+ }
461
+ return removed;
462
+ }
463
+ async function rotateJsonl(path, maxBytes, rotations) {
464
+ const { rename } = await import("fs/promises");
465
+ let s;
466
+ try {
467
+ s = await stat(path);
468
+ } catch {
469
+ return;
470
+ }
471
+ if (s.size <= maxBytes) return;
472
+ for (let i = rotations - 1; i >= 1; i--) {
473
+ const src = i === 1 ? path : `${path}.${i - 1}`;
474
+ const dst = `${path}.${i}`;
475
+ try {
476
+ await rename(src, dst);
477
+ } catch {
478
+ }
479
+ }
480
+ }
481
+ var RETENTION;
482
+ var init_storage_limits = __esm({
483
+ "src/storage-limits.ts"() {
484
+ "use strict";
485
+ RETENTION = {
486
+ /** Session files older than this (days) are pruned. */
487
+ sessionMaxAgeDays: 30,
488
+ /** Max number of session files to keep. */
489
+ sessionMaxCount: 100,
490
+ /** Usage log day entries older than this (days) are pruned. */
491
+ usageDayMaxAgeDays: 90,
492
+ /** Usage log session entries older than this (days) are pruned. */
493
+ usageSessionMaxAgeDays: 30,
494
+ /** Max number of session entries in usage log. */
495
+ usageSessionMaxCount: 200,
496
+ /** Max size of cost-debug JSONL before rotation (bytes). */
497
+ costDebugMaxBytes: 5 * 1024 * 1024,
498
+ /** Number of rotated cost-debug files to keep. */
499
+ costDebugRotations: 2
500
+ };
501
+ }
502
+ });
503
+
409
504
  // src/cost-debug.ts
410
505
  import { appendFile, mkdir as mkdir2 } from "fs/promises";
411
506
  import { homedir as homedir2 } from "os";
412
- import { join as join2 } from "path";
507
+ import { join as join3 } from "path";
413
508
  function debugDir() {
414
- const xdg = process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share");
415
- return join2(xdg, "kimiflare");
509
+ const xdg = process.env.XDG_DATA_HOME || join3(homedir2(), ".local", "share");
510
+ return join3(xdg, "kimiflare");
416
511
  }
417
512
  function debugPath() {
418
- return join2(debugDir(), "cost-debug.jsonl");
513
+ return join3(debugDir(), "cost-debug.jsonl");
419
514
  }
420
515
  function now() {
421
516
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -471,6 +566,7 @@ function buildToolStats(results) {
471
566
  }
472
567
  async function logCostDebug(entry) {
473
568
  await mkdir2(debugDir(), { recursive: true });
569
+ await rotateJsonl(debugPath(), RETENTION.costDebugMaxBytes, RETENTION.costDebugRotations);
474
570
  await appendFile(debugPath(), JSON.stringify(entry) + "\n", "utf8");
475
571
  }
476
572
  function serializePrefix(messages) {
@@ -556,13 +652,15 @@ async function logTurnDebug(ctx) {
556
652
  toolTotalRawBytes: toolTotalRaw,
557
653
  toolTotalReducedBytes: toolTotalReduced,
558
654
  toolSavingsPct: toolTotalRaw > 0 ? Math.round((toolTotalRaw - toolTotalReduced) / toolTotalRaw * 100) : 0,
559
- cacheDiagnostics
655
+ cacheDiagnostics,
656
+ compaction: ctx.compaction
560
657
  });
561
658
  }
562
659
  var LOG_VERSION;
563
660
  var init_cost_debug = __esm({
564
661
  "src/cost-debug.ts"() {
565
662
  "use strict";
663
+ init_storage_limits();
566
664
  LOG_VERSION = 1;
567
665
  }
568
666
  });
@@ -923,11 +1021,11 @@ var init_mode = __esm({
923
1021
 
924
1022
  // src/agent/system-prompt.ts
925
1023
  import { platform, release, homedir as homedir3 } from "os";
926
- import { basename, join as join3 } from "path";
1024
+ import { basename, join as join4 } from "path";
927
1025
  import { readFileSync, statSync } from "fs";
928
1026
  function loadContextFile(cwd) {
929
1027
  for (const name of CONTEXT_FILENAMES) {
930
- const path = join3(cwd, name);
1028
+ const path = join4(cwd, name);
931
1029
  try {
932
1030
  const s = statSync(path);
933
1031
  if (!s.isFile() || s.size > MAX_CONTEXT_BYTES) continue;
@@ -1037,7 +1135,7 @@ var init_paths = __esm({
1037
1135
  });
1038
1136
 
1039
1137
  // src/tools/read.ts
1040
- import { readFile as readFile2, stat } from "fs/promises";
1138
+ import { readFile as readFile2, stat as stat2 } from "fs/promises";
1041
1139
  var MAX_BYTES, readTool;
1042
1140
  var init_read = __esm({
1043
1141
  "src/tools/read.ts"() {
@@ -1061,7 +1159,7 @@ var init_read = __esm({
1061
1159
  render: ({ path }) => ({ title: `read ${collapsePath(path, process.cwd())}` }),
1062
1160
  async run(args, ctx) {
1063
1161
  const abs = resolvePath(ctx.cwd, args.path);
1064
- const st = await stat(abs);
1162
+ const st = await stat2(abs);
1065
1163
  if (st.size > MAX_BYTES) throw new Error(`file too large: ${st.size} bytes (max ${MAX_BYTES})`);
1066
1164
  const text = await readFile2(abs, "utf8");
1067
1165
  const lines = text.split("\n");
@@ -1173,7 +1271,7 @@ var init_edit = __esm({
1173
1271
  // src/tools/bash.ts
1174
1272
  import { spawn } from "child_process";
1175
1273
  import { tmpdir } from "os";
1176
- import { join as join4 } from "path";
1274
+ import { join as join5 } from "path";
1177
1275
  function formatBashTitle(raw) {
1178
1276
  let cmd = (raw ?? "").trim();
1179
1277
  const m = cmd.match(/^cd\s+([^\s&;]+)\s*(?:&&|;)\s*(.*)$/);
@@ -1189,7 +1287,7 @@ function injectCoauthor(command, coauthor) {
1189
1287
  const isRebaseContinue = /\bgit\s+rebase\b/.test(trimmed) && !/\b--abort\b|\b--skip\b/.test(trimmed);
1190
1288
  const mentionsGit = /\bgit\b/.test(trimmed);
1191
1289
  if (!createsCommit && !isRebaseContinue && !mentionsGit) return command;
1192
- const tmpFile = join4(tmpdir(), `kf-coauthor-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
1290
+ const tmpFile = join5(tmpdir(), `kf-coauthor-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
1193
1291
  const amendBlock = `
1194
1292
  if ! git log -1 --pretty=%B 2>/dev/null | grep -qF "${trailer}"; then
1195
1293
  git log -1 --pretty=%B | git interpret-trailers --trailer "${trailer}" > "${tmpFile}" && git commit --amend -F "${tmpFile}" --no-edit && rm -f "${tmpFile}"
@@ -1700,16 +1798,16 @@ var init_executor = __esm({
1700
1798
  // src/util/update-check.ts
1701
1799
  import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
1702
1800
  import { homedir as homedir5 } from "os";
1703
- import { join as join5, dirname as dirname2 } from "path";
1801
+ import { join as join6, dirname as dirname2 } from "path";
1704
1802
  import { fileURLToPath } from "url";
1705
1803
  function cachePath() {
1706
- const xdg = process.env.XDG_CONFIG_HOME || join5(homedir5(), ".config");
1707
- return join5(xdg, "kimiflare", "update-check.json");
1804
+ const xdg = process.env.XDG_CONFIG_HOME || join6(homedir5(), ".config");
1805
+ return join6(xdg, "kimiflare", "update-check.json");
1708
1806
  }
1709
1807
  async function findPackageJson(startDir) {
1710
1808
  let dir = startDir;
1711
1809
  while (true) {
1712
- const candidate = join5(dir, "package.json");
1810
+ const candidate = join6(dir, "package.json");
1713
1811
  try {
1714
1812
  const raw = await readFile6(candidate, "utf8");
1715
1813
  const parsed = JSON.parse(raw);
@@ -1882,6 +1980,413 @@ Do not include speculation. Do not include chat-style pleasantries. Use short bu
1882
1980
  }
1883
1981
  });
1884
1982
 
1983
+ // src/agent/session-state.ts
1984
+ function emptySessionState(task = "") {
1985
+ return {
1986
+ task,
1987
+ user_constraints: [],
1988
+ repo_facts: [],
1989
+ files_touched: [],
1990
+ files_modified: [],
1991
+ confirmed_findings: [],
1992
+ open_questions: [],
1993
+ recent_failures: [],
1994
+ decisions: [],
1995
+ next_actions: [],
1996
+ artifact_index: {}
1997
+ };
1998
+ }
1999
+ function formatRecalledArtifacts(recalled) {
2000
+ if (recalled.length === 0) return "";
2001
+ const lines = ["[recalled artifacts]"];
2002
+ for (const { id, artifact } of recalled) {
2003
+ lines.push(`--- artifact:${id} (${artifact.type} from ${artifact.source}) ---`);
2004
+ lines.push(artifact.raw);
2005
+ }
2006
+ return lines.join("\n");
2007
+ }
2008
+ function serializeSessionState(state) {
2009
+ const lines = [];
2010
+ lines.push(`task: ${state.task || "(none)"}`);
2011
+ if (state.user_constraints.length) lines.push(`constraints:
2012
+ ${state.user_constraints.map((c) => " - " + c).join("\n")}`);
2013
+ if (state.repo_facts.length) lines.push(`repo_facts:
2014
+ ${state.repo_facts.map((f) => " - " + f).join("\n")}`);
2015
+ if (state.files_touched.length) lines.push(`files_touched: ${state.files_touched.join(", ")}`);
2016
+ if (state.files_modified.length) lines.push(`files_modified: ${state.files_modified.join(", ")}`);
2017
+ if (state.confirmed_findings.length) lines.push(`findings:
2018
+ ${state.confirmed_findings.map((f) => " - " + f).join("\n")}`);
2019
+ if (state.open_questions.length) lines.push(`open_questions:
2020
+ ${state.open_questions.map((q) => " - " + q).join("\n")}`);
2021
+ if (state.recent_failures.length) lines.push(`recent_failures:
2022
+ ${state.recent_failures.map((f) => " - " + f).join("\n")}`);
2023
+ if (state.decisions.length) lines.push(`decisions:
2024
+ ${state.decisions.map((d) => " - " + d).join("\n")}`);
2025
+ if (state.next_actions.length) lines.push(`next_actions:
2026
+ ${state.next_actions.map((a) => " - " + a).join("\n")}`);
2027
+ if (Object.keys(state.artifact_index).length) {
2028
+ lines.push("artifact_index:");
2029
+ for (const [id, meta] of Object.entries(state.artifact_index)) {
2030
+ lines.push(` ${id}: [${meta.type}] ${meta.summary}`);
2031
+ }
2032
+ }
2033
+ return lines.join("\n");
2034
+ }
2035
+ function buildSessionStateMessage(state) {
2036
+ return {
2037
+ role: "system",
2038
+ content: `[compiled session state]
2039
+ ${serializeSessionState(state)}`
2040
+ };
2041
+ }
2042
+ var ArtifactStore;
2043
+ var init_session_state = __esm({
2044
+ "src/agent/session-state.ts"() {
2045
+ "use strict";
2046
+ ArtifactStore = class {
2047
+ artifacts = /* @__PURE__ */ new Map();
2048
+ maxArtifacts;
2049
+ maxTotalChars;
2050
+ constructor(opts2) {
2051
+ this.maxArtifacts = opts2?.maxArtifacts ?? 200;
2052
+ this.maxTotalChars = opts2?.maxTotalChars ?? 5e5;
2053
+ }
2054
+ add(a) {
2055
+ while (this.totalChars() + a.raw.length > this.maxTotalChars && this.artifacts.size > 0) {
2056
+ this.evictOldest();
2057
+ }
2058
+ while (this.artifacts.size >= this.maxArtifacts) {
2059
+ this.evictOldest();
2060
+ }
2061
+ this.artifacts.set(a.id, a);
2062
+ }
2063
+ get(id) {
2064
+ return this.artifacts.get(id);
2065
+ }
2066
+ has(id) {
2067
+ return this.artifacts.has(id);
2068
+ }
2069
+ list() {
2070
+ return [...this.artifacts.values()].sort((a, b) => a.ts < b.ts ? -1 : 1);
2071
+ }
2072
+ recall(ids) {
2073
+ const out = [];
2074
+ for (const id of ids) {
2075
+ const a = this.artifacts.get(id);
2076
+ if (a) out.push({ id, artifact: a });
2077
+ }
2078
+ return out;
2079
+ }
2080
+ size() {
2081
+ return this.artifacts.size;
2082
+ }
2083
+ totalChars() {
2084
+ let sum = 0;
2085
+ for (const a of this.artifacts.values()) {
2086
+ sum += a.raw.length;
2087
+ }
2088
+ return sum;
2089
+ }
2090
+ evictOldest() {
2091
+ let oldest;
2092
+ for (const a of this.artifacts.values()) {
2093
+ if (!oldest || a.ts < oldest.ts) oldest = a;
2094
+ }
2095
+ if (oldest) this.artifacts.delete(oldest.id);
2096
+ }
2097
+ };
2098
+ }
2099
+ });
2100
+
2101
+ // src/agent/compaction.ts
2102
+ function approxTokens2(n) {
2103
+ return Math.round(n / 4);
2104
+ }
2105
+ function estimateMessageTokens(m) {
2106
+ let chars = 0;
2107
+ if (typeof m.content === "string") {
2108
+ chars = m.content.length;
2109
+ } else if (Array.isArray(m.content)) {
2110
+ chars = m.content.map((p) => p.type === "text" ? p.text.length : 0).reduce((a, b) => a + b, 0);
2111
+ }
2112
+ if (m.reasoning_content) chars += m.reasoning_content.length;
2113
+ if (m.tool_calls) {
2114
+ for (const tc of m.tool_calls) {
2115
+ chars += tc.function.name.length + tc.function.arguments.length;
2116
+ }
2117
+ }
2118
+ return approxTokens2(chars);
2119
+ }
2120
+ function estimatePromptTokens(messages) {
2121
+ return messages.reduce((sum, m) => sum + estimateMessageTokens(m), 0);
2122
+ }
2123
+ function groupIntoTurns(messages) {
2124
+ const prefix = [];
2125
+ let i = 0;
2126
+ while (i < messages.length && messages[i].role === "system") {
2127
+ prefix.push(messages[i]);
2128
+ i++;
2129
+ }
2130
+ const turns = [];
2131
+ while (i < messages.length) {
2132
+ if (messages[i].role !== "user") {
2133
+ i++;
2134
+ continue;
2135
+ }
2136
+ const user = messages[i];
2137
+ i++;
2138
+ if (i >= messages.length || messages[i].role !== "assistant") {
2139
+ continue;
2140
+ }
2141
+ const assistant = messages[i];
2142
+ i++;
2143
+ const tools = [];
2144
+ while (i < messages.length && messages[i].role === "tool") {
2145
+ tools.push(messages[i]);
2146
+ i++;
2147
+ }
2148
+ turns.push({ user, assistant, tools });
2149
+ }
2150
+ return { prefix, turns };
2151
+ }
2152
+ function makeArtifactId(type, index) {
2153
+ return `${type}_${Date.now()}_${index}`;
2154
+ }
2155
+ function extractArtifactsFromTurn(turn, startIndex, store) {
2156
+ const artifacts = [];
2157
+ const stateDelta = {
2158
+ files_touched: [],
2159
+ files_modified: [],
2160
+ confirmed_findings: [],
2161
+ recent_failures: [],
2162
+ decisions: [],
2163
+ next_actions: []
2164
+ };
2165
+ const toolCalls = turn.assistant.tool_calls ?? [];
2166
+ for (let ti = 0; ti < turn.tools.length; ti++) {
2167
+ const tm = turn.tools[ti];
2168
+ const tc = toolCalls[ti];
2169
+ const name = tm.name ?? tc?.function.name ?? "unknown";
2170
+ const content = typeof tm.content === "string" ? tm.content : "";
2171
+ let type = "tool_result";
2172
+ let summary = `${name} result`;
2173
+ let path;
2174
+ if (name === "read") {
2175
+ type = "read_slice";
2176
+ try {
2177
+ const args = tc ? JSON.parse(tc.function.arguments) : {};
2178
+ path = args.path;
2179
+ summary = `read ${path ?? "file"}`;
2180
+ if (path && !stateDelta.files_touched.includes(path)) stateDelta.files_touched.push(path);
2181
+ } catch {
2182
+ summary = "read file";
2183
+ }
2184
+ } else if (name === "bash") {
2185
+ type = "bash_log";
2186
+ try {
2187
+ const args = tc ? JSON.parse(tc.function.arguments) : {};
2188
+ const cmd = args.command ?? "";
2189
+ summary = `bash: ${cmd.slice(0, 60)}`;
2190
+ if (content.includes("Error") || content.includes("error") || content.includes("FAIL")) {
2191
+ stateDelta.recent_failures.push(`bash failed: ${cmd.slice(0, 80)}`);
2192
+ }
2193
+ } catch {
2194
+ summary = "bash command";
2195
+ }
2196
+ } else if (name === "grep") {
2197
+ type = "grep_result";
2198
+ summary = `grep results (${content.split("\n").length} lines)`;
2199
+ } else if (name === "web_fetch") {
2200
+ type = "web_fetch";
2201
+ try {
2202
+ const args = tc ? JSON.parse(tc.function.arguments) : {};
2203
+ summary = `web_fetch: ${args.url ?? "url"}`;
2204
+ } catch {
2205
+ summary = "web_fetch";
2206
+ }
2207
+ } else if (name === "write" || name === "edit") {
2208
+ try {
2209
+ const args = tc ? JSON.parse(tc.function.arguments) : {};
2210
+ path = args.path;
2211
+ if (path && !stateDelta.files_modified.includes(path)) stateDelta.files_modified.push(path);
2212
+ if (path && !stateDelta.files_touched.includes(path)) stateDelta.files_touched.push(path);
2213
+ } catch {
2214
+ }
2215
+ continue;
2216
+ } else if (name === "glob") {
2217
+ try {
2218
+ const args = tc ? JSON.parse(tc.function.arguments) : {};
2219
+ summary = `glob: ${args.pattern ?? ""}`;
2220
+ } catch {
2221
+ summary = "glob";
2222
+ }
2223
+ } else if (name === "tasks_set") {
2224
+ try {
2225
+ const args = tc ? JSON.parse(tc.function.arguments) : {};
2226
+ const tasks = args.tasks ?? [];
2227
+ const inProgress = tasks.filter((t) => t.status === "in_progress").map((t) => t.title);
2228
+ const pending = tasks.filter((t) => t.status === "pending").map((t) => t.title);
2229
+ if (inProgress.length) stateDelta.next_actions.push(...inProgress);
2230
+ if (pending.length) stateDelta.next_actions.push(...pending);
2231
+ summary = `tasks_set: ${tasks.length} tasks`;
2232
+ } catch {
2233
+ summary = "tasks_set";
2234
+ }
2235
+ }
2236
+ const maxRaw = 5e4;
2237
+ const raw = content.length > maxRaw ? content.slice(0, maxRaw) + `
2238
+ ...[${content.length - maxRaw} chars truncated]` : content;
2239
+ const artifact = {
2240
+ id: makeArtifactId(type, startIndex + ti),
2241
+ type,
2242
+ summary,
2243
+ raw,
2244
+ source: name,
2245
+ path,
2246
+ ts: (/* @__PURE__ */ new Date()).toISOString()
2247
+ };
2248
+ artifacts.push(artifact);
2249
+ if (!content.includes("Error") && !content.includes("error") && content.length > 0 && content.length < 2e3) {
2250
+ stateDelta.confirmed_findings.push(`${name}: ${content.slice(0, 200)}`);
2251
+ }
2252
+ }
2253
+ const assistantText = typeof turn.assistant.content === "string" ? turn.assistant.content : "";
2254
+ if (assistantText.length > 0) {
2255
+ const decisionPatterns = [
2256
+ /(?:decided?|will|plan to|going to|should|need to)\s+(.{10,200})/gi,
2257
+ /(?:let's|let us)\s+(.{10,200})/gi
2258
+ ];
2259
+ for (const pattern of decisionPatterns) {
2260
+ let match;
2261
+ while ((match = pattern.exec(assistantText)) !== null) {
2262
+ const decision = match[1].trim().replace(/\.$/, "");
2263
+ if (decision.length > 10 && !stateDelta.decisions.includes(decision)) {
2264
+ stateDelta.decisions.push(decision);
2265
+ }
2266
+ }
2267
+ }
2268
+ }
2269
+ return { artifacts, stateDelta };
2270
+ }
2271
+ function mergeState(state, delta) {
2272
+ const mergeArr = (a, b) => {
2273
+ if (!b) return a;
2274
+ const set = new Set(a);
2275
+ for (const item of b) set.add(item);
2276
+ return [...set];
2277
+ };
2278
+ return {
2279
+ ...state,
2280
+ task: state.task || delta.task || "",
2281
+ user_constraints: mergeArr(state.user_constraints, delta.user_constraints),
2282
+ repo_facts: mergeArr(state.repo_facts, delta.repo_facts),
2283
+ files_touched: mergeArr(state.files_touched, delta.files_touched),
2284
+ files_modified: mergeArr(state.files_modified, delta.files_modified),
2285
+ confirmed_findings: mergeArr(state.confirmed_findings, delta.confirmed_findings),
2286
+ open_questions: mergeArr(state.open_questions, delta.open_questions),
2287
+ recent_failures: mergeArr(state.recent_failures, delta.recent_failures),
2288
+ decisions: mergeArr(state.decisions, delta.decisions),
2289
+ next_actions: mergeArr(state.next_actions, delta.next_actions),
2290
+ artifact_index: { ...state.artifact_index, ...delta.artifact_index }
2291
+ };
2292
+ }
2293
+ function shouldCompact(opts2) {
2294
+ const tokenThreshold = opts2.tokenThreshold ?? 8e4;
2295
+ const turnThreshold = opts2.turnThreshold ?? 12;
2296
+ const tokens = estimatePromptTokens(opts2.messages);
2297
+ const { turns } = groupIntoTurns(opts2.messages);
2298
+ return tokens > tokenThreshold || turns.length > turnThreshold;
2299
+ }
2300
+ function compactMessages2(opts2) {
2301
+ const keepLastTurns = opts2.keepLastTurns ?? 4;
2302
+ const { prefix, turns } = groupIntoTurns(opts2.messages);
2303
+ const tokensBefore = estimatePromptTokens(opts2.messages);
2304
+ if (turns.length <= keepLastTurns) {
2305
+ return {
2306
+ newMessages: opts2.messages,
2307
+ newState: opts2.state,
2308
+ metrics: {
2309
+ estimatedTokensBefore: tokensBefore,
2310
+ estimatedTokensAfter: tokensBefore,
2311
+ archivedArtifacts: 0,
2312
+ recalledArtifacts: 0,
2313
+ rawTurnsRemoved: 0,
2314
+ rawTurnsKept: turns.length
2315
+ }
2316
+ };
2317
+ }
2318
+ const toCompact = turns.slice(0, turns.length - keepLastTurns);
2319
+ const toKeep = turns.slice(turns.length - keepLastTurns);
2320
+ let newState = { ...opts2.state };
2321
+ let archivedCount = 0;
2322
+ for (let i = 0; i < toCompact.length; i++) {
2323
+ const turn = toCompact[i];
2324
+ const { artifacts, stateDelta } = extractArtifactsFromTurn(turn, i, opts2.store);
2325
+ for (const artifact of artifacts) {
2326
+ opts2.store.add(artifact);
2327
+ archivedCount++;
2328
+ newState.artifact_index[artifact.id] = {
2329
+ type: artifact.type,
2330
+ summary: artifact.summary,
2331
+ source: artifact.source,
2332
+ path: artifact.path
2333
+ };
2334
+ }
2335
+ newState = mergeState(newState, stateDelta);
2336
+ if (!newState.task && typeof turn.user.content === "string") {
2337
+ newState.task = turn.user.content.slice(0, 200);
2338
+ }
2339
+ }
2340
+ const workingMemory = [];
2341
+ for (const turn of toKeep) {
2342
+ workingMemory.push(turn.user);
2343
+ workingMemory.push(turn.assistant);
2344
+ for (const tm of turn.tools) {
2345
+ workingMemory.push(tm);
2346
+ }
2347
+ }
2348
+ const stateMsg = buildSessionStateMessage(newState);
2349
+ const newMessages = [...prefix, stateMsg, ...workingMemory];
2350
+ const tokensAfter = estimatePromptTokens(newMessages);
2351
+ const metrics = {
2352
+ estimatedTokensBefore: tokensBefore,
2353
+ estimatedTokensAfter: tokensAfter,
2354
+ archivedArtifacts: archivedCount,
2355
+ recalledArtifacts: 0,
2356
+ rawTurnsRemoved: toCompact.length,
2357
+ rawTurnsKept: toKeep.length
2358
+ };
2359
+ return { newMessages, newState, metrics };
2360
+ }
2361
+ function recallArtifacts(messages, store, state) {
2362
+ const text = messages.map((m) => typeof m.content === "string" ? m.content : "").join(" ");
2363
+ const ids = [];
2364
+ for (const [id, meta] of Object.entries(state.artifact_index)) {
2365
+ if (meta.path && text.includes(meta.path)) {
2366
+ ids.push(id);
2367
+ }
2368
+ }
2369
+ for (const failure of state.recent_failures) {
2370
+ const keyword = failure.split(":")[0];
2371
+ if (keyword && text.toLowerCase().includes(keyword.toLowerCase())) {
2372
+ for (const [id, meta] of Object.entries(state.artifact_index)) {
2373
+ if (meta.source === "bash" && !ids.includes(id)) {
2374
+ ids.push(id);
2375
+ }
2376
+ }
2377
+ }
2378
+ }
2379
+ const uniqueIds = [...new Set(ids)].slice(0, 5);
2380
+ const recalled = store.recall(uniqueIds);
2381
+ return { ids: uniqueIds, recalled };
2382
+ }
2383
+ var init_compaction = __esm({
2384
+ "src/agent/compaction.ts"() {
2385
+ "use strict";
2386
+ init_session_state();
2387
+ }
2388
+ });
2389
+
1885
2390
  // src/mcp/adapter.ts
1886
2391
  function mcpToolToSpec(serverName, mcpTool, client) {
1887
2392
  const prefix = `mcp_${sanitizeName(serverName)}_`;
@@ -3834,12 +4339,20 @@ var init_theme = __esm({
3834
4339
  });
3835
4340
 
3836
4341
  // src/sessions.ts
3837
- import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir5, readdir, stat as stat2 } from "fs/promises";
4342
+ var sessions_exports = {};
4343
+ __export(sessions_exports, {
4344
+ listSessions: () => listSessions,
4345
+ loadSession: () => loadSession,
4346
+ makeSessionId: () => makeSessionId,
4347
+ pruneSessions: () => pruneSessions,
4348
+ saveSession: () => saveSession
4349
+ });
4350
+ import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir5, readdir as readdir2, stat as stat3 } from "fs/promises";
3838
4351
  import { homedir as homedir6 } from "os";
3839
- import { join as join6 } from "path";
4352
+ import { join as join7 } from "path";
3840
4353
  function sessionsDir() {
3841
- const xdg = process.env.XDG_DATA_HOME || join6(homedir6(), ".local", "share");
3842
- return join6(xdg, "kimiflare", "sessions");
4354
+ const xdg = process.env.XDG_DATA_HOME || join7(homedir6(), ".local", "share");
4355
+ return join7(xdg, "kimiflare", "sessions");
3843
4356
  }
3844
4357
  function sanitize(text) {
3845
4358
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
@@ -3852,24 +4365,29 @@ function makeSessionId(firstPrompt) {
3852
4365
  async function saveSession(file) {
3853
4366
  const dir = sessionsDir();
3854
4367
  await mkdir5(dir, { recursive: true });
3855
- const path = join6(dir, `${file.id}.json`);
4368
+ const path = join7(dir, `${file.id}.json`);
3856
4369
  await writeFile5(path, JSON.stringify(file, null, 2), "utf8");
3857
4370
  return path;
3858
4371
  }
4372
+ async function pruneSessions() {
4373
+ const dir = sessionsDir();
4374
+ const files = await listFilesByMtime(dir, /\.json$/);
4375
+ return pruneFiles(files, RETENTION.sessionMaxAgeDays, RETENTION.sessionMaxCount);
4376
+ }
3859
4377
  async function listSessions(limit = 30) {
3860
4378
  const dir = sessionsDir();
3861
4379
  let entries;
3862
4380
  try {
3863
- entries = await readdir(dir);
4381
+ entries = await readdir2(dir);
3864
4382
  } catch {
3865
4383
  return [];
3866
4384
  }
3867
4385
  const summaries = [];
3868
4386
  for (const name of entries) {
3869
4387
  if (!name.endsWith(".json")) continue;
3870
- const path = join6(dir, name);
4388
+ const path = join7(dir, name);
3871
4389
  try {
3872
- const [s, raw] = await Promise.all([stat2(path), readFile7(path, "utf8")]);
4390
+ const [s, raw] = await Promise.all([stat3(path), readFile7(path, "utf8")]);
3873
4391
  const parsed = JSON.parse(raw);
3874
4392
  const firstUser = parsed.messages.find((m) => m.role === "user");
3875
4393
  const firstPrompt = typeof firstUser?.content === "string" ? firstUser.content : firstUser?.content ? firstUser.content.find((p) => p.type === "text")?.text ?? "(no prompt)" : "(no prompt)";
@@ -3894,6 +4412,7 @@ async function loadSession(filePath) {
3894
4412
  var init_sessions = __esm({
3895
4413
  "src/sessions.ts"() {
3896
4414
  "use strict";
4415
+ init_storage_limits();
3897
4416
  }
3898
4417
  });
3899
4418
 
@@ -3939,17 +4458,21 @@ var init_image = __esm({
3939
4458
  // src/usage-tracker.ts
3940
4459
  import { readFile as readFile9, writeFile as writeFile6, mkdir as mkdir6 } from "fs/promises";
3941
4460
  import { homedir as homedir7 } from "os";
3942
- import { join as join7 } from "path";
4461
+ import { join as join8 } from "path";
3943
4462
  function usageDir() {
3944
- const xdg = process.env.XDG_DATA_HOME || join7(homedir7(), ".local", "share");
3945
- return join7(xdg, "kimiflare");
4463
+ const xdg = process.env.XDG_DATA_HOME || join8(homedir7(), ".local", "share");
4464
+ return join8(xdg, "kimiflare");
3946
4465
  }
3947
4466
  function usagePath() {
3948
- return join7(usageDir(), "usage.json");
4467
+ return join8(usageDir(), "usage.json");
3949
4468
  }
3950
4469
  function today() {
3951
4470
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3952
4471
  }
4472
+ function cutoffDate(daysBack) {
4473
+ const d = new Date(Date.now() - daysBack * 24 * 60 * 60 * 1e3);
4474
+ return d.toISOString().slice(0, 10);
4475
+ }
3953
4476
  async function loadLog() {
3954
4477
  try {
3955
4478
  const raw = await readFile9(usagePath(), "utf8");
@@ -3979,8 +4502,18 @@ function getOrCreateSession(log, sessionId, date) {
3979
4502
  }
3980
4503
  return session;
3981
4504
  }
4505
+ function pruneUsageLog(log) {
4506
+ const dayCutoff = cutoffDate(RETENTION.usageDayMaxAgeDays);
4507
+ const sessionCutoff = cutoffDate(RETENTION.usageSessionMaxAgeDays);
4508
+ const days = log.days.filter((d) => d.date >= dayCutoff);
4509
+ let sessions = log.sessions.filter((s) => s.date >= sessionCutoff);
4510
+ if (sessions.length > RETENTION.usageSessionMaxCount) {
4511
+ sessions = sessions.sort((a, b) => b.date < a.date ? -1 : b.date > a.date ? 1 : 0).slice(0, RETENTION.usageSessionMaxCount);
4512
+ }
4513
+ return { ...log, days, sessions };
4514
+ }
3982
4515
  async function recordUsage(sessionId, usage) {
3983
- const log = await loadLog();
4516
+ const log = pruneUsageLog(await loadLog());
3984
4517
  const date = today();
3985
4518
  const cost = calculateCost(usage.prompt_tokens, usage.completion_tokens, usage.prompt_tokens_details?.cached_tokens ?? 0);
3986
4519
  const day = getOrCreateDay(log, date);
@@ -3996,7 +4529,7 @@ async function recordUsage(sessionId, usage) {
3996
4529
  await saveLog(log);
3997
4530
  }
3998
4531
  async function getCostReport(sessionId) {
3999
- const log = await loadLog();
4532
+ const log = pruneUsageLog(await loadLog());
4000
4533
  const date = today();
4001
4534
  const currentMonth = date.slice(0, 7);
4002
4535
  const session = log.sessions.find((s) => s.id === sessionId) ?? { date, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0 };
@@ -4055,6 +4588,7 @@ var init_usage_tracker = __esm({
4055
4588
  "src/usage-tracker.ts"() {
4056
4589
  "use strict";
4057
4590
  init_pricing();
4591
+ init_storage_limits();
4058
4592
  LOG_VERSION2 = 1;
4059
4593
  }
4060
4594
  });
@@ -4067,8 +4601,8 @@ __export(app_exports, {
4067
4601
  import { useState as useState6, useRef as useRef3, useEffect as useEffect4, useCallback } from "react";
4068
4602
  import { Box as Box12, Text as Text13, useApp, useInput as useInput2, render } from "ink";
4069
4603
  import { existsSync } from "fs";
4070
- import { join as join8 } from "path";
4071
- import { unlink } from "fs/promises";
4604
+ import { join as join9 } from "path";
4605
+ import { unlink as unlink2 } from "fs/promises";
4072
4606
  import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
4073
4607
  function capEvents(prev) {
4074
4608
  if (prev.length <= MAX_EVENTS) return prev;
@@ -4145,11 +4679,27 @@ function App({ initialCfg, initialUpdateResult }) {
4145
4679
  const tasksRef = useRef3([]);
4146
4680
  const usageRef = useRef3(null);
4147
4681
  const updateCheckedRef = useRef3(false);
4682
+ const sessionStateRef = useRef3(emptySessionState());
4683
+ const artifactStoreRef = useRef3(new ArtifactStore());
4684
+ const compiledContextRef = useRef3(initialCfg?.compiledContext === true);
4148
4685
  const updateNudgedRef = useRef3(false);
4149
4686
  const compactSuggestedRef = useRef3(false);
4150
4687
  const mcpManagerRef = useRef3(new McpManager());
4151
4688
  const mcpToolsRef = useRef3([]);
4152
4689
  const mcpInitRef = useRef3(false);
4690
+ useEffect4(() => {
4691
+ if (!cfg) return;
4692
+ void Promise.resolve().then(() => (init_sessions(), sessions_exports)).then(
4693
+ ({ pruneSessions: pruneSessions2 }) => pruneSessions2().then((removed) => {
4694
+ if (removed > 0) {
4695
+ setEvents((e) => [
4696
+ ...e,
4697
+ { kind: "info", key: mkKey(), text: `pruned ${removed} old session files` }
4698
+ ]);
4699
+ }
4700
+ })
4701
+ );
4702
+ }, [cfg]);
4153
4703
  useEffect4(() => {
4154
4704
  if (!cfg || updateCheckedRef.current) return;
4155
4705
  updateCheckedRef.current = true;
@@ -4351,7 +4901,8 @@ function App({ initialCfg, initialUpdateResult }) {
4351
4901
  model: cfg.model,
4352
4902
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4353
4903
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4354
- messages: messagesRef.current
4904
+ messages: messagesRef.current,
4905
+ sessionState: compiledContextRef.current ? sessionStateRef.current : void 0
4355
4906
  });
4356
4907
  } catch {
4357
4908
  }
@@ -4416,29 +4967,55 @@ function App({ initialCfg, initialUpdateResult }) {
4416
4967
  const controller = new AbortController();
4417
4968
  activeControllerRef.current = controller;
4418
4969
  try {
4419
- const result = await compactMessages({
4420
- accountId: cfg.accountId,
4421
- apiToken: cfg.apiToken,
4422
- model: cfg.model,
4423
- messages: messagesRef.current,
4424
- signal: controller.signal
4425
- });
4426
- if (result.replacedCount === 0) {
4427
- setEvents((e) => [
4428
- ...e,
4429
- { kind: "info", key: mkKey(), text: "nothing to compact yet" }
4430
- ]);
4970
+ if (compiledContextRef.current) {
4971
+ const result = compactMessages2({
4972
+ messages: messagesRef.current,
4973
+ state: sessionStateRef.current,
4974
+ store: artifactStoreRef.current
4975
+ });
4976
+ if (result.metrics.rawTurnsRemoved === 0) {
4977
+ setEvents((e) => [
4978
+ ...e,
4979
+ { kind: "info", key: mkKey(), text: "nothing to compact yet" }
4980
+ ]);
4981
+ } else {
4982
+ messagesRef.current = result.newMessages;
4983
+ sessionStateRef.current = result.newState;
4984
+ setEvents((e) => [
4985
+ ...e,
4986
+ {
4987
+ kind: "info",
4988
+ key: mkKey(),
4989
+ text: `compacted ${result.metrics.rawTurnsRemoved} turns \u2192 ${result.metrics.estimatedTokensBefore} \u2192 ${result.metrics.estimatedTokensAfter} tokens, ${result.metrics.archivedArtifacts} artifacts`
4990
+ }
4991
+ ]);
4992
+ await saveSessionSafe();
4993
+ }
4431
4994
  } else {
4432
- messagesRef.current = result.newMessages;
4433
- setEvents((e) => [
4434
- ...e,
4435
- {
4436
- kind: "info",
4437
- key: mkKey(),
4438
- text: `compacted ${result.replacedCount} messages into a summary`
4439
- }
4440
- ]);
4441
- await saveSessionSafe();
4995
+ const result = await compactMessages({
4996
+ accountId: cfg.accountId,
4997
+ apiToken: cfg.apiToken,
4998
+ model: cfg.model,
4999
+ messages: messagesRef.current,
5000
+ signal: controller.signal
5001
+ });
5002
+ if (result.replacedCount === 0) {
5003
+ setEvents((e) => [
5004
+ ...e,
5005
+ { kind: "info", key: mkKey(), text: "nothing to compact yet" }
5006
+ ]);
5007
+ } else {
5008
+ messagesRef.current = result.newMessages;
5009
+ setEvents((e) => [
5010
+ ...e,
5011
+ {
5012
+ kind: "info",
5013
+ key: mkKey(),
5014
+ text: `compacted ${result.replacedCount} messages into a summary`
5015
+ }
5016
+ ]);
5017
+ await saveSessionSafe();
5018
+ }
4442
5019
  }
4443
5020
  } catch (e) {
4444
5021
  if (e.name !== "AbortError") {
@@ -4465,13 +5042,13 @@ function App({ initialCfg, initialUpdateResult }) {
4465
5042
  }
4466
5043
  const cwd = process.cwd();
4467
5044
  for (const name of ["KIMI.md", "KIMIFLARE.md", "AGENT.md"]) {
4468
- if (existsSync(join8(cwd, name))) {
5045
+ if (existsSync(join9(cwd, name))) {
4469
5046
  setEvents((e) => [
4470
5047
  ...e,
4471
5048
  {
4472
5049
  kind: "info",
4473
5050
  key: mkKey(),
4474
- text: `${name} already exists at ${join8(cwd, name)} \u2014 delete it first if you want to regenerate`
5051
+ text: `${name} already exists at ${join9(cwd, name)} \u2014 delete it first if you want to regenerate`
4475
5052
  }
4476
5053
  ]);
4477
5054
  return;
@@ -4588,7 +5165,7 @@ function App({ initialCfg, initialUpdateResult }) {
4588
5165
  })
4589
5166
  }
4590
5167
  });
4591
- if (existsSync(join8(cwd, "KIMI.md"))) {
5168
+ if (existsSync(join9(cwd, "KIMI.md"))) {
4592
5169
  if (cacheStableRef.current) {
4593
5170
  messagesRef.current[1] = {
4594
5171
  role: "system",
@@ -4637,6 +5214,10 @@ function App({ initialCfg, initialUpdateResult }) {
4637
5214
  const file = await loadSession(picked.filePath);
4638
5215
  messagesRef.current = file.messages;
4639
5216
  sessionIdRef.current = file.id;
5217
+ if (file.sessionState && compiledContextRef.current) {
5218
+ sessionStateRef.current = file.sessionState;
5219
+ artifactStoreRef.current = new ArtifactStore();
5220
+ }
4640
5221
  setEvents([
4641
5222
  {
4642
5223
  kind: "info",
@@ -4699,6 +5280,8 @@ function App({ initialCfg, initialUpdateResult }) {
4699
5280
  messagesRef.current = [messagesRef.current[0]];
4700
5281
  }
4701
5282
  sessionIdRef.current = null;
5283
+ sessionStateRef.current = emptySessionState();
5284
+ artifactStoreRef.current = new ArtifactStore();
4702
5285
  setEvents([]);
4703
5286
  setUsage(null);
4704
5287
  setTasks([]);
@@ -4926,7 +5509,7 @@ use: /thinking low | medium | high`
4926
5509
  return true;
4927
5510
  }
4928
5511
  if (c === "/logout") {
4929
- unlink(configPath()).catch(() => {
5512
+ unlink2(configPath()).catch(() => {
4930
5513
  });
4931
5514
  setEvents((e) => [
4932
5515
  ...e,
@@ -4987,6 +5570,17 @@ use: /thinking low | medium | high`
4987
5570
  }
4988
5571
  setEvents((e) => [...e, { kind: "user", key: mkKey(), text: display, images: images.length > 0 ? images : void 0 }]);
4989
5572
  messagesRef.current.push({ role: "user", content });
5573
+ if (compiledContextRef.current) {
5574
+ const { ids, recalled } = recallArtifacts(messagesRef.current, artifactStoreRef.current, sessionStateRef.current);
5575
+ if (recalled.length > 0) {
5576
+ const recalledText = formatRecalledArtifacts(recalled);
5577
+ messagesRef.current.push({ role: "system", content: recalledText });
5578
+ sessionStateRef.current = {
5579
+ ...sessionStateRef.current,
5580
+ artifact_index: { ...sessionStateRef.current.artifact_index }
5581
+ };
5582
+ }
5583
+ }
4990
5584
  setBusy(true);
4991
5585
  setTurnStartedAt(Date.now());
4992
5586
  const controller = new AbortController();
@@ -5096,6 +5690,26 @@ use: /thinking low | medium | high`
5096
5690
  }
5097
5691
  });
5098
5692
  await saveSessionSafe();
5693
+ if (compiledContextRef.current && shouldCompact({ messages: messagesRef.current })) {
5694
+ const result = compactMessages2({
5695
+ messages: messagesRef.current,
5696
+ state: sessionStateRef.current,
5697
+ store: artifactStoreRef.current
5698
+ });
5699
+ if (result.metrics.rawTurnsRemoved > 0) {
5700
+ messagesRef.current = result.newMessages;
5701
+ sessionStateRef.current = result.newState;
5702
+ setEvents((e) => [
5703
+ ...e,
5704
+ {
5705
+ kind: "info",
5706
+ key: mkKey(),
5707
+ text: `auto-compacted: ${result.metrics.estimatedTokensBefore} \u2192 ${result.metrics.estimatedTokensAfter} tokens (${result.metrics.archivedArtifacts} artifacts)`
5708
+ }
5709
+ ]);
5710
+ await saveSessionSafe();
5711
+ }
5712
+ }
5099
5713
  } catch (e) {
5100
5714
  if (e.name === "AbortError") {
5101
5715
  setEvents((es) => [...es, { kind: "info", key: mkKey(), text: "(aborted)" }]);
@@ -5292,6 +5906,8 @@ var init_app = __esm({
5292
5906
  init_loop();
5293
5907
  init_system_prompt();
5294
5908
  init_compact();
5909
+ init_compaction();
5910
+ init_session_state();
5295
5911
  init_executor();
5296
5912
  init_manager();
5297
5913
  init_messages();
@@ -5336,11 +5952,11 @@ init_update_check();
5336
5952
  import { Command } from "commander";
5337
5953
  import { readFileSync as readFileSync2 } from "fs";
5338
5954
  import { fileURLToPath as fileURLToPath2 } from "url";
5339
- import { dirname as dirname3, join as join9 } from "path";
5955
+ import { dirname as dirname3, join as join10 } from "path";
5340
5956
  function readPackageVersion() {
5341
5957
  try {
5342
5958
  const here = dirname3(fileURLToPath2(import.meta.url));
5343
- const pkg = JSON.parse(readFileSync2(join9(here, "..", "package.json"), "utf8"));
5959
+ const pkg = JSON.parse(readFileSync2(join10(here, "..", "package.json"), "utf8"));
5344
5960
  return pkg.version ?? "0.0.0";
5345
5961
  } catch {
5346
5962
  return "0.0.0";