@plumpslabs/kuma 2.3.30 → 2.3.32

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