knit-mcp 0.9.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.
@@ -5,13 +5,13 @@ import {
5
5
  isBundledCore,
6
6
  knownAgents,
7
7
  rawAgentUrl
8
- } from "./chunk-7PPC6IG6.js";
8
+ } from "./chunk-ST4X7LZT.js";
9
9
  import {
10
10
  agentsCacheFile,
11
11
  projectAgentFile,
12
12
  projectAgentsDir,
13
13
  sessionsJsonlPath
14
- } from "./chunk-XFS2XGZI.js";
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";
@@ -339,8 +339,16 @@ import { existsSync as existsSync3, mkdirSync as mkdirSync3, appendFileSync, rea
339
339
  import { dirname as dirname2 } from "path";
340
340
  function appendSession(rootPath, entry) {
341
341
  const path = sessionsJsonlPath(rootPath);
342
- mkdirSync3(dirname2(path), { recursive: true });
343
- appendFileSync(path, JSON.stringify(entry) + "\n", "utf-8");
342
+ try {
343
+ mkdirSync3(dirname2(path), { recursive: true });
344
+ appendFileSync(path, JSON.stringify(entry) + "\n", "utf-8");
345
+ } catch (err) {
346
+ process.stderr.write(
347
+ `[knit] session append failed at ${path}: ${err.message}
348
+ `
349
+ );
350
+ throw err;
351
+ }
344
352
  }
345
353
  function searchSessions(rootPath, query, limit = 10) {
346
354
  const lines = readAllLines(rootPath);
@@ -1,5 +1,5 @@
1
1
  // src/engine/scanner.ts
2
- import { readFileSync, existsSync } from "fs";
2
+ import { readFileSync, existsSync, readdirSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { execSync } from "child_process";
5
5
 
@@ -89,6 +89,63 @@ function scanProject(rootPath) {
89
89
  git: detectGit(rootPath)
90
90
  };
91
91
  }
92
+ function scanProjectFingerprint(rootPath) {
93
+ const stack = detectStack(rootPath);
94
+ const pm = detectPackageManager(rootPath);
95
+ const languages = [];
96
+ if (stack.language && stack.language !== "unknown") languages.push(stack.language);
97
+ if (stack.language !== "python" && existsSync(join(rootPath, "pyproject.toml"))) languages.push("python");
98
+ if (stack.language !== "go" && existsSync(join(rootPath, "go.mod"))) languages.push("go");
99
+ if (stack.language !== "rust" && existsSync(join(rootPath, "Cargo.toml"))) languages.push("rust");
100
+ return {
101
+ languages,
102
+ framework: stack.framework,
103
+ testRunner: stack.testFramework,
104
+ linter: detectLinter(rootPath, stack.language),
105
+ buildCommand: stack.buildCommand,
106
+ lintCommand: stack.lintCommand,
107
+ typecheckCommand: stack.typecheckCommand,
108
+ packageManager: pm === "unknown" ? null : pm,
109
+ ciFiles: detectCiFiles(rootPath),
110
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString()
111
+ };
112
+ }
113
+ function detectLinter(rootPath, language) {
114
+ if (existsSync(join(rootPath, ".eslintrc.json")) || existsSync(join(rootPath, ".eslintrc.js")) || existsSync(join(rootPath, "eslint.config.js")) || existsSync(join(rootPath, "eslint.config.mjs")) || existsSync(join(rootPath, "eslint.config.ts"))) return "eslint";
115
+ if (existsSync(join(rootPath, ".ruff.toml")) || existsSync(join(rootPath, "ruff.toml"))) return "ruff";
116
+ if (existsSync(join(rootPath, ".golangci.yml")) || existsSync(join(rootPath, ".golangci.yaml"))) return "golangci-lint";
117
+ if (existsSync(join(rootPath, "clippy.toml"))) return "clippy";
118
+ if (language === "python" && existsSync(join(rootPath, "pyproject.toml"))) {
119
+ try {
120
+ const py = readFileSync(join(rootPath, "pyproject.toml"), "utf-8");
121
+ if (py.includes("ruff")) return "ruff";
122
+ if (py.includes("flake8")) return "flake8";
123
+ if (py.includes("pylint")) return "pylint";
124
+ } catch {
125
+ }
126
+ }
127
+ if (language === "go") return "golangci-lint";
128
+ if (language === "rust") return "clippy";
129
+ return null;
130
+ }
131
+ function detectCiFiles(rootPath) {
132
+ const out = [];
133
+ const ghDir = join(rootPath, ".github", "workflows");
134
+ if (existsSync(ghDir)) {
135
+ try {
136
+ for (const f of readdirSync(ghDir)) {
137
+ if (f.endsWith(".yml") || f.endsWith(".yaml")) {
138
+ out.push(join(".github/workflows", f));
139
+ }
140
+ }
141
+ } catch {
142
+ }
143
+ }
144
+ for (const f of [".gitlab-ci.yml", ".circleci/config.yml", ".travis.yml", "Jenkinsfile", "azure-pipelines.yml"]) {
145
+ if (existsSync(join(rootPath, f))) out.push(f);
146
+ }
147
+ return out;
148
+ }
92
149
  function detectPackageManager(root) {
93
150
  if (existsSync(join(root, "bun.lockb")) || existsSync(join(root, "bun.lock"))) return "bun";
94
151
  if (existsSync(join(root, "pnpm-lock.yaml"))) return "pnpm";
@@ -278,5 +335,6 @@ export {
278
335
  categoryOf,
279
336
  rawAgentUrl,
280
337
  isBundledCore,
281
- scanProject
338
+ scanProject,
339
+ scanProjectFingerprint
282
340
  };
@@ -0,0 +1,328 @@
1
+ import {
2
+ HOOKS_VERSION,
3
+ generateSettings
4
+ } from "./chunk-HROSQ5MS.js";
5
+ import {
6
+ installAgentsForProject,
7
+ prewarmLatestVersion,
8
+ pruneSessionsByAge
9
+ } from "./chunk-RZOVZYTF.js";
10
+ import {
11
+ buildKnowledge,
12
+ buildReverseDependencies
13
+ } from "./chunk-MOOVNMIN.js";
14
+ import {
15
+ scanProject
16
+ } from "./chunk-ST4X7LZT.js";
17
+ import {
18
+ readLearnings
19
+ } from "./chunk-M3YZOJNW.js";
20
+ import {
21
+ persistScanResult,
22
+ scanIntegrations
23
+ } from "./chunk-VB2TIR6L.js";
24
+ import {
25
+ KNIT_MARKER_START,
26
+ generateClaudeMd,
27
+ spliceKnitBlock
28
+ } from "./chunk-7UFS67HP.js";
29
+ import {
30
+ importFromMarkdown,
31
+ loadKnowledgeBaseSafe,
32
+ saveKnowledgeBase
33
+ } from "./chunk-WKQHCLLO.js";
34
+ import {
35
+ knowledgePath,
36
+ knowledgebasePath,
37
+ learningsDir,
38
+ learningsFilePath,
39
+ legacyClaudeDir,
40
+ legacyKnowledgePath,
41
+ legacyKnowledgebasePath,
42
+ legacyLearningsDir,
43
+ legacyTeamsPath,
44
+ migrationBreadcrumbPath,
45
+ projectDataDir,
46
+ teamsPath
47
+ } from "./chunk-27TA2ZQZ.js";
48
+
49
+ // src/mcp/cache.ts
50
+ import { execSync } from "child_process";
51
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, readdirSync, statSync } from "fs";
52
+ import { join, basename, dirname } from "path";
53
+
54
+ // src/generators/learnings.ts
55
+ function generateLearningsContent(config) {
56
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
57
+ return `# Project Learnings \u2014 ${config.name}
58
+
59
+ > Recursive learning log. Check this BEFORE starting any task.
60
+ > Grep by \`#tag\` to find relevant lessons for the domain you're working in.
61
+
62
+ ---
63
+
64
+ ## ${date} Project initialized with Knit workflow
65
+ **Domain(s):** All \u2014 workflow infrastructure
66
+ **Approach:** Auto-detected stack (${config.stack.language}${config.stack.framework ? " + " + config.stack.framework : ""}), generated ${config.domains.length} domains, wired hooks for ${config.targetAgent}.
67
+ **Outcome:** Success \u2014 workflow infrastructure in place
68
+ **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.
69
+ **Tags:** #workflow #all #bootstrap
70
+ `;
71
+ }
72
+
73
+ // src/mcp/cache.ts
74
+ var cache = null;
75
+ var hooksRefreshed = /* @__PURE__ */ new Set();
76
+ function maybeRefreshHooks(rootPath, config) {
77
+ if (hooksRefreshed.has(rootPath)) return;
78
+ hooksRefreshed.add(rootPath);
79
+ const settingsPath = join(rootPath, ".claude", "settings.local.json");
80
+ if (!existsSync(settingsPath)) return;
81
+ try {
82
+ const existing = JSON.parse(readFileSync(settingsPath, "utf-8"));
83
+ const storedVersion = existing?._knitHooks?.version ?? (existing?._engramHooks ? 0 : 0);
84
+ if (storedVersion < HOOKS_VERSION) {
85
+ writeKnitHooks(rootPath, config);
86
+ }
87
+ } catch {
88
+ }
89
+ }
90
+ function getBrain(rootPath) {
91
+ if (cache && cache.rootPath === rootPath) {
92
+ return cache;
93
+ }
94
+ void prewarmLatestVersion();
95
+ let autoInitialized = false;
96
+ const haveCentralized = existsSync(knowledgePath(rootPath));
97
+ const haveLegacy = existsSync(legacyKnowledgePath(rootPath));
98
+ if (!haveCentralized) {
99
+ if (haveLegacy) {
100
+ migrateLegacyData(rootPath);
101
+ } else {
102
+ autoInitialize(rootPath);
103
+ autoInitialized = true;
104
+ }
105
+ }
106
+ const scan = scanProject(rootPath);
107
+ const knowledge = buildKnowledge(rootPath, scan);
108
+ const reverseDeps = buildReverseDependencies(knowledge.importGraph);
109
+ const projectName = detectProjectName(rootPath);
110
+ const kbLoad = loadKnowledgeBaseSafe(knowledgebasePath(rootPath), projectName);
111
+ const knowledgeBase = kbLoad.kb;
112
+ const config = {
113
+ name: projectName,
114
+ packageManager: scan.packageManager,
115
+ stack: scan.stack,
116
+ domains: scan.domains,
117
+ targetAgent: "claude-code",
118
+ tokenOptimization: "standard"
119
+ };
120
+ writeFileSync(knowledgePath(rootPath), JSON.stringify(knowledge, null, 2), "utf-8");
121
+ if (!kbLoad.loadFailed) {
122
+ saveKnowledgeBase(knowledgebasePath(rootPath), knowledgeBase);
123
+ }
124
+ if (!autoInitialized) {
125
+ maybeRefreshHooks(rootPath, config);
126
+ }
127
+ cache = {
128
+ rootPath,
129
+ knowledge,
130
+ reverseDeps,
131
+ knowledgeBase,
132
+ config,
133
+ loadedAt: Date.now(),
134
+ autoInitialized
135
+ };
136
+ return cache;
137
+ }
138
+ function autoInitialize(rootPath) {
139
+ const scan = scanProject(rootPath);
140
+ const knowledge = buildKnowledge(rootPath, scan);
141
+ const projectName = detectProjectName(rootPath);
142
+ const config = {
143
+ name: projectName,
144
+ packageManager: scan.packageManager,
145
+ stack: scan.stack,
146
+ domains: scan.domains,
147
+ targetAgent: "claude-code",
148
+ tokenOptimization: "standard"
149
+ };
150
+ mkdirSync(projectDataDir(rootPath), { recursive: true });
151
+ mkdirSync(learningsDir(rootPath), { recursive: true });
152
+ writeProjectClaudeMd(rootPath, config, knowledge);
153
+ writeKnitHooks(rootPath, config);
154
+ installAgentsForProject(rootPath, config, knowledge, null).catch((err) => {
155
+ process.stderr.write(`[knit] agent install background error: ${err?.message ?? err}
156
+ `);
157
+ });
158
+ Promise.resolve().then(() => {
159
+ try {
160
+ pruneSessionsByAge(rootPath, 90);
161
+ } catch (e) {
162
+ const msg = e instanceof Error ? e.message : String(e);
163
+ process.stderr.write(`[knit] session prune background error: ${msg}
164
+ `);
165
+ }
166
+ });
167
+ Promise.resolve().then(() => {
168
+ try {
169
+ const result = scanIntegrations(rootPath);
170
+ persistScanResult(rootPath, result);
171
+ } catch (e) {
172
+ const msg = e instanceof Error ? e.message : String(e);
173
+ process.stderr.write(`[knit] integration scan background error: ${msg}
174
+ `);
175
+ }
176
+ });
177
+ const learningsPath = learningsFilePath(rootPath, projectName);
178
+ if (!existsSync(learningsPath)) {
179
+ writeFileSync(learningsPath, generateLearningsContent(config), "utf-8");
180
+ }
181
+ const kbPath = knowledgebasePath(rootPath);
182
+ const kb = loadKnowledgeBaseSafe(kbPath, projectName).kb;
183
+ const entries = readLearnings(learningsPath);
184
+ importFromMarkdown(kb, entries);
185
+ saveKnowledgeBase(kbPath, kb);
186
+ writeFileSync(knowledgePath(rootPath), JSON.stringify(knowledge, null, 2), "utf-8");
187
+ }
188
+ function migrateLegacyData(rootPath) {
189
+ mkdirSync(projectDataDir(rootPath), { recursive: true });
190
+ mkdirSync(learningsDir(rootPath), { recursive: true });
191
+ copyIfExists(legacyKnowledgePath(rootPath), knowledgePath(rootPath));
192
+ copyIfExists(legacyKnowledgebasePath(rootPath), knowledgebasePath(rootPath));
193
+ copyIfExists(legacyTeamsPath(rootPath), teamsPath(rootPath));
194
+ const legacyLearn = legacyLearningsDir(rootPath);
195
+ if (existsSync(legacyLearn)) {
196
+ for (const file of readdirSync(legacyLearn)) {
197
+ const src = join(legacyLearn, file);
198
+ const dst = join(learningsDir(rootPath), file);
199
+ try {
200
+ if (statSync(src).isFile() && !existsSync(dst)) {
201
+ copyFileSync(src, dst);
202
+ }
203
+ } catch {
204
+ }
205
+ }
206
+ }
207
+ const breadcrumb = migrationBreadcrumbPath(rootPath);
208
+ const newPath = projectDataDir(rootPath);
209
+ if (!existsSync(breadcrumb) && existsSync(legacyClaudeDir(rootPath))) {
210
+ const note = `Knit data migrated to ~/.knit/ on ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.
211
+
212
+ Centralized location for this project:
213
+ ${newPath}
214
+
215
+ The legacy files in this .claude/ directory are no longer read by engram and
216
+ can be deleted at your discretion. Future learnings, knowledge indexes, and
217
+ session memory live in the new path.
218
+ `;
219
+ try {
220
+ writeFileSync(breadcrumb, note, "utf-8");
221
+ } catch {
222
+ }
223
+ }
224
+ }
225
+ function writeProjectClaudeMd(rootPath, config, knowledge) {
226
+ const claudeMdPath = join(rootPath, "CLAUDE.md");
227
+ const block = generateClaudeMd(config, knowledge);
228
+ if (!existsSync(claudeMdPath)) {
229
+ writeFileSync(claudeMdPath, block, "utf-8");
230
+ return;
231
+ }
232
+ const existing = readFileSync(claudeMdPath, "utf-8");
233
+ if (existing.includes(KNIT_MARKER_START)) {
234
+ const { content } = spliceKnitBlock(existing, block);
235
+ writeFileSync(claudeMdPath, content, "utf-8");
236
+ return;
237
+ }
238
+ const sidecarDir = join(rootPath, ".claude");
239
+ const sidecarPath = join(sidecarDir, "KNIT.md");
240
+ mkdirSync(sidecarDir, { recursive: true });
241
+ const sidecar = `<!-- This file is Knit's per-project workflow. -->
242
+ <!-- Your CLAUDE.md exists without Knit markers, so Knit wrote here instead of clobbering it. -->
243
+ <!-- To include this content in CLAUDE.md, add: @.claude/KNIT.md -->
244
+
245
+ ${block}`;
246
+ writeFileSync(sidecarPath, sidecar, "utf-8");
247
+ }
248
+ function copyIfExists(src, dst) {
249
+ if (existsSync(src) && !existsSync(dst)) {
250
+ mkdirSync(dirname(dst), { recursive: true });
251
+ copyFileSync(src, dst);
252
+ }
253
+ }
254
+ function writeKnitHooks(rootPath, config) {
255
+ const claudeDir = join(rootPath, ".claude");
256
+ const settingsPath = join(claudeDir, "settings.local.json");
257
+ const fresh = generateSettings(config, rootPath);
258
+ if (!existsSync(settingsPath)) {
259
+ mkdirSync(claudeDir, { recursive: true });
260
+ writeFileSync(settingsPath, JSON.stringify(fresh, null, 2), "utf-8");
261
+ return;
262
+ }
263
+ let existing;
264
+ try {
265
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
266
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
267
+ return;
268
+ }
269
+ existing = parsed;
270
+ } catch {
271
+ return;
272
+ }
273
+ const userHooksRaw = existing.hooks;
274
+ let userHooks;
275
+ if (userHooksRaw === void 0) {
276
+ userHooks = {};
277
+ } else if (userHooksRaw && typeof userHooksRaw === "object" && !Array.isArray(userHooksRaw)) {
278
+ for (const v of Object.values(userHooksRaw)) {
279
+ if (!Array.isArray(v)) return;
280
+ }
281
+ userHooks = { ...userHooksRaw };
282
+ } else {
283
+ return;
284
+ }
285
+ for (const event of Object.keys(fresh.hooks)) {
286
+ const userEntries = Array.isArray(userHooks[event]) ? userHooks[event] : [];
287
+ const preserved = userEntries.filter((entry) => {
288
+ if (!entry || typeof entry !== "object") return true;
289
+ const e = entry;
290
+ return e._knitOwned !== true && e._engramOwned !== true;
291
+ });
292
+ userHooks[event] = [...preserved, ...fresh.hooks[event]];
293
+ }
294
+ const merged = {
295
+ ...existing,
296
+ hooks: userHooks,
297
+ _knitHooks: { ...fresh._knitHooks, merged: true }
298
+ };
299
+ delete merged._engramHooks;
300
+ mkdirSync(claudeDir, { recursive: true });
301
+ writeFileSync(settingsPath, JSON.stringify(merged, null, 2), "utf-8");
302
+ }
303
+ function detectProjectName(rootPath) {
304
+ let name = basename(rootPath);
305
+ try {
306
+ const pkg = JSON.parse(readFileSync(join(rootPath, "package.json"), "utf-8"));
307
+ if (pkg.name) name = pkg.name;
308
+ } catch {
309
+ }
310
+ return name;
311
+ }
312
+ function refreshBrain(rootPath) {
313
+ cache = null;
314
+ return getBrain(rootPath);
315
+ }
316
+ function detectProjectRoot() {
317
+ try {
318
+ return execSync("git rev-parse --show-toplevel 2>/dev/null", { encoding: "utf-8" }).trim();
319
+ } catch {
320
+ return process.cwd();
321
+ }
322
+ }
323
+
324
+ export {
325
+ getBrain,
326
+ refreshBrain,
327
+ detectProjectRoot
328
+ };
@@ -3,11 +3,11 @@ import {
3
3
  KNIT_MARKER_START,
4
4
  LEGACY_ENGRAM_MARKER_END,
5
5
  LEGACY_ENGRAM_MARKER_START
6
- } from "./chunk-KLNUEE3O.js";
6
+ } from "./chunk-7UFS67HP.js";
7
7
  import {
8
8
  integrationsConfigPath,
9
9
  projectDataDir
10
- } from "./chunk-XFS2XGZI.js";
10
+ } from "./chunk-27TA2ZQZ.js";
11
11
 
12
12
  // src/engine/integration-scanner.ts
13
13
  import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, renameSync, unlinkSync } from "fs";
@@ -1,6 +1,6 @@
1
1
  // src/engine/knowledgebase.ts
2
2
  import { randomUUID } from "crypto";
3
- import { readFileSync, writeFileSync, statSync, existsSync, mkdirSync } from "fs";
3
+ import { readFileSync, writeFileSync, statSync, existsSync, mkdirSync, renameSync, unlinkSync } from "fs";
4
4
  import { dirname } from "path";
5
5
  function createKnowledgeBase(projectName) {
6
6
  return {
@@ -17,28 +17,53 @@ function createKnowledgeBase(projectName) {
17
17
  };
18
18
  }
19
19
  function loadKnowledgeBase(filePath, projectName) {
20
+ return loadKnowledgeBaseSafe(filePath, projectName).kb;
21
+ }
22
+ function loadKnowledgeBaseSafe(filePath, projectName) {
20
23
  if (!existsSync(filePath)) {
21
- return createKnowledgeBase(projectName);
24
+ return { kb: createKnowledgeBase(projectName), loadFailed: false };
22
25
  }
23
26
  try {
24
27
  const stat = statSync(filePath);
25
28
  if (stat.size > 10 * 1024 * 1024) {
26
- return createKnowledgeBase(projectName);
29
+ process.stderr.write(
30
+ `[knit] knowledgebase.json at ${filePath} exceeds 10MB \u2014 refusing to load. Original file preserved.
31
+ `
32
+ );
33
+ return { kb: createKnowledgeBase(projectName), loadFailed: true };
27
34
  }
28
35
  const raw = readFileSync(filePath, "utf-8");
29
36
  const kb = JSON.parse(raw);
30
- if (kb.version !== 1) return createKnowledgeBase(projectName);
31
- if (!Array.isArray(kb.entries)) return createKnowledgeBase(projectName);
32
- if (!kb.metrics || typeof kb.metrics.totalSessions !== "number") return createKnowledgeBase(projectName);
33
- return kb;
34
- } catch {
35
- return createKnowledgeBase(projectName);
37
+ if (kb.version !== 1 || !Array.isArray(kb.entries) || !kb.metrics || typeof kb.metrics.totalSessions !== "number") {
38
+ process.stderr.write(
39
+ `[knit] knowledgebase.json at ${filePath} failed structural validation \u2014 refusing to overwrite. Original file preserved.
40
+ `
41
+ );
42
+ return { kb: createKnowledgeBase(projectName), loadFailed: true };
43
+ }
44
+ return { kb, loadFailed: false };
45
+ } catch (err) {
46
+ process.stderr.write(
47
+ `[knit] knowledgebase.json at ${filePath} could not be parsed (${err.message}) \u2014 refusing to overwrite. Original file preserved.
48
+ `
49
+ );
50
+ return { kb: createKnowledgeBase(projectName), loadFailed: true };
36
51
  }
37
52
  }
38
53
  function saveKnowledgeBase(filePath, kb) {
39
54
  const dir = dirname(filePath);
40
55
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
41
- writeFileSync(filePath, JSON.stringify(kb, null, 2), "utf-8");
56
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
57
+ try {
58
+ writeFileSync(tmpPath, JSON.stringify(kb, null, 2), "utf-8");
59
+ renameSync(tmpPath, filePath);
60
+ } catch (err) {
61
+ try {
62
+ unlinkSync(tmpPath);
63
+ } catch {
64
+ }
65
+ throw err;
66
+ }
42
67
  }
43
68
  function addEntry(kb, entry) {
44
69
  const kbEntry = {
@@ -102,6 +127,13 @@ function getStaleEntries(kb, olderThanDays = 30) {
102
127
  function recordCacheHit(kb) {
103
128
  kb.metrics.cacheHits++;
104
129
  }
130
+ function bumpMetric(kb, key, delta = 1) {
131
+ kb.metrics[key] = (kb.metrics[key] ?? 0) + delta;
132
+ }
133
+ function bumpClassificationTier(kb, tier, delta = 1) {
134
+ if (!kb.metrics.classificationsByTier) kb.metrics.classificationsByTier = {};
135
+ kb.metrics.classificationsByTier[tier] = (kb.metrics.classificationsByTier[tier] ?? 0) + delta;
136
+ }
105
137
  function getKBSummary(kb) {
106
138
  const totalEntries = kb.entries.length;
107
139
  const accessedEntries = kb.entries.filter((e) => e.accessCount > 0).length;
@@ -126,6 +158,7 @@ function getKBSummary(kb) {
126
158
 
127
159
  export {
128
160
  loadKnowledgeBase,
161
+ loadKnowledgeBaseSafe,
129
162
  saveKnowledgeBase,
130
163
  addEntry,
131
164
  importFromMarkdown,
@@ -134,5 +167,7 @@ export {
134
167
  getTopEntries,
135
168
  getStaleEntries,
136
169
  recordCacheHit,
170
+ bumpMetric,
171
+ bumpClassificationTier,
137
172
  getKBSummary
138
173
  };
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  // src/cli.ts
7
7
  import { Command } from "commander";
8
8
  var args = process.argv.slice(2);
9
- var hasSubcommand = args.length > 0 && ["setup", "status", "refresh", "install-agents", "export", "--help", "-h", "--version", "-V"].includes(args[0]);
9
+ var hasSubcommand = args.length > 0 && ["setup", "status", "refresh", "install-agents", "export", "doctor", "--help", "-h", "--version", "-V"].includes(args[0]);
10
10
  var isTTY = process.stdin.isTTY;
11
11
  if (hasSubcommand) {
12
12
  runCLI();
@@ -20,10 +20,11 @@ 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-RQWRIM2Y.js");
24
- const { refreshCommand } = await import("./refresh-BXN32CNA.js");
25
- const { installAgentsCommand } = await import("./install-agents-D2KJQUH3.js");
26
- const { exportCommand } = await import("./export-I5Y26WUL.js");
23
+ const { statusCommand } = await import("./status-VJDB75X2.js");
24
+ const { refreshCommand } = await import("./refresh-SMJ2NGIW.js");
25
+ const { installAgentsCommand } = await import("./install-agents-OBDCWCPB.js");
26
+ const { exportCommand } = await import("./export-CGSEUYZA.js");
27
+ const { doctorCommand } = await import("./doctor-4DN2P2JR.js");
27
28
  const ENGRAM_GRADIENT = gradient(["#7c3aed", "#2563eb", "#06b6d4"]);
28
29
  const banner = `
29
30
  \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
@@ -75,6 +76,14 @@ async function runCLI() {
75
76
  process.exit(1);
76
77
  }
77
78
  });
79
+ program.command("doctor").description("Install health check: version, MCP registration, HOOKS_VERSION drift, knowledgebase, dangling symlinks").argument("[directory]", "Project directory", ".").action(async (directory) => {
80
+ try {
81
+ await doctorCommand(directory);
82
+ } catch (error) {
83
+ console.error(chalk.red(" Error:"), error instanceof Error ? error.message : error);
84
+ process.exit(1);
85
+ }
86
+ });
78
87
  program.command("export").description("Export knit learnings into a target format (e.g. an Obsidian vault)").argument("<format>", "Export format (currently only: obsidian)").argument("<vault-path>", "Output directory (Obsidian vault path)").option("--filter <tag>", "Only export entries tagged with this tag (e.g. #auth)").action(async (format, vaultPath, options) => {
79
88
  try {
80
89
  await exportCommand(format, vaultPath, options);
@@ -89,11 +98,11 @@ async function runMCP() {
89
98
  const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
90
99
  const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
91
100
  const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
92
- const { getBrain, detectProjectRoot, refreshBrain } = await import("./cache-JSN6ETUF.js");
93
- const { getActiveToolDefinitionsForBrain, handleToolCall } = await import("./tools-EISDGPS5.js");
94
- const { buildInstructions } = await import("./instructions-4FI32YZU.js");
101
+ const { getBrain, detectProjectRoot, refreshBrain } = await import("./cache-3LPETDUT.js");
102
+ const { getActiveToolDefinitionsForBrain, handleToolCall } = await import("./tools-MIROTK2A.js");
103
+ const { buildInstructions } = await import("./instructions-JARSXQPO.js");
95
104
  const { registerToolsListChangedNotifier } = await import("./notifier-4L27HKHI.js");
96
- const { loadScanResult } = await import("./integration-scanner-PS47AHGM.js");
105
+ const { loadScanResult } = await import("./integration-scanner-LBD2PIZ3.js");
97
106
  const ROOT_PATH = detectProjectRoot();
98
107
  const PER_PROJECT_INSTRUCTIONS = buildInstructions(loadScanResult(ROOT_PATH));
99
108
  const server = new Server(