@plumpslabs/kuma 2.3.8 → 2.3.10
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/{agentDetector-G4RBMZ6X.js → agentDetector-3OTNKKBS.js} +4 -4
- package/dist/{chunk-NAM7SCBT.js → chunk-3TX6Q37T.js} +26 -1
- package/dist/{chunk-L5WU2HTN.js → chunk-J2CBDLXB.js} +46 -5
- package/dist/{chunk-RTGLQDMI.js → chunk-OJALFQ4H.js} +2 -2
- package/dist/{chunk-53J56QCQ.js → chunk-Q2444HWO.js} +1 -1
- package/dist/{chunk-SLRRDKQ2.js → chunk-R6B3VUSB.js} +2 -2
- package/dist/{chunk-PTEPJK6M.js → chunk-RI3DKY62.js} +1 -1
- package/dist/{chunk-FL2TLLMX.js → chunk-SPHI2BOO.js} +1 -1
- package/dist/{chunk-4OB3XMHT.js → chunk-ZKDYTYMS.js} +108 -2
- package/dist/{contextDigest-4PCK3DSM.js → contextDigest-5JXLH56Z.js} +3 -3
- package/dist/{domainRules-LDZPOIGZ.js → domainRules-ED6EOFVY.js} +2 -2
- package/dist/index.js +259 -71
- package/dist/{init-AJAESUZZ.js → init-LLP6JDFR.js} +1 -1
- package/dist/kumaCheckpoint-XIQWRMGV.js +207 -0
- package/dist/kumaCodeScanner-LFJGPJNH.js +566 -0
- package/dist/kumaContractEngine-KX27T4N7.js +305 -0
- package/dist/{kumaDb-6D53ERFP.js → kumaDb-IC7UX7PU.js} +3 -1
- package/dist/{kumaDriftDetector-OESI5GTC.js → kumaDriftDetector-QBY6OJOO.js} +1 -1
- package/dist/{kumaGotchas-DU5H6N7X.js → kumaGotchas-5HODT5RI.js} +3 -3
- package/dist/{kumaGraph-AWP743TJ.js → kumaGraph-GUQM75WL.js} +8 -2
- package/dist/{kumaMemory-7TCUDVZX.js → kumaMemory-FMOWIODH.js} +2 -2
- package/dist/{kumaMiner-FFXSRHAO.js → kumaMiner-3RXEOCBP.js} +3 -3
- package/dist/{kumaPolicyEngine-S2PXN3C7.js → kumaPolicyEngine-ORBWL5PT.js} +2 -2
- package/dist/kumaProgressiveContext-CVSNPY3W.js +231 -0
- package/dist/{kumaSearch-QGB4QVXS.js → kumaSearch-B5WVYHHD.js} +1 -1
- package/dist/kumaTrajectory-T6DXGOG4.js +460 -0
- package/dist/{kumaVerifier-ARSGPZAM.js → kumaVerifier-KAYJHR5K.js} +2 -2
- package/dist/{kumaVisualize-TECABHUY.js → kumaVisualize-AFL7STLL.js} +1 -1
- package/dist/{safetyAudit-55IVCG5B.js → safetyAudit-32WSA4Z5.js} +2 -2
- package/dist/{safetyScore-GR5N55CU.js → safetyScore-WU27EOG4.js} +5 -5
- package/dist/{sessionMemory-CA34TCPF.js → sessionMemory-4UWA3E5J.js} +1 -1
- package/dist/{skillGenerator-F6YO4H5D.js → skillGenerator-ILZFWO5O.js} +6 -8
- package/package.json +18 -13
- package/packages/ide/studio/dist/index.js +140 -0
- package/packages/ide/studio/public/index.html +500 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { serve } from "@hono/node-server";
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { join as join2, dirname } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { spawn } from "child_process";
|
|
9
|
+
|
|
10
|
+
// src/api.ts
|
|
11
|
+
import { Hono } from "hono";
|
|
12
|
+
import { cors } from "hono/cors";
|
|
13
|
+
|
|
14
|
+
// src/db.ts
|
|
15
|
+
import { execSync } from "child_process";
|
|
16
|
+
import { existsSync } from "fs";
|
|
17
|
+
import { join, resolve } from "path";
|
|
18
|
+
function findKumaDb(startDir) {
|
|
19
|
+
let current = startDir ? resolve(startDir) : process.cwd();
|
|
20
|
+
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, "..");
|
|
24
|
+
if (parent === current) break;
|
|
25
|
+
current = parent;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function query(sql) {
|
|
30
|
+
const dbPath = findKumaDb();
|
|
31
|
+
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();
|
|
37
|
+
}
|
|
38
|
+
function queryJson(sql) {
|
|
39
|
+
const raw = query(sql);
|
|
40
|
+
if (!raw) return [];
|
|
41
|
+
return JSON.parse(raw);
|
|
42
|
+
}
|
|
43
|
+
function getDashboardData() {
|
|
44
|
+
return {
|
|
45
|
+
stats: queryJson(`
|
|
46
|
+
SELECT json_object(
|
|
47
|
+
'node_count', (SELECT COUNT(*) FROM nodes),
|
|
48
|
+
'edge_count', (SELECT COUNT(*) FROM edges),
|
|
49
|
+
'gotcha_count', (SELECT COUNT(*) FROM known_gotchas),
|
|
50
|
+
'trajectory_count', (SELECT COUNT(*) FROM trajectories),
|
|
51
|
+
'skill_count', (SELECT COUNT(*) FROM distilled_skills),
|
|
52
|
+
'health_score', COALESCE((SELECT score FROM health_snapshots ORDER BY created_at DESC LIMIT 1), 0)
|
|
53
|
+
)
|
|
54
|
+
`),
|
|
55
|
+
nodes: queryJson(
|
|
56
|
+
`SELECT json_group_array(json_object('id',id,'name',name,'type',type,'file_path',file_path)) FROM nodes ORDER BY created_at DESC`
|
|
57
|
+
),
|
|
58
|
+
edges: queryJson(
|
|
59
|
+
`SELECT json_group_array(json_object('source',source_id,'target',target_id,'relation',type)) FROM edges`
|
|
60
|
+
),
|
|
61
|
+
gotchas: queryJson(
|
|
62
|
+
`SELECT json_group_array(json_object('id',id,'file_path',file_path,'description',description,'severity',severity,'workaround',workaround)) FROM known_gotchas ORDER BY severity DESC`
|
|
63
|
+
),
|
|
64
|
+
trajectories: queryJson(
|
|
65
|
+
`SELECT json_group_array(json_object('id',id,'goal',goal,'total_duration_ms',total_duration_ms,'success_rate',success_rate,'created_at',created_at)) FROM trajectories ORDER BY created_at DESC LIMIT 20`
|
|
66
|
+
),
|
|
67
|
+
health: queryJson(
|
|
68
|
+
`SELECT json_group_array(json_object('score',score,'summary',summary,'risk_level',risk_level,'created_at',created_at)) FROM health_snapshots ORDER BY created_at DESC LIMIT 10`
|
|
69
|
+
)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/api.ts
|
|
74
|
+
var api = new Hono();
|
|
75
|
+
api.use("/*", cors());
|
|
76
|
+
api.get("/status", (c) => {
|
|
77
|
+
const dbPath = findKumaDb();
|
|
78
|
+
return c.json({
|
|
79
|
+
ok: !!dbPath,
|
|
80
|
+
dbPath,
|
|
81
|
+
project: dbPath ? dbPath.replace("/.kuma/kuma.db", "").split("/").pop() : null
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
api.get("/dashboard", (c) => {
|
|
85
|
+
try {
|
|
86
|
+
const data = getDashboardData();
|
|
87
|
+
return c.json(data);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
return c.json({ error: e.message }, 500);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
var api_default = api;
|
|
93
|
+
|
|
94
|
+
// src/index.ts
|
|
95
|
+
import { Hono as Hono2 } from "hono";
|
|
96
|
+
var args = process.argv.slice(2);
|
|
97
|
+
var PORT = parseInt(args.find((a) => a.startsWith("--port="))?.split("=")[1] || "3322", 10);
|
|
98
|
+
var customDir = args.find((a) => a.startsWith("--dir="))?.split("=")[1];
|
|
99
|
+
var showHelp = args.includes("--help") || args.includes("-h");
|
|
100
|
+
if (showHelp) {
|
|
101
|
+
console.error(`\u2662 Kuma Studio \u2014 Usage:
|
|
102
|
+
bun run src/index.ts [options]
|
|
103
|
+
|
|
104
|
+
Options:
|
|
105
|
+
--port=PORT Server port (default: 3322)
|
|
106
|
+
--dir=PATH Project directory (default: auto-detect from cwd)
|
|
107
|
+
--help, -h Show this help
|
|
108
|
+
`);
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
112
|
+
if (customDir) {
|
|
113
|
+
process.chdir(customDir);
|
|
114
|
+
}
|
|
115
|
+
var app = new Hono2();
|
|
116
|
+
app.route("/api", api_default);
|
|
117
|
+
var htmlPath = join2(__dirname, "..", "public", "index.html");
|
|
118
|
+
var htmlCache = null;
|
|
119
|
+
try {
|
|
120
|
+
htmlCache = readFileSync(htmlPath, "utf-8");
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
app.get("/", (c) => {
|
|
124
|
+
if (!htmlCache) {
|
|
125
|
+
return c.text("index.html not found. Run from packages/ide/studio/", 500);
|
|
126
|
+
}
|
|
127
|
+
return c.html(htmlCache);
|
|
128
|
+
});
|
|
129
|
+
serve({ fetch: app.fetch, port: PORT }, (info) => {
|
|
130
|
+
const dbPath = findKumaDb();
|
|
131
|
+
const projectName = dbPath ? dbPath.replace("/.kuma/kuma.db", "").split("/").pop() : "unknown";
|
|
132
|
+
console.error(`\u2662 Kuma Studio`);
|
|
133
|
+
console.error(` Project: ${projectName}`);
|
|
134
|
+
console.error(` DB: ${dbPath || "not found"}`);
|
|
135
|
+
console.error(` Server: http://localhost:${PORT}`);
|
|
136
|
+
console.error(``);
|
|
137
|
+
const url = `http://localhost:${PORT}`;
|
|
138
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
139
|
+
spawn(cmd, [url], { stdio: "ignore", detached: true });
|
|
140
|
+
});
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Kuma Studio</title>
|
|
7
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>♢</text></svg>">
|
|
8
|
+
<script src="https://unpkg.com/vis-network@9.1.9/dist/vis-network.min.js"></script>
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #1a1a2e; --surface: #16213e; --border: #2a2a4a;
|
|
12
|
+
--text: #e0e0e0; --muted: #888; --accent: #7c3aed; --accent2: #06b6d4;
|
|
13
|
+
--green: #22c55e; --yellow: #eab308; --orange: #f97316; --red: #ef4444;
|
|
14
|
+
}
|
|
15
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
16
|
+
body {
|
|
17
|
+
font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
|
|
18
|
+
background: var(--bg); color: var(--text); height: 100vh; overflow: hidden;
|
|
19
|
+
}
|
|
20
|
+
header {
|
|
21
|
+
display: flex; align-items: center; gap: 16px; padding: 12px 24px;
|
|
22
|
+
background: var(--surface); border-bottom: 1px solid var(--border);
|
|
23
|
+
-webkit-app-region: drag;
|
|
24
|
+
}
|
|
25
|
+
header h1 { font-size: 18px; font-weight: 700; color: var(--accent); letter-spacing: -0.5px; }
|
|
26
|
+
header .status { font-size: 12px; color: var(--muted); }
|
|
27
|
+
header .status .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
|
|
28
|
+
header .status .dot.on { background: var(--green); }
|
|
29
|
+
header .status .dot.off { background: var(--red); }
|
|
30
|
+
.stats-bar {
|
|
31
|
+
display: flex; gap: 8px; padding: 8px 24px; background: var(--surface);
|
|
32
|
+
border-bottom: 1px solid var(--border); font-size: 12px;
|
|
33
|
+
overflow-x: auto;
|
|
34
|
+
}
|
|
35
|
+
.stat { display: flex; align-items: center; gap: 4px; padding: 4px 10px; border-radius: 6px; background: var(--bg); white-space: nowrap; }
|
|
36
|
+
.stat .num { font-weight: 700; color: var(--accent2); }
|
|
37
|
+
.stat .lbl { color: var(--muted); }
|
|
38
|
+
#main { display: flex; height: calc(100vh - 100px); }
|
|
39
|
+
#graph-panel { flex: 1; position: relative; min-width: 0; overflow: hidden; }
|
|
40
|
+
#graph-container { width: 100%; height: 100%; background: radial-gradient(ellipse at center, #1e1e3a 0%, var(--bg) 70%); }
|
|
41
|
+
#sidebar { width: 340px; min-width: 340px; background: var(--surface); border-left: 1px solid var(--border); display: flex; flex-direction: column; }
|
|
42
|
+
.tabs { display: flex; border-bottom: 1px solid var(--border); }
|
|
43
|
+
.tab {
|
|
44
|
+
flex: 1; padding: 10px; text-align: center; cursor: pointer; font-size: 12px;
|
|
45
|
+
color: var(--muted); border-bottom: 2px solid transparent; transition: all 0.2s;
|
|
46
|
+
}
|
|
47
|
+
.tab:hover { color: var(--text); }
|
|
48
|
+
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
49
|
+
.tab-content { flex: 1; overflow-y: auto; padding: 12px; }
|
|
50
|
+
.panel { display: none; }
|
|
51
|
+
.panel.active { display: block; }
|
|
52
|
+
.gotcha-item {
|
|
53
|
+
padding: 10px 12px; margin-bottom: 8px; border-radius: 8px;
|
|
54
|
+
background: var(--bg); border-left: 3px solid var(--muted);
|
|
55
|
+
}
|
|
56
|
+
.gotcha-item .file { font-size: 11px; color: var(--accent2); margin-bottom: 4px; word-break: break-all; }
|
|
57
|
+
.gotcha-item .desc { font-size: 13px; margin-bottom: 4px; }
|
|
58
|
+
.gotcha-item .sev-tag { display: inline-block; font-size: 10px; padding: 2px 6px; border-radius: 4px; font-weight: 600; }
|
|
59
|
+
.sev-critical { background: #ef444420; color: var(--red); border-color: var(--red); }
|
|
60
|
+
.sev-high { background: #f9731620; color: var(--orange); border-color: var(--orange); }
|
|
61
|
+
.sev-medium { background: #eab30820; color: var(--yellow); border-color: var(--yellow); }
|
|
62
|
+
.sev-low { background: #22c55e20; color: var(--green); border-color: var(--green); }
|
|
63
|
+
.traj-item { padding: 8px 12px; margin-bottom: 6px; background: var(--bg); border-radius: 6px; }
|
|
64
|
+
.traj-item .goal { font-size: 13px; font-weight: 600; word-break: break-all; }
|
|
65
|
+
.traj-item .meta { font-size: 11px; color: var(--muted); margin-top: 2px; }
|
|
66
|
+
.health-item { padding: 8px 12px; margin-bottom: 6px; background: var(--bg); border-radius: 6px; }
|
|
67
|
+
.health-item .score { font-size: 20px; font-weight: 700; }
|
|
68
|
+
.health-item .score.good { color: var(--green); }
|
|
69
|
+
.health-item .score.ok { color: var(--yellow); }
|
|
70
|
+
.health-item .score.bad { color: var(--red); }
|
|
71
|
+
.health-item .meta { font-size: 11px; color: var(--muted); }
|
|
72
|
+
.empty { text-align: center; color: var(--muted); padding: 40px 20px; font-size: 14px; }
|
|
73
|
+
#legend {
|
|
74
|
+
position: absolute; bottom: 16px; left: 16px; background: rgba(22,33,62,0.92);
|
|
75
|
+
border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px;
|
|
76
|
+
font-size: 11px; z-index: 10; max-height: 200px; overflow-y: auto;
|
|
77
|
+
backdrop-filter: blur(8px);
|
|
78
|
+
}
|
|
79
|
+
#legend .title { font-weight: 600; margin-bottom: 6px; color: var(--muted); }
|
|
80
|
+
.legend-item { display: flex; align-items: center; gap: 6px; margin-bottom: 3px; }
|
|
81
|
+
.legend-dot { width: 10px; height: 10px; border-radius: 50%; }
|
|
82
|
+
#graph-tooltip {
|
|
83
|
+
position: absolute; display: none; background: rgba(22,33,62,0.95);
|
|
84
|
+
border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px;
|
|
85
|
+
font-size: 12px; z-index: 20; max-width: 320px;
|
|
86
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.5); backdrop-filter: blur(8px);
|
|
87
|
+
pointer-events: none;
|
|
88
|
+
}
|
|
89
|
+
#search-box {
|
|
90
|
+
position: absolute; top: 16px; right: 16px; z-index: 10;
|
|
91
|
+
}
|
|
92
|
+
#search-box input {
|
|
93
|
+
background: rgba(22,33,62,0.92); border: 1px solid var(--border);
|
|
94
|
+
border-radius: 8px; padding: 8px 12px; color: var(--text); font-size: 12px;
|
|
95
|
+
width: 200px; outline: none; backdrop-filter: blur(8px);
|
|
96
|
+
transition: border-color 0.2s;
|
|
97
|
+
}
|
|
98
|
+
#search-box input:focus { border-color: var(--accent); }
|
|
99
|
+
#search-box input::placeholder { color: var(--muted); }
|
|
100
|
+
#graph-controls {
|
|
101
|
+
position: absolute; bottom: 16px; right: 16px; z-index: 10;
|
|
102
|
+
display: flex; gap: 6px;
|
|
103
|
+
}
|
|
104
|
+
#graph-controls button {
|
|
105
|
+
background: rgba(22,33,62,0.92); border: 1px solid var(--border);
|
|
106
|
+
border-radius: 6px; color: var(--text); padding: 6px 10px;
|
|
107
|
+
font-size: 11px; cursor: pointer; backdrop-filter: blur(8px);
|
|
108
|
+
transition: background 0.2s;
|
|
109
|
+
}
|
|
110
|
+
#graph-controls button:hover { background: var(--accent); border-color: var(--accent); }
|
|
111
|
+
#loading {
|
|
112
|
+
position: fixed; inset: 0; background: var(--bg); display: flex;
|
|
113
|
+
align-items: center; justify-content: center; z-index: 100;
|
|
114
|
+
font-size: 18px; color: var(--accent); flex-direction: column; gap: 16px;
|
|
115
|
+
}
|
|
116
|
+
#loading .spinner { width: 32px; height: 32px; border: 3px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; }
|
|
117
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
118
|
+
::-webkit-scrollbar { width: 6px; }
|
|
119
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
120
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
121
|
+
</style>
|
|
122
|
+
</head>
|
|
123
|
+
<body>
|
|
124
|
+
<div id="loading"><div class="spinner"></div>Connecting to Kuma...</div>
|
|
125
|
+
<header>
|
|
126
|
+
<h1>♢ Kuma Studio</h1>
|
|
127
|
+
<div class="status"><span class="dot" id="status-dot"></span><span id="status-text">connecting...</span></div>
|
|
128
|
+
<button onclick="refetch()" id="refetch-btn" style="margin-left:auto;background:rgba(124,58,237,0.15);border:1px solid rgba(124,58,237,0.3);border-radius:6px;color:var(--accent);padding:5px 12px;font-size:11px;cursor:pointer;transition:all 0.2s;-webkit-app-region:no-drag" onmouseover="this.style.background='rgba(124,58,237,0.3)'" onmouseout="this.style.background='rgba(124,58,237,0.15)'">↻ Refetch</button>
|
|
129
|
+
</header>
|
|
130
|
+
<div class="stats-bar" id="stats-bar"></div>
|
|
131
|
+
<div id="main">
|
|
132
|
+
<div id="graph-panel">
|
|
133
|
+
<div id="graph-container"></div>
|
|
134
|
+
<div id="search-box"><input id="node-search" type="text" placeholder="Search nodes..." /></div>
|
|
135
|
+
<div id="graph-controls">
|
|
136
|
+
<button onclick="if(network)network.fit()">Fit</button>
|
|
137
|
+
<button onclick="if(network)network.startSimulation()">Restart</button>
|
|
138
|
+
<button onclick="if(network)network.stopSimulation()">Stop</button>
|
|
139
|
+
</div>
|
|
140
|
+
<div id="graph-tooltip"></div>
|
|
141
|
+
<div id="legend"></div>
|
|
142
|
+
</div>
|
|
143
|
+
<div id="sidebar">
|
|
144
|
+
<div class="tabs">
|
|
145
|
+
<div class="tab active" data-tab="gotchas">⚠ Gotchas</div>
|
|
146
|
+
<div class="tab" data-tab="trajectories">↗ Trajectories</div>
|
|
147
|
+
<div class="tab" data-tab="health">♥ Health</div>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="tab-content" id="tab-content"></div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<script>
|
|
154
|
+
const API = "/api";
|
|
155
|
+
let network = null, nodesData = null, allNodes = null, allEdges = null;
|
|
156
|
+
|
|
157
|
+
async function init() {
|
|
158
|
+
var status = await fetch(API+"/status").then(function(r){return r.json()});
|
|
159
|
+
var dot = document.getElementById("status-dot");
|
|
160
|
+
var txt = document.getElementById("status-text");
|
|
161
|
+
if (status.ok) {
|
|
162
|
+
dot.className = "dot on";
|
|
163
|
+
txt.textContent = status.project || "connected";
|
|
164
|
+
} else {
|
|
165
|
+
dot.className = "dot off";
|
|
166
|
+
txt.textContent = "no kuma.db found";
|
|
167
|
+
}
|
|
168
|
+
document.getElementById("loading").style.display = "none";
|
|
169
|
+
var data = await fetch(API+"/dashboard").then(function(r){return r.json()});
|
|
170
|
+
if (data.error) return showError(data.error);
|
|
171
|
+
setupTabs();
|
|
172
|
+
renderStats(data);
|
|
173
|
+
renderGraph(data);
|
|
174
|
+
renderGotchas(data);
|
|
175
|
+
renderTrajectories(data);
|
|
176
|
+
renderHealth(data);
|
|
177
|
+
setupSearch();
|
|
178
|
+
}
|
|
179
|
+
init();
|
|
180
|
+
|
|
181
|
+
function showError(msg) {
|
|
182
|
+
document.getElementById("graph-container").innerHTML = '<div class="empty">⚠ '+msg+'</div>';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function renderStats(data) {
|
|
186
|
+
var s = data.stats || {};
|
|
187
|
+
var hc = s.health_score;
|
|
188
|
+
var hcColor = hc >= 80 ? "var(--green)" : hc >= 50 ? "var(--yellow)" : "var(--red)";
|
|
189
|
+
document.getElementById("stats-bar").innerHTML =
|
|
190
|
+
'<div class="stat"><span class="num">'+(s.node_count||0)+'</span><span class="lbl">nodes</span></div>'+
|
|
191
|
+
'<div class="stat"><span class="num">'+(s.edge_count||0)+'</span><span class="lbl">edges</span></div>'+
|
|
192
|
+
'<div class="stat"><span class="num">'+(s.gotcha_count||0)+'</span><span class="lbl">gotchas</span></div>'+
|
|
193
|
+
'<div class="stat"><span class="num">'+(s.trajectory_count||0)+'</span><span class="lbl">trajectories</span></div>'+
|
|
194
|
+
'<div class="stat"><span class="num" style="color:'+hcColor+'">'+(hc||0)+'</span><span class="lbl">health</span></div>';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Truncate long labels for display */
|
|
198
|
+
function truncateLabel(name, maxLen) {
|
|
199
|
+
if (!name) return "";
|
|
200
|
+
if (name.length <= maxLen) return name;
|
|
201
|
+
return name.substring(0, maxLen-2) + "..";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Calculate node size based on label length and type */
|
|
205
|
+
function calcNodeSize(name, type) {
|
|
206
|
+
var base = 14;
|
|
207
|
+
if (type === "function") base = 16;
|
|
208
|
+
if (type === "class") base = 18;
|
|
209
|
+
if (type === "module") base = 12;
|
|
210
|
+
if (type === "component") base = 16;
|
|
211
|
+
if (type === "route") base = 14;
|
|
212
|
+
if (type === "test") base = 13;
|
|
213
|
+
// Gentle scaling for long labels
|
|
214
|
+
var len = name ? Math.min(name.length, 20) : 4;
|
|
215
|
+
return Math.min(28, Math.max(base, base + len * 0.4));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
function renderGraph(data) {
|
|
220
|
+
allNodes = data.nodes || [];
|
|
221
|
+
allEdges = data.edges || [];
|
|
222
|
+
if (!allNodes.length) {
|
|
223
|
+
document.getElementById("graph-container").innerHTML = '<div class="empty">Knowledge Graph is empty</div>';
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
var types = [...new Set(allNodes.map(function(n){return n.type||'unknown'}))];
|
|
228
|
+
var colors = {function:'#7c3aed',class:'#06b6d4',module:'#22c55e',
|
|
229
|
+
component:'#f97316',file:'#eab308',route:'#ec4899',test:'#14b8a6'};
|
|
230
|
+
var shapes = {function:'box',class:'hexagon',module:'ellipse',
|
|
231
|
+
component:'diamond',file:'square',route:'triangle',test:'star'};
|
|
232
|
+
|
|
233
|
+
// Limit nodes for performance if > 200
|
|
234
|
+
var maxNodes = 300;
|
|
235
|
+
var displayNodes = allNodes;
|
|
236
|
+
var truncated = allNodes.length > maxNodes;
|
|
237
|
+
if (truncated) {
|
|
238
|
+
displayNodes = allNodes.slice(0, maxNodes);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
var visNodes = new vis.DataSet(displayNodes.map(function(n){
|
|
242
|
+
var label = truncateLabel(n.name, 28);
|
|
243
|
+
var size = calcNodeSize(n.name, n.type);
|
|
244
|
+
var color = colors[n.type] || '#888';
|
|
245
|
+
return {
|
|
246
|
+
id: n.id,
|
|
247
|
+
label: label,
|
|
248
|
+
title: '<b>' + n.name + '</b> <span style="color:#888">' + n.type + '</span>' + (n.file_path ? '<br><span style="color:#06b6d4;font-size:11px">' + n.file_path + '</span>' : ''),
|
|
249
|
+
color: { background: color, border: color, highlight: { background: color, border: '#fff' } },
|
|
250
|
+
shape: shapes[n.type] || 'dot',
|
|
251
|
+
font: { color: '#e0e0e0', size: Math.min(14, Math.max(10, size * 0.5)), face: 'monospace' },
|
|
252
|
+
borderWidth: 0,
|
|
253
|
+
size: size,
|
|
254
|
+
mass: 1 + (size / 40),
|
|
255
|
+
};
|
|
256
|
+
}));
|
|
257
|
+
|
|
258
|
+
var nodeIds = new Set(displayNodes.map(function(n){return n.id}));
|
|
259
|
+
var displayEdges = allEdges.filter(function(e){
|
|
260
|
+
return nodeIds.has(e.source) && nodeIds.has(e.target);
|
|
261
|
+
}).slice(0, 500);
|
|
262
|
+
|
|
263
|
+
var visEdges = new vis.DataSet(displayEdges.map(function(e){
|
|
264
|
+
return {
|
|
265
|
+
from: e.source, to: e.target,
|
|
266
|
+
label: e.relation || '',
|
|
267
|
+
arrows: { to: { enabled: true, scaleFactor: 0.6, type: 'arrow' } },
|
|
268
|
+
color: { color: '#4a4a6a', highlight: '#7c3aed', hover: '#7c3aed', opacity: 0.5 },
|
|
269
|
+
font: { size: 10, color: '#666', strokeWidth: 0, background: 'transparent' },
|
|
270
|
+
smooth: { type: 'continuous', roundness: 0.3 },
|
|
271
|
+
width: 1.5,
|
|
272
|
+
hoverWidth: 2.5,
|
|
273
|
+
selectionWidth: 2.5,
|
|
274
|
+
};
|
|
275
|
+
}));
|
|
276
|
+
|
|
277
|
+
var container = document.getElementById("graph-container");
|
|
278
|
+
network = new vis.Network(container, {nodes: visNodes, edges: visEdges}, {
|
|
279
|
+
physics: {
|
|
280
|
+
enabled: true,
|
|
281
|
+
solver: 'barnesHut',
|
|
282
|
+
barnesHut: {
|
|
283
|
+
gravitationalConstant: -2000,
|
|
284
|
+
centralGravity: 0.3,
|
|
285
|
+
springLength: 150,
|
|
286
|
+
springConstant: 0.04,
|
|
287
|
+
damping: 0.6,
|
|
288
|
+
avoidOverlap: 0.5,
|
|
289
|
+
},
|
|
290
|
+
stabilization: { iterations: 400, updateInterval: 25, onlyDynamicEdges: false },
|
|
291
|
+
adaptiveTimestep: true,
|
|
292
|
+
},
|
|
293
|
+
interaction: {
|
|
294
|
+
hover: true,
|
|
295
|
+
tooltipDelay: 300,
|
|
296
|
+
zoomView: true,
|
|
297
|
+
dragView: true,
|
|
298
|
+
dragNodes: true,
|
|
299
|
+
keyboard: true,
|
|
300
|
+
multiselect: true,
|
|
301
|
+
selectable: true,
|
|
302
|
+
selectConnectedEdges: false,
|
|
303
|
+
},
|
|
304
|
+
nodes: {
|
|
305
|
+
borderWidth: 0,
|
|
306
|
+
font: { color: '#e0e0e0', size: 12, face: 'monospace' },
|
|
307
|
+
margin: { top: 4, bottom: 4, left: 6, right: 6 },
|
|
308
|
+
shadow: { enabled: true, color: 'rgba(124,58,237,0.15)', size: 6, x: 0, y: 2 },
|
|
309
|
+
},
|
|
310
|
+
edges: {
|
|
311
|
+
smooth: { type: 'continuous', roundness: 0.3 },
|
|
312
|
+
width: 1.5,
|
|
313
|
+
color: { color: '#4a4a6a', highlight: '#7c3aed' },
|
|
314
|
+
},
|
|
315
|
+
configure: { enabled: false },
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Click node → show tooltip with file path
|
|
319
|
+
network.on('click', function(params) {
|
|
320
|
+
var tip = document.getElementById("graph-tooltip");
|
|
321
|
+
if (params.nodes.length) {
|
|
322
|
+
var n = allNodes.find(function(x){return x.id===params.nodes[0]});
|
|
323
|
+
if (n) {
|
|
324
|
+
tip.style.display = 'block';
|
|
325
|
+
tip.innerHTML = '<b>' + n.name + '</b> <span style="color:var(--muted)">' + n.type + '</span>' +
|
|
326
|
+
(n.file_path ? '<br><span style="color:var(--accent2);font-size:11px;word-break:break-all">' + n.file_path + '</span>' : '');
|
|
327
|
+
// Position tooltip near the clicked node
|
|
328
|
+
var pos = network.getPositions([n.id]);
|
|
329
|
+
if (pos && pos[n.id]) {
|
|
330
|
+
var canvasPos = network.canvasToDOM(pos[n.id]);
|
|
331
|
+
var panelRect = container.getBoundingClientRect();
|
|
332
|
+
var tipX = Math.min(canvasPos.x + 12, panelRect.width - 340);
|
|
333
|
+
var tipY = Math.max(canvasPos.y - 10, 0);
|
|
334
|
+
tip.style.left = Math.max(tipX, 8) + 'px';
|
|
335
|
+
tip.style.top = tipY + 'px';
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
tip.style.display = 'none';
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Hover highlight
|
|
344
|
+
network.on('hoverNode', function(params) {
|
|
345
|
+
var nodeId = params.node;
|
|
346
|
+
network.body.data.nodes.updateOnly({
|
|
347
|
+
id: nodeId,
|
|
348
|
+
borderWidth: 2,
|
|
349
|
+
borderWidthSelected: 2,
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
network.on('blurNode', function(params) {
|
|
353
|
+
network.body.data.nodes.updateOnly({
|
|
354
|
+
id: params.node,
|
|
355
|
+
borderWidth: 0,
|
|
356
|
+
borderWidthSelected: 0,
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
renderLegend(types, colors);
|
|
361
|
+
|
|
362
|
+
// Show truncation warning
|
|
363
|
+
if (truncated) {
|
|
364
|
+
var warn = document.createElement('div');
|
|
365
|
+
warn.style.cssText = 'position:absolute;top:16px;left:16px;z-index:10;background:rgba(234,179,8,0.15);color:var(--yellow);border:1px solid rgba(234,179,8,0.3);border-radius:8px;padding:8px 14px;font-size:12px;backdrop-filter:blur(8px);pointer-events:none';
|
|
366
|
+
warn.innerHTML = '⚠ Showing ' + maxNodes + ' / ' + allNodes.length + ' nodes';
|
|
367
|
+
document.getElementById('graph-panel').appendChild(warn);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Fit after stabilization
|
|
371
|
+
network.once('stabilizationIterationsDone', function() {
|
|
372
|
+
network.fit({ animation: { duration: 500, easingFunction: 'easeOutQuad' } });
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function refetch() {
|
|
377
|
+
var btn = document.getElementById('refetch-btn');
|
|
378
|
+
btn.innerHTML = '↻ Refetching...';
|
|
379
|
+
btn.style.opacity = '0.6';
|
|
380
|
+
try {
|
|
381
|
+
// Update status too
|
|
382
|
+
var status = await fetch(API+'/status').then(function(r){return r.json()});
|
|
383
|
+
var dot = document.getElementById('status-dot');
|
|
384
|
+
var txt = document.getElementById('status-text');
|
|
385
|
+
if (status.ok) {
|
|
386
|
+
dot.className = 'dot on';
|
|
387
|
+
txt.textContent = status.project || 'connected';
|
|
388
|
+
} else {
|
|
389
|
+
dot.className = 'dot off';
|
|
390
|
+
txt.textContent = 'no kuma.db found';
|
|
391
|
+
}
|
|
392
|
+
// Refetch dashboard data
|
|
393
|
+
var data = await fetch(API+'/dashboard').then(function(r){return r.json()});
|
|
394
|
+
if (data.error) { showError(data.error); return; }
|
|
395
|
+
renderStats(data);
|
|
396
|
+
renderGraph(data);
|
|
397
|
+
renderGotchas(data);
|
|
398
|
+
renderTrajectories(data);
|
|
399
|
+
renderHealth(data);
|
|
400
|
+
} catch(e) {
|
|
401
|
+
showError('Refetch failed: ' + e.message);
|
|
402
|
+
}
|
|
403
|
+
btn.innerHTML = '↻ Refetch';
|
|
404
|
+
btn.style.opacity = '1';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function renderLegend(types, colors){
|
|
408
|
+
document.getElementById("legend").innerHTML = '<div class="title">Legend</div>' +
|
|
409
|
+
types.map(function(t){
|
|
410
|
+
return '<div class="legend-item"><div class="legend-dot" style="background:'+(colors[t]||'#888')+'"></div>' + t + '</div>';
|
|
411
|
+
}).join('');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function setupTabs(){
|
|
415
|
+
var content = document.getElementById("tab-content");
|
|
416
|
+
content.innerHTML =
|
|
417
|
+
'<div class="panel active" id="panel-gotchas"><div class="empty">Loading...</div></div>' +
|
|
418
|
+
'<div class="panel" id="panel-trajectories"><div class="empty">Loading...</div></div>' +
|
|
419
|
+
'<div class="panel" id="panel-health"><div class="empty">Loading...</div></div>';
|
|
420
|
+
document.querySelectorAll('.tab').forEach(function(tab){
|
|
421
|
+
tab.onclick = function(){
|
|
422
|
+
document.querySelectorAll('.tab').forEach(function(t){t.classList.remove('active')});
|
|
423
|
+
this.classList.add('active');
|
|
424
|
+
document.querySelectorAll('.panel').forEach(function(p){p.classList.remove('active')});
|
|
425
|
+
var p = document.getElementById('panel-'+this.dataset.tab);
|
|
426
|
+
if(p) p.classList.add('active');
|
|
427
|
+
};
|
|
428
|
+
});
|
|
429
|
+
document.querySelector('.tab.active').click();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function renderGotchas(data){
|
|
433
|
+
var items = data.gotchas || [];
|
|
434
|
+
var el = document.getElementById("panel-gotchas");
|
|
435
|
+
if(!el) return;
|
|
436
|
+
if(!items.length){el.innerHTML='<div class="empty">No gotchas yet</div>';return;}
|
|
437
|
+
el.innerHTML = items.map(function(g){
|
|
438
|
+
var color = g.severity==='critical'?'red':g.severity==='high'?'orange':g.severity==='medium'?'yellow':'green';
|
|
439
|
+
return '<div class="gotcha-item" style="border-color:var(--'+color+')">' +
|
|
440
|
+
'<div class="file">'+(g.file_path||'global')+'</div>' +
|
|
441
|
+
'<div class="desc">'+g.description+'</div>' +
|
|
442
|
+
'<span class="sev-tag sev-'+g.severity+'">'+g.severity+'</span>' +
|
|
443
|
+
(g.workaround ? ' <span style="font-size:11px;color:var(--muted)">💡 '+g.workaround+'</span>' : '') +
|
|
444
|
+
'</div>';
|
|
445
|
+
}).join('');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function renderTrajectories(data){
|
|
449
|
+
var items = data.trajectories || [];
|
|
450
|
+
var el = document.getElementById("panel-trajectories");
|
|
451
|
+
if(!el) return;
|
|
452
|
+
if(!items.length){el.innerHTML='<div class="empty">No trajectories yet</div>';return;}
|
|
453
|
+
el.innerHTML = items.map(function(t){
|
|
454
|
+
var dur = t.total_duration_ms ? (t.total_duration_ms/1000).toFixed(1)+'s' : '-';
|
|
455
|
+
var rate = t.success_rate != null ? (t.success_rate*100).toFixed(0)+'%' : '-';
|
|
456
|
+
var date = t.created_at ? new Date(t.created_at*1000).toLocaleDateString() : '-';
|
|
457
|
+
return '<div class="traj-item"><div class="goal">'+t.goal+'</div><div class="meta">⏱ '+dur+' · ✔ '+rate+' · '+date+'</div></div>';
|
|
458
|
+
}).join('');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function renderHealth(data){
|
|
462
|
+
var items = data.health || [];
|
|
463
|
+
var el = document.getElementById("panel-health");
|
|
464
|
+
if(!el) return;
|
|
465
|
+
if(!items.length){el.innerHTML='<div class="empty">No health data yet</div>';return;}
|
|
466
|
+
el.innerHTML = items.map(function(h){
|
|
467
|
+
var cls = h.score>=80?'good':h.score>=50?'ok':'bad';
|
|
468
|
+
var summary = h.summary||h.risk_level||'-';
|
|
469
|
+
var date = h.created_at ? new Date(h.created_at*1000).toLocaleDateString() : '-';
|
|
470
|
+
return '<div class="health-item"><div class="score '+cls+'">'+h.score+'</div><div class="meta">'+summary+' · '+date+'</div></div>';
|
|
471
|
+
}).join('');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function setupSearch() {
|
|
475
|
+
document.getElementById("node-search").oninput = function() {
|
|
476
|
+
var query = this.value.toLowerCase().trim();
|
|
477
|
+
if (!network || !allNodes) return;
|
|
478
|
+
if (!query) {
|
|
479
|
+
network.body.data.nodes.forEach(function(n){
|
|
480
|
+
network.body.data.nodes.update({id:n.id,opacity:1.0});
|
|
481
|
+
});
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
var matched = new Set();
|
|
485
|
+
allNodes.forEach(function(n){
|
|
486
|
+
if (n.name.toLowerCase().includes(query) || (n.file_path && n.file_path.toLowerCase().includes(query))) {
|
|
487
|
+
matched.add(n.id);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
network.body.data.nodes.forEach(function(n){
|
|
491
|
+
network.body.data.nodes.update({
|
|
492
|
+
id: n.id,
|
|
493
|
+
opacity: matched.has(n.id) ? 1.0 : 0.15,
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
</script>
|
|
499
|
+
</body>
|
|
500
|
+
</html>
|