@a13xu/lucid 1.9.4 → 1.10.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/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
@@ -36,6 +36,27 @@ export interface FileRewardRow {
36
36
  use_count: number;
37
37
  last_rewarded: number | null;
38
38
  }
39
+ export interface PlanRow {
40
+ id: number;
41
+ title: string;
42
+ description: string;
43
+ user_story: string;
44
+ status: string;
45
+ created_at: number;
46
+ updated_at: number;
47
+ }
48
+ export interface PlanTaskRow {
49
+ id: number;
50
+ plan_id: number;
51
+ seq: number;
52
+ title: string;
53
+ description: string;
54
+ test_criteria: string;
55
+ status: string;
56
+ notes: string;
57
+ created_at: number;
58
+ updated_at: number;
59
+ }
39
60
  export interface Statements {
40
61
  getFileByPath: Stmt<[string], FileContentRow>;
41
62
  upsertFile: WriteStmt<[string, Buffer, string, number, number, string]>;
@@ -83,6 +104,17 @@ export interface Statements {
83
104
  upsertFileReward: WriteStmt<[string, number]>;
84
105
  getFileRewards: Stmt<[], FileRewardRow>;
85
106
  getTopFileRewards: Stmt<[number], FileRewardRow>;
107
+ insertPlan: WriteStmt<[string, string, string]>;
108
+ getPlanById: Stmt<[number], PlanRow>;
109
+ getAllPlans: Stmt<[], PlanRow>;
110
+ updatePlanStatus: WriteStmt<[string, number]>;
111
+ insertPlanTask: WriteStmt<[number, number, string, string, string]>;
112
+ getTasksByPlanId: Stmt<[number], PlanTaskRow>;
113
+ getTaskById: Stmt<[number], PlanTaskRow>;
114
+ updateTaskStatus: WriteStmt<[string, string, number]>;
115
+ countRemainingTasks: Stmt<[number], {
116
+ count: number;
117
+ }>;
86
118
  }
87
119
  export declare function prepareStatements(db: Database.Database): Statements;
88
120
  export {};
package/build/database.js CHANGED
@@ -143,6 +143,33 @@ function createSchema(db) {
143
143
  use_count INTEGER NOT NULL DEFAULT 0,
144
144
  last_rewarded INTEGER
145
145
  );
146
+
147
+ -- Planning tables
148
+ CREATE TABLE IF NOT EXISTS plans (
149
+ id INTEGER PRIMARY KEY,
150
+ title TEXT NOT NULL,
151
+ description TEXT NOT NULL,
152
+ user_story TEXT NOT NULL,
153
+ status TEXT NOT NULL DEFAULT 'active',
154
+ created_at INTEGER DEFAULT (unixepoch()),
155
+ updated_at INTEGER DEFAULT (unixepoch())
156
+ );
157
+
158
+ CREATE TABLE IF NOT EXISTS plan_tasks (
159
+ id INTEGER PRIMARY KEY,
160
+ plan_id INTEGER NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
161
+ seq INTEGER NOT NULL,
162
+ title TEXT NOT NULL,
163
+ description TEXT NOT NULL,
164
+ test_criteria TEXT NOT NULL,
165
+ status TEXT NOT NULL DEFAULT 'pending',
166
+ notes TEXT NOT NULL DEFAULT '[]',
167
+ created_at INTEGER DEFAULT (unixepoch()),
168
+ updated_at INTEGER DEFAULT (unixepoch())
169
+ );
170
+
171
+ CREATE INDEX IF NOT EXISTS idx_plan_tasks_plan ON plan_tasks(plan_id, seq);
172
+ CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status);
146
173
  `);
147
174
  }
148
175
  export function prepareStatements(db) {
@@ -223,5 +250,16 @@ export function prepareStatements(db) {
223
250
  last_rewarded = unixepoch()`),
224
251
  getFileRewards: db.prepare("SELECT * FROM file_rewards"),
225
252
  getTopFileRewards: db.prepare("SELECT * FROM file_rewards WHERE total_reward > 0 ORDER BY total_reward DESC LIMIT ?"),
253
+ // plans
254
+ insertPlan: db.prepare("INSERT INTO plans (title, description, user_story) VALUES (?, ?, ?)"),
255
+ getPlanById: db.prepare("SELECT * FROM plans WHERE id = ?"),
256
+ getAllPlans: db.prepare("SELECT * FROM plans ORDER BY created_at DESC"),
257
+ updatePlanStatus: db.prepare("UPDATE plans SET status = ?, updated_at = unixepoch() WHERE id = ?"),
258
+ // plan_tasks
259
+ insertPlanTask: db.prepare("INSERT INTO plan_tasks (plan_id, seq, title, description, test_criteria) VALUES (?, ?, ?, ?, ?)"),
260
+ getTasksByPlanId: db.prepare("SELECT * FROM plan_tasks WHERE plan_id = ? ORDER BY seq"),
261
+ getTaskById: db.prepare("SELECT * FROM plan_tasks WHERE id = ?"),
262
+ updateTaskStatus: db.prepare("UPDATE plan_tasks SET status = ?, notes = ?, updated_at = unixepoch() WHERE id = ?"),
263
+ countRemainingTasks: db.prepare("SELECT COUNT(*) as count FROM plan_tasks WHERE plan_id = ? AND status != 'done'"),
226
264
  };
227
265
  }
package/build/index.js CHANGED
@@ -20,6 +20,7 @@ import { handleSyncFile, SyncFileSchema, handleSyncProject, SyncProjectSchema, }
20
20
  import { handleGetContext, GetContextSchema, handleGetRecent, GetRecentSchema, } from "./tools/context.js";
21
21
  import { handleReward, RewardSchema, handlePenalize, PenalizeSchema, handleShowRewards, ShowRewardsSchema, } from "./tools/reward.js";
22
22
  import { handleGetCodingRules, handleCheckCodeQuality, CheckCodeQualitySchema, } from "./tools/coding-guard.js";
23
+ import { handlePlanCreate, PlanCreateSchema, handlePlanList, PlanListSchema, handlePlanGet, PlanGetSchema, handlePlanUpdateTask, PlanUpdateTaskSchema, } from "./tools/plan.js";
23
24
  // ---------------------------------------------------------------------------
24
25
  // Init DB
25
26
  // ---------------------------------------------------------------------------
@@ -52,7 +53,7 @@ else {
52
53
  // ---------------------------------------------------------------------------
53
54
  // MCP Server
54
55
  // ---------------------------------------------------------------------------
55
- const server = new Server({ name: "lucid", version: "1.9.4" }, { capabilities: { tools: {} } });
56
+ const server = new Server({ name: "lucid", version: "1.10.0" }, { capabilities: { tools: {} } });
56
57
  // ---------------------------------------------------------------------------
57
58
  // Tool definitions
58
59
  // ---------------------------------------------------------------------------
@@ -322,6 +323,75 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
322
323
  },
323
324
  },
324
325
  },
326
+ // ── Planning ─────────────────────────────────────────────────────────────
327
+ {
328
+ name: "plan_create",
329
+ description: "Create a plan with user story, ordered tasks, and test criteria. " +
330
+ "Call BEFORE writing any code to establish intent and acceptance criteria.",
331
+ inputSchema: {
332
+ type: "object",
333
+ properties: {
334
+ title: { type: "string", description: "Short plan title." },
335
+ description: { type: "string", description: "What this plan accomplishes." },
336
+ user_story: { type: "string", description: "As a [user], I want [goal], so that [benefit]." },
337
+ tasks: {
338
+ type: "array",
339
+ description: "Ordered list of implementation tasks (1–20).",
340
+ items: {
341
+ type: "object",
342
+ properties: {
343
+ title: { type: "string" },
344
+ description: { type: "string" },
345
+ test_criteria: { type: "string", description: "How to verify this task is done." },
346
+ },
347
+ required: ["title", "description", "test_criteria"],
348
+ },
349
+ minItems: 1,
350
+ maxItems: 20,
351
+ },
352
+ },
353
+ required: ["title", "description", "user_story", "tasks"],
354
+ },
355
+ },
356
+ {
357
+ name: "plan_list",
358
+ description: "List plans with progress summary. Defaults to active plans.",
359
+ inputSchema: {
360
+ type: "object",
361
+ properties: {
362
+ status: {
363
+ type: "string",
364
+ enum: ["active", "completed", "abandoned", "all"],
365
+ description: "Filter by plan status (default: active).",
366
+ },
367
+ },
368
+ },
369
+ },
370
+ {
371
+ name: "plan_get",
372
+ description: "Get full plan details: tasks, test criteria, status, and notes.",
373
+ inputSchema: {
374
+ type: "object",
375
+ properties: {
376
+ plan_id: { type: "number", description: "Plan ID from plan_create or plan_list." },
377
+ },
378
+ required: ["plan_id"],
379
+ },
380
+ },
381
+ {
382
+ name: "plan_update_task",
383
+ description: "Update a task status. Auto-completes the plan when all tasks are done. " +
384
+ "Statuses: pending → in_progress → done (or blocked).",
385
+ inputSchema: {
386
+ type: "object",
387
+ properties: {
388
+ task_id: { type: "number", description: "Task ID from plan_get." },
389
+ status: { type: "string", enum: ["pending", "in_progress", "done", "blocked"] },
390
+ note: { type: "string", description: "Optional note appended to task history." },
391
+ },
392
+ required: ["task_id", "status"],
393
+ },
394
+ },
325
395
  ],
326
396
  }));
327
397
  // ---------------------------------------------------------------------------
@@ -404,6 +474,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
404
474
  case "check_code_quality":
405
475
  text = handleCheckCodeQuality(CheckCodeQualitySchema.parse(args));
406
476
  break;
477
+ // Planning
478
+ case "plan_create":
479
+ text = handlePlanCreate(db, stmts, PlanCreateSchema.parse(args));
480
+ break;
481
+ case "plan_list":
482
+ text = handlePlanList(stmts, PlanListSchema.parse(args));
483
+ break;
484
+ case "plan_get":
485
+ text = handlePlanGet(stmts, PlanGetSchema.parse(args));
486
+ break;
487
+ case "plan_update_task":
488
+ text = handlePlanUpdateTask(stmts, PlanUpdateTaskSchema.parse(args));
489
+ break;
407
490
  default:
408
491
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
409
492
  }
@@ -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[];
@@ -73,14 +73,8 @@ function extractGeneric(source) {
73
73
  }
74
74
  return { exports: [], description: "", todos };
75
75
  }
76
- export function indexFile(filepath) {
77
- let source;
78
- try {
79
- source = readFileSync(filepath, { encoding: "utf-8" });
80
- }
81
- catch {
82
- return null;
83
- }
76
+ /** Build a FileIndex from already-read source — no IO. */
77
+ export function buildFileIndex(filepath, source) {
84
78
  const ext = extname(filepath).toLowerCase();
85
79
  const module = filepath.replace(/\\/g, "/");
86
80
  let extracted;
@@ -103,6 +97,16 @@ export function indexFile(filepath) {
103
97
  }
104
98
  return { module, language, ...extracted };
105
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
+ }
106
110
  export function upsertFileIndex(index, source, stmts) {
107
111
  const fileHash = sha256(source);
108
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
  // ---------------------------------------------------------------------------
@@ -232,54 +233,23 @@ const MAX_SOURCE_FILES = 10_000;
232
233
  function indexSourceFile(filepath, rootDir, projectName, stmts) {
233
234
  const content = readFile(filepath);
234
235
  if (!content)
235
- return [];
236
- const exports = [];
237
- const lang = extname(filepath);
238
- // TypeScript / JavaScript
239
- if ([".ts", ".tsx", ".js", ".jsx"].includes(lang)) {
240
- for (const m of content.matchAll(/export\s+(?:async\s+)?(?:function|class|const|type|interface)\s+(\w+)/g)) {
241
- exports.push(m[1]);
242
- }
243
- }
244
- // Python
245
- if (lang === ".py") {
246
- for (const m of content.matchAll(/^(?:def|class|async def)\s+(\w+)/gm)) {
247
- if (!m[1].startsWith("_"))
248
- exports.push(m[1]);
249
- }
250
- }
251
- // Vue SFC — extract component name + defineExpose + named exports from <script>
252
- if (lang === ".vue") {
253
- // Component name from filename (always present)
254
- exports.push(basename(filepath, ".vue"));
255
- const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
256
- if (scriptMatch) {
257
- const sc = scriptMatch[1];
258
- // defineExpose({ foo, bar }) — what the component exposes to parents via template refs
259
- const exposeMatch = sc.match(/defineExpose\(\s*\{([^}]+)\}/);
260
- if (exposeMatch) {
261
- for (const m of exposeMatch[1].matchAll(/\b([a-zA-Z_]\w*)\b/g)) {
262
- if (!["true", "false", "null", "undefined"].includes(m[1]))
263
- exports.push(m[1]);
264
- }
265
- }
266
- // Named exports (composables, types re-exported from SFC)
267
- for (const m of sc.matchAll(/export\s+(?:async\s+)?(?:function|class|const|type|interface)\s+(\w+)/g)) {
268
- exports.push(m[1]);
269
- }
270
- }
271
- }
272
- if (exports.length === 0)
273
- return [];
274
- // Cale relativă față de rădăcina proiectului
275
- const relPath = filepath.replace(/\\/g, "/").replace(rootDir.replace(/\\/g, "/") + "/", "");
276
- const obs = [`exports from ${relPath}: ${exports.slice(0, 10).join(", ")}`];
277
- upsert(stmts, projectName, "project", obs);
278
- return exports;
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);
246
+ }
247
+ return { exports: fileIdx.exports, stored: result.stored };
279
248
  }
280
249
  function scanSources(dir, projectName, stmts, results) {
281
250
  const rootDir = dir.replace(/\\/g, "/");
282
251
  let fileCount = 0;
252
+ let storedCount = 0;
283
253
  const exportedSymbols = [];
284
254
  function walk(d) {
285
255
  if (fileCount >= MAX_SOURCE_FILES)
@@ -306,9 +276,11 @@ function scanSources(dir, projectName, stmts, results) {
306
276
  walk(full);
307
277
  }
308
278
  else if (SOURCE_EXTS.has(extname(entry).toLowerCase())) {
309
- const syms = indexSourceFile(full, rootDir, projectName, stmts);
279
+ const { exports: syms, stored } = indexSourceFile(full, rootDir, projectName, stmts);
310
280
  exportedSymbols.push(...syms);
311
281
  fileCount++;
282
+ if (stored)
283
+ storedCount++;
312
284
  if (fileCount >= MAX_SOURCE_FILES)
313
285
  return;
314
286
  }
@@ -320,7 +292,7 @@ function scanSources(dir, projectName, stmts, results) {
320
292
  entity: projectName,
321
293
  type: "project",
322
294
  observations: fileCount,
323
- source: `${fileCount} source files (${exportedSymbols.length} exports)`,
295
+ source: `${fileCount} source files (${storedCount} compressed, ${exportedSymbols.length} exports)`,
324
296
  });
325
297
  }
326
298
  }
@@ -0,0 +1,75 @@
1
+ import Database from "better-sqlite3";
2
+ import { z } from "zod";
3
+ import type { Statements } from "../database.js";
4
+ export declare const PlanCreateSchema: z.ZodObject<{
5
+ title: z.ZodString;
6
+ description: z.ZodString;
7
+ user_story: z.ZodString;
8
+ tasks: z.ZodArray<z.ZodObject<{
9
+ title: z.ZodString;
10
+ description: z.ZodString;
11
+ test_criteria: z.ZodString;
12
+ }, "strip", z.ZodTypeAny, {
13
+ description: string;
14
+ title: string;
15
+ test_criteria: string;
16
+ }, {
17
+ description: string;
18
+ title: string;
19
+ test_criteria: string;
20
+ }>, "many">;
21
+ }, "strip", z.ZodTypeAny, {
22
+ description: string;
23
+ title: string;
24
+ user_story: string;
25
+ tasks: {
26
+ description: string;
27
+ title: string;
28
+ test_criteria: string;
29
+ }[];
30
+ }, {
31
+ description: string;
32
+ title: string;
33
+ user_story: string;
34
+ tasks: {
35
+ description: string;
36
+ title: string;
37
+ test_criteria: string;
38
+ }[];
39
+ }>;
40
+ export declare const PlanListSchema: z.ZodObject<{
41
+ status: z.ZodDefault<z.ZodOptional<z.ZodEnum<["active", "completed", "abandoned", "all"]>>>;
42
+ }, "strip", z.ZodTypeAny, {
43
+ status: "all" | "active" | "completed" | "abandoned";
44
+ }, {
45
+ status?: "all" | "active" | "completed" | "abandoned" | undefined;
46
+ }>;
47
+ export declare const PlanGetSchema: z.ZodObject<{
48
+ plan_id: z.ZodNumber;
49
+ }, "strip", z.ZodTypeAny, {
50
+ plan_id: number;
51
+ }, {
52
+ plan_id: number;
53
+ }>;
54
+ export declare const PlanUpdateTaskSchema: z.ZodObject<{
55
+ task_id: z.ZodNumber;
56
+ status: z.ZodEnum<["pending", "in_progress", "done", "blocked"]>;
57
+ note: z.ZodOptional<z.ZodString>;
58
+ }, "strip", z.ZodTypeAny, {
59
+ status: "blocked" | "done" | "pending" | "in_progress";
60
+ task_id: number;
61
+ note?: string | undefined;
62
+ }, {
63
+ status: "blocked" | "done" | "pending" | "in_progress";
64
+ task_id: number;
65
+ note?: string | undefined;
66
+ }>;
67
+ type PlanCreateArgs = z.infer<typeof PlanCreateSchema>;
68
+ type PlanListArgs = z.infer<typeof PlanListSchema>;
69
+ type PlanGetArgs = z.infer<typeof PlanGetSchema>;
70
+ type PlanUpdateTaskArgs = z.infer<typeof PlanUpdateTaskSchema>;
71
+ export declare function handlePlanCreate(db: Database.Database, stmts: Statements, args: PlanCreateArgs): string;
72
+ export declare function handlePlanList(stmts: Statements, args: PlanListArgs): string;
73
+ export declare function handlePlanGet(stmts: Statements, args: PlanGetArgs): string;
74
+ export declare function handlePlanUpdateTask(stmts: Statements, args: PlanUpdateTaskArgs): string;
75
+ export {};
@@ -0,0 +1,148 @@
1
+ import { z } from "zod";
2
+ // ---------------------------------------------------------------------------
3
+ // Zod schemas
4
+ // ---------------------------------------------------------------------------
5
+ export const PlanCreateSchema = z.object({
6
+ title: z.string().min(1),
7
+ description: z.string().min(1),
8
+ user_story: z.string().min(1).describe("As a [user], I want [goal], so that [benefit]"),
9
+ tasks: z.array(z.object({
10
+ title: z.string().min(1),
11
+ description: z.string().min(1),
12
+ test_criteria: z.string().min(1),
13
+ })).min(1).max(20),
14
+ });
15
+ export const PlanListSchema = z.object({
16
+ status: z.enum(["active", "completed", "abandoned", "all"]).optional().default("active"),
17
+ });
18
+ export const PlanGetSchema = z.object({
19
+ plan_id: z.number().int().positive(),
20
+ });
21
+ export const PlanUpdateTaskSchema = z.object({
22
+ task_id: z.number().int().positive(),
23
+ status: z.enum(["pending", "in_progress", "done", "blocked"]),
24
+ note: z.string().optional(),
25
+ });
26
+ // ---------------------------------------------------------------------------
27
+ // Helpers
28
+ // ---------------------------------------------------------------------------
29
+ const STATUS_ICONS = {
30
+ pending: "⬜",
31
+ in_progress: "🔄",
32
+ done: "✅",
33
+ blocked: "🚫",
34
+ };
35
+ const PLAN_STATUS_ICONS = {
36
+ active: "active",
37
+ completed: "completed",
38
+ abandoned: "abandoned",
39
+ };
40
+ function progressBar(done, total) {
41
+ if (total === 0)
42
+ return "░".repeat(10);
43
+ const filled = Math.round((done / total) * 10);
44
+ return "█".repeat(filled) + "░".repeat(10 - filled);
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // Handlers
48
+ // ---------------------------------------------------------------------------
49
+ export function handlePlanCreate(db, stmts, args) {
50
+ const { title, description, user_story, tasks } = args;
51
+ const planId = db.transaction(() => {
52
+ const result = stmts.insertPlan.run(title, description, user_story);
53
+ const id = result.lastInsertRowid;
54
+ for (let i = 0; i < tasks.length; i++) {
55
+ const t = tasks[i];
56
+ stmts.insertPlanTask.run(id, i + 1, t.title, t.description, t.test_criteria);
57
+ }
58
+ return id;
59
+ })();
60
+ const lines = [
61
+ `[PLAN #${planId} active] ${title}`,
62
+ `User Story: ${user_story}`,
63
+ ``,
64
+ ];
65
+ for (let i = 0; i < tasks.length; i++) {
66
+ lines.push(`[TASK ${i + 1} #${planId * 100 + i + 1} pending] ${tasks[i].title}`);
67
+ }
68
+ lines.push(``, `Progress: 0/${tasks.length} done`);
69
+ return lines.join("\n");
70
+ }
71
+ export function handlePlanList(stmts, args) {
72
+ const { status } = args;
73
+ const all = stmts.getAllPlans.all();
74
+ const filtered = status === "all" ? all : all.filter(p => p.status === status);
75
+ if (filtered.length === 0) {
76
+ return `No ${status === "all" ? "" : status + " "}plans found.`;
77
+ }
78
+ const lines = [];
79
+ for (const plan of filtered) {
80
+ const tasks = stmts.getTasksByPlanId.all(plan.id);
81
+ const doneCount = tasks.filter(t => t.status === "done").length;
82
+ const label = PLAN_STATUS_ICONS[plan.status] ?? plan.status;
83
+ lines.push(`[#${plan.id} ${label}] ${plan.title} — ${doneCount}/${tasks.length} tasks done`);
84
+ }
85
+ return lines.join("\n");
86
+ }
87
+ export function handlePlanGet(stmts, args) {
88
+ const plan = stmts.getPlanById.get(args.plan_id);
89
+ if (!plan)
90
+ return `Error: Plan #${args.plan_id} not found.`;
91
+ const tasks = stmts.getTasksByPlanId.all(plan.id);
92
+ const doneCount = tasks.filter(t => t.status === "done").length;
93
+ const total = tasks.length;
94
+ const bar = progressBar(doneCount, total);
95
+ const label = PLAN_STATUS_ICONS[plan.status] ?? plan.status;
96
+ const lines = [
97
+ `[PLAN #${plan.id} | ${label}] ${plan.title}`,
98
+ `User Story: ${plan.user_story}`,
99
+ `Progress: ${doneCount}/${total} done ${bar}`,
100
+ ``,
101
+ ];
102
+ for (const task of tasks) {
103
+ const icon = STATUS_ICONS[task.status] ?? "❓";
104
+ lines.push(`[${task.seq}] ${icon} ${task.status} — ${task.title}`);
105
+ lines.push(` Desc: ${task.description}`);
106
+ lines.push(` Test: ${task.test_criteria}`);
107
+ let parsedNotes = [];
108
+ try {
109
+ parsedNotes = JSON.parse(task.notes);
110
+ }
111
+ catch { /* ignore */ }
112
+ for (const n of parsedNotes) {
113
+ const date = new Date(n.ts * 1000).toISOString().slice(0, 10);
114
+ lines.push(` Note: ${date} — ${n.text}`);
115
+ }
116
+ lines.push(``);
117
+ }
118
+ return lines.join("\n").trimEnd();
119
+ }
120
+ export function handlePlanUpdateTask(stmts, args) {
121
+ const { task_id, status, note } = args;
122
+ const task = stmts.getTaskById.get(task_id);
123
+ if (!task)
124
+ return `Error: Task #${task_id} not found.`;
125
+ let notes = [];
126
+ try {
127
+ notes = JSON.parse(task.notes);
128
+ }
129
+ catch { /* ignore */ }
130
+ if (note) {
131
+ notes.push({ text: note, ts: Math.floor(Date.now() / 1000) });
132
+ }
133
+ const notesJson = JSON.stringify(notes);
134
+ stmts.updateTaskStatus.run(status, notesJson, task_id);
135
+ const lines = [`✅ Task #${task_id} → ${status}`];
136
+ if (status === "done") {
137
+ const remaining = stmts.countRemainingTasks.get(task.plan_id);
138
+ if (remaining && remaining.count === 0) {
139
+ stmts.updatePlanStatus.run("completed", task.plan_id);
140
+ const plan = stmts.getPlanById.get(task.plan_id);
141
+ const taskCount = stmts.getTasksByPlanId.all(task.plan_id).length;
142
+ lines.push(`🎉 Plan #${task.plan_id} completat! Toate ${taskCount} task-uri done.`);
143
+ if (plan)
144
+ lines.push(` "${plan.title}"`);
145
+ }
146
+ }
147
+ return lines.join("\n");
148
+ }
@@ -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.4",
3
+ "version": "1.10.0",
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,6 +39,9 @@
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
  },