@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.
- package/LICENSE +21 -21
- package/README.md +221 -99
- package/build/config.d.ts +37 -0
- package/build/config.js +45 -0
- package/build/database.d.ts +54 -0
- package/build/database.js +175 -62
- package/build/guardian/checklist.js +66 -66
- 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 +241 -2
- package/build/indexer/ast.d.ts +9 -0
- package/build/indexer/ast.js +158 -0
- package/build/indexer/file.d.ts +15 -0
- package/build/indexer/file.js +100 -0
- package/build/indexer/project.d.ts +8 -0
- package/build/indexer/project.js +320 -0
- 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/store/content.d.ts +3 -0
- package/build/store/content.js +11 -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/grep.d.ts +17 -0
- package/build/tools/grep.js +65 -0
- package/build/tools/init.d.ts +51 -0
- package/build/tools/init.js +212 -0
- 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.d.ts +18 -0
- package/build/tools/sync.js +76 -0
- package/package.json +55 -48
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAF (Web Application Firewall) rules for MCP tool inputs.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Path traversal & directory escape
|
|
6
|
+
* - Null-byte & CRLF injection
|
|
7
|
+
* - ReDoS (catastrophic backtracking) detection
|
|
8
|
+
* - Input size limits (DoS prevention)
|
|
9
|
+
* - Suspicious injection patterns (SQLi, command injection)
|
|
10
|
+
* - Sensitive data leakage in outputs
|
|
11
|
+
*/
|
|
12
|
+
import { resolve, normalize } from "path";
|
|
13
|
+
const PASS = { blocked: false, violations: [] };
|
|
14
|
+
function block(rule, severity, detail) {
|
|
15
|
+
return { blocked: true, violations: [{ rule, severity, detail }] };
|
|
16
|
+
}
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Input size limits
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const MAX_LENGTHS = {
|
|
21
|
+
query: 2_000,
|
|
22
|
+
pattern: 500,
|
|
23
|
+
path: 1_000,
|
|
24
|
+
code: 200_000, // check_drift: allow large snippets
|
|
25
|
+
observation: 10_000,
|
|
26
|
+
entity: 500,
|
|
27
|
+
command: 2_000,
|
|
28
|
+
default: 50_000,
|
|
29
|
+
};
|
|
30
|
+
export function checkSize(field, value) {
|
|
31
|
+
const limit = MAX_LENGTHS[field] ?? MAX_LENGTHS["default"];
|
|
32
|
+
if (value.length > limit) {
|
|
33
|
+
return block("SIZE_LIMIT", "medium", `Field "${field}" exceeds max length (${value.length} > ${limit})`);
|
|
34
|
+
}
|
|
35
|
+
return PASS;
|
|
36
|
+
}
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Null-byte & CRLF injection
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
export function checkInjection(value) {
|
|
41
|
+
if (value.includes("\0")) {
|
|
42
|
+
return block("NULL_BYTE", "high", "Null byte detected in input");
|
|
43
|
+
}
|
|
44
|
+
if (/\r\n|\r/.test(value) && value.includes("HTTP/")) {
|
|
45
|
+
return block("CRLF_INJECTION", "high", "CRLF injection pattern detected");
|
|
46
|
+
}
|
|
47
|
+
return PASS;
|
|
48
|
+
}
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Path traversal
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Normalize and verify path stays within an allowed root
|
|
53
|
+
export function checkPath(inputPath, allowedRoot) {
|
|
54
|
+
const injection = checkInjection(inputPath);
|
|
55
|
+
if (injection.blocked)
|
|
56
|
+
return injection;
|
|
57
|
+
// Detect traversal patterns before resolution
|
|
58
|
+
if (/\.\.[/\\]/.test(inputPath) || inputPath.includes("..%2F") || inputPath.includes("..%5C")) {
|
|
59
|
+
return block("PATH_TRAVERSAL", "critical", "Directory traversal sequence detected");
|
|
60
|
+
}
|
|
61
|
+
// Detect absolute paths to sensitive OS locations
|
|
62
|
+
const normalized = normalize(inputPath).replace(/\\/g, "/");
|
|
63
|
+
const sensitiveRoots = ["/etc/", "/proc/", "/sys/", "/dev/", "/root/",
|
|
64
|
+
"C:/Windows/", "C:\\Windows\\"];
|
|
65
|
+
for (const root of sensitiveRoots) {
|
|
66
|
+
if (normalized.toLowerCase().startsWith(root.toLowerCase())) {
|
|
67
|
+
return block("SENSITIVE_PATH", "critical", `Access to sensitive system path blocked`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// If an allowed root is provided, verify path stays within it
|
|
71
|
+
if (allowedRoot) {
|
|
72
|
+
const resolvedPath = resolve(inputPath);
|
|
73
|
+
const resolvedRoot = resolve(allowedRoot);
|
|
74
|
+
if (!resolvedPath.startsWith(resolvedRoot)) {
|
|
75
|
+
return block("PATH_ESCAPE", "critical", `Path escapes allowed root (${resolvedRoot})`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return PASS;
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// ReDoS detection (for regex inputs in grep_code)
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Patterns that indicate catastrophic backtracking risk
|
|
84
|
+
const REDOS_PATTERNS = [
|
|
85
|
+
{ re: /\([^)]*[+*][^)]*\)[+*{]/, name: "nested-quantifier" }, // (a+)+
|
|
86
|
+
{ re: /\([^)]*\|[^)]*\)[+*{]/, name: "alternation-quantifier" }, // (a|b)+
|
|
87
|
+
{ re: /[+*]\s*[+*]/, name: "consecutive-quantifiers" }, // .* .*
|
|
88
|
+
{ re: /\{[0-9]{3,},/, name: "large-repetition" }, // {1000,}
|
|
89
|
+
{ re: /(\(\?[^)]*\)){3,}/, name: "excessive-lookahead" }, // (?=...)(?=...)(?=...)
|
|
90
|
+
];
|
|
91
|
+
export function checkReDoS(pattern) {
|
|
92
|
+
for (const { re, name } of REDOS_PATTERNS) {
|
|
93
|
+
if (re.test(pattern)) {
|
|
94
|
+
return block("REDOS", "high", `Regex pattern may cause catastrophic backtracking (rule: ${name})`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Also attempt to measure compile time as a secondary check
|
|
98
|
+
try {
|
|
99
|
+
const start = Date.now();
|
|
100
|
+
new RegExp(pattern);
|
|
101
|
+
if (Date.now() - start > 100) {
|
|
102
|
+
return block("REDOS", "medium", "Regex compilation was unexpectedly slow");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
return block("INVALID_REGEX", "low", `Invalid regex pattern: ${e.message}`);
|
|
107
|
+
}
|
|
108
|
+
return PASS;
|
|
109
|
+
}
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Command/SQL injection patterns (defensive — prepared stmts are primary guard)
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
const INJECTION_PATTERNS = [
|
|
114
|
+
{ re: /;\s*(DROP|DELETE|TRUNCATE|ALTER)\s+/i, rule: "SQL_INJECTION", severity: "critical" },
|
|
115
|
+
{ re: /UNION\s+(?:ALL\s+)?SELECT/i, rule: "SQL_UNION", severity: "high" },
|
|
116
|
+
{ re: /'\s*OR\s+'1'\s*=\s*'1/i, rule: "SQL_TAUTOLOGY", severity: "high" },
|
|
117
|
+
{ re: /`[^`]*`|;\s*[a-z]+\s+\/|&&|\|\|/, rule: "CMD_INJECTION", severity: "high" },
|
|
118
|
+
{ re: /\$\([^)]+\)|`[^`]+`/, rule: "SHELL_SUBSTITUTION", severity: "high" },
|
|
119
|
+
];
|
|
120
|
+
export function checkInjectionPatterns(value) {
|
|
121
|
+
for (const { re, rule, severity } of INJECTION_PATTERNS) {
|
|
122
|
+
if (re.test(value)) {
|
|
123
|
+
return block(rule, severity, `Injection pattern detected (rule: ${rule})`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return PASS;
|
|
127
|
+
}
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Sensitive data leak detection (for outbound content)
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
const SENSITIVE_LEAK_PATTERNS = [
|
|
132
|
+
/sk-[a-zA-Z0-9]{20,}/, // OpenAI API key
|
|
133
|
+
/AKIA[0-9A-Z]{16}/, // AWS access key
|
|
134
|
+
/(?:eyJ)[a-zA-Z0-9_-]{10,}\.(?:eyJ)[a-zA-Z0-9_-]{10,}/, // JWT
|
|
135
|
+
/-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/, // PEM key
|
|
136
|
+
/(?:password|passwd|secret|token)\s*[=:]\s*['"]?[^\s'"]{8,}/i, // generic
|
|
137
|
+
];
|
|
138
|
+
/** Check if a text blob about to be returned contains secrets. */
|
|
139
|
+
export function checkOutputLeakage(text) {
|
|
140
|
+
const violations = [];
|
|
141
|
+
for (const pattern of SENSITIVE_LEAK_PATTERNS) {
|
|
142
|
+
if (pattern.test(text)) {
|
|
143
|
+
violations.push({
|
|
144
|
+
rule: "DATA_LEAKAGE",
|
|
145
|
+
severity: "critical",
|
|
146
|
+
detail: "Output may contain sensitive credentials or keys",
|
|
147
|
+
});
|
|
148
|
+
break; // one warning is enough
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return violations;
|
|
152
|
+
}
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Composite check for common string fields
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
export function checkStringField(field, value, opts) {
|
|
157
|
+
const size = checkSize(field, value);
|
|
158
|
+
if (size.blocked)
|
|
159
|
+
return size;
|
|
160
|
+
const inj = checkInjection(value);
|
|
161
|
+
if (inj.blocked)
|
|
162
|
+
return inj;
|
|
163
|
+
if (opts?.isPath) {
|
|
164
|
+
const path = checkPath(value, opts.allowedRoot);
|
|
165
|
+
if (path.blocked)
|
|
166
|
+
return path;
|
|
167
|
+
}
|
|
168
|
+
if (opts?.isRegex) {
|
|
169
|
+
const redos = checkReDoS(value);
|
|
170
|
+
if (redos.blocked)
|
|
171
|
+
return redos;
|
|
172
|
+
}
|
|
173
|
+
return PASS;
|
|
174
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { deflateSync, inflateSync } from "zlib";
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
export function compress(source) {
|
|
4
|
+
return deflateSync(Buffer.from(source, "utf-8"), { level: 9 });
|
|
5
|
+
}
|
|
6
|
+
export function decompress(blob) {
|
|
7
|
+
return inflateSync(blob).toString("utf-8");
|
|
8
|
+
}
|
|
9
|
+
export function sha256(source) {
|
|
10
|
+
return createHash("sha256").update(source).digest("hex");
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const CheckCodeQualitySchema: z.ZodEffects<z.ZodObject<{
|
|
3
|
+
path: z.ZodOptional<z.ZodString>;
|
|
4
|
+
code: z.ZodOptional<z.ZodString>;
|
|
5
|
+
language: z.ZodOptional<z.ZodEnum<["python", "javascript", "typescript", "vue", "generic"]>>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
path?: string | undefined;
|
|
8
|
+
code?: string | undefined;
|
|
9
|
+
language?: "python" | "javascript" | "typescript" | "generic" | "vue" | undefined;
|
|
10
|
+
}, {
|
|
11
|
+
path?: string | undefined;
|
|
12
|
+
code?: string | undefined;
|
|
13
|
+
language?: "python" | "javascript" | "typescript" | "generic" | "vue" | undefined;
|
|
14
|
+
}>, {
|
|
15
|
+
path?: string | undefined;
|
|
16
|
+
code?: string | undefined;
|
|
17
|
+
language?: "python" | "javascript" | "typescript" | "generic" | "vue" | undefined;
|
|
18
|
+
}, {
|
|
19
|
+
path?: string | undefined;
|
|
20
|
+
code?: string | undefined;
|
|
21
|
+
language?: "python" | "javascript" | "typescript" | "generic" | "vue" | undefined;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function handleGetCodingRules(): string;
|
|
24
|
+
export declare function handleCheckCodeQuality(args: z.infer<typeof CheckCodeQualitySchema>): string;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { extname } from "path";
|
|
4
|
+
import { analyzeCodeQuality, formatQualityReport } from "../guardian/coding-analyzer.js";
|
|
5
|
+
import { CODING_RULES } from "../guardian/coding-rules.js";
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Schemas
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
export const CheckCodeQualitySchema = z
|
|
10
|
+
.object({
|
|
11
|
+
path: z.string().optional().describe("Absolute or relative path to the file to analyze."),
|
|
12
|
+
code: z.string().optional().describe("Code snippet to analyze inline."),
|
|
13
|
+
language: z
|
|
14
|
+
.enum(["python", "javascript", "typescript", "vue", "generic"])
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("Language hint. Auto-detected from file extension if path is provided."),
|
|
17
|
+
})
|
|
18
|
+
.refine((args) => args.path !== undefined || args.code !== undefined, {
|
|
19
|
+
message: "Provide either path (file to read) or code (inline snippet).",
|
|
20
|
+
});
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Extension → language map for synthetic snippet paths
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
const LANG_EXT = {
|
|
25
|
+
python: ".py",
|
|
26
|
+
javascript: ".js",
|
|
27
|
+
typescript: ".ts",
|
|
28
|
+
vue: ".vue",
|
|
29
|
+
generic: ".txt",
|
|
30
|
+
};
|
|
31
|
+
// Languages where frontend rules activate (by extension)
|
|
32
|
+
const FRONTEND_EXTS = new Set([".tsx", ".jsx", ".vue"]);
|
|
33
|
+
function syntheticPath(lang) {
|
|
34
|
+
const ext = LANG_EXT[lang] ?? ".txt";
|
|
35
|
+
return `<snippet>${ext}`;
|
|
36
|
+
}
|
|
37
|
+
function inferLang(filepath) {
|
|
38
|
+
const ext = extname(filepath).toLowerCase();
|
|
39
|
+
const map = {
|
|
40
|
+
".py": "python",
|
|
41
|
+
".js": "javascript",
|
|
42
|
+
".jsx": "javascript",
|
|
43
|
+
".ts": "typescript",
|
|
44
|
+
".tsx": "typescript",
|
|
45
|
+
".vue": "vue",
|
|
46
|
+
};
|
|
47
|
+
return map[ext];
|
|
48
|
+
}
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Handlers
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
export function handleGetCodingRules() {
|
|
53
|
+
return CODING_RULES;
|
|
54
|
+
}
|
|
55
|
+
export function handleCheckCodeQuality(args) {
|
|
56
|
+
let source;
|
|
57
|
+
let filepath;
|
|
58
|
+
let lang;
|
|
59
|
+
if (args.path !== undefined) {
|
|
60
|
+
try {
|
|
61
|
+
source = readFileSync(args.path, "utf-8");
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
return `❌ Cannot read file "${args.path}": ${err instanceof Error ? err.message : String(err)}`;
|
|
65
|
+
}
|
|
66
|
+
filepath = args.path;
|
|
67
|
+
// Explicit language overrides, otherwise infer from extension
|
|
68
|
+
lang = args.language ?? inferLang(args.path);
|
|
69
|
+
// Warn if extension is frontend but language was explicitly set to non-frontend
|
|
70
|
+
const ext = extname(args.path).toLowerCase();
|
|
71
|
+
if (FRONTEND_EXTS.has(ext) && lang && !["typescript", "javascript", "vue"].includes(lang)) {
|
|
72
|
+
lang = inferLang(args.path) ?? lang;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
source = args.code;
|
|
77
|
+
lang = args.language ?? "generic";
|
|
78
|
+
filepath = syntheticPath(lang);
|
|
79
|
+
}
|
|
80
|
+
const issues = analyzeCodeQuality(filepath, source, lang);
|
|
81
|
+
return formatQualityReport(filepath, issues);
|
|
82
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Statements } from "../database.js";
|
|
3
|
+
export declare const GetContextSchema: z.ZodObject<{
|
|
4
|
+
query: z.ZodString;
|
|
5
|
+
maxTokens: z.ZodOptional<z.ZodNumber>;
|
|
6
|
+
dirs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
7
|
+
recentOnly: z.ZodOptional<z.ZodBoolean>;
|
|
8
|
+
recentHours: z.ZodOptional<z.ZodNumber>;
|
|
9
|
+
skeletonOnly: z.ZodOptional<z.ZodBoolean>;
|
|
10
|
+
topK: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
}, "strip", z.ZodTypeAny, {
|
|
12
|
+
query: string;
|
|
13
|
+
maxTokens?: number | undefined;
|
|
14
|
+
dirs?: string[] | undefined;
|
|
15
|
+
recentOnly?: boolean | undefined;
|
|
16
|
+
recentHours?: number | undefined;
|
|
17
|
+
skeletonOnly?: boolean | undefined;
|
|
18
|
+
topK?: number | undefined;
|
|
19
|
+
}, {
|
|
20
|
+
query: string;
|
|
21
|
+
maxTokens?: number | undefined;
|
|
22
|
+
dirs?: string[] | undefined;
|
|
23
|
+
recentOnly?: boolean | undefined;
|
|
24
|
+
recentHours?: number | undefined;
|
|
25
|
+
skeletonOnly?: boolean | undefined;
|
|
26
|
+
topK?: number | undefined;
|
|
27
|
+
}>;
|
|
28
|
+
export declare function handleGetContext(stmts: Statements, args: z.infer<typeof GetContextSchema>): Promise<string>;
|
|
29
|
+
export declare const GetRecentSchema: z.ZodObject<{
|
|
30
|
+
hours: z.ZodOptional<z.ZodNumber>;
|
|
31
|
+
withDiffs: z.ZodOptional<z.ZodBoolean>;
|
|
32
|
+
}, "strip", z.ZodTypeAny, {
|
|
33
|
+
hours?: number | undefined;
|
|
34
|
+
withDiffs?: boolean | undefined;
|
|
35
|
+
}, {
|
|
36
|
+
hours?: number | undefined;
|
|
37
|
+
withDiffs?: boolean | undefined;
|
|
38
|
+
}>;
|
|
39
|
+
export declare function handleGetRecent(stmts: Statements, args: z.infer<typeof GetRecentSchema>): string;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Statements } from "../database.js";
|
|
3
|
+
export declare const GrepCodeSchema: z.ZodObject<{
|
|
4
|
+
pattern: z.ZodString;
|
|
5
|
+
language: z.ZodOptional<z.ZodEnum<["python", "javascript", "typescript", "generic"]>>;
|
|
6
|
+
context: z.ZodDefault<z.ZodNumber>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
pattern: string;
|
|
9
|
+
context: number;
|
|
10
|
+
language?: "python" | "javascript" | "typescript" | "generic" | undefined;
|
|
11
|
+
}, {
|
|
12
|
+
pattern: string;
|
|
13
|
+
language?: "python" | "javascript" | "typescript" | "generic" | undefined;
|
|
14
|
+
context?: number | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
export type GrepCodeInput = z.infer<typeof GrepCodeSchema>;
|
|
17
|
+
export declare function handleGrepCode(stmts: Statements, input: GrepCodeInput): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { decompress } from "../store/content.js";
|
|
3
|
+
export const GrepCodeSchema = z.object({
|
|
4
|
+
pattern: z.string().min(1),
|
|
5
|
+
language: z.enum(["python", "javascript", "typescript", "generic"]).optional(),
|
|
6
|
+
context: z.number().int().min(0).max(10).default(2),
|
|
7
|
+
});
|
|
8
|
+
export function handleGrepCode(stmts, input) {
|
|
9
|
+
let regex;
|
|
10
|
+
try {
|
|
11
|
+
regex = new RegExp(input.pattern, "i");
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return `Invalid regex pattern: ${input.pattern}`;
|
|
15
|
+
}
|
|
16
|
+
const files = stmts.getAllFiles.all();
|
|
17
|
+
const matches = [];
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
if (input.language && file.language !== input.language)
|
|
20
|
+
continue;
|
|
21
|
+
let source;
|
|
22
|
+
try {
|
|
23
|
+
source = decompress(file.content);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
continue; // skip fișiere corupte
|
|
27
|
+
}
|
|
28
|
+
const lines = source.split("\n");
|
|
29
|
+
for (let i = 0; i < lines.length; i++) {
|
|
30
|
+
if (!regex.test(lines[i]))
|
|
31
|
+
continue;
|
|
32
|
+
matches.push({
|
|
33
|
+
filepath: file.filepath,
|
|
34
|
+
line: i + 1,
|
|
35
|
+
text: lines[i],
|
|
36
|
+
contextBefore: lines.slice(Math.max(0, i - input.context), i),
|
|
37
|
+
contextAfter: lines.slice(i + 1, i + 1 + input.context),
|
|
38
|
+
});
|
|
39
|
+
if (matches.length >= 30)
|
|
40
|
+
break; // cap la 30 match-uri
|
|
41
|
+
}
|
|
42
|
+
if (matches.length >= 30)
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
if (matches.length === 0) {
|
|
46
|
+
return `No matches for /${input.pattern}/ in ${files.length} indexed file(s).`;
|
|
47
|
+
}
|
|
48
|
+
const lines = [
|
|
49
|
+
`Found ${matches.length} match(es) for /${input.pattern}/ across ${files.length} file(s):\n`,
|
|
50
|
+
];
|
|
51
|
+
let lastFile = "";
|
|
52
|
+
for (const m of matches) {
|
|
53
|
+
if (m.filepath !== lastFile) {
|
|
54
|
+
lines.push(`── ${m.filepath}`);
|
|
55
|
+
lastFile = m.filepath;
|
|
56
|
+
}
|
|
57
|
+
for (const l of m.contextBefore)
|
|
58
|
+
lines.push(` ${m.line - m.contextBefore.length + m.contextBefore.indexOf(l)}│ ${l}`);
|
|
59
|
+
lines.push(`▶ ${m.line}│ ${m.text}`);
|
|
60
|
+
for (const l of m.contextAfter)
|
|
61
|
+
lines.push(` ${m.line + 1 + m.contextAfter.indexOf(l)}│ ${l}`);
|
|
62
|
+
lines.push("");
|
|
63
|
+
}
|
|
64
|
+
return lines.join("\n");
|
|
65
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Statements } from "../database.js";
|
|
3
|
+
export declare const InitProjectSchema: z.ZodObject<{
|
|
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>;
|
|
25
|
+
}, "strip", z.ZodTypeAny, {
|
|
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;
|
|
37
|
+
}, {
|
|
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;
|
|
49
|
+
}>;
|
|
50
|
+
export type InitProjectInput = z.infer<typeof InitProjectSchema>;
|
|
51
|
+
export declare function handleInitProject(stmts: Statements, input: InitProjectInput): Promise<string>;
|