@a13xu/lucid 1.1.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.js +66 -66
- package/build/index.js +78 -0
- 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/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
package/build/index.js
CHANGED
|
@@ -11,6 +11,9 @@ 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
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";
|
|
14
17
|
// ---------------------------------------------------------------------------
|
|
15
18
|
// Init DB
|
|
16
19
|
// ---------------------------------------------------------------------------
|
|
@@ -90,6 +93,67 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
90
93
|
description: "Get memory usage statistics.",
|
|
91
94
|
inputSchema: { type: "object", properties: {} },
|
|
92
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
|
+
},
|
|
93
157
|
// ── Logic Guardian ───────────────────────────────────────────────────────
|
|
94
158
|
{
|
|
95
159
|
name: "validate_file",
|
|
@@ -156,6 +220,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
156
220
|
case "memory_stats":
|
|
157
221
|
text = memoryStats(db, stmts);
|
|
158
222
|
break;
|
|
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;
|
|
159
237
|
// Logic Guardian
|
|
160
238
|
case "validate_file":
|
|
161
239
|
text = handleValidateFile(ValidateFileSchema.parse(args));
|
|
@@ -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
|
+
}
|
|
@@ -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,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;
|