@plumpslabs/kuma 2.3.22 → 2.3.23
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/dist/index.js +1134 -1087
- package/package.json +1 -1
- package/packages/ide/studio/dist/index.js +55 -6
- package/packages/ide/studio/public/index.html +435 -311
package/dist/index.js
CHANGED
|
@@ -311,7 +311,7 @@ function createSchema(db) {
|
|
|
311
311
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
312
312
|
source_id TEXT NOT NULL REFERENCES nodes(id),
|
|
313
313
|
target_id TEXT NOT NULL REFERENCES nodes(id),
|
|
314
|
-
type TEXT NOT NULL CHECK(type IN ('calls','imports','defines','tests','routes','implements','extends','depends_on','owns','modified_by','contains','composes','flows_through','triggers','syncs_with')),
|
|
314
|
+
type TEXT NOT NULL CHECK(type IN ('calls','imports','defines','tests','routes','implements','extends','depends_on','owns','modified_by','contains','composes','flows_through','triggers','syncs_with','affects')),
|
|
315
315
|
weight REAL DEFAULT 1.0,
|
|
316
316
|
metadata TEXT DEFAULT '{}',
|
|
317
317
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
|
@@ -326,7 +326,7 @@ function createSchema(db) {
|
|
|
326
326
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
327
327
|
source_id TEXT NOT NULL REFERENCES nodes(id),
|
|
328
328
|
target_id TEXT NOT NULL REFERENCES nodes(id),
|
|
329
|
-
type TEXT NOT NULL CHECK(type IN ('calls','imports','defines','tests','routes','implements','extends','depends_on','owns','modified_by','contains','composes','flows_through','triggers','syncs_with')),
|
|
329
|
+
type TEXT NOT NULL CHECK(type IN ('calls','imports','defines','tests','routes','implements','extends','depends_on','owns','modified_by','contains','composes','flows_through','triggers','syncs_with','affects')),
|
|
330
330
|
weight REAL DEFAULT 1.0,
|
|
331
331
|
metadata TEXT DEFAULT '{}',
|
|
332
332
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
|
@@ -717,22 +717,20 @@ async function listResearchCache() {
|
|
|
717
717
|
async function recordChange(entry) {
|
|
718
718
|
try {
|
|
719
719
|
const db = await getDb();
|
|
720
|
-
const root = process.cwd();
|
|
721
|
-
const fullPath = path2.resolve(root, entry.filePath);
|
|
722
|
-
let previousContent = null;
|
|
723
|
-
if (entry.changeType === "modified" && fs2.existsSync(fullPath)) {
|
|
724
|
-
try {
|
|
725
|
-
previousContent = fs2.readFileSync(fullPath, "utf-8");
|
|
726
|
-
} catch {
|
|
727
|
-
}
|
|
728
|
-
} else if (entry.changeType === "created") {
|
|
729
|
-
previousContent = "";
|
|
730
|
-
}
|
|
731
720
|
db.run(
|
|
732
721
|
`INSERT INTO change_log (session_id, file_path, change_type, symbol, diff_summary, git_commit_hash, previous_content) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
733
|
-
[entry.sessionId ?? null, entry.filePath, entry.changeType, entry.symbol ?? null, entry.diffSummary ?? null, entry.gitCommitHash ?? null,
|
|
722
|
+
[entry.sessionId ?? null, entry.filePath, entry.changeType, entry.symbol ?? null, entry.diffSummary ?? null, entry.gitCommitHash ?? null, null]
|
|
734
723
|
);
|
|
735
724
|
saveDb();
|
|
725
|
+
try {
|
|
726
|
+
const countResult = db.exec("SELECT COUNT(*) FROM change_log");
|
|
727
|
+
const count = countResult[0]?.values?.[0] || 0;
|
|
728
|
+
if (count > 600) {
|
|
729
|
+
db.exec("DELETE FROM change_log WHERE id IN (SELECT id FROM change_log ORDER BY created_at ASC LIMIT 200)");
|
|
730
|
+
saveDb();
|
|
731
|
+
}
|
|
732
|
+
} catch {
|
|
733
|
+
}
|
|
736
734
|
} catch (err) {
|
|
737
735
|
console.error(`[KumaDB] Failed to record change: ${err}`);
|
|
738
736
|
}
|
|
@@ -1077,16 +1075,69 @@ async function runGarbageCollection() {
|
|
|
1077
1075
|
try {
|
|
1078
1076
|
const db = await getDb();
|
|
1079
1077
|
let removed = 0;
|
|
1078
|
+
const getCount = (sql) => {
|
|
1079
|
+
try {
|
|
1080
|
+
const result = db.exec(sql);
|
|
1081
|
+
return result[0]?.values?.[0] || 0;
|
|
1082
|
+
} catch {
|
|
1083
|
+
return 0;
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1080
1086
|
const orphanResult = db.exec(`DELETE FROM nodes WHERE id NOT IN (SELECT source_id FROM edges UNION SELECT target_id FROM edges) AND file_path IS NULL AND updated_at < strftime('%s','now','-30 days')`);
|
|
1081
1087
|
removed += orphanResult[0]?.values?.length || 0;
|
|
1088
|
+
const staleEdges = db.exec(`DELETE FROM edges WHERE weight < 0.1 AND updated_at < strftime('%s','now','-30 days')`);
|
|
1089
|
+
removed += staleEdges[0]?.values?.length || 0;
|
|
1082
1090
|
db.exec(`DELETE FROM tool_calls WHERE created_at < strftime('%s','now','-90 days')`);
|
|
1083
1091
|
db.exec(`DELETE FROM experiences WHERE created_at < strftime('%s','now','-90 days')`);
|
|
1092
|
+
const changeCount = getCount("SELECT COUNT(*) FROM change_log");
|
|
1093
|
+
if (changeCount > 500) {
|
|
1094
|
+
db.exec(`DELETE FROM change_log WHERE id IN (SELECT id FROM change_log ORDER BY created_at ASC LIMIT ${changeCount - 500})`);
|
|
1095
|
+
}
|
|
1096
|
+
const verifCount = getCount("SELECT COUNT(*) FROM verifications");
|
|
1097
|
+
if (verifCount > 100) {
|
|
1098
|
+
db.exec(`DELETE FROM verifications WHERE id IN (SELECT id FROM verifications ORDER BY created_at ASC LIMIT ${verifCount - 100})`);
|
|
1099
|
+
}
|
|
1100
|
+
const healthCount = getCount("SELECT COUNT(*) FROM health_snapshots");
|
|
1101
|
+
if (healthCount > 50) {
|
|
1102
|
+
db.exec(`DELETE FROM health_snapshots WHERE id IN (SELECT id FROM health_snapshots ORDER BY created_at ASC LIMIT ${healthCount - 50})`);
|
|
1103
|
+
}
|
|
1104
|
+
const auditCount = getCount("SELECT COUNT(*) FROM safety_audit");
|
|
1105
|
+
if (auditCount > 200) {
|
|
1106
|
+
db.exec(`DELETE FROM safety_audit WHERE id IN (SELECT id FROM safety_audit ORDER BY created_at ASC LIMIT ${auditCount - 200})`);
|
|
1107
|
+
}
|
|
1108
|
+
db.exec(`DELETE FROM research_cache WHERE updated_at < strftime('%s','now','-60 days')`);
|
|
1109
|
+
const secCount = getCount("SELECT COUNT(*) FROM security_findings");
|
|
1110
|
+
if (secCount > 100) {
|
|
1111
|
+
db.exec(`DELETE FROM security_findings WHERE id IN (SELECT id FROM security_findings ORDER BY created_at ASC LIMIT ${secCount - 100})`);
|
|
1112
|
+
}
|
|
1113
|
+
const ctxCount = getCount("SELECT COUNT(*) FROM context_notes");
|
|
1114
|
+
if (ctxCount > 200) {
|
|
1115
|
+
db.exec(`DELETE FROM context_notes WHERE id IN (SELECT id FROM context_notes ORDER BY created_at ASC LIMIT ${ctxCount - 200})`);
|
|
1116
|
+
}
|
|
1117
|
+
const benchCount = getCount("SELECT COUNT(*) FROM benchmarks");
|
|
1118
|
+
if (benchCount > 100) {
|
|
1119
|
+
db.exec(`DELETE FROM benchmarks WHERE id IN (SELECT id FROM benchmarks ORDER BY created_at ASC LIMIT ${benchCount - 100})`);
|
|
1120
|
+
}
|
|
1121
|
+
const decCount = getCount("SELECT COUNT(*) FROM decision_log");
|
|
1122
|
+
if (decCount > 200) {
|
|
1123
|
+
db.exec(`DELETE FROM decision_log WHERE id IN (SELECT id FROM decision_log ORDER BY created_at ASC LIMIT ${decCount - 200})`);
|
|
1124
|
+
}
|
|
1125
|
+
db.exec(`DELETE FROM known_gotchas WHERE file_path NOT IN (SELECT DISTINCT file_path FROM nodes WHERE file_path IS NOT NULL) AND file_path NOT LIKE '%::%'`);
|
|
1126
|
+
db.exec(`DELETE FROM scratch_entries WHERE created_at < strftime('%s','now','-7 days')`);
|
|
1127
|
+
const costCount = getCount("SELECT COUNT(*) FROM cost_tracking");
|
|
1128
|
+
if (costCount > 500) {
|
|
1129
|
+
db.exec(`DELETE FROM cost_tracking WHERE id IN (SELECT id FROM cost_tracking ORDER BY created_at ASC LIMIT ${costCount - 500})`);
|
|
1130
|
+
}
|
|
1131
|
+
const sessCount = getCount("SELECT COUNT(*) FROM sessions");
|
|
1132
|
+
if (sessCount > 50) {
|
|
1133
|
+
db.exec(`DELETE FROM sessions WHERE id IN (SELECT id FROM sessions ORDER BY started_at ASC LIMIT ${sessCount - 50})`);
|
|
1134
|
+
}
|
|
1084
1135
|
try {
|
|
1085
1136
|
db.exec("VACUUM");
|
|
1086
1137
|
} catch {
|
|
1087
1138
|
}
|
|
1088
1139
|
saveDb();
|
|
1089
|
-
return `\u{1F9F9} GC complete
|
|
1140
|
+
return `\u{1F9F9} GC complete \u2014 cleaned 17 tables. Database vacuumed.`;
|
|
1090
1141
|
} catch (err) {
|
|
1091
1142
|
return `\u274C GC failed: ${err}`;
|
|
1092
1143
|
}
|
|
@@ -1324,7 +1375,9 @@ var init_sessionMemory = __esm({
|
|
|
1324
1375
|
searchResults: new Map(parsed.searchResults || []),
|
|
1325
1376
|
dependencyGraph: new Map(parsed.dependencyGraph || []),
|
|
1326
1377
|
toolCalls: parsed.toolCalls || [],
|
|
1327
|
-
conventions: parsed.conventions
|
|
1378
|
+
conventions: parsed.conventions,
|
|
1379
|
+
recordings: parsed.recordings || { archFlows: 0, gotchas: 0, decisions: 0, researchSaves: 0, total: 0 },
|
|
1380
|
+
metrics: parsed.metrics || { filesRead: 0, filesEdited: 0, researchTimeSaved: 0 }
|
|
1328
1381
|
};
|
|
1329
1382
|
this.initialized = true;
|
|
1330
1383
|
console.error(`[SessionMemory] Loaded persistent session memory.json`);
|
|
@@ -1342,7 +1395,9 @@ var init_sessionMemory = __esm({
|
|
|
1342
1395
|
failedFiles: /* @__PURE__ */ new Map(),
|
|
1343
1396
|
searchResults: /* @__PURE__ */ new Map(),
|
|
1344
1397
|
dependencyGraph: /* @__PURE__ */ new Map(),
|
|
1345
|
-
toolCalls: []
|
|
1398
|
+
toolCalls: [],
|
|
1399
|
+
recordings: { archFlows: 0, gotchas: 0, decisions: 0, researchSaves: 0, total: 0 },
|
|
1400
|
+
metrics: { filesRead: 0, filesEdited: 0, researchTimeSaved: 0 }
|
|
1346
1401
|
};
|
|
1347
1402
|
this.initialized = true;
|
|
1348
1403
|
this.save();
|
|
@@ -1490,7 +1545,9 @@ No unresolved issues.`;
|
|
|
1490
1545
|
searchResults: Array.from(this.state.searchResults.entries()),
|
|
1491
1546
|
dependencyGraph: Array.from(this.state.dependencyGraph.entries()),
|
|
1492
1547
|
toolCalls: this.state.toolCalls,
|
|
1493
|
-
conventions: this.state.conventions
|
|
1548
|
+
conventions: this.state.conventions,
|
|
1549
|
+
recordings: this.state.recordings,
|
|
1550
|
+
metrics: this.state.metrics
|
|
1494
1551
|
};
|
|
1495
1552
|
fs3.writeFileSync(path3.join(kumaDir, "memory.json"), JSON.stringify(serialized, null, 2), "utf-8");
|
|
1496
1553
|
} catch (err) {
|
|
@@ -1516,6 +1573,10 @@ No unresolved issues.`;
|
|
|
1516
1573
|
existing.modifiedAt = Date.now();
|
|
1517
1574
|
existing.status = "modified";
|
|
1518
1575
|
} else {
|
|
1576
|
+
if (this.state.modifiedFiles.size >= 200) {
|
|
1577
|
+
const oldest = this.state.modifiedFiles.keys().next().value;
|
|
1578
|
+
if (oldest) this.state.modifiedFiles.delete(oldest);
|
|
1579
|
+
}
|
|
1519
1580
|
this.state.modifiedFiles.set(filePath, {
|
|
1520
1581
|
filePath,
|
|
1521
1582
|
modifiedAt: Date.now(),
|
|
@@ -1547,7 +1608,14 @@ No unresolved issues.`;
|
|
|
1547
1608
|
timestamp: Date.now(),
|
|
1548
1609
|
resolved: false
|
|
1549
1610
|
});
|
|
1611
|
+
if (failures.length > 10) {
|
|
1612
|
+
failures.splice(0, failures.length - 10);
|
|
1613
|
+
}
|
|
1550
1614
|
this.state.failedFiles.set(task, failures);
|
|
1615
|
+
if (this.state.failedFiles.size > 100) {
|
|
1616
|
+
const oldest = this.state.failedFiles.keys().next().value;
|
|
1617
|
+
if (oldest) this.state.failedFiles.delete(oldest);
|
|
1618
|
+
}
|
|
1551
1619
|
this.save();
|
|
1552
1620
|
this.ensureMemoriesDir();
|
|
1553
1621
|
this.writeMemoryFile("known-issues", this.generateKnownIssuesMd());
|
|
@@ -1572,6 +1640,10 @@ No unresolved issues.`;
|
|
|
1572
1640
|
}
|
|
1573
1641
|
addSearchResult(query, files) {
|
|
1574
1642
|
this.ensureInit();
|
|
1643
|
+
if (this.state.searchResults.size >= 50) {
|
|
1644
|
+
const oldest = this.state.searchResults.keys().next().value;
|
|
1645
|
+
if (oldest) this.state.searchResults.delete(oldest);
|
|
1646
|
+
}
|
|
1575
1647
|
this.state.searchResults.set(query, files);
|
|
1576
1648
|
this.save();
|
|
1577
1649
|
}
|
|
@@ -1580,6 +1652,9 @@ No unresolved issues.`;
|
|
|
1580
1652
|
const deps = this.state.dependencyGraph.get(file) ?? [];
|
|
1581
1653
|
if (!deps.includes(dependsOn)) {
|
|
1582
1654
|
deps.push(dependsOn);
|
|
1655
|
+
if (deps.length > 20) {
|
|
1656
|
+
deps.splice(0, deps.length - 20);
|
|
1657
|
+
}
|
|
1583
1658
|
this.state.dependencyGraph.set(file, deps);
|
|
1584
1659
|
this.save();
|
|
1585
1660
|
}
|
|
@@ -1879,6 +1954,95 @@ No unresolved issues.`;
|
|
|
1879
1954
|
return { isLooping: false };
|
|
1880
1955
|
}
|
|
1881
1956
|
// ============================================================
|
|
1957
|
+
// RECORDING TRACKING — Enforcement & Feedback
|
|
1958
|
+
// ============================================================
|
|
1959
|
+
/**
|
|
1960
|
+
* Increment recording counter for a specific type.
|
|
1961
|
+
* Called by kumaMemoryTool after successful record operations.
|
|
1962
|
+
*/
|
|
1963
|
+
recordMemoryAction(type) {
|
|
1964
|
+
this.ensureInit();
|
|
1965
|
+
switch (type) {
|
|
1966
|
+
case "arch_flow":
|
|
1967
|
+
this.state.recordings.archFlows++;
|
|
1968
|
+
break;
|
|
1969
|
+
case "gotcha":
|
|
1970
|
+
this.state.recordings.gotchas++;
|
|
1971
|
+
break;
|
|
1972
|
+
case "decision":
|
|
1973
|
+
this.state.recordings.decisions++;
|
|
1974
|
+
break;
|
|
1975
|
+
case "research_save":
|
|
1976
|
+
this.state.recordings.researchSaves++;
|
|
1977
|
+
break;
|
|
1978
|
+
}
|
|
1979
|
+
this.state.recordings.total++;
|
|
1980
|
+
this.save();
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Get recording summary for this session.
|
|
1984
|
+
* Used by guard to detect if agent hasn't recorded anything.
|
|
1985
|
+
*/
|
|
1986
|
+
getRecordingSummary() {
|
|
1987
|
+
this.ensureInit();
|
|
1988
|
+
const r = this.state.recordings;
|
|
1989
|
+
const missing = [];
|
|
1990
|
+
if (r.archFlows === 0) missing.push("arch_flow");
|
|
1991
|
+
if (r.gotchas === 0) missing.push("gotcha");
|
|
1992
|
+
if (r.decisions === 0) missing.push("decision");
|
|
1993
|
+
if (r.researchSaves === 0) missing.push("research_save");
|
|
1994
|
+
return {
|
|
1995
|
+
...r,
|
|
1996
|
+
hasAnyRecordings: r.total > 0,
|
|
1997
|
+
missingRecordings: missing
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
// ============================================================
|
|
2001
|
+
// SESSION METRICS — Time saved tracking
|
|
2002
|
+
// ============================================================
|
|
2003
|
+
/**
|
|
2004
|
+
* Track a file read operation.
|
|
2005
|
+
* Estimates time saved if research_cache exists for this file.
|
|
2006
|
+
*/
|
|
2007
|
+
trackFileRead(filePath) {
|
|
2008
|
+
this.ensureInit();
|
|
2009
|
+
this.state.metrics.filesRead++;
|
|
2010
|
+
try {
|
|
2011
|
+
const researchDir = path3.join(this.state.projectRoot, ".kuma", "research");
|
|
2012
|
+
const cacheFile = path3.join(researchDir, `${filePath}.json`);
|
|
2013
|
+
if (fs3.existsSync(cacheFile)) {
|
|
2014
|
+
this.state.metrics.researchTimeSaved += 4e3;
|
|
2015
|
+
}
|
|
2016
|
+
} catch {
|
|
2017
|
+
}
|
|
2018
|
+
this.save();
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Track a file edit operation.
|
|
2022
|
+
*/
|
|
2023
|
+
trackFileEdit(_filePath) {
|
|
2024
|
+
this.ensureInit();
|
|
2025
|
+
this.state.metrics.filesEdited++;
|
|
2026
|
+
this.save();
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Get metrics summary for session.
|
|
2030
|
+
*/
|
|
2031
|
+
getMetricsSummary() {
|
|
2032
|
+
this.ensureInit();
|
|
2033
|
+
const m = this.state.metrics;
|
|
2034
|
+
const durationMs = Date.now() - this.state.startTime;
|
|
2035
|
+
const durationMin = Math.round(durationMs / 6e4);
|
|
2036
|
+
const savedSec = Math.round(m.researchTimeSaved / 1e3);
|
|
2037
|
+
return {
|
|
2038
|
+
filesRead: m.filesRead,
|
|
2039
|
+
filesEdited: m.filesEdited,
|
|
2040
|
+
researchTimeSaved: m.researchTimeSaved,
|
|
2041
|
+
researchTimeSavedFormatted: savedSec > 60 ? `${Math.round(savedSec / 60)}m ${savedSec % 60}s` : `${savedSec}s`,
|
|
2042
|
+
sessionDuration: durationMin > 60 ? `${Math.round(durationMin / 60)}h ${durationMin % 60}m` : `${durationMin}m`
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
// ============================================================
|
|
1882
2046
|
// V3: Change Tracking (Selective Undo)
|
|
1883
2047
|
// ============================================================
|
|
1884
2048
|
/**
|
|
@@ -1997,7 +2161,7 @@ async function detectStaleNodes() {
|
|
|
1997
2161
|
SELECT id, type, name, file_path FROM nodes
|
|
1998
2162
|
WHERE file_path IS NOT NULL
|
|
1999
2163
|
AND length(file_path) > 0
|
|
2000
|
-
ORDER BY updated_at DESC LIMIT
|
|
2164
|
+
ORDER BY updated_at DESC LIMIT 2000
|
|
2001
2165
|
`);
|
|
2002
2166
|
while (stmt.step()) {
|
|
2003
2167
|
const row = stmt.getAsObject();
|
|
@@ -2182,16 +2346,18 @@ __export(kumaGraph_exports, {
|
|
|
2182
2346
|
formatFlow: () => formatFlow,
|
|
2183
2347
|
formatImpact: () => formatImpact,
|
|
2184
2348
|
getGraphStats: () => getGraphStats,
|
|
2185
|
-
listFederatedReferences: () => listFederatedReferences,
|
|
2186
2349
|
nodeId: () => nodeId,
|
|
2350
|
+
propagateImpact: () => propagateImpact,
|
|
2187
2351
|
queryGraph: () => queryGraph,
|
|
2352
|
+
recordAffects: () => recordAffects,
|
|
2188
2353
|
recordApiRoute: () => recordApiRoute,
|
|
2189
2354
|
recordDomainFlow: () => recordDomainFlow,
|
|
2355
|
+
recordFeature: () => recordFeature,
|
|
2190
2356
|
recordFileDefinition: () => recordFileDefinition,
|
|
2191
2357
|
recordFunctionCall: () => recordFunctionCall,
|
|
2192
2358
|
recordImport: () => recordImport,
|
|
2193
2359
|
recordTestRelation: () => recordTestRelation,
|
|
2194
|
-
|
|
2360
|
+
retrieveForTask: () => retrieveForTask,
|
|
2195
2361
|
searchGraph: () => searchGraph,
|
|
2196
2362
|
traceFlow: () => traceFlow,
|
|
2197
2363
|
upsertNode: () => upsertNode
|
|
@@ -2199,6 +2365,60 @@ __export(kumaGraph_exports, {
|
|
|
2199
2365
|
function nodeId(type, name) {
|
|
2200
2366
|
return `${type}::${name}`;
|
|
2201
2367
|
}
|
|
2368
|
+
async function pruneGraphIfNeeded() {
|
|
2369
|
+
try {
|
|
2370
|
+
const db = await getDb();
|
|
2371
|
+
const getCount = (sql) => {
|
|
2372
|
+
try {
|
|
2373
|
+
const result = db.exec(sql);
|
|
2374
|
+
return result[0]?.values?.[0] || 0;
|
|
2375
|
+
} catch {
|
|
2376
|
+
return 0;
|
|
2377
|
+
}
|
|
2378
|
+
};
|
|
2379
|
+
const nodeCount = getCount("SELECT COUNT(*) FROM nodes");
|
|
2380
|
+
const edgeCount = getCount("SELECT COUNT(*) FROM edges");
|
|
2381
|
+
if (nodeCount <= GRAPH_LIMITS.maxNodes && edgeCount <= GRAPH_LIMITS.maxEdges) {
|
|
2382
|
+
return { pruned: false, removedNodes: 0, removedEdges: 0 };
|
|
2383
|
+
}
|
|
2384
|
+
let removedNodes = 0;
|
|
2385
|
+
let removedEdges = 0;
|
|
2386
|
+
if (edgeCount > GRAPH_LIMITS.maxEdges) {
|
|
2387
|
+
const result = db.exec(`DELETE FROM edges WHERE weight < ${GRAPH_LIMITS.minWeight} AND metadata NOT LIKE '%stale:0%'`);
|
|
2388
|
+
removedEdges = result[0]?.values?.length || 0;
|
|
2389
|
+
}
|
|
2390
|
+
const currentNodes = getCount("SELECT COUNT(*) FROM nodes");
|
|
2391
|
+
if (currentNodes > GRAPH_LIMITS.maxNodes) {
|
|
2392
|
+
const result = db.exec(`
|
|
2393
|
+
DELETE FROM nodes WHERE id IN (
|
|
2394
|
+
SELECT id FROM nodes
|
|
2395
|
+
WHERE id NOT IN (SELECT source_id FROM edges UNION SELECT target_id FROM edges)
|
|
2396
|
+
ORDER BY updated_at ASC
|
|
2397
|
+
LIMIT ${GRAPH_LIMITS.pruneBatch}
|
|
2398
|
+
)
|
|
2399
|
+
`);
|
|
2400
|
+
removedNodes = result[0]?.values?.length || 0;
|
|
2401
|
+
}
|
|
2402
|
+
const finalNodes = getCount("SELECT COUNT(*) FROM nodes");
|
|
2403
|
+
if (finalNodes > GRAPH_LIMITS.maxNodes) {
|
|
2404
|
+
const excess = finalNodes - GRAPH_LIMITS.maxNodes + 100;
|
|
2405
|
+
const result = db.exec(`
|
|
2406
|
+
DELETE FROM nodes WHERE id IN (
|
|
2407
|
+
SELECT id FROM nodes ORDER BY updated_at ASC LIMIT ${excess}
|
|
2408
|
+
)
|
|
2409
|
+
`);
|
|
2410
|
+
removedNodes += result[0]?.values?.length || 0;
|
|
2411
|
+
}
|
|
2412
|
+
if (removedNodes > 0 || removedEdges > 0) {
|
|
2413
|
+
saveDb(db);
|
|
2414
|
+
return { pruned: true, removedNodes, removedEdges };
|
|
2415
|
+
}
|
|
2416
|
+
return { pruned: false, removedNodes: 0, removedEdges: 0 };
|
|
2417
|
+
} catch (err) {
|
|
2418
|
+
console.error(`[KumaGraph] Prune failed: ${err}`);
|
|
2419
|
+
return { pruned: false, removedNodes: 0, removedEdges: 0 };
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2202
2422
|
async function upsertNode(node) {
|
|
2203
2423
|
try {
|
|
2204
2424
|
const db = await getDb();
|
|
@@ -2219,6 +2439,14 @@ async function upsertNode(node) {
|
|
|
2219
2439
|
} catch {
|
|
2220
2440
|
}
|
|
2221
2441
|
saveDb(db);
|
|
2442
|
+
try {
|
|
2443
|
+
const countResult = db.exec("SELECT COUNT(*) FROM nodes");
|
|
2444
|
+
const nodeCount = countResult[0]?.values?.[0] || 0;
|
|
2445
|
+
if (nodeCount % 50 === 0 && nodeCount > GRAPH_LIMITS.maxNodes * 0.8) {
|
|
2446
|
+
await pruneGraphIfNeeded();
|
|
2447
|
+
}
|
|
2448
|
+
} catch {
|
|
2449
|
+
}
|
|
2222
2450
|
} catch (err) {
|
|
2223
2451
|
console.error(`[KumaGraph] Failed to upsert node: ${err}`);
|
|
2224
2452
|
}
|
|
@@ -2327,6 +2555,14 @@ async function recordDomainFlow(params) {
|
|
|
2327
2555
|
metadata: { relation: hop.relation, description: hop.description }
|
|
2328
2556
|
});
|
|
2329
2557
|
edgeCount++;
|
|
2558
|
+
await addEdge({
|
|
2559
|
+
sourceId: fromId,
|
|
2560
|
+
targetId: toId,
|
|
2561
|
+
type: "affects",
|
|
2562
|
+
weight: 0.8,
|
|
2563
|
+
metadata: { reason: "arch_flow", domain: params.domain }
|
|
2564
|
+
});
|
|
2565
|
+
edgeCount++;
|
|
2330
2566
|
await addEdge({
|
|
2331
2567
|
sourceId: domainId,
|
|
2332
2568
|
targetId: fromId,
|
|
@@ -2403,6 +2639,14 @@ async function recordDomainFlow(params) {
|
|
|
2403
2639
|
metadata: { domain: params.domain }
|
|
2404
2640
|
});
|
|
2405
2641
|
edgeCount++;
|
|
2642
|
+
await addEdge({
|
|
2643
|
+
sourceId: domainId,
|
|
2644
|
+
targetId: fileId,
|
|
2645
|
+
type: "affects",
|
|
2646
|
+
weight: 0.7,
|
|
2647
|
+
metadata: { reason: "arch_flow" }
|
|
2648
|
+
});
|
|
2649
|
+
edgeCount++;
|
|
2406
2650
|
} catch {
|
|
2407
2651
|
}
|
|
2408
2652
|
}
|
|
@@ -2430,6 +2674,214 @@ async function clearGraph() {
|
|
|
2430
2674
|
return -1;
|
|
2431
2675
|
}
|
|
2432
2676
|
}
|
|
2677
|
+
async function recordFeature(params) {
|
|
2678
|
+
let nodeCount = 0;
|
|
2679
|
+
let edgeCount = 0;
|
|
2680
|
+
try {
|
|
2681
|
+
const featureId = nodeId("feature", params.name);
|
|
2682
|
+
await upsertNode({
|
|
2683
|
+
id: featureId,
|
|
2684
|
+
type: "feature",
|
|
2685
|
+
name: params.name,
|
|
2686
|
+
metadata: {
|
|
2687
|
+
description: params.description || "",
|
|
2688
|
+
tags: params.tags || [],
|
|
2689
|
+
risk: params.risk || "medium"
|
|
2690
|
+
}
|
|
2691
|
+
});
|
|
2692
|
+
nodeCount++;
|
|
2693
|
+
if (params.files) {
|
|
2694
|
+
for (const fp of params.files) {
|
|
2695
|
+
const fileId = nodeId("file", fp);
|
|
2696
|
+
await upsertNode({ id: fileId, type: "file", name: fp });
|
|
2697
|
+
await addEdge({
|
|
2698
|
+
sourceId: featureId,
|
|
2699
|
+
targetId: fileId,
|
|
2700
|
+
type: "owns",
|
|
2701
|
+
weight: 1,
|
|
2702
|
+
metadata: { feature: params.name }
|
|
2703
|
+
});
|
|
2704
|
+
nodeCount++;
|
|
2705
|
+
edgeCount++;
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
flushDb();
|
|
2709
|
+
return { nodeCount, edgeCount };
|
|
2710
|
+
} catch (err) {
|
|
2711
|
+
console.error(`[KumaGraph] Failed to record feature: ${err}`);
|
|
2712
|
+
return { nodeCount, edgeCount };
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
async function recordAffects(sourceId, targetId, weight = 0.8, reason) {
|
|
2716
|
+
await addEdge({
|
|
2717
|
+
sourceId,
|
|
2718
|
+
targetId,
|
|
2719
|
+
type: "affects",
|
|
2720
|
+
weight,
|
|
2721
|
+
metadata: { reason: reason || "manual" }
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
async function propagateImpact(startNodeId, maxDepth = 5, minWeight = 0.1) {
|
|
2725
|
+
try {
|
|
2726
|
+
const db = await getDb();
|
|
2727
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2728
|
+
const results = [];
|
|
2729
|
+
const queue = [[startNodeId, 0, 1]];
|
|
2730
|
+
while (queue.length > 0) {
|
|
2731
|
+
const [currentId, depth, accWeight] = queue.shift();
|
|
2732
|
+
if (visited.has(currentId) || depth > maxDepth) continue;
|
|
2733
|
+
visited.add(currentId);
|
|
2734
|
+
const stmt = db.prepare(`
|
|
2735
|
+
SELECT e.target_id, e.weight, n.type, n.name, n.file_path
|
|
2736
|
+
FROM edges e
|
|
2737
|
+
LEFT JOIN nodes n ON n.id = e.target_id
|
|
2738
|
+
WHERE e.source_id = ? AND e.type = 'affects' AND e.weight >= ?
|
|
2739
|
+
`);
|
|
2740
|
+
stmt.bind([currentId, minWeight]);
|
|
2741
|
+
while (stmt.step()) {
|
|
2742
|
+
const row = stmt.getAsObject();
|
|
2743
|
+
const targetId = row.target_id;
|
|
2744
|
+
const edgeWeight = row.weight || 0.8;
|
|
2745
|
+
const propagatedWeight = accWeight * edgeWeight;
|
|
2746
|
+
if (!visited.has(targetId) && propagatedWeight >= minWeight) {
|
|
2747
|
+
results.push({
|
|
2748
|
+
id: targetId,
|
|
2749
|
+
type: row.type,
|
|
2750
|
+
name: row.name,
|
|
2751
|
+
depth: depth + 1,
|
|
2752
|
+
weight: Math.round(propagatedWeight * 100) / 100,
|
|
2753
|
+
filePath: row.file_path || void 0
|
|
2754
|
+
});
|
|
2755
|
+
queue.push([targetId, depth + 1, propagatedWeight]);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
stmt.free();
|
|
2759
|
+
}
|
|
2760
|
+
return results.sort((a, b) => b.weight - a.weight);
|
|
2761
|
+
} catch (err) {
|
|
2762
|
+
console.error(`[KumaGraph] Failed to propagate impact: ${err}`);
|
|
2763
|
+
return [];
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
async function retrieveForTask(taskDescription, maxNodes = 10) {
|
|
2767
|
+
try {
|
|
2768
|
+
const db = await getDb();
|
|
2769
|
+
const keywords = taskDescription.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2);
|
|
2770
|
+
if (keywords.length === 0) {
|
|
2771
|
+
return `\u{1F50D} **Task Retrieval** \u2014 No meaningful keywords in "${taskDescription}".`;
|
|
2772
|
+
}
|
|
2773
|
+
const allNodes = [];
|
|
2774
|
+
try {
|
|
2775
|
+
const ftsQuery = keywords.join(" OR ");
|
|
2776
|
+
const stmt = db.prepare(`
|
|
2777
|
+
SELECT n.id, n.type, n.name, n.file_path
|
|
2778
|
+
FROM nodes_fts f
|
|
2779
|
+
JOIN nodes n ON n.rowid = f.rowid
|
|
2780
|
+
WHERE nodes_fts MATCH ?
|
|
2781
|
+
LIMIT ?
|
|
2782
|
+
`);
|
|
2783
|
+
stmt.bind([ftsQuery, maxNodes * 2]);
|
|
2784
|
+
while (stmt.step()) {
|
|
2785
|
+
const row = stmt.getAsObject();
|
|
2786
|
+
allNodes.push({
|
|
2787
|
+
id: row.id,
|
|
2788
|
+
type: row.type,
|
|
2789
|
+
name: row.name,
|
|
2790
|
+
filePath: row.file_path || void 0,
|
|
2791
|
+
relevance: 0.8
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
stmt.free();
|
|
2795
|
+
} catch {
|
|
2796
|
+
}
|
|
2797
|
+
if (allNodes.length === 0) {
|
|
2798
|
+
for (const kw of keywords) {
|
|
2799
|
+
const stmt = db.prepare(`
|
|
2800
|
+
SELECT id, type, name, file_path FROM nodes
|
|
2801
|
+
WHERE name LIKE ? OR file_path LIKE ?
|
|
2802
|
+
LIMIT ?
|
|
2803
|
+
`);
|
|
2804
|
+
stmt.bind([`%${kw}%`, `%${kw}%`, maxNodes]);
|
|
2805
|
+
while (stmt.step()) {
|
|
2806
|
+
const row = stmt.getAsObject();
|
|
2807
|
+
allNodes.push({
|
|
2808
|
+
id: row.id,
|
|
2809
|
+
type: row.type,
|
|
2810
|
+
name: row.name,
|
|
2811
|
+
filePath: row.file_path || void 0,
|
|
2812
|
+
relevance: 0.6
|
|
2813
|
+
});
|
|
2814
|
+
}
|
|
2815
|
+
stmt.free();
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2819
|
+
const prioritized = [];
|
|
2820
|
+
const typePriority = {
|
|
2821
|
+
feature: 10,
|
|
2822
|
+
feature_domain: 9,
|
|
2823
|
+
class: 8,
|
|
2824
|
+
api_route: 7,
|
|
2825
|
+
db_table: 6,
|
|
2826
|
+
file: 5,
|
|
2827
|
+
function: 4,
|
|
2828
|
+
component: 3,
|
|
2829
|
+
gotcha: 2,
|
|
2830
|
+
decision: 1
|
|
2831
|
+
};
|
|
2832
|
+
for (const node of allNodes) {
|
|
2833
|
+
if (!seen.has(node.id)) {
|
|
2834
|
+
seen.add(node.id);
|
|
2835
|
+
node.relevance += typePriority[node.type] || 0;
|
|
2836
|
+
prioritized.push(node);
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
prioritized.sort((a, b) => b.relevance - a.relevance);
|
|
2840
|
+
const topNodes = prioritized.slice(0, maxNodes);
|
|
2841
|
+
if (topNodes.length === 0) {
|
|
2842
|
+
return `\u{1F50D} **Task Retrieval** \u2014 No relevant nodes found for "${taskDescription}".
|
|
2843
|
+
|
|
2844
|
+
\u{1F4A1} Try recording features, arch_flows, or gotchas first.`;
|
|
2845
|
+
}
|
|
2846
|
+
const lines = [
|
|
2847
|
+
`\u{1F50D} **Task Context** \u2014 "${taskDescription}"`,
|
|
2848
|
+
`\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`,
|
|
2849
|
+
`\u{1F4CA} ${topNodes.length} relevant node(s) found
|
|
2850
|
+
`
|
|
2851
|
+
];
|
|
2852
|
+
for (const node of topNodes) {
|
|
2853
|
+
const emoji = node.type === "feature" ? "\u2B50" : node.type === "feature_domain" ? "\u{1F3DB}\uFE0F" : node.type === "class" ? "\u{1F3D7}\uFE0F" : node.type === "api_route" ? "\u{1F310}" : node.type === "db_table" ? "\u{1F5C4}\uFE0F" : node.type === "file" ? "\u{1F4C4}" : node.type === "function" ? "\u{1F527}" : node.type === "component" ? "\u25C7" : node.type === "gotcha" ? "\u26A0\uFE0F" : "\u{1F4CC}";
|
|
2854
|
+
lines.push(`${emoji} **${node.name}** (${node.type})`);
|
|
2855
|
+
if (node.filePath) lines.push(` \u{1F4CD} ${node.filePath}`);
|
|
2856
|
+
try {
|
|
2857
|
+
const stmt = db.prepare(`
|
|
2858
|
+
SELECT e.type AS edgeType, n.type AS nodeType, n.name AS nodeName
|
|
2859
|
+
FROM edges e
|
|
2860
|
+
LEFT JOIN nodes n ON n.id = (CASE WHEN e.source_id = ? THEN e.target_id ELSE e.source_id END)
|
|
2861
|
+
WHERE (e.source_id = ? OR e.target_id = ?) AND n.id IS NOT NULL
|
|
2862
|
+
ORDER BY e.weight DESC
|
|
2863
|
+
LIMIT 3
|
|
2864
|
+
`);
|
|
2865
|
+
stmt.bind([node.id, node.id, node.id]);
|
|
2866
|
+
const connections = [];
|
|
2867
|
+
while (stmt.step()) {
|
|
2868
|
+
const row = stmt.getAsObject();
|
|
2869
|
+
connections.push(`${row.edgeType}\u2192${row.nodeName}`);
|
|
2870
|
+
}
|
|
2871
|
+
stmt.free();
|
|
2872
|
+
if (connections.length > 0) {
|
|
2873
|
+
lines.push(` \u{1F517} ${connections.join(", ")}`);
|
|
2874
|
+
}
|
|
2875
|
+
} catch {
|
|
2876
|
+
}
|
|
2877
|
+
lines.push("");
|
|
2878
|
+
}
|
|
2879
|
+
lines.push("\u{1F4A1} Use this context instead of scanning the full codebase.");
|
|
2880
|
+
return lines.join("\n");
|
|
2881
|
+
} catch (err) {
|
|
2882
|
+
return `Error retrieving task context: ${err}`;
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2433
2885
|
async function queryGraph(params) {
|
|
2434
2886
|
try {
|
|
2435
2887
|
const db = await getDb();
|
|
@@ -2701,27 +3153,27 @@ async function codebaseSearchFallback(query, limit = 20) {
|
|
|
2701
3153
|
if (results.length >= limit || depth > MAX_WALK_DEPTH) return;
|
|
2702
3154
|
let entries = [];
|
|
2703
3155
|
try {
|
|
2704
|
-
entries =
|
|
3156
|
+
entries = fs26.readdirSync(dir);
|
|
2705
3157
|
} catch {
|
|
2706
3158
|
return;
|
|
2707
3159
|
}
|
|
2708
3160
|
for (const entry of entries) {
|
|
2709
3161
|
if (entry.startsWith(".") || entry === "node_modules" || entry === "dist" || entry === "build") continue;
|
|
2710
|
-
const full =
|
|
3162
|
+
const full = path26.join(dir, entry);
|
|
2711
3163
|
try {
|
|
2712
|
-
const stat =
|
|
3164
|
+
const stat = fs26.statSync(full);
|
|
2713
3165
|
if (stat.isDirectory()) {
|
|
2714
3166
|
walk2(full, depth + 1);
|
|
2715
3167
|
} else if (entry.toLowerCase().includes(query.toLowerCase())) {
|
|
2716
|
-
results.push(
|
|
3168
|
+
results.push(path26.relative(root, full));
|
|
2717
3169
|
}
|
|
2718
3170
|
} catch {
|
|
2719
3171
|
}
|
|
2720
3172
|
}
|
|
2721
3173
|
};
|
|
2722
3174
|
var walk = walk2;
|
|
2723
|
-
const
|
|
2724
|
-
const
|
|
3175
|
+
const fs26 = await import("fs");
|
|
3176
|
+
const path26 = await import("path");
|
|
2725
3177
|
const root = process.cwd();
|
|
2726
3178
|
const results = [];
|
|
2727
3179
|
const MAX_WALK_DEPTH = 8;
|
|
@@ -2741,114 +3193,19 @@ async function codebaseSearchFallback(query, limit = 20) {
|
|
|
2741
3193
|
}
|
|
2742
3194
|
}
|
|
2743
3195
|
}
|
|
2744
|
-
async function resolveFederatedNode(uri) {
|
|
2745
|
-
try {
|
|
2746
|
-
if (!uri.startsWith("kuma://")) {
|
|
2747
|
-
return `\u26A0\uFE0F Invalid URI scheme: "${uri.substring(0, 20)}...". Use kuma://project/node-id`;
|
|
2748
|
-
}
|
|
2749
|
-
const pathPart = uri.replace("kuma://", "");
|
|
2750
|
-
const firstSlash = pathPart.indexOf("/");
|
|
2751
|
-
if (firstSlash === -1) {
|
|
2752
|
-
return `\u26A0\uFE0F Invalid federated URI: "${uri}". Format: kuma://<project>/<node-id>`;
|
|
2753
|
-
}
|
|
2754
|
-
const projectName = pathPart.substring(0, firstSlash);
|
|
2755
|
-
const nodeRef = pathPart.substring(firstSlash + 1);
|
|
2756
|
-
const db = await getDb();
|
|
2757
|
-
const stmt = db.prepare(`
|
|
2758
|
-
SELECT id, type, name, file_path, metadata FROM nodes
|
|
2759
|
-
WHERE id = ? OR name LIKE ?
|
|
2760
|
-
LIMIT 5
|
|
2761
|
-
`);
|
|
2762
|
-
stmt.bind([nodeRef, `%${nodeRef}%`]);
|
|
2763
|
-
const results = [];
|
|
2764
|
-
while (stmt.step()) results.push(stmt.getAsObject());
|
|
2765
|
-
stmt.free();
|
|
2766
|
-
if (results.length > 0) {
|
|
2767
|
-
const lines = [
|
|
2768
|
-
`\u{1F517} **Federated Node Resolved** (local)`,
|
|
2769
|
-
`\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`,
|
|
2770
|
-
`\u{1F4C1} Project: ${projectName}`,
|
|
2771
|
-
`\u{1F517} URI: ${uri}`,
|
|
2772
|
-
""
|
|
2773
|
-
];
|
|
2774
|
-
for (const r of results) {
|
|
2775
|
-
lines.push(` \u2022 **${r.name}** (${r.type}) \u2014 ${r.file_path || "no path"}`);
|
|
2776
|
-
}
|
|
2777
|
-
lines.push("", "\u{1F4A1} Federated cross-repo resolution is active. Remote resolution via HTTP coming soon.");
|
|
2778
|
-
return lines.join("\n");
|
|
2779
|
-
}
|
|
2780
|
-
const federatedNodes = await registerFederatedReference(uri, projectName, nodeRef);
|
|
2781
|
-
return [
|
|
2782
|
-
`\u{1F517} **Federated Reference**`,
|
|
2783
|
-
`\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`,
|
|
2784
|
-
`\u{1F4C1} Remote project: ${projectName}`,
|
|
2785
|
-
`\u{1F517} URI: ${uri}`,
|
|
2786
|
-
`\u{1F4CC} Node ref: ${nodeRef}`,
|
|
2787
|
-
"",
|
|
2788
|
-
`\u{1F4DD} Registered as federated reference for future resolution.`,
|
|
2789
|
-
`\u{1F4A1} Remote graph resolution will be available when the target project is reachable.`,
|
|
2790
|
-
federatedNodes > 0 ? `\u2705 Created ${federatedNodes} federated node(s) in local graph.` : ""
|
|
2791
|
-
].filter(Boolean).join("\n");
|
|
2792
|
-
} catch (err) {
|
|
2793
|
-
return `\u274C Federated resolution failed: ${err}`;
|
|
2794
|
-
}
|
|
2795
|
-
}
|
|
2796
|
-
async function registerFederatedReference(uri, projectName, nodeRef) {
|
|
2797
|
-
try {
|
|
2798
|
-
await upsertNode({
|
|
2799
|
-
id: `federated::${uri}`,
|
|
2800
|
-
type: "module",
|
|
2801
|
-
name: `[${projectName}] ${nodeRef}`,
|
|
2802
|
-
metadata: {
|
|
2803
|
-
federated: true,
|
|
2804
|
-
uri,
|
|
2805
|
-
project: projectName,
|
|
2806
|
-
ref: nodeRef,
|
|
2807
|
-
resolved: false
|
|
2808
|
-
}
|
|
2809
|
-
});
|
|
2810
|
-
return 1;
|
|
2811
|
-
} catch {
|
|
2812
|
-
return 0;
|
|
2813
|
-
}
|
|
2814
|
-
}
|
|
2815
|
-
async function listFederatedReferences() {
|
|
2816
|
-
try {
|
|
2817
|
-
const db = await getDb();
|
|
2818
|
-
const stmt = db.prepare(`
|
|
2819
|
-
SELECT id, name, metadata FROM nodes
|
|
2820
|
-
WHERE metadata LIKE '%"federated":true%'
|
|
2821
|
-
ORDER BY updated_at DESC
|
|
2822
|
-
LIMIT 20
|
|
2823
|
-
`);
|
|
2824
|
-
const results = [];
|
|
2825
|
-
while (stmt.step()) results.push(stmt.getAsObject());
|
|
2826
|
-
stmt.free();
|
|
2827
|
-
if (results.length === 0) {
|
|
2828
|
-
return "\u{1F517} **No federated references.** Use kuma_memory({ action: 'federated', uri: 'kuma://project/node-id' }) to add one.";
|
|
2829
|
-
}
|
|
2830
|
-
const lines = [
|
|
2831
|
-
"\u{1F517} **Federated References**",
|
|
2832
|
-
"\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",
|
|
2833
|
-
""
|
|
2834
|
-
];
|
|
2835
|
-
for (const r of results) {
|
|
2836
|
-
const meta = JSON.parse(r.metadata || "{}");
|
|
2837
|
-
lines.push(` \u{1F517} **${r.name}**`);
|
|
2838
|
-
lines.push(` URI: ${meta.uri || "unknown"}`);
|
|
2839
|
-
lines.push(` Resolved: ${meta.resolved ? "\u2705" : "\u23F3"}`);
|
|
2840
|
-
lines.push("");
|
|
2841
|
-
}
|
|
2842
|
-
return lines.join("\n");
|
|
2843
|
-
} catch (err) {
|
|
2844
|
-
return `Error: ${err}`;
|
|
2845
|
-
}
|
|
2846
|
-
}
|
|
2847
3196
|
async function getGraphStats() {
|
|
2848
3197
|
try {
|
|
2849
3198
|
const db = await getDb();
|
|
2850
|
-
const
|
|
2851
|
-
|
|
3199
|
+
const getCount = (sql) => {
|
|
3200
|
+
try {
|
|
3201
|
+
const result = db.exec(sql);
|
|
3202
|
+
return result[0]?.values?.[0] || 0;
|
|
3203
|
+
} catch {
|
|
3204
|
+
return 0;
|
|
3205
|
+
}
|
|
3206
|
+
};
|
|
3207
|
+
const nodeCount = getCount("SELECT COUNT(*) as c FROM nodes");
|
|
3208
|
+
const edgeCount = getCount("SELECT COUNT(*) as c FROM edges");
|
|
2852
3209
|
const typeCounts = [];
|
|
2853
3210
|
try {
|
|
2854
3211
|
const stmt = db.prepare("SELECT type, COUNT(*) as cnt FROM nodes GROUP BY type ORDER BY cnt DESC");
|
|
@@ -2863,6 +3220,7 @@ async function getGraphStats() {
|
|
|
2863
3220
|
`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
|
|
2864
3221
|
"",
|
|
2865
3222
|
`\u{1F4CA} **${nodeCount} nodes** | **${edgeCount} edges**`,
|
|
3223
|
+
`\u{1F4CF} **Limits:** ${nodeCount}/${GRAPH_LIMITS.maxNodes} nodes (${Math.round(nodeCount / GRAPH_LIMITS.maxNodes * 100)}%) | ${edgeCount}/${GRAPH_LIMITS.maxEdges} edges (${Math.round(edgeCount / GRAPH_LIMITS.maxEdges * 100)}%)`,
|
|
2866
3224
|
""
|
|
2867
3225
|
];
|
|
2868
3226
|
if (typeCounts.length > 0) {
|
|
@@ -3059,11 +3417,22 @@ function formatFlow(entryPoint, steps) {
|
|
|
3059
3417
|
lines.push("", `\u{1F4A1} ${steps.length} step(s) traversed.`);
|
|
3060
3418
|
return lines.join("\n");
|
|
3061
3419
|
}
|
|
3420
|
+
var GRAPH_LIMITS;
|
|
3062
3421
|
var init_kumaGraph = __esm({
|
|
3063
3422
|
"src/engine/kumaGraph.ts"() {
|
|
3064
3423
|
"use strict";
|
|
3065
3424
|
init_kumaDb();
|
|
3066
3425
|
init_kumaSelfHeal();
|
|
3426
|
+
GRAPH_LIMITS = {
|
|
3427
|
+
maxNodes: 5e3,
|
|
3428
|
+
// Max nodes before pruning
|
|
3429
|
+
maxEdges: 1e4,
|
|
3430
|
+
// Max edges before pruning
|
|
3431
|
+
pruneBatch: 500,
|
|
3432
|
+
// Remove this many old nodes when limit hit
|
|
3433
|
+
minWeight: 0.1
|
|
3434
|
+
// Minimum edge weight before deletion
|
|
3435
|
+
};
|
|
3067
3436
|
}
|
|
3068
3437
|
});
|
|
3069
3438
|
|
|
@@ -5931,495 +6300,57 @@ async function addGotcha(entry) {
|
|
|
5931
6300
|
`- **Issue**: ${entry.description}`,
|
|
5932
6301
|
`- **Severity**: ${severity}`,
|
|
5933
6302
|
entry.workaround ? `- **Workaround**: ${entry.workaround}` : "",
|
|
5934
|
-
`- **Added**: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`
|
|
5935
|
-
].filter(Boolean).join("\n");
|
|
5936
|
-
db.run(
|
|
5937
|
-
`INSERT INTO known_gotchas (file_path, description, severity, workaround) VALUES (?, ?, ?, ?)`,
|
|
5938
|
-
[entry.filePath, entry.description, severity, entry.workaround || null]
|
|
5939
|
-
);
|
|
5940
|
-
const mdResult = appendToLayer("gotcha", formatted);
|
|
5941
|
-
saveDb();
|
|
5942
|
-
sessionMemory.recordToolCall("kuma_gotcha_add", {
|
|
5943
|
-
filePath: entry.filePath,
|
|
5944
|
-
severity
|
|
5945
|
-
});
|
|
5946
|
-
return `\u2705 **Gotcha recorded**: ${entry.filePath} \u2014 ${entry.description}
|
|
5947
|
-
${mdResult}`;
|
|
5948
|
-
} catch (err) {
|
|
5949
|
-
return `\u274C Failed to add gotcha: ${err}`;
|
|
5950
|
-
}
|
|
5951
|
-
}
|
|
5952
|
-
async function listGotchas(params) {
|
|
5953
|
-
try {
|
|
5954
|
-
await ensureGotchasSchema();
|
|
5955
|
-
const db = await getDb();
|
|
5956
|
-
let sql = "SELECT * FROM known_gotchas WHERE 1=1";
|
|
5957
|
-
const bind = [];
|
|
5958
|
-
if (params.filePath) {
|
|
5959
|
-
sql += " AND file_path LIKE ?";
|
|
5960
|
-
bind.push(`%${params.filePath}%`);
|
|
5961
|
-
}
|
|
5962
|
-
if (params.severity) {
|
|
5963
|
-
sql += " AND severity = ?";
|
|
5964
|
-
bind.push(params.severity);
|
|
5965
|
-
}
|
|
5966
|
-
sql += " ORDER BY severity DESC, created_at DESC LIMIT 50";
|
|
5967
|
-
const stmt = db.prepare(sql);
|
|
5968
|
-
stmt.bind(bind);
|
|
5969
|
-
const results = [];
|
|
5970
|
-
while (stmt.step()) results.push(stmt.getAsObject());
|
|
5971
|
-
stmt.free();
|
|
5972
|
-
if (results.length === 0) {
|
|
5973
|
-
return "\u2705 **No gotchas recorded** \u2014 legacy codebase looks clean.";
|
|
5974
|
-
}
|
|
5975
|
-
const lines = [
|
|
5976
|
-
`\u26A0\uFE0F **Known Gotchas** \u2014 ${results.length} recorded`,
|
|
5977
|
-
"\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",
|
|
5978
|
-
""
|
|
5979
|
-
];
|
|
5980
|
-
for (const g of results) {
|
|
5981
|
-
const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
5982
|
-
lines.push(`${icon} [${g.severity}] ${g.file_path}`);
|
|
5983
|
-
lines.push(` \u{1F4DD} ${g.description?.toString().substring(0, 100)}`);
|
|
5984
|
-
if (g.workaround) lines.push(` \u{1F4A1} ${g.workaround.substring(0, 100)}`);
|
|
5985
|
-
lines.push("");
|
|
5986
|
-
}
|
|
5987
|
-
return lines.join("\n");
|
|
5988
|
-
} catch (err) {
|
|
5989
|
-
return `Error: ${err}`;
|
|
5990
|
-
}
|
|
5991
|
-
}
|
|
5992
|
-
function checkGotchasForFile(filePath) {
|
|
5993
|
-
const warnings = [];
|
|
5994
|
-
const markdownWarnings = checkFileGotchas(filePath);
|
|
5995
|
-
try {
|
|
5996
|
-
const gotchas = getActiveGotchas();
|
|
5997
|
-
for (const g of gotchas) {
|
|
5998
|
-
if (filePath.includes(g.filePath) || g.filePath.includes(filePath)) {
|
|
5999
|
-
if (!markdownWarnings.some((w) => w.includes(g.description))) {
|
|
6000
|
-
const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
6001
|
-
warnings.push(`${icon} **Known Gotcha**: ${g.description} (${g.severity})`);
|
|
6002
|
-
}
|
|
6003
|
-
}
|
|
6004
|
-
}
|
|
6005
|
-
} catch {
|
|
6006
|
-
}
|
|
6007
|
-
return [.../* @__PURE__ */ new Set([...markdownWarnings, ...warnings])];
|
|
6008
|
-
}
|
|
6009
|
-
async function syncGotchasToDb() {
|
|
6010
|
-
try {
|
|
6011
|
-
await ensureGotchasSchema();
|
|
6012
|
-
const db = await getDb();
|
|
6013
|
-
const gotchas = getActiveGotchas();
|
|
6014
|
-
let synced = 0;
|
|
6015
|
-
for (const g of gotchas) {
|
|
6016
|
-
const checkStmt = db.prepare(
|
|
6017
|
-
`SELECT id FROM known_gotchas WHERE file_path = ? AND description = ?`
|
|
6018
|
-
);
|
|
6019
|
-
checkStmt.bind([g.filePath, g.description]);
|
|
6020
|
-
const exists = checkStmt.step();
|
|
6021
|
-
checkStmt.free();
|
|
6022
|
-
if (exists) continue;
|
|
6023
|
-
db.run(
|
|
6024
|
-
`INSERT INTO known_gotchas (file_path, description, severity) VALUES (?, ?, ?)`,
|
|
6025
|
-
[g.filePath, g.description, g.severity]
|
|
6026
|
-
);
|
|
6027
|
-
synced++;
|
|
6028
|
-
}
|
|
6029
|
-
saveDb();
|
|
6030
|
-
return { synced };
|
|
6031
|
-
} catch {
|
|
6032
|
-
return { synced: 0 };
|
|
6033
|
-
}
|
|
6034
|
-
}
|
|
6035
|
-
var init_kumaGotchas = __esm({
|
|
6036
|
-
"src/engine/kumaGotchas.ts"() {
|
|
6037
|
-
"use strict";
|
|
6038
|
-
init_sessionMemory();
|
|
6039
|
-
init_kumaDb();
|
|
6040
|
-
init_domainRules();
|
|
6041
|
-
}
|
|
6042
|
-
});
|
|
6043
|
-
|
|
6044
|
-
// src/engine/kumaTrajectory.ts
|
|
6045
|
-
var kumaTrajectory_exports = {};
|
|
6046
|
-
__export(kumaTrajectory_exports, {
|
|
6047
|
-
findSimilarTrajectories: () => findSimilarTrajectories,
|
|
6048
|
-
generateTestFromTrajectory: () => generateTestFromTrajectory,
|
|
6049
|
-
generateTestFromTrajectoryId: () => generateTestFromTrajectoryId,
|
|
6050
|
-
listDistilledSkills: () => listDistilledSkills,
|
|
6051
|
-
listGeneratedTests: () => listGeneratedTests,
|
|
6052
|
-
listTrajectories: () => listTrajectories,
|
|
6053
|
-
recordTrajectory: () => recordTrajectory
|
|
6054
|
-
});
|
|
6055
|
-
import fs14 from "fs";
|
|
6056
|
-
import path14 from "path";
|
|
6057
|
-
async function ensureTrajectorySchema() {
|
|
6058
|
-
const db = await getDb();
|
|
6059
|
-
db.exec(`
|
|
6060
|
-
CREATE TABLE IF NOT EXISTS trajectories (
|
|
6061
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6062
|
-
goal TEXT NOT NULL,
|
|
6063
|
-
steps TEXT NOT NULL DEFAULT '[]',
|
|
6064
|
-
total_duration_ms INTEGER DEFAULT 0,
|
|
6065
|
-
success_rate REAL DEFAULT 0.0,
|
|
6066
|
-
complexity INTEGER DEFAULT 0,
|
|
6067
|
-
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
|
6068
|
-
);
|
|
6069
|
-
CREATE INDEX IF NOT EXISTS idx_traj_goal ON trajectories(goal);
|
|
6070
|
-
CREATE INDEX IF NOT EXISTS idx_traj_complexity ON trajectories(complexity);
|
|
6071
|
-
CREATE INDEX IF NOT EXISTS idx_traj_created ON trajectories(created_at);
|
|
6072
|
-
`);
|
|
6073
|
-
db.exec(`
|
|
6074
|
-
CREATE TABLE IF NOT EXISTS distilled_skills (
|
|
6075
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6076
|
-
name TEXT NOT NULL UNIQUE,
|
|
6077
|
-
description TEXT NOT NULL,
|
|
6078
|
-
pattern TEXT NOT NULL, -- JSON: tool sequence pattern
|
|
6079
|
-
parameters TEXT DEFAULT '[]', -- JSON: parameterized inputs
|
|
6080
|
-
success_count INTEGER DEFAULT 1,
|
|
6081
|
-
avg_duration_ms INTEGER DEFAULT 0,
|
|
6082
|
-
source_trajectory_id INTEGER,
|
|
6083
|
-
last_used_at INTEGER,
|
|
6084
|
-
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
|
6085
|
-
);
|
|
6086
|
-
CREATE INDEX IF NOT EXISTS idx_skills_name ON distilled_skills(name);
|
|
6087
|
-
CREATE INDEX IF NOT EXISTS idx_skills_used ON distilled_skills(last_used_at);
|
|
6088
|
-
`);
|
|
6089
|
-
db.exec(`
|
|
6090
|
-
CREATE TABLE IF NOT EXISTS generated_tests (
|
|
6091
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6092
|
-
trajectory_id INTEGER,
|
|
6093
|
-
file_path TEXT NOT NULL,
|
|
6094
|
-
test_framework TEXT NOT NULL,
|
|
6095
|
-
description TEXT,
|
|
6096
|
-
test_code TEXT NOT NULL,
|
|
6097
|
-
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
|
6098
|
-
);
|
|
6099
|
-
CREATE INDEX IF NOT EXISTS idx_gen_tests_traj ON generated_tests(trajectory_id);
|
|
6100
|
-
`);
|
|
6101
|
-
saveDb();
|
|
6102
|
-
}
|
|
6103
|
-
async function recordTrajectory(goal) {
|
|
6104
|
-
try {
|
|
6105
|
-
await ensureTrajectorySchema();
|
|
6106
|
-
const calls = sessionMemory.getToolCallHistory(100);
|
|
6107
|
-
const steps = calls.map((c) => ({
|
|
6108
|
-
toolName: c.toolName,
|
|
6109
|
-
params: c.params,
|
|
6110
|
-
success: true,
|
|
6111
|
-
durationMs: 0,
|
|
6112
|
-
timestamp: c.timestamp
|
|
6113
|
-
}));
|
|
6114
|
-
const totalDurationMs = steps.length > 0 ? steps[steps.length - 1].timestamp - steps[0].timestamp : 0;
|
|
6115
|
-
const errorCalls = steps.filter((s) => {
|
|
6116
|
-
const action = s.params.action;
|
|
6117
|
-
return action === "verify" && s.params.success === false;
|
|
6118
|
-
}).length;
|
|
6119
|
-
const successRate = steps.length > 0 ? (steps.length - errorCalls) / steps.length : 1;
|
|
6120
|
-
const complexity = Math.min(
|
|
6121
|
-
100,
|
|
6122
|
-
Math.round(
|
|
6123
|
-
steps.length * 5 + // 5 pts per step
|
|
6124
|
-
errorCalls * 15 + // 15 pts per error
|
|
6125
|
-
Math.min(totalDurationMs / 6e4 * 10, 30)
|
|
6126
|
-
// 10 pts per minute (max 30)
|
|
6127
|
-
)
|
|
6128
|
-
);
|
|
6129
|
-
const db2 = await getDb();
|
|
6130
|
-
db2.run(
|
|
6131
|
-
`INSERT INTO trajectories (goal, steps, total_duration_ms, success_rate, complexity)
|
|
6132
|
-
VALUES (?, ?, ?, ?, ?)`,
|
|
6133
|
-
[
|
|
6134
|
-
goal.substring(0, 200),
|
|
6135
|
-
JSON.stringify(steps),
|
|
6136
|
-
totalDurationMs,
|
|
6137
|
-
successRate,
|
|
6138
|
-
complexity
|
|
6139
|
-
]
|
|
6140
|
-
);
|
|
6141
|
-
saveDb();
|
|
6142
|
-
const result = db2.exec("SELECT last_insert_rowid() as id");
|
|
6143
|
-
const id = result[0]?.values[0]?.[0];
|
|
6144
|
-
if (id !== null && complexity > 40 && successRate > 0.8) {
|
|
6145
|
-
await distillSkill(id, goal, steps).catch(() => {
|
|
6146
|
-
});
|
|
6147
|
-
if (goal.toLowerCase().includes("fix") || goal.toLowerCase().includes("bug") || goal.toLowerCase().includes("error")) {
|
|
6148
|
-
await generateTestFromTrajectory(id, goal, steps).catch(() => {
|
|
6149
|
-
});
|
|
6150
|
-
}
|
|
6151
|
-
}
|
|
6152
|
-
return id ?? null;
|
|
6153
|
-
} catch (err) {
|
|
6154
|
-
console.error(`[Trajectory] Record error: ${err}`);
|
|
6155
|
-
return null;
|
|
6156
|
-
}
|
|
6157
|
-
}
|
|
6158
|
-
async function distillSkill(trajectoryId, goal, steps) {
|
|
6159
|
-
try {
|
|
6160
|
-
const db = await getDb();
|
|
6161
|
-
const toolSequence = steps.map((s) => s.toolName);
|
|
6162
|
-
const patternKey = toolSequence.join(" \u2192 ");
|
|
6163
|
-
const uniqueTools = [...new Set(toolSequence)];
|
|
6164
|
-
const skillName = `trajectory:${uniqueTools.slice(0, 3).join("-")}`;
|
|
6165
|
-
const checkStmt = db.prepare(
|
|
6166
|
-
"SELECT id, success_count, avg_duration_ms FROM distilled_skills WHERE name = ?"
|
|
6167
|
-
);
|
|
6168
|
-
checkStmt.bind([skillName]);
|
|
6169
|
-
const exists = checkStmt.step();
|
|
6170
|
-
let existingRow = null;
|
|
6171
|
-
if (exists) {
|
|
6172
|
-
existingRow = checkStmt.getAsObject();
|
|
6173
|
-
}
|
|
6174
|
-
checkStmt.free();
|
|
6175
|
-
const totalDuration = steps.reduce((sum, s) => sum + s.durationMs, 0);
|
|
6176
|
-
if (existingRow) {
|
|
6177
|
-
const existingId = existingRow.id;
|
|
6178
|
-
const existingCount = existingRow.success_count;
|
|
6179
|
-
const existingAvg = existingRow.avg_duration_ms;
|
|
6180
|
-
db.run(
|
|
6181
|
-
`UPDATE distilled_skills
|
|
6182
|
-
SET success_count = ?, avg_duration_ms = ?, last_used_at = strftime('%s','now')
|
|
6183
|
-
WHERE id = ?`,
|
|
6184
|
-
[
|
|
6185
|
-
existingCount + 1,
|
|
6186
|
-
Math.round((existingAvg * existingCount + totalDuration) / (existingCount + 1)),
|
|
6187
|
-
existingId
|
|
6188
|
-
]
|
|
6189
|
-
);
|
|
6190
|
-
} else {
|
|
6191
|
-
const description = [
|
|
6192
|
-
`Distilled trajectory pattern for "${goal.substring(0, 60)}"`,
|
|
6193
|
-
`Tool sequence: ${patternKey}`,
|
|
6194
|
-
`${steps.length} steps, ${uniqueTools.length} unique tools`
|
|
6195
|
-
].join(". ");
|
|
6196
|
-
db.run(
|
|
6197
|
-
`INSERT INTO distilled_skills (name, description, pattern, parameters, success_count, avg_duration_ms, source_trajectory_id, last_used_at)
|
|
6198
|
-
VALUES (?, ?, ?, ?, 1, ?, ?, strftime('%s','now'))`,
|
|
6199
|
-
[
|
|
6200
|
-
skillName,
|
|
6201
|
-
description,
|
|
6202
|
-
JSON.stringify(toolSequence),
|
|
6203
|
-
JSON.stringify(extractParameters(steps)),
|
|
6204
|
-
totalDuration,
|
|
6205
|
-
trajectoryId
|
|
6206
|
-
]
|
|
6207
|
-
);
|
|
6208
|
-
}
|
|
6209
|
-
saveDb();
|
|
6210
|
-
console.error(`[Trajectory] Distilled skill "${skillName}" from trajectory #${trajectoryId}`);
|
|
6211
|
-
} catch (err) {
|
|
6212
|
-
console.error(`[Trajectory] Distill error: ${err}`);
|
|
6213
|
-
}
|
|
6214
|
-
}
|
|
6215
|
-
function extractParameters(steps) {
|
|
6216
|
-
const params = /* @__PURE__ */ new Set();
|
|
6217
|
-
for (const step of steps) {
|
|
6218
|
-
for (const [key] of Object.entries(step.params)) {
|
|
6219
|
-
if (["scope", "target", "query", "filePath", "action"].includes(key)) {
|
|
6220
|
-
params.add(key);
|
|
6221
|
-
}
|
|
6222
|
-
}
|
|
6223
|
-
}
|
|
6224
|
-
return Array.from(params);
|
|
6225
|
-
}
|
|
6226
|
-
async function generateTestFromTrajectory(trajectoryId, goal, steps) {
|
|
6227
|
-
try {
|
|
6228
|
-
await ensureTrajectorySchema();
|
|
6229
|
-
const modifiedFiles = steps.filter((s) => s.params.filePath || s.params.target).map((s) => s.params.filePath || s.params.target).filter(Boolean);
|
|
6230
|
-
if (modifiedFiles.length === 0) return null;
|
|
6231
|
-
const framework = detectTestFramework();
|
|
6232
|
-
const primaryFile = modifiedFiles[0];
|
|
6233
|
-
const testCode = synthesizeTestCode(goal, primaryFile, modifiedFiles, framework);
|
|
6234
|
-
const testFilePath = determineTestPath(primaryFile, framework);
|
|
6235
|
-
const genTest = {
|
|
6236
|
-
filePath: testFilePath,
|
|
6237
|
-
framework,
|
|
6238
|
-
code: testCode,
|
|
6239
|
-
description: `Auto-generated from trajectory #${trajectoryId}: ${goal.substring(0, 80)}`
|
|
6240
|
-
};
|
|
6241
|
-
const db = await getDb();
|
|
6242
|
-
db.run(
|
|
6243
|
-
`INSERT INTO generated_tests (trajectory_id, file_path, test_framework, description, test_code)
|
|
6244
|
-
VALUES (?, ?, ?, ?, ?)`,
|
|
6245
|
-
[trajectoryId, testFilePath, framework, genTest.description, testCode]
|
|
6246
|
-
);
|
|
6247
|
-
saveDb();
|
|
6248
|
-
const root = getProjectRoot();
|
|
6249
|
-
const fullPath = path14.resolve(root, testFilePath);
|
|
6250
|
-
const dir = path14.dirname(fullPath);
|
|
6251
|
-
if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
|
|
6252
|
-
fs14.writeFileSync(fullPath, testCode, "utf-8");
|
|
6253
|
-
console.error(`[Trajectory] Generated test: ${testFilePath} from trajectory #${trajectoryId}`);
|
|
6254
|
-
return genTest;
|
|
6255
|
-
} catch (err) {
|
|
6256
|
-
console.error(`[Trajectory] Test generation error: ${err}`);
|
|
6257
|
-
return null;
|
|
6258
|
-
}
|
|
6259
|
-
}
|
|
6260
|
-
async function generateTestFromTrajectoryId(trajectoryId) {
|
|
6261
|
-
try {
|
|
6262
|
-
await ensureTrajectorySchema();
|
|
6263
|
-
const db = await getDb();
|
|
6264
|
-
const stmt = db.prepare("SELECT id, goal, steps FROM trajectories WHERE id = ?");
|
|
6265
|
-
stmt.bind([trajectoryId]);
|
|
6266
|
-
if (!stmt.step()) {
|
|
6267
|
-
stmt.free();
|
|
6268
|
-
return `\u274C Trajectory #${trajectoryId} not found.`;
|
|
6269
|
-
}
|
|
6270
|
-
const row = stmt.getAsObject();
|
|
6271
|
-
stmt.free();
|
|
6272
|
-
const goal = row.goal;
|
|
6273
|
-
const steps = JSON.parse(row.steps);
|
|
6274
|
-
const result = await generateTestFromTrajectory(trajectoryId, goal, steps);
|
|
6275
|
-
if (!result) {
|
|
6276
|
-
return "\u26A0\uFE0F Could not generate test \u2014 no modified files found in trajectory.";
|
|
6277
|
-
}
|
|
6278
|
-
return [
|
|
6279
|
-
`\u{1F9EA} **Test Generated** from trajectory #${trajectoryId}`,
|
|
6280
|
-
`\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\u2501\u2501\u2501`,
|
|
6281
|
-
``,
|
|
6282
|
-
`\u{1F4C1} **File**: ${result.filePath}`,
|
|
6283
|
-
`\u26A1 **Framework**: ${result.framework}`,
|
|
6284
|
-
`\u{1F4DD} **Description**: ${result.description}`,
|
|
6285
|
-
``,
|
|
6286
|
-
`\`\`\`${result.framework === "jest" ? "typescript" : "javascript"}`,
|
|
6287
|
-
result.code.substring(0, 800),
|
|
6288
|
-
result.code.length > 800 ? "..." : "",
|
|
6289
|
-
`\`\`\``,
|
|
6290
|
-
``,
|
|
6291
|
-
`\u{1F4A1} Test file has been written to disk. Run your test suite to verify.`
|
|
6303
|
+
`- **Added**: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`
|
|
6292
6304
|
].filter(Boolean).join("\n");
|
|
6305
|
+
db.run(
|
|
6306
|
+
`INSERT INTO known_gotchas (file_path, description, severity, workaround) VALUES (?, ?, ?, ?)`,
|
|
6307
|
+
[entry.filePath, entry.description, severity, entry.workaround || null]
|
|
6308
|
+
);
|
|
6309
|
+
const mdResult = appendToLayer("gotcha", formatted);
|
|
6310
|
+
saveDb();
|
|
6311
|
+
sessionMemory.recordToolCall("kuma_gotcha_add", {
|
|
6312
|
+
filePath: entry.filePath,
|
|
6313
|
+
severity
|
|
6314
|
+
});
|
|
6315
|
+
return `\u2705 **Gotcha recorded**: ${entry.filePath} \u2014 ${entry.description}
|
|
6316
|
+
${mdResult}`;
|
|
6293
6317
|
} catch (err) {
|
|
6294
|
-
return `\u274C
|
|
6318
|
+
return `\u274C Failed to add gotcha: ${err}`;
|
|
6295
6319
|
}
|
|
6296
6320
|
}
|
|
6297
|
-
async function
|
|
6321
|
+
async function listGotchas(params) {
|
|
6298
6322
|
try {
|
|
6299
|
-
await
|
|
6323
|
+
await ensureGotchasSchema();
|
|
6300
6324
|
const db = await getDb();
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
const results = [];
|
|
6307
|
-
while (stmt.step()) results.push(stmt.getAsObject());
|
|
6308
|
-
stmt.free();
|
|
6309
|
-
if (results.length === 0) {
|
|
6310
|
-
return "\u{1F9EA} **No generated tests yet.** Tests are auto-generated from successful fix trajectories.";
|
|
6325
|
+
let sql = "SELECT * FROM known_gotchas WHERE 1=1";
|
|
6326
|
+
const bind = [];
|
|
6327
|
+
if (params.filePath) {
|
|
6328
|
+
sql += " AND file_path LIKE ?";
|
|
6329
|
+
bind.push(`%${params.filePath}%`);
|
|
6311
6330
|
}
|
|
6312
|
-
|
|
6313
|
-
"
|
|
6314
|
-
|
|
6315
|
-
""
|
|
6316
|
-
];
|
|
6317
|
-
for (const r of results) {
|
|
6318
|
-
const time = new Date(r.created_at * 1e3).toLocaleString();
|
|
6319
|
-
lines.push(` \u{1F4C4} **${r.file_path}**`);
|
|
6320
|
-
lines.push(` \u26A1 ${r.test_framework} | \u{1F550} ${time}`);
|
|
6321
|
-
if (r.goal) lines.push(` \u{1F3AF} ${r.goal.substring(0, 60)}`);
|
|
6322
|
-
lines.push("");
|
|
6331
|
+
if (params.severity) {
|
|
6332
|
+
sql += " AND severity = ?";
|
|
6333
|
+
bind.push(params.severity);
|
|
6323
6334
|
}
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
}
|
|
6328
|
-
}
|
|
6329
|
-
function detectTestFramework() {
|
|
6330
|
-
const root = getProjectRoot();
|
|
6331
|
-
try {
|
|
6332
|
-
const pkg = JSON.parse(fs14.readFileSync(path14.join(root, "package.json"), "utf-8"));
|
|
6333
|
-
if (pkg.devDependencies?.vitest) return "vitest";
|
|
6334
|
-
if (pkg.devDependencies?.jest || pkg.devDependencies?.["@jest/globals"]) return "jest";
|
|
6335
|
-
} catch {
|
|
6336
|
-
}
|
|
6337
|
-
if (fs14.existsSync(path14.join(root, "vitest.config.ts")) || fs14.existsSync(path14.join(root, "vitest.config.js"))) return "vitest";
|
|
6338
|
-
if (fs14.existsSync(path14.join(root, "jest.config.ts")) || fs14.existsSync(path14.join(root, "jest.config.js"))) return "jest";
|
|
6339
|
-
return "node";
|
|
6340
|
-
}
|
|
6341
|
-
function determineTestPath(filePath, _framework) {
|
|
6342
|
-
const dir = path14.dirname(filePath);
|
|
6343
|
-
const basename = path14.basename(filePath, path14.extname(filePath));
|
|
6344
|
-
return path14.join(dir, `__tests__`, `${basename}.fix.test.ts`);
|
|
6345
|
-
}
|
|
6346
|
-
function synthesizeTestCode(goal, primaryFile, modifiedFiles, framework) {
|
|
6347
|
-
const importPath = primaryFile.replace(/\.ts$/, "").replace(/\.js$/, "");
|
|
6348
|
-
const describeBlock = `describe('Fix: ${goal.substring(0, 60)}', () => {`;
|
|
6349
|
-
const testBody = `
|
|
6350
|
-
it('should not regress the fix applied in ${path14.basename(primaryFile)}', async () => {
|
|
6351
|
-
// Auto-generated from Kuma trajectory \u2014 regression test
|
|
6352
|
-
// Source file: ${primaryFile}
|
|
6353
|
-
// Goal: ${goal}
|
|
6354
|
-
// Modified files: ${modifiedFiles.slice(0, 3).join(", ")}
|
|
6355
|
-
|
|
6356
|
-
// TODO: Replace with actual test logic
|
|
6357
|
-
// const { yourFunction } = await import('${importPath}');
|
|
6358
|
-
// const result = yourFunction();
|
|
6359
|
-
// expect(result).toBeDefined();
|
|
6360
|
-
|
|
6361
|
-
expect(true).toBe(true);
|
|
6362
|
-
});
|
|
6363
|
-
`;
|
|
6364
|
-
const closeBlock = `});`;
|
|
6365
|
-
switch (framework) {
|
|
6366
|
-
case "vitest":
|
|
6367
|
-
case "jest":
|
|
6368
|
-
return [
|
|
6369
|
-
`// ============================================================`,
|
|
6370
|
-
`// AUTO-GENERATED REGRESSION TEST`,
|
|
6371
|
-
`// Generated by Kuma Trajectory-to-Test Generator (Issue #28)`,
|
|
6372
|
-
`// ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
6373
|
-
`// Goal: ${goal}`,
|
|
6374
|
-
`// Files: ${modifiedFiles.join(", ")}`,
|
|
6375
|
-
`// ============================================================`,
|
|
6376
|
-
``,
|
|
6377
|
-
`import { describe, it, expect } from '@jest/globals';`,
|
|
6378
|
-
``,
|
|
6379
|
-
describeBlock,
|
|
6380
|
-
testBody,
|
|
6381
|
-
closeBlock,
|
|
6382
|
-
``
|
|
6383
|
-
].join("\n");
|
|
6384
|
-
default:
|
|
6385
|
-
return [
|
|
6386
|
-
`// AUTO-GENERATED REGRESSION TEST`,
|
|
6387
|
-
`// Goal: ${goal}`,
|
|
6388
|
-
`// Files: ${modifiedFiles.join(", ")}`,
|
|
6389
|
-
``,
|
|
6390
|
-
`const assert = require('assert');`,
|
|
6391
|
-
``,
|
|
6392
|
-
describeBlock,
|
|
6393
|
-
testBody.replace("import", "// ").replace("expect", "// expect"),
|
|
6394
|
-
closeBlock,
|
|
6395
|
-
``
|
|
6396
|
-
].join("\n");
|
|
6397
|
-
}
|
|
6398
|
-
}
|
|
6399
|
-
async function listDistilledSkills() {
|
|
6400
|
-
try {
|
|
6401
|
-
await ensureTrajectorySchema();
|
|
6402
|
-
const db = await getDb();
|
|
6403
|
-
const stmt = db.prepare(`
|
|
6404
|
-
SELECT * FROM distilled_skills ORDER BY success_count DESC, last_used_at DESC LIMIT 20
|
|
6405
|
-
`);
|
|
6335
|
+
sql += " ORDER BY severity DESC, created_at DESC LIMIT 50";
|
|
6336
|
+
const stmt = db.prepare(sql);
|
|
6337
|
+
stmt.bind(bind);
|
|
6406
6338
|
const results = [];
|
|
6407
6339
|
while (stmt.step()) results.push(stmt.getAsObject());
|
|
6408
6340
|
stmt.free();
|
|
6409
6341
|
if (results.length === 0) {
|
|
6410
|
-
return "\
|
|
6342
|
+
return "\u2705 **No gotchas recorded** \u2014 legacy codebase looks clean.";
|
|
6411
6343
|
}
|
|
6412
6344
|
const lines = [
|
|
6413
|
-
|
|
6414
|
-
"\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
|
|
6345
|
+
`\u26A0\uFE0F **Known Gotchas** \u2014 ${results.length} recorded`,
|
|
6346
|
+
"\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",
|
|
6415
6347
|
""
|
|
6416
6348
|
];
|
|
6417
|
-
for (const
|
|
6418
|
-
const
|
|
6419
|
-
|
|
6420
|
-
lines.push(`
|
|
6421
|
-
lines.push(`
|
|
6422
|
-
lines.push(` Used ${s.success_count}x | Avg ${s.avg_duration_ms}ms`);
|
|
6349
|
+
for (const g of results) {
|
|
6350
|
+
const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
6351
|
+
lines.push(`${icon} [${g.severity}] ${g.file_path}`);
|
|
6352
|
+
lines.push(` \u{1F4DD} ${g.description?.toString().substring(0, 100)}`);
|
|
6353
|
+
if (g.workaround) lines.push(` \u{1F4A1} ${g.workaround.substring(0, 100)}`);
|
|
6423
6354
|
lines.push("");
|
|
6424
6355
|
}
|
|
6425
6356
|
return lines.join("\n");
|
|
@@ -6427,76 +6358,55 @@ async function listDistilledSkills() {
|
|
|
6427
6358
|
return `Error: ${err}`;
|
|
6428
6359
|
}
|
|
6429
6360
|
}
|
|
6430
|
-
|
|
6361
|
+
function checkGotchasForFile(filePath) {
|
|
6362
|
+
const warnings = [];
|
|
6363
|
+
const markdownWarnings = checkFileGotchas(filePath);
|
|
6431
6364
|
try {
|
|
6432
|
-
|
|
6433
|
-
const
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
`);
|
|
6441
|
-
stmt.bind([`%${errorPattern}%`, limit]);
|
|
6442
|
-
const results = [];
|
|
6443
|
-
while (stmt.step()) results.push(stmt.getAsObject());
|
|
6444
|
-
stmt.free();
|
|
6445
|
-
if (results.length === 0) return "";
|
|
6446
|
-
const lines = [
|
|
6447
|
-
"\u{1F504} **Similar Past Trajectories Found**",
|
|
6448
|
-
"\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\u2501\u2501",
|
|
6449
|
-
""
|
|
6450
|
-
];
|
|
6451
|
-
for (const r of results) {
|
|
6452
|
-
const successIcon = r.success_rate > 0.8 ? "\u2705" : "\u26A0\uFE0F";
|
|
6453
|
-
lines.push(`${successIcon} #${r.id} \u2014 ${r.goal.substring(0, 60)}`);
|
|
6454
|
-
lines.push(` Complexity: ${r.complexity} | Duration: ${Math.round(r.total_duration_ms / 1e3)}s | Rate: ${Math.round(r.success_rate * 100)}%`);
|
|
6455
|
-
lines.push("");
|
|
6365
|
+
const gotchas = getActiveGotchas();
|
|
6366
|
+
for (const g of gotchas) {
|
|
6367
|
+
if (filePath.includes(g.filePath) || g.filePath.includes(filePath)) {
|
|
6368
|
+
if (!markdownWarnings.some((w) => w.includes(g.description))) {
|
|
6369
|
+
const icon = g.severity === "critical" ? "\u{1F534}" : g.severity === "high" ? "\u{1F7E0}" : g.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
6370
|
+
warnings.push(`${icon} **Known Gotcha**: ${g.description} (${g.severity})`);
|
|
6371
|
+
}
|
|
6372
|
+
}
|
|
6456
6373
|
}
|
|
6457
|
-
return lines.join("\n");
|
|
6458
6374
|
} catch {
|
|
6459
|
-
return "";
|
|
6460
6375
|
}
|
|
6376
|
+
return [.../* @__PURE__ */ new Set([...markdownWarnings, ...warnings])];
|
|
6461
6377
|
}
|
|
6462
|
-
async function
|
|
6378
|
+
async function syncGotchasToDb() {
|
|
6463
6379
|
try {
|
|
6464
|
-
await
|
|
6380
|
+
await ensureGotchasSchema();
|
|
6465
6381
|
const db = await getDb();
|
|
6466
|
-
const
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
];
|
|
6482
|
-
for (const r of results) {
|
|
6483
|
-
const time = new Date(r.created_at * 1e3).toLocaleString();
|
|
6484
|
-
const successIcon = r.success_rate > 0.8 ? "\u2705" : "\u26A0\uFE0F";
|
|
6485
|
-
lines.push(` ${successIcon} #${r.id} \u2014 ${r.goal.substring(0, 50)} @ ${time}`);
|
|
6486
|
-
lines.push(` ${Math.round(r.total_duration_ms / 1e3)}s | ${Math.round(r.success_rate * 100)}% success | complexity: ${r.complexity}`);
|
|
6487
|
-
lines.push("");
|
|
6382
|
+
const gotchas = getActiveGotchas();
|
|
6383
|
+
let synced = 0;
|
|
6384
|
+
for (const g of gotchas) {
|
|
6385
|
+
const checkStmt = db.prepare(
|
|
6386
|
+
`SELECT id FROM known_gotchas WHERE file_path = ? AND description = ?`
|
|
6387
|
+
);
|
|
6388
|
+
checkStmt.bind([g.filePath, g.description]);
|
|
6389
|
+
const exists = checkStmt.step();
|
|
6390
|
+
checkStmt.free();
|
|
6391
|
+
if (exists) continue;
|
|
6392
|
+
db.run(
|
|
6393
|
+
`INSERT INTO known_gotchas (file_path, description, severity) VALUES (?, ?, ?)`,
|
|
6394
|
+
[g.filePath, g.description, g.severity]
|
|
6395
|
+
);
|
|
6396
|
+
synced++;
|
|
6488
6397
|
}
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6398
|
+
saveDb();
|
|
6399
|
+
return { synced };
|
|
6400
|
+
} catch {
|
|
6401
|
+
return { synced: 0 };
|
|
6492
6402
|
}
|
|
6493
6403
|
}
|
|
6494
|
-
var
|
|
6495
|
-
"src/engine/
|
|
6404
|
+
var init_kumaGotchas = __esm({
|
|
6405
|
+
"src/engine/kumaGotchas.ts"() {
|
|
6496
6406
|
"use strict";
|
|
6497
|
-
init_kumaDb();
|
|
6498
6407
|
init_sessionMemory();
|
|
6499
|
-
|
|
6408
|
+
init_kumaDb();
|
|
6409
|
+
init_domainRules();
|
|
6500
6410
|
}
|
|
6501
6411
|
});
|
|
6502
6412
|
|
|
@@ -6669,19 +6579,19 @@ __export(kumaPolicyEngine_exports, {
|
|
|
6669
6579
|
processOverride: () => processOverride,
|
|
6670
6580
|
savePolicyConfig: () => savePolicyConfig
|
|
6671
6581
|
});
|
|
6672
|
-
import
|
|
6673
|
-
import
|
|
6582
|
+
import fs19 from "fs";
|
|
6583
|
+
import path19 from "path";
|
|
6674
6584
|
function loadPolicyConfig() {
|
|
6675
6585
|
try {
|
|
6676
6586
|
const root = getProjectRoot();
|
|
6677
|
-
const policyPath =
|
|
6678
|
-
if (
|
|
6679
|
-
const content =
|
|
6587
|
+
const policyPath = path19.join(root, ".kuma", "POLICY.json");
|
|
6588
|
+
if (fs19.existsSync(policyPath)) {
|
|
6589
|
+
const content = fs19.readFileSync(policyPath, "utf-8");
|
|
6680
6590
|
const config = JSON.parse(content);
|
|
6681
6591
|
return { ...DEFAULT_POLICY_CONFIG, ...config };
|
|
6682
6592
|
}
|
|
6683
|
-
const ymlPath =
|
|
6684
|
-
if (
|
|
6593
|
+
const ymlPath = path19.join(root, ".kuma", "policy.yml");
|
|
6594
|
+
if (fs19.existsSync(ymlPath)) {
|
|
6685
6595
|
console.error("[PolicyEngine] Found legacy policy.yml \u2014 consider migrating to POLICY.json");
|
|
6686
6596
|
}
|
|
6687
6597
|
} catch (err) {
|
|
@@ -6692,10 +6602,10 @@ function loadPolicyConfig() {
|
|
|
6692
6602
|
function savePolicyConfig(config) {
|
|
6693
6603
|
try {
|
|
6694
6604
|
const root = getProjectRoot();
|
|
6695
|
-
const policyPath =
|
|
6696
|
-
const dir =
|
|
6697
|
-
if (!
|
|
6698
|
-
|
|
6605
|
+
const policyPath = path19.join(root, ".kuma", "POLICY.json");
|
|
6606
|
+
const dir = path19.dirname(policyPath);
|
|
6607
|
+
if (!fs19.existsSync(dir)) fs19.mkdirSync(dir, { recursive: true });
|
|
6608
|
+
fs19.writeFileSync(policyPath, JSON.stringify(config, null, 2), "utf-8");
|
|
6699
6609
|
return `\u2705 Policy config saved to .kuma/POLICY.json (${config.rules.length} rules)`;
|
|
6700
6610
|
} catch (err) {
|
|
6701
6611
|
return `\u274C Failed to save policy: ${err}`;
|
|
@@ -6980,14 +6890,14 @@ __export(kumaAstValidator_exports, {
|
|
|
6980
6890
|
validateDiff: () => validateDiff,
|
|
6981
6891
|
validateFile: () => validateFile
|
|
6982
6892
|
});
|
|
6983
|
-
import
|
|
6984
|
-
import
|
|
6893
|
+
import fs20 from "fs";
|
|
6894
|
+
import path20 from "path";
|
|
6985
6895
|
function loadImportWhitelist() {
|
|
6986
6896
|
try {
|
|
6987
6897
|
const root = getProjectRoot();
|
|
6988
|
-
const whitelistPath =
|
|
6989
|
-
if (
|
|
6990
|
-
return JSON.parse(
|
|
6898
|
+
const whitelistPath = path20.join(root, ".kuma", "import-whitelist.json");
|
|
6899
|
+
if (fs20.existsSync(whitelistPath)) {
|
|
6900
|
+
return JSON.parse(fs20.readFileSync(whitelistPath, "utf-8"));
|
|
6991
6901
|
}
|
|
6992
6902
|
} catch {
|
|
6993
6903
|
}
|
|
@@ -7061,9 +6971,9 @@ function validateCodeContent(code, filePath) {
|
|
|
7061
6971
|
function validateFile(filePath) {
|
|
7062
6972
|
try {
|
|
7063
6973
|
const root = getProjectRoot();
|
|
7064
|
-
const fullPath =
|
|
7065
|
-
if (!
|
|
7066
|
-
const content =
|
|
6974
|
+
const fullPath = path20.resolve(root, filePath);
|
|
6975
|
+
if (!fs20.existsSync(fullPath)) return [];
|
|
6976
|
+
const content = fs20.readFileSync(fullPath, "utf-8");
|
|
7067
6977
|
return validateCodeContent(content, filePath);
|
|
7068
6978
|
} catch {
|
|
7069
6979
|
return [];
|
|
@@ -7134,16 +7044,16 @@ __export(kumaCheckpoint_exports, {
|
|
|
7134
7044
|
pruneCheckpoints: () => pruneCheckpoints,
|
|
7135
7045
|
rollbackToCheckpoint: () => rollbackToCheckpoint
|
|
7136
7046
|
});
|
|
7137
|
-
import
|
|
7138
|
-
import
|
|
7047
|
+
import fs21 from "fs";
|
|
7048
|
+
import path21 from "path";
|
|
7139
7049
|
async function createCheckpoint(label, description) {
|
|
7140
7050
|
try {
|
|
7141
7051
|
const root = getProjectRoot();
|
|
7142
|
-
const cpDir =
|
|
7143
|
-
if (
|
|
7052
|
+
const cpDir = path21.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
|
|
7053
|
+
if (fs21.existsSync(cpDir)) {
|
|
7144
7054
|
return `\u26A0\uFE0F Checkpoint "${label}" already exists. Use a different label or remove it first.`;
|
|
7145
7055
|
}
|
|
7146
|
-
|
|
7056
|
+
fs21.mkdirSync(cpDir, { recursive: true });
|
|
7147
7057
|
const db = await getDb();
|
|
7148
7058
|
const filesStmt = db.prepare(
|
|
7149
7059
|
"SELECT DISTINCT file_path FROM change_log ORDER BY id DESC LIMIT 100"
|
|
@@ -7152,19 +7062,19 @@ async function createCheckpoint(label, description) {
|
|
|
7152
7062
|
while (filesStmt.step()) {
|
|
7153
7063
|
const row = filesStmt.getAsObject();
|
|
7154
7064
|
const fp = row.file_path;
|
|
7155
|
-
const fullPath =
|
|
7156
|
-
if (
|
|
7157
|
-
const content =
|
|
7065
|
+
const fullPath = path21.resolve(root, fp);
|
|
7066
|
+
if (fs21.existsSync(fullPath)) {
|
|
7067
|
+
const content = fs21.readFileSync(fullPath, "utf-8");
|
|
7158
7068
|
const hash = simpleHash2(content);
|
|
7159
|
-
const fileDir =
|
|
7160
|
-
if (!
|
|
7161
|
-
|
|
7069
|
+
const fileDir = path21.dirname(path21.join(cpDir, "files", fp));
|
|
7070
|
+
if (!fs21.existsSync(fileDir)) fs21.mkdirSync(fileDir, { recursive: true });
|
|
7071
|
+
fs21.writeFileSync(path21.join(cpDir, "files", fp), content, "utf-8");
|
|
7162
7072
|
files.push({ path: fp, hash });
|
|
7163
7073
|
}
|
|
7164
7074
|
}
|
|
7165
7075
|
filesStmt.free();
|
|
7166
7076
|
const dbData = db.export();
|
|
7167
|
-
|
|
7077
|
+
fs21.writeFileSync(path21.join(cpDir, "kuma.db"), Buffer.from(dbData));
|
|
7168
7078
|
const manifest = {
|
|
7169
7079
|
label,
|
|
7170
7080
|
timestamp: Date.now(),
|
|
@@ -7172,8 +7082,8 @@ async function createCheckpoint(label, description) {
|
|
|
7172
7082
|
dbSnapshot: true,
|
|
7173
7083
|
description
|
|
7174
7084
|
};
|
|
7175
|
-
|
|
7176
|
-
|
|
7085
|
+
fs21.writeFileSync(
|
|
7086
|
+
path21.join(cpDir, "manifest.json"),
|
|
7177
7087
|
JSON.stringify(manifest, null, 2),
|
|
7178
7088
|
"utf-8"
|
|
7179
7089
|
);
|
|
@@ -7199,37 +7109,37 @@ async function createCheckpoint(label, description) {
|
|
|
7199
7109
|
async function rollbackToCheckpoint(label) {
|
|
7200
7110
|
try {
|
|
7201
7111
|
const root = getProjectRoot();
|
|
7202
|
-
const cpDir =
|
|
7203
|
-
const manifestPath =
|
|
7204
|
-
if (!
|
|
7112
|
+
const cpDir = path21.join(root, CHECKPOINT_DIR, sanitizeLabel(label));
|
|
7113
|
+
const manifestPath = path21.join(cpDir, "manifest.json");
|
|
7114
|
+
if (!fs21.existsSync(manifestPath)) {
|
|
7205
7115
|
return `\u274C Checkpoint "${label}" not found. Use kuma_safety({ action: 'checkpoint_list' }) to see available checkpoints.`;
|
|
7206
7116
|
}
|
|
7207
7117
|
const manifest = JSON.parse(
|
|
7208
|
-
|
|
7118
|
+
fs21.readFileSync(manifestPath, "utf-8")
|
|
7209
7119
|
);
|
|
7210
7120
|
let restored = 0;
|
|
7211
7121
|
let failed = 0;
|
|
7212
7122
|
for (const f of manifest.files) {
|
|
7213
|
-
const snapshotPath =
|
|
7214
|
-
if (
|
|
7123
|
+
const snapshotPath = path21.join(cpDir, "files", f.path);
|
|
7124
|
+
if (fs21.existsSync(snapshotPath)) {
|
|
7215
7125
|
try {
|
|
7216
|
-
const content =
|
|
7217
|
-
const targetPath =
|
|
7218
|
-
const targetDir =
|
|
7219
|
-
if (!
|
|
7220
|
-
|
|
7126
|
+
const content = fs21.readFileSync(snapshotPath, "utf-8");
|
|
7127
|
+
const targetPath = path21.resolve(root, f.path);
|
|
7128
|
+
const targetDir = path21.dirname(targetPath);
|
|
7129
|
+
if (!fs21.existsSync(targetDir)) fs21.mkdirSync(targetDir, { recursive: true });
|
|
7130
|
+
fs21.writeFileSync(targetPath, content, "utf-8");
|
|
7221
7131
|
restored++;
|
|
7222
7132
|
} catch {
|
|
7223
7133
|
failed++;
|
|
7224
7134
|
}
|
|
7225
7135
|
}
|
|
7226
7136
|
}
|
|
7227
|
-
const dbSnapshotPath =
|
|
7228
|
-
if (
|
|
7229
|
-
const kumaDir =
|
|
7230
|
-
const dbPath =
|
|
7231
|
-
const snapshotData =
|
|
7232
|
-
|
|
7137
|
+
const dbSnapshotPath = path21.join(cpDir, "kuma.db");
|
|
7138
|
+
if (fs21.existsSync(dbSnapshotPath)) {
|
|
7139
|
+
const kumaDir = path21.join(root, ".kuma");
|
|
7140
|
+
const dbPath = path21.join(kumaDir, "kuma.db");
|
|
7141
|
+
const snapshotData = fs21.readFileSync(dbSnapshotPath);
|
|
7142
|
+
fs21.writeFileSync(dbPath, snapshotData);
|
|
7233
7143
|
resetDbInstance();
|
|
7234
7144
|
}
|
|
7235
7145
|
sessionMemory.recordToolCall("kuma_checkpoint_rollback", {
|
|
@@ -7254,18 +7164,18 @@ async function rollbackToCheckpoint(label) {
|
|
|
7254
7164
|
function listCheckpoints() {
|
|
7255
7165
|
try {
|
|
7256
7166
|
const root = getProjectRoot();
|
|
7257
|
-
const cpDir =
|
|
7258
|
-
if (!
|
|
7167
|
+
const cpDir = path21.join(root, CHECKPOINT_DIR);
|
|
7168
|
+
if (!fs21.existsSync(cpDir)) {
|
|
7259
7169
|
return "\u{1F4ED} No checkpoints found. Use kuma_safety({ action: 'checkpoint', label: 'pre-feature-x' }) to create one.";
|
|
7260
7170
|
}
|
|
7261
|
-
const entries =
|
|
7171
|
+
const entries = fs21.readdirSync(cpDir);
|
|
7262
7172
|
const checkpoints = [];
|
|
7263
7173
|
for (const entry of entries) {
|
|
7264
|
-
const manifestPath =
|
|
7265
|
-
if (
|
|
7174
|
+
const manifestPath = path21.join(cpDir, entry, "manifest.json");
|
|
7175
|
+
if (fs21.existsSync(manifestPath)) {
|
|
7266
7176
|
try {
|
|
7267
7177
|
const manifest = JSON.parse(
|
|
7268
|
-
|
|
7178
|
+
fs21.readFileSync(manifestPath, "utf-8")
|
|
7269
7179
|
);
|
|
7270
7180
|
checkpoints.push({ label: entry, manifest });
|
|
7271
7181
|
} catch {
|
|
@@ -7297,12 +7207,12 @@ function listCheckpoints() {
|
|
|
7297
7207
|
function pruneCheckpoints(keep = 5) {
|
|
7298
7208
|
try {
|
|
7299
7209
|
const root = getProjectRoot();
|
|
7300
|
-
const cpDir =
|
|
7301
|
-
if (!
|
|
7302
|
-
const entries =
|
|
7210
|
+
const cpDir = path21.join(root, CHECKPOINT_DIR);
|
|
7211
|
+
if (!fs21.existsSync(cpDir)) return "\u{1F4ED} No checkpoints to prune.";
|
|
7212
|
+
const entries = fs21.readdirSync(cpDir).map((e) => ({ name: e, time: fs21.statSync(path21.join(cpDir, e)).mtimeMs })).sort((a, b) => b.time - a.time);
|
|
7303
7213
|
let removed = 0;
|
|
7304
7214
|
for (let i = keep; i < entries.length; i++) {
|
|
7305
|
-
|
|
7215
|
+
fs21.rmSync(path21.join(cpDir, entries[i].name), { recursive: true, force: true });
|
|
7306
7216
|
removed++;
|
|
7307
7217
|
}
|
|
7308
7218
|
return removed > 0 ? `\u{1F9F9} Pruned **${removed} old checkpoint(s)**. Kept ${Math.min(keep, entries.length)} most recent.` : "\u2705 No checkpoints needed pruning.";
|
|
@@ -7342,18 +7252,18 @@ __export(kumaContractEngine_exports, {
|
|
|
7342
7252
|
loadContracts: () => loadContracts,
|
|
7343
7253
|
runContractChecks: () => runContractChecks
|
|
7344
7254
|
});
|
|
7345
|
-
import
|
|
7346
|
-
import
|
|
7255
|
+
import fs22 from "fs";
|
|
7256
|
+
import path22 from "path";
|
|
7347
7257
|
function contractsDir() {
|
|
7348
|
-
return
|
|
7258
|
+
return path22.join(getProjectRoot(), ".kuma", "contracts");
|
|
7349
7259
|
}
|
|
7350
7260
|
function ensureContractsDir() {
|
|
7351
7261
|
const dir = contractsDir();
|
|
7352
|
-
if (!
|
|
7353
|
-
|
|
7354
|
-
const defaultPath =
|
|
7355
|
-
if (!
|
|
7356
|
-
|
|
7262
|
+
if (!fs22.existsSync(dir)) {
|
|
7263
|
+
fs22.mkdirSync(dir, { recursive: true });
|
|
7264
|
+
const defaultPath = path22.join(dir, "default.json");
|
|
7265
|
+
if (!fs22.existsSync(defaultPath)) {
|
|
7266
|
+
fs22.writeFileSync(
|
|
7357
7267
|
defaultPath,
|
|
7358
7268
|
JSON.stringify(DEFAULT_CONTRACTS, null, 2),
|
|
7359
7269
|
"utf-8"
|
|
@@ -7366,10 +7276,10 @@ function loadContracts() {
|
|
|
7366
7276
|
const dir = contractsDir();
|
|
7367
7277
|
const contracts = [];
|
|
7368
7278
|
try {
|
|
7369
|
-
const files =
|
|
7279
|
+
const files = fs22.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
7370
7280
|
for (const file of files) {
|
|
7371
7281
|
try {
|
|
7372
|
-
const content =
|
|
7282
|
+
const content = fs22.readFileSync(path22.join(dir, file), "utf-8");
|
|
7373
7283
|
const config = JSON.parse(content);
|
|
7374
7284
|
contracts.push(config);
|
|
7375
7285
|
} catch (err) {
|
|
@@ -7442,7 +7352,7 @@ function evaluateCondition(condition, contextPath) {
|
|
|
7442
7352
|
switch (condition.type) {
|
|
7443
7353
|
case "file_exists": {
|
|
7444
7354
|
const targetPath = resolveTarget(condition.target, contextPath, root);
|
|
7445
|
-
const exists = targetPath.some((p) =>
|
|
7355
|
+
const exists = targetPath.some((p) => fs22.existsSync(p));
|
|
7446
7356
|
return {
|
|
7447
7357
|
passed: exists,
|
|
7448
7358
|
message: exists ? `\u2705 File exists: ${condition.target}` : `\u274C Required file not found: ${condition.target} (${condition.description})`
|
|
@@ -7450,7 +7360,7 @@ function evaluateCondition(condition, contextPath) {
|
|
|
7450
7360
|
}
|
|
7451
7361
|
case "file_not_exists": {
|
|
7452
7362
|
const targetPath = resolveTarget(condition.target, contextPath, root);
|
|
7453
|
-
const exists = targetPath.some((p) =>
|
|
7363
|
+
const exists = targetPath.some((p) => fs22.existsSync(p));
|
|
7454
7364
|
return {
|
|
7455
7365
|
passed: !exists,
|
|
7456
7366
|
message: !exists ? `\u2705 File does not exist: ${condition.target}` : `\u274C File should not exist: ${condition.target} (${condition.description})`
|
|
@@ -7458,9 +7368,9 @@ function evaluateCondition(condition, contextPath) {
|
|
|
7458
7368
|
}
|
|
7459
7369
|
case "contains": {
|
|
7460
7370
|
if (!contextPath) return { passed: true, message: "\u26A0\uFE0F No file to check" };
|
|
7461
|
-
const fullPath =
|
|
7462
|
-
if (!
|
|
7463
|
-
const content =
|
|
7371
|
+
const fullPath = path22.resolve(root, contextPath);
|
|
7372
|
+
if (!fs22.existsSync(fullPath)) return { passed: true, message: "\u26A0\uFE0F File not found" };
|
|
7373
|
+
const content = fs22.readFileSync(fullPath, "utf-8");
|
|
7464
7374
|
const hasContent = condition.value ? content.includes(condition.value) : false;
|
|
7465
7375
|
return {
|
|
7466
7376
|
passed: hasContent,
|
|
@@ -7472,12 +7382,12 @@ function evaluateCondition(condition, contextPath) {
|
|
|
7472
7382
|
const root2 = getProjectRoot();
|
|
7473
7383
|
const targets = resolveTarget(condition.target, contextPath, root2);
|
|
7474
7384
|
for (const t of targets) {
|
|
7475
|
-
if (
|
|
7476
|
-
const content =
|
|
7385
|
+
if (fs22.existsSync(t)) {
|
|
7386
|
+
const content = fs22.readFileSync(t, "utf-8");
|
|
7477
7387
|
if (condition.value && content.includes(condition.value)) {
|
|
7478
7388
|
return {
|
|
7479
7389
|
passed: false,
|
|
7480
|
-
message: `\u274C Found forbidden pattern "${condition.value}" in ${
|
|
7390
|
+
message: `\u274C Found forbidden pattern "${condition.value}" in ${path22.relative(root2, t)} (${condition.description})`
|
|
7481
7391
|
};
|
|
7482
7392
|
}
|
|
7483
7393
|
}
|
|
@@ -7486,15 +7396,15 @@ function evaluateCondition(condition, contextPath) {
|
|
|
7486
7396
|
}
|
|
7487
7397
|
case "has_test_file": {
|
|
7488
7398
|
if (!contextPath) return { passed: true, message: "\u26A0\uFE0F No file to check" };
|
|
7489
|
-
const basename =
|
|
7490
|
-
const dir =
|
|
7399
|
+
const basename = path22.basename(contextPath, path22.extname(contextPath));
|
|
7400
|
+
const dir = path22.dirname(contextPath);
|
|
7491
7401
|
const possibleTests = [
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7402
|
+
path22.join(dir, `${basename}.test.ts`),
|
|
7403
|
+
path22.join(dir, `${basename}.spec.ts`),
|
|
7404
|
+
path22.join(dir, `__tests__/${basename}.test.ts`),
|
|
7405
|
+
path22.join(dir, `__tests__/${basename}.spec.ts`)
|
|
7496
7406
|
];
|
|
7497
|
-
const hasTest = possibleTests.some((t) =>
|
|
7407
|
+
const hasTest = possibleTests.some((t) => fs22.existsSync(t));
|
|
7498
7408
|
return {
|
|
7499
7409
|
passed: hasTest,
|
|
7500
7410
|
message: hasTest ? `\u2705 Test file exists` : `\u274C No test file found for ${contextPath} (${condition.description})`
|
|
@@ -7518,18 +7428,18 @@ function resolveTarget(pattern, contextPath, root) {
|
|
|
7518
7428
|
const r = root || getProjectRoot();
|
|
7519
7429
|
if (pattern.startsWith("*")) {
|
|
7520
7430
|
if (contextPath) {
|
|
7521
|
-
const dir =
|
|
7431
|
+
const dir = path22.dirname(path22.resolve(r, contextPath));
|
|
7522
7432
|
const suffix = pattern.replace("*", "");
|
|
7523
7433
|
try {
|
|
7524
|
-
const files =
|
|
7525
|
-
return files.filter((f) => f.endsWith(suffix) || f.includes(suffix)).map((f) =>
|
|
7434
|
+
const files = fs22.readdirSync(dir);
|
|
7435
|
+
return files.filter((f) => f.endsWith(suffix) || f.includes(suffix)).map((f) => path22.join(dir, f));
|
|
7526
7436
|
} catch {
|
|
7527
|
-
return [
|
|
7437
|
+
return [path22.join(dir, pattern.replace("*", ""))];
|
|
7528
7438
|
}
|
|
7529
7439
|
}
|
|
7530
7440
|
return [];
|
|
7531
7441
|
}
|
|
7532
|
-
return [
|
|
7442
|
+
return [path22.resolve(r, pattern)];
|
|
7533
7443
|
}
|
|
7534
7444
|
async function runContractChecks(toolName, filePath, phase = "pre") {
|
|
7535
7445
|
const verdicts = phase === "pre" ? evaluatePreConditions(toolName, filePath) : evaluatePostConditions(toolName, filePath);
|
|
@@ -7650,37 +7560,37 @@ __export(kumaVerifier_exports, {
|
|
|
7650
7560
|
getRunningVerificationPid: () => getRunningVerificationPid,
|
|
7651
7561
|
runAutoVerification: () => runAutoVerification
|
|
7652
7562
|
});
|
|
7653
|
-
import
|
|
7654
|
-
import
|
|
7563
|
+
import fs23 from "fs";
|
|
7564
|
+
import path23 from "path";
|
|
7655
7565
|
import { exec } from "child_process";
|
|
7656
7566
|
function getLockDir(root = process.cwd()) {
|
|
7657
|
-
return
|
|
7567
|
+
return path23.resolve(root, LOCK_DIR_NAME);
|
|
7658
7568
|
}
|
|
7659
7569
|
function getPidFile(root = process.cwd()) {
|
|
7660
|
-
return
|
|
7570
|
+
return path23.join(getLockDir(root), "pid");
|
|
7661
7571
|
}
|
|
7662
7572
|
function acquireFileLock(root) {
|
|
7663
7573
|
const lockDir = getLockDir(root);
|
|
7664
7574
|
try {
|
|
7665
|
-
|
|
7666
|
-
|
|
7575
|
+
fs23.mkdirSync(lockDir, { recursive: false });
|
|
7576
|
+
fs23.writeFileSync(getPidFile(root), String(process.pid), "utf-8");
|
|
7667
7577
|
return true;
|
|
7668
7578
|
} catch {
|
|
7669
7579
|
try {
|
|
7670
|
-
if (
|
|
7671
|
-
const pid = parseInt(
|
|
7580
|
+
if (fs23.existsSync(getPidFile(root))) {
|
|
7581
|
+
const pid = parseInt(fs23.readFileSync(getPidFile(root), "utf-8"), 10);
|
|
7672
7582
|
try {
|
|
7673
7583
|
process.kill(pid, 0);
|
|
7674
7584
|
return false;
|
|
7675
7585
|
} catch {
|
|
7676
|
-
|
|
7586
|
+
fs23.rmSync(lockDir, { recursive: true, force: true });
|
|
7677
7587
|
return acquireFileLock(root);
|
|
7678
7588
|
}
|
|
7679
7589
|
}
|
|
7680
7590
|
} catch {
|
|
7681
7591
|
}
|
|
7682
7592
|
try {
|
|
7683
|
-
|
|
7593
|
+
fs23.rmSync(lockDir, { recursive: true, force: true });
|
|
7684
7594
|
return acquireFileLock(root);
|
|
7685
7595
|
} catch {
|
|
7686
7596
|
return false;
|
|
@@ -7689,7 +7599,7 @@ function acquireFileLock(root) {
|
|
|
7689
7599
|
}
|
|
7690
7600
|
function releaseFileLock(root) {
|
|
7691
7601
|
try {
|
|
7692
|
-
|
|
7602
|
+
fs23.rmSync(getLockDir(root), { recursive: true, force: true });
|
|
7693
7603
|
} catch {
|
|
7694
7604
|
}
|
|
7695
7605
|
}
|
|
@@ -7712,36 +7622,36 @@ function getRunningVerificationPid() {
|
|
|
7712
7622
|
return _currentProcess?.pid ?? null;
|
|
7713
7623
|
}
|
|
7714
7624
|
function detectTestRunner(root = process.cwd()) {
|
|
7715
|
-
const pkgPath =
|
|
7716
|
-
if (
|
|
7625
|
+
const pkgPath = path23.join(root, "package.json");
|
|
7626
|
+
if (fs23.existsSync(pkgPath)) {
|
|
7717
7627
|
try {
|
|
7718
|
-
const pkg = JSON.parse(
|
|
7628
|
+
const pkg = JSON.parse(fs23.readFileSync(pkgPath, "utf-8"));
|
|
7719
7629
|
if (pkg.scripts && pkg.scripts.test) {
|
|
7720
|
-
if (
|
|
7721
|
-
if (
|
|
7630
|
+
if (fs23.existsSync(path23.join(root, "pnpm-lock.yaml"))) return { runner: "pnpm", baseCommand: "pnpm test" };
|
|
7631
|
+
if (fs23.existsSync(path23.join(root, "yarn.lock"))) return { runner: "yarn", baseCommand: "yarn test" };
|
|
7722
7632
|
return { runner: "npm", baseCommand: "npm test" };
|
|
7723
7633
|
}
|
|
7724
7634
|
} catch {
|
|
7725
7635
|
}
|
|
7726
7636
|
}
|
|
7727
|
-
if (
|
|
7728
|
-
if (
|
|
7729
|
-
if (
|
|
7730
|
-
if (
|
|
7637
|
+
if (fs23.existsSync(path23.join(root, "pytest.ini")) || fs23.existsSync(path23.join(root, "pyproject.toml"))) return { runner: "pytest", baseCommand: "pytest" };
|
|
7638
|
+
if (fs23.existsSync(path23.join(root, "Cargo.toml"))) return { runner: "cargo", baseCommand: "cargo test" };
|
|
7639
|
+
if (fs23.existsSync(path23.join(root, "go.mod"))) return { runner: "go", baseCommand: "go test ./..." };
|
|
7640
|
+
if (fs23.existsSync(path23.join(root, "Makefile"))) {
|
|
7731
7641
|
try {
|
|
7732
|
-
const makefileContent =
|
|
7642
|
+
const makefileContent = fs23.readFileSync(path23.join(root, "Makefile"), "utf-8");
|
|
7733
7643
|
const hasTestTarget = /^test:/m.test(makefileContent) || /^\s+test:/.test(makefileContent);
|
|
7734
7644
|
if (hasTestTarget) return { runner: "make", baseCommand: "make test" };
|
|
7735
7645
|
} catch {
|
|
7736
7646
|
}
|
|
7737
7647
|
}
|
|
7738
|
-
if (
|
|
7648
|
+
if (fs23.existsSync(path23.join(root, "package.json"))) {
|
|
7739
7649
|
try {
|
|
7740
|
-
const pkg = JSON.parse(
|
|
7650
|
+
const pkg = JSON.parse(fs23.readFileSync(path23.join(root, "package.json"), "utf-8"));
|
|
7741
7651
|
if (pkg.dependencies || pkg.devDependencies) {
|
|
7742
|
-
if (
|
|
7743
|
-
const srcDir =
|
|
7744
|
-
if (
|
|
7652
|
+
if (fs23.existsSync(path23.join(root, "tsconfig.json"))) return { runner: "tsc", baseCommand: "npx tsc --noEmit" };
|
|
7653
|
+
const srcDir = path23.join(root, "src");
|
|
7654
|
+
if (fs23.existsSync(srcDir)) return { runner: "node", baseCommand: `node -c "${srcDir}/**/*.js" 2>/dev/null || node --check $(find src -name '*.js' 2>/dev/null | head -5)` };
|
|
7745
7655
|
}
|
|
7746
7656
|
} catch {
|
|
7747
7657
|
}
|
|
@@ -7841,7 +7751,7 @@ async function runAutoVerification(options = {}) {
|
|
|
7841
7751
|
const heartbeatTimer = setInterval(() => {
|
|
7842
7752
|
if (getLockDir(root)) {
|
|
7843
7753
|
try {
|
|
7844
|
-
|
|
7754
|
+
fs23.writeFileSync(getPidFile(root), String(process.pid), "utf-8");
|
|
7845
7755
|
} catch {
|
|
7846
7756
|
}
|
|
7847
7757
|
}
|
|
@@ -7918,8 +7828,8 @@ __export(init_exports, {
|
|
|
7918
7828
|
generateInitMdContent: () => generateInitMdContent,
|
|
7919
7829
|
runInit: () => runInit
|
|
7920
7830
|
});
|
|
7921
|
-
import
|
|
7922
|
-
import
|
|
7831
|
+
import fs24 from "fs";
|
|
7832
|
+
import path24 from "path";
|
|
7923
7833
|
function configFilePath(type) {
|
|
7924
7834
|
switch (type) {
|
|
7925
7835
|
case "claude":
|
|
@@ -8366,14 +8276,11 @@ function generateInitMdContent() {
|
|
|
8366
8276
|
"| `todo` | Persistent todo CRUD | When managing tasks |",
|
|
8367
8277
|
"| `context` | Inject context notes | Additional notes when needed |",
|
|
8368
8278
|
"| `domain_rules` | Layer 1: business rules | Record business rules when discovered |",
|
|
8369
|
-
"| `
|
|
8370
|
-
"| `gen_test` | Generate test from trajectory | After trajectory analysis |",
|
|
8371
|
-
"| `trajectory` | List trajectories | Review past agent trajectories |",
|
|
8372
|
-
"| `skills` | List distilled skills | See auto-learned patterns |",
|
|
8279
|
+
"| `feature` | Record high-level feature with owns edges | Define features like 'Authentication', 'Billing' |",
|
|
8373
8280
|
"| `add_node` | Create structural nodes (low impact \u2014 grep faster) | Rarely; only for visualize debugging |",
|
|
8374
8281
|
"",
|
|
8375
8282
|
"| `delete_node` | Delete specific nodes, gotchas, todos, or decisions by ID | When you saved wrong data or need cleanup |",
|
|
8376
|
-
"| `clear` | Wipe ALL graph nodes, edges, gotchas
|
|
8283
|
+
"| `clear` | Wipe ALL graph nodes, edges, gotchas | Emergency reset \u2014 irreversible |",
|
|
8377
8284
|
"| `layers` | Show all 3 layers summary | Overview memory layers |",
|
|
8378
8285
|
"",
|
|
8379
8286
|
"### \u{1F6E1}\uFE0F kuma_safety \u2014 Safety & Policy",
|
|
@@ -8445,6 +8352,7 @@ function generateInitMdContent() {
|
|
|
8445
8352
|
"| \u{1F539} `small_square` | variable | Variables/constants | AUTO by scanner |",
|
|
8446
8353
|
"| \u{1F504} `circle` | workflow | Workflow definitions | AUTO by scanner |",
|
|
8447
8354
|
"| \u{1F4C4} `note` | context | Manual context notes | MANUAL \u2014 kuma_memory context |",
|
|
8355
|
+
"| \u2B50 `star` | feature | High-level feature (Authentication, Billing) | MANUAL \u2014 kuma_memory feature |",
|
|
8448
8356
|
"| \u{1F3DB}\uFE0F `pillar` | feature_domain | High-level feature domain anchor | MANUAL \u2014 kuma_memory arch_flow (domain:) |",
|
|
8449
8357
|
"| \u{1F517} `link` | cross_service_link | Cross-service/inter-file flow step | MANUAL \u2014 kuma_memory arch_flow (hops:) |",
|
|
8450
8358
|
"| \u{1F3F7}\uFE0F `tag` | domain_rule | Business rules (Layer 1) | MANUAL \u2014 kuma_memory domain_rules |",
|
|
@@ -8461,7 +8369,8 @@ function generateInitMdContent() {
|
|
|
8461
8369
|
"- `extends` \u2014 class extends class (AUTO)",
|
|
8462
8370
|
"- `contains` \u2014 file contains function (AUTO)",
|
|
8463
8371
|
"- `composes` \u2014 component contains sub-component (AUTO)",
|
|
8464
|
-
"- `owns` \u2014 domain owns file (MANUAL \u2014 arch_flow)",
|
|
8372
|
+
"- `owns` \u2014 domain/file owns file (MANUAL \u2014 arch_flow, feature)",
|
|
8373
|
+
"- `affects` \u2014 impact propagation chain (MANUAL \u2014 arch_flow, feature)",
|
|
8465
8374
|
"- `flows_through` \u2014 flow step to next step (MANUAL \u2014 arch_flow)",
|
|
8466
8375
|
"- `triggers` \u2014 flow triggers action (MANUAL)",
|
|
8467
8376
|
"- `syncs_with` \u2014 cross-service sync (MANUAL)",
|
|
@@ -8490,11 +8399,17 @@ function generateInitMdContent() {
|
|
|
8490
8399
|
"\u{1F9E0} Graph is persistent \u2014 nodes/edges accumulate across sessions.",
|
|
8491
8400
|
"",
|
|
8492
8401
|
"ON DISCOVERY (When needed):",
|
|
8402
|
+
" kuma_memory \u2192 feature \u2014 Record high-level feature (Authentication, Billing)",
|
|
8493
8403
|
" kuma_memory \u2192 domain_rules \u2014 Business rules (Layer 1)",
|
|
8494
8404
|
" kuma_memory \u2192 context \u2014 Additional notes",
|
|
8495
8405
|
" kuma_memory \u2192 todo \u2014 Task management",
|
|
8496
8406
|
" kuma_memory \u2192 mine \u2014 Historical context",
|
|
8497
8407
|
" kuma_memory \u2192 add_node \u2014 Manual function/class/component node (for visualize)",
|
|
8408
|
+
"",
|
|
8409
|
+
"GRAPH-AS-RETRIEVAL (search now includes):",
|
|
8410
|
+
" \u2022 Task-focused retrieval: top N relevant nodes for a task description",
|
|
8411
|
+
" \u2022 Impact analysis: which nodes are affected by a change",
|
|
8412
|
+
" \u2022 arch_flow now auto-creates affects edges for impact propagation",
|
|
8498
8413
|
"```",
|
|
8499
8414
|
"",
|
|
8500
8415
|
"## \u{1F9E0} How the Scanner Works",
|
|
@@ -8589,10 +8504,10 @@ function generateInitMdContent() {
|
|
|
8589
8504
|
].join("\n");
|
|
8590
8505
|
}
|
|
8591
8506
|
function handleOpencodeSecondary(root, results) {
|
|
8592
|
-
const skillPath =
|
|
8507
|
+
const skillPath = path24.resolve(root, ".agents", "skills", "kuma", "SKILL.md");
|
|
8593
8508
|
if (results.some((r) => r.filePath === ".agents/skills/kuma/SKILL.md")) return;
|
|
8594
8509
|
try {
|
|
8595
|
-
const dir =
|
|
8510
|
+
const dir = path24.dirname(skillPath);
|
|
8596
8511
|
const content = [
|
|
8597
8512
|
"---",
|
|
8598
8513
|
"name: kuma-mcp",
|
|
@@ -8625,18 +8540,18 @@ function handleOpencodeSecondary(root, results) {
|
|
|
8625
8540
|
"",
|
|
8626
8541
|
"\u{1F4D6} Full rules: `.kuma/init.md`"
|
|
8627
8542
|
].join("\n");
|
|
8628
|
-
if (
|
|
8629
|
-
const existing =
|
|
8543
|
+
if (fs24.existsSync(skillPath)) {
|
|
8544
|
+
const existing = fs24.readFileSync(skillPath, "utf-8");
|
|
8630
8545
|
if (existing.includes("kuma-mcp")) {
|
|
8631
8546
|
results.push({ type: "opencode", filePath: ".agents/skills/kuma/SKILL.md", action: "skipped" });
|
|
8632
8547
|
return;
|
|
8633
8548
|
}
|
|
8634
8549
|
const newContent = existing.trimEnd() + "\n\n---\n\n" + content;
|
|
8635
|
-
|
|
8550
|
+
fs24.writeFileSync(skillPath, newContent, "utf-8");
|
|
8636
8551
|
results.push({ type: "opencode", filePath: ".agents/skills/kuma/SKILL.md", action: "appended" });
|
|
8637
8552
|
} else {
|
|
8638
|
-
if (!
|
|
8639
|
-
|
|
8553
|
+
if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
|
|
8554
|
+
fs24.writeFileSync(skillPath, content, "utf-8");
|
|
8640
8555
|
results.push({ type: "opencode", filePath: ".agents/skills/kuma/SKILL.md", action: "created" });
|
|
8641
8556
|
}
|
|
8642
8557
|
} catch (err) {
|
|
@@ -8649,21 +8564,21 @@ function handleOpencodeSecondary(root, results) {
|
|
|
8649
8564
|
}
|
|
8650
8565
|
}
|
|
8651
8566
|
function handleCodexSecondary(root, results) {
|
|
8652
|
-
const tomlPath =
|
|
8567
|
+
const tomlPath = path24.resolve(root, ".codex/config.toml");
|
|
8653
8568
|
if (results.some((r) => r.filePath === ".codex/config.toml")) return;
|
|
8654
8569
|
try {
|
|
8655
|
-
const dir =
|
|
8656
|
-
if (!
|
|
8657
|
-
if (
|
|
8658
|
-
const existingContent =
|
|
8570
|
+
const dir = path24.dirname(tomlPath);
|
|
8571
|
+
if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
|
|
8572
|
+
if (fs24.existsSync(tomlPath)) {
|
|
8573
|
+
const existingContent = fs24.readFileSync(tomlPath, "utf-8");
|
|
8659
8574
|
if (existingContent.includes("kuma")) {
|
|
8660
8575
|
results.push({ type: "codex", filePath: ".codex/config.toml", action: "skipped" });
|
|
8661
8576
|
return;
|
|
8662
8577
|
}
|
|
8663
|
-
|
|
8578
|
+
fs24.writeFileSync(tomlPath, existingContent.trimEnd() + "\n\n" + codexConfigTomlTemplate(), "utf-8");
|
|
8664
8579
|
results.push({ type: "codex", filePath: ".codex/config.toml", action: "appended" });
|
|
8665
8580
|
} else {
|
|
8666
|
-
|
|
8581
|
+
fs24.writeFileSync(tomlPath, codexConfigTomlTemplate(), "utf-8");
|
|
8667
8582
|
results.push({ type: "codex", filePath: ".codex/config.toml", action: "created" });
|
|
8668
8583
|
}
|
|
8669
8584
|
} catch (err) {
|
|
@@ -8676,11 +8591,11 @@ function handleCodexSecondary(root, results) {
|
|
|
8676
8591
|
}
|
|
8677
8592
|
}
|
|
8678
8593
|
function handleQwenSecondary(root, results) {
|
|
8679
|
-
const settingsPath =
|
|
8594
|
+
const settingsPath = path24.resolve(root, "settings.json");
|
|
8680
8595
|
if (results.some((r) => r.filePath === "settings.json")) return;
|
|
8681
8596
|
try {
|
|
8682
|
-
if (
|
|
8683
|
-
const existingContent =
|
|
8597
|
+
if (fs24.existsSync(settingsPath)) {
|
|
8598
|
+
const existingContent = fs24.readFileSync(settingsPath, "utf-8");
|
|
8684
8599
|
if (existingContent.includes("kuma")) {
|
|
8685
8600
|
if (!existingContent.includes("_Generated by Kuma MCP_")) {
|
|
8686
8601
|
try {
|
|
@@ -8688,7 +8603,7 @@ function handleQwenSecondary(root, results) {
|
|
|
8688
8603
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
8689
8604
|
if (!parsed.mcpServers.kuma) {
|
|
8690
8605
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
8691
|
-
|
|
8606
|
+
fs24.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
8692
8607
|
results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
|
|
8693
8608
|
return;
|
|
8694
8609
|
}
|
|
@@ -8703,13 +8618,13 @@ function handleQwenSecondary(root, results) {
|
|
|
8703
8618
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
8704
8619
|
if (!parsed.mcpServers.kuma) {
|
|
8705
8620
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
8706
|
-
|
|
8621
|
+
fs24.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
8707
8622
|
results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
|
|
8708
8623
|
}
|
|
8709
8624
|
} catch {
|
|
8710
8625
|
}
|
|
8711
8626
|
} else {
|
|
8712
|
-
|
|
8627
|
+
fs24.writeFileSync(settingsPath, qwenSettingsTemplate(), "utf-8");
|
|
8713
8628
|
results.push({ type: "qwen", filePath: "settings.json", action: "created" });
|
|
8714
8629
|
}
|
|
8715
8630
|
} catch (err) {
|
|
@@ -8812,18 +8727,18 @@ function getCombinedAgentsMd(selectedTypes) {
|
|
|
8812
8727
|
return lines.join("\n");
|
|
8813
8728
|
}
|
|
8814
8729
|
function handleAntigravityMcpConfig(root, results) {
|
|
8815
|
-
const mcpPath =
|
|
8730
|
+
const mcpPath = path24.resolve(root, ".agents/mcp_config.json");
|
|
8816
8731
|
if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
|
|
8817
8732
|
try {
|
|
8818
|
-
const mcpDir =
|
|
8819
|
-
if (
|
|
8820
|
-
const existingContent =
|
|
8733
|
+
const mcpDir = path24.dirname(mcpPath);
|
|
8734
|
+
if (fs24.existsSync(mcpPath)) {
|
|
8735
|
+
const existingContent = fs24.readFileSync(mcpPath, "utf-8");
|
|
8821
8736
|
if (existingContent.includes("kuma")) {
|
|
8822
8737
|
if (!existingContent.includes("_Generated by Kuma MCP_")) {
|
|
8823
8738
|
const trimmed = existingContent.trimEnd();
|
|
8824
8739
|
if (trimmed.endsWith("}")) {
|
|
8825
8740
|
const updated = trimmed.slice(0, -1).trimEnd() + ',\n "_kuma_note": "Kuma MCP - Generated by kuma init"\n}\n';
|
|
8826
|
-
|
|
8741
|
+
fs24.writeFileSync(mcpPath, updated, "utf-8");
|
|
8827
8742
|
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
8828
8743
|
}
|
|
8829
8744
|
}
|
|
@@ -8832,11 +8747,11 @@ function handleAntigravityMcpConfig(root, results) {
|
|
|
8832
8747
|
const parsed = JSON.parse(existingContent);
|
|
8833
8748
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
8834
8749
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
8835
|
-
|
|
8750
|
+
fs24.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
8836
8751
|
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
8837
8752
|
} else {
|
|
8838
|
-
if (!
|
|
8839
|
-
|
|
8753
|
+
if (!fs24.existsSync(mcpDir)) fs24.mkdirSync(mcpDir, { recursive: true });
|
|
8754
|
+
fs24.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
|
|
8840
8755
|
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "created" });
|
|
8841
8756
|
}
|
|
8842
8757
|
} catch (err) {
|
|
@@ -8849,18 +8764,18 @@ function handleAntigravityMcpConfig(root, results) {
|
|
|
8849
8764
|
}
|
|
8850
8765
|
}
|
|
8851
8766
|
function handleAiderSecondary(root, results) {
|
|
8852
|
-
const ymlPath =
|
|
8767
|
+
const ymlPath = path24.resolve(root, ".aider.conf.yml");
|
|
8853
8768
|
if (results.some((r) => r.filePath === ".aider.conf.yml")) return;
|
|
8854
8769
|
try {
|
|
8855
8770
|
const conventionsRef = "read: CONVENTIONS.md";
|
|
8856
|
-
if (
|
|
8857
|
-
const existingContent =
|
|
8771
|
+
if (fs24.existsSync(ymlPath)) {
|
|
8772
|
+
const existingContent = fs24.readFileSync(ymlPath, "utf-8");
|
|
8858
8773
|
if (existingContent.includes("CONVENTIONS.md") || existingContent.includes("kuma")) {
|
|
8859
8774
|
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "skipped" });
|
|
8860
8775
|
return;
|
|
8861
8776
|
}
|
|
8862
8777
|
const newContent = existingContent.trimEnd() + "\n\n# Kuma MCP conventions\n" + conventionsRef + "\n";
|
|
8863
|
-
|
|
8778
|
+
fs24.writeFileSync(ymlPath, newContent, "utf-8");
|
|
8864
8779
|
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "appended" });
|
|
8865
8780
|
} else {
|
|
8866
8781
|
const content = [
|
|
@@ -8870,7 +8785,7 @@ function handleAiderSecondary(root, results) {
|
|
|
8870
8785
|
conventionsRef,
|
|
8871
8786
|
""
|
|
8872
8787
|
].join("\n");
|
|
8873
|
-
|
|
8788
|
+
fs24.writeFileSync(ymlPath, content, "utf-8");
|
|
8874
8789
|
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "created" });
|
|
8875
8790
|
}
|
|
8876
8791
|
} catch (err) {
|
|
@@ -8883,10 +8798,10 @@ function handleAiderSecondary(root, results) {
|
|
|
8883
8798
|
}
|
|
8884
8799
|
}
|
|
8885
8800
|
function handleCopilotSecondary(root, results) {
|
|
8886
|
-
const skillPath =
|
|
8801
|
+
const skillPath = path24.resolve(root, ".github/skills/kuma/SKILL.md");
|
|
8887
8802
|
if (results.some((r) => r.filePath === ".github/skills/kuma/SKILL.md")) return;
|
|
8888
8803
|
try {
|
|
8889
|
-
const dir =
|
|
8804
|
+
const dir = path24.dirname(skillPath);
|
|
8890
8805
|
const content = [
|
|
8891
8806
|
"---",
|
|
8892
8807
|
"name: kuma-mcp",
|
|
@@ -8897,17 +8812,17 @@ function handleCopilotSecondary(root, results) {
|
|
|
8897
8812
|
"",
|
|
8898
8813
|
"\u{1F4D6} Read `.kuma/init.md` for detailed rules."
|
|
8899
8814
|
].join("\n");
|
|
8900
|
-
if (
|
|
8901
|
-
const existingContent =
|
|
8815
|
+
if (fs24.existsSync(skillPath)) {
|
|
8816
|
+
const existingContent = fs24.readFileSync(skillPath, "utf-8");
|
|
8902
8817
|
if (existingContent.includes("kuma")) {
|
|
8903
8818
|
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "skipped" });
|
|
8904
8819
|
return;
|
|
8905
8820
|
}
|
|
8906
|
-
|
|
8821
|
+
fs24.writeFileSync(skillPath, existingContent.trimEnd() + "\n\n" + content, "utf-8");
|
|
8907
8822
|
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "appended" });
|
|
8908
8823
|
} else {
|
|
8909
|
-
if (!
|
|
8910
|
-
|
|
8824
|
+
if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
|
|
8825
|
+
fs24.writeFileSync(skillPath, content, "utf-8");
|
|
8911
8826
|
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "created" });
|
|
8912
8827
|
}
|
|
8913
8828
|
} catch (err) {
|
|
@@ -8920,21 +8835,21 @@ function handleCopilotSecondary(root, results) {
|
|
|
8920
8835
|
}
|
|
8921
8836
|
}
|
|
8922
8837
|
function handleOpenclawSecondary(root, results) {
|
|
8923
|
-
const mcpPath =
|
|
8838
|
+
const mcpPath = path24.resolve(root, ".agents/mcp_config.json");
|
|
8924
8839
|
if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
|
|
8925
8840
|
try {
|
|
8926
|
-
const dir =
|
|
8927
|
-
if (
|
|
8928
|
-
const existingContent =
|
|
8841
|
+
const dir = path24.dirname(mcpPath);
|
|
8842
|
+
if (fs24.existsSync(mcpPath)) {
|
|
8843
|
+
const existingContent = fs24.readFileSync(mcpPath, "utf-8");
|
|
8929
8844
|
if (existingContent.includes("kuma")) return;
|
|
8930
8845
|
const parsed = JSON.parse(existingContent);
|
|
8931
8846
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
8932
8847
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
8933
|
-
|
|
8848
|
+
fs24.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
8934
8849
|
results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
8935
8850
|
} else {
|
|
8936
|
-
if (!
|
|
8937
|
-
|
|
8851
|
+
if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
|
|
8852
|
+
fs24.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
|
|
8938
8853
|
results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "created" });
|
|
8939
8854
|
}
|
|
8940
8855
|
} catch (err) {
|
|
@@ -8947,7 +8862,7 @@ function handleOpenclawSecondary(root, results) {
|
|
|
8947
8862
|
}
|
|
8948
8863
|
}
|
|
8949
8864
|
function handleQuickrefGeneration(root, results) {
|
|
8950
|
-
const quickrefPath =
|
|
8865
|
+
const quickrefPath = path24.resolve(root, ".kuma/quickref.md");
|
|
8951
8866
|
const content = [
|
|
8952
8867
|
"# Kuma Quick Reference",
|
|
8953
8868
|
"",
|
|
@@ -8981,17 +8896,17 @@ function handleQuickrefGeneration(root, results) {
|
|
|
8981
8896
|
"_Generated by Kuma MCP - https://github.com/plumpslabs/kuma_"
|
|
8982
8897
|
].join("\n");
|
|
8983
8898
|
try {
|
|
8984
|
-
const kumaDir =
|
|
8985
|
-
if (!
|
|
8986
|
-
if (
|
|
8987
|
-
const existing =
|
|
8899
|
+
const kumaDir = path24.dirname(quickrefPath);
|
|
8900
|
+
if (!fs24.existsSync(kumaDir)) fs24.mkdirSync(kumaDir, { recursive: true });
|
|
8901
|
+
if (fs24.existsSync(quickrefPath)) {
|
|
8902
|
+
const existing = fs24.readFileSync(quickrefPath, "utf-8");
|
|
8988
8903
|
if (existing.includes("_Generated by Kuma MCP_")) {
|
|
8989
8904
|
results.push({ type: "claude", filePath: ".kuma/quickref.md", action: "skipped" });
|
|
8990
8905
|
return;
|
|
8991
8906
|
}
|
|
8992
8907
|
results.push({ type: "claude", filePath: ".kuma/quickref.md", action: "skipped" });
|
|
8993
8908
|
} else {
|
|
8994
|
-
|
|
8909
|
+
fs24.writeFileSync(quickrefPath, content, "utf-8");
|
|
8995
8910
|
results.push({ type: "claude", filePath: ".kuma/quickref.md", action: "created" });
|
|
8996
8911
|
}
|
|
8997
8912
|
} catch (err) {
|
|
@@ -9004,20 +8919,20 @@ function handleQuickrefGeneration(root, results) {
|
|
|
9004
8919
|
}
|
|
9005
8920
|
}
|
|
9006
8921
|
function handleInitMdGeneration(root, results) {
|
|
9007
|
-
const initMdPath =
|
|
8922
|
+
const initMdPath = path24.resolve(root, ".kuma/init.md");
|
|
9008
8923
|
try {
|
|
9009
|
-
const kumaDir =
|
|
9010
|
-
if (!
|
|
9011
|
-
if (
|
|
9012
|
-
const existing =
|
|
8924
|
+
const kumaDir = path24.dirname(initMdPath);
|
|
8925
|
+
if (!fs24.existsSync(kumaDir)) fs24.mkdirSync(kumaDir, { recursive: true });
|
|
8926
|
+
if (fs24.existsSync(initMdPath)) {
|
|
8927
|
+
const existing = fs24.readFileSync(initMdPath, "utf-8");
|
|
9013
8928
|
if (existing.includes("_Generated by Kuma MCP_")) {
|
|
9014
8929
|
results.push({ type: "claude", filePath: ".kuma/init.md", action: "skipped" });
|
|
9015
8930
|
return;
|
|
9016
8931
|
}
|
|
9017
|
-
|
|
8932
|
+
fs24.writeFileSync(initMdPath, existing.trimEnd() + "\n\n" + generateInitMdContent(), "utf-8");
|
|
9018
8933
|
results.push({ type: "claude", filePath: ".kuma/init.md", action: "appended" });
|
|
9019
8934
|
} else {
|
|
9020
|
-
|
|
8935
|
+
fs24.writeFileSync(initMdPath, generateInitMdContent(), "utf-8");
|
|
9021
8936
|
results.push({ type: "claude", filePath: ".kuma/init.md", action: "created" });
|
|
9022
8937
|
}
|
|
9023
8938
|
} catch (err) {
|
|
@@ -9031,20 +8946,20 @@ function handleInitMdGeneration(root, results) {
|
|
|
9031
8946
|
handleQuickrefGeneration(root, results);
|
|
9032
8947
|
}
|
|
9033
8948
|
function handleCodewhaleSecondary(root, results) {
|
|
9034
|
-
const mcpPath =
|
|
8949
|
+
const mcpPath = path24.resolve(root, ".codewhale/mcp.json");
|
|
9035
8950
|
if (results.some((r) => r.filePath === ".codewhale/mcp.json")) return;
|
|
9036
8951
|
try {
|
|
9037
|
-
const dir =
|
|
9038
|
-
if (
|
|
9039
|
-
const existingContent =
|
|
8952
|
+
const dir = path24.dirname(mcpPath);
|
|
8953
|
+
if (fs24.existsSync(mcpPath)) {
|
|
8954
|
+
const existingContent = fs24.readFileSync(mcpPath, "utf-8");
|
|
9040
8955
|
if (existingContent.includes("kuma")) return;
|
|
9041
8956
|
const parsed = JSON.parse(existingContent);
|
|
9042
8957
|
parsed.mcpServers = parsed.mcpServers || {};
|
|
9043
8958
|
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
9044
|
-
|
|
8959
|
+
fs24.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
9045
8960
|
results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "appended" });
|
|
9046
8961
|
} else {
|
|
9047
|
-
if (!
|
|
8962
|
+
if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
|
|
9048
8963
|
const config = {
|
|
9049
8964
|
mcpServers: {
|
|
9050
8965
|
kuma: {
|
|
@@ -9054,7 +8969,7 @@ function handleCodewhaleSecondary(root, results) {
|
|
|
9054
8969
|
}
|
|
9055
8970
|
}
|
|
9056
8971
|
};
|
|
9057
|
-
|
|
8972
|
+
fs24.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
9058
8973
|
results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "created" });
|
|
9059
8974
|
}
|
|
9060
8975
|
} catch (err) {
|
|
@@ -9076,28 +8991,28 @@ function runInit(options) {
|
|
|
9076
8991
|
let agentsMdHandled = false;
|
|
9077
8992
|
for (const type of selected) {
|
|
9078
8993
|
const relativePath = configFilePath(type);
|
|
9079
|
-
const fullPath =
|
|
8994
|
+
const fullPath = path24.resolve(root, relativePath);
|
|
9080
8995
|
const getTemplate = TEMPLATES[type];
|
|
9081
8996
|
try {
|
|
9082
8997
|
if (AGENTS_MD_TYPES.includes(type) && !agentsMdHandled) {
|
|
9083
8998
|
agentsMdHandled = true;
|
|
9084
8999
|
const combinedContent = getCombinedAgentsMd(new Set(agentsMdSelected));
|
|
9085
|
-
if (
|
|
9000
|
+
if (fs24.existsSync(fullPath)) {
|
|
9086
9001
|
if (options.skipExisting) {
|
|
9087
9002
|
results.push({ type, filePath: relativePath, action: "skipped" });
|
|
9088
9003
|
} else {
|
|
9089
|
-
const existingContent =
|
|
9004
|
+
const existingContent = fs24.readFileSync(fullPath, "utf-8");
|
|
9090
9005
|
if (existingContent.includes("_Generated by Kuma MCP_")) {
|
|
9091
9006
|
results.push({ type, filePath: relativePath, action: "skipped" });
|
|
9092
9007
|
} else {
|
|
9093
|
-
|
|
9008
|
+
fs24.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
|
|
9094
9009
|
results.push({ type, filePath: relativePath, action: "appended" });
|
|
9095
9010
|
}
|
|
9096
9011
|
}
|
|
9097
9012
|
} else {
|
|
9098
|
-
const dir =
|
|
9099
|
-
if (!
|
|
9100
|
-
|
|
9013
|
+
const dir = path24.dirname(fullPath);
|
|
9014
|
+
if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
|
|
9015
|
+
fs24.writeFileSync(fullPath, combinedContent, "utf-8");
|
|
9101
9016
|
results.push({ type, filePath: relativePath, action: "created" });
|
|
9102
9017
|
}
|
|
9103
9018
|
if (selectedSet.has("opencode")) handleOpencodeSecondary(root, results);
|
|
@@ -9109,12 +9024,12 @@ function runInit(options) {
|
|
|
9109
9024
|
continue;
|
|
9110
9025
|
} else {
|
|
9111
9026
|
const template = getTemplate();
|
|
9112
|
-
if (
|
|
9027
|
+
if (fs24.existsSync(fullPath)) {
|
|
9113
9028
|
if (options.skipExisting) {
|
|
9114
9029
|
results.push({ type, filePath: relativePath, action: "skipped" });
|
|
9115
9030
|
continue;
|
|
9116
9031
|
}
|
|
9117
|
-
const existingContent =
|
|
9032
|
+
const existingContent = fs24.readFileSync(fullPath, "utf-8");
|
|
9118
9033
|
if (existingContent.includes("_Generated by Kuma MCP_")) {
|
|
9119
9034
|
if (type === "antigravity") {
|
|
9120
9035
|
handleAntigravityMcpConfig(root, results);
|
|
@@ -9127,14 +9042,14 @@ function runInit(options) {
|
|
|
9127
9042
|
continue;
|
|
9128
9043
|
}
|
|
9129
9044
|
const newContent = existingContent.trimEnd() + APPEND_SEPARATOR + template;
|
|
9130
|
-
|
|
9045
|
+
fs24.writeFileSync(fullPath, newContent, "utf-8");
|
|
9131
9046
|
results.push({ type, filePath: relativePath, action: "appended" });
|
|
9132
9047
|
} else {
|
|
9133
|
-
const dir =
|
|
9134
|
-
if (!
|
|
9135
|
-
|
|
9048
|
+
const dir = path24.dirname(fullPath);
|
|
9049
|
+
if (!fs24.existsSync(dir)) {
|
|
9050
|
+
fs24.mkdirSync(dir, { recursive: true });
|
|
9136
9051
|
}
|
|
9137
|
-
|
|
9052
|
+
fs24.writeFileSync(fullPath, template, "utf-8");
|
|
9138
9053
|
results.push({ type, filePath: relativePath, action: "created" });
|
|
9139
9054
|
}
|
|
9140
9055
|
if (type === "antigravity") {
|
|
@@ -9290,19 +9205,19 @@ __export(agentDetector_exports, {
|
|
|
9290
9205
|
getSkillPath: () => getSkillPath,
|
|
9291
9206
|
skillExists: () => skillExists
|
|
9292
9207
|
});
|
|
9293
|
-
import
|
|
9294
|
-
import
|
|
9208
|
+
import fs25 from "fs";
|
|
9209
|
+
import path25 from "path";
|
|
9295
9210
|
function checkFile(root, filePath) {
|
|
9296
9211
|
try {
|
|
9297
|
-
return
|
|
9212
|
+
return fs25.existsSync(path25.join(root, filePath));
|
|
9298
9213
|
} catch {
|
|
9299
9214
|
return false;
|
|
9300
9215
|
}
|
|
9301
9216
|
}
|
|
9302
9217
|
function checkDir(root, dirPath) {
|
|
9303
9218
|
try {
|
|
9304
|
-
const fullPath =
|
|
9305
|
-
return
|
|
9219
|
+
const fullPath = path25.join(root, dirPath);
|
|
9220
|
+
return fs25.existsSync(fullPath) && fs25.statSync(fullPath).isDirectory();
|
|
9306
9221
|
} catch {
|
|
9307
9222
|
return false;
|
|
9308
9223
|
}
|
|
@@ -9370,7 +9285,7 @@ function getAgentLabel(type) {
|
|
|
9370
9285
|
function skillExists(type, projectRoot) {
|
|
9371
9286
|
const root = projectRoot ?? getProjectRoot();
|
|
9372
9287
|
const skillPath = getSkillPath(type);
|
|
9373
|
-
return
|
|
9288
|
+
return fs25.existsSync(path25.join(root, skillPath));
|
|
9374
9289
|
}
|
|
9375
9290
|
var AGENT_DETECTORS;
|
|
9376
9291
|
var init_agentDetector = __esm({
|
|
@@ -10339,8 +10254,8 @@ init_kumaDb();
|
|
|
10339
10254
|
init_kumaSelfHeal();
|
|
10340
10255
|
init_kumaMemory();
|
|
10341
10256
|
init_pathValidator();
|
|
10342
|
-
import
|
|
10343
|
-
import
|
|
10257
|
+
import fs14 from "fs";
|
|
10258
|
+
import path14 from "path";
|
|
10344
10259
|
var MEMORY_ALIASES = {
|
|
10345
10260
|
// Session synonyms
|
|
10346
10261
|
"session": "session",
|
|
@@ -10421,29 +10336,14 @@ var MEMORY_ALIASES = {
|
|
|
10421
10336
|
"layers": "layers",
|
|
10422
10337
|
"3-layer": "layers",
|
|
10423
10338
|
"memory-layers": "layers",
|
|
10424
|
-
// Federated synonyms (Issue #27)
|
|
10425
|
-
"federated": "federated",
|
|
10426
|
-
"federated-graph": "federated",
|
|
10427
|
-
"kuma-uri": "federated",
|
|
10428
|
-
"remote-graph": "federated",
|
|
10429
|
-
// Test generation synonyms (Issue #28)
|
|
10430
|
-
"gen_test": "gen_test",
|
|
10431
|
-
"gen-test": "gen_test",
|
|
10432
|
-
"generate-test": "gen_test",
|
|
10433
|
-
"gentest": "gen_test",
|
|
10434
|
-
// Trajectory synonyms
|
|
10435
|
-
"trajectory": "trajectory",
|
|
10436
|
-
"trajectories": "trajectory",
|
|
10437
|
-
"traj": "trajectory",
|
|
10438
|
-
// Skills synonyms
|
|
10439
|
-
"skills": "skills",
|
|
10440
|
-
"distilled-skills": "skills",
|
|
10441
|
-
"skill-list": "skills",
|
|
10442
10339
|
// Add node synonyms
|
|
10443
|
-
"add_node": "add_node",
|
|
10444
10340
|
"add-node": "add_node",
|
|
10445
|
-
"node": "add_node",
|
|
10446
10341
|
"create-node": "add_node",
|
|
10342
|
+
// Feature synonyms
|
|
10343
|
+
"feature": "feature",
|
|
10344
|
+
"features": "feature",
|
|
10345
|
+
"record-feature": "feature",
|
|
10346
|
+
"define-feature": "feature",
|
|
10447
10347
|
"record-node": "add_node",
|
|
10448
10348
|
// Delete node synonyms
|
|
10449
10349
|
"delete_node": "delete_node",
|
|
@@ -10495,22 +10395,16 @@ async function handleMemory(params) {
|
|
|
10495
10395
|
return handleGotchaAction(params);
|
|
10496
10396
|
case "layers":
|
|
10497
10397
|
return handleLayersSummary(params);
|
|
10498
|
-
case "federated":
|
|
10499
|
-
return handleFederated(params);
|
|
10500
|
-
case "gen_test":
|
|
10501
|
-
return handleGenTest(params);
|
|
10502
|
-
case "trajectory":
|
|
10503
|
-
return handleTrajectoryList(params);
|
|
10504
|
-
case "skills":
|
|
10505
|
-
return handleSkillsList(params);
|
|
10506
10398
|
case "add_node":
|
|
10507
10399
|
return handleAddNode(params);
|
|
10400
|
+
case "feature":
|
|
10401
|
+
return handleFeature(params);
|
|
10508
10402
|
case "delete_node":
|
|
10509
10403
|
return handleDeleteNode(params);
|
|
10510
10404
|
case "clear": {
|
|
10511
10405
|
const { clearGraph: clearGraph2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
|
|
10512
10406
|
await clearGraph2();
|
|
10513
|
-
return "\u{1F5D1}\uFE0F **Knowledge Graph Cleared** \u2014 All nodes, edges,
|
|
10407
|
+
return "\u{1F5D1}\uFE0F **Knowledge Graph Cleared** \u2014 All nodes, edges, and gotchas have been wiped from disk and memory.";
|
|
10514
10408
|
}
|
|
10515
10409
|
default:
|
|
10516
10410
|
return `Unknown action "${action}".`;
|
|
@@ -10528,8 +10422,13 @@ async function handleDecision(params) {
|
|
|
10528
10422
|
Use kuma_memory({ action: 'decision', title: '...', context: '...', rationale: '...', outcome: '...' }) to record.` : "\u2705 No decision needed at this time.";
|
|
10529
10423
|
}
|
|
10530
10424
|
case "record":
|
|
10531
|
-
if (!params.title
|
|
10532
|
-
|
|
10425
|
+
if (!params.title) return `\u274C **decision format error:** title is required.
|
|
10426
|
+
|
|
10427
|
+
\u2705 Correct: kuma_memory({ action: "decision", decisionAction: "record", title: "Use bcrypt vs argon2", rationale: "bcrypt is battle-tested, argon2 is newer but less adoption", context: "Login system security", outcome: "Chose bcrypt" })`;
|
|
10428
|
+
if (!params.rationale) return `\u274C **decision format error:** rationale is required.
|
|
10429
|
+
|
|
10430
|
+
\u2705 Correct: kuma_memory({ action: "decision", decisionAction: "record", title: "${params.title}", rationale: "WHY you chose this option", context: "optional context", outcome: "optional outcome" })`;
|
|
10431
|
+
const decisionResult = await recordDecision({
|
|
10533
10432
|
title: params.title,
|
|
10534
10433
|
context: params.context || "",
|
|
10535
10434
|
options: [],
|
|
@@ -10537,6 +10436,8 @@ Use kuma_memory({ action: 'decision', title: '...', context: '...', rationale: '
|
|
|
10537
10436
|
outcome: params.outcome || "implemented",
|
|
10538
10437
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10539
10438
|
});
|
|
10439
|
+
sessionMemory.recordMemoryAction("decision");
|
|
10440
|
+
return decisionResult;
|
|
10540
10441
|
default:
|
|
10541
10442
|
return formatDecisionTemplate();
|
|
10542
10443
|
}
|
|
@@ -10562,11 +10463,12 @@ async function handleResearchSave(params) {
|
|
|
10562
10463
|
} catch {
|
|
10563
10464
|
}
|
|
10564
10465
|
try {
|
|
10565
|
-
const researchDir =
|
|
10566
|
-
if (!
|
|
10567
|
-
|
|
10466
|
+
const researchDir = path14.join(getProjectRoot(), ".kuma", "research");
|
|
10467
|
+
if (!fs14.existsSync(researchDir)) fs14.mkdirSync(researchDir, { recursive: true });
|
|
10468
|
+
fs14.writeFileSync(path14.join(researchDir, `${scope}.json`), JSON.stringify(JSON.parse(record), null, 2), "utf-8");
|
|
10568
10469
|
} catch {
|
|
10569
10470
|
}
|
|
10471
|
+
sessionMemory.recordMemoryAction("research_save");
|
|
10570
10472
|
return `\u2705 Research "${scope}" saved.`;
|
|
10571
10473
|
}
|
|
10572
10474
|
async function handleSession(params) {
|
|
@@ -10575,13 +10477,23 @@ async function handleSession(params) {
|
|
|
10575
10477
|
if (topic) return typeof summary.content === "string" ? summary.content : JSON.stringify(summary, null, 2);
|
|
10576
10478
|
const modifiedFiles = summary.modifiedFiles || [];
|
|
10577
10479
|
const failures = summary.unresolvedFailures || [];
|
|
10480
|
+
const recordingSummary = sessionMemory.getRecordingSummary();
|
|
10481
|
+
const metricsSummary = sessionMemory.getMetricsSummary();
|
|
10578
10482
|
const lines = [
|
|
10579
10483
|
"\u{1F4CB} **Session Summary**\n\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\n",
|
|
10580
10484
|
`\u{1F3AF} Goal: ${summary.currentGoal || "not set"}`,
|
|
10581
10485
|
`\u{1F550} Duration: ${summary.sessionDuration}`,
|
|
10582
10486
|
`\u{1F6E0}\uFE0F Tool calls: ${summary.toolCallCount}
|
|
10583
|
-
|
|
10487
|
+
`,
|
|
10488
|
+
`\u{1F9E0} **Recordings:** ${recordingSummary.total} total \u2014 ${recordingSummary.archFlows} arch_flow, ${recordingSummary.gotchas} gotcha, ${recordingSummary.decisions} decision, ${recordingSummary.researchSaves} research_save`,
|
|
10489
|
+
`\u{1F4CA} **Metrics:** ${metricsSummary.filesRead} files read, ${metricsSummary.filesEdited} files edited`,
|
|
10490
|
+
`\u23F1\uFE0F **Time saved:** ~${metricsSummary.researchTimeSavedFormatted} (from research cache)`
|
|
10584
10491
|
];
|
|
10492
|
+
if (recordingSummary.missingRecordings.length > 0 && recordingSummary.total === 0) {
|
|
10493
|
+
lines.push(`
|
|
10494
|
+
\u26A0\uFE0F **No recordings yet!** Missing: ${recordingSummary.missingRecordings.join(", ")}`);
|
|
10495
|
+
lines.push(`\u{1F4A1} Tip: Record findings after reading files. arch_flow + gotcha are exponential value.`);
|
|
10496
|
+
}
|
|
10585
10497
|
if (modifiedFiles.length > 0) {
|
|
10586
10498
|
lines.push(`**Modified Files** (${modifiedFiles.length}):`);
|
|
10587
10499
|
for (const f of modifiedFiles.slice(0, 10)) {
|
|
@@ -10613,6 +10525,33 @@ async function handleSearch(params) {
|
|
|
10613
10525
|
const memResults = sessionMemory.searchMemory(query, limit);
|
|
10614
10526
|
const { searchGraph: searchGraph2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
|
|
10615
10527
|
const graphResults = await searchGraph2(query, Math.min(limit, 10));
|
|
10528
|
+
let taskContext = "";
|
|
10529
|
+
try {
|
|
10530
|
+
const { retrieveForTask: retrieveForTask2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
|
|
10531
|
+
taskContext = await retrieveForTask2(query, 10);
|
|
10532
|
+
} catch {
|
|
10533
|
+
}
|
|
10534
|
+
let impactAnalysis = "";
|
|
10535
|
+
try {
|
|
10536
|
+
const { propagateImpact: propagateImpact2, searchGraph: sg } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
|
|
10537
|
+
const searchRes = await sg(query, 1);
|
|
10538
|
+
const nodeIdMatch = searchRes.match(/\*\*([^*]+)\*\* \(([^)]+)\)/);
|
|
10539
|
+
if (nodeIdMatch) {
|
|
10540
|
+
const nodeType = nodeIdMatch[2];
|
|
10541
|
+
const nodeName = nodeIdMatch[1];
|
|
10542
|
+
const nid = `${nodeType}::${nodeName}`;
|
|
10543
|
+
const impacts = await propagateImpact2(nid, 3, 0.2);
|
|
10544
|
+
if (impacts.length > 0) {
|
|
10545
|
+
const impactLines = impacts.slice(0, 5).map(
|
|
10546
|
+
(i) => ` \u2022 ${i.name} (${i.type}) \u2014 depth ${i.depth}, weight ${i.weight}`
|
|
10547
|
+
);
|
|
10548
|
+
impactAnalysis = `
|
|
10549
|
+
\u{1F4A5} **Impact Analysis** \u2014 ${impacts.length} node(s) affected by "${nodeName}":
|
|
10550
|
+
${impactLines.join("\n")}`;
|
|
10551
|
+
}
|
|
10552
|
+
}
|
|
10553
|
+
} catch {
|
|
10554
|
+
}
|
|
10616
10555
|
let hybridResults = "";
|
|
10617
10556
|
try {
|
|
10618
10557
|
const { hybridSearch: hybridSearch2, formatHybridResults: formatHybridResults2 } = await Promise.resolve().then(() => (init_kumaSearch(), kumaSearch_exports));
|
|
@@ -10630,7 +10569,14 @@ async function handleSearch(params) {
|
|
|
10630
10569
|
for (const r of memResults.slice(0, 5)) lines.push(` \u2022 ${r.content.substring(0, 120)}`);
|
|
10631
10570
|
lines.push("");
|
|
10632
10571
|
}
|
|
10572
|
+
if (taskContext) {
|
|
10573
|
+
lines.push(taskContext);
|
|
10574
|
+
lines.push("");
|
|
10575
|
+
}
|
|
10633
10576
|
lines.push("**Knowledge Graph:\n" + graphResults);
|
|
10577
|
+
if (impactAnalysis) {
|
|
10578
|
+
lines.push(impactAnalysis);
|
|
10579
|
+
}
|
|
10634
10580
|
if (hybridResults) {
|
|
10635
10581
|
lines.push("");
|
|
10636
10582
|
lines.push(hybridResults);
|
|
@@ -10722,12 +10668,24 @@ async function handleLayerAction(layer, params) {
|
|
|
10722
10668
|
const gotchasStr = gotchasMatch ? gotchasMatch[1].trim() : "";
|
|
10723
10669
|
const decisionsStr = decisionsMatch ? decisionsMatch[1].trim() : "";
|
|
10724
10670
|
const filesStr = filesMatch ? filesMatch[1].trim() : "";
|
|
10671
|
+
if (hopsStr && !content.includes("\u2192")) {
|
|
10672
|
+
return `\u274C **arch_flow format error:** hops must use \u2192 separator.
|
|
10673
|
+
|
|
10674
|
+
\u274C Wrong: hops: file1.js, file2.js
|
|
10675
|
+
\u274C Wrong: hops: file1.js\\nfile2.js
|
|
10676
|
+
\u2705 Correct: domain: <name> | hops: file1.js \u2192 file2.js \u2192 file3.js`;
|
|
10677
|
+
}
|
|
10725
10678
|
const hops = hopsStr ? hopsStr.split("\u2192").map((h) => h.trim()).filter(Boolean).map((h, i, arr) => ({
|
|
10726
10679
|
from: i === 0 ? domain : arr[i - 1],
|
|
10727
10680
|
to: h,
|
|
10728
10681
|
relation: "flows",
|
|
10729
10682
|
description: h
|
|
10730
10683
|
})) : [];
|
|
10684
|
+
if (hops.length === 0) {
|
|
10685
|
+
return `\u274C **arch_flow format error:** no hops found after \u2192 separator.
|
|
10686
|
+
|
|
10687
|
+
\u2705 Correct: domain: <name> | hops: file1.js \u2192 file2.js`;
|
|
10688
|
+
}
|
|
10731
10689
|
const gotchas = gotchasStr ? gotchasStr.split(",").map((g) => g.trim()).filter(Boolean) : [];
|
|
10732
10690
|
const decisions = decisionsStr ? decisionsStr.split(",").map((d) => d.trim()).filter(Boolean) : [];
|
|
10733
10691
|
const filePaths = filesStr ? filesStr.split(",").map((f) => f.trim()).filter(Boolean) : [];
|
|
@@ -10735,6 +10693,7 @@ async function handleLayerAction(layer, params) {
|
|
|
10735
10693
|
const { recordDomainFlow: recordDomainFlow2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
|
|
10736
10694
|
const flow = await recordDomainFlow2({ domain, hops, gotchas, decisions, filePaths });
|
|
10737
10695
|
await writeLayer2("arch_flow", content);
|
|
10696
|
+
sessionMemory.recordMemoryAction("arch_flow");
|
|
10738
10697
|
return `\u2705 Domain flow "${domain}" recorded \u2014 ${flow.nodeCount} nodes, ${flow.edgeCount} edges created.
|
|
10739
10698
|
\u{1F3DB}\uFE0F **Domain Anchor:** ${domain}
|
|
10740
10699
|
\u{1F504} **Hops:** ${hops.length}
|
|
@@ -10745,7 +10704,15 @@ async function handleLayerAction(layer, params) {
|
|
|
10745
10704
|
return `\u2705 Architecture flow saved (text only). Graph recording failed: ${err}`;
|
|
10746
10705
|
}
|
|
10747
10706
|
}
|
|
10748
|
-
return
|
|
10707
|
+
return `\u274C **arch_flow format not recognized.** Use this format:
|
|
10708
|
+
|
|
10709
|
+
domain: <DomainName> | hops: <file1.tsx> \u2192 <file2.js> \u2192 <file3.ts>
|
|
10710
|
+
|
|
10711
|
+
Fields:
|
|
10712
|
+
- domain: short name (e.g. InviteUserFlow)
|
|
10713
|
+
- hops: file names separated by \u2192 (NOT commas, NOT newlines)
|
|
10714
|
+
- gotchas: optional, comma-separated
|
|
10715
|
+
- decisions: optional, comma-separated`;
|
|
10749
10716
|
}
|
|
10750
10717
|
if (params.content) {
|
|
10751
10718
|
return writeLayer2(layer, params.content);
|
|
@@ -10754,13 +10721,25 @@ async function handleLayerAction(layer, params) {
|
|
|
10754
10721
|
}
|
|
10755
10722
|
async function handleGotchaAction(params) {
|
|
10756
10723
|
if (params.content && params.scope) {
|
|
10724
|
+
if (!params.scope.trim()) {
|
|
10725
|
+
return `\u274C **gotcha format error:** scope (file path) is required.
|
|
10726
|
+
|
|
10727
|
+
\u2705 Correct: kuma_memory({ action: "gotcha", scope: "path/to/file.ts", content: "bug description", status: "high" })`;
|
|
10728
|
+
}
|
|
10729
|
+
if (!params.content.trim()) {
|
|
10730
|
+
return `\u274C **gotcha format error:** content (description) is required.
|
|
10731
|
+
|
|
10732
|
+
\u2705 Correct: kuma_memory({ action: "gotcha", scope: "path/to/file.ts", content: "bug description", status: "high" })`;
|
|
10733
|
+
}
|
|
10757
10734
|
const { addGotcha: addGotcha2 } = await Promise.resolve().then(() => (init_kumaGotchas(), kumaGotchas_exports));
|
|
10758
|
-
|
|
10735
|
+
const result = await addGotcha2({
|
|
10759
10736
|
filePath: params.scope,
|
|
10760
10737
|
description: params.content,
|
|
10761
10738
|
severity: params.status || "medium",
|
|
10762
10739
|
workaround: params.description
|
|
10763
10740
|
});
|
|
10741
|
+
sessionMemory.recordMemoryAction("gotcha");
|
|
10742
|
+
return result;
|
|
10764
10743
|
}
|
|
10765
10744
|
const { listGotchas: listGotchas2, syncGotchasToDb: syncGotchasToDb2 } = await Promise.resolve().then(() => (init_kumaGotchas(), kumaGotchas_exports));
|
|
10766
10745
|
await syncGotchasToDb2();
|
|
@@ -10796,19 +10775,13 @@ async function handleDeleteNode(params) {
|
|
|
10796
10775
|
saveDb2(db);
|
|
10797
10776
|
return `\u{1F5D1}\uFE0F **Decision #${id} deleted.**`;
|
|
10798
10777
|
}
|
|
10799
|
-
case "trajectory":
|
|
10800
|
-
case "trajectories": {
|
|
10801
|
-
db.run("DELETE FROM trajectories WHERE id = ?", [id]);
|
|
10802
|
-
saveDb2(db);
|
|
10803
|
-
return `\u{1F5D1}\uFE0F **Trajectory #${id} deleted.**`;
|
|
10804
|
-
}
|
|
10805
10778
|
case "checkpoint": {
|
|
10806
10779
|
db.run("DELETE FROM health_snapshots WHERE id = ?", [id]);
|
|
10807
10780
|
saveDb2(db);
|
|
10808
10781
|
return `\u{1F5D1}\uFE0F **Checkpoint #${id} deleted.**`;
|
|
10809
10782
|
}
|
|
10810
10783
|
default:
|
|
10811
|
-
return `\u26A0\uFE0F Unknown scope "${params.scope}". Supported: gotcha, todo, decision,
|
|
10784
|
+
return `\u26A0\uFE0F Unknown scope "${params.scope}". Supported: gotcha, todo, decision, checkpoint`;
|
|
10812
10785
|
}
|
|
10813
10786
|
}
|
|
10814
10787
|
if (params.target) {
|
|
@@ -10841,38 +10814,7 @@ async function handleDeleteNode(params) {
|
|
|
10841
10814
|
return `\u{1F5D1}\uFE0F **Node #${id} deleted.**`;
|
|
10842
10815
|
}
|
|
10843
10816
|
}
|
|
10844
|
-
return "\u26A0\uFE0F Provide `target` (node ID) to delete, or `scope` + `target` (numeric ID) for gotcha/todo/decision/
|
|
10845
|
-
}
|
|
10846
|
-
async function handleFederated(params) {
|
|
10847
|
-
sessionMemory.recordToolCall("kuma_memory_federated", { scope: params.scope, uri: params.uri });
|
|
10848
|
-
if (params.uri) {
|
|
10849
|
-
const { resolveFederatedNode: resolveFederatedNode2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
|
|
10850
|
-
return await resolveFederatedNode2(params.uri);
|
|
10851
|
-
}
|
|
10852
|
-
const { listFederatedReferences: listFederatedReferences2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
|
|
10853
|
-
return await listFederatedReferences2();
|
|
10854
|
-
}
|
|
10855
|
-
async function handleGenTest(params) {
|
|
10856
|
-
sessionMemory.recordToolCall("kuma_memory_gen_test", { target: params.target });
|
|
10857
|
-
if (params.target) {
|
|
10858
|
-
const id = parseInt(params.target, 10);
|
|
10859
|
-
if (!isNaN(id)) {
|
|
10860
|
-
const { generateTestFromTrajectoryId: generateTestFromTrajectoryId2 } = await Promise.resolve().then(() => (init_kumaTrajectory(), kumaTrajectory_exports));
|
|
10861
|
-
return await generateTestFromTrajectoryId2(id);
|
|
10862
|
-
}
|
|
10863
|
-
}
|
|
10864
|
-
const { listGeneratedTests: listGeneratedTests2 } = await Promise.resolve().then(() => (init_kumaTrajectory(), kumaTrajectory_exports));
|
|
10865
|
-
return await listGeneratedTests2();
|
|
10866
|
-
}
|
|
10867
|
-
async function handleTrajectoryList(params) {
|
|
10868
|
-
sessionMemory.recordToolCall("kuma_memory_trajectory", {});
|
|
10869
|
-
const { listTrajectories: listTrajectories2 } = await Promise.resolve().then(() => (init_kumaTrajectory(), kumaTrajectory_exports));
|
|
10870
|
-
return await listTrajectories2(params.limit || 10);
|
|
10871
|
-
}
|
|
10872
|
-
async function handleSkillsList(_params) {
|
|
10873
|
-
sessionMemory.recordToolCall("kuma_memory_skills", {});
|
|
10874
|
-
const { listDistilledSkills: listDistilledSkills2 } = await Promise.resolve().then(() => (init_kumaTrajectory(), kumaTrajectory_exports));
|
|
10875
|
-
return await listDistilledSkills2();
|
|
10817
|
+
return "\u26A0\uFE0F Provide `target` (node ID) to delete, or `scope` + `target` (numeric ID) for gotcha/todo/decision/checkpoint.\n\nExamples:\n- `delete_node`, target: 'function::sendMessage'\n- `delete_node`, scope: 'gotcha', target: '42'\n- `delete_node`, scope: 'todo', target: '7'\n- `delete_node`, scope: 'decision', target: '3'\n- `delete_node`, scope: 'checkpoint', target: '1'";
|
|
10876
10818
|
}
|
|
10877
10819
|
async function handleAddNode(params) {
|
|
10878
10820
|
const type = params.title;
|
|
@@ -10907,6 +10849,29 @@ async function handleAddNode(params) {
|
|
|
10907
10849
|
return `\u274C Failed to create node: ${err}`;
|
|
10908
10850
|
}
|
|
10909
10851
|
}
|
|
10852
|
+
async function handleFeature(params) {
|
|
10853
|
+
sessionMemory.recordToolCall("kuma_memory_feature", { title: params.title, scope: params.scope });
|
|
10854
|
+
if (!params.title) {
|
|
10855
|
+
return "\u26A0\uFE0F `title` (feature name) required.\n\n\u2705 Correct: kuma_memory({ action: 'feature', title: 'Authentication', content: 'description', scope: 'src/auth/login.ts,src/auth/session.ts' })\n\n\u2022 title = feature name (e.g. 'Authentication', 'Billing')\n\u2022 content = description (optional)\n\u2022 scope = comma-separated file paths (optional)\n\u2022 tags = comma-separated tags (optional)\n\u2022 status = risk level: low/medium/high/critical (optional)";
|
|
10856
|
+
}
|
|
10857
|
+
const { recordFeature: recordFeature2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
|
|
10858
|
+
const files = params.scope ? params.scope.split(",").map((f) => f.trim()).filter(Boolean) : [];
|
|
10859
|
+
const tags = params.tags ? params.tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
|
|
10860
|
+
const risk = params.status || "medium";
|
|
10861
|
+
const result = await recordFeature2({
|
|
10862
|
+
name: params.title,
|
|
10863
|
+
description: params.content,
|
|
10864
|
+
files,
|
|
10865
|
+
tags,
|
|
10866
|
+
risk
|
|
10867
|
+
});
|
|
10868
|
+
sessionMemory.recordMemoryAction("research_save");
|
|
10869
|
+
return `\u2705 **Feature "${params.title}" recorded** \u2014 ${result.nodeCount} node(s), ${result.edgeCount} edge(s)
|
|
10870
|
+
\u2B50 Level: feature
|
|
10871
|
+
\u{1F4C1} Files: ${files.length}
|
|
10872
|
+
\u{1F3F7}\uFE0F Tags: ${tags.length}
|
|
10873
|
+
\u26A0\uFE0F Risk: ${risk}`;
|
|
10874
|
+
}
|
|
10910
10875
|
async function handleLayersSummary(_params) {
|
|
10911
10876
|
const { getLayersSummary: getLayersSummary2 } = await Promise.resolve().then(() => (init_domainRules(), domainRules_exports));
|
|
10912
10877
|
return getLayersSummary2();
|
|
@@ -10917,72 +10882,72 @@ init_sessionMemory();
|
|
|
10917
10882
|
|
|
10918
10883
|
// src/engine/kumaLock.ts
|
|
10919
10884
|
init_pathValidator();
|
|
10920
|
-
import
|
|
10921
|
-
import
|
|
10885
|
+
import fs15 from "fs";
|
|
10886
|
+
import path15 from "path";
|
|
10922
10887
|
var LOCKS_DIR = ".kuma/locks";
|
|
10923
10888
|
function locksDir() {
|
|
10924
|
-
return
|
|
10889
|
+
return path15.join(getProjectRoot(), LOCKS_DIR);
|
|
10925
10890
|
}
|
|
10926
10891
|
function ensureLocksDir() {
|
|
10927
10892
|
const dir = locksDir();
|
|
10928
|
-
if (!
|
|
10893
|
+
if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
|
|
10929
10894
|
}
|
|
10930
10895
|
function lockPath(filePath) {
|
|
10931
10896
|
const safeName = filePath.replace(/[^a-zA-Z0-9_./-]/g, "_");
|
|
10932
|
-
return
|
|
10897
|
+
return path15.join(locksDir(), `${safeName}.lock.json`);
|
|
10933
10898
|
}
|
|
10934
10899
|
function acquireLock(filePath, agentId) {
|
|
10935
10900
|
ensureLocksDir();
|
|
10936
10901
|
const lp = lockPath(filePath);
|
|
10937
10902
|
const id = agentId || `agent-${process.pid}`;
|
|
10938
|
-
if (
|
|
10903
|
+
if (fs15.existsSync(lp)) {
|
|
10939
10904
|
try {
|
|
10940
|
-
const existing = JSON.parse(
|
|
10905
|
+
const existing = JSON.parse(fs15.readFileSync(lp, "utf-8"));
|
|
10941
10906
|
if (existing.agentId === id) {
|
|
10942
10907
|
return `\u{1F512} **Already locked** by you (${id}) on ${new Date(existing.acquiredAt).toISOString()}`;
|
|
10943
10908
|
}
|
|
10944
10909
|
const elapsed = Math.floor((Date.now() - existing.acquiredAt) / 1e3);
|
|
10945
10910
|
if (elapsed > 300) {
|
|
10946
|
-
|
|
10911
|
+
fs15.unlinkSync(lp);
|
|
10947
10912
|
} else {
|
|
10948
10913
|
return `\u{1F512} **Locked** by ${existing.agentId} since ${new Date(existing.acquiredAt).toISOString()} (${elapsed}s ago)`;
|
|
10949
10914
|
}
|
|
10950
10915
|
} catch {
|
|
10951
|
-
|
|
10916
|
+
fs15.unlinkSync(lp);
|
|
10952
10917
|
}
|
|
10953
10918
|
}
|
|
10954
10919
|
const entry = { filePath, agentId: id, acquiredAt: Date.now(), status: "locked" };
|
|
10955
|
-
|
|
10920
|
+
fs15.writeFileSync(lp, JSON.stringify(entry, null, 2), "utf-8");
|
|
10956
10921
|
return `\u{1F513} **Lock acquired** on \`${filePath}\` by ${id}`;
|
|
10957
10922
|
}
|
|
10958
10923
|
function releaseLock(filePath, agentId) {
|
|
10959
10924
|
ensureLocksDir();
|
|
10960
10925
|
const lp = lockPath(filePath);
|
|
10961
10926
|
const id = agentId || `agent-${process.pid}`;
|
|
10962
|
-
if (!
|
|
10927
|
+
if (!fs15.existsSync(lp)) {
|
|
10963
10928
|
return `\u26A0\uFE0F No lock found for \`${filePath}\``;
|
|
10964
10929
|
}
|
|
10965
10930
|
try {
|
|
10966
|
-
const existing = JSON.parse(
|
|
10931
|
+
const existing = JSON.parse(fs15.readFileSync(lp, "utf-8"));
|
|
10967
10932
|
if (existing.agentId !== id) {
|
|
10968
10933
|
return `\u26A0\uFE0F Cannot release lock held by ${existing.agentId}. Use force:true to override.`;
|
|
10969
10934
|
}
|
|
10970
|
-
|
|
10935
|
+
fs15.unlinkSync(lp);
|
|
10971
10936
|
return `\u{1F513} **Lock released** on \`${filePath}\``;
|
|
10972
10937
|
} catch {
|
|
10973
|
-
|
|
10938
|
+
fs15.unlinkSync(lp);
|
|
10974
10939
|
return `\u{1F513} **Lock released** (force cleanup) on \`${filePath}\``;
|
|
10975
10940
|
}
|
|
10976
10941
|
}
|
|
10977
10942
|
function listLocks() {
|
|
10978
10943
|
ensureLocksDir();
|
|
10979
10944
|
const dir = locksDir();
|
|
10980
|
-
const files =
|
|
10945
|
+
const files = fs15.readdirSync(dir).filter((f) => f.endsWith(".lock.json"));
|
|
10981
10946
|
if (files.length === 0) return "\u{1F513} No active locks.";
|
|
10982
10947
|
const lines = ["\u{1F512} **Active Locks:**", ""];
|
|
10983
10948
|
for (const f of files) {
|
|
10984
10949
|
try {
|
|
10985
|
-
const entry = JSON.parse(
|
|
10950
|
+
const entry = JSON.parse(fs15.readFileSync(path15.join(dir, f), "utf-8"));
|
|
10986
10951
|
const elapsed = Math.floor((Date.now() - entry.acquiredAt) / 1e3);
|
|
10987
10952
|
lines.push(` \u2022 \`${entry.filePath}\` \u2014 locked by ${entry.agentId} (${elapsed}s ago)`);
|
|
10988
10953
|
} catch {
|
|
@@ -10993,9 +10958,9 @@ function listLocks() {
|
|
|
10993
10958
|
}
|
|
10994
10959
|
function isLocked(filePath) {
|
|
10995
10960
|
const lp = lockPath(filePath);
|
|
10996
|
-
if (!
|
|
10961
|
+
if (!fs15.existsSync(lp)) return { locked: false };
|
|
10997
10962
|
try {
|
|
10998
|
-
const entry = JSON.parse(
|
|
10963
|
+
const entry = JSON.parse(fs15.readFileSync(lp, "utf-8"));
|
|
10999
10964
|
return { locked: true, by: entry.agentId, since: entry.acquiredAt };
|
|
11000
10965
|
} catch {
|
|
11001
10966
|
return { locked: false };
|
|
@@ -11005,16 +10970,16 @@ function cleanStaleLocks() {
|
|
|
11005
10970
|
ensureLocksDir();
|
|
11006
10971
|
const dir = locksDir();
|
|
11007
10972
|
let count = 0;
|
|
11008
|
-
for (const f of
|
|
10973
|
+
for (const f of fs15.readdirSync(dir).filter((f2) => f2.endsWith(".lock.json"))) {
|
|
11009
10974
|
try {
|
|
11010
|
-
const entry = JSON.parse(
|
|
10975
|
+
const entry = JSON.parse(fs15.readFileSync(path15.join(dir, f), "utf-8"));
|
|
11011
10976
|
if (Date.now() - entry.acquiredAt > 3e5) {
|
|
11012
|
-
|
|
10977
|
+
fs15.unlinkSync(path15.join(dir, f));
|
|
11013
10978
|
count++;
|
|
11014
10979
|
}
|
|
11015
10980
|
} catch {
|
|
11016
10981
|
try {
|
|
11017
|
-
|
|
10982
|
+
fs15.unlinkSync(path15.join(dir, f));
|
|
11018
10983
|
count++;
|
|
11019
10984
|
} catch {
|
|
11020
10985
|
}
|
|
@@ -11030,8 +10995,8 @@ init_safetyAudit();
|
|
|
11030
10995
|
|
|
11031
10996
|
// src/tools/kumaPolicy.ts
|
|
11032
10997
|
init_pathValidator();
|
|
11033
|
-
import
|
|
11034
|
-
import
|
|
10998
|
+
import fs16 from "fs";
|
|
10999
|
+
import path16 from "path";
|
|
11035
11000
|
var DEFAULT_POLICY = {
|
|
11036
11001
|
never_touch: [
|
|
11037
11002
|
".env",
|
|
@@ -11057,15 +11022,15 @@ var DEFAULT_POLICY = {
|
|
|
11057
11022
|
};
|
|
11058
11023
|
function loadPolicy() {
|
|
11059
11024
|
const root = getProjectRoot();
|
|
11060
|
-
const ymlPath =
|
|
11061
|
-
const yamlPath =
|
|
11025
|
+
const ymlPath = path16.join(root, ".kuma", "policy.yml");
|
|
11026
|
+
const yamlPath = path16.join(root, ".kuma", "policy.yaml");
|
|
11062
11027
|
let policyContent = null;
|
|
11063
11028
|
let policyPath = null;
|
|
11064
|
-
if (
|
|
11065
|
-
policyContent =
|
|
11029
|
+
if (fs16.existsSync(ymlPath)) {
|
|
11030
|
+
policyContent = fs16.readFileSync(ymlPath, "utf-8");
|
|
11066
11031
|
policyPath = ymlPath;
|
|
11067
|
-
} else if (
|
|
11068
|
-
policyContent =
|
|
11032
|
+
} else if (fs16.existsSync(yamlPath)) {
|
|
11033
|
+
policyContent = fs16.readFileSync(yamlPath, "utf-8");
|
|
11069
11034
|
policyPath = yamlPath;
|
|
11070
11035
|
}
|
|
11071
11036
|
if (!policyContent) {
|
|
@@ -11175,9 +11140,9 @@ function checkFilePathPolicy(filePath, policy) {
|
|
|
11175
11140
|
if (policy.max_file_size && policy.max_file_size > 0) {
|
|
11176
11141
|
try {
|
|
11177
11142
|
const root = getProjectRoot();
|
|
11178
|
-
const fullPath =
|
|
11179
|
-
if (
|
|
11180
|
-
const sizeKB =
|
|
11143
|
+
const fullPath = path16.join(root, filePath);
|
|
11144
|
+
if (fs16.existsSync(fullPath)) {
|
|
11145
|
+
const sizeKB = fs16.statSync(fullPath).size / 1024;
|
|
11181
11146
|
if (sizeKB > policy.max_file_size) {
|
|
11182
11147
|
violations.push({
|
|
11183
11148
|
rule: "max_file_size",
|
|
@@ -11344,12 +11309,12 @@ async function safetyCheck(action, filePath, command) {
|
|
|
11344
11309
|
}
|
|
11345
11310
|
if (filePath) {
|
|
11346
11311
|
try {
|
|
11347
|
-
const
|
|
11348
|
-
const
|
|
11312
|
+
const fs26 = await import("fs");
|
|
11313
|
+
const path26 = await import("path");
|
|
11349
11314
|
const root = process.cwd();
|
|
11350
|
-
const fullPath =
|
|
11351
|
-
if (
|
|
11352
|
-
const stat =
|
|
11315
|
+
const fullPath = path26.resolve(root, filePath);
|
|
11316
|
+
if (fs26.existsSync(fullPath)) {
|
|
11317
|
+
const stat = fs26.statSync(fullPath);
|
|
11353
11318
|
const sizeKB = Math.round(stat.size / 1024);
|
|
11354
11319
|
if (sizeKB > 500) {
|
|
11355
11320
|
checks.push({
|
|
@@ -11359,12 +11324,12 @@ async function safetyCheck(action, filePath, command) {
|
|
|
11359
11324
|
});
|
|
11360
11325
|
}
|
|
11361
11326
|
if (filePath.endsWith(".ts") || filePath.endsWith(".js")) {
|
|
11362
|
-
const baseName =
|
|
11327
|
+
const baseName = path26.basename(filePath, path26.extname(filePath));
|
|
11363
11328
|
const testPatterns = [
|
|
11364
11329
|
filePath.replace(/\.(ts|js)$/, ".test.$1"),
|
|
11365
11330
|
filePath.replace(/\.(ts|js)$/, ".spec.$1"),
|
|
11366
11331
|
filePath.replace(/^src\//, "tests/").replace(/\.(ts|js)$/, ".test.$1"),
|
|
11367
|
-
`**/__tests__/**/${baseName}.test.${
|
|
11332
|
+
`**/__tests__/**/${baseName}.test.${path26.extname(filePath).slice(1)}`
|
|
11368
11333
|
];
|
|
11369
11334
|
let hasTests = false;
|
|
11370
11335
|
const fg = await import("fast-glob");
|
|
@@ -11380,7 +11345,7 @@ async function safetyCheck(action, filePath, command) {
|
|
|
11380
11345
|
name: "Tests Check",
|
|
11381
11346
|
passed: true,
|
|
11382
11347
|
// Not a blocker, just informative
|
|
11383
|
-
detail: `\u{1F9EA} No test file found for "${
|
|
11348
|
+
detail: `\u{1F9EA} No test file found for "${path26.basename(filePath)}" \u2014 consider adding tests`
|
|
11384
11349
|
});
|
|
11385
11350
|
} else {
|
|
11386
11351
|
checks.push({
|
|
@@ -11469,8 +11434,8 @@ init_sessionMemory();
|
|
|
11469
11434
|
// src/guards/antiPatternDetector.ts
|
|
11470
11435
|
init_pathValidator();
|
|
11471
11436
|
init_sessionMemory();
|
|
11472
|
-
import
|
|
11473
|
-
import
|
|
11437
|
+
import fs17 from "fs";
|
|
11438
|
+
import path17 from "path";
|
|
11474
11439
|
import { execSync as execSync5 } from "child_process";
|
|
11475
11440
|
var SCRIPT_PATCH_PATTERNS = [
|
|
11476
11441
|
"writeFileSync",
|
|
@@ -11497,20 +11462,20 @@ function findPatchScripts(projectRoot) {
|
|
|
11497
11462
|
const warnings = [];
|
|
11498
11463
|
const recentFiles = scanRecentFiles(projectRoot);
|
|
11499
11464
|
for (const file of recentFiles) {
|
|
11500
|
-
const ext =
|
|
11465
|
+
const ext = path17.extname(file).toLowerCase();
|
|
11501
11466
|
if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
|
|
11502
|
-
const relativePath =
|
|
11467
|
+
const relativePath = path17.relative(projectRoot, file);
|
|
11503
11468
|
if (relativePath.startsWith("src") || relativePath.startsWith("test") || relativePath.startsWith("node_modules") || relativePath.startsWith(".") || relativePath.startsWith(".kuma/scratch")) {
|
|
11504
11469
|
continue;
|
|
11505
11470
|
}
|
|
11506
11471
|
try {
|
|
11507
|
-
const content =
|
|
11472
|
+
const content = fs17.readFileSync(file, "utf-8").toLowerCase();
|
|
11508
11473
|
const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
|
|
11509
11474
|
if (matchedPattern) {
|
|
11510
11475
|
warnings.push({
|
|
11511
11476
|
severity: "high",
|
|
11512
11477
|
pattern: "script-patching",
|
|
11513
|
-
message: `Created script file that modifies other files: ${
|
|
11478
|
+
message: `Created script file that modifies other files: ${path17.basename(file)}`,
|
|
11514
11479
|
suggestion: "Use **precise_diff_editor** instead \u2014 it has fuzzy matching, auto-backup, and rollback support",
|
|
11515
11480
|
evidence: `File: ${relativePath} contains '${matchedPattern}'`,
|
|
11516
11481
|
filePath: relativePath
|
|
@@ -11544,15 +11509,15 @@ function detectBashGrepUsage() {
|
|
|
11544
11509
|
function scanRecentFiles(projectRoot) {
|
|
11545
11510
|
const recent = [];
|
|
11546
11511
|
try {
|
|
11547
|
-
const entries =
|
|
11512
|
+
const entries = fs17.readdirSync(projectRoot, { withFileTypes: true });
|
|
11548
11513
|
const now = Date.now();
|
|
11549
11514
|
const recentThreshold = 30 * 60 * 1e3;
|
|
11550
11515
|
for (const entry of entries) {
|
|
11551
11516
|
if (!entry.isFile()) continue;
|
|
11552
11517
|
try {
|
|
11553
|
-
const stat =
|
|
11518
|
+
const stat = fs17.statSync(path17.join(projectRoot, entry.name));
|
|
11554
11519
|
if (now - stat.mtimeMs < recentThreshold || now - stat.ctimeMs < recentThreshold) {
|
|
11555
|
-
recent.push(
|
|
11520
|
+
recent.push(path17.join(projectRoot, entry.name));
|
|
11556
11521
|
}
|
|
11557
11522
|
} catch {
|
|
11558
11523
|
continue;
|
|
@@ -11576,14 +11541,14 @@ function detectGitPatchScripts(projectRoot) {
|
|
|
11576
11541
|
const prefix = line.substring(0, 2);
|
|
11577
11542
|
if (prefix !== "??" && prefix !== "A ") continue;
|
|
11578
11543
|
const file = line.substring(3).trim();
|
|
11579
|
-
const ext =
|
|
11544
|
+
const ext = path17.extname(file).toLowerCase();
|
|
11580
11545
|
if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
|
|
11581
11546
|
const isRootLevel = !file.includes("/");
|
|
11582
11547
|
const isScriptsDir = file.startsWith("scripts/") || file.startsWith("patches/");
|
|
11583
11548
|
if (!isRootLevel && !isScriptsDir) continue;
|
|
11584
|
-
const fullPath =
|
|
11585
|
-
if (
|
|
11586
|
-
const content =
|
|
11549
|
+
const fullPath = path17.join(projectRoot, file);
|
|
11550
|
+
if (fs17.existsSync(fullPath)) {
|
|
11551
|
+
const content = fs17.readFileSync(fullPath, "utf-8").toLowerCase();
|
|
11587
11552
|
const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
|
|
11588
11553
|
if (matchedPattern) {
|
|
11589
11554
|
warnings.push({
|
|
@@ -11606,7 +11571,7 @@ function detectGitPatchScripts(projectRoot) {
|
|
|
11606
11571
|
if (deletedStdout) {
|
|
11607
11572
|
const deletedFiles = deletedStdout.split("\n").filter(Boolean);
|
|
11608
11573
|
for (const file of deletedFiles) {
|
|
11609
|
-
const ext =
|
|
11574
|
+
const ext = path17.extname(file).toLowerCase();
|
|
11610
11575
|
if (SCRIPT_EXTENSIONS.includes(ext)) {
|
|
11611
11576
|
warnings.push({
|
|
11612
11577
|
severity: "high",
|
|
@@ -11668,21 +11633,21 @@ function detectAllAntiPatterns() {
|
|
|
11668
11633
|
// src/engine/contextSnapshot.ts
|
|
11669
11634
|
init_sessionMemory();
|
|
11670
11635
|
init_pathValidator();
|
|
11671
|
-
import
|
|
11672
|
-
import
|
|
11636
|
+
import fs18 from "fs";
|
|
11637
|
+
import path18 from "path";
|
|
11673
11638
|
import { execSync as execSync6 } from "child_process";
|
|
11674
11639
|
var SNAPSHOT_DIR = "context-snapshots";
|
|
11675
11640
|
function snapshotDir() {
|
|
11676
|
-
return
|
|
11641
|
+
return path18.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
|
|
11677
11642
|
}
|
|
11678
11643
|
function ensureSnapshotDir() {
|
|
11679
11644
|
const dir = snapshotDir();
|
|
11680
|
-
if (!
|
|
11681
|
-
|
|
11645
|
+
if (!fs18.existsSync(dir)) {
|
|
11646
|
+
fs18.mkdirSync(dir, { recursive: true });
|
|
11682
11647
|
}
|
|
11683
11648
|
}
|
|
11684
11649
|
function snapshotFilePath(timestamp) {
|
|
11685
|
-
return
|
|
11650
|
+
return path18.join(snapshotDir(), `${timestamp}.json`);
|
|
11686
11651
|
}
|
|
11687
11652
|
function saveSnapshot(goal) {
|
|
11688
11653
|
try {
|
|
@@ -11703,7 +11668,7 @@ function saveSnapshot(goal) {
|
|
|
11703
11668
|
hasConventions: !!summary.hasConventions
|
|
11704
11669
|
};
|
|
11705
11670
|
try {
|
|
11706
|
-
|
|
11671
|
+
fs18.writeFileSync(snapshotFilePath(Date.now()), JSON.stringify(snapshot, null, 2), "utf-8");
|
|
11707
11672
|
} catch {
|
|
11708
11673
|
}
|
|
11709
11674
|
sessionMemory.recordToolCall("kuma_context", { action: "save", goal: snapshot.goal });
|
|
@@ -11802,6 +11767,55 @@ async function handleKumaGuard(params) {
|
|
|
11802
11767
|
});
|
|
11803
11768
|
}
|
|
11804
11769
|
}
|
|
11770
|
+
const recordingSummary = sessionMemory.getRecordingSummary();
|
|
11771
|
+
if (check === "all" || check === "drift") {
|
|
11772
|
+
const readCalls = stats.toolCalls.filter(
|
|
11773
|
+
(c) => c.toolName === "read" || c.toolName === "grep" || c.toolName === "glob" || c.toolName === "smart_file_picker"
|
|
11774
|
+
).length;
|
|
11775
|
+
if (stats.toolCallCount >= 10 && !recordingSummary.hasAnyRecordings) {
|
|
11776
|
+
warnings.push({
|
|
11777
|
+
severity: "high",
|
|
11778
|
+
pattern: "no-recordings-critical",
|
|
11779
|
+
message: `\u{1F6AB} BLOCKING: ${stats.toolCallCount} tool calls with 0 knowledge recordings. You are wasting future sessions by not recording.`,
|
|
11780
|
+
suggestion: "STOP. Record now:\n- kuma_memory({ action: 'research_save', scope: '<file>' }) after reading files\n- kuma_memory({ action: 'gotcha', scope: '<file>', content: '<bug>' }) when finding bugs\n- kuma_memory({ action: 'arch_flow', content: 'domain: <X> | hops: <file1> \u2192 <file2>' }) when tracing flows"
|
|
11781
|
+
});
|
|
11782
|
+
} else if (stats.toolCallCount >= 5 && !recordingSummary.hasAnyRecordings) {
|
|
11783
|
+
warnings.push({
|
|
11784
|
+
severity: "medium",
|
|
11785
|
+
pattern: "no-recordings",
|
|
11786
|
+
message: `${stats.toolCallCount} tool calls made but 0 knowledge recordings. Agent is not building persistent knowledge.`,
|
|
11787
|
+
suggestion: "Record findings after reading files. arch_flow + gotcha are exponential value."
|
|
11788
|
+
});
|
|
11789
|
+
} else if (readCalls >= 3 && recordingSummary.researchSaves === 0) {
|
|
11790
|
+
warnings.push({
|
|
11791
|
+
severity: "low",
|
|
11792
|
+
pattern: "read-without-record",
|
|
11793
|
+
message: `${readCalls} file reads but 0 research_save calls. Reading without recording = wasted context.`,
|
|
11794
|
+
suggestion: "After reading unfamiliar files: kuma_memory({ action: 'research_save', scope: '<file>' })"
|
|
11795
|
+
});
|
|
11796
|
+
}
|
|
11797
|
+
if (recordingSummary.total > 0) {
|
|
11798
|
+
}
|
|
11799
|
+
const errorCalls = stats.toolCalls.filter(
|
|
11800
|
+
(c) => c.toolName === "execute_safe_command" && JSON.stringify(c.params).includes("error")
|
|
11801
|
+
).length;
|
|
11802
|
+
if (errorCalls >= 2 && recordingSummary.gotchas === 0) {
|
|
11803
|
+
warnings.push({
|
|
11804
|
+
severity: "medium",
|
|
11805
|
+
pattern: "error-without-gotcha",
|
|
11806
|
+
message: `${errorCalls} error encounters but 0 gotchas recorded. Errors = future gotchas.`,
|
|
11807
|
+
suggestion: "Record gotchas for bugs you encounter:\nkuma_memory({ action: 'gotcha', scope: '<file>', content: '<what went wrong>', status: 'high' })"
|
|
11808
|
+
});
|
|
11809
|
+
}
|
|
11810
|
+
if (readCalls >= 4 && recordingSummary.archFlows === 0) {
|
|
11811
|
+
warnings.push({
|
|
11812
|
+
severity: "low",
|
|
11813
|
+
pattern: "trace-without-arch-flow",
|
|
11814
|
+
message: `${readCalls} file reads (possible flow tracing) but 0 arch_flows recorded. Flow knowledge dissipates fast.`,
|
|
11815
|
+
suggestion: "After tracing a flow, record it:\nkuma_memory({ action: 'arch_flow', content: 'domain: <Name> | hops: <file1> \u2192 <file2> \u2192 <file3>' })"
|
|
11816
|
+
});
|
|
11817
|
+
}
|
|
11818
|
+
}
|
|
11805
11819
|
if (check === "context") {
|
|
11806
11820
|
const snapshot = saveSnapshot(stats.goal);
|
|
11807
11821
|
if (!snapshot) {
|
|
@@ -11843,7 +11857,24 @@ async function handleKumaGuard(params) {
|
|
|
11843
11857
|
hasRunTests: stats.hasRunTests
|
|
11844
11858
|
}
|
|
11845
11859
|
};
|
|
11846
|
-
|
|
11860
|
+
const metricsSummary = sessionMemory.getMetricsSummary();
|
|
11861
|
+
const reportWithRecordings = {
|
|
11862
|
+
...report,
|
|
11863
|
+
recordings: {
|
|
11864
|
+
archFlows: recordingSummary.archFlows,
|
|
11865
|
+
gotchas: recordingSummary.gotchas,
|
|
11866
|
+
decisions: recordingSummary.decisions,
|
|
11867
|
+
researchSaves: recordingSummary.researchSaves,
|
|
11868
|
+
total: recordingSummary.total
|
|
11869
|
+
},
|
|
11870
|
+
metrics: {
|
|
11871
|
+
filesRead: metricsSummary.filesRead,
|
|
11872
|
+
filesEdited: metricsSummary.filesEdited,
|
|
11873
|
+
researchTimeSaved: metricsSummary.researchTimeSavedFormatted,
|
|
11874
|
+
sessionDuration: metricsSummary.sessionDuration
|
|
11875
|
+
}
|
|
11876
|
+
};
|
|
11877
|
+
return JSON.stringify(reportWithRecordings, null, 2);
|
|
11847
11878
|
}
|
|
11848
11879
|
|
|
11849
11880
|
// src/tools/kumaSafetyTool.ts
|
|
@@ -11948,12 +11979,12 @@ async function handleSecurity(params) {
|
|
|
11948
11979
|
sessionMemory.recordToolCall("kuma_safety_security", {});
|
|
11949
11980
|
if (params.filePath) {
|
|
11950
11981
|
try {
|
|
11951
|
-
const
|
|
11952
|
-
const
|
|
11982
|
+
const fs26 = await import("fs");
|
|
11983
|
+
const path26 = await import("path");
|
|
11953
11984
|
const root = process.cwd();
|
|
11954
|
-
const fullPath =
|
|
11955
|
-
if (
|
|
11956
|
-
const content =
|
|
11985
|
+
const fullPath = path26.resolve(root, params.filePath);
|
|
11986
|
+
if (fs26.existsSync(fullPath) && fs26.statSync(fullPath).isFile()) {
|
|
11987
|
+
const content = fs26.readFileSync(fullPath, "utf-8");
|
|
11957
11988
|
const lines = content.split("\n");
|
|
11958
11989
|
const findings = [];
|
|
11959
11990
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -12007,23 +12038,23 @@ async function handleGitignore(_params) {
|
|
|
12007
12038
|
}
|
|
12008
12039
|
async function handleClean(_params) {
|
|
12009
12040
|
sessionMemory.recordToolCall("kuma_safety_clean", {});
|
|
12010
|
-
const
|
|
12011
|
-
const
|
|
12041
|
+
const fs26 = await import("fs");
|
|
12042
|
+
const path26 = await import("path");
|
|
12012
12043
|
const root = process.cwd();
|
|
12013
|
-
const scratchDir =
|
|
12044
|
+
const scratchDir = path26.resolve(root, ".kuma", "scratch");
|
|
12014
12045
|
let removed = 0;
|
|
12015
|
-
if (
|
|
12046
|
+
if (fs26.existsSync(scratchDir)) {
|
|
12016
12047
|
try {
|
|
12017
|
-
const entries =
|
|
12048
|
+
const entries = fs26.readdirSync(scratchDir);
|
|
12018
12049
|
for (const entry of entries) {
|
|
12019
|
-
const fullPath =
|
|
12050
|
+
const fullPath = path26.join(scratchDir, entry);
|
|
12020
12051
|
try {
|
|
12021
|
-
const stat =
|
|
12052
|
+
const stat = fs26.statSync(fullPath);
|
|
12022
12053
|
if (stat.isFile()) {
|
|
12023
|
-
|
|
12054
|
+
fs26.unlinkSync(fullPath);
|
|
12024
12055
|
removed++;
|
|
12025
12056
|
} else if (stat.isDirectory()) {
|
|
12026
|
-
|
|
12057
|
+
fs26.rmSync(fullPath, { recursive: true, force: true });
|
|
12027
12058
|
removed++;
|
|
12028
12059
|
}
|
|
12029
12060
|
} catch {
|
|
@@ -12125,13 +12156,30 @@ async function handleVerify(params) {
|
|
|
12125
12156
|
return `\u23F3 **Handler rate limit** \u2014 verify was just called ${Math.floor((now - _lastVerifyCall) / 1e3)}s ago. Please wait ${remaining}s before calling verify again.`;
|
|
12126
12157
|
}
|
|
12127
12158
|
_lastVerifyCall = now;
|
|
12159
|
+
const recordingSummary = sessionMemory.getRecordingSummary();
|
|
12160
|
+
const stats = sessionMemory.getSummary();
|
|
12161
|
+
const toolCallCount = stats.toolCallCount || 0;
|
|
12162
|
+
let recordingWarning = "";
|
|
12163
|
+
if (toolCallCount >= 5 && !recordingSummary.hasAnyRecordings) {
|
|
12164
|
+
recordingWarning = `
|
|
12165
|
+
|
|
12166
|
+
\u26A0\uFE0F **RECORDING MISSING:** You made ${toolCallCount} tool calls with 0 recordings. Before switching tasks, record what you learned:
|
|
12167
|
+
- kuma_memory({ action: 'research_save', scope: '<file>' })
|
|
12168
|
+
- kuma_memory({ action: 'gotcha', ... }) if you found bugs
|
|
12169
|
+
- kuma_memory({ action: 'arch_flow', ... }) if you traced a flow`;
|
|
12170
|
+
} else if (recordingSummary.total > 0) {
|
|
12171
|
+
recordingWarning = `
|
|
12172
|
+
|
|
12173
|
+
\u2705 **Recordings:** ${recordingSummary.total} total (${recordingSummary.archFlows} arch_flow, ${recordingSummary.gotchas} gotcha, ${recordingSummary.decisions} decision, ${recordingSummary.researchSaves} research_save)`;
|
|
12174
|
+
}
|
|
12128
12175
|
sessionMemory.recordToolCall("kuma_safety_verify", { scope: params.scope || params.filePath });
|
|
12129
12176
|
const { runAutoVerification: runAutoVerification2 } = await Promise.resolve().then(() => (init_kumaVerifier(), kumaVerifier_exports));
|
|
12130
|
-
|
|
12177
|
+
const verifyResult = await runAutoVerification2({
|
|
12131
12178
|
scope: params.scope || params.filePath,
|
|
12132
12179
|
force: params.force,
|
|
12133
12180
|
timeoutMs: 3e4
|
|
12134
12181
|
});
|
|
12182
|
+
return verifyResult + recordingWarning;
|
|
12135
12183
|
}
|
|
12136
12184
|
|
|
12137
12185
|
// src/manifest.ts
|
|
@@ -12169,8 +12217,8 @@ function registerAllTools(server) {
|
|
|
12169
12217
|
"kuma_memory",
|
|
12170
12218
|
"**Call after research/editing** (including research-only). Record what matters, skip what doesn't.\n\n\u{1F534} MUST RECORD \u2014 High Impact (saves agent time next session):\n\u2022 STEP 5: `research_save` \u2014 after exploring area (creates search cache).\n\u2022 STEP 6: `gotcha` \u2014 IMMEDIATELY when you discover bugs/quirks. No re-research.\n\u2022 STEP 7: `arch_flow` \u2014 IMMEDIATELY after EACH flow hop. Saves reading 5-10 files.\n\u2022 STEP 8: `decision` \u2014 IMMEDIATELY when choosing between options. Preserves rationale.\n\n\u{1F7E2} SKIP using MCP (agent native tools are faster):\n\u2022 Function/class nodes \u2192 grep\n\u2022 Component/route nodes \u2192 glob or check directly\n\u2022 Import edges \u2192 read imports directly\n\u2022 Visual graph \u2192 for humans, not AI agents\n\nOther actions: session, heal, search, changes, benchmark, layers.",
|
|
12171
12219
|
{
|
|
12172
|
-
action: z.enum(["decision", "mine", "research_save", "session", "heal", "search", "changes", "todo", "context", "benchmark", "decision_log", "domain_rules", "arch_flow", "gotcha", "layers", "
|
|
12173
|
-
scope: z.string().optional().describe("Scope for research_save/search/todo/context/mine
|
|
12220
|
+
action: z.enum(["decision", "mine", "research_save", "session", "heal", "search", "changes", "todo", "context", "benchmark", "decision_log", "domain_rules", "arch_flow", "gotcha", "layers", "add_node", "delete_node", "clear", "feature"]).describe("Memory action: decision=ADR, mine=mine git log & comments, research_save=save (creates file + graph node), session=summary, heal=repair, search=search (now with task retrieval + impact analysis), changes=log, todo=manage todos, context=inject notes, benchmark=capture/diff, decision_log=manage decisions, domain_rules=Layer 1 business rules, arch_flow=Layer 2 architecture flow (now auto-creates affects edges), gotcha=Layer 3 known gotchas, layers=all 3 layers summary, add_node=manually create function/class/component structural nodes, delete_node=delete a specific node/gotcha/todo/decision by ID or scope+target, clear=wipe all nodes/edges/gotchas, feature=record high-level feature with owns edges to files"),
|
|
12221
|
+
scope: z.string().optional().describe("Scope for research_save/search/todo/context/mine"),
|
|
12174
12222
|
query: z.string().optional().describe("Search query for search action"),
|
|
12175
12223
|
content: z.string().optional().describe("Content/notes for research_save / context"),
|
|
12176
12224
|
record: z.string().optional().describe("JSON record string for research_save"),
|
|
@@ -12186,7 +12234,7 @@ function registerAllTools(server) {
|
|
|
12186
12234
|
healAction: z.enum(["check", "heal"]).optional().describe("Heal sub-action"),
|
|
12187
12235
|
topic: z.string().optional().describe("Memory topic for session"),
|
|
12188
12236
|
limit: z.number().min(1).max(100).optional().describe("Result limit"),
|
|
12189
|
-
target: z.string().optional().describe("File path for changes / decision_log ID
|
|
12237
|
+
target: z.string().optional().describe("File path for changes / decision_log ID"),
|
|
12190
12238
|
since: z.number().optional().describe("Timestamp filter for changes"),
|
|
12191
12239
|
compact: z.boolean().optional().default(false).describe("Compact output mode"),
|
|
12192
12240
|
// Todo params
|
|
@@ -12223,8 +12271,7 @@ function registerAllTools(server) {
|
|
|
12223
12271
|
topic: params.topic,
|
|
12224
12272
|
limit: params.limit,
|
|
12225
12273
|
target: params.target,
|
|
12226
|
-
since: params.since
|
|
12227
|
-
uri: params.uri
|
|
12274
|
+
since: params.since
|
|
12228
12275
|
});
|
|
12229
12276
|
return { content: [{ type: "text", text }] };
|
|
12230
12277
|
} catch (err) {
|
|
@@ -12358,14 +12405,14 @@ async function main() {
|
|
|
12358
12405
|
} catch {
|
|
12359
12406
|
}
|
|
12360
12407
|
try {
|
|
12361
|
-
const
|
|
12362
|
-
const
|
|
12363
|
-
const lockDir =
|
|
12364
|
-
if (
|
|
12365
|
-
const pidFile =
|
|
12366
|
-
if (
|
|
12408
|
+
const path26 = await import("path");
|
|
12409
|
+
const fs26 = await import("fs");
|
|
12410
|
+
const lockDir = path26.resolve(process.cwd(), ".kuma/verifier.lock");
|
|
12411
|
+
if (fs26.existsSync(lockDir)) {
|
|
12412
|
+
const pidFile = path26.join(lockDir, "pid");
|
|
12413
|
+
if (fs26.existsSync(pidFile)) {
|
|
12367
12414
|
try {
|
|
12368
|
-
const pid = parseInt(
|
|
12415
|
+
const pid = parseInt(fs26.readFileSync(pidFile, "utf-8"), 10);
|
|
12369
12416
|
if (pid && pid !== process.pid) {
|
|
12370
12417
|
try {
|
|
12371
12418
|
process.kill(-pid, "SIGKILL");
|
|
@@ -12381,7 +12428,7 @@ async function main() {
|
|
|
12381
12428
|
} catch {
|
|
12382
12429
|
}
|
|
12383
12430
|
}
|
|
12384
|
-
|
|
12431
|
+
fs26.rmSync(lockDir, { recursive: true, force: true });
|
|
12385
12432
|
}
|
|
12386
12433
|
if (killedCount === 0) {
|
|
12387
12434
|
console.error("\u2705 No running verifications found to kill.");
|
|
@@ -12521,23 +12568,23 @@ async function main() {
|
|
|
12521
12568
|
});
|
|
12522
12569
|
const output = formatInitResults(results);
|
|
12523
12570
|
console.log(output);
|
|
12524
|
-
const
|
|
12525
|
-
const
|
|
12526
|
-
const matchaSkills =
|
|
12527
|
-
const matchaAgents =
|
|
12571
|
+
const fs26 = await import("fs");
|
|
12572
|
+
const path26 = await import("path");
|
|
12573
|
+
const matchaSkills = path26.resolve(process.cwd(), "skills/matcha/SKILL.md");
|
|
12574
|
+
const matchaAgents = path26.resolve(
|
|
12528
12575
|
process.cwd(),
|
|
12529
12576
|
".agents/skills/matcha/SKILL.md"
|
|
12530
12577
|
);
|
|
12531
|
-
const matchaRootSkills =
|
|
12578
|
+
const matchaRootSkills = path26.resolve(
|
|
12532
12579
|
process.cwd(),
|
|
12533
12580
|
"skills/matcha/SKILL.md"
|
|
12534
12581
|
);
|
|
12535
|
-
const matchaAgentsMd =
|
|
12536
|
-
const matchaWindsurfRules =
|
|
12582
|
+
const matchaAgentsMd = path26.resolve(process.cwd(), "AGENTS.md");
|
|
12583
|
+
const matchaWindsurfRules = path26.resolve(
|
|
12537
12584
|
process.cwd(),
|
|
12538
12585
|
".windsurf"
|
|
12539
12586
|
);
|
|
12540
|
-
if (
|
|
12587
|
+
if (fs26.existsSync(matchaSkills) || fs26.existsSync(matchaAgents) || fs26.existsSync(matchaRootSkills) || fs26.existsSync(matchaAgentsMd) || fs26.existsSync(matchaWindsurfRules)) {
|
|
12541
12588
|
console.error(
|
|
12542
12589
|
"\n\u{1F375} Hey, I see matcha is installed \u2014 they pair well together!"
|
|
12543
12590
|
);
|
|
@@ -12551,13 +12598,13 @@ async function main() {
|
|
|
12551
12598
|
(async () => {
|
|
12552
12599
|
try {
|
|
12553
12600
|
const { generateInitMdContent: generateInitMdContent2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
12554
|
-
const
|
|
12555
|
-
const
|
|
12556
|
-
const initMdPath =
|
|
12557
|
-
if (!
|
|
12558
|
-
const kumaDir =
|
|
12559
|
-
if (!
|
|
12560
|
-
|
|
12601
|
+
const fs26 = await import("fs");
|
|
12602
|
+
const path26 = await import("path");
|
|
12603
|
+
const initMdPath = path26.resolve(process.cwd(), ".kuma/init.md");
|
|
12604
|
+
if (!fs26.existsSync(initMdPath)) {
|
|
12605
|
+
const kumaDir = path26.dirname(initMdPath);
|
|
12606
|
+
if (!fs26.existsSync(kumaDir)) fs26.mkdirSync(kumaDir, { recursive: true });
|
|
12607
|
+
fs26.writeFileSync(initMdPath, generateInitMdContent2(), "utf-8");
|
|
12561
12608
|
console.error(`[${SERVER_NAME}] Auto-generated .kuma/init.md`);
|
|
12562
12609
|
}
|
|
12563
12610
|
} catch (err) {
|
|
@@ -12568,8 +12615,8 @@ async function main() {
|
|
|
12568
12615
|
try {
|
|
12569
12616
|
const { detectAgent: detectAgent2, getSkillPath: getSkillPath2, getAgentLabel: getAgentLabel2 } = await Promise.resolve().then(() => (init_agentDetector(), agentDetector_exports));
|
|
12570
12617
|
const { generateSkill: generateSkill2, getSecondaryFiles: getSecondaryFiles2 } = await Promise.resolve().then(() => (init_skillGenerator(), skillGenerator_exports));
|
|
12571
|
-
const
|
|
12572
|
-
const
|
|
12618
|
+
const fs26 = await import("fs");
|
|
12619
|
+
const path26 = await import("path");
|
|
12573
12620
|
const detection = detectAgent2();
|
|
12574
12621
|
if (!detection.primary) {
|
|
12575
12622
|
console.error(`[${SERVER_NAME}] No AI agent detected \u2014 skipping auto-skill creation`);
|
|
@@ -12577,22 +12624,22 @@ async function main() {
|
|
|
12577
12624
|
}
|
|
12578
12625
|
const agentType = detection.primary;
|
|
12579
12626
|
const skillPath = getSkillPath2(agentType);
|
|
12580
|
-
const fullPath =
|
|
12581
|
-
if (
|
|
12627
|
+
const fullPath = path26.resolve(process.cwd(), skillPath);
|
|
12628
|
+
if (fs26.existsSync(fullPath)) {
|
|
12582
12629
|
console.error(`[${SERVER_NAME}] Skill exists for ${getAgentLabel2(agentType)} \u2014 skipping`);
|
|
12583
12630
|
return;
|
|
12584
12631
|
}
|
|
12585
|
-
const dir =
|
|
12586
|
-
if (!
|
|
12587
|
-
|
|
12632
|
+
const dir = path26.dirname(fullPath);
|
|
12633
|
+
if (!fs26.existsSync(dir)) fs26.mkdirSync(dir, { recursive: true });
|
|
12634
|
+
fs26.writeFileSync(fullPath, generateSkill2(agentType), "utf-8");
|
|
12588
12635
|
console.error(`[${SERVER_NAME}] Auto-created ${skillPath} for ${getAgentLabel2(agentType)}`);
|
|
12589
12636
|
const secondaryFiles = getSecondaryFiles2(agentType);
|
|
12590
12637
|
for (const sf of secondaryFiles) {
|
|
12591
|
-
const sfPath =
|
|
12592
|
-
const sfDir =
|
|
12593
|
-
if (!
|
|
12594
|
-
if (!
|
|
12595
|
-
|
|
12638
|
+
const sfPath = path26.resolve(process.cwd(), sf.path);
|
|
12639
|
+
const sfDir = path26.dirname(sfPath);
|
|
12640
|
+
if (!fs26.existsSync(sfPath)) {
|
|
12641
|
+
if (!fs26.existsSync(sfDir)) fs26.mkdirSync(sfDir, { recursive: true });
|
|
12642
|
+
fs26.writeFileSync(sfPath, sf.content, "utf-8");
|
|
12596
12643
|
console.error(`[${SERVER_NAME}] Auto-created ${sf.path}`);
|
|
12597
12644
|
}
|
|
12598
12645
|
}
|
|
@@ -12617,11 +12664,11 @@ async function main() {
|
|
|
12617
12664
|
console.error(`[${SERVER_NAME}] \u26A0\uFE0F Graph auto-population: ${err}`);
|
|
12618
12665
|
}
|
|
12619
12666
|
try {
|
|
12620
|
-
const
|
|
12621
|
-
const
|
|
12622
|
-
const scratchDir =
|
|
12623
|
-
if (!
|
|
12624
|
-
|
|
12667
|
+
const fs26 = await import("fs");
|
|
12668
|
+
const path26 = await import("path");
|
|
12669
|
+
const scratchDir = path26.resolve(process.cwd(), ".kuma", "scratch");
|
|
12670
|
+
if (!fs26.existsSync(scratchDir)) {
|
|
12671
|
+
fs26.mkdirSync(scratchDir, { recursive: true });
|
|
12625
12672
|
console.error(`[${SERVER_NAME}] \u2705 Created .kuma/scratch/ for temporary debug artifacts`);
|
|
12626
12673
|
}
|
|
12627
12674
|
} catch (err) {
|
|
@@ -12639,11 +12686,11 @@ async function main() {
|
|
|
12639
12686
|
console.error(`[${SERVER_NAME}] \u26A0\uFE0F Session DB record: ${err}`);
|
|
12640
12687
|
}
|
|
12641
12688
|
try {
|
|
12642
|
-
const
|
|
12643
|
-
const
|
|
12644
|
-
const hooksDir =
|
|
12645
|
-
if (!
|
|
12646
|
-
|
|
12689
|
+
const fs26 = await import("fs");
|
|
12690
|
+
const path26 = await import("path");
|
|
12691
|
+
const hooksDir = path26.resolve(process.cwd(), ".kuma", "hooks");
|
|
12692
|
+
if (!fs26.existsSync(hooksDir)) {
|
|
12693
|
+
fs26.mkdirSync(hooksDir, { recursive: true });
|
|
12647
12694
|
const hookContent = `#!/bin/bash
|
|
12648
12695
|
# Kuma auto-capture hook (post-commit)
|
|
12649
12696
|
# Auto-updates knowledge graph after git commits
|
|
@@ -12651,13 +12698,13 @@ if command -v npx &> /dev/null; then
|
|
|
12651
12698
|
npx -y @plumpslabs/kuma --hook post-commit 2>/dev/null || true
|
|
12652
12699
|
fi
|
|
12653
12700
|
`;
|
|
12654
|
-
const gitHooksDir =
|
|
12655
|
-
if (
|
|
12656
|
-
const postCommitPath =
|
|
12657
|
-
if (!
|
|
12658
|
-
|
|
12701
|
+
const gitHooksDir = path26.resolve(process.cwd(), ".git", "hooks");
|
|
12702
|
+
if (fs26.existsSync(gitHooksDir)) {
|
|
12703
|
+
const postCommitPath = path26.join(gitHooksDir, "post-commit");
|
|
12704
|
+
if (!fs26.existsSync(postCommitPath)) {
|
|
12705
|
+
fs26.writeFileSync(postCommitPath, hookContent, "utf-8");
|
|
12659
12706
|
try {
|
|
12660
|
-
|
|
12707
|
+
fs26.chmodSync(postCommitPath, 493);
|
|
12661
12708
|
} catch {
|
|
12662
12709
|
}
|
|
12663
12710
|
console.error(`[${SERVER_NAME}] \u2705 Created .git/hooks/post-commit for auto-capture`);
|