@plumpslabs/kuma 2.3.31 → 2.3.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +321 -184
- package/dist/index.js +1469 -521
- package/package.json +1 -1
- package/packages/ide/studio/dist/index.js +298 -101
- package/packages/ide/studio/public/index.html +486 -74
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plumpslabs/kuma",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.33",
|
|
4
4
|
"description": "Safety-first context & orchestration engine for AI coding agents. MCP server with mandatory research pipeline, knowledge graph, impact analysis, decision memory, and safety guard — works with any MCP client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -3,135 +3,328 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { serve } from "@hono/node-server";
|
|
5
5
|
import { readFileSync } from "fs";
|
|
6
|
-
import { join
|
|
6
|
+
import { join, dirname } from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import { spawn } from "child_process";
|
|
9
9
|
|
|
10
10
|
// src/api.ts
|
|
11
11
|
import { Hono } from "hono";
|
|
12
|
-
import { cors } from "hono/cors";
|
|
13
12
|
|
|
14
13
|
// src/db.ts
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import
|
|
14
|
+
import crypto from "crypto";
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import path from "path";
|
|
17
|
+
import { createRequire } from "module";
|
|
18
|
+
var require2 = createRequire(import.meta.url);
|
|
19
|
+
var sqlJs = null;
|
|
20
|
+
async function getSqlJs() {
|
|
21
|
+
if (!sqlJs) {
|
|
22
|
+
const initSqlJs = await require2("sql.js");
|
|
23
|
+
sqlJs = await initSqlJs();
|
|
24
|
+
}
|
|
25
|
+
return sqlJs;
|
|
26
|
+
}
|
|
18
27
|
function findKumaDb(startDir) {
|
|
19
|
-
let current = startDir ? resolve(startDir) : process.cwd();
|
|
28
|
+
let current = startDir ? path.resolve(startDir) : process.cwd();
|
|
20
29
|
for (let i = 0; i < 20; i++) {
|
|
21
|
-
const candidate = join(current, ".kuma", "kuma.db");
|
|
22
|
-
if (existsSync(candidate)) return candidate;
|
|
23
|
-
const parent = resolve(current, "..");
|
|
30
|
+
const candidate = path.join(current, ".kuma", "kuma.db");
|
|
31
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
32
|
+
const parent = path.resolve(current, "..");
|
|
24
33
|
if (parent === current) break;
|
|
25
34
|
current = parent;
|
|
26
35
|
}
|
|
27
36
|
return null;
|
|
28
37
|
}
|
|
29
|
-
function
|
|
38
|
+
function projectRootFromDb(dbPath) {
|
|
39
|
+
return path.dirname(path.dirname(dbPath));
|
|
40
|
+
}
|
|
41
|
+
async function openDb() {
|
|
42
|
+
const SQL = await getSqlJs();
|
|
30
43
|
const dbPath = findKumaDb();
|
|
31
44
|
if (!dbPath) throw new Error("No .kuma/kuma.db found");
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
maxBuffer: 2 * 1024 * 1024,
|
|
35
|
-
timeout: 1e4
|
|
36
|
-
}).trim();
|
|
45
|
+
const buffer = fs.readFileSync(dbPath);
|
|
46
|
+
return new SQL.Database(buffer);
|
|
37
47
|
}
|
|
38
|
-
function
|
|
39
|
-
const
|
|
40
|
-
|
|
48
|
+
function readSessionMetrics(dbPath) {
|
|
49
|
+
const empty = {
|
|
50
|
+
metrics: { filesRead: 0, filesEdited: 0, researchTimeSaved: 0 },
|
|
51
|
+
recordings: { archFlows: 0, gotchas: 0, decisions: 0, researchSaves: 0, total: 0 }
|
|
52
|
+
};
|
|
41
53
|
try {
|
|
42
|
-
|
|
54
|
+
const root = projectRootFromDb(dbPath);
|
|
55
|
+
const memPath = path.join(root, ".kuma", "memory.json");
|
|
56
|
+
if (!fs.existsSync(memPath)) return empty;
|
|
57
|
+
const parsed = JSON.parse(fs.readFileSync(memPath, "utf-8"));
|
|
58
|
+
return {
|
|
59
|
+
metrics: {
|
|
60
|
+
filesRead: parsed?.metrics?.filesRead ?? 0,
|
|
61
|
+
filesEdited: parsed?.metrics?.filesEdited ?? 0,
|
|
62
|
+
researchTimeSaved: parsed?.metrics?.researchTimeSaved ?? 0
|
|
63
|
+
},
|
|
64
|
+
recordings: {
|
|
65
|
+
archFlows: parsed?.recordings?.archFlows ?? 0,
|
|
66
|
+
gotchas: parsed?.recordings?.gotchas ?? 0,
|
|
67
|
+
decisions: parsed?.recordings?.decisions ?? 0,
|
|
68
|
+
researchSaves: parsed?.recordings?.researchSaves ?? 0,
|
|
69
|
+
total: parsed?.recordings?.total ?? 0
|
|
70
|
+
}
|
|
71
|
+
};
|
|
43
72
|
} catch {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
73
|
+
return empty;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function detectStaleNodes(db, dbPath) {
|
|
77
|
+
try {
|
|
78
|
+
const root = projectRootFromDb(dbPath);
|
|
79
|
+
const stmt = db.prepare(
|
|
80
|
+
`SELECT id, file_path, metadata, COUNT(*) as cnt FROM nodes
|
|
81
|
+
WHERE file_path IS NOT NULL AND file_path != '' AND file_path NOT LIKE '%::%'
|
|
82
|
+
GROUP BY file_path ORDER BY cnt DESC LIMIT 500`
|
|
83
|
+
);
|
|
84
|
+
const missing = [];
|
|
85
|
+
let checked = 0;
|
|
86
|
+
let staleNodes = 0;
|
|
87
|
+
while (stmt.step()) {
|
|
88
|
+
const row = stmt.getAsObject();
|
|
89
|
+
const fp = row.file_path;
|
|
90
|
+
const metadata = row.metadata || "{}";
|
|
91
|
+
checked++;
|
|
92
|
+
const fullPath = path.join(root, fp);
|
|
93
|
+
if (!fs.existsSync(fullPath)) {
|
|
94
|
+
const count = Number(row.cnt ?? 1);
|
|
95
|
+
missing.push({ filePath: fp, nodeCount: count, reason: "file_missing" });
|
|
96
|
+
staleNodes += count;
|
|
97
|
+
} else {
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(metadata);
|
|
100
|
+
const storedHash = parsed.contentHash;
|
|
101
|
+
if (storedHash) {
|
|
102
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
103
|
+
const currentHash = crypto.createHash("md5").update(content).digest("hex");
|
|
104
|
+
if (currentHash !== storedHash) {
|
|
105
|
+
const count = Number(row.cnt ?? 1);
|
|
106
|
+
missing.push({ filePath: fp, nodeCount: count, reason: "content_changed" });
|
|
107
|
+
staleNodes += count;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
}
|
|
50
113
|
}
|
|
114
|
+
stmt.free();
|
|
115
|
+
return { checked, staleNodes, missing };
|
|
116
|
+
} catch {
|
|
117
|
+
return { checked: 0, staleNodes: 0, missing: [] };
|
|
51
118
|
}
|
|
52
119
|
}
|
|
53
|
-
function getDashboardData() {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
120
|
+
async function getDashboardData() {
|
|
121
|
+
const db = await openDb();
|
|
122
|
+
const dbPath = findKumaDb();
|
|
123
|
+
try {
|
|
124
|
+
const run = (sql) => {
|
|
125
|
+
try {
|
|
126
|
+
const stmt = db.prepare(sql);
|
|
127
|
+
const rows = [];
|
|
128
|
+
while (stmt.step()) rows.push(stmt.getAsObject());
|
|
129
|
+
stmt.free();
|
|
130
|
+
return rows;
|
|
131
|
+
} catch {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const first = (sql, key) => {
|
|
136
|
+
const rows = run(sql);
|
|
137
|
+
if (!rows.length) return 0;
|
|
138
|
+
const v = rows[0][key];
|
|
139
|
+
return Number(v ?? 0) || 0;
|
|
140
|
+
};
|
|
141
|
+
const jsonRows = (sql) => {
|
|
142
|
+
const rows = run(sql);
|
|
143
|
+
if (!rows.length) return [];
|
|
144
|
+
const key = Object.keys(rows[0])[0];
|
|
145
|
+
const raw = rows[0][key];
|
|
146
|
+
if (raw === null || raw === void 0) return [];
|
|
147
|
+
try {
|
|
148
|
+
const parsed = JSON.parse(String(raw));
|
|
149
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
150
|
+
} catch {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const stats = {
|
|
155
|
+
node_count: first(`SELECT COUNT(*) as c FROM nodes`, "c"),
|
|
156
|
+
edge_count: first(`SELECT COUNT(*) as c FROM edges`, "c"),
|
|
157
|
+
gotcha_count: first(`SELECT COUNT(*) as c FROM known_gotchas`, "c"),
|
|
158
|
+
feature_count: first(`SELECT COUNT(*) as c FROM nodes WHERE type = 'feature'`, "c"),
|
|
159
|
+
health_score: first(
|
|
160
|
+
`SELECT COALESCE((SELECT score FROM health_snapshots ORDER BY created_at DESC LIMIT 1), 0) as c`,
|
|
161
|
+
"c"
|
|
62
162
|
)
|
|
63
|
-
|
|
64
|
-
nodes
|
|
65
|
-
`SELECT json_group_array(json_object('id',id,'name',name,'type',type,'file_path',file_path,'severity',COALESCE(severity,'medium'),'confidence',COALESCE(confidence,0.8),'metadata',COALESCE(metadata,'{}'))) FROM nodes ORDER BY updated_at DESC`
|
|
66
|
-
)
|
|
67
|
-
edges
|
|
163
|
+
};
|
|
164
|
+
const nodes = jsonRows(
|
|
165
|
+
`SELECT json_group_array(json_object('id',id,'name',name,'type',type,'file_path',file_path,'severity',COALESCE(severity,'medium'),'confidence',COALESCE(confidence,0.8),'metadata',COALESCE(metadata,'{}'))) FROM (SELECT * FROM nodes ORDER BY updated_at DESC)`
|
|
166
|
+
);
|
|
167
|
+
const edges = jsonRows(
|
|
68
168
|
`SELECT json_group_array(json_object('source',source_id,'target',target_id,'relation',type,'weight',weight)) FROM edges`
|
|
69
|
-
)
|
|
70
|
-
gotchas
|
|
71
|
-
`SELECT json_group_array(json_object('id',id,'file_path',file_path,'description',REPLACE(REPLACE(description,char(10),' '),char(13),''),'severity',severity,'workaround',REPLACE(REPLACE(COALESCE(workaround,''),char(10),' '),char(13),''),'added_by',COALESCE(added_by,'agent'),'created_at',created_at)) FROM known_gotchas ORDER BY
|
|
72
|
-
)
|
|
73
|
-
features
|
|
74
|
-
`SELECT json_group_array(json_object('id',id,'name',name,'metadata',COALESCE(metadata,'{}'))) FROM nodes WHERE type = 'feature' ORDER BY
|
|
75
|
-
)
|
|
76
|
-
health
|
|
77
|
-
`SELECT json_group_array(json_object('score',score,'summary',REPLACE(REPLACE(COALESCE(summary,''),char(10),' '),char(13),''),'risk_level',risk_level,'created_at',created_at)) FROM health_snapshots ORDER BY created_at DESC LIMIT 10`
|
|
78
|
-
)
|
|
79
|
-
|
|
169
|
+
);
|
|
170
|
+
const gotchas = jsonRows(
|
|
171
|
+
`SELECT json_group_array(json_object('id',id,'file_path',file_path,'description',REPLACE(REPLACE(description,char(10),' '),char(13),''),'severity',severity,'workaround',REPLACE(REPLACE(COALESCE(workaround,''),char(10),' '),char(13),''),'added_by',COALESCE(added_by,'agent'),'created_at',created_at)) FROM (SELECT * FROM known_gotchas ORDER BY created_at DESC)`
|
|
172
|
+
);
|
|
173
|
+
const features = jsonRows(
|
|
174
|
+
`SELECT json_group_array(json_object('id',id,'name',name,'metadata',COALESCE(metadata,'{}'))) FROM (SELECT * FROM nodes WHERE type = 'feature' ORDER BY updated_at DESC)`
|
|
175
|
+
);
|
|
176
|
+
const health = jsonRows(
|
|
177
|
+
`SELECT json_group_array(json_object('score',score,'summary',REPLACE(REPLACE(COALESCE(summary,''),char(10),' '),char(13),''),'risk_level',risk_level,'created_at',created_at)) FROM (SELECT * FROM health_snapshots ORDER BY created_at DESC LIMIT 10)`
|
|
178
|
+
);
|
|
179
|
+
const sessionMetrics = readSessionMetrics(dbPath);
|
|
180
|
+
const verifTotal = first(`SELECT COUNT(*) as c FROM verifications`, "c");
|
|
181
|
+
const verifPassed = first(`SELECT COUNT(*) as c FROM verifications WHERE passed = 1`, "c");
|
|
182
|
+
const recentSessions = run(
|
|
183
|
+
`SELECT started_at, COALESCE(goal,'') as goal, tool_calls, edits, rollbacks, failures, safety_score
|
|
184
|
+
FROM (SELECT * FROM sessions ORDER BY started_at DESC LIMIT 8)`
|
|
185
|
+
).map((r) => ({
|
|
186
|
+
startedAt: Number(r.started_at ?? 0),
|
|
187
|
+
goal: r.goal || "",
|
|
188
|
+
toolCalls: Number(r.tool_calls ?? 0),
|
|
189
|
+
edits: Number(r.edits ?? 0),
|
|
190
|
+
rollbacks: Number(r.rollbacks ?? 0),
|
|
191
|
+
failures: Number(r.failures ?? 0),
|
|
192
|
+
safetyScore: r.safety_score == null ? null : Number(r.safety_score)
|
|
193
|
+
}));
|
|
194
|
+
const efficiency = {
|
|
195
|
+
sessions: first(`SELECT COUNT(*) as c FROM sessions`, "c"),
|
|
196
|
+
toolCalls: first(`SELECT COUNT(*) as c FROM tool_calls`, "c"),
|
|
197
|
+
gotchas: first(`SELECT COUNT(*) as c FROM known_gotchas`, "c"),
|
|
198
|
+
archFlows: sessionMetrics.recordings.archFlows,
|
|
199
|
+
decisions: first(`SELECT COUNT(*) as c FROM (SELECT id FROM decision_log UNION SELECT id FROM nodes WHERE type = 'decision')`, "c"),
|
|
200
|
+
researchCacheScopes: first(`SELECT COUNT(*) as c FROM research_cache`, "c"),
|
|
201
|
+
verifications: verifTotal,
|
|
202
|
+
verificationPassRate: verifTotal > 0 ? Math.round(verifPassed / verifTotal * 100) : null,
|
|
203
|
+
metrics: sessionMetrics.metrics,
|
|
204
|
+
recordings: sessionMetrics.recordings,
|
|
205
|
+
recentSessions
|
|
206
|
+
};
|
|
207
|
+
const staleness = detectStaleNodes(db, dbPath);
|
|
208
|
+
return { stats, nodes, edges, gotchas, features, health, efficiency, staleness };
|
|
209
|
+
} finally {
|
|
210
|
+
db.close();
|
|
211
|
+
}
|
|
80
212
|
}
|
|
81
|
-
function getNodeDetail(nodeId) {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
return JSON.parse(r);
|
|
108
|
-
} catch {
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
}).filter(Boolean) : [];
|
|
112
|
-
const gotchaFilter = filePath ? `file_path = '${filePath}'` : `file_path = '${escaped}' OR file_path LIKE '%${shortName}%'`;
|
|
113
|
-
const rawGotcha = query(
|
|
114
|
-
`SELECT json_object('id',id,'file_path',file_path,'description',REPLACE(REPLACE(description,char(10),' '),char(13),''),'severity',severity,'workaround',REPLACE(REPLACE(COALESCE(workaround,''),char(10),' '),char(13),''),'added_by',COALESCE(added_by,'agent'),'created_at',created_at) FROM known_gotchas WHERE ${gotchaFilter}`
|
|
115
|
-
);
|
|
116
|
-
const gotchas = rawGotcha ? rawGotcha.split("\n").filter(Boolean).map((r) => {
|
|
213
|
+
async function getNodeDetail(nodeId) {
|
|
214
|
+
const db = await openDb();
|
|
215
|
+
try {
|
|
216
|
+
const run = (sql) => {
|
|
217
|
+
try {
|
|
218
|
+
const stmt = db.prepare(sql);
|
|
219
|
+
const rows = [];
|
|
220
|
+
while (stmt.step()) rows.push(stmt.getAsObject());
|
|
221
|
+
stmt.free();
|
|
222
|
+
return rows;
|
|
223
|
+
} catch {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const escaped = nodeId.replace(/'/g, "''");
|
|
228
|
+
const parts = nodeId.split("::");
|
|
229
|
+
const shortName = parts.pop()?.replace(/'/g, "''") || "";
|
|
230
|
+
const filePath = parts[0] === "gotcha" && parts.length >= 2 ? parts[1]?.replace(/'/g, "''") : "";
|
|
231
|
+
const scope = parts[0] === "research" && parts.length >= 2 ? parts.slice(1).join("::").replace(/'/g, "''") : "";
|
|
232
|
+
const nodeRows = run(
|
|
233
|
+
`SELECT json_object('id',id,'name',name,'type',type,'file_path',COALESCE(file_path,''),'metadata',COALESCE(metadata,'{}'),'severity',COALESCE(severity,'medium'),'confidence',COALESCE(confidence,0.8),'last_verified_at',last_verified_at,'created_at',created_at,'updated_at',updated_at) FROM nodes WHERE id = '${escaped}'`
|
|
234
|
+
);
|
|
235
|
+
if (!nodeRows.length) return null;
|
|
236
|
+
const rawNode = nodeRows[0][Object.keys(nodeRows[0])[0]];
|
|
237
|
+
let node = null;
|
|
117
238
|
try {
|
|
118
|
-
|
|
239
|
+
node = typeof rawNode === "string" ? JSON.parse(rawNode) : rawNode;
|
|
119
240
|
} catch {
|
|
120
241
|
return null;
|
|
121
242
|
}
|
|
122
|
-
|
|
123
|
-
|
|
243
|
+
if (!node) return null;
|
|
244
|
+
const parseRelation = (rows, label) => rows.map((r) => {
|
|
245
|
+
const out = {
|
|
246
|
+
relation: r.relation,
|
|
247
|
+
weight: r.weight
|
|
248
|
+
};
|
|
249
|
+
if (label === "out") {
|
|
250
|
+
out.target = r.target;
|
|
251
|
+
out.target_name = r.target_name;
|
|
252
|
+
out.target_type = r.target_type;
|
|
253
|
+
} else {
|
|
254
|
+
out.source = r.source;
|
|
255
|
+
out.source_name = r.source_name;
|
|
256
|
+
out.source_type = r.source_type;
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
}).filter(Boolean);
|
|
260
|
+
const outgoing = parseRelation(
|
|
261
|
+
run(
|
|
262
|
+
`SELECT e.target_id as target, e.type as relation, e.weight, n.name as target_name, n.type as target_type
|
|
263
|
+
FROM edges e LEFT JOIN nodes n ON n.id = e.target_id
|
|
264
|
+
WHERE e.source_id = '${escaped}'`
|
|
265
|
+
),
|
|
266
|
+
"out"
|
|
267
|
+
);
|
|
268
|
+
const incoming = parseRelation(
|
|
269
|
+
run(
|
|
270
|
+
`SELECT e.source_id as source, e.type as relation, e.weight, n.name as source_name, n.type as source_type
|
|
271
|
+
FROM edges e LEFT JOIN nodes n ON n.id = e.source_id
|
|
272
|
+
WHERE e.target_id = '${escaped}'`
|
|
273
|
+
),
|
|
274
|
+
"in"
|
|
275
|
+
);
|
|
276
|
+
const gotchaFilter = filePath ? `file_path = '${filePath}'` : `file_path = '${escaped}' OR file_path LIKE '%${shortName}%'`;
|
|
277
|
+
const gotchaRows = run(
|
|
278
|
+
`SELECT json_object('id',id,'file_path',file_path,'description',REPLACE(REPLACE(description,char(10),' '),char(13),''),'severity',severity,'workaround',REPLACE(REPLACE(COALESCE(workaround,''),char(10),' '),char(13),''),'added_by',COALESCE(added_by,'agent'),'created_at',created_at) FROM known_gotchas WHERE ${gotchaFilter}`
|
|
279
|
+
);
|
|
280
|
+
const gotchas = gotchaRows.map((r) => {
|
|
281
|
+
const raw = r[Object.keys(r)[0]];
|
|
282
|
+
if (raw === null || raw === void 0) return null;
|
|
283
|
+
try {
|
|
284
|
+
return JSON.parse(String(raw));
|
|
285
|
+
} catch {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
}).filter(Boolean);
|
|
289
|
+
return { node, outgoing, incoming, gotchas };
|
|
290
|
+
} finally {
|
|
291
|
+
db.close();
|
|
292
|
+
}
|
|
124
293
|
}
|
|
125
294
|
|
|
126
295
|
// src/api.ts
|
|
127
|
-
import
|
|
296
|
+
import fs2 from "fs";
|
|
128
297
|
var api = new Hono();
|
|
129
|
-
|
|
298
|
+
var LOCALHOST_ORIGINS = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/i;
|
|
299
|
+
function isLocalhostOrigin(origin) {
|
|
300
|
+
if (!origin) return false;
|
|
301
|
+
return LOCALHOST_ORIGINS.test(origin);
|
|
302
|
+
}
|
|
303
|
+
api.use("/*", async (c, next) => {
|
|
304
|
+
const origin = c.req.header("origin");
|
|
305
|
+
if (origin && !isLocalhostOrigin(origin)) {
|
|
306
|
+
return c.json({ error: "Forbidden: cross-origin requests are not allowed." }, 403);
|
|
307
|
+
}
|
|
308
|
+
if (origin) {
|
|
309
|
+
c.header("Access-Control-Allow-Origin", origin);
|
|
310
|
+
c.header("Vary", "Origin");
|
|
311
|
+
c.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
312
|
+
c.header("Access-Control-Allow-Headers", "Content-Type");
|
|
313
|
+
}
|
|
314
|
+
await next();
|
|
315
|
+
});
|
|
316
|
+
function confirmGuard(c) {
|
|
317
|
+
const confirm = c.req.query("confirm");
|
|
318
|
+
if (confirm !== "1") {
|
|
319
|
+
return c.json({ ok: false, error: "Confirmation required: pass ?confirm=1" }, 400);
|
|
320
|
+
}
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
130
323
|
api.get("/status", (c) => {
|
|
131
324
|
const dbPath = findKumaDb();
|
|
132
325
|
let dbSize = "0 KB";
|
|
133
|
-
if (dbPath &&
|
|
134
|
-
const bytes =
|
|
326
|
+
if (dbPath && fs2.existsSync(dbPath)) {
|
|
327
|
+
const bytes = fs2.statSync(dbPath).size;
|
|
135
328
|
dbSize = bytes > 1024 * 1024 ? (bytes / (1024 * 1024)).toFixed(2) + " MB" : (bytes / 1024).toFixed(1) + " KB";
|
|
136
329
|
}
|
|
137
330
|
return c.json({
|
|
@@ -141,18 +334,18 @@ api.get("/status", (c) => {
|
|
|
141
334
|
project: dbPath ? dbPath.replace("/.kuma/kuma.db", "").split("/").pop() : null
|
|
142
335
|
});
|
|
143
336
|
});
|
|
144
|
-
api.get("/dashboard", (c) => {
|
|
337
|
+
api.get("/dashboard", async (c) => {
|
|
145
338
|
try {
|
|
146
|
-
const data = getDashboardData();
|
|
339
|
+
const data = await getDashboardData();
|
|
147
340
|
return c.json(data);
|
|
148
341
|
} catch (e) {
|
|
149
342
|
return c.json({ error: e.message }, 500);
|
|
150
343
|
}
|
|
151
344
|
});
|
|
152
|
-
api.get("/node/:id", (c) => {
|
|
345
|
+
api.get("/node/:id", async (c) => {
|
|
153
346
|
try {
|
|
154
347
|
const nodeId = c.req.param("id");
|
|
155
|
-
const detail = getNodeDetail(nodeId);
|
|
348
|
+
const detail = await getNodeDetail(nodeId);
|
|
156
349
|
if (!detail) return c.json({ error: "Node not found" }, 404);
|
|
157
350
|
return c.json(detail);
|
|
158
351
|
} catch (e) {
|
|
@@ -160,17 +353,21 @@ api.get("/node/:id", (c) => {
|
|
|
160
353
|
}
|
|
161
354
|
});
|
|
162
355
|
api.post("/reset-db", (c) => {
|
|
356
|
+
const blocked = confirmGuard(c);
|
|
357
|
+
if (blocked) return blocked;
|
|
163
358
|
try {
|
|
164
359
|
const dbPath = findKumaDb();
|
|
165
|
-
if (dbPath &&
|
|
166
|
-
|
|
360
|
+
if (dbPath && fs2.existsSync(dbPath)) {
|
|
361
|
+
fs2.unlinkSync(dbPath);
|
|
167
362
|
}
|
|
168
363
|
return c.json({ ok: true, message: "Kuma DB reset successfully" });
|
|
169
364
|
} catch (e) {
|
|
170
|
-
return c.json({ error: e.message }, 500);
|
|
365
|
+
return c.json({ ok: false, error: e.message }, 500);
|
|
171
366
|
}
|
|
172
367
|
});
|
|
173
368
|
api.post("/stop-server", (c) => {
|
|
369
|
+
const blocked = confirmGuard(c);
|
|
370
|
+
if (blocked) return blocked;
|
|
174
371
|
setTimeout(() => process.exit(0), 500);
|
|
175
372
|
return c.json({ ok: true, message: "Kuma Studio server shutting down" });
|
|
176
373
|
});
|
|
@@ -199,7 +396,7 @@ if (customDir) {
|
|
|
199
396
|
}
|
|
200
397
|
var app = new Hono2();
|
|
201
398
|
app.route("/api", api_default);
|
|
202
|
-
var htmlPath =
|
|
399
|
+
var htmlPath = join(__dirname, "..", "public", "index.html");
|
|
203
400
|
var htmlCache = null;
|
|
204
401
|
try {
|
|
205
402
|
htmlCache = readFileSync(htmlPath, "utf-8");
|