@a13xu/lucid 1.13.0 → 1.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -21
- 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 +176 -0
- 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 +16 -1
- 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/package.json +64 -59
- package/skills/lucid-audit/SKILL.md +43 -23
- package/skills/lucid-context/SKILL.md +54 -20
- package/skills/lucid-plan/SKILL.md +25 -33
- package/skills/lucid-security/SKILL.md +22 -40
- package/skills/lucid-start/SKILL.md +70 -0
- package/skills/lucid-webdev/SKILL.md +31 -109
package/build/index.js
CHANGED
|
@@ -23,6 +23,109 @@ import { handleGetCodingRules, handleCheckCodeQuality, CheckCodeQualitySchema, }
|
|
|
23
23
|
import { handlePlanCreate, PlanCreateSchema, handlePlanList, PlanListSchema, handlePlanGet, PlanGetSchema, handlePlanUpdateTask, PlanUpdateTaskSchema, } from "./tools/plan.js";
|
|
24
24
|
import { UpdateLucidSchema, handleUpdateLucid, checkForUpdatesOnStartup, getCurrentVersion, } from "./tools/updater.js";
|
|
25
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
|
+
}
|
|
26
129
|
// ---------------------------------------------------------------------------
|
|
27
130
|
// Init DB
|
|
28
131
|
// ---------------------------------------------------------------------------
|
|
@@ -222,6 +325,69 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
222
325
|
},
|
|
223
326
|
},
|
|
224
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
|
+
},
|
|
225
391
|
// ── Reward System ────────────────────────────────────────────────────────
|
|
226
392
|
{
|
|
227
393
|
name: "reward",
|
|
@@ -632,6 +798,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
632
798
|
case "get_recent":
|
|
633
799
|
text = handleGetRecent(stmts, GetRecentSchema.parse(args));
|
|
634
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;
|
|
635
811
|
// Reward System
|
|
636
812
|
case "reward":
|
|
637
813
|
text = handleReward(stmts, RewardSchema.parse(args));
|
|
@@ -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
|
|
@@ -6,6 +6,7 @@ import { extractSkeleton, renderSkeleton } from "../indexer/ast.js";
|
|
|
6
6
|
import { searchQdrant } from "./qdrant.js";
|
|
7
7
|
import { getQdrantConfig } from "../config.js";
|
|
8
8
|
import { getFileRewardsMap } from "../memory/experience.js";
|
|
9
|
+
import { tryCompressTextSemantic } from "../compression/semantic.js";
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
// Token estimation (1 token ≈ 4 chars is the standard heuristic)
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
@@ -183,6 +184,11 @@ export async function assembleContext(query, stmts, cfg, opts = {}) {
|
|
|
183
184
|
}
|
|
184
185
|
if (isRecent)
|
|
185
186
|
reason += " +recent";
|
|
187
|
+
// Semantic compression — applied after skeleton/full decision, before token counting
|
|
188
|
+
if (cfg.semanticCompression?.enabled) {
|
|
189
|
+
content = await tryCompressTextSemantic(content, cfg.semanticCompression.ratio ?? 0.5, cfg.semanticCompression.minLength ?? 300);
|
|
190
|
+
reason += " +compressed";
|
|
191
|
+
}
|
|
186
192
|
const contentTokens = estimateTokens(content);
|
|
187
193
|
if (contentTokens < 10) {
|
|
188
194
|
skippedFiles++;
|
|
@@ -8,7 +8,7 @@ export interface VectorChunk {
|
|
|
8
8
|
score: number;
|
|
9
9
|
}
|
|
10
10
|
/** Index one file into Qdrant (called by sync_file when Qdrant is configured). */
|
|
11
|
-
export declare function indexFileInQdrant(filepath: string, text: string, cfg: QdrantCfg): Promise<void>;
|
|
11
|
+
export declare function indexFileInQdrant(filepath: string, text: string, cfg: QdrantCfg, compressionCfg?: ResolvedConfig["semanticCompression"]): Promise<void>;
|
|
12
12
|
/** Top-k semantic search across all indexed chunks. */
|
|
13
13
|
export declare function searchQdrant(query: string, topK: number, cfg: QdrantCfg): Promise<VectorChunk[]>;
|
|
14
14
|
/** Check if Qdrant collection exists and is reachable. */
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Only active when QDRANT_URL is set (via env var or lucid.config.json)
|
|
3
3
|
// Falls back silently to TF-IDF when unavailable
|
|
4
4
|
import { safeFetch } from "../security/ssrf.js";
|
|
5
|
+
import { tryCompressTextSemantic } from "../compression/semantic.js";
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Embedding generation (OpenAI-compatible endpoint)
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
@@ -85,16 +86,24 @@ function stableId(s) {
|
|
|
85
86
|
// Public API
|
|
86
87
|
// ---------------------------------------------------------------------------
|
|
87
88
|
/** Index one file into Qdrant (called by sync_file when Qdrant is configured). */
|
|
88
|
-
export async function indexFileInQdrant(filepath, text, cfg) {
|
|
89
|
+
export async function indexFileInQdrant(filepath, text, cfg, compressionCfg) {
|
|
89
90
|
await ensureCollection(cfg);
|
|
90
91
|
const chunks = chunkFile(filepath, text);
|
|
91
92
|
if (chunks.length === 0)
|
|
92
93
|
return;
|
|
94
|
+
const compressForEmbedding = compressionCfg?.enabled && compressionCfg.applyToEmbeddings !== false;
|
|
93
95
|
// Batch embed (max 96 texts per request for most providers)
|
|
94
96
|
const BATCH = 32;
|
|
95
97
|
for (let b = 0; b < chunks.length; b += BATCH) {
|
|
96
98
|
const batch = chunks.slice(b, b + BATCH);
|
|
97
|
-
|
|
99
|
+
let textsToEmbed;
|
|
100
|
+
if (compressForEmbedding) {
|
|
101
|
+
textsToEmbed = await Promise.all(batch.map((c) => tryCompressTextSemantic(c.text, compressionCfg.ratio ?? 0.5, compressionCfg.minLength ?? 300)));
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
textsToEmbed = batch.map((c) => c.text);
|
|
105
|
+
}
|
|
106
|
+
const vectors = await embed(textsToEmbed, cfg);
|
|
98
107
|
const points = batch.map((c, idx) => ({
|
|
99
108
|
id: c.id,
|
|
100
109
|
vector: vectors[idx],
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const CompressTextSchema: z.ZodObject<{
|
|
3
|
+
text: z.ZodString;
|
|
4
|
+
ratio: z.ZodOptional<z.ZodNumber>;
|
|
5
|
+
min_length: z.ZodOptional<z.ZodNumber>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
text: string;
|
|
8
|
+
ratio?: number | undefined;
|
|
9
|
+
min_length?: number | undefined;
|
|
10
|
+
}, {
|
|
11
|
+
text: string;
|
|
12
|
+
ratio?: number | undefined;
|
|
13
|
+
min_length?: number | undefined;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function handleCompressText(args: z.infer<typeof CompressTextSchema>): Promise<string>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { compressTextSemantic } from "../compression/semantic.js";
|
|
3
|
+
export const CompressTextSchema = z.object({
|
|
4
|
+
text: z.string().min(1).describe("Text to compress"),
|
|
5
|
+
ratio: z.number().min(0.1).max(0.9).optional().describe("Target compression ratio: 0.3 = keep 30%, 0.5 = keep 50% (default: 0.5)"),
|
|
6
|
+
min_length: z.number().int().optional().describe("Skip compression for texts shorter than this in chars (default: 300)"),
|
|
7
|
+
});
|
|
8
|
+
export async function handleCompressText(args) {
|
|
9
|
+
const result = await compressTextSemantic(args.text, args.ratio ?? 0.5, args.min_length ?? 300);
|
|
10
|
+
return JSON.stringify({
|
|
11
|
+
compressed: result.compressed,
|
|
12
|
+
original_length: result.originalLength,
|
|
13
|
+
compressed_length: result.compressedLength,
|
|
14
|
+
ratio_kept: result.ratio,
|
|
15
|
+
method: result.method,
|
|
16
|
+
tokens_saved: Math.ceil((result.originalLength - result.compressedLength) / 4),
|
|
17
|
+
}, null, 2);
|
|
18
|
+
}
|
package/build/tools/init.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolve, join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
3
4
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
5
|
import { fileURLToPath } from "url";
|
|
5
6
|
import { indexProject } from "../indexer/project.js";
|
|
@@ -35,7 +36,7 @@ const LUCID_HOOK = {
|
|
|
35
36
|
hooks: [
|
|
36
37
|
{
|
|
37
38
|
type: "command",
|
|
38
|
-
command: `echo '🔄 ${LUCID_MARKER}(path)
|
|
39
|
+
command: `lucid-sync 2>/dev/null || echo '🔄 ${LUCID_MARKER}(path) — install lucid globally: npm i -g @a13xu/lucid'`,
|
|
39
40
|
},
|
|
40
41
|
],
|
|
41
42
|
};
|
|
@@ -167,6 +168,17 @@ export async function handleInitProject(stmts, input) {
|
|
|
167
168
|
else if (skillsResult.skipped.length > 0) {
|
|
168
169
|
lines.push(`📚 Skills: already installed (${skillsResult.skipped.length} skill(s))`);
|
|
169
170
|
}
|
|
171
|
+
// ── Global skills (~/.claude/skills/) ────────────────────────────────────
|
|
172
|
+
const globalSkillsResult = installGlobalSkills();
|
|
173
|
+
if (globalSkillsResult.installed.length > 0) {
|
|
174
|
+
lines.push(`🌐 Global skills installed in ~/.claude/skills/:`);
|
|
175
|
+
for (const s of globalSkillsResult.installed) {
|
|
176
|
+
lines.push(` • /${s} (available in all projects)`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else if (globalSkillsResult.skipped.length > 0) {
|
|
180
|
+
lines.push(`🌐 Global skills: already installed (${globalSkillsResult.skipped.length} skill(s))`);
|
|
181
|
+
}
|
|
170
182
|
// ── CLAUDE.md injection ───────────────────────────────────────────────────
|
|
171
183
|
const injected = injectClaudeMdInstruction(dir);
|
|
172
184
|
if (injected) {
|
|
@@ -277,6 +289,9 @@ function installSkills(projectDir) {
|
|
|
277
289
|
}
|
|
278
290
|
return result;
|
|
279
291
|
}
|
|
292
|
+
function installGlobalSkills() {
|
|
293
|
+
return installSkills(homedir());
|
|
294
|
+
}
|
|
280
295
|
function buildChannelSummary(cfg) {
|
|
281
296
|
const channels = [];
|
|
282
297
|
if (cfg.adminEmail && cfg.smtpHost)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const SuggestModelSchema: z.ZodObject<{
|
|
3
|
+
task_description: z.ZodString;
|
|
4
|
+
}, "strip", z.ZodTypeAny, {
|
|
5
|
+
task_description: string;
|
|
6
|
+
}, {
|
|
7
|
+
task_description: string;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function handleSuggestModel(args: z.infer<typeof SuggestModelSchema>): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const SuggestModelSchema = z.object({
|
|
3
|
+
task_description: z.string().min(1).describe("Natural language description of the task you are about to perform"),
|
|
4
|
+
});
|
|
5
|
+
const HAIKU_TRIGGERS = [
|
|
6
|
+
"list", "show", "find", "search", "where", "what is",
|
|
7
|
+
"recall", "get recent", "status",
|
|
8
|
+
];
|
|
9
|
+
const MODEL_IDS = {
|
|
10
|
+
haiku: "claude-haiku-4-5-20251001",
|
|
11
|
+
sonnet: "claude-sonnet-4-6",
|
|
12
|
+
};
|
|
13
|
+
const CONTEXT_BUDGETS = {
|
|
14
|
+
haiku: 2000,
|
|
15
|
+
sonnet: 8000,
|
|
16
|
+
};
|
|
17
|
+
export function handleSuggestModel(args) {
|
|
18
|
+
const lower = args.task_description.toLowerCase();
|
|
19
|
+
const haikuTrigger = HAIKU_TRIGGERS.find((t) => lower.includes(t));
|
|
20
|
+
const model = haikuTrigger ? "haiku" : "sonnet";
|
|
21
|
+
const reasoning = haikuTrigger
|
|
22
|
+
? `Task matches retrieval/lookup pattern ("${haikuTrigger}") — Haiku is faster for read-only queries.`
|
|
23
|
+
: "No simple retrieval trigger detected — defaulting to Sonnet for reasoning, code generation, and analysis.";
|
|
24
|
+
return JSON.stringify({
|
|
25
|
+
model,
|
|
26
|
+
model_id: MODEL_IDS[model],
|
|
27
|
+
reasoning,
|
|
28
|
+
context_budget: CONTEXT_BUDGETS[model],
|
|
29
|
+
}, null, 2);
|
|
30
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Statements } from "../database.js";
|
|
3
|
+
export declare const SmartContextSchema: z.ZodObject<{
|
|
4
|
+
query: z.ZodString;
|
|
5
|
+
task_type: z.ZodOptional<z.ZodEnum<["simple", "moderate", "complex"]>>;
|
|
6
|
+
dirs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
query: string;
|
|
9
|
+
dirs?: string[] | undefined;
|
|
10
|
+
task_type?: "simple" | "moderate" | "complex" | undefined;
|
|
11
|
+
}, {
|
|
12
|
+
query: string;
|
|
13
|
+
dirs?: string[] | undefined;
|
|
14
|
+
task_type?: "simple" | "moderate" | "complex" | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function handleSmartContext(stmts: Statements, args: z.infer<typeof SmartContextSchema>): Promise<string>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { assembleContext } from "../retrieval/context.js";
|
|
3
|
+
import { recall } from "./recall.js";
|
|
4
|
+
import { loadConfig } from "../config.js";
|
|
5
|
+
import { createExperience } from "../memory/experience.js";
|
|
6
|
+
export const SmartContextSchema = z.object({
|
|
7
|
+
query: z.string().min(1).describe("What you are working on — used for both code retrieval and knowledge graph search"),
|
|
8
|
+
task_type: z.enum(["simple", "moderate", "complex"]).optional().describe("Token budget: simple=2000, moderate=6000 (default), complex=12000"),
|
|
9
|
+
dirs: z.array(z.string()).optional().describe("Whitelist: only return files from these directories"),
|
|
10
|
+
});
|
|
11
|
+
const TASK_BUDGETS = {
|
|
12
|
+
simple: 2000,
|
|
13
|
+
moderate: 6000,
|
|
14
|
+
complex: 12000,
|
|
15
|
+
};
|
|
16
|
+
export async function handleSmartContext(stmts, args) {
|
|
17
|
+
const cfg = loadConfig();
|
|
18
|
+
const maxTokens = TASK_BUDGETS[args.task_type ?? "moderate"] ?? 6000;
|
|
19
|
+
// 1. Knowledge graph entities (synchronous)
|
|
20
|
+
const recallResult = recall(stmts, { query: args.query });
|
|
21
|
+
// 2. Code context with adaptive budget (async)
|
|
22
|
+
const contextResult = await assembleContext(args.query, stmts, cfg, {
|
|
23
|
+
maxTokens,
|
|
24
|
+
dirs: args.dirs,
|
|
25
|
+
});
|
|
26
|
+
// 3. Log experience so reward()/penalize() work after this call
|
|
27
|
+
const expId = createExperience(args.query, contextResult.files.map((f) => f.filepath), contextResult.strategy, stmts);
|
|
28
|
+
const budgetUsedPct = Math.round((contextResult.totalTokens / maxTokens) * 100);
|
|
29
|
+
const sections = [
|
|
30
|
+
"## Knowledge Context (entities)",
|
|
31
|
+
recallResult,
|
|
32
|
+
"",
|
|
33
|
+
"## Code Context (files)",
|
|
34
|
+
];
|
|
35
|
+
if (contextResult.files.length === 0) {
|
|
36
|
+
sections.push("No relevant files found. Run init_project() or sync_project() first.");
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
for (const f of contextResult.files) {
|
|
40
|
+
sections.push(`// ─── ${f.filepath} [${f.language}] ~${f.tokens}t (${f.reason}) ───`);
|
|
41
|
+
sections.push(f.content);
|
|
42
|
+
sections.push("");
|
|
43
|
+
}
|
|
44
|
+
if (contextResult.truncated) {
|
|
45
|
+
sections.push(`// ⚠️ Truncated — ${contextResult.skippedFiles} files skipped. Use task_type="complex" for more.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
sections.push("", "---");
|
|
49
|
+
sections.push(`Strategy: ${contextResult.strategy}`);
|
|
50
|
+
sections.push(`Files: ${contextResult.files.length} files, ${contextResult.totalTokens} tokens`);
|
|
51
|
+
sections.push(`Budget used: ${budgetUsedPct}%`);
|
|
52
|
+
sections.push(`Experience #${expId} logged. Call reward() if helpful, penalize() if not.`);
|
|
53
|
+
return sections.join("\n");
|
|
54
|
+
}
|
package/build/tools/sync.js
CHANGED
|
@@ -6,6 +6,8 @@ import { indexProject } from "../indexer/project.js";
|
|
|
6
6
|
import { computeDiff } from "../retrieval/context.js";
|
|
7
7
|
import { decompress } from "../store/content.js";
|
|
8
8
|
import { implicitRewardFromSync } from "../memory/experience.js";
|
|
9
|
+
import { indexFileInQdrant } from "../retrieval/qdrant.js";
|
|
10
|
+
import { loadConfig, getQdrantConfig } from "../config.js";
|
|
9
11
|
const SUPPORTED_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".vue", ".py", ".go", ".rs"]);
|
|
10
12
|
// ---------------------------------------------------------------------------
|
|
11
13
|
// sync_file
|
|
@@ -51,6 +53,12 @@ export function handleSyncFile(stmts, args) {
|
|
|
51
53
|
const implicitRewarded = implicitRewardFromSync(filepath, stmts);
|
|
52
54
|
if (implicitRewarded)
|
|
53
55
|
lines.push(` 🎯 Implicit reward +0.3 (file was in recent context)`);
|
|
56
|
+
// Qdrant vector indexing — fire-and-forget (Qdrant is optional, silent on failure)
|
|
57
|
+
const cfg = loadConfig();
|
|
58
|
+
const qdrantCfg = getQdrantConfig(cfg);
|
|
59
|
+
if (qdrantCfg) {
|
|
60
|
+
void indexFileInQdrant(filepath, source, qdrantCfg, cfg.semanticCompression).catch(() => { });
|
|
61
|
+
}
|
|
54
62
|
return lines.join("\n");
|
|
55
63
|
}
|
|
56
64
|
// ---------------------------------------------------------------------------
|