engramx 0.1.1 → 0.2.1
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 +57 -7
- package/dist/{chunk-44GN6IRQ.js → chunk-22ADJYQ5.js} +443 -25
- package/dist/chunk-YQ3FPGPC.js +361 -0
- package/dist/cli.js +57 -9
- package/dist/{core-M5N34VUU.js → core-HWOM7GSU.js} +3 -1
- package/dist/index.js +15 -3
- package/dist/serve.js +65 -12
- package/package.json +1 -1
- package/dist/chunk-WJUA4VZ7.js +0 -247
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/core.ts
|
|
2
|
-
import { join as
|
|
3
|
-
import { existsSync as
|
|
2
|
+
import { join as join4, resolve as resolve2 } from "path";
|
|
3
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2, unlinkSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
4
5
|
|
|
5
6
|
// src/graph/store.ts
|
|
6
7
|
import initSqlJs from "sql.js";
|
|
@@ -164,7 +165,7 @@ var GraphStore = class _GraphStore {
|
|
|
164
165
|
`SELECT n.*, COUNT(*) as degree
|
|
165
166
|
FROM nodes n
|
|
166
167
|
JOIN edges e ON e.source = n.id OR e.target = n.id
|
|
167
|
-
WHERE n.kind NOT IN ('file', 'import', 'module')
|
|
168
|
+
WHERE n.kind NOT IN ('file', 'import', 'module', 'concept')
|
|
168
169
|
GROUP BY n.id
|
|
169
170
|
ORDER BY degree DESC
|
|
170
171
|
LIMIT ?`
|
|
@@ -293,7 +294,34 @@ var GraphStore = class _GraphStore {
|
|
|
293
294
|
}
|
|
294
295
|
};
|
|
295
296
|
|
|
297
|
+
// src/graph/render-utils.ts
|
|
298
|
+
function sliceGraphemeSafe(s, max) {
|
|
299
|
+
if (max <= 0) return "";
|
|
300
|
+
if (s.length <= max) return s;
|
|
301
|
+
let cut = max;
|
|
302
|
+
const code = s.charCodeAt(cut - 1);
|
|
303
|
+
if (code >= 55296 && code <= 56319) cut--;
|
|
304
|
+
return s.slice(0, cut);
|
|
305
|
+
}
|
|
306
|
+
function truncateGraphemeSafe(s, max) {
|
|
307
|
+
if (max <= 0) return "";
|
|
308
|
+
if (s.length <= max) return s;
|
|
309
|
+
let cut = max - 1;
|
|
310
|
+
if (cut <= 0) return "";
|
|
311
|
+
const code = s.charCodeAt(cut - 1);
|
|
312
|
+
if (code >= 55296 && code <= 56319) cut--;
|
|
313
|
+
return s.slice(0, cut) + "\u2026";
|
|
314
|
+
}
|
|
315
|
+
|
|
296
316
|
// src/graph/query.ts
|
|
317
|
+
var MISTAKE_SCORE_BOOST = 2.5;
|
|
318
|
+
var KEYWORD_SCORE_DOWNWEIGHT = 0.5;
|
|
319
|
+
var MAX_MISTAKE_LABEL_CHARS = 500;
|
|
320
|
+
function isHiddenKeyword(node) {
|
|
321
|
+
if (node.kind !== "concept") return false;
|
|
322
|
+
const meta = node.metadata;
|
|
323
|
+
return meta?.subkind === "keyword";
|
|
324
|
+
}
|
|
297
325
|
var CHARS_PER_TOKEN = 4;
|
|
298
326
|
function scoreNodes(store, terms) {
|
|
299
327
|
const allNodes = store.getAllNodes();
|
|
@@ -306,7 +334,11 @@ function scoreNodes(store, terms) {
|
|
|
306
334
|
if (label.includes(t)) score += 2;
|
|
307
335
|
if (file.includes(t)) score += 1;
|
|
308
336
|
}
|
|
309
|
-
if (score > 0)
|
|
337
|
+
if (score > 0) {
|
|
338
|
+
if (node.kind === "mistake") score *= MISTAKE_SCORE_BOOST;
|
|
339
|
+
if (isHiddenKeyword(node)) score *= KEYWORD_SCORE_DOWNWEIGHT;
|
|
340
|
+
scored.push({ score, node });
|
|
341
|
+
}
|
|
310
342
|
}
|
|
311
343
|
return scored.sort((a, b) => b.score - a.score);
|
|
312
344
|
}
|
|
@@ -321,6 +353,12 @@ function queryGraph(store, question, options = {}) {
|
|
|
321
353
|
for (const n of startNodes) store.incrementQueryCount(n.id);
|
|
322
354
|
const visited = new Set(startNodes.map((n) => n.id));
|
|
323
355
|
const collectedEdges = [];
|
|
356
|
+
const shouldSkipEdgeFrom = (currentNodeId, edge) => {
|
|
357
|
+
if (edge.relation !== "triggered_by") return false;
|
|
358
|
+
const currentNode = store.getNode(currentNodeId);
|
|
359
|
+
if (!currentNode) return false;
|
|
360
|
+
return !isHiddenKeyword(currentNode);
|
|
361
|
+
};
|
|
324
362
|
if (mode === "bfs") {
|
|
325
363
|
let frontier = new Set(startNodes.map((n) => n.id));
|
|
326
364
|
for (let d = 0; d < depth; d++) {
|
|
@@ -328,6 +366,7 @@ function queryGraph(store, question, options = {}) {
|
|
|
328
366
|
for (const nid of frontier) {
|
|
329
367
|
const neighbors = store.getNeighbors(nid);
|
|
330
368
|
for (const { node, edge } of neighbors) {
|
|
369
|
+
if (shouldSkipEdgeFrom(nid, edge)) continue;
|
|
331
370
|
if (!visited.has(node.id)) {
|
|
332
371
|
nextFrontier.add(node.id);
|
|
333
372
|
collectedEdges.push(edge);
|
|
@@ -344,6 +383,7 @@ function queryGraph(store, question, options = {}) {
|
|
|
344
383
|
if (d > depth) continue;
|
|
345
384
|
const neighbors = store.getNeighbors(id);
|
|
346
385
|
for (const { node, edge } of neighbors) {
|
|
386
|
+
if (shouldSkipEdgeFrom(id, edge)) continue;
|
|
347
387
|
if (!visited.has(node.id)) {
|
|
348
388
|
visited.add(node.id);
|
|
349
389
|
stack.push({ id: node.id, d: d + 1 });
|
|
@@ -420,12 +460,28 @@ function shortestPath(store, sourceTerm, targetTerm, maxHops = 8) {
|
|
|
420
460
|
function renderSubgraph(nodes, edges, tokenBudget) {
|
|
421
461
|
const charBudget = tokenBudget * CHARS_PER_TOKEN;
|
|
422
462
|
const lines = [];
|
|
463
|
+
const mistakes2 = nodes.filter((n) => n.kind === "mistake");
|
|
464
|
+
const visible = nodes.filter(
|
|
465
|
+
(n) => n.kind !== "mistake" && !isHiddenKeyword(n)
|
|
466
|
+
);
|
|
467
|
+
const hiddenKeywordIds = new Set(
|
|
468
|
+
nodes.filter(isHiddenKeyword).map((n) => n.id)
|
|
469
|
+
);
|
|
470
|
+
if (mistakes2.length > 0) {
|
|
471
|
+
lines.push("\u26A0\uFE0F PAST MISTAKES (relevant to your query):");
|
|
472
|
+
for (const m of mistakes2) {
|
|
473
|
+
const label = truncateGraphemeSafe(m.label, MAX_MISTAKE_LABEL_CHARS);
|
|
474
|
+
const confNote = m.confidence === "EXTRACTED" ? "" : ` [confidence ${m.confidenceScore}]`;
|
|
475
|
+
lines.push(` - ${label} (from ${m.sourceFile})${confNote}`);
|
|
476
|
+
}
|
|
477
|
+
lines.push("");
|
|
478
|
+
}
|
|
423
479
|
const degreeMap = /* @__PURE__ */ new Map();
|
|
424
480
|
for (const e of edges) {
|
|
425
481
|
degreeMap.set(e.source, (degreeMap.get(e.source) ?? 0) + 1);
|
|
426
482
|
degreeMap.set(e.target, (degreeMap.get(e.target) ?? 0) + 1);
|
|
427
483
|
}
|
|
428
|
-
const sorted = [...
|
|
484
|
+
const sorted = [...visible].sort(
|
|
429
485
|
(a, b) => (degreeMap.get(b.id) ?? 0) - (degreeMap.get(a.id) ?? 0)
|
|
430
486
|
);
|
|
431
487
|
for (const n of sorted) {
|
|
@@ -433,7 +489,18 @@ function renderSubgraph(nodes, edges, tokenBudget) {
|
|
|
433
489
|
`NODE ${n.label} [${n.kind}] src=${n.sourceFile} ${n.sourceLocation ?? ""}`
|
|
434
490
|
);
|
|
435
491
|
}
|
|
492
|
+
const skillConceptIds = new Set(
|
|
493
|
+
nodes.filter(
|
|
494
|
+
(n) => n.kind === "concept" && n.metadata?.subkind === "skill"
|
|
495
|
+
).map((n) => n.id)
|
|
496
|
+
);
|
|
436
497
|
for (const e of edges) {
|
|
498
|
+
if (hiddenKeywordIds.has(e.source) || hiddenKeywordIds.has(e.target)) {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (e.relation === "similar_to" && skillConceptIds.has(e.source) && skillConceptIds.has(e.target)) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
437
504
|
const srcNode = nodes.find((n) => n.id === e.source);
|
|
438
505
|
const tgtNode = nodes.find((n) => n.id === e.target);
|
|
439
506
|
if (srcNode && tgtNode) {
|
|
@@ -445,7 +512,7 @@ function renderSubgraph(nodes, edges, tokenBudget) {
|
|
|
445
512
|
}
|
|
446
513
|
let output = lines.join("\n");
|
|
447
514
|
if (output.length > charBudget) {
|
|
448
|
-
output = output
|
|
515
|
+
output = sliceGraphemeSafe(output, charBudget) + `
|
|
449
516
|
... (truncated to ~${tokenBudget} token budget)`;
|
|
450
517
|
}
|
|
451
518
|
return output;
|
|
@@ -1031,34 +1098,358 @@ function learnFromSession(text, sourceLabel = "session") {
|
|
|
1031
1098
|
return mineText(text, sourceLabel);
|
|
1032
1099
|
}
|
|
1033
1100
|
|
|
1101
|
+
// src/miners/skills-miner.ts
|
|
1102
|
+
import {
|
|
1103
|
+
existsSync as existsSync4,
|
|
1104
|
+
readFileSync as readFileSync4,
|
|
1105
|
+
readdirSync as readdirSync3,
|
|
1106
|
+
realpathSync as realpathSync2,
|
|
1107
|
+
statSync
|
|
1108
|
+
} from "fs";
|
|
1109
|
+
import { basename as basename3, dirname as dirname2, join as join3 } from "path";
|
|
1110
|
+
function makeId4(...parts) {
|
|
1111
|
+
return parts.filter(Boolean).join("_").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_|_$/g, "").toLowerCase().slice(0, 120);
|
|
1112
|
+
}
|
|
1113
|
+
function parseFrontmatter(content) {
|
|
1114
|
+
if (!content.startsWith("---")) {
|
|
1115
|
+
return { data: {}, body: content, parseOk: false };
|
|
1116
|
+
}
|
|
1117
|
+
const closeMatch = content.slice(3).match(/\n---\s*(\n|$)/);
|
|
1118
|
+
if (!closeMatch || closeMatch.index === void 0) {
|
|
1119
|
+
return { data: {}, body: content, parseOk: false };
|
|
1120
|
+
}
|
|
1121
|
+
const yamlBlock = content.slice(3, 3 + closeMatch.index).trim();
|
|
1122
|
+
const bodyStart = 3 + closeMatch.index + closeMatch[0].length;
|
|
1123
|
+
const body = content.slice(bodyStart);
|
|
1124
|
+
try {
|
|
1125
|
+
const data = parseYaml(yamlBlock);
|
|
1126
|
+
return { data, body, parseOk: true };
|
|
1127
|
+
} catch {
|
|
1128
|
+
return { data: {}, body, parseOk: false };
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
function parseYaml(block) {
|
|
1132
|
+
const data = {};
|
|
1133
|
+
const lines = block.split("\n");
|
|
1134
|
+
let i = 0;
|
|
1135
|
+
while (i < lines.length) {
|
|
1136
|
+
const line = lines[i];
|
|
1137
|
+
if (!line.trim() || line.trim().startsWith("#")) {
|
|
1138
|
+
i++;
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
const topMatch = line.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
|
|
1142
|
+
if (!topMatch) {
|
|
1143
|
+
throw new Error(`YAML parse: unexpected line ${i}: ${line}`);
|
|
1144
|
+
}
|
|
1145
|
+
const [, key, rest] = topMatch;
|
|
1146
|
+
if (rest === ">" || rest === "|") {
|
|
1147
|
+
const style = rest;
|
|
1148
|
+
const collected = [];
|
|
1149
|
+
i++;
|
|
1150
|
+
while (i < lines.length) {
|
|
1151
|
+
const next = lines[i];
|
|
1152
|
+
if (next === "" || /^\s/.test(next)) {
|
|
1153
|
+
collected.push(next.replace(/^ {2}/, "").replace(/^\t/, ""));
|
|
1154
|
+
i++;
|
|
1155
|
+
} else {
|
|
1156
|
+
break;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
data[key] = style === ">" ? collected.filter((l) => l.trim()).join(" ").trim() : collected.join("\n").trim();
|
|
1160
|
+
} else if (rest === "") {
|
|
1161
|
+
const nested = {};
|
|
1162
|
+
i++;
|
|
1163
|
+
while (i < lines.length && /^\s+\S/.test(lines[i])) {
|
|
1164
|
+
const childMatch = lines[i].match(
|
|
1165
|
+
/^\s+([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/
|
|
1166
|
+
);
|
|
1167
|
+
if (childMatch) {
|
|
1168
|
+
nested[childMatch[1]] = stripQuotes(childMatch[2]);
|
|
1169
|
+
}
|
|
1170
|
+
i++;
|
|
1171
|
+
}
|
|
1172
|
+
data[key] = nested;
|
|
1173
|
+
} else {
|
|
1174
|
+
data[key] = stripQuotes(rest);
|
|
1175
|
+
i++;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return data;
|
|
1179
|
+
}
|
|
1180
|
+
function stripQuotes(s) {
|
|
1181
|
+
const trimmed = s.trim();
|
|
1182
|
+
if (trimmed.length < 2) return trimmed;
|
|
1183
|
+
const first = trimmed[0];
|
|
1184
|
+
const last = trimmed[trimmed.length - 1];
|
|
1185
|
+
if (first === '"' && last === '"') {
|
|
1186
|
+
return trimmed.slice(1, -1).replace(
|
|
1187
|
+
/\\u([0-9a-fA-F]{4})/g,
|
|
1188
|
+
(_, hex) => String.fromCharCode(parseInt(hex, 16))
|
|
1189
|
+
).replace(/\\"/g, '"').replace(/\\'/g, "'").replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\\\/g, "\\");
|
|
1190
|
+
}
|
|
1191
|
+
if (first === "'" && last === "'") {
|
|
1192
|
+
return trimmed.slice(1, -1).replace(/''/g, "'");
|
|
1193
|
+
}
|
|
1194
|
+
return trimmed;
|
|
1195
|
+
}
|
|
1196
|
+
var QUOTED_PHRASE_RE = /[\u0022\u0027\u201C\u201D\u2018\u2019]([^\u0022\u0027\u201C\u201D\u2018\u2019\n]{4,100}?)[\u0022\u0027\u201C\u201D\u2018\u2019]/g;
|
|
1197
|
+
var USE_WHEN_RE = /\bUse when\s+(.+?)(?=\.\s+[A-Z]|[\n\r]|$)/g;
|
|
1198
|
+
function extractTriggers(text) {
|
|
1199
|
+
const triggers = /* @__PURE__ */ new Set();
|
|
1200
|
+
for (const m of text.matchAll(QUOTED_PHRASE_RE)) {
|
|
1201
|
+
const t = m[1]?.trim();
|
|
1202
|
+
if (t && t.length >= 4) triggers.add(t);
|
|
1203
|
+
}
|
|
1204
|
+
for (const m of text.matchAll(USE_WHEN_RE)) {
|
|
1205
|
+
const t = m[1]?.trim().replace(/[.,;]+$/, "");
|
|
1206
|
+
if (t && t.length > 0 && t.length < 120) triggers.add(t);
|
|
1207
|
+
}
|
|
1208
|
+
return [...triggers];
|
|
1209
|
+
}
|
|
1210
|
+
function extractRelatedSkills(body) {
|
|
1211
|
+
const sectionMatch = body.match(
|
|
1212
|
+
/##\s*Related Skills\s*\r?\n([\s\S]*?)(?=\r?\n##|\r?\n#[^#]|$)/i
|
|
1213
|
+
);
|
|
1214
|
+
if (!sectionMatch) return [];
|
|
1215
|
+
const section = sectionMatch[1];
|
|
1216
|
+
const names = [];
|
|
1217
|
+
for (const line of section.split(/\r?\n/)) {
|
|
1218
|
+
const m = line.match(/^[\s]*[-*+]\s+`?([a-z][a-z0-9-]*)`?/i);
|
|
1219
|
+
if (m) names.push(m[1].toLowerCase());
|
|
1220
|
+
}
|
|
1221
|
+
return [...new Set(names)];
|
|
1222
|
+
}
|
|
1223
|
+
function discoverSkillFiles(skillsDir) {
|
|
1224
|
+
if (!existsSync4(skillsDir)) return [];
|
|
1225
|
+
let entries;
|
|
1226
|
+
try {
|
|
1227
|
+
entries = readdirSync3(skillsDir, {
|
|
1228
|
+
withFileTypes: true,
|
|
1229
|
+
encoding: "utf-8"
|
|
1230
|
+
});
|
|
1231
|
+
} catch {
|
|
1232
|
+
return [];
|
|
1233
|
+
}
|
|
1234
|
+
const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name));
|
|
1235
|
+
const results = [];
|
|
1236
|
+
for (const entry of sorted) {
|
|
1237
|
+
if (entry.name.startsWith(".")) continue;
|
|
1238
|
+
let isDir = entry.isDirectory();
|
|
1239
|
+
if (entry.isSymbolicLink()) {
|
|
1240
|
+
try {
|
|
1241
|
+
const resolved = realpathSync2(join3(skillsDir, entry.name));
|
|
1242
|
+
isDir = statSync(resolved).isDirectory();
|
|
1243
|
+
} catch {
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
if (!isDir) continue;
|
|
1248
|
+
const skillMdPath = join3(skillsDir, entry.name, "SKILL.md");
|
|
1249
|
+
if (existsSync4(skillMdPath)) {
|
|
1250
|
+
results.push(skillMdPath);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return results;
|
|
1254
|
+
}
|
|
1255
|
+
function mineSkills(skillsDir) {
|
|
1256
|
+
const result = {
|
|
1257
|
+
nodes: [],
|
|
1258
|
+
edges: [],
|
|
1259
|
+
skillCount: 0,
|
|
1260
|
+
anomalies: []
|
|
1261
|
+
};
|
|
1262
|
+
const skillFiles = discoverSkillFiles(skillsDir);
|
|
1263
|
+
if (skillFiles.length === 0) return result;
|
|
1264
|
+
const now = Date.now();
|
|
1265
|
+
const keywordIds = /* @__PURE__ */ new Set();
|
|
1266
|
+
const skillIdByDirName = /* @__PURE__ */ new Map();
|
|
1267
|
+
const pendingRelated = [];
|
|
1268
|
+
for (const skillPath of skillFiles) {
|
|
1269
|
+
let content;
|
|
1270
|
+
try {
|
|
1271
|
+
content = readFileSync4(skillPath, "utf-8");
|
|
1272
|
+
} catch {
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
const skillDirName = basename3(dirname2(skillPath));
|
|
1276
|
+
const { data, body, parseOk } = parseFrontmatter(content);
|
|
1277
|
+
const hasFrontmatter = parseOk && Object.keys(data).length > 0;
|
|
1278
|
+
let name;
|
|
1279
|
+
let description;
|
|
1280
|
+
let version;
|
|
1281
|
+
if (hasFrontmatter) {
|
|
1282
|
+
name = String(data.name ?? skillDirName);
|
|
1283
|
+
description = String(data.description ?? "");
|
|
1284
|
+
const meta = data.metadata;
|
|
1285
|
+
if (meta && typeof meta === "object" && "version" in meta) {
|
|
1286
|
+
version = String(meta.version);
|
|
1287
|
+
}
|
|
1288
|
+
} else {
|
|
1289
|
+
result.anomalies.push(skillPath);
|
|
1290
|
+
const headingMatch = content.match(/^#\s+(.+)$/m);
|
|
1291
|
+
name = headingMatch?.[1]?.trim() ?? skillDirName;
|
|
1292
|
+
const paragraphs = content.split(/\r?\n\s*\r?\n/).map((p) => p.trim()).filter((p) => p && !p.startsWith("#"));
|
|
1293
|
+
description = paragraphs[0] ?? "";
|
|
1294
|
+
}
|
|
1295
|
+
const skillId = makeId4("skill", skillDirName);
|
|
1296
|
+
skillIdByDirName.set(skillDirName.toLowerCase(), skillId);
|
|
1297
|
+
const sourceFileRel = `${skillDirName}/SKILL.md`;
|
|
1298
|
+
const sizeLines = content.split("\n").length;
|
|
1299
|
+
result.nodes.push({
|
|
1300
|
+
id: skillId,
|
|
1301
|
+
label: name,
|
|
1302
|
+
kind: "concept",
|
|
1303
|
+
sourceFile: sourceFileRel,
|
|
1304
|
+
sourceLocation: skillPath,
|
|
1305
|
+
confidence: "EXTRACTED",
|
|
1306
|
+
confidenceScore: 1,
|
|
1307
|
+
lastVerified: now,
|
|
1308
|
+
queryCount: 0,
|
|
1309
|
+
metadata: {
|
|
1310
|
+
miner: "skills",
|
|
1311
|
+
subkind: "skill",
|
|
1312
|
+
description: truncateGraphemeSafe(description, 500),
|
|
1313
|
+
sizeLines,
|
|
1314
|
+
hasFrontmatter,
|
|
1315
|
+
version,
|
|
1316
|
+
skillDir: skillDirName
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
result.skillCount++;
|
|
1320
|
+
const triggers = extractTriggers(description + "\n\n" + body);
|
|
1321
|
+
for (const trigger of triggers) {
|
|
1322
|
+
const normalized = trigger.toLowerCase().trim();
|
|
1323
|
+
if (normalized.length === 0 || normalized.length > 120) continue;
|
|
1324
|
+
const keywordId = makeId4("keyword", normalized);
|
|
1325
|
+
if (!keywordIds.has(keywordId)) {
|
|
1326
|
+
result.nodes.push({
|
|
1327
|
+
id: keywordId,
|
|
1328
|
+
label: trigger,
|
|
1329
|
+
kind: "concept",
|
|
1330
|
+
sourceFile: sourceFileRel,
|
|
1331
|
+
sourceLocation: null,
|
|
1332
|
+
confidence: "EXTRACTED",
|
|
1333
|
+
confidenceScore: 1,
|
|
1334
|
+
lastVerified: now,
|
|
1335
|
+
queryCount: 0,
|
|
1336
|
+
metadata: { miner: "skills", subkind: "keyword" }
|
|
1337
|
+
});
|
|
1338
|
+
keywordIds.add(keywordId);
|
|
1339
|
+
}
|
|
1340
|
+
result.edges.push({
|
|
1341
|
+
source: keywordId,
|
|
1342
|
+
target: skillId,
|
|
1343
|
+
relation: "triggered_by",
|
|
1344
|
+
confidence: "EXTRACTED",
|
|
1345
|
+
confidenceScore: 1,
|
|
1346
|
+
sourceFile: sourceFileRel,
|
|
1347
|
+
sourceLocation: skillPath,
|
|
1348
|
+
lastVerified: now,
|
|
1349
|
+
metadata: { miner: "skills" }
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
if (hasFrontmatter || body.length > 0) {
|
|
1353
|
+
const relatedNames = extractRelatedSkills(body);
|
|
1354
|
+
for (const relatedName of relatedNames) {
|
|
1355
|
+
pendingRelated.push({ fromId: skillId, toName: relatedName });
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
for (const { fromId, toName } of pendingRelated) {
|
|
1360
|
+
const toId = skillIdByDirName.get(toName.toLowerCase());
|
|
1361
|
+
if (toId && toId !== fromId) {
|
|
1362
|
+
result.edges.push({
|
|
1363
|
+
source: fromId,
|
|
1364
|
+
target: toId,
|
|
1365
|
+
relation: "similar_to",
|
|
1366
|
+
confidence: "INFERRED",
|
|
1367
|
+
confidenceScore: 0.8,
|
|
1368
|
+
sourceFile: "SKILL.md",
|
|
1369
|
+
sourceLocation: null,
|
|
1370
|
+
lastVerified: Date.now(),
|
|
1371
|
+
metadata: { miner: "skills", via: "related_skills_section" }
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return result;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1034
1378
|
// src/core.ts
|
|
1035
1379
|
var ENGRAM_DIR = ".engram";
|
|
1036
1380
|
var DB_FILE = "graph.db";
|
|
1381
|
+
var LOCK_FILE = "init.lock";
|
|
1382
|
+
var DEFAULT_SKILLS_DIR = join4(homedir(), ".claude", "skills");
|
|
1037
1383
|
function getDbPath(projectRoot) {
|
|
1038
|
-
return
|
|
1384
|
+
return join4(projectRoot, ENGRAM_DIR, DB_FILE);
|
|
1039
1385
|
}
|
|
1040
1386
|
async function getStore(projectRoot) {
|
|
1041
1387
|
return GraphStore.open(getDbPath(projectRoot));
|
|
1042
1388
|
}
|
|
1043
|
-
async function init(projectRoot) {
|
|
1389
|
+
async function init(projectRoot, options = {}) {
|
|
1044
1390
|
const root = resolve2(projectRoot);
|
|
1045
1391
|
const start = Date.now();
|
|
1046
|
-
mkdirSync2(
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1392
|
+
mkdirSync2(join4(root, ENGRAM_DIR), { recursive: true });
|
|
1393
|
+
const lockPath = join4(root, ENGRAM_DIR, LOCK_FILE);
|
|
1394
|
+
try {
|
|
1395
|
+
writeFileSync2(lockPath, String(process.pid), { flag: "wx" });
|
|
1396
|
+
} catch (err) {
|
|
1397
|
+
if (err.code === "EEXIST") {
|
|
1398
|
+
throw new Error(
|
|
1399
|
+
`engram: another init is running on ${root} (lock: ${lockPath}). If no other process is active, delete the lock file manually.`
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
throw err;
|
|
1403
|
+
}
|
|
1053
1404
|
try {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1405
|
+
const { nodes, edges, fileCount, totalLines } = extractDirectory(root);
|
|
1406
|
+
const gitResult = mineGitHistory(root);
|
|
1407
|
+
const sessionResult = mineSessionHistory(root);
|
|
1408
|
+
let skillCount = 0;
|
|
1409
|
+
let skillNodes = [];
|
|
1410
|
+
let skillEdges = [];
|
|
1411
|
+
if (options.withSkills) {
|
|
1412
|
+
const skillsDir = typeof options.withSkills === "string" ? options.withSkills : DEFAULT_SKILLS_DIR;
|
|
1413
|
+
const skillsResult = mineSkills(skillsDir);
|
|
1414
|
+
skillCount = skillsResult.skillCount;
|
|
1415
|
+
skillNodes = skillsResult.nodes;
|
|
1416
|
+
skillEdges = skillsResult.edges;
|
|
1417
|
+
}
|
|
1418
|
+
const allNodes = [
|
|
1419
|
+
...nodes,
|
|
1420
|
+
...gitResult.nodes,
|
|
1421
|
+
...sessionResult.nodes,
|
|
1422
|
+
...skillNodes
|
|
1423
|
+
];
|
|
1424
|
+
const allEdges = [
|
|
1425
|
+
...edges,
|
|
1426
|
+
...gitResult.edges,
|
|
1427
|
+
...sessionResult.edges,
|
|
1428
|
+
...skillEdges
|
|
1429
|
+
];
|
|
1430
|
+
const store = await getStore(root);
|
|
1431
|
+
try {
|
|
1432
|
+
store.clearAll();
|
|
1433
|
+
store.bulkUpsert(allNodes, allEdges);
|
|
1434
|
+
store.setStat("last_mined", String(Date.now()));
|
|
1435
|
+
store.setStat("project_root", root);
|
|
1436
|
+
} finally {
|
|
1437
|
+
store.close();
|
|
1438
|
+
}
|
|
1439
|
+
return {
|
|
1440
|
+
nodes: allNodes.length,
|
|
1441
|
+
edges: allEdges.length,
|
|
1442
|
+
fileCount,
|
|
1443
|
+
totalLines,
|
|
1444
|
+
timeMs: Date.now() - start,
|
|
1445
|
+
skillCount
|
|
1446
|
+
};
|
|
1058
1447
|
} finally {
|
|
1059
|
-
|
|
1448
|
+
try {
|
|
1449
|
+
unlinkSync(lockPath);
|
|
1450
|
+
} catch {
|
|
1451
|
+
}
|
|
1060
1452
|
}
|
|
1061
|
-
return { nodes: allNodes.length, edges: allEdges.length, fileCount, totalLines, timeMs: Date.now() - start };
|
|
1062
1453
|
}
|
|
1063
1454
|
async function query(projectRoot, question, options = {}) {
|
|
1064
1455
|
const store = await getStore(projectRoot);
|
|
@@ -1110,6 +1501,28 @@ async function learn(projectRoot, text, sourceLabel = "manual") {
|
|
|
1110
1501
|
}
|
|
1111
1502
|
return { nodesAdded: nodes.length };
|
|
1112
1503
|
}
|
|
1504
|
+
async function mistakes(projectRoot, options = {}) {
|
|
1505
|
+
const store = await getStore(projectRoot);
|
|
1506
|
+
try {
|
|
1507
|
+
let items = store.getAllNodes().filter((n) => n.kind === "mistake");
|
|
1508
|
+
if (options.sinceDays !== void 0) {
|
|
1509
|
+
const cutoff = Date.now() - options.sinceDays * 24 * 60 * 60 * 1e3;
|
|
1510
|
+
items = items.filter((m) => m.lastVerified >= cutoff);
|
|
1511
|
+
}
|
|
1512
|
+
items.sort((a, b) => b.lastVerified - a.lastVerified);
|
|
1513
|
+
const limit = options.limit ?? 20;
|
|
1514
|
+
return items.slice(0, limit).map((m) => ({
|
|
1515
|
+
id: m.id,
|
|
1516
|
+
label: m.label,
|
|
1517
|
+
confidence: m.confidence,
|
|
1518
|
+
confidenceScore: m.confidenceScore,
|
|
1519
|
+
sourceFile: m.sourceFile,
|
|
1520
|
+
lastVerified: m.lastVerified
|
|
1521
|
+
}));
|
|
1522
|
+
} finally {
|
|
1523
|
+
store.close();
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1113
1526
|
async function benchmark(projectRoot, questions) {
|
|
1114
1527
|
const root = resolve2(projectRoot);
|
|
1115
1528
|
const store = await getStore(root);
|
|
@@ -1121,8 +1534,8 @@ async function benchmark(projectRoot, questions) {
|
|
|
1121
1534
|
if (node.sourceFile && !seenFiles.has(node.sourceFile)) {
|
|
1122
1535
|
seenFiles.add(node.sourceFile);
|
|
1123
1536
|
try {
|
|
1124
|
-
const fullPath =
|
|
1125
|
-
if (
|
|
1537
|
+
const fullPath = join4(root, node.sourceFile);
|
|
1538
|
+
if (existsSync5(fullPath)) fullCorpusChars += readFileSync5(fullPath, "utf-8").length;
|
|
1126
1539
|
} catch {
|
|
1127
1540
|
}
|
|
1128
1541
|
}
|
|
@@ -1143,8 +1556,8 @@ async function benchmark(projectRoot, questions) {
|
|
|
1143
1556
|
let relevantChars = 0;
|
|
1144
1557
|
for (const f of matchedFiles) {
|
|
1145
1558
|
try {
|
|
1146
|
-
const fullPath =
|
|
1147
|
-
if (
|
|
1559
|
+
const fullPath = join4(root, f);
|
|
1560
|
+
if (existsSync5(fullPath)) relevantChars += readFileSync5(fullPath, "utf-8").length;
|
|
1148
1561
|
} catch {
|
|
1149
1562
|
}
|
|
1150
1563
|
}
|
|
@@ -1174,11 +1587,15 @@ async function benchmark(projectRoot, questions) {
|
|
|
1174
1587
|
|
|
1175
1588
|
export {
|
|
1176
1589
|
GraphStore,
|
|
1590
|
+
sliceGraphemeSafe,
|
|
1591
|
+
truncateGraphemeSafe,
|
|
1592
|
+
MAX_MISTAKE_LABEL_CHARS,
|
|
1177
1593
|
queryGraph,
|
|
1178
1594
|
shortestPath,
|
|
1179
1595
|
SUPPORTED_EXTENSIONS,
|
|
1180
1596
|
extractFile,
|
|
1181
1597
|
extractDirectory,
|
|
1598
|
+
mineSkills,
|
|
1182
1599
|
getDbPath,
|
|
1183
1600
|
getStore,
|
|
1184
1601
|
init,
|
|
@@ -1187,5 +1604,6 @@ export {
|
|
|
1187
1604
|
godNodes,
|
|
1188
1605
|
stats,
|
|
1189
1606
|
learn,
|
|
1607
|
+
mistakes,
|
|
1190
1608
|
benchmark
|
|
1191
1609
|
};
|