@plumpslabs/kuma 2.3.6 → 2.3.8
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 +25 -3
- package/dist/{chunk-K64NSHBR.js → chunk-4OB3XMHT.js} +2 -2
- package/dist/chunk-53J56QCQ.js +282 -0
- package/dist/{chunk-FOQQ2CSL.js → chunk-FL2TLLMX.js} +1 -1
- package/dist/{chunk-GDNAWLHF.js → chunk-NAM7SCBT.js} +63 -1
- package/dist/chunk-PTEPJK6M.js +155 -0
- package/dist/{chunk-X5TPBDKO.js → chunk-RTGLQDMI.js} +2 -2
- package/dist/{chunk-BI7KD3SG.js → chunk-SLRRDKQ2.js} +2 -2
- package/dist/contextDigest-4PCK3DSM.js +177 -0
- package/dist/domainRules-LDZPOIGZ.js +20 -0
- package/dist/index.js +261 -182
- package/dist/kumaAstValidator-CNM7FHYA.js +150 -0
- package/dist/{kumaDb-DJUDLYBJ.js → kumaDb-6D53ERFP.js} +1 -1
- package/dist/kumaDriftDetector-OESI5GTC.js +237 -0
- package/dist/kumaGotchas-DU5H6N7X.js +151 -0
- package/dist/{kumaGraph-35TAIBWD.js → kumaGraph-AWP743TJ.js} +2 -2
- package/dist/{kumaMemory-5SR3TGRL.js → kumaMemory-7TCUDVZX.js} +2 -2
- package/dist/{kumaMiner-BIDSZE3Q.js → kumaMiner-FFXSRHAO.js} +5 -5
- package/dist/kumaPolicyEngine-S2PXN3C7.js +311 -0
- package/dist/kumaSearch-QGB4QVXS.js +321 -0
- package/dist/{kumaVerifier-B4D7NOFT.js → kumaVerifier-ARSGPZAM.js} +27 -4
- package/dist/kumaVisualize-TECABHUY.js +248 -0
- package/dist/safetyAudit-55IVCG5B.js +12 -0
- package/dist/{safetyScore-PJGRRBWP.js → safetyScore-GR5N55CU.js} +5 -5
- package/dist/{sessionMemory-HBBXUSIP.js → sessionMemory-CA34TCPF.js} +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sessionMemory
|
|
3
|
+
} from "./chunk-SLRRDKQ2.js";
|
|
4
|
+
import {
|
|
5
|
+
getProjectRoot
|
|
6
|
+
} from "./chunk-E2KFPEBT.js";
|
|
7
|
+
|
|
8
|
+
// src/engine/kumaPolicyEngine.ts
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
var DEFAULT_POLICY_CONFIG = {
|
|
12
|
+
version: 1,
|
|
13
|
+
name: "Kuma Default Security Policy",
|
|
14
|
+
description: "Default safety policy for AI coding agents",
|
|
15
|
+
rules: [
|
|
16
|
+
{
|
|
17
|
+
id: "block-rm-rf",
|
|
18
|
+
description: "Block recursive force deletion",
|
|
19
|
+
severity: "error",
|
|
20
|
+
patterns: ["rm -rf", "rm -fr", "rm --recursive --force"],
|
|
21
|
+
action: "block",
|
|
22
|
+
requireOverride: true
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "block-git-force-push",
|
|
26
|
+
description: "Block force push to git",
|
|
27
|
+
severity: "error",
|
|
28
|
+
patterns: ["git push --force", "git push -f"],
|
|
29
|
+
action: "block",
|
|
30
|
+
requireOverride: true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "block-npm-publish",
|
|
34
|
+
description: "Block package publishing",
|
|
35
|
+
severity: "error",
|
|
36
|
+
patterns: ["npm publish", "yarn publish", "pnpm publish"],
|
|
37
|
+
action: "block",
|
|
38
|
+
requireOverride: true
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "block-pipe-to-shell",
|
|
42
|
+
description: "Block curl/wget pipe to shell",
|
|
43
|
+
severity: "error",
|
|
44
|
+
patterns: ["curl | bash", "curl | sh", "wget -O - | bash", "curl | sudo"],
|
|
45
|
+
action: "block",
|
|
46
|
+
requireOverride: true
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "warn-destructive-db",
|
|
50
|
+
description: "Warn about destructive database operations",
|
|
51
|
+
severity: "warning",
|
|
52
|
+
patterns: ["DROP DATABASE", "DROP TABLE", "TRUNCATE", "DELETE FROM"],
|
|
53
|
+
action: "warn"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "block-prod-deploy",
|
|
57
|
+
description: "Block production deployments without override",
|
|
58
|
+
severity: "error",
|
|
59
|
+
patterns: ["deploy --production", "deploy --prod", "deploy:production"],
|
|
60
|
+
action: "block",
|
|
61
|
+
requireOverride: true
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
block_commands: [
|
|
65
|
+
"rm -rf",
|
|
66
|
+
"rm -fr",
|
|
67
|
+
"git push --force",
|
|
68
|
+
"git push -f",
|
|
69
|
+
"npm publish",
|
|
70
|
+
"yarn publish",
|
|
71
|
+
"pnpm publish",
|
|
72
|
+
"curl | bash",
|
|
73
|
+
"curl | sh"
|
|
74
|
+
],
|
|
75
|
+
never_touch: [
|
|
76
|
+
".env",
|
|
77
|
+
".env.local",
|
|
78
|
+
".env.production",
|
|
79
|
+
".env.development",
|
|
80
|
+
"package-lock.json",
|
|
81
|
+
"yarn.lock",
|
|
82
|
+
"pnpm-lock.yaml",
|
|
83
|
+
"node_modules/**"
|
|
84
|
+
]
|
|
85
|
+
};
|
|
86
|
+
function loadPolicyConfig() {
|
|
87
|
+
try {
|
|
88
|
+
const root = getProjectRoot();
|
|
89
|
+
const policyPath = path.join(root, ".kuma", "POLICY.json");
|
|
90
|
+
if (fs.existsSync(policyPath)) {
|
|
91
|
+
const content = fs.readFileSync(policyPath, "utf-8");
|
|
92
|
+
const config = JSON.parse(content);
|
|
93
|
+
return { ...DEFAULT_POLICY_CONFIG, ...config };
|
|
94
|
+
}
|
|
95
|
+
const ymlPath = path.join(root, ".kuma", "policy.yml");
|
|
96
|
+
if (fs.existsSync(ymlPath)) {
|
|
97
|
+
console.error("[PolicyEngine] Found legacy policy.yml \u2014 consider migrating to POLICY.json");
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error(`[PolicyEngine] Failed to load policy config: ${err}`);
|
|
101
|
+
}
|
|
102
|
+
return DEFAULT_POLICY_CONFIG;
|
|
103
|
+
}
|
|
104
|
+
function savePolicyConfig(config) {
|
|
105
|
+
try {
|
|
106
|
+
const root = getProjectRoot();
|
|
107
|
+
const policyPath = path.join(root, ".kuma", "POLICY.json");
|
|
108
|
+
const dir = path.dirname(policyPath);
|
|
109
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
110
|
+
fs.writeFileSync(policyPath, JSON.stringify(config, null, 2), "utf-8");
|
|
111
|
+
return `\u2705 Policy config saved to .kuma/POLICY.json (${config.rules.length} rules)`;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
return `\u274C Failed to save policy: ${err}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function evaluateCommand(command) {
|
|
117
|
+
const config = loadPolicyConfig();
|
|
118
|
+
const warnings = [];
|
|
119
|
+
const cmdLower = command.toLowerCase();
|
|
120
|
+
for (const rule of config.rules) {
|
|
121
|
+
const matches = rule.patterns.some((p) => cmdLower.includes(p.toLowerCase()));
|
|
122
|
+
if (!matches) continue;
|
|
123
|
+
if (rule.action === "block" || rule.severity === "error") {
|
|
124
|
+
return {
|
|
125
|
+
allowed: false,
|
|
126
|
+
blockedBy: rule,
|
|
127
|
+
warnings,
|
|
128
|
+
requiresOverride: rule.requireOverride ?? false,
|
|
129
|
+
message: `\u26D4 Policy violation: "${rule.description}" (rule: ${rule.id})`
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (rule.action === "warn") {
|
|
133
|
+
warnings.push(rule);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const cmd of config.block_commands || []) {
|
|
137
|
+
if (cmdLower.includes(cmd.toLowerCase())) {
|
|
138
|
+
return {
|
|
139
|
+
allowed: false,
|
|
140
|
+
blockedBy: {
|
|
141
|
+
id: "block-command",
|
|
142
|
+
description: `Blocked command: ${cmd}`,
|
|
143
|
+
severity: "error",
|
|
144
|
+
patterns: [cmd],
|
|
145
|
+
action: "block",
|
|
146
|
+
requireOverride: true
|
|
147
|
+
},
|
|
148
|
+
warnings,
|
|
149
|
+
requiresOverride: true,
|
|
150
|
+
message: `\u26D4 Command matches blocked pattern: "${cmd}"`
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
allowed: true,
|
|
156
|
+
blockedBy: null,
|
|
157
|
+
warnings,
|
|
158
|
+
requiresOverride: false,
|
|
159
|
+
message: warnings.length > 0 ? `\u26A0\uFE0F Command allowed with ${warnings.length} warning(s)` : "\u2705 Command allowed by policy"
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function evaluateFilePath(filePath) {
|
|
163
|
+
const config = loadPolicyConfig();
|
|
164
|
+
const filesToCheck = config.never_touch || DEFAULT_POLICY_CONFIG.never_touch || [];
|
|
165
|
+
for (const pattern of filesToCheck) {
|
|
166
|
+
if (matchesGlob(pattern, filePath)) {
|
|
167
|
+
return {
|
|
168
|
+
allowed: false,
|
|
169
|
+
blockedBy: {
|
|
170
|
+
id: "never-touch",
|
|
171
|
+
description: `File matches never_touch pattern: ${pattern}`,
|
|
172
|
+
severity: "error",
|
|
173
|
+
patterns: [pattern],
|
|
174
|
+
action: "block",
|
|
175
|
+
requireOverride: true
|
|
176
|
+
},
|
|
177
|
+
warnings: [],
|
|
178
|
+
requiresOverride: true,
|
|
179
|
+
message: `\u26D4 File "${filePath}" is protected by never_touch policy (pattern: ${pattern}). Use kuma_safety({ action: 'override' }) to bypass.`
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
allowed: true,
|
|
185
|
+
blockedBy: null,
|
|
186
|
+
warnings: [],
|
|
187
|
+
requiresOverride: false,
|
|
188
|
+
message: "\u2705 File allowed by policy"
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function evaluateDatabaseAction(action) {
|
|
192
|
+
const cmdLower = action.toLowerCase();
|
|
193
|
+
if ((cmdLower.includes("drop") || cmdLower.includes("truncate")) && (cmdLower.includes("database") || cmdLower.includes("table") || cmdLower.includes("schema"))) {
|
|
194
|
+
return {
|
|
195
|
+
allowed: false,
|
|
196
|
+
blockedBy: {
|
|
197
|
+
id: "block-destructive-db",
|
|
198
|
+
description: "Destructive database operation blocked",
|
|
199
|
+
severity: "error",
|
|
200
|
+
patterns: ["DROP DATABASE", "DROP TABLE", "TRUNCATE", "DELETE FROM"],
|
|
201
|
+
action: "block",
|
|
202
|
+
requireOverride: true
|
|
203
|
+
},
|
|
204
|
+
warnings: [],
|
|
205
|
+
requiresOverride: true,
|
|
206
|
+
message: `\u26D4 Destructive database action blocked: "${action}". Use kuma_safety({ action: 'override' }) with a clear reason.`
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
allowed: true,
|
|
211
|
+
blockedBy: null,
|
|
212
|
+
warnings: [],
|
|
213
|
+
requiresOverride: false,
|
|
214
|
+
message: "\u2705 Database action allowed by policy"
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
async function processOverride(request) {
|
|
218
|
+
const { recordAudit } = await import("./safetyAudit-55IVCG5B.js");
|
|
219
|
+
await recordAudit({
|
|
220
|
+
timestamp: Math.floor(Date.now() / 1e3),
|
|
221
|
+
toolName: request.toolName,
|
|
222
|
+
action: "policy_override",
|
|
223
|
+
filePath: request.filePath,
|
|
224
|
+
riskLevel: "high",
|
|
225
|
+
policyViolations: 1,
|
|
226
|
+
allowed: true,
|
|
227
|
+
durationMs: 0,
|
|
228
|
+
metadata: {
|
|
229
|
+
override: true,
|
|
230
|
+
reason: request.reason,
|
|
231
|
+
command: request.command
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
sessionMemory.recordToolCall("kuma_policy_override", {
|
|
235
|
+
toolName: request.toolName,
|
|
236
|
+
reason: request.reason,
|
|
237
|
+
filePath: request.filePath,
|
|
238
|
+
command: request.command
|
|
239
|
+
});
|
|
240
|
+
return [
|
|
241
|
+
`\u26A0\uFE0F **Policy Override** \u2014 ${request.toolName}`,
|
|
242
|
+
`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
|
|
243
|
+
"",
|
|
244
|
+
`\u{1F4DD} Reason: ${request.reason}`,
|
|
245
|
+
request.filePath ? `\u{1F4C4} File: ${request.filePath}` : "",
|
|
246
|
+
request.command ? `\u{1F4BB} Command: ${request.command}` : "",
|
|
247
|
+
"",
|
|
248
|
+
"\u{1F534} This override is recorded in the safety audit trail.",
|
|
249
|
+
"\u{1F534} Overrides reduce project safety \u2014 use sparingly."
|
|
250
|
+
].filter(Boolean).join("\n");
|
|
251
|
+
}
|
|
252
|
+
function formatPolicyStatus() {
|
|
253
|
+
const config = loadPolicyConfig();
|
|
254
|
+
const lines = [
|
|
255
|
+
"\u{1F4DC} **Policy-as-Code Engine**",
|
|
256
|
+
"\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
|
|
257
|
+
"",
|
|
258
|
+
`\u{1F4CB} Policy: ${config.name}`,
|
|
259
|
+
`\u{1F522} Version: ${config.version}`,
|
|
260
|
+
`\u{1F4DD} Rules: ${config.rules.length}`,
|
|
261
|
+
"",
|
|
262
|
+
"**Rules:**"
|
|
263
|
+
];
|
|
264
|
+
for (const rule of config.rules) {
|
|
265
|
+
const icon = rule.action === "block" ? "\u{1F534}" : rule.action === "warn" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
266
|
+
const override = rule.requireOverride ? " \u{1F511} override" : "";
|
|
267
|
+
lines.push(` ${icon} [${rule.action}] ${rule.description}${override}`);
|
|
268
|
+
lines.push(` Patterns: ${rule.patterns.slice(0, 3).join(", ")}`);
|
|
269
|
+
}
|
|
270
|
+
const neverTouch = config.never_touch || [];
|
|
271
|
+
if (neverTouch.length > 0) {
|
|
272
|
+
lines.push("", "**Protected Files (never_touch):**");
|
|
273
|
+
for (const p of neverTouch) {
|
|
274
|
+
lines.push(` \u{1F6E1}\uFE0F ${p}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return lines.join("\n");
|
|
278
|
+
}
|
|
279
|
+
function matchesGlob(pattern, filePath) {
|
|
280
|
+
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
281
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
282
|
+
let regexStr = "^";
|
|
283
|
+
for (let i = 0; i < normalizedPattern.length; i++) {
|
|
284
|
+
const ch = normalizedPattern[i];
|
|
285
|
+
if (ch === "*" && normalizedPattern[i + 1] === "*" && normalizedPattern[i + 2] === "/") {
|
|
286
|
+
regexStr += "(.+/)?";
|
|
287
|
+
i += 2;
|
|
288
|
+
} else if (ch === "*") {
|
|
289
|
+
regexStr += "[^/]*";
|
|
290
|
+
} else if (ch === "?") {
|
|
291
|
+
regexStr += "[^/]";
|
|
292
|
+
} else {
|
|
293
|
+
regexStr += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
regexStr += "$";
|
|
297
|
+
try {
|
|
298
|
+
return new RegExp(regexStr).test(normalizedPath);
|
|
299
|
+
} catch {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
export {
|
|
304
|
+
evaluateCommand,
|
|
305
|
+
evaluateDatabaseAction,
|
|
306
|
+
evaluateFilePath,
|
|
307
|
+
formatPolicyStatus,
|
|
308
|
+
loadPolicyConfig,
|
|
309
|
+
processOverride,
|
|
310
|
+
savePolicyConfig
|
|
311
|
+
};
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDb
|
|
3
|
+
} from "./chunk-NAM7SCBT.js";
|
|
4
|
+
import {
|
|
5
|
+
getProjectRoot
|
|
6
|
+
} from "./chunk-E2KFPEBT.js";
|
|
7
|
+
|
|
8
|
+
// src/engine/kumaSearch.ts
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
var SYNONYM_MAP = {
|
|
12
|
+
timeout: ["timeout", "duration", "expiry", "ttl", "deadline", "latency"],
|
|
13
|
+
duration: ["duration", "timeout", "length", "period", "interval"],
|
|
14
|
+
delay: ["delay", "wait", "latency", "defer", "throttle"],
|
|
15
|
+
latency: ["latency", "delay", "response time", "speed", "performance"],
|
|
16
|
+
error: ["error", "failure", "exception", "bug", "fault", "crash"],
|
|
17
|
+
crash: ["crash", "panic", "oom", "out of memory", "segfault"],
|
|
18
|
+
fail: ["fail", "error", "break", "regression", "broken"],
|
|
19
|
+
auth: ["auth", "authentication", "login", "oauth", "session", "token", "credential"],
|
|
20
|
+
login: ["login", "signin", "auth", "authenticate"],
|
|
21
|
+
token: ["token", "jwt", "session", "api key", "secret"],
|
|
22
|
+
permission: ["permission", "access", "role", "rbac", "authorization", "acl"],
|
|
23
|
+
database: ["database", "db", "storage", "persistence", "sql", "nosql"],
|
|
24
|
+
query: ["query", "search", "fetch", "select", "lookup"],
|
|
25
|
+
index: ["index", "key", "lookup", "search"],
|
|
26
|
+
cache: ["cache", "memcached", "redis", "buffer", "temporary storage"],
|
|
27
|
+
api: ["api", "endpoint", "route", "rest", "graphql", "service"],
|
|
28
|
+
route: ["route", "endpoint", "path", "handler"],
|
|
29
|
+
request: ["request", "http", "call", "fetch", "invocation"],
|
|
30
|
+
response: ["response", "reply", "result", "output"],
|
|
31
|
+
performance: ["performance", "speed", "latency", "throughput", "efficiency"],
|
|
32
|
+
memory: ["memory", "ram", "heap", "allocation", "leak"],
|
|
33
|
+
cpu: ["cpu", "processor", "compute", "thread", "worker"],
|
|
34
|
+
architecture: ["architecture", "design", "pattern", "structure", "system"],
|
|
35
|
+
service: ["service", "microservice", "module", "component", "layer"],
|
|
36
|
+
config: ["config", "configuration", "setting", "env", "environment"],
|
|
37
|
+
deploy: ["deploy", "release", "rollout", "publish", "ship"],
|
|
38
|
+
build: ["build", "compile", "bundle", "transpile", "package"],
|
|
39
|
+
test: ["test", "spec", "unit test", "integration", "e2e"],
|
|
40
|
+
lint: ["lint", "format", "style", "prettier", "eslint"],
|
|
41
|
+
redis: ["redis", "cache", "session store", "message queue"],
|
|
42
|
+
postgres: ["postgres", "postgresql", "psql", "relational db", "sql"],
|
|
43
|
+
docker: ["docker", "container", "image", "dockerfile", "compose"],
|
|
44
|
+
typescript: ["typescript", "ts", "type system", "compiler"],
|
|
45
|
+
prisma: ["prisma", "orm", "database client", "schema"],
|
|
46
|
+
create: ["create", "add", "new", "make", "generate"],
|
|
47
|
+
update: ["update", "modify", "change", "edit", "patch"],
|
|
48
|
+
delete: ["delete", "remove", "drop", "clear", "purge"]
|
|
49
|
+
};
|
|
50
|
+
function expandQueryTerms(query) {
|
|
51
|
+
const terms = query.toLowerCase().split(/[\s_-]+/).filter((t) => t.length > 2);
|
|
52
|
+
const expanded = /* @__PURE__ */ new Map();
|
|
53
|
+
for (const term of terms) {
|
|
54
|
+
const sources = /* @__PURE__ */ new Set();
|
|
55
|
+
sources.add(term);
|
|
56
|
+
if (SYNONYM_MAP[term]) {
|
|
57
|
+
for (const syn of SYNONYM_MAP[term]) {
|
|
58
|
+
sources.add(syn);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const [key, synonyms] of Object.entries(SYNONYM_MAP)) {
|
|
62
|
+
if (synonyms.includes(term)) {
|
|
63
|
+
sources.add(key);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
expanded.set(term, sources);
|
|
67
|
+
}
|
|
68
|
+
return expanded;
|
|
69
|
+
}
|
|
70
|
+
var _vectorCache = null;
|
|
71
|
+
var VECTOR_CACHE_TTL = 3e5;
|
|
72
|
+
async function buildSearchVectors() {
|
|
73
|
+
if (_vectorCache && Date.now() - _vectorCache.builtAt < VECTOR_CACHE_TTL) {
|
|
74
|
+
return _vectorCache.vectors;
|
|
75
|
+
}
|
|
76
|
+
const vectors = [];
|
|
77
|
+
try {
|
|
78
|
+
const db = await getDb();
|
|
79
|
+
const stmt = db.prepare("SELECT id, name, metadata, file_path FROM nodes ORDER BY updated_at DESC LIMIT 500");
|
|
80
|
+
while (stmt.step()) {
|
|
81
|
+
const row = stmt.getAsObject();
|
|
82
|
+
const name = row.name || "";
|
|
83
|
+
const metaStr = row.metadata || "{}";
|
|
84
|
+
let metaText = "";
|
|
85
|
+
try {
|
|
86
|
+
const meta = JSON.parse(metaStr);
|
|
87
|
+
metaText = Object.values(meta).join(" ");
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
const filePath = row.file_path || "";
|
|
91
|
+
const nodeText = `${name} ${metaText} ${filePath}`.toLowerCase();
|
|
92
|
+
const rawTerms = extractTerms(nodeText);
|
|
93
|
+
const termCounts = /* @__PURE__ */ new Map();
|
|
94
|
+
for (const t of rawTerms) {
|
|
95
|
+
termCounts.set(t, (termCounts.get(t) || 0) + 1);
|
|
96
|
+
}
|
|
97
|
+
vectors.push({
|
|
98
|
+
terms: termCounts,
|
|
99
|
+
source: name || row.id,
|
|
100
|
+
sourceType: "graph"
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
stmt.free();
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const root = getProjectRoot();
|
|
108
|
+
const memDir = path.join(root, ".kuma", "memories");
|
|
109
|
+
if (fs.existsSync(memDir)) {
|
|
110
|
+
const files = fs.readdirSync(memDir).filter((f) => f.endsWith(".md")).slice(0, 10);
|
|
111
|
+
for (const file of files) {
|
|
112
|
+
try {
|
|
113
|
+
const content = fs.readFileSync(path.join(memDir, file), "utf-8");
|
|
114
|
+
const text = content.toLowerCase();
|
|
115
|
+
const rawTerms = extractTerms(text);
|
|
116
|
+
const termCounts = /* @__PURE__ */ new Map();
|
|
117
|
+
for (const t of rawTerms) {
|
|
118
|
+
termCounts.set(t, (termCounts.get(t) || 0) + 1);
|
|
119
|
+
}
|
|
120
|
+
vectors.push({
|
|
121
|
+
terms: termCounts,
|
|
122
|
+
source: file.replace(/\.md$/, ""),
|
|
123
|
+
sourceType: "memory"
|
|
124
|
+
});
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const db = await getDb();
|
|
133
|
+
const stmt = db.prepare("SELECT scope, record FROM research_cache ORDER BY updated_at DESC LIMIT 50");
|
|
134
|
+
while (stmt.step()) {
|
|
135
|
+
const row = stmt.getAsObject();
|
|
136
|
+
const scope = row.scope || "";
|
|
137
|
+
const record = row.record || "";
|
|
138
|
+
const text = `${scope} ${record}`.toLowerCase();
|
|
139
|
+
const rawTerms = extractTerms(text);
|
|
140
|
+
const termCounts = /* @__PURE__ */ new Map();
|
|
141
|
+
for (const t of rawTerms) {
|
|
142
|
+
termCounts.set(t, (termCounts.get(t) || 0) + 1);
|
|
143
|
+
}
|
|
144
|
+
vectors.push({
|
|
145
|
+
terms: termCounts,
|
|
146
|
+
source: scope,
|
|
147
|
+
sourceType: "research"
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
stmt.free();
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
const docCount = vectors.length || 1;
|
|
154
|
+
const docFreq = /* @__PURE__ */ new Map();
|
|
155
|
+
for (const vec of vectors) {
|
|
156
|
+
const seen = /* @__PURE__ */ new Set();
|
|
157
|
+
for (const [term] of vec.terms) {
|
|
158
|
+
if (!seen.has(term)) {
|
|
159
|
+
docFreq.set(term, (docFreq.get(term) || 0) + 1);
|
|
160
|
+
seen.add(term);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const vec of vectors) {
|
|
165
|
+
const maxFreq = Math.max(1, ...Array.from(vec.terms.values()));
|
|
166
|
+
for (const [term, rawFreq] of vec.terms) {
|
|
167
|
+
const tf = 0.5 + 0.5 * rawFreq / maxFreq;
|
|
168
|
+
const docsWithTerm = docFreq.get(term) || 1;
|
|
169
|
+
const idf = Math.log(1 + (docCount - docsWithTerm + 0.5) / (docsWithTerm + 0.5));
|
|
170
|
+
vec.terms.set(term, tf * idf);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
_vectorCache = { vectors, builtAt: Date.now() };
|
|
174
|
+
return vectors;
|
|
175
|
+
}
|
|
176
|
+
function extractTerms(text) {
|
|
177
|
+
return text.toLowerCase().split(/[\s,.;:!?()\[\]{}"'\/\\|@#$%^&*+=<>~`]+/).filter((t) => t.length > 2 && t.length < 50 && !/^\d+$/.test(t));
|
|
178
|
+
}
|
|
179
|
+
function cosineSimilarity(queryVec, docVec) {
|
|
180
|
+
let dotProduct = 0;
|
|
181
|
+
let queryMagnitude = 0;
|
|
182
|
+
let docMagnitude = 0;
|
|
183
|
+
for (const [term, qWeight] of queryVec) {
|
|
184
|
+
queryMagnitude += qWeight * qWeight;
|
|
185
|
+
const dWeight = docVec.get(term) || 0;
|
|
186
|
+
dotProduct += qWeight * dWeight;
|
|
187
|
+
}
|
|
188
|
+
for (const [, dWeight] of docVec) {
|
|
189
|
+
docMagnitude += dWeight * dWeight;
|
|
190
|
+
}
|
|
191
|
+
const mag = Math.sqrt(queryMagnitude) * Math.sqrt(docMagnitude);
|
|
192
|
+
if (mag === 0) return 0;
|
|
193
|
+
return dotProduct / mag;
|
|
194
|
+
}
|
|
195
|
+
async function hybridSearch(query, limit = 10) {
|
|
196
|
+
const results = [];
|
|
197
|
+
const expandedTerms = expandQueryTerms(query);
|
|
198
|
+
const allSearchTerms = /* @__PURE__ */ new Set();
|
|
199
|
+
for (const [, sources] of expandedTerms) {
|
|
200
|
+
for (const term of sources) {
|
|
201
|
+
allSearchTerms.add(term);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const searchTerms = Array.from(allSearchTerms);
|
|
205
|
+
const queryVector = /* @__PURE__ */ new Map();
|
|
206
|
+
for (const [originalTerm, sources] of expandedTerms) {
|
|
207
|
+
for (const term of sources) {
|
|
208
|
+
const weight = term === originalTerm ? 2 : 1;
|
|
209
|
+
queryVector.set(term, (queryVector.get(term) || 0) + weight);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const db = await getDb();
|
|
214
|
+
const likeClauses = searchTerms.map(() => `name LIKE ?`).join(" OR ");
|
|
215
|
+
const sql = `SELECT id, name, type, file_path, metadata FROM nodes WHERE (${likeClauses}) LIMIT ${limit * 3}`;
|
|
216
|
+
const stmt = db.prepare(sql);
|
|
217
|
+
stmt.bind(searchTerms.map((t) => `%${t}%`));
|
|
218
|
+
const seen = /* @__PURE__ */ new Set();
|
|
219
|
+
while (stmt.step()) {
|
|
220
|
+
const row = stmt.getAsObject();
|
|
221
|
+
const name = row.name || "";
|
|
222
|
+
const filePath = row.file_path || "";
|
|
223
|
+
const sourceKey = name + filePath;
|
|
224
|
+
if (seen.has(sourceKey)) continue;
|
|
225
|
+
seen.add(sourceKey);
|
|
226
|
+
const lowerText = `${name} ${filePath}`.toLowerCase();
|
|
227
|
+
const matchedTerms = searchTerms.filter((t) => lowerText.includes(t));
|
|
228
|
+
const keywordScore = matchedTerms.length > 0 ? Math.round(matchedTerms.length / Math.max(1, searchTerms.length) * 100) : 0;
|
|
229
|
+
results.push({
|
|
230
|
+
source: name,
|
|
231
|
+
sourceType: "graph",
|
|
232
|
+
score: keywordScore,
|
|
233
|
+
keywordScore,
|
|
234
|
+
vectorScore: 0,
|
|
235
|
+
matchedTerms
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
stmt.free();
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.error(`[KumaSearch] DB search failed: ${err}`);
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const vectors = await buildSearchVectors();
|
|
244
|
+
for (const vec of vectors) {
|
|
245
|
+
const vectorScore = Math.round(cosineSimilarity(queryVector, vec.terms) * 100);
|
|
246
|
+
const existing = results.find((r) => r.source === vec.source);
|
|
247
|
+
if (existing) {
|
|
248
|
+
existing.vectorScore = vectorScore;
|
|
249
|
+
existing.score = Math.min(100, Math.round(existing.keywordScore * 0.6 + vectorScore * 0.4));
|
|
250
|
+
for (const [term] of vec.terms) {
|
|
251
|
+
if (queryVector.has(term) && !existing.matchedTerms.includes(term)) {
|
|
252
|
+
existing.matchedTerms.push(term);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} else if (vectorScore > 0) {
|
|
256
|
+
const matchedTerms = [];
|
|
257
|
+
for (const [term] of vec.terms) {
|
|
258
|
+
if (queryVector.has(term)) matchedTerms.push(term);
|
|
259
|
+
}
|
|
260
|
+
results.push({
|
|
261
|
+
source: vec.source,
|
|
262
|
+
sourceType: vec.sourceType,
|
|
263
|
+
score: Math.round(vectorScore * 0.4),
|
|
264
|
+
keywordScore: 0,
|
|
265
|
+
vectorScore,
|
|
266
|
+
matchedTerms
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} catch (err) {
|
|
271
|
+
console.error(`[KumaSearch] Vector search failed: ${err}`);
|
|
272
|
+
}
|
|
273
|
+
const uniqueResults = Array.from(
|
|
274
|
+
new Map(results.map((r) => [r.source, r])).values()
|
|
275
|
+
);
|
|
276
|
+
return uniqueResults.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
277
|
+
}
|
|
278
|
+
function formatHybridResults(query, results) {
|
|
279
|
+
const expandedTerms = expandQueryTerms(query);
|
|
280
|
+
const originalTerms = Array.from(expandedTerms.keys()).join(", ");
|
|
281
|
+
const totalSynonyms = Array.from(expandedTerms.values()).reduce(
|
|
282
|
+
(sum, s) => sum + s.size,
|
|
283
|
+
0
|
|
284
|
+
);
|
|
285
|
+
const lines = [
|
|
286
|
+
"**Hybrid Search** \u2014 " + query,
|
|
287
|
+
"----------------------------------------",
|
|
288
|
+
"",
|
|
289
|
+
"Expanded query: " + originalTerms + " (" + totalSynonyms + " total terms with synonyms)",
|
|
290
|
+
""
|
|
291
|
+
];
|
|
292
|
+
if (results.length === 0) {
|
|
293
|
+
lines.push("No results found. Try different keywords or research first.");
|
|
294
|
+
return lines.join("\n");
|
|
295
|
+
}
|
|
296
|
+
lines.push(results.length + " result(s) \u2014 ranked by hybrid relevance");
|
|
297
|
+
lines.push("");
|
|
298
|
+
for (let i = 0; i < results.length; i++) {
|
|
299
|
+
const r = results[i];
|
|
300
|
+
const typeIcon = r.sourceType === "graph" ? "file" : r.sourceType === "memory" ? "memory" : "research";
|
|
301
|
+
const matchType = r.vectorScore > r.keywordScore ? "semantic match" : r.keywordScore > 0 ? "keyword match" : "expanded match";
|
|
302
|
+
lines.push(i + 1 + ". [" + typeIcon + "] " + r.source + " \u2014 " + r.score + "%");
|
|
303
|
+
lines.push(" keyword: " + r.keywordScore + "% | vector: " + r.vectorScore + "% (" + matchType + ")");
|
|
304
|
+
if (r.matchedTerms.length > 0) {
|
|
305
|
+
lines.push(" Terms: " + r.matchedTerms.slice(0, 6).join(", "));
|
|
306
|
+
}
|
|
307
|
+
lines.push("");
|
|
308
|
+
}
|
|
309
|
+
lines.push("Hybrid search combines keyword matching with semantic synonym expansion.");
|
|
310
|
+
return lines.join("\n");
|
|
311
|
+
}
|
|
312
|
+
function clearSearchCache() {
|
|
313
|
+
_vectorCache = null;
|
|
314
|
+
}
|
|
315
|
+
export {
|
|
316
|
+
buildSearchVectors,
|
|
317
|
+
clearSearchCache,
|
|
318
|
+
expandQueryTerms,
|
|
319
|
+
formatHybridResults,
|
|
320
|
+
hybridSearch
|
|
321
|
+
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sessionMemory
|
|
3
|
+
} from "./chunk-SLRRDKQ2.js";
|
|
1
4
|
import {
|
|
2
5
|
getLatestVerifications,
|
|
3
6
|
saveVerification
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import {
|
|
6
|
-
sessionMemory
|
|
7
|
-
} from "./chunk-BI7KD3SG.js";
|
|
7
|
+
} from "./chunk-NAM7SCBT.js";
|
|
8
8
|
import "./chunk-E2KFPEBT.js";
|
|
9
9
|
|
|
10
10
|
// src/engine/kumaVerifier.ts
|
|
@@ -56,6 +56,24 @@ var _localRunning = false;
|
|
|
56
56
|
var _currentProcess = null;
|
|
57
57
|
var STALE_RESULT_MS = 3e5;
|
|
58
58
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
59
|
+
var RUNAWAY_WINDOW_MS = 3e5;
|
|
60
|
+
var RUNAWAY_MAX_CALLS = 3;
|
|
61
|
+
var _verifyCallTimestamps = [];
|
|
62
|
+
function checkRunaway() {
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
while (_verifyCallTimestamps.length > 0 && _verifyCallTimestamps[0] < now - RUNAWAY_WINDOW_MS) {
|
|
65
|
+
_verifyCallTimestamps.shift();
|
|
66
|
+
}
|
|
67
|
+
if (_verifyCallTimestamps.length >= RUNAWAY_MAX_CALLS) {
|
|
68
|
+
const oldestInWindow = _verifyCallTimestamps[0];
|
|
69
|
+
const waitMs = RUNAWAY_WINDOW_MS - (now - oldestInWindow);
|
|
70
|
+
const waitSec = Math.ceil(waitMs / 1e3);
|
|
71
|
+
return `\u26D4 **Runaway protection active** \u2014 ${RUNAWAY_MAX_CALLS}+ verify calls in the last 5 minutes. Please wait ${waitSec}s before trying again.
|
|
72
|
+
\u{1F4A1} This prevents resource exhaustion (CPU/RAM) from uncontrolled test spawning.`;
|
|
73
|
+
}
|
|
74
|
+
_verifyCallTimestamps.push(now);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
59
77
|
function getRunningVerificationPid() {
|
|
60
78
|
return _currentProcess?.pid ?? null;
|
|
61
79
|
}
|
|
@@ -126,6 +144,11 @@ async function runAutoVerification(options = {}) {
|
|
|
126
144
|
const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
127
145
|
const denial = checkAllowed(root);
|
|
128
146
|
if (denial) return denial;
|
|
147
|
+
const runawayBlock = checkRunaway();
|
|
148
|
+
if (runawayBlock) {
|
|
149
|
+
releaseFileLock(root);
|
|
150
|
+
return runawayBlock;
|
|
151
|
+
}
|
|
129
152
|
try {
|
|
130
153
|
if (!options.force) {
|
|
131
154
|
const cached = await checkStaleness(scope);
|