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.
- package/LICENSE +21 -0
- package/README.md +323 -0
- package/THIRD-PARTY-NOTICES.md +50 -0
- package/dist/agents/core/code-reviewer.md +296 -0
- package/dist/agents/core/golang-pro.md +286 -0
- package/dist/agents/core/python-pro.md +286 -0
- package/dist/agents/core/qa-expert.md +296 -0
- package/dist/agents/core/security-engineer.md +286 -0
- package/dist/agents/core/typescript-pro.md +286 -0
- package/dist/cache-C6LI7UVN.js +16 -0
- package/dist/chunk-BAUQEFYY.js +138 -0
- package/dist/chunk-FEOG4WTP.js +87 -0
- package/dist/chunk-GRSYI2RR.js +57 -0
- package/dist/chunk-LW6NOFHF.js +282 -0
- package/dist/chunk-NZXLCN4Q.js +720 -0
- package/dist/chunk-QMICM263.js +552 -0
- package/dist/chunk-TH5QPD5E.js +399 -0
- package/dist/chunk-YI37OAJ7.js +145 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +122 -0
- package/dist/export-3MA272OR.js +238 -0
- package/dist/install-agents-2UVEAP2W.js +76 -0
- package/dist/refresh-3UK7NS5A.js +76 -0
- package/dist/setup-EQMYVVZ6.js +104 -0
- package/dist/status-56MCC7KE.js +145 -0
- package/dist/tools-VHBH4PPR.js +2041 -0
- package/package.json +72 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import {
|
|
2
|
+
VOLTAGENT_PINNED_SHA,
|
|
3
|
+
VOLTAGENT_REF,
|
|
4
|
+
categoryOf,
|
|
5
|
+
isBundledCore,
|
|
6
|
+
knownAgents,
|
|
7
|
+
rawAgentUrl
|
|
8
|
+
} from "./chunk-LW6NOFHF.js";
|
|
9
|
+
import {
|
|
10
|
+
agentsCacheFile,
|
|
11
|
+
projectAgentFile,
|
|
12
|
+
projectAgentsDir,
|
|
13
|
+
sessionsJsonlPath
|
|
14
|
+
} from "./chunk-YI37OAJ7.js";
|
|
15
|
+
|
|
16
|
+
// src/engine/install-agents.ts
|
|
17
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
18
|
+
|
|
19
|
+
// src/engine/agent-fetcher.ts
|
|
20
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
21
|
+
import { dirname, join } from "path";
|
|
22
|
+
import { fileURLToPath } from "url";
|
|
23
|
+
function bareName(name) {
|
|
24
|
+
return name.replace(/^(knit|engram)-/, "");
|
|
25
|
+
}
|
|
26
|
+
function attributionComment(name, category, ref) {
|
|
27
|
+
return `<!--
|
|
28
|
+
Vendored by engram from:
|
|
29
|
+
https://github.com/VoltAgent/awesome-claude-code-subagents
|
|
30
|
+
@${ref}/categories/${category}/${name}.md
|
|
31
|
+
License: MIT (see github.com/VoltAgent/awesome-claude-code-subagents/blob/main/LICENSE).
|
|
32
|
+
This file was copied verbatim with this header prepended; the original
|
|
33
|
+
YAML frontmatter and prompt content are unchanged.
|
|
34
|
+
-->
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
function injectAttribution(body, name, category, ref) {
|
|
38
|
+
const fmEnd = body.indexOf("\n---", 3);
|
|
39
|
+
if (fmEnd < 0) return body;
|
|
40
|
+
const head = body.slice(0, fmEnd + 4);
|
|
41
|
+
const tail = body.slice(fmEnd + 4);
|
|
42
|
+
return `${head}
|
|
43
|
+
${attributionComment(name, category, ref)}${tail}`;
|
|
44
|
+
}
|
|
45
|
+
var AgentFetchError = class extends Error {
|
|
46
|
+
constructor(message, cause) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.cause = cause;
|
|
49
|
+
this.name = "AgentFetchError";
|
|
50
|
+
}
|
|
51
|
+
cause;
|
|
52
|
+
};
|
|
53
|
+
async function fetchAgent(name, opts = {}) {
|
|
54
|
+
const bare = bareName(name);
|
|
55
|
+
if (isBundledCore(bare)) {
|
|
56
|
+
const bundled = readBundledCore(bare, opts.bundledCoreDir);
|
|
57
|
+
if (bundled !== null) return bundled;
|
|
58
|
+
}
|
|
59
|
+
const ref = opts.ref || VOLTAGENT_REF;
|
|
60
|
+
const cat = categoryOf(bare);
|
|
61
|
+
if (!cat) {
|
|
62
|
+
throw new AgentFetchError(`Unknown agent: "${name}". Not in engram's registry.`);
|
|
63
|
+
}
|
|
64
|
+
const cachePath = agentsCacheFile(ref, cat, bare);
|
|
65
|
+
if (existsSync(cachePath)) {
|
|
66
|
+
return readFileSync(cachePath, "utf-8");
|
|
67
|
+
}
|
|
68
|
+
if (process.env.ENGRAM_OFFLINE === "1") {
|
|
69
|
+
throw new AgentFetchError(
|
|
70
|
+
`Agent "${name}" not bundled and not cached, and ENGRAM_OFFLINE=1 is set. Either unset ENGRAM_OFFLINE or run \`engram install-agents\` when online to populate the cache.`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const url = rawAgentUrl(bare, ref);
|
|
74
|
+
if (!url) {
|
|
75
|
+
throw new AgentFetchError(`Cannot construct URL for agent "${name}".`);
|
|
76
|
+
}
|
|
77
|
+
const fetchFn = opts.fetchFn || defaultFetch;
|
|
78
|
+
let res;
|
|
79
|
+
try {
|
|
80
|
+
res = await fetchFn(url);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
throw new AgentFetchError(`Network error fetching "${name}" from ${url}`, err);
|
|
83
|
+
}
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
throw new AgentFetchError(`Fetch failed for "${name}" (HTTP ${res.status}) \u2014 ${url}`);
|
|
86
|
+
}
|
|
87
|
+
const body = await res.text();
|
|
88
|
+
if (!body || body.length < 20) {
|
|
89
|
+
throw new AgentFetchError(`Fetched body for "${name}" is empty or suspiciously short.`);
|
|
90
|
+
}
|
|
91
|
+
const refForAttribution = ref === VOLTAGENT_REF ? VOLTAGENT_PINNED_SHA : ref;
|
|
92
|
+
const augmented = injectAttribution(body, bare, cat, refForAttribution);
|
|
93
|
+
mkdirSync(dirname(cachePath), { recursive: true });
|
|
94
|
+
writeFileSync(cachePath, augmented, "utf-8");
|
|
95
|
+
return augmented;
|
|
96
|
+
}
|
|
97
|
+
var defaultFetch = (url) => {
|
|
98
|
+
if (typeof globalThis.fetch !== "function") {
|
|
99
|
+
return Promise.reject(new Error("Global fetch unavailable. Requires Node 18+."));
|
|
100
|
+
}
|
|
101
|
+
return globalThis.fetch(url);
|
|
102
|
+
};
|
|
103
|
+
function readBundledCore(name, customDir) {
|
|
104
|
+
const dir = customDir || bundledCoreDir();
|
|
105
|
+
const file = join(dir, `${name}.md`);
|
|
106
|
+
if (!existsSync(file)) return null;
|
|
107
|
+
try {
|
|
108
|
+
return readFileSync(file, "utf-8");
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function bundledCoreDir() {
|
|
114
|
+
const here = fileURLToPath(import.meta.url);
|
|
115
|
+
let dir = dirname(here);
|
|
116
|
+
for (let i = 0; i < 8; i++) {
|
|
117
|
+
const candidate = join(dir, "dist", "agents", "core");
|
|
118
|
+
if (existsSync(candidate)) return candidate;
|
|
119
|
+
const parent = dirname(dir);
|
|
120
|
+
if (parent === dir) break;
|
|
121
|
+
dir = parent;
|
|
122
|
+
}
|
|
123
|
+
return join(process.cwd(), "dist", "agents", "core");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/generators/agent-md.ts
|
|
127
|
+
var ENGRAM_AGENT_MARKER_START = "<!-- engram:context:start -->";
|
|
128
|
+
var ENGRAM_AGENT_MARKER_END = "<!-- engram:context:end -->";
|
|
129
|
+
function personalizeAgent(baseMd, inputs) {
|
|
130
|
+
const block = buildContextBlock(inputs);
|
|
131
|
+
const startIdx = baseMd.indexOf(ENGRAM_AGENT_MARKER_START);
|
|
132
|
+
const endIdx = baseMd.indexOf(ENGRAM_AGENT_MARKER_END);
|
|
133
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
134
|
+
const before = baseMd.slice(0, startIdx);
|
|
135
|
+
const after = baseMd.slice(endIdx + ENGRAM_AGENT_MARKER_END.length);
|
|
136
|
+
return `${before.trimEnd()}
|
|
137
|
+
|
|
138
|
+
${block}
|
|
139
|
+
${after.trimStart() ? "\n" + after.trimStart() : ""}`;
|
|
140
|
+
}
|
|
141
|
+
return `${baseMd.trimEnd()}
|
|
142
|
+
|
|
143
|
+
${block}
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
function buildContextBlock(inputs) {
|
|
147
|
+
const { config, knowledge, relevantLearnings, falsePositives } = inputs;
|
|
148
|
+
const lines = [];
|
|
149
|
+
lines.push(ENGRAM_AGENT_MARKER_START);
|
|
150
|
+
lines.push("");
|
|
151
|
+
lines.push("## Project context (knit-managed; do not edit by hand)");
|
|
152
|
+
lines.push("");
|
|
153
|
+
const stack = [config.stack.language, config.stack.framework].filter(Boolean).join(" + ");
|
|
154
|
+
lines.push(`**Project:** ${config.name}`);
|
|
155
|
+
if (stack) lines.push(`**Stack:** ${stack}`);
|
|
156
|
+
if (config.stack.testFramework) lines.push(`**Tests:** ${config.stack.testFramework}`);
|
|
157
|
+
lines.push("");
|
|
158
|
+
if (knowledge && knowledge.summary.highFanoutFiles.length > 0) {
|
|
159
|
+
const top = knowledge.summary.highFanoutFiles.slice(0, 10);
|
|
160
|
+
lines.push(`**High-fanout files (change carefully):**`);
|
|
161
|
+
for (const f of top) lines.push(`- \`${f}\``);
|
|
162
|
+
lines.push("");
|
|
163
|
+
}
|
|
164
|
+
if (knowledge && knowledge.summary.untestedFiles.length > 0) {
|
|
165
|
+
const top = knowledge.summary.untestedFiles.slice(0, 5);
|
|
166
|
+
lines.push(`**Untested source files (sample):**`);
|
|
167
|
+
for (const f of top) lines.push(`- \`${f}\``);
|
|
168
|
+
if (knowledge.summary.untestedFiles.length > 5) {
|
|
169
|
+
lines.push(`- (+${knowledge.summary.untestedFiles.length - 5} more)`);
|
|
170
|
+
}
|
|
171
|
+
lines.push("");
|
|
172
|
+
}
|
|
173
|
+
if (relevantLearnings && relevantLearnings.length > 0) {
|
|
174
|
+
lines.push(`**Recent relevant learnings:**`);
|
|
175
|
+
for (const l of relevantLearnings.slice(0, 5)) {
|
|
176
|
+
lines.push(`- ${l.summary} \u2014 ${l.lesson}`);
|
|
177
|
+
}
|
|
178
|
+
lines.push("");
|
|
179
|
+
}
|
|
180
|
+
if (falsePositives && falsePositives.length > 0) {
|
|
181
|
+
lines.push(`**Known false positives \u2014 DO NOT flag these:**`);
|
|
182
|
+
for (const fp of falsePositives.slice(0, 5)) {
|
|
183
|
+
lines.push(`- ${fp.summary} (${fp.lesson})`);
|
|
184
|
+
}
|
|
185
|
+
lines.push("");
|
|
186
|
+
}
|
|
187
|
+
lines.push("## Knit MCP tools you can call");
|
|
188
|
+
lines.push("");
|
|
189
|
+
lines.push("You have access to engram's MCP. Call these when you need depth:");
|
|
190
|
+
lines.push("- `knit_query_dependents(file_path)` \u2014 what depends on a file");
|
|
191
|
+
lines.push("- `knit_get_false_positives()` \u2014 full FP list, not just what's above");
|
|
192
|
+
lines.push("- `knit_search_learnings(domains)` \u2014 search past lessons by tag");
|
|
193
|
+
lines.push("- `knit_search_sessions(query)` \u2014 has a past session touched this area?");
|
|
194
|
+
lines.push("");
|
|
195
|
+
lines.push(ENGRAM_AGENT_MARKER_END);
|
|
196
|
+
return lines.join("\n");
|
|
197
|
+
}
|
|
198
|
+
function selectRelevantLearnings(allLearnings, agentName, limit = 5) {
|
|
199
|
+
const interests = inferInterestTags(agentName);
|
|
200
|
+
const scored = allLearnings.map((entry) => {
|
|
201
|
+
let score = 0;
|
|
202
|
+
for (const tag of entry.tags) {
|
|
203
|
+
const tagLower = tag.toLowerCase();
|
|
204
|
+
for (const interest of interests) {
|
|
205
|
+
if (tagLower.includes(interest)) score += 1;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { entry, score, date: entry.date };
|
|
209
|
+
});
|
|
210
|
+
return scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score || (b.date > a.date ? 1 : -1)).slice(0, limit).map((s) => s.entry);
|
|
211
|
+
}
|
|
212
|
+
function inferInterestTags(agentName) {
|
|
213
|
+
const base = agentName.replace(/^(knit|engram)-/, "");
|
|
214
|
+
const parts = base.split("-");
|
|
215
|
+
return [base, ...parts].map((s) => s.toLowerCase());
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/engine/install-agents.ts
|
|
219
|
+
async function installAgentsForProject(rootPath, config, knowledge, knowledgeBase, opts = {}) {
|
|
220
|
+
const targets = opts.only ? opts.only : opts.all ? knownAgents() : agentsNeededByProject(config);
|
|
221
|
+
const result = {
|
|
222
|
+
installed: [],
|
|
223
|
+
alreadyCurrent: [],
|
|
224
|
+
skippedUserCurated: [],
|
|
225
|
+
failed: []
|
|
226
|
+
};
|
|
227
|
+
mkdirSync2(projectAgentsDir(rootPath), { recursive: true });
|
|
228
|
+
const fps = knowledgeBase ? knowledgeBase.entries.filter((e) => e.tags.includes("#false-positive")) : [];
|
|
229
|
+
for (const name of targets) {
|
|
230
|
+
const outFile = projectAgentFile(rootPath, name);
|
|
231
|
+
if (existsSync2(outFile) && !opts.refresh) {
|
|
232
|
+
try {
|
|
233
|
+
const existing = readFileSync2(outFile, "utf-8");
|
|
234
|
+
if (!existing.includes(ENGRAM_AGENT_MARKER_START)) {
|
|
235
|
+
result.skippedUserCurated.push(name);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
result.skippedUserCurated.push(name);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const baseMd = await fetchAgent(name, opts.refresh ? { ref: void 0 } : {});
|
|
245
|
+
const relevant = knowledgeBase ? selectRelevantLearnings(knowledgeBase.entries, name) : [];
|
|
246
|
+
const personalized = personalizeAgent(baseMd, {
|
|
247
|
+
config,
|
|
248
|
+
knowledge,
|
|
249
|
+
relevantLearnings: relevant,
|
|
250
|
+
falsePositives: fps
|
|
251
|
+
});
|
|
252
|
+
if (existsSync2(outFile)) {
|
|
253
|
+
try {
|
|
254
|
+
const existing = readFileSync2(outFile, "utf-8");
|
|
255
|
+
if (existing.trim() === personalized.trim()) {
|
|
256
|
+
result.alreadyCurrent.push(name);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
writeFileSync2(outFile, personalized, "utf-8");
|
|
263
|
+
result.installed.push(name);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
const msg = err instanceof AgentFetchError ? err.message : String(err);
|
|
266
|
+
result.failed.push({ name, error: msg });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
function agentsNeededByProject(config) {
|
|
272
|
+
const names = /* @__PURE__ */ new Set();
|
|
273
|
+
for (const domain of config.domains) {
|
|
274
|
+
for (const agent of domain.agents) names.add(agent);
|
|
275
|
+
}
|
|
276
|
+
return Array.from(names);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/engine/sessions.ts
|
|
280
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, appendFileSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync3, renameSync } from "fs";
|
|
281
|
+
import { dirname as dirname2 } from "path";
|
|
282
|
+
function appendSession(rootPath, entry) {
|
|
283
|
+
const path = sessionsJsonlPath(rootPath);
|
|
284
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
285
|
+
appendFileSync(path, JSON.stringify(entry) + "\n", "utf-8");
|
|
286
|
+
}
|
|
287
|
+
function searchSessions(rootPath, query, limit = 10) {
|
|
288
|
+
const lines = readAllLines(rootPath);
|
|
289
|
+
if (lines.length === 0) return [];
|
|
290
|
+
const q = query.toLowerCase();
|
|
291
|
+
const matches = [];
|
|
292
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
293
|
+
const entry = parseLine(lines[i]);
|
|
294
|
+
if (!entry) continue;
|
|
295
|
+
const haystack = [
|
|
296
|
+
entry.summary ?? "",
|
|
297
|
+
(entry.tags ?? []).join(" "),
|
|
298
|
+
entry.branch ?? ""
|
|
299
|
+
].join(" ").toLowerCase();
|
|
300
|
+
if (haystack.includes(q)) {
|
|
301
|
+
matches.push(entry);
|
|
302
|
+
if (matches.length >= limit) break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return matches;
|
|
306
|
+
}
|
|
307
|
+
function getRecentSessions(rootPath, n = 3) {
|
|
308
|
+
const lines = readAllLines(rootPath);
|
|
309
|
+
if (lines.length === 0) return [];
|
|
310
|
+
const start = Math.max(0, lines.length - n);
|
|
311
|
+
const recent = [];
|
|
312
|
+
for (let i = lines.length - 1; i >= start; i--) {
|
|
313
|
+
const entry = parseLine(lines[i]);
|
|
314
|
+
if (entry) recent.push(entry);
|
|
315
|
+
}
|
|
316
|
+
return recent;
|
|
317
|
+
}
|
|
318
|
+
function sessionCount(rootPath) {
|
|
319
|
+
return readAllLines(rootPath).length;
|
|
320
|
+
}
|
|
321
|
+
function pruneSessionsByAge(rootPath, maxAgeDays) {
|
|
322
|
+
const path = sessionsJsonlPath(rootPath);
|
|
323
|
+
if (!existsSync3(path)) return { kept: 0, pruned: 0 };
|
|
324
|
+
const lines = readAllLines(rootPath);
|
|
325
|
+
if (lines.length === 0) return { kept: 0, pruned: 0 };
|
|
326
|
+
const cutoffMs = Date.now() - maxAgeDays * 864e5;
|
|
327
|
+
const keptLines = [];
|
|
328
|
+
let pruned = 0;
|
|
329
|
+
for (const line of lines) {
|
|
330
|
+
const entry = parseLine(line);
|
|
331
|
+
if (!entry) {
|
|
332
|
+
keptLines.push(line);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const dateMs = parseDateUtc(entry.date);
|
|
336
|
+
if (dateMs === null) {
|
|
337
|
+
keptLines.push(line);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (dateMs >= cutoffMs) {
|
|
341
|
+
keptLines.push(line);
|
|
342
|
+
} else {
|
|
343
|
+
pruned++;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (pruned === 0) {
|
|
347
|
+
return { kept: keptLines.length, pruned: 0 };
|
|
348
|
+
}
|
|
349
|
+
const tmpPath = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
350
|
+
const body = keptLines.length === 0 ? "" : keptLines.join("\n") + "\n";
|
|
351
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
352
|
+
writeFileSync3(tmpPath, body, "utf-8");
|
|
353
|
+
renameSync(tmpPath, path);
|
|
354
|
+
return { kept: keptLines.length, pruned };
|
|
355
|
+
}
|
|
356
|
+
function readAllLines(rootPath) {
|
|
357
|
+
const path = sessionsJsonlPath(rootPath);
|
|
358
|
+
if (!existsSync3(path)) return [];
|
|
359
|
+
try {
|
|
360
|
+
const stat = statSync(path);
|
|
361
|
+
if (stat.size === 0) return [];
|
|
362
|
+
if (stat.size > 100 * 1024 * 1024) return [];
|
|
363
|
+
return readFileSync3(path, "utf-8").split("\n").filter(Boolean);
|
|
364
|
+
} catch {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function parseDateUtc(date) {
|
|
369
|
+
if (typeof date !== "string") return null;
|
|
370
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(date);
|
|
371
|
+
if (!m) return null;
|
|
372
|
+
const y = Number(m[1]);
|
|
373
|
+
const mo = Number(m[2]);
|
|
374
|
+
const d = Number(m[3]);
|
|
375
|
+
if (!Number.isFinite(y) || !Number.isFinite(mo) || !Number.isFinite(d)) return null;
|
|
376
|
+
if (mo < 1 || mo > 12 || d < 1 || d > 31) return null;
|
|
377
|
+
const ms = Date.UTC(y, mo - 1, d);
|
|
378
|
+
return Number.isFinite(ms) ? ms : null;
|
|
379
|
+
}
|
|
380
|
+
function parseLine(line) {
|
|
381
|
+
try {
|
|
382
|
+
const obj = JSON.parse(line);
|
|
383
|
+
if (typeof obj !== "object" || obj === null) return null;
|
|
384
|
+
if (typeof obj.id !== "string" || typeof obj.date !== "string") return null;
|
|
385
|
+
if (!obj.outcome) obj.outcome = "unknown";
|
|
386
|
+
return obj;
|
|
387
|
+
} catch {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export {
|
|
393
|
+
installAgentsForProject,
|
|
394
|
+
appendSession,
|
|
395
|
+
searchSessions,
|
|
396
|
+
getRecentSessions,
|
|
397
|
+
sessionCount,
|
|
398
|
+
pruneSessionsByAge
|
|
399
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// src/engine/paths.ts
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join as join2 } from "path";
|
|
4
|
+
|
|
5
|
+
// src/engine/project-id.ts
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
8
|
+
import { resolve, dirname, join } from "path";
|
|
9
|
+
function projectId(rootPath) {
|
|
10
|
+
const canonical = canonicalRepoRoot(rootPath);
|
|
11
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
12
|
+
}
|
|
13
|
+
function canonicalRepoRoot(rootPath) {
|
|
14
|
+
const dotGit = join(rootPath, ".git");
|
|
15
|
+
if (!existsSync(dotGit)) return resolve(rootPath);
|
|
16
|
+
try {
|
|
17
|
+
if (statSync(dotGit).isDirectory()) {
|
|
18
|
+
return resolve(rootPath);
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
return resolve(rootPath);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const content = readFileSync(dotGit, "utf-8");
|
|
25
|
+
const match = content.match(/^gitdir:\s*(.+)$/m);
|
|
26
|
+
if (!match) return resolve(rootPath);
|
|
27
|
+
const gitDir = resolve(dirname(dotGit), match[1].trim());
|
|
28
|
+
const marker = `${join(".git", "worktrees")}`;
|
|
29
|
+
const idx = gitDir.lastIndexOf(`/${marker}/`);
|
|
30
|
+
if (idx === -1) return resolve(rootPath);
|
|
31
|
+
return gitDir.slice(0, idx);
|
|
32
|
+
} catch {
|
|
33
|
+
return resolve(rootPath);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/engine/paths.ts
|
|
38
|
+
function knitRoot() {
|
|
39
|
+
return process.env.KNIT_HOME || process.env.ENGRAM_HOME || join2(homedir(), ".knit");
|
|
40
|
+
}
|
|
41
|
+
function projectDataDir(rootPath) {
|
|
42
|
+
return join2(knitRoot(), "projects", projectId(rootPath));
|
|
43
|
+
}
|
|
44
|
+
function knowledgePath(rootPath) {
|
|
45
|
+
return join2(projectDataDir(rootPath), "knowledge.json");
|
|
46
|
+
}
|
|
47
|
+
function knowledgebasePath(rootPath) {
|
|
48
|
+
return join2(projectDataDir(rootPath), "knowledgebase.json");
|
|
49
|
+
}
|
|
50
|
+
function teamsPath(rootPath) {
|
|
51
|
+
return join2(projectDataDir(rootPath), "teams.json");
|
|
52
|
+
}
|
|
53
|
+
function worktreesRegistryPath(rootPath) {
|
|
54
|
+
return join2(projectDataDir(rootPath), "worktrees.json");
|
|
55
|
+
}
|
|
56
|
+
function globalDataDir() {
|
|
57
|
+
return join2(knitRoot(), "global");
|
|
58
|
+
}
|
|
59
|
+
function globalLearningsPath() {
|
|
60
|
+
return join2(globalDataDir(), "learnings.jsonl");
|
|
61
|
+
}
|
|
62
|
+
function agentsCacheDir(ref) {
|
|
63
|
+
return join2(knitRoot(), "agents", "cache", sanitizeRef(ref));
|
|
64
|
+
}
|
|
65
|
+
function agentsCacheFile(ref, category, name) {
|
|
66
|
+
return join2(agentsCacheDir(ref), category, `${name}.md`);
|
|
67
|
+
}
|
|
68
|
+
function projectAgentsDir(rootPath) {
|
|
69
|
+
return join2(rootPath, ".claude", "agents");
|
|
70
|
+
}
|
|
71
|
+
function projectAgentFile(rootPath, name) {
|
|
72
|
+
const bare = name.replace(/^(knit|engram)-/, "");
|
|
73
|
+
return join2(projectAgentsDir(rootPath), `knit-${bare}.md`);
|
|
74
|
+
}
|
|
75
|
+
function sanitizeRef(ref) {
|
|
76
|
+
return ref.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
77
|
+
}
|
|
78
|
+
function sessionsJsonlPath(rootPath) {
|
|
79
|
+
return join2(projectDataDir(rootPath), "sessions.jsonl");
|
|
80
|
+
}
|
|
81
|
+
function protocolConfigPath(rootPath) {
|
|
82
|
+
return join2(projectDataDir(rootPath), "protocol-config.json");
|
|
83
|
+
}
|
|
84
|
+
function classificationMarkerPath(rootPath) {
|
|
85
|
+
return join2(projectDataDir(rootPath), ".classified-current");
|
|
86
|
+
}
|
|
87
|
+
function sessionMarkerPath(rootPath) {
|
|
88
|
+
return join2(projectDataDir(rootPath), ".session-loaded");
|
|
89
|
+
}
|
|
90
|
+
function learningsDir(rootPath) {
|
|
91
|
+
return join2(projectDataDir(rootPath), "learnings");
|
|
92
|
+
}
|
|
93
|
+
function learningsFilePath(rootPath, projectName) {
|
|
94
|
+
const slug = projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
95
|
+
return join2(learningsDir(rootPath), `${slug}.md`);
|
|
96
|
+
}
|
|
97
|
+
function sessionsLogPath(rootPath) {
|
|
98
|
+
return join2(learningsDir(rootPath), "sessions.md");
|
|
99
|
+
}
|
|
100
|
+
function legacyClaudeDir(rootPath) {
|
|
101
|
+
return join2(rootPath, ".claude");
|
|
102
|
+
}
|
|
103
|
+
function legacyKnowledgePath(rootPath) {
|
|
104
|
+
return join2(legacyClaudeDir(rootPath), "knowledge.json");
|
|
105
|
+
}
|
|
106
|
+
function legacyKnowledgebasePath(rootPath) {
|
|
107
|
+
return join2(legacyClaudeDir(rootPath), "knowledgebase.json");
|
|
108
|
+
}
|
|
109
|
+
function legacyTeamsPath(rootPath) {
|
|
110
|
+
return join2(legacyClaudeDir(rootPath), "teams.json");
|
|
111
|
+
}
|
|
112
|
+
function legacyLearningsDir(rootPath) {
|
|
113
|
+
return join2(legacyClaudeDir(rootPath), "learnings");
|
|
114
|
+
}
|
|
115
|
+
function migrationBreadcrumbPath(rootPath) {
|
|
116
|
+
return join2(legacyClaudeDir(rootPath), "MIGRATED.txt");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export {
|
|
120
|
+
projectId,
|
|
121
|
+
canonicalRepoRoot,
|
|
122
|
+
knitRoot,
|
|
123
|
+
projectDataDir,
|
|
124
|
+
knowledgePath,
|
|
125
|
+
knowledgebasePath,
|
|
126
|
+
teamsPath,
|
|
127
|
+
worktreesRegistryPath,
|
|
128
|
+
globalLearningsPath,
|
|
129
|
+
agentsCacheFile,
|
|
130
|
+
projectAgentsDir,
|
|
131
|
+
projectAgentFile,
|
|
132
|
+
sessionsJsonlPath,
|
|
133
|
+
protocolConfigPath,
|
|
134
|
+
classificationMarkerPath,
|
|
135
|
+
sessionMarkerPath,
|
|
136
|
+
learningsDir,
|
|
137
|
+
learningsFilePath,
|
|
138
|
+
sessionsLogPath,
|
|
139
|
+
legacyClaudeDir,
|
|
140
|
+
legacyKnowledgePath,
|
|
141
|
+
legacyKnowledgebasePath,
|
|
142
|
+
legacyTeamsPath,
|
|
143
|
+
legacyLearningsDir,
|
|
144
|
+
migrationBreadcrumbPath
|
|
145
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
var args = process.argv.slice(2);
|
|
6
|
+
var hasSubcommand = args.length > 0 && ["setup", "status", "refresh", "install-agents", "export", "--help", "-h", "--version", "-V"].includes(args[0]);
|
|
7
|
+
var isTTY = process.stdin.isTTY;
|
|
8
|
+
if (hasSubcommand) {
|
|
9
|
+
runCLI();
|
|
10
|
+
} else if (isTTY) {
|
|
11
|
+
process.argv.push("--help");
|
|
12
|
+
runCLI();
|
|
13
|
+
} else {
|
|
14
|
+
runMCP();
|
|
15
|
+
}
|
|
16
|
+
async function runCLI() {
|
|
17
|
+
const gradient = (await import("gradient-string")).default;
|
|
18
|
+
const chalk = (await import("chalk")).default;
|
|
19
|
+
const { setupCommand } = await import("./setup-EQMYVVZ6.js");
|
|
20
|
+
const { statusCommand } = await import("./status-56MCC7KE.js");
|
|
21
|
+
const { refreshCommand } = await import("./refresh-3UK7NS5A.js");
|
|
22
|
+
const { installAgentsCommand } = await import("./install-agents-2UVEAP2W.js");
|
|
23
|
+
const { exportCommand } = await import("./export-3MA272OR.js");
|
|
24
|
+
const ENGRAM_GRADIENT = gradient(["#7c3aed", "#2563eb", "#06b6d4"]);
|
|
25
|
+
const banner = `
|
|
26
|
+
\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
|
|
27
|
+
\u2551 \u2551
|
|
28
|
+
\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2551
|
|
29
|
+
\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2551
|
|
30
|
+
\u2551 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557 \u2551
|
|
31
|
+
\u2551 \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2551
|
|
32
|
+
\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2551
|
|
33
|
+
\u2551 \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u2551
|
|
34
|
+
\u2551 \u2551
|
|
35
|
+
\u2551 knit \u2014 the second brain \u2551
|
|
36
|
+
\u2551 \u2551
|
|
37
|
+
\u255A\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\u255D`;
|
|
38
|
+
const program = new Command();
|
|
39
|
+
program.name("knit").description("The second brain for Claude Code \u2014 MCP server + analytics dashboard").version("0.4.1").hook("preAction", () => {
|
|
40
|
+
console.log(ENGRAM_GRADIENT.multiline(banner));
|
|
41
|
+
console.log();
|
|
42
|
+
});
|
|
43
|
+
program.command("setup").description("Add Knit MCP to your Claude Code settings (one time)").option("--global", "Add to global ~/.claude/settings.json (default)", true).option("--local", "Add to project .claude/settings.json only", false).action(async (options) => {
|
|
44
|
+
try {
|
|
45
|
+
await setupCommand(options);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(chalk.red(" Error:"), error instanceof Error ? error.message : error);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
program.command("status").description("Dashboard: sessions, learnings, hit rate, knowledge health").argument("[directory]", "Project directory", ".").action(async (directory) => {
|
|
52
|
+
try {
|
|
53
|
+
await statusCommand(directory);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(chalk.red(" Error:"), error instanceof Error ? error.message : error);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
program.command("refresh").description("Force rebuild knowledge brain and CLAUDE.md").argument("[directory]", "Project directory", ".").action(async (directory) => {
|
|
60
|
+
try {
|
|
61
|
+
await refreshCommand(directory);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(chalk.red(" Error:"), error instanceof Error ? error.message : error);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
program.command("install-agents").description("Install VoltAgent subagents into <project>/.claude/agents/, personalized with project context").argument("[directory]", "Project directory", ".").option("--refresh", "Re-fetch from network even if cached", false).option("--all", "Install every known agent (not just ones referenced by current domains)", false).action(async (directory, options) => {
|
|
68
|
+
try {
|
|
69
|
+
await installAgentsCommand(directory, options);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error(chalk.red(" Error:"), error instanceof Error ? error.message : error);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
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) => {
|
|
76
|
+
try {
|
|
77
|
+
await exportCommand(format, vaultPath, options);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error(chalk.red(" Error:"), error instanceof Error ? error.message : error);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
program.parse();
|
|
84
|
+
}
|
|
85
|
+
async function runMCP() {
|
|
86
|
+
const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
|
|
87
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
88
|
+
const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
|
|
89
|
+
const { getBrain, detectProjectRoot, refreshBrain } = await import("./cache-C6LI7UVN.js");
|
|
90
|
+
const { getToolDefinitions, handleToolCall } = await import("./tools-VHBH4PPR.js");
|
|
91
|
+
const ROOT_PATH = detectProjectRoot();
|
|
92
|
+
const server = new Server(
|
|
93
|
+
{ name: "knit-brain", version: "0.4.1" },
|
|
94
|
+
{ capabilities: { tools: {} } }
|
|
95
|
+
);
|
|
96
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
97
|
+
tools: getToolDefinitions()
|
|
98
|
+
}));
|
|
99
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
100
|
+
const { name, arguments: params } = request.params;
|
|
101
|
+
try {
|
|
102
|
+
if (name === "knit_refresh_index") {
|
|
103
|
+
refreshBrain(ROOT_PATH);
|
|
104
|
+
return {
|
|
105
|
+
content: [{ type: "text", text: JSON.stringify({ status: "refreshed", root: ROOT_PATH }) }]
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const brain = getBrain(ROOT_PATH);
|
|
109
|
+
const result = handleToolCall(name, params || {}, brain);
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: "text", text: result }]
|
|
112
|
+
};
|
|
113
|
+
} catch (error) {
|
|
114
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Internal error: ${message}` }) }]
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
const transport = new StdioServerTransport();
|
|
121
|
+
await server.connect(transport);
|
|
122
|
+
}
|