@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.
- package/build/compression/semantic.d.ts +31 -0
- package/build/compression/semantic.js +196 -0
- package/build/config.d.ts +15 -0
- package/build/http/routes.d.ts +3 -0
- package/build/http/routes.js +56 -0
- package/build/http/server.d.ts +7 -0
- package/build/http/server.js +11 -0
- package/build/index.js +202 -2
- package/build/lucid-sync.d.ts +15 -0
- package/build/lucid-sync.js +72 -0
- package/build/retrieval/context.js +6 -0
- package/build/retrieval/qdrant.d.ts +1 -1
- package/build/retrieval/qdrant.js +11 -2
- package/build/tools/compress.d.ts +15 -0
- package/build/tools/compress.js +18 -0
- package/build/tools/init.js +62 -11
- package/build/tools/model-advisor.d.ts +9 -0
- package/build/tools/model-advisor.js +30 -0
- package/build/tools/smart-context.d.ts +16 -0
- package/build/tools/smart-context.js +54 -0
- package/build/tools/sync.js +8 -0
- package/build/tools/updater.d.ts +11 -0
- package/build/tools/updater.js +133 -0
- package/package.json +64 -59
- package/skills/lucid-audit/SKILL.md +73 -53
- package/skills/lucid-context/SKILL.md +69 -35
- package/skills/lucid-plan/SKILL.md +52 -60
- package/skills/lucid-security/SKILL.md +41 -59
- package/skills/lucid-start/SKILL.md +70 -0
- package/skills/lucid-webdev/SKILL.md +45 -123
|
@@ -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,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,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.
|
|
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(
|
|
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
|