@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,312 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
2
|
+
import { join, extname, basename } from "path";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Helpers
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
function readFile(path) {
|
|
7
|
+
try {
|
|
8
|
+
return readFileSync(path, { encoding: "utf-8" });
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function upsert(stmts, name, type, observations) {
|
|
15
|
+
const existing = stmts.getEntityByName.get(name);
|
|
16
|
+
if (existing) {
|
|
17
|
+
const current = JSON.parse(existing.observations);
|
|
18
|
+
const merged = [...current];
|
|
19
|
+
for (const obs of observations) {
|
|
20
|
+
if (!merged.includes(obs))
|
|
21
|
+
merged.push(obs);
|
|
22
|
+
}
|
|
23
|
+
stmts.updateEntity.run(JSON.stringify(merged), existing.id);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
stmts.insertEntity.run(name, type, JSON.stringify(observations));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function relate(stmts, from, to, type) {
|
|
30
|
+
const fromRow = stmts.getEntityByName.get(from);
|
|
31
|
+
const toRow = stmts.getEntityByName.get(to);
|
|
32
|
+
if (fromRow && toRow) {
|
|
33
|
+
stmts.insertRelation.run(fromRow.id, toRow.id, type);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Parsers per file type
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
function indexClaudeMd(path, stmts, results) {
|
|
40
|
+
const content = readFile(path);
|
|
41
|
+
if (!content)
|
|
42
|
+
return;
|
|
43
|
+
// Indexează fiecare secțiune H2 ca observație separată pe entitatea proiectului
|
|
44
|
+
const sections = content.split(/\n##\s+/).filter(Boolean);
|
|
45
|
+
const observations = [];
|
|
46
|
+
for (const section of sections) {
|
|
47
|
+
const lines = section.trim().split("\n");
|
|
48
|
+
const title = lines[0]?.trim() ?? "directive";
|
|
49
|
+
const body = lines.slice(1).join("\n").trim();
|
|
50
|
+
if (body.length > 0) {
|
|
51
|
+
observations.push(`[${title}] ${body.slice(0, 300)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (observations.length > 0) {
|
|
55
|
+
upsert(stmts, "CLAUDE.md directives", "convention", observations);
|
|
56
|
+
results.push({ entity: "CLAUDE.md directives", type: "convention", observations: observations.length, source: "CLAUDE.md" });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function indexPackageJson(path, stmts, results) {
|
|
60
|
+
const content = readFile(path);
|
|
61
|
+
if (!content)
|
|
62
|
+
return;
|
|
63
|
+
let pkg;
|
|
64
|
+
try {
|
|
65
|
+
pkg = JSON.parse(content);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const name = pkg["name"] ?? "project";
|
|
71
|
+
const version = pkg["version"] ?? "unknown";
|
|
72
|
+
const description = pkg["description"] ?? "";
|
|
73
|
+
const projectName = name.replace(/^@[\w-]+\//, ""); // strip scope
|
|
74
|
+
const obs = [`version: ${version}`];
|
|
75
|
+
if (description)
|
|
76
|
+
obs.push(`description: ${description}`);
|
|
77
|
+
// Scripts
|
|
78
|
+
const scripts = pkg["scripts"];
|
|
79
|
+
if (scripts) {
|
|
80
|
+
for (const [k, v] of Object.entries(scripts).slice(0, 6)) {
|
|
81
|
+
obs.push(`script ${k}: ${v}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
upsert(stmts, projectName, "project", obs);
|
|
85
|
+
results.push({ entity: projectName, type: "project", observations: obs.length, source: "package.json" });
|
|
86
|
+
// Dependențe principale
|
|
87
|
+
const deps = {
|
|
88
|
+
...(pkg["dependencies"] ?? {}),
|
|
89
|
+
...(pkg["devDependencies"] ?? {}),
|
|
90
|
+
};
|
|
91
|
+
for (const [dep, ver] of Object.entries(deps).slice(0, 20)) {
|
|
92
|
+
upsert(stmts, dep, "tool", [`version: ${ver}`, `used in: ${projectName}`]);
|
|
93
|
+
relate(stmts, projectName, dep, "depends_on");
|
|
94
|
+
}
|
|
95
|
+
if (Object.keys(deps).length > 0) {
|
|
96
|
+
results.push({ entity: `${Object.keys(deps).length} dependencies`, type: "tool", observations: 1, source: "package.json" });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function indexPyprojectToml(path, stmts, results) {
|
|
100
|
+
const content = readFile(path);
|
|
101
|
+
if (!content)
|
|
102
|
+
return;
|
|
103
|
+
const nameMatch = content.match(/^name\s*=\s*["']([^"']+)["']/m);
|
|
104
|
+
const versionMatch = content.match(/^version\s*=\s*["']([^"']+)["']/m);
|
|
105
|
+
const descMatch = content.match(/^description\s*=\s*["']([^"']+)["']/m);
|
|
106
|
+
const name = nameMatch?.[1] ?? "project";
|
|
107
|
+
const obs = [];
|
|
108
|
+
if (versionMatch?.[1])
|
|
109
|
+
obs.push(`version: ${versionMatch[1]}`);
|
|
110
|
+
if (descMatch?.[1])
|
|
111
|
+
obs.push(`description: ${descMatch[1]}`);
|
|
112
|
+
// Dependencies
|
|
113
|
+
const depsSection = content.match(/\[tool\.poetry\.dependencies\]([\s\S]*?)(?=\[|$)/)?.[1]
|
|
114
|
+
?? content.match(/dependencies\s*=\s*\[([\s\S]*?)\]/)?.[1]
|
|
115
|
+
?? "";
|
|
116
|
+
const deps = [...depsSection.matchAll(/["']?([\w-]+)["']?\s*[=:]/g)].map((m) => m[1]).filter(Boolean);
|
|
117
|
+
for (const dep of deps.slice(0, 20)) {
|
|
118
|
+
upsert(stmts, dep, "tool", [`used in: ${name}`]);
|
|
119
|
+
relate(stmts, name, dep, "depends_on");
|
|
120
|
+
}
|
|
121
|
+
upsert(stmts, name, "project", obs);
|
|
122
|
+
results.push({ entity: name, type: "project", observations: obs.length, source: "pyproject.toml" });
|
|
123
|
+
}
|
|
124
|
+
function indexReadme(path, projectName, stmts, results) {
|
|
125
|
+
const content = readFile(path);
|
|
126
|
+
if (!content)
|
|
127
|
+
return;
|
|
128
|
+
// Prima secțiune (descriere)
|
|
129
|
+
const firstParagraph = content.replace(/^#[^\n]*\n/, "").trim().split("\n\n")[0] ?? "";
|
|
130
|
+
if (firstParagraph.length < 10)
|
|
131
|
+
return;
|
|
132
|
+
upsert(stmts, projectName, "project", [
|
|
133
|
+
`README: ${firstParagraph.slice(0, 400)}`,
|
|
134
|
+
]);
|
|
135
|
+
results.push({ entity: projectName, type: "project", observations: 1, source: "README.md" });
|
|
136
|
+
}
|
|
137
|
+
function indexMcpJson(path, stmts, results) {
|
|
138
|
+
const content = readFile(path);
|
|
139
|
+
if (!content)
|
|
140
|
+
return;
|
|
141
|
+
let cfg;
|
|
142
|
+
try {
|
|
143
|
+
cfg = JSON.parse(content);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const servers = cfg.mcpServers ?? {};
|
|
149
|
+
for (const [serverName, config] of Object.entries(servers)) {
|
|
150
|
+
const c = config;
|
|
151
|
+
const obs = [`MCP server configured in project`];
|
|
152
|
+
if (c.command)
|
|
153
|
+
obs.push(`command: ${c.command} ${(c.args ?? []).join(" ")}`);
|
|
154
|
+
upsert(stmts, serverName, "tool", obs);
|
|
155
|
+
results.push({ entity: serverName, type: "tool", observations: obs.length, source: ".mcp.json" });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function indexLogicGuardianYaml(path, stmts, results) {
|
|
159
|
+
const content = readFile(path);
|
|
160
|
+
if (!content)
|
|
161
|
+
return;
|
|
162
|
+
// Extrage known_drift_patterns
|
|
163
|
+
const patternMatches = [...content.matchAll(/id:\s*["']?(DRIFT-\d+)["']?\s*\n\s*name:\s*["']?([^\n"']+)["']?\s*\n\s*description:\s*["']?([^\n"']+)/g)];
|
|
164
|
+
for (const m of patternMatches) {
|
|
165
|
+
const [, id, name, desc] = m;
|
|
166
|
+
upsert(stmts, `${id}: ${name.trim()}`, "pattern", [
|
|
167
|
+
`drift pattern: ${desc.trim()}`,
|
|
168
|
+
"source: logic-guardian.yaml",
|
|
169
|
+
]);
|
|
170
|
+
}
|
|
171
|
+
// Invarianți de proiect
|
|
172
|
+
const invariantsMatch = content.match(/project_invariants:([\s\S]*?)(?=\n\w|\n#|$)/);
|
|
173
|
+
if (invariantsMatch) {
|
|
174
|
+
const invariants = [...invariantsMatch[1].matchAll(/-\s+"([^"]+)"/g)].map((m) => m[1]);
|
|
175
|
+
if (invariants.length > 0) {
|
|
176
|
+
upsert(stmts, "project invariants", "convention", invariants);
|
|
177
|
+
results.push({ entity: "project invariants", type: "convention", observations: invariants.length, source: "logic-guardian.yaml" });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (patternMatches.length > 0) {
|
|
181
|
+
results.push({ entity: `${patternMatches.length} drift patterns`, type: "pattern", observations: patternMatches.length, source: "logic-guardian.yaml" });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Source file indexing — extrage exporturi, clase, funcții principale
|
|
185
|
+
const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
|
|
186
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "build", "dist", "__pycache__", ".next", "venv", ".venv", "target"]);
|
|
187
|
+
const MAX_SOURCE_FILES = 30;
|
|
188
|
+
function indexSourceFile(filepath, projectName, stmts) {
|
|
189
|
+
const content = readFile(filepath);
|
|
190
|
+
if (!content)
|
|
191
|
+
return [];
|
|
192
|
+
const exports = [];
|
|
193
|
+
const lang = extname(filepath);
|
|
194
|
+
// TypeScript / JavaScript
|
|
195
|
+
if ([".ts", ".tsx", ".js", ".jsx"].includes(lang)) {
|
|
196
|
+
for (const m of content.matchAll(/export\s+(?:async\s+)?(?:function|class|const|type|interface)\s+(\w+)/g)) {
|
|
197
|
+
exports.push(m[1]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Python
|
|
201
|
+
if (lang === ".py") {
|
|
202
|
+
for (const m of content.matchAll(/^(?:def|class|async def)\s+(\w+)/gm)) {
|
|
203
|
+
if (!m[1].startsWith("_"))
|
|
204
|
+
exports.push(m[1]);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (exports.length === 0)
|
|
208
|
+
return [];
|
|
209
|
+
const relPath = filepath.replace(/\\/g, "/").split("/src/")[1] ?? basename(filepath);
|
|
210
|
+
const obs = [`exports from ${relPath}: ${exports.slice(0, 10).join(", ")}`];
|
|
211
|
+
upsert(stmts, projectName, "project", obs);
|
|
212
|
+
return exports;
|
|
213
|
+
}
|
|
214
|
+
function scanSources(dir, projectName, stmts, results) {
|
|
215
|
+
const srcDir = join(dir, "src");
|
|
216
|
+
const scanDir = existsSync(srcDir) ? srcDir : dir;
|
|
217
|
+
let fileCount = 0;
|
|
218
|
+
const exportedSymbols = [];
|
|
219
|
+
function walk(d, depth) {
|
|
220
|
+
if (depth > 3 || fileCount >= MAX_SOURCE_FILES)
|
|
221
|
+
return;
|
|
222
|
+
let entries;
|
|
223
|
+
try {
|
|
224
|
+
entries = readdirSync(d);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
for (const entry of entries) {
|
|
230
|
+
if (SKIP_DIRS.has(entry))
|
|
231
|
+
continue;
|
|
232
|
+
const full = join(d, entry);
|
|
233
|
+
const stat = statSync(full);
|
|
234
|
+
if (stat.isDirectory()) {
|
|
235
|
+
walk(full, depth + 1);
|
|
236
|
+
}
|
|
237
|
+
else if (SOURCE_EXTS.has(extname(entry))) {
|
|
238
|
+
const syms = indexSourceFile(full, projectName, stmts);
|
|
239
|
+
exportedSymbols.push(...syms);
|
|
240
|
+
fileCount++;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
walk(scanDir, 0);
|
|
245
|
+
if (fileCount > 0) {
|
|
246
|
+
results.push({
|
|
247
|
+
entity: projectName,
|
|
248
|
+
type: "project",
|
|
249
|
+
observations: fileCount,
|
|
250
|
+
source: `${fileCount} source files (${exportedSymbols.length} exports)`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// Main entry point
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
export function indexProject(directory, stmts) {
|
|
258
|
+
const results = [];
|
|
259
|
+
const dir = directory.replace(/\\/g, "/");
|
|
260
|
+
// Detectează numele proiectului din package.json sau pyproject.toml
|
|
261
|
+
let projectName = basename(dir);
|
|
262
|
+
const pkgPath = join(dir, "package.json");
|
|
263
|
+
if (existsSync(pkgPath)) {
|
|
264
|
+
const raw = readFile(pkgPath);
|
|
265
|
+
if (raw) {
|
|
266
|
+
try {
|
|
267
|
+
const pkg = JSON.parse(raw);
|
|
268
|
+
if (pkg.name)
|
|
269
|
+
projectName = pkg.name.replace(/^@[\w-]+\//, "");
|
|
270
|
+
}
|
|
271
|
+
catch { /* ignore */ }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// 1. CLAUDE.md — cel mai important
|
|
275
|
+
const claudeMdPaths = ["CLAUDE.md", ".claude/CLAUDE.md", "claude.md"];
|
|
276
|
+
for (const p of claudeMdPaths) {
|
|
277
|
+
const full = join(dir, p);
|
|
278
|
+
if (existsSync(full)) {
|
|
279
|
+
indexClaudeMd(full, stmts, results);
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// 2. package.json
|
|
284
|
+
if (existsSync(join(dir, "package.json"))) {
|
|
285
|
+
indexPackageJson(join(dir, "package.json"), stmts, results);
|
|
286
|
+
}
|
|
287
|
+
// 3. pyproject.toml
|
|
288
|
+
if (existsSync(join(dir, "pyproject.toml"))) {
|
|
289
|
+
indexPyprojectToml(join(dir, "pyproject.toml"), stmts, results);
|
|
290
|
+
}
|
|
291
|
+
// 4. README
|
|
292
|
+
for (const p of ["README.md", "readme.md", "Readme.md"]) {
|
|
293
|
+
if (existsSync(join(dir, p))) {
|
|
294
|
+
indexReadme(join(dir, p), projectName, stmts, results);
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// 5. MCP config
|
|
299
|
+
for (const p of [".mcp.json", "mcp.json"]) {
|
|
300
|
+
if (existsSync(join(dir, p))) {
|
|
301
|
+
indexMcpJson(join(dir, p), stmts, results);
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// 6. Logic Guardian config
|
|
306
|
+
if (existsSync(join(dir, "logic-guardian.yaml"))) {
|
|
307
|
+
indexLogicGuardianYaml(join(dir, "logic-guardian.yaml"), stmts, results);
|
|
308
|
+
}
|
|
309
|
+
// 7. Surse
|
|
310
|
+
scanSources(dir, projectName, stmts, results);
|
|
311
|
+
return results;
|
|
312
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { deflateSync, inflateSync } from "zlib";
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
export function compress(source) {
|
|
4
|
+
return deflateSync(Buffer.from(source, "utf-8"), { level: 9 });
|
|
5
|
+
}
|
|
6
|
+
export function decompress(blob) {
|
|
7
|
+
return inflateSync(blob).toString("utf-8");
|
|
8
|
+
}
|
|
9
|
+
export function sha256(source) {
|
|
10
|
+
return createHash("sha256").update(source).digest("hex");
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Statements } from "../database.js";
|
|
3
|
+
export declare const GrepCodeSchema: z.ZodObject<{
|
|
4
|
+
pattern: z.ZodString;
|
|
5
|
+
language: z.ZodOptional<z.ZodEnum<["python", "javascript", "typescript", "generic"]>>;
|
|
6
|
+
context: z.ZodDefault<z.ZodNumber>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
pattern: string;
|
|
9
|
+
context: number;
|
|
10
|
+
language?: "python" | "javascript" | "typescript" | "generic" | undefined;
|
|
11
|
+
}, {
|
|
12
|
+
pattern: string;
|
|
13
|
+
language?: "python" | "javascript" | "typescript" | "generic" | undefined;
|
|
14
|
+
context?: number | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
export type GrepCodeInput = z.infer<typeof GrepCodeSchema>;
|
|
17
|
+
export declare function handleGrepCode(stmts: Statements, input: GrepCodeInput): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { decompress } from "../store/content.js";
|
|
3
|
+
export const GrepCodeSchema = z.object({
|
|
4
|
+
pattern: z.string().min(1),
|
|
5
|
+
language: z.enum(["python", "javascript", "typescript", "generic"]).optional(),
|
|
6
|
+
context: z.number().int().min(0).max(10).default(2),
|
|
7
|
+
});
|
|
8
|
+
export function handleGrepCode(stmts, input) {
|
|
9
|
+
let regex;
|
|
10
|
+
try {
|
|
11
|
+
regex = new RegExp(input.pattern, "i");
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return `Invalid regex pattern: ${input.pattern}`;
|
|
15
|
+
}
|
|
16
|
+
const files = stmts.getAllFiles.all();
|
|
17
|
+
const matches = [];
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
if (input.language && file.language !== input.language)
|
|
20
|
+
continue;
|
|
21
|
+
let source;
|
|
22
|
+
try {
|
|
23
|
+
source = decompress(file.content);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
continue; // skip fișiere corupte
|
|
27
|
+
}
|
|
28
|
+
const lines = source.split("\n");
|
|
29
|
+
for (let i = 0; i < lines.length; i++) {
|
|
30
|
+
if (!regex.test(lines[i]))
|
|
31
|
+
continue;
|
|
32
|
+
matches.push({
|
|
33
|
+
filepath: file.filepath,
|
|
34
|
+
line: i + 1,
|
|
35
|
+
text: lines[i],
|
|
36
|
+
contextBefore: lines.slice(Math.max(0, i - input.context), i),
|
|
37
|
+
contextAfter: lines.slice(i + 1, i + 1 + input.context),
|
|
38
|
+
});
|
|
39
|
+
if (matches.length >= 30)
|
|
40
|
+
break; // cap la 30 match-uri
|
|
41
|
+
}
|
|
42
|
+
if (matches.length >= 30)
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
if (matches.length === 0) {
|
|
46
|
+
return `No matches for /${input.pattern}/ in ${files.length} indexed file(s).`;
|
|
47
|
+
}
|
|
48
|
+
const lines = [
|
|
49
|
+
`Found ${matches.length} match(es) for /${input.pattern}/ across ${files.length} file(s):\n`,
|
|
50
|
+
];
|
|
51
|
+
let lastFile = "";
|
|
52
|
+
for (const m of matches) {
|
|
53
|
+
if (m.filepath !== lastFile) {
|
|
54
|
+
lines.push(`── ${m.filepath}`);
|
|
55
|
+
lastFile = m.filepath;
|
|
56
|
+
}
|
|
57
|
+
for (const l of m.contextBefore)
|
|
58
|
+
lines.push(` ${m.line - m.contextBefore.length + m.contextBefore.indexOf(l)}│ ${l}`);
|
|
59
|
+
lines.push(`▶ ${m.line}│ ${m.text}`);
|
|
60
|
+
for (const l of m.contextAfter)
|
|
61
|
+
lines.push(` ${m.line + 1 + m.contextAfter.indexOf(l)}│ ${l}`);
|
|
62
|
+
lines.push("");
|
|
63
|
+
}
|
|
64
|
+
return lines.join("\n");
|
|
65
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const ValidateFileSchema: z.ZodObject<{
|
|
3
|
+
path: z.ZodString;
|
|
4
|
+
}, "strip", z.ZodTypeAny, {
|
|
5
|
+
path: string;
|
|
6
|
+
}, {
|
|
7
|
+
path: string;
|
|
8
|
+
}>;
|
|
9
|
+
export declare const CheckDriftSchema: z.ZodObject<{
|
|
10
|
+
code: z.ZodString;
|
|
11
|
+
language: z.ZodOptional<z.ZodEnum<["python", "javascript", "typescript", "generic"]>>;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
code: string;
|
|
14
|
+
language?: "python" | "javascript" | "typescript" | "generic" | undefined;
|
|
15
|
+
}, {
|
|
16
|
+
code: string;
|
|
17
|
+
language?: "python" | "javascript" | "typescript" | "generic" | undefined;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function handleValidateFile(args: z.infer<typeof ValidateFileSchema>): string;
|
|
20
|
+
export declare function handleCheckDrift(args: z.infer<typeof CheckDriftSchema>): string;
|
|
21
|
+
export declare function handleGetChecklist(): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { writeFileSync, unlinkSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { validateFile, validateSource, formatReport, } from "../guardian/validator.js";
|
|
6
|
+
import { CHECKLIST } from "../guardian/checklist.js";
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Schemas
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
export const ValidateFileSchema = z.object({
|
|
11
|
+
path: z.string().min(1),
|
|
12
|
+
});
|
|
13
|
+
export const CheckDriftSchema = z.object({
|
|
14
|
+
code: z.string().min(1),
|
|
15
|
+
language: z.enum(["python", "javascript", "typescript", "generic"]).optional(),
|
|
16
|
+
});
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Handlers
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
export function handleValidateFile(args) {
|
|
21
|
+
const issues = validateFile(args.path);
|
|
22
|
+
return formatReport(args.path, issues);
|
|
23
|
+
}
|
|
24
|
+
export function handleCheckDrift(args) {
|
|
25
|
+
const lang = args.language ?? "generic";
|
|
26
|
+
const extMap = {
|
|
27
|
+
python: ".py",
|
|
28
|
+
javascript: ".js",
|
|
29
|
+
typescript: ".ts",
|
|
30
|
+
generic: ".txt",
|
|
31
|
+
};
|
|
32
|
+
const ext = extMap[lang] ?? ".txt";
|
|
33
|
+
const tmpPath = join(tmpdir(), `lucid-drift-${Date.now()}${ext}`);
|
|
34
|
+
try {
|
|
35
|
+
writeFileSync(tmpPath, args.code, "utf-8");
|
|
36
|
+
const issues = validateSource(tmpPath, args.code, lang === "generic" ? undefined : lang);
|
|
37
|
+
if (issues.length === 0) {
|
|
38
|
+
return "✅ No drift patterns detected in this code snippet.";
|
|
39
|
+
}
|
|
40
|
+
const lines = [`Found ${issues.length} potential issue(s):\n`];
|
|
41
|
+
for (const issue of issues) {
|
|
42
|
+
const icon = { critical: "🔴", high: "🟠", medium: "🟡", low: "🔵", info: "ℹ️" }[issue.severity];
|
|
43
|
+
lines.push(`${icon} [${issue.driftId}] line ${issue.line} — ${issue.message}`);
|
|
44
|
+
if (issue.suggestion)
|
|
45
|
+
lines.push(` 💡 ${issue.suggestion}`);
|
|
46
|
+
}
|
|
47
|
+
return lines.join("\n");
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
try {
|
|
51
|
+
unlinkSync(tmpPath);
|
|
52
|
+
}
|
|
53
|
+
catch { /* ignore */ }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function handleGetChecklist() {
|
|
57
|
+
return CHECKLIST;
|
|
58
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Statements } from "../database.js";
|
|
3
|
+
export declare const InitProjectSchema: z.ZodObject<{
|
|
4
|
+
directory: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
directory?: string | undefined;
|
|
7
|
+
}, {
|
|
8
|
+
directory?: string | undefined;
|
|
9
|
+
}>;
|
|
10
|
+
export type InitProjectInput = z.infer<typeof InitProjectSchema>;
|
|
11
|
+
export declare function handleInitProject(stmts: Statements, input: InitProjectInput): string;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolve, join } from "path";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
+
import { indexProject } from "../indexer/project.js";
|
|
5
|
+
export const InitProjectSchema = z.object({
|
|
6
|
+
directory: z.string().optional(),
|
|
7
|
+
});
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Instalează PostToolUse hook în .claude/settings.json
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
function installHook(dir) {
|
|
12
|
+
const claudeDir = join(dir, ".claude");
|
|
13
|
+
const settingsPath = join(claudeDir, "settings.json");
|
|
14
|
+
const HOOK_CMD = 'node -e "const p=process.argv[1]; if(p) require(\'child_process\').execSync(\'node \'+require(\'path\').resolve(\'node_modules/.bin/lucid\'||\'\')+\' --noop\', {stdio:\'ignore\'})" "$TOOL_INPUT_PATH" 2>/dev/null || true';
|
|
15
|
+
// Hook mai simplu și portabil: apelează sync_file prin claude mcp
|
|
16
|
+
const HOOK = {
|
|
17
|
+
matcher: "Write|Edit|NotebookEdit",
|
|
18
|
+
command: "echo '{\"tool\":\"sync_file\",\"path\":\"'\"$TOOL_INPUT_PATH\"'\"}' | true",
|
|
19
|
+
};
|
|
20
|
+
// Citim sau cream settings.json
|
|
21
|
+
let settings = {};
|
|
22
|
+
if (existsSync(settingsPath)) {
|
|
23
|
+
try {
|
|
24
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return { installed: false, reason: "Could not parse .claude/settings.json" };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Verifică dacă hook-ul e deja instalat
|
|
31
|
+
const hooks = settings["hooks"] ?? {};
|
|
32
|
+
const postToolUse = hooks["PostToolUse"] ?? [];
|
|
33
|
+
const alreadyInstalled = postToolUse.some((h) => h.matcher?.includes("Write") && String(h).includes("lucid"));
|
|
34
|
+
if (alreadyInstalled) {
|
|
35
|
+
return { installed: false, reason: "already installed" };
|
|
36
|
+
}
|
|
37
|
+
// Adaugă hook-ul — notifică Claude să cheme sync_file
|
|
38
|
+
const lucidHook = {
|
|
39
|
+
matcher: "Write|Edit|NotebookEdit",
|
|
40
|
+
command: "echo '🔄 Lucid: call sync_file(path) to keep knowledge graph up to date'",
|
|
41
|
+
};
|
|
42
|
+
hooks["PostToolUse"] = [...postToolUse, lucidHook];
|
|
43
|
+
settings["hooks"] = hooks;
|
|
44
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
45
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
46
|
+
return { installed: true, reason: "hook added to .claude/settings.json" };
|
|
47
|
+
}
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Adaugă instrucțiune în CLAUDE.md
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
const LUCID_SYNC_INSTRUCTION = `
|
|
52
|
+
<!-- LUCID_SYNC -->
|
|
53
|
+
## 🔄 Lucid — Auto-sync after file changes
|
|
54
|
+
|
|
55
|
+
After EVERY Write, Edit, or NotebookEdit tool call, you MUST immediately call:
|
|
56
|
+
\`\`\`
|
|
57
|
+
sync_file(path="<path of the file you just wrote/edited>")
|
|
58
|
+
\`\`\`
|
|
59
|
+
|
|
60
|
+
This keeps the Lucid knowledge graph up to date with the latest code.
|
|
61
|
+
If multiple files changed (refactor, git pull), call sync_project() instead.
|
|
62
|
+
<!-- /LUCID_SYNC -->
|
|
63
|
+
`;
|
|
64
|
+
function injectClaudeMdInstruction(dir) {
|
|
65
|
+
const claudeMdPath = join(dir, "CLAUDE.md");
|
|
66
|
+
if (!existsSync(claudeMdPath))
|
|
67
|
+
return false;
|
|
68
|
+
const content = readFileSync(claudeMdPath, "utf-8");
|
|
69
|
+
if (content.includes("LUCID_SYNC"))
|
|
70
|
+
return false; // already injected
|
|
71
|
+
writeFileSync(claudeMdPath, content.trimEnd() + "\n" + LUCID_SYNC_INSTRUCTION, "utf-8");
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Main handler
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
export function handleInitProject(stmts, input) {
|
|
78
|
+
const dir = resolve(input.directory ?? process.cwd());
|
|
79
|
+
const results = indexProject(dir, stmts);
|
|
80
|
+
const lines = [`✅ Project indexed: ${dir}`, ``];
|
|
81
|
+
if (results.length === 0) {
|
|
82
|
+
lines.push("No indexable files found.");
|
|
83
|
+
lines.push("Expected: CLAUDE.md, package.json, README.md, src/");
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
lines.push(`Indexed ${results.length} source(s):`);
|
|
87
|
+
for (const r of results) {
|
|
88
|
+
lines.push(` • [${r.type}] "${r.entity}" — ${r.observations} observation(s) from ${r.source}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Instalează hook PostToolUse
|
|
92
|
+
lines.push(``);
|
|
93
|
+
const hookResult = installHook(dir);
|
|
94
|
+
if (hookResult.installed) {
|
|
95
|
+
lines.push(`🔗 Claude Code hook installed (.claude/settings.json)`);
|
|
96
|
+
lines.push(` After every Write/Edit, you will see a reminder to call sync_file().`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
lines.push(`🔗 Hook: ${hookResult.reason}`);
|
|
100
|
+
}
|
|
101
|
+
// Injectează instrucțiune în CLAUDE.md
|
|
102
|
+
const injected = injectClaudeMdInstruction(dir);
|
|
103
|
+
if (injected) {
|
|
104
|
+
lines.push(`📋 CLAUDE.md updated with sync_file() instruction`);
|
|
105
|
+
}
|
|
106
|
+
lines.push(``);
|
|
107
|
+
lines.push(`From now on, call sync_file(path) after every file you write or edit.`);
|
|
108
|
+
lines.push(`Use recall() to query accumulated project knowledge.`);
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Statements } from "../database.js";
|
|
3
|
+
export declare const SyncFileSchema: z.ZodObject<{
|
|
4
|
+
path: z.ZodString;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
path: string;
|
|
7
|
+
}, {
|
|
8
|
+
path: string;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function handleSyncFile(stmts: Statements, args: z.infer<typeof SyncFileSchema>): string;
|
|
11
|
+
export declare const SyncProjectSchema: z.ZodObject<{
|
|
12
|
+
directory: z.ZodOptional<z.ZodString>;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
directory?: string | undefined;
|
|
15
|
+
}, {
|
|
16
|
+
directory?: string | undefined;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function handleSyncProject(stmts: Statements, args: z.infer<typeof SyncProjectSchema>): string;
|