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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Doctor: verifies install integrity, prints fixable diagnostics
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
4
|
+
import { join, resolve } from "path";
|
|
5
|
+
import { getDb, closeDb } from "../db/sqlite/schema";
|
|
6
|
+
import { getGlobalMetrics } from "../util/metrics";
|
|
7
|
+
|
|
8
|
+
const CTX_ROOT = resolve(join(import.meta.dir, "..", ".."));
|
|
9
|
+
const CLAUDE_DIR = resolve(join(CTX_ROOT, ".."));
|
|
10
|
+
const SETTINGS = join(CLAUDE_DIR, "settings.json");
|
|
11
|
+
|
|
12
|
+
interface Check {
|
|
13
|
+
name: string;
|
|
14
|
+
status: "ok" | "warn" | "fail";
|
|
15
|
+
detail: string;
|
|
16
|
+
fix?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function runDoctor(): Promise<number> {
|
|
20
|
+
const checks: Check[] = [];
|
|
21
|
+
|
|
22
|
+
// 1. Bun available
|
|
23
|
+
try {
|
|
24
|
+
const proc = Bun.spawnSync(["bun", "--version"], { stdout: "pipe" });
|
|
25
|
+
if (proc.success) {
|
|
26
|
+
checks.push({ name: "Bun runtime", status: "ok", detail: proc.stdout.toString().trim() });
|
|
27
|
+
} else {
|
|
28
|
+
checks.push({ name: "Bun runtime", status: "fail", detail: "bun command failed" });
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
checks.push({
|
|
32
|
+
name: "Bun runtime",
|
|
33
|
+
status: "fail",
|
|
34
|
+
detail: "Bun not on PATH",
|
|
35
|
+
fix: "Install Bun from https://bun.sh",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. node_modules present
|
|
40
|
+
checks.push(
|
|
41
|
+
existsSync(join(CTX_ROOT, "node_modules"))
|
|
42
|
+
? { name: "Dependencies", status: "ok", detail: "node_modules exists" }
|
|
43
|
+
: { name: "Dependencies", status: "fail", detail: "node_modules missing", fix: "Run: bun install" }
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// 3. SQLite DB accessible
|
|
47
|
+
try {
|
|
48
|
+
const db = getDb();
|
|
49
|
+
const tables = db.prepare("SELECT COUNT(*) as c FROM sqlite_master WHERE type='table'").get() as any;
|
|
50
|
+
checks.push({
|
|
51
|
+
name: "SQLite database",
|
|
52
|
+
status: "ok",
|
|
53
|
+
detail: `${tables.c} tables initialized`,
|
|
54
|
+
});
|
|
55
|
+
closeDb();
|
|
56
|
+
} catch (e) {
|
|
57
|
+
checks.push({
|
|
58
|
+
name: "SQLite database",
|
|
59
|
+
status: "fail",
|
|
60
|
+
detail: `Cannot open DB: ${e}`,
|
|
61
|
+
fix: "Run: bun run src/cli/index.ts init",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 4. LanceDB module loadable
|
|
66
|
+
try {
|
|
67
|
+
await import("@lancedb/lancedb");
|
|
68
|
+
checks.push({ name: "LanceDB module", status: "ok", detail: "loaded" });
|
|
69
|
+
} catch (e) {
|
|
70
|
+
checks.push({
|
|
71
|
+
name: "LanceDB module",
|
|
72
|
+
status: "warn",
|
|
73
|
+
detail: `module load failed: ${String(e).slice(0, 80)}`,
|
|
74
|
+
fix: "Vector search will be disabled. Re-run: bun install",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 5. Hooks registered in settings.json
|
|
79
|
+
if (!existsSync(SETTINGS)) {
|
|
80
|
+
checks.push({
|
|
81
|
+
name: "Hooks registration",
|
|
82
|
+
status: "fail",
|
|
83
|
+
detail: `${SETTINGS} missing`,
|
|
84
|
+
fix: "Create settings.json — run: bun run scripts/install-hooks.ts",
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
try {
|
|
88
|
+
const raw = readFileSync(SETTINGS, "utf8");
|
|
89
|
+
const cfg = JSON.parse(raw);
|
|
90
|
+
const hookNames = ["PreToolUse", "PostToolUse", "Stop"];
|
|
91
|
+
const missing: string[] = [];
|
|
92
|
+
for (const name of hookNames) {
|
|
93
|
+
const hooks = cfg?.hooks?.[name] || [];
|
|
94
|
+
const found = JSON.stringify(hooks).includes("autoctxd");
|
|
95
|
+
if (!found) missing.push(name);
|
|
96
|
+
}
|
|
97
|
+
if (missing.length === 0) {
|
|
98
|
+
checks.push({
|
|
99
|
+
name: "Hooks registration",
|
|
100
|
+
status: "ok",
|
|
101
|
+
detail: "all 3 hooks registered",
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
checks.push({
|
|
105
|
+
name: "Hooks registration",
|
|
106
|
+
status: "warn",
|
|
107
|
+
detail: `missing: ${missing.join(", ")}`,
|
|
108
|
+
fix: "Run: bun run scripts/install-hooks.ts",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
checks.push({
|
|
113
|
+
name: "Hooks registration",
|
|
114
|
+
status: "warn",
|
|
115
|
+
detail: `Cannot parse settings.json: ${e}`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 6. Data directory writable
|
|
121
|
+
const dataDir = process.env.AUTOCTXD_DATA_DIR || join(CTX_ROOT, "data");
|
|
122
|
+
try {
|
|
123
|
+
const probe = join(dataDir, ".doctor-probe");
|
|
124
|
+
writeFileSync(probe, "x");
|
|
125
|
+
try { unlinkSync(probe); } catch {}
|
|
126
|
+
checks.push({ name: "Data directory", status: "ok", detail: dataDir });
|
|
127
|
+
} catch (e) {
|
|
128
|
+
checks.push({
|
|
129
|
+
name: "Data directory",
|
|
130
|
+
status: "fail",
|
|
131
|
+
detail: `${dataDir} not writable: ${e}`,
|
|
132
|
+
fix: "Check directory permissions",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 7. Metrics summary (informational)
|
|
137
|
+
try {
|
|
138
|
+
const m = getGlobalMetrics();
|
|
139
|
+
checks.push({
|
|
140
|
+
name: "Accumulated metrics",
|
|
141
|
+
status: "ok",
|
|
142
|
+
detail: `${m.sessionsWithContext} sessions · ${m.totalInjected} tokens injected · ~${m.totalSaved} tokens saved`,
|
|
143
|
+
});
|
|
144
|
+
closeDb();
|
|
145
|
+
} catch {}
|
|
146
|
+
|
|
147
|
+
// Print
|
|
148
|
+
console.log("\n╔══════════════════════════════════════╗");
|
|
149
|
+
console.log("║ autoctxd doctor ║");
|
|
150
|
+
console.log("╚══════════════════════════════════════╝\n");
|
|
151
|
+
|
|
152
|
+
let fails = 0;
|
|
153
|
+
for (const c of checks) {
|
|
154
|
+
const icon = c.status === "ok" ? "✓" : c.status === "warn" ? "!" : "✗";
|
|
155
|
+
const pad = c.name.padEnd(22);
|
|
156
|
+
console.log(` [${icon}] ${pad} ${c.detail}`);
|
|
157
|
+
if (c.fix) {
|
|
158
|
+
console.log(` → ${c.fix}`);
|
|
159
|
+
}
|
|
160
|
+
if (c.status === "fail") fails++;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log("");
|
|
164
|
+
if (fails === 0) {
|
|
165
|
+
console.log(" All checks passed. autoctxd is healthy.\n");
|
|
166
|
+
return 0;
|
|
167
|
+
} else {
|
|
168
|
+
console.log(` ${fails} check(s) failed. Run the suggested fixes above.\n`);
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// `autoctxd embeddings` subcommands.
|
|
2
|
+
//
|
|
3
|
+
// embeddings list show all providers and their availability
|
|
4
|
+
// embeddings status show the active provider + diagnostics
|
|
5
|
+
// embeddings switch <name> [--yes] change the active provider; rebuilds the
|
|
6
|
+
// vector table by re-embedding every summary
|
|
7
|
+
// and decision in SQLite
|
|
8
|
+
// embeddings reembed [--yes] force-rebuild the vector table without
|
|
9
|
+
// changing the provider (use after upgrading
|
|
10
|
+
// the underlying model)
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
ALL_PROVIDER_NAMES,
|
|
14
|
+
buildProvider,
|
|
15
|
+
getConfiguredProviderName,
|
|
16
|
+
setProvider,
|
|
17
|
+
} from "../db/vector/providers/factory";
|
|
18
|
+
import type { EmbeddingProviderName } from "../db/vector/providers/types";
|
|
19
|
+
import { getDb } from "../db/sqlite/schema";
|
|
20
|
+
import { addVector, dropTable } from "../db/vector/client";
|
|
21
|
+
import { generateEmbedding, clearEmbeddingCache } from "../db/vector/embeddings";
|
|
22
|
+
|
|
23
|
+
export async function runEmbeddingsCommand(args: string[]): Promise<number> {
|
|
24
|
+
const sub = args[0];
|
|
25
|
+
switch (sub) {
|
|
26
|
+
case "list":
|
|
27
|
+
return await listProviders();
|
|
28
|
+
case "status":
|
|
29
|
+
return await showStatus();
|
|
30
|
+
case "switch":
|
|
31
|
+
return await switchProvider(args[1], args.includes("--yes"));
|
|
32
|
+
case "reembed":
|
|
33
|
+
return await reembedAll(args.includes("--yes"));
|
|
34
|
+
default:
|
|
35
|
+
printHelp();
|
|
36
|
+
return sub ? 1 : 0;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function printHelp() {
|
|
41
|
+
console.log(`
|
|
42
|
+
autoctxd embeddings — manage the embedding provider
|
|
43
|
+
|
|
44
|
+
list list all providers with availability checks
|
|
45
|
+
status show the active provider and diagnostics
|
|
46
|
+
switch <name> [--yes] change provider (tfidf | minilm | ollama)
|
|
47
|
+
re-embeds all stored summaries automatically
|
|
48
|
+
reembed [--yes] re-embed everything without changing provider
|
|
49
|
+
(run after upgrading the underlying model)
|
|
50
|
+
|
|
51
|
+
Environment overrides:
|
|
52
|
+
AUTOCTXD_EMBEDDING provider name (overrides config file)
|
|
53
|
+
AUTOCTXD_OLLAMA_URL default http://localhost:11434
|
|
54
|
+
AUTOCTXD_OLLAMA_MODEL default nomic-embed-text
|
|
55
|
+
AUTOCTXD_MODEL_DIR where transformers.js caches MiniLM (default ~/.claude/autoctxd/models)
|
|
56
|
+
`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function listProviders(): Promise<number> {
|
|
60
|
+
const active = getConfiguredProviderName();
|
|
61
|
+
console.log("");
|
|
62
|
+
for (const name of ALL_PROVIDER_NAMES) {
|
|
63
|
+
const p = buildProvider(name);
|
|
64
|
+
const check = await p.check();
|
|
65
|
+
const marker = name === active ? "→" : " ";
|
|
66
|
+
const status = check.ok ? "ready" : "unavailable";
|
|
67
|
+
console.log(` ${marker} ${name.padEnd(8)} ${status.padEnd(12)} ${p.label}`);
|
|
68
|
+
if (!check.ok) {
|
|
69
|
+
console.log(` └── ${check.reason}`);
|
|
70
|
+
}
|
|
71
|
+
if (p.dispose) await p.dispose().catch(() => {});
|
|
72
|
+
}
|
|
73
|
+
console.log("");
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function showStatus(): Promise<number> {
|
|
78
|
+
const active = getConfiguredProviderName();
|
|
79
|
+
const p = buildProvider(active);
|
|
80
|
+
const check = await p.check();
|
|
81
|
+
|
|
82
|
+
console.log("");
|
|
83
|
+
console.log(` Active provider: ${active}`);
|
|
84
|
+
console.log(` Label: ${p.label}`);
|
|
85
|
+
console.log(` Dim: ${p.dim}`);
|
|
86
|
+
console.log(` Status: ${check.ok ? "ready" : "unavailable — " + check.reason}`);
|
|
87
|
+
|
|
88
|
+
// Cache stats
|
|
89
|
+
try {
|
|
90
|
+
const db = getDb();
|
|
91
|
+
const rows = db
|
|
92
|
+
.prepare("SELECT provider, COUNT(*) as c FROM embeddings_cache GROUP BY provider")
|
|
93
|
+
.all() as Array<{ provider: string; c: number }>;
|
|
94
|
+
if (rows.length > 0) {
|
|
95
|
+
console.log(` Cache rows by provider:`);
|
|
96
|
+
for (const r of rows) console.log(` ${r.provider.padEnd(8)} ${r.c}`);
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// ignore
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log("");
|
|
103
|
+
if (p.dispose) await p.dispose().catch(() => {});
|
|
104
|
+
return check.ok ? 0 : 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function switchProvider(target: string | undefined, yes: boolean): Promise<number> {
|
|
108
|
+
if (!target || !ALL_PROVIDER_NAMES.includes(target as EmbeddingProviderName)) {
|
|
109
|
+
console.error(`error: provider name required (one of: ${ALL_PROVIDER_NAMES.join(", ")})`);
|
|
110
|
+
return 2;
|
|
111
|
+
}
|
|
112
|
+
const name = target as EmbeddingProviderName;
|
|
113
|
+
|
|
114
|
+
// Verify the target provider is actually usable before we clear data
|
|
115
|
+
const candidate = buildProvider(name);
|
|
116
|
+
const check = await candidate.check();
|
|
117
|
+
if (!check.ok) {
|
|
118
|
+
console.error(`error: provider '${name}' is not available — ${check.reason}`);
|
|
119
|
+
if (candidate.dispose) await candidate.dispose().catch(() => {});
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
if (candidate.dispose) await candidate.dispose().catch(() => {});
|
|
123
|
+
|
|
124
|
+
const current = getConfiguredProviderName();
|
|
125
|
+
if (current === name) {
|
|
126
|
+
console.log(`Already using ${name}.`);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!yes) {
|
|
131
|
+
console.log("");
|
|
132
|
+
console.log(`This will switch from '${current}' to '${name}' and rebuild the vector index.`);
|
|
133
|
+
console.log("Existing summaries are preserved; only the embeddings are recomputed.");
|
|
134
|
+
console.log("Re-run with --yes to confirm.");
|
|
135
|
+
console.log("");
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log(`Switching ${current} → ${name} ...`);
|
|
140
|
+
await setProvider(name);
|
|
141
|
+
return await reembedAll(true);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function reembedAll(yes: boolean): Promise<number> {
|
|
145
|
+
const active = getConfiguredProviderName();
|
|
146
|
+
const provider = buildProvider(active);
|
|
147
|
+
const check = await provider.check();
|
|
148
|
+
if (!check.ok) {
|
|
149
|
+
console.error(`error: active provider '${active}' is not available — ${check.reason}`);
|
|
150
|
+
if (provider.dispose) await provider.dispose().catch(() => {});
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
if (provider.dispose) await provider.dispose().catch(() => {});
|
|
154
|
+
|
|
155
|
+
if (!yes) {
|
|
156
|
+
console.log("");
|
|
157
|
+
console.log(`This will rebuild the vector index using provider '${active}'.`);
|
|
158
|
+
console.log("Re-run with --yes to confirm.");
|
|
159
|
+
console.log("");
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const db = getDb();
|
|
164
|
+
const summaries = db
|
|
165
|
+
.prepare("SELECT id, session_id, level, text, project_path, created_at FROM summaries ORDER BY id ASC")
|
|
166
|
+
.all() as Array<{ id: number; session_id: string | null; level: number; text: string; project_path: string | null; created_at: string }>;
|
|
167
|
+
const decisions = db
|
|
168
|
+
.prepare("SELECT id, project_path, title, decision_text, created_at FROM decisions ORDER BY id ASC")
|
|
169
|
+
.all() as Array<{ id: number; project_path: string | null; title: string; decision_text: string; created_at: string }>;
|
|
170
|
+
|
|
171
|
+
console.log(`Dropping vector table and re-embedding ${summaries.length} summaries + ${decisions.length} decisions...`);
|
|
172
|
+
await dropTable();
|
|
173
|
+
clearEmbeddingCache();
|
|
174
|
+
|
|
175
|
+
let n = 0;
|
|
176
|
+
for (const s of summaries) {
|
|
177
|
+
const v = await generateEmbedding(s.text);
|
|
178
|
+
await addVector({
|
|
179
|
+
id: `summary-${s.id}`,
|
|
180
|
+
session_id: s.session_id || "",
|
|
181
|
+
project_path: s.project_path || "",
|
|
182
|
+
text: s.text,
|
|
183
|
+
level: s.level,
|
|
184
|
+
created_at: s.created_at,
|
|
185
|
+
vector: Array.from(v),
|
|
186
|
+
});
|
|
187
|
+
n++;
|
|
188
|
+
if (n % 25 === 0) process.stdout.write(` ${n}/${summaries.length + decisions.length}\r`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const d of decisions) {
|
|
192
|
+
const v = await generateEmbedding(`${d.title} ${d.decision_text}`);
|
|
193
|
+
await addVector({
|
|
194
|
+
id: `decision-${d.id}`,
|
|
195
|
+
session_id: "",
|
|
196
|
+
project_path: d.project_path || "",
|
|
197
|
+
text: `DECISION: ${d.title} — ${d.decision_text}`,
|
|
198
|
+
level: 9,
|
|
199
|
+
created_at: d.created_at,
|
|
200
|
+
vector: Array.from(v),
|
|
201
|
+
});
|
|
202
|
+
n++;
|
|
203
|
+
if (n % 25 === 0) process.stdout.write(` ${n}/${summaries.length + decisions.length}\r`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(` ${n}/${summaries.length + decisions.length}`);
|
|
207
|
+
console.log(`Done. Provider '${active}' is now active with ${n} re-embedded items.`);
|
|
208
|
+
return 0;
|
|
209
|
+
}
|