@a13xu/lucid 1.12.0 → 1.16.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.
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Semantic compression using LLMLingua-2
3
+ * Model: microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank
4
+ *
5
+ * Reduces text by identifying and dropping semantically unimportant tokens.
6
+ * Uses @huggingface/transformers (ONNX Runtime) for local inference.
7
+ *
8
+ * Pipeline is loaded lazily on first use and cached in memory.
9
+ * Model files are cached in ~/.lucid/models/ after first download (~700MB).
10
+ *
11
+ * Falls back to original text on any error — safe to call unconditionally.
12
+ */
13
+ export interface SemanticCompressionResult {
14
+ compressed: string;
15
+ originalLength: number;
16
+ compressedLength: number;
17
+ /** Fraction of tokens kept (1.0 = no compression) */
18
+ ratio: number;
19
+ method: "llmlingua2" | "fallback";
20
+ }
21
+ /**
22
+ * Compress text using LLMLingua-2 token importance scoring.
23
+ *
24
+ * @param text Input text to compress
25
+ * @param targetRatio Target compression ratio (0.3 = keep 30%, 0.5 = keep 50%)
26
+ * @param minLength Skip compression for texts shorter than this (chars)
27
+ */
28
+ export declare function compressTextSemantic(text: string, targetRatio?: number, minLength?: number): Promise<SemanticCompressionResult>;
29
+ export declare function tryCompressTextSemantic(text: string, targetRatio?: number, minLength?: number): Promise<string>;
30
+ export declare function isSemanticCompressionAvailable(): Promise<boolean>;
31
+ export declare function warmUpSemanticCompression(): void;
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Semantic compression using LLMLingua-2
3
+ * Model: microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank
4
+ *
5
+ * Reduces text by identifying and dropping semantically unimportant tokens.
6
+ * Uses @huggingface/transformers (ONNX Runtime) for local inference.
7
+ *
8
+ * Pipeline is loaded lazily on first use and cached in memory.
9
+ * Model files are cached in ~/.lucid/models/ after first download (~700MB).
10
+ *
11
+ * Falls back to original text on any error — safe to call unconditionally.
12
+ */
13
+ import { join } from "path";
14
+ import { homedir } from "os";
15
+ import { mkdirSync } from "fs";
16
+ const MODEL_ID = "microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank";
17
+ const MODELS_DIR = join(homedir(), ".lucid", "models");
18
+ let _pipeline = null;
19
+ let _loadError = null;
20
+ let _loading = false;
21
+ async function getPipeline() {
22
+ if (_loadError)
23
+ throw _loadError;
24
+ if (_pipeline)
25
+ return _pipeline;
26
+ if (_loading) {
27
+ // Wait for concurrent load
28
+ await new Promise((resolve) => {
29
+ const check = setInterval(() => {
30
+ if (!_loading) {
31
+ clearInterval(check);
32
+ resolve();
33
+ }
34
+ }, 100);
35
+ });
36
+ if (_loadError)
37
+ throw _loadError;
38
+ if (_pipeline)
39
+ return _pipeline;
40
+ }
41
+ _loading = true;
42
+ try {
43
+ mkdirSync(MODELS_DIR, { recursive: true });
44
+ // Dynamic import keeps startup fast when compression is not used
45
+ const { pipeline, env } = await import("@huggingface/transformers");
46
+ env.cacheDir = MODELS_DIR;
47
+ env.allowRemoteModels = true;
48
+ process.stderr.write(`[Lucid] Loading LLMLingua-2 model (first run: downloads ~700MB to ${MODELS_DIR})…\n`);
49
+ _pipeline = (await pipeline("token-classification", MODEL_ID, {
50
+ dtype: "q8", // 8-bit quantization — smaller, faster, minimal quality loss
51
+ device: "cpu",
52
+ }));
53
+ process.stderr.write("[Lucid] LLMLingua-2 model ready.\n");
54
+ return _pipeline;
55
+ }
56
+ catch (e) {
57
+ _loadError = e instanceof Error ? e : new Error(String(e));
58
+ throw _loadError;
59
+ }
60
+ finally {
61
+ _loading = false;
62
+ }
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // Core compression
66
+ // ---------------------------------------------------------------------------
67
+ /**
68
+ * Compress text using LLMLingua-2 token importance scoring.
69
+ *
70
+ * @param text Input text to compress
71
+ * @param targetRatio Target compression ratio (0.3 = keep 30%, 0.5 = keep 50%)
72
+ * @param minLength Skip compression for texts shorter than this (chars)
73
+ */
74
+ export async function compressTextSemantic(text, targetRatio = 0.5, minLength = 300) {
75
+ if (text.length < minLength) {
76
+ return {
77
+ compressed: text,
78
+ originalLength: text.length,
79
+ compressedLength: text.length,
80
+ ratio: 1.0,
81
+ method: "fallback",
82
+ };
83
+ }
84
+ const pipe = await getPipeline();
85
+ // Run token classification — each token gets entity "LABEL_0" (drop) / "LABEL_1" (keep)
86
+ const tokens = await pipe(text, {
87
+ // Disable aggregation to get per-sub-token results with offsets
88
+ aggregation_strategy: "none",
89
+ });
90
+ if (!tokens || tokens.length === 0) {
91
+ return {
92
+ compressed: text,
93
+ originalLength: text.length,
94
+ compressedLength: text.length,
95
+ ratio: 1.0,
96
+ method: "fallback",
97
+ };
98
+ }
99
+ // Determine importance threshold:
100
+ // Sort all "keep" scores descending, keep the top (targetRatio * N) tokens
101
+ const keepScores = tokens
102
+ .filter((t) => t.entity === "LABEL_1" || t.entity === "1")
103
+ .map((t) => t.score)
104
+ .sort((a, b) => b - a);
105
+ // If not enough LABEL_1 tokens, use score-based threshold
106
+ let threshold;
107
+ if (keepScores.length > 0) {
108
+ const cutoffIdx = Math.floor(tokens.length * targetRatio);
109
+ // Find the score at the cutoff rank among all tokens sorted by score
110
+ const allScores = tokens.map((t) => ({
111
+ score: t.entity === "LABEL_1" || t.entity === "1" ? t.score : 1 - t.score,
112
+ })).sort((a, b) => b.score - a.score);
113
+ threshold = allScores[Math.min(cutoffIdx, allScores.length - 1)]?.score ?? 0.5;
114
+ }
115
+ else {
116
+ // Fallback: use raw score threshold
117
+ threshold = 0.5;
118
+ }
119
+ // Mark characters to keep based on token offsets
120
+ const keepChars = new Uint8Array(text.length);
121
+ for (const token of tokens) {
122
+ const isImportant = token.entity === "LABEL_1" ||
123
+ token.entity === "1" ||
124
+ (token.entity !== "LABEL_0" && token.entity !== "0" && token.score >= threshold);
125
+ if (isImportant && token.start !== undefined && token.end !== undefined) {
126
+ keepChars.fill(1, token.start, token.end);
127
+ }
128
+ }
129
+ // Always keep structural markers (newlines, sentence boundaries)
130
+ const FORCE_KEEP = new Set(["\n", ".", "!", "?", ","]);
131
+ for (let i = 0; i < text.length; i++) {
132
+ if (FORCE_KEEP.has(text[i]))
133
+ keepChars[i] = 1;
134
+ }
135
+ // Reconstruct compressed text from character mask
136
+ let compressed = "";
137
+ let prevKept = false;
138
+ for (let i = 0; i < text.length; i++) {
139
+ if (keepChars[i]) {
140
+ // Preserve a single space when skipping tokens in mid-sentence
141
+ if (!prevKept && compressed.length > 0 && text[i] !== " " && !FORCE_KEEP.has(text[i - 1] ?? "")) {
142
+ compressed += " ";
143
+ }
144
+ compressed += text[i];
145
+ prevKept = true;
146
+ }
147
+ else {
148
+ prevKept = false;
149
+ }
150
+ }
151
+ // Clean up artefacts from compression
152
+ compressed = compressed
153
+ .replace(/ +/g, " ") // multiple spaces → single
154
+ .replace(/\n{3,}/g, "\n\n") // more than 2 newlines → 2
155
+ .replace(/ ([.,!?])/g, "$1") // space before punctuation → no space
156
+ .trim();
157
+ const keptCount = keepScores.length;
158
+ const actualRatio = tokens.length > 0 ? keptCount / tokens.length : 1.0;
159
+ return {
160
+ compressed,
161
+ originalLength: text.length,
162
+ compressedLength: compressed.length,
163
+ ratio: actualRatio,
164
+ method: "llmlingua2",
165
+ };
166
+ }
167
+ // ---------------------------------------------------------------------------
168
+ // Safe wrapper — always returns a string, never throws
169
+ // ---------------------------------------------------------------------------
170
+ export async function tryCompressTextSemantic(text, targetRatio = 0.5, minLength = 300) {
171
+ try {
172
+ const result = await compressTextSemantic(text, targetRatio, minLength);
173
+ return result.compressed;
174
+ }
175
+ catch {
176
+ return text;
177
+ }
178
+ }
179
+ // ---------------------------------------------------------------------------
180
+ // Availability check — call before bulk compression to fail fast
181
+ // ---------------------------------------------------------------------------
182
+ export async function isSemanticCompressionAvailable() {
183
+ try {
184
+ await getPipeline();
185
+ return true;
186
+ }
187
+ catch {
188
+ return false;
189
+ }
190
+ }
191
+ // ---------------------------------------------------------------------------
192
+ // Warm-up (optional — call at startup to pre-load model)
193
+ // ---------------------------------------------------------------------------
194
+ export function warmUpSemanticCompression() {
195
+ getPipeline().catch(() => { });
196
+ }
package/build/config.d.ts CHANGED
@@ -12,6 +12,21 @@ export interface LucidConfig {
12
12
  recentWindowHours?: number;
13
13
  /** Security guard configuration */
14
14
  security?: SecurityConfig;
15
+ /**
16
+ * Semantic compression via LLMLingua-2 (microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank).
17
+ * When enabled, file content is compressed before being returned to Claude and before Qdrant embedding.
18
+ * Model is downloaded on first use (~700MB) and cached in ~/.lucid/models/.
19
+ */
20
+ semanticCompression?: {
21
+ /** Enable semantic compression (default: false — opt-in) */
22
+ enabled?: boolean;
23
+ /** Target compression ratio: 0.3 = keep 30%, 0.5 = keep 50% (default: 0.5) */
24
+ ratio?: number;
25
+ /** Skip compression for texts shorter than this in chars (default: 300) */
26
+ minLength?: number;
27
+ /** Also compress text before Qdrant embedding generation (default: true when enabled) */
28
+ applyToEmbeddings?: boolean;
29
+ };
15
30
  /** Optional Qdrant vector search (falls back to TF-IDF if not configured) */
16
31
  qdrant?: {
17
32
  url: string;
@@ -0,0 +1,3 @@
1
+ import { Router } from "express";
2
+ import type { Statements } from "../database.js";
3
+ export declare function createRoutes(stmts: Statements): Router;
@@ -0,0 +1,56 @@
1
+ import { Router } from "express";
2
+ import { handleSyncFile, handleSyncProject } from "../tools/sync.js";
3
+ import { handleGetContext } from "../tools/context.js";
4
+ import { handleValidateFile } from "../tools/guardian.js";
5
+ import { getCurrentVersion } from "../tools/updater.js";
6
+ export function createRoutes(stmts) {
7
+ const router = Router();
8
+ // POST /sync — sync a single file
9
+ router.post("/sync", (req, res) => {
10
+ try {
11
+ const result = handleSyncFile(stmts, req.body);
12
+ res.json({ ok: true, result });
13
+ }
14
+ catch (e) {
15
+ res.status(500).json({ ok: false, error: String(e) });
16
+ }
17
+ });
18
+ // POST /sync-project — sync entire project directory
19
+ router.post("/sync-project", (req, res) => {
20
+ try {
21
+ const result = handleSyncProject(stmts, req.body);
22
+ res.json({ ok: true, result });
23
+ }
24
+ catch (e) {
25
+ res.status(500).json({ ok: false, error: String(e) });
26
+ }
27
+ });
28
+ // GET /context?q=...&maxTokens=4000 — retrieve relevant context
29
+ router.get("/context", async (req, res) => {
30
+ try {
31
+ const result = await handleGetContext(stmts, {
32
+ query: String(req.query["q"] ?? ""),
33
+ maxTokens: req.query["maxTokens"] ? Number(req.query["maxTokens"]) : 4000,
34
+ });
35
+ res.json({ ok: true, result });
36
+ }
37
+ catch (e) {
38
+ res.status(500).json({ ok: false, error: String(e) });
39
+ }
40
+ });
41
+ // POST /validate — validate a file for drift/quality issues
42
+ router.post("/validate", (req, res) => {
43
+ try {
44
+ const result = handleValidateFile(req.body);
45
+ res.json({ ok: true, result });
46
+ }
47
+ catch (e) {
48
+ res.status(500).json({ ok: false, error: String(e) });
49
+ }
50
+ });
51
+ // GET /health — liveness check
52
+ router.get("/health", (_req, res) => {
53
+ res.json({ ok: true, version: getCurrentVersion() });
54
+ });
55
+ return router;
56
+ }
@@ -0,0 +1,7 @@
1
+ import type { Server } from "http";
2
+ import type { Statements } from "../database.js";
3
+ export interface HttpServerOptions {
4
+ port?: number;
5
+ host?: string;
6
+ }
7
+ export declare function startHttpServer(stmts: Statements, options?: HttpServerOptions): Server;
@@ -0,0 +1,11 @@
1
+ import express from "express";
2
+ import { createRoutes } from "./routes.js";
3
+ export function startHttpServer(stmts, options = {}) {
4
+ const { port = 7821, host = "127.0.0.1" } = options;
5
+ const app = express();
6
+ app.use(express.json());
7
+ app.use("/", createRoutes(stmts));
8
+ return app.listen(port, host, () => {
9
+ process.stderr.write(`[Lucid] HTTP server listening on ${host}:${port}\n`);
10
+ });
11
+ }
package/build/index.js CHANGED
@@ -21,7 +21,111 @@ import { handleGetContext, GetContextSchema, handleGetRecent, GetRecentSchema, }
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
23
  import { handlePlanCreate, PlanCreateSchema, handlePlanList, PlanListSchema, handlePlanGet, PlanGetSchema, handlePlanUpdateTask, PlanUpdateTaskSchema, } from "./tools/plan.js";
24
+ import { UpdateLucidSchema, handleUpdateLucid, checkForUpdatesOnStartup, getCurrentVersion, } from "./tools/updater.js";
24
25
  import { GenerateComponentSchema, handleGenerateComponent, ScaffoldPageSchema, handleScaffoldPage, SeoMetaSchema, handleSeoMeta, AccessibilityAuditSchema, handleAccessibilityAudit, ApiClientSchema, handleApiClient, TestGeneratorSchema, handleTestGenerator, ResponsiveLayoutSchema, handleResponsiveLayout, SecurityScanSchema, handleSecurityScan, DesignTokensSchema, handleDesignTokens, PerfHintsSchema, handlePerfHints, } from "./tools/webdev/index.js";
26
+ import { handleSmartContext, SmartContextSchema } from "./tools/smart-context.js";
27
+ import { handleSuggestModel, SuggestModelSchema } from "./tools/model-advisor.js";
28
+ import { handleCompressText, CompressTextSchema } from "./tools/compress.js";
29
+ // ---------------------------------------------------------------------------
30
+ // CLI mode: lucid watch | lucid status | lucid stop
31
+ // ---------------------------------------------------------------------------
32
+ const [, , _cliCmd, ..._cliArgs] = process.argv;
33
+ if (_cliCmd === "watch" || _cliCmd === "status" || _cliCmd === "stop") {
34
+ await runCli(_cliCmd, _cliArgs);
35
+ process.exit(0);
36
+ }
37
+ async function runCli(cmd, args) {
38
+ const { join } = await import("path");
39
+ const { homedir } = await import("os");
40
+ const { existsSync, mkdirSync, writeFileSync, readFileSync } = await import("fs");
41
+ const PID_DIR = join(homedir(), ".lucid");
42
+ const PID_FILE = join(PID_DIR, "watch.pid");
43
+ if (cmd === "status") {
44
+ if (!existsSync(PID_FILE)) {
45
+ console.log("Lucid daemon: not running");
46
+ return;
47
+ }
48
+ const pid = readFileSync(PID_FILE, "utf-8").trim();
49
+ try {
50
+ process.kill(Number(pid), 0);
51
+ console.log(`Lucid daemon: running (PID ${pid})`);
52
+ }
53
+ catch {
54
+ console.log("Lucid daemon: not running (stale PID file)");
55
+ }
56
+ return;
57
+ }
58
+ if (cmd === "stop") {
59
+ if (!existsSync(PID_FILE)) {
60
+ console.log("Lucid daemon: not running");
61
+ return;
62
+ }
63
+ const pid = readFileSync(PID_FILE, "utf-8").trim();
64
+ try {
65
+ process.kill(Number(pid), "SIGTERM");
66
+ console.log(`Lucid daemon stopped (PID ${pid})`);
67
+ }
68
+ catch {
69
+ console.log("Lucid daemon: not running (stale PID file)");
70
+ }
71
+ return;
72
+ }
73
+ // cmd === "watch"
74
+ const portIdx = args.indexOf("--port");
75
+ const port = portIdx >= 0 ? Number(args[portIdx + 1]) : 7821;
76
+ const noHttp = args.includes("--no-http");
77
+ const watchDir = args.find((a) => !a.startsWith("--")) ?? process.cwd();
78
+ const { initDatabase, prepareStatements } = await import("./database.js");
79
+ const db = initDatabase();
80
+ const stmts = prepareStatements(db);
81
+ if (!noHttp) {
82
+ const { startHttpServer } = await import("./http/server.js");
83
+ startHttpServer(stmts, { port });
84
+ }
85
+ mkdirSync(PID_DIR, { recursive: true });
86
+ writeFileSync(PID_FILE, String(process.pid), "utf-8");
87
+ const chokidar = await import("chokidar");
88
+ const watcher = chokidar.watch(watchDir, {
89
+ ignored: [/node_modules/, /\.git/, /[/\\]build[/\\]/, /[/\\]dist[/\\]/, /\.d\.ts$/],
90
+ persistent: true,
91
+ ignoreInitial: true,
92
+ });
93
+ const DEBOUNCE_MS = 300;
94
+ const timers = new Map();
95
+ const syncPath = (filePath) => {
96
+ const existing = timers.get(filePath);
97
+ if (existing)
98
+ clearTimeout(existing);
99
+ timers.set(filePath, setTimeout(() => {
100
+ timers.delete(filePath);
101
+ if (!noHttp) {
102
+ fetch(`http://localhost:${port}/sync`, {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/json" },
105
+ body: JSON.stringify({ path: filePath }),
106
+ }).catch(() => { });
107
+ }
108
+ else {
109
+ import("./tools/sync.js").then(({ handleSyncFile }) => {
110
+ handleSyncFile(stmts, { path: filePath });
111
+ }).catch(() => { });
112
+ }
113
+ }, DEBOUNCE_MS));
114
+ };
115
+ watcher.on("add", syncPath).on("change", syncPath);
116
+ process.stderr.write(`[Lucid] Watching ${watchDir}${noHttp ? " (no HTTP)" : ` on port ${port}`}\n`);
117
+ const shutdown = () => {
118
+ watcher.close().catch(() => { });
119
+ try {
120
+ db.pragma("wal_checkpoint(FULL)");
121
+ }
122
+ catch { /* ignore */ }
123
+ process.exit(0);
124
+ };
125
+ process.on("SIGINT", shutdown);
126
+ process.on("SIGTERM", shutdown);
127
+ await new Promise(() => { });
128
+ }
25
129
  // ---------------------------------------------------------------------------
26
130
  // Init DB
27
131
  // ---------------------------------------------------------------------------
@@ -54,7 +158,7 @@ else {
54
158
  // ---------------------------------------------------------------------------
55
159
  // MCP Server
56
160
  // ---------------------------------------------------------------------------
57
- const server = new Server({ name: "lucid", version: "1.12.0" }, { capabilities: { tools: {} } });
161
+ const server = new Server({ name: "lucid", version: "1.13.0" }, { capabilities: { tools: {} } });
58
162
  // ---------------------------------------------------------------------------
59
163
  // Tool definitions
60
164
  // ---------------------------------------------------------------------------
@@ -221,6 +325,69 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
221
325
  },
222
326
  },
223
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
+ },
224
391
  // ── Reward System ────────────────────────────────────────────────────────
225
392
  {
226
393
  name: "reward",
@@ -393,6 +560,23 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
393
560
  required: ["task_id", "status"],
394
561
  },
395
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
+ },
396
580
  // ── Web Dev Skills ───────────────────────────────────────────────────────
397
581
  {
398
582
  name: "generate_component",
@@ -614,6 +798,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
614
798
  case "get_recent":
615
799
  text = handleGetRecent(stmts, GetRecentSchema.parse(args));
616
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;
617
811
  // Reward System
618
812
  case "reward":
619
813
  text = handleReward(stmts, RewardSchema.parse(args));
@@ -654,6 +848,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
654
848
  case "plan_update_task":
655
849
  text = handlePlanUpdateTask(stmts, PlanUpdateTaskSchema.parse(args));
656
850
  break;
851
+ // Updater
852
+ case "update_lucid":
853
+ text = await handleUpdateLucid(UpdateLucidSchema.parse(args));
854
+ break;
657
855
  // Web Dev Skills
658
856
  case "generate_component":
659
857
  text = handleGenerateComponent(GenerateComponentSchema.parse(args));
@@ -703,4 +901,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
703
901
  // ---------------------------------------------------------------------------
704
902
  const transport = new StdioServerTransport();
705
903
  await server.connect(transport);
706
- console.error("[lucid] Server started on stdio.");
904
+ console.error(`[lucid] Server v${getCurrentVersion()} started on stdio.`);
905
+ // Non-blocking — logs to stderr if update is available
906
+ checkForUpdatesOnStartup().catch(() => { });
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lucid-sync — PostToolUse hook script for Lucid.
4
+ *
5
+ * Called by Claude Code's PostToolUse hook after Write/Edit/NotebookEdit.
6
+ * Reads tool input from stdin (JSON), extracts the modified file path,
7
+ * then syncs it to Lucid's SQLite index.
8
+ *
9
+ * Fallback chain:
10
+ * 1. POST http://localhost:7821/sync (if lucid watch daemon is running)
11
+ * 2. Direct SQLite write (always works, no daemon needed)
12
+ *
13
+ * Never throws — hook failures must not interrupt Claude Code.
14
+ */
15
+ export {};
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lucid-sync — PostToolUse hook script for Lucid.
4
+ *
5
+ * Called by Claude Code's PostToolUse hook after Write/Edit/NotebookEdit.
6
+ * Reads tool input from stdin (JSON), extracts the modified file path,
7
+ * then syncs it to Lucid's SQLite index.
8
+ *
9
+ * Fallback chain:
10
+ * 1. POST http://localhost:7821/sync (if lucid watch daemon is running)
11
+ * 2. Direct SQLite write (always works, no daemon needed)
12
+ *
13
+ * Never throws — hook failures must not interrupt Claude Code.
14
+ */
15
+ import { readFileSync } from "fs";
16
+ function getFilePathFromStdin() {
17
+ try {
18
+ const raw = readFileSync("/dev/stdin", "utf-8").trim();
19
+ if (!raw)
20
+ return process.argv[2] ?? null;
21
+ const data = JSON.parse(raw);
22
+ const ti = data.tool_input ?? {};
23
+ return ti.file_path ?? ti.notebook_path ?? ti.path ?? process.argv[2] ?? null;
24
+ }
25
+ catch {
26
+ return process.argv[2] ?? null;
27
+ }
28
+ }
29
+ // ---------------------------------------------------------------------------
30
+ // HTTP sync (fast — daemon must be running on port 7821)
31
+ // ---------------------------------------------------------------------------
32
+ async function tryHttpSync(filePath, port = 7821) {
33
+ try {
34
+ const controller = new AbortController();
35
+ const timer = setTimeout(() => controller.abort(), 500);
36
+ const res = await fetch(`http://localhost:${port}/sync`, {
37
+ method: "POST",
38
+ headers: { "Content-Type": "application/json" },
39
+ body: JSON.stringify({ path: filePath }),
40
+ signal: controller.signal,
41
+ });
42
+ clearTimeout(timer);
43
+ return res.ok;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ // ---------------------------------------------------------------------------
50
+ // Direct SQLite sync (fallback — always available)
51
+ // ---------------------------------------------------------------------------
52
+ async function syncDirect(filePath) {
53
+ const { initDatabase, prepareStatements } = await import("./database.js");
54
+ const { handleSyncFile } = await import("./tools/sync.js");
55
+ const db = initDatabase();
56
+ const stmts = prepareStatements(db);
57
+ handleSyncFile(stmts, { path: filePath });
58
+ db.close();
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // Main
62
+ // ---------------------------------------------------------------------------
63
+ async function main() {
64
+ const filePath = getFilePathFromStdin();
65
+ if (!filePath)
66
+ return;
67
+ const httpOk = await tryHttpSync(filePath);
68
+ if (!httpOk) {
69
+ await syncDirect(filePath);
70
+ }
71
+ }
72
+ main().catch(() => { }); // never propagate errors to Claude Code