knit-mcp 0.6.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.
@@ -0,0 +1,720 @@
1
+ import {
2
+ KNIT_MARKER_START,
3
+ buildKnowledge,
4
+ buildReverseDependencies,
5
+ generateClaudeMd,
6
+ spliceKnitBlock
7
+ } from "./chunk-QMICM263.js";
8
+ import {
9
+ readLearnings
10
+ } from "./chunk-GRSYI2RR.js";
11
+ import {
12
+ installAgentsForProject,
13
+ pruneSessionsByAge
14
+ } from "./chunk-TH5QPD5E.js";
15
+ import {
16
+ scanProject
17
+ } from "./chunk-LW6NOFHF.js";
18
+ import {
19
+ importFromMarkdown,
20
+ loadKnowledgeBase,
21
+ saveKnowledgeBase
22
+ } from "./chunk-BAUQEFYY.js";
23
+ import {
24
+ classificationMarkerPath,
25
+ knowledgePath,
26
+ knowledgebasePath,
27
+ learningsDir,
28
+ learningsFilePath,
29
+ legacyClaudeDir,
30
+ legacyKnowledgePath,
31
+ legacyKnowledgebasePath,
32
+ legacyLearningsDir,
33
+ legacyTeamsPath,
34
+ migrationBreadcrumbPath,
35
+ projectDataDir,
36
+ protocolConfigPath,
37
+ sessionMarkerPath,
38
+ sessionsJsonlPath,
39
+ sessionsLogPath,
40
+ teamsPath
41
+ } from "./chunk-YI37OAJ7.js";
42
+
43
+ // src/mcp/cache.ts
44
+ import { execSync } from "child_process";
45
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, readdirSync, statSync } from "fs";
46
+ import { join, basename, dirname } from "path";
47
+
48
+ // src/generators/learnings.ts
49
+ function generateLearningsContent(config) {
50
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
51
+ return `# Project Learnings \u2014 ${config.name}
52
+
53
+ > Recursive learning log. Check this BEFORE starting any task.
54
+ > Grep by \`#tag\` to find relevant lessons for the domain you're working in.
55
+
56
+ ---
57
+
58
+ ## ${date} Project initialized with Engram workflow
59
+ **Domain(s):** All \u2014 workflow infrastructure
60
+ **Approach:** Auto-detected stack (${config.stack.language}${config.stack.framework ? " + " + config.stack.framework : ""}), generated ${config.domains.length} domains, wired hooks for ${config.targetAgent}.
61
+ **Outcome:** Success \u2014 workflow infrastructure in place
62
+ **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.
63
+ **Tags:** #workflow #all #bootstrap
64
+ `;
65
+ }
66
+
67
+ // src/generators/settings.ts
68
+ var HOOKS_VERSION = 3;
69
+ function generateSettings(config, rootPath) {
70
+ return {
71
+ mcpServers: {
72
+ "knit-brain": {
73
+ command: "npx",
74
+ args: ["-y", "@piyushdua/engram-dev@latest"]
75
+ }
76
+ },
77
+ hooks: generateHooks(config, rootPath),
78
+ _knitHooks: { version: HOOKS_VERSION, generatedAt: (/* @__PURE__ */ new Date()).toISOString() }
79
+ };
80
+ }
81
+ function jsLit(s) {
82
+ return JSON.stringify(s.replace(/\\/g, "/"));
83
+ }
84
+ function nodeHook(script) {
85
+ const compact = script.split("\n").map((l) => l.replace(/\/\/.*$/, "").trim()).filter((l) => l.length > 0).join(" ");
86
+ return `node -e '${compact}'`;
87
+ }
88
+ var REPO_ROOT_JS = `
89
+ const __getRoot = () => {
90
+ try {
91
+ return require("child_process").execSync("git rev-parse --show-toplevel", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
92
+ } catch { return process.cwd(); }
93
+ };
94
+ `;
95
+ var GIT_GET_JS = `
96
+ const __git = (cmd, root, fallback) => {
97
+ try {
98
+ return require("child_process").execSync(cmd, { cwd: root, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
99
+ } catch { return fallback === undefined ? "" : fallback; }
100
+ };
101
+ `;
102
+ function generateHooks(config, rootPath) {
103
+ const KB_PATH = knowledgebasePath(rootPath);
104
+ const LEARN_FILE = learningsFilePath(rootPath, config.name);
105
+ const SESSIONS_MD = sessionsLogPath(rootPath);
106
+ const SESSIONS_JSONL = sessionsJsonlPath(rootPath);
107
+ const LEARN_DIR = learningsDir(rootPath);
108
+ const ENGRAM_DIR = projectDataDir(rootPath);
109
+ const PROTOCOL_CONFIG = protocolConfigPath(rootPath);
110
+ const CLASSIFIED_MARKER = classificationMarkerPath(rootPath);
111
+ const SESSION_MARKER = sessionMarkerPath(rootPath);
112
+ const hooks = {
113
+ SessionStart: [
114
+ // Protocol Guard layer 1: drop a marker that knit_load_session
115
+ // should be the first MCP call. Hook itself is best-effort; it doesn't
116
+ // BLOCK on missing load_session, only the per-turn classification gate blocks.
117
+ {
118
+ _knitOwned: true,
119
+ hooks: [
120
+ {
121
+ type: "command",
122
+ command: nodeHook(`
123
+ try {
124
+ const fs = require("fs");
125
+ const path = require("path");
126
+ const p = ${jsLit(SESSION_MARKER)};
127
+ fs.mkdirSync(path.dirname(p), { recursive: true });
128
+ fs.writeFileSync(p, new Date().toISOString());
129
+ console.error("[knit] session marker written. Call knit_load_session as your first MCP call.");
130
+ } catch (e) {}
131
+ `),
132
+ timeout: 5
133
+ }
134
+ ]
135
+ }
136
+ ],
137
+ UserPromptSubmit: [
138
+ // Protocol Guard: each user turn invalidates the previous classification.
139
+ // knit_classify_task must be called fresh per turn before Edit/Write.
140
+ {
141
+ _knitOwned: true,
142
+ hooks: [
143
+ {
144
+ type: "command",
145
+ command: nodeHook(`
146
+ try {
147
+ const fs = require("fs");
148
+ const p = ${jsLit(CLASSIFIED_MARKER)};
149
+ if (fs.existsSync(p)) fs.rmSync(p, { force: true });
150
+ } catch (e) {}
151
+ `),
152
+ timeout: 5
153
+ }
154
+ ]
155
+ }
156
+ ],
157
+ PreToolUse: [
158
+ {
159
+ _knitOwned: true,
160
+ matcher: "Bash",
161
+ hooks: [
162
+ {
163
+ type: "command",
164
+ command: nodeHook(`
165
+ let d = "";
166
+ process.stdin.on("data", (c) => d += c);
167
+ process.stdin.on("end", () => {
168
+ try {
169
+ const i = JSON.parse(d);
170
+ const c = (i.tool_input && i.tool_input.command) || "";
171
+ if (/^git\\s+(push\\b.*\\s(--force|-f)|reset\\s+--hard|commit.*--no-verify)/.test(c)) {
172
+ console.log(JSON.stringify({ decision: "block", reason: "Destructive git operation blocked by Engram. Ask the user first." }));
173
+ }
174
+ } catch (e) {}
175
+ });
176
+ `),
177
+ timeout: 5
178
+ }
179
+ ]
180
+ },
181
+ // Protocol Guard layer 2: gate Edit/Write/MultiEdit on prior knit_classify_task.
182
+ {
183
+ _knitOwned: true,
184
+ matcher: "Edit|Write|MultiEdit",
185
+ hooks: [
186
+ {
187
+ type: "command",
188
+ command: nodeHook(`
189
+ try {
190
+ const fs = require("fs");
191
+ const cfgPath = ${jsLit(PROTOCOL_CONFIG)};
192
+ const markerPath = ${jsLit(CLASSIFIED_MARKER)};
193
+ let level = "warn";
194
+ if (fs.existsSync(cfgPath)) {
195
+ try {
196
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
197
+ if (cfg && (cfg.level === "off" || cfg.level === "warn" || cfg.level === "block")) level = cfg.level;
198
+ } catch (parseErr) {
199
+ console.error("[knit] protocol-config.json unreadable, defaulting strictness=warn:", parseErr && parseErr.message ? parseErr.message : parseErr);
200
+ }
201
+ }
202
+ if (level === "off") return;
203
+ const hasMarker = fs.existsSync(markerPath);
204
+ if (hasMarker) return;
205
+ if (level === "block") {
206
+ console.error("[knit] BLOCKED: call knit_classify_task before Edit/Write. The Protocol Guard prevents implementation without classification.");
207
+ process.exit(2);
208
+ }
209
+ console.error("[knit] reminder: call knit_classify_task before Edit/Write. Set strictness=block via knit_set_protocol_strictness to make this a hard gate.");
210
+ } catch (hookErr) {
211
+ console.error("[knit] protocol-guard hook crashed, allowing tool through:", hookErr && hookErr.message ? hookErr.message : hookErr);
212
+ }
213
+ `),
214
+ timeout: 5
215
+ }
216
+ ]
217
+ }
218
+ ],
219
+ PostToolUse: [],
220
+ Stop: []
221
+ };
222
+ if (config.stack.language === "typescript" && config.stack.typecheckCommand) {
223
+ hooks.PostToolUse.push({
224
+ _knitOwned: true,
225
+ matcher: "Write|Edit",
226
+ hooks: [
227
+ {
228
+ type: "command",
229
+ command: nodeHook(`
230
+ let d = "";
231
+ process.stdin.on("data", (c) => d += c);
232
+ process.stdin.on("end", () => {
233
+ try {
234
+ const i = JSON.parse(d);
235
+ const f = (i.tool_input && i.tool_input.file_path) || (i.tool_response && i.tool_response.filePath) || "";
236
+ if (!/\\.tsx?$/.test(f)) return;
237
+ ${REPO_ROOT_JS}
238
+ require("child_process").execSync("npx tsc --noEmit --pretty false", { cwd: __getRoot(), stdio: "inherit" });
239
+ } catch (e) {}
240
+ });
241
+ `),
242
+ timeout: 30,
243
+ statusMessage: "Type checking..."
244
+ }
245
+ ]
246
+ });
247
+ }
248
+ if (config.stack.language === "python") {
249
+ hooks.PostToolUse.push({
250
+ _knitOwned: true,
251
+ matcher: "Write|Edit",
252
+ hooks: [
253
+ {
254
+ type: "command",
255
+ command: nodeHook(`
256
+ let d = "";
257
+ process.stdin.on("data", (c) => d += c);
258
+ process.stdin.on("end", () => {
259
+ try {
260
+ const i = JSON.parse(d);
261
+ const f = (i.tool_input && i.tool_input.file_path) || (i.tool_response && i.tool_response.filePath) || "";
262
+ if (!/\\.py$/.test(f)) return;
263
+ ${REPO_ROOT_JS}
264
+ require("child_process").execSync("python3 -m py_compile " + JSON.stringify(f), { cwd: __getRoot(), stdio: "inherit" });
265
+ } catch (e) {}
266
+ });
267
+ `),
268
+ timeout: 15,
269
+ statusMessage: "Checking Python syntax..."
270
+ }
271
+ ]
272
+ });
273
+ }
274
+ if (config.stack.language === "go") {
275
+ hooks.PostToolUse.push({
276
+ _knitOwned: true,
277
+ matcher: "Write|Edit",
278
+ hooks: [
279
+ {
280
+ type: "command",
281
+ command: nodeHook(`
282
+ let d = "";
283
+ process.stdin.on("data", (c) => d += c);
284
+ process.stdin.on("end", () => {
285
+ try {
286
+ const i = JSON.parse(d);
287
+ const f = (i.tool_input && i.tool_input.file_path) || (i.tool_response && i.tool_response.filePath) || "";
288
+ if (!/\\.go$/.test(f)) return;
289
+ ${REPO_ROOT_JS}
290
+ require("child_process").execSync("go vet ./...", { cwd: __getRoot(), stdio: "inherit" });
291
+ } catch (e) {}
292
+ });
293
+ `),
294
+ timeout: 30,
295
+ statusMessage: "Running go vet..."
296
+ }
297
+ ]
298
+ });
299
+ }
300
+ if (config.stack.language === "rust") {
301
+ hooks.PostToolUse.push({
302
+ _knitOwned: true,
303
+ matcher: "Write|Edit",
304
+ hooks: [
305
+ {
306
+ type: "command",
307
+ command: nodeHook(`
308
+ let d = "";
309
+ process.stdin.on("data", (c) => d += c);
310
+ process.stdin.on("end", () => {
311
+ try {
312
+ const i = JSON.parse(d);
313
+ const f = (i.tool_input && i.tool_input.file_path) || (i.tool_response && i.tool_response.filePath) || "";
314
+ if (!/\\.rs$/.test(f)) return;
315
+ ${REPO_ROOT_JS}
316
+ require("child_process").execSync("cargo check", { cwd: __getRoot(), stdio: "inherit" });
317
+ } catch (e) {}
318
+ });
319
+ `),
320
+ timeout: 60,
321
+ statusMessage: "Running cargo check..."
322
+ }
323
+ ]
324
+ });
325
+ }
326
+ const steps = [];
327
+ if (config.stack.typecheckCommand) steps.push(["TYPECHECK", config.stack.typecheckCommand]);
328
+ if (config.stack.lintCommand) steps.push(["LINT", config.stack.lintCommand]);
329
+ if (config.stack.buildCommand) steps.push(["BUILD", config.stack.buildCommand]);
330
+ if (steps.length > 0) {
331
+ hooks.Stop.push({
332
+ _knitOwned: true,
333
+ hooks: [
334
+ {
335
+ type: "command",
336
+ command: nodeHook(`
337
+ ${REPO_ROOT_JS}
338
+ const steps = ${JSON.stringify(steps)};
339
+ for (const [name, cmd] of steps) {
340
+ console.log("--- " + name + " ---");
341
+ try {
342
+ require("child_process").execSync(cmd, { cwd: __getRoot(), stdio: "inherit" });
343
+ } catch (e) { break; }
344
+ }
345
+ `),
346
+ timeout: 120,
347
+ statusMessage: "Engram: final build verification..."
348
+ }
349
+ ]
350
+ });
351
+ }
352
+ hooks.Stop.push({
353
+ _knitOwned: true,
354
+ hooks: [
355
+ {
356
+ type: "command",
357
+ command: nodeHook(`
358
+ try {
359
+ const fs = require("fs");
360
+ ${REPO_ROOT_JS}
361
+ ${GIT_GET_JS}
362
+ const dir = ${jsLit(LEARN_DIR)};
363
+ const file = ${jsLit(SESSIONS_MD)};
364
+ fs.mkdirSync(dir, { recursive: true });
365
+ if (!fs.existsSync(file)) fs.writeFileSync(file, "# Session Log\\n");
366
+ const root = __getRoot();
367
+ const branch = __git("git branch --show-current", root);
368
+ const commits = __git("git log --oneline -3", root).split("\\n").filter(Boolean).map((l) => " - " + l).join("\\n");
369
+ const changed = __git("git diff --stat HEAD", root).split("\\n").filter(Boolean).pop() || "";
370
+ const ts = new Date().toISOString().replace("T", " ").slice(0, 19) + " UTC";
371
+ let out = "\\n## Session " + ts + "\\n- Branch: " + branch + "\\n- Recent commits:\\n" + commits + "\\n";
372
+ if (changed) out += "- Uncommitted: " + changed + "\\n";
373
+ out += "\\n";
374
+ fs.appendFileSync(file, out);
375
+ } catch (e) {}
376
+ `),
377
+ timeout: 10,
378
+ statusMessage: "Engram: capturing session state..."
379
+ }
380
+ ]
381
+ });
382
+ hooks.Stop.push({
383
+ _knitOwned: true,
384
+ hooks: [
385
+ {
386
+ type: "command",
387
+ command: nodeHook(`
388
+ try {
389
+ const fs = require("fs");
390
+ ${REPO_ROOT_JS}
391
+ ${GIT_GET_JS}
392
+ const dir = ${jsLit(ENGRAM_DIR)};
393
+ const file = ${jsLit(SESSIONS_JSONL)};
394
+ fs.mkdirSync(dir, { recursive: true });
395
+ const root = __getRoot();
396
+ const branch = __git("git branch --show-current", root, null);
397
+ const filesMod = __git("git diff --name-only HEAD", root).split("\\n").filter(Boolean).length;
398
+ const commits = __git("git log --oneline -3", root).split("\\n").filter(Boolean).map((l) => l.split(" ")[0]).join(" ");
399
+ const now = new Date();
400
+ const entry = {
401
+ id: now.getTime() + "-" + process.pid,
402
+ date: now.toISOString().slice(0, 10),
403
+ timestamp: now.toISOString(),
404
+ branch: branch,
405
+ filesModified: filesMod,
406
+ commits: commits,
407
+ };
408
+ fs.appendFileSync(file, JSON.stringify(entry) + "\\n");
409
+ } catch (e) {}
410
+ `),
411
+ timeout: 10,
412
+ statusMessage: "Engram: recording session tuple..."
413
+ }
414
+ ]
415
+ });
416
+ hooks.Stop.push({
417
+ _knitOwned: true,
418
+ hooks: [
419
+ {
420
+ type: "command",
421
+ command: nodeHook(`
422
+ try {
423
+ const fs = require("fs");
424
+ const file = ${jsLit(LEARN_FILE)};
425
+ if (!fs.existsSync(file)) return;
426
+ const ageSec = (Date.now() - fs.statSync(file).mtimeMs) / 1000;
427
+ if (ageSec > 300) {
428
+ console.log("");
429
+ console.log("[Engram] LEARN was not recorded this session. That's fine if nothing reusable surfaced.");
430
+ console.log(" If something did, call knit_record_learning in your next session.");
431
+ console.log("");
432
+ }
433
+ } catch (e) {}
434
+ `),
435
+ timeout: 5,
436
+ statusMessage: "Engram: checking LEARN compliance..."
437
+ }
438
+ ]
439
+ });
440
+ hooks.Stop.push({
441
+ _knitOwned: true,
442
+ hooks: [
443
+ {
444
+ type: "command",
445
+ command: nodeHook(`
446
+ try {
447
+ const fs = require("fs");
448
+ ${REPO_ROOT_JS}
449
+ ${GIT_GET_JS}
450
+ const p = ${jsLit(KB_PATH)};
451
+ if (!fs.existsSync(p)) return;
452
+ const kb = JSON.parse(fs.readFileSync(p, "utf-8"));
453
+ const root = __getRoot();
454
+ const files = __git("git diff --name-only HEAD", root).split("\\n").filter(Boolean).length;
455
+ const branch = __git("git branch --show-current", root, null) || null;
456
+ kb.metrics.totalSessions++;
457
+ kb.metrics.sessions.push({
458
+ date: new Date().toISOString().split("T")[0],
459
+ branch: branch,
460
+ filesModified: files,
461
+ learningsAccessed: 0,
462
+ learningsAdded: 0,
463
+ domainsTouched: [],
464
+ });
465
+ if (kb.metrics.sessions.length > 20) kb.metrics.sessions = kb.metrics.sessions.slice(-20);
466
+ fs.writeFileSync(p, JSON.stringify(kb, null, 2));
467
+ } catch (e) {}
468
+ `),
469
+ timeout: 10,
470
+ statusMessage: "Engram: updating session metrics..."
471
+ }
472
+ ]
473
+ });
474
+ return hooks;
475
+ }
476
+
477
+ // src/mcp/cache.ts
478
+ var cache = null;
479
+ var hooksRefreshed = /* @__PURE__ */ new Set();
480
+ function maybeRefreshHooks(rootPath, config) {
481
+ if (hooksRefreshed.has(rootPath)) return;
482
+ hooksRefreshed.add(rootPath);
483
+ const settingsPath = join(rootPath, ".claude", "settings.local.json");
484
+ if (!existsSync(settingsPath)) return;
485
+ try {
486
+ const existing = JSON.parse(readFileSync(settingsPath, "utf-8"));
487
+ const storedVersion = existing?._knitHooks?.version ?? 0;
488
+ if (storedVersion < HOOKS_VERSION) {
489
+ writeKnitHooks(rootPath, config);
490
+ }
491
+ } catch {
492
+ }
493
+ }
494
+ function getBrain(rootPath) {
495
+ if (cache && cache.rootPath === rootPath) {
496
+ return cache;
497
+ }
498
+ let autoInitialized = false;
499
+ const haveCentralized = existsSync(knowledgePath(rootPath));
500
+ const haveLegacy = existsSync(legacyKnowledgePath(rootPath));
501
+ if (!haveCentralized) {
502
+ if (haveLegacy) {
503
+ migrateLegacyData(rootPath);
504
+ } else {
505
+ autoInitialize(rootPath);
506
+ autoInitialized = true;
507
+ }
508
+ }
509
+ const scan = scanProject(rootPath);
510
+ const knowledge = buildKnowledge(rootPath, scan);
511
+ const reverseDeps = buildReverseDependencies(knowledge.importGraph);
512
+ const projectName = detectProjectName(rootPath);
513
+ const knowledgeBase = loadKnowledgeBase(knowledgebasePath(rootPath), projectName);
514
+ const config = {
515
+ name: projectName,
516
+ packageManager: scan.packageManager,
517
+ stack: scan.stack,
518
+ domains: scan.domains,
519
+ targetAgent: "claude-code",
520
+ tokenOptimization: "standard"
521
+ };
522
+ writeFileSync(knowledgePath(rootPath), JSON.stringify(knowledge, null, 2), "utf-8");
523
+ saveKnowledgeBase(knowledgebasePath(rootPath), knowledgeBase);
524
+ if (!autoInitialized) {
525
+ maybeRefreshHooks(rootPath, config);
526
+ }
527
+ cache = {
528
+ rootPath,
529
+ knowledge,
530
+ reverseDeps,
531
+ knowledgeBase,
532
+ config,
533
+ loadedAt: Date.now(),
534
+ autoInitialized
535
+ };
536
+ return cache;
537
+ }
538
+ function autoInitialize(rootPath) {
539
+ const scan = scanProject(rootPath);
540
+ const knowledge = buildKnowledge(rootPath, scan);
541
+ const projectName = detectProjectName(rootPath);
542
+ const config = {
543
+ name: projectName,
544
+ packageManager: scan.packageManager,
545
+ stack: scan.stack,
546
+ domains: scan.domains,
547
+ targetAgent: "claude-code",
548
+ tokenOptimization: "standard"
549
+ };
550
+ mkdirSync(projectDataDir(rootPath), { recursive: true });
551
+ mkdirSync(learningsDir(rootPath), { recursive: true });
552
+ writeProjectClaudeMd(rootPath, config, knowledge);
553
+ writeKnitHooks(rootPath, config);
554
+ installAgentsForProject(rootPath, config, knowledge, null).catch((err) => {
555
+ process.stderr.write(`[knit] agent install background error: ${err?.message ?? err}
556
+ `);
557
+ });
558
+ Promise.resolve().then(() => {
559
+ try {
560
+ pruneSessionsByAge(rootPath, 90);
561
+ } catch (e) {
562
+ const msg = e instanceof Error ? e.message : String(e);
563
+ process.stderr.write(`[knit] session prune background error: ${msg}
564
+ `);
565
+ }
566
+ });
567
+ const learningsPath = learningsFilePath(rootPath, projectName);
568
+ if (!existsSync(learningsPath)) {
569
+ writeFileSync(learningsPath, generateLearningsContent(config), "utf-8");
570
+ }
571
+ const kbPath = knowledgebasePath(rootPath);
572
+ const kb = loadKnowledgeBase(kbPath, projectName);
573
+ const entries = readLearnings(learningsPath);
574
+ importFromMarkdown(kb, entries);
575
+ saveKnowledgeBase(kbPath, kb);
576
+ writeFileSync(knowledgePath(rootPath), JSON.stringify(knowledge, null, 2), "utf-8");
577
+ }
578
+ function migrateLegacyData(rootPath) {
579
+ mkdirSync(projectDataDir(rootPath), { recursive: true });
580
+ mkdirSync(learningsDir(rootPath), { recursive: true });
581
+ copyIfExists(legacyKnowledgePath(rootPath), knowledgePath(rootPath));
582
+ copyIfExists(legacyKnowledgebasePath(rootPath), knowledgebasePath(rootPath));
583
+ copyIfExists(legacyTeamsPath(rootPath), teamsPath(rootPath));
584
+ const legacyLearn = legacyLearningsDir(rootPath);
585
+ if (existsSync(legacyLearn)) {
586
+ for (const file of readdirSync(legacyLearn)) {
587
+ const src = join(legacyLearn, file);
588
+ const dst = join(learningsDir(rootPath), file);
589
+ try {
590
+ if (statSync(src).isFile() && !existsSync(dst)) {
591
+ copyFileSync(src, dst);
592
+ }
593
+ } catch {
594
+ }
595
+ }
596
+ }
597
+ const breadcrumb = migrationBreadcrumbPath(rootPath);
598
+ const newPath = projectDataDir(rootPath);
599
+ if (!existsSync(breadcrumb) && existsSync(legacyClaudeDir(rootPath))) {
600
+ const note = `Knit data migrated to ~/.knit/ on ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.
601
+
602
+ Centralized location for this project:
603
+ ${newPath}
604
+
605
+ The legacy files in this .claude/ directory are no longer read by engram and
606
+ can be deleted at your discretion. Future learnings, knowledge indexes, and
607
+ session memory live in the new path.
608
+ `;
609
+ try {
610
+ writeFileSync(breadcrumb, note, "utf-8");
611
+ } catch {
612
+ }
613
+ }
614
+ }
615
+ function writeProjectClaudeMd(rootPath, config, knowledge) {
616
+ const claudeMdPath = join(rootPath, "CLAUDE.md");
617
+ const block = generateClaudeMd(config, knowledge);
618
+ if (!existsSync(claudeMdPath)) {
619
+ writeFileSync(claudeMdPath, block, "utf-8");
620
+ return;
621
+ }
622
+ const existing = readFileSync(claudeMdPath, "utf-8");
623
+ if (existing.includes(KNIT_MARKER_START)) {
624
+ const { content } = spliceKnitBlock(existing, block);
625
+ writeFileSync(claudeMdPath, content, "utf-8");
626
+ return;
627
+ }
628
+ const sidecarDir = join(rootPath, ".claude");
629
+ const sidecarPath = join(sidecarDir, "KNIT.md");
630
+ mkdirSync(sidecarDir, { recursive: true });
631
+ const sidecar = `<!-- This file is engram's per-project workflow. -->
632
+ <!-- Your CLAUDE.md exists without engram markers, so engram wrote here instead of clobbering it. -->
633
+ <!-- To include this content in CLAUDE.md, add: @.claude/KNIT.md -->
634
+
635
+ ${block}`;
636
+ writeFileSync(sidecarPath, sidecar, "utf-8");
637
+ }
638
+ function copyIfExists(src, dst) {
639
+ if (existsSync(src) && !existsSync(dst)) {
640
+ mkdirSync(dirname(dst), { recursive: true });
641
+ copyFileSync(src, dst);
642
+ }
643
+ }
644
+ function writeKnitHooks(rootPath, config) {
645
+ const claudeDir = join(rootPath, ".claude");
646
+ const settingsPath = join(claudeDir, "settings.local.json");
647
+ const fresh = generateSettings(config, rootPath);
648
+ if (!existsSync(settingsPath)) {
649
+ mkdirSync(claudeDir, { recursive: true });
650
+ writeFileSync(settingsPath, JSON.stringify(fresh, null, 2), "utf-8");
651
+ return;
652
+ }
653
+ let existing;
654
+ try {
655
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
656
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
657
+ return;
658
+ }
659
+ existing = parsed;
660
+ } catch {
661
+ return;
662
+ }
663
+ if ("_knitHooks" in existing) {
664
+ mkdirSync(claudeDir, { recursive: true });
665
+ writeFileSync(settingsPath, JSON.stringify(fresh, null, 2), "utf-8");
666
+ return;
667
+ }
668
+ const userHooksRaw = existing.hooks;
669
+ let userHooks;
670
+ if (userHooksRaw === void 0) {
671
+ userHooks = {};
672
+ } else if (userHooksRaw && typeof userHooksRaw === "object" && !Array.isArray(userHooksRaw)) {
673
+ for (const v of Object.values(userHooksRaw)) {
674
+ if (!Array.isArray(v)) return;
675
+ }
676
+ userHooks = { ...userHooksRaw };
677
+ } else {
678
+ return;
679
+ }
680
+ for (const event of Object.keys(fresh.hooks)) {
681
+ const userEntries = Array.isArray(userHooks[event]) ? userHooks[event] : [];
682
+ const preserved = userEntries.filter((entry) => {
683
+ return !(entry && typeof entry === "object" && entry._knitOwned === true);
684
+ });
685
+ userHooks[event] = [...preserved, ...fresh.hooks[event]];
686
+ }
687
+ const merged = {
688
+ ...existing,
689
+ hooks: userHooks,
690
+ _knitHooks: { ...fresh._knitHooks, merged: true }
691
+ };
692
+ mkdirSync(claudeDir, { recursive: true });
693
+ writeFileSync(settingsPath, JSON.stringify(merged, null, 2), "utf-8");
694
+ }
695
+ function detectProjectName(rootPath) {
696
+ let name = basename(rootPath);
697
+ try {
698
+ const pkg = JSON.parse(readFileSync(join(rootPath, "package.json"), "utf-8"));
699
+ if (pkg.name) name = pkg.name;
700
+ } catch {
701
+ }
702
+ return name;
703
+ }
704
+ function refreshBrain(rootPath) {
705
+ cache = null;
706
+ return getBrain(rootPath);
707
+ }
708
+ function detectProjectRoot() {
709
+ try {
710
+ return execSync("git rev-parse --show-toplevel 2>/dev/null", { encoding: "utf-8" }).trim();
711
+ } catch {
712
+ return process.cwd();
713
+ }
714
+ }
715
+
716
+ export {
717
+ getBrain,
718
+ refreshBrain,
719
+ detectProjectRoot
720
+ };