knit-mcp 0.11.2 → 0.12.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
@@ -3,7 +3,7 @@
3
3
  <a href="https://github.com/PDgit12/knit/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/PDgit12/knit/ci.yml?style=for-the-badge&label=CI&color=10b981" alt="CI" /></a>
4
4
  <img src="https://img.shields.io/badge/license-MIT-3b82f6?style=for-the-badge" alt="license" />
5
5
  <img src="https://img.shields.io/badge/node-%E2%89%A518-339933?style=for-the-badge&logo=node.js&logoColor=white" alt="node" />
6
- <img src="https://img.shields.io/badge/tests-664%20passing-22c55e?style=for-the-badge" alt="tests" />
6
+ <img src="https://img.shields.io/badge/tests-665%20passing-22c55e?style=for-the-badge" alt="tests" />
7
7
  <img src="https://img.shields.io/badge/MCP%20tools-53-7c3aed?style=for-the-badge" alt="tools" />
8
8
  </p>
9
9
 
@@ -452,6 +452,9 @@ LongMemEval-S R@5/R@10 + LOCOMO LLM-as-Judge runs are on the roadmap (v0.13+). U
452
452
 
453
453
  | Version | Headline |
454
454
  |---|---|
455
+ | **v0.12.0** | **Picture Perfect: Structural Enforcement.** Diagnostic → enforcing. Budget verdict surfaces in the MCP `instructions` field at handshake (before any tool description is read). `knit_load_session` carries `budget_health` + `learnings_health` nudges. `engram doctor` exits non-zero on over-budget; `engram setup` runs doctor as final step. New PostToolUse hook warns immediately on over-budget CLAUDE.md edits (HOOKS_VERSION 11→12; auto-rolls to existing users). This repo dogfoods: hand-curated 16KB CLAUDE.md migrated to lean 3.8KB + `.claude/MARKETING.md` sidecar. New `npm run bench:tokens` measures real MCP-on vs MCP-off cost: 93% smaller per-recall call, 50% smaller per-classify, payback at 3 recall calls. 53 tools, 705 tests. |
456
+ | **v0.11.4** | Dogfood audit · ran a full audit of Knit's own codebase using its own `knit_spawn_team_worktree` primitive (4 parallel teams: Core Logic, Infrastructure, UI, Quality Assurance). Fixes: HIGH `engram refresh` no longer clobbers user-curated CLAUDE.md (now uses `spliceKnitBlock` like `cache.ts`); `saveSource`/`loadSource` validate `sourceId`; `appendGlobalLearning` propagates write failures; `redactSecrets` applied to `label`/`tags`/`domains` across all persistence boundaries; 100KB response ceiling on `knit_generate_test_cases`; full v0.11 tool surface now documented in `workflow-protocol.ts` generator (was frozen at the v0.4 surface). Plus: 16 key tools reclassified with `[PROTOCOL]`/`[REVIEW]`/`[MEMORY]`/`[GRAPH]` prefixes so the LLM picks the right tool reliably. 53 tools, 687 tests. |
457
+ | **v0.11.3** | Propagation patch · `update_available` flag now surfaces in `knit_load_session` response (≈100% session reach vs. brain_status' low reach) + startup stderr nag on stale versions. Helps FUTURE upgrades land faster; doesn't retroactively reach v0.10.x users. 53 tools, 665 tests. |
455
458
  | **v0.11.2** | Pre-publish polish · chunk cap (2000) + `errorResponse` envelope across handlers + CLAUDE.md generator surfaces v0.11 tools · new `engram doctor` install health-check CLI · upgrade-path smoke test caught + fixed a data-loss bug in cache.ts (Case B was wiping user permissions on upgrade) · 11 real exploit-payload integration tests prove C1/C2/H1 fixes hold · `npm run bench` ships a synthetic retrieval harness (50 Q&A) measuring 86% top-1 / 96% R@5. 53 tools, 664 tests. |
456
459
  | **v0.11.1** | Audit-driven hardening · 3 CRITICAL (source_id path traversal, post-edit tsc shell injection, live calibration bug) + 10 HIGH fixes from a 5-agent audit, implemented in 3 parallel `knit_spawn_team_worktree` teams. HOOKS_VERSION 11 (auto-upgrades existing users). New `knit_delete_requirements` tool. Honest comparison vs mem0/Letta added. 53 tools, 636 tests. |
457
460
  | **v0.11.0** | Verify Layer + auto-config foundation · mandatory `knit_verify_claim` REVIEW gate · post-edit diff verify + universal `tsc` check · drift detector · self-healing classifier (per-project calibration) · `knit_index_requirements` + `knit_generate_test_cases` (BM25 over long specs) · `knit_get_fingerprint` + `knit_infer_domains` + `knit_compose_template` (zero-config CLAUDE.md). 52 tools, 625 tests. |
@@ -2,15 +2,16 @@ import {
2
2
  detectProjectRoot,
3
3
  getBrain,
4
4
  refreshBrain
5
- } from "./chunk-TWHNYJAJ.js";
6
- import "./chunk-HROSQ5MS.js";
7
- import "./chunk-RZOVZYTF.js";
5
+ } from "./chunk-KKLAJLPO.js";
6
+ import "./chunk-GATMQQK5.js";
7
+ import "./chunk-WADRF26Z.js";
8
+ import "./chunk-WKQHCLLO.js";
8
9
  import "./chunk-MOOVNMIN.js";
9
10
  import "./chunk-ST4X7LZT.js";
10
11
  import "./chunk-M3YZOJNW.js";
12
+ import "./chunk-POXT5OYN.js";
11
13
  import "./chunk-VB2TIR6L.js";
12
14
  import "./chunk-7UFS67HP.js";
13
- import "./chunk-WKQHCLLO.js";
14
15
  import "./chunk-27TA2ZQZ.js";
15
16
  export {
16
17
  detectProjectRoot,
@@ -62,7 +62,7 @@ async function fetchAgent(name, opts = {}) {
62
62
  throw new AgentFetchError(`Unknown agent: "${name}". Not in engram's registry.`);
63
63
  }
64
64
  const cachePath = agentsCacheFile(ref, cat, bare);
65
- if (existsSync(cachePath)) {
65
+ if (!opts.refresh && existsSync(cachePath)) {
66
66
  return readFileSync(cachePath, "utf-8");
67
67
  }
68
68
  if (process.env.KNIT_OFFLINE === "1" || process.env.ENGRAM_OFFLINE === "1") {
@@ -250,7 +250,7 @@ async function installAgentsForProject(rootPath, config, knowledge, knowledgeBas
250
250
  }
251
251
  }
252
252
  try {
253
- const baseMd = await fetchAgent(name, opts.refresh ? { ref: void 0 } : {});
253
+ const baseMd = await fetchAgent(name, opts.refresh ? { ref: void 0, refresh: true } : {});
254
254
  const relevant = knowledgeBase ? selectRelevantLearnings(knowledgeBase.entries, name) : [];
255
255
  const personalized = personalizeAgent(baseMd, {
256
256
  config,
@@ -285,55 +285,6 @@ 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
-
337
288
  // src/engine/sessions.ts
338
289
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, appendFileSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync3, renameSync } from "fs";
339
290
  import { dirname as dirname2 } from "path";
@@ -466,9 +417,6 @@ function parseLine(line) {
466
417
 
467
418
  export {
468
419
  installAgentsForProject,
469
- getCachedLatestVersion,
470
- prewarmLatestVersion,
471
- isNewerVersion,
472
420
  appendSession,
473
421
  searchSessions,
474
422
  getRecentSessions,
@@ -1,12 +1,16 @@
1
+ import {
2
+ installAgentsForProject,
3
+ pruneSessionsByAge
4
+ } from "./chunk-GATMQQK5.js";
1
5
  import {
2
6
  HOOKS_VERSION,
3
7
  generateSettings
4
- } from "./chunk-HROSQ5MS.js";
8
+ } from "./chunk-WADRF26Z.js";
5
9
  import {
6
- installAgentsForProject,
7
- prewarmLatestVersion,
8
- pruneSessionsByAge
9
- } from "./chunk-RZOVZYTF.js";
10
+ importFromMarkdown,
11
+ loadKnowledgeBaseSafe,
12
+ saveKnowledgeBase
13
+ } from "./chunk-WKQHCLLO.js";
10
14
  import {
11
15
  buildKnowledge,
12
16
  buildReverseDependencies
@@ -17,6 +21,9 @@ import {
17
21
  import {
18
22
  readLearnings
19
23
  } from "./chunk-M3YZOJNW.js";
24
+ import {
25
+ prewarmLatestVersion
26
+ } from "./chunk-POXT5OYN.js";
20
27
  import {
21
28
  persistScanResult,
22
29
  scanIntegrations
@@ -26,11 +33,6 @@ import {
26
33
  generateClaudeMd,
27
34
  spliceKnitBlock
28
35
  } from "./chunk-7UFS67HP.js";
29
- import {
30
- importFromMarkdown,
31
- loadKnowledgeBaseSafe,
32
- saveKnowledgeBase
33
- } from "./chunk-WKQHCLLO.js";
34
36
  import {
35
37
  knowledgePath,
36
38
  knowledgebasePath,
@@ -9,8 +9,16 @@ import { existsSync, mkdirSync, appendFileSync, readFileSync, statSync } from "f
9
9
  import { dirname, basename } from "path";
10
10
  function appendGlobalLearning(entry) {
11
11
  const path = globalLearningsPath();
12
- mkdirSync(dirname(path), { recursive: true });
13
- appendFileSync(path, JSON.stringify(entry) + "\n", "utf-8");
12
+ try {
13
+ mkdirSync(dirname(path), { recursive: true });
14
+ appendFileSync(path, JSON.stringify(entry) + "\n", "utf-8");
15
+ } catch (err) {
16
+ process.stderr.write(
17
+ `[knit] global learning append failed at ${path}: ${err.message}
18
+ `
19
+ );
20
+ throw err;
21
+ }
14
22
  }
15
23
  function searchGlobalLearnings(query, limit = 10) {
16
24
  const lines = readAllLines();
@@ -0,0 +1,66 @@
1
+ // src/mcp/update-check.ts
2
+ var REGISTRY_DIST_TAGS_URL = "https://registry.npmjs.org/-/package/knit-mcp/dist-tags";
3
+ var FETCH_TIMEOUT_MS = 2e3;
4
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
5
+ var cachedLatest = null;
6
+ var lastCheckedAt = 0;
7
+ var inFlight = null;
8
+ function getCachedLatestVersion() {
9
+ if (Date.now() - lastCheckedAt > CACHE_TTL_MS) {
10
+ prewarmLatestVersion();
11
+ }
12
+ return cachedLatest;
13
+ }
14
+ function prewarmLatestVersion() {
15
+ if (inFlight) return;
16
+ if (Date.now() - lastCheckedAt < CACHE_TTL_MS && cachedLatest !== null) return;
17
+ inFlight = doFetch().finally(() => {
18
+ inFlight = null;
19
+ });
20
+ }
21
+ async function doFetch() {
22
+ const controller = new AbortController();
23
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
24
+ try {
25
+ const res = await fetch(REGISTRY_DIST_TAGS_URL, { signal: controller.signal });
26
+ if (!res.ok) return;
27
+ const data = await res.json();
28
+ if (typeof data.latest === "string" && data.latest.length > 0) {
29
+ cachedLatest = data.latest;
30
+ lastCheckedAt = Date.now();
31
+ }
32
+ } catch {
33
+ } finally {
34
+ clearTimeout(timeout);
35
+ }
36
+ }
37
+ function isNewerVersion(latest, current) {
38
+ const parse = (v) => {
39
+ const stripped = v.replace(/[-+].*$/, "");
40
+ const parts = stripped.split(".").map((n) => parseInt(n, 10) || 0);
41
+ return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
42
+ };
43
+ const [a1, a2, a3] = parse(latest);
44
+ const [b1, b2, b3] = parse(current);
45
+ if (a1 !== b1) return a1 > b1;
46
+ if (a2 !== b2) return a2 > b2;
47
+ return a3 > b3;
48
+ }
49
+ function __setCachedLatestForTests(version, checkedAtMs = Date.now()) {
50
+ cachedLatest = version;
51
+ lastCheckedAt = checkedAtMs;
52
+ inFlight = null;
53
+ }
54
+ function __resetUpdateCheckForTests() {
55
+ cachedLatest = null;
56
+ lastCheckedAt = 0;
57
+ inFlight = null;
58
+ }
59
+
60
+ export {
61
+ getCachedLatestVersion,
62
+ prewarmLatestVersion,
63
+ isNewerVersion,
64
+ __setCachedLatestForTests,
65
+ __resetUpdateCheckForTests
66
+ };
@@ -1,4 +1,6 @@
1
1
  // src/mcp/instructions.ts
2
+ import { statSync } from "fs";
3
+ import { join } from "path";
2
4
  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
5
 
4
6
  ALWAYS at session start:
@@ -32,8 +34,28 @@ Knit provides inputs; you make the calls. When in doubt, under-classify \u2014 e
32
34
 
33
35
  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.`;
34
36
  var KNIT_INSTRUCTIONS = KNIT_INSTRUCTIONS_BASE;
35
- function buildInstructions(scan) {
36
- if (!scan) return KNIT_INSTRUCTIONS_BASE;
37
+ var CLAUDE_MD_BUDGET_BYTES = 6500;
38
+ function buildBudgetVerdict(rootPath) {
39
+ let bytes = 0;
40
+ try {
41
+ bytes = statSync(join(rootPath, "CLAUDE.md")).size;
42
+ } catch {
43
+ return "";
44
+ }
45
+ if (bytes <= CLAUDE_MD_BUDGET_BYTES) return "";
46
+ const verdict = bytes > CLAUDE_MD_BUDGET_BYTES * 1.25 ? "over-budget" : "warn";
47
+ const kb = Math.round(bytes / 1024 * 10) / 10;
48
+ const targetKb = Math.round(CLAUDE_MD_BUDGET_BYTES / 1024 * 10) / 10;
49
+ return `BUDGET ${verdict}: CLAUDE.md is ${kb}KB / ${targetKb}KB target. Run \`engram doctor\` to see the full per-surface report and \`engram refresh\` to regenerate the marker block.`;
50
+ }
51
+ function buildInstructions(scan, rootPath) {
52
+ const budgetLine = rootPath ? buildBudgetVerdict(rootPath) : "";
53
+ const budgetSuffix = budgetLine ? `
54
+
55
+ \u2014 Budget check \u2014
56
+
57
+ ${budgetLine}` : "";
58
+ if (!scan) return KNIT_INSTRUCTIONS_BASE + budgetSuffix;
37
59
  const addenda = [];
38
60
  if (scan.detected.ruflo.present) {
39
61
  addenda.push(
@@ -60,12 +82,14 @@ function buildInstructions(scan) {
60
82
  `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.`
61
83
  );
62
84
  }
63
- if (addenda.length === 0) return KNIT_INSTRUCTIONS_BASE;
64
- 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.";
85
+ if (addenda.length === 0) return KNIT_INSTRUCTIONS_BASE + budgetSuffix;
86
+ 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." + budgetSuffix;
65
87
  }
66
88
 
67
89
  export {
68
90
  KNIT_INSTRUCTIONS_BASE,
69
91
  KNIT_INSTRUCTIONS,
92
+ CLAUDE_MD_BUDGET_BYTES,
93
+ buildBudgetVerdict,
70
94
  buildInstructions
71
95
  };
@@ -1,9 +1,12 @@
1
+ import {
2
+ HOOKS_VERSION
3
+ } from "./chunk-WADRF26Z.js";
1
4
  import {
2
5
  VERSION
3
6
  } from "./chunk-UTVFELXS.js";
4
7
  import {
5
- HOOKS_VERSION
6
- } from "./chunk-HROSQ5MS.js";
8
+ CLAUDE_MD_BUDGET_BYTES
9
+ } from "./chunk-QQNHF4XY.js";
7
10
  import {
8
11
  knowledgebasePath,
9
12
  projectDataDir
@@ -125,6 +128,45 @@ function runDoctor(rootPath) {
125
128
  } catch {
126
129
  }
127
130
  }
131
+ const claudeMdPath = join(rootPath, "CLAUDE.md");
132
+ if (existsSync(claudeMdPath)) {
133
+ try {
134
+ const bytes = statSync(claudeMdPath).size;
135
+ const kb = Math.round(bytes / 1024 * 10) / 10;
136
+ const targetKb = Math.round(CLAUDE_MD_BUDGET_BYTES / 1024 * 10) / 10;
137
+ if (bytes <= CLAUDE_MD_BUDGET_BYTES) {
138
+ checks.push({
139
+ name: "Token budget",
140
+ status: "ok",
141
+ detail: `CLAUDE.md ${kb}KB / ${targetKb}KB target \u2014 healthy`
142
+ });
143
+ } else if (bytes <= CLAUDE_MD_BUDGET_BYTES * 1.25) {
144
+ checks.push({
145
+ name: "Token budget",
146
+ status: "warn",
147
+ detail: `CLAUDE.md ${kb}KB / ${targetKb}KB target \u2014 over budget, within 25% slack. Run \`engram refresh\` or trim the file.`
148
+ });
149
+ } else {
150
+ checks.push({
151
+ name: "Token budget",
152
+ status: "error",
153
+ detail: `CLAUDE.md ${kb}KB / ${targetKb}KB target \u2014 over budget by >25%. Move long-form content to .claude/MARKETING.md or run \`engram refresh\`.`
154
+ });
155
+ }
156
+ } catch (err) {
157
+ checks.push({
158
+ name: "Token budget",
159
+ status: "warn",
160
+ detail: `CLAUDE.md unreadable: ${err.message}`
161
+ });
162
+ }
163
+ } else {
164
+ checks.push({
165
+ name: "Token budget",
166
+ status: "info",
167
+ detail: "no CLAUDE.md yet \u2014 created on first MCP call"
168
+ });
169
+ }
128
170
  const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
129
171
  if (Number.isFinite(nodeMajor) && nodeMajor < 18) {
130
172
  checks.push({
@@ -173,7 +215,8 @@ async function doctorCommand(directory) {
173
215
  console.log(chalk.green(" All checks passed \u2014 install is healthy."));
174
216
  }
175
217
  }
218
+
176
219
  export {
177
- doctorCommand,
178
- runDoctor
220
+ runDoctor,
221
+ doctorCommand
179
222
  };
@@ -15,7 +15,7 @@ import {
15
15
  } from "./chunk-27TA2ZQZ.js";
16
16
 
17
17
  // src/generators/settings.ts
18
- var HOOKS_VERSION = 11;
18
+ var HOOKS_VERSION = 12;
19
19
  function generateSettings(config, rootPath) {
20
20
  return {
21
21
  mcpServers: {
@@ -288,6 +288,39 @@ function generateHooks(config, rootPath) {
288
288
  }
289
289
  ]
290
290
  });
291
+ hooks.PostToolUse.push({
292
+ _knitOwned: true,
293
+ matcher: "Write|Edit|MultiEdit",
294
+ hooks: [
295
+ {
296
+ type: "command",
297
+ command: nodeHook(`
298
+ let d = "";
299
+ process.stdin.on("data", (c) => d += c);
300
+ process.stdin.on("end", () => {
301
+ try {
302
+ const fs = require("fs");
303
+ const path = require("path");
304
+ const i = JSON.parse(d);
305
+ const ti = i.tool_input || {};
306
+ const f = ti.file_path || (i.tool_response && i.tool_response.filePath) || "";
307
+ if (!f) return;
308
+ if (path.basename(f) !== "CLAUDE.md") return;
309
+ const TARGET = 6500;
310
+ const SLACK = 6500 * 1.25;
311
+ let size = 0;
312
+ try { size = fs.statSync(f).size; } catch { return; }
313
+ if (size <= TARGET) return;
314
+ const kb = Math.round(size/1024*10)/10;
315
+ const verdict = size > SLACK ? "over-budget" : "warn";
316
+ process.stderr.write("[knit] BUDGET " + verdict + ": " + f + " is now " + kb + "KB (target 6.5KB). Move long-form content to .claude/MARKETING.md or run \\\`engram refresh\\\`.\\n");
317
+ } catch (e) { try { process.stderr.write('[knit] claude-md size watch hook failed: ' + (e && e.message ? e.message : e) + '\\n'); } catch {} }
318
+ });
319
+ `),
320
+ timeout: 5
321
+ }
322
+ ]
323
+ });
291
324
  hooks.PostToolUse.push({
292
325
  _knitOwned: true,
293
326
  matcher: "Write|Edit|MultiEdit",
package/dist/cli.js CHANGED
@@ -19,12 +19,12 @@ if (hasSubcommand) {
19
19
  async function runCLI() {
20
20
  const gradient = (await import("gradient-string")).default;
21
21
  const chalk = (await import("chalk")).default;
22
- const { setupCommand } = await import("./setup-5TUUWLIJ.js");
23
- const { statusCommand } = await import("./status-VJDB75X2.js");
24
- const { refreshCommand } = await import("./refresh-SMJ2NGIW.js");
25
- const { installAgentsCommand } = await import("./install-agents-OBDCWCPB.js");
26
- const { exportCommand } = await import("./export-CGSEUYZA.js");
27
- const { doctorCommand } = await import("./doctor-4DN2P2JR.js");
22
+ const { setupCommand } = await import("./setup-62ZH7GEI.js");
23
+ const { statusCommand } = await import("./status-2SEITNIE.js");
24
+ const { refreshCommand } = await import("./refresh-S62AZ3QA.js");
25
+ const { installAgentsCommand } = await import("./install-agents-AH7EBB4J.js");
26
+ const { exportCommand } = await import("./export-4BO6HCXP.js");
27
+ const { doctorCommand } = await import("./doctor-NI22FPXV.js");
28
28
  const ENGRAM_GRADIENT = gradient(["#7c3aed", "#2563eb", "#06b6d4"]);
29
29
  const banner = `
30
30
  \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
@@ -98,13 +98,24 @@ async function runMCP() {
98
98
  const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
99
99
  const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
100
100
  const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
101
- const { getBrain, detectProjectRoot, refreshBrain } = await import("./cache-3LPETDUT.js");
102
- const { getActiveToolDefinitionsForBrain, handleToolCall } = await import("./tools-MIROTK2A.js");
103
- const { buildInstructions } = await import("./instructions-JARSXQPO.js");
101
+ const { getBrain, detectProjectRoot, refreshBrain } = await import("./cache-46OKHDRR.js");
102
+ const { getActiveToolDefinitionsForBrain, handleToolCall } = await import("./tools-ZQHRWMVK.js");
103
+ const { buildInstructions } = await import("./instructions-CF6K57Z2.js");
104
104
  const { registerToolsListChangedNotifier } = await import("./notifier-4L27HKHI.js");
105
105
  const { loadScanResult } = await import("./integration-scanner-LBD2PIZ3.js");
106
+ const { prewarmLatestVersion, getCachedLatestVersion, isNewerVersion } = await import("./update-check-GQVDVT2N.js");
106
107
  const ROOT_PATH = detectProjectRoot();
107
108
  const PER_PROJECT_INSTRUCTIONS = buildInstructions(loadScanResult(ROOT_PATH));
109
+ prewarmLatestVersion();
110
+ setTimeout(() => {
111
+ const latest = getCachedLatestVersion();
112
+ if (latest && isNewerVersion(latest, VERSION)) {
113
+ process.stderr.write(
114
+ `[knit] update available: v${VERSION} installed, v${latest} on npm \u2014 restart Claude Code to upgrade (clear npx cache if needed: \`rm -rf ~/.npm/_npx/\`). Changelog: https://github.com/PDgit12/knit/blob/main/CHANGELOG.md
115
+ `
116
+ );
117
+ }
118
+ }, 250);
108
119
  const server = new Server(
109
120
  { name: "knit-brain", version: VERSION },
110
121
  {
@@ -0,0 +1,12 @@
1
+ import {
2
+ doctorCommand,
3
+ runDoctor
4
+ } from "./chunk-TE6NP6BZ.js";
5
+ import "./chunk-WADRF26Z.js";
6
+ import "./chunk-UTVFELXS.js";
7
+ import "./chunk-QQNHF4XY.js";
8
+ import "./chunk-27TA2ZQZ.js";
9
+ export {
10
+ doctorCommand,
11
+ runDoctor
12
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getRecentGlobalLearnings
3
- } from "./chunk-ORKWLA33.js";
3
+ } from "./chunk-OINYMLOV.js";
4
4
  import {
5
5
  loadKnowledgeBase
6
6
  } from "./chunk-WKQHCLLO.js";
@@ -1,16 +1,17 @@
1
1
  import {
2
2
  getBrain
3
- } from "./chunk-TWHNYJAJ.js";
4
- import "./chunk-HROSQ5MS.js";
3
+ } from "./chunk-KKLAJLPO.js";
5
4
  import {
6
5
  installAgentsForProject
7
- } from "./chunk-RZOVZYTF.js";
6
+ } from "./chunk-GATMQQK5.js";
7
+ import "./chunk-WADRF26Z.js";
8
+ import "./chunk-WKQHCLLO.js";
8
9
  import "./chunk-MOOVNMIN.js";
9
10
  import "./chunk-ST4X7LZT.js";
10
11
  import "./chunk-M3YZOJNW.js";
12
+ import "./chunk-POXT5OYN.js";
11
13
  import "./chunk-VB2TIR6L.js";
12
14
  import "./chunk-7UFS67HP.js";
13
- import "./chunk-WKQHCLLO.js";
14
15
  import "./chunk-27TA2ZQZ.js";
15
16
 
16
17
  // src/commands/install-agents.ts
@@ -1,10 +1,14 @@
1
1
  import {
2
+ CLAUDE_MD_BUDGET_BYTES,
2
3
  KNIT_INSTRUCTIONS,
3
4
  KNIT_INSTRUCTIONS_BASE,
5
+ buildBudgetVerdict,
4
6
  buildInstructions
5
- } from "./chunk-LV73YTVN.js";
7
+ } from "./chunk-QQNHF4XY.js";
6
8
  export {
9
+ CLAUDE_MD_BUDGET_BYTES,
7
10
  KNIT_INSTRUCTIONS,
8
11
  KNIT_INSTRUCTIONS_BASE,
12
+ buildBudgetVerdict,
9
13
  buildInstructions
10
14
  };
@@ -8,7 +8,9 @@ import {
8
8
  findFalsePositives
9
9
  } from "./chunk-M3YZOJNW.js";
10
10
  import {
11
- generateClaudeMd
11
+ KNIT_MARKER_START,
12
+ generateClaudeMd,
13
+ spliceKnitBlock
12
14
  } from "./chunk-7UFS67HP.js";
13
15
  import {
14
16
  knowledgePath,
@@ -57,9 +59,19 @@ async function refreshCommand(targetDir) {
57
59
  tokenOptimization: "standard"
58
60
  };
59
61
  const genSpinner = ora({ text: chalk.dim("Regenerating CLAUDE.md..."), spinner: "dots" }).start();
60
- writeFileSync(join(rootPath, "CLAUDE.md"), generateClaudeMd(config, knowledge, falsePositives), "utf-8");
62
+ const claudeMdPath = join(rootPath, "CLAUDE.md");
63
+ const newBlock = generateClaudeMd(config, knowledge, falsePositives);
64
+ const existing = existsSync(claudeMdPath) ? readFileSync(claudeMdPath, "utf-8") : "";
65
+ if (existing && existing.includes(KNIT_MARKER_START)) {
66
+ const { content } = spliceKnitBlock(existing, newBlock);
67
+ writeFileSync(claudeMdPath, content, "utf-8");
68
+ } else if (!existing) {
69
+ writeFileSync(claudeMdPath, newBlock, "utf-8");
70
+ } else {
71
+ genSpinner.warn(chalk.yellow("CLAUDE.md is user-curated (no Knit markers). Skipping write to avoid clobber."));
72
+ }
61
73
  writeFileSync(knowledgePath(rootPath), JSON.stringify(knowledge, null, 2), "utf-8");
62
- genSpinner.succeed(chalk.dim("CLAUDE.md + knowledge.json updated"));
74
+ if (genSpinner.isSpinning) genSpinner.succeed(chalk.dim("CLAUDE.md + knowledge.json updated"));
63
75
  console.log();
64
76
  console.log(chalk.bold(" Refresh complete"));
65
77
  if (falsePositives.length > 0) {
@@ -1,3 +1,11 @@
1
+ import {
2
+ runDoctor
3
+ } from "./chunk-TE6NP6BZ.js";
4
+ import "./chunk-WADRF26Z.js";
5
+ import "./chunk-UTVFELXS.js";
6
+ import "./chunk-QQNHF4XY.js";
7
+ import "./chunk-27TA2ZQZ.js";
8
+
1
9
  // src/commands/setup.ts
2
10
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
3
11
  import { join, dirname } from "path";
@@ -89,7 +97,7 @@ For new projects, call \`knit_brain_status\` first \u2014 triggers auto-initiali
89
97
  console.log(chalk.bold(" How it works"));
90
98
  console.log(` ${chalk.cyan("1.")} Open ${chalk.bold("any project")} in Claude Code`);
91
99
  console.log(` ${chalk.cyan("2.")} Agent calls \`knit_classify_task\` \u2192 brain auto-initializes`);
92
- console.log(` ${chalk.cyan("3.")} Agent gets 35 tools: imports, exports, tests, learnings, teams`);
100
+ console.log(` ${chalk.cyan("3.")} Agent gets 53+ tools: imports, exports, tests, learnings, teams, requirements`);
93
101
  console.log(` ${chalk.cyan("4.")} Brain compounds with every session \u2014 gets smarter over time`);
94
102
  console.log();
95
103
  console.log(chalk.dim(" No CLI needed after this. The MCP server handles everything."));
@@ -98,6 +106,28 @@ For new projects, call \`knit_brain_status\` first \u2014 triggers auto-initiali
98
106
  console.log(chalk.dim(` Config written to: ${settingsPath}`));
99
107
  }
100
108
  console.log();
109
+ console.log(chalk.bold(" Install health check"));
110
+ console.log();
111
+ try {
112
+ const report = runDoctor(process.cwd());
113
+ for (const c of report.checks) {
114
+ const icon = c.status === "ok" ? chalk.green("\u2713") : c.status === "warn" ? chalk.yellow("\u26A0") : c.status === "error" ? chalk.red("\u2717") : chalk.gray("\xB7");
115
+ console.log(` ${icon} ${c.name.padEnd(22)} ${chalk.dim(c.detail)}`);
116
+ }
117
+ const errors = report.checks.filter((c) => c.status === "error").length;
118
+ const warnings = report.checks.filter((c) => c.status === "warn").length;
119
+ console.log();
120
+ if (errors > 0) {
121
+ console.log(chalk.red(` ${errors} error(s) \u2014 run \`engram doctor\` for full details + fix commands.`));
122
+ } else if (warnings > 0) {
123
+ console.log(chalk.yellow(` ${warnings} warning(s) \u2014 setup complete, but check items above.`));
124
+ } else {
125
+ console.log(chalk.green(" All checks passed \u2014 install is healthy."));
126
+ }
127
+ } catch (err) {
128
+ console.log(chalk.dim(` (doctor skipped: ${err.message})`));
129
+ }
130
+ console.log();
101
131
  }
102
132
  export {
103
133
  setupCommand
@@ -1,40 +1,28 @@
1
- import {
2
- notifyToolsListChanged
3
- } from "./chunk-WMESQUZU.js";
4
- import {
5
- KNIT_INSTRUCTIONS
6
- } from "./chunk-LV73YTVN.js";
7
- import {
8
- VERSION
9
- } from "./chunk-UTVFELXS.js";
10
1
  import {
11
2
  appendSession,
12
- getCachedLatestVersion,
13
3
  getRecentSessions,
14
4
  installAgentsForProject,
15
- isNewerVersion,
16
5
  loadAllSessions,
17
6
  pruneSessionsByAge,
18
7
  searchSessions,
19
8
  sessionCount
20
- } from "./chunk-RZOVZYTF.js";
21
- import {
22
- scanProject,
23
- scanProjectFingerprint
24
- } from "./chunk-ST4X7LZT.js";
25
- import {
26
- loadScanResult,
27
- persistScanResult,
28
- scanIntegrations
29
- } from "./chunk-VB2TIR6L.js";
30
- import "./chunk-7UFS67HP.js";
9
+ } from "./chunk-GATMQQK5.js";
31
10
  import {
32
11
  appendGlobalLearning,
33
12
  buildGlobalLearning,
34
13
  getRecentGlobalLearnings,
35
14
  loadAllGlobalLearnings,
36
15
  searchGlobalLearnings
37
- } from "./chunk-ORKWLA33.js";
16
+ } from "./chunk-OINYMLOV.js";
17
+ import {
18
+ notifyToolsListChanged
19
+ } from "./chunk-WMESQUZU.js";
20
+ import {
21
+ VERSION
22
+ } from "./chunk-UTVFELXS.js";
23
+ import {
24
+ KNIT_INSTRUCTIONS
25
+ } from "./chunk-QQNHF4XY.js";
38
26
  import {
39
27
  addEntry,
40
28
  bumpClassificationTier,
@@ -45,6 +33,20 @@ import {
45
33
  recordCacheHit,
46
34
  saveKnowledgeBase
47
35
  } from "./chunk-WKQHCLLO.js";
36
+ import {
37
+ scanProject,
38
+ scanProjectFingerprint
39
+ } from "./chunk-ST4X7LZT.js";
40
+ import {
41
+ getCachedLatestVersion,
42
+ isNewerVersion
43
+ } from "./chunk-POXT5OYN.js";
44
+ import {
45
+ loadScanResult,
46
+ persistScanResult,
47
+ scanIntegrations
48
+ } from "./chunk-VB2TIR6L.js";
49
+ import "./chunk-7UFS67HP.js";
48
50
  import {
49
51
  calibrationPath,
50
52
  canonicalRepoRoot,
@@ -395,7 +397,48 @@ function tools(_) {
395
397
  | \`knit_define_team\` | Create a custom team. |
396
398
  | \`knit_start_team_review\` | Start a parallel review board. |
397
399
  | \`knit_post_team_findings\` | Each team posts to the shared board. |
398
- | \`knit_get_board_summary\` | Cross-team findings, severity-gated. |`;
400
+ | \`knit_get_board_summary\` | Cross-team findings, severity-gated. |
401
+ | \`knit_spawn_team_worktree\` | Spawn a git worktree for a team to do isolated parallel writes. |
402
+ | \`knit_list_team_worktrees\` | List active team worktrees. |
403
+ | \`knit_finalize_team_worktree\` | Merge or discard a team worktree branch. |
404
+
405
+ **Cross-project memory** (v0.4+):
406
+ | Tool | Use when |
407
+ |------|----------|
408
+ | \`knit_record_global_learning\` | Cross-project insight (applies beyond current repo). |
409
+ | \`knit_search_global_learnings\` | Search learnings across all your projects. |
410
+ | \`knit_reflect\` | Surface candidate patterns from session history. |
411
+ | \`knit_get_suggestions\` | Adaptive warnings derived from past patterns for given domains. |
412
+ | \`knit_install_agent\` | Fetch a specialized VoltAgent on demand. |
413
+ | \`knit_prune_sessions\` | Garbage-collect old session log entries. |
414
+ | \`knit_consolidate_learnings\` | Promote stable patterns to consolidated tier. |
415
+ | \`knit_get_learning\` | Fetch a single learning by id. |
416
+
417
+ **Feature gating + protocol guard** (v0.5+):
418
+ | Tool | Use when |
419
+ |------|----------|
420
+ | \`knit_list_features\` | See which tools are hidden behind tier gates and how to enable them. |
421
+ | \`knit_enable_feature\` / \`knit_disable_feature\` | Toggle a feature on/off for this project. |
422
+ | \`knit_set_protocol_strictness\` / \`knit_get_protocol_strictness\` | off / warn / block \u2014 controls Protocol Guard hook. |
423
+ | \`knit_scan_integrations\` | Detect external integrations the project uses. |
424
+ | \`knit_compounding_metrics\` | Per-session token + hit-rate metrics. |
425
+ | \`knit_get_metrics_history\` | History of compounding metrics for trend lines. |
426
+ | \`knit_verify_claim\` | Cheap fact-check against the knowledge graph before quoting code. |
427
+
428
+ **Self-healing classifier** (v0.11):
429
+ | Tool | Use when |
430
+ |------|----------|
431
+ | \`knit_get_calibration\` / \`knit_reset_calibration\` | Inspect or reset per-project classifier tuning. |
432
+ | \`knit_get_fingerprint\` | Project fingerprint used for shape-aware tier gating. |
433
+ | \`knit_infer_domains\` | Auto-derive domain tags from files. |
434
+ | \`knit_compose_template\` | Compose a generated artifact from a template. |
435
+
436
+ **Requirements ingestion** (v0.11):
437
+ | Tool | Use when |
438
+ |------|----------|
439
+ | \`knit_index_requirements\` | Chunk + BM25-index a long spec for retrieval. |
440
+ | \`knit_generate_test_cases\` | Retrieve relevant chunks for a feature query (token-bounded). |
441
+ | \`knit_list_requirements\` / \`knit_delete_requirements\` | Manage indexed sources. |`;
399
442
  }
400
443
 
401
444
  // src/engine/worktrees.ts
@@ -639,7 +682,6 @@ function classifyOrigin(supporting) {
639
682
  else global = true;
640
683
  if (local && global) return "mixed";
641
684
  }
642
- if (local && global) return "mixed";
643
685
  return local ? "local" : "global";
644
686
  }
645
687
  function getAdaptiveSuggestions(kb, taskDomains) {
@@ -1487,6 +1529,9 @@ function chunkRequirements(content, minChars = 50) {
1487
1529
  return chunks;
1488
1530
  }
1489
1531
  function saveSource(rootPath, source) {
1532
+ if (typeof source.sourceId !== "string" || !/^[A-Za-z0-9._-]{1,80}$/.test(source.sourceId)) {
1533
+ throw new Error(`[knit] saveSource: invalid sourceId "${source.sourceId}"`);
1534
+ }
1490
1535
  const path = requirementSourcePath(rootPath, source.sourceId);
1491
1536
  mkdirSync5(dirname5(path), { recursive: true });
1492
1537
  const tmp = `${path}.tmp`;
@@ -1494,6 +1539,7 @@ function saveSource(rootPath, source) {
1494
1539
  renameSync3(tmp, path);
1495
1540
  }
1496
1541
  function loadSource(rootPath, sourceId) {
1542
+ if (typeof sourceId !== "string" || !/^[A-Za-z0-9._-]{1,80}$/.test(sourceId)) return null;
1497
1543
  const path = requirementSourcePath(rootPath, sourceId);
1498
1544
  if (!existsSync5(path)) return null;
1499
1545
  try {
@@ -1883,8 +1929,8 @@ function handleSearchLearnings(params, brain) {
1883
1929
  const limit = Math.max(1, Math.min(50, parseInt(params.limit || "10", 10) || 10));
1884
1930
  if (!query && domains.length === 0) {
1885
1931
  return JSON.stringify({
1932
+ status: "error",
1886
1933
  error: "Provide either query (BM25 free-text) or domains (tag filter), or both. query=auth domains=#api filters BM25 results to entries tagged #api.",
1887
- query: [],
1888
1934
  results: [],
1889
1935
  count: 0
1890
1936
  });
@@ -1966,20 +2012,23 @@ var TOKEN_BUDGETS = {
1966
2012
  /** Generated CLAUDE.md block. v0.7 trim landed at ~2KB on typical projects;
1967
2013
  * 6.5KB target allows for projects with many domains / large project map. */
1968
2014
  claude_md_bytes: 6500,
1969
- /** Tier-gated tools/list response. v0.12 typical: 38 Tier-1 active × ~280
1970
- * bytes ≈ 10.6KB. 11KB target allows headroom for the v0.12 + v0.13
1971
- * growth without immediately warning; full 51-tool exposure
1972
- * (everything enabled) sits in warn range, surfacing the bloat. */
1973
- tool_registry_bytes: 11e3,
2015
+ /** Tier-gated tools/list response. v0.12 typical: 40 Tier-1 active × ~280
2016
+ * bytes ≈ 11.2KB. 12KB target gives Tier-1 healthy headroom; full
2017
+ * 51-tool exposure (everything enabled) sits in warn range, surfacing
2018
+ * the bloat. Bumped from 11000 in v0.12 to reflect actual Tier-1 size. */
2019
+ tool_registry_bytes: 12e3,
1974
2020
  /** MCP server `instructions` field — sent at handshake. v0.11.1 surfaces
1975
2021
  * 9 new tools (verify_claim, calibration, requirements ingestion,
1976
- * fingerprint, infer_domains, compose_template) → ~3.5KB. The
1977
- * discoverability-vs-budget trade-off favors surfacing real tools. */
2022
+ * fingerprint, infer_domains, compose_template) → ~3.5KB. v0.12 may
2023
+ * append a one-line budget verdict (~200B) when CLAUDE.md is over
2024
+ * budget. The discoverability-vs-budget trade-off favors surfacing
2025
+ * real tools. */
1978
2026
  instructions_bytes: 4e3,
1979
2027
  /** Sum of the three above — the per-session fixed cost Knit imposes.
1980
- * v0.12 typical: ~14KB (CLAUDE.md ~2KB + tools ~10.6KB + instructions ~2KB);
1981
- * 20KB target covers the union with slack as more tools come online. */
1982
- per_session_overhead_bytes: 2e4
2028
+ * v0.12 typical: ~15KB (CLAUDE.md ~2KB + tools ~11.2KB + instructions
2029
+ * ~2.6KB); 22KB target covers the union with slack as more tools
2030
+ * come online. Bumped from 20000 alongside tool_registry. */
2031
+ per_session_overhead_bytes: 22e3
1983
2032
  };
1984
2033
  function verdict(actual, target) {
1985
2034
  if (actual <= target) return "healthy";
@@ -2889,9 +2938,7 @@ function handleGetLearning(params, brain) {
2889
2938
  if (!id) return errorResponse("id parameter is required");
2890
2939
  const entry = brain.knowledgeBase.entries.find((e) => e.id === id);
2891
2940
  if (!entry) {
2892
- return JSON.stringify({
2893
- error: `No learning with id="${id}". List active ones via knit_search_learnings (default returns id + summary).`
2894
- });
2941
+ return errorResponse(`No learning with id="${id}". List active ones via knit_search_learnings (default returns id + summary).`);
2895
2942
  }
2896
2943
  recordCacheHit(brain.knowledgeBase);
2897
2944
  return JSON.stringify({
@@ -2914,11 +2961,11 @@ function handleRecordLearning(params, brain) {
2914
2961
  const entry = {
2915
2962
  date,
2916
2963
  summary: redactSecrets(params.summary || "Untitled learning"),
2917
- domains: (params.domains || "general").split(",").map((d) => d.trim()),
2964
+ domains: (params.domains || "general").split(",").map((d) => redactSecrets(d.trim())),
2918
2965
  approach: redactSecrets(params.approach || ""),
2919
2966
  outcome: ["success", "partial", "failure"].includes(params.outcome) ? params.outcome : "success",
2920
2967
  lesson: redactSecrets(params.lesson || ""),
2921
- tags: (params.tags || "").split(/\s+/).filter((t) => t.startsWith("#"))
2968
+ tags: (params.tags || "").split(/\s+/).filter((t) => t.startsWith("#")).map((t) => redactSecrets(t))
2922
2969
  };
2923
2970
  addEntry(brain.knowledgeBase, entry);
2924
2971
  saveKnowledgeBase(knowledgebasePath(brain.rootPath), brain.knowledgeBase);
@@ -2946,7 +2993,7 @@ function handleRecordLearning(params, brain) {
2946
2993
  }
2947
2994
  function handleRecordFalsePositive(params, brain) {
2948
2995
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2949
- const tags = [...(params.tags || "").split(/\s+/).filter((t) => t.startsWith("#")), "#false-positive"];
2996
+ const tags = [...(params.tags || "").split(/\s+/).filter((t) => t.startsWith("#")).map((t) => redactSecrets(t)), "#false-positive"];
2950
2997
  const entry = {
2951
2998
  date,
2952
2999
  summary: redactSecrets(params.summary || "Untitled FP"),
@@ -3085,7 +3132,8 @@ function handleIndexRequirements(params, brain) {
3085
3132
  return JSON.stringify({ status: "error", error: "Invalid source_id \u2014 must be 1-80 chars, alphanumeric + . _ - only" });
3086
3133
  }
3087
3134
  const sourceId = userSourceId || slugifySourceId(filePath);
3088
- const label = (params.label || "").trim() || void 0;
3135
+ const rawLabel = (params.label || "").trim();
3136
+ const label = rawLabel ? redactSecrets(rawLabel) : void 0;
3089
3137
  const source = {
3090
3138
  sourceId,
3091
3139
  sourcePath: filePath,
@@ -3142,8 +3190,13 @@ function handleGenerateTestCases(params, brain) {
3142
3190
  if (s) sourcesToSearch.push(s);
3143
3191
  }
3144
3192
  }
3193
+ const MAX_RESPONSE_BYTES = 100 * 1024;
3145
3194
  const hits = retrieveTopChunks(sourcesToSearch, feature, topN);
3146
- const contextBytes = hits.reduce((s, h) => s + Buffer.byteLength(h.chunk.text, "utf-8"), 0);
3195
+ let contextBytes = hits.reduce((s, h) => s + Buffer.byteLength(h.chunk.text, "utf-8"), 0);
3196
+ while (contextBytes > MAX_RESPONSE_BYTES && hits.length > 1) {
3197
+ const dropped = hits.pop();
3198
+ contextBytes -= Buffer.byteLength(dropped.chunk.text, "utf-8");
3199
+ }
3147
3200
  const totalBytes = sourcesToSearch.reduce((s, src) => s + src.sourceBytes, 0);
3148
3201
  const reductionPct = totalBytes > 0 ? Math.round(100 - contextBytes / totalBytes * 100) : 0;
3149
3202
  return JSON.stringify({
@@ -3510,6 +3563,57 @@ function parseLoadSessionInclude(raw) {
3510
3563
  raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
3511
3564
  );
3512
3565
  }
3566
+ function computeBudgetHealth(brain) {
3567
+ let claudeMdBytes = 0;
3568
+ try {
3569
+ claudeMdBytes = statSync3(join3(brain.rootPath, "CLAUDE.md")).size;
3570
+ } catch {
3571
+ }
3572
+ const shape = detectProjectShape(brain);
3573
+ const listing = computeFeatureListing(shape);
3574
+ const toolRegistryBytes = listing.totals.active * 280;
3575
+ const instructionsBytes = KNIT_INSTRUCTIONS.length;
3576
+ const perSessionOverheadBytes = claudeMdBytes + toolRegistryBytes + instructionsBytes;
3577
+ const cv = verdict(claudeMdBytes, TOKEN_BUDGETS.claude_md_bytes);
3578
+ const tv = verdict(toolRegistryBytes, TOKEN_BUDGETS.tool_registry_bytes);
3579
+ const iv = verdict(instructionsBytes, TOKEN_BUDGETS.instructions_bytes);
3580
+ const ov = verdict(perSessionOverheadBytes, TOKEN_BUDGETS.per_session_overhead_bytes);
3581
+ const verdicts = [cv, tv, iv, ov];
3582
+ const overall = verdicts.includes("over-budget") ? "over-budget" : verdicts.includes("warn") ? "warn" : "healthy";
3583
+ if (overall === "healthy") return void 0;
3584
+ const rank = (v) => v === "over-budget" ? 2 : v === "warn" ? 1 : 0;
3585
+ const surfaces = [
3586
+ ["claude_md", cv],
3587
+ ["tool_registry", tv],
3588
+ ["instructions", iv]
3589
+ ];
3590
+ surfaces.sort((a, b) => rank(b[1]) - rank(a[1]));
3591
+ const worst = surfaces[0][0];
3592
+ const suggestions = {
3593
+ claude_md: "CLAUDE.md is over the 6.5KB target \u2014 run `engram refresh` to splice the lean marker-block, or check that the file is using the generator (not hand-curated).",
3594
+ tool_registry: "Tool registry over budget \u2014 call knit_list_features to see which Tier-2/3 tools are active and disable any you do not need.",
3595
+ instructions: "Instructions block over budget \u2014 likely a v0.x \u2192 v0.y growth. Restart Claude Code to pick up the trimmed instructions."
3596
+ };
3597
+ return {
3598
+ verdict: overall,
3599
+ per_session_kb: Math.round(perSessionOverheadBytes / 1024 * 10) / 10,
3600
+ worst_surface: worst,
3601
+ suggestion: suggestions[worst]
3602
+ };
3603
+ }
3604
+ function computeLearningsHealth(brain) {
3605
+ const total = brain.knowledgeBase.entries.length;
3606
+ if (total < 5) return void 0;
3607
+ const accessed = brain.knowledgeBase.entries.filter((e) => e.accessCount > 0).length;
3608
+ const pct = total > 0 ? Math.round(accessed / total * 100) : 0;
3609
+ if (pct >= 30) return { total, accessed_pct: pct, verdict: "healthy" };
3610
+ return {
3611
+ total,
3612
+ accessed_pct: pct,
3613
+ verdict: "low-utilization",
3614
+ suggestion: `${total} learnings recorded but only ${pct}% have been recalled. Either call knit_search_learnings before re-investigating, or prune stale entries with knit_consolidate_learnings.`
3615
+ };
3616
+ }
3513
3617
  function handleLoadSession(params, brain) {
3514
3618
  const include = parseLoadSessionInclude(params.include);
3515
3619
  const wantAll = include.has("all");
@@ -3581,6 +3685,18 @@ function handleLoadSession(params, brain) {
3581
3685
  if (wantAll || include.has("patterns")) {
3582
3686
  patterns = reflect(brain.knowledgeBase).slice(0, 3).map((p) => ({ type: p.type, description: p.description, confidence: p.confidence }));
3583
3687
  }
3688
+ let updateAvailable;
3689
+ const cachedLatest = getCachedLatestVersion();
3690
+ if (cachedLatest && isNewerVersion(cachedLatest, VERSION)) {
3691
+ updateAvailable = {
3692
+ current: VERSION,
3693
+ latest: cachedLatest,
3694
+ upgrade: "Restart Claude Code (quit fully + reopen) to spawn a fresh MCP. If npx serves cache, run: rm -rf ~/.npm/_npx/$(ls ~/.npm/_npx | head -1) then reopen.",
3695
+ changelog: "https://github.com/PDgit12/knit/blob/main/CHANGELOG.md"
3696
+ };
3697
+ }
3698
+ const budgetHealth = computeBudgetHealth(brain);
3699
+ const learningsHealth = computeLearningsHealth(brain);
3584
3700
  const response = {
3585
3701
  session_context: {
3586
3702
  last_session: lastSession,
@@ -3598,6 +3714,9 @@ function handleLoadSession(params, brain) {
3598
3714
  ...metrics !== void 0 ? { metrics } : {},
3599
3715
  ...recentSessions !== void 0 ? { recent_sessions: recentSessions } : {}
3600
3716
  },
3717
+ ...updateAvailable ? { update_available: updateAvailable } : {},
3718
+ ...budgetHealth ? { budget_health: budgetHealth } : {},
3719
+ ...learningsHealth && learningsHealth.verdict === "low-utilization" ? { learnings_health: learningsHealth } : {},
3601
3720
  instruction: handoff2 ? "UNFINISHED WORK DETECTED. Read the handoff above \u2014 pick up where the last session left off. Do NOT start fresh." : topLearnings.length > 0 ? `Session loaded. ${topLearnings.length} key learnings, ${fps.length} false positives. Call knit_classify_task to begin. Use include=patterns,teams,metrics,recent_sessions,full_learnings,full_knowledge for more.` : "Fresh brain \u2014 no past learnings yet. Call knit_classify_task to begin."
3602
3721
  };
3603
3722
  return JSON.stringify(response);
@@ -3611,7 +3730,7 @@ function handleSaveSessionSummary(params, brain) {
3611
3730
  date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
3612
3731
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3613
3732
  summary: redactSecrets((params.summary || "").slice(0, 500)),
3614
- tags: (params.tags || "").split(/\s+/).filter((t) => t.startsWith("#")),
3733
+ tags: (params.tags || "").split(/\s+/).filter((t) => t.startsWith("#")).map((t) => redactSecrets(t)),
3615
3734
  outcome,
3616
3735
  filesTouched: params.files_touched ? params.files_touched.split(",").map((f) => f.trim()).filter(Boolean) : void 0,
3617
3736
  domainsTouched: params.domains ? params.domains.split(",").map((d) => d.trim()).filter(Boolean) : void 0
@@ -3813,32 +3932,32 @@ function getToolDefinitions() {
3813
3932
  // ── Query (read the brain) ───────────────────────────────────
3814
3933
  {
3815
3934
  name: "knit_query_imports",
3816
- description: "Reverse deps for a file \u2014 who imports it.",
3935
+ description: "[GRAPH] Reverse deps \u2014 WHO IMPORTS this file. Use to find blast radius before editing.",
3817
3936
  inputSchema: { type: "object", properties: { file_path: { type: "string", description: "Relative file path." } }, required: ["file_path"] }
3818
3937
  },
3819
3938
  {
3820
3939
  name: "knit_query_dependents",
3821
- description: "Forward deps for a file \u2014 what it imports.",
3940
+ description: "[GRAPH] Forward deps \u2014 WHAT THIS FILE IMPORTS. Opposite of knit_query_imports (who imports it).",
3822
3941
  inputSchema: { type: "object", properties: { file_path: { type: "string", description: "Relative file path." } }, required: ["file_path"] }
3823
3942
  },
3824
3943
  {
3825
3944
  name: "knit_query_exports",
3826
- description: "Exports from a file: functions, classes, types, constants.",
3945
+ description: "[GRAPH] Exports from a file: functions, classes, types, constants. Use when verifying a claim or finding the canonical definition.",
3827
3946
  inputSchema: { type: "object", properties: { file_path: { type: "string", description: "Relative file path." } }, required: ["file_path"] }
3828
3947
  },
3829
3948
  {
3830
3949
  name: "knit_query_tests",
3831
- description: 'Tests for a file, or list all untested files with filter="untested".',
3950
+ description: '[GRAPH] Tests covering a file, OR list all untested files with filter="untested". Use before declaring "tested" on changed code.',
3832
3951
  inputSchema: { type: "object", properties: { file_path: { type: "string", description: "Relative file path (optional)." }, filter: { type: "string", description: '"untested" to list all untested files.' } } }
3833
3952
  },
3834
3953
  {
3835
3954
  name: "knit_find_fanout",
3836
- description: "High-fanout files \u2014 imported by many others.",
3955
+ description: "[GRAPH] High-fanout files \u2014 imported by many others. Editing these is high-risk; surfaces in classify_task risk_tier.",
3837
3956
  inputSchema: { type: "object", properties: { min_importers: { type: "string", description: "Minimum importers to qualify (default: 3)." } } }
3838
3957
  },
3839
3958
  {
3840
3959
  name: "knit_search_learnings",
3841
- description: 'BM25 + import-graph hybrid. Pass query="text" for BM25, domains="#tag" filter, files="src/a.ts,src/b.ts" for graph boost on learnings about neighbors. All combinable.',
3960
+ description: "[MEMORY] Search THIS PROJECT's learnings. Use BEFORE re-investigating. Companions: knit_search_global_learnings (cross-project), knit_search_sessions (past tasks). BM25 + graph boost via files=.",
3842
3961
  inputSchema: {
3843
3962
  type: "object",
3844
3963
  properties: {
@@ -3862,7 +3981,7 @@ function getToolDefinitions() {
3862
3981
  // ── Update (write to the brain) ──────────────────────────────
3863
3982
  {
3864
3983
  name: "knit_classify_task",
3865
- description: "Call first. Returns risk_tier (drives plan mode), scope_tier (drives phases), change_kind, phases, auto_plan_mode, tier. Optional context_budget_remaining (0-100) downgrades gracefully.",
3984
+ description: "[PROTOCOL] BEFORE any Edit/Write. Returns risk_tier (drives plan mode), scope_tier (drives phases), change_kind, auto_plan_mode. Optional context_budget_remaining (0-100) downgrades gracefully.",
3866
3985
  inputSchema: { type: "object", properties: { files_to_touch: { type: "string", description: 'Comma-separated files, or "unknown" for new projects.' }, description: { type: "string", description: "Brief task description." }, verbose: { type: "string", description: '"true" to include reasoning + cross_domain_ripple + files_count (debug fields).' }, context_budget_remaining: { type: "string", description: "Integer 0\u2013100 \u2014 percent of host agent context window remaining. <30 triggers scope downgrade + skips OPTIMIZE phase. Defaults to 100." } }, required: ["files_to_touch"] }
3867
3986
  },
3868
3987
  {
@@ -3872,7 +3991,7 @@ function getToolDefinitions() {
3872
3991
  },
3873
3992
  {
3874
3993
  name: "knit_record_learning",
3875
- description: "Record a non-obvious, reusable insight. Skip if a future search wouldn't be glad it exists.",
3994
+ description: "[MEMORY-WRITE] Record a non-obvious THIS-PROJECT insight. Skip substring repeats. For cross-project: knit_record_global_learning. For FPs: knit_record_false_positive.",
3876
3995
  inputSchema: { type: "object", properties: { summary: { type: "string", description: "One-line summary." }, domains: { type: "string", description: "Comma-separated domains." }, approach: { type: "string", description: "What approach was taken." }, outcome: { type: "string", description: "success | partial | failure." }, lesson: { type: "string", description: "What to repeat or avoid." }, tags: { type: "string", description: 'Space-separated tags (e.g. "#api #auth").' } }, required: ["summary", "lesson", "tags"] }
3877
3996
  },
3878
3997
  {
@@ -3927,7 +4046,7 @@ function getToolDefinitions() {
3927
4046
  },
3928
4047
  {
3929
4048
  name: "knit_save_handoff",
3930
- description: "Save state for the next session. failed_attempts is the load-bearing field.",
4049
+ description: "[END OF SESSION \u2014 UNFINISHED] Save state when work is incomplete or context degraded. failed_attempts is the load-bearing field. For finished work use knit_save_session_summary.",
3931
4050
  inputSchema: { type: "object", properties: { goal: { type: "string", description: "What we're trying to accomplish." }, current_state: { type: "string", description: "Where we are now." }, files_in_flight: { type: "string", description: "Files being modified." }, what_changed: { type: "string", description: "Commits and edits." }, failed_attempts: { type: "string", description: "What was tried and why it failed." }, decisions_made: { type: "string", description: "Important choices." }, next_step: { type: "string", description: "ONE most important next thing." } }, required: ["goal", "current_state", "failed_attempts", "next_step"] }
3932
4051
  },
3933
4052
  {
@@ -3978,12 +4097,12 @@ function getToolDefinitions() {
3978
4097
  // ── Session memory ───────────────────────────────────────────
3979
4098
  {
3980
4099
  name: "knit_load_session",
3981
- description: "Call at session start. Returns handoff, top learnings, false positives by default. Opt in to more via include=patterns,teams,metrics,recent_sessions,full_learnings,full_knowledge,all.",
4100
+ description: "[PROTOCOL FIRST] Call once at session start. Returns handoff, top learnings, FPs, update_available. Opt in via include=patterns,teams,metrics,recent_sessions,full_learnings,all.",
3982
4101
  inputSchema: { type: "object", properties: { include: { type: "string", description: "Comma-separated optional sections." } } }
3983
4102
  },
3984
4103
  {
3985
4104
  name: "knit_save_session_summary",
3986
- description: "Opt-in. Record a session summary a future search would want to find.",
4105
+ description: "[END OF SESSION] Record a session summary so future knit_search_sessions can find this work. Pair with knit_save_handoff when work is unfinished.",
3987
4106
  inputSchema: {
3988
4107
  type: "object",
3989
4108
  properties: {
@@ -4008,7 +4127,7 @@ function getToolDefinitions() {
4008
4127
  },
4009
4128
  {
4010
4129
  name: "knit_search_sessions",
4011
- description: 'Search past sessions by free text. "Have I done this before?"',
4130
+ description: '[MEMORY] "Have I done this task before?" \u2014 search past SESSION summaries. Different from knit_search_learnings (lessons) and knit_search_global_learnings (cross-project).',
4012
4131
  inputSchema: {
4013
4132
  type: "object",
4014
4133
  properties: {
@@ -4055,7 +4174,7 @@ function getToolDefinitions() {
4055
4174
  // ── Cross-project learnings (Model C — global pool) ─────────
4056
4175
  {
4057
4176
  name: "knit_record_global_learning",
4058
- description: "Opt-in. Record a learning to the cross-project pool when it generalizes beyond this project.",
4177
+ description: "[MEMORY-WRITE] Record a learning that generalizes across MULTIPLE projects. Sparingly \u2014 most learnings are project-specific. Companion: knit_record_learning (this project only).",
4059
4178
  inputSchema: {
4060
4179
  type: "object",
4061
4180
  properties: {
@@ -4069,7 +4188,7 @@ function getToolDefinitions() {
4069
4188
  },
4070
4189
  {
4071
4190
  name: "knit_search_global_learnings",
4072
- description: "Search the cross-project learnings pool across all projects on this machine.",
4191
+ description: "[MEMORY] Search learnings ACROSS ALL projects on this machine. Use when starting a new domain. Companion: knit_search_learnings (this project only).",
4073
4192
  inputSchema: {
4074
4193
  type: "object",
4075
4194
  properties: {
@@ -4166,12 +4285,12 @@ function getToolDefinitions() {
4166
4285
  },
4167
4286
  {
4168
4287
  name: "knit_verify_claim",
4169
- description: 'Fact-check one claim against the knowledge graph. Patterns: "A imports B", "X exports Y", "A is tested by B", "X exists". Verdict: verified | contradicted | unparseable.',
4288
+ description: '[REVIEW] Fact-check one claim before LEARN on standard/complex scope. Patterns: "A imports B", "X exports Y", "A is tested by B", "X exists". Verdict: verified | contradicted | unparseable.',
4170
4289
  inputSchema: { type: "object", properties: { claim: { type: "string", description: "One claim about the codebase to verify." } }, required: ["claim"] }
4171
4290
  },
4172
4291
  {
4173
4292
  name: "knit_get_learning",
4174
- description: "Fetch one full learning by id. Pair with knit_search_learnings (default returns headlines) for hierarchical retrieval \u2014 expand only what you need.",
4293
+ description: "[MEMORY] Expand ONE learning by id (from a prior knit_search_learnings hit). Hierarchical retrieval \u2014 search returns headlines, this fetches details. Saves tokens vs. dumping full bodies.",
4175
4294
  inputSchema: { type: "object", properties: { id: { type: "string", description: "Learning id from a prior knit_search_learnings result." } }, required: ["id"] }
4176
4295
  },
4177
4296
  {
@@ -4260,7 +4379,7 @@ function handleToolCall(toolName, params, brain) {
4260
4379
  const allowAbsolute = toolName === "knit_index_requirements";
4261
4380
  const bad = decoded.includes("..") || decoded.includes("\0") || !allowAbsolute && (decoded.startsWith("/") || /^[A-Za-z]:\//.test(decoded));
4262
4381
  if (bad) {
4263
- return JSON.stringify({ error: "Invalid file path \u2014 no traversal or absolute paths allowed" });
4382
+ return JSON.stringify({ status: "error", error: "Invalid file path \u2014 no traversal or absolute paths allowed" });
4264
4383
  }
4265
4384
  params.file_path = decoded;
4266
4385
  }
@@ -4271,7 +4390,7 @@ function handleToolCall(toolName, params, brain) {
4271
4390
  }
4272
4391
  const handler = handlers[toolName];
4273
4392
  if (!handler) {
4274
- return JSON.stringify({ error: `Unknown tool: ${toolName}` });
4393
+ return JSON.stringify({ status: "error", error: `Unknown tool: ${toolName}` });
4275
4394
  }
4276
4395
  return handler(params, brain);
4277
4396
  }
@@ -0,0 +1,14 @@
1
+ import {
2
+ __resetUpdateCheckForTests,
3
+ __setCachedLatestForTests,
4
+ getCachedLatestVersion,
5
+ isNewerVersion,
6
+ prewarmLatestVersion
7
+ } from "./chunk-POXT5OYN.js";
8
+ export {
9
+ __resetUpdateCheckForTests,
10
+ __setCachedLatestForTests,
11
+ getCachedLatestVersion,
12
+ isNewerVersion,
13
+ prewarmLatestVersion
14
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knit-mcp",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "description": "Knit — second brain for Claude Code. MCP server giving any AI agent project-scoped memory, tiered workflow protocol, and parallel team worktrees.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,7 +14,10 @@
14
14
  "lint": "eslint src/ tests/ --ext .ts",
15
15
  "test": "vitest run",
16
16
  "test:watch": "vitest",
17
- "bench": "tsx benchmarks/retrieval-synthetic.ts",
17
+ "bench": "npm run bench:retrieval",
18
+ "bench:retrieval": "tsx benchmarks/retrieval-synthetic.ts",
19
+ "bench:tokens": "tsx benchmarks/token-economy.ts",
20
+ "bench:all": "npm run bench:retrieval && npm run bench:tokens",
18
21
  "prepublishOnly": "npm run typecheck && npm run lint && npm run test && npm run build"
19
22
  },
20
23
  "keywords": [
@@ -1,12 +1,12 @@
1
- import {
2
- readLearnings
3
- } from "./chunk-M3YZOJNW.js";
4
1
  import {
5
2
  getKBSummary,
6
3
  getStaleEntries,
7
4
  getTopEntries,
8
5
  loadKnowledgeBase
9
6
  } from "./chunk-WKQHCLLO.js";
7
+ import {
8
+ readLearnings
9
+ } from "./chunk-M3YZOJNW.js";
10
10
  import {
11
11
  knowledgePath,
12
12
  knowledgebasePath,