engramx 0.5.3 → 1.0.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/CHANGELOG.md +71 -0
- package/README.md +264 -291
- package/dist/aider-context-TNGSXMVY.js +54 -0
- package/dist/{chunk-IIFAAYDO.js → chunk-6SFMVYUN.js} +5 -0
- package/dist/chunk-CEAANHHX.js +88 -0
- package/dist/{chunk-KEV4LNM6.js → chunk-QOG4K427.js} +1 -1
- package/dist/chunk-SBHGK5WA.js +104 -0
- package/dist/cli.js +743 -131
- package/dist/{core-UXIP2GDR.js → core-77MHT3QV.js} +2 -1
- package/dist/cursor-mdc-HWVUZUZH.js +75 -0
- package/dist/exporter-A3VSLS4U.js +47 -0
- package/dist/importer-LU2YFZDY.js +67 -0
- package/dist/index.js +3 -2
- package/dist/migrate-5ZJWF2HD.js +10 -0
- package/dist/serve.js +2 -1
- package/dist/server-I3C74ZLB.js +193 -0
- package/dist/tuner-2LVIEE5V.js +113 -0
- package/package.json +11 -3
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// src/generators/cursor-mdc.ts
|
|
2
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
5
|
+
function detectGlobs(fileNodes) {
|
|
6
|
+
const extCounts = /* @__PURE__ */ new Map();
|
|
7
|
+
for (const n of fileNodes) {
|
|
8
|
+
const match = n.sourceFile.match(/\.([^./]+)$/);
|
|
9
|
+
if (match) {
|
|
10
|
+
const ext = match[1];
|
|
11
|
+
extCounts.set(ext, (extCounts.get(ext) ?? 0) + 1);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const exts = [...extCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6).map(([ext]) => ext);
|
|
15
|
+
if (exts.length === 0) return ["src/**/*"];
|
|
16
|
+
return exts.map((ext) => `src/**/*.${ext}`);
|
|
17
|
+
}
|
|
18
|
+
function bullet(label) {
|
|
19
|
+
return `- ${label}`;
|
|
20
|
+
}
|
|
21
|
+
function buildFrontmatter(globs) {
|
|
22
|
+
const globList = globs.map((g) => `"${g}"`).join(", ");
|
|
23
|
+
return [
|
|
24
|
+
"---",
|
|
25
|
+
"description: engram-generated context spine \u2014 architecture, decisions, and known issues",
|
|
26
|
+
"alwaysApply: false",
|
|
27
|
+
`globs: [${globList}]`,
|
|
28
|
+
"---"
|
|
29
|
+
].join("\n");
|
|
30
|
+
}
|
|
31
|
+
function buildSection(heading, bullets) {
|
|
32
|
+
if (bullets.length === 0) return "";
|
|
33
|
+
return [`## ${heading}`, "", ...bullets, ""].join("\n");
|
|
34
|
+
}
|
|
35
|
+
async function generateCursorMdc(projectPath) {
|
|
36
|
+
const { getStore } = await import("./core-77MHT3QV.js");
|
|
37
|
+
const store = await getStore(projectPath);
|
|
38
|
+
try {
|
|
39
|
+
const allNodes = store.getAllNodes();
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const cutoff = now - THIRTY_DAYS_MS;
|
|
42
|
+
const patternBullets = allNodes.filter(
|
|
43
|
+
(n) => n.kind === "pattern" && n.confidenceScore >= 0.8 && n.metadata.type !== "hot_file"
|
|
44
|
+
).map((n) => bullet(n.label));
|
|
45
|
+
const decisionBullets = allNodes.filter((n) => n.kind === "decision" && n.lastVerified >= cutoff).sort((a, b) => b.lastVerified - a.lastVerified).map((n) => bullet(n.label));
|
|
46
|
+
const mistakeBullets = allNodes.filter((n) => n.kind === "mistake").sort((a, b) => b.queryCount - a.queryCount).slice(0, 5).map((n) => bullet(n.label));
|
|
47
|
+
const godNodes = store.getGodNodes(10);
|
|
48
|
+
const godBullets = godNodes.map(
|
|
49
|
+
({ node, degree }) => `- \`${node.label}\` (${node.kind}, ${degree} connections) \u2014 ${node.sourceFile}`
|
|
50
|
+
);
|
|
51
|
+
const hotFileBullets = allNodes.filter(
|
|
52
|
+
(n) => n.kind === "pattern" && n.metadata.type === "hot_file"
|
|
53
|
+
).map((n) => bullet(n.label));
|
|
54
|
+
const fileNodes = allNodes.filter((n) => n.kind === "file");
|
|
55
|
+
const globs = detectGlobs(fileNodes);
|
|
56
|
+
const sections = [
|
|
57
|
+
buildSection("Architecture Patterns", patternBullets),
|
|
58
|
+
buildSection("Active Decisions", decisionBullets),
|
|
59
|
+
buildSection("Known Landmines", mistakeBullets),
|
|
60
|
+
buildSection("Core Entities", godBullets),
|
|
61
|
+
buildSection("Hot Files", hotFileBullets)
|
|
62
|
+
].filter((s) => s.length > 0);
|
|
63
|
+
const content = [buildFrontmatter(globs), "", ...sections].join("\n");
|
|
64
|
+
const outPath = join(projectPath, ".cursor", "rules", "engram-context.mdc");
|
|
65
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
66
|
+
writeFileSync(outPath, content, "utf-8");
|
|
67
|
+
const nodeCount = patternBullets.length + decisionBullets.length + mistakeBullets.length + godBullets.length + hotFileBullets.length;
|
|
68
|
+
return { filePath: outPath, sections: sections.length, nodes: nodeCount };
|
|
69
|
+
} finally {
|
|
70
|
+
store.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export {
|
|
74
|
+
generateCursorMdc
|
|
75
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/ccs/exporter.ts
|
|
2
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
var CCS_FILE = join(".context", "index.md");
|
|
5
|
+
var HEADER = "# Project Context\n\n> Auto-generated by engram. See https://github.com/Agentic-Insights/codebase-context-spec\n";
|
|
6
|
+
function bullet(label) {
|
|
7
|
+
return `- ${label}`;
|
|
8
|
+
}
|
|
9
|
+
function buildSection(heading, nodes) {
|
|
10
|
+
if (nodes.length === 0) return "";
|
|
11
|
+
const bullets = nodes.map((n) => bullet(n.label));
|
|
12
|
+
return [`## ${heading}`, "", ...bullets, ""].join("\n");
|
|
13
|
+
}
|
|
14
|
+
async function exportCcs(projectRoot) {
|
|
15
|
+
const { getStore } = await import("./core-77MHT3QV.js");
|
|
16
|
+
const store = await getStore(projectRoot);
|
|
17
|
+
try {
|
|
18
|
+
const allNodes = store.getAllNodes();
|
|
19
|
+
const patternNodes = allNodes.filter(
|
|
20
|
+
(n) => n.kind === "pattern" && n.confidenceScore >= 0.8 && n.metadata.type !== "hot_file"
|
|
21
|
+
).sort((a, b) => b.queryCount - a.queryCount);
|
|
22
|
+
const decisionNodes = allNodes.filter((n) => n.kind === "decision").sort((a, b) => b.lastVerified - a.lastVerified);
|
|
23
|
+
const mistakeNodes = allNodes.filter((n) => n.kind === "mistake").sort((a, b) => b.queryCount - a.queryCount);
|
|
24
|
+
const conceptNodes = allNodes.filter((n) => n.kind === "concept" && n.queryCount > 0).sort((a, b) => b.queryCount - a.queryCount).slice(0, 10);
|
|
25
|
+
const sections = [
|
|
26
|
+
buildSection("Architecture Patterns", patternNodes),
|
|
27
|
+
buildSection("Decisions", decisionNodes),
|
|
28
|
+
buildSection("Known Issues", mistakeNodes),
|
|
29
|
+
buildSection("Key Concepts", conceptNodes)
|
|
30
|
+
].filter((s) => s.length > 0);
|
|
31
|
+
const content = [HEADER, ...sections].join("\n");
|
|
32
|
+
const outPath = join(projectRoot, CCS_FILE);
|
|
33
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
34
|
+
writeFileSync(outPath, content, "utf-8");
|
|
35
|
+
const nodesExported = patternNodes.length + decisionNodes.length + mistakeNodes.length + conceptNodes.length;
|
|
36
|
+
return {
|
|
37
|
+
filePath: outPath,
|
|
38
|
+
sectionsWritten: sections.length,
|
|
39
|
+
nodesExported
|
|
40
|
+
};
|
|
41
|
+
} finally {
|
|
42
|
+
store.close();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
exportCcs
|
|
47
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/ccs/importer.ts
|
|
2
|
+
import { readFileSync, existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
var CCS_SOURCE = "ccs:.context/index.md";
|
|
6
|
+
var CCS_PATH = join(".context", "index.md");
|
|
7
|
+
function headingToKind(heading) {
|
|
8
|
+
const lower = heading.toLowerCase();
|
|
9
|
+
if (lower.includes("decision")) return "decision";
|
|
10
|
+
if (lower.includes("issue") || lower.includes("problem") || lower.includes("known issue")) return "mistake";
|
|
11
|
+
if (lower.includes("architecture") || lower.includes("design") || lower.includes("convention") || lower.includes("pattern")) {
|
|
12
|
+
return "pattern";
|
|
13
|
+
}
|
|
14
|
+
return "concept";
|
|
15
|
+
}
|
|
16
|
+
function parseBullet(line) {
|
|
17
|
+
const trimmed = line.trimStart();
|
|
18
|
+
if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) {
|
|
19
|
+
return trimmed.slice(2).trim();
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
async function importCcs(projectRoot) {
|
|
24
|
+
const filePath = join(projectRoot, CCS_PATH);
|
|
25
|
+
if (!existsSync(filePath)) {
|
|
26
|
+
return { nodesCreated: 0, sectionsFound: 0 };
|
|
27
|
+
}
|
|
28
|
+
const { getStore } = await import("./core-77MHT3QV.js");
|
|
29
|
+
const store = await getStore(projectRoot);
|
|
30
|
+
try {
|
|
31
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
32
|
+
const lines = raw.replace(/\r\n/g, "\n").split("\n");
|
|
33
|
+
let sectionsFound = 0;
|
|
34
|
+
let nodesCreated = 0;
|
|
35
|
+
let currentKind = "concept";
|
|
36
|
+
for (const line of lines) {
|
|
37
|
+
const headingMatch = line.match(/^##\s+(.+)/);
|
|
38
|
+
if (headingMatch) {
|
|
39
|
+
currentKind = headingToKind(headingMatch[1]);
|
|
40
|
+
sectionsFound++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const bulletText = parseBullet(line);
|
|
44
|
+
if (!bulletText) continue;
|
|
45
|
+
store.upsertNode({
|
|
46
|
+
id: randomUUID(),
|
|
47
|
+
label: bulletText,
|
|
48
|
+
kind: currentKind,
|
|
49
|
+
sourceFile: CCS_SOURCE,
|
|
50
|
+
sourceLocation: null,
|
|
51
|
+
confidence: "EXTRACTED",
|
|
52
|
+
confidenceScore: 0.9,
|
|
53
|
+
lastVerified: Date.now(),
|
|
54
|
+
queryCount: 0,
|
|
55
|
+
metadata: {}
|
|
56
|
+
});
|
|
57
|
+
nodesCreated++;
|
|
58
|
+
}
|
|
59
|
+
store.save();
|
|
60
|
+
return { nodesCreated, sectionsFound };
|
|
61
|
+
} finally {
|
|
62
|
+
store.close();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
importCcs
|
|
67
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
generateSummary,
|
|
5
5
|
install,
|
|
6
6
|
uninstall
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-QOG4K427.js";
|
|
8
8
|
import {
|
|
9
9
|
GraphStore,
|
|
10
10
|
SUPPORTED_EXTENSIONS,
|
|
@@ -23,7 +23,8 @@ import {
|
|
|
23
23
|
sliceGraphemeSafe,
|
|
24
24
|
stats,
|
|
25
25
|
truncateGraphemeSafe
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-6SFMVYUN.js";
|
|
27
|
+
import "./chunk-SBHGK5WA.js";
|
|
27
28
|
export {
|
|
28
29
|
GraphStore,
|
|
29
30
|
SUPPORTED_EXTENSIONS,
|
package/dist/serve.js
CHANGED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import {
|
|
2
|
+
learn,
|
|
3
|
+
query,
|
|
4
|
+
stats
|
|
5
|
+
} from "./chunk-6SFMVYUN.js";
|
|
6
|
+
import "./chunk-SBHGK5WA.js";
|
|
7
|
+
|
|
8
|
+
// src/server/http.ts
|
|
9
|
+
import { createServer } from "http";
|
|
10
|
+
import { writeFileSync, unlinkSync, mkdirSync, existsSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { createRequire } from "module";
|
|
13
|
+
var require2 = createRequire(import.meta.url);
|
|
14
|
+
var PKG_VERSION = (() => {
|
|
15
|
+
for (const p of ["../package.json", "../../package.json"]) {
|
|
16
|
+
try {
|
|
17
|
+
return require2(p).version;
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return "0.0.0";
|
|
22
|
+
})();
|
|
23
|
+
var PROVIDERS = [
|
|
24
|
+
"structure",
|
|
25
|
+
"mistakes",
|
|
26
|
+
"git",
|
|
27
|
+
"mempalace",
|
|
28
|
+
"context7",
|
|
29
|
+
"obsidian"
|
|
30
|
+
];
|
|
31
|
+
function parseUrl(req) {
|
|
32
|
+
return new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
33
|
+
}
|
|
34
|
+
async function readBody(req) {
|
|
35
|
+
const chunks = [];
|
|
36
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
37
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
38
|
+
}
|
|
39
|
+
function json(res, status, data) {
|
|
40
|
+
res.writeHead(status, {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
"Access-Control-Allow-Origin": "*",
|
|
43
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
44
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type"
|
|
45
|
+
});
|
|
46
|
+
res.end(JSON.stringify(data));
|
|
47
|
+
}
|
|
48
|
+
function checkAuth(req, res) {
|
|
49
|
+
const token = process.env.ENGRAM_API_TOKEN;
|
|
50
|
+
if (!token) return true;
|
|
51
|
+
const header = req.headers.authorization ?? "";
|
|
52
|
+
if (header === `Bearer ${token}`) return true;
|
|
53
|
+
json(res, 401, { error: "Unauthorized" });
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
function handleHealth(_req, res, startedAt) {
|
|
57
|
+
json(res, 200, {
|
|
58
|
+
ok: true,
|
|
59
|
+
version: PKG_VERSION,
|
|
60
|
+
uptime: Math.floor((Date.now() - startedAt) / 1e3)
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async function handleQuery(req, res, projectRoot) {
|
|
64
|
+
const url = parseUrl(req);
|
|
65
|
+
const q = url.searchParams.get("q");
|
|
66
|
+
if (!q) {
|
|
67
|
+
json(res, 400, { error: "Missing query parameter 'q'" });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const budget = parseInt(url.searchParams.get("budget") ?? "2000", 10);
|
|
71
|
+
try {
|
|
72
|
+
const result = await query(projectRoot, q, { tokenBudget: isNaN(budget) ? 2e3 : budget });
|
|
73
|
+
json(res, 200, {
|
|
74
|
+
text: result.text,
|
|
75
|
+
estimatedTokens: result.estimatedTokens,
|
|
76
|
+
providers: [...PROVIDERS]
|
|
77
|
+
});
|
|
78
|
+
} catch (err) {
|
|
79
|
+
json(res, 500, { error: "Query failed", detail: String(err) });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function handleStats(_req, res, projectRoot) {
|
|
83
|
+
try {
|
|
84
|
+
const result = await stats(projectRoot);
|
|
85
|
+
json(res, 200, result);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
json(res, 500, { error: "Stats failed", detail: String(err) });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function handleProviders(_req, res) {
|
|
91
|
+
const list = PROVIDERS.map((name) => ({ name, available: true }));
|
|
92
|
+
json(res, 200, list);
|
|
93
|
+
}
|
|
94
|
+
async function handleLearn(req, res, projectRoot) {
|
|
95
|
+
let body;
|
|
96
|
+
try {
|
|
97
|
+
body = await readBody(req);
|
|
98
|
+
} catch {
|
|
99
|
+
json(res, 400, { error: "Failed to read request body" });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
let parsed;
|
|
103
|
+
try {
|
|
104
|
+
parsed = JSON.parse(body);
|
|
105
|
+
} catch {
|
|
106
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!parsed.content || typeof parsed.content !== "string") {
|
|
110
|
+
json(res, 400, { error: "Missing 'content' in request body" });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
await learn(projectRoot, parsed.content, parsed.file ?? "http-api");
|
|
115
|
+
json(res, 201, { ok: true });
|
|
116
|
+
} catch (err) {
|
|
117
|
+
json(res, 500, { error: "Learn failed", detail: String(err) });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function writePid(projectRoot) {
|
|
121
|
+
const dir = join(projectRoot, ".engram");
|
|
122
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
123
|
+
writeFileSync(join(dir, "http-server.pid"), String(process.pid), "utf-8");
|
|
124
|
+
}
|
|
125
|
+
function removePid(projectRoot) {
|
|
126
|
+
try {
|
|
127
|
+
unlinkSync(join(projectRoot, ".engram", "http-server.pid"));
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function createHttpServer(projectRoot, port) {
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
const startedAt = Date.now();
|
|
134
|
+
const server = createServer(async (req, res) => {
|
|
135
|
+
if (req.method === "OPTIONS") {
|
|
136
|
+
res.writeHead(204, {
|
|
137
|
+
"Access-Control-Allow-Origin": "*",
|
|
138
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
139
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type"
|
|
140
|
+
});
|
|
141
|
+
res.end();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!checkAuth(req, res)) return;
|
|
145
|
+
const url = parseUrl(req);
|
|
146
|
+
const path = url.pathname;
|
|
147
|
+
try {
|
|
148
|
+
if (req.method === "GET" && path === "/health") {
|
|
149
|
+
handleHealth(req, res, startedAt);
|
|
150
|
+
} else if (req.method === "GET" && path === "/query") {
|
|
151
|
+
await handleQuery(req, res, projectRoot);
|
|
152
|
+
} else if (req.method === "GET" && path === "/stats") {
|
|
153
|
+
await handleStats(req, res, projectRoot);
|
|
154
|
+
} else if (req.method === "GET" && path === "/providers") {
|
|
155
|
+
handleProviders(req, res);
|
|
156
|
+
} else if (req.method === "POST" && path === "/learn") {
|
|
157
|
+
await handleLearn(req, res, projectRoot);
|
|
158
|
+
} else {
|
|
159
|
+
json(res, 404, { error: "Not found" });
|
|
160
|
+
}
|
|
161
|
+
} catch (err) {
|
|
162
|
+
json(res, 500, { error: "Internal server error", detail: String(err) });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
server.on("error", (err) => {
|
|
166
|
+
removePid(projectRoot);
|
|
167
|
+
reject(err);
|
|
168
|
+
});
|
|
169
|
+
server.listen(port, "127.0.0.1", () => {
|
|
170
|
+
writePid(projectRoot);
|
|
171
|
+
const cleanup = () => {
|
|
172
|
+
removePid(projectRoot);
|
|
173
|
+
server.close(() => process.exit(0));
|
|
174
|
+
};
|
|
175
|
+
process.on("SIGINT", cleanup);
|
|
176
|
+
process.on("SIGTERM", cleanup);
|
|
177
|
+
resolve();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/server/index.ts
|
|
183
|
+
var DEFAULT_PORT = 7337;
|
|
184
|
+
async function startHttpServer(projectRoot, port = DEFAULT_PORT) {
|
|
185
|
+
await createHttpServer(projectRoot, port);
|
|
186
|
+
process.stdout.write(
|
|
187
|
+
`engram HTTP server listening on http://127.0.0.1:${port}
|
|
188
|
+
`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
export {
|
|
192
|
+
startHttpServer
|
|
193
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readConfig,
|
|
3
|
+
readHookLog,
|
|
4
|
+
writeConfig
|
|
5
|
+
} from "./chunk-CEAANHHX.js";
|
|
6
|
+
|
|
7
|
+
// src/tuner/index.ts
|
|
8
|
+
var MIN_CONFIDENCE_SAMPLES = 10;
|
|
9
|
+
var CONFIDENCE_HYSTERESIS = 0.1;
|
|
10
|
+
var ALWAYS_ON_MIN_EVENTS = 20;
|
|
11
|
+
function analyzeTuning(projectRoot) {
|
|
12
|
+
const entries = readHookLog(projectRoot);
|
|
13
|
+
if (entries.length === 0) {
|
|
14
|
+
return { changes: [], entriesAnalyzed: 0, daysSpanned: 0 };
|
|
15
|
+
}
|
|
16
|
+
const config = readConfig(projectRoot);
|
|
17
|
+
const changes = [];
|
|
18
|
+
const deniedReads = entries.filter(
|
|
19
|
+
(e) => e.event === "PreToolUse" && e.decision === "deny"
|
|
20
|
+
);
|
|
21
|
+
const providerHits = /* @__PURE__ */ new Map();
|
|
22
|
+
const confidences = [];
|
|
23
|
+
for (const entry of deniedReads) {
|
|
24
|
+
const prov = entry.provider;
|
|
25
|
+
if (typeof prov === "string") {
|
|
26
|
+
providerHits.set(prov, (providerHits.get(prov) ?? 0) + 1);
|
|
27
|
+
}
|
|
28
|
+
if (typeof entry.confidence === "number") {
|
|
29
|
+
confidences.push(entry.confidence);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (confidences.length >= MIN_CONFIDENCE_SAMPLES) {
|
|
33
|
+
const sorted = [...confidences].sort((a, b) => a - b);
|
|
34
|
+
const median = sorted[Math.floor(sorted.length / 2)];
|
|
35
|
+
if (median > config.confidenceThreshold + CONFIDENCE_HYSTERESIS) {
|
|
36
|
+
const proposed = Math.round(median * 100) / 100;
|
|
37
|
+
changes.push({
|
|
38
|
+
field: "confidenceThreshold",
|
|
39
|
+
current: config.confidenceThreshold,
|
|
40
|
+
proposed,
|
|
41
|
+
reason: `Median result confidence is ${median.toFixed(2)} \u2014 raising threshold reduces noise`
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (deniedReads.length >= ALWAYS_ON_MIN_EVENTS) {
|
|
46
|
+
const knownProviders = ["engram:structure", "engram:mistakes", "engram:git", "mempalace", "context7", "obsidian"];
|
|
47
|
+
for (const pName of knownProviders) {
|
|
48
|
+
const hits = providerHits.get(pName) ?? 0;
|
|
49
|
+
const override = config.providers[pName];
|
|
50
|
+
const alreadyDisabled = override?.enabled === false;
|
|
51
|
+
if (hits === 0 && !alreadyDisabled) {
|
|
52
|
+
changes.push({
|
|
53
|
+
field: `providers.${pName}.enabled`,
|
|
54
|
+
current: true,
|
|
55
|
+
proposed: false,
|
|
56
|
+
reason: `Provider "${pName}" has 0 hits across ${deniedReads.length} intercepted reads \u2014 disabling saves latency`
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (deniedReads.length >= ALWAYS_ON_MIN_EVENTS) {
|
|
62
|
+
for (const [pName, hits] of providerHits) {
|
|
63
|
+
const hitRate = hits / deniedReads.length;
|
|
64
|
+
if (hitRate >= 0.9) {
|
|
65
|
+
const currentBudget = config.providers[pName]?.tokenBudget ?? 200;
|
|
66
|
+
const proposed = Math.round(currentBudget * 1.2);
|
|
67
|
+
if (proposed !== currentBudget) {
|
|
68
|
+
changes.push({
|
|
69
|
+
field: `providers.${pName}.tokenBudget`,
|
|
70
|
+
current: currentBudget,
|
|
71
|
+
proposed,
|
|
72
|
+
reason: `Provider "${pName}" contributes on ${Math.round(hitRate * 100)}% of reads \u2014 increasing budget allows richer output`
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const timestamps = entries.map((e) => e.ts).filter((ts) => typeof ts === "string").map((ts) => new Date(ts).getTime()).filter((t) => !isNaN(t));
|
|
79
|
+
const daysSpanned = timestamps.length >= 2 ? Math.ceil(
|
|
80
|
+
(Math.max(...timestamps) - Math.min(...timestamps)) / (24 * 3600 * 1e3)
|
|
81
|
+
) : 0;
|
|
82
|
+
return { changes, entriesAnalyzed: entries.length, daysSpanned };
|
|
83
|
+
}
|
|
84
|
+
function applyTuning(projectRoot, proposal) {
|
|
85
|
+
const config = readConfig(projectRoot);
|
|
86
|
+
let updated = { ...config };
|
|
87
|
+
for (const change of proposal.changes) {
|
|
88
|
+
if (change.field === "confidenceThreshold") {
|
|
89
|
+
updated = { ...updated, confidenceThreshold: change.proposed };
|
|
90
|
+
} else if (change.field === "totalTokenBudget") {
|
|
91
|
+
updated = { ...updated, totalTokenBudget: change.proposed };
|
|
92
|
+
} else if (change.field.startsWith("providers.")) {
|
|
93
|
+
const parts = change.field.split(".");
|
|
94
|
+
const providerName = parts[1];
|
|
95
|
+
const key = parts[2];
|
|
96
|
+
if (providerName && key) {
|
|
97
|
+
const existing = updated.providers[providerName] ?? {};
|
|
98
|
+
updated = {
|
|
99
|
+
...updated,
|
|
100
|
+
providers: {
|
|
101
|
+
...updated.providers,
|
|
102
|
+
[providerName]: { ...existing, [key]: change.proposed }
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
writeConfig(projectRoot, updated);
|
|
109
|
+
}
|
|
110
|
+
export {
|
|
111
|
+
analyzeTuning,
|
|
112
|
+
applyTuning
|
|
113
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engramx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "The context spine for AI coding agents. 6 providers assembled into rich context packets per Read interception. Up to 90% session-level token savings. Local SQLite, zero native deps, zero cloud.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
"dev": "tsup --watch",
|
|
23
23
|
"test": "vitest",
|
|
24
24
|
"lint": "tsc --noEmit",
|
|
25
|
-
"prepublishOnly": "npm run build"
|
|
25
|
+
"prepublishOnly": "npm run build",
|
|
26
|
+
"bench": "tsx bench/runner.ts",
|
|
27
|
+
"stress": "tsx bench/stress-test.ts"
|
|
26
28
|
},
|
|
27
29
|
"keywords": [
|
|
28
30
|
"structural-code-graph",
|
|
@@ -55,7 +57,13 @@
|
|
|
55
57
|
"dependencies": {
|
|
56
58
|
"chalk": "^5.6.2",
|
|
57
59
|
"commander": "^14.0.3",
|
|
58
|
-
"sql.js": "^1.14.1"
|
|
60
|
+
"sql.js": "^1.14.1",
|
|
61
|
+
"tree-sitter-go": "^0.25.0",
|
|
62
|
+
"tree-sitter-javascript": "^0.25.0",
|
|
63
|
+
"tree-sitter-python": "^0.25.0",
|
|
64
|
+
"tree-sitter-rust": "^0.24.0",
|
|
65
|
+
"tree-sitter-typescript": "^0.23.2",
|
|
66
|
+
"web-tree-sitter": "^0.26.8"
|
|
59
67
|
},
|
|
60
68
|
"devDependencies": {
|
|
61
69
|
"@types/node": "^25.5.2",
|