@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.
Files changed (46) hide show
  1. package/README.md +118 -14
  2. package/build/config.d.ts +37 -0
  3. package/build/config.js +45 -0
  4. package/build/database.d.ts +36 -1
  5. package/build/database.js +85 -1
  6. package/build/guardian/coding-analyzer.d.ts +11 -0
  7. package/build/guardian/coding-analyzer.js +393 -0
  8. package/build/guardian/coding-rules.d.ts +1 -0
  9. package/build/guardian/coding-rules.js +97 -0
  10. package/build/index.js +164 -3
  11. package/build/indexer/ast.d.ts +9 -0
  12. package/build/indexer/ast.js +158 -0
  13. package/build/indexer/project.js +21 -13
  14. package/build/memory/experience.d.ts +11 -0
  15. package/build/memory/experience.js +85 -0
  16. package/build/retrieval/context.d.ts +29 -0
  17. package/build/retrieval/context.js +219 -0
  18. package/build/retrieval/qdrant.d.ts +16 -0
  19. package/build/retrieval/qdrant.js +135 -0
  20. package/build/retrieval/tfidf.d.ts +14 -0
  21. package/build/retrieval/tfidf.js +64 -0
  22. package/build/security/alerts.d.ts +44 -0
  23. package/build/security/alerts.js +228 -0
  24. package/build/security/env.d.ts +24 -0
  25. package/build/security/env.js +85 -0
  26. package/build/security/guard.d.ts +35 -0
  27. package/build/security/guard.js +133 -0
  28. package/build/security/ratelimit.d.ts +34 -0
  29. package/build/security/ratelimit.js +105 -0
  30. package/build/security/smtp.d.ts +26 -0
  31. package/build/security/smtp.js +125 -0
  32. package/build/security/ssrf.d.ts +18 -0
  33. package/build/security/ssrf.js +109 -0
  34. package/build/security/waf.d.ts +33 -0
  35. package/build/security/waf.js +174 -0
  36. package/build/tools/coding-guard.d.ts +24 -0
  37. package/build/tools/coding-guard.js +82 -0
  38. package/build/tools/context.d.ts +39 -0
  39. package/build/tools/context.js +105 -0
  40. package/build/tools/init.d.ts +41 -1
  41. package/build/tools/init.js +124 -22
  42. package/build/tools/remember.d.ts +4 -4
  43. package/build/tools/reward.d.ts +29 -0
  44. package/build/tools/reward.js +154 -0
  45. package/build/tools/sync.js +15 -0
  46. package/package.json +9 -2
package/build/index.js CHANGED
@@ -4,6 +4,9 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { z } from "zod";
6
6
  import { initDatabase, prepareStatements } from "./database.js";
7
+ import { guardRequest, guardOutput, configureGuard } from "./security/guard.js";
8
+ import { allowHost } from "./security/ssrf.js";
9
+ import { loadConfig } from "./config.js";
7
10
  import { remember, RememberSchema } from "./tools/remember.js";
8
11
  import { relate, RelateSchema } from "./tools/relate.js";
9
12
  import { recall, RecallSchema } from "./tools/recall.js";
@@ -14,15 +17,42 @@ import { handleValidateFile, ValidateFileSchema, handleCheckDrift, CheckDriftSch
14
17
  import { handleGrepCode, GrepCodeSchema } from "./tools/grep.js";
15
18
  import { handleInitProject, InitProjectSchema } from "./tools/init.js";
16
19
  import { handleSyncFile, SyncFileSchema, handleSyncProject, SyncProjectSchema, } from "./tools/sync.js";
20
+ import { handleGetContext, GetContextSchema, handleGetRecent, GetRecentSchema, } from "./tools/context.js";
21
+ import { handleReward, RewardSchema, handlePenalize, PenalizeSchema, handleShowRewards, ShowRewardsSchema, } from "./tools/reward.js";
22
+ import { handleGetCodingRules, handleCheckCodeQuality, CheckCodeQualitySchema, } from "./tools/coding-guard.js";
17
23
  // ---------------------------------------------------------------------------
18
24
  // Init DB
19
25
  // ---------------------------------------------------------------------------
20
26
  const db = initDatabase();
21
27
  const stmts = prepareStatements(db);
22
28
  // ---------------------------------------------------------------------------
29
+ // Security guard — initialize from config + env
30
+ // ---------------------------------------------------------------------------
31
+ const _appCfg = loadConfig();
32
+ configureGuard(_appCfg.security ?? {});
33
+ // Register Qdrant host in SSRF allowlist if configured
34
+ const _qdrantUrl = process.env["QDRANT_URL"] ?? _appCfg.qdrant?.url;
35
+ if (_qdrantUrl) {
36
+ try {
37
+ allowHost(_qdrantUrl);
38
+ }
39
+ catch { /* ignore invalid URL */ }
40
+ }
41
+ const _embeddingUrl = process.env["EMBEDDING_URL"] ?? _appCfg.qdrant?.embeddingUrl;
42
+ if (_embeddingUrl) {
43
+ try {
44
+ allowHost(_embeddingUrl);
45
+ }
46
+ catch { /* ignore */ }
47
+ }
48
+ else {
49
+ // Default embedding endpoint
50
+ allowHost("https://api.openai.com");
51
+ }
52
+ // ---------------------------------------------------------------------------
23
53
  // MCP Server
24
54
  // ---------------------------------------------------------------------------
25
- const server = new Server({ name: "lucid", version: "1.1.0" }, { capabilities: { tools: {} } });
55
+ const server = new Server({ name: "lucid", version: "1.9.1" }, { capabilities: { tools: {} } });
26
56
  // ---------------------------------------------------------------------------
27
57
  // Tool definitions
28
58
  // ---------------------------------------------------------------------------
@@ -154,6 +184,79 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
154
184
  required: ["pattern"],
155
185
  },
156
186
  },
187
+ // ── Context & Token Optimization ─────────────────────────────────────────
188
+ {
189
+ name: "get_context",
190
+ description: "Retrieve the minimal relevant context for a task or query. " +
191
+ "Uses TF-IDF scoring (or Qdrant vector search if configured) to rank files by relevance, " +
192
+ "applies recency boost for recently modified files, and returns skeletons (signatures only) " +
193
+ "for large files to stay within the token budget. " +
194
+ "Configure limits in lucid.config.json. Set QDRANT_URL env var for vector search.",
195
+ inputSchema: {
196
+ type: "object",
197
+ properties: {
198
+ query: { type: "string", description: "What you are working on or searching for" },
199
+ maxTokens: { type: "number", description: "Total token budget (default 4000)" },
200
+ dirs: { type: "array", items: { type: "string" }, description: "Whitelist directories (e.g. [\"src\", \"backend\"])" },
201
+ recentOnly: { type: "boolean", description: "Only files modified within recentWindowHours" },
202
+ recentHours: { type: "number", description: "Override recent window (hours)" },
203
+ skeletonOnly: { type: "boolean", description: "Always show skeleton (signatures only)" },
204
+ topK: { type: "number", description: "Max files to consider (default 10)" },
205
+ },
206
+ required: ["query"],
207
+ },
208
+ },
209
+ {
210
+ name: "get_recent",
211
+ description: "Return files modified recently with line-level diffs. " +
212
+ "Shows what changed in each file since the previous sync. " +
213
+ "Useful for catching up after a git pull or resuming a session.",
214
+ inputSchema: {
215
+ type: "object",
216
+ properties: {
217
+ hours: { type: "number", description: "Look back N hours (default 24)" },
218
+ withDiffs: { type: "boolean", description: "Include line diffs (default true)" },
219
+ },
220
+ },
221
+ },
222
+ // ── Reward System ────────────────────────────────────────────────────────
223
+ {
224
+ name: "reward",
225
+ description: "Signal that the last get_context() result was helpful (+1 reward). " +
226
+ "The files returned in that context will be ranked higher in future similar queries. " +
227
+ "Call this after a get_context() result led to a correct fix or useful code.",
228
+ inputSchema: {
229
+ type: "object",
230
+ properties: {
231
+ note: { type: "string", description: "Optional note about what worked (stored for future reference)" },
232
+ },
233
+ },
234
+ },
235
+ {
236
+ name: "penalize",
237
+ description: "Signal that the last get_context() result was unhelpful (-1 reward). " +
238
+ "The files returned in that context will be ranked lower in future similar queries. " +
239
+ "Call this after a get_context() result missed important files or was irrelevant.",
240
+ inputSchema: {
241
+ type: "object",
242
+ properties: {
243
+ note: { type: "string", description: "Optional note about what was missing or wrong" },
244
+ },
245
+ },
246
+ },
247
+ {
248
+ name: "show_rewards",
249
+ description: "Show the top rewarded experiences and most rewarded files. " +
250
+ "Rewards decay exponentially (half-life ~14 days). " +
251
+ "Use this to understand which context queries and files have been most valuable.",
252
+ inputSchema: {
253
+ type: "object",
254
+ properties: {
255
+ query: { type: "string", description: "Filter experiences by query text (optional)" },
256
+ topK: { type: "number", description: "Number of top results to show (default 10)" },
257
+ },
258
+ },
259
+ },
157
260
  // ── Logic Guardian ───────────────────────────────────────────────────────
158
261
  {
159
262
  name: "validate_file",
@@ -191,6 +294,34 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
191
294
  "Call this before marking any implementation task as done.",
192
295
  inputSchema: { type: "object", properties: {} },
193
296
  },
297
+ // ── Coding Guard ─────────────────────────────────────────────────────────
298
+ {
299
+ name: "coding_rules",
300
+ description: "Get the 25 Golden Rules coding checklist. Covers clarity, naming, single responsibility, " +
301
+ "error handling, frontend component size/reuse/props, singleton rules, library selection, " +
302
+ "and architecture separation. Review before marking any task done.",
303
+ inputSchema: { type: "object", properties: {} },
304
+ },
305
+ {
306
+ name: "check_code_quality",
307
+ description: "Analyze a file or code snippet against the 25 Golden Rules. " +
308
+ "Detects: file/function size violations, vague naming, deep nesting, dead code, and — " +
309
+ "for React/Vue component files — inline styles, prop explosion, fetch-in-component, " +
310
+ "direct DOM access, mixed styling systems. " +
311
+ "Complements validate_file (which checks logic correctness).",
312
+ inputSchema: {
313
+ type: "object",
314
+ properties: {
315
+ path: { type: "string", description: "Absolute or relative path to the file to analyze." },
316
+ code: { type: "string", description: "Code snippet to analyze inline." },
317
+ language: {
318
+ type: "string",
319
+ enum: ["python", "javascript", "typescript", "vue", "generic"],
320
+ description: "Language hint. Auto-detected from file extension if path is provided.",
321
+ },
322
+ },
323
+ },
324
+ },
194
325
  ],
195
326
  }));
196
327
  // ---------------------------------------------------------------------------
@@ -198,6 +329,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
198
329
  // ---------------------------------------------------------------------------
199
330
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
200
331
  const { name, arguments: args } = request.params;
332
+ // Security: rate limit + WAF check before any execution
333
+ const guard = guardRequest(name, args);
334
+ if (guard.blocked) {
335
+ return { content: [{ type: "text", text: guard.reason ?? "Request blocked by security guard" }], isError: true };
336
+ }
201
337
  try {
202
338
  let text;
203
339
  switch (name) {
@@ -222,7 +358,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
222
358
  break;
223
359
  // Init & Sync
224
360
  case "init_project":
225
- text = handleInitProject(stmts, InitProjectSchema.parse(args));
361
+ text = await handleInitProject(stmts, InitProjectSchema.parse(args));
226
362
  break;
227
363
  case "sync_file":
228
364
  text = handleSyncFile(stmts, SyncFileSchema.parse(args));
@@ -234,6 +370,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
234
370
  case "grep_code":
235
371
  text = handleGrepCode(stmts, GrepCodeSchema.parse(args));
236
372
  break;
373
+ // Context & Token Optimization
374
+ case "get_context":
375
+ text = await handleGetContext(stmts, GetContextSchema.parse(args));
376
+ break;
377
+ case "get_recent":
378
+ text = handleGetRecent(stmts, GetRecentSchema.parse(args));
379
+ break;
380
+ // Reward System
381
+ case "reward":
382
+ text = handleReward(stmts, RewardSchema.parse(args));
383
+ break;
384
+ case "penalize":
385
+ text = handlePenalize(stmts, PenalizeSchema.parse(args));
386
+ break;
387
+ case "show_rewards":
388
+ text = handleShowRewards(stmts, ShowRewardsSchema.parse(args));
389
+ break;
237
390
  // Logic Guardian
238
391
  case "validate_file":
239
392
  text = handleValidateFile(ValidateFileSchema.parse(args));
@@ -244,10 +397,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
244
397
  case "get_checklist":
245
398
  text = handleGetChecklist();
246
399
  break;
400
+ // Coding Guard
401
+ case "coding_rules":
402
+ text = handleGetCodingRules();
403
+ break;
404
+ case "check_code_quality":
405
+ text = handleCheckCodeQuality(CheckCodeQualitySchema.parse(args));
406
+ break;
247
407
  default:
248
408
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
249
409
  }
250
- return { content: [{ type: "text", text }] };
410
+ // Security: scan output for sensitive data leakage
411
+ return { content: [{ type: "text", text: guardOutput(name, text) }] };
251
412
  }
252
413
  catch (err) {
253
414
  const message = err instanceof z.ZodError
@@ -0,0 +1,9 @@
1
+ export interface Skeleton {
2
+ imports: string[];
3
+ exports: string[];
4
+ todos: string[];
5
+ summary: string;
6
+ }
7
+ export declare function extractSkeleton(source: string, language: string): Skeleton;
8
+ /** Render skeleton as compact text for context assembly. */
9
+ export declare function renderSkeleton(sk: Skeleton, filepath: string): string;
@@ -0,0 +1,158 @@
1
+ // Structural skeleton extraction — regex-based AST-like parsing
2
+ // Returns only signatures, imports, and TODO comments (no function bodies)
3
+ // Used by get_context when a file exceeds the per-file token budget
4
+ // ---------------------------------------------------------------------------
5
+ // TypeScript / JavaScript
6
+ // ---------------------------------------------------------------------------
7
+ function skeletonTS(source) {
8
+ const lines = source.split("\n");
9
+ const imports = [];
10
+ const exports = [];
11
+ const todos = [];
12
+ let summary = "";
13
+ // Grab first JSDoc comment as summary
14
+ const jsdoc = source.match(/^\/\*\*([\s\S]*?)\*\//m);
15
+ if (jsdoc) {
16
+ summary = jsdoc[1].replace(/\s*\*\s*/g, " ").trim().slice(0, 150);
17
+ }
18
+ let i = 0;
19
+ while (i < lines.length) {
20
+ const line = lines[i];
21
+ const trimmed = line.trim();
22
+ // Imports
23
+ if (/^import\s/.test(trimmed)) {
24
+ // Multi-line import: collect until ';'
25
+ let full = line;
26
+ while (!full.includes(";") && i + 1 < lines.length) {
27
+ i++;
28
+ full += " " + lines[i].trim();
29
+ }
30
+ imports.push(full.replace(/\s+/g, " ").trim());
31
+ i++;
32
+ continue;
33
+ }
34
+ // Exported declarations
35
+ if (/^export\s/.test(trimmed)) {
36
+ // Grab JSDoc above if present
37
+ let sig = line;
38
+ // If it's a function/class/interface, find the signature (up to first '{' or ';')
39
+ if (/^export\s+(async\s+)?function|^export\s+(abstract\s+)?class|^export\s+interface/.test(trimmed)) {
40
+ let j = i;
41
+ let full = "";
42
+ while (j < lines.length) {
43
+ full += lines[j] + "\n";
44
+ if (lines[j].includes("{") || lines[j].includes(";"))
45
+ break;
46
+ j++;
47
+ }
48
+ // Show only up to opening brace
49
+ sig = full.split("{")[0].replace(/\n/g, " ").replace(/\s+/g, " ").trim() + " { … }";
50
+ }
51
+ else if (/^export\s+(type|interface)\s/.test(trimmed)) {
52
+ // Multi-line type — take first line
53
+ sig = trimmed.split("{")[0].trim() + (trimmed.includes("{") ? " { … }" : "");
54
+ }
55
+ else {
56
+ // const/enum/default — take line
57
+ sig = trimmed.slice(0, 120);
58
+ }
59
+ exports.push(sig);
60
+ i++;
61
+ continue;
62
+ }
63
+ // TODOs
64
+ if (/\/\/\s*(TODO|FIXME|HACK)/i.test(trimmed)) {
65
+ todos.push(trimmed.slice(0, 100));
66
+ }
67
+ i++;
68
+ }
69
+ return { imports, exports, todos, summary };
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // Python
73
+ // ---------------------------------------------------------------------------
74
+ function skeletonPython(source) {
75
+ const lines = source.split("\n");
76
+ const imports = [];
77
+ const exports = [];
78
+ const todos = [];
79
+ let summary = "";
80
+ // Module docstring
81
+ const docMatch = source.match(/^['"]{3}([\s\S]*?)['"]{3}/m);
82
+ if (docMatch)
83
+ summary = docMatch[1].trim().slice(0, 150);
84
+ for (let i = 0; i < lines.length; i++) {
85
+ const line = lines[i];
86
+ const trimmed = line.trim();
87
+ if (trimmed.startsWith("import ") || trimmed.startsWith("from ")) {
88
+ imports.push(trimmed.slice(0, 100));
89
+ continue;
90
+ }
91
+ // Public function/class/async def at top level (no indent)
92
+ if (/^(def|class|async def)\s+(\w)/.test(trimmed) && !trimmed.startsWith("_")) {
93
+ // Collect signature (may span multiple lines until ':')
94
+ let sig = line;
95
+ let j = i + 1;
96
+ while (!sig.includes(":") && j < lines.length) {
97
+ sig += " " + lines[j].trim();
98
+ j++;
99
+ }
100
+ sig = sig.split(":")[0].replace(/\s+/g, " ").trim() + ":";
101
+ exports.push(sig.slice(0, 120));
102
+ continue;
103
+ }
104
+ if (/^\s*#\s*(TODO|FIXME|HACK)/i.test(line)) {
105
+ todos.push(trimmed.slice(0, 100));
106
+ }
107
+ }
108
+ return { imports, exports, todos, summary };
109
+ }
110
+ // ---------------------------------------------------------------------------
111
+ // Generic (markdown, yaml, json, etc.)
112
+ // ---------------------------------------------------------------------------
113
+ function skeletonGeneric(source) {
114
+ const lines = source.split("\n").slice(0, 30);
115
+ const todos = [];
116
+ for (const line of source.split("\n")) {
117
+ if (/(?:\/\/|#)\s*(TODO|FIXME|HACK)/i.test(line)) {
118
+ todos.push(line.trim().slice(0, 100));
119
+ }
120
+ }
121
+ return {
122
+ imports: [],
123
+ exports: [],
124
+ todos,
125
+ summary: lines.join("\n").slice(0, 300),
126
+ };
127
+ }
128
+ // ---------------------------------------------------------------------------
129
+ // Public
130
+ // ---------------------------------------------------------------------------
131
+ export function extractSkeleton(source, language) {
132
+ switch (language) {
133
+ case "typescript":
134
+ case "javascript":
135
+ return skeletonTS(source);
136
+ case "python":
137
+ return skeletonPython(source);
138
+ default:
139
+ return skeletonGeneric(source);
140
+ }
141
+ }
142
+ /** Render skeleton as compact text for context assembly. */
143
+ export function renderSkeleton(sk, filepath) {
144
+ const parts = [`// ${filepath} [skeleton]`];
145
+ if (sk.summary)
146
+ parts.push(`// ${sk.summary}`);
147
+ if (sk.imports.length > 0)
148
+ parts.push(sk.imports.slice(0, 8).join("\n"));
149
+ if (sk.exports.length > 0) {
150
+ parts.push("// — exports —");
151
+ parts.push(sk.exports.join("\n"));
152
+ }
153
+ if (sk.todos.length > 0) {
154
+ parts.push("// — TODOs —");
155
+ parts.push(sk.todos.join("\n"));
156
+ }
157
+ return parts.join("\n\n");
158
+ }
@@ -183,9 +183,9 @@ function indexLogicGuardianYaml(path, stmts, results) {
183
183
  }
184
184
  // Source file indexing — extrage exporturi, clase, funcții principale
185
185
  const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
186
- const SKIP_DIRS = new Set(["node_modules", ".git", "build", "dist", "__pycache__", ".next", "venv", ".venv", "target"]);
187
- const MAX_SOURCE_FILES = 30;
188
- function indexSourceFile(filepath, projectName, stmts) {
186
+ const SKIP_DIRS = new Set(["node_modules", ".git", "build", "dist", "__pycache__", ".next", "venv", ".venv", "target", ".cache", "coverage", ".nyc_output"]);
187
+ const MAX_SOURCE_FILES = 10_000;
188
+ function indexSourceFile(filepath, rootDir, projectName, stmts) {
189
189
  const content = readFile(filepath);
190
190
  if (!content)
191
191
  return [];
@@ -206,18 +206,18 @@ function indexSourceFile(filepath, projectName, stmts) {
206
206
  }
207
207
  if (exports.length === 0)
208
208
  return [];
209
- const relPath = filepath.replace(/\\/g, "/").split("/src/")[1] ?? basename(filepath);
209
+ // Cale relativă față de rădăcina proiectului
210
+ const relPath = filepath.replace(/\\/g, "/").replace(rootDir.replace(/\\/g, "/") + "/", "");
210
211
  const obs = [`exports from ${relPath}: ${exports.slice(0, 10).join(", ")}`];
211
212
  upsert(stmts, projectName, "project", obs);
212
213
  return exports;
213
214
  }
214
215
  function scanSources(dir, projectName, stmts, results) {
215
- const srcDir = join(dir, "src");
216
- const scanDir = existsSync(srcDir) ? srcDir : dir;
216
+ const rootDir = dir.replace(/\\/g, "/");
217
217
  let fileCount = 0;
218
218
  const exportedSymbols = [];
219
- function walk(d, depth) {
220
- if (depth > 3 || fileCount >= MAX_SOURCE_FILES)
219
+ function walk(d) {
220
+ if (fileCount >= MAX_SOURCE_FILES)
221
221
  return;
222
222
  let entries;
223
223
  try {
@@ -230,18 +230,26 @@ function scanSources(dir, projectName, stmts, results) {
230
230
  if (SKIP_DIRS.has(entry))
231
231
  continue;
232
232
  const full = join(d, entry);
233
- const stat = statSync(full);
233
+ let stat;
234
+ try {
235
+ stat = statSync(full);
236
+ }
237
+ catch {
238
+ continue;
239
+ }
234
240
  if (stat.isDirectory()) {
235
- walk(full, depth + 1);
241
+ walk(full);
236
242
  }
237
- else if (SOURCE_EXTS.has(extname(entry))) {
238
- const syms = indexSourceFile(full, projectName, stmts);
243
+ else if (SOURCE_EXTS.has(extname(entry).toLowerCase())) {
244
+ const syms = indexSourceFile(full, rootDir, projectName, stmts);
239
245
  exportedSymbols.push(...syms);
240
246
  fileCount++;
247
+ if (fileCount >= MAX_SOURCE_FILES)
248
+ return;
241
249
  }
242
250
  }
243
251
  }
244
- walk(scanDir, 0);
252
+ walk(dir);
245
253
  if (fileCount > 0) {
246
254
  results.push({
247
255
  entity: projectName,
@@ -0,0 +1,11 @@
1
+ import type { Statements } from "../database.js";
2
+ export declare const getLastExperienceId: () => number | null;
3
+ export declare const setLastExperienceId: (id: number) => void;
4
+ export declare function decayedReward(reward: number, rewardedAt: number | null): number;
5
+ export declare function createExperience(query: string, contextFps: string[], strategy: string, stmts: Statements): number;
6
+ export declare function rewardExperience(id: number, delta: number, feedback: string | null, stmts: Statements): {
7
+ query: string;
8
+ fps: string[];
9
+ } | null;
10
+ export declare function implicitRewardFromSync(filepath: string, stmts: Statements): boolean;
11
+ export declare function getFileRewardsMap(stmts: Statements): Map<string, number>;
@@ -0,0 +1,85 @@
1
+ // Reward system — lightweight RL from get_context usage signals
2
+ // Sources: explicit reward()/penalize(), implicit sync_file(), temporal decay
3
+ // ---------------------------------------------------------------------------
4
+ // In-process tracking of the last created experience (server is long-running)
5
+ // ---------------------------------------------------------------------------
6
+ let _lastId = null;
7
+ export const getLastExperienceId = () => _lastId;
8
+ export const setLastExperienceId = (id) => { _lastId = id; };
9
+ // ---------------------------------------------------------------------------
10
+ // Exponential decay — half-life ≈ 14 days (λ = ln(2)/14 ≈ 0.0495 ≈ 0.05)
11
+ // ---------------------------------------------------------------------------
12
+ export function decayedReward(reward, rewardedAt) {
13
+ if (rewardedAt === null || rewardedAt === 0)
14
+ return 0;
15
+ const daysSince = Math.max(0, (Date.now() / 1000 - rewardedAt) / 86400);
16
+ return reward * Math.exp(-0.05 * daysSince);
17
+ }
18
+ // ---------------------------------------------------------------------------
19
+ // Helpers
20
+ // ---------------------------------------------------------------------------
21
+ function tokenizeQuery(query) {
22
+ return query.toLowerCase().split(/\s+/).filter((t) => t.length > 1).join(" ");
23
+ }
24
+ function parseFps(contextFps) {
25
+ try {
26
+ return JSON.parse(contextFps);
27
+ }
28
+ catch {
29
+ return [];
30
+ }
31
+ }
32
+ // ---------------------------------------------------------------------------
33
+ // Create experience — called after get_context returns results
34
+ // ---------------------------------------------------------------------------
35
+ export function createExperience(query, contextFps, strategy, stmts) {
36
+ const result = stmts.insertExperience.run(query, tokenizeQuery(query), JSON.stringify(contextFps), strategy);
37
+ const id = Number(result.lastInsertRowid);
38
+ setLastExperienceId(id);
39
+ return id;
40
+ }
41
+ // ---------------------------------------------------------------------------
42
+ // Reward or penalize an experience (explicit feedback)
43
+ // ---------------------------------------------------------------------------
44
+ export function rewardExperience(id, delta, feedback, stmts) {
45
+ const exp = stmts.getExperienceById.get(id);
46
+ if (!exp)
47
+ return null;
48
+ stmts.updateExperienceReward.run(delta, feedback, id);
49
+ const fps = parseFps(exp.context_fps);
50
+ for (const fp of fps) {
51
+ stmts.upsertFileReward.run(fp, delta);
52
+ }
53
+ return { query: exp.query, fps };
54
+ }
55
+ // ---------------------------------------------------------------------------
56
+ // Implicit reward from sync_file — if filepath was in the last context → +0.3
57
+ // ---------------------------------------------------------------------------
58
+ const IMPLICIT_DELTA = 0.3;
59
+ export function implicitRewardFromSync(filepath, stmts) {
60
+ const lastId = getLastExperienceId();
61
+ if (lastId === null)
62
+ return false;
63
+ const exp = stmts.getExperienceById.get(lastId);
64
+ if (!exp)
65
+ return false;
66
+ const fps = parseFps(exp.context_fps);
67
+ if (!fps.includes(filepath))
68
+ return false;
69
+ stmts.updateExperienceReward.run(IMPLICIT_DELTA, null, lastId);
70
+ stmts.upsertFileReward.run(filepath, IMPLICIT_DELTA);
71
+ return true;
72
+ }
73
+ // ---------------------------------------------------------------------------
74
+ // Get file rewards map — for ranking boost in assembleContext()
75
+ // ---------------------------------------------------------------------------
76
+ export function getFileRewardsMap(stmts) {
77
+ const rows = stmts.getFileRewards.all();
78
+ const map = new Map();
79
+ for (const row of rows) {
80
+ const d = decayedReward(row.total_reward, row.last_rewarded);
81
+ if (d > 0)
82
+ map.set(row.filepath, d);
83
+ }
84
+ return map;
85
+ }
@@ -0,0 +1,29 @@
1
+ import type { Statements } from "../database.js";
2
+ import type { ResolvedConfig } from "../config.js";
3
+ export declare function estimateTokens(text: string): number;
4
+ export declare function extractFragments(source: string, query: string, contextLines?: number): string;
5
+ export declare function computeDiff(prev: string, curr: string, maxChanges?: number): string;
6
+ export interface ContextFile {
7
+ filepath: string;
8
+ language: string;
9
+ tokens: number;
10
+ content: string;
11
+ reason: string;
12
+ }
13
+ export interface ContextResult {
14
+ files: ContextFile[];
15
+ totalTokens: number;
16
+ strategy: "qdrant" | "tfidf" | "recent";
17
+ truncated: boolean;
18
+ skippedFiles: number;
19
+ }
20
+ export interface ContextOptions {
21
+ maxTokens?: number;
22
+ maxTokensPerFile?: number;
23
+ dirs?: string[];
24
+ recentOnly?: boolean;
25
+ recentHours?: number;
26
+ skeletonOnly?: boolean;
27
+ topK?: number;
28
+ }
29
+ export declare function assembleContext(query: string, stmts: Statements, cfg: ResolvedConfig, opts?: ContextOptions): Promise<ContextResult>;