@triedotdev/mcp 1.0.94 → 1.0.97
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 +145 -137
- package/dist/{chunk-JAAIHNOE.js → chunk-APMV77PU.js} +21 -6
- package/dist/chunk-APMV77PU.js.map +1 -0
- package/dist/{chunk-HLSBTOVE.js → chunk-B3MNN3XB.js} +13 -18
- package/dist/{chunk-HLSBTOVE.js.map → chunk-B3MNN3XB.js.map} +1 -1
- package/dist/{chunk-JO6RVXS6.js → chunk-F4NJ4CBP.js} +2 -2
- package/dist/{chunk-AZRCKBGF.js → chunk-FNCCZ3XB.js} +1222 -75
- package/dist/chunk-FNCCZ3XB.js.map +1 -0
- package/dist/chunk-G76DYVGX.js +136 -0
- package/dist/chunk-G76DYVGX.js.map +1 -0
- package/dist/chunk-HSNE46VE.js +956 -0
- package/dist/chunk-HSNE46VE.js.map +1 -0
- package/dist/{chunk-STEFLYPR.js → chunk-IXO4G4D3.js} +2 -2
- package/dist/{chunk-OEYIOOYB.js → chunk-JDHR5BDR.js} +2 -3
- package/dist/chunk-NIASHOAB.js +1304 -0
- package/dist/chunk-NIASHOAB.js.map +1 -0
- package/dist/{chunk-CKM6A3G6.js → chunk-OVRG5RP3.js} +6 -7
- package/dist/chunk-OVRG5RP3.js.map +1 -0
- package/dist/{chunk-RYRVEO2B.js → chunk-R3I2GCZC.js} +3 -3
- package/dist/{chunk-WT3XQCG2.js → chunk-R4AAPFXC.js} +2 -2
- package/dist/{chunk-IIF5XDCJ.js → chunk-SLL2MDJD.js} +786 -4694
- package/dist/chunk-SLL2MDJD.js.map +1 -0
- package/dist/cli/create-agent.js +931 -7
- package/dist/cli/create-agent.js.map +1 -1
- package/dist/cli/main.js +151 -383
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/yolo-daemon.js +13 -20
- package/dist/cli/yolo-daemon.js.map +1 -1
- package/dist/{goal-manager-HOZ7R2QV.js → goal-manager-LAOT4QQX.js} +6 -6
- package/dist/guardian-agent-M352CBE5.js +19 -0
- package/dist/index.js +1025 -1550
- package/dist/index.js.map +1 -1
- package/dist/{issue-store-DXIOP6AK.js → issue-store-W2X33X2X.js} +4 -4
- package/dist/{progress-LHI66U7B.js → progress-PQVEM7BR.js} +2 -2
- package/dist/{vibe-code-signatures-C5A4BHXD.js → vibe-code-signatures-ELEWJFGZ.js} +3 -3
- package/dist/{vulnerability-signatures-SVIHJQO5.js → vulnerability-signatures-EIJQX2TS.js} +3 -3
- package/dist/workers/agent-worker.js +2 -11
- package/dist/workers/agent-worker.js.map +1 -1
- package/package.json +2 -2
- package/dist/agent-smith-MYQ35URL.js +0 -14
- package/dist/agent-smith-runner-4TBONXCP.js +0 -573
- package/dist/agent-smith-runner-4TBONXCP.js.map +0 -1
- package/dist/cache-manager-RMPRPD5T.js +0 -10
- package/dist/chunk-AZRCKBGF.js.map +0 -1
- package/dist/chunk-CKM6A3G6.js.map +0 -1
- package/dist/chunk-E2ZATINO.js +0 -10879
- package/dist/chunk-E2ZATINO.js.map +0 -1
- package/dist/chunk-FFWNZUG2.js +0 -266
- package/dist/chunk-FFWNZUG2.js.map +0 -1
- package/dist/chunk-FK6DQKDY.js +0 -175
- package/dist/chunk-FK6DQKDY.js.map +0 -1
- package/dist/chunk-IFGF33R5.js +0 -279
- package/dist/chunk-IFGF33R5.js.map +0 -1
- package/dist/chunk-IIF5XDCJ.js.map +0 -1
- package/dist/chunk-JAAIHNOE.js.map +0 -1
- package/dist/chunk-ODWDESYP.js +0 -141
- package/dist/chunk-ODWDESYP.js.map +0 -1
- package/dist/chunk-OWBWNXSC.js +0 -955
- package/dist/chunk-OWBWNXSC.js.map +0 -1
- package/dist/chunk-Q764X2WD.js +0 -2124
- package/dist/chunk-Q764X2WD.js.map +0 -1
- package/dist/chunk-RE6ZWXJC.js +0 -279
- package/dist/chunk-RE6ZWXJC.js.map +0 -1
- package/dist/chunk-RNJ6JKMA.js +0 -2270
- package/dist/chunk-RNJ6JKMA.js.map +0 -1
- package/dist/chunk-Y62VM3ER.js +0 -536
- package/dist/chunk-Y62VM3ER.js.map +0 -1
- package/dist/git-45LZUUYA.js +0 -29
- package/dist/guardian-agent-RB2UQP5V.js +0 -21
- package/dist/progress-LHI66U7B.js.map +0 -1
- package/dist/vibe-code-signatures-C5A4BHXD.js.map +0 -1
- package/dist/vulnerability-signatures-SVIHJQO5.js.map +0 -1
- /package/dist/{chunk-JO6RVXS6.js.map → chunk-F4NJ4CBP.js.map} +0 -0
- /package/dist/{chunk-STEFLYPR.js.map → chunk-IXO4G4D3.js.map} +0 -0
- /package/dist/{chunk-OEYIOOYB.js.map → chunk-JDHR5BDR.js.map} +0 -0
- /package/dist/{chunk-RYRVEO2B.js.map → chunk-R3I2GCZC.js.map} +0 -0
- /package/dist/{chunk-WT3XQCG2.js.map → chunk-R4AAPFXC.js.map} +0 -0
- /package/dist/{agent-smith-MYQ35URL.js.map → goal-manager-LAOT4QQX.js.map} +0 -0
- /package/dist/{cache-manager-RMPRPD5T.js.map → guardian-agent-M352CBE5.js.map} +0 -0
- /package/dist/{git-45LZUUYA.js.map → issue-store-W2X33X2X.js.map} +0 -0
- /package/dist/{goal-manager-HOZ7R2QV.js.map → progress-PQVEM7BR.js.map} +0 -0
- /package/dist/{guardian-agent-RB2UQP5V.js.map → vibe-code-signatures-ELEWJFGZ.js.map} +0 -0
- /package/dist/{issue-store-DXIOP6AK.js.map → vulnerability-signatures-EIJQX2TS.js.map} +0 -0
|
@@ -0,0 +1,1304 @@
|
|
|
1
|
+
import {
|
|
2
|
+
scanForVulnerabilities
|
|
3
|
+
} from "./chunk-F4NJ4CBP.js";
|
|
4
|
+
import {
|
|
5
|
+
scanForVibeCodeIssues
|
|
6
|
+
} from "./chunk-IXO4G4D3.js";
|
|
7
|
+
import {
|
|
8
|
+
BackupManager,
|
|
9
|
+
GlobalPatternsIndexSchema,
|
|
10
|
+
atomicWriteJSON,
|
|
11
|
+
safeParseAndValidate,
|
|
12
|
+
searchIssues
|
|
13
|
+
} from "./chunk-JDHR5BDR.js";
|
|
14
|
+
import {
|
|
15
|
+
getTrieDirectory,
|
|
16
|
+
getWorkingDirectory
|
|
17
|
+
} from "./chunk-R4AAPFXC.js";
|
|
18
|
+
|
|
19
|
+
// src/memory/global-memory.ts
|
|
20
|
+
import { mkdir, writeFile, readFile, readdir } from "fs/promises";
|
|
21
|
+
import { createHash } from "crypto";
|
|
22
|
+
import { existsSync } from "fs";
|
|
23
|
+
import { join } from "path";
|
|
24
|
+
import { homedir } from "os";
|
|
25
|
+
var GLOBAL_TRIE_DIR = join(homedir(), ".trie");
|
|
26
|
+
var GLOBAL_MEMORY_DIR = join(GLOBAL_TRIE_DIR, "memory");
|
|
27
|
+
async function recordToGlobalMemory(issues, projectName, projectPath, healthScore = 0) {
|
|
28
|
+
await mkdir(GLOBAL_MEMORY_DIR, { recursive: true });
|
|
29
|
+
await mkdir(join(GLOBAL_MEMORY_DIR, "projects"), { recursive: true });
|
|
30
|
+
const patterns = await loadGlobalPatterns();
|
|
31
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
32
|
+
for (const issue of issues) {
|
|
33
|
+
const patternId = extractPatternId(issue);
|
|
34
|
+
const existing = patterns.find((p) => p.id === patternId);
|
|
35
|
+
if (existing) {
|
|
36
|
+
existing.occurrences++;
|
|
37
|
+
existing.lastSeen = now;
|
|
38
|
+
if (!existing.projects.includes(projectName)) {
|
|
39
|
+
existing.projects.push(projectName);
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
patterns.push({
|
|
43
|
+
id: patternId,
|
|
44
|
+
pattern: issue.issue.slice(0, 200),
|
|
45
|
+
description: issue.fix.slice(0, 200),
|
|
46
|
+
severity: issue.severity,
|
|
47
|
+
agent: issue.agent,
|
|
48
|
+
occurrences: 1,
|
|
49
|
+
projects: [projectName],
|
|
50
|
+
firstSeen: now,
|
|
51
|
+
lastSeen: now
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
await saveGlobalPatterns(patterns);
|
|
56
|
+
const summaryPath = join(GLOBAL_MEMORY_DIR, "projects", `${sanitizeName(projectName)}.json`);
|
|
57
|
+
const summary = {
|
|
58
|
+
name: projectName,
|
|
59
|
+
path: projectPath,
|
|
60
|
+
lastScan: now,
|
|
61
|
+
healthScore,
|
|
62
|
+
totalIssues: issues.length,
|
|
63
|
+
patterns: [...new Set(issues.map((i) => extractPatternId(i)))]
|
|
64
|
+
};
|
|
65
|
+
await atomicWriteJSON(summaryPath, summary);
|
|
66
|
+
}
|
|
67
|
+
async function findCrossProjectPatterns(minOccurrences = 2) {
|
|
68
|
+
const patterns = await loadGlobalPatterns();
|
|
69
|
+
return patterns.filter((p) => p.projects.length >= minOccurrences).sort((a, b) => b.occurrences - a.occurrences);
|
|
70
|
+
}
|
|
71
|
+
async function listTrackedProjects() {
|
|
72
|
+
const projectsDir = join(GLOBAL_MEMORY_DIR, "projects");
|
|
73
|
+
try {
|
|
74
|
+
if (!existsSync(projectsDir)) return [];
|
|
75
|
+
const files = await readdir(projectsDir);
|
|
76
|
+
const summaries = [];
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
if (!file.endsWith(".json")) continue;
|
|
79
|
+
try {
|
|
80
|
+
const content = await readFile(join(projectsDir, file), "utf-8");
|
|
81
|
+
summaries.push(JSON.parse(content));
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return summaries.sort(
|
|
86
|
+
(a, b) => new Date(b.lastScan).getTime() - new Date(a.lastScan).getTime()
|
|
87
|
+
);
|
|
88
|
+
} catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function getGlobalMemoryStats() {
|
|
93
|
+
const patterns = await loadGlobalPatterns();
|
|
94
|
+
const projects = await listTrackedProjects();
|
|
95
|
+
const MAX_PATTERNS = 500;
|
|
96
|
+
const patternsByAgent = {};
|
|
97
|
+
for (const pattern of patterns) {
|
|
98
|
+
patternsByAgent[pattern.agent] = (patternsByAgent[pattern.agent] || 0) + 1;
|
|
99
|
+
}
|
|
100
|
+
const totalOccurrences = patterns.reduce((sum, p) => sum + p.occurrences, 0);
|
|
101
|
+
const avgOccurrences = patterns.length > 0 ? totalOccurrences / patterns.length : 0;
|
|
102
|
+
return {
|
|
103
|
+
totalPatterns: patterns.length,
|
|
104
|
+
crossProjectPatterns: patterns.filter((p) => p.projects.length >= 2).length,
|
|
105
|
+
trackedProjects: projects.length,
|
|
106
|
+
totalOccurrences,
|
|
107
|
+
fixedPatterns: patterns.filter((p) => p.fixApplied).length,
|
|
108
|
+
patternsByAgent,
|
|
109
|
+
capacityInfo: {
|
|
110
|
+
current: patterns.length,
|
|
111
|
+
max: MAX_PATTERNS,
|
|
112
|
+
percentFull: Math.round(patterns.length / MAX_PATTERNS * 100),
|
|
113
|
+
isAtCap: patterns.length >= MAX_PATTERNS
|
|
114
|
+
},
|
|
115
|
+
deduplicationStats: {
|
|
116
|
+
uniquePatterns: patterns.length,
|
|
117
|
+
averageOccurrences: Math.round(avgOccurrences * 10) / 10
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async function updateGlobalMemoryMd() {
|
|
122
|
+
const patterns = await loadGlobalPatterns();
|
|
123
|
+
const crossProject = patterns.filter((p) => p.projects.length >= 2);
|
|
124
|
+
const projects = await listTrackedProjects();
|
|
125
|
+
const lines = [
|
|
126
|
+
"# Global Trie Memory",
|
|
127
|
+
"",
|
|
128
|
+
"> Auto-generated file tracking patterns across all your projects.",
|
|
129
|
+
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
130
|
+
"",
|
|
131
|
+
"## Summary",
|
|
132
|
+
"",
|
|
133
|
+
`- **Projects tracked:** ${projects.length}`,
|
|
134
|
+
`- **Total patterns:** ${patterns.length}`,
|
|
135
|
+
`- **Cross-project patterns:** ${crossProject.length}`,
|
|
136
|
+
"",
|
|
137
|
+
"## Cross-Project Patterns",
|
|
138
|
+
"",
|
|
139
|
+
"These issues appear in multiple projects:",
|
|
140
|
+
""
|
|
141
|
+
];
|
|
142
|
+
for (const p of crossProject.slice(0, 20)) {
|
|
143
|
+
lines.push(
|
|
144
|
+
`### ${p.pattern.slice(0, 60)}${p.pattern.length > 60 ? "..." : ""}`,
|
|
145
|
+
"",
|
|
146
|
+
`- **Severity:** ${p.severity}`,
|
|
147
|
+
`- **Agent:** ${p.agent}`,
|
|
148
|
+
`- **Occurrences:** ${p.occurrences} across ${p.projects.length} projects`,
|
|
149
|
+
`- **Projects:** ${p.projects.slice(0, 5).join(", ")}${p.projects.length > 5 ? "..." : ""}`
|
|
150
|
+
);
|
|
151
|
+
if (p.fixApplied) {
|
|
152
|
+
lines.push(`- **Fixed in:** ${p.fixApplied.project} on ${p.fixApplied.timestamp.split("T")[0]}`);
|
|
153
|
+
} else {
|
|
154
|
+
lines.push("- **Status:** Not fixed");
|
|
155
|
+
}
|
|
156
|
+
lines.push("");
|
|
157
|
+
}
|
|
158
|
+
lines.push(
|
|
159
|
+
"## Tracked Projects",
|
|
160
|
+
"",
|
|
161
|
+
"| Project | Last Scan | Health | Issues |",
|
|
162
|
+
"|---------|-----------|--------|--------|"
|
|
163
|
+
);
|
|
164
|
+
for (const p of projects.slice(0, 20)) {
|
|
165
|
+
lines.push(`| ${p.name} | ${p.lastScan.split("T")[0]} | ${p.healthScore}% | ${p.totalIssues} |`);
|
|
166
|
+
}
|
|
167
|
+
lines.push("", "---", "", "*This file is auto-generated by Trie. Do not edit manually.*");
|
|
168
|
+
await mkdir(GLOBAL_MEMORY_DIR, { recursive: true });
|
|
169
|
+
await writeFile(join(GLOBAL_MEMORY_DIR, "GLOBAL_MEMORY.md"), lines.join("\n"));
|
|
170
|
+
}
|
|
171
|
+
async function searchGlobalPatterns(query, options = {}) {
|
|
172
|
+
const patterns = await loadGlobalPatterns();
|
|
173
|
+
const limit = options.limit || 10;
|
|
174
|
+
const queryTerms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 2);
|
|
175
|
+
const scored = patterns.filter((p) => {
|
|
176
|
+
if (options.severity && !options.severity.includes(p.severity)) return false;
|
|
177
|
+
if (options.agent && p.agent !== options.agent) return false;
|
|
178
|
+
return true;
|
|
179
|
+
}).map((p) => {
|
|
180
|
+
const text = `${p.pattern} ${p.description} ${p.agent}`.toLowerCase();
|
|
181
|
+
let score = 0;
|
|
182
|
+
for (const term of queryTerms) {
|
|
183
|
+
if (text.includes(term)) score++;
|
|
184
|
+
}
|
|
185
|
+
return { pattern: p, score };
|
|
186
|
+
}).filter((s) => s.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
187
|
+
return scored.map((s) => s.pattern);
|
|
188
|
+
}
|
|
189
|
+
async function loadGlobalPatterns() {
|
|
190
|
+
const patternsPath = join(GLOBAL_MEMORY_DIR, "global-patterns.json");
|
|
191
|
+
try {
|
|
192
|
+
if (existsSync(patternsPath)) {
|
|
193
|
+
const content = await readFile(patternsPath, "utf-8");
|
|
194
|
+
const result = safeParseAndValidate(content, GlobalPatternsIndexSchema);
|
|
195
|
+
if (result.success) {
|
|
196
|
+
return result.data;
|
|
197
|
+
}
|
|
198
|
+
const backupManager = new BackupManager(patternsPath);
|
|
199
|
+
if (await backupManager.recoverFromBackup()) {
|
|
200
|
+
const recovered = await readFile(patternsPath, "utf-8");
|
|
201
|
+
const recoveredResult = safeParseAndValidate(recovered, GlobalPatternsIndexSchema);
|
|
202
|
+
if (recoveredResult.success) {
|
|
203
|
+
return recoveredResult.data;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
}
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
async function saveGlobalPatterns(patterns) {
|
|
212
|
+
await mkdir(GLOBAL_MEMORY_DIR, { recursive: true });
|
|
213
|
+
const patternsPath = join(GLOBAL_MEMORY_DIR, "global-patterns.json");
|
|
214
|
+
const patternMap = /* @__PURE__ */ new Map();
|
|
215
|
+
for (const pattern of patterns) {
|
|
216
|
+
const existing = patternMap.get(pattern.id);
|
|
217
|
+
if (existing) {
|
|
218
|
+
existing.occurrences += pattern.occurrences;
|
|
219
|
+
for (const proj of pattern.projects) {
|
|
220
|
+
if (!existing.projects.includes(proj)) {
|
|
221
|
+
existing.projects.push(proj);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
existing.lastSeen = pattern.lastSeen > existing.lastSeen ? pattern.lastSeen : existing.lastSeen;
|
|
225
|
+
} else {
|
|
226
|
+
patternMap.set(pattern.id, pattern);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const deduplicated = Array.from(patternMap.values());
|
|
230
|
+
const pruned = intelligentPruneGlobalPatterns(deduplicated, 500);
|
|
231
|
+
const backupManager = new BackupManager(patternsPath);
|
|
232
|
+
await backupManager.createBackup();
|
|
233
|
+
await atomicWriteJSON(patternsPath, pruned);
|
|
234
|
+
}
|
|
235
|
+
function intelligentPruneGlobalPatterns(patterns, targetCount) {
|
|
236
|
+
if (patterns.length <= targetCount) {
|
|
237
|
+
return patterns;
|
|
238
|
+
}
|
|
239
|
+
const severityWeight = {
|
|
240
|
+
critical: 100,
|
|
241
|
+
high: 50,
|
|
242
|
+
moderate: 20,
|
|
243
|
+
low: 10,
|
|
244
|
+
info: 5
|
|
245
|
+
};
|
|
246
|
+
const scored = patterns.map((pattern) => {
|
|
247
|
+
const ageInDays = (Date.now() - new Date(pattern.lastSeen).getTime()) / (1e3 * 60 * 60 * 24);
|
|
248
|
+
const recencyScore = Math.max(0, 100 - ageInDays * 2);
|
|
249
|
+
const severityScore = severityWeight[pattern.severity] || 10;
|
|
250
|
+
const crossProjectBonus = (pattern.projects.length - 1) * 30;
|
|
251
|
+
const fixedBonus = pattern.fixApplied ? 20 : 0;
|
|
252
|
+
const occurrenceScore = Math.min(pattern.occurrences * 2, 100);
|
|
253
|
+
return {
|
|
254
|
+
pattern,
|
|
255
|
+
score: recencyScore + severityScore + crossProjectBonus + fixedBonus + occurrenceScore
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, targetCount).map((s) => s.pattern);
|
|
259
|
+
}
|
|
260
|
+
function extractPatternId(issue) {
|
|
261
|
+
const normalized = issue.issue.toLowerCase().replace(/`[^`]+`/g, "CODE").replace(/\b\d+\b/g, "N").replace(/['"]/g, "").slice(0, 100);
|
|
262
|
+
const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 12);
|
|
263
|
+
return `${issue.agent}-${issue.severity}-${hash}`;
|
|
264
|
+
}
|
|
265
|
+
function sanitizeName(name) {
|
|
266
|
+
return name.replace(/[^a-zA-Z0-9-_]/g, "-").toLowerCase();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/ai/client.ts
|
|
270
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
271
|
+
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
272
|
+
import { join as join2 } from "path";
|
|
273
|
+
var clientInstance = null;
|
|
274
|
+
var apiKeyChecked = false;
|
|
275
|
+
var apiKeyAvailable = false;
|
|
276
|
+
function isAIAvailable() {
|
|
277
|
+
if (!apiKeyChecked) {
|
|
278
|
+
checkAPIKey();
|
|
279
|
+
}
|
|
280
|
+
return apiKeyAvailable;
|
|
281
|
+
}
|
|
282
|
+
function checkAPIKey() {
|
|
283
|
+
apiKeyChecked = true;
|
|
284
|
+
const envApiKey = process.env.ANTHROPIC_API_KEY;
|
|
285
|
+
if (envApiKey && envApiKey.length > 10) {
|
|
286
|
+
apiKeyAvailable = true;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const workDir = getWorkingDirectory(void 0, true);
|
|
291
|
+
const configPath = join2(getTrieDirectory(workDir), "config.json");
|
|
292
|
+
if (existsSync2(configPath)) {
|
|
293
|
+
const configContent = readFileSync(configPath, "utf-8");
|
|
294
|
+
const config = JSON.parse(configContent);
|
|
295
|
+
if (config.apiKeys?.anthropic && config.apiKeys.anthropic.length > 10) {
|
|
296
|
+
process.env.ANTHROPIC_API_KEY = config.apiKeys.anthropic;
|
|
297
|
+
apiKeyAvailable = true;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const workDir = getWorkingDirectory(void 0, true);
|
|
305
|
+
const envFiles = [".env", ".env.local", ".env.production"];
|
|
306
|
+
for (const envFile of envFiles) {
|
|
307
|
+
const envPath = join2(workDir, envFile);
|
|
308
|
+
if (existsSync2(envPath)) {
|
|
309
|
+
const envContent = readFileSync(envPath, "utf-8");
|
|
310
|
+
const lines = envContent.split("\n");
|
|
311
|
+
for (const line of lines) {
|
|
312
|
+
const match = line.match(/^\s*ANTHROPIC_API_KEY\s*=\s*(.+)$/);
|
|
313
|
+
if (match && match[1]) {
|
|
314
|
+
const key = match[1].trim().replace(/^["']|["']$/g, "");
|
|
315
|
+
if (key.length > 10) {
|
|
316
|
+
process.env.ANTHROPIC_API_KEY = key;
|
|
317
|
+
apiKeyAvailable = true;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
}
|
|
326
|
+
apiKeyAvailable = false;
|
|
327
|
+
}
|
|
328
|
+
function tryGetClient() {
|
|
329
|
+
if (!isAIAvailable()) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
if (!clientInstance) {
|
|
333
|
+
clientInstance = new Anthropic();
|
|
334
|
+
}
|
|
335
|
+
return clientInstance;
|
|
336
|
+
}
|
|
337
|
+
async function runAIAnalysis(request) {
|
|
338
|
+
const client = tryGetClient();
|
|
339
|
+
if (!client) {
|
|
340
|
+
return {
|
|
341
|
+
success: false,
|
|
342
|
+
content: "",
|
|
343
|
+
error: "AI not available - ANTHROPIC_API_KEY not set"
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
const response = await client.messages.create({
|
|
348
|
+
model: "claude-sonnet-4-20250514",
|
|
349
|
+
max_tokens: request.maxTokens || 4096,
|
|
350
|
+
temperature: request.temperature ?? 0.3,
|
|
351
|
+
system: request.systemPrompt,
|
|
352
|
+
messages: [
|
|
353
|
+
{
|
|
354
|
+
role: "user",
|
|
355
|
+
content: request.userPrompt
|
|
356
|
+
}
|
|
357
|
+
]
|
|
358
|
+
});
|
|
359
|
+
const textContent = response.content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
|
|
360
|
+
return {
|
|
361
|
+
success: true,
|
|
362
|
+
content: textContent,
|
|
363
|
+
tokensUsed: {
|
|
364
|
+
input: response.usage.input_tokens,
|
|
365
|
+
output: response.usage.output_tokens
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
} catch (error) {
|
|
369
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
370
|
+
if (errorMessage.includes("authentication") || errorMessage.includes("API key")) {
|
|
371
|
+
return {
|
|
372
|
+
success: false,
|
|
373
|
+
content: "",
|
|
374
|
+
error: "Invalid API key. Check your ANTHROPIC_API_KEY."
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (errorMessage.includes("rate limit")) {
|
|
378
|
+
return {
|
|
379
|
+
success: false,
|
|
380
|
+
content: "",
|
|
381
|
+
error: "Rate limited. Try again in a moment."
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
success: false,
|
|
386
|
+
content: "",
|
|
387
|
+
error: `AI analysis failed: ${errorMessage}`
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/storage/tiered-storage.ts
|
|
393
|
+
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
394
|
+
import { join as join3 } from "path";
|
|
395
|
+
import Database from "better-sqlite3";
|
|
396
|
+
var TieredStorage = class {
|
|
397
|
+
workDir;
|
|
398
|
+
hotCache = /* @__PURE__ */ new Map();
|
|
399
|
+
warmDb = null;
|
|
400
|
+
constructor(workDir) {
|
|
401
|
+
this.workDir = workDir;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Initialize storage directories and database
|
|
405
|
+
*/
|
|
406
|
+
async initialize() {
|
|
407
|
+
const trieDir = getTrieDirectory(this.workDir);
|
|
408
|
+
await mkdir2(join3(trieDir, "hot"), { recursive: true });
|
|
409
|
+
await mkdir2(join3(trieDir, "warm"), { recursive: true });
|
|
410
|
+
await mkdir2(join3(trieDir, "cold"), { recursive: true });
|
|
411
|
+
const dbPath = join3(trieDir, "warm", "decisions.db");
|
|
412
|
+
this.warmDb = new Database(dbPath);
|
|
413
|
+
this.warmDb.exec(`
|
|
414
|
+
CREATE TABLE IF NOT EXISTS decisions (
|
|
415
|
+
id TEXT PRIMARY KEY,
|
|
416
|
+
decision TEXT NOT NULL,
|
|
417
|
+
context TEXT NOT NULL,
|
|
418
|
+
reasoning TEXT,
|
|
419
|
+
timestamp TEXT NOT NULL,
|
|
420
|
+
who TEXT,
|
|
421
|
+
files TEXT NOT NULL, -- JSON array
|
|
422
|
+
tags TEXT NOT NULL, -- JSON array
|
|
423
|
+
expandedTags TEXT, -- JSON array (with synonyms)
|
|
424
|
+
relatedTo TEXT, -- JSON array
|
|
425
|
+
tradeoffs TEXT, -- JSON array
|
|
426
|
+
status TEXT NOT NULL,
|
|
427
|
+
supersededBy TEXT,
|
|
428
|
+
dependencies TEXT, -- JSON array (npm packages, services)
|
|
429
|
+
codebaseArea TEXT, -- JSON array (frontend, backend, auth, etc.)
|
|
430
|
+
domain TEXT, -- JSON array (payments, compliance, etc.)
|
|
431
|
+
lastAccessed TEXT NOT NULL,
|
|
432
|
+
accessCount INTEGER DEFAULT 0
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
436
|
+
id TEXT PRIMARY KEY,
|
|
437
|
+
fact TEXT NOT NULL,
|
|
438
|
+
source TEXT NOT NULL,
|
|
439
|
+
timestamp TEXT NOT NULL,
|
|
440
|
+
tags TEXT NOT NULL, -- JSON array
|
|
441
|
+
expandedTags TEXT, -- JSON array (with synonyms)
|
|
442
|
+
relatedDecisions TEXT, -- JSON array
|
|
443
|
+
confidence REAL NOT NULL,
|
|
444
|
+
lastAccessed TEXT NOT NULL,
|
|
445
|
+
accessCount INTEGER DEFAULT 0
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
CREATE TABLE IF NOT EXISTS blockers (
|
|
449
|
+
id TEXT PRIMARY KEY,
|
|
450
|
+
blocker TEXT NOT NULL,
|
|
451
|
+
impact TEXT NOT NULL,
|
|
452
|
+
affectedAreas TEXT NOT NULL, -- JSON array
|
|
453
|
+
timestamp TEXT NOT NULL,
|
|
454
|
+
resolvedAt TEXT,
|
|
455
|
+
resolution TEXT,
|
|
456
|
+
tags TEXT NOT NULL, -- JSON array
|
|
457
|
+
expandedTags TEXT, -- JSON array (with synonyms)
|
|
458
|
+
lastAccessed TEXT NOT NULL,
|
|
459
|
+
accessCount INTEGER DEFAULT 0
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
CREATE TABLE IF NOT EXISTS questions (
|
|
463
|
+
id TEXT PRIMARY KEY,
|
|
464
|
+
question TEXT NOT NULL,
|
|
465
|
+
context TEXT NOT NULL,
|
|
466
|
+
timestamp TEXT NOT NULL,
|
|
467
|
+
answeredAt TEXT,
|
|
468
|
+
answer TEXT,
|
|
469
|
+
relatedDecisions TEXT, -- JSON array
|
|
470
|
+
tags TEXT NOT NULL, -- JSON array
|
|
471
|
+
expandedTags TEXT, -- JSON array (with synonyms)
|
|
472
|
+
lastAccessed TEXT NOT NULL,
|
|
473
|
+
accessCount INTEGER DEFAULT 0
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_tags ON decisions(tags);
|
|
477
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_expanded ON decisions(expandedTags);
|
|
478
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_timestamp ON decisions(timestamp);
|
|
479
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_status ON decisions(status);
|
|
480
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_area ON decisions(codebaseArea);
|
|
481
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain);
|
|
482
|
+
CREATE INDEX IF NOT EXISTS idx_facts_tags ON facts(tags);
|
|
483
|
+
CREATE INDEX IF NOT EXISTS idx_facts_expanded ON facts(expandedTags);
|
|
484
|
+
CREATE INDEX IF NOT EXISTS idx_blockers_impact ON blockers(impact);
|
|
485
|
+
CREATE INDEX IF NOT EXISTS idx_blockers_resolved ON blockers(resolvedAt);
|
|
486
|
+
`);
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Store extracted signal with enriched metadata in warm storage
|
|
490
|
+
*/
|
|
491
|
+
async storeSignal(signal, metadata) {
|
|
492
|
+
if (!this.warmDb) await this.initialize();
|
|
493
|
+
if (!this.warmDb) throw new Error("Database not initialized");
|
|
494
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
495
|
+
const decisionStmt = this.warmDb.prepare(`
|
|
496
|
+
INSERT OR REPLACE INTO decisions
|
|
497
|
+
(id, decision, context, reasoning, timestamp, who, files, tags, expandedTags, relatedTo, tradeoffs, status, supersededBy, dependencies, codebaseArea, domain, lastAccessed, accessCount)
|
|
498
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
|
499
|
+
`);
|
|
500
|
+
for (const dec of signal.decisions) {
|
|
501
|
+
decisionStmt.run(
|
|
502
|
+
dec.id,
|
|
503
|
+
dec.decision,
|
|
504
|
+
dec.context,
|
|
505
|
+
dec.reasoning || null,
|
|
506
|
+
dec.when,
|
|
507
|
+
dec.who || null,
|
|
508
|
+
JSON.stringify(dec.files),
|
|
509
|
+
JSON.stringify(dec.tags),
|
|
510
|
+
JSON.stringify(metadata?.expandedTags || []),
|
|
511
|
+
JSON.stringify(dec.relatedTo || []),
|
|
512
|
+
JSON.stringify(dec.tradeoffs || []),
|
|
513
|
+
dec.status,
|
|
514
|
+
dec.supersededBy || null,
|
|
515
|
+
JSON.stringify(metadata?.dependencies || []),
|
|
516
|
+
JSON.stringify(metadata?.codebaseArea || []),
|
|
517
|
+
JSON.stringify(metadata?.domain || []),
|
|
518
|
+
now
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
const factStmt = this.warmDb.prepare(`
|
|
522
|
+
INSERT OR REPLACE INTO facts
|
|
523
|
+
(id, fact, source, timestamp, tags, expandedTags, relatedDecisions, confidence, lastAccessed, accessCount)
|
|
524
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
|
525
|
+
`);
|
|
526
|
+
for (const fact of signal.facts) {
|
|
527
|
+
factStmt.run(
|
|
528
|
+
fact.id,
|
|
529
|
+
fact.fact,
|
|
530
|
+
fact.source,
|
|
531
|
+
fact.when,
|
|
532
|
+
JSON.stringify(fact.tags),
|
|
533
|
+
JSON.stringify(metadata?.expandedTags || []),
|
|
534
|
+
JSON.stringify(fact.relatedDecisions || []),
|
|
535
|
+
fact.confidence,
|
|
536
|
+
now
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const blockerStmt = this.warmDb.prepare(`
|
|
540
|
+
INSERT OR REPLACE INTO blockers
|
|
541
|
+
(id, blocker, impact, affectedAreas, timestamp, resolvedAt, resolution, tags, expandedTags, lastAccessed, accessCount)
|
|
542
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
|
543
|
+
`);
|
|
544
|
+
for (const blocker of signal.blockers) {
|
|
545
|
+
blockerStmt.run(
|
|
546
|
+
blocker.id,
|
|
547
|
+
blocker.blocker,
|
|
548
|
+
blocker.impact,
|
|
549
|
+
JSON.stringify(blocker.affectedAreas),
|
|
550
|
+
blocker.when,
|
|
551
|
+
blocker.resolvedAt || null,
|
|
552
|
+
blocker.resolution || null,
|
|
553
|
+
JSON.stringify(blocker.tags),
|
|
554
|
+
JSON.stringify(metadata?.expandedTags || []),
|
|
555
|
+
now
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
const questionStmt = this.warmDb.prepare(`
|
|
559
|
+
INSERT OR REPLACE INTO questions
|
|
560
|
+
(id, question, context, timestamp, answeredAt, answer, relatedDecisions, tags, expandedTags, lastAccessed, accessCount)
|
|
561
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
|
562
|
+
`);
|
|
563
|
+
for (const q of signal.questions) {
|
|
564
|
+
questionStmt.run(
|
|
565
|
+
q.id,
|
|
566
|
+
q.question,
|
|
567
|
+
q.context,
|
|
568
|
+
q.when,
|
|
569
|
+
q.answeredAt || null,
|
|
570
|
+
q.answer || null,
|
|
571
|
+
JSON.stringify(q.relatedDecisions || []),
|
|
572
|
+
JSON.stringify(q.tags),
|
|
573
|
+
JSON.stringify(metadata?.expandedTags || []),
|
|
574
|
+
now
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Query decisions from warm storage with expanded tag matching
|
|
580
|
+
*/
|
|
581
|
+
async queryDecisions(query) {
|
|
582
|
+
if (!this.warmDb) await this.initialize();
|
|
583
|
+
if (!this.warmDb) throw new Error("Database not initialized");
|
|
584
|
+
let sql = "SELECT * FROM decisions WHERE status = ?";
|
|
585
|
+
const params = ["active"];
|
|
586
|
+
if (query.tags && query.tags.length > 0) {
|
|
587
|
+
sql += " AND (" + query.tags.map(() => "(tags LIKE ? OR expandedTags LIKE ?)").join(" OR ") + ")";
|
|
588
|
+
for (const tag of query.tags) {
|
|
589
|
+
params.push(`%"${tag}"%`, `%"${tag}"%`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (query.filters?.codebaseArea) {
|
|
593
|
+
sql += " AND codebaseArea LIKE ?";
|
|
594
|
+
params.push(`%"${query.filters.codebaseArea}"%`);
|
|
595
|
+
}
|
|
596
|
+
if (query.filters?.domain) {
|
|
597
|
+
sql += " AND domain LIKE ?";
|
|
598
|
+
params.push(`%"${query.filters.domain}"%`);
|
|
599
|
+
}
|
|
600
|
+
if (query.timeWindow) {
|
|
601
|
+
if (query.timeWindow.start) {
|
|
602
|
+
sql += " AND timestamp >= ?";
|
|
603
|
+
params.push(query.timeWindow.start);
|
|
604
|
+
}
|
|
605
|
+
if (query.timeWindow.end) {
|
|
606
|
+
sql += " AND timestamp <= ?";
|
|
607
|
+
params.push(query.timeWindow.end);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
sql += " ORDER BY timestamp DESC";
|
|
611
|
+
if (query.limit) {
|
|
612
|
+
sql += " LIMIT ?";
|
|
613
|
+
params.push(query.limit);
|
|
614
|
+
}
|
|
615
|
+
const rows = this.warmDb.prepare(sql).all(...params);
|
|
616
|
+
return rows.map((row) => ({
|
|
617
|
+
id: row.id,
|
|
618
|
+
decision: row.decision,
|
|
619
|
+
context: row.context,
|
|
620
|
+
reasoning: row.reasoning,
|
|
621
|
+
when: row.when,
|
|
622
|
+
who: row.who,
|
|
623
|
+
files: JSON.parse(row.files),
|
|
624
|
+
tags: JSON.parse(row.tags),
|
|
625
|
+
relatedTo: JSON.parse(row.relatedTo || "[]"),
|
|
626
|
+
tradeoffs: JSON.parse(row.tradeoffs || "[]"),
|
|
627
|
+
status: row.status,
|
|
628
|
+
supersededBy: row.supersededBy
|
|
629
|
+
}));
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Query blockers from warm storage with expanded tag matching
|
|
633
|
+
*/
|
|
634
|
+
async queryBlockers(query) {
|
|
635
|
+
if (!this.warmDb) await this.initialize();
|
|
636
|
+
if (!this.warmDb) throw new Error("Database not initialized");
|
|
637
|
+
let sql = "SELECT * FROM blockers WHERE resolvedAt IS NULL";
|
|
638
|
+
const params = [];
|
|
639
|
+
if (query.tags && query.tags.length > 0) {
|
|
640
|
+
sql += " AND (" + query.tags.map(() => "(tags LIKE ? OR expandedTags LIKE ?)").join(" OR ") + ")";
|
|
641
|
+
for (const tag of query.tags) {
|
|
642
|
+
params.push(`%"${tag}"%`, `%"${tag}"%`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
sql += ' ORDER BY CASE impact WHEN "critical" THEN 1 WHEN "high" THEN 2 WHEN "medium" THEN 3 ELSE 4 END, timestamp DESC';
|
|
646
|
+
if (query.limit) {
|
|
647
|
+
sql += " LIMIT ?";
|
|
648
|
+
params.push(query.limit);
|
|
649
|
+
}
|
|
650
|
+
const rows = this.warmDb.prepare(sql).all(...params);
|
|
651
|
+
return rows.map((row) => ({
|
|
652
|
+
id: row.id,
|
|
653
|
+
blocker: row.blocker,
|
|
654
|
+
impact: row.impact,
|
|
655
|
+
affectedAreas: JSON.parse(row.affectedAreas),
|
|
656
|
+
when: row.when,
|
|
657
|
+
resolvedAt: row.resolvedAt,
|
|
658
|
+
resolution: row.resolution,
|
|
659
|
+
tags: JSON.parse(row.tags)
|
|
660
|
+
}));
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Get hot cache item
|
|
664
|
+
*/
|
|
665
|
+
getHot(key) {
|
|
666
|
+
return this.hotCache.get(key);
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Set hot cache item
|
|
670
|
+
*/
|
|
671
|
+
setHot(key, value) {
|
|
672
|
+
this.hotCache.set(key, value);
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Clear hot cache
|
|
676
|
+
*/
|
|
677
|
+
clearHot() {
|
|
678
|
+
this.hotCache.clear();
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Archive old data to cold storage
|
|
682
|
+
*/
|
|
683
|
+
async archiveToCold(olderThanDays = 90) {
|
|
684
|
+
if (!this.warmDb) await this.initialize();
|
|
685
|
+
if (!this.warmDb) throw new Error("Database not initialized");
|
|
686
|
+
const cutoffDate = /* @__PURE__ */ new Date();
|
|
687
|
+
cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
|
|
688
|
+
const cutoff = cutoffDate.toISOString();
|
|
689
|
+
const coldDir = join3(getTrieDirectory(this.workDir), "cold");
|
|
690
|
+
const archivePath = join3(coldDir, `archive-${Date.now()}.json`);
|
|
691
|
+
const decisions = this.warmDb.prepare("SELECT * FROM decisions WHERE timestamp < ? AND accessCount < 3").all(cutoff);
|
|
692
|
+
const facts = this.warmDb.prepare("SELECT * FROM facts WHERE timestamp < ? AND accessCount < 3").all(cutoff);
|
|
693
|
+
const blockers = this.warmDb.prepare("SELECT * FROM blockers WHERE timestamp < ? AND resolvedAt IS NOT NULL").all(cutoff);
|
|
694
|
+
const questions = this.warmDb.prepare("SELECT * FROM questions WHERE timestamp < ? AND answeredAt IS NOT NULL").all(cutoff);
|
|
695
|
+
await writeFile2(archivePath, JSON.stringify({ decisions, facts, blockers, questions }, null, 2));
|
|
696
|
+
this.warmDb.prepare("DELETE FROM decisions WHERE timestamp < ? AND accessCount < 3").run(cutoff);
|
|
697
|
+
this.warmDb.prepare("DELETE FROM facts WHERE timestamp < ? AND accessCount < 3").run(cutoff);
|
|
698
|
+
this.warmDb.prepare("DELETE FROM blockers WHERE timestamp < ? AND resolvedAt IS NOT NULL").run(cutoff);
|
|
699
|
+
this.warmDb.prepare("DELETE FROM questions WHERE timestamp < ? AND answeredAt IS NOT NULL").run(cutoff);
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Close database connection
|
|
703
|
+
*/
|
|
704
|
+
close() {
|
|
705
|
+
if (this.warmDb) {
|
|
706
|
+
this.warmDb.close();
|
|
707
|
+
this.warmDb = null;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
var storageInstances = /* @__PURE__ */ new Map();
|
|
712
|
+
function getStorage(workDir) {
|
|
713
|
+
if (!storageInstances.has(workDir)) {
|
|
714
|
+
storageInstances.set(workDir, new TieredStorage(workDir));
|
|
715
|
+
}
|
|
716
|
+
return storageInstances.get(workDir);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/guardian/gotcha-predictor.ts
|
|
720
|
+
import fs from "fs";
|
|
721
|
+
import path from "path";
|
|
722
|
+
var GotchaPredictor = class {
|
|
723
|
+
projectPath;
|
|
724
|
+
graph;
|
|
725
|
+
constructor(projectPath, graph) {
|
|
726
|
+
this.projectPath = projectPath;
|
|
727
|
+
this.graph = graph;
|
|
728
|
+
}
|
|
729
|
+
async predictGotchas(changedFiles) {
|
|
730
|
+
const gotchas = [];
|
|
731
|
+
const tickets = (await this.graph.listNodes()).filter((n) => n.type === "linear-ticket");
|
|
732
|
+
for (const file of changedFiles) {
|
|
733
|
+
const fileGotchas = await this.predictForFile(file, tickets);
|
|
734
|
+
gotchas.push(...fileGotchas);
|
|
735
|
+
}
|
|
736
|
+
return gotchas;
|
|
737
|
+
}
|
|
738
|
+
async predictForFile(filePath, tickets) {
|
|
739
|
+
const gotchas = [];
|
|
740
|
+
const fullPath = path.resolve(this.projectPath, filePath);
|
|
741
|
+
if (!fs.existsSync(fullPath)) return [];
|
|
742
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
743
|
+
const vulnerabilities = await scanForVulnerabilities(content, filePath);
|
|
744
|
+
const vibeIssues = await scanForVibeCodeIssues(content, filePath);
|
|
745
|
+
const signatures = [
|
|
746
|
+
...vulnerabilities.map((v) => v.category),
|
|
747
|
+
...vibeIssues.map((v) => v.category)
|
|
748
|
+
];
|
|
749
|
+
const storage = getStorage(this.projectPath);
|
|
750
|
+
await storage.initialize();
|
|
751
|
+
const tags = this.extractTagsFromFile(filePath, signatures);
|
|
752
|
+
const relevantDecisions = await storage.queryDecisions({
|
|
753
|
+
tags,
|
|
754
|
+
limit: 10
|
|
755
|
+
});
|
|
756
|
+
const activeBlockers = await storage.queryBlockers({
|
|
757
|
+
tags,
|
|
758
|
+
limit: 5
|
|
759
|
+
});
|
|
760
|
+
for (const decision of relevantDecisions) {
|
|
761
|
+
if (decision.files.some((f) => f.includes(filePath) || filePath.includes(f))) {
|
|
762
|
+
gotchas.push({
|
|
763
|
+
id: `gotcha-decision-${decision.id}`,
|
|
764
|
+
message: `Decision: ${decision.decision}`,
|
|
765
|
+
confidence: 0.85,
|
|
766
|
+
riskLevel: "medium",
|
|
767
|
+
precedentId: decision.id,
|
|
768
|
+
recommendation: decision.reasoning || "Review this decision before making changes",
|
|
769
|
+
evidence: {
|
|
770
|
+
pastIncidents: [],
|
|
771
|
+
matchingPatterns: decision.tags,
|
|
772
|
+
relatedTickets: []
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
for (const blocker of activeBlockers) {
|
|
778
|
+
gotchas.push({
|
|
779
|
+
id: `gotcha-blocker-${blocker.id}`,
|
|
780
|
+
message: `\u26A0\uFE0F Active Blocker: ${blocker.blocker}`,
|
|
781
|
+
confidence: 0.95,
|
|
782
|
+
riskLevel: blocker.impact === "critical" ? "critical" : blocker.impact === "high" ? "high" : "medium",
|
|
783
|
+
recommendation: `This area is currently blocked. Consider resolving this before making changes.`,
|
|
784
|
+
evidence: {
|
|
785
|
+
pastIncidents: [],
|
|
786
|
+
matchingPatterns: blocker.tags,
|
|
787
|
+
relatedTickets: []
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
for (const ticket of tickets) {
|
|
792
|
+
const ticketData = ticket.data;
|
|
793
|
+
const intentMatch = this.correlateIntentWithSignatures(ticketData, signatures);
|
|
794
|
+
if (intentMatch) {
|
|
795
|
+
gotchas.push(intentMatch);
|
|
796
|
+
}
|
|
797
|
+
const historicalPrecedents = await this.findHistoricalPrecedents(filePath, ticketData);
|
|
798
|
+
if (historicalPrecedents) {
|
|
799
|
+
gotchas.push(historicalPrecedents);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return gotchas;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Extract tags from file path and signatures for storage queries
|
|
806
|
+
*/
|
|
807
|
+
extractTagsFromFile(filePath, signatures) {
|
|
808
|
+
const tags = /* @__PURE__ */ new Set();
|
|
809
|
+
const normalized = filePath.toLowerCase();
|
|
810
|
+
if (normalized.includes("/auth/")) tags.add("auth");
|
|
811
|
+
if (normalized.includes("/payment/")) tags.add("payments");
|
|
812
|
+
if (normalized.includes("/api/")) tags.add("api");
|
|
813
|
+
if (normalized.includes("/frontend/") || normalized.includes("/ui/")) tags.add("ui");
|
|
814
|
+
if (normalized.includes("/backend/")) tags.add("backend");
|
|
815
|
+
if (normalized.includes("/database/") || normalized.includes("/models/")) tags.add("database");
|
|
816
|
+
signatures.forEach((sig) => tags.add(sig.toLowerCase()));
|
|
817
|
+
return Array.from(tags);
|
|
818
|
+
}
|
|
819
|
+
correlateIntentWithSignatures(ticket, signatures) {
|
|
820
|
+
const relevantSignatures = signatures.filter(
|
|
821
|
+
(sig) => ticket.intentVibe.some((vibe) => this.vibeToSignatureMap(vibe).includes(sig))
|
|
822
|
+
);
|
|
823
|
+
if (relevantSignatures.length > 0) {
|
|
824
|
+
return {
|
|
825
|
+
id: `gotcha-intent-${ticket.ticketId}-${Date.now()}`,
|
|
826
|
+
message: `[${ticket.ticketId}] Working on "${ticket.title}" (${ticket.intentVibe.join(", ")}) in a file with ${relevantSignatures.join(", ")} signatures.`,
|
|
827
|
+
confidence: 0.8,
|
|
828
|
+
riskLevel: "high",
|
|
829
|
+
recommendation: `Be careful with ${relevantSignatures[0]} patterns as they correlate with the intent of your ticket.`,
|
|
830
|
+
evidence: {
|
|
831
|
+
pastIncidents: [],
|
|
832
|
+
matchingPatterns: relevantSignatures,
|
|
833
|
+
relatedTickets: [ticket.ticketId]
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
async findHistoricalPrecedents(filePath, ticket) {
|
|
840
|
+
const similarIssues = await searchIssues(ticket.description, {
|
|
841
|
+
workDir: this.projectPath,
|
|
842
|
+
limit: 3
|
|
843
|
+
});
|
|
844
|
+
const relevantIssues = similarIssues.filter((r) => r.issue.file === filePath || r.issue.file.includes(path.basename(filePath)));
|
|
845
|
+
const [firstMatch] = relevantIssues;
|
|
846
|
+
if (firstMatch) {
|
|
847
|
+
const issue = firstMatch.issue;
|
|
848
|
+
return {
|
|
849
|
+
id: `gotcha-precedent-${ticket.ticketId}-${Date.now()}`,
|
|
850
|
+
message: `A similar task in the past caused an issue: "${issue.issue}"`,
|
|
851
|
+
confidence: 0.9,
|
|
852
|
+
riskLevel: "critical",
|
|
853
|
+
precedentId: issue.id,
|
|
854
|
+
recommendation: `Last time we worked on something similar here, we had to fix: "${issue.fix}". Check this first.`,
|
|
855
|
+
evidence: {
|
|
856
|
+
pastIncidents: [issue.id],
|
|
857
|
+
matchingPatterns: [],
|
|
858
|
+
relatedTickets: [ticket.ticketId]
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
vibeToSignatureMap(vibe) {
|
|
865
|
+
const map = {
|
|
866
|
+
"performance": ["giant-file", "performance", "react-antipattern"],
|
|
867
|
+
"security": ["injection", "secrets", "auth", "xss", "crypto"],
|
|
868
|
+
"auth": ["auth", "secrets", "config"],
|
|
869
|
+
"bug": ["no-error-handling", "async", "error-handling"],
|
|
870
|
+
"feature": ["mixing-concerns", "hardcoded"],
|
|
871
|
+
"refactor": ["code-smell", "giant-file", "mixing-concerns"]
|
|
872
|
+
};
|
|
873
|
+
return map[vibe] || [];
|
|
874
|
+
}
|
|
875
|
+
async synthesizeGotchaExplanation(gotcha) {
|
|
876
|
+
const client = tryGetClient();
|
|
877
|
+
if (!client) return gotcha.message;
|
|
878
|
+
const prompt = `
|
|
879
|
+
You are a JIT Defect Predictor. You found a potential "gotcha" for a developer.
|
|
880
|
+
|
|
881
|
+
Ticket context: ${gotcha.evidence.relatedTickets.join(", ")}
|
|
882
|
+
Signatures detected: ${gotcha.evidence.matchingPatterns.join(", ")}
|
|
883
|
+
Past incidents: ${gotcha.evidence.pastIncidents.join(", ")}
|
|
884
|
+
|
|
885
|
+
Raw message: ${gotcha.message}
|
|
886
|
+
Recommendation: ${gotcha.recommendation}
|
|
887
|
+
|
|
888
|
+
Explain this gotcha in a concise, human-friendly way (max 2 sentences).
|
|
889
|
+
Make it sound like a senior dev giving a helpful nudge.
|
|
890
|
+
`;
|
|
891
|
+
try {
|
|
892
|
+
const response = await client.messages.create({
|
|
893
|
+
model: "claude-3-5-sonnet-20240620",
|
|
894
|
+
max_tokens: 100,
|
|
895
|
+
messages: [{ role: "user", content: prompt }]
|
|
896
|
+
});
|
|
897
|
+
const text = response.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
898
|
+
return text.trim() || gotcha.message;
|
|
899
|
+
} catch {
|
|
900
|
+
return gotcha.message;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
// src/context/graph.ts
|
|
906
|
+
import crypto from "crypto";
|
|
907
|
+
import path3 from "path";
|
|
908
|
+
|
|
909
|
+
// src/context/store.ts
|
|
910
|
+
import Database2 from "better-sqlite3";
|
|
911
|
+
import fs2 from "fs";
|
|
912
|
+
import path2 from "path";
|
|
913
|
+
var ContextStore = class {
|
|
914
|
+
db;
|
|
915
|
+
dbFilePath;
|
|
916
|
+
constructor(projectPath, dbFilePath) {
|
|
917
|
+
this.dbFilePath = dbFilePath ?? path2.join(getTrieDirectory(projectPath), "context.db");
|
|
918
|
+
this.ensureDirectory();
|
|
919
|
+
this.db = new Database2(this.dbFilePath);
|
|
920
|
+
this.configure();
|
|
921
|
+
this.prepareSchema();
|
|
922
|
+
}
|
|
923
|
+
get databasePath() {
|
|
924
|
+
return this.dbFilePath;
|
|
925
|
+
}
|
|
926
|
+
addNode(node) {
|
|
927
|
+
const stmt = this.db.prepare(
|
|
928
|
+
`INSERT INTO nodes (id, type, data, created_at, updated_at)
|
|
929
|
+
VALUES (@id, @type, @data, @created_at, @updated_at)`
|
|
930
|
+
);
|
|
931
|
+
stmt.run({
|
|
932
|
+
id: node.id,
|
|
933
|
+
type: node.type,
|
|
934
|
+
data: JSON.stringify(node.data),
|
|
935
|
+
created_at: node.created_at,
|
|
936
|
+
updated_at: node.updated_at
|
|
937
|
+
});
|
|
938
|
+
return node;
|
|
939
|
+
}
|
|
940
|
+
upsertNode(node) {
|
|
941
|
+
const stmt = this.db.prepare(
|
|
942
|
+
`INSERT INTO nodes (id, type, data, created_at, updated_at)
|
|
943
|
+
VALUES (@id, @type, @data, @created_at, @updated_at)
|
|
944
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
945
|
+
type=excluded.type,
|
|
946
|
+
data=excluded.data,
|
|
947
|
+
updated_at=excluded.updated_at`
|
|
948
|
+
);
|
|
949
|
+
stmt.run({
|
|
950
|
+
id: node.id,
|
|
951
|
+
type: node.type,
|
|
952
|
+
data: JSON.stringify(node.data),
|
|
953
|
+
created_at: node.created_at,
|
|
954
|
+
updated_at: node.updated_at
|
|
955
|
+
});
|
|
956
|
+
return node;
|
|
957
|
+
}
|
|
958
|
+
getNode(id) {
|
|
959
|
+
const row = this.db.prepare("SELECT * FROM nodes WHERE id = ?").get(id);
|
|
960
|
+
return row ? this.mapNodeRow(row) : null;
|
|
961
|
+
}
|
|
962
|
+
getNodeByType(type, id) {
|
|
963
|
+
const row = this.db.prepare("SELECT * FROM nodes WHERE id = ? AND type = ?").get(id, type);
|
|
964
|
+
return row ? this.mapNodeRow(row) : null;
|
|
965
|
+
}
|
|
966
|
+
updateNode(id, updates, updatedAt) {
|
|
967
|
+
const existing = this.getNode(id);
|
|
968
|
+
if (!existing) {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
const merged = {
|
|
972
|
+
...existing,
|
|
973
|
+
data: { ...existing.data, ...updates },
|
|
974
|
+
updated_at: updatedAt
|
|
975
|
+
};
|
|
976
|
+
this.db.prepare(
|
|
977
|
+
`UPDATE nodes SET data = @data, updated_at = @updated_at
|
|
978
|
+
WHERE id = @id`
|
|
979
|
+
).run({
|
|
980
|
+
id,
|
|
981
|
+
data: JSON.stringify(merged.data),
|
|
982
|
+
updated_at: merged.updated_at
|
|
983
|
+
});
|
|
984
|
+
return merged;
|
|
985
|
+
}
|
|
986
|
+
deleteNode(id) {
|
|
987
|
+
const deleteEdges = this.db.prepare("DELETE FROM edges WHERE from_id = ? OR to_id = ?");
|
|
988
|
+
const deleteNodeStmt = this.db.prepare("DELETE FROM nodes WHERE id = ?");
|
|
989
|
+
const transaction = this.db.transaction((nodeId) => {
|
|
990
|
+
deleteEdges.run(nodeId, nodeId);
|
|
991
|
+
deleteNodeStmt.run(nodeId);
|
|
992
|
+
});
|
|
993
|
+
transaction(id);
|
|
994
|
+
}
|
|
995
|
+
listNodes() {
|
|
996
|
+
const rows = this.db.prepare("SELECT * FROM nodes").all();
|
|
997
|
+
return rows.map((row) => this.mapNodeRow(row));
|
|
998
|
+
}
|
|
999
|
+
findNodesByType(type) {
|
|
1000
|
+
const rows = this.db.prepare("SELECT * FROM nodes WHERE type = ?").all(type);
|
|
1001
|
+
return rows.map((row) => this.mapNodeRow(row));
|
|
1002
|
+
}
|
|
1003
|
+
addEdge(edge) {
|
|
1004
|
+
const stmt = this.db.prepare(
|
|
1005
|
+
`INSERT INTO edges (id, from_id, to_id, type, weight, metadata, created_at)
|
|
1006
|
+
VALUES (@id, @from_id, @to_id, @type, @weight, @metadata, @created_at)`
|
|
1007
|
+
);
|
|
1008
|
+
stmt.run({
|
|
1009
|
+
id: edge.id,
|
|
1010
|
+
from_id: edge.from_id,
|
|
1011
|
+
to_id: edge.to_id,
|
|
1012
|
+
type: edge.type,
|
|
1013
|
+
weight: edge.weight,
|
|
1014
|
+
metadata: JSON.stringify(edge.metadata ?? {}),
|
|
1015
|
+
created_at: edge.created_at
|
|
1016
|
+
});
|
|
1017
|
+
return edge;
|
|
1018
|
+
}
|
|
1019
|
+
upsertEdge(edge) {
|
|
1020
|
+
const stmt = this.db.prepare(
|
|
1021
|
+
`INSERT INTO edges (id, from_id, to_id, type, weight, metadata, created_at)
|
|
1022
|
+
VALUES (@id, @from_id, @to_id, @type, @weight, @metadata, @created_at)
|
|
1023
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1024
|
+
from_id=excluded.from_id,
|
|
1025
|
+
to_id=excluded.to_id,
|
|
1026
|
+
type=excluded.type,
|
|
1027
|
+
weight=excluded.weight,
|
|
1028
|
+
metadata=excluded.metadata`
|
|
1029
|
+
);
|
|
1030
|
+
stmt.run({
|
|
1031
|
+
id: edge.id,
|
|
1032
|
+
from_id: edge.from_id,
|
|
1033
|
+
to_id: edge.to_id,
|
|
1034
|
+
type: edge.type,
|
|
1035
|
+
weight: edge.weight,
|
|
1036
|
+
metadata: JSON.stringify(edge.metadata ?? {}),
|
|
1037
|
+
created_at: edge.created_at
|
|
1038
|
+
});
|
|
1039
|
+
return edge;
|
|
1040
|
+
}
|
|
1041
|
+
getEdge(id) {
|
|
1042
|
+
const row = this.db.prepare("SELECT * FROM edges WHERE id = ?").get(id);
|
|
1043
|
+
return row ? this.mapEdgeRow(row) : null;
|
|
1044
|
+
}
|
|
1045
|
+
getEdges(nodeId, direction = "both") {
|
|
1046
|
+
let rows;
|
|
1047
|
+
if (direction === "in") {
|
|
1048
|
+
rows = this.db.prepare("SELECT * FROM edges WHERE to_id = ?").all(nodeId);
|
|
1049
|
+
} else if (direction === "out") {
|
|
1050
|
+
rows = this.db.prepare("SELECT * FROM edges WHERE from_id = ?").all(nodeId);
|
|
1051
|
+
} else {
|
|
1052
|
+
rows = this.db.prepare("SELECT * FROM edges WHERE from_id = ? OR to_id = ?").all(nodeId, nodeId);
|
|
1053
|
+
}
|
|
1054
|
+
return rows.map((row) => this.mapEdgeRow(row));
|
|
1055
|
+
}
|
|
1056
|
+
listEdges() {
|
|
1057
|
+
const rows = this.db.prepare("SELECT * FROM edges").all();
|
|
1058
|
+
return rows.map((row) => this.mapEdgeRow(row));
|
|
1059
|
+
}
|
|
1060
|
+
deleteEdge(id) {
|
|
1061
|
+
this.db.prepare("DELETE FROM edges WHERE id = ?").run(id);
|
|
1062
|
+
}
|
|
1063
|
+
close() {
|
|
1064
|
+
this.db.close();
|
|
1065
|
+
}
|
|
1066
|
+
ensureDirectory() {
|
|
1067
|
+
fs2.mkdirSync(path2.dirname(this.dbFilePath), { recursive: true });
|
|
1068
|
+
}
|
|
1069
|
+
configure() {
|
|
1070
|
+
this.db.pragma("journal_mode = WAL");
|
|
1071
|
+
this.db.pragma("busy_timeout = 5000");
|
|
1072
|
+
this.db.pragma("synchronous = NORMAL");
|
|
1073
|
+
}
|
|
1074
|
+
prepareSchema() {
|
|
1075
|
+
this.db.exec(`
|
|
1076
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
1077
|
+
id TEXT PRIMARY KEY,
|
|
1078
|
+
type TEXT NOT NULL,
|
|
1079
|
+
data TEXT NOT NULL,
|
|
1080
|
+
created_at TEXT NOT NULL,
|
|
1081
|
+
updated_at TEXT NOT NULL
|
|
1082
|
+
);
|
|
1083
|
+
|
|
1084
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
1085
|
+
id TEXT PRIMARY KEY,
|
|
1086
|
+
from_id TEXT NOT NULL,
|
|
1087
|
+
to_id TEXT NOT NULL,
|
|
1088
|
+
type TEXT NOT NULL,
|
|
1089
|
+
weight REAL NOT NULL DEFAULT 1,
|
|
1090
|
+
metadata TEXT DEFAULT '{}',
|
|
1091
|
+
created_at TEXT NOT NULL
|
|
1092
|
+
);
|
|
1093
|
+
|
|
1094
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_type ON nodes(type);
|
|
1095
|
+
CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(from_id);
|
|
1096
|
+
CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(to_id);
|
|
1097
|
+
CREATE INDEX IF NOT EXISTS idx_edges_type ON edges(type);
|
|
1098
|
+
`);
|
|
1099
|
+
}
|
|
1100
|
+
mapNodeRow(row) {
|
|
1101
|
+
return {
|
|
1102
|
+
id: row.id,
|
|
1103
|
+
type: row.type,
|
|
1104
|
+
data: JSON.parse(row.data),
|
|
1105
|
+
created_at: row.created_at,
|
|
1106
|
+
updated_at: row.updated_at
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
mapEdgeRow(row) {
|
|
1110
|
+
return {
|
|
1111
|
+
id: row.id,
|
|
1112
|
+
from_id: row.from_id,
|
|
1113
|
+
to_id: row.to_id,
|
|
1114
|
+
type: row.type,
|
|
1115
|
+
weight: row.weight,
|
|
1116
|
+
created_at: row.created_at,
|
|
1117
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : {}
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1122
|
+
// src/context/graph.ts
|
|
1123
|
+
var ContextGraph = class {
|
|
1124
|
+
store;
|
|
1125
|
+
projectPath;
|
|
1126
|
+
constructor(projectPath, dbPath, store) {
|
|
1127
|
+
this.projectPath = projectPath;
|
|
1128
|
+
this.store = store ?? new ContextStore(projectPath, dbPath);
|
|
1129
|
+
}
|
|
1130
|
+
get projectRoot() {
|
|
1131
|
+
return this.projectPath;
|
|
1132
|
+
}
|
|
1133
|
+
async addNode(type, data) {
|
|
1134
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1135
|
+
const id = this.generateNodeId(type, data);
|
|
1136
|
+
const node = {
|
|
1137
|
+
id,
|
|
1138
|
+
type,
|
|
1139
|
+
data,
|
|
1140
|
+
created_at: now,
|
|
1141
|
+
updated_at: now
|
|
1142
|
+
};
|
|
1143
|
+
this.store.addNode(node);
|
|
1144
|
+
return node;
|
|
1145
|
+
}
|
|
1146
|
+
async getNode(type, id) {
|
|
1147
|
+
return this.store.getNodeByType(type, id);
|
|
1148
|
+
}
|
|
1149
|
+
async updateNode(type, id, updates) {
|
|
1150
|
+
const updated = this.store.updateNode(id, updates, (/* @__PURE__ */ new Date()).toISOString());
|
|
1151
|
+
if (updated && updated.type !== type) {
|
|
1152
|
+
throw new Error(`Type mismatch for node ${id}: expected ${type} but found ${updated.type}`);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
async deleteNode(_type, id) {
|
|
1156
|
+
this.store.deleteNode(id);
|
|
1157
|
+
}
|
|
1158
|
+
async addEdge(fromId, toId, type, metadata = {}, weight = 1) {
|
|
1159
|
+
const edge = {
|
|
1160
|
+
id: crypto.randomUUID(),
|
|
1161
|
+
from_id: fromId,
|
|
1162
|
+
to_id: toId,
|
|
1163
|
+
type,
|
|
1164
|
+
weight,
|
|
1165
|
+
metadata,
|
|
1166
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1167
|
+
};
|
|
1168
|
+
this.store.addEdge(edge);
|
|
1169
|
+
return edge;
|
|
1170
|
+
}
|
|
1171
|
+
async getEdges(nodeId, direction = "both") {
|
|
1172
|
+
return this.store.getEdges(nodeId, direction);
|
|
1173
|
+
}
|
|
1174
|
+
async getIncidentsForFile(filePath) {
|
|
1175
|
+
const fileNode = this.findFileNode(filePath);
|
|
1176
|
+
if (!fileNode) return [];
|
|
1177
|
+
const incidents = /* @__PURE__ */ new Map();
|
|
1178
|
+
const affectEdges = this.store.getEdges(fileNode.id, "in").filter((edge) => edge.type === "affects");
|
|
1179
|
+
for (const edge of affectEdges) {
|
|
1180
|
+
const changeId = edge.from_id;
|
|
1181
|
+
const leadEdges = this.store.getEdges(changeId, "out").filter((e) => e.type === "leadTo");
|
|
1182
|
+
const causedByEdges = this.store.getEdges(changeId, "in").filter((e) => e.type === "causedBy");
|
|
1183
|
+
for (const le of leadEdges) {
|
|
1184
|
+
const incident = this.store.getNodeByType("incident", le.to_id);
|
|
1185
|
+
if (incident) incidents.set(incident.id, incident);
|
|
1186
|
+
}
|
|
1187
|
+
for (const ce of causedByEdges) {
|
|
1188
|
+
const incident = this.store.getNodeByType("incident", ce.from_id);
|
|
1189
|
+
if (incident) incidents.set(incident.id, incident);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return Array.from(incidents.values());
|
|
1193
|
+
}
|
|
1194
|
+
async getPatternsForFile(filePath) {
|
|
1195
|
+
const normalized = this.normalizePath(filePath);
|
|
1196
|
+
const nodes = this.store.findNodesByType("pattern");
|
|
1197
|
+
return nodes.filter(
|
|
1198
|
+
(node) => node.data.appliesTo.some((pattern) => normalized.includes(pattern) || filePath.includes(pattern))
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
async getRecentChanges(limit) {
|
|
1202
|
+
const nodes = this.store.findNodesByType("change");
|
|
1203
|
+
return nodes.sort((a, b) => new Date(b.data.timestamp).getTime() - new Date(a.data.timestamp).getTime()).slice(0, limit);
|
|
1204
|
+
}
|
|
1205
|
+
async calculateFileRisk(filePath) {
|
|
1206
|
+
const fileNode = this.findFileNode(filePath);
|
|
1207
|
+
if (!fileNode) return "low";
|
|
1208
|
+
const incidentScore = Math.min(fileNode.data.incidentCount * 2, 6);
|
|
1209
|
+
const changeScore = Math.min(fileNode.data.changeCount, 4);
|
|
1210
|
+
const baseScore = this.riskLevelToScore(fileNode.data.riskLevel);
|
|
1211
|
+
const total = baseScore + incidentScore + changeScore;
|
|
1212
|
+
if (total >= 10) return "critical";
|
|
1213
|
+
if (total >= 7) return "high";
|
|
1214
|
+
if (total >= 4) return "medium";
|
|
1215
|
+
return "low";
|
|
1216
|
+
}
|
|
1217
|
+
async listNodes() {
|
|
1218
|
+
return this.store.listNodes();
|
|
1219
|
+
}
|
|
1220
|
+
async listEdges() {
|
|
1221
|
+
return this.store.listEdges();
|
|
1222
|
+
}
|
|
1223
|
+
async deleteEdge(id) {
|
|
1224
|
+
this.store.deleteEdge(id);
|
|
1225
|
+
}
|
|
1226
|
+
async getSnapshot() {
|
|
1227
|
+
return {
|
|
1228
|
+
nodes: this.store.listNodes(),
|
|
1229
|
+
edges: this.store.listEdges(),
|
|
1230
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
async applySnapshot(snapshot) {
|
|
1234
|
+
for (const node of snapshot.nodes) {
|
|
1235
|
+
const existing = this.store.getNode(node.id);
|
|
1236
|
+
if (!existing || this.isNewer(node.updated_at, existing.updated_at)) {
|
|
1237
|
+
this.store.upsertNode(node);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
for (const edge of snapshot.edges) {
|
|
1241
|
+
const existing = this.store.getEdge(edge.id);
|
|
1242
|
+
if (!existing) {
|
|
1243
|
+
this.store.upsertEdge(edge);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
generateNodeId(type, data) {
|
|
1248
|
+
if (type === "file") {
|
|
1249
|
+
const fileData = data;
|
|
1250
|
+
return this.normalizePath(fileData.path);
|
|
1251
|
+
}
|
|
1252
|
+
if (type === "change") {
|
|
1253
|
+
const changeData = data;
|
|
1254
|
+
if (changeData.commitHash) {
|
|
1255
|
+
return changeData.commitHash;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if (type === "linear-ticket") {
|
|
1259
|
+
const ticketData = data;
|
|
1260
|
+
return `linear:${ticketData.ticketId}`;
|
|
1261
|
+
}
|
|
1262
|
+
return crypto.randomUUID();
|
|
1263
|
+
}
|
|
1264
|
+
findFileNode(filePath) {
|
|
1265
|
+
const normalized = this.normalizePath(filePath);
|
|
1266
|
+
const nodes = this.store.findNodesByType("file");
|
|
1267
|
+
return nodes.find(
|
|
1268
|
+
(node) => node.id === normalized || this.normalizePath(node.data.path) === normalized || node.data.path === filePath
|
|
1269
|
+
) ?? null;
|
|
1270
|
+
}
|
|
1271
|
+
normalizePath(filePath) {
|
|
1272
|
+
return path3.resolve(this.projectPath, filePath);
|
|
1273
|
+
}
|
|
1274
|
+
riskLevelToScore(level) {
|
|
1275
|
+
switch (level) {
|
|
1276
|
+
case "critical":
|
|
1277
|
+
return 6;
|
|
1278
|
+
case "high":
|
|
1279
|
+
return 4;
|
|
1280
|
+
case "medium":
|
|
1281
|
+
return 2;
|
|
1282
|
+
default:
|
|
1283
|
+
return 0;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
isNewer(incoming, existing) {
|
|
1287
|
+
return new Date(incoming).getTime() >= new Date(existing).getTime();
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
export {
|
|
1292
|
+
recordToGlobalMemory,
|
|
1293
|
+
findCrossProjectPatterns,
|
|
1294
|
+
listTrackedProjects,
|
|
1295
|
+
getGlobalMemoryStats,
|
|
1296
|
+
updateGlobalMemoryMd,
|
|
1297
|
+
searchGlobalPatterns,
|
|
1298
|
+
isAIAvailable,
|
|
1299
|
+
runAIAnalysis,
|
|
1300
|
+
getStorage,
|
|
1301
|
+
GotchaPredictor,
|
|
1302
|
+
ContextGraph
|
|
1303
|
+
};
|
|
1304
|
+
//# sourceMappingURL=chunk-NIASHOAB.js.map
|