knit-mcp 0.7.1 → 0.9.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/README.md CHANGED
@@ -59,6 +59,27 @@ Adds the Knit MCP server to your Claude Code config (`~/.claude.json`). No per-p
59
59
 
60
60
  **Supported shells:** macOS, Linux, WSL, Git Bash, and Windows PowerShell. The generated hooks use POSIX-style single-quoted `node -e '…'` payloads. Windows `cmd.exe` does not treat single quotes as delimiters and is not supported as the hook-runner shell — on Windows, use PowerShell (default in modern Windows Terminal) or Git Bash. If you hit a hook error on Windows, file an issue with the shell you're using.
61
61
 
62
+ ### Quiet mode (no hook enforcement)
63
+
64
+ Knit ships Protocol Guard in `warn` mode by default — hooks print reminders, they never block. If you want it fully silent (no PreToolUse classification gate, no reminder messages), run this once inside Claude Code:
65
+
66
+ > `knit_set_protocol_strictness({ level: "off" })`
67
+
68
+ The other hooks (LEARN compliance, KB metrics, final build verification) stay as observability nudges — they print, they don't gate. To remove them too, see Uninstall below.
69
+
70
+ ### Uninstall
71
+
72
+ ```bash
73
+ rm -rf ~/.knit # all per-project + global memory
74
+ ```
75
+
76
+ Then:
77
+ 1. Remove `"knit-brain"` from `mcpServers` in `~/.claude.json`
78
+ 2. Delete the `<!-- knit:start --> ... <!-- knit:end -->` block from each project's `CLAUDE.md`
79
+ 3. Remove `_knitOwned` entries from each project's `.claude/settings.local.json` (or delete the file if Knit was the only thing in it)
80
+
81
+ Total time: ~30 seconds per project. Knit doesn't write anywhere else on your machine.
82
+
62
83
  ## How data is stored
63
84
 
64
85
  Knit data is centralized — not in every repo's working tree:
@@ -2,13 +2,15 @@ import {
2
2
  detectProjectRoot,
3
3
  getBrain,
4
4
  refreshBrain
5
- } from "./chunk-X6TGTET3.js";
6
- import "./chunk-3XR77YJM.js";
7
- import "./chunk-SLN5ABF5.js";
5
+ } from "./chunk-4K4FHOKE.js";
6
+ import "./chunk-BU3VHX3W.js";
7
+ import "./chunk-MOOVNMIN.js";
8
8
  import "./chunk-7PPC6IG6.js";
9
9
  import "./chunk-M3YZOJNW.js";
10
+ import "./chunk-FLNV2IQC.js";
11
+ import "./chunk-KLNUEE3O.js";
10
12
  import "./chunk-BAUQEFYY.js";
11
- import "./chunk-HBMF62U4.js";
13
+ import "./chunk-XFS2XGZI.js";
12
14
  export {
13
15
  detectProjectRoot,
14
16
  getBrain,
@@ -1,20 +1,27 @@
1
1
  import {
2
2
  installAgentsForProject,
3
+ prewarmLatestVersion,
3
4
  pruneSessionsByAge
4
- } from "./chunk-3XR77YJM.js";
5
+ } from "./chunk-BU3VHX3W.js";
5
6
  import {
6
- KNIT_MARKER_START,
7
7
  buildKnowledge,
8
- buildReverseDependencies,
9
- generateClaudeMd,
10
- spliceKnitBlock
11
- } from "./chunk-SLN5ABF5.js";
8
+ buildReverseDependencies
9
+ } from "./chunk-MOOVNMIN.js";
12
10
  import {
13
11
  scanProject
14
12
  } from "./chunk-7PPC6IG6.js";
15
13
  import {
16
14
  readLearnings
17
15
  } from "./chunk-M3YZOJNW.js";
16
+ import {
17
+ persistScanResult,
18
+ scanIntegrations
19
+ } from "./chunk-FLNV2IQC.js";
20
+ import {
21
+ KNIT_MARKER_START,
22
+ generateClaudeMd,
23
+ spliceKnitBlock
24
+ } from "./chunk-KLNUEE3O.js";
18
25
  import {
19
26
  importFromMarkdown,
20
27
  loadKnowledgeBase,
@@ -34,11 +41,12 @@ import {
34
41
  migrationBreadcrumbPath,
35
42
  projectDataDir,
36
43
  protocolConfigPath,
44
+ searchMarkerPath,
37
45
  sessionMarkerPath,
38
46
  sessionsJsonlPath,
39
47
  sessionsLogPath,
40
48
  teamsPath
41
- } from "./chunk-HBMF62U4.js";
49
+ } from "./chunk-XFS2XGZI.js";
42
50
 
43
51
  // src/mcp/cache.ts
44
52
  import { execSync } from "child_process";
@@ -65,7 +73,7 @@ function generateLearningsContent(config) {
65
73
  }
66
74
 
67
75
  // src/generators/settings.ts
68
- var HOOKS_VERSION = 6;
76
+ var HOOKS_VERSION = 7;
69
77
  function generateSettings(config, rootPath) {
70
78
  return {
71
79
  mcpServers: {
@@ -111,6 +119,9 @@ function generateHooks(config, rootPath) {
111
119
  const PROTOCOL_CONFIG = protocolConfigPath(rootPath);
112
120
  const CLASSIFIED_MARKER = classificationMarkerPath(rootPath);
113
121
  const SESSION_MARKER = sessionMarkerPath(rootPath);
122
+ const SEARCH_MARKER = searchMarkerPath(rootPath);
123
+ const CLAUDE_MD = `${rootPath}/CLAUDE.md`;
124
+ void knowledgePath;
114
125
  const hooks = {
115
126
  SessionStart: [
116
127
  // Protocol Guard layer 1: drop a marker that knit_load_session
@@ -149,6 +160,10 @@ function generateHooks(config, rootPath) {
149
160
  const fs = require("fs");
150
161
  const p = ${jsLit(CLASSIFIED_MARKER)};
151
162
  if (fs.existsSync(p)) fs.rmSync(p, { force: true });
163
+ // v0.9 #5: per-turn search marker is also cleared at the turn boundary
164
+ // so the next non-trivial task has to call knit_search_learnings again.
165
+ const sm = ${jsLit(SEARCH_MARKER)};
166
+ if (fs.existsSync(sm)) fs.rmSync(sm, { force: true });
152
167
  } catch (e) {}
153
168
  `),
154
169
  timeout: 5
@@ -192,6 +207,7 @@ function generateHooks(config, rootPath) {
192
207
  const fs = require("fs");
193
208
  const cfgPath = ${jsLit(PROTOCOL_CONFIG)};
194
209
  const markerPath = ${jsLit(CLASSIFIED_MARKER)};
210
+ const searchMarkerPath = ${jsLit(SEARCH_MARKER)};
195
211
  let level = "warn";
196
212
  if (fs.existsSync(cfgPath)) {
197
213
  try {
@@ -202,13 +218,33 @@ function generateHooks(config, rootPath) {
202
218
  }
203
219
  }
204
220
  if (level === "off") return;
221
+ // Classification gate (v0.5).
205
222
  const hasMarker = fs.existsSync(markerPath);
206
- if (hasMarker) return;
207
- if (level === "block") {
208
- console.error("[knit] BLOCKED: call knit_classify_task before Edit/Write. The Protocol Guard prevents implementation without classification.");
209
- process.exit(2);
223
+ if (!hasMarker) {
224
+ if (level === "block") {
225
+ console.error("[knit] BLOCKED: call knit_classify_task before Edit/Write. The Protocol Guard prevents implementation without classification.");
226
+ process.exit(2);
227
+ }
228
+ console.error("[knit] reminder: call knit_classify_task before Edit/Write. Set strictness=block via knit_set_protocol_strictness to make this a hard gate.");
229
+ return;
230
+ }
231
+ // v0.9 #5: search gate. For standard/complex tasks, knit_search_learnings
232
+ // (or knit_search_global_learnings) must run before the Edit lands \u2014
233
+ // otherwise the agent is re-investigating without checking memory.
234
+ try {
235
+ const marker = JSON.parse(fs.readFileSync(markerPath, "utf-8"));
236
+ if (marker && (marker.tier === "standard" || marker.tier === "complex")) {
237
+ if (!fs.existsSync(searchMarkerPath)) {
238
+ if (level === "block") {
239
+ console.error("[knit] BLOCKED: " + marker.tier + " task \u2014 call knit_search_learnings or knit_search_global_learnings before Edit/Write so memory is checked before re-investigation.");
240
+ process.exit(2);
241
+ }
242
+ console.error("[knit] reminder: " + marker.tier + " task \u2014 call knit_search_learnings before Edit/Write. Skipping memory check means re-doing work the project already learned.");
243
+ }
244
+ }
245
+ } catch (markerErr) {
246
+ // Marker exists but JSON unreadable \u2014 be lenient.
210
247
  }
211
- console.error("[knit] reminder: call knit_classify_task before Edit/Write. Set strictness=block via knit_set_protocol_strictness to make this a hard gate.");
212
248
  } catch (hookErr) {
213
249
  console.error("[knit] protocol-guard hook crashed, allowing tool through:", hookErr && hookErr.message ? hookErr.message : hookErr);
214
250
  }
@@ -216,11 +252,106 @@ function generateHooks(config, rootPath) {
216
252
  timeout: 5
217
253
  }
218
254
  ]
255
+ },
256
+ // v0.9 #9 — Pre-write content inspection. Reads the proposed Write/Edit
257
+ // content from tool_input, parses local import statements, and reports
258
+ // any relative paths that don't resolve on disk. Warn-level by default
259
+ // (the existing classification gate handles block mode); soft signal,
260
+ // never blocks on its own.
261
+ {
262
+ _knitOwned: true,
263
+ matcher: "Write|Edit|MultiEdit",
264
+ hooks: [
265
+ {
266
+ type: "command",
267
+ command: nodeHook(`
268
+ let d = "";
269
+ process.stdin.on("data", (c) => d += c);
270
+ process.stdin.on("end", () => {
271
+ try {
272
+ const fs = require("fs");
273
+ const path = require("path");
274
+ const i = JSON.parse(d);
275
+ const filePath = (i.tool_input && i.tool_input.file_path) || "";
276
+ if (!/\\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(filePath)) return;
277
+ // Pull proposed content from any of the Edit/Write shapes.
278
+ let content = (i.tool_input && (i.tool_input.content || i.tool_input.new_string)) || "";
279
+ if (i.tool_input && Array.isArray(i.tool_input.edits)) {
280
+ content = i.tool_input.edits.map((e) => e && e.new_string ? e.new_string : "").join("\\n");
281
+ }
282
+ if (!content) return;
283
+ const dir = path.dirname(filePath);
284
+ const re = /^import\\s+(?:[^'"]+?\\s+from\\s+)?['"]([^'"]+)['"]/gm;
285
+ const unresolved = [];
286
+ let m;
287
+ while ((m = re.exec(content)) !== null) {
288
+ const target = m[1];
289
+ if (!target.startsWith(".") && !target.startsWith("/")) continue;
290
+ const candidates = [target, target + ".ts", target + ".tsx", target + ".js", target + ".jsx", target + "/index.ts", target + "/index.tsx", target + "/index.js"];
291
+ let resolved = false;
292
+ for (const c of candidates) {
293
+ const abs = path.resolve(dir, c);
294
+ if (fs.existsSync(abs)) { resolved = true; break; }
295
+ }
296
+ if (!resolved) unresolved.push(target);
297
+ }
298
+ if (unresolved.length > 0) {
299
+ console.error("[knit] heads-up: proposed edit references " + unresolved.length + " unresolved relative import(s): " + unresolved.join(", ") + ". Likely hallucinated paths \u2014 verify with knit_query_imports or knit_verify_claim before relying on them.");
300
+ }
301
+ } catch (e) {}
302
+ });
303
+ `),
304
+ timeout: 5
305
+ }
306
+ ]
219
307
  }
220
308
  ],
221
309
  PostToolUse: [],
222
310
  Stop: []
223
311
  };
312
+ hooks.PostToolUse.push({
313
+ _knitOwned: true,
314
+ matcher: "Write|Edit|MultiEdit",
315
+ hooks: [
316
+ {
317
+ type: "command",
318
+ command: nodeHook(`
319
+ let d = "";
320
+ process.stdin.on("data", (c) => d += c);
321
+ process.stdin.on("end", () => {
322
+ try {
323
+ const fs = require("fs");
324
+ const path = require("path");
325
+ const i = JSON.parse(d);
326
+ const f = (i.tool_input && i.tool_input.file_path) || (i.tool_response && i.tool_response.filePath) || "";
327
+ if (!/\\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(f)) return;
328
+ if (!fs.existsSync(f)) return;
329
+ const content = fs.readFileSync(f, "utf-8");
330
+ const dir = path.dirname(f);
331
+ const re = /^import\\s+(?:[^'"]+?\\s+from\\s+)?['"]([^'"]+)['"]/gm;
332
+ const unresolved = [];
333
+ let m;
334
+ while ((m = re.exec(content)) !== null) {
335
+ const target = m[1];
336
+ if (!target.startsWith(".") && !target.startsWith("/")) continue;
337
+ const candidates = [target, target + ".ts", target + ".tsx", target + ".js", target + ".jsx", target + "/index.ts", target + "/index.tsx", target + "/index.js"];
338
+ let resolved = false;
339
+ for (const c of candidates) {
340
+ if (fs.existsSync(path.resolve(dir, c))) { resolved = true; break; }
341
+ }
342
+ if (!resolved) unresolved.push(target);
343
+ }
344
+ if (unresolved.length > 0) {
345
+ console.error("[knit] post-write check: " + f + " has " + unresolved.length + " unresolved relative import(s): " + unresolved.join(", ") + ". Run typecheck before relying on this file.");
346
+ }
347
+ } catch (e) {}
348
+ });
349
+ `),
350
+ timeout: 5,
351
+ statusMessage: "Knit: validating imports..."
352
+ }
353
+ ]
354
+ });
224
355
  if (config.stack.language === "typescript" && config.stack.typecheckCommand) {
225
356
  hooks.PostToolUse.push({
226
357
  _knitOwned: true,
@@ -351,6 +482,27 @@ function generateHooks(config, rootPath) {
351
482
  ]
352
483
  });
353
484
  }
485
+ hooks.Stop.push({
486
+ _knitOwned: true,
487
+ hooks: [
488
+ {
489
+ type: "command",
490
+ command: nodeHook(`
491
+ try {
492
+ const fs = require("fs");
493
+ const p = ${jsLit(CLAUDE_MD)};
494
+ if (!fs.existsSync(p)) return;
495
+ const size = fs.statSync(p).size;
496
+ if (size > 12500) {
497
+ console.error("[knit] budget watch: CLAUDE.md is " + Math.round(size/1024*10)/10 + "KB (target 6.5KB; over-budget threshold 12.5KB). Call knit_brain_status to confirm and consider regenerating via knit refresh.");
498
+ }
499
+ } catch (e) {}
500
+ `),
501
+ timeout: 5,
502
+ statusMessage: "Knit: budget check..."
503
+ }
504
+ ]
505
+ });
354
506
  hooks.Stop.push({
355
507
  _knitOwned: true,
356
508
  hooks: [
@@ -497,6 +649,7 @@ function getBrain(rootPath) {
497
649
  if (cache && cache.rootPath === rootPath) {
498
650
  return cache;
499
651
  }
652
+ void prewarmLatestVersion();
500
653
  let autoInitialized = false;
501
654
  const haveCentralized = existsSync(knowledgePath(rootPath));
502
655
  const haveLegacy = existsSync(legacyKnowledgePath(rootPath));
@@ -563,6 +716,16 @@ function autoInitialize(rootPath) {
563
716
  } catch (e) {
564
717
  const msg = e instanceof Error ? e.message : String(e);
565
718
  process.stderr.write(`[knit] session prune background error: ${msg}
719
+ `);
720
+ }
721
+ });
722
+ Promise.resolve().then(() => {
723
+ try {
724
+ const result = scanIntegrations(rootPath);
725
+ persistScanResult(rootPath, result);
726
+ } catch (e) {
727
+ const msg = e instanceof Error ? e.message : String(e);
728
+ process.stderr.write(`[knit] integration scan background error: ${msg}
566
729
  `);
567
730
  }
568
731
  });
@@ -0,0 +1,66 @@
1
+ // src/mcp/instructions.ts
2
+ var KNIT_INSTRUCTIONS_BASE = `Knit is a memory + workflow layer for this project. It provides per-project memory across sessions, a knowledge graph (imports/exports/tests), and a tier-routed workflow protocol.
3
+
4
+ ALWAYS at session start:
5
+ 1. Call knit_load_session \u2014 returns prior handoff, top learnings, false positives. If has_unfinished_work is true, resume that handoff instead of starting fresh.
6
+ 2. For any non-trivial task, call knit_classify_task BEFORE editing or writing \u2014 returns tier (inquiry / trivial / standard / complex) and phases.
7
+ 3. If tier=complex with auto_plan_mode=true, call EnterPlanMode immediately. Do not start editing.
8
+ 4. If tier=inquiry, just answer \u2014 no plan mode, no phases. Re-classify only if scope grows into writes.
9
+ 5. Before reporting a task done, call knit_record_learning if anything non-obvious surfaced (not a substring restatement of prior learnings).
10
+
11
+ When to reach for other Knit tools:
12
+ - knit_query_imports / knit_query_exports / knit_query_dependents / knit_query_tests \u2014 use instead of grep when the knowledge index is fresh.
13
+ - knit_search_learnings \u2014 call before re-investigating a domain. The point of memory is to skip what you've already learned.
14
+ - knit_search_sessions \u2014 answers "have I done this before?"
15
+ - knit_search_global_learnings \u2014 same, but across all your projects (Knit's cross-project pool).
16
+ - knit_get_workflow({phase}) \u2014 fetch protocol depth for one phase on demand. Do not try to remember the workflow; ask for it.
17
+ - knit_list_features \u2014 if you want to do X but the tool isn't visible, this surfaces what's hidden and how to enable it.
18
+ - knit_save_handoff \u2014 call when context degrades or session ends so the next session resumes cleanly.
19
+
20
+ The protocol enforces a 4-tier classifier:
21
+ - Inquiry: read-only "what / where / audit / explain" \u2014 just answer.
22
+ - Trivial: single-file obvious change \u2014 EXECUTE \u2192 VERIFY \u2192 LEARN.
23
+ - Standard: bug fix or single-domain feature \u2014 RESEARCH \u2192 EXECUTE \u2192 OPTIMIZE \u2192 REVIEW \u2192 LEARN.
24
+ - Complex: cross-domain, types/auth-touching, high-fanout, or any task spanning more than one commit \u2014 full 6 phases with auto plan mode on RESEARCH.
25
+
26
+ Knit provides inputs; you make the calls. When in doubt, under-classify \u2014 easier to escalate mid-task than to downgrade.
27
+
28
+ Citation rule: when you state a fact about this codebase ("file X imports Y", "function Z is defined in W", "tests for A live in B"), cite the Knit tool result that verified it \u2014 e.g. "(per knit_query_imports)". If you can't cite a tool result, mark the claim as 'unverified' explicitly. This makes hallucinations visible at the claim level instead of letting them ship as confident-sounding prose. The verifier exists; use it.`;
29
+ var KNIT_INSTRUCTIONS = KNIT_INSTRUCTIONS_BASE;
30
+ function buildInstructions(scan) {
31
+ if (!scan) return KNIT_INSTRUCTIONS_BASE;
32
+ const addenda = [];
33
+ if (scan.detected.ruflo.present) {
34
+ addenda.push(
35
+ "DETECTED: Ruflo (multi-agent orchestration) is installed alongside Knit on this project. For multi-agent swarms, federation, or large-scale orchestration, defer to Ruflo's tools (`memory_store`, `swarm_init`, `agent_spawn`, etc). Knit's domain in this project: per-project memory + tier-routed classification + token discipline. Do NOT duplicate Ruflo's routing logic with Knit's tier protocol when Ruflo is driving the workflow."
36
+ );
37
+ }
38
+ if (scan.detected.gstack.present) {
39
+ addenda.push(
40
+ "DETECTED: gstack slash commands are installed. For routing decisions (`/plan`, `/ship`, `/qa`, `/cso`, `/investigate`), prefer the gstack command. Knit operates underneath as the memory + classification layer; the gstack command can invoke Knit tools internally."
41
+ );
42
+ }
43
+ if (scan.detected.codetour.present) {
44
+ addenda.push(
45
+ "DETECTED: CodeTour is configured (.tours/*.tour). When asked to walk through code or explain architecture, surface relevant tours via the CodeTour extension rather than reconstructing the explanation from scratch."
46
+ );
47
+ }
48
+ if (scan.detected.conductor.present) {
49
+ addenda.push(
50
+ "DETECTED: Conductor is installed. For cross-workspace handoff and context-restore flows, defer to Conductor's primitives; Knit's `knit_save_handoff` / `knit_load_session` continue to handle the per-project memory layer."
51
+ );
52
+ }
53
+ if (scan.detected.custom_workflow_sections.length > 0) {
54
+ addenda.push(
55
+ `DETECTED: this project's CLAUDE.md has user-curated workflow sections (${scan.detected.custom_workflow_sections.join("; ")}). Treat that as the canonical workflow doc for project-specific phases; Knit's tier protocol applies underneath as the routing layer.`
56
+ );
57
+ }
58
+ if (addenda.length === 0) return KNIT_INSTRUCTIONS_BASE;
59
+ return KNIT_INSTRUCTIONS_BASE + "\n\n\u2014 Per-project integrations \u2014\n\n" + addenda.join("\n\n") + "\n\nGeneral rule: when an integration above provides a higher-level routing primitive (slash command, swarm orchestrator, methodology framework), use it. Knit handles the substrate it doesn't cover: memory, classification, and the workflow protocol for tasks the integration doesn't route.";
60
+ }
61
+
62
+ export {
63
+ KNIT_INSTRUCTIONS_BASE,
64
+ KNIT_INSTRUCTIONS,
65
+ buildInstructions
66
+ };
@@ -2,7 +2,7 @@ import {
2
2
  canonicalRepoRoot,
3
3
  globalLearningsPath,
4
4
  projectId
5
- } from "./chunk-HBMF62U4.js";
5
+ } from "./chunk-XFS2XGZI.js";
6
6
 
7
7
  // src/engine/global-learnings.ts
8
8
  import { existsSync, mkdirSync, appendFileSync, readFileSync, statSync } from "fs";
@@ -44,6 +44,15 @@ function getRecentGlobalLearnings(n = 5) {
44
44
  }
45
45
  return out;
46
46
  }
47
+ function loadAllGlobalLearnings() {
48
+ const lines = readAllLines();
49
+ const out = [];
50
+ for (const line of lines) {
51
+ const entry = parseLine(line);
52
+ if (entry) out.push(entry);
53
+ }
54
+ return out;
55
+ }
47
56
  function buildGlobalLearning(sourceProjectRoot, payload) {
48
57
  return {
49
58
  id: payload.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
@@ -83,5 +92,6 @@ export {
83
92
  appendGlobalLearning,
84
93
  searchGlobalLearnings,
85
94
  getRecentGlobalLearnings,
95
+ loadAllGlobalLearnings,
86
96
  buildGlobalLearning
87
97
  };
@@ -11,7 +11,7 @@ import {
11
11
  projectAgentFile,
12
12
  projectAgentsDir,
13
13
  sessionsJsonlPath
14
- } from "./chunk-HBMF62U4.js";
14
+ } from "./chunk-XFS2XGZI.js";
15
15
 
16
16
  // src/engine/install-agents.ts
17
17
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
@@ -285,6 +285,55 @@ function agentsNeededByProject(config) {
285
285
  return Array.from(names);
286
286
  }
287
287
 
288
+ // src/mcp/update-check.ts
289
+ var REGISTRY_DIST_TAGS_URL = "https://registry.npmjs.org/-/package/knit-mcp/dist-tags";
290
+ var FETCH_TIMEOUT_MS = 2e3;
291
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
292
+ var cachedLatest = null;
293
+ var lastCheckedAt = 0;
294
+ var inFlight = null;
295
+ function getCachedLatestVersion() {
296
+ if (Date.now() - lastCheckedAt > CACHE_TTL_MS) {
297
+ prewarmLatestVersion();
298
+ }
299
+ return cachedLatest;
300
+ }
301
+ function prewarmLatestVersion() {
302
+ if (inFlight) return;
303
+ if (Date.now() - lastCheckedAt < CACHE_TTL_MS && cachedLatest !== null) return;
304
+ inFlight = doFetch().finally(() => {
305
+ inFlight = null;
306
+ });
307
+ }
308
+ async function doFetch() {
309
+ const controller = new AbortController();
310
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
311
+ try {
312
+ const res = await fetch(REGISTRY_DIST_TAGS_URL, { signal: controller.signal });
313
+ if (!res.ok) return;
314
+ const data = await res.json();
315
+ if (typeof data.latest === "string" && data.latest.length > 0) {
316
+ cachedLatest = data.latest;
317
+ lastCheckedAt = Date.now();
318
+ }
319
+ } catch {
320
+ } finally {
321
+ clearTimeout(timeout);
322
+ }
323
+ }
324
+ function isNewerVersion(latest, current) {
325
+ const parse = (v) => {
326
+ const stripped = v.replace(/[-+].*$/, "");
327
+ const parts = stripped.split(".").map((n) => parseInt(n, 10) || 0);
328
+ return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
329
+ };
330
+ const [a1, a2, a3] = parse(latest);
331
+ const [b1, b2, b3] = parse(current);
332
+ if (a1 !== b1) return a1 > b1;
333
+ if (a2 !== b2) return a2 > b2;
334
+ return a3 > b3;
335
+ }
336
+
288
337
  // src/engine/sessions.ts
289
338
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, appendFileSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync3, renameSync } from "fs";
290
339
  import { dirname as dirname2 } from "path";
@@ -327,6 +376,15 @@ function getRecentSessions(rootPath, n = 3) {
327
376
  function sessionCount(rootPath) {
328
377
  return readAllLines(rootPath).length;
329
378
  }
379
+ function loadAllSessions(rootPath) {
380
+ const lines = readAllLines(rootPath);
381
+ const out = [];
382
+ for (const line of lines) {
383
+ const entry = parseLine(line);
384
+ if (entry) out.push(entry);
385
+ }
386
+ return out;
387
+ }
330
388
  function pruneSessionsByAge(rootPath, maxAgeDays) {
331
389
  const path = sessionsJsonlPath(rootPath);
332
390
  if (!existsSync3(path)) return { kept: 0, pruned: 0 };
@@ -400,9 +458,13 @@ function parseLine(line) {
400
458
 
401
459
  export {
402
460
  installAgentsForProject,
461
+ getCachedLatestVersion,
462
+ prewarmLatestVersion,
463
+ isNewerVersion,
403
464
  appendSession,
404
465
  searchSessions,
405
466
  getRecentSessions,
406
467
  sessionCount,
468
+ loadAllSessions,
407
469
  pruneSessionsByAge
408
470
  };