@a13xu/lucid 1.0.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +117 -99
- package/build/database.d.ts +19 -0
- package/build/database.js +91 -62
- package/build/guardian/checklist.d.ts +1 -0
- package/build/guardian/checklist.js +67 -0
- package/build/guardian/validator.d.ts +21 -0
- package/build/guardian/validator.js +332 -0
- package/build/index.js +142 -32
- package/build/indexer/file.d.ts +15 -0
- package/build/indexer/file.js +100 -0
- package/build/indexer/project.d.ts +8 -0
- package/build/indexer/project.js +312 -0
- package/build/store/content.d.ts +3 -0
- package/build/store/content.js +11 -0
- package/build/tools/grep.d.ts +17 -0
- package/build/tools/grep.js +65 -0
- package/build/tools/guardian.d.ts +21 -0
- package/build/tools/guardian.js +58 -0
- package/build/tools/init.d.ts +11 -0
- package/build/tools/init.js +110 -0
- package/build/tools/sync.d.ts +18 -0
- package/build/tools/sync.js +61 -0
- package/package.json +48 -48
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { extname } from "path";
|
|
3
|
+
const SEVERITY_ORDER = {
|
|
4
|
+
critical: 0, high: 1, medium: 2, low: 3, info: 4,
|
|
5
|
+
};
|
|
6
|
+
const SEVERITY_ICON = {
|
|
7
|
+
critical: "🔴", high: "🟠", medium: "🟡", low: "🔵", info: "ℹ️",
|
|
8
|
+
};
|
|
9
|
+
export function formatIssue(issue) {
|
|
10
|
+
const icon = SEVERITY_ICON[issue.severity];
|
|
11
|
+
let s = `${icon} [${issue.driftId}] ${issue.file}:${issue.line} — ${issue.message}`;
|
|
12
|
+
if (issue.suggestion)
|
|
13
|
+
s += `\n 💡 ${issue.suggestion}`;
|
|
14
|
+
if (issue.snippet)
|
|
15
|
+
s += `\n 📄 ${issue.snippet.trim().slice(0, 80)}`;
|
|
16
|
+
return s;
|
|
17
|
+
}
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Language detection
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const LANG_MAP = {
|
|
22
|
+
".py": "python",
|
|
23
|
+
".js": "javascript",
|
|
24
|
+
".jsx": "javascript",
|
|
25
|
+
".ts": "typescript",
|
|
26
|
+
".tsx": "typescript",
|
|
27
|
+
".rs": "rust",
|
|
28
|
+
".go": "go",
|
|
29
|
+
};
|
|
30
|
+
export function detectLanguage(filepath) {
|
|
31
|
+
return LANG_MAP[extname(filepath).toLowerCase()] ?? "generic";
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Python analyzer (regex-based port of AST checks)
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
function analyzePython(filepath, lines) {
|
|
37
|
+
const issues = [];
|
|
38
|
+
for (let i = 0; i < lines.length; i++) {
|
|
39
|
+
const line = lines[i];
|
|
40
|
+
const num = i + 1;
|
|
41
|
+
const trimmed = line.trim();
|
|
42
|
+
// Mutable default argument: def f(x=[], def f(x={})
|
|
43
|
+
if (/def\s+\w+\s*\(/.test(trimmed) && /=\s*[\[\{]/.test(trimmed)) {
|
|
44
|
+
issues.push({
|
|
45
|
+
file: filepath, line: num, severity: "high",
|
|
46
|
+
driftId: "PY-MUT-DEFAULT",
|
|
47
|
+
message: "Mutable default argument in function definition",
|
|
48
|
+
suggestion: "Use None as default and create inside function body.",
|
|
49
|
+
snippet: trimmed,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// Bare except:
|
|
53
|
+
if (/^\s*except\s*:/.test(line)) {
|
|
54
|
+
issues.push({
|
|
55
|
+
file: filepath, line: num, severity: "high",
|
|
56
|
+
driftId: "PY-BARE-EXCEPT",
|
|
57
|
+
message: "Bare `except:` catches everything including KeyboardInterrupt",
|
|
58
|
+
suggestion: "Use `except Exception:` or catch specific exceptions.",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// Silent except (except followed by pass on next line)
|
|
62
|
+
if (/^\s*except[\s:]/.test(line) && i + 1 < lines.length) {
|
|
63
|
+
const next = lines[i + 1].trim();
|
|
64
|
+
if (next === "pass") {
|
|
65
|
+
issues.push({
|
|
66
|
+
file: filepath, line: num, severity: "critical",
|
|
67
|
+
driftId: "DRIFT-002",
|
|
68
|
+
message: "Exception silently swallowed with `pass`",
|
|
69
|
+
suggestion: "Log the error, re-raise, or handle explicitly.",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// == None instead of is None
|
|
74
|
+
if (/==\s*None/.test(trimmed) && !trimmed.startsWith("#")) {
|
|
75
|
+
issues.push({
|
|
76
|
+
file: filepath, line: num, severity: "medium",
|
|
77
|
+
driftId: "PY-IS-NONE",
|
|
78
|
+
message: "Using `== None` instead of `is None`",
|
|
79
|
+
suggestion: "Use `is None` for None checks (PEP 8).",
|
|
80
|
+
snippet: trimmed,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// != None instead of is not None
|
|
84
|
+
if (/!=\s*None/.test(trimmed) && !trimmed.startsWith("#")) {
|
|
85
|
+
issues.push({
|
|
86
|
+
file: filepath, line: num, severity: "medium",
|
|
87
|
+
driftId: "PY-IS-NONE",
|
|
88
|
+
message: "Using `!= None` instead of `is not None`",
|
|
89
|
+
suggestion: "Use `is not None` for None checks (PEP 8).",
|
|
90
|
+
snippet: trimmed,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// f-string without interpolation
|
|
94
|
+
if (/f['"][^{'"]*['"]/.test(trimmed) && !trimmed.includes("{")) {
|
|
95
|
+
issues.push({
|
|
96
|
+
file: filepath, line: num, severity: "low",
|
|
97
|
+
driftId: "PY-FSTRING-EMPTY",
|
|
98
|
+
message: "f-string without any interpolation",
|
|
99
|
+
suggestion: "Remove the `f` prefix or add variables.",
|
|
100
|
+
snippet: trimmed,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// async def without await in body (heuristic: next non-empty line doesn't await)
|
|
104
|
+
if (/^\s*async\s+def\s+/.test(line)) {
|
|
105
|
+
// Collect the function body (next ~20 lines) and check for await
|
|
106
|
+
const body = lines.slice(i + 1, i + 20).join("\n");
|
|
107
|
+
if (!body.includes("await ") && !body.includes("async for") && !body.includes("async with")) {
|
|
108
|
+
issues.push({
|
|
109
|
+
file: filepath, line: num, severity: "medium",
|
|
110
|
+
driftId: "DRIFT-003",
|
|
111
|
+
message: "async function may not use await — could be incorrectly async",
|
|
112
|
+
suggestion: "Verify this function actually needs to be async.",
|
|
113
|
+
snippet: trimmed,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return issues;
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// JavaScript / TypeScript analyzer
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
function analyzeJavaScript(filepath, lines) {
|
|
124
|
+
const issues = [];
|
|
125
|
+
for (let i = 0; i < lines.length; i++) {
|
|
126
|
+
const line = lines[i];
|
|
127
|
+
const num = i + 1;
|
|
128
|
+
const trimmed = line.trim();
|
|
129
|
+
// Skip comments
|
|
130
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*"))
|
|
131
|
+
continue;
|
|
132
|
+
// == instead of === (but not !== or ===)
|
|
133
|
+
if (/[^!=><]={2}[^=]/.test(trimmed) && !/={3}/.test(trimmed)) {
|
|
134
|
+
issues.push({
|
|
135
|
+
file: filepath, line: num, severity: "medium",
|
|
136
|
+
driftId: "JS-STRICT-EQ",
|
|
137
|
+
message: "Using `==` instead of `===`",
|
|
138
|
+
suggestion: "Use strict equality `===` unless coercion is intentional.",
|
|
139
|
+
snippet: trimmed,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
// console.log left in
|
|
143
|
+
if (/console\.log\s*\(/.test(trimmed)) {
|
|
144
|
+
issues.push({
|
|
145
|
+
file: filepath, line: num, severity: "low",
|
|
146
|
+
driftId: "JS-CONSOLE",
|
|
147
|
+
message: "console.log() left in code",
|
|
148
|
+
suggestion: "Remove or replace with proper logging.",
|
|
149
|
+
snippet: trimmed,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
// .then() without .catch() on same line
|
|
153
|
+
if (/\.then\s*\(/.test(trimmed) && !/\.catch\s*\(/.test(trimmed)) {
|
|
154
|
+
issues.push({
|
|
155
|
+
file: filepath, line: num, severity: "medium",
|
|
156
|
+
driftId: "JS-UNCAUGHT-PROMISE",
|
|
157
|
+
message: "`.then()` without `.catch()` — unhandled promise rejection",
|
|
158
|
+
suggestion: "Add `.catch()` or use try/catch with async/await.",
|
|
159
|
+
snippet: trimmed,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// .sort() without comparator
|
|
163
|
+
if (/\.sort\s*\(\s*\)/.test(trimmed)) {
|
|
164
|
+
issues.push({
|
|
165
|
+
file: filepath, line: num, severity: "high",
|
|
166
|
+
driftId: "JS-SORT-DEFAULT",
|
|
167
|
+
message: "`.sort()` without comparator sorts as strings",
|
|
168
|
+
suggestion: "Use `.sort((a, b) => a - b)` for numeric sort.",
|
|
169
|
+
snippet: trimmed,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// any type in TypeScript
|
|
173
|
+
if (/:\s*any\b/.test(trimmed) || /as\s+any\b/.test(trimmed)) {
|
|
174
|
+
issues.push({
|
|
175
|
+
file: filepath, line: num, severity: "low",
|
|
176
|
+
driftId: "TS-ANY",
|
|
177
|
+
message: "`any` type leaking through — disables type safety",
|
|
178
|
+
suggestion: "Use a specific type or `unknown` with type narrowing.",
|
|
179
|
+
snippet: trimmed,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
// non-null assertion masking bugs
|
|
183
|
+
if (/\w!\.\w/.test(trimmed) || /\w!\[/.test(trimmed)) {
|
|
184
|
+
issues.push({
|
|
185
|
+
file: filepath, line: num, severity: "medium",
|
|
186
|
+
driftId: "TS-NON-NULL",
|
|
187
|
+
message: "Non-null assertion `!` — could crash if value is actually null",
|
|
188
|
+
suggestion: "Add explicit null check instead.",
|
|
189
|
+
snippet: trimmed,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// Early return with potential wrong value (heuristic)
|
|
193
|
+
if (/return\s+true\b/.test(trimmed) || /return\s+false\b/.test(trimmed)) {
|
|
194
|
+
issues.push({
|
|
195
|
+
file: filepath, line: num, severity: "info",
|
|
196
|
+
driftId: "DRIFT-005",
|
|
197
|
+
message: "Boolean return — verify this is the correct value for this branch",
|
|
198
|
+
suggestion: "Logic inversions are the #1 LLM drift pattern. Double-check.",
|
|
199
|
+
snippet: trimmed,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return issues;
|
|
204
|
+
}
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Generic analyzer (language-agnostic)
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
function analyzeGeneric(filepath, lines) {
|
|
209
|
+
const issues = [];
|
|
210
|
+
// TODO / FIXME / HACK markers
|
|
211
|
+
for (let i = 0; i < lines.length; i++) {
|
|
212
|
+
const line = lines[i];
|
|
213
|
+
const num = i + 1;
|
|
214
|
+
for (const marker of ["TODO", "FIXME", "HACK", "XXX", "BUG"]) {
|
|
215
|
+
if (line.toUpperCase().includes(marker) && /[/#*\-]/.test(line)) {
|
|
216
|
+
issues.push({
|
|
217
|
+
file: filepath, line: num, severity: "info",
|
|
218
|
+
driftId: "MARKER",
|
|
219
|
+
message: `${marker} marker found`,
|
|
220
|
+
snippet: line.trim(),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Near-duplicate blocks (copy-paste drift detection)
|
|
226
|
+
const BLOCK_SIZE = 5;
|
|
227
|
+
const seen = new Map();
|
|
228
|
+
for (let i = 0; i <= lines.length - BLOCK_SIZE; i++) {
|
|
229
|
+
const block = lines.slice(i, i + BLOCK_SIZE).map((l) => l.trim()).filter(Boolean);
|
|
230
|
+
if (block.length < 3)
|
|
231
|
+
continue;
|
|
232
|
+
// Normalize: replace identifiers with placeholder
|
|
233
|
+
const sig = block.map((l) => l.replace(/\b[a-z_]\w*\b/g, "_")).join("|");
|
|
234
|
+
const prev = seen.get(sig);
|
|
235
|
+
if (prev !== undefined && i - prev > BLOCK_SIZE) {
|
|
236
|
+
issues.push({
|
|
237
|
+
file: filepath, line: i + 1, severity: "medium",
|
|
238
|
+
driftId: "DRIFT-007",
|
|
239
|
+
message: `Near-duplicate block (similar to line ${prev + 1})`,
|
|
240
|
+
suggestion: "Verify all variable names were updated correctly in the copy.",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
seen.set(sig, i);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Magic numbers (2+ digit numbers not in common whitelist)
|
|
248
|
+
const MAGIC_WHITELIST = new Set([10, 16, 32, 64, 100, 128, 256, 512, 1000, 1024, 2048, 4096, 8080, 3000, 8000]);
|
|
249
|
+
for (let i = 0; i < lines.length; i++) {
|
|
250
|
+
const line = lines[i];
|
|
251
|
+
const trimmed = line.trim();
|
|
252
|
+
if (/^[/#*\-]/.test(trimmed))
|
|
253
|
+
continue;
|
|
254
|
+
for (const match of trimmed.matchAll(/(?<![.\w])(\d{2,})(?![.\w])/g)) {
|
|
255
|
+
const num = parseInt(match[1], 10);
|
|
256
|
+
if (!MAGIC_WHITELIST.has(num) && !/port|size|limit|max|min/i.test(trimmed)) {
|
|
257
|
+
issues.push({
|
|
258
|
+
file: filepath, line: i + 1, severity: "low",
|
|
259
|
+
driftId: "MAGIC-NUM",
|
|
260
|
+
message: `Magic number \`${num}\` — consider a named constant`,
|
|
261
|
+
snippet: trimmed,
|
|
262
|
+
});
|
|
263
|
+
break; // one per line is enough
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return issues;
|
|
268
|
+
}
|
|
269
|
+
export function validateSource(filepath, source, lang) {
|
|
270
|
+
const language = lang ?? detectLanguage(filepath);
|
|
271
|
+
const lines = source.split("\n");
|
|
272
|
+
const issues = [];
|
|
273
|
+
if (language === "python") {
|
|
274
|
+
issues.push(...analyzePython(filepath, lines));
|
|
275
|
+
}
|
|
276
|
+
else if (language === "javascript" || language === "typescript") {
|
|
277
|
+
issues.push(...analyzeJavaScript(filepath, lines));
|
|
278
|
+
}
|
|
279
|
+
issues.push(...analyzeGeneric(filepath, lines));
|
|
280
|
+
issues.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
|
|
281
|
+
return issues;
|
|
282
|
+
}
|
|
283
|
+
export function validateFile(filepath) {
|
|
284
|
+
let source;
|
|
285
|
+
try {
|
|
286
|
+
source = readFileSync(filepath, { encoding: "utf-8" });
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
return [{
|
|
290
|
+
file: filepath, line: 0, severity: "critical",
|
|
291
|
+
driftId: "IO-ERROR",
|
|
292
|
+
message: `Cannot read file: ${err instanceof Error ? err.message : String(err)}`,
|
|
293
|
+
}];
|
|
294
|
+
}
|
|
295
|
+
return validateSource(filepath, source);
|
|
296
|
+
}
|
|
297
|
+
export function formatReport(filepath, issues) {
|
|
298
|
+
const lines = [
|
|
299
|
+
"=".repeat(60),
|
|
300
|
+
"🛡️ LOGIC GUARDIAN — Validation Report",
|
|
301
|
+
"=".repeat(60),
|
|
302
|
+
`File: ${filepath}`,
|
|
303
|
+
`Issues: ${issues.length}`,
|
|
304
|
+
"",
|
|
305
|
+
];
|
|
306
|
+
if (issues.length === 0) {
|
|
307
|
+
lines.push("✅ No issues detected. Proceed with confidence.");
|
|
308
|
+
lines.push("");
|
|
309
|
+
lines.push("⚠️ Automated checks catch ~40% of drift patterns.");
|
|
310
|
+
lines.push(" The manual checklist (Passes 1-5) catches the rest.");
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
const bySeverity = (s) => issues.filter((i) => i.severity === s);
|
|
314
|
+
for (const sev of ["critical", "high", "medium", "low", "info"]) {
|
|
315
|
+
const group = bySeverity(sev);
|
|
316
|
+
if (group.length > 0) {
|
|
317
|
+
lines.push(`--- ${sev.toUpperCase()} (${group.length}) ---`);
|
|
318
|
+
for (const issue of group)
|
|
319
|
+
lines.push(formatIssue(issue));
|
|
320
|
+
lines.push("");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const criticalCount = bySeverity("critical").length;
|
|
324
|
+
const highCount = bySeverity("high").length;
|
|
325
|
+
const passed = criticalCount === 0 && highCount === 0;
|
|
326
|
+
lines.push(passed
|
|
327
|
+
? "✅ PASS — No critical or high severity issues."
|
|
328
|
+
: "❌ FAIL — Fix critical/high issues before proceeding.");
|
|
329
|
+
}
|
|
330
|
+
lines.push("=".repeat(60));
|
|
331
|
+
return lines.join("\n");
|
|
332
|
+
}
|
package/build/index.js
CHANGED
|
@@ -10,6 +10,10 @@ import { recall, RecallSchema } from "./tools/recall.js";
|
|
|
10
10
|
import { recallAll } from "./tools/recall-all.js";
|
|
11
11
|
import { forget, ForgetSchema } from "./tools/forget.js";
|
|
12
12
|
import { memoryStats } from "./tools/stats.js";
|
|
13
|
+
import { handleValidateFile, ValidateFileSchema, handleCheckDrift, CheckDriftSchema, handleGetChecklist, } from "./tools/guardian.js";
|
|
14
|
+
import { handleGrepCode, GrepCodeSchema } from "./tools/grep.js";
|
|
15
|
+
import { handleInitProject, InitProjectSchema } from "./tools/init.js";
|
|
16
|
+
import { handleSyncFile, SyncFileSchema, handleSyncProject, SyncProjectSchema, } from "./tools/sync.js";
|
|
13
17
|
// ---------------------------------------------------------------------------
|
|
14
18
|
// Init DB
|
|
15
19
|
// ---------------------------------------------------------------------------
|
|
@@ -18,12 +22,13 @@ const stmts = prepareStatements(db);
|
|
|
18
22
|
// ---------------------------------------------------------------------------
|
|
19
23
|
// MCP Server
|
|
20
24
|
// ---------------------------------------------------------------------------
|
|
21
|
-
const server = new Server({ name: "lucid", version: "1.
|
|
25
|
+
const server = new Server({ name: "lucid", version: "1.1.0" }, { capabilities: { tools: {} } });
|
|
22
26
|
// ---------------------------------------------------------------------------
|
|
23
27
|
// Tool definitions
|
|
24
28
|
// ---------------------------------------------------------------------------
|
|
25
29
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
26
30
|
tools: [
|
|
31
|
+
// ── Memory ──────────────────────────────────────────────────────────────
|
|
27
32
|
{
|
|
28
33
|
name: "remember",
|
|
29
34
|
description: "Store a fact, decision, or observation about an entity in the knowledge graph.",
|
|
@@ -88,6 +93,104 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
88
93
|
description: "Get memory usage statistics.",
|
|
89
94
|
inputSchema: { type: "object", properties: {} },
|
|
90
95
|
},
|
|
96
|
+
// ── Init / Indexing ──────────────────────────────────────────────────────
|
|
97
|
+
{
|
|
98
|
+
name: "init_project",
|
|
99
|
+
description: "Scan and index a project directory into the knowledge graph. " +
|
|
100
|
+
"Reads CLAUDE.md (directives, conventions), package.json / pyproject.toml (dependencies, scripts), " +
|
|
101
|
+
"README.md (description), .mcp.json (MCP servers), logic-guardian.yaml (drift patterns), " +
|
|
102
|
+
"and source files (exported functions/classes). " +
|
|
103
|
+
"Call this once when starting work on a project to bootstrap memory with project context.",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {
|
|
107
|
+
directory: {
|
|
108
|
+
type: "string",
|
|
109
|
+
description: "Absolute path to the project root. Defaults to current working directory.",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "sync_file",
|
|
116
|
+
description: "Index or re-index a single source file after it was written or modified. " +
|
|
117
|
+
"Extracts exports, description, and open TODOs, then updates the knowledge graph. " +
|
|
118
|
+
"IMPORTANT: call this automatically after every Write or Edit tool call.",
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: "object",
|
|
121
|
+
properties: {
|
|
122
|
+
path: { type: "string", description: "Absolute or relative path to the modified file." },
|
|
123
|
+
},
|
|
124
|
+
required: ["path"],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "sync_project",
|
|
129
|
+
description: "Re-index the entire project directory incrementally. " +
|
|
130
|
+
"Use this when multiple files have changed (e.g. after a refactor or git pull).",
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: {
|
|
134
|
+
directory: {
|
|
135
|
+
type: "string",
|
|
136
|
+
description: "Project root directory. Defaults to current working directory.",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "grep_code",
|
|
143
|
+
description: "Search indexed source files using a regex pattern. " +
|
|
144
|
+
"Decompresses stored binary content and returns only matching lines with context. " +
|
|
145
|
+
"Token-efficient: returns ~20-50 tokens instead of full file contents. " +
|
|
146
|
+
"Useful for finding function calls, variable usages, import patterns.",
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: {
|
|
150
|
+
pattern: { type: "string", description: "Regex pattern to search for." },
|
|
151
|
+
language: { type: "string", enum: ["python", "javascript", "typescript", "generic"], description: "Filter by language." },
|
|
152
|
+
context: { type: "number", description: "Lines of context before/after each match (0-10, default 2)." },
|
|
153
|
+
},
|
|
154
|
+
required: ["pattern"],
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
// ── Logic Guardian ───────────────────────────────────────────────────────
|
|
158
|
+
{
|
|
159
|
+
name: "validate_file",
|
|
160
|
+
description: "Run Logic Guardian validation on a source file. Detects LLM drift patterns: " +
|
|
161
|
+
"logic inversions, null propagation, type confusion, copy-paste drift, silent exceptions, and more. " +
|
|
162
|
+
"Supports Python, JavaScript, TypeScript. Use after writing or modifying any code.",
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: "object",
|
|
165
|
+
properties: {
|
|
166
|
+
path: { type: "string", description: "Absolute or relative path to the file to validate." },
|
|
167
|
+
},
|
|
168
|
+
required: ["path"],
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: "check_drift",
|
|
173
|
+
description: "Analyze a code snippet for LLM drift patterns without saving to disk. " +
|
|
174
|
+
"Use this to validate code before writing it to a file.",
|
|
175
|
+
inputSchema: {
|
|
176
|
+
type: "object",
|
|
177
|
+
properties: {
|
|
178
|
+
code: { type: "string", description: "The code snippet to analyze." },
|
|
179
|
+
language: {
|
|
180
|
+
type: "string",
|
|
181
|
+
enum: ["python", "javascript", "typescript", "generic"],
|
|
182
|
+
description: "Programming language. Defaults to 'generic'.",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
required: ["code"],
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: "get_checklist",
|
|
190
|
+
description: "Get the full Logic Guardian validation checklist (5 passes). " +
|
|
191
|
+
"Call this before marking any implementation task as done.",
|
|
192
|
+
inputSchema: { type: "object", properties: {} },
|
|
193
|
+
},
|
|
91
194
|
],
|
|
92
195
|
}));
|
|
93
196
|
// ---------------------------------------------------------------------------
|
|
@@ -98,52 +201,59 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
98
201
|
try {
|
|
99
202
|
let text;
|
|
100
203
|
switch (name) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
text = remember(stmts,
|
|
204
|
+
// Memory
|
|
205
|
+
case "remember":
|
|
206
|
+
text = remember(stmts, RememberSchema.parse(args));
|
|
104
207
|
break;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const input = RelateSchema.parse(args);
|
|
108
|
-
text = relate(stmts, input);
|
|
208
|
+
case "relate":
|
|
209
|
+
text = relate(stmts, RelateSchema.parse(args));
|
|
109
210
|
break;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const input = RecallSchema.parse(args);
|
|
113
|
-
text = recall(stmts, input);
|
|
211
|
+
case "recall":
|
|
212
|
+
text = recall(stmts, RecallSchema.parse(args));
|
|
114
213
|
break;
|
|
115
|
-
|
|
116
|
-
case "recall_all": {
|
|
214
|
+
case "recall_all":
|
|
117
215
|
text = recallAll(db, stmts);
|
|
118
216
|
break;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const input = ForgetSchema.parse(args);
|
|
122
|
-
text = forget(stmts, input);
|
|
217
|
+
case "forget":
|
|
218
|
+
text = forget(stmts, ForgetSchema.parse(args));
|
|
123
219
|
break;
|
|
124
|
-
|
|
125
|
-
case "memory_stats": {
|
|
220
|
+
case "memory_stats":
|
|
126
221
|
text = memoryStats(db, stmts);
|
|
127
222
|
break;
|
|
128
|
-
|
|
223
|
+
// Init & Sync
|
|
224
|
+
case "init_project":
|
|
225
|
+
text = handleInitProject(stmts, InitProjectSchema.parse(args));
|
|
226
|
+
break;
|
|
227
|
+
case "sync_file":
|
|
228
|
+
text = handleSyncFile(stmts, SyncFileSchema.parse(args));
|
|
229
|
+
break;
|
|
230
|
+
case "sync_project":
|
|
231
|
+
text = handleSyncProject(stmts, SyncProjectSchema.parse(args));
|
|
232
|
+
break;
|
|
233
|
+
// Grep
|
|
234
|
+
case "grep_code":
|
|
235
|
+
text = handleGrepCode(stmts, GrepCodeSchema.parse(args));
|
|
236
|
+
break;
|
|
237
|
+
// Logic Guardian
|
|
238
|
+
case "validate_file":
|
|
239
|
+
text = handleValidateFile(ValidateFileSchema.parse(args));
|
|
240
|
+
break;
|
|
241
|
+
case "check_drift":
|
|
242
|
+
text = handleCheckDrift(CheckDriftSchema.parse(args));
|
|
243
|
+
break;
|
|
244
|
+
case "get_checklist":
|
|
245
|
+
text = handleGetChecklist();
|
|
246
|
+
break;
|
|
129
247
|
default:
|
|
130
|
-
return {
|
|
131
|
-
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
132
|
-
isError: true,
|
|
133
|
-
};
|
|
248
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
134
249
|
}
|
|
135
250
|
return { content: [{ type: "text", text }] };
|
|
136
251
|
}
|
|
137
252
|
catch (err) {
|
|
138
253
|
const message = err instanceof z.ZodError
|
|
139
254
|
? `Validation error: ${err.errors.map((e) => e.message).join(", ")}`
|
|
140
|
-
: err instanceof Error
|
|
141
|
-
|
|
142
|
-
: String(err);
|
|
143
|
-
return {
|
|
144
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
145
|
-
isError: true,
|
|
146
|
-
};
|
|
255
|
+
: err instanceof Error ? err.message : String(err);
|
|
256
|
+
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
147
257
|
}
|
|
148
258
|
});
|
|
149
259
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Statements } from "../database.js";
|
|
2
|
+
export interface FileIndex {
|
|
3
|
+
module: string;
|
|
4
|
+
exports: string[];
|
|
5
|
+
description: string;
|
|
6
|
+
todos: string[];
|
|
7
|
+
language: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function indexFile(filepath: string): FileIndex | null;
|
|
10
|
+
export interface UpsertResult {
|
|
11
|
+
observations: string[];
|
|
12
|
+
stored: boolean;
|
|
13
|
+
savedBytes: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function upsertFileIndex(index: FileIndex, source: string, stmts: Statements): UpsertResult;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { extname } from "path";
|
|
3
|
+
import { compress, sha256 } from "../store/content.js";
|
|
4
|
+
function extractTS(source) {
|
|
5
|
+
const exports = [];
|
|
6
|
+
const todos = [];
|
|
7
|
+
// Exported symbols
|
|
8
|
+
for (const m of source.matchAll(/export\s+(?:async\s+)?(?:function|class|const|type|interface|enum)\s+(\w+)/g)) {
|
|
9
|
+
exports.push(m[1]);
|
|
10
|
+
}
|
|
11
|
+
// First JSDoc / block comment as description
|
|
12
|
+
const docMatch = source.match(/^\/\*\*([\s\S]*?)\*\//m) ?? source.match(/^\/\/(.*)/m);
|
|
13
|
+
const description = docMatch
|
|
14
|
+
? docMatch[1].replace(/\s*\*\s*/g, " ").trim().slice(0, 200)
|
|
15
|
+
: "";
|
|
16
|
+
// TODOs
|
|
17
|
+
for (const m of source.matchAll(/\/\/\s*(TODO|FIXME|HACK)[:\s]+(.+)/gi)) {
|
|
18
|
+
todos.push(`${m[1]}: ${m[2].trim()}`);
|
|
19
|
+
}
|
|
20
|
+
return { exports, description, todos };
|
|
21
|
+
}
|
|
22
|
+
function extractPython(source) {
|
|
23
|
+
const exports = [];
|
|
24
|
+
const todos = [];
|
|
25
|
+
// Public functions and classes
|
|
26
|
+
for (const m of source.matchAll(/^(?:def|class|async def)\s+(\w+)/gm)) {
|
|
27
|
+
if (!m[1].startsWith("_"))
|
|
28
|
+
exports.push(m[1]);
|
|
29
|
+
}
|
|
30
|
+
// Module docstring
|
|
31
|
+
const docMatch = source.match(/^["']{3}([\s\S]*?)["']{3}/m);
|
|
32
|
+
const description = docMatch ? docMatch[1].trim().slice(0, 200) : "";
|
|
33
|
+
// TODOs
|
|
34
|
+
for (const m of source.matchAll(/#\s*(TODO|FIXME|HACK)[:\s]+(.+)/gi)) {
|
|
35
|
+
todos.push(`${m[1]}: ${m[2].trim()}`);
|
|
36
|
+
}
|
|
37
|
+
return { exports, description, todos };
|
|
38
|
+
}
|
|
39
|
+
function extractGeneric(source) {
|
|
40
|
+
const todos = [];
|
|
41
|
+
for (const m of source.matchAll(/(?:\/\/|#)\s*(TODO|FIXME|HACK)[:\s]+(.+)/gi)) {
|
|
42
|
+
todos.push(`${m[1]}: ${m[2].trim()}`);
|
|
43
|
+
}
|
|
44
|
+
return { exports: [], description: "", todos };
|
|
45
|
+
}
|
|
46
|
+
export function indexFile(filepath) {
|
|
47
|
+
let source;
|
|
48
|
+
try {
|
|
49
|
+
source = readFileSync(filepath, { encoding: "utf-8" });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const ext = extname(filepath).toLowerCase();
|
|
55
|
+
const module = filepath.replace(/\\/g, "/");
|
|
56
|
+
let extracted;
|
|
57
|
+
let language;
|
|
58
|
+
if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
|
|
59
|
+
extracted = extractTS(source);
|
|
60
|
+
language = ext.includes("ts") ? "typescript" : "javascript";
|
|
61
|
+
}
|
|
62
|
+
else if (ext === ".py") {
|
|
63
|
+
extracted = extractPython(source);
|
|
64
|
+
language = "python";
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
extracted = extractGeneric(source);
|
|
68
|
+
language = "generic";
|
|
69
|
+
}
|
|
70
|
+
return { module, language, ...extracted };
|
|
71
|
+
}
|
|
72
|
+
export function upsertFileIndex(index, source, stmts) {
|
|
73
|
+
const fileHash = sha256(source);
|
|
74
|
+
// Change detection — skip everything se hash-ul e identic
|
|
75
|
+
const existing = stmts.getFileByPath.get(index.module);
|
|
76
|
+
if (existing?.content_hash === fileHash) {
|
|
77
|
+
return { observations: [], stored: false, savedBytes: 0 };
|
|
78
|
+
}
|
|
79
|
+
// Comprimă și stochează conținutul binar
|
|
80
|
+
const blob = compress(source);
|
|
81
|
+
stmts.upsertFile.run(index.module, blob, fileHash, Buffer.byteLength(source, "utf-8"), blob.byteLength, index.language);
|
|
82
|
+
// Index structural în entities (compact, pentru recall)
|
|
83
|
+
const observations = [];
|
|
84
|
+
if (index.description)
|
|
85
|
+
observations.push(`description: ${index.description}`);
|
|
86
|
+
if (index.exports.length > 0)
|
|
87
|
+
observations.push(`exports: ${index.exports.join(", ")}`);
|
|
88
|
+
if (index.todos.length > 0)
|
|
89
|
+
observations.push(`TODOs: ${index.todos.join(" | ")}`);
|
|
90
|
+
observations.push(`language: ${index.language}`);
|
|
91
|
+
const entityRow = stmts.getEntityByName.get(index.module);
|
|
92
|
+
if (entityRow) {
|
|
93
|
+
stmts.updateEntity.run(JSON.stringify(observations), entityRow.id);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
stmts.insertEntity.run(index.module, "pattern", JSON.stringify(observations));
|
|
97
|
+
}
|
|
98
|
+
const savedBytes = Buffer.byteLength(source, "utf-8") - blob.byteLength;
|
|
99
|
+
return { observations, stored: true, savedBytes };
|
|
100
|
+
}
|