@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
|
@@ -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";
|
|
@@ -29,16 +30,37 @@ export const InitProjectSchema = z.object({
|
|
|
29
30
|
projectName: z.string().optional(),
|
|
30
31
|
});
|
|
31
32
|
const LUCID_MARKER = "Lucid: call sync_file";
|
|
33
|
+
const LUCID_UPDATE_MARKER = "lucid-update-check";
|
|
32
34
|
const LUCID_HOOK = {
|
|
33
35
|
matcher: "Write|Edit|NotebookEdit",
|
|
34
36
|
hooks: [
|
|
35
37
|
{
|
|
36
38
|
type: "command",
|
|
37
|
-
command: `echo '🔄 ${LUCID_MARKER}(path)
|
|
39
|
+
command: `lucid-sync 2>/dev/null || echo '🔄 ${LUCID_MARKER}(path) — install lucid globally: npm i -g @a13xu/lucid'`,
|
|
38
40
|
},
|
|
39
41
|
],
|
|
40
42
|
};
|
|
41
|
-
|
|
43
|
+
// SessionStart hook: checks npm registry and notifies if update is available.
|
|
44
|
+
// Uses only Node.js built-in https module — no external dependencies required.
|
|
45
|
+
const LUCID_UPDATE_HOOK = {
|
|
46
|
+
hooks: [
|
|
47
|
+
{
|
|
48
|
+
type: "command",
|
|
49
|
+
command: `node -e "const h=require('https');` +
|
|
50
|
+
`h.get('https://registry.npmjs.org/@a13xu/lucid/latest',` +
|
|
51
|
+
`function(r){var d='';r.on('data',function(c){d+=c});` +
|
|
52
|
+
`r.on('end',function(){` +
|
|
53
|
+
`try{var v=JSON.parse(d).version;` +
|
|
54
|
+
`var s=require('child_process').execSync(` +
|
|
55
|
+
`'npm list -g @a13xu/lucid --depth=0 2>/dev/null',{encoding:'utf8'});` +
|
|
56
|
+
`var m=s.match(/lucid@([\\d.]+)/);` +
|
|
57
|
+
`if(m&&m[1]&&v!==m[1])` +
|
|
58
|
+
`console.log('[Lucid] Update available: v'+m[1]+' → v'+v+'. Call update_lucid().')}` +
|
|
59
|
+
`catch(e){}})}).on('error',function(){})" 2>/dev/null || true`,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
function installHooks(dir) {
|
|
42
64
|
const claudeDir = join(dir, ".claude");
|
|
43
65
|
const settingsPath = join(claudeDir, "settings.json");
|
|
44
66
|
let settings = {};
|
|
@@ -51,20 +73,34 @@ function installHook(dir) {
|
|
|
51
73
|
}
|
|
52
74
|
}
|
|
53
75
|
const hooks = (settings["hooks"] ?? {});
|
|
76
|
+
let changed = false;
|
|
77
|
+
// ── PostToolUse: sync_file reminder ──────────────────────────────────────
|
|
54
78
|
const postToolUse = hooks["PostToolUse"] ?? [];
|
|
55
|
-
|
|
56
|
-
const alreadyInstalled = postToolUse.some((h) => {
|
|
79
|
+
const syncAlreadyInstalled = postToolUse.some((h) => {
|
|
57
80
|
const cmd = h.command ?? h.hooks?.[0]?.command ?? "";
|
|
58
81
|
return cmd.includes(LUCID_MARKER);
|
|
59
82
|
});
|
|
60
|
-
if (
|
|
83
|
+
if (!syncAlreadyInstalled) {
|
|
84
|
+
hooks["PostToolUse"] = [...postToolUse, LUCID_HOOK];
|
|
85
|
+
changed = true;
|
|
86
|
+
}
|
|
87
|
+
// ── SessionStart: version check ───────────────────────────────────────────
|
|
88
|
+
const sessionStart = hooks["SessionStart"] ?? [];
|
|
89
|
+
const updateAlreadyInstalled = sessionStart.some((h) => {
|
|
90
|
+
const cmd = h.command ?? h.hooks?.[0]?.command ?? "";
|
|
91
|
+
return cmd.includes(LUCID_UPDATE_MARKER);
|
|
92
|
+
});
|
|
93
|
+
if (!updateAlreadyInstalled) {
|
|
94
|
+
hooks["SessionStart"] = [...sessionStart, LUCID_UPDATE_HOOK];
|
|
95
|
+
changed = true;
|
|
96
|
+
}
|
|
97
|
+
if (!changed) {
|
|
61
98
|
return { installed: false, reason: "already installed" };
|
|
62
99
|
}
|
|
63
|
-
hooks["PostToolUse"] = [...postToolUse, LUCID_HOOK];
|
|
64
100
|
settings["hooks"] = hooks;
|
|
65
101
|
mkdirSync(claudeDir, { recursive: true });
|
|
66
102
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
67
|
-
return { installed: true, reason: "
|
|
103
|
+
return { installed: true, reason: "hooks added to .claude/settings.json" };
|
|
68
104
|
}
|
|
69
105
|
// ---------------------------------------------------------------------------
|
|
70
106
|
// Adaugă instrucțiune în CLAUDE.md
|
|
@@ -111,13 +147,14 @@ export async function handleInitProject(stmts, input) {
|
|
|
111
147
|
}
|
|
112
148
|
// ── Hook PostToolUse ──────────────────────────────────────────────────────
|
|
113
149
|
lines.push(``);
|
|
114
|
-
const hookResult =
|
|
150
|
+
const hookResult = installHooks(dir);
|
|
115
151
|
if (hookResult.installed) {
|
|
116
|
-
lines.push(`🔗 Claude Code
|
|
117
|
-
lines.push(`
|
|
152
|
+
lines.push(`🔗 Claude Code hooks installed (.claude/settings.json)`);
|
|
153
|
+
lines.push(` PostToolUse: reminder to call sync_file() after every Write/Edit`);
|
|
154
|
+
lines.push(` SessionStart: auto-check for Lucid updates on session start`);
|
|
118
155
|
}
|
|
119
156
|
else {
|
|
120
|
-
lines.push(`🔗
|
|
157
|
+
lines.push(`🔗 Hooks: ${hookResult.reason}`);
|
|
121
158
|
}
|
|
122
159
|
// ── Skills ────────────────────────────────────────────────────────────────
|
|
123
160
|
const skillsResult = installSkills(dir);
|
|
@@ -131,6 +168,17 @@ export async function handleInitProject(stmts, input) {
|
|
|
131
168
|
else if (skillsResult.skipped.length > 0) {
|
|
132
169
|
lines.push(`📚 Skills: already installed (${skillsResult.skipped.length} skill(s))`);
|
|
133
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
|
+
}
|
|
134
182
|
// ── CLAUDE.md injection ───────────────────────────────────────────────────
|
|
135
183
|
const injected = injectClaudeMdInstruction(dir);
|
|
136
184
|
if (injected) {
|
|
@@ -241,6 +289,9 @@ function installSkills(projectDir) {
|
|
|
241
289
|
}
|
|
242
290
|
return result;
|
|
243
291
|
}
|
|
292
|
+
function installGlobalSkills() {
|
|
293
|
+
return installSkills(homedir());
|
|
294
|
+
}
|
|
244
295
|
function buildChannelSummary(cfg) {
|
|
245
296
|
const channels = [];
|
|
246
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
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare function getCurrentVersion(): string;
|
|
3
|
+
export declare function checkForUpdatesOnStartup(): Promise<void>;
|
|
4
|
+
export declare const UpdateLucidSchema: z.ZodObject<{
|
|
5
|
+
force: z.ZodOptional<z.ZodBoolean>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
force?: boolean | undefined;
|
|
8
|
+
}, {
|
|
9
|
+
force?: boolean | undefined;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function handleUpdateLucid(args: z.infer<typeof UpdateLucidSchema>): Promise<string>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { readFileSync, existsSync } from "fs";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Package root resolution
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// build/tools/updater.js → ../../ = package root
|
|
10
|
+
const PACKAGE_ROOT = join(fileURLToPath(new URL(".", import.meta.url)), "../..");
|
|
11
|
+
export function getCurrentVersion() {
|
|
12
|
+
try {
|
|
13
|
+
const pkg = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8"));
|
|
14
|
+
return pkg.version ?? "unknown";
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return "unknown";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// npm registry check
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
async function fetchLatestVersion() {
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch("https://registry.npmjs.org/@a13xu/lucid/latest", { signal: AbortSignal.timeout(5000) });
|
|
26
|
+
if (!res.ok)
|
|
27
|
+
return null;
|
|
28
|
+
const data = (await res.json());
|
|
29
|
+
return data.version ?? null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function compareVersions(a, b) {
|
|
36
|
+
const pa = a.split(".").map(Number);
|
|
37
|
+
const pb = b.split(".").map(Number);
|
|
38
|
+
for (let i = 0; i < 3; i++) {
|
|
39
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
40
|
+
if (diff !== 0)
|
|
41
|
+
return diff;
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Install method detection
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
function detectInstallMethod() {
|
|
49
|
+
// If the package root is inside node_modules, it's npm-installed
|
|
50
|
+
if (PACKAGE_ROOT.includes("node_modules")) {
|
|
51
|
+
return "global-npm";
|
|
52
|
+
}
|
|
53
|
+
// Check if there's a .git folder — local source checkout
|
|
54
|
+
if (existsSync(join(PACKAGE_ROOT, ".git"))) {
|
|
55
|
+
return "local-source";
|
|
56
|
+
}
|
|
57
|
+
return "global-npm";
|
|
58
|
+
}
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Startup check (non-blocking — call from index.ts without await)
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
export async function checkForUpdatesOnStartup() {
|
|
63
|
+
const current = getCurrentVersion();
|
|
64
|
+
const latest = await fetchLatestVersion();
|
|
65
|
+
if (latest && compareVersions(latest, current) > 0) {
|
|
66
|
+
console.error(`[lucid] ⬆️ Update available: v${current} → v${latest}. ` +
|
|
67
|
+
`Call update_lucid() to update, or: npm install -g @a13xu/lucid@latest`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Schema & handler
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
export const UpdateLucidSchema = z.object({
|
|
74
|
+
force: z
|
|
75
|
+
.boolean()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe("Force reinstall even if already on latest version"),
|
|
78
|
+
});
|
|
79
|
+
// Example call:
|
|
80
|
+
// handleUpdateLucid({ force: false })
|
|
81
|
+
export async function handleUpdateLucid(args) {
|
|
82
|
+
const current = getCurrentVersion();
|
|
83
|
+
const lines = [`🔍 Checking for updates... (current: v${current})`];
|
|
84
|
+
const latest = await fetchLatestVersion();
|
|
85
|
+
if (!latest) {
|
|
86
|
+
return lines.concat("❌ Could not reach npm registry. Check your internet connection.").join("\n");
|
|
87
|
+
}
|
|
88
|
+
lines.push(`📦 Latest on npm: v${latest}`);
|
|
89
|
+
const upToDate = compareVersions(latest, current) <= 0;
|
|
90
|
+
if (upToDate && !args.force) {
|
|
91
|
+
lines.push(`✅ Lucid is up to date.`);
|
|
92
|
+
return lines.join("\n");
|
|
93
|
+
}
|
|
94
|
+
if (upToDate && args.force) {
|
|
95
|
+
lines.push(`⚠️ Already on v${latest}, reinstalling (force=true)...`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
lines.push(`🔄 Updating v${current} → v${latest}...`);
|
|
99
|
+
}
|
|
100
|
+
const method = detectInstallMethod();
|
|
101
|
+
if (method === "local-source") {
|
|
102
|
+
lines.push(``);
|
|
103
|
+
lines.push(`📁 Local source installation detected (${PACKAGE_ROOT}).`);
|
|
104
|
+
lines.push(`Run these commands to update:`);
|
|
105
|
+
lines.push(` cd "${PACKAGE_ROOT}"`);
|
|
106
|
+
lines.push(` git pull`);
|
|
107
|
+
lines.push(` npm run build`);
|
|
108
|
+
lines.push(`Then restart Claude Code.`);
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
// Global npm install
|
|
112
|
+
try {
|
|
113
|
+
lines.push(`Running: npm install -g @a13xu/lucid@${latest}`);
|
|
114
|
+
execSync(`npm install -g @a13xu/lucid@${latest}`, {
|
|
115
|
+
timeout: 120_000,
|
|
116
|
+
stdio: "pipe",
|
|
117
|
+
});
|
|
118
|
+
lines.push(`✅ Updated to v${latest}`);
|
|
119
|
+
lines.push(``);
|
|
120
|
+
lines.push(`⚠️ Restart Claude Code to load the new version:`);
|
|
121
|
+
lines.push(` • VS Code: Cmd/Ctrl+Shift+P → "Restart Claude Code"`);
|
|
122
|
+
lines.push(` • CLI: exit and re-run \`claude\``);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
126
|
+
lines.push(`❌ Auto-update failed: ${msg}`);
|
|
127
|
+
lines.push(` Run manually: npm install -g @a13xu/lucid@${latest}`);
|
|
128
|
+
if (process.platform !== "win32") {
|
|
129
|
+
lines.push(` Or with sudo: sudo npm install -g @a13xu/lucid@${latest}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return lines.join("\n");
|
|
133
|
+
}
|
package/package.json
CHANGED
|
@@ -1,59 +1,64 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@a13xu/lucid",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Token-efficient memory, code indexing, and validation for Claude Code agents — SQLite + FTS5, TF-IDF + Qdrant retrieval, AST skeleton pruning, diff-aware context, Logic Guardian drift detection",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"lucid": "./build/index.js"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"build/**/*.
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"claude
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@a13xu/lucid",
|
|
3
|
+
"version": "1.16.0",
|
|
4
|
+
"description": "Token-efficient memory, code indexing, and validation for Claude Code agents — SQLite + FTS5, TF-IDF + Qdrant retrieval, AST skeleton pruning, diff-aware context, Logic Guardian drift detection",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lucid": "./build/index.js",
|
|
8
|
+
"lucid-sync": "./build/lucid-sync.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"build/**/*.js",
|
|
12
|
+
"build/**/*.d.ts",
|
|
13
|
+
"skills/**/*.md",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"claude",
|
|
23
|
+
"claude-code",
|
|
24
|
+
"memory",
|
|
25
|
+
"sqlite",
|
|
26
|
+
"knowledge-graph",
|
|
27
|
+
"code-indexing",
|
|
28
|
+
"logic-guardian",
|
|
29
|
+
"drift-detection",
|
|
30
|
+
"tfidf",
|
|
31
|
+
"qdrant",
|
|
32
|
+
"token-optimization",
|
|
33
|
+
"context-pruning",
|
|
34
|
+
"ai",
|
|
35
|
+
"anthropic"
|
|
36
|
+
],
|
|
37
|
+
"author": "a13xu",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/a13xu/lucid.git"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/a13xu/lucid#readme",
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@huggingface/transformers": "^4.0.0",
|
|
52
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
53
|
+
"better-sqlite3": "^12.0.0",
|
|
54
|
+
"chokidar": "^4.0.3",
|
|
55
|
+
"express": "^5.2.1",
|
|
56
|
+
"zod": "^3.23.8"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
60
|
+
"@types/express": "^5.0.6",
|
|
61
|
+
"@types/node": "^22.0.0",
|
|
62
|
+
"typescript": "^5.4.0"
|
|
63
|
+
}
|
|
64
|
+
}
|