autoctxd 0.4.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 +62 -0
- package/CONTRIBUTING.md +80 -0
- package/LICENSE +21 -0
- package/README.md +301 -0
- package/SECURITY.md +81 -0
- package/package.json +55 -0
- package/scripts/install-hooks.ts +80 -0
- package/scripts/install.ps1 +71 -0
- package/scripts/install.sh +67 -0
- package/scripts/uninstall-hooks.ts +57 -0
- package/src/ai/active-guard.ts +96 -0
- package/src/ai/adaptive-ranker.ts +48 -0
- package/src/ai/classifier.ts +256 -0
- package/src/ai/compressor.ts +129 -0
- package/src/ai/decision-chains.ts +100 -0
- package/src/ai/decision-extractor.ts +148 -0
- package/src/ai/pattern-detector.ts +147 -0
- package/src/ai/proactive.ts +78 -0
- package/src/cli/doctor.ts +171 -0
- package/src/cli/embeddings.ts +209 -0
- package/src/cli/index.ts +574 -0
- package/src/cli/reclassify.ts +134 -0
- package/src/context/builder.ts +97 -0
- package/src/context/formatter.ts +109 -0
- package/src/context/ranker.ts +84 -0
- package/src/db/sqlite/decisions.ts +56 -0
- package/src/db/sqlite/feedback.ts +92 -0
- package/src/db/sqlite/observations.ts +58 -0
- package/src/db/sqlite/schema.ts +366 -0
- package/src/db/sqlite/sessions.ts +50 -0
- package/src/db/sqlite/summaries.ts +69 -0
- package/src/db/vector/client.ts +134 -0
- package/src/db/vector/embeddings.ts +119 -0
- package/src/db/vector/providers/factory.ts +99 -0
- package/src/db/vector/providers/minilm.ts +90 -0
- package/src/db/vector/providers/ollama.ts +92 -0
- package/src/db/vector/providers/tfidf.ts +98 -0
- package/src/db/vector/providers/types.ts +39 -0
- package/src/db/vector/search.ts +131 -0
- package/src/hooks/post-tool-use.ts +205 -0
- package/src/hooks/pre-tool-use.ts +305 -0
- package/src/hooks/stop.ts +334 -0
- package/src/mcp/server.ts +293 -0
- package/src/server/dashboard.html +268 -0
- package/src/server/dashboard.ts +170 -0
- package/src/util/debug.ts +56 -0
- package/src/util/ignore.ts +171 -0
- package/src/util/metrics.ts +236 -0
- package/src/util/path.ts +57 -0
- package/tsconfig.json +14 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// autoctxd CLI — Full command suite
|
|
3
|
+
|
|
4
|
+
import { getDb, closeDb } from "../db/sqlite/schema";
|
|
5
|
+
import { hybridSearch } from "../db/vector/search";
|
|
6
|
+
import { closeVectorDb } from "../db/vector/client";
|
|
7
|
+
import { runReclassifyCli } from "./reclassify";
|
|
8
|
+
import { runDoctor } from "./doctor";
|
|
9
|
+
import { runEmbeddingsCommand } from "./embeddings";
|
|
10
|
+
import { getGlobalMetrics } from "../util/metrics";
|
|
11
|
+
import { getFeedbackStats } from "../db/sqlite/feedback";
|
|
12
|
+
import { startDashboardServer } from "../server/dashboard";
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const command = args[0];
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
try {
|
|
19
|
+
switch (command) {
|
|
20
|
+
case "stats":
|
|
21
|
+
stats();
|
|
22
|
+
break;
|
|
23
|
+
case "sessions":
|
|
24
|
+
sessions();
|
|
25
|
+
break;
|
|
26
|
+
case "decisions":
|
|
27
|
+
decisions(args[1]);
|
|
28
|
+
break;
|
|
29
|
+
case "search":
|
|
30
|
+
await search(args.slice(1).join(" "));
|
|
31
|
+
break;
|
|
32
|
+
case "show":
|
|
33
|
+
show(args[1], args[2]);
|
|
34
|
+
break;
|
|
35
|
+
case "patterns":
|
|
36
|
+
patterns(args[1]);
|
|
37
|
+
break;
|
|
38
|
+
case "digest":
|
|
39
|
+
digest(args[1]);
|
|
40
|
+
break;
|
|
41
|
+
case "export":
|
|
42
|
+
exportContext(args[1]);
|
|
43
|
+
break;
|
|
44
|
+
case "init":
|
|
45
|
+
init();
|
|
46
|
+
break;
|
|
47
|
+
case "reset":
|
|
48
|
+
reset();
|
|
49
|
+
break;
|
|
50
|
+
case "reclassify":
|
|
51
|
+
runReclassifyCli(args.slice(1));
|
|
52
|
+
break;
|
|
53
|
+
case "doctor":
|
|
54
|
+
process.exit(await runDoctor());
|
|
55
|
+
break;
|
|
56
|
+
case "metrics":
|
|
57
|
+
showMetrics();
|
|
58
|
+
break;
|
|
59
|
+
case "dashboard":
|
|
60
|
+
await startDashboardServer(parseInt(args[1] || "4589", 10));
|
|
61
|
+
return; // keep process alive
|
|
62
|
+
case "mcp":
|
|
63
|
+
await import("../mcp/server");
|
|
64
|
+
return; // server owns the event loop
|
|
65
|
+
case "feedback":
|
|
66
|
+
showFeedbackStats();
|
|
67
|
+
break;
|
|
68
|
+
case "embeddings":
|
|
69
|
+
process.exit(await runEmbeddingsCommand(args.slice(1)));
|
|
70
|
+
break;
|
|
71
|
+
case "cleanup-decisions":
|
|
72
|
+
cleanupDecisions(args.slice(1));
|
|
73
|
+
break;
|
|
74
|
+
default:
|
|
75
|
+
help();
|
|
76
|
+
}
|
|
77
|
+
} finally {
|
|
78
|
+
closeDb();
|
|
79
|
+
await closeVectorDb();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function help() {
|
|
84
|
+
console.log(`
|
|
85
|
+
autoctxd — Persistent memory for Claude Code
|
|
86
|
+
|
|
87
|
+
Commands:
|
|
88
|
+
stats Usage statistics
|
|
89
|
+
sessions List recent sessions
|
|
90
|
+
decisions [project] Show architectural decisions
|
|
91
|
+
search <query> Hybrid search (semantic + full-text)
|
|
92
|
+
show session <id> Show session details
|
|
93
|
+
patterns [project] Show detected work patterns
|
|
94
|
+
digest [project] Show/generate weekly digests
|
|
95
|
+
export [project] Export context to markdown
|
|
96
|
+
init Initialize database
|
|
97
|
+
reset Clear all data (careful!)
|
|
98
|
+
reclassify [--dry-run] [--all] [--recompress] [--project <name>]
|
|
99
|
+
Re-run classifier on existing observations.
|
|
100
|
+
(defaults to only 'other'; --all for every row)
|
|
101
|
+
--recompress also rewrites historical session
|
|
102
|
+
summaries with the current compressor.
|
|
103
|
+
--project <name> scopes recompression to one project.
|
|
104
|
+
doctor Verify install integrity
|
|
105
|
+
metrics Show token-savings metrics
|
|
106
|
+
dashboard [port] Launch local web dashboard (default port 4589)
|
|
107
|
+
mcp Run as MCP server (stdio) — for Claude Desktop/Cursor/Windsurf
|
|
108
|
+
feedback Show learning stats (feedback accumulated)
|
|
109
|
+
embeddings <sub> Manage embedding provider (list/status/switch/reembed)
|
|
110
|
+
cleanup-decisions [--dry-run]
|
|
111
|
+
Delete decisions left over from the pre-0.4 extractor
|
|
112
|
+
(npm install of generic words, monitor noise) and
|
|
113
|
+
collapse cross-session duplicates.
|
|
114
|
+
|
|
115
|
+
Examples:
|
|
116
|
+
autoctxd search "race condition async"
|
|
117
|
+
autoctxd decisions ./my-app
|
|
118
|
+
autoctxd export ./my-app > context.md
|
|
119
|
+
autoctxd stats
|
|
120
|
+
`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function init() {
|
|
124
|
+
getDb();
|
|
125
|
+
console.log("Database initialized successfully.");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function reset() {
|
|
129
|
+
const db = getDb();
|
|
130
|
+
const tables = ["observations", "summaries", "decisions", "patterns",
|
|
131
|
+
"embeddings_cache", "token_metrics", "sessions"];
|
|
132
|
+
for (const table of tables) {
|
|
133
|
+
db.exec(`DELETE FROM ${table}`);
|
|
134
|
+
}
|
|
135
|
+
// Reset FTS
|
|
136
|
+
try {
|
|
137
|
+
db.exec("DELETE FROM observations_fts");
|
|
138
|
+
db.exec("DELETE FROM decisions_fts");
|
|
139
|
+
} catch {}
|
|
140
|
+
console.log("All data cleared.");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function cleanupDecisions(flags: string[]) {
|
|
144
|
+
const db = getDb();
|
|
145
|
+
const dryRun = flags.includes("--dry-run");
|
|
146
|
+
|
|
147
|
+
// Patterns that the pre-0.4 extractor produced from log noise. Anything
|
|
148
|
+
// here is verifiably not an architectural decision — it's a generic word
|
|
149
|
+
// the regex captured because "npm install <word>" appeared in a Monitor
|
|
150
|
+
// command, a comment, or a task description.
|
|
151
|
+
const noiseTitles = [
|
|
152
|
+
"Added npm dep: output",
|
|
153
|
+
"Added npm dep: progress",
|
|
154
|
+
"Added npm dep: tsx",
|
|
155
|
+
"Added npm dep: tests",
|
|
156
|
+
"Added npm dep: test",
|
|
157
|
+
"Added npm dep: dev",
|
|
158
|
+
"Added npm dep: prod",
|
|
159
|
+
"Added npm dep: build",
|
|
160
|
+
"Added npm dep: start",
|
|
161
|
+
"Added npm dep: all",
|
|
162
|
+
"Added npm dep: true",
|
|
163
|
+
"Added npm dep: false",
|
|
164
|
+
"Added npm dep: latest",
|
|
165
|
+
"Added npm dep: debug",
|
|
166
|
+
"Added bun dep: output",
|
|
167
|
+
"Added bun dep: progress",
|
|
168
|
+
"Added pnpm dep: output",
|
|
169
|
+
"Added pnpm dep: progress",
|
|
170
|
+
"Added yarn dep: output",
|
|
171
|
+
"Added yarn dep: progress",
|
|
172
|
+
"Added pip dep: output",
|
|
173
|
+
"Added pip dep: progress",
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
const placeholders = noiseTitles.map(() => "?").join(",");
|
|
177
|
+
|
|
178
|
+
// Tool-prefixed rows that the pre-0.4 extractor allowed through whenever
|
|
179
|
+
// the classifier scored an observation as type=decision (a stray keyword
|
|
180
|
+
// would do it). New code rejects these at the source; this clears the
|
|
181
|
+
// historical leftovers so the dashboard stops showing them.
|
|
182
|
+
const prefixNoiseClause = `(
|
|
183
|
+
decision_text LIKE 'Bash:%'
|
|
184
|
+
OR decision_text LIKE 'PowerShell:%'
|
|
185
|
+
OR decision_text LIKE 'Edit:%'
|
|
186
|
+
OR decision_text LIKE 'Edited %'
|
|
187
|
+
OR decision_text LIKE 'Monitor:%'
|
|
188
|
+
OR decision_text LIKE 'ToolSearch:%'
|
|
189
|
+
OR decision_text LIKE 'TodoWrite%'
|
|
190
|
+
OR decision_text LIKE 'TaskStop:%'
|
|
191
|
+
OR decision_text LIKE 'TaskOutput:%'
|
|
192
|
+
)`;
|
|
193
|
+
|
|
194
|
+
const before = (db.prepare("SELECT COUNT(*) as c FROM decisions").get() as any).c;
|
|
195
|
+
|
|
196
|
+
const noiseCount = (db.prepare(
|
|
197
|
+
`SELECT COUNT(*) as c FROM decisions WHERE title IN (${placeholders})`
|
|
198
|
+
).get(...noiseTitles) as any).c;
|
|
199
|
+
|
|
200
|
+
const prefixCount = (db.prepare(
|
|
201
|
+
`SELECT COUNT(*) as c FROM decisions WHERE ${prefixNoiseClause}`
|
|
202
|
+
).get() as any).c;
|
|
203
|
+
|
|
204
|
+
const dupCount = (db.prepare(`
|
|
205
|
+
SELECT COUNT(*) as c FROM decisions
|
|
206
|
+
WHERE id NOT IN (
|
|
207
|
+
SELECT MIN(id) FROM decisions
|
|
208
|
+
GROUP BY COALESCE(project_path, ''), title
|
|
209
|
+
)
|
|
210
|
+
`).get() as any).c;
|
|
211
|
+
|
|
212
|
+
console.log("\nautoctxd cleanup-decisions");
|
|
213
|
+
console.log("─".repeat(40));
|
|
214
|
+
console.log(` decisions in DB: ${before}`);
|
|
215
|
+
console.log(` generic-word noise: ${noiseCount}`);
|
|
216
|
+
console.log(` tool-prefixed noise: ${prefixCount}`);
|
|
217
|
+
console.log(` cross-session duplicates: ${dupCount}`);
|
|
218
|
+
|
|
219
|
+
if (dryRun) {
|
|
220
|
+
console.log("\n (--dry-run — no changes written)");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
db.exec("BEGIN");
|
|
225
|
+
try {
|
|
226
|
+
db.prepare(`DELETE FROM decisions WHERE title IN (${placeholders})`).run(...noiseTitles);
|
|
227
|
+
db.exec(`DELETE FROM decisions WHERE ${prefixNoiseClause}`);
|
|
228
|
+
db.exec(`
|
|
229
|
+
DELETE FROM decisions WHERE id NOT IN (
|
|
230
|
+
SELECT MIN(id) FROM decisions
|
|
231
|
+
GROUP BY COALESCE(project_path, ''), title
|
|
232
|
+
)
|
|
233
|
+
`);
|
|
234
|
+
db.exec("COMMIT");
|
|
235
|
+
} catch (e) {
|
|
236
|
+
db.exec("ROLLBACK");
|
|
237
|
+
throw e;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const after = (db.prepare("SELECT COUNT(*) as c FROM decisions").get() as any).c;
|
|
241
|
+
console.log(`\n deleted: ${before - after}`);
|
|
242
|
+
console.log(` remaining: ${after}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function stats() {
|
|
246
|
+
const db = getDb();
|
|
247
|
+
const sessionCount = (db.prepare("SELECT COUNT(*) as c FROM sessions").get() as any).c;
|
|
248
|
+
const obsCount = (db.prepare("SELECT COUNT(*) as c FROM observations").get() as any).c;
|
|
249
|
+
const decisionCount = (db.prepare("SELECT COUNT(*) as c FROM decisions").get() as any).c;
|
|
250
|
+
const patternCount = (db.prepare("SELECT COUNT(*) as c FROM patterns").get() as any).c;
|
|
251
|
+
const summaryCount = (db.prepare("SELECT COUNT(*) as c FROM summaries").get() as any).c;
|
|
252
|
+
const cacheCount = (db.prepare("SELECT COUNT(*) as c FROM embeddings_cache").get() as any).c;
|
|
253
|
+
|
|
254
|
+
const topTypes = db.prepare(`
|
|
255
|
+
SELECT type, COUNT(*) as c FROM observations GROUP BY type ORDER BY c DESC LIMIT 8
|
|
256
|
+
`).all() as Array<{ type: string; c: number }>;
|
|
257
|
+
|
|
258
|
+
const topProjects = db.prepare(`
|
|
259
|
+
SELECT project_path, COUNT(*) as c FROM sessions
|
|
260
|
+
WHERE project_path IS NOT NULL
|
|
261
|
+
GROUP BY project_path ORDER BY c DESC LIMIT 5
|
|
262
|
+
`).all() as Array<{ project_path: string; c: number }>;
|
|
263
|
+
|
|
264
|
+
console.log(`
|
|
265
|
+
╔══════════════════════════════════════╗
|
|
266
|
+
║ autoctxd Statistics ║
|
|
267
|
+
╚══════════════════════════════════════╝
|
|
268
|
+
|
|
269
|
+
Sessions: ${sessionCount}
|
|
270
|
+
Observations: ${obsCount}
|
|
271
|
+
Summaries: ${summaryCount}
|
|
272
|
+
Decisions: ${decisionCount}
|
|
273
|
+
Patterns: ${patternCount}
|
|
274
|
+
Cached embeds: ${cacheCount}
|
|
275
|
+
`);
|
|
276
|
+
|
|
277
|
+
if (topTypes.length > 0) {
|
|
278
|
+
console.log("Observation types:");
|
|
279
|
+
for (const t of topTypes) {
|
|
280
|
+
const bar = "█".repeat(Math.min(20, Math.ceil(t.c / Math.max(1, topTypes[0].c) * 20)));
|
|
281
|
+
console.log(` ${t.type.padEnd(15)} ${bar} ${t.c}`);
|
|
282
|
+
}
|
|
283
|
+
console.log("");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (topProjects.length > 0) {
|
|
287
|
+
console.log("Top projects:");
|
|
288
|
+
for (const p of topProjects) {
|
|
289
|
+
console.log(` ${p.project_path} (${p.c} sessions)`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function sessions() {
|
|
295
|
+
const db = getDb();
|
|
296
|
+
const rows = db.prepare(`
|
|
297
|
+
SELECT session_id, project_path, git_branch, started_at, ended_at, total_observations
|
|
298
|
+
FROM sessions ORDER BY started_at DESC LIMIT 20
|
|
299
|
+
`).all() as any[];
|
|
300
|
+
|
|
301
|
+
console.log("\nRecent Sessions:");
|
|
302
|
+
console.log("─".repeat(90));
|
|
303
|
+
for (const r of rows) {
|
|
304
|
+
const status = r.ended_at ? "done" : "active";
|
|
305
|
+
const branch = r.git_branch ? ` [${r.git_branch}]` : "";
|
|
306
|
+
const path = r.project_path ? truncPath(r.project_path, 30) : "?";
|
|
307
|
+
console.log(` [${status.padEnd(6)}] ${r.session_id.slice(0, 12)}... | ${path}${branch} | ${r.started_at} | ${r.total_observations || 0} obs`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function decisions(project?: string) {
|
|
312
|
+
const db = getDb();
|
|
313
|
+
const query = project
|
|
314
|
+
? db.prepare("SELECT * FROM decisions WHERE project_path LIKE ? ORDER BY created_at DESC").all(`%${project}%`)
|
|
315
|
+
: db.prepare("SELECT * FROM decisions ORDER BY created_at DESC LIMIT 20").all();
|
|
316
|
+
|
|
317
|
+
console.log("\nArchitectural Decisions:");
|
|
318
|
+
console.log("─".repeat(80));
|
|
319
|
+
for (const d of query as any[]) {
|
|
320
|
+
console.log(` [${(d.created_at || "").slice(0, 10)}] ${d.title}`);
|
|
321
|
+
console.log(` ${d.decision_text.slice(0, 120)}`);
|
|
322
|
+
if (d.alternatives) console.log(` Rejected: ${d.alternatives}`);
|
|
323
|
+
if (d.rationale) console.log(` Reason: ${d.rationale}`);
|
|
324
|
+
if (d.files_affected) console.log(` Files: ${d.files_affected}`);
|
|
325
|
+
console.log("");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if ((query as any[]).length === 0) {
|
|
329
|
+
console.log(" No decisions recorded yet.");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function search(query: string) {
|
|
334
|
+
if (!query) {
|
|
335
|
+
console.log("Usage: autoctxd search <query>");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
console.log(`\nSearching for "${query}"...`);
|
|
340
|
+
console.log("─".repeat(80));
|
|
341
|
+
|
|
342
|
+
const results = await hybridSearch(query, { limit: 15 });
|
|
343
|
+
|
|
344
|
+
for (const r of results) {
|
|
345
|
+
const icon = r.source === "vector" ? "~" : r.source === "fts_decisions" ? "!" : ">";
|
|
346
|
+
console.log(` [${icon}] ${r.text.slice(0, 120)}`);
|
|
347
|
+
if (r.metadata.project_path) console.log(` Project: ${r.metadata.project_path}`);
|
|
348
|
+
if (r.metadata.created_at) console.log(` Date: ${r.metadata.created_at}`);
|
|
349
|
+
if (r.metadata.type) console.log(` Type: ${r.metadata.type} | Score: ${r.metadata.importance}`);
|
|
350
|
+
console.log("");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (results.length === 0) {
|
|
354
|
+
console.log(" No results found.");
|
|
355
|
+
} else {
|
|
356
|
+
console.log(` Legend: [~] semantic match [!] decision match [>] text match`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function show(type: string, id: string) {
|
|
361
|
+
if (type !== "session" || !id) {
|
|
362
|
+
console.log("Usage: autoctxd show session <id>");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const db = getDb();
|
|
367
|
+
const session = db.prepare("SELECT * FROM sessions WHERE session_id LIKE ?").get(`${id}%`) as any;
|
|
368
|
+
if (!session) {
|
|
369
|
+
console.log("Session not found.");
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
console.log(`\n╔══════════════════════════════════════╗`);
|
|
374
|
+
console.log(`║ Session: ${session.session_id.slice(0, 26)} ║`);
|
|
375
|
+
console.log(`╚══════════════════════════════════════╝`);
|
|
376
|
+
console.log(`Project: ${session.project_path}`);
|
|
377
|
+
console.log(`Branch: ${session.git_branch || "n/a"}`);
|
|
378
|
+
console.log(`Started: ${session.started_at}`);
|
|
379
|
+
console.log(`Ended: ${session.ended_at || "still active"}`);
|
|
380
|
+
console.log(`Total obs: ${session.total_observations || 0}`);
|
|
381
|
+
|
|
382
|
+
const obs = db.prepare(`
|
|
383
|
+
SELECT * FROM observations WHERE session_id = ? ORDER BY timestamp ASC
|
|
384
|
+
`).all(session.session_id) as any[];
|
|
385
|
+
|
|
386
|
+
if (obs.length > 0) {
|
|
387
|
+
console.log("\nTimeline:");
|
|
388
|
+
console.log("─".repeat(80));
|
|
389
|
+
for (const o of obs) {
|
|
390
|
+
const time = o.timestamp.split(" ")[1] || o.timestamp;
|
|
391
|
+
const imp = "●".repeat(Math.min(5, Math.ceil(o.importance_score / 2)));
|
|
392
|
+
console.log(` ${time} [${o.type.padEnd(12)}] ${imp} ${o.summary.slice(0, 80)}`);
|
|
393
|
+
if (o.file_paths) {
|
|
394
|
+
console.log(`${"".padEnd(12)}files: ${o.file_paths}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const summaries = db.prepare(`SELECT * FROM summaries WHERE session_id = ?`).all(session.session_id) as any[];
|
|
400
|
+
if (summaries.length > 0) {
|
|
401
|
+
console.log("\nSession Summary:");
|
|
402
|
+
console.log("─".repeat(80));
|
|
403
|
+
console.log(summaries[0].text);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function patterns(project?: string) {
|
|
408
|
+
const db = getDb();
|
|
409
|
+
const query = project
|
|
410
|
+
? db.prepare("SELECT * FROM patterns WHERE project_path LIKE ? ORDER BY frequency DESC").all(`%${project}%`)
|
|
411
|
+
: db.prepare("SELECT * FROM patterns ORDER BY frequency DESC LIMIT 20").all();
|
|
412
|
+
|
|
413
|
+
console.log("\nDetected Patterns:");
|
|
414
|
+
console.log("─".repeat(80));
|
|
415
|
+
for (const p of query as any[]) {
|
|
416
|
+
const freq = "█".repeat(Math.min(10, p.frequency));
|
|
417
|
+
console.log(` [${p.pattern_type}] ${p.description}`);
|
|
418
|
+
console.log(` Frequency: ${freq} (${p.frequency}x) | Last: ${p.last_seen}`);
|
|
419
|
+
if (p.examples) console.log(` Examples: ${p.examples.slice(0, 100)}`);
|
|
420
|
+
console.log("");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if ((query as any[]).length === 0) {
|
|
424
|
+
console.log(" No patterns detected yet. Patterns emerge after multiple sessions.");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function digest(project?: string) {
|
|
429
|
+
const db = getDb();
|
|
430
|
+
const query = project
|
|
431
|
+
? db.prepare("SELECT * FROM summaries WHERE level = 2 AND project_path LIKE ? ORDER BY created_at DESC LIMIT 5").all(`%${project}%`)
|
|
432
|
+
: db.prepare("SELECT * FROM summaries WHERE level = 2 ORDER BY created_at DESC LIMIT 5").all();
|
|
433
|
+
|
|
434
|
+
console.log("\nWeekly Digests:");
|
|
435
|
+
console.log("─".repeat(80));
|
|
436
|
+
for (const d of query as any[]) {
|
|
437
|
+
console.log(` [${d.created_at}] Project: ${d.project_path || "all"}`);
|
|
438
|
+
console.log(d.text.split("\n").map((l: string) => ` ${l}`).join("\n"));
|
|
439
|
+
console.log("");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if ((query as any[]).length === 0) {
|
|
443
|
+
console.log(" No digests yet. Digests are auto-generated weekly when sessions accumulate.");
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function exportContext(project?: string) {
|
|
448
|
+
const db = getDb();
|
|
449
|
+
const projectFilter = project || "%";
|
|
450
|
+
const isFilter = project ? true : false;
|
|
451
|
+
|
|
452
|
+
const lines: string[] = [];
|
|
453
|
+
lines.push("# Claude-CTX Context Export");
|
|
454
|
+
lines.push(`Generated: ${new Date().toISOString()}`);
|
|
455
|
+
if (project) lines.push(`Project filter: ${project}`);
|
|
456
|
+
lines.push("");
|
|
457
|
+
|
|
458
|
+
// Decisions
|
|
459
|
+
const decs = (isFilter
|
|
460
|
+
? db.prepare("SELECT * FROM decisions WHERE project_path LIKE ? ORDER BY created_at DESC").all(`%${projectFilter}%`)
|
|
461
|
+
: db.prepare("SELECT * FROM decisions ORDER BY created_at DESC LIMIT 20").all()
|
|
462
|
+
) as any[];
|
|
463
|
+
|
|
464
|
+
if (decs.length > 0) {
|
|
465
|
+
lines.push("## Architectural Decisions");
|
|
466
|
+
lines.push("");
|
|
467
|
+
for (const d of decs) {
|
|
468
|
+
lines.push(`### ${d.title}`);
|
|
469
|
+
lines.push(`- **Date:** ${d.created_at}`);
|
|
470
|
+
lines.push(`- **Decision:** ${d.decision_text}`);
|
|
471
|
+
if (d.alternatives) lines.push(`- **Rejected:** ${d.alternatives}`);
|
|
472
|
+
if (d.rationale) lines.push(`- **Reason:** ${d.rationale}`);
|
|
473
|
+
if (d.files_affected) lines.push(`- **Files:** ${d.files_affected}`);
|
|
474
|
+
lines.push("");
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Recent summaries
|
|
479
|
+
const sums = (isFilter
|
|
480
|
+
? db.prepare("SELECT * FROM summaries WHERE project_path LIKE ? AND level = 1 ORDER BY created_at DESC LIMIT 10").all(`%${projectFilter}%`)
|
|
481
|
+
: db.prepare("SELECT * FROM summaries WHERE level = 1 ORDER BY created_at DESC LIMIT 10").all()
|
|
482
|
+
) as any[];
|
|
483
|
+
|
|
484
|
+
if (sums.length > 0) {
|
|
485
|
+
lines.push("## Recent Session Summaries");
|
|
486
|
+
lines.push("");
|
|
487
|
+
for (const s of sums) {
|
|
488
|
+
lines.push(`### Session ${s.session_id || "unknown"} (${s.created_at})`);
|
|
489
|
+
lines.push("```");
|
|
490
|
+
lines.push(s.text);
|
|
491
|
+
lines.push("```");
|
|
492
|
+
lines.push("");
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Patterns
|
|
497
|
+
const pats = (isFilter
|
|
498
|
+
? db.prepare("SELECT * FROM patterns WHERE project_path LIKE ? ORDER BY frequency DESC").all(`%${projectFilter}%`)
|
|
499
|
+
: db.prepare("SELECT * FROM patterns ORDER BY frequency DESC LIMIT 10").all()
|
|
500
|
+
) as any[];
|
|
501
|
+
|
|
502
|
+
if (pats.length > 0) {
|
|
503
|
+
lines.push("## Detected Patterns");
|
|
504
|
+
lines.push("");
|
|
505
|
+
for (const p of pats as any[]) {
|
|
506
|
+
lines.push(`- **[${p.pattern_type}]** ${p.description} (seen ${p.frequency}x)`);
|
|
507
|
+
}
|
|
508
|
+
lines.push("");
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Stats
|
|
512
|
+
const sessionCount = (db.prepare("SELECT COUNT(*) as c FROM sessions").get() as any).c;
|
|
513
|
+
const obsCount = (db.prepare("SELECT COUNT(*) as c FROM observations").get() as any).c;
|
|
514
|
+
|
|
515
|
+
lines.push("## Statistics");
|
|
516
|
+
lines.push(`- Total sessions: ${sessionCount}`);
|
|
517
|
+
lines.push(`- Total observations: ${obsCount}`);
|
|
518
|
+
lines.push(`- Decisions: ${decs.length}`);
|
|
519
|
+
lines.push(`- Patterns: ${pats.length}`);
|
|
520
|
+
lines.push("");
|
|
521
|
+
|
|
522
|
+
console.log(lines.join("\n"));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function showFeedbackStats() {
|
|
526
|
+
const s = getFeedbackStats();
|
|
527
|
+
console.log(`
|
|
528
|
+
╔══════════════════════════════════════╗
|
|
529
|
+
║ autoctxd Learning Stats ║
|
|
530
|
+
╚══════════════════════════════════════╝
|
|
531
|
+
|
|
532
|
+
Total feedback entries: ${s.total}
|
|
533
|
+
Useful: ${s.useful}
|
|
534
|
+
Irrelevant: ${s.irrelevant}
|
|
535
|
+
Wrong: ${s.wrong}
|
|
536
|
+
`);
|
|
537
|
+
if (s.total === 0) {
|
|
538
|
+
console.log(` No feedback yet. autoctxd learns as you (or Claude) call
|
|
539
|
+
the record_feedback MCP tool to rate injected items.
|
|
540
|
+
`);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
console.log("By target type:");
|
|
544
|
+
for (const [type, counts] of Object.entries(s.byType)) {
|
|
545
|
+
console.log(` ${type.padEnd(14)} useful:${counts.useful} irrelevant:${counts.irrelevant} wrong:${counts.wrong}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function showMetrics() {
|
|
550
|
+
const m = getGlobalMetrics();
|
|
551
|
+
console.log(`
|
|
552
|
+
╔══════════════════════════════════════╗
|
|
553
|
+
║ autoctxd Token Metrics ║
|
|
554
|
+
╚══════════════════════════════════════╝
|
|
555
|
+
|
|
556
|
+
Sessions with injected context: ${m.sessionsWithContext}
|
|
557
|
+
Total tokens injected: ${m.totalInjected.toLocaleString()}
|
|
558
|
+
Estimated tokens saved: ~${m.totalSaved.toLocaleString()}
|
|
559
|
+
Avg tokens per session: ${m.avgInjectedPerSession}
|
|
560
|
+
|
|
561
|
+
ROI: for every injected token, you avoid re-explaining ~${
|
|
562
|
+
m.totalInjected > 0 ? (m.totalSaved / m.totalInjected).toFixed(2) : "0"
|
|
563
|
+
} tokens of context.
|
|
564
|
+
`);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function truncPath(path: string, maxLen: number): string {
|
|
568
|
+
if (path.length <= maxLen) return path;
|
|
569
|
+
const parts = path.split(/[/\\]/);
|
|
570
|
+
if (parts.length <= 2) return "..." + path.slice(-(maxLen - 3));
|
|
571
|
+
return "..." + parts.slice(-2).join("/");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
main();
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Re-runs classifier on existing observations to clean up historical "other" entries.
|
|
2
|
+
|
|
3
|
+
import { getDb } from "../db/sqlite/schema";
|
|
4
|
+
import { classifyObservation } from "../ai/classifier";
|
|
5
|
+
import { compressSession } from "../ai/compressor";
|
|
6
|
+
import { insertSummary } from "../db/sqlite/summaries";
|
|
7
|
+
import { getObservationsBySession } from "../db/sqlite/observations";
|
|
8
|
+
|
|
9
|
+
interface StoredObs {
|
|
10
|
+
id: number;
|
|
11
|
+
tool_name: string | null;
|
|
12
|
+
summary: string;
|
|
13
|
+
file_paths: string | null;
|
|
14
|
+
type: string;
|
|
15
|
+
importance_score: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function reclassifyAll(options: { onlyOther?: boolean; dryRun?: boolean } = {}): {
|
|
19
|
+
scanned: number;
|
|
20
|
+
updated: number;
|
|
21
|
+
changes: Map<string, number>;
|
|
22
|
+
} {
|
|
23
|
+
const { onlyOther = false, dryRun = false } = options;
|
|
24
|
+
const db = getDb();
|
|
25
|
+
|
|
26
|
+
const rows = (onlyOther
|
|
27
|
+
? db.prepare("SELECT id, tool_name, summary, file_paths, type, importance_score FROM observations WHERE type = 'other'").all()
|
|
28
|
+
: db.prepare("SELECT id, tool_name, summary, file_paths, type, importance_score FROM observations").all()
|
|
29
|
+
) as StoredObs[];
|
|
30
|
+
|
|
31
|
+
const update = db.prepare("UPDATE observations SET type = ?, importance_score = ? WHERE id = ?");
|
|
32
|
+
const changes = new Map<string, number>();
|
|
33
|
+
let updated = 0;
|
|
34
|
+
|
|
35
|
+
for (const row of rows) {
|
|
36
|
+
const filePaths = row.file_paths ? row.file_paths.split(",").map(s => s.trim()).filter(Boolean) : [];
|
|
37
|
+
const result = classifyObservation(row.tool_name || "", row.summary, filePaths);
|
|
38
|
+
|
|
39
|
+
if (result.type !== row.type || result.importance !== row.importance_score) {
|
|
40
|
+
const key = `${row.type} → ${result.type}`;
|
|
41
|
+
changes.set(key, (changes.get(key) || 0) + 1);
|
|
42
|
+
|
|
43
|
+
if (!dryRun) {
|
|
44
|
+
update.run(result.type, result.importance, row.id);
|
|
45
|
+
}
|
|
46
|
+
updated++;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { scanned: rows.length, updated, changes };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RecompressResult {
|
|
54
|
+
sessionsScanned: number;
|
|
55
|
+
summariesRewritten: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function recompressAll(options: { dryRun?: boolean; project?: string } = {}): RecompressResult {
|
|
59
|
+
const { dryRun = false, project } = options;
|
|
60
|
+
const db = getDb();
|
|
61
|
+
|
|
62
|
+
// Pick sessions that already have a Level-1 summary — those are the rows that
|
|
63
|
+
// show up in "RECENT SESSIONS" in the injected context block.
|
|
64
|
+
const rows = (project
|
|
65
|
+
? db.prepare(`
|
|
66
|
+
SELECT DISTINCT s.session_id, s.project_path
|
|
67
|
+
FROM summaries s
|
|
68
|
+
WHERE s.level = 1 AND s.session_id IS NOT NULL
|
|
69
|
+
AND s.project_path LIKE ?
|
|
70
|
+
`).all(`%${project}%`)
|
|
71
|
+
: db.prepare(`
|
|
72
|
+
SELECT DISTINCT session_id, project_path FROM summaries
|
|
73
|
+
WHERE level = 1 AND session_id IS NOT NULL
|
|
74
|
+
`).all()
|
|
75
|
+
) as Array<{ session_id: string; project_path: string | null }>;
|
|
76
|
+
|
|
77
|
+
let rewritten = 0;
|
|
78
|
+
for (const row of rows) {
|
|
79
|
+
const observations = getObservationsBySession(row.session_id);
|
|
80
|
+
if (observations.length === 0) continue;
|
|
81
|
+
|
|
82
|
+
const summary = compressSession(observations, row.project_path || undefined);
|
|
83
|
+
if (!dryRun) {
|
|
84
|
+
// insertSummary upserts on (session_id, level=1) — see summaries.ts
|
|
85
|
+
insertSummary({
|
|
86
|
+
session_id: row.session_id,
|
|
87
|
+
level: 1,
|
|
88
|
+
text: summary.text,
|
|
89
|
+
project_path: row.project_path || undefined,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
rewritten++;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { sessionsScanned: rows.length, summariesRewritten: rewritten };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function runReclassifyCli(args: string[]): void {
|
|
99
|
+
const dryRun = args.includes("--dry-run");
|
|
100
|
+
const recompress = args.includes("--recompress");
|
|
101
|
+
const onlyOther = args.includes("--only-other") || !args.includes("--all");
|
|
102
|
+
const projectIdx = args.indexOf("--project");
|
|
103
|
+
const project = projectIdx >= 0 ? args[projectIdx + 1] : undefined;
|
|
104
|
+
|
|
105
|
+
console.log(`\nReclassifying observations...`);
|
|
106
|
+
console.log(` Mode: ${onlyOther ? "only 'other' type" : "all observations"}${dryRun ? " (DRY RUN)" : ""}`);
|
|
107
|
+
console.log("─".repeat(60));
|
|
108
|
+
|
|
109
|
+
const { scanned, updated, changes } = reclassifyAll({ onlyOther, dryRun });
|
|
110
|
+
|
|
111
|
+
console.log(`\n Scanned: ${scanned}`);
|
|
112
|
+
console.log(` Updated: ${updated}${dryRun ? " (not applied)" : ""}`);
|
|
113
|
+
|
|
114
|
+
if (changes.size > 0) {
|
|
115
|
+
console.log(`\n Type transitions:`);
|
|
116
|
+
const sorted = [...changes.entries()].sort((a, b) => b[1] - a[1]);
|
|
117
|
+
for (const [transition, count] of sorted) {
|
|
118
|
+
console.log(` ${transition} (${count}x)`);
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
console.log(`\n No changes needed — classifier agrees with stored types.`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (recompress) {
|
|
125
|
+
console.log(`\nRecompressing historical session summaries${project ? ` for project matching "${project}"` : ""}...`);
|
|
126
|
+
console.log("─".repeat(60));
|
|
127
|
+
const r = recompressAll({ dryRun, project });
|
|
128
|
+
console.log(` Sessions scanned: ${r.sessionsScanned}`);
|
|
129
|
+
console.log(` Summaries rewritten: ${r.summariesRewritten}${dryRun ? " (not applied)" : ""}`);
|
|
130
|
+
if (r.summariesRewritten === 0 && r.sessionsScanned === 0) {
|
|
131
|
+
console.log(` Nothing to rewrite — no level-1 summaries found.`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|