0agent 1.0.4 → 1.0.6
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/bin/0agent.js +60 -10
- package/dist/daemon.mjs +41 -13
- package/dist/graph.html +451 -0
- package/package.json +1 -1
package/bin/0agent.js
CHANGED
|
@@ -115,27 +115,68 @@ async function runInit() {
|
|
|
115
115
|
mkdirSync(resolve(AGENT_DIR, 'skills', 'builtin'), { recursive: true });
|
|
116
116
|
mkdirSync(resolve(AGENT_DIR, 'skills', 'custom'), { recursive: true });
|
|
117
117
|
|
|
118
|
-
console.log(' Step 1 of
|
|
118
|
+
console.log(' Step 1 of 5: LLM Provider\n');
|
|
119
119
|
const provider = await choose(' Which LLM provider?', [
|
|
120
120
|
'Anthropic (Claude) ← recommended',
|
|
121
121
|
'OpenAI (GPT-4o)',
|
|
122
|
+
'xAI (Grok)',
|
|
123
|
+
'Google (Gemini)',
|
|
122
124
|
'Ollama (local, free)',
|
|
123
125
|
], 0);
|
|
124
|
-
const providerKey = ['anthropic', 'openai', 'ollama'][provider];
|
|
126
|
+
const providerKey = ['anthropic', 'openai', 'xai', 'gemini', 'ollama'][provider];
|
|
127
|
+
|
|
128
|
+
// Model selection per provider
|
|
129
|
+
const MODELS = {
|
|
130
|
+
anthropic: [
|
|
131
|
+
'claude-sonnet-4-6 ← recommended (fast + smart)',
|
|
132
|
+
'claude-opus-4-6 (most capable, slower)',
|
|
133
|
+
'claude-haiku-4-5 (fastest, cheapest)',
|
|
134
|
+
],
|
|
135
|
+
openai: [
|
|
136
|
+
'gpt-4o ← recommended',
|
|
137
|
+
'gpt-4o-mini (faster, cheaper)',
|
|
138
|
+
'o3-mini (reasoning)',
|
|
139
|
+
],
|
|
140
|
+
xai: [
|
|
141
|
+
'grok-3 ← recommended',
|
|
142
|
+
'grok-3-mini',
|
|
143
|
+
],
|
|
144
|
+
gemini: [
|
|
145
|
+
'gemini-2.0-flash ← recommended',
|
|
146
|
+
'gemini-2.0-pro',
|
|
147
|
+
],
|
|
148
|
+
ollama: [
|
|
149
|
+
'llama3.1 ← recommended',
|
|
150
|
+
'mistral',
|
|
151
|
+
'codellama',
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
let model = '';
|
|
156
|
+
if (MODELS[providerKey]) {
|
|
157
|
+
console.log();
|
|
158
|
+
const modelIdx = await choose(' Which model?', MODELS[providerKey], 0);
|
|
159
|
+
model = MODELS[providerKey][modelIdx].split(/\s+/)[0];
|
|
160
|
+
}
|
|
125
161
|
|
|
126
162
|
let apiKey = '';
|
|
127
163
|
if (providerKey !== 'ollama') {
|
|
128
164
|
apiKey = await ask(`\n API Key: `);
|
|
129
165
|
if (!apiKey.trim()) {
|
|
130
166
|
console.log(' ⚠️ No API key provided. You can set it later in ~/.0agent/config.yaml');
|
|
167
|
+
} else {
|
|
168
|
+
// Validate key format
|
|
169
|
+
const keyPrefixes = { anthropic: 'sk-ant-', openai: 'sk-', xai: 'xai-', gemini: 'AI' };
|
|
170
|
+
const expectedPrefix = keyPrefixes[providerKey];
|
|
171
|
+
if (expectedPrefix && !apiKey.startsWith(expectedPrefix)) {
|
|
172
|
+
console.log(` ⚠️ Key doesn't look like a ${providerKey} key (expected: ${expectedPrefix}...)`);
|
|
173
|
+
} else {
|
|
174
|
+
console.log(' ✓ API key format looks valid');
|
|
175
|
+
}
|
|
131
176
|
}
|
|
132
177
|
}
|
|
133
178
|
|
|
134
|
-
|
|
135
|
-
: providerKey === 'openai' ? 'gpt-4o'
|
|
136
|
-
: 'llama3';
|
|
137
|
-
|
|
138
|
-
console.log('\n Step 2 of 4: Embedding model\n');
|
|
179
|
+
console.log('\n Step 2 of 5: Embedding model\n');
|
|
139
180
|
const embedding = await choose(' Embedding backend?', [
|
|
140
181
|
'Local via Ollama (nomic-embed-text) ← free, private',
|
|
141
182
|
'OpenAI text-embedding-3-small (cloud)',
|
|
@@ -143,19 +184,28 @@ async function runInit() {
|
|
|
143
184
|
], 0);
|
|
144
185
|
const embeddingProvider = ['nomic-ollama', 'openai', 'none'][embedding];
|
|
145
186
|
|
|
146
|
-
console.log('\n Step 3 of
|
|
187
|
+
console.log('\n Step 3 of 5: Sandbox backend\n');
|
|
147
188
|
const sandboxes = detectSandboxes();
|
|
148
189
|
console.log(` Detected: ${sandboxes.join(', ') || 'process (fallback)'}`);
|
|
149
190
|
const sandboxChoice = sandboxes[0] ?? 'process';
|
|
150
191
|
console.log(` Using: ${sandboxChoice}`);
|
|
151
192
|
|
|
152
|
-
console.log('\n Step 4 of
|
|
193
|
+
console.log('\n Step 4 of 5: Seed graph\n');
|
|
153
194
|
const seed = await choose(' Start with a seed graph?', [
|
|
154
195
|
'software-engineering (skills + sprint workflow) ← recommended',
|
|
155
196
|
'scratch (empty graph)',
|
|
156
197
|
], 0);
|
|
157
198
|
const seedName = seed === 0 ? 'software-engineering' : null;
|
|
158
199
|
|
|
200
|
+
// Step 5: confirm
|
|
201
|
+
console.log('\n Step 5 of 5: Ready\n');
|
|
202
|
+
console.log(` Provider: ${providerKey}`);
|
|
203
|
+
console.log(` Model: ${model}`);
|
|
204
|
+
console.log(` API Key: ${apiKey ? apiKey.slice(0, 8) + '••••••••' : '(not set)'}`);
|
|
205
|
+
console.log(` Sandbox: ${sandboxChoice}`);
|
|
206
|
+
console.log(` Seed: ${seedName ?? 'scratch'}`);
|
|
207
|
+
console.log();
|
|
208
|
+
|
|
159
209
|
// Write config
|
|
160
210
|
const dbPath = resolve(AGENT_DIR, 'graph.db');
|
|
161
211
|
const hnswPath = resolve(AGENT_DIR, 'hnsw.bin');
|
|
@@ -168,7 +218,7 @@ version: "1"
|
|
|
168
218
|
llm_providers:
|
|
169
219
|
- provider: ${providerKey}
|
|
170
220
|
model: ${model}
|
|
171
|
-
api_key: ${apiKey || '
|
|
221
|
+
api_key: "${apiKey || ''}"
|
|
172
222
|
is_default: true
|
|
173
223
|
|
|
174
224
|
embedding:
|
package/dist/daemon.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// packages/daemon/src/ZeroAgentDaemon.ts
|
|
2
2
|
import { writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "node:fs";
|
|
3
|
-
import { resolve as
|
|
3
|
+
import { resolve as resolve3 } from "node:path";
|
|
4
4
|
import { homedir as homedir3 } from "node:os";
|
|
5
5
|
|
|
6
6
|
// packages/core/src/graph/GraphNode.ts
|
|
@@ -2202,6 +2202,9 @@ var SkillRegistry = class {
|
|
|
2202
2202
|
// packages/daemon/src/HTTPServer.ts
|
|
2203
2203
|
import { Hono as Hono8 } from "hono";
|
|
2204
2204
|
import { serve } from "@hono/node-server";
|
|
2205
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
2206
|
+
import { resolve as resolve2, dirname } from "node:path";
|
|
2207
|
+
import { fileURLToPath } from "node:url";
|
|
2205
2208
|
|
|
2206
2209
|
// packages/daemon/src/routes/health.ts
|
|
2207
2210
|
import { Hono } from "hono";
|
|
@@ -2453,6 +2456,24 @@ function skillRoutes(deps) {
|
|
|
2453
2456
|
}
|
|
2454
2457
|
|
|
2455
2458
|
// packages/daemon/src/HTTPServer.ts
|
|
2459
|
+
function findGraphHtml() {
|
|
2460
|
+
const candidates = [
|
|
2461
|
+
resolve2(dirname(fileURLToPath(import.meta.url)), "graph.html"),
|
|
2462
|
+
// dev (src/)
|
|
2463
|
+
resolve2(dirname(fileURLToPath(import.meta.url)), "..", "graph.html"),
|
|
2464
|
+
// bundled (dist/../)
|
|
2465
|
+
resolve2(dirname(fileURLToPath(import.meta.url)), "..", "dist", "graph.html")
|
|
2466
|
+
];
|
|
2467
|
+
for (const p of candidates) {
|
|
2468
|
+
try {
|
|
2469
|
+
readFileSync3(p);
|
|
2470
|
+
return p;
|
|
2471
|
+
} catch {
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
return candidates[0];
|
|
2475
|
+
}
|
|
2476
|
+
var GRAPH_HTML_PATH = findGraphHtml();
|
|
2456
2477
|
var HTTPServer = class {
|
|
2457
2478
|
app;
|
|
2458
2479
|
server = null;
|
|
@@ -2467,12 +2488,19 @@ var HTTPServer = class {
|
|
|
2467
2488
|
this.app.route("/api/traces", traceRoutes({ traceStore: deps.traceStore }));
|
|
2468
2489
|
this.app.route("/api/subagents", subagentRoutes());
|
|
2469
2490
|
this.app.route("/api/skills", skillRoutes({ skillRegistry: deps.skillRegistry }));
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2491
|
+
const serveGraph = (c) => {
|
|
2492
|
+
try {
|
|
2493
|
+
const html = readFileSync3(GRAPH_HTML_PATH, "utf8");
|
|
2494
|
+
return c.html(html);
|
|
2495
|
+
} catch {
|
|
2496
|
+
return c.html("<p>Graph UI not found. Run: pnpm build</p>");
|
|
2497
|
+
}
|
|
2498
|
+
};
|
|
2499
|
+
this.app.get("/", serveGraph);
|
|
2500
|
+
this.app.get("/graph", serveGraph);
|
|
2473
2501
|
}
|
|
2474
2502
|
start() {
|
|
2475
|
-
return new Promise((
|
|
2503
|
+
return new Promise((resolve5) => {
|
|
2476
2504
|
this.server = serve(
|
|
2477
2505
|
{
|
|
2478
2506
|
fetch: this.app.fetch,
|
|
@@ -2480,20 +2508,20 @@ var HTTPServer = class {
|
|
|
2480
2508
|
hostname: this.deps.host
|
|
2481
2509
|
},
|
|
2482
2510
|
() => {
|
|
2483
|
-
|
|
2511
|
+
resolve5();
|
|
2484
2512
|
}
|
|
2485
2513
|
);
|
|
2486
2514
|
});
|
|
2487
2515
|
}
|
|
2488
2516
|
stop() {
|
|
2489
|
-
return new Promise((
|
|
2517
|
+
return new Promise((resolve5, reject) => {
|
|
2490
2518
|
if (!this.server) {
|
|
2491
|
-
|
|
2519
|
+
resolve5();
|
|
2492
2520
|
return;
|
|
2493
2521
|
}
|
|
2494
2522
|
this.server.close((err) => {
|
|
2495
2523
|
if (err) reject(err);
|
|
2496
|
-
else
|
|
2524
|
+
else resolve5();
|
|
2497
2525
|
});
|
|
2498
2526
|
});
|
|
2499
2527
|
}
|
|
@@ -2517,11 +2545,11 @@ var ZeroAgentDaemon = class {
|
|
|
2517
2545
|
startedAt = 0;
|
|
2518
2546
|
pidFilePath;
|
|
2519
2547
|
constructor() {
|
|
2520
|
-
this.pidFilePath =
|
|
2548
|
+
this.pidFilePath = resolve3(homedir3(), ".0agent", "daemon.pid");
|
|
2521
2549
|
}
|
|
2522
2550
|
async start(opts) {
|
|
2523
2551
|
this.config = await loadConfig(opts?.config_path);
|
|
2524
|
-
const dotDir =
|
|
2552
|
+
const dotDir = resolve3(homedir3(), ".0agent");
|
|
2525
2553
|
if (!existsSync3(dotDir)) {
|
|
2526
2554
|
mkdirSync2(dotDir, { recursive: true });
|
|
2527
2555
|
}
|
|
@@ -2623,10 +2651,10 @@ var ZeroAgentDaemon = class {
|
|
|
2623
2651
|
};
|
|
2624
2652
|
|
|
2625
2653
|
// packages/daemon/src/start.ts
|
|
2626
|
-
import { resolve as
|
|
2654
|
+
import { resolve as resolve4 } from "node:path";
|
|
2627
2655
|
import { homedir as homedir4 } from "node:os";
|
|
2628
2656
|
import { existsSync as existsSync4 } from "node:fs";
|
|
2629
|
-
var CONFIG_PATH = process.env["ZEROAGENT_CONFIG"] ??
|
|
2657
|
+
var CONFIG_PATH = process.env["ZEROAGENT_CONFIG"] ?? resolve4(homedir4(), ".0agent", "config.yaml");
|
|
2630
2658
|
if (!existsSync4(CONFIG_PATH)) {
|
|
2631
2659
|
console.error(`
|
|
2632
2660
|
0agent is not initialised.
|
package/dist/graph.html
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
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">
|
|
6
|
+
<title>0agent — Knowledge Graph</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body { background: #0d0d0d; color: #e0e0e0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; overflow: hidden; }
|
|
10
|
+
|
|
11
|
+
#graph { width: 100vw; height: 100vh; }
|
|
12
|
+
|
|
13
|
+
/* Top bar */
|
|
14
|
+
#topbar {
|
|
15
|
+
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
|
16
|
+
display: flex; align-items: center; gap: 12px;
|
|
17
|
+
padding: 10px 16px; background: rgba(13,13,13,0.85);
|
|
18
|
+
border-bottom: 1px solid #222; backdrop-filter: blur(8px);
|
|
19
|
+
}
|
|
20
|
+
#topbar h1 { font-size: 15px; font-weight: 600; color: #7aa2f7; letter-spacing: -0.3px; }
|
|
21
|
+
#topbar .stats { font-size: 12px; color: #555; }
|
|
22
|
+
#topbar .ws-dot {
|
|
23
|
+
width: 7px; height: 7px; border-radius: 50%; background: #555;
|
|
24
|
+
transition: background 0.3s;
|
|
25
|
+
}
|
|
26
|
+
#topbar .ws-dot.live { background: #9ece6a; box-shadow: 0 0 6px #9ece6a88; }
|
|
27
|
+
#topbar .spacer { flex: 1; }
|
|
28
|
+
#topbar input {
|
|
29
|
+
background: #1a1a1a; border: 1px solid #333; color: #e0e0e0;
|
|
30
|
+
padding: 5px 10px; border-radius: 6px; font-size: 12px; width: 200px;
|
|
31
|
+
outline: none;
|
|
32
|
+
}
|
|
33
|
+
#topbar input:focus { border-color: #7aa2f7; }
|
|
34
|
+
#topbar select {
|
|
35
|
+
background: #1a1a1a; border: 1px solid #333; color: #e0e0e0;
|
|
36
|
+
padding: 5px 8px; border-radius: 6px; font-size: 12px; outline: none;
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Side panel */
|
|
41
|
+
#panel {
|
|
42
|
+
position: fixed; top: 0; right: 0; bottom: 0; width: 320px; z-index: 90;
|
|
43
|
+
background: rgba(13,13,13,0.95); border-left: 1px solid #222;
|
|
44
|
+
transform: translateX(100%); transition: transform 0.25s ease;
|
|
45
|
+
overflow-y: auto; padding: 56px 0 16px;
|
|
46
|
+
backdrop-filter: blur(12px);
|
|
47
|
+
}
|
|
48
|
+
#panel.open { transform: translateX(0); }
|
|
49
|
+
#panel-close {
|
|
50
|
+
position: absolute; top: 44px; right: 12px;
|
|
51
|
+
background: none; border: none; color: #555; cursor: pointer;
|
|
52
|
+
font-size: 18px; line-height: 1;
|
|
53
|
+
}
|
|
54
|
+
#panel-close:hover { color: #e0e0e0; }
|
|
55
|
+
.panel-section { padding: 12px 16px; border-bottom: 1px solid #1a1a1a; }
|
|
56
|
+
.panel-section:last-child { border-bottom: none; }
|
|
57
|
+
.panel-label { font-size: 10px; font-weight: 600; text-transform: uppercase;
|
|
58
|
+
letter-spacing: 0.8px; color: #555; margin-bottom: 6px; }
|
|
59
|
+
.panel-value { font-size: 13px; color: #e0e0e0; word-break: break-word; }
|
|
60
|
+
.panel-badge {
|
|
61
|
+
display: inline-block; font-size: 10px; font-weight: 600; padding: 2px 8px;
|
|
62
|
+
border-radius: 10px; margin-right: 4px; margin-bottom: 4px;
|
|
63
|
+
}
|
|
64
|
+
.edge-row {
|
|
65
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
66
|
+
padding: 4px 0; border-bottom: 1px solid #111; font-size: 12px;
|
|
67
|
+
}
|
|
68
|
+
.edge-row:last-child { border-bottom: none; }
|
|
69
|
+
.edge-label { color: #8080a0; flex: 1; }
|
|
70
|
+
.edge-target { color: #7aa2f7; cursor: pointer; }
|
|
71
|
+
.edge-target:hover { text-decoration: underline; }
|
|
72
|
+
.weight-bar {
|
|
73
|
+
width: 40px; height: 4px; border-radius: 2px; background: #222;
|
|
74
|
+
position: relative; overflow: hidden;
|
|
75
|
+
}
|
|
76
|
+
.weight-fill { height: 100%; border-radius: 2px; transition: width 0.5s; }
|
|
77
|
+
.personality-line { font-size: 12px; color: #aaa; margin-bottom: 4px; line-height: 1.5; }
|
|
78
|
+
|
|
79
|
+
/* Tooltip */
|
|
80
|
+
#tooltip {
|
|
81
|
+
position: fixed; z-index: 200; pointer-events: none;
|
|
82
|
+
background: rgba(13,13,13,0.95); border: 1px solid #333;
|
|
83
|
+
border-radius: 8px; padding: 8px 12px; font-size: 12px;
|
|
84
|
+
max-width: 220px; display: none;
|
|
85
|
+
}
|
|
86
|
+
#tooltip .tip-label { font-weight: 600; color: #e0e0e0; margin-bottom: 3px; }
|
|
87
|
+
#tooltip .tip-type { font-size: 10px; color: #555; text-transform: uppercase; }
|
|
88
|
+
#tooltip .tip-weight { font-size: 11px; color: #9ece6a; margin-top: 3px; }
|
|
89
|
+
|
|
90
|
+
/* Legend */
|
|
91
|
+
#legend {
|
|
92
|
+
position: fixed; bottom: 16px; left: 16px; z-index: 100;
|
|
93
|
+
background: rgba(13,13,13,0.85); border: 1px solid #222;
|
|
94
|
+
border-radius: 8px; padding: 10px 14px; font-size: 11px;
|
|
95
|
+
backdrop-filter: blur(8px);
|
|
96
|
+
}
|
|
97
|
+
#legend .legend-title { font-size: 10px; font-weight: 600; text-transform: uppercase;
|
|
98
|
+
letter-spacing: 0.6px; color: #555; margin-bottom: 6px; }
|
|
99
|
+
.legend-item { display: flex; align-items: center; gap: 7px; margin-bottom: 4px; color: #aaa; }
|
|
100
|
+
.legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
101
|
+
|
|
102
|
+
/* Weight change flash */
|
|
103
|
+
@keyframes flash { 0%,100%{opacity:1} 50%{opacity:0.3} }
|
|
104
|
+
</style>
|
|
105
|
+
</head>
|
|
106
|
+
<body>
|
|
107
|
+
|
|
108
|
+
<div id="topbar">
|
|
109
|
+
<span class="ws-dot" id="wsdot"></span>
|
|
110
|
+
<h1>0agent</h1>
|
|
111
|
+
<span class="stats" id="stats">loading…</span>
|
|
112
|
+
<span class="spacer"></span>
|
|
113
|
+
<input type="text" id="search" placeholder="Search nodes…" />
|
|
114
|
+
<select id="filter">
|
|
115
|
+
<option value="all">All types</option>
|
|
116
|
+
<option value="entity">Entity</option>
|
|
117
|
+
<option value="context">Context</option>
|
|
118
|
+
<option value="strategy">Strategy</option>
|
|
119
|
+
<option value="plan">Plan</option>
|
|
120
|
+
<option value="step">Step</option>
|
|
121
|
+
<option value="signal">Signal</option>
|
|
122
|
+
<option value="outcome">Outcome</option>
|
|
123
|
+
<option value="hypothesis">Hypothesis</option>
|
|
124
|
+
</select>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div id="graph"></div>
|
|
128
|
+
|
|
129
|
+
<div id="panel">
|
|
130
|
+
<button id="panel-close">✕</button>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div id="tooltip"></div>
|
|
134
|
+
|
|
135
|
+
<div id="legend">
|
|
136
|
+
<div class="legend-title">Node types</div>
|
|
137
|
+
<div class="legend-item"><span class="legend-dot" style="background:#7aa2f7"></span>Entity</div>
|
|
138
|
+
<div class="legend-item"><span class="legend-dot" style="background:#9ece6a"></span>Context</div>
|
|
139
|
+
<div class="legend-item"><span class="legend-dot" style="background:#e0af68"></span>Strategy</div>
|
|
140
|
+
<div class="legend-item"><span class="legend-dot" style="background:#bb9af7"></span>Plan</div>
|
|
141
|
+
<div class="legend-item"><span class="legend-dot" style="background:#f7768e"></span>Step</div>
|
|
142
|
+
<div class="legend-item"><span class="legend-dot" style="background:#2ac3de"></span>Signal</div>
|
|
143
|
+
<div class="legend-item"><span class="legend-dot" style="background:#1abc9c"></span>Outcome</div>
|
|
144
|
+
<div class="legend-item"><span class="legend-dot" style="background:#ff9e64"></span>Hypothesis</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<!-- 3D Force Graph (WebGL) — same engine Obsidian uses -->
|
|
148
|
+
<script src="https://unpkg.com/3d-force-graph@1.73.2/dist/3d-force-graph.min.js"></script>
|
|
149
|
+
|
|
150
|
+
<script>
|
|
151
|
+
const NODE_COLORS = {
|
|
152
|
+
entity: '#7aa2f7',
|
|
153
|
+
context: '#9ece6a',
|
|
154
|
+
strategy: '#e0af68',
|
|
155
|
+
plan: '#bb9af7',
|
|
156
|
+
step: '#f7768e',
|
|
157
|
+
signal: '#2ac3de',
|
|
158
|
+
outcome: '#1abc9c',
|
|
159
|
+
tool: '#73daca',
|
|
160
|
+
constraint: '#565f89',
|
|
161
|
+
hypothesis: '#ff9e64',
|
|
162
|
+
};
|
|
163
|
+
const DEFAULT_COLOR = '#565f89';
|
|
164
|
+
|
|
165
|
+
// Graph state
|
|
166
|
+
let graphData = { nodes: [], links: [] };
|
|
167
|
+
let nodeMap = new Map(); // id → node
|
|
168
|
+
let edgeMap = new Map(); // id → edge
|
|
169
|
+
let selectedNode = null;
|
|
170
|
+
let filterType = 'all';
|
|
171
|
+
let searchQuery = '';
|
|
172
|
+
|
|
173
|
+
// ─── Init 3D graph ─────────────────────────────────────────────────────────
|
|
174
|
+
const Graph = ForceGraph3D()(document.getElementById('graph'))
|
|
175
|
+
.backgroundColor('#0d0d0d')
|
|
176
|
+
.nodeId('id')
|
|
177
|
+
.nodeLabel(n => `${n.label} (${n.type})`)
|
|
178
|
+
.nodeColor(n => {
|
|
179
|
+
const c = NODE_COLORS[n.type] || DEFAULT_COLOR;
|
|
180
|
+
if (searchQuery && !n.label.toLowerCase().includes(searchQuery)) return '#222';
|
|
181
|
+
if (filterType !== 'all' && n.type !== filterType) return '#1a1a1a';
|
|
182
|
+
if (n._flash) return '#ffffff';
|
|
183
|
+
return c;
|
|
184
|
+
})
|
|
185
|
+
.nodeVal(n => Math.max(1, Math.log2((n.visit_count || 1) + 1) * 3))
|
|
186
|
+
.nodeOpacity(0.9)
|
|
187
|
+
.linkColor(e => {
|
|
188
|
+
const w = e.weight ?? 0.5;
|
|
189
|
+
if (w >= 0.7) return '#9ece6a88';
|
|
190
|
+
if (w >= 0.4) return '#e0af6888';
|
|
191
|
+
return '#f7768e88';
|
|
192
|
+
})
|
|
193
|
+
.linkWidth(e => Math.max(0.3, (e.weight ?? 0.5) * 3))
|
|
194
|
+
.linkDirectionalArrowLength(4)
|
|
195
|
+
.linkDirectionalArrowRelPos(1)
|
|
196
|
+
.linkDirectionalParticles(e => e.weight > 0.7 ? 2 : 0)
|
|
197
|
+
.linkDirectionalParticleSpeed(0.006)
|
|
198
|
+
.linkDirectionalParticleColor(e => NODE_COLORS[e._toType] || '#7aa2f788')
|
|
199
|
+
.onNodeClick(node => showPanel(node))
|
|
200
|
+
.onNodeHover(node => showTooltip(node))
|
|
201
|
+
.onBackgroundClick(() => closePanel());
|
|
202
|
+
|
|
203
|
+
// Responsive resize
|
|
204
|
+
window.addEventListener('resize', () => {
|
|
205
|
+
Graph.width(window.innerWidth).height(window.innerHeight);
|
|
206
|
+
});
|
|
207
|
+
Graph.width(window.innerWidth).height(window.innerHeight);
|
|
208
|
+
|
|
209
|
+
// ─── Data loading ───────────────────────────────────────────────────────────
|
|
210
|
+
async function loadGraph() {
|
|
211
|
+
try {
|
|
212
|
+
const [nodesRes, edgesRes] = await Promise.all([
|
|
213
|
+
fetch('/api/graph/nodes?limit=500'),
|
|
214
|
+
fetch('/api/graph/edges?limit=2000'),
|
|
215
|
+
]);
|
|
216
|
+
const nodesData = await nodesRes.json();
|
|
217
|
+
const edgesData = await edgesRes.json();
|
|
218
|
+
|
|
219
|
+
const nodes = (Array.isArray(nodesData) ? nodesData : nodesData.nodes ?? []);
|
|
220
|
+
const edges = (Array.isArray(edgesData) ? edgesData : edgesData.edges ?? []);
|
|
221
|
+
|
|
222
|
+
nodeMap.clear();
|
|
223
|
+
edgeMap.clear();
|
|
224
|
+
|
|
225
|
+
for (const n of nodes) nodeMap.set(n.id, n);
|
|
226
|
+
|
|
227
|
+
const links = edges
|
|
228
|
+
.filter(e => nodeMap.has(e.from_node) && nodeMap.has(e.to_node))
|
|
229
|
+
.map(e => ({
|
|
230
|
+
...e,
|
|
231
|
+
source: e.from_node,
|
|
232
|
+
target: e.to_node,
|
|
233
|
+
_toType: nodeMap.get(e.to_node)?.type,
|
|
234
|
+
}));
|
|
235
|
+
|
|
236
|
+
for (const e of links) edgeMap.set(e.id, e);
|
|
237
|
+
|
|
238
|
+
graphData = { nodes, links };
|
|
239
|
+
Graph.graphData(graphData);
|
|
240
|
+
updateStats();
|
|
241
|
+
} catch (e) {
|
|
242
|
+
console.error('Failed to load graph:', e);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function updateStats() {
|
|
247
|
+
document.getElementById('stats').textContent =
|
|
248
|
+
`${graphData.nodes.length} nodes · ${graphData.links.length} edges`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ─── WebSocket live updates ─────────────────────────────────────────────────
|
|
252
|
+
const wsDot = document.getElementById('wsdot');
|
|
253
|
+
let ws;
|
|
254
|
+
|
|
255
|
+
function connectWS() {
|
|
256
|
+
ws = new WebSocket(`ws://${location.host}/ws`);
|
|
257
|
+
ws.onopen = () => {
|
|
258
|
+
wsDot.classList.add('live');
|
|
259
|
+
ws.send(JSON.stringify({ type: 'subscribe', topics: ['graph', 'sessions', 'stats'] }));
|
|
260
|
+
};
|
|
261
|
+
ws.onclose = () => {
|
|
262
|
+
wsDot.classList.remove('live');
|
|
263
|
+
setTimeout(connectWS, 3000);
|
|
264
|
+
};
|
|
265
|
+
ws.onmessage = (e) => {
|
|
266
|
+
const event = JSON.parse(e.data);
|
|
267
|
+
handleEvent(event);
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function handleEvent(event) {
|
|
272
|
+
switch (event.type) {
|
|
273
|
+
case 'graph.weight_updated': {
|
|
274
|
+
const link = edgeMap.get(event.edge_id);
|
|
275
|
+
if (link) {
|
|
276
|
+
link.weight = event.new_weight;
|
|
277
|
+
// Flash the connected nodes
|
|
278
|
+
const srcNode = nodeMap.get(link.from_node || link.source?.id);
|
|
279
|
+
if (srcNode) { srcNode._flash = true; setTimeout(() => { srcNode._flash = false; Graph.refresh(); }, 600); }
|
|
280
|
+
Graph.refresh();
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
case 'daemon.stats':
|
|
285
|
+
// Reload graph periodically when stats show growth
|
|
286
|
+
if (event.graph_nodes > graphData.nodes.length) loadGraph();
|
|
287
|
+
break;
|
|
288
|
+
case 'session.completed':
|
|
289
|
+
// Small delay then reload to pick up new nodes
|
|
290
|
+
setTimeout(loadGraph, 800);
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ─── Side panel ─────────────────────────────────────────────────────────────
|
|
296
|
+
function showPanel(node) {
|
|
297
|
+
if (!node) return;
|
|
298
|
+
selectedNode = node;
|
|
299
|
+
const panel = document.getElementById('panel');
|
|
300
|
+
panel.classList.add('open');
|
|
301
|
+
|
|
302
|
+
// Build content
|
|
303
|
+
const edges = graphData.links.filter(
|
|
304
|
+
l => (l.source?.id ?? l.source) === node.id || (l.target?.id ?? l.target) === node.id
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const color = NODE_COLORS[node.type] || DEFAULT_COLOR;
|
|
308
|
+
|
|
309
|
+
let html = `
|
|
310
|
+
<div class="panel-section">
|
|
311
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
|
|
312
|
+
<span style="width:10px;height:10px;border-radius:50%;background:${color};flex-shrink:0"></span>
|
|
313
|
+
<span style="font-size:15px;font-weight:600">${escHtml(node.label)}</span>
|
|
314
|
+
</div>
|
|
315
|
+
<span class="panel-badge" style="background:${color}22;color:${color}">${node.type}</span>
|
|
316
|
+
${node.metadata?.is_skill ? '<span class="panel-badge" style="background:#7aa2f722;color:#7aa2f7">skill</span>' : ''}
|
|
317
|
+
</div>
|
|
318
|
+
<div class="panel-section">
|
|
319
|
+
<div class="panel-label">Stats</div>
|
|
320
|
+
<div class="panel-value">Visits: ${node.visit_count ?? 0}</div>
|
|
321
|
+
<div class="panel-value" style="color:#555;font-size:11px;margin-top:3px">
|
|
322
|
+
Last seen: ${node.last_seen ? new Date(node.last_seen).toLocaleString() : '—'}
|
|
323
|
+
</div>
|
|
324
|
+
</div>`;
|
|
325
|
+
|
|
326
|
+
// Personality profile (for entity nodes)
|
|
327
|
+
if (node.type === 'entity') {
|
|
328
|
+
const personalityEdge = graphData.links.find(
|
|
329
|
+
l => (l.source?.id ?? l.source) === node.id && nodeMap.get(l.target?.id ?? l.target)?.label === '__personality_profile__'
|
|
330
|
+
);
|
|
331
|
+
if (personalityEdge) {
|
|
332
|
+
const profileNode = nodeMap.get(personalityEdge.target?.id ?? personalityEdge.target);
|
|
333
|
+
if (profileNode?.content?.[0]?.data) {
|
|
334
|
+
try {
|
|
335
|
+
const p = JSON.parse(profileNode.content[0].data);
|
|
336
|
+
html += `
|
|
337
|
+
<div class="panel-section">
|
|
338
|
+
<div class="panel-label">Personality (${p.interaction_count} interactions)</div>
|
|
339
|
+
${p.communication_style ? `<div class="personality-line"><b>Style:</b> ${escHtml(p.communication_style)}</div>` : ''}
|
|
340
|
+
${p.working_context ? `<div class="personality-line"><b>Context:</b> ${escHtml(p.working_context)}</div>` : ''}
|
|
341
|
+
${p.timezone !== 'UTC' && p.timezone ? `<div class="personality-line"><b>TZ:</b> ${escHtml(p.timezone)}</div>` : ''}
|
|
342
|
+
${p.response_preferences?.length ? `<div class="personality-line"><b>Prefs:</b> ${p.response_preferences.map(escHtml).join(', ')}</div>` : ''}
|
|
343
|
+
</div>`;
|
|
344
|
+
} catch {}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Edges
|
|
350
|
+
if (edges.length > 0) {
|
|
351
|
+
html += `<div class="panel-section"><div class="panel-label">Connections (${edges.length})</div>`;
|
|
352
|
+
for (const edge of edges.slice(0, 20)) {
|
|
353
|
+
const srcId = edge.source?.id ?? edge.source;
|
|
354
|
+
const tgtId = edge.target?.id ?? edge.target;
|
|
355
|
+
const isOut = srcId === node.id;
|
|
356
|
+
const otherId = isOut ? tgtId : srcId;
|
|
357
|
+
const other = nodeMap.get(otherId);
|
|
358
|
+
const w = edge.weight ?? 0.5;
|
|
359
|
+
const wColor = w >= 0.7 ? '#9ece6a' : w >= 0.4 ? '#e0af68' : '#f7768e';
|
|
360
|
+
html += `
|
|
361
|
+
<div class="edge-row">
|
|
362
|
+
<span class="edge-label">${isOut ? '→' : '←'} ${escHtml(edge.type)}</span>
|
|
363
|
+
<span class="edge-target" onclick="focusNode('${otherId}')">${escHtml(other?.label ?? otherId)}</span>
|
|
364
|
+
<div style="display:flex;align-items:center;gap:4px;margin-left:8px">
|
|
365
|
+
<div class="weight-bar"><div class="weight-fill" style="width:${(w*100).toFixed(0)}%;background:${wColor}"></div></div>
|
|
366
|
+
<span style="font-size:10px;color:${wColor};min-width:28px">${w.toFixed(2)}</span>
|
|
367
|
+
</div>
|
|
368
|
+
</div>`;
|
|
369
|
+
}
|
|
370
|
+
if (edges.length > 20) html += `<div style="font-size:11px;color:#555;margin-top:6px">+${edges.length - 20} more</div>`;
|
|
371
|
+
html += '</div>';
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Node metadata
|
|
375
|
+
if (node.metadata && Object.keys(node.metadata).length > 0) {
|
|
376
|
+
const meta = Object.entries(node.metadata)
|
|
377
|
+
.filter(([k]) => !['is_personality_profile'].includes(k))
|
|
378
|
+
.slice(0, 8);
|
|
379
|
+
if (meta.length > 0) {
|
|
380
|
+
html += `<div class="panel-section"><div class="panel-label">Metadata</div>`;
|
|
381
|
+
for (const [k, v] of meta) {
|
|
382
|
+
html += `<div style="font-size:11px;color:#555;margin-bottom:2px"><span style="color:#7aa2f7">${escHtml(k)}</span>: ${escHtml(String(v))}</div>`;
|
|
383
|
+
}
|
|
384
|
+
html += '</div>';
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
panel.innerHTML = `<button id="panel-close" onclick="closePanel()">✕</button>${html}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function closePanel() {
|
|
392
|
+
document.getElementById('panel').classList.remove('open');
|
|
393
|
+
selectedNode = null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function focusNode(id) {
|
|
397
|
+
const node = nodeMap.get(id);
|
|
398
|
+
if (!node) return;
|
|
399
|
+
Graph.centerAt(node.x, node.y, node.z, 600);
|
|
400
|
+
Graph.zoom(2, 600);
|
|
401
|
+
setTimeout(() => showPanel(node), 650);
|
|
402
|
+
}
|
|
403
|
+
window.focusNode = focusNode;
|
|
404
|
+
|
|
405
|
+
// ─── Tooltip ────────────────────────────────────────────────────────────────
|
|
406
|
+
const tooltip = document.getElementById('tooltip');
|
|
407
|
+
|
|
408
|
+
function showTooltip(node) {
|
|
409
|
+
if (!node) { tooltip.style.display = 'none'; return; }
|
|
410
|
+
const edges = graphData.links.filter(
|
|
411
|
+
l => (l.source?.id ?? l.source) === node.id || (l.target?.id ?? l.target) === node.id
|
|
412
|
+
);
|
|
413
|
+
const avgWeight = edges.length
|
|
414
|
+
? (edges.reduce((s, e) => s + (e.weight ?? 0.5), 0) / edges.length).toFixed(2)
|
|
415
|
+
: '—';
|
|
416
|
+
tooltip.innerHTML = `
|
|
417
|
+
<div class="tip-label">${escHtml(node.label)}</div>
|
|
418
|
+
<div class="tip-type">${node.type}</div>
|
|
419
|
+
<div class="tip-weight">${edges.length} connections · avg weight ${avgWeight}</div>`;
|
|
420
|
+
tooltip.style.display = 'block';
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
document.addEventListener('mousemove', e => {
|
|
424
|
+
tooltip.style.left = (e.clientX + 14) + 'px';
|
|
425
|
+
tooltip.style.top = (e.clientY - 10) + 'px';
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// ─── Search & filter ─────────────────────────────────────────────────────────
|
|
429
|
+
document.getElementById('search').addEventListener('input', e => {
|
|
430
|
+
searchQuery = e.target.value.toLowerCase();
|
|
431
|
+
Graph.refresh();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
document.getElementById('filter').addEventListener('change', e => {
|
|
435
|
+
filterType = e.target.value;
|
|
436
|
+
Graph.refresh();
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
440
|
+
function escHtml(s) {
|
|
441
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ─── Boot ─────────────────────────────────────────────────────────────────────
|
|
445
|
+
loadGraph();
|
|
446
|
+
connectWS();
|
|
447
|
+
// Reload every 30s to catch any missed updates
|
|
448
|
+
setInterval(loadGraph, 30_000);
|
|
449
|
+
</script>
|
|
450
|
+
</body>
|
|
451
|
+
</html>
|