knit-mcp 0.11.2 → 0.11.4

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,8 @@ 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.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. |
456
+ | **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
457
  | **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
458
  | **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
459
  | **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";
5
+ } from "./chunk-I63UMEBF.js";
6
6
  import "./chunk-HROSQ5MS.js";
7
- import "./chunk-RZOVZYTF.js";
7
+ import "./chunk-GATMQQK5.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,
@@ -4,9 +4,13 @@ import {
4
4
  } from "./chunk-HROSQ5MS.js";
5
5
  import {
6
6
  installAgentsForProject,
7
- prewarmLatestVersion,
8
7
  pruneSessionsByAge
9
- } from "./chunk-RZOVZYTF.js";
8
+ } from "./chunk-GATMQQK5.js";
9
+ import {
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
+ };
package/dist/cli.js CHANGED
@@ -20,10 +20,10 @@ async function runCLI() {
20
20
  const gradient = (await import("gradient-string")).default;
21
21
  const chalk = (await import("chalk")).default;
22
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");
23
+ const { statusCommand } = await import("./status-2SEITNIE.js");
24
+ const { refreshCommand } = await import("./refresh-S62AZ3QA.js");
25
+ const { installAgentsCommand } = await import("./install-agents-WDBQBWMN.js");
26
+ const { exportCommand } = await import("./export-4BO6HCXP.js");
27
27
  const { doctorCommand } = await import("./doctor-4DN2P2JR.js");
28
28
  const ENGRAM_GRADIENT = gradient(["#7c3aed", "#2563eb", "#06b6d4"]);
29
29
  const banner = `
@@ -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");
101
+ const { getBrain, detectProjectRoot, refreshBrain } = await import("./cache-7HSMIYDJ.js");
102
+ const { getActiveToolDefinitionsForBrain, handleToolCall } = await import("./tools-ECHCPLCB.js");
103
103
  const { buildInstructions } = await import("./instructions-JARSXQPO.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
  {
@@ -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";
3
+ } from "./chunk-I63UMEBF.js";
4
4
  import "./chunk-HROSQ5MS.js";
5
5
  import {
6
6
  installAgentsForProject
7
- } from "./chunk-RZOVZYTF.js";
7
+ } from "./chunk-GATMQQK5.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
@@ -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,10 @@
1
+ import {
2
+ appendGlobalLearning,
3
+ buildGlobalLearning,
4
+ getRecentGlobalLearnings,
5
+ loadAllGlobalLearnings,
6
+ searchGlobalLearnings
7
+ } from "./chunk-OINYMLOV.js";
1
8
  import {
2
9
  notifyToolsListChanged
3
10
  } from "./chunk-WMESQUZU.js";
@@ -9,32 +16,13 @@ import {
9
16
  } from "./chunk-UTVFELXS.js";
10
17
  import {
11
18
  appendSession,
12
- getCachedLatestVersion,
13
19
  getRecentSessions,
14
20
  installAgentsForProject,
15
- isNewerVersion,
16
21
  loadAllSessions,
17
22
  pruneSessionsByAge,
18
23
  searchSessions,
19
24
  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";
31
- import {
32
- appendGlobalLearning,
33
- buildGlobalLearning,
34
- getRecentGlobalLearnings,
35
- loadAllGlobalLearnings,
36
- searchGlobalLearnings
37
- } from "./chunk-ORKWLA33.js";
25
+ } from "./chunk-GATMQQK5.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
  });
@@ -2889,9 +2935,7 @@ function handleGetLearning(params, brain) {
2889
2935
  if (!id) return errorResponse("id parameter is required");
2890
2936
  const entry = brain.knowledgeBase.entries.find((e) => e.id === id);
2891
2937
  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
- });
2938
+ return errorResponse(`No learning with id="${id}". List active ones via knit_search_learnings (default returns id + summary).`);
2895
2939
  }
2896
2940
  recordCacheHit(brain.knowledgeBase);
2897
2941
  return JSON.stringify({
@@ -2914,11 +2958,11 @@ function handleRecordLearning(params, brain) {
2914
2958
  const entry = {
2915
2959
  date,
2916
2960
  summary: redactSecrets(params.summary || "Untitled learning"),
2917
- domains: (params.domains || "general").split(",").map((d) => d.trim()),
2961
+ domains: (params.domains || "general").split(",").map((d) => redactSecrets(d.trim())),
2918
2962
  approach: redactSecrets(params.approach || ""),
2919
2963
  outcome: ["success", "partial", "failure"].includes(params.outcome) ? params.outcome : "success",
2920
2964
  lesson: redactSecrets(params.lesson || ""),
2921
- tags: (params.tags || "").split(/\s+/).filter((t) => t.startsWith("#"))
2965
+ tags: (params.tags || "").split(/\s+/).filter((t) => t.startsWith("#")).map((t) => redactSecrets(t))
2922
2966
  };
2923
2967
  addEntry(brain.knowledgeBase, entry);
2924
2968
  saveKnowledgeBase(knowledgebasePath(brain.rootPath), brain.knowledgeBase);
@@ -2946,7 +2990,7 @@ function handleRecordLearning(params, brain) {
2946
2990
  }
2947
2991
  function handleRecordFalsePositive(params, brain) {
2948
2992
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2949
- const tags = [...(params.tags || "").split(/\s+/).filter((t) => t.startsWith("#")), "#false-positive"];
2993
+ const tags = [...(params.tags || "").split(/\s+/).filter((t) => t.startsWith("#")).map((t) => redactSecrets(t)), "#false-positive"];
2950
2994
  const entry = {
2951
2995
  date,
2952
2996
  summary: redactSecrets(params.summary || "Untitled FP"),
@@ -3085,7 +3129,8 @@ function handleIndexRequirements(params, brain) {
3085
3129
  return JSON.stringify({ status: "error", error: "Invalid source_id \u2014 must be 1-80 chars, alphanumeric + . _ - only" });
3086
3130
  }
3087
3131
  const sourceId = userSourceId || slugifySourceId(filePath);
3088
- const label = (params.label || "").trim() || void 0;
3132
+ const rawLabel = (params.label || "").trim();
3133
+ const label = rawLabel ? redactSecrets(rawLabel) : void 0;
3089
3134
  const source = {
3090
3135
  sourceId,
3091
3136
  sourcePath: filePath,
@@ -3142,8 +3187,13 @@ function handleGenerateTestCases(params, brain) {
3142
3187
  if (s) sourcesToSearch.push(s);
3143
3188
  }
3144
3189
  }
3190
+ const MAX_RESPONSE_BYTES = 100 * 1024;
3145
3191
  const hits = retrieveTopChunks(sourcesToSearch, feature, topN);
3146
- const contextBytes = hits.reduce((s, h) => s + Buffer.byteLength(h.chunk.text, "utf-8"), 0);
3192
+ let contextBytes = hits.reduce((s, h) => s + Buffer.byteLength(h.chunk.text, "utf-8"), 0);
3193
+ while (contextBytes > MAX_RESPONSE_BYTES && hits.length > 1) {
3194
+ const dropped = hits.pop();
3195
+ contextBytes -= Buffer.byteLength(dropped.chunk.text, "utf-8");
3196
+ }
3147
3197
  const totalBytes = sourcesToSearch.reduce((s, src) => s + src.sourceBytes, 0);
3148
3198
  const reductionPct = totalBytes > 0 ? Math.round(100 - contextBytes / totalBytes * 100) : 0;
3149
3199
  return JSON.stringify({
@@ -3581,6 +3631,16 @@ function handleLoadSession(params, brain) {
3581
3631
  if (wantAll || include.has("patterns")) {
3582
3632
  patterns = reflect(brain.knowledgeBase).slice(0, 3).map((p) => ({ type: p.type, description: p.description, confidence: p.confidence }));
3583
3633
  }
3634
+ let updateAvailable;
3635
+ const cachedLatest = getCachedLatestVersion();
3636
+ if (cachedLatest && isNewerVersion(cachedLatest, VERSION)) {
3637
+ updateAvailable = {
3638
+ current: VERSION,
3639
+ latest: cachedLatest,
3640
+ 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.",
3641
+ changelog: "https://github.com/PDgit12/knit/blob/main/CHANGELOG.md"
3642
+ };
3643
+ }
3584
3644
  const response = {
3585
3645
  session_context: {
3586
3646
  last_session: lastSession,
@@ -3598,6 +3658,7 @@ function handleLoadSession(params, brain) {
3598
3658
  ...metrics !== void 0 ? { metrics } : {},
3599
3659
  ...recentSessions !== void 0 ? { recent_sessions: recentSessions } : {}
3600
3660
  },
3661
+ ...updateAvailable ? { update_available: updateAvailable } : {},
3601
3662
  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
3663
  };
3603
3664
  return JSON.stringify(response);
@@ -3611,7 +3672,7 @@ function handleSaveSessionSummary(params, brain) {
3611
3672
  date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
3612
3673
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3613
3674
  summary: redactSecrets((params.summary || "").slice(0, 500)),
3614
- tags: (params.tags || "").split(/\s+/).filter((t) => t.startsWith("#")),
3675
+ tags: (params.tags || "").split(/\s+/).filter((t) => t.startsWith("#")).map((t) => redactSecrets(t)),
3615
3676
  outcome,
3616
3677
  filesTouched: params.files_touched ? params.files_touched.split(",").map((f) => f.trim()).filter(Boolean) : void 0,
3617
3678
  domainsTouched: params.domains ? params.domains.split(",").map((d) => d.trim()).filter(Boolean) : void 0
@@ -3813,32 +3874,32 @@ function getToolDefinitions() {
3813
3874
  // ── Query (read the brain) ───────────────────────────────────
3814
3875
  {
3815
3876
  name: "knit_query_imports",
3816
- description: "Reverse deps for a file \u2014 who imports it.",
3877
+ description: "[GRAPH] Reverse deps \u2014 WHO IMPORTS this file. Use to find blast radius before editing.",
3817
3878
  inputSchema: { type: "object", properties: { file_path: { type: "string", description: "Relative file path." } }, required: ["file_path"] }
3818
3879
  },
3819
3880
  {
3820
3881
  name: "knit_query_dependents",
3821
- description: "Forward deps for a file \u2014 what it imports.",
3882
+ description: "[GRAPH] Forward deps \u2014 WHAT THIS FILE IMPORTS. Opposite of knit_query_imports (who imports it).",
3822
3883
  inputSchema: { type: "object", properties: { file_path: { type: "string", description: "Relative file path." } }, required: ["file_path"] }
3823
3884
  },
3824
3885
  {
3825
3886
  name: "knit_query_exports",
3826
- description: "Exports from a file: functions, classes, types, constants.",
3887
+ description: "[GRAPH] Exports from a file: functions, classes, types, constants. Use when verifying a claim or finding the canonical definition.",
3827
3888
  inputSchema: { type: "object", properties: { file_path: { type: "string", description: "Relative file path." } }, required: ["file_path"] }
3828
3889
  },
3829
3890
  {
3830
3891
  name: "knit_query_tests",
3831
- description: 'Tests for a file, or list all untested files with filter="untested".',
3892
+ description: '[GRAPH] Tests covering a file, OR list all untested files with filter="untested". Use before declaring "tested" on changed code.',
3832
3893
  inputSchema: { type: "object", properties: { file_path: { type: "string", description: "Relative file path (optional)." }, filter: { type: "string", description: '"untested" to list all untested files.' } } }
3833
3894
  },
3834
3895
  {
3835
3896
  name: "knit_find_fanout",
3836
- description: "High-fanout files \u2014 imported by many others.",
3897
+ description: "[GRAPH] High-fanout files \u2014 imported by many others. Editing these is high-risk; surfaces in classify_task risk_tier.",
3837
3898
  inputSchema: { type: "object", properties: { min_importers: { type: "string", description: "Minimum importers to qualify (default: 3)." } } }
3838
3899
  },
3839
3900
  {
3840
3901
  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.',
3902
+ 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
3903
  inputSchema: {
3843
3904
  type: "object",
3844
3905
  properties: {
@@ -3862,7 +3923,7 @@ function getToolDefinitions() {
3862
3923
  // ── Update (write to the brain) ──────────────────────────────
3863
3924
  {
3864
3925
  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.",
3926
+ 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
3927
  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
3928
  },
3868
3929
  {
@@ -3872,7 +3933,7 @@ function getToolDefinitions() {
3872
3933
  },
3873
3934
  {
3874
3935
  name: "knit_record_learning",
3875
- description: "Record a non-obvious, reusable insight. Skip if a future search wouldn't be glad it exists.",
3936
+ 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
3937
  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
3938
  },
3878
3939
  {
@@ -3927,7 +3988,7 @@ function getToolDefinitions() {
3927
3988
  },
3928
3989
  {
3929
3990
  name: "knit_save_handoff",
3930
- description: "Save state for the next session. failed_attempts is the load-bearing field.",
3991
+ 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
3992
  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
3993
  },
3933
3994
  {
@@ -3978,12 +4039,12 @@ function getToolDefinitions() {
3978
4039
  // ── Session memory ───────────────────────────────────────────
3979
4040
  {
3980
4041
  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.",
4042
+ 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
4043
  inputSchema: { type: "object", properties: { include: { type: "string", description: "Comma-separated optional sections." } } }
3983
4044
  },
3984
4045
  {
3985
4046
  name: "knit_save_session_summary",
3986
- description: "Opt-in. Record a session summary a future search would want to find.",
4047
+ 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
4048
  inputSchema: {
3988
4049
  type: "object",
3989
4050
  properties: {
@@ -4008,7 +4069,7 @@ function getToolDefinitions() {
4008
4069
  },
4009
4070
  {
4010
4071
  name: "knit_search_sessions",
4011
- description: 'Search past sessions by free text. "Have I done this before?"',
4072
+ 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
4073
  inputSchema: {
4013
4074
  type: "object",
4014
4075
  properties: {
@@ -4055,7 +4116,7 @@ function getToolDefinitions() {
4055
4116
  // ── Cross-project learnings (Model C — global pool) ─────────
4056
4117
  {
4057
4118
  name: "knit_record_global_learning",
4058
- description: "Opt-in. Record a learning to the cross-project pool when it generalizes beyond this project.",
4119
+ 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
4120
  inputSchema: {
4060
4121
  type: "object",
4061
4122
  properties: {
@@ -4069,7 +4130,7 @@ function getToolDefinitions() {
4069
4130
  },
4070
4131
  {
4071
4132
  name: "knit_search_global_learnings",
4072
- description: "Search the cross-project learnings pool across all projects on this machine.",
4133
+ description: "[MEMORY] Search learnings ACROSS ALL projects on this machine. Use when starting a new domain. Companion: knit_search_learnings (this project only).",
4073
4134
  inputSchema: {
4074
4135
  type: "object",
4075
4136
  properties: {
@@ -4166,12 +4227,12 @@ function getToolDefinitions() {
4166
4227
  },
4167
4228
  {
4168
4229
  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.',
4230
+ 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
4231
  inputSchema: { type: "object", properties: { claim: { type: "string", description: "One claim about the codebase to verify." } }, required: ["claim"] }
4171
4232
  },
4172
4233
  {
4173
4234
  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.",
4235
+ 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
4236
  inputSchema: { type: "object", properties: { id: { type: "string", description: "Learning id from a prior knit_search_learnings result." } }, required: ["id"] }
4176
4237
  },
4177
4238
  {
@@ -4260,7 +4321,7 @@ function handleToolCall(toolName, params, brain) {
4260
4321
  const allowAbsolute = toolName === "knit_index_requirements";
4261
4322
  const bad = decoded.includes("..") || decoded.includes("\0") || !allowAbsolute && (decoded.startsWith("/") || /^[A-Za-z]:\//.test(decoded));
4262
4323
  if (bad) {
4263
- return JSON.stringify({ error: "Invalid file path \u2014 no traversal or absolute paths allowed" });
4324
+ return JSON.stringify({ status: "error", error: "Invalid file path \u2014 no traversal or absolute paths allowed" });
4264
4325
  }
4265
4326
  params.file_path = decoded;
4266
4327
  }
@@ -4271,7 +4332,7 @@ function handleToolCall(toolName, params, brain) {
4271
4332
  }
4272
4333
  const handler = handlers[toolName];
4273
4334
  if (!handler) {
4274
- return JSON.stringify({ error: `Unknown tool: ${toolName}` });
4335
+ return JSON.stringify({ status: "error", error: `Unknown tool: ${toolName}` });
4275
4336
  }
4276
4337
  return handler(params, brain);
4277
4338
  }
@@ -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.11.4",
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": {
@@ -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,