@a13xu/lucid 1.1.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +221 -99
  3. package/build/config.d.ts +37 -0
  4. package/build/config.js +45 -0
  5. package/build/database.d.ts +54 -0
  6. package/build/database.js +175 -62
  7. package/build/guardian/checklist.js +66 -66
  8. package/build/guardian/coding-analyzer.d.ts +11 -0
  9. package/build/guardian/coding-analyzer.js +393 -0
  10. package/build/guardian/coding-rules.d.ts +1 -0
  11. package/build/guardian/coding-rules.js +97 -0
  12. package/build/index.js +241 -2
  13. package/build/indexer/ast.d.ts +9 -0
  14. package/build/indexer/ast.js +158 -0
  15. package/build/indexer/file.d.ts +15 -0
  16. package/build/indexer/file.js +100 -0
  17. package/build/indexer/project.d.ts +8 -0
  18. package/build/indexer/project.js +320 -0
  19. package/build/memory/experience.d.ts +11 -0
  20. package/build/memory/experience.js +85 -0
  21. package/build/retrieval/context.d.ts +29 -0
  22. package/build/retrieval/context.js +219 -0
  23. package/build/retrieval/qdrant.d.ts +16 -0
  24. package/build/retrieval/qdrant.js +135 -0
  25. package/build/retrieval/tfidf.d.ts +14 -0
  26. package/build/retrieval/tfidf.js +64 -0
  27. package/build/security/alerts.d.ts +44 -0
  28. package/build/security/alerts.js +228 -0
  29. package/build/security/env.d.ts +24 -0
  30. package/build/security/env.js +85 -0
  31. package/build/security/guard.d.ts +35 -0
  32. package/build/security/guard.js +133 -0
  33. package/build/security/ratelimit.d.ts +34 -0
  34. package/build/security/ratelimit.js +105 -0
  35. package/build/security/smtp.d.ts +26 -0
  36. package/build/security/smtp.js +125 -0
  37. package/build/security/ssrf.d.ts +18 -0
  38. package/build/security/ssrf.js +109 -0
  39. package/build/security/waf.d.ts +33 -0
  40. package/build/security/waf.js +174 -0
  41. package/build/store/content.d.ts +3 -0
  42. package/build/store/content.js +11 -0
  43. package/build/tools/coding-guard.d.ts +24 -0
  44. package/build/tools/coding-guard.js +82 -0
  45. package/build/tools/context.d.ts +39 -0
  46. package/build/tools/context.js +105 -0
  47. package/build/tools/grep.d.ts +17 -0
  48. package/build/tools/grep.js +65 -0
  49. package/build/tools/init.d.ts +51 -0
  50. package/build/tools/init.js +212 -0
  51. package/build/tools/remember.d.ts +4 -4
  52. package/build/tools/reward.d.ts +29 -0
  53. package/build/tools/reward.js +154 -0
  54. package/build/tools/sync.d.ts +18 -0
  55. package/build/tools/sync.js +76 -0
  56. package/package.json +55 -48
@@ -0,0 +1,212 @@
1
+ import { z } from "zod";
2
+ import { resolve, join } from "path";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
+ import { indexProject } from "../indexer/project.js";
5
+ import { saveAdminConfig, loadAdminConfig, isAdminConfigured, sendTestAlert, } from "../security/alerts.js";
6
+ export const InitProjectSchema = z.object({
7
+ directory: z.string().optional(),
8
+ // ── Admin alert configuration (asked once at project init) ──────────────
9
+ /** Display name of the security admin */
10
+ adminName: z.string().optional(),
11
+ /** Email address to send security alerts to */
12
+ adminEmail: z.string().email().optional(),
13
+ /** SMTP server hostname (e.g. smtp.gmail.com) */
14
+ smtpHost: z.string().optional(),
15
+ /** SMTP port: 587 (STARTTLS, default) or 465 (direct TLS) */
16
+ smtpPort: z.number().int().min(1).max(65535).optional(),
17
+ /** SMTP login username (often same as adminEmail) */
18
+ smtpUser: z.string().optional(),
19
+ /** "From" display name + address (e.g. "Lucid Security <alerts@co.com>") */
20
+ smtpFrom: z.string().optional(),
21
+ /** Generic HTTP webhook URL (receives JSON POST, HMAC-signed if LUCID_WEBHOOK_SECRET is set) */
22
+ webhookUrl: z.string().url().optional(),
23
+ /** Slack incoming webhook URL */
24
+ slackWebhookUrl: z.string().url().optional(),
25
+ /** Which severities trigger an alert: default ["critical","high"] */
26
+ alertOn: z.array(z.enum(["critical", "high", "medium", "low"])).optional(),
27
+ /** Human-readable project name shown in alerts */
28
+ projectName: z.string().optional(),
29
+ });
30
+ const LUCID_MARKER = "Lucid: call sync_file";
31
+ const LUCID_HOOK = {
32
+ matcher: { tools: ["Write", "Edit", "NotebookEdit"] },
33
+ hooks: [
34
+ {
35
+ type: "command",
36
+ command: `echo '🔄 ${LUCID_MARKER}(path) to keep knowledge graph up to date'`,
37
+ },
38
+ ],
39
+ };
40
+ function installHook(dir) {
41
+ const claudeDir = join(dir, ".claude");
42
+ const settingsPath = join(claudeDir, "settings.json");
43
+ let settings = {};
44
+ if (existsSync(settingsPath)) {
45
+ try {
46
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
47
+ }
48
+ catch {
49
+ return { installed: false, reason: "Could not parse .claude/settings.json" };
50
+ }
51
+ }
52
+ const hooks = (settings["hooks"] ?? {});
53
+ const postToolUse = hooks["PostToolUse"] ?? [];
54
+ // Detectează atât formatul vechi cât și cel nou
55
+ const alreadyInstalled = postToolUse.some((h) => {
56
+ const cmd = h.command ?? h.hooks?.[0]?.command ?? "";
57
+ return cmd.includes(LUCID_MARKER);
58
+ });
59
+ if (alreadyInstalled) {
60
+ return { installed: false, reason: "already installed" };
61
+ }
62
+ hooks["PostToolUse"] = [...postToolUse, LUCID_HOOK];
63
+ settings["hooks"] = hooks;
64
+ mkdirSync(claudeDir, { recursive: true });
65
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
66
+ return { installed: true, reason: "hook added to .claude/settings.json" };
67
+ }
68
+ // ---------------------------------------------------------------------------
69
+ // Adaugă instrucțiune în CLAUDE.md
70
+ // ---------------------------------------------------------------------------
71
+ const LUCID_SYNC_INSTRUCTION = `
72
+ <!-- LUCID_SYNC -->
73
+ ## 🔄 Lucid — Auto-sync after file changes
74
+
75
+ After EVERY Write, Edit, or NotebookEdit tool call, you MUST immediately call:
76
+ \`\`\`
77
+ sync_file(path="<path of the file you just wrote/edited>")
78
+ \`\`\`
79
+
80
+ This keeps the Lucid knowledge graph up to date with the latest code.
81
+ If multiple files changed (refactor, git pull), call sync_project() instead.
82
+ <!-- /LUCID_SYNC -->
83
+ `;
84
+ function injectClaudeMdInstruction(dir) {
85
+ const claudeMdPath = join(dir, "CLAUDE.md");
86
+ if (!existsSync(claudeMdPath))
87
+ return false;
88
+ const content = readFileSync(claudeMdPath, "utf-8");
89
+ if (content.includes("LUCID_SYNC"))
90
+ return false; // already injected
91
+ writeFileSync(claudeMdPath, content.trimEnd() + "\n" + LUCID_SYNC_INSTRUCTION, "utf-8");
92
+ return true;
93
+ }
94
+ // ---------------------------------------------------------------------------
95
+ // Main handler
96
+ // ---------------------------------------------------------------------------
97
+ export async function handleInitProject(stmts, input) {
98
+ const dir = resolve(input.directory ?? process.cwd());
99
+ const results = indexProject(dir, stmts);
100
+ const lines = [`✅ Project indexed: ${dir}`, ``];
101
+ if (results.length === 0) {
102
+ lines.push("No indexable files found.");
103
+ lines.push("Expected: CLAUDE.md, package.json, README.md, src/");
104
+ }
105
+ else {
106
+ lines.push(`Indexed ${results.length} source(s):`);
107
+ for (const r of results) {
108
+ lines.push(` • [${r.type}] "${r.entity}" — ${r.observations} observation(s) from ${r.source}`);
109
+ }
110
+ }
111
+ // ── Hook PostToolUse ──────────────────────────────────────────────────────
112
+ lines.push(``);
113
+ const hookResult = installHook(dir);
114
+ if (hookResult.installed) {
115
+ lines.push(`🔗 Claude Code hook installed (.claude/settings.json)`);
116
+ lines.push(` After every Write/Edit, you will see a reminder to call sync_file().`);
117
+ }
118
+ else {
119
+ lines.push(`🔗 Hook: ${hookResult.reason}`);
120
+ }
121
+ // ── CLAUDE.md injection ───────────────────────────────────────────────────
122
+ const injected = injectClaudeMdInstruction(dir);
123
+ if (injected) {
124
+ lines.push(`📋 CLAUDE.md updated with sync_file() instruction`);
125
+ }
126
+ // ── Security admin configuration ──────────────────────────────────────────
127
+ lines.push(``);
128
+ lines.push(`🔒 Security Alerts`);
129
+ // Save any admin params provided in this call
130
+ const adminFields = {
131
+ adminName: input.adminName,
132
+ adminEmail: input.adminEmail,
133
+ smtpHost: input.smtpHost,
134
+ smtpPort: input.smtpPort,
135
+ smtpUser: input.smtpUser,
136
+ smtpFrom: input.smtpFrom,
137
+ webhookUrl: input.webhookUrl,
138
+ slackWebhookUrl: input.slackWebhookUrl,
139
+ alertOn: input.alertOn,
140
+ projectName: input.projectName ?? results.find((r) => r.type === "project")?.entity,
141
+ };
142
+ const hasNewAdmin = Object.values(adminFields).some((v) => v !== undefined);
143
+ if (hasNewAdmin) {
144
+ // Strip undefined values before saving
145
+ const clean = Object.fromEntries(Object.entries(adminFields).filter(([, v]) => v !== undefined));
146
+ saveAdminConfig(dir, clean);
147
+ lines.push(` Saved admin config → .claude/lucid-admin.json`);
148
+ // Test alert channels
149
+ const testResults = await sendTestAlert(dir);
150
+ lines.push(` Test alert results:`);
151
+ for (const r of testResults)
152
+ lines.push(` ${r}`);
153
+ }
154
+ else {
155
+ // Check existing config
156
+ const existing = loadAdminConfig(dir);
157
+ if (isAdminConfigured()) {
158
+ lines.push(` Admin: ${existing.adminName ?? existing.adminEmail ?? "configured"}`);
159
+ lines.push(` Channels: ${buildChannelSummary(existing)}`);
160
+ lines.push(` Alerting on: ${(existing.alertOn ?? ["critical", "high"]).join(", ")}`);
161
+ }
162
+ else {
163
+ // Not configured — prompt user
164
+ lines.push(``);
165
+ lines.push(` ⚠️ No security admin configured. Security alerts will only appear in logs.`);
166
+ lines.push(``);
167
+ lines.push(` To enable alerts, re-run init_project() with admin parameters:`);
168
+ lines.push(``);
169
+ lines.push(` Minimal (webhook only):`);
170
+ lines.push(` init_project(`);
171
+ lines.push(` adminName="Your Name",`);
172
+ lines.push(` adminEmail="admin@yourcompany.com",`);
173
+ lines.push(` webhookUrl="https://hooks.yourservice.com/...",`);
174
+ lines.push(` )`);
175
+ lines.push(``);
176
+ lines.push(` With Slack:`);
177
+ lines.push(` init_project(`);
178
+ lines.push(` adminName="Your Name",`);
179
+ lines.push(` adminEmail="admin@yourcompany.com",`);
180
+ lines.push(` slackWebhookUrl="https://hooks.slack.com/services/...",`);
181
+ lines.push(` )`);
182
+ lines.push(``);
183
+ lines.push(` With Email (SMTP):`);
184
+ lines.push(` init_project(`);
185
+ lines.push(` adminName="Your Name",`);
186
+ lines.push(` adminEmail="admin@yourcompany.com",`);
187
+ lines.push(` smtpHost="smtp.gmail.com",`);
188
+ lines.push(` smtpPort=587,`);
189
+ lines.push(` smtpUser="alerts@yourcompany.com",`);
190
+ lines.push(` )`);
191
+ lines.push(` # Then set in your environment:`);
192
+ lines.push(` export LUCID_SMTP_PASS="your-app-password"`);
193
+ lines.push(``);
194
+ lines.push(` SMTP password must be in LUCID_SMTP_PASS env var (never as a parameter).`);
195
+ lines.push(` Webhook HMAC signing: set LUCID_WEBHOOK_SECRET env var.`);
196
+ }
197
+ }
198
+ lines.push(``);
199
+ lines.push(`From now on, call sync_file(path) after every file you write or edit.`);
200
+ lines.push(`Use recall() to query accumulated project knowledge.`);
201
+ return lines.join("\n");
202
+ }
203
+ function buildChannelSummary(cfg) {
204
+ const channels = [];
205
+ if (cfg.adminEmail && cfg.smtpHost)
206
+ channels.push(`email(${cfg.adminEmail})`);
207
+ if (cfg.webhookUrl)
208
+ channels.push(`webhook`);
209
+ if (cfg.slackWebhookUrl)
210
+ channels.push(`slack`);
211
+ return channels.length > 0 ? channels.join(", ") : "none";
212
+ }
@@ -5,13 +5,13 @@ export declare const RememberSchema: z.ZodObject<{
5
5
  entityType: z.ZodEnum<["person", "project", "decision", "pattern", "tool", "config", "bug", "convention"]>;
6
6
  observation: z.ZodString;
7
7
  }, "strip", z.ZodTypeAny, {
8
- entity: string;
9
- entityType: "person" | "project" | "decision" | "pattern" | "tool" | "config" | "bug" | "convention";
10
8
  observation: string;
11
- }, {
12
9
  entity: string;
13
- entityType: "person" | "project" | "decision" | "pattern" | "tool" | "config" | "bug" | "convention";
10
+ entityType: "tool" | "pattern" | "person" | "project" | "decision" | "config" | "bug" | "convention";
11
+ }, {
14
12
  observation: string;
13
+ entity: string;
14
+ entityType: "tool" | "pattern" | "person" | "project" | "decision" | "config" | "bug" | "convention";
15
15
  }>;
16
16
  export type RememberInput = z.infer<typeof RememberSchema>;
17
17
  export declare function remember(stmts: Statements, input: RememberInput): string;
@@ -0,0 +1,29 @@
1
+ import { z } from "zod";
2
+ import type { Statements } from "../database.js";
3
+ export declare const RewardSchema: z.ZodObject<{
4
+ note: z.ZodOptional<z.ZodString>;
5
+ }, "strip", z.ZodTypeAny, {
6
+ note?: string | undefined;
7
+ }, {
8
+ note?: string | undefined;
9
+ }>;
10
+ export declare const PenalizeSchema: z.ZodObject<{
11
+ note: z.ZodOptional<z.ZodString>;
12
+ }, "strip", z.ZodTypeAny, {
13
+ note?: string | undefined;
14
+ }, {
15
+ note?: string | undefined;
16
+ }>;
17
+ export declare const ShowRewardsSchema: z.ZodObject<{
18
+ query: z.ZodOptional<z.ZodString>;
19
+ topK: z.ZodOptional<z.ZodNumber>;
20
+ }, "strip", z.ZodTypeAny, {
21
+ query?: string | undefined;
22
+ topK?: number | undefined;
23
+ }, {
24
+ query?: string | undefined;
25
+ topK?: number | undefined;
26
+ }>;
27
+ export declare function handleReward(stmts: Statements, args: z.infer<typeof RewardSchema>): string;
28
+ export declare function handlePenalize(stmts: Statements, args: z.infer<typeof PenalizeSchema>): string;
29
+ export declare function handleShowRewards(stmts: Statements, args: z.infer<typeof ShowRewardsSchema>): string;
@@ -0,0 +1,154 @@
1
+ // Reward / penalize / show_rewards tool handlers
2
+ import { z } from "zod";
3
+ import { getLastExperienceId, rewardExperience, decayedReward, } from "../memory/experience.js";
4
+ // ---------------------------------------------------------------------------
5
+ // Schemas
6
+ // ---------------------------------------------------------------------------
7
+ export const RewardSchema = z.object({
8
+ note: z.string().optional().describe("Optional note about what worked"),
9
+ });
10
+ export const PenalizeSchema = z.object({
11
+ note: z.string().optional().describe("Optional note about what was missing or wrong"),
12
+ });
13
+ export const ShowRewardsSchema = z.object({
14
+ query: z.string().optional().describe("Filter experiences by query text"),
15
+ topK: z.number().int().min(1).max(50).optional().describe("Number of top results to show (default 10)"),
16
+ });
17
+ // ---------------------------------------------------------------------------
18
+ // Helpers
19
+ // ---------------------------------------------------------------------------
20
+ function formatAge(unixSec) {
21
+ const diffSec = Math.floor(Date.now() / 1000) - unixSec;
22
+ if (diffSec < 3600)
23
+ return `${Math.round(diffSec / 60)}m ago`;
24
+ if (diffSec < 86400)
25
+ return `${Math.round(diffSec / 3600)}h ago`;
26
+ return `${Math.round(diffSec / 86400)} days ago`;
27
+ }
28
+ function parseFps(contextFps) {
29
+ try {
30
+ return JSON.parse(contextFps);
31
+ }
32
+ catch {
33
+ return [];
34
+ }
35
+ }
36
+ // ---------------------------------------------------------------------------
37
+ // reward — explicit +1 on the last experience
38
+ // ---------------------------------------------------------------------------
39
+ export function handleReward(stmts, args) {
40
+ const lastId = getLastExperienceId();
41
+ if (lastId === null) {
42
+ return "❌ No recent get_context() call to reward. Call get_context() first.";
43
+ }
44
+ const result = rewardExperience(lastId, 1.0, args.note ?? null, stmts);
45
+ if (!result) {
46
+ return `❌ Experience #${lastId} not found in database.`;
47
+ }
48
+ const lines = [
49
+ `✅ Experience #${lastId} rewarded (+1)`,
50
+ ` Query: "${result.query}"`,
51
+ ];
52
+ if (args.note)
53
+ lines.push(` Note: ${args.note}`);
54
+ if (result.fps.length > 0) {
55
+ lines.push(` Rewarded files:`);
56
+ for (const fp of result.fps.slice(0, 5)) {
57
+ lines.push(` +1.0 ${fp}`);
58
+ }
59
+ if (result.fps.length > 5)
60
+ lines.push(` … and ${result.fps.length - 5} more`);
61
+ }
62
+ // Show accumulated reward for these files
63
+ const allRewards = stmts.getFileRewards.all();
64
+ const relevant = allRewards.filter((fr) => result.fps.includes(fr.filepath));
65
+ if (relevant.length > 0) {
66
+ const total = relevant.reduce((sum, fr) => sum + fr.total_reward, 0);
67
+ lines.push(` Total reward score for these files: ${total.toFixed(1)} (accumulated)`);
68
+ }
69
+ return lines.join("\n");
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // penalize — explicit -1 on the last experience
73
+ // ---------------------------------------------------------------------------
74
+ export function handlePenalize(stmts, args) {
75
+ const lastId = getLastExperienceId();
76
+ if (lastId === null) {
77
+ return "❌ No recent get_context() call to penalize. Call get_context() first.";
78
+ }
79
+ const result = rewardExperience(lastId, -1.0, args.note ?? null, stmts);
80
+ if (!result) {
81
+ return `❌ Experience #${lastId} not found in database.`;
82
+ }
83
+ const lines = [
84
+ `❌ Experience #${lastId} penalized (-1)`,
85
+ ` Query: "${result.query}"`,
86
+ ];
87
+ if (args.note)
88
+ lines.push(` Note: ${args.note}`);
89
+ if (result.fps.length > 0) {
90
+ lines.push(` Penalized files:`);
91
+ for (const fp of result.fps.slice(0, 5)) {
92
+ lines.push(` -1.0 ${fp}`);
93
+ }
94
+ if (result.fps.length > 5)
95
+ lines.push(` … and ${result.fps.length - 5} more`);
96
+ }
97
+ return lines.join("\n");
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // show_rewards — display top experiences + most rewarded files
101
+ // ---------------------------------------------------------------------------
102
+ export function handleShowRewards(stmts, args) {
103
+ const topK = args.topK ?? 10;
104
+ const lines = [];
105
+ // Get experiences
106
+ let experiences;
107
+ if (args.query) {
108
+ try {
109
+ // FTS search: append * for prefix match
110
+ const ftsQuery = args.query.trim().split(/\s+/).map((t) => `${t}*`).join(" ");
111
+ experiences = stmts.searchExperiencesFTS.all(ftsQuery, topK);
112
+ }
113
+ catch {
114
+ // Fall back to top by reward if FTS query is malformed
115
+ experiences = stmts.getTopExperiences.all(topK);
116
+ }
117
+ }
118
+ else {
119
+ experiences = stmts.getTopExperiences.all(topK);
120
+ }
121
+ if (experiences.length === 0) {
122
+ lines.push("No rewarded experiences yet. Use get_context() then reward() to start building context memory.");
123
+ }
124
+ else {
125
+ lines.push("🏆 Top rewarded experiences (decayed):", "");
126
+ for (const exp of experiences) {
127
+ const decayed = decayedReward(exp.reward, exp.rewarded_at);
128
+ const ageStr = exp.rewarded_at ? formatAge(exp.rewarded_at) : "never rewarded";
129
+ const sign = exp.reward >= 0 ? "+" : "";
130
+ lines.push(`#${exp.id} ${sign}${decayed.toFixed(1)} "${exp.query}" (${ageStr})`);
131
+ const fps = parseFps(exp.context_fps);
132
+ if (fps.length > 0) {
133
+ const shown = fps.slice(0, 3).join(", ");
134
+ const extra = fps.length > 3 ? ` +${fps.length - 3} more` : "";
135
+ lines.push(` → ${shown}${extra}`);
136
+ }
137
+ if (exp.feedback)
138
+ lines.push(` note: ${exp.feedback}`);
139
+ }
140
+ }
141
+ // Most rewarded files (decayed)
142
+ lines.push("", "📁 Most rewarded files:");
143
+ const topFiles = stmts.getTopFileRewards.all(topK);
144
+ if (topFiles.length === 0) {
145
+ lines.push(" (none yet)");
146
+ }
147
+ else {
148
+ topFiles.forEach((fr, i) => {
149
+ const decayed = decayedReward(fr.total_reward, fr.last_rewarded);
150
+ lines.push(` ${i + 1}. ${fr.filepath} reward=${decayed.toFixed(1)} used=${fr.use_count}x`);
151
+ });
152
+ }
153
+ return lines.join("\n");
154
+ }
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ import type { Statements } from "../database.js";
3
+ export declare const SyncFileSchema: z.ZodObject<{
4
+ path: z.ZodString;
5
+ }, "strip", z.ZodTypeAny, {
6
+ path: string;
7
+ }, {
8
+ path: string;
9
+ }>;
10
+ export declare function handleSyncFile(stmts: Statements, args: z.infer<typeof SyncFileSchema>): string;
11
+ export declare const SyncProjectSchema: z.ZodObject<{
12
+ directory: z.ZodOptional<z.ZodString>;
13
+ }, "strip", z.ZodTypeAny, {
14
+ directory?: string | undefined;
15
+ }, {
16
+ directory?: string | undefined;
17
+ }>;
18
+ export declare function handleSyncProject(stmts: Statements, args: z.infer<typeof SyncProjectSchema>): string;
@@ -0,0 +1,76 @@
1
+ import { z } from "zod";
2
+ import { resolve, extname } from "path";
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { indexFile, upsertFileIndex } from "../indexer/file.js";
5
+ import { indexProject } from "../indexer/project.js";
6
+ import { computeDiff } from "../retrieval/context.js";
7
+ import { decompress } from "../store/content.js";
8
+ import { implicitRewardFromSync } from "../memory/experience.js";
9
+ const SUPPORTED_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
10
+ // ---------------------------------------------------------------------------
11
+ // sync_file
12
+ // ---------------------------------------------------------------------------
13
+ export const SyncFileSchema = z.object({
14
+ path: z.string().min(1),
15
+ });
16
+ export function handleSyncFile(stmts, args) {
17
+ const filepath = resolve(args.path);
18
+ if (!existsSync(filepath))
19
+ return `File not found: ${filepath}`;
20
+ if (!SUPPORTED_EXTS.has(extname(filepath).toLowerCase())) {
21
+ return `Unsupported file type: ${extname(filepath)}. Supported: ${[...SUPPORTED_EXTS].join(", ")}`;
22
+ }
23
+ const index = indexFile(filepath);
24
+ if (!index)
25
+ return `Could not read file: ${filepath}`;
26
+ const source = readFileSync(filepath, "utf-8");
27
+ // Capture previous content before upsert (for diff)
28
+ const prevRow = stmts.getFileByPath.get(filepath);
29
+ const prevSource = prevRow ? decompress(prevRow.content) : null;
30
+ const result = upsertFileIndex(index, source, stmts);
31
+ if (!result.stored) {
32
+ return `⏭️ Unchanged: ${filepath} (hash match — skipped)`;
33
+ }
34
+ // Store diff when file changed
35
+ if (prevRow && prevSource !== null) {
36
+ const diff = computeDiff(prevSource, source);
37
+ stmts.upsertDiff.run(filepath, prevRow.content_hash, diff);
38
+ }
39
+ const ratio = Math.round((1 - (result.savedBytes + Buffer.byteLength(source, "utf-8") - result.savedBytes) / Buffer.byteLength(source, "utf-8")) * 100);
40
+ const saved = Math.round(result.savedBytes / 1024 * 10) / 10;
41
+ const lines = [
42
+ `✅ Synced: ${filepath}`,
43
+ ` exports: ${index.exports.join(", ") || "none"}`,
44
+ ` compressed: saved ${saved}KB`,
45
+ ];
46
+ if (index.description)
47
+ lines.push(` description: ${index.description}`);
48
+ if (index.todos.length > 0)
49
+ lines.push(` TODOs: ${index.todos.length} open`);
50
+ // Implicit reward: if this file was in the last get_context result → +0.3
51
+ const implicitRewarded = implicitRewardFromSync(filepath, stmts);
52
+ if (implicitRewarded)
53
+ lines.push(` 🎯 Implicit reward +0.3 (file was in recent context)`);
54
+ return lines.join("\n");
55
+ }
56
+ // ---------------------------------------------------------------------------
57
+ // sync_project
58
+ // ---------------------------------------------------------------------------
59
+ export const SyncProjectSchema = z.object({
60
+ directory: z.string().optional(),
61
+ });
62
+ export function handleSyncProject(stmts, args) {
63
+ const dir = resolve(args.directory ?? process.cwd());
64
+ const results = indexProject(dir, stmts);
65
+ if (results.length === 0)
66
+ return `No changes indexed in: ${dir}`;
67
+ const stats = stmts.fileStorageStats.get();
68
+ const ratio = stats.total_original > 0
69
+ ? Math.round((1 - stats.total_compressed / stats.total_original) * 100)
70
+ : 0;
71
+ return [
72
+ `✅ Project re-synced: ${dir}`,
73
+ ` ${results.length} source(s) updated`,
74
+ ` Storage: ${Math.round(stats.total_compressed / 1024)}KB compressed / ${Math.round(stats.total_original / 1024)}KB original (${ratio}% saved)`,
75
+ ].join("\n");
76
+ }
package/package.json CHANGED
@@ -1,48 +1,55 @@
1
- {
2
- "name": "@a13xu/lucid",
3
- "version": "1.1.0",
4
- "description": "Persistent memory for Claude Code agents — SQLite + FTS5 knowledge graph via MCP",
5
- "type": "module",
6
- "bin": {
7
- "lucid": "./build/index.js"
8
- },
9
- "files": [
10
- "build/**/*.js",
11
- "build/**/*.d.ts",
12
- "README.md"
13
- ],
14
- "scripts": {
15
- "build": "tsc",
16
- "prepublishOnly": "npm run build"
17
- },
18
- "keywords": [
19
- "mcp",
20
- "claude",
21
- "claude-code",
22
- "memory",
23
- "sqlite",
24
- "knowledge-graph",
25
- "ai",
26
- "anthropic"
27
- ],
28
- "author": "a13xu",
29
- "license": "MIT",
30
- "repository": {
31
- "type": "git",
32
- "url": "https://github.com/a13xu/lucid.git"
33
- },
34
- "homepage": "https://github.com/a13xu/lucid#readme",
35
- "engines": {
36
- "node": ">=18"
37
- },
38
- "dependencies": {
39
- "@modelcontextprotocol/sdk": "^1.0.0",
40
- "better-sqlite3": "^11.0.0",
41
- "zod": "^3.23.8"
42
- },
43
- "devDependencies": {
44
- "@types/better-sqlite3": "^7.6.11",
45
- "@types/node": "^20.0.0",
46
- "typescript": "^5.4.0"
47
- }
48
- }
1
+ {
2
+ "name": "@a13xu/lucid",
3
+ "version": "1.9.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
+ },
9
+ "files": [
10
+ "build/**/*.js",
11
+ "build/**/*.d.ts",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "claude",
21
+ "claude-code",
22
+ "memory",
23
+ "sqlite",
24
+ "knowledge-graph",
25
+ "code-indexing",
26
+ "logic-guardian",
27
+ "drift-detection",
28
+ "tfidf",
29
+ "qdrant",
30
+ "token-optimization",
31
+ "context-pruning",
32
+ "ai",
33
+ "anthropic"
34
+ ],
35
+ "author": "a13xu",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/a13xu/lucid.git"
40
+ },
41
+ "homepage": "https://github.com/a13xu/lucid#readme",
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.0.0",
47
+ "better-sqlite3": "^11.0.0",
48
+ "zod": "^3.23.8"
49
+ },
50
+ "devDependencies": {
51
+ "@types/better-sqlite3": "^7.6.11",
52
+ "@types/node": "^20.0.0",
53
+ "typescript": "^5.4.0"
54
+ }
55
+ }