@plumpslabs/kuma 2.4.9 → 2.4.11

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.4.9",
3
+ "version": "2.4.11",
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",
@@ -27,6 +27,7 @@
27
27
  "@modelcontextprotocol/sdk": "^1.7.0",
28
28
  "fast-glob": "^3.3.2",
29
29
  "sql.js": "^1.14.1",
30
+ "typescript": "^5.9.3",
30
31
  "zod": "^3.24.0"
31
32
  },
32
33
  "optionalDependencies": {
@@ -40,8 +41,7 @@
40
41
  "jest-mock": "^30.4.1",
41
42
  "ts-jest": "^29.2.0",
42
43
  "ts-node": "^10.9.2",
43
- "tsup": "^8.3.0",
44
- "typescript": "^5.9.3"
44
+ "tsup": "^8.3.0"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=18.0.0"
@@ -182,11 +182,31 @@ async function getDashboardData() {
182
182
  const pkg = JSON.parse(fs.readFileSync(rootPkgPath, "utf-8"));
183
183
  const pnpmWs = path.join(root, "pnpm-workspace.yaml");
184
184
  const isWs = Boolean(pkg.workspaces || fs.existsSync(pnpmWs));
185
+ const packages = [];
186
+ if (isWs) {
187
+ for (const dirName of ["packages", "apps", "libs", "services", "crates"]) {
188
+ const dirPath = path.join(root, dirName);
189
+ if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
190
+ for (const sub of fs.readdirSync(dirPath)) {
191
+ const subPkg = path.join(dirPath, sub, "package.json");
192
+ if (fs.existsSync(subPkg)) {
193
+ try {
194
+ const sp = JSON.parse(fs.readFileSync(subPkg, "utf-8"));
195
+ packages.push({ name: sp.name || sub, path: `${dirName}/${sub}` });
196
+ } catch {
197
+ packages.push({ name: sub, path: `${dirName}/${sub}` });
198
+ }
199
+ }
200
+ }
201
+ }
202
+ }
203
+ }
185
204
  workspace = {
186
205
  isWorkspace: isWs,
187
206
  name: pkg.name || path.basename(root),
188
207
  version: pkg.version || "1.0.0",
189
- type: isWs ? fs.existsSync(pnpmWs) ? "pnpm" : "npm/yarn" : "single-package"
208
+ type: isWs ? fs.existsSync(pnpmWs) ? "pnpm" : "npm/yarn" : "single-package",
209
+ packages
190
210
  };
191
211
  }
192
212
  } catch {
@@ -323,7 +343,118 @@ async function getNodeDetail(nodeId) {
323
343
  return null;
324
344
  }
325
345
  }).filter(Boolean);
326
- return { node, outgoing, incoming, gotchas };
346
+ const inDegree = incoming.length;
347
+ const outDegree = outgoing.length;
348
+ const isHub = inDegree >= 4;
349
+ return { node, outgoing, incoming, gotchas, centrality: { inDegree, outDegree, isHub } };
350
+ } finally {
351
+ db.close();
352
+ }
353
+ }
354
+ function saveDbToDisk(db) {
355
+ const dbPath = findKumaDb();
356
+ if (!dbPath) throw new Error("No .kuma/kuma.db found");
357
+ const data = db.export();
358
+ const buffer = Buffer.from(data);
359
+ const tmpPath = `${dbPath}.tmp.${process.pid}.${Date.now()}`;
360
+ fs.writeFileSync(tmpPath, buffer);
361
+ try {
362
+ fs.renameSync(tmpPath, dbPath);
363
+ } catch {
364
+ fs.writeFileSync(dbPath, buffer);
365
+ try {
366
+ fs.unlinkSync(tmpPath);
367
+ } catch {
368
+ }
369
+ }
370
+ }
371
+ async function upsertStudioNode(data) {
372
+ const db = await openDb();
373
+ try {
374
+ const type = data.type || "feature_domain";
375
+ const name = (data.name || "").trim();
376
+ const filePath = (data.file_path || "").trim();
377
+ let id = data.id;
378
+ if (!id) {
379
+ if (type === "file" && filePath) {
380
+ id = `file::${filePath}`;
381
+ } else if (type === "function" && filePath) {
382
+ id = `function::${filePath}::${name}`;
383
+ } else {
384
+ id = `${type}::${name}`;
385
+ }
386
+ }
387
+ const meta = data.metadata || {};
388
+ if (data.description) {
389
+ meta.description = data.description;
390
+ }
391
+ const metaStr = JSON.stringify(meta);
392
+ db.run(
393
+ `INSERT INTO nodes (id, type, name, file_path, metadata, severity, confidence, updated_at)
394
+ VALUES (?, ?, ?, ?, ?, 'medium', 0.8, strftime('%s','now'))
395
+ ON CONFLICT(id) DO UPDATE SET
396
+ type = excluded.type,
397
+ name = excluded.name,
398
+ file_path = COALESCE(excluded.file_path, nodes.file_path),
399
+ metadata = excluded.metadata,
400
+ updated_at = strftime('%s','now')`,
401
+ [id, type, name, filePath || null, metaStr]
402
+ );
403
+ try {
404
+ const terms = `${name} ${filePath} ${metaStr}`.toLowerCase().split(/[\s,.;:!?()\[\]{}"'\/\\|@#$%^&*+=<>~`_-]+/).filter((t) => t.length > 2 && t.length < 40);
405
+ const uniqueTerms = Array.from(new Set(terms));
406
+ db.run(`DELETE FROM node_tokens WHERE node_id = ?`, [id]);
407
+ for (const term of uniqueTerms.slice(0, 30)) {
408
+ db.run(`INSERT OR IGNORE INTO node_tokens (token, node_id) VALUES (?, ?)`, [term, id]);
409
+ }
410
+ } catch {
411
+ }
412
+ saveDbToDisk(db);
413
+ return { id, type, name, file_path: filePath, metadata: meta };
414
+ } finally {
415
+ db.close();
416
+ }
417
+ }
418
+ async function deleteStudioNode(id) {
419
+ const db = await openDb();
420
+ try {
421
+ db.run(`DELETE FROM nodes WHERE id = ?`, [id]);
422
+ db.run(`DELETE FROM edges WHERE source_id = ? OR target_id = ?`, [id, id]);
423
+ try {
424
+ db.run(`DELETE FROM node_tokens WHERE node_id = ?`, [id]);
425
+ } catch {
426
+ }
427
+ saveDbToDisk(db);
428
+ } finally {
429
+ db.close();
430
+ }
431
+ }
432
+ async function createStudioEdge(edge) {
433
+ const db = await openDb();
434
+ try {
435
+ const weight = edge.weight ?? 1;
436
+ const metadata = JSON.stringify(edge.metadata ?? {});
437
+ db.run(
438
+ `INSERT INTO edges (source_id, target_id, type, weight, metadata)
439
+ VALUES (?, ?, ?, ?, ?)
440
+ ON CONFLICT(source_id, target_id, type) DO UPDATE SET
441
+ weight = excluded.weight,
442
+ metadata = excluded.metadata`,
443
+ [edge.source, edge.target, edge.type, weight, metadata]
444
+ );
445
+ saveDbToDisk(db);
446
+ } finally {
447
+ db.close();
448
+ }
449
+ }
450
+ async function deleteStudioEdge(source, target, type) {
451
+ const db = await openDb();
452
+ try {
453
+ db.run(
454
+ `DELETE FROM edges WHERE source_id = ? AND target_id = ? AND type = ?`,
455
+ [source, target, type]
456
+ );
457
+ saveDbToDisk(db);
327
458
  } finally {
328
459
  db.close();
329
460
  }
@@ -389,6 +520,52 @@ api.get("/node/:id", async (c) => {
389
520
  return c.json({ error: e.message }, 500);
390
521
  }
391
522
  });
523
+ api.post("/node", async (c) => {
524
+ try {
525
+ const body = await c.req.json();
526
+ if (!body || !body.name || !body.type) {
527
+ return c.json({ error: "Missing required fields: name and type are required" }, 400);
528
+ }
529
+ const node = await upsertStudioNode(body);
530
+ return c.json({ ok: true, node });
531
+ } catch (e) {
532
+ return c.json({ error: e.message }, 500);
533
+ }
534
+ });
535
+ api.delete("/node/:id", async (c) => {
536
+ try {
537
+ const id = c.req.param("id");
538
+ if (!id) return c.json({ error: "Node id required" }, 400);
539
+ await deleteStudioNode(id);
540
+ return c.json({ ok: true, id });
541
+ } catch (e) {
542
+ return c.json({ error: e.message }, 500);
543
+ }
544
+ });
545
+ api.post("/edge", async (c) => {
546
+ try {
547
+ const body = await c.req.json();
548
+ if (!body || !body.source || !body.target || !body.type) {
549
+ return c.json({ error: "Missing required fields: source, target, and type are required" }, 400);
550
+ }
551
+ await createStudioEdge(body);
552
+ return c.json({ ok: true, edge: body });
553
+ } catch (e) {
554
+ return c.json({ error: e.message }, 500);
555
+ }
556
+ });
557
+ api.delete("/edge", async (c) => {
558
+ try {
559
+ const body = await c.req.json();
560
+ if (!body || !body.source || !body.target || !body.type) {
561
+ return c.json({ error: "Missing required fields: source, target, and type are required" }, 400);
562
+ }
563
+ await deleteStudioEdge(body.source, body.target, body.type);
564
+ return c.json({ ok: true, edge: body });
565
+ } catch (e) {
566
+ return c.json({ error: e.message }, 500);
567
+ }
568
+ });
392
569
  api.post("/reset-db", (c) => {
393
570
  const blocked = confirmGuard(c);
394
571
  if (blocked) return blocked;
@@ -440,10 +617,13 @@ try {
440
617
  } catch {
441
618
  }
442
619
  app.get("/", (c) => {
443
- if (!htmlCache) {
620
+ try {
621
+ const html = readFileSync(htmlPath, "utf-8");
622
+ return c.html(html);
623
+ } catch {
624
+ if (htmlCache) return c.html(htmlCache);
444
625
  return c.text("index.html not found. Run from packages/ide/studio/", 500);
445
626
  }
446
- return c.html(htmlCache);
447
627
  });
448
628
  serve({ fetch: app.fetch, port: PORT }, (info) => {
449
629
  const dbPath = findKumaDb();