@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.
@@ -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,3 @@
1
+ export declare function compress(source: string): Buffer;
2
+ export declare function decompress(blob: Buffer): string;
3
+ export declare function sha256(source: string): string;
@@ -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;