knit-mcp 0.10.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -5
- package/dist/cache-3LPETDUT.js +19 -0
- package/dist/{chunk-BW4JUY74.js → chunk-27TA2ZQZ.js} +16 -0
- package/dist/{chunk-KLNUEE3O.js → chunk-7UFS67HP.js} +9 -1
- package/dist/{chunk-A7DIGJI4.js → chunk-HROSQ5MS.js} +211 -347
- package/dist/{chunk-5NCSZYRJ.js → chunk-LV73YTVN.js} +5 -0
- package/dist/{chunk-RD2CPNTQ.js → chunk-ORKWLA33.js} +1 -1
- package/dist/{chunk-2N67YTOQ.js → chunk-RZOVZYTF.js} +2 -2
- package/dist/{chunk-7PPC6IG6.js → chunk-ST4X7LZT.js} +60 -2
- package/dist/chunk-TWHNYJAJ.js +328 -0
- package/dist/{chunk-P47VGEXO.js → chunk-VB2TIR6L.js} +2 -2
- package/dist/cli.js +18 -9
- package/dist/doctor-4DN2P2JR.js +179 -0
- package/dist/{export-4A7VE6VG.js → export-CGSEUYZA.js} +2 -2
- package/dist/{install-agents-O2YVJLO6.js → install-agents-OBDCWCPB.js} +7 -6
- package/dist/{instructions-4FI32YZU.js → instructions-JARSXQPO.js} +1 -1
- package/dist/{integration-scanner-YU43QNVC.js → integration-scanner-LBD2PIZ3.js} +3 -3
- package/dist/{refresh-W3I7QDDP.js → refresh-SMJ2NGIW.js} +3 -3
- package/dist/{status-KX4U7HYL.js → status-VJDB75X2.js} +1 -1
- package/dist/{tools-AI6QKEOU.js → tools-MIROTK2A.js} +863 -80
- package/package.json +2 -1
- package/dist/cache-PH7DZ6Y4.js +0 -18
|
@@ -1,32 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
installAgentsForProject,
|
|
3
|
-
prewarmLatestVersion,
|
|
4
|
-
pruneSessionsByAge
|
|
5
|
-
} from "./chunk-2N67YTOQ.js";
|
|
6
|
-
import {
|
|
7
|
-
buildKnowledge,
|
|
8
|
-
buildReverseDependencies
|
|
9
|
-
} from "./chunk-MOOVNMIN.js";
|
|
10
|
-
import {
|
|
11
|
-
scanProject
|
|
12
|
-
} from "./chunk-7PPC6IG6.js";
|
|
13
|
-
import {
|
|
14
|
-
readLearnings
|
|
15
|
-
} from "./chunk-M3YZOJNW.js";
|
|
16
|
-
import {
|
|
17
|
-
persistScanResult,
|
|
18
|
-
scanIntegrations
|
|
19
|
-
} from "./chunk-P47VGEXO.js";
|
|
20
|
-
import {
|
|
21
|
-
KNIT_MARKER_START,
|
|
22
|
-
generateClaudeMd,
|
|
23
|
-
spliceKnitBlock
|
|
24
|
-
} from "./chunk-KLNUEE3O.js";
|
|
25
|
-
import {
|
|
26
|
-
importFromMarkdown,
|
|
27
|
-
loadKnowledgeBaseSafe,
|
|
28
|
-
saveKnowledgeBase
|
|
29
|
-
} from "./chunk-WKQHCLLO.js";
|
|
30
1
|
import {
|
|
31
2
|
claimMarkerPath,
|
|
32
3
|
classificationMarkerPath,
|
|
@@ -34,47 +5,17 @@ import {
|
|
|
34
5
|
knowledgebasePath,
|
|
35
6
|
learningsDir,
|
|
36
7
|
learningsFilePath,
|
|
37
|
-
legacyClaudeDir,
|
|
38
|
-
legacyKnowledgePath,
|
|
39
|
-
legacyKnowledgebasePath,
|
|
40
|
-
legacyLearningsDir,
|
|
41
|
-
legacyTeamsPath,
|
|
42
|
-
migrationBreadcrumbPath,
|
|
43
8
|
projectDataDir,
|
|
44
9
|
protocolConfigPath,
|
|
45
10
|
searchMarkerPath,
|
|
46
11
|
sessionMarkerPath,
|
|
47
12
|
sessionsJsonlPath,
|
|
48
13
|
sessionsLogPath,
|
|
49
|
-
|
|
50
|
-
} from "./chunk-
|
|
51
|
-
|
|
52
|
-
// src/mcp/cache.ts
|
|
53
|
-
import { execSync } from "child_process";
|
|
54
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
55
|
-
import { join, basename, dirname } from "path";
|
|
56
|
-
|
|
57
|
-
// src/generators/learnings.ts
|
|
58
|
-
function generateLearningsContent(config) {
|
|
59
|
-
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
60
|
-
return `# Project Learnings \u2014 ${config.name}
|
|
61
|
-
|
|
62
|
-
> Recursive learning log. Check this BEFORE starting any task.
|
|
63
|
-
> Grep by \`#tag\` to find relevant lessons for the domain you're working in.
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
## ${date} Project initialized with Knit workflow
|
|
68
|
-
**Domain(s):** All \u2014 workflow infrastructure
|
|
69
|
-
**Approach:** Auto-detected stack (${config.stack.language}${config.stack.framework ? " + " + config.stack.framework : ""}), generated ${config.domains.length} domains, wired hooks for ${config.targetAgent}.
|
|
70
|
-
**Outcome:** Success \u2014 workflow infrastructure in place
|
|
71
|
-
**Lesson:** This learnings file is the institutional memory. Every task should append an entry. Every session should check relevant tags before starting work. The LEARN phase is a hard exit gate \u2014 no task completes without updating this file.
|
|
72
|
-
**Tags:** #workflow #all #bootstrap
|
|
73
|
-
`;
|
|
74
|
-
}
|
|
14
|
+
turnEditLogPath
|
|
15
|
+
} from "./chunk-27TA2ZQZ.js";
|
|
75
16
|
|
|
76
17
|
// src/generators/settings.ts
|
|
77
|
-
var HOOKS_VERSION =
|
|
18
|
+
var HOOKS_VERSION = 11;
|
|
78
19
|
function generateSettings(config, rootPath) {
|
|
79
20
|
return {
|
|
80
21
|
mcpServers: {
|
|
@@ -122,6 +63,7 @@ function generateHooks(config, rootPath) {
|
|
|
122
63
|
const SESSION_MARKER = sessionMarkerPath(rootPath);
|
|
123
64
|
const SEARCH_MARKER = searchMarkerPath(rootPath);
|
|
124
65
|
const CLAIM_MARKER = claimMarkerPath(rootPath);
|
|
66
|
+
const TURN_EDIT_LOG = turnEditLogPath(rootPath);
|
|
125
67
|
const CLAUDE_MD = `${rootPath}/CLAUDE.md`;
|
|
126
68
|
void knowledgePath;
|
|
127
69
|
const hooks = {
|
|
@@ -170,6 +112,10 @@ function generateHooks(config, rootPath) {
|
|
|
170
112
|
// requires fresh verify_claim per non-trivial task.
|
|
171
113
|
const cm = ${jsLit(CLAIM_MARKER)};
|
|
172
114
|
if (fs.existsSync(cm)) fs.rmSync(cm, { force: true });
|
|
115
|
+
// v0.11 slice 3: per-turn edit log \u2014 the Stop-hook drift detector
|
|
116
|
+
// re-classifies the actual touched set and surfaces scope creep.
|
|
117
|
+
const tl = ${jsLit(TURN_EDIT_LOG)};
|
|
118
|
+
if (fs.existsSync(tl)) fs.rmSync(tl, { force: true });
|
|
173
119
|
} catch (e) {}
|
|
174
120
|
`),
|
|
175
121
|
timeout: 5
|
|
@@ -315,6 +261,155 @@ function generateHooks(config, rootPath) {
|
|
|
315
261
|
PostToolUse: [],
|
|
316
262
|
Stop: []
|
|
317
263
|
};
|
|
264
|
+
hooks.PostToolUse.push({
|
|
265
|
+
_knitOwned: true,
|
|
266
|
+
matcher: "Write|Edit|MultiEdit",
|
|
267
|
+
hooks: [
|
|
268
|
+
{
|
|
269
|
+
type: "command",
|
|
270
|
+
command: nodeHook(`
|
|
271
|
+
let d = "";
|
|
272
|
+
process.stdin.on("data", (c) => d += c);
|
|
273
|
+
process.stdin.on("end", () => {
|
|
274
|
+
try {
|
|
275
|
+
const fs = require("fs");
|
|
276
|
+
const path = require("path");
|
|
277
|
+
const i = JSON.parse(d);
|
|
278
|
+
const ti = i.tool_input || {};
|
|
279
|
+
const f = ti.file_path || (i.tool_response && i.tool_response.filePath) || "";
|
|
280
|
+
if (!f) return;
|
|
281
|
+
const logPath = ${jsLit(TURN_EDIT_LOG)};
|
|
282
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
283
|
+
fs.appendFileSync(logPath, JSON.stringify({ file: f, ts: new Date().toISOString() }) + "\\n");
|
|
284
|
+
} catch (e) { try { process.stderr.write('[knit] turn-edit appender hook failed: ' + (e && e.message ? e.message : e) + '\\n'); } catch {} }
|
|
285
|
+
});
|
|
286
|
+
`),
|
|
287
|
+
timeout: 5
|
|
288
|
+
}
|
|
289
|
+
]
|
|
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 i = JSON.parse(d);
|
|
304
|
+
const toolName = i.tool_name || "";
|
|
305
|
+
const ti = i.tool_input || {};
|
|
306
|
+
const f = ti.file_path || (i.tool_response && i.tool_response.filePath) || "";
|
|
307
|
+
if (!f || !fs.existsSync(f)) return;
|
|
308
|
+
const cur = fs.readFileSync(f, "utf-8");
|
|
309
|
+
if (toolName === "Write") {
|
|
310
|
+
const intended = ti.content || "";
|
|
311
|
+
if (cur === intended) {
|
|
312
|
+
console.error("[knit] verify: write landed \u2014 " + f);
|
|
313
|
+
} else {
|
|
314
|
+
const lenDelta = cur.length - intended.length;
|
|
315
|
+
console.error("[knit] verify: write DRIFTED \u2014 " + f + " differs by " + lenDelta + " char(s) from intent. Something modified the file after Write.");
|
|
316
|
+
}
|
|
317
|
+
} else if (toolName === "Edit") {
|
|
318
|
+
const newStr = ti.new_string || "";
|
|
319
|
+
const oldStr = ti.old_string || "";
|
|
320
|
+
if (newStr && cur.indexOf(newStr) !== -1) {
|
|
321
|
+
console.error("[knit] verify: edit landed \u2014 " + f);
|
|
322
|
+
} else if (oldStr && cur.indexOf(oldStr) !== -1) {
|
|
323
|
+
console.error("[knit] verify: edit DRIFTED \u2014 " + f + " still contains the old_string. Edit may have silently failed.");
|
|
324
|
+
} else {
|
|
325
|
+
console.error("[knit] verify: edit ambiguous \u2014 " + f + " contains neither new_string nor old_string. Inspect manually.");
|
|
326
|
+
}
|
|
327
|
+
} else if (toolName === "MultiEdit") {
|
|
328
|
+
const edits = Array.isArray(ti.edits) ? ti.edits : [];
|
|
329
|
+
let landed = 0;
|
|
330
|
+
let drifted = 0;
|
|
331
|
+
for (const e of edits) {
|
|
332
|
+
if (e && e.new_string && cur.indexOf(e.new_string) !== -1) landed++;
|
|
333
|
+
else drifted++;
|
|
334
|
+
}
|
|
335
|
+
if (drifted === 0) {
|
|
336
|
+
console.error("[knit] verify: all " + landed + " edits landed \u2014 " + f);
|
|
337
|
+
} else {
|
|
338
|
+
console.error("[knit] verify: " + drifted + " of " + (landed + drifted) + " edits DRIFTED \u2014 " + f + ". Some new_string values not found in file post-edit.");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
} catch (e) { try { process.stderr.write('[knit] diff verifier hook failed: ' + (e && e.message ? e.message : e) + '\\n'); } catch {} }
|
|
342
|
+
});
|
|
343
|
+
`),
|
|
344
|
+
timeout: 5,
|
|
345
|
+
statusMessage: "Knit: verifying edit landed..."
|
|
346
|
+
}
|
|
347
|
+
]
|
|
348
|
+
});
|
|
349
|
+
hooks.PostToolUse.push({
|
|
350
|
+
_knitOwned: true,
|
|
351
|
+
matcher: "Write|Edit|MultiEdit",
|
|
352
|
+
hooks: [
|
|
353
|
+
{
|
|
354
|
+
type: "command",
|
|
355
|
+
command: nodeHook(`
|
|
356
|
+
let d = "";
|
|
357
|
+
process.stdin.on("data", (c) => d += c);
|
|
358
|
+
process.stdin.on("end", () => {
|
|
359
|
+
try {
|
|
360
|
+
const fs = require("fs");
|
|
361
|
+
const path = require("path");
|
|
362
|
+
const cp = require("child_process");
|
|
363
|
+
const i = JSON.parse(d);
|
|
364
|
+
const ti = i.tool_input || {};
|
|
365
|
+
const f = ti.file_path || (i.tool_response && i.tool_response.filePath) || "";
|
|
366
|
+
if (!/\\.(?:ts|tsx|mts|cts)$/.test(f)) return;
|
|
367
|
+
// Walk up to find tsconfig.json (project root).
|
|
368
|
+
let dir = path.dirname(f);
|
|
369
|
+
let projectRoot = null;
|
|
370
|
+
for (let depth = 0; depth < 10; depth++) {
|
|
371
|
+
if (fs.existsSync(path.join(dir, "tsconfig.json"))) { projectRoot = dir; break; }
|
|
372
|
+
const parent = path.dirname(dir);
|
|
373
|
+
if (parent === dir) break;
|
|
374
|
+
dir = parent;
|
|
375
|
+
}
|
|
376
|
+
if (!projectRoot) return;
|
|
377
|
+
// Prefer local tsc; fall back to npx.
|
|
378
|
+
const localTsc = path.join(projectRoot, "node_modules", ".bin", "tsc");
|
|
379
|
+
const tscBin = fs.existsSync(localTsc) ? localTsc : "npx";
|
|
380
|
+
const tscArgs = fs.existsSync(localTsc) ? ["--noEmit", "--pretty", "false"] : ["--no-install", "tsc", "--noEmit", "--pretty", "false"];
|
|
381
|
+
let out = "";
|
|
382
|
+
let failed = false;
|
|
383
|
+
try {
|
|
384
|
+
cp.execFileSync(tscBin, tscArgs, { cwd: projectRoot, stdio: ["ignore", "pipe", "pipe"], timeout: 15000, encoding: "utf-8" });
|
|
385
|
+
} catch (err) {
|
|
386
|
+
failed = true;
|
|
387
|
+
out = (err && (err.stdout || err.stderr) || "").toString();
|
|
388
|
+
}
|
|
389
|
+
if (!failed) {
|
|
390
|
+
console.error("[knit] tsc check: clean \u2014 " + f);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
// Filter tsc output to errors mentioning the touched file (or its dir).
|
|
394
|
+
const touched = path.basename(f);
|
|
395
|
+
const lines = out.split("\\n").map((s) => s.trim()).filter(Boolean);
|
|
396
|
+
const relevant = lines.filter((l) => l.indexOf(touched) !== -1 || l.indexOf(f) !== -1);
|
|
397
|
+
const errorCount = lines.filter((l) => /error TS\\d+:/.test(l)).length;
|
|
398
|
+
if (relevant.length > 0) {
|
|
399
|
+
console.error("[knit] tsc check: " + relevant.length + " error(s) referencing " + touched + ":");
|
|
400
|
+
for (const l of relevant.slice(0, 6)) console.error(" " + l);
|
|
401
|
+
if (relevant.length > 6) console.error(" ... and " + (relevant.length - 6) + " more");
|
|
402
|
+
} else if (errorCount > 0) {
|
|
403
|
+
console.error("[knit] tsc check: project has " + errorCount + " type error(s) (none in " + touched + " directly \u2014 likely a cross-file ripple). Run \`npx tsc --noEmit\` for full output.");
|
|
404
|
+
}
|
|
405
|
+
} catch (e) { try { process.stderr.write('[knit] tsc check hook failed: ' + (e && e.message ? e.message : e) + '\\n'); } catch {} }
|
|
406
|
+
});
|
|
407
|
+
`),
|
|
408
|
+
timeout: 20,
|
|
409
|
+
statusMessage: "Knit: tsc on edit..."
|
|
410
|
+
}
|
|
411
|
+
]
|
|
412
|
+
});
|
|
318
413
|
hooks.PostToolUse.push({
|
|
319
414
|
_knitOwned: true,
|
|
320
415
|
matcher: "Write|Edit|MultiEdit",
|
|
@@ -358,32 +453,6 @@ function generateHooks(config, rootPath) {
|
|
|
358
453
|
}
|
|
359
454
|
]
|
|
360
455
|
});
|
|
361
|
-
if (config.stack.language === "typescript" && config.stack.typecheckCommand) {
|
|
362
|
-
hooks.PostToolUse.push({
|
|
363
|
-
_knitOwned: true,
|
|
364
|
-
matcher: "Write|Edit",
|
|
365
|
-
hooks: [
|
|
366
|
-
{
|
|
367
|
-
type: "command",
|
|
368
|
-
command: nodeHook(`
|
|
369
|
-
let d = "";
|
|
370
|
-
process.stdin.on("data", (c) => d += c);
|
|
371
|
-
process.stdin.on("end", () => {
|
|
372
|
-
try {
|
|
373
|
-
const i = JSON.parse(d);
|
|
374
|
-
const f = (i.tool_input && i.tool_input.file_path) || (i.tool_response && i.tool_response.filePath) || "";
|
|
375
|
-
if (!/\\.tsx?$/.test(f)) return;
|
|
376
|
-
${REPO_ROOT_JS}
|
|
377
|
-
require("child_process").execSync("npx tsc --noEmit --pretty false", { cwd: __getRoot(), stdio: "inherit" });
|
|
378
|
-
} catch (e) {}
|
|
379
|
-
});
|
|
380
|
-
`),
|
|
381
|
-
timeout: 30,
|
|
382
|
-
statusMessage: "Type checking..."
|
|
383
|
-
}
|
|
384
|
-
]
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
456
|
if (config.stack.language === "python") {
|
|
388
457
|
hooks.PostToolUse.push({
|
|
389
458
|
_knitOwned: true,
|
|
@@ -488,6 +557,58 @@ function generateHooks(config, rootPath) {
|
|
|
488
557
|
]
|
|
489
558
|
});
|
|
490
559
|
}
|
|
560
|
+
hooks.Stop.push({
|
|
561
|
+
_knitOwned: true,
|
|
562
|
+
hooks: [
|
|
563
|
+
{
|
|
564
|
+
type: "command",
|
|
565
|
+
command: nodeHook(`
|
|
566
|
+
try {
|
|
567
|
+
const fs = require("fs");
|
|
568
|
+
const path = require("path");
|
|
569
|
+
const classifiedPath = ${jsLit(CLASSIFIED_MARKER)};
|
|
570
|
+
const logPath = ${jsLit(TURN_EDIT_LOG)};
|
|
571
|
+
if (!fs.existsSync(classifiedPath) || !fs.existsSync(logPath)) return;
|
|
572
|
+
let marker;
|
|
573
|
+
try {
|
|
574
|
+
marker = JSON.parse(fs.readFileSync(classifiedPath, "utf-8"));
|
|
575
|
+
} catch (e) { return; }
|
|
576
|
+
const originalScope = (marker && (marker.scopeTier || marker.tier)) || "";
|
|
577
|
+
const originalRisk = (marker && marker.riskTier) || "low";
|
|
578
|
+
if (!originalScope) return;
|
|
579
|
+
// Parse turn-edit log \u2192 unique file set.
|
|
580
|
+
const seen = new Set();
|
|
581
|
+
const raw = fs.readFileSync(logPath, "utf-8");
|
|
582
|
+
for (const line of raw.split("\\n")) {
|
|
583
|
+
if (!line.trim()) continue;
|
|
584
|
+
try { const e = JSON.parse(line); if (e.file) seen.add(e.file); } catch (e2) {}
|
|
585
|
+
}
|
|
586
|
+
const touched = Array.from(seen);
|
|
587
|
+
const n = touched.length;
|
|
588
|
+
if (n === 0) return;
|
|
589
|
+
// Scope drift: trivial classification but turn touched 3+ files.
|
|
590
|
+
const scopeDrift = (originalScope === "trivial" && n >= 3) ||
|
|
591
|
+
(originalScope === "standard" && n >= 6);
|
|
592
|
+
// Risk drift: low-risk classification but turn touched risky files.
|
|
593
|
+
const riskyPatterns = /(\\btypes?\\.tsx?|\\bschema\\.|\\bauth\\.|\\bsecurity\\.|migrations?\\/)/i;
|
|
594
|
+
const riskyHit = touched.find((f) => riskyPatterns.test(f));
|
|
595
|
+
const riskDrift = originalRisk === "low" && !!riskyHit;
|
|
596
|
+
if (!scopeDrift && !riskDrift) return;
|
|
597
|
+
console.error("[knit] drift detector \u2014 turn touched " + n + " file(s); classification was scope=" + originalScope + ", risk=" + originalRisk);
|
|
598
|
+
if (scopeDrift) {
|
|
599
|
+
console.error(" scope drift: " + n + " files exceeds the " + originalScope + " threshold. Next time, re-classify when scope grows.");
|
|
600
|
+
}
|
|
601
|
+
if (riskDrift) {
|
|
602
|
+
console.error(" risk drift: low-risk classification but touched " + riskyHit + " \u2014 high-risk pattern (types/schema/auth/migrations). Should have triggered plan mode.");
|
|
603
|
+
}
|
|
604
|
+
console.error(" touched: " + touched.slice(0, 8).join(", ") + (touched.length > 8 ? " ... and " + (touched.length - 8) + " more" : ""));
|
|
605
|
+
} catch (e) {}
|
|
606
|
+
`),
|
|
607
|
+
timeout: 5,
|
|
608
|
+
statusMessage: "Knit: drift detector..."
|
|
609
|
+
}
|
|
610
|
+
]
|
|
611
|
+
});
|
|
491
612
|
hooks.Stop.push({
|
|
492
613
|
_knitOwned: true,
|
|
493
614
|
hooks: [
|
|
@@ -673,264 +794,7 @@ function generateHooks(config, rootPath) {
|
|
|
673
794
|
return hooks;
|
|
674
795
|
}
|
|
675
796
|
|
|
676
|
-
// src/mcp/cache.ts
|
|
677
|
-
var cache = null;
|
|
678
|
-
var hooksRefreshed = /* @__PURE__ */ new Set();
|
|
679
|
-
function maybeRefreshHooks(rootPath, config) {
|
|
680
|
-
if (hooksRefreshed.has(rootPath)) return;
|
|
681
|
-
hooksRefreshed.add(rootPath);
|
|
682
|
-
const settingsPath = join(rootPath, ".claude", "settings.local.json");
|
|
683
|
-
if (!existsSync(settingsPath)) return;
|
|
684
|
-
try {
|
|
685
|
-
const existing = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
686
|
-
const storedVersion = existing?._knitHooks?.version ?? (existing?._engramHooks ? 0 : 0);
|
|
687
|
-
if (storedVersion < HOOKS_VERSION) {
|
|
688
|
-
writeKnitHooks(rootPath, config);
|
|
689
|
-
}
|
|
690
|
-
} catch {
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
function getBrain(rootPath) {
|
|
694
|
-
if (cache && cache.rootPath === rootPath) {
|
|
695
|
-
return cache;
|
|
696
|
-
}
|
|
697
|
-
void prewarmLatestVersion();
|
|
698
|
-
let autoInitialized = false;
|
|
699
|
-
const haveCentralized = existsSync(knowledgePath(rootPath));
|
|
700
|
-
const haveLegacy = existsSync(legacyKnowledgePath(rootPath));
|
|
701
|
-
if (!haveCentralized) {
|
|
702
|
-
if (haveLegacy) {
|
|
703
|
-
migrateLegacyData(rootPath);
|
|
704
|
-
} else {
|
|
705
|
-
autoInitialize(rootPath);
|
|
706
|
-
autoInitialized = true;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
const scan = scanProject(rootPath);
|
|
710
|
-
const knowledge = buildKnowledge(rootPath, scan);
|
|
711
|
-
const reverseDeps = buildReverseDependencies(knowledge.importGraph);
|
|
712
|
-
const projectName = detectProjectName(rootPath);
|
|
713
|
-
const kbLoad = loadKnowledgeBaseSafe(knowledgebasePath(rootPath), projectName);
|
|
714
|
-
const knowledgeBase = kbLoad.kb;
|
|
715
|
-
const config = {
|
|
716
|
-
name: projectName,
|
|
717
|
-
packageManager: scan.packageManager,
|
|
718
|
-
stack: scan.stack,
|
|
719
|
-
domains: scan.domains,
|
|
720
|
-
targetAgent: "claude-code",
|
|
721
|
-
tokenOptimization: "standard"
|
|
722
|
-
};
|
|
723
|
-
writeFileSync(knowledgePath(rootPath), JSON.stringify(knowledge, null, 2), "utf-8");
|
|
724
|
-
if (!kbLoad.loadFailed) {
|
|
725
|
-
saveKnowledgeBase(knowledgebasePath(rootPath), knowledgeBase);
|
|
726
|
-
}
|
|
727
|
-
if (!autoInitialized) {
|
|
728
|
-
maybeRefreshHooks(rootPath, config);
|
|
729
|
-
}
|
|
730
|
-
cache = {
|
|
731
|
-
rootPath,
|
|
732
|
-
knowledge,
|
|
733
|
-
reverseDeps,
|
|
734
|
-
knowledgeBase,
|
|
735
|
-
config,
|
|
736
|
-
loadedAt: Date.now(),
|
|
737
|
-
autoInitialized
|
|
738
|
-
};
|
|
739
|
-
return cache;
|
|
740
|
-
}
|
|
741
|
-
function autoInitialize(rootPath) {
|
|
742
|
-
const scan = scanProject(rootPath);
|
|
743
|
-
const knowledge = buildKnowledge(rootPath, scan);
|
|
744
|
-
const projectName = detectProjectName(rootPath);
|
|
745
|
-
const config = {
|
|
746
|
-
name: projectName,
|
|
747
|
-
packageManager: scan.packageManager,
|
|
748
|
-
stack: scan.stack,
|
|
749
|
-
domains: scan.domains,
|
|
750
|
-
targetAgent: "claude-code",
|
|
751
|
-
tokenOptimization: "standard"
|
|
752
|
-
};
|
|
753
|
-
mkdirSync(projectDataDir(rootPath), { recursive: true });
|
|
754
|
-
mkdirSync(learningsDir(rootPath), { recursive: true });
|
|
755
|
-
writeProjectClaudeMd(rootPath, config, knowledge);
|
|
756
|
-
writeKnitHooks(rootPath, config);
|
|
757
|
-
installAgentsForProject(rootPath, config, knowledge, null).catch((err) => {
|
|
758
|
-
process.stderr.write(`[knit] agent install background error: ${err?.message ?? err}
|
|
759
|
-
`);
|
|
760
|
-
});
|
|
761
|
-
Promise.resolve().then(() => {
|
|
762
|
-
try {
|
|
763
|
-
pruneSessionsByAge(rootPath, 90);
|
|
764
|
-
} catch (e) {
|
|
765
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
766
|
-
process.stderr.write(`[knit] session prune background error: ${msg}
|
|
767
|
-
`);
|
|
768
|
-
}
|
|
769
|
-
});
|
|
770
|
-
Promise.resolve().then(() => {
|
|
771
|
-
try {
|
|
772
|
-
const result = scanIntegrations(rootPath);
|
|
773
|
-
persistScanResult(rootPath, result);
|
|
774
|
-
} catch (e) {
|
|
775
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
776
|
-
process.stderr.write(`[knit] integration scan background error: ${msg}
|
|
777
|
-
`);
|
|
778
|
-
}
|
|
779
|
-
});
|
|
780
|
-
const learningsPath = learningsFilePath(rootPath, projectName);
|
|
781
|
-
if (!existsSync(learningsPath)) {
|
|
782
|
-
writeFileSync(learningsPath, generateLearningsContent(config), "utf-8");
|
|
783
|
-
}
|
|
784
|
-
const kbPath = knowledgebasePath(rootPath);
|
|
785
|
-
const kb = loadKnowledgeBaseSafe(kbPath, projectName).kb;
|
|
786
|
-
const entries = readLearnings(learningsPath);
|
|
787
|
-
importFromMarkdown(kb, entries);
|
|
788
|
-
saveKnowledgeBase(kbPath, kb);
|
|
789
|
-
writeFileSync(knowledgePath(rootPath), JSON.stringify(knowledge, null, 2), "utf-8");
|
|
790
|
-
}
|
|
791
|
-
function migrateLegacyData(rootPath) {
|
|
792
|
-
mkdirSync(projectDataDir(rootPath), { recursive: true });
|
|
793
|
-
mkdirSync(learningsDir(rootPath), { recursive: true });
|
|
794
|
-
copyIfExists(legacyKnowledgePath(rootPath), knowledgePath(rootPath));
|
|
795
|
-
copyIfExists(legacyKnowledgebasePath(rootPath), knowledgebasePath(rootPath));
|
|
796
|
-
copyIfExists(legacyTeamsPath(rootPath), teamsPath(rootPath));
|
|
797
|
-
const legacyLearn = legacyLearningsDir(rootPath);
|
|
798
|
-
if (existsSync(legacyLearn)) {
|
|
799
|
-
for (const file of readdirSync(legacyLearn)) {
|
|
800
|
-
const src = join(legacyLearn, file);
|
|
801
|
-
const dst = join(learningsDir(rootPath), file);
|
|
802
|
-
try {
|
|
803
|
-
if (statSync(src).isFile() && !existsSync(dst)) {
|
|
804
|
-
copyFileSync(src, dst);
|
|
805
|
-
}
|
|
806
|
-
} catch {
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
const breadcrumb = migrationBreadcrumbPath(rootPath);
|
|
811
|
-
const newPath = projectDataDir(rootPath);
|
|
812
|
-
if (!existsSync(breadcrumb) && existsSync(legacyClaudeDir(rootPath))) {
|
|
813
|
-
const note = `Knit data migrated to ~/.knit/ on ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.
|
|
814
|
-
|
|
815
|
-
Centralized location for this project:
|
|
816
|
-
${newPath}
|
|
817
|
-
|
|
818
|
-
The legacy files in this .claude/ directory are no longer read by engram and
|
|
819
|
-
can be deleted at your discretion. Future learnings, knowledge indexes, and
|
|
820
|
-
session memory live in the new path.
|
|
821
|
-
`;
|
|
822
|
-
try {
|
|
823
|
-
writeFileSync(breadcrumb, note, "utf-8");
|
|
824
|
-
} catch {
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
function writeProjectClaudeMd(rootPath, config, knowledge) {
|
|
829
|
-
const claudeMdPath = join(rootPath, "CLAUDE.md");
|
|
830
|
-
const block = generateClaudeMd(config, knowledge);
|
|
831
|
-
if (!existsSync(claudeMdPath)) {
|
|
832
|
-
writeFileSync(claudeMdPath, block, "utf-8");
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
const existing = readFileSync(claudeMdPath, "utf-8");
|
|
836
|
-
if (existing.includes(KNIT_MARKER_START)) {
|
|
837
|
-
const { content } = spliceKnitBlock(existing, block);
|
|
838
|
-
writeFileSync(claudeMdPath, content, "utf-8");
|
|
839
|
-
return;
|
|
840
|
-
}
|
|
841
|
-
const sidecarDir = join(rootPath, ".claude");
|
|
842
|
-
const sidecarPath = join(sidecarDir, "KNIT.md");
|
|
843
|
-
mkdirSync(sidecarDir, { recursive: true });
|
|
844
|
-
const sidecar = `<!-- This file is Knit's per-project workflow. -->
|
|
845
|
-
<!-- Your CLAUDE.md exists without Knit markers, so Knit wrote here instead of clobbering it. -->
|
|
846
|
-
<!-- To include this content in CLAUDE.md, add: @.claude/KNIT.md -->
|
|
847
|
-
|
|
848
|
-
${block}`;
|
|
849
|
-
writeFileSync(sidecarPath, sidecar, "utf-8");
|
|
850
|
-
}
|
|
851
|
-
function copyIfExists(src, dst) {
|
|
852
|
-
if (existsSync(src) && !existsSync(dst)) {
|
|
853
|
-
mkdirSync(dirname(dst), { recursive: true });
|
|
854
|
-
copyFileSync(src, dst);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
function writeKnitHooks(rootPath, config) {
|
|
858
|
-
const claudeDir = join(rootPath, ".claude");
|
|
859
|
-
const settingsPath = join(claudeDir, "settings.local.json");
|
|
860
|
-
const fresh = generateSettings(config, rootPath);
|
|
861
|
-
if (!existsSync(settingsPath)) {
|
|
862
|
-
mkdirSync(claudeDir, { recursive: true });
|
|
863
|
-
writeFileSync(settingsPath, JSON.stringify(fresh, null, 2), "utf-8");
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
let existing;
|
|
867
|
-
try {
|
|
868
|
-
const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
869
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
existing = parsed;
|
|
873
|
-
} catch {
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
if ("_knitHooks" in existing || "_engramHooks" in existing) {
|
|
877
|
-
mkdirSync(claudeDir, { recursive: true });
|
|
878
|
-
writeFileSync(settingsPath, JSON.stringify(fresh, null, 2), "utf-8");
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
const userHooksRaw = existing.hooks;
|
|
882
|
-
let userHooks;
|
|
883
|
-
if (userHooksRaw === void 0) {
|
|
884
|
-
userHooks = {};
|
|
885
|
-
} else if (userHooksRaw && typeof userHooksRaw === "object" && !Array.isArray(userHooksRaw)) {
|
|
886
|
-
for (const v of Object.values(userHooksRaw)) {
|
|
887
|
-
if (!Array.isArray(v)) return;
|
|
888
|
-
}
|
|
889
|
-
userHooks = { ...userHooksRaw };
|
|
890
|
-
} else {
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
for (const event of Object.keys(fresh.hooks)) {
|
|
894
|
-
const userEntries = Array.isArray(userHooks[event]) ? userHooks[event] : [];
|
|
895
|
-
const preserved = userEntries.filter((entry) => {
|
|
896
|
-
if (!entry || typeof entry !== "object") return true;
|
|
897
|
-
const e = entry;
|
|
898
|
-
return e._knitOwned !== true && e._engramOwned !== true;
|
|
899
|
-
});
|
|
900
|
-
userHooks[event] = [...preserved, ...fresh.hooks[event]];
|
|
901
|
-
}
|
|
902
|
-
const merged = {
|
|
903
|
-
...existing,
|
|
904
|
-
hooks: userHooks,
|
|
905
|
-
_knitHooks: { ...fresh._knitHooks, merged: true }
|
|
906
|
-
};
|
|
907
|
-
delete merged._engramHooks;
|
|
908
|
-
mkdirSync(claudeDir, { recursive: true });
|
|
909
|
-
writeFileSync(settingsPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
910
|
-
}
|
|
911
|
-
function detectProjectName(rootPath) {
|
|
912
|
-
let name = basename(rootPath);
|
|
913
|
-
try {
|
|
914
|
-
const pkg = JSON.parse(readFileSync(join(rootPath, "package.json"), "utf-8"));
|
|
915
|
-
if (pkg.name) name = pkg.name;
|
|
916
|
-
} catch {
|
|
917
|
-
}
|
|
918
|
-
return name;
|
|
919
|
-
}
|
|
920
|
-
function refreshBrain(rootPath) {
|
|
921
|
-
cache = null;
|
|
922
|
-
return getBrain(rootPath);
|
|
923
|
-
}
|
|
924
|
-
function detectProjectRoot() {
|
|
925
|
-
try {
|
|
926
|
-
return execSync("git rev-parse --show-toplevel 2>/dev/null", { encoding: "utf-8" }).trim();
|
|
927
|
-
} catch {
|
|
928
|
-
return process.cwd();
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
797
|
export {
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
detectProjectRoot
|
|
798
|
+
HOOKS_VERSION,
|
|
799
|
+
generateSettings
|
|
936
800
|
};
|
|
@@ -16,6 +16,11 @@ When to reach for other Knit tools:
|
|
|
16
16
|
- knit_get_workflow({phase}) \u2014 fetch protocol depth for one phase on demand. Do not try to remember the workflow; ask for it.
|
|
17
17
|
- knit_list_features \u2014 if you want to do X but the tool isn't visible, this surfaces what's hidden and how to enable it.
|
|
18
18
|
- knit_save_handoff \u2014 call when context degrades or session ends so the next session resumes cleanly.
|
|
19
|
+
- knit_verify_claim \u2014 fact-check one claim against the graph BEFORE LEARN on standard/complex scope (Stop-hook gate enforces this).
|
|
20
|
+
- knit_index_requirements / knit_generate_test_cases \u2014 for long-form spec / RFC / Jira-export ingestion. Indexes once, retrieves only relevant chunks per feature query.
|
|
21
|
+
- knit_get_fingerprint \u2014 detected stack (lang/framework/test/lint/build/CI) for choosing the right tooling per project.
|
|
22
|
+
- knit_infer_domains + knit_compose_template \u2014 propose CLAUDE.md auto-config sections from git co-change + import-graph + colocation signals.
|
|
23
|
+
- knit_get_calibration / knit_record_false_positive (with a direction tag like #complex-was-trivial) \u2014 feed the self-healing classifier so it tunes per-project over time.
|
|
19
24
|
|
|
20
25
|
The protocol enforces a 4-tier classifier:
|
|
21
26
|
- Inquiry: read-only "what / where / audit / explain" \u2014 just answer.
|
|
@@ -5,13 +5,13 @@ import {
|
|
|
5
5
|
isBundledCore,
|
|
6
6
|
knownAgents,
|
|
7
7
|
rawAgentUrl
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-ST4X7LZT.js";
|
|
9
9
|
import {
|
|
10
10
|
agentsCacheFile,
|
|
11
11
|
projectAgentFile,
|
|
12
12
|
projectAgentsDir,
|
|
13
13
|
sessionsJsonlPath
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-27TA2ZQZ.js";
|
|
15
15
|
|
|
16
16
|
// src/engine/install-agents.ts
|
|
17
17
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|