@a13xu/lucid 1.9.2 → 1.9.5

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/README.md CHANGED
@@ -1,8 +1,14 @@
1
1
  # @a13xu/lucid
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@a13xu/lucid)](https://www.npmjs.com/package/@a13xu/lucid)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@a13xu/lucid)](https://www.npmjs.com/package/@a13xu/lucid)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ > **MCP server for Claude Code** — persistent memory, smart code indexing, and code quality validation. Works out of the box with zero configuration.
8
+
3
9
  Token-efficient memory, code indexing, and validation for Claude Code agents — backed by **SQLite + FTS5**.
4
10
 
5
- Stores a persistent knowledge graph (entities, relations, observations), indexes source files as compressed binary with change detection, retrieves minimal relevant context via TF-IDF or Qdrant, and validates code for LLM drift patterns.
11
+ Stores a persistent knowledge graph (entities, relations, observations), indexes source files as compressed binary with change detection, retrieves minimal relevant context via TF-IDF or Qdrant, and validates code for LLM drift patterns. Supports TypeScript, JavaScript, Python, **Vue, Nuxt**.
6
12
 
7
13
  ## Install
8
14
 
@@ -226,6 +232,15 @@ echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},
226
232
 
227
233
  In Claude Code: run `/mcp` — you should see `lucid` with 20 tools.
228
234
 
235
+ ## Contributing
236
+
237
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/a13xu/lucid).
238
+
239
+ 1. Fork the repo
240
+ 2. `npm install` → `npm run build`
241
+ 3. Test locally: `claude mcp add --transport stdio lucid-dev -- node /path/to/lucid/build/index.js`
242
+ 4. Open a PR
243
+
229
244
  ## Tech stack
230
245
 
231
246
  - **Runtime:** Node.js 18+, TypeScript, ES modules
package/build/index.js CHANGED
@@ -52,7 +52,7 @@ else {
52
52
  // ---------------------------------------------------------------------------
53
53
  // MCP Server
54
54
  // ---------------------------------------------------------------------------
55
- const server = new Server({ name: "lucid", version: "1.9.2" }, { capabilities: { tools: {} } });
55
+ const server = new Server({ name: "lucid", version: "1.9.5" }, { capabilities: { tools: {} } });
56
56
  // ---------------------------------------------------------------------------
57
57
  // Tool definitions
58
58
  // ---------------------------------------------------------------------------
@@ -178,7 +178,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
178
178
  type: "object",
179
179
  properties: {
180
180
  pattern: { type: "string", description: "Regex pattern to search for." },
181
- language: { type: "string", enum: ["python", "javascript", "typescript", "generic"], description: "Filter by language." },
181
+ language: { type: "string", enum: ["python", "javascript", "typescript", "vue", "generic"], description: "Filter by language." },
182
182
  context: { type: "number", description: "Lines of context before/after each match (0-10, default 2)." },
183
183
  },
184
184
  required: ["pattern"],
@@ -108,6 +108,32 @@ function skeletonPython(source) {
108
108
  return { imports, exports, todos, summary };
109
109
  }
110
110
  // ---------------------------------------------------------------------------
111
+ // Vue SFC
112
+ // ---------------------------------------------------------------------------
113
+ function skeletonVue(source) {
114
+ // Extract <script> or <script setup> block and run TS skeleton on it
115
+ const scriptMatch = source.match(/<script[^>]*>([\s\S]*?)<\/script>/);
116
+ const sk = scriptMatch ? skeletonTS(scriptMatch[1]) : { imports: [], exports: [], todos: [], summary: "" };
117
+ // Prepend Vue macro signatures (defineProps, defineEmits, defineExpose)
118
+ const scriptContent = scriptMatch?.[1] ?? "";
119
+ for (const macro of ["defineProps", "defineEmits", "defineExpose"]) {
120
+ const m = scriptContent.match(new RegExp(`${macro}[\\s\\S]*?(?=\\n\\n|\\n[^\\s]|$)`, "m"));
121
+ if (m)
122
+ sk.exports.unshift(m[0].split("\n")[0].slice(0, 120));
123
+ }
124
+ // HTML comment as summary fallback
125
+ if (!sk.summary) {
126
+ const htmlComment = source.match(/<!--\s*([\s\S]*?)\s*-->/)?.[1];
127
+ if (htmlComment)
128
+ sk.summary = htmlComment.replace(/\n/g, " ").trim().slice(0, 150);
129
+ }
130
+ // Also note top-level template structure (first tag inside <template>)
131
+ const templateMatch = source.match(/<template[^>]*>\s*<(\w[\w-]*)/);
132
+ if (templateMatch)
133
+ sk.exports.unshift(`<template> root: <${templateMatch[1]}>`);
134
+ return sk;
135
+ }
136
+ // ---------------------------------------------------------------------------
111
137
  // Generic (markdown, yaml, json, etc.)
112
138
  // ---------------------------------------------------------------------------
113
139
  function skeletonGeneric(source) {
@@ -135,6 +161,8 @@ export function extractSkeleton(source, language) {
135
161
  return skeletonTS(source);
136
162
  case "python":
137
163
  return skeletonPython(source);
164
+ case "vue":
165
+ return skeletonVue(source);
138
166
  default:
139
167
  return skeletonGeneric(source);
140
168
  }
@@ -6,6 +6,8 @@ export interface FileIndex {
6
6
  todos: string[];
7
7
  language: string;
8
8
  }
9
+ /** Build a FileIndex from already-read source — no IO. */
10
+ export declare function buildFileIndex(filepath: string, source: string): FileIndex;
9
11
  export declare function indexFile(filepath: string): FileIndex | null;
10
12
  export interface UpsertResult {
11
13
  observations: string[];
@@ -36,6 +36,36 @@ function extractPython(source) {
36
36
  }
37
37
  return { exports, description, todos };
38
38
  }
39
+ function extractVue(source) {
40
+ const exports = [];
41
+ const todos = [];
42
+ // Extract <script> or <script setup> block
43
+ const scriptMatch = source.match(/<script[^>]*>([\s\S]*?)<\/script>/);
44
+ const scriptContent = scriptMatch?.[1] ?? "";
45
+ // defineExpose({ foo, bar }) — symbols available to parent components
46
+ const exposeMatch = scriptContent.match(/defineExpose\(\s*\{([^}]+)\}/);
47
+ if (exposeMatch) {
48
+ for (const m of exposeMatch[1].matchAll(/\b([a-zA-Z_]\w*)\b/g)) {
49
+ if (!["true", "false", "null", "undefined"].includes(m[1])) {
50
+ exports.push(m[1]);
51
+ }
52
+ }
53
+ }
54
+ // Regular named exports inside <script> (non-setup composables, types, etc.)
55
+ for (const m of scriptContent.matchAll(/export\s+(?:async\s+)?(?:function|class|const|type|interface|enum)\s+(\w+)/g)) {
56
+ exports.push(m[1]);
57
+ }
58
+ // TODOs from entire SFC (template + script + style)
59
+ for (const m of source.matchAll(/\/\/\s*(TODO|FIXME|HACK)[:\s]+(.+)/gi)) {
60
+ todos.push(`${m[1]}: ${m[2].trim()}`);
61
+ }
62
+ // Description: first HTML comment, then first JSDoc in script
63
+ const htmlComment = source.match(/<!--\s*([\s\S]*?)\s*-->/)?.[1] ?? "";
64
+ const jsdoc = scriptContent.match(/^\/\*\*([\s\S]*?)\*\//m)?.[1] ?? "";
65
+ const raw = htmlComment || jsdoc;
66
+ const description = raw.replace(/\s*\*\s*/g, " ").trim().slice(0, 200);
67
+ return { exports, description, todos };
68
+ }
39
69
  function extractGeneric(source) {
40
70
  const todos = [];
41
71
  for (const m of source.matchAll(/(?:\/\/|#)\s*(TODO|FIXME|HACK)[:\s]+(.+)/gi)) {
@@ -43,14 +73,8 @@ function extractGeneric(source) {
43
73
  }
44
74
  return { exports: [], description: "", todos };
45
75
  }
46
- export function indexFile(filepath) {
47
- let source;
48
- try {
49
- source = readFileSync(filepath, { encoding: "utf-8" });
50
- }
51
- catch {
52
- return null;
53
- }
76
+ /** Build a FileIndex from already-read source — no IO. */
77
+ export function buildFileIndex(filepath, source) {
54
78
  const ext = extname(filepath).toLowerCase();
55
79
  const module = filepath.replace(/\\/g, "/");
56
80
  let extracted;
@@ -63,12 +87,26 @@ export function indexFile(filepath) {
63
87
  extracted = extractPython(source);
64
88
  language = "python";
65
89
  }
90
+ else if (ext === ".vue") {
91
+ extracted = extractVue(source);
92
+ language = "vue";
93
+ }
66
94
  else {
67
95
  extracted = extractGeneric(source);
68
96
  language = "generic";
69
97
  }
70
98
  return { module, language, ...extracted };
71
99
  }
100
+ export function indexFile(filepath) {
101
+ let source;
102
+ try {
103
+ source = readFileSync(filepath, { encoding: "utf-8" });
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ return buildFileIndex(filepath, source);
109
+ }
72
110
  export function upsertFileIndex(index, source, stmts) {
73
111
  const fileHash = sha256(source);
74
112
  // Change detection — skip everything se hash-ul e identic
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from "fs";
2
2
  import { join, extname, basename } from "path";
3
+ import { buildFileIndex, upsertFileIndex } from "./file.js";
3
4
  // ---------------------------------------------------------------------------
4
5
  // Helpers
5
6
  // ---------------------------------------------------------------------------
@@ -181,40 +182,74 @@ function indexLogicGuardianYaml(path, stmts, results) {
181
182
  results.push({ entity: `${patternMatches.length} drift patterns`, type: "pattern", observations: patternMatches.length, source: "logic-guardian.yaml" });
182
183
  }
183
184
  }
185
+ function indexNuxtConfig(path, projectName, stmts, results) {
186
+ const content = readFile(path);
187
+ if (!content)
188
+ return;
189
+ const obs = [`nuxt config: ${basename(path)}`];
190
+ // Modules list: modules: ['@nuxtjs/...', ...]
191
+ const modulesMatch = content.match(/modules\s*:\s*\[([^\]]+)\]/);
192
+ if (modulesMatch) {
193
+ const modules = [...modulesMatch[1].matchAll(/['"`](@?[\w/@-]+)['"`]/g)].map((m) => m[1]);
194
+ if (modules.length > 0)
195
+ obs.push(`nuxt modules: ${modules.join(", ")}`);
196
+ }
197
+ // extends (Nuxt layers)
198
+ const extendsMatch = content.match(/extends\s*:\s*\[?['"`]([^'"`]+)['"`]/);
199
+ if (extendsMatch)
200
+ obs.push(`extends layer: ${extendsMatch[1]}`);
201
+ // ssr setting
202
+ const ssrMatch = content.match(/ssr\s*:\s*(true|false)/);
203
+ if (ssrMatch)
204
+ obs.push(`ssr: ${ssrMatch[1]}`);
205
+ // runtimeConfig public keys
206
+ const rtMatch = content.match(/runtimeConfig\s*:\s*\{([\s\S]*?)(?=\n\s{0,4}\w|\n\})/);
207
+ if (rtMatch) {
208
+ const keys = [...rtMatch[1].matchAll(/^\s{2,}(\w+)\s*:/gm)].map((m) => m[1]);
209
+ if (keys.length > 0)
210
+ obs.push(`runtimeConfig keys: ${keys.slice(0, 10).join(", ")}`);
211
+ }
212
+ upsert(stmts, projectName, "project", obs);
213
+ results.push({ entity: projectName, type: "project", observations: obs.length, source: basename(path) });
214
+ }
184
215
  // 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", ".cache", "coverage", ".nyc_output"]);
216
+ const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".vue", ".py", ".go", ".rs"]);
217
+ const SKIP_DIRS = new Set([
218
+ "node_modules", ".git",
219
+ // Generic build output
220
+ "build", "dist",
221
+ // Python
222
+ "__pycache__", "venv", ".venv",
223
+ // Rust
224
+ "target",
225
+ // Next.js
226
+ ".next",
227
+ // Nuxt
228
+ ".nuxt", ".output",
229
+ // General
230
+ ".cache", "coverage", ".nyc_output",
231
+ ]);
187
232
  const MAX_SOURCE_FILES = 10_000;
188
233
  function indexSourceFile(filepath, rootDir, projectName, stmts) {
189
234
  const content = readFile(filepath);
190
235
  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
- }
236
+ return { exports: [], stored: false };
237
+ // Build structured index (language-aware, single read)
238
+ const fileIdx = buildFileIndex(filepath, content);
239
+ // Store compressed content in source file index → enables get_context() + grep_code()
240
+ const result = upsertFileIndex(fileIdx, content, stmts);
241
+ // Add exports to knowledge graph (for recall())
242
+ if (fileIdx.exports.length > 0) {
243
+ const relPath = filepath.replace(/\\/g, "/").replace(rootDir.replace(/\\/g, "/") + "/", "");
244
+ const obs = [`exports from ${relPath}: ${fileIdx.exports.slice(0, 10).join(", ")}`];
245
+ upsert(stmts, projectName, "project", obs);
206
246
  }
207
- if (exports.length === 0)
208
- return [];
209
- // Cale relativă față de rădăcina proiectului
210
- const relPath = filepath.replace(/\\/g, "/").replace(rootDir.replace(/\\/g, "/") + "/", "");
211
- const obs = [`exports from ${relPath}: ${exports.slice(0, 10).join(", ")}`];
212
- upsert(stmts, projectName, "project", obs);
213
- return exports;
247
+ return { exports: fileIdx.exports, stored: result.stored };
214
248
  }
215
249
  function scanSources(dir, projectName, stmts, results) {
216
250
  const rootDir = dir.replace(/\\/g, "/");
217
251
  let fileCount = 0;
252
+ let storedCount = 0;
218
253
  const exportedSymbols = [];
219
254
  function walk(d) {
220
255
  if (fileCount >= MAX_SOURCE_FILES)
@@ -241,9 +276,11 @@ function scanSources(dir, projectName, stmts, results) {
241
276
  walk(full);
242
277
  }
243
278
  else if (SOURCE_EXTS.has(extname(entry).toLowerCase())) {
244
- const syms = indexSourceFile(full, rootDir, projectName, stmts);
279
+ const { exports: syms, stored } = indexSourceFile(full, rootDir, projectName, stmts);
245
280
  exportedSymbols.push(...syms);
246
281
  fileCount++;
282
+ if (stored)
283
+ storedCount++;
247
284
  if (fileCount >= MAX_SOURCE_FILES)
248
285
  return;
249
286
  }
@@ -255,7 +292,7 @@ function scanSources(dir, projectName, stmts, results) {
255
292
  entity: projectName,
256
293
  type: "project",
257
294
  observations: fileCount,
258
- source: `${fileCount} source files (${exportedSymbols.length} exports)`,
295
+ source: `${fileCount} source files (${storedCount} compressed, ${exportedSymbols.length} exports)`,
259
296
  });
260
297
  }
261
298
  }
@@ -314,7 +351,22 @@ export function indexProject(directory, stmts) {
314
351
  if (existsSync(join(dir, "logic-guardian.yaml"))) {
315
352
  indexLogicGuardianYaml(join(dir, "logic-guardian.yaml"), stmts, results);
316
353
  }
317
- // 7. Surse
354
+ // 7. Nuxt config files (nuxt.config.ts/.js, app.config.ts)
355
+ for (const p of ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mts"]) {
356
+ const full = join(dir, p);
357
+ if (existsSync(full)) {
358
+ indexNuxtConfig(full, projectName, stmts, results);
359
+ break;
360
+ }
361
+ }
362
+ for (const p of ["app.config.ts", "app.config.js"]) {
363
+ const full = join(dir, p);
364
+ if (existsSync(full)) {
365
+ indexNuxtConfig(full, projectName, stmts, results);
366
+ break;
367
+ }
368
+ }
369
+ // 8. Surse
318
370
  scanSources(dir, projectName, stmts, results);
319
371
  return results;
320
372
  }
@@ -6,7 +6,7 @@ import { indexProject } from "../indexer/project.js";
6
6
  import { computeDiff } from "../retrieval/context.js";
7
7
  import { decompress } from "../store/content.js";
8
8
  import { implicitRewardFromSync } from "../memory/experience.js";
9
- const SUPPORTED_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
9
+ const SUPPORTED_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".vue", ".py", ".go", ".rs"]);
10
10
  // ---------------------------------------------------------------------------
11
11
  // sync_file
12
12
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a13xu/lucid",
3
- "version": "1.9.2",
3
+ "version": "1.9.5",
4
4
  "description": "Token-efficient memory, code indexing, and validation for Claude Code agents — SQLite + FTS5, TF-IDF + Qdrant retrieval, AST skeleton pruning, diff-aware context, Logic Guardian drift detection",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,17 +39,20 @@
39
39
  "url": "https://github.com/a13xu/lucid.git"
40
40
  },
41
41
  "homepage": "https://github.com/a13xu/lucid#readme",
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
42
45
  "engines": {
43
46
  "node": ">=18"
44
47
  },
45
48
  "dependencies": {
46
49
  "@modelcontextprotocol/sdk": "^1.0.0",
47
- "better-sqlite3": "^11.0.0",
50
+ "better-sqlite3": "^12.0.0",
48
51
  "zod": "^3.23.8"
49
52
  },
50
53
  "devDependencies": {
51
54
  "@types/better-sqlite3": "^7.6.11",
52
- "@types/node": "^20.0.0",
55
+ "@types/node": "^22.0.0",
53
56
  "typescript": "^5.4.0"
54
57
  }
55
58
  }