@a13xu/lucid 1.4.0 → 1.9.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 +118 -14
- package/build/config.d.ts +37 -0
- package/build/config.js +45 -0
- package/build/database.d.ts +36 -1
- package/build/database.js +85 -1
- package/build/guardian/coding-analyzer.d.ts +11 -0
- package/build/guardian/coding-analyzer.js +393 -0
- package/build/guardian/coding-rules.d.ts +1 -0
- package/build/guardian/coding-rules.js +97 -0
- package/build/index.js +164 -3
- package/build/indexer/ast.d.ts +9 -0
- package/build/indexer/ast.js +158 -0
- package/build/indexer/project.js +21 -13
- package/build/memory/experience.d.ts +11 -0
- package/build/memory/experience.js +85 -0
- package/build/retrieval/context.d.ts +29 -0
- package/build/retrieval/context.js +219 -0
- package/build/retrieval/qdrant.d.ts +16 -0
- package/build/retrieval/qdrant.js +135 -0
- package/build/retrieval/tfidf.d.ts +14 -0
- package/build/retrieval/tfidf.js +64 -0
- package/build/security/alerts.d.ts +44 -0
- package/build/security/alerts.js +228 -0
- package/build/security/env.d.ts +24 -0
- package/build/security/env.js +85 -0
- package/build/security/guard.d.ts +35 -0
- package/build/security/guard.js +133 -0
- package/build/security/ratelimit.d.ts +34 -0
- package/build/security/ratelimit.js +105 -0
- package/build/security/smtp.d.ts +26 -0
- package/build/security/smtp.js +125 -0
- package/build/security/ssrf.d.ts +18 -0
- package/build/security/ssrf.js +109 -0
- package/build/security/waf.d.ts +33 -0
- package/build/security/waf.js +174 -0
- package/build/tools/coding-guard.d.ts +24 -0
- package/build/tools/coding-guard.js +82 -0
- package/build/tools/context.d.ts +39 -0
- package/build/tools/context.js +105 -0
- package/build/tools/init.d.ts +41 -1
- package/build/tools/init.js +124 -22
- package/build/tools/remember.d.ts +4 -4
- package/build/tools/reward.d.ts +29 -0
- package/build/tools/reward.js +154 -0
- package/build/tools/sync.js +15 -0
- package/package.json +9 -2
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { assembleContext } from "../retrieval/context.js";
|
|
3
|
+
import { loadConfig } from "../config.js";
|
|
4
|
+
import { decompress } from "../store/content.js";
|
|
5
|
+
import { createExperience } from "../memory/experience.js";
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// get_context — smart token-efficient context retrieval
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
export const GetContextSchema = z.object({
|
|
10
|
+
query: z.string().min(1).describe("What you are working on or searching for"),
|
|
11
|
+
maxTokens: z.number().int().min(100).max(32000).optional()
|
|
12
|
+
.describe("Total token budget (default from lucid.config.json, typically 4000)"),
|
|
13
|
+
dirs: z.array(z.string()).optional()
|
|
14
|
+
.describe("Whitelist: only return files from these directories (e.g. [\"src\", \"backend\"])"),
|
|
15
|
+
recentOnly: z.boolean().optional()
|
|
16
|
+
.describe("Only return files modified within recentWindowHours"),
|
|
17
|
+
recentHours: z.number().optional()
|
|
18
|
+
.describe("Override recentWindowHours for this call"),
|
|
19
|
+
skeletonOnly: z.boolean().optional()
|
|
20
|
+
.describe("Always show skeleton (signatures only) even for small files"),
|
|
21
|
+
topK: z.number().int().min(1).max(50).optional()
|
|
22
|
+
.describe("Max files to consider (Qdrant: top-k chunks)"),
|
|
23
|
+
});
|
|
24
|
+
export async function handleGetContext(stmts, args) {
|
|
25
|
+
const cfg = loadConfig();
|
|
26
|
+
const result = await assembleContext(args.query, stmts, cfg, {
|
|
27
|
+
maxTokens: args.maxTokens,
|
|
28
|
+
dirs: args.dirs,
|
|
29
|
+
recentOnly: args.recentOnly,
|
|
30
|
+
recentHours: args.recentHours,
|
|
31
|
+
skeletonOnly: args.skeletonOnly,
|
|
32
|
+
topK: args.topK,
|
|
33
|
+
});
|
|
34
|
+
if (result.files.length === 0) {
|
|
35
|
+
return [
|
|
36
|
+
`⚠️ No relevant files found for: "${args.query}"`,
|
|
37
|
+
` Strategy: ${result.strategy}`,
|
|
38
|
+
` Tip: run init_project() or sync_project() first to index files`,
|
|
39
|
+
].join("\n");
|
|
40
|
+
}
|
|
41
|
+
// Log experience for RL reward system
|
|
42
|
+
const contextFps = result.files.map((f) => f.filepath);
|
|
43
|
+
const expId = createExperience(args.query, contextFps, result.strategy, stmts);
|
|
44
|
+
const lines = [
|
|
45
|
+
`// get_context: "${args.query}"`,
|
|
46
|
+
`// Strategy: ${result.strategy} | ${result.files.length} files | ~${result.totalTokens} tokens`,
|
|
47
|
+
`// Experience #${expId} logged. Call reward() if this context helped, penalize() if not.`,
|
|
48
|
+
result.truncated ? `// ⚠️ Truncated (${result.skippedFiles} files skipped — increase maxTokens to see more)` : "",
|
|
49
|
+
"",
|
|
50
|
+
].filter((l) => l !== undefined);
|
|
51
|
+
for (const f of result.files) {
|
|
52
|
+
lines.push(`// ─── ${f.filepath} [${f.language}] ~${f.tokens}t (${f.reason}) ───`);
|
|
53
|
+
lines.push(f.content);
|
|
54
|
+
lines.push("");
|
|
55
|
+
}
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// get_recent — recently modified files with diffs
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
export const GetRecentSchema = z.object({
|
|
62
|
+
hours: z.number().positive().optional()
|
|
63
|
+
.describe("Look back N hours (default 24)"),
|
|
64
|
+
withDiffs: z.boolean().optional()
|
|
65
|
+
.describe("Include line-level diffs (default true)"),
|
|
66
|
+
});
|
|
67
|
+
export function handleGetRecent(stmts, args) {
|
|
68
|
+
const cfg = loadConfig();
|
|
69
|
+
const hours = args.hours ?? cfg.recentWindowHours;
|
|
70
|
+
const withDiffs = args.withDiffs ?? true;
|
|
71
|
+
const cutoff = Math.floor(Date.now() / 1000) - hours * 3600;
|
|
72
|
+
const recentFiles = stmts.getRecentFiles.all(cutoff);
|
|
73
|
+
if (recentFiles.length === 0) {
|
|
74
|
+
return `No files modified in the last ${hours}h.\nTip: call sync_file(path) after each file change.`;
|
|
75
|
+
}
|
|
76
|
+
const recentDiffs = withDiffs
|
|
77
|
+
? stmts.getRecentDiffs.all(cutoff)
|
|
78
|
+
: [];
|
|
79
|
+
const diffMap = new Map(recentDiffs.map((d) => [d.filepath, d]));
|
|
80
|
+
const lines = [
|
|
81
|
+
`// ${recentFiles.length} file(s) modified in the last ${hours}h`,
|
|
82
|
+
"",
|
|
83
|
+
];
|
|
84
|
+
for (const f of recentFiles) {
|
|
85
|
+
const age = Math.round((Date.now() / 1000 - f.indexed_at) / 60);
|
|
86
|
+
const ageStr = age < 60 ? `${age}m ago` : `${Math.round(age / 60)}h ago`;
|
|
87
|
+
lines.push(`// ─── ${f.filepath} [${f.language}] (${ageStr}) ───`);
|
|
88
|
+
const diff = diffMap.get(f.filepath);
|
|
89
|
+
if (diff) {
|
|
90
|
+
lines.push(diff.diff_text);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// New file — show first ~20 lines
|
|
94
|
+
const row = stmts.getFileByPath.get(f.filepath);
|
|
95
|
+
if (row) {
|
|
96
|
+
const src = decompress(row.content).split("\n").slice(0, 20).join("\n");
|
|
97
|
+
lines.push(src);
|
|
98
|
+
if (row.original_size > src.length)
|
|
99
|
+
lines.push("… [new file, showing first 20 lines]");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
lines.push("");
|
|
103
|
+
}
|
|
104
|
+
return lines.join("\n");
|
|
105
|
+
}
|
package/build/tools/init.d.ts
CHANGED
|
@@ -2,10 +2,50 @@ import { z } from "zod";
|
|
|
2
2
|
import type { Statements } from "../database.js";
|
|
3
3
|
export declare const InitProjectSchema: z.ZodObject<{
|
|
4
4
|
directory: z.ZodOptional<z.ZodString>;
|
|
5
|
+
/** Display name of the security admin */
|
|
6
|
+
adminName: z.ZodOptional<z.ZodString>;
|
|
7
|
+
/** Email address to send security alerts to */
|
|
8
|
+
adminEmail: z.ZodOptional<z.ZodString>;
|
|
9
|
+
/** SMTP server hostname (e.g. smtp.gmail.com) */
|
|
10
|
+
smtpHost: z.ZodOptional<z.ZodString>;
|
|
11
|
+
/** SMTP port: 587 (STARTTLS, default) or 465 (direct TLS) */
|
|
12
|
+
smtpPort: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
/** SMTP login username (often same as adminEmail) */
|
|
14
|
+
smtpUser: z.ZodOptional<z.ZodString>;
|
|
15
|
+
/** "From" display name + address (e.g. "Lucid Security <alerts@co.com>") */
|
|
16
|
+
smtpFrom: z.ZodOptional<z.ZodString>;
|
|
17
|
+
/** Generic HTTP webhook URL (receives JSON POST, HMAC-signed if LUCID_WEBHOOK_SECRET is set) */
|
|
18
|
+
webhookUrl: z.ZodOptional<z.ZodString>;
|
|
19
|
+
/** Slack incoming webhook URL */
|
|
20
|
+
slackWebhookUrl: z.ZodOptional<z.ZodString>;
|
|
21
|
+
/** Which severities trigger an alert: default ["critical","high"] */
|
|
22
|
+
alertOn: z.ZodOptional<z.ZodArray<z.ZodEnum<["critical", "high", "medium", "low"]>, "many">>;
|
|
23
|
+
/** Human-readable project name shown in alerts */
|
|
24
|
+
projectName: z.ZodOptional<z.ZodString>;
|
|
5
25
|
}, "strip", z.ZodTypeAny, {
|
|
6
26
|
directory?: string | undefined;
|
|
27
|
+
adminName?: string | undefined;
|
|
28
|
+
adminEmail?: string | undefined;
|
|
29
|
+
smtpHost?: string | undefined;
|
|
30
|
+
smtpPort?: number | undefined;
|
|
31
|
+
smtpUser?: string | undefined;
|
|
32
|
+
smtpFrom?: string | undefined;
|
|
33
|
+
webhookUrl?: string | undefined;
|
|
34
|
+
slackWebhookUrl?: string | undefined;
|
|
35
|
+
alertOn?: ("low" | "medium" | "high" | "critical")[] | undefined;
|
|
36
|
+
projectName?: string | undefined;
|
|
7
37
|
}, {
|
|
8
38
|
directory?: string | undefined;
|
|
39
|
+
adminName?: string | undefined;
|
|
40
|
+
adminEmail?: string | undefined;
|
|
41
|
+
smtpHost?: string | undefined;
|
|
42
|
+
smtpPort?: number | undefined;
|
|
43
|
+
smtpUser?: string | undefined;
|
|
44
|
+
smtpFrom?: string | undefined;
|
|
45
|
+
webhookUrl?: string | undefined;
|
|
46
|
+
slackWebhookUrl?: string | undefined;
|
|
47
|
+
alertOn?: ("low" | "medium" | "high" | "critical")[] | undefined;
|
|
48
|
+
projectName?: string | undefined;
|
|
9
49
|
}>;
|
|
10
50
|
export type InitProjectInput = z.infer<typeof InitProjectSchema>;
|
|
11
|
-
export declare function handleInitProject(stmts: Statements, input: InitProjectInput): string
|
|
51
|
+
export declare function handleInitProject(stmts: Statements, input: InitProjectInput): Promise<string>;
|
package/build/tools/init.js
CHANGED
|
@@ -2,22 +2,44 @@ import { z } from "zod";
|
|
|
2
2
|
import { resolve, join } from "path";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
4
|
import { indexProject } from "../indexer/project.js";
|
|
5
|
+
import { saveAdminConfig, loadAdminConfig, isAdminConfigured, sendTestAlert, } from "../security/alerts.js";
|
|
5
6
|
export const InitProjectSchema = z.object({
|
|
6
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(),
|
|
7
29
|
});
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
};
|
|
11
40
|
function installHook(dir) {
|
|
12
41
|
const claudeDir = join(dir, ".claude");
|
|
13
42
|
const settingsPath = join(claudeDir, "settings.json");
|
|
14
|
-
const HOOK_CMD = 'node -e "const p=process.argv[1]; if(p) require(\'child_process\').execSync(\'node \'+require(\'path\').resolve(\'node_modules/.bin/lucid\'||\'\')+\' --noop\', {stdio:\'ignore\'})" "$TOOL_INPUT_PATH" 2>/dev/null || true';
|
|
15
|
-
// Hook mai simplu și portabil: apelează sync_file prin claude mcp
|
|
16
|
-
const HOOK = {
|
|
17
|
-
matcher: "Write|Edit|NotebookEdit",
|
|
18
|
-
command: "echo '{\"tool\":\"sync_file\",\"path\":\"'\"$TOOL_INPUT_PATH\"'\"}' | true",
|
|
19
|
-
};
|
|
20
|
-
// Citim sau cream settings.json
|
|
21
43
|
let settings = {};
|
|
22
44
|
if (existsSync(settingsPath)) {
|
|
23
45
|
try {
|
|
@@ -27,19 +49,17 @@ function installHook(dir) {
|
|
|
27
49
|
return { installed: false, reason: "Could not parse .claude/settings.json" };
|
|
28
50
|
}
|
|
29
51
|
}
|
|
30
|
-
|
|
31
|
-
const hooks = settings["hooks"] ?? {};
|
|
52
|
+
const hooks = (settings["hooks"] ?? {});
|
|
32
53
|
const postToolUse = hooks["PostToolUse"] ?? [];
|
|
33
|
-
|
|
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
|
+
});
|
|
34
59
|
if (alreadyInstalled) {
|
|
35
60
|
return { installed: false, reason: "already installed" };
|
|
36
61
|
}
|
|
37
|
-
|
|
38
|
-
const lucidHook = {
|
|
39
|
-
matcher: "Write|Edit|NotebookEdit",
|
|
40
|
-
command: "echo '🔄 Lucid: call sync_file(path) to keep knowledge graph up to date'",
|
|
41
|
-
};
|
|
42
|
-
hooks["PostToolUse"] = [...postToolUse, lucidHook];
|
|
62
|
+
hooks["PostToolUse"] = [...postToolUse, LUCID_HOOK];
|
|
43
63
|
settings["hooks"] = hooks;
|
|
44
64
|
mkdirSync(claudeDir, { recursive: true });
|
|
45
65
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
@@ -74,7 +94,7 @@ function injectClaudeMdInstruction(dir) {
|
|
|
74
94
|
// ---------------------------------------------------------------------------
|
|
75
95
|
// Main handler
|
|
76
96
|
// ---------------------------------------------------------------------------
|
|
77
|
-
export function handleInitProject(stmts, input) {
|
|
97
|
+
export async function handleInitProject(stmts, input) {
|
|
78
98
|
const dir = resolve(input.directory ?? process.cwd());
|
|
79
99
|
const results = indexProject(dir, stmts);
|
|
80
100
|
const lines = [`✅ Project indexed: ${dir}`, ``];
|
|
@@ -88,7 +108,7 @@ export function handleInitProject(stmts, input) {
|
|
|
88
108
|
lines.push(` • [${r.type}] "${r.entity}" — ${r.observations} observation(s) from ${r.source}`);
|
|
89
109
|
}
|
|
90
110
|
}
|
|
91
|
-
//
|
|
111
|
+
// ── Hook PostToolUse ──────────────────────────────────────────────────────
|
|
92
112
|
lines.push(``);
|
|
93
113
|
const hookResult = installHook(dir);
|
|
94
114
|
if (hookResult.installed) {
|
|
@@ -98,13 +118,95 @@ export function handleInitProject(stmts, input) {
|
|
|
98
118
|
else {
|
|
99
119
|
lines.push(`🔗 Hook: ${hookResult.reason}`);
|
|
100
120
|
}
|
|
101
|
-
//
|
|
121
|
+
// ── CLAUDE.md injection ───────────────────────────────────────────────────
|
|
102
122
|
const injected = injectClaudeMdInstruction(dir);
|
|
103
123
|
if (injected) {
|
|
104
124
|
lines.push(`📋 CLAUDE.md updated with sync_file() instruction`);
|
|
105
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
|
+
}
|
|
106
198
|
lines.push(``);
|
|
107
199
|
lines.push(`From now on, call sync_file(path) after every file you write or edit.`);
|
|
108
200
|
lines.push(`Use recall() to query accumulated project knowledge.`);
|
|
109
201
|
return lines.join("\n");
|
|
110
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: "
|
|
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
|
+
}
|
package/build/tools/sync.js
CHANGED
|
@@ -3,6 +3,9 @@ import { resolve, extname } from "path";
|
|
|
3
3
|
import { existsSync, readFileSync } from "fs";
|
|
4
4
|
import { indexFile, upsertFileIndex } from "../indexer/file.js";
|
|
5
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";
|
|
6
9
|
const SUPPORTED_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
|
|
7
10
|
// ---------------------------------------------------------------------------
|
|
8
11
|
// sync_file
|
|
@@ -21,10 +24,18 @@ export function handleSyncFile(stmts, args) {
|
|
|
21
24
|
if (!index)
|
|
22
25
|
return `Could not read file: ${filepath}`;
|
|
23
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;
|
|
24
30
|
const result = upsertFileIndex(index, source, stmts);
|
|
25
31
|
if (!result.stored) {
|
|
26
32
|
return `⏭️ Unchanged: ${filepath} (hash match — skipped)`;
|
|
27
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
|
+
}
|
|
28
39
|
const ratio = Math.round((1 - (result.savedBytes + Buffer.byteLength(source, "utf-8") - result.savedBytes) / Buffer.byteLength(source, "utf-8")) * 100);
|
|
29
40
|
const saved = Math.round(result.savedBytes / 1024 * 10) / 10;
|
|
30
41
|
const lines = [
|
|
@@ -36,6 +47,10 @@ export function handleSyncFile(stmts, args) {
|
|
|
36
47
|
lines.push(` description: ${index.description}`);
|
|
37
48
|
if (index.todos.length > 0)
|
|
38
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)`);
|
|
39
54
|
return lines.join("\n");
|
|
40
55
|
}
|
|
41
56
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a13xu/lucid",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.9.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
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"lucid": "./build/index.js"
|
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
"memory",
|
|
23
23
|
"sqlite",
|
|
24
24
|
"knowledge-graph",
|
|
25
|
+
"code-indexing",
|
|
26
|
+
"logic-guardian",
|
|
27
|
+
"drift-detection",
|
|
28
|
+
"tfidf",
|
|
29
|
+
"qdrant",
|
|
30
|
+
"token-optimization",
|
|
31
|
+
"context-pruning",
|
|
25
32
|
"ai",
|
|
26
33
|
"anthropic"
|
|
27
34
|
],
|