@a13xu/lucid 1.16.1 → 1.19.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.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/build/compression/semantic.js +5 -1
  3. package/build/database.d.ts +51 -0
  4. package/build/database.js +86 -0
  5. package/build/guardian/checklist.d.ts +2 -1
  6. package/build/guardian/checklist.js +20 -1
  7. package/build/guardian/coding-rules.d.ts +2 -1
  8. package/build/guardian/coding-rules.js +20 -1
  9. package/build/guardian/session-tracker.d.ts +34 -0
  10. package/build/guardian/session-tracker.js +105 -0
  11. package/build/guardian/truncate-guard.d.ts +54 -0
  12. package/build/guardian/truncate-guard.js +136 -0
  13. package/build/index.js +745 -742
  14. package/build/local-llm/client.d.ts +20 -0
  15. package/build/local-llm/client.js +140 -0
  16. package/build/local-llm/config.d.ts +11 -0
  17. package/build/local-llm/config.js +50 -0
  18. package/build/local-llm/runtimes.d.ts +16 -0
  19. package/build/local-llm/runtimes.js +82 -0
  20. package/build/local-llm/setup-cli.d.ts +5 -0
  21. package/build/local-llm/setup-cli.js +298 -0
  22. package/build/local-llm/types.d.ts +34 -0
  23. package/build/local-llm/types.js +5 -0
  24. package/build/tools/backup.d.ts +47 -0
  25. package/build/tools/backup.js +107 -0
  26. package/build/tools/delegate-local.d.ts +23 -0
  27. package/build/tools/delegate-local.js +75 -0
  28. package/build/tools/init.js +124 -2
  29. package/build/tools/plan.js +2 -2
  30. package/build/tools/session.d.ts +13 -0
  31. package/build/tools/session.js +59 -0
  32. package/package.json +3 -1
  33. package/skills/lucid-audit/SKILL.md +11 -0
  34. package/skills/lucid-context/SKILL.md +9 -0
  35. package/skills/lucid-plan/SKILL.md +9 -0
  36. package/skills/lucid-security/SKILL.md +9 -0
  37. package/skills/lucid-start/SKILL.md +9 -0
  38. package/skills/lucid-webdev/SKILL.md +14 -0
package/build/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
4
  import { z } from "zod";
6
5
  import { initDatabase, prepareStatements } from "./database.js";
7
6
  import { guardRequest, guardOutput, configureGuard } from "./security/guard.js";
@@ -26,6 +25,10 @@ import { GenerateComponentSchema, handleGenerateComponent, ScaffoldPageSchema, h
26
25
  import { handleSmartContext, SmartContextSchema } from "./tools/smart-context.js";
27
26
  import { handleSuggestModel, SuggestModelSchema } from "./tools/model-advisor.js";
28
27
  import { handleCompressText, CompressTextSchema } from "./tools/compress.js";
28
+ import { handleBackupFile, BackupFileSchema, handleRestoreFile, RestoreFileSchema, handleCheckTruncateRisk, CheckTruncateRiskSchema, } from "./tools/backup.js";
29
+ import { handleSessionStatus, SessionStatusSchema } from "./tools/session.js";
30
+ import { handleDelegateLocal, DelegateLocalSchema, handleLocalLlmStatus, LocalLlmStatusSchema, } from "./tools/delegate-local.js";
31
+ import { loadLocalConfig } from "./local-llm/config.js";
29
32
  // ---------------------------------------------------------------------------
30
33
  // CLI mode: lucid watch | lucid status | lucid stop
31
34
  // ---------------------------------------------------------------------------
@@ -34,6 +37,201 @@ if (_cliCmd === "watch" || _cliCmd === "status" || _cliCmd === "stop") {
34
37
  await runCli(_cliCmd, _cliArgs);
35
38
  process.exit(0);
36
39
  }
40
+ if (_cliCmd === "guard") {
41
+ const exitCode = await runGuardCli(_cliArgs);
42
+ process.exit(exitCode);
43
+ }
44
+ if (_cliCmd === "session") {
45
+ const exitCode = await runSessionCli(_cliArgs);
46
+ process.exit(exitCode);
47
+ }
48
+ if (_cliCmd === "local") {
49
+ const { runLocalLlmCli } = await import("./local-llm/setup-cli.js");
50
+ const exitCode = await runLocalLlmCli(_cliArgs);
51
+ process.exit(exitCode);
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // `lucid guard <subcmd>` — invoked from Claude Code hooks (PreToolUse, etc.)
55
+ //
56
+ // Subcommands:
57
+ // pre-edit Read PreToolUse JSON from stdin, snapshot the file,
58
+ // then assess truncate risk. Exit 2 = block (hard).
59
+ // pre-edit --path P Same, but path provided as flag (no stdin parse).
60
+ // clear Clear cascade lock by purging recent truncate_events.
61
+ // status Show cascade-lock status + last events.
62
+ // ---------------------------------------------------------------------------
63
+ async function runGuardCli(args) {
64
+ const sub = args[0];
65
+ const { initDatabase, prepareStatements } = await import("./database.js");
66
+ const db = initDatabase();
67
+ const stmts = prepareStatements(db);
68
+ if (sub === "clear") {
69
+ db.exec("DELETE FROM truncate_events");
70
+ process.stderr.write("[Lucid guard] Cascade lock cleared.\n");
71
+ return 0;
72
+ }
73
+ if (sub === "status") {
74
+ const { isCascadeBlocked, TUNABLES } = await import("./guardian/truncate-guard.js");
75
+ const cascade = isCascadeBlocked(stmts);
76
+ const since = Math.floor(Date.now() / 1000) - TUNABLES.CASCADE_WINDOW_SECONDS;
77
+ const events = stmts.recentTruncateEvents.all(since);
78
+ process.stdout.write(`Cascade locked: ${cascade.blocked} (${cascade.count}/${TUNABLES.CASCADE_THRESHOLD} ` +
79
+ `within ${TUNABLES.CASCADE_WINDOW_SECONDS}s)\n`);
80
+ for (const e of events) {
81
+ process.stdout.write(` ${new Date(e.created_at * 1000).toISOString()} ` +
82
+ `${e.filepath} ${e.prev_size}B→${e.new_size}B (${(e.shrink_ratio * 100).toFixed(0)}%)\n`);
83
+ }
84
+ return 0;
85
+ }
86
+ if (sub === "pre-edit") {
87
+ return await guardPreEdit(stmts, args.slice(1));
88
+ }
89
+ process.stderr.write(`Usage: lucid guard <pre-edit|clear|status>\n`);
90
+ return 64; // EX_USAGE
91
+ }
92
+ async function guardPreEdit(stmts, flagArgs) {
93
+ const { backupFile, assessTruncate, recordTruncateEvent } = await import("./guardian/truncate-guard.js");
94
+ // Override switch — never blocks. Useful for one-off legitimate truncates.
95
+ if (process.env["LUCID_TRUNCATE_OVERRIDE"] === "1")
96
+ return 0;
97
+ const pathFlagIdx = flagArgs.indexOf("--path");
98
+ let path = pathFlagIdx >= 0 ? flagArgs[pathFlagIdx + 1] : undefined;
99
+ let content = null;
100
+ let toolName = "Write";
101
+ // Try parsing PreToolUse JSON from stdin (Claude Code hook protocol).
102
+ // Skip stdin read when --path is given OR when stdin is a TTY (manual run).
103
+ if (!path && !process.stdin.isTTY) {
104
+ const raw = await readStdin();
105
+ if (raw.trim()) {
106
+ try {
107
+ const payload = JSON.parse(raw);
108
+ toolName = payload.tool_name ?? "Write";
109
+ const ti = payload.tool_input ?? {};
110
+ path = ti.file_path ?? ti.path;
111
+ if (typeof ti.content === "string")
112
+ content = ti.content;
113
+ else if (Array.isArray(ti.edits)) {
114
+ // MultiEdit — sum up final state crudely: use new_strings concatenated.
115
+ content = ti.edits.map((e) => e.new_string ?? "").join("\n");
116
+ }
117
+ else if (typeof ti.new_string === "string") {
118
+ content = ti.new_string;
119
+ }
120
+ }
121
+ catch {
122
+ // Non-JSON stdin — ignore, fall through to "no path" error.
123
+ }
124
+ }
125
+ }
126
+ if (!path) {
127
+ process.stderr.write("[Lucid guard] No file path in hook input — allowing.\n");
128
+ return 0;
129
+ }
130
+ // Snapshot BEFORE assessing — even if we end up blocking, we want the version
131
+ // that's about to be overwritten safely stored.
132
+ const snap = backupFile(stmts, path, `pre-${toolName.toLowerCase()}`);
133
+ if (snap.saved) {
134
+ process.stderr.write(`[Lucid guard] 📸 Snapshot stored for ${path}\n`);
135
+ }
136
+ const verdict = assessTruncate(path, content, stmts);
137
+ if (!verdict.blocked)
138
+ return 0;
139
+ recordTruncateEvent(stmts, path, verdict.prevSize, verdict.newSize, true);
140
+ // Exit code 2 → Claude Code blocks the tool call and surfaces stderr.
141
+ process.stderr.write(`🛑 [Lucid guard] BLOCK [${verdict.rule}] ${path}\n` +
142
+ ` ${verdict.reason}\n` +
143
+ (verdict.cascade
144
+ ? ` Override: set LUCID_TRUNCATE_OVERRIDE=1 or run "lucid guard clear".\n`
145
+ : ` prev=${verdict.prevSize}B new=${verdict.newSize}B keep=${(verdict.shrinkRatio * 100).toFixed(0)}%\n` +
146
+ ` Restore via: restore_file(path="${path}")\n`));
147
+ return 2;
148
+ }
149
+ // ---------------------------------------------------------------------------
150
+ // `lucid session <subcmd>` — invoked from UserPromptSubmit & PreCompact hooks.
151
+ //
152
+ // Subcommands:
153
+ // tick Read UserPromptSubmit JSON from stdin → emit hints to stdout
154
+ // (Claude Code injects stdout as additional context).
155
+ // compact Read PreCompact JSON from stdin → reset session counters.
156
+ // status Print recent sessions + active hint thresholds.
157
+ // reset [--id S] Reset a single session counter (or the latest if no --id).
158
+ // ---------------------------------------------------------------------------
159
+ async function runSessionCli(args) {
160
+ const sub = args[0];
161
+ const { initDatabase, prepareStatements } = await import("./database.js");
162
+ const db = initDatabase();
163
+ const stmts = prepareStatements(db);
164
+ if (sub === "tick")
165
+ return await sessionTickCli(stmts);
166
+ if (sub === "compact")
167
+ return await sessionCompactCli(stmts);
168
+ if (sub === "status") {
169
+ const { handleSessionStatus } = await import("./tools/session.js");
170
+ process.stdout.write(handleSessionStatus(stmts, { limit: 10 }) + "\n");
171
+ return 0;
172
+ }
173
+ if (sub === "reset") {
174
+ const idIdx = args.indexOf("--id");
175
+ if (idIdx >= 0 && args[idIdx + 1]) {
176
+ stmts.resetCliSessionCount.run(args[idIdx + 1]);
177
+ process.stderr.write(`[Lucid session] Reset counters for ${args[idIdx + 1]}\n`);
178
+ }
179
+ else {
180
+ const recent = stmts.recentCliSessions.all(1);
181
+ if (recent.length === 0) {
182
+ process.stderr.write("No sessions tracked.\n");
183
+ return 0;
184
+ }
185
+ stmts.resetCliSessionCount.run(recent[0].session_id);
186
+ process.stderr.write(`[Lucid session] Reset counters for ${recent[0].session_id}\n`);
187
+ }
188
+ return 0;
189
+ }
190
+ process.stderr.write(`Usage: lucid session <tick|compact|status|reset>\n`);
191
+ return 64;
192
+ }
193
+ async function sessionTickCli(stmts) {
194
+ const { tickSession } = await import("./guardian/session-tracker.js");
195
+ const payload = await readHookPayload();
196
+ const sid = payload?.session_id ?? "unknown-session";
197
+ const cwd = payload?.cwd ?? null;
198
+ const result = tickSession(stmts, sid, cwd);
199
+ // Emit hints to stdout — Claude Code injects them as additional context.
200
+ for (const h of result.hints)
201
+ process.stdout.write(h + "\n");
202
+ return 0;
203
+ }
204
+ async function sessionCompactCli(stmts) {
205
+ const { markCompactEvent } = await import("./guardian/session-tracker.js");
206
+ const payload = await readHookPayload();
207
+ const sid = payload?.session_id ?? "unknown-session";
208
+ markCompactEvent(stmts, sid);
209
+ process.stderr.write(`[Lucid session] /compact recorded — counters reset for ${sid}\n`);
210
+ return 0;
211
+ }
212
+ async function readHookPayload() {
213
+ if (process.stdin.isTTY)
214
+ return null;
215
+ const raw = await readStdin();
216
+ if (!raw.trim())
217
+ return null;
218
+ try {
219
+ return JSON.parse(raw);
220
+ }
221
+ catch {
222
+ return null;
223
+ }
224
+ }
225
+ function readStdin() {
226
+ return new Promise((resolveStdin) => {
227
+ let buf = "";
228
+ const timer = setTimeout(() => resolveStdin(buf), 250);
229
+ process.stdin.setEncoding("utf-8");
230
+ process.stdin.on("data", (chunk) => { buf += chunk; });
231
+ process.stdin.on("end", () => { clearTimeout(timer); resolveStdin(buf); });
232
+ process.stdin.on("error", () => { clearTimeout(timer); resolveStdin(buf); });
233
+ });
234
+ }
37
235
  async function runCli(cmd, args) {
38
236
  const { join } = await import("path");
39
237
  const { homedir } = await import("os");
@@ -136,13 +334,12 @@ const stmts = prepareStatements(db);
136
334
  // ---------------------------------------------------------------------------
137
335
  const _appCfg = loadConfig();
138
336
  configureGuard(_appCfg.security ?? {});
139
- // Register Qdrant host in SSRF allowlist if configured
140
337
  const _qdrantUrl = process.env["QDRANT_URL"] ?? _appCfg.qdrant?.url;
141
338
  if (_qdrantUrl) {
142
339
  try {
143
340
  allowHost(_qdrantUrl);
144
341
  }
145
- catch { /* ignore invalid URL */ }
342
+ catch { /* ignore */ }
146
343
  }
147
344
  const _embeddingUrl = process.env["EMBEDDING_URL"] ?? _appCfg.qdrant?.embeddingUrl;
148
345
  if (_embeddingUrl) {
@@ -152,755 +349,561 @@ if (_embeddingUrl) {
152
349
  catch { /* ignore */ }
153
350
  }
154
351
  else {
155
- // Default embedding endpoint
156
352
  allowHost("https://api.openai.com");
157
353
  }
354
+ // Local-LLM endpoint (may be remote — user-opted-in via `lucid local init`)
355
+ const _localCfg = loadLocalConfig();
356
+ if (_localCfg?.enabled) {
357
+ try {
358
+ allowHost(_localCfg.endpoint);
359
+ }
360
+ catch { /* ignore */ }
361
+ }
158
362
  // ---------------------------------------------------------------------------
159
- // MCP Server
160
- // ---------------------------------------------------------------------------
161
- const server = new Server({ name: "lucid", version: "1.13.0" }, { capabilities: { tools: {} } });
162
- // ---------------------------------------------------------------------------
163
- // Tool definitions
164
- // ---------------------------------------------------------------------------
165
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
166
- tools: [
167
- // ── Memory ──────────────────────────────────────────────────────────────
168
- {
169
- name: "remember",
170
- description: "Store a fact, decision, or observation about an entity in the knowledge graph.",
171
- inputSchema: {
172
- type: "object",
173
- properties: {
174
- entity: { type: "string", description: "Entity name (project, person, concept, tool)" },
175
- entityType: {
176
- type: "string",
177
- enum: ["person", "project", "decision", "pattern", "tool", "config", "bug", "convention"],
178
- },
179
- observation: { type: "string", description: "The fact to remember" },
180
- },
181
- required: ["entity", "entityType", "observation"],
182
- },
183
- },
184
- {
185
- name: "relate",
186
- description: "Create a directed relationship between two entities in the knowledge graph.",
187
- inputSchema: {
188
- type: "object",
189
- properties: {
190
- from: { type: "string", description: "Source entity name" },
191
- to: { type: "string", description: "Target entity name" },
192
- relationType: {
193
- type: "string",
194
- enum: ["uses", "depends_on", "created_by", "part_of", "replaced_by", "conflicts_with", "tested_by"],
195
- },
196
- },
197
- required: ["from", "to", "relationType"],
198
- },
199
- },
200
- {
201
- name: "recall",
202
- description: "Search memory using full-text search. Fast, indexed, supports partial matches and stemming.",
203
- inputSchema: {
204
- type: "object",
205
- properties: {
206
- query: { type: "string", description: "Search terms" },
207
- },
208
- required: ["query"],
209
- },
210
- },
211
- {
212
- name: "recall_all",
213
- description: "Get the entire knowledge graph with statistics.",
214
- inputSchema: { type: "object", properties: {} },
215
- },
216
- {
217
- name: "forget",
218
- description: "Remove an entity and all its relations from memory.",
219
- inputSchema: {
220
- type: "object",
221
- properties: {
222
- entity: { type: "string", description: "Entity name to remove" },
223
- },
224
- required: ["entity"],
225
- },
226
- },
227
- {
228
- name: "memory_stats",
229
- description: "Get memory usage statistics.",
230
- inputSchema: { type: "object", properties: {} },
231
- },
232
- // ── Init / Indexing ──────────────────────────────────────────────────────
233
- {
234
- name: "init_project",
235
- description: "Scan and index a project directory into the knowledge graph. " +
236
- "Reads CLAUDE.md (directives, conventions), package.json / pyproject.toml (dependencies, scripts), " +
237
- "README.md (description), .mcp.json (MCP servers), logic-guardian.yaml (drift patterns), " +
238
- "and source files (exported functions/classes). " +
239
- "Call this once when starting work on a project to bootstrap memory with project context.",
240
- inputSchema: {
241
- type: "object",
242
- properties: {
243
- directory: {
244
- type: "string",
245
- description: "Absolute path to the project root. Defaults to current working directory.",
246
- },
247
- },
248
- },
249
- },
250
- {
251
- name: "sync_file",
252
- description: "Index or re-index a single source file after it was written or modified. " +
253
- "Extracts exports, description, and open TODOs, then updates the knowledge graph. " +
254
- "IMPORTANT: call this automatically after every Write or Edit tool call.",
255
- inputSchema: {
256
- type: "object",
257
- properties: {
258
- path: { type: "string", description: "Absolute or relative path to the modified file." },
259
- },
260
- required: ["path"],
261
- },
262
- },
263
- {
264
- name: "sync_project",
265
- description: "Re-index the entire project directory incrementally. " +
266
- "Use this when multiple files have changed (e.g. after a refactor or git pull).",
267
- inputSchema: {
268
- type: "object",
269
- properties: {
270
- directory: {
271
- type: "string",
272
- description: "Project root directory. Defaults to current working directory.",
273
- },
274
- },
275
- },
276
- },
277
- {
278
- name: "grep_code",
279
- description: "Search indexed source files using a regex pattern. " +
280
- "Decompresses stored binary content and returns only matching lines with context. " +
281
- "Token-efficient: returns ~20-50 tokens instead of full file contents. " +
282
- "Useful for finding function calls, variable usages, import patterns.",
283
- inputSchema: {
284
- type: "object",
285
- properties: {
286
- pattern: { type: "string", description: "Regex pattern to search for." },
287
- language: { type: "string", enum: ["python", "javascript", "typescript", "vue", "generic"], description: "Filter by language." },
288
- context: { type: "number", description: "Lines of context before/after each match (0-10, default 2)." },
289
- },
290
- required: ["pattern"],
291
- },
292
- },
293
- // ── Context & Token Optimization ─────────────────────────────────────────
294
- {
295
- name: "get_context",
296
- description: "Retrieve the minimal relevant context for a task or query. " +
297
- "Uses TF-IDF scoring (or Qdrant vector search if configured) to rank files by relevance, " +
298
- "applies recency boost for recently modified files, and returns skeletons (signatures only) " +
299
- "for large files to stay within the token budget. " +
300
- "Configure limits in lucid.config.json. Set QDRANT_URL env var for vector search.",
301
- inputSchema: {
302
- type: "object",
303
- properties: {
304
- query: { type: "string", description: "What you are working on or searching for" },
305
- maxTokens: { type: "number", description: "Total token budget (default 4000)" },
306
- dirs: { type: "array", items: { type: "string" }, description: "Whitelist directories (e.g. [\"src\", \"backend\"])" },
307
- recentOnly: { type: "boolean", description: "Only files modified within recentWindowHours" },
308
- recentHours: { type: "number", description: "Override recent window (hours)" },
309
- skeletonOnly: { type: "boolean", description: "Always show skeleton (signatures only)" },
310
- topK: { type: "number", description: "Max files to consider (default 10)" },
311
- },
312
- required: ["query"],
313
- },
314
- },
315
- {
316
- name: "get_recent",
317
- description: "Return files modified recently with line-level diffs. " +
318
- "Shows what changed in each file since the previous sync. " +
319
- "Useful for catching up after a git pull or resuming a session.",
320
- inputSchema: {
321
- type: "object",
322
- properties: {
323
- hours: { type: "number", description: "Look back N hours (default 24)" },
324
- withDiffs: { type: "boolean", description: "Include line diffs (default true)" },
325
- },
326
- },
327
- },
328
- // ── Smart Context + Model Advisor ─────────────────────────────────────────
329
- {
330
- name: "smart_context",
331
- description: "Combined: knowledge graph (recall) + code files (get_context) in one call. " +
332
- "Use instead of calling recall() + get_context() separately. " +
333
- "task_type adjusts token budget: simple=2000, moderate=6000, complex=12000. " +
334
- "Logs an experience so reward()/penalize() work after this call.",
335
- inputSchema: {
336
- type: "object",
337
- properties: {
338
- query: { type: "string", description: "What you are working on" },
339
- task_type: {
340
- type: "string",
341
- enum: ["simple", "moderate", "complex"],
342
- description: "Token budget: simple=2000, moderate=6000 (default), complex=12000",
343
- },
344
- dirs: {
345
- type: "array",
346
- items: { type: "string" },
347
- description: "Whitelist directories (e.g. [\"src\", \"backend\"])",
348
- },
349
- },
350
- required: ["query"],
351
- },
352
- },
353
- {
354
- name: "suggest_model",
355
- description: "Classify task complexity → recommend Claude model. " +
356
- "Returns { model, model_id, reasoning, context_budget }. " +
357
- "Call at the start of any workflow. Simple lookups → Haiku; everything else → Sonnet (default).",
358
- inputSchema: {
359
- type: "object",
360
- properties: {
361
- task_description: {
362
- type: "string",
363
- description: "Natural language description of the task you are about to perform",
364
- },
365
- },
366
- required: ["task_description"],
367
- },
368
- },
369
- {
370
- name: "compress_text",
371
- description: "Compress text using LLMLingua-2 semantic compression (microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank). " +
372
- "Identifies and drops semantically unimportant tokens while preserving meaning. " +
373
- "Model downloads ~700MB on first use and is cached in ~/.lucid/models/. " +
374
- "Returns compressed text with stats (original/compressed length, ratio, tokens saved).",
375
- inputSchema: {
376
- type: "object",
377
- properties: {
378
- text: { type: "string", description: "Text to compress" },
379
- ratio: {
380
- type: "number",
381
- description: "Target compression ratio: 0.3 = keep 30%, 0.5 = keep 50% (default: 0.5)",
382
- },
383
- min_length: {
384
- type: "number",
385
- description: "Skip compression for texts shorter than this in chars (default: 300)",
386
- },
387
- },
388
- required: ["text"],
389
- },
390
- },
391
- // ── Reward System ────────────────────────────────────────────────────────
392
- {
393
- name: "reward",
394
- description: "Signal that the last get_context() result was helpful (+1 reward). " +
395
- "The files returned in that context will be ranked higher in future similar queries. " +
396
- "Call this after a get_context() result led to a correct fix or useful code.",
397
- inputSchema: {
398
- type: "object",
399
- properties: {
400
- note: { type: "string", description: "Optional note about what worked (stored for future reference)" },
401
- },
402
- },
403
- },
404
- {
405
- name: "penalize",
406
- description: "Signal that the last get_context() result was unhelpful (-1 reward). " +
407
- "The files returned in that context will be ranked lower in future similar queries. " +
408
- "Call this after a get_context() result missed important files or was irrelevant.",
409
- inputSchema: {
410
- type: "object",
411
- properties: {
412
- note: { type: "string", description: "Optional note about what was missing or wrong" },
413
- },
414
- },
415
- },
416
- {
417
- name: "show_rewards",
418
- description: "Show the top rewarded experiences and most rewarded files. " +
419
- "Rewards decay exponentially (half-life ~14 days). " +
420
- "Use this to understand which context queries and files have been most valuable.",
421
- inputSchema: {
422
- type: "object",
423
- properties: {
424
- query: { type: "string", description: "Filter experiences by query text (optional)" },
425
- topK: { type: "number", description: "Number of top results to show (default 10)" },
426
- },
427
- },
428
- },
429
- // ── Logic Guardian ───────────────────────────────────────────────────────
430
- {
431
- name: "validate_file",
432
- description: "Run Logic Guardian validation on a source file. Detects LLM drift patterns: " +
433
- "logic inversions, null propagation, type confusion, copy-paste drift, silent exceptions, and more. " +
434
- "Supports Python, JavaScript, TypeScript. Use after writing or modifying any code.",
435
- inputSchema: {
436
- type: "object",
437
- properties: {
438
- path: { type: "string", description: "Absolute or relative path to the file to validate." },
439
- },
440
- required: ["path"],
441
- },
442
- },
443
- {
444
- name: "check_drift",
445
- description: "Analyze a code snippet for LLM drift patterns without saving to disk. " +
446
- "Use this to validate code before writing it to a file.",
447
- inputSchema: {
448
- type: "object",
449
- properties: {
450
- code: { type: "string", description: "The code snippet to analyze." },
451
- language: {
452
- type: "string",
453
- enum: ["python", "javascript", "typescript", "generic"],
454
- description: "Programming language. Defaults to 'generic'.",
455
- },
456
- },
457
- required: ["code"],
458
- },
459
- },
460
- {
461
- name: "get_checklist",
462
- description: "Get the full Logic Guardian validation checklist (5 passes). " +
463
- "Call this before marking any implementation task as done.",
464
- inputSchema: { type: "object", properties: {} },
465
- },
466
- // ── Coding Guard ─────────────────────────────────────────────────────────
467
- {
468
- name: "coding_rules",
469
- description: "Get the 25 Golden Rules coding checklist. Covers clarity, naming, single responsibility, " +
470
- "error handling, frontend component size/reuse/props, singleton rules, library selection, " +
471
- "and architecture separation. Review before marking any task done.",
472
- inputSchema: { type: "object", properties: {} },
473
- },
474
- {
475
- name: "check_code_quality",
476
- description: "Analyze a file or code snippet against the 25 Golden Rules. " +
477
- "Detects: file/function size violations, vague naming, deep nesting, dead code, and — " +
478
- "for React/Vue component files — inline styles, prop explosion, fetch-in-component, " +
479
- "direct DOM access, mixed styling systems. " +
480
- "Complements validate_file (which checks logic correctness).",
481
- inputSchema: {
482
- type: "object",
483
- properties: {
484
- path: { type: "string", description: "Absolute or relative path to the file to analyze." },
485
- code: { type: "string", description: "Code snippet to analyze inline." },
486
- language: {
487
- type: "string",
488
- enum: ["python", "javascript", "typescript", "vue", "generic"],
489
- description: "Language hint. Auto-detected from file extension if path is provided.",
490
- },
491
- },
492
- },
493
- },
494
- // ── Planning ─────────────────────────────────────────────────────────────
495
- {
496
- name: "plan_create",
497
- description: "Create a plan with user story, ordered tasks, and test criteria. " +
498
- "Call BEFORE writing any code to establish intent and acceptance criteria.",
499
- inputSchema: {
500
- type: "object",
501
- properties: {
502
- title: { type: "string", description: "Short plan title." },
503
- description: { type: "string", description: "What this plan accomplishes." },
504
- user_story: { type: "string", description: "As a [user], I want [goal], so that [benefit]." },
505
- tasks: {
506
- type: "array",
507
- description: "Ordered list of implementation tasks (1–20).",
508
- items: {
509
- type: "object",
510
- properties: {
511
- title: { type: "string" },
512
- description: { type: "string" },
513
- test_criteria: { type: "string", description: "How to verify this task is done." },
514
- },
515
- required: ["title", "description", "test_criteria"],
516
- },
517
- minItems: 1,
518
- maxItems: 20,
519
- },
520
- },
521
- required: ["title", "description", "user_story", "tasks"],
522
- },
523
- },
524
- {
525
- name: "plan_list",
526
- description: "List plans with progress summary. Defaults to active plans.",
527
- inputSchema: {
528
- type: "object",
529
- properties: {
530
- status: {
531
- type: "string",
532
- enum: ["active", "completed", "abandoned", "all"],
533
- description: "Filter by plan status (default: active).",
534
- },
535
- },
536
- },
537
- },
538
- {
539
- name: "plan_get",
540
- description: "Get full plan details: tasks, test criteria, status, and notes.",
541
- inputSchema: {
542
- type: "object",
543
- properties: {
544
- plan_id: { type: "number", description: "Plan ID from plan_create or plan_list." },
545
- },
546
- required: ["plan_id"],
547
- },
548
- },
549
- {
550
- name: "plan_update_task",
551
- description: "Update a task status. Auto-completes the plan when all tasks are done. " +
552
- "Statuses: pending → in_progress → done (or blocked).",
553
- inputSchema: {
554
- type: "object",
555
- properties: {
556
- task_id: { type: "number", description: "Task ID from plan_get." },
557
- status: { type: "string", enum: ["pending", "in_progress", "done", "blocked"] },
558
- note: { type: "string", description: "Optional note appended to task history." },
559
- },
560
- required: ["task_id", "status"],
561
- },
562
- },
563
- // ── Updater ──────────────────────────────────────────────────────────────
564
- {
565
- name: "update_lucid",
566
- description: "Check for a newer version of Lucid on npm and update automatically. " +
567
- "For global npm installs: runs npm install -g @a13xu/lucid@latest. " +
568
- "For local source installs: shows git pull + npm run build instructions. " +
569
- "After updating, restart Claude Code to load the new version.",
570
- inputSchema: {
571
- type: "object",
572
- properties: {
573
- force: {
574
- type: "boolean",
575
- description: "Force reinstall even if already on latest version (default false)",
576
- },
577
- },
578
- },
579
- },
580
- // ── Web Dev Skills ───────────────────────────────────────────────────────
581
- {
582
- name: "generate_component",
583
- description: "Generate a complete component scaffold from a natural language description. " +
584
- "Supports React (TSX/JSX) and Vue/Nuxt (Composition API + <script setup>). " +
585
- "Styling options: Tailwind CSS, CSS Modules, or none.",
586
- inputSchema: {
587
- type: "object",
588
- properties: {
589
- description: { type: "string", description: "Natural language description of the component" },
590
- framework: { type: "string", enum: ["react", "vue", "nuxt"], description: "Target framework" },
591
- styling: { type: "string", enum: ["tailwind", "css-modules", "none"], description: "Styling approach" },
592
- typescript: { type: "boolean", description: "Whether to use TypeScript" },
593
- },
594
- required: ["description", "framework", "styling", "typescript"],
595
- },
596
- },
597
- {
598
- name: "scaffold_page",
599
- description: "Generate a full page scaffold with layout, SEO head meta, and placeholder sections. " +
600
- "Supports Nuxt (useHead), Next.js (Metadata API), and plain Vue.",
601
- inputSchema: {
602
- type: "object",
603
- properties: {
604
- page_name: { type: "string", description: "Page name (e.g. About, Dashboard)" },
605
- framework: { type: "string", enum: ["nuxt", "next", "vue"], description: "Target framework" },
606
- sections: { type: "array", items: { type: "string" }, description: "Section names (e.g. hero, features, footer)" },
607
- seo_title: { type: "string", description: "Optional SEO title" },
608
- },
609
- required: ["page_name", "framework", "sections"],
610
- },
611
- },
612
- {
613
- name: "seo_meta",
614
- description: "Generate complete SEO metadata for a page: HTML meta tags, Open Graph, Twitter Card, " +
615
- "and JSON-LD structured data (Article, Product, WebSite, or WebPage).",
616
- inputSchema: {
617
- type: "object",
618
- properties: {
619
- title: { type: "string", description: "Page title" },
620
- description: { type: "string", description: "Meta description (≤160 chars recommended)" },
621
- keywords: { type: "array", items: { type: "string" }, description: "SEO keywords" },
622
- page_type: { type: "string", enum: ["article", "product", "landing", "home"], description: "Page type for JSON-LD" },
623
- url: { type: "string", description: "Canonical page URL" },
624
- image_url: { type: "string", description: "OG/Twitter image URL" },
625
- },
626
- required: ["title", "description", "keywords", "page_type"],
627
- },
628
- },
629
- {
630
- name: "accessibility_audit",
631
- description: "Audit HTML, JSX, or Vue template snippets for WCAG accessibility violations. " +
632
- "Checks: missing alt text, unlabeled inputs, empty buttons/links, positive tabindex, " +
633
- "non-interactive click handlers, open-in-new-tab links, and more. " +
634
- "Returns severity (critical/warning/info), WCAG criterion, and corrected code.",
635
- inputSchema: {
636
- type: "object",
637
- properties: {
638
- code: { type: "string", description: "HTML, JSX, or Vue snippet to audit" },
639
- wcag_level: { type: "string", enum: ["A", "AA", "AAA"], description: "WCAG conformance level" },
640
- framework: { type: "string", enum: ["html", "jsx", "vue"], description: "Code framework" },
641
- },
642
- required: ["code", "wcag_level", "framework"],
643
- },
644
- },
645
- {
646
- name: "api_client",
647
- description: "Generate a typed TypeScript async function for a REST API endpoint. " +
648
- "Includes full types, error handling (throws on non-2xx), and a usage example. " +
649
- "Auth strategies: Bearer token, cookie, API key, or none.",
650
- inputSchema: {
651
- type: "object",
652
- properties: {
653
- endpoint: { type: "string", description: "API endpoint path (e.g. /users/:id)" },
654
- method: { type: "string", enum: ["GET", "POST", "PUT", "PATCH", "DELETE"] },
655
- request_schema: { type: "string", description: "TypeScript type for request body" },
656
- response_schema: { type: "string", description: "TypeScript type for response" },
657
- auth: { type: "string", enum: ["bearer", "cookie", "apikey", "none"] },
658
- base_url_var: { type: "string", description: "Env var name for base URL (e.g. NEXT_PUBLIC_API_URL)" },
659
- },
660
- required: ["endpoint", "method", "auth"],
661
- },
662
- },
663
- {
664
- name: "test_generator",
665
- description: "Generate a complete test file for a function, component, or API handler. " +
666
- "Covers: happy path, edge cases (empty/null/boundary), error path, and mock setup. " +
667
- "Frameworks: Vitest, Jest, or Playwright (e2e). " +
668
- "Component testing: Vue Test Utils or React Testing Library.",
669
- inputSchema: {
670
- type: "object",
671
- properties: {
672
- code: { type: "string", description: "Source code to generate tests for" },
673
- test_framework: { type: "string", enum: ["vitest", "jest", "playwright"] },
674
- test_type: { type: "string", enum: ["unit", "integration", "e2e"] },
675
- component_framework: { type: "string", enum: ["vue", "react", "none"] },
676
- },
677
- required: ["code", "test_framework", "test_type"],
678
- },
679
- },
680
- {
681
- name: "responsive_layout",
682
- description: "Generate a responsive mobile-first layout from a wireframe description. " +
683
- "Outputs: Tailwind CSS utility classes, CSS Grid with named areas, or Flexbox with media queries. " +
684
- "Container types: full-width, centered max-width, or sidebar layout.",
685
- inputSchema: {
686
- type: "object",
687
- properties: {
688
- description: { type: "string", description: "Wireframe description (e.g. 'sidebar + main + right panel')" },
689
- framework: { type: "string", enum: ["tailwind", "css-grid", "flexbox"] },
690
- breakpoints: { type: "array", items: { type: "string" }, description: "Breakpoint names (e.g. ['mobile', 'tablet', 'desktop'])" },
691
- container: { type: "string", enum: ["full", "centered", "sidebar"] },
692
- },
693
- required: ["description", "framework", "breakpoints"],
694
- },
695
- },
696
- {
697
- name: "security_scan",
698
- description: "Scan JavaScript/TypeScript/HTML/Vue code for common web security vulnerabilities. " +
699
- "Detects: XSS (innerHTML, v-html, dangerouslySetInnerHTML), code injection (eval, new Function), " +
700
- "SQL injection, hardcoded secrets, open redirects, prototype pollution, path traversal, " +
701
- "render-blocking scripts, and insecure CORS. Context-aware: frontend vs backend vs API rules. " +
702
- "Complements validate_file (logic drift) — this focuses on web security patterns.",
703
- inputSchema: {
704
- type: "object",
705
- properties: {
706
- code: { type: "string", description: "Code snippet to scan" },
707
- language: { type: "string", enum: ["javascript", "typescript", "html", "vue"] },
708
- context: { type: "string", enum: ["frontend", "backend", "api"] },
709
- },
710
- required: ["code", "language", "context"],
711
- },
712
- },
713
- {
714
- name: "design_tokens",
715
- description: "Generate a complete design system token set from a brand color and mood. " +
716
- "Produces: 11-step color scales (50–950), neutral scale, semantic color aliases, " +
717
- "typography scale, spacing, border-radius, and shadow tokens. " +
718
- "Output formats: CSS custom properties, Tailwind config, or JSON.",
719
- inputSchema: {
720
- type: "object",
721
- properties: {
722
- brand_name: { type: "string", description: "Brand or project name" },
723
- primary_color: { type: "string", description: "Primary color as hex (#3B82F6) or name (blue)" },
724
- mood: { type: "string", enum: ["minimal", "bold", "playful", "corporate"] },
725
- output_format: { type: "string", enum: ["css-variables", "tailwind-config", "json"] },
726
- },
727
- required: ["brand_name", "primary_color", "mood", "output_format"],
728
- },
729
- },
730
- {
731
- name: "perf_hints",
732
- description: "Analyze a component or page file for Core Web Vitals (CWV) and web performance issues. " +
733
- "Detects: missing LCP image priority, images without dimensions (CLS), render-blocking scripts, " +
734
- "fetch-in-render (TTFB), heavy click handlers (INP), missing useMemo/computed, " +
735
- "whole-library imports, and inline style objects. Issues ranked by CWV metric impact.",
736
- inputSchema: {
737
- type: "object",
738
- properties: {
739
- code: { type: "string", description: "Component or page source code to analyze" },
740
- framework: { type: "string", enum: ["react", "vue", "nuxt", "vanilla"] },
741
- context: { type: "string", enum: ["component", "page", "layout"] },
742
- },
743
- required: ["code", "framework", "context"],
744
- },
745
- },
746
- ],
747
- }));
748
- // ---------------------------------------------------------------------------
749
- // Tool handlers
363
+ // MCP Server (high-level McpServer API, SDK 1.27+)
750
364
  // ---------------------------------------------------------------------------
751
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
752
- const { name, arguments: args } = request.params;
753
- // Security: rate limit + WAF check before any execution
754
- const guard = guardRequest(name, args);
755
- if (guard.blocked) {
756
- return { content: [{ type: "text", text: guard.reason ?? "Request blocked by security guard" }], isError: true };
757
- }
758
- try {
759
- let text;
760
- switch (name) {
761
- // Memory
762
- case "remember":
763
- text = remember(stmts, RememberSchema.parse(args));
764
- break;
765
- case "relate":
766
- text = relate(stmts, RelateSchema.parse(args));
767
- break;
768
- case "recall":
769
- text = recall(stmts, RecallSchema.parse(args));
770
- break;
771
- case "recall_all":
772
- text = recallAll(db, stmts);
773
- break;
774
- case "forget":
775
- text = forget(stmts, ForgetSchema.parse(args));
776
- break;
777
- case "memory_stats":
778
- text = memoryStats(db, stmts);
779
- break;
780
- // Init & Sync
781
- case "init_project":
782
- text = await handleInitProject(stmts, InitProjectSchema.parse(args));
783
- break;
784
- case "sync_file":
785
- text = handleSyncFile(stmts, SyncFileSchema.parse(args));
786
- break;
787
- case "sync_project":
788
- text = handleSyncProject(stmts, SyncProjectSchema.parse(args));
789
- break;
790
- // Grep
791
- case "grep_code":
792
- text = handleGrepCode(stmts, GrepCodeSchema.parse(args));
793
- break;
794
- // Context & Token Optimization
795
- case "get_context":
796
- text = await handleGetContext(stmts, GetContextSchema.parse(args));
797
- break;
798
- case "get_recent":
799
- text = handleGetRecent(stmts, GetRecentSchema.parse(args));
800
- break;
801
- // Smart Context + Model Advisor
802
- case "smart_context":
803
- text = await handleSmartContext(stmts, SmartContextSchema.parse(args));
804
- break;
805
- case "suggest_model":
806
- text = handleSuggestModel(SuggestModelSchema.parse(args));
807
- break;
808
- case "compress_text":
809
- text = await handleCompressText(CompressTextSchema.parse(args));
810
- break;
811
- // Reward System
812
- case "reward":
813
- text = handleReward(stmts, RewardSchema.parse(args));
814
- break;
815
- case "penalize":
816
- text = handlePenalize(stmts, PenalizeSchema.parse(args));
817
- break;
818
- case "show_rewards":
819
- text = handleShowRewards(stmts, ShowRewardsSchema.parse(args));
820
- break;
821
- // Logic Guardian
822
- case "validate_file":
823
- text = handleValidateFile(ValidateFileSchema.parse(args));
824
- break;
825
- case "check_drift":
826
- text = handleCheckDrift(CheckDriftSchema.parse(args));
827
- break;
828
- case "get_checklist":
829
- text = handleGetChecklist();
830
- break;
831
- // Coding Guard
832
- case "coding_rules":
833
- text = handleGetCodingRules();
834
- break;
835
- case "check_code_quality":
836
- text = handleCheckCodeQuality(CheckCodeQualitySchema.parse(args));
837
- break;
838
- // Planning
839
- case "plan_create":
840
- text = handlePlanCreate(db, stmts, PlanCreateSchema.parse(args));
841
- break;
842
- case "plan_list":
843
- text = handlePlanList(stmts, PlanListSchema.parse(args));
844
- break;
845
- case "plan_get":
846
- text = handlePlanGet(stmts, PlanGetSchema.parse(args));
847
- break;
848
- case "plan_update_task":
849
- text = handlePlanUpdateTask(stmts, PlanUpdateTaskSchema.parse(args));
850
- break;
851
- // Updater
852
- case "update_lucid":
853
- text = await handleUpdateLucid(UpdateLucidSchema.parse(args));
854
- break;
855
- // Web Dev Skills
856
- case "generate_component":
857
- text = handleGenerateComponent(GenerateComponentSchema.parse(args));
858
- break;
859
- case "scaffold_page":
860
- text = handleScaffoldPage(ScaffoldPageSchema.parse(args));
861
- break;
862
- case "seo_meta":
863
- text = handleSeoMeta(SeoMetaSchema.parse(args));
864
- break;
865
- case "accessibility_audit":
866
- text = handleAccessibilityAudit(AccessibilityAuditSchema.parse(args));
867
- break;
868
- case "api_client":
869
- text = handleApiClient(ApiClientSchema.parse(args));
870
- break;
871
- case "test_generator":
872
- text = handleTestGenerator(TestGeneratorSchema.parse(args));
873
- break;
874
- case "responsive_layout":
875
- text = handleResponsiveLayout(ResponsiveLayoutSchema.parse(args));
876
- break;
877
- case "security_scan":
878
- text = handleSecurityScan(SecurityScanSchema.parse(args));
879
- break;
880
- case "design_tokens":
881
- text = handleDesignTokens(DesignTokensSchema.parse(args));
882
- break;
883
- case "perf_hints":
884
- text = handlePerfHints(PerfHintsSchema.parse(args));
885
- break;
886
- default:
887
- return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
365
+ const SERVER_VERSION = getCurrentVersion();
366
+ const server = new McpServer({ name: "lucid", version: SERVER_VERSION }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
367
+ function tx(name, handler) {
368
+ return async (args) => {
369
+ const guard = guardRequest(name, args);
370
+ if (guard.blocked) {
371
+ return {
372
+ content: [{ type: "text", text: guard.reason ?? "Request blocked by security guard" }],
373
+ isError: true,
374
+ };
375
+ }
376
+ try {
377
+ const out = await handler(args);
378
+ if (typeof out === "string") {
379
+ return { content: [{ type: "text", text: guardOutput(name, out) }] };
380
+ }
381
+ return {
382
+ content: [{ type: "text", text: guardOutput(name, out.text) }],
383
+ structuredContent: out.structured,
384
+ };
385
+ }
386
+ catch (err) {
387
+ const msg = err instanceof z.ZodError
388
+ ? `Validation error: ${err.errors.map((e) => e.message).join(", ")}`
389
+ : err instanceof Error ? err.message : String(err);
390
+ return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
391
+ }
392
+ };
393
+ }
394
+ // Helpers that produce both text + structured output for tools whose handlers
395
+ // already return JSON. Avoids touching downstream handler files.
396
+ const memoryStatsRich = () => {
397
+ const text = memoryStats(db, stmts);
398
+ return { text, structured: JSON.parse(text) };
399
+ };
400
+ const recallAllRich = () => {
401
+ const text = recallAll(db, stmts);
402
+ return { text, structured: JSON.parse(text) };
403
+ };
404
+ const recallRich = (args) => {
405
+ const text = recall(stmts, args);
406
+ // recall returns either "No results..." text or JSON array.
407
+ const trimmed = text.trim();
408
+ if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
409
+ try {
410
+ return { text, structured: { entities: JSON.parse(text) } };
411
+ }
412
+ catch {
413
+ return text;
888
414
  }
889
- // Security: scan output for sensitive data leakage
890
- return { content: [{ type: "text", text: guardOutput(name, text) }] };
891
- }
892
- catch (err) {
893
- const message = err instanceof z.ZodError
894
- ? `Validation error: ${err.errors.map((e) => e.message).join(", ")}`
895
- : err instanceof Error ? err.message : String(err);
896
- return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
897
415
  }
416
+ return text;
417
+ };
418
+ // Output schemas (Zod raw shapes) for structured-content tools
419
+ const memoryStatsOutputShape = {
420
+ entity_count: z.number().int(),
421
+ relation_count: z.number().int(),
422
+ observation_count: z.number().int(),
423
+ db_size_bytes: z.number().int(),
424
+ db_size_kb: z.number().int(),
425
+ wal_mode: z.boolean(),
426
+ fts5_enabled: z.boolean(),
427
+ };
428
+ const entityShape = {
429
+ id: z.number().int(),
430
+ name: z.string(),
431
+ type: z.string(),
432
+ observations: z.array(z.string()),
433
+ created_at: z.number(),
434
+ updated_at: z.number(),
435
+ relations: z.array(z.object({
436
+ from: z.string(), to: z.string(), type: z.string(),
437
+ })),
438
+ };
439
+ const recallAllOutputShape = {
440
+ stats: z.object(memoryStatsOutputShape),
441
+ entities: z.array(z.object(entityShape)),
442
+ };
443
+ const recallOutputShape = {
444
+ entities: z.array(z.object(entityShape)),
445
+ };
446
+ // ---------------------------------------------------------------------------
447
+ // Tools — Memory
448
+ // ---------------------------------------------------------------------------
449
+ server.registerTool("remember", {
450
+ title: "Remember",
451
+ description: "Store a fact, decision, or observation about an entity in the knowledge graph.",
452
+ inputSchema: RememberSchema.shape,
453
+ }, tx("remember", (args) => remember(stmts, args)));
454
+ server.registerTool("relate", {
455
+ title: "Relate",
456
+ description: "Create a directed relationship between two entities in the knowledge graph.",
457
+ inputSchema: RelateSchema.shape,
458
+ }, tx("relate", (args) => relate(stmts, args)));
459
+ server.registerTool("recall", {
460
+ title: "Recall",
461
+ description: "Search memory using full-text search. Fast, indexed, supports partial matches and stemming.",
462
+ inputSchema: RecallSchema.shape,
463
+ outputSchema: recallOutputShape,
464
+ }, tx("recall", (args) => recallRich(args)));
465
+ server.registerTool("recall_all", {
466
+ title: "Recall All",
467
+ description: "Get the entire knowledge graph with statistics.",
468
+ outputSchema: recallAllOutputShape,
469
+ }, tx("recall_all", () => recallAllRich()));
470
+ server.registerTool("forget", {
471
+ title: "Forget",
472
+ description: "Remove an entity and all its relations from memory.",
473
+ inputSchema: ForgetSchema.shape,
474
+ }, tx("forget", (args) => forget(stmts, args)));
475
+ server.registerTool("memory_stats", {
476
+ title: "Memory Stats",
477
+ description: "Get memory usage statistics.",
478
+ outputSchema: memoryStatsOutputShape,
479
+ }, tx("memory_stats", () => memoryStatsRich()));
480
+ // ---------------------------------------------------------------------------
481
+ // Tools — Init / Indexing
482
+ // ---------------------------------------------------------------------------
483
+ server.registerTool("init_project", {
484
+ title: "Init Project",
485
+ description: "Scan and index a project directory into the knowledge graph. " +
486
+ "Reads CLAUDE.md, package.json/pyproject.toml, README.md, .mcp.json, logic-guardian.yaml, " +
487
+ "and source files (exported functions/classes). Call once when starting work on a project.",
488
+ inputSchema: InitProjectSchema.shape,
489
+ }, tx("init_project", async (args) => handleInitProject(stmts, args)));
490
+ server.registerTool("sync_file", {
491
+ title: "Sync File",
492
+ description: "Index or re-index a single source file after it was written or modified. " +
493
+ "IMPORTANT: call this automatically after every Write or Edit tool call.",
494
+ inputSchema: SyncFileSchema.shape,
495
+ }, tx("sync_file", (args) => handleSyncFile(stmts, args)));
496
+ server.registerTool("sync_project", {
497
+ title: "Sync Project",
498
+ description: "Re-index the entire project directory incrementally (after refactor or git pull).",
499
+ inputSchema: SyncProjectSchema.shape,
500
+ }, tx("sync_project", (args) => handleSyncProject(stmts, args)));
501
+ server.registerTool("grep_code", {
502
+ title: "Grep Code",
503
+ description: "Search indexed source files using a regex pattern. Decompresses stored content and returns " +
504
+ "only matching lines with context. Token-efficient (~20-50 tokens vs full file).",
505
+ inputSchema: GrepCodeSchema.shape,
506
+ }, tx("grep_code", (args) => handleGrepCode(stmts, args)));
507
+ // ---------------------------------------------------------------------------
508
+ // Tools — Context & Token Optimization
509
+ // ---------------------------------------------------------------------------
510
+ server.registerTool("get_context", {
511
+ title: "Get Context",
512
+ description: "Retrieve the minimal relevant context for a task or query. TF-IDF (or Qdrant) ranking " +
513
+ "+ recency boost + skeleton pruning to stay within token budget.",
514
+ inputSchema: GetContextSchema.shape,
515
+ }, tx("get_context", async (args) => handleGetContext(stmts, args)));
516
+ server.registerTool("get_recent", {
517
+ title: "Get Recent",
518
+ description: "Return files modified recently with line-level diffs. Useful after a git pull or session resume.",
519
+ inputSchema: GetRecentSchema.shape,
520
+ }, tx("get_recent", (args) => handleGetRecent(stmts, args)));
521
+ server.registerTool("smart_context", {
522
+ title: "Smart Context",
523
+ description: "Combined: knowledge graph (recall) + code files (get_context) in one call. " +
524
+ "task_type adjusts token budget: simple=2000, moderate=6000, complex=12000.",
525
+ inputSchema: SmartContextSchema.shape,
526
+ }, tx("smart_context", async (args) => handleSmartContext(stmts, args)));
527
+ server.registerTool("suggest_model", {
528
+ title: "Suggest Model",
529
+ description: "Classify task complexity → recommend Claude model. Returns { model, model_id, reasoning, context_budget }. " +
530
+ "Call at the start of any workflow.",
531
+ inputSchema: SuggestModelSchema.shape,
532
+ }, tx("suggest_model", (args) => handleSuggestModel(args)));
533
+ server.registerTool("compress_text", {
534
+ title: "Compress Text",
535
+ description: "Compress text using LLMLingua-2 semantic compression. Model downloads ~700MB on first use " +
536
+ "(cached in ~/.lucid/models/). Returns compressed text with stats.",
537
+ inputSchema: CompressTextSchema.shape,
538
+ }, tx("compress_text", async (args) => handleCompressText(args)));
539
+ // ---------------------------------------------------------------------------
540
+ // Tools — Reward System
541
+ // ---------------------------------------------------------------------------
542
+ server.registerTool("reward", {
543
+ title: "Reward",
544
+ description: "Signal that the last get_context() result was helpful (+1 reward). " +
545
+ "Files in that context will be ranked higher in future similar queries.",
546
+ inputSchema: RewardSchema.shape,
547
+ }, tx("reward", (args) => handleReward(stmts, args)));
548
+ server.registerTool("penalize", {
549
+ title: "Penalize",
550
+ description: "Signal that the last get_context() result was unhelpful (-1 reward). " +
551
+ "Files in that context will be ranked lower in future similar queries.",
552
+ inputSchema: PenalizeSchema.shape,
553
+ }, tx("penalize", (args) => handlePenalize(stmts, args)));
554
+ server.registerTool("show_rewards", {
555
+ title: "Show Rewards",
556
+ description: "Show the top rewarded experiences and most rewarded files. " +
557
+ "Rewards decay exponentially (half-life ~14 days).",
558
+ inputSchema: ShowRewardsSchema.shape,
559
+ }, tx("show_rewards", (args) => handleShowRewards(stmts, args)));
560
+ // ---------------------------------------------------------------------------
561
+ // Tools — Logic Guardian
562
+ // ---------------------------------------------------------------------------
563
+ server.registerTool("validate_file", {
564
+ title: "Validate File",
565
+ description: "Run Logic Guardian validation on a source file. Detects LLM drift: logic inversions, " +
566
+ "null propagation, type confusion, copy-paste drift, silent exceptions. Python/JS/TS.",
567
+ inputSchema: ValidateFileSchema.shape,
568
+ }, tx("validate_file", (args) => handleValidateFile(args)));
569
+ server.registerTool("check_drift", {
570
+ title: "Check Drift",
571
+ description: "Analyze a code snippet for LLM drift patterns without saving to disk.",
572
+ inputSchema: CheckDriftSchema.shape,
573
+ }, tx("check_drift", (args) => handleCheckDrift(args)));
574
+ server.registerTool("get_checklist", {
575
+ title: "Get Checklist",
576
+ description: "Get the full Logic Guardian validation checklist (5 passes).",
577
+ }, tx("get_checklist", () => handleGetChecklist()));
578
+ // ---------------------------------------------------------------------------
579
+ // Tools — Coding Guard
580
+ // ---------------------------------------------------------------------------
581
+ server.registerTool("coding_rules", {
582
+ title: "Coding Rules",
583
+ description: "Get the 25 Golden Rules coding checklist. Covers clarity, naming, single responsibility, " +
584
+ "frontend rules, library selection, architecture separation.",
585
+ }, tx("coding_rules", () => handleGetCodingRules()));
586
+ // CheckCodeQualitySchema uses .refine(); pass the raw shape to MCP and re-parse
587
+ // inside the handler so the refinement runs.
588
+ const checkCodeQualityShape = {
589
+ path: z.string().optional().describe("Absolute or relative path to the file to analyze."),
590
+ code: z.string().optional().describe("Code snippet to analyze inline."),
591
+ language: z.enum(["python", "javascript", "typescript", "vue", "generic"]).optional()
592
+ .describe("Language hint. Auto-detected from file extension if path is provided."),
593
+ };
594
+ server.registerTool("check_code_quality", {
595
+ title: "Check Code Quality",
596
+ description: "Analyze a file or snippet against the 25 Golden Rules. Detects size violations, vague naming, " +
597
+ "deep nesting, dead code, inline styles, prop explosion, fetch-in-component.",
598
+ inputSchema: checkCodeQualityShape,
599
+ }, tx("check_code_quality", (args) => handleCheckCodeQuality(CheckCodeQualitySchema.parse(args))));
600
+ // ---------------------------------------------------------------------------
601
+ // Tools — Local LLM (Ollama / LM Studio / llama.cpp / remote endpoint)
602
+ // ---------------------------------------------------------------------------
603
+ server.registerTool("delegate_local", {
604
+ title: "Delegate to Local LLM",
605
+ description: "Send a prompt to the user-configured local LLM (Ollama / LM Studio / llama.cpp / remote " +
606
+ "endpoint). Returns the raw completion. Configure once via `lucid local init`. Best for " +
607
+ "small specialized tasks (docstrings, type hints, simple refactors, regex). Claude should " +
608
+ "review the output before applying it via Edit/Write.",
609
+ inputSchema: DelegateLocalSchema.shape,
610
+ }, tx("delegate_local", async (args) => handleDelegateLocal(args)));
611
+ server.registerTool("local_llm_status", {
612
+ title: "Local LLM Status",
613
+ description: "Inspect the local-LLM configuration: runtime, endpoint, model, reachability. " +
614
+ "Returns setup instructions if not yet configured.",
615
+ inputSchema: LocalLlmStatusSchema.shape,
616
+ }, tx("local_llm_status", async () => handleLocalLlmStatus()));
617
+ // ---------------------------------------------------------------------------
618
+ // Tools — Session Cost Tracker
619
+ // ---------------------------------------------------------------------------
620
+ server.registerTool("session_status", {
621
+ title: "Session Status",
622
+ description: "Show recent Claude Code sessions with prompt counts, idle time, and /compact " +
623
+ "history. Use to inspect when /compact or /clear hints are about to fire.",
624
+ inputSchema: SessionStatusSchema.shape,
625
+ }, tx("session_status", (args) => handleSessionStatus(stmts, args)));
626
+ // ---------------------------------------------------------------------------
627
+ // Tools — Backup & Truncate Guard
628
+ // ---------------------------------------------------------------------------
629
+ server.registerTool("backup_file", {
630
+ title: "Backup File",
631
+ description: "Snapshot the current on-disk content of a file into Lucid's versioned backup store " +
632
+ "(zlib-compressed, last 10 versions kept). Use before risky edits.",
633
+ inputSchema: BackupFileSchema.shape,
634
+ }, tx("backup_file", (args) => handleBackupFile(stmts, args)));
635
+ server.registerTool("restore_file", {
636
+ title: "Restore File",
637
+ description: "Restore a file from a previous Lucid backup. version=1 is the latest snapshot, " +
638
+ "2 is the one before, etc. Pass dry_run=true to preview without writing.",
639
+ inputSchema: RestoreFileSchema.shape,
640
+ }, tx("restore_file", (args) => handleRestoreFile(stmts, args)));
641
+ server.registerTool("check_truncate_risk", {
642
+ title: "Check Truncate Risk",
643
+ description: "Assess whether writing new_content (or new_size) to path would constitute a destructive " +
644
+ "truncate (empty/whitespace overwrite, >70% shrink, or active cascade lock). Read-only by default.",
645
+ inputSchema: CheckTruncateRiskSchema.shape,
646
+ }, tx("check_truncate_risk", (args) => handleCheckTruncateRisk(stmts, args)));
647
+ // ---------------------------------------------------------------------------
648
+ // Tools — Planning
649
+ // ---------------------------------------------------------------------------
650
+ server.registerTool("plan_create", {
651
+ title: "Plan Create",
652
+ description: "Create a plan with user story, ordered tasks, and test criteria. " +
653
+ "Call BEFORE writing any code to establish intent and acceptance criteria.",
654
+ inputSchema: PlanCreateSchema.shape,
655
+ }, tx("plan_create", (args) => handlePlanCreate(db, stmts, args)));
656
+ server.registerTool("plan_list", {
657
+ title: "Plan List",
658
+ description: "List plans with progress summary. Defaults to active plans.",
659
+ inputSchema: PlanListSchema.shape,
660
+ }, tx("plan_list", (args) => handlePlanList(stmts, args)));
661
+ server.registerTool("plan_get", {
662
+ title: "Plan Get",
663
+ description: "Get full plan details: tasks, test criteria, status, and notes.",
664
+ inputSchema: PlanGetSchema.shape,
665
+ }, tx("plan_get", (args) => handlePlanGet(stmts, args)));
666
+ server.registerTool("plan_update_task", {
667
+ title: "Plan Update Task",
668
+ description: "Update a task status. Auto-completes the plan when all tasks are done. " +
669
+ "Statuses: pending → in_progress → done (or blocked).",
670
+ inputSchema: PlanUpdateTaskSchema.shape,
671
+ }, tx("plan_update_task", (args) => handlePlanUpdateTask(stmts, args)));
672
+ // ---------------------------------------------------------------------------
673
+ // Tools — Updater
674
+ // ---------------------------------------------------------------------------
675
+ server.registerTool("update_lucid", {
676
+ title: "Update Lucid",
677
+ description: "Check for a newer version of Lucid on npm and update automatically. " +
678
+ "Restart Claude Code after updating.",
679
+ inputSchema: UpdateLucidSchema.shape,
680
+ }, tx("update_lucid", async (args) => handleUpdateLucid(args)));
681
+ // ---------------------------------------------------------------------------
682
+ // Tools — Web Dev Skills
683
+ // ---------------------------------------------------------------------------
684
+ server.registerTool("generate_component", {
685
+ title: "Generate Component",
686
+ description: "Generate a complete component scaffold from a description. React (TSX/JSX) or Vue/Nuxt. " +
687
+ "Styling: Tailwind, CSS Modules, or none.",
688
+ inputSchema: GenerateComponentSchema.shape,
689
+ }, tx("generate_component", (args) => handleGenerateComponent(args)));
690
+ server.registerTool("scaffold_page", {
691
+ title: "Scaffold Page",
692
+ description: "Generate a full page scaffold with layout, SEO head meta, and placeholder sections. " +
693
+ "Nuxt (useHead), Next.js (Metadata API), or plain Vue.",
694
+ inputSchema: ScaffoldPageSchema.shape,
695
+ }, tx("scaffold_page", (args) => handleScaffoldPage(args)));
696
+ server.registerTool("seo_meta", {
697
+ title: "SEO Meta",
698
+ description: "Generate complete SEO metadata: HTML meta tags, Open Graph, Twitter Card, JSON-LD " +
699
+ "(Article, Product, WebSite, WebPage).",
700
+ inputSchema: SeoMetaSchema.shape,
701
+ }, tx("seo_meta", (args) => handleSeoMeta(args)));
702
+ server.registerTool("accessibility_audit", {
703
+ title: "Accessibility Audit",
704
+ description: "Audit HTML/JSX/Vue snippets for WCAG violations. Checks: alt text, labels, empty buttons, " +
705
+ "tabindex, click handlers, target=_blank. Returns severity + WCAG criterion + corrected code.",
706
+ inputSchema: AccessibilityAuditSchema.shape,
707
+ }, tx("accessibility_audit", (args) => handleAccessibilityAudit(args)));
708
+ server.registerTool("api_client", {
709
+ title: "API Client",
710
+ description: "Generate a typed TypeScript async function for a REST endpoint. Includes types, " +
711
+ "error handling (throws on non-2xx), usage example. Auth: bearer/cookie/apikey/none.",
712
+ inputSchema: ApiClientSchema.shape,
713
+ }, tx("api_client", (args) => handleApiClient(args)));
714
+ server.registerTool("test_generator", {
715
+ title: "Test Generator",
716
+ description: "Generate a complete test file. Covers happy path, edge cases, error path, mock setup. " +
717
+ "Frameworks: Vitest, Jest, Playwright. Component: Vue Test Utils or React Testing Library.",
718
+ inputSchema: TestGeneratorSchema.shape,
719
+ }, tx("test_generator", (args) => handleTestGenerator(args)));
720
+ server.registerTool("responsive_layout", {
721
+ title: "Responsive Layout",
722
+ description: "Generate a responsive mobile-first layout from a wireframe description. " +
723
+ "Tailwind utility classes, CSS Grid (named areas), or Flexbox + media queries.",
724
+ inputSchema: ResponsiveLayoutSchema.shape,
725
+ }, tx("responsive_layout", (args) => handleResponsiveLayout(args)));
726
+ server.registerTool("security_scan", {
727
+ title: "Security Scan",
728
+ description: "Scan JS/TS/HTML/Vue for web security vulns: XSS, code injection, SQL injection, " +
729
+ "hardcoded secrets, open redirects, prototype pollution, path traversal, insecure CORS. " +
730
+ "Context-aware (frontend/backend/api).",
731
+ inputSchema: SecurityScanSchema.shape,
732
+ }, tx("security_scan", (args) => handleSecurityScan(args)));
733
+ server.registerTool("design_tokens", {
734
+ title: "Design Tokens",
735
+ description: "Generate a complete design system token set from a brand color and mood. " +
736
+ "11-step color scales, neutrals, semantic aliases, type/spacing/radius/shadow tokens. " +
737
+ "Output: CSS vars, Tailwind config, or JSON.",
738
+ inputSchema: DesignTokensSchema.shape,
739
+ }, tx("design_tokens", (args) => handleDesignTokens(args)));
740
+ server.registerTool("perf_hints", {
741
+ title: "Perf Hints",
742
+ description: "Analyze a component or page for Core Web Vitals issues. Detects LCP image priority, " +
743
+ "CLS dimensions, render-blocking scripts, fetch-in-render, INP, missing memoization, " +
744
+ "whole-library imports. Issues ranked by CWV metric impact.",
745
+ inputSchema: PerfHintsSchema.shape,
746
+ }, tx("perf_hints", (args) => handlePerfHints(args)));
747
+ // ---------------------------------------------------------------------------
748
+ // Resources — read-only knowledge graph + config snapshots
749
+ // ---------------------------------------------------------------------------
750
+ server.registerResource("memory-stats", "lucid://memory/stats", {
751
+ title: "Memory Stats",
752
+ description: "Current memory usage: entity/relation/observation counts and DB size.",
753
+ mimeType: "application/json",
754
+ }, async (uri) => ({
755
+ contents: [{ uri: uri.href, mimeType: "application/json", text: memoryStats(db, stmts) }],
756
+ }));
757
+ server.registerResource("memory-graph", "lucid://memory/graph", {
758
+ title: "Memory Graph",
759
+ description: "Full knowledge graph snapshot: all entities, relations, and observations.",
760
+ mimeType: "application/json",
761
+ }, async (uri) => ({
762
+ contents: [{ uri: uri.href, mimeType: "application/json", text: recallAll(db, stmts) }],
763
+ }));
764
+ server.registerResource("memory-recent", new ResourceTemplate("lucid://memory/recent/{hours}", { list: undefined }), {
765
+ title: "Recent Activity",
766
+ description: "Files modified in the last {hours} hours, with line-level diffs.",
767
+ mimeType: "text/markdown",
768
+ }, async (uri, vars) => {
769
+ const hours = Number(vars["hours"]);
770
+ const safeHours = Number.isFinite(hours) && hours > 0 ? Math.min(hours, 720) : 24;
771
+ const text = handleGetRecent(stmts, { hours: safeHours, withDiffs: true });
772
+ return { contents: [{ uri: uri.href, mimeType: "text/markdown", text }] };
773
+ });
774
+ server.registerResource("plan-list", "lucid://plan/list", {
775
+ title: "Active Plans",
776
+ description: "All active development plans with progress summary.",
777
+ mimeType: "text/markdown",
778
+ }, async (uri) => ({
779
+ contents: [{
780
+ uri: uri.href,
781
+ mimeType: "text/markdown",
782
+ text: handlePlanList(stmts, { status: "active" }),
783
+ }],
784
+ }));
785
+ server.registerResource("checklist", "lucid://guardian/checklist", {
786
+ title: "Logic Guardian Checklist",
787
+ description: "Full 5-pass validation checklist Claude must run before completing any task.",
788
+ mimeType: "text/markdown",
789
+ }, async (uri) => ({
790
+ contents: [{ uri: uri.href, mimeType: "text/markdown", text: handleGetChecklist() }],
791
+ }));
792
+ server.registerResource("coding-rules", "lucid://guardian/coding-rules", {
793
+ title: "25 Golden Rules",
794
+ description: "Coding-quality checklist: clarity, naming, single responsibility, frontend rules.",
795
+ mimeType: "text/markdown",
796
+ }, async (uri) => ({
797
+ contents: [{ uri: uri.href, mimeType: "text/markdown", text: handleGetCodingRules() }],
798
+ }));
799
+ server.registerResource("config", "lucid://config", {
800
+ title: "Lucid Configuration",
801
+ description: "Effective configuration (lucid.config.json + env overrides).",
802
+ mimeType: "application/json",
803
+ }, async (uri) => ({
804
+ contents: [{
805
+ uri: uri.href,
806
+ mimeType: "application/json",
807
+ text: JSON.stringify({
808
+ version: SERVER_VERSION,
809
+ config: _appCfg,
810
+ env: {
811
+ MEMORY_DB_PATH: process.env["MEMORY_DB_PATH"] ?? null,
812
+ QDRANT_URL: _qdrantUrl ?? null,
813
+ EMBEDDING_URL: _embeddingUrl ?? null,
814
+ },
815
+ }, null, 2),
816
+ }],
817
+ }));
818
+ // ---------------------------------------------------------------------------
819
+ // Prompts — reusable workflows the user can invoke as slash commands
820
+ // ---------------------------------------------------------------------------
821
+ server.registerPrompt("validate-changes", {
822
+ title: "Validate recent changes",
823
+ description: "Run the Logic Guardian 5-pass validation across files modified in the last N hours.",
824
+ argsSchema: { hours: z.string().optional() },
825
+ }, ({ hours }) => {
826
+ const h = hours ? Number(hours) : 24;
827
+ return {
828
+ messages: [{
829
+ role: "user",
830
+ content: {
831
+ type: "text",
832
+ text: `Run Logic Guardian validation on every file modified in the last ${h} hours.\n\n` +
833
+ `Steps:\n` +
834
+ `1. Call \`get_recent\` with hours=${h} to list changed files.\n` +
835
+ `2. For EACH file, call \`validate_file(path)\` and \`check_code_quality(path)\`.\n` +
836
+ `3. Apply the 5-pass checklist from \`get_checklist\`.\n` +
837
+ `4. Report: per-file findings + a single summary table (file × pass × issue count).\n` +
838
+ `5. Stop and ask before fixing anything — report only.`,
839
+ },
840
+ }],
841
+ };
842
+ });
843
+ server.registerPrompt("audit-file", {
844
+ title: "Audit a single file",
845
+ description: "Run the full Lucid audit pipeline (validate + drift + coding rules + security) on one file.",
846
+ argsSchema: { path: z.string() },
847
+ }, ({ path }) => ({
848
+ messages: [{
849
+ role: "user",
850
+ content: {
851
+ type: "text",
852
+ text: `Audit \`${path}\` with the full Lucid pipeline:\n\n` +
853
+ `1. \`validate_file(path="${path}")\` — Logic Guardian drift detection.\n` +
854
+ `2. \`check_code_quality(path="${path}")\` — 25 Golden Rules.\n` +
855
+ `3. Read the file content, then \`security_scan(code, language, context)\` if it's web code.\n` +
856
+ `4. Apply the 5-pass checklist (\`get_checklist\`).\n` +
857
+ `5. Report findings grouped by severity (high/medium/low). Do not fix yet.`,
858
+ },
859
+ }],
860
+ }));
861
+ server.registerPrompt("plan-feature", {
862
+ title: "Plan a new feature",
863
+ description: "Scaffold a Lucid plan from a feature description with tasks and test criteria.",
864
+ argsSchema: { feature: z.string() },
865
+ }, ({ feature }) => ({
866
+ messages: [{
867
+ role: "user",
868
+ content: {
869
+ type: "text",
870
+ text: `Create a Lucid plan for this feature:\n\n"${feature}"\n\n` +
871
+ `Steps:\n` +
872
+ `1. Call \`smart_context(query="${feature}", task_type="moderate")\` to gather relevant files.\n` +
873
+ `2. Draft a user story: "As a [user], I want [goal], so that [benefit]."\n` +
874
+ `3. Break into 3–8 tasks. EACH task needs explicit \`test_criteria\` (how to verify done).\n` +
875
+ `4. Call \`plan_create({title, description, user_story, tasks})\`.\n` +
876
+ `5. Show the plan ID and the task list.`,
877
+ },
878
+ }],
879
+ }));
880
+ server.registerPrompt("security-review", {
881
+ title: "Security review of recent changes",
882
+ description: "Scan recently changed web code for XSS, injection, secrets, SSRF, and OWASP Top 10 patterns.",
883
+ argsSchema: { hours: z.string().optional() },
884
+ }, ({ hours }) => {
885
+ const h = hours ? Number(hours) : 24;
886
+ return {
887
+ messages: [{
888
+ role: "user",
889
+ content: {
890
+ type: "text",
891
+ text: `Security review of files changed in the last ${h} hours.\n\n` +
892
+ `1. Call \`get_recent\` with hours=${h}.\n` +
893
+ `2. Filter to JS/TS/HTML/Vue files only.\n` +
894
+ `3. For each, read content and call \`security_scan(code, language, context)\` ` +
895
+ `with context inferred from the path (frontend/backend/api).\n` +
896
+ `4. Report findings as a table: file × vuln class × severity × line.\n` +
897
+ `5. Recommend fixes only after the report is complete.`,
898
+ },
899
+ }],
900
+ };
898
901
  });
899
902
  // ---------------------------------------------------------------------------
900
903
  // Start
901
904
  // ---------------------------------------------------------------------------
902
905
  const transport = new StdioServerTransport();
903
906
  await server.connect(transport);
904
- console.error(`[lucid] Server v${getCurrentVersion()} started on stdio.`);
907
+ console.error(`[lucid] Server v${SERVER_VERSION} started on stdio (tools + resources + prompts).`);
905
908
  // Non-blocking — logs to stderr if update is available
906
909
  checkForUpdatesOnStartup().catch(() => { });