@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 +16 -1
- package/build/database.d.ts +32 -0
- package/build/database.js +38 -0
- package/build/index.js +84 -1
- package/build/indexer/file.d.ts +2 -0
- package/build/indexer/file.js +12 -8
- package/build/indexer/project.js +18 -46
- package/build/tools/plan.d.ts +75 -0
- package/build/tools/plan.js +148 -0
- package/build/tools/sync.js +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# @a13xu/lucid
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@a13xu/lucid)
|
|
4
|
+
[](https://www.npmjs.com/package/@a13xu/lucid)
|
|
5
|
+
[](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/database.d.ts
CHANGED
|
@@ -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.
|
|
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
|
}
|
package/build/indexer/file.d.ts
CHANGED
|
@@ -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[];
|
package/build/indexer/file.js
CHANGED
|
@@ -73,14 +73,8 @@ function extractGeneric(source) {
|
|
|
73
73
|
}
|
|
74
74
|
return { exports: [], description: "", todos };
|
|
75
75
|
}
|
|
76
|
-
|
|
77
|
-
|
|
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
|
package/build/indexer/project.js
CHANGED
|
@@ -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
|
-
|
|
237
|
-
const
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
+
}
|
package/build/tools/sync.js
CHANGED
|
@@ -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.
|
|
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
|
},
|