@cef-ai/cli 0.2.1

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.
Files changed (55) hide show
  1. package/README.md +1273 -0
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.js +66 -0
  4. package/dist/commands/deploy/clone.d.ts +13 -0
  5. package/dist/commands/deploy/clone.js +143 -0
  6. package/dist/commands/deploy/delete.d.ts +14 -0
  7. package/dist/commands/deploy/delete.js +134 -0
  8. package/dist/commands/deploy/deploy.d.ts +24 -0
  9. package/dist/commands/deploy/deploy.js +654 -0
  10. package/dist/commands/deploy/index.d.ts +2 -0
  11. package/dist/commands/deploy/index.js +216 -0
  12. package/dist/commands/deploy/status.d.ts +14 -0
  13. package/dist/commands/deploy/status.js +61 -0
  14. package/dist/commands/dev/index.d.ts +2 -0
  15. package/dist/commands/dev/index.js +134 -0
  16. package/dist/commands/dev/server.d.ts +14 -0
  17. package/dist/commands/dev/server.js +167 -0
  18. package/dist/commands/dev/ui.d.ts +1 -0
  19. package/dist/commands/dev/ui.js +984 -0
  20. package/dist/commands/playground/index.d.ts +2 -0
  21. package/dist/commands/playground/index.js +75 -0
  22. package/dist/commands/playground/test.d.ts +15 -0
  23. package/dist/commands/playground/test.js +166 -0
  24. package/dist/commands/service/index.d.ts +2 -0
  25. package/dist/commands/service/index.js +75 -0
  26. package/dist/lib/api.d.ts +146 -0
  27. package/dist/lib/api.js +330 -0
  28. package/dist/lib/config.d.ts +12 -0
  29. package/dist/lib/config.js +120 -0
  30. package/dist/lib/runtime/agent-router.d.ts +12 -0
  31. package/dist/lib/runtime/agent-router.js +78 -0
  32. package/dist/lib/runtime/context-builder.d.ts +27 -0
  33. package/dist/lib/runtime/context-builder.js +166 -0
  34. package/dist/lib/runtime/cubby-manager.d.ts +20 -0
  35. package/dist/lib/runtime/cubby-manager.js +161 -0
  36. package/dist/lib/runtime/index.d.ts +51 -0
  37. package/dist/lib/runtime/index.js +183 -0
  38. package/dist/lib/runtime/log-collector.d.ts +17 -0
  39. package/dist/lib/runtime/log-collector.js +61 -0
  40. package/dist/lib/runtime/stream-manager.d.ts +10 -0
  41. package/dist/lib/runtime/stream-manager.js +77 -0
  42. package/dist/lib/runtime/types.d.ts +76 -0
  43. package/dist/lib/runtime/types.js +2 -0
  44. package/dist/lib/typecheck.d.ts +31 -0
  45. package/dist/lib/typecheck.js +175 -0
  46. package/dist/lib/types.d.ts +246 -0
  47. package/dist/lib/types.js +2 -0
  48. package/dist/lib/utils.d.ts +4 -0
  49. package/dist/lib/utils.js +33 -0
  50. package/example/.env.example +6 -0
  51. package/example/agents/object-detection/tasks/yolo.ts +28 -0
  52. package/example/agents/parking-violation-detector/tasks/detect.ts +326 -0
  53. package/example/cef.config.yaml +91 -0
  54. package/example/engagements/engagement.ts +902 -0
  55. package/package.json +69 -0
@@ -0,0 +1,984 @@
1
+ export function getDevUiHtml() {
2
+ return `<!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <title>CEF Dev</title>
8
+ <style>
9
+ *{margin:0;padding:0;box-sizing:border-box}
10
+ :root{
11
+ --bg:#09090b;--surface:#111113;--surface2:#18181b;--surface3:#1e1e22;
12
+ --border:#27272a;--border2:#3f3f46;
13
+ --text:#fafafa;--text2:#a1a1aa;--text3:#71717a;
14
+ --accent:#3b82f6;--accent2:#2563eb;
15
+ --success:#22c55e;--warn:#f59e0b;--error:#ef4444;
16
+ --mono:'SF Mono','Fira Code','JetBrains Mono','Consolas',monospace;
17
+ --sans:system-ui,-apple-system,'Segoe UI',sans-serif;
18
+ --radius:8px;
19
+ }
20
+ html,body{height:100%;background:var(--bg);color:var(--text);font:13px/1.5 var(--sans);overflow:hidden}
21
+ ::-webkit-scrollbar{width:6px;height:6px}
22
+ ::-webkit-scrollbar-track{background:transparent}
23
+ ::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
24
+ ::-webkit-scrollbar-thumb:hover{background:var(--border2)}
25
+ ::selection{background:var(--accent);color:#fff}
26
+
27
+ button{font:12px/1 var(--sans);cursor:pointer;border:none;border-radius:6px;padding:7px 16px;font-weight:500;transition:all .12s ease}
28
+ button:hover{filter:brightness(1.1)}
29
+ button:active{transform:scale(.98)}
30
+ textarea,input[type=text]{font:12px/1.5 var(--mono);background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:var(--radius);padding:10px 12px;resize:vertical;width:100%;outline:none;transition:border-color .15s,box-shadow .15s}
31
+ textarea:focus,input[type=text]:focus{border-color:var(--accent);box-shadow:0 0 0 2px rgba(59,130,246,.15)}
32
+ pre{font:12px/1.5 var(--mono);white-space:pre-wrap;word-break:break-all}
33
+ table{width:100%;border-collapse:collapse;font:12px/1.4 var(--mono)}
34
+ th,td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--border)}
35
+ th{color:var(--text3);font-weight:500;font-size:11px;text-transform:uppercase;letter-spacing:.3px;position:sticky;top:0;background:var(--surface2)}
36
+ td{color:var(--text2)}
37
+ tr:hover td{background:rgba(255,255,255,.02)}
38
+
39
+ /* ─── Layout ─── */
40
+ #app{display:grid;grid-template-columns:200px 1fr;grid-template-rows:48px 1fr auto;height:100vh}
41
+
42
+ /* Header */
43
+ #header{grid-column:1/-1;display:flex;align-items:center;padding:0 20px;background:var(--surface);border-bottom:1px solid var(--border);gap:10px}
44
+ #header h1{font-size:13px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;color:var(--text2)}
45
+ #header h1 span{color:var(--accent)}
46
+ .pill{font:10px/1 var(--mono);color:var(--text3);background:var(--surface2);padding:3px 10px;border-radius:12px;border:1px solid var(--border)}
47
+
48
+ /* Sidebar */
49
+ #sidebar{background:var(--surface);border-right:1px solid var(--border);overflow-y:auto;padding:4px 0}
50
+ .s-label{font:10px/1 var(--sans);color:var(--text3);text-transform:uppercase;letter-spacing:.8px;padding:16px 14px 6px;font-weight:600}
51
+ .s-item{display:flex;align-items:center;padding:6px 14px;cursor:pointer;font-size:12px;color:var(--text2);gap:8px;border-left:2px solid transparent;transition:all .1s}
52
+ .s-item:hover{background:var(--surface2);color:var(--text)}
53
+ .s-item.active{background:var(--surface2);color:var(--text);border-left-color:var(--accent)}
54
+ .s-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
55
+ .s-dot-eng{background:var(--accent)}
56
+ .s-dot-agent{background:var(--success)}
57
+ .s-dot-cubby{background:var(--warn)}
58
+ .s-task{color:var(--text3);font-weight:400}
59
+
60
+ /* Main */
61
+ #main{overflow-y:auto;padding:24px 28px;background:var(--bg)}
62
+
63
+ /* Panels */
64
+ .panel{display:none;max-width:600px;animation:fadeIn .15s ease}.panel.active{display:block}.panel.active.panel-wide{max-width:none}
65
+ @keyframes fadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
66
+ .panel-title{font-size:16px;font-weight:600;margin-bottom:20px;display:flex;align-items:center;gap:10px}
67
+ .field{margin-bottom:14px}
68
+ .field-label{display:block;font-size:10px;color:var(--text3);margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px;font-weight:600}
69
+ .btn-primary{background:var(--accent);color:#fff}
70
+ .btn-primary:hover{background:var(--accent2)}
71
+ .btn-secondary{background:var(--surface2);color:var(--text2);border:1px solid var(--border)}
72
+ .btn-secondary:hover{background:var(--surface3,#3f3f46);color:var(--text1)}
73
+ .btn-danger{background:#991b1b;color:#fca5a5}
74
+ .btn-danger:hover{background:#b91c1c}
75
+ .btn-ghost{background:transparent;color:var(--text2);border:1px solid var(--border);padding:6px 12px}
76
+ .btn-ghost:hover{background:var(--surface2);color:var(--text)}
77
+ .btn-row{display:flex;gap:8px;margin:14px 0}
78
+ .result{margin-top:14px;padding:14px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);max-height:280px;overflow:auto}
79
+ .result-meta{font-size:11px;color:var(--text3);margin-top:6px;display:flex;gap:12px}
80
+ .badge{display:inline-block;font:10px/1 var(--mono);padding:2px 6px;border-radius:4px}
81
+ .badge-ok{background:#052e16;color:var(--success);border:1px solid #14532d}
82
+ .badge-err{background:#1c0a0a;color:var(--error);border:1px solid #450a0a}
83
+ .error-banner{background:#1c0a0a;border:1px solid #450a0a;color:#fca5a5;padding:10px 14px;border-radius:var(--radius);margin-top:10px;font-size:12px}
84
+ .success-banner{background:#052e16;border:1px solid #14532d;color:#86efac;padding:10px 14px;border-radius:var(--radius);margin-top:10px;font-size:12px}
85
+ .table-wrap{max-height:calc(100vh - 280px);overflow:auto;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface)}
86
+ #cubby-sql-result .table-wrap{max-height:calc(100vh - 600px)}
87
+
88
+ /* Schema cards */
89
+ .schema-pair{display:flex;gap:12px;margin-bottom:16px}
90
+ .schema-card{flex:1;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:12px 14px}
91
+ .schema-card .st-title{font:10px/1 var(--sans);text-transform:uppercase;letter-spacing:.6px;color:var(--text3);margin-bottom:8px;font-weight:600}
92
+ .schema-row{font:11px/1.8 var(--mono);display:flex;align-items:baseline;gap:6px;flex-wrap:wrap}
93
+ .schema-row .sk{color:var(--text)}
94
+ .schema-row .st{color:var(--accent);opacity:.7}
95
+ .schema-row .sr{font:9px/1 var(--sans);color:var(--error);background:#1c0a0a;padding:1px 4px;border-radius:3px}
96
+ .schema-row .sd{color:var(--text3);font-size:10px;font-style:italic}
97
+
98
+ /* Cubby tabs */
99
+ .tab-bar{display:flex;gap:0;margin-bottom:16px;border-bottom:1px solid var(--border)}
100
+ .tab{padding:7px 16px;font-size:12px;color:var(--text3);cursor:pointer;border-bottom:2px solid transparent;font-weight:500;transition:color .1s}
101
+ .tab:hover{color:var(--text2)}
102
+ .tab.active{color:var(--text);border-bottom-color:var(--accent)}
103
+ .cubby-table-item{display:flex;align-items:center;padding:10px 14px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:8px;cursor:pointer;transition:border-color .1s}
104
+ .cubby-table-item:hover{border-color:var(--border2)}
105
+ .cubby-table-item .cti-name{font:13px/1 var(--mono);font-weight:500}
106
+ .cubby-table-item .cti-count{font-size:11px;color:var(--text3);margin-left:auto}
107
+
108
+ /* Stream push */
109
+ .stream-area{margin-top:20px;padding-top:20px;border-top:1px solid var(--border)}
110
+ .stream-area .tab-bar{margin-bottom:14px}
111
+ #file-drop-zone.drag-over{border-color:var(--accent);background:rgba(59,130,246,.05)}
112
+ .progress-bar{height:3px;background:var(--surface2);border-radius:2px;margin:8px 0;overflow:hidden}
113
+ .progress-bar .fill{height:100%;background:var(--accent);transition:width .1s;border-radius:2px}
114
+ .progress-text{font:11px var(--mono);color:var(--text3)}
115
+
116
+ /* ─── Bottom panel (resizable) ─── */
117
+ #bottom{grid-column:1/-1;display:flex;flex-direction:column;background:var(--surface);min-height:40px;max-height:60vh}
118
+ #bottom.collapsed{min-height:40px;max-height:40px}
119
+ #bottom.collapsed #log-container,#bottom.collapsed #flow-container{display:none!important}
120
+
121
+ #resize-handle{height:3px;background:transparent;cursor:ns-resize;flex-shrink:0;position:relative}
122
+ #resize-handle::after{content:'';position:absolute;left:50%;top:0;transform:translateX(-50%);width:40px;height:3px;background:var(--border);border-radius:2px;transition:background .15s}
123
+ #resize-handle:hover::after{background:var(--accent)}
124
+
125
+ .bottom-bar{display:flex;align-items:center;padding:0 12px;height:36px;flex-shrink:0;border-top:1px solid var(--border);gap:2px}
126
+ .btab{padding:4px 12px;font-size:11px;color:var(--text3);cursor:pointer;border-radius:4px;font-weight:500;transition:all .1s;user-select:none}
127
+ .btab:hover{color:var(--text2);background:var(--surface2)}
128
+ .btab.active{color:var(--text);background:var(--surface2)}
129
+ .bottom-actions{margin-left:auto;display:flex;align-items:center;gap:6px}
130
+ #log-filter{width:140px;font-size:11px;padding:4px 8px;border-radius:5px;background:var(--surface2);border:1px solid var(--border)}
131
+ .icon-btn{background:none;border:none;color:var(--text3);cursor:pointer;padding:4px;border-radius:4px;font-size:14px;line-height:1;display:flex;align-items:center}
132
+ .icon-btn:hover{color:var(--text);background:var(--surface2)}
133
+
134
+ #log-container,#flow-container{flex:1;overflow-y:auto;font:11px/1.7 var(--mono);padding:6px 14px}
135
+ .log-line{padding:1px 0;border-bottom:1px solid rgba(255,255,255,.02)}
136
+ .log-line .ts{color:var(--text3);margin-right:6px}
137
+ .log-line .agent{color:var(--accent);margin-right:4px}
138
+ .log-line.warn{color:var(--warn)}
139
+ .log-line.warn .ts{color:var(--warn);opacity:.5}
140
+ .log-line.error{color:var(--error)}
141
+ .log-line.error .ts{color:var(--error);opacity:.5}
142
+
143
+ .flow-entry{padding:2px 0;font-size:11px;line-height:1.6;white-space:pre}
144
+ .fe-time{color:var(--text3)}
145
+ .fe-type{font-weight:500}
146
+ .fe-eng{color:var(--accent)}
147
+ .fe-agent{color:var(--success)}
148
+ .fe-cubby{color:var(--warn)}
149
+ .fe-err{color:var(--error)}
150
+ .fe-ok{color:var(--success)}
151
+ .fe-dim{color:var(--text3)}
152
+
153
+ /* Empty state */
154
+ .empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--text3);gap:8px}
155
+ .empty-state .empty-icon{font-size:32px;opacity:.3}
156
+ .empty{color:var(--text3);font-size:12px;padding:24px 0;text-align:center}
157
+
158
+ /* Graph panel */
159
+ #cubby-graph{position:relative}
160
+ #graph-canvas{width:100%;height:calc(100vh - 200px);min-height:400px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);overflow:hidden}
161
+ #graph-detail{position:absolute;top:8px;right:8px;width:260px;background:var(--surface2);border:1px solid var(--border2);border-radius:var(--radius);padding:12px;display:none;font-size:11px;max-height:400px;overflow-y:auto}
162
+ #graph-detail .gd-type{font:10px/1 var(--sans);text-transform:uppercase;letter-spacing:.5px;font-weight:600;margin-bottom:8px;padding:2px 6px;border-radius:3px;display:inline-block}
163
+ .gd-row{display:flex;gap:6px;margin-bottom:4px;line-height:1.4}
164
+ .gd-key{color:var(--text3);min-width:80px;flex-shrink:0}
165
+ .gd-val{color:var(--text2);word-break:break-all}
166
+ #graph-legend{position:absolute;bottom:8px;left:8px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:8px 12px;font-size:10px}
167
+ #graph-legend .gl-row{display:flex;align-items:center;gap:6px;color:var(--text3);margin-bottom:3px}
168
+ #graph-legend .gl-row:last-child{margin-bottom:0}
169
+ .gl-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
170
+ #graph-stats{font:11px var(--mono);color:var(--text3);margin-bottom:8px}
171
+ </style>
172
+ <script src="https://unpkg.com/vis-network@9.1.9/standalone/umd/vis-network.min.js"></script>
173
+ </head>
174
+ <body>
175
+ <div id="app">
176
+ <div id="header">
177
+ <h1><span>CEF</span> Dev</h1>
178
+ <span class="pill" id="status-pill">connecting</span>
179
+ </div>
180
+
181
+ <div id="sidebar"></div>
182
+
183
+ <div id="main">
184
+ <div class="empty-state" id="empty-state">
185
+ <div class="empty-icon">&#9671;</div>
186
+ <div>Select an item from the sidebar</div>
187
+ </div>
188
+
189
+ <div class="panel" id="panel-engagement">
190
+ <div class="panel-title"><span class="s-dot s-dot-eng"></span> <span id="eng-name"></span> <span style="font-size:10px;font-weight:400;color:var(--text3);background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:2px 7px;margin-left:4px">engagement</span></div>
191
+ <div id="eng-desc" style="font-size:11px;color:var(--text3);margin-bottom:14px"></div>
192
+ <div id="eng-schema-hints"></div>
193
+ <div class="field">
194
+ <label class="field-label">Payload</label>
195
+ <textarea id="eng-payload" rows="4">{}</textarea>
196
+ </div>
197
+ <div style="display:flex;gap:8px;align-items:flex-end;margin-bottom:10px;flex-wrap:wrap">
198
+ <div style="display:flex;gap:6px;align-items:flex-end">
199
+ <div class="field" style="margin:0">
200
+ <label class="field-label">Repeat</label>
201
+ <input type="number" id="eng-repeat" value="1" min="1" max="999" style="width:70px;padding:6px 8px">
202
+ </div>
203
+ <div class="field" style="margin:0">
204
+ <label class="field-label">Delay ms</label>
205
+ <input type="number" id="eng-delay" value="300" min="0" style="width:80px;padding:6px 8px">
206
+ </div>
207
+ </div>
208
+ <div class="field" style="margin:0;flex:1;min-width:180px">
209
+ <label class="field-label">or blast from file (JSON array)</label>
210
+ <input type="file" id="eng-file-blast" accept=".json" style="width:100%;padding:5px 0;font-size:11px;color:var(--text2);background:none;border:none;cursor:pointer">
211
+ </div>
212
+ </div>
213
+ <div class="btn-row">
214
+ <button class="btn-primary" id="eng-start">Start</button>
215
+ <button class="btn-danger" id="eng-stop" style="display:none">Stop</button>
216
+ </div>
217
+ <div id="eng-result"></div>
218
+ <div class="stream-area" id="eng-stream-area" style="display:none">
219
+ <div class="panel-title" style="font-size:14px">Stream Data</div>
220
+ <div class="tab-bar">
221
+ <div class="tab active" data-stab="single">Single</div>
222
+ <div class="tab" data-stab="bulk">Bulk</div>
223
+ <div class="tab" data-stab="file">File</div>
224
+ </div>
225
+ <div id="stream-single">
226
+ <div class="field">
227
+ <label class="field-label">Packet</label>
228
+ <textarea id="eng-stream-payload" rows="3">{ }</textarea>
229
+ </div>
230
+ <button class="btn-primary" id="eng-push">Push</button>
231
+ </div>
232
+ <div id="stream-bulk" style="display:none">
233
+ <div class="field">
234
+ <label class="field-label">JSON Lines (one event per line) or JSON array</label>
235
+ <textarea id="eng-bulk-payload" rows="6">{ }
236
+ { }</textarea>
237
+ </div>
238
+ <div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">
239
+ <label class="field-label" style="margin:0;white-space:nowrap">Delay (ms)</label>
240
+ <input type="text" id="eng-bulk-delay" value="100" style="width:70px;padding:6px 8px">
241
+ </div>
242
+ <div class="btn-row" style="margin-top:0">
243
+ <button class="btn-primary" id="eng-bulk-send">Send All</button>
244
+ <button class="btn-danger" id="eng-bulk-stop" style="display:none">Stop</button>
245
+ </div>
246
+ <div id="eng-bulk-progress"></div>
247
+ </div>
248
+ <div id="stream-file" style="display:none">
249
+ <div id="file-drop-zone" style="border:2px dashed var(--border);border-radius:var(--radius);padding:28px;text-align:center;cursor:pointer;transition:border-color .15s;margin-bottom:12px">
250
+ <div style="color:var(--text3);font-size:12px">Drop a <strong>.json</strong> or <strong>.jsonl</strong> file here, or click to browse</div>
251
+ <input type="file" id="eng-file-input" accept=".json,.jsonl" style="display:none">
252
+ <div id="file-info" style="margin-top:8px;font:11px var(--mono);color:var(--text2)"></div>
253
+ </div>
254
+ <div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">
255
+ <label class="field-label" style="margin:0;white-space:nowrap">Delay (ms)</label>
256
+ <input type="text" id="eng-file-delay" value="100" style="width:70px;padding:6px 8px">
257
+ </div>
258
+ <div class="btn-row" style="margin-top:0">
259
+ <button class="btn-primary" id="eng-file-send" disabled>Send File</button>
260
+ <button class="btn-danger" id="eng-file-stop" style="display:none">Stop</button>
261
+ </div>
262
+ <div id="eng-file-progress"></div>
263
+ </div>
264
+ <div id="eng-push-result"></div>
265
+ </div>
266
+ </div>
267
+
268
+ <div class="panel" id="panel-agent">
269
+ <div class="panel-title"><span class="s-dot s-dot-agent"></span> <span id="agent-label"></span> <span style="font-size:10px;font-weight:400;color:var(--text3);background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:2px 7px;margin-left:4px">agent task</span></div>
270
+ <div id="agent-desc" style="font-size:11px;color:var(--text3);margin-bottom:14px"></div>
271
+ <div id="agent-schema-hints"></div>
272
+ <div class="field">
273
+ <label class="field-label">Payload</label>
274
+ <textarea id="agent-payload" rows="5">{}</textarea>
275
+ </div>
276
+ <div class="btn-row">
277
+ <button class="btn-primary" id="agent-call">Call</button>
278
+ <button class="btn-secondary" id="agent-batch" style="display:none">Run Batch</button>
279
+ </div>
280
+ <div id="agent-result"></div>
281
+ </div>
282
+
283
+ <div class="panel" id="panel-cubby">
284
+ <div class="panel-title"><span class="s-dot s-dot-cubby"></span> <span id="cubby-label"></span></div>
285
+ <div class="tab-bar">
286
+ <div class="tab active" data-tab="browse">Browse</div>
287
+ <div class="tab" data-tab="sql">SQL</div>
288
+ <div class="tab" data-tab="graph" id="graph-tab" style="display:none">Graph</div>
289
+ </div>
290
+ <div id="cubby-browse"><div id="cubby-tables"></div></div>
291
+ <div id="cubby-sql" style="display:none">
292
+ <div style="display:flex;gap:8px;align-items:flex-end;margin-bottom:10px">
293
+ <div class="field" style="flex:1;margin:0">
294
+ <label class="field-label">Saved Queries</label>
295
+ <select id="query-select" style="width:100%;background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:var(--radius);padding:7px 10px;font-size:12px;outline:none">
296
+ <option value="">— select a saved query —</option>
297
+ </select>
298
+ </div>
299
+ <button class="btn-ghost" id="query-delete" title="Delete selected query" style="display:none;padding:7px 10px">&#10005;</button>
300
+ </div>
301
+ <div class="field">
302
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
303
+ <label class="field-label" style="margin:0">Query</label>
304
+ <div style="display:flex;gap:6px;align-items:center">
305
+ <input type="text" id="query-name" placeholder="Name to save as..." style="width:160px;padding:4px 8px;font-size:11px;border-radius:5px;background:var(--surface2);border:1px solid var(--border);color:var(--text)">
306
+ <button class="btn-ghost" id="query-save" style="padding:4px 10px;font-size:11px">Save</button>
307
+ </div>
308
+ </div>
309
+ <textarea id="cubby-sql-input" rows="4">SELECT * FROM </textarea>
310
+ </div>
311
+ <button class="btn-primary" id="cubby-run">Run</button>
312
+ <div id="cubby-sql-result"></div>
313
+ </div>
314
+ <div id="cubby-graph">
315
+ <div id="graph-stats"></div>
316
+ <div style="position:relative">
317
+ <div id="graph-canvas"></div>
318
+ <div id="graph-detail">
319
+ <div id="gd-type-badge" class="gd-type"></div>
320
+ <div id="gd-attrs"></div>
321
+ </div>
322
+ <div id="graph-legend">
323
+ <div class="gl-row"><div class="gl-dot" style="background:#6366f1"></div>artifact</div>
324
+ <div class="gl-row"><div class="gl-dot" style="background:#f59e0b"></div>work</div>
325
+ <div class="gl-row"><div class="gl-dot" style="background:#22c55e"></div>operator</div>
326
+ <div class="gl-row"><div class="gl-dot" style="background:#ec4899"></div>eval</div>
327
+ <div class="gl-row"><div class="gl-dot" style="background:#71717a"></div>artifact_map</div>
328
+ </div>
329
+ </div>
330
+ </div>
331
+ </div>
332
+ </div>
333
+
334
+ <div id="bottom" style="height:220px">
335
+ <div id="resize-handle"></div>
336
+ <div class="bottom-bar">
337
+ <div class="btab active" data-btab="logs">Logs</div>
338
+ <div class="btab" data-btab="flow">Flow</div>
339
+ <div class="bottom-actions">
340
+ <input type="text" id="log-filter" placeholder="Filter...">
341
+ <button class="icon-btn" id="btn-clear" title="Clear">&#10005;</button>
342
+ <button class="icon-btn" id="btn-collapse" title="Collapse">&#9660;</button>
343
+ </div>
344
+ </div>
345
+ <div id="log-container"></div>
346
+ <div id="flow-container" style="display:none"></div>
347
+ </div>
348
+ </div>
349
+
350
+ <script>
351
+ (function(){
352
+ const API='';
353
+ let topology=null,activeEngagement=null,seqNum=0,ws=null,logFilter='',blastPayloads=null,blastAbort=false,batchRunning=false,batchAbort=false;
354
+
355
+ async function api(method,path,body){
356
+ const opts={method,headers:{'Content-Type':'application/json'}};
357
+ if(body!==undefined)opts.body=JSON.stringify(body);
358
+ const res=await fetch(API+path,opts);return res.json();
359
+ }
360
+
361
+ // ─── WebSocket ───
362
+ function connectWs(){
363
+ const proto=location.protocol==='https:'?'wss:':'ws:';
364
+ ws=new WebSocket(proto+'//'+location.host+'/ws');
365
+ ws.onopen=()=>{$('status-pill').textContent=location.host};
366
+ ws.onclose=()=>{$('status-pill').textContent='reconnecting';setTimeout(connectWs,2000)};
367
+ ws.onmessage=(e)=>{const m=JSON.parse(e.data);if(m.type==='log')appendLog(m.data);if(m.type==='trace')appendTrace(m.data)};
368
+ }
369
+
370
+ // ─── Logs ───
371
+ function appendLog(entry){
372
+ const c=$('log-container'),d=document.createElement('div');
373
+ d.className='log-line'+(entry.level!=='log'?' '+entry.level:'');
374
+ const ts=new Date(entry.timestamp).toISOString().slice(11,23);
375
+ const ag=entry.agentAlias?(entry.agentAlias+(entry.taskAlias?'.'+entry.taskAlias:'')):'';
376
+ const pf=ag?'<span class="agent">['+ag+']</span> ':'';
377
+ const args=entry.args.map(a=>typeof a==='string'?a:JSON.stringify(a)).join(' ');
378
+ d.innerHTML='<span class="ts">'+ts+'</span>'+pf+esc(args);
379
+ if(logFilter&&!args.toLowerCase().includes(logFilter))d.style.display='none';
380
+ c.appendChild(d);while(c.children.length>500)c.removeChild(c.firstChild);
381
+ c.scrollTop=c.scrollHeight;
382
+ }
383
+
384
+ // ─── Flow ───
385
+ function appendTrace(ev){
386
+ const c=$('flow-container'),d=document.createElement('div');
387
+ d.className='flow-entry';
388
+ const ts=new Date(ev.timestamp).toISOString().slice(11,23);
389
+ let ind='',det='',cls='';
390
+ switch(ev.type){
391
+ case'engagement:start':cls='fe-eng';det=ev.callee||'';break;
392
+ case'agent:call':ind=' ';cls='fe-agent';det=ev.callee+(ev.caller?' <span class="fe-dim">\\u2190 '+esc(ev.caller)+'</span>':'');break;
393
+ case'agent:return':ind=' ';cls=ev.success?'fe-ok':'fe-err';det=ev.callee+' <span class="fe-dim">'+(ev.durationMs||0).toFixed(1)+'ms</span>'+(ev.success?'':' <span class="fe-err">'+esc(ev.error||'FAILED')+'</span>');break;
394
+ case'cubby:query':ind=' ';cls='fe-cubby';det=(ev.cubbyAlias||'')+' <span class="fe-dim">'+esc(ev.sql||'')+'</span>';break;
395
+ case'cubby:exec':ind=' ';cls='fe-cubby';det=(ev.cubbyAlias||'')+' <span class="fe-dim">'+esc(ev.sql||'')+'</span>'+(ev.rowsAffected!=null?' <span class="fe-dim">('+ev.rowsAffected+')</span>':'');break;
396
+ }
397
+ d.innerHTML=ind+'<span class="fe-time">'+ts+'</span> <span class="fe-type '+cls+'">'+esc(ev.type)+'</span> '+det;
398
+ c.appendChild(d);c.scrollTop=c.scrollHeight;
399
+ }
400
+
401
+ // ─── Sidebar ───
402
+ async function loadTopology(){topology=await api('GET','/api/topology');renderSidebar()}
403
+ function renderSidebar(){
404
+ const el=$('sidebar');let h='';
405
+ if(topology.engagements.length){
406
+ h+='<div class="s-label">Engagements</div>';
407
+ for(const e of topology.engagements)h+='<div class="s-item" data-type="engagement" data-name="'+ea(e.name)+'"><span class="s-dot s-dot-eng"></span>'+esc(e.name)+'</div>';
408
+ }
409
+ if(topology.agents.length){
410
+ h+='<div class="s-label">Agents</div>';
411
+ for(const a of topology.agents)for(const t of a.tasks)
412
+ h+='<div class="s-item" data-type="agent" data-alias="'+ea(a.alias)+'" data-task="'+ea(t.alias)+'"><span class="s-dot s-dot-agent"></span>'+esc(a.alias)+'.<span class="s-task">'+esc(t.alias)+'</span></div>';
413
+ }
414
+ if(topology.cubbies.length){
415
+ h+='<div class="s-label">Cubbies</div>';
416
+ for(const c of topology.cubbies)h+='<div class="s-item" data-type="cubby" data-alias="'+ea(c.alias)+'"><span class="s-dot s-dot-cubby"></span>'+esc(c.alias)+'</div>';
417
+ const graphCubbies=topology.cubbies.filter(c=>c.alias);
418
+ if(graphCubbies.length)h+='<div class="s-item" data-type="graph-viz" style="margin-top:4px"><span class="s-dot" style="background:#22c55e"></span> Graph Visualizer</div>';
419
+ }
420
+ el.innerHTML=h;
421
+ el.querySelectorAll('.s-item').forEach(item=>{
422
+ item.addEventListener('click',()=>{
423
+ el.querySelectorAll('.s-item').forEach(i=>i.classList.remove('active'));
424
+ item.classList.add('active');
425
+ const t=item.dataset.type;
426
+ if(t==='engagement')showEngagement(item.dataset.name);
427
+ else if(t==='agent')showAgent(item.dataset.alias,item.dataset.task);
428
+ else if(t==='cubby')showCubby(item.dataset.alias);
429
+ else if(t==='graph-viz'){const gc=topology.cubbies[0];if(gc){showCubby(gc.alias);setTimeout(()=>{const tab=document.querySelector('#panel-cubby .tab[data-tab="graph"]');if(tab)tab.click();},50);}}
430
+ });
431
+ });
432
+ }
433
+
434
+ // ─── Panels ───
435
+ function hideAll(){document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));$('empty-state').style.display='none'}
436
+
437
+ function showEngagement(name){
438
+ hideAll();$('panel-engagement').classList.add('active');
439
+ $('eng-name').textContent=name;$('eng-result').innerHTML='';
440
+ const ec=topology.engagements.find(e=>e.name===name),params=ec?.parameters;
441
+ $('eng-desc').textContent=ec?.description||'';
442
+ if(params&&params.properties){
443
+ const s=schemaToSample(params);
444
+ if('streamId' in s&&!s.streamId)s.streamId=name.toLowerCase().replace(/\\s+/g,'-')+'-stream';
445
+ $('eng-payload').value=JSON.stringify(s,null,2);
446
+ }else{$('eng-payload').value=JSON.stringify({streamId:name.toLowerCase().replace(/\\s+/g,'-')+'-stream'},null,2)}
447
+ $('eng-schema-hints').innerHTML=renderSchemaHints(params);
448
+ if(activeEngagement&&activeEngagement.name===name){$('eng-stream-area').style.display='';$('eng-stop').style.display='';$('eng-start').style.display='none'}
449
+ else{$('eng-stream-area').style.display='none';$('eng-stop').style.display='none';$('eng-start').style.display=''}
450
+ }
451
+
452
+ function showAgent(alias,task){
453
+ hideAll();$('panel-agent').classList.add('active');
454
+ const lbl=$('agent-label');lbl.textContent=alias+'.'+task;lbl.dataset.alias=alias;lbl.dataset.task=task;
455
+ $('agent-result').innerHTML='';
456
+ const ac=topology.agents.find(a=>a.alias===alias),tc=ac?.tasks.find(t=>t.alias===task);
457
+ const params=tc?.parameters,returns=tc?.returns;
458
+ $('agent-desc').textContent=tc?.description||ac?.description||'';
459
+ $('agent-payload').value=params&&params.properties?JSON.stringify(schemaToSample(params),null,2):'{}';
460
+ $('agent-schema-hints').innerHTML=renderSchemaHints(params,returns);
461
+ var bb=$('agent-batch');
462
+ if(tc&&tc.batchSql){bb.style.display='';bb.dataset.batchSql=tc.batchSql;bb.dataset.batchCubby=tc.batchCubby||'';}
463
+ else{bb.style.display='none';}
464
+ }
465
+
466
+ function showCubby(alias){
467
+ hideAll();$('panel-cubby').classList.add('active');
468
+ const lbl=$('cubby-label');lbl.textContent=alias;lbl.dataset.alias=alias;
469
+ document.querySelectorAll('#panel-cubby .tab').forEach(t=>t.classList.remove('active'));
470
+ document.querySelector('#panel-cubby .tab[data-tab="browse"]').classList.add('active');
471
+ $('cubby-browse').style.display='';$('cubby-sql').style.display='none';$('cubby-graph').style.display='none';
472
+ $('graph-tab').style.display='';
473
+ $('panel-cubby').classList.add('panel-wide');
474
+ qtRefreshSelect(alias);
475
+ loadCubbyBrowse(alias);
476
+ }
477
+
478
+ function renderSchemaHints(params,returns){
479
+ const hp=params&&params.properties&&Object.keys(params.properties).length>0;
480
+ const hr=returns&&returns.properties&&Object.keys(returns.properties).length>0;
481
+ if(!hp&&!hr)return'';
482
+ let h='<div class="schema-pair">';
483
+ if(hp){
484
+ h+='<div class="schema-card"><div class="st-title">Input</div>';
485
+ for(const[k,v]of Object.entries(params.properties)){
486
+ const req=(params.required||[]).includes(k)?'<span class="sr">required</span>':'';
487
+ h+='<div class="schema-row"><span class="sk">'+esc(k)+'</span><span class="st">'+esc(v.type||'any')+'</span>'+req;
488
+ if(v.description)h+=' <span class="sd">'+esc(v.description)+'</span>';
489
+ h+='</div>';
490
+ }
491
+ h+='</div>';
492
+ }
493
+ if(hr){
494
+ h+='<div class="schema-card"><div class="st-title">Output</div>';
495
+ for(const[k,v]of Object.entries(returns.properties))
496
+ h+='<div class="schema-row"><span class="sk">'+esc(k)+'</span><span class="st">'+esc(v.type||'any')+'</span></div>';
497
+ h+='</div>';
498
+ }
499
+ return h+'</div>';
500
+ }
501
+
502
+ function schemaToSample(s){
503
+ if(!s||!s.properties)return{};const o={};
504
+ for(const[k,v]of Object.entries(s.properties)){
505
+ if(v.type==='string')o[k]='';else if(v.type==='number')o[k]=0;
506
+ else if(v.type==='boolean')o[k]=false;else if(v.type==='array')o[k]=[];
507
+ else if(v.type==='object')o[k]={};else o[k]=null;
508
+ }return o;
509
+ }
510
+
511
+ // ─── Engagement actions ───
512
+ $('eng-file-blast').addEventListener('change',function(){
513
+ if(!this.files||!this.files[0]){blastPayloads=null;return;}
514
+ var reader=new FileReader();
515
+ reader.onload=function(e){
516
+ try{
517
+ var parsed=JSON.parse(e.target.result);
518
+ blastPayloads=Array.isArray(parsed)?parsed:[parsed];
519
+ $('eng-result').innerHTML='<div class="success-banner">Loaded '+blastPayloads.length+' payloads from file</div>';
520
+ $('eng-repeat').value=''+blastPayloads.length;
521
+ }catch(err){blastPayloads=null;$('eng-result').innerHTML='<div class="error-banner">Invalid JSON: '+esc(err.message||String(err))+'</div>';}
522
+ };
523
+ reader.readAsText(this.files[0]);
524
+ });
525
+ $('eng-start').addEventListener('click',async function(){
526
+ const name=$('eng-name').textContent;
527
+ const delay=parseInt($('eng-delay').value)||0;
528
+ let payloads;
529
+ if(blastPayloads){
530
+ payloads=blastPayloads;
531
+ }else{
532
+ let base;try{base=JSON.parse($('eng-payload').value)}catch{base={}}
533
+ const n=Math.max(1,parseInt($('eng-repeat').value)||1);
534
+ payloads=Array.from({length:n},function(){return Object.assign({},base);});
535
+ }
536
+ blastAbort=false;
537
+ this.style.display='none';$('eng-stop').style.display='';
538
+ let ok=0,errors=0;
539
+ for(var i=0;i<payloads.length;i++){
540
+ if(blastAbort)break;
541
+ var payload=Object.assign({},payloads[i]);
542
+ payload.streamId=name.toLowerCase().replace(/\\s+/g,'-')+'-'+i+'-'+Date.now();
543
+ $('eng-result').innerHTML='<div class="success-banner">Firing '+(i+1)+'/'+payloads.length+'...</div>';
544
+ try{
545
+ const r=await api('POST','/api/trigger',{engagement:name,payload});
546
+ if(r.error){errors++;}else{ok++;if(payloads.length===1){activeEngagement={executionId:r.executionId,streamId:sid,name};seqNum=0;$('eng-stream-area').style.display='';}}
547
+ }catch(e){errors++;}
548
+ if(delay&&i<payloads.length-1)await new Promise(function(r){setTimeout(r,delay);});
549
+ }
550
+ var msg=blastAbort?'Aborted after '+(i)+'/'+payloads.length+' — '+ok+' ok':'Done — '+ok+'/'+payloads.length+' ok'+(errors?' ('+errors+' errors)':'');
551
+ $('eng-result').innerHTML='<div class="'+(errors||blastAbort?'error-banner':'success-banner')+'">'+esc(msg)+'</div>';
552
+ $('eng-stop').style.display='none';this.style.display='';
553
+ });
554
+ $('eng-stop').addEventListener('click',function(){
555
+ blastAbort=true;activeEngagement=null;$('eng-stream-area').style.display='none';this.style.display='none';
556
+ $('eng-start').style.display='';
557
+ });
558
+ // Stream push tab switching
559
+ document.querySelectorAll('[data-stab]').forEach(tab=>{
560
+ tab.addEventListener('click',()=>{
561
+ document.querySelectorAll('[data-stab]').forEach(t=>t.classList.remove('active'));tab.classList.add('active');
562
+ $('stream-single').style.display=tab.dataset.stab==='single'?'':'none';
563
+ $('stream-bulk').style.display=tab.dataset.stab==='bulk'?'':'none';
564
+ $('stream-file').style.display=tab.dataset.stab==='file'?'':'none';
565
+ });
566
+ });
567
+
568
+ // Helper: push one packet to active stream
569
+ async function pushOne(payloadStr){
570
+ if(!activeEngagement)return;
571
+ seqNum++;
572
+ await api('POST','/api/streams/'+encodeURIComponent(activeEngagement.streamId)+'/push',{data:{sequenceNum:seqNum,payload:payloadStr}});
573
+ }
574
+
575
+ // Helper: parse lines from text (JSON Lines or JSON array)
576
+ function parseLines(text){
577
+ const trimmed=text.trim();
578
+ if(trimmed.startsWith('[')){try{const arr=JSON.parse(trimmed);if(Array.isArray(arr))return arr.map(o=>JSON.stringify(o))}catch{}}
579
+ return trimmed.split('\\n').map(l=>l.trim()).filter(l=>l&&!l.startsWith('//'));
580
+ }
581
+
582
+ let bulkAbort=false;
583
+
584
+ // Helper: send lines with delay and progress
585
+ async function sendLines(lines,delayMs,progressEl,stopBtn,sendBtn){
586
+ bulkAbort=false;stopBtn.style.display='';sendBtn.style.display='none';
587
+ progressEl.innerHTML='<div class="progress-bar"><div class="fill" style="width:0%"></div></div><div class="progress-text">0 / '+lines.length+'</div>';
588
+ const bar=progressEl.querySelector('.fill'),txt=progressEl.querySelector('.progress-text');
589
+ let sent=0;
590
+ for(const line of lines){
591
+ if(bulkAbort)break;
592
+ try{await pushOne(line);sent++}catch(e){progressEl.innerHTML+='<div class="error-banner">Failed at '+sent+': '+esc(e.message)+'</div>';break}
593
+ const pct=Math.round(sent/lines.length*100);
594
+ bar.style.width=pct+'%';txt.textContent=sent+' / '+lines.length;
595
+ if(delayMs>0&&sent<lines.length)await new Promise(r=>setTimeout(r,delayMs));
596
+ }
597
+ stopBtn.style.display='none';sendBtn.style.display='';
598
+ if(!bulkAbort)$('eng-push-result').innerHTML='<div class="success-banner">Sent '+sent+' packets</div>';
599
+ else $('eng-push-result').innerHTML='<div class="success-banner">Stopped after '+sent+' / '+lines.length+'</div>';
600
+ }
601
+
602
+ // Single push
603
+ $('eng-push').addEventListener('click',async function(){
604
+ if(!activeEngagement)return;this.textContent='...';
605
+ try{await pushOne($('eng-stream-payload').value);
606
+ $('eng-push-result').innerHTML='<div class="success-banner">Pushed <span class="fe-dim">seq '+seqNum+'</span></div>';
607
+ }catch(e){$('eng-push-result').innerHTML='<div class="error-banner">'+esc(e.message)+'</div>'}
608
+ this.textContent='Push';
609
+ });
610
+
611
+ // Bulk send
612
+ $('eng-bulk-send').addEventListener('click',async function(){
613
+ if(!activeEngagement)return;
614
+ const lines=parseLines($('eng-bulk-payload').value);
615
+ if(!lines.length)return;
616
+ const delay=parseInt($('eng-bulk-delay').value)||0;
617
+ await sendLines(lines,delay,$('eng-bulk-progress'),$('eng-bulk-stop'),this);
618
+ });
619
+ $('eng-bulk-stop').addEventListener('click',()=>{bulkAbort=true});
620
+
621
+ // File upload
622
+ let fileLines=[];
623
+ $('file-drop-zone').addEventListener('click',()=>$('eng-file-input').click());
624
+ $('file-drop-zone').addEventListener('dragover',(e)=>{e.preventDefault();e.currentTarget.classList.add('drag-over')});
625
+ $('file-drop-zone').addEventListener('dragleave',(e)=>{e.currentTarget.classList.remove('drag-over')});
626
+ $('file-drop-zone').addEventListener('drop',(e)=>{e.preventDefault();e.currentTarget.classList.remove('drag-over');if(e.dataTransfer.files.length)handleFile(e.dataTransfer.files[0])});
627
+ $('eng-file-input').addEventListener('change',(e)=>{if(e.target.files.length)handleFile(e.target.files[0])});
628
+
629
+ function handleFile(file){
630
+ const reader=new FileReader();
631
+ reader.onload=(e)=>{
632
+ fileLines=parseLines(e.target.result);
633
+ $('file-info').textContent=file.name+' ('+fileLines.length+' events)';
634
+ $('eng-file-send').disabled=false;
635
+ };
636
+ reader.readAsText(file);
637
+ }
638
+
639
+ $('eng-file-send').addEventListener('click',async function(){
640
+ if(!activeEngagement||!fileLines.length)return;
641
+ const delay=parseInt($('eng-file-delay').value)||0;
642
+ await sendLines(fileLines,delay,$('eng-file-progress'),$('eng-file-stop'),this);
643
+ });
644
+ $('eng-file-stop').addEventListener('click',()=>{bulkAbort=true});
645
+
646
+ // ─── Agent actions ───
647
+ $('agent-call').addEventListener('click',async function(){
648
+ const lbl=$('agent-label'),alias=lbl.dataset.alias,task=lbl.dataset.task;
649
+ let payload;try{payload=JSON.parse($('agent-payload').value)}catch{payload={}}
650
+ this.textContent='...';
651
+ try{
652
+ const r=await api('POST','/api/agents/'+alias+'/'+task,{payload});
653
+ if(r.error&&!r.success){$('agent-result').innerHTML='<div class="error-banner">'+esc(r.error)+'</div>'}
654
+ else{$('agent-result').innerHTML='<div class="result"><pre>'+esc(JSON.stringify(r.result,null,2))+'</pre></div><div class="result-meta"><span>'+(r.durationMs||0).toFixed(1)+'ms</span><span class="badge '+(r.success?'badge-ok':'badge-err')+'">'+(r.success?'ok':'error')+'</span></div>'}
655
+ }catch(e){$('agent-result').innerHTML='<div class="error-banner">'+esc(e.message)+'</div>'}
656
+ this.textContent='Call';
657
+ });
658
+
659
+ $('agent-batch').addEventListener('click',async function(){
660
+ if(batchRunning){batchAbort=true;return;}
661
+ const lbl=$('agent-label'),alias=lbl.dataset.alias,task=lbl.dataset.task;
662
+ const batchSql=this.dataset.batchSql,batchCubby=this.dataset.batchCubby;
663
+ if(!batchSql||!batchCubby){$('agent-result').innerHTML='<div class="error-banner">No batch config on this task</div>';return;}
664
+ $('agent-result').innerHTML='<div class="success-banner">Querying '+esc(batchCubby)+'...</div>';
665
+ var r=await api('POST','/api/cubbies/'+batchCubby+'/query',{sql:batchSql,params:[]});
666
+ if(r.error){$('agent-result').innerHTML='<div class="error-banner">Query failed: '+esc(r.error)+'</div>';return;}
667
+ var rows=r.rows||[];
668
+ if(!rows.length){$('agent-result').innerHTML='<div class="success-banner">No rows matched — nothing to run</div>';return;}
669
+ const ac=topology.agents.find(function(a){return a.alias===alias;}),tc=ac&&ac.tasks.find(function(t){return t.alias===task;});
670
+ const paramKeys=tc&&tc.parameters&&tc.parameters.properties?Object.keys(tc.parameters.properties):[];
671
+ batchRunning=true;batchAbort=false;
672
+ this.textContent='Stop';this.classList.replace('btn-secondary','btn-danger');
673
+ var ok=0,errors=0,i=0;
674
+ for(;i<rows.length;i++){
675
+ if(batchAbort)break;
676
+ $('agent-result').innerHTML='<div class="success-banner">Running '+(i+1)+'/'+rows.length+' — '+ok+' ok'+( errors?' '+errors+' err':'')+'</div>';
677
+ var payload={};
678
+ for(var j=0;j<paramKeys.length&&j<rows[i].length;j++)payload[paramKeys[j]]=rows[i][j];
679
+ try{
680
+ var res=await api('POST','/api/agents/'+alias+'/'+task,{payload});
681
+ if(res.error&&!res.success)errors++;else ok++;
682
+ }catch(e){errors++;}
683
+ await new Promise(function(r){setTimeout(r,300);});
684
+ }
685
+ batchRunning=false;
686
+ this.textContent='Run Batch';this.classList.replace('btn-danger','btn-secondary');
687
+ var msg=batchAbort?'Stopped after '+i+'/'+rows.length+' — '+ok+' ok':'Done — '+ok+'/'+rows.length+' ok'+(errors?' ('+errors+' errors)':'');
688
+ $('agent-result').innerHTML='<div class="'+(errors||batchAbort?'error-banner':'success-banner')+'">'+esc(msg)+'</div>';
689
+ });
690
+
691
+ // ─── Cubby tabs ───
692
+ document.querySelectorAll('#panel-cubby .tab').forEach(tab=>{
693
+ tab.addEventListener('click',()=>{
694
+ document.querySelectorAll('#panel-cubby .tab').forEach(t=>t.classList.remove('active'));tab.classList.add('active');
695
+ $('cubby-browse').style.display=tab.dataset.tab==='browse'?'':'none';
696
+ $('cubby-sql').style.display=tab.dataset.tab==='sql'?'':'none';
697
+ $('cubby-graph').style.display=tab.dataset.tab==='graph'?'':'none';
698
+ if(tab.dataset.tab==='graph'){$('panel-cubby').classList.add('panel-wide');loadCubbyGraph($('cubby-label').dataset.alias);}
699
+ else if(tab.dataset.tab==='sql'){$('panel-cubby').classList.add('panel-wide');}
700
+ else $('panel-cubby').classList.remove('panel-wide');
701
+ });
702
+ });
703
+
704
+ // ─── Graph tab ───
705
+ const GRAPH_NODE_COLORS={
706
+ artifact:{bg:'#6366f1',border:'#818cf8'},
707
+ work:{bg:'#f59e0b',border:'#fbbf24'},
708
+ operator:{bg:'#22c55e',border:'#4ade80'},
709
+ eval:{bg:'#ec4899',border:'#f472b6'},
710
+ artifact_map:{bg:'#52525b',border:'#71717a'},
711
+ };
712
+ let graphNet=null,graphVisNodes=null,hiddenTypes=new Set();
713
+
714
+ async function loadCubbyGraph(alias){
715
+ const canvas=$('graph-canvas');
716
+ canvas.innerHTML='<div class="empty">Loading graph...</div>';
717
+ $('graph-detail').style.display='none';
718
+ $('graph-stats').textContent='';
719
+ try{
720
+ const [nr,er]=await Promise.all([
721
+ api('POST','/api/cubbies/'+alias+'/query',{sql:"SELECT id,version,type,ref,created_at,attribute FROM nodes"}),
722
+ api('POST','/api/cubbies/'+alias+'/query',{sql:"SELECT id,src,type,dst FROM edges"}),
723
+ ]);
724
+ if(nr.error){canvas.innerHTML='<div class="empty">This cubby has no nodes table — graph view requires nodes + edges tables</div>';return}
725
+ const rows=nr.rows||[],erows=er.rows||[];
726
+ if(!rows.length){canvas.innerHTML='<div class="empty">No nodes yet — run an ingest first</div>';return}
727
+
728
+ // Build vis nodes — ALL types, ALL versions as separate nodes
729
+ const visNodes=[],versionedNode={},latestVer={},nodeVisId={};
730
+ let idx=1;
731
+ for(const r of rows){
732
+ const[id,ver,type,ref,created_at,attr]=r;
733
+ const col=GRAPH_NODE_COLORS[type]||GRAPH_NODE_COLORS.artifact_map;
734
+ const a=safeJ(attr);
735
+ const label=type==='artifact'?(a.title||ref||id).slice(0,22)+'\\nv'+ver
736
+ :type==='operator'?(a.name||ref||'operator').slice(0,18)
737
+ :type==='work'?(a.type||'work')+'\\nv'+ver
738
+ :(type+' v'+ver);
739
+ const shape=type==='operator'?'diamond':type==='work'?'box':'ellipse';
740
+ visNodes.push({id:idx,label,
741
+ color:{background:col.bg,border:col.border,highlight:{background:col.border,border:'#fff'}},
742
+ font:{color:'#fff',size:11,face:"'SF Mono','Fira Code',monospace",multi:false},
743
+ shape,borderWidth:2,
744
+ _raw:{id,ver,type,ref,created_at,attr}});
745
+ nodeVisId[id+'|'+ver]=idx;
746
+ if(!versionedNode[id])versionedNode[id]={};
747
+ versionedNode[id][ver]=idx;
748
+ if(!latestVer[id]||parseInt(ver)>parseInt(latestVer[id].ver))latestVer[id]={ver,visId:idx};
749
+ idx++;
750
+ }
751
+
752
+ // Build vis edges
753
+ // nodeVer[id] = version from the nodes table (single row per unique id for work/operator/eval)
754
+ const nodeVer={};
755
+ for(const r of rows)nodeVer[r[0]]=r[1];
756
+
757
+ const visEdges=[];
758
+ for(const r of erows){
759
+ const[eid,src,type,dst]=r;
760
+ // src: use its single vis node (work/operator each have one entry)
761
+ const sVisId=latestVer[src]?.visId;
762
+ // dst: for versioned nodes (artifact), prefer version matching src's version
763
+ const srcVersion=nodeVer[src];
764
+ const dVisId=(versionedNode[dst]&&srcVersion&&versionedNode[dst][srcVersion])
765
+ ?versionedNode[dst][srcVersion]
766
+ :latestVer[dst]?.visId;
767
+ if(!sVisId||!dVisId)continue;
768
+ visEdges.push({id:eid,from:sVisId,to:dVisId,label:type,
769
+ font:{size:9,color:'#52525b',align:'middle'},
770
+ color:{color:'#3f3f46',highlight:'#818cf8'},
771
+ arrows:{to:{enabled:true,scaleFactor:0.5}},
772
+ smooth:{type:'curvedCW',roundness:0.2}});
773
+ }
774
+
775
+ // Synthetic PREV_VERSION dashed edges — chain artifact versions into a timeline
776
+ for(const[nodeId,verMap]of Object.entries(versionedNode)){
777
+ const ntype=rows.find(function(r){return r[0]===nodeId;})?.[2];
778
+ if(ntype!=='artifact')continue;
779
+ const versions=Object.keys(verMap).map(Number).sort(function(a,b){return a-b;});
780
+ for(var vi=1;vi<versions.length;vi++){
781
+ const fromVisId=verMap[String(versions[vi-1])],toVisId=verMap[String(versions[vi])];
782
+ if(!fromVisId||!toVisId)continue;
783
+ visEdges.push({id:nodeId+'|PREV|v'+versions[vi],from:fromVisId,to:toVisId,
784
+ label:'',dashes:true,
785
+ font:{size:8,color:'#3f3f46',align:'middle'},
786
+ color:{color:'#3f3f46',highlight:'#52525b'},
787
+ arrows:{to:{enabled:true,scaleFactor:0.3}},
788
+ smooth:{type:'continuous'}});
789
+ }
790
+ }
791
+
792
+ // Count node types for legend
793
+ const typeCounts={};
794
+ for(const n of visNodes)typeCounts[n._raw.type]=(typeCounts[n._raw.type]||0)+1;
795
+
796
+ $('graph-stats').innerHTML=visNodes.length+' nodes · '+visEdges.length+' edges';
797
+
798
+ // Legend toggles — click to show/hide each type without full reload
799
+ var legendHtml='<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px">';
800
+ for(const[type,col]of Object.entries(GRAPH_NODE_COLORS)){
801
+ if(!typeCounts[type])continue;
802
+ const isHidden=hiddenTypes.has(type);
803
+ legendHtml+='<button onclick="window.graphToggleType(\\''+type+'\\')" style="display:flex;align-items:center;gap:5px;background:'+(isHidden?'var(--surface2)':col.bg+'22')+';border:1px solid '+(isHidden?'var(--border)':col.bg+'66')+';color:'+(isHidden?'var(--text3)':col.bg)+';border-radius:4px;padding:2px 8px;font-size:10px;cursor:pointer">'
804
+ +'<span style="width:8px;height:8px;border-radius:50%;background:'+(isHidden?'var(--border)':col.bg)+';display:inline-block"></span>'+esc(type)+' <span style="opacity:.6">'+typeCounts[type]+'</span></button>';
805
+ }
806
+ legendHtml+='</div>';
807
+ $('graph-stats').innerHTML+= legendHtml;
808
+
809
+ canvas.innerHTML='';
810
+ if(graphNet)graphNet.destroy();
811
+ graphVisNodes=new window.vis.DataSet(visNodes.map(function(n){return Object.assign({},n,{hidden:hiddenTypes.has(n._raw.type)});}));
812
+ graphNet=new window.vis.Network(canvas,
813
+ {nodes:graphVisNodes,edges:new window.vis.DataSet(visEdges)},
814
+ {physics:{barnesHut:{gravitationalConstant:-12000,springLength:200,springConstant:0.02,damping:0.15,centralGravity:0.05},stabilization:{iterations:200}},
815
+ interaction:{hover:true},layout:{improvedLayout:rows.length<80}});
816
+
817
+ graphNet.on('click',(p)=>{
818
+ if(!p.nodes.length){$('graph-detail').style.display='none';return}
819
+ const n=visNodes.find(v=>v.id===p.nodes[0]);if(!n)return;
820
+ const{id,ver,type,ref,created_at,attr}=n._raw,a=safeJ(attr);
821
+ const col=GRAPH_NODE_COLORS[type]||GRAPH_NODE_COLORS.artifact_map;
822
+ $('gd-type-badge').textContent=type;
823
+ $('gd-type-badge').style.cssText='background:'+col.bg+'22;color:'+col.bg+';border:1px solid '+col.bg+'44';
824
+ const rows=[['id',id],['version',ver],['ref',ref],['created_at',created_at],
825
+ ...Object.entries(a).filter(([k])=>k!=='segments'&&k!=='embedding').map(([k,v])=>[k,typeof v==='object'?JSON.stringify(v):String(v??'')])
826
+ ].filter(([,v])=>v&&v!=='null');
827
+ if(a.segments?.length)rows.push(['segments',a.segments.length+' blocks']);
828
+ $('gd-attrs').innerHTML=rows.map(([k,v])=>'<div class="gd-row"><span class="gd-key">'+esc(k)+'</span><span class="gd-val">'+esc(String(v))+'</span></div>').join('');
829
+ $('graph-detail').style.display='';
830
+ });
831
+ }catch(e){canvas.innerHTML='<div class="error-banner">'+esc(e.message||String(e))+'</div>'}
832
+ }
833
+
834
+ window.loadCubbyGraph=loadCubbyGraph;
835
+ window.graphToggleType=function(type){
836
+ if(hiddenTypes.has(type))hiddenTypes.delete(type);else hiddenTypes.add(type);
837
+ if(!graphVisNodes)return;
838
+ // Update node visibility in-place — no full reload
839
+ graphVisNodes.update(graphVisNodes.get().filter(function(n){return n._raw&&n._raw.type===type;}).map(function(n){return{id:n.id,hidden:hiddenTypes.has(type)};}));
840
+ // Refresh legend button states
841
+ document.querySelectorAll('#graph-stats button').forEach(function(btn){
842
+ var t=btn.textContent.trim().split(' ')[0];
843
+ var col=GRAPH_NODE_COLORS[t];if(!col)return;
844
+ var hidden=hiddenTypes.has(t);
845
+ btn.style.background=hidden?'var(--surface2)':col.bg+'22';
846
+ btn.style.borderColor=hidden?'var(--border)':col.bg+'66';
847
+ btn.style.color=hidden?'var(--text3)':col.bg;
848
+ var dot=btn.querySelector('span');if(dot)dot.style.background=hidden?'var(--border)':col.bg;
849
+ });
850
+ };
851
+ function safeJ(s){try{return JSON.parse(s)}catch{return{}}}
852
+ async function loadCubbyBrowse(alias){
853
+ const el=$('cubby-tables');el.innerHTML='<div class="empty">Loading...</div>';
854
+ try{
855
+ const t=await api('POST','/api/cubbies/'+alias+'/query',{sql:"SELECT name FROM sqlite_master WHERE type='table' AND name != 'schema_migrations'"});
856
+ if(!t.rows||!t.rows.length){el.innerHTML='<div class="empty">No tables</div>';return}
857
+ let h='';
858
+ for(const row of t.rows){
859
+ const tn=row[0],cr=await api('POST','/api/cubbies/'+alias+'/query',{sql:'SELECT COUNT(*) FROM "'+tn+'"'});
860
+ h+='<div class="cubby-table-item" onclick="browseCubbyTable(\\''+ea(alias)+'\\',\\''+ea(tn)+'\\')">';
861
+ h+='<span class="cti-name">'+esc(tn)+'</span><span class="cti-count">'+(cr.rows?.[0]?.[0]??'?')+' rows</span></div>';
862
+ }
863
+ el.innerHTML=h;
864
+ }catch(e){el.innerHTML='<div class="error-banner">'+esc(e.message)+'</div>'}
865
+ }
866
+ window.browseCubbyTable=async function(alias,table){
867
+ const el=$('cubby-tables');
868
+ try{const r=await api('POST','/api/cubbies/'+alias+'/query',{sql:'SELECT * FROM "'+table+'" LIMIT 100'});
869
+ el.innerHTML='<div style="display:flex;gap:8px;justify-content:flex-end;margin-bottom:8px">'
870
+ +'<button class="btn-ghost" onclick="loadCubbyBrowse(\\''+ea(alias)+'\\')">\\u2190 Back</button>'
871
+ +'<button class="btn-danger" style="font-size:11px;padding:5px 12px" onclick="clearCubbyTable(\\''+ea(alias)+'\\',\\''+ea(table)+'\\')">Clear table</button>'
872
+ +'</div>'+renderTable(r);
873
+ }catch(e){el.innerHTML='<div class="error-banner">'+esc(e.message)+'</div>'}
874
+ };
875
+ window.clearCubbyTable=async function(alias,table){
876
+ if(!confirm('Delete all rows from "'+table+'"?'))return;
877
+ try{
878
+ await api('POST','/api/cubbies/'+alias+'/exec',{sql:'DELETE FROM "'+table+'"'});
879
+ window.browseCubbyTable(alias,table);
880
+ }catch(e){alert('Error: '+e.message);}
881
+ };
882
+ window.loadCubbyBrowse=loadCubbyBrowse;
883
+
884
+ $('cubby-run').addEventListener('click',async function(){
885
+ const alias=$('cubby-label').dataset.alias,sql=$('cubby-sql-input').value.trim();if(!sql)return;
886
+ const isW=/^\\s*(INSERT|UPDATE|DELETE|CREATE|DROP|ALTER)/i.test(sql),ep=isW?'exec':'query';
887
+ this.textContent='...';
888
+ try{const r=await api('POST','/api/cubbies/'+alias+'/'+ep,{sql});
889
+ if(r.error){$('cubby-sql-result').innerHTML='<div class="error-banner">'+esc(r.error)+'</div>'}
890
+ else if(r.columns){$('cubby-sql-result').innerHTML=renderTable(r)+'<div class="result-meta"><span>'+r.meta.rowsRead+' rows</span><span>'+(r.meta.duration*1000).toFixed(2)+'ms</span></div>'}
891
+ else{$('cubby-sql-result').innerHTML='<div class="success-banner">'+(r.rowsAffected??0)+' rows affected</div>'}
892
+ }catch(e){$('cubby-sql-result').innerHTML='<div class="error-banner">'+esc(e.message)+'</div>'}
893
+ this.textContent='Run';
894
+ });
895
+
896
+ // ─── Query template manager (stored in cubby saved_queries table) ───
897
+ async function qtRefreshSelect(alias){
898
+ var sel=$('query-select'),prev=sel.value;
899
+ try{
900
+ var r=await api('POST','/api/cubbies/'+alias+'/query',{sql:'SELECT name FROM saved_queries ORDER BY name',params:[]});
901
+ sel.innerHTML='<option value="">— select a saved query —</option>';
902
+ (r.rows||[]).forEach(function(row){var o=document.createElement('option');o.value=row[0];o.textContent=row[0];sel.appendChild(o);});
903
+ sel.value=prev||'';
904
+ $('query-delete').style.display=sel.value?'':'none';
905
+ }catch(e){}
906
+ }
907
+ $('query-select').addEventListener('change',async function(){
908
+ var alias=$('cubby-label').dataset.alias,name=this.value;
909
+ $('query-delete').style.display=name?'':'none';
910
+ if(!name)return;
911
+ try{
912
+ var r=await api('POST','/api/cubbies/'+alias+'/query',{sql:'SELECT sql_text FROM saved_queries WHERE name=?',params:[name]});
913
+ if(r.rows&&r.rows[0]){$('cubby-sql-input').value=r.rows[0][0];$('query-name').value=name;}
914
+ }catch(e){}
915
+ });
916
+ $('query-save').addEventListener('click',async function(){
917
+ var alias=$('cubby-label').dataset.alias,name=$('query-name').value.trim(),sql=$('cubby-sql-input').value.trim();
918
+ if(!name||!sql)return;
919
+ try{
920
+ await api('POST','/api/cubbies/'+alias+'/exec',{sql:'INSERT OR REPLACE INTO saved_queries(name,sql_text) VALUES(?,?)',params:[name,sql]});
921
+ await qtRefreshSelect(alias);$('query-select').value=name;$('query-delete').style.display='';
922
+ }catch(e){}
923
+ });
924
+ $('query-delete').addEventListener('click',async function(){
925
+ var alias=$('cubby-label').dataset.alias,name=$('query-select').value;if(!name)return;
926
+ try{
927
+ await api('POST','/api/cubbies/'+alias+'/exec',{sql:'DELETE FROM saved_queries WHERE name=?',params:[name]});
928
+ await qtRefreshSelect(alias);$('query-name').value='';
929
+ }catch(e){}
930
+ });
931
+
932
+ // ─── Bottom panel: tabs, resize, collapse ───
933
+ document.querySelectorAll('.btab').forEach(tab=>{
934
+ tab.addEventListener('click',()=>{
935
+ document.querySelectorAll('.btab').forEach(t=>t.classList.remove('active'));tab.classList.add('active');
936
+ $('log-container').style.display=tab.dataset.btab==='logs'?'':'none';
937
+ $('flow-container').style.display=tab.dataset.btab==='flow'?'':'none';
938
+ });
939
+ });
940
+ $('log-filter').addEventListener('input',function(){
941
+ logFilter=this.value.toLowerCase();
942
+ $('log-container').querySelectorAll('.log-line').forEach(l=>{l.style.display=(!logFilter||l.textContent.toLowerCase().includes(logFilter))?'':'none'});
943
+ });
944
+ $('btn-clear').addEventListener('click',()=>{$('log-container').innerHTML='';$('flow-container').innerHTML=''});
945
+ $('btn-collapse').addEventListener('click',function(){
946
+ const b=$('bottom');
947
+ if(b.classList.toggle('collapsed')){this.innerHTML='&#9650;';this.title='Expand'}
948
+ else{this.innerHTML='&#9660;';this.title='Collapse'}
949
+ });
950
+
951
+ // Resize handle drag
952
+ (function(){
953
+ const handle=$('resize-handle'),bottom=$('bottom');
954
+ let startY,startH;
955
+ handle.addEventListener('mousedown',(e)=>{
956
+ e.preventDefault();startY=e.clientY;startH=bottom.offsetHeight;
957
+ const onMove=(e)=>{const h=Math.max(40,Math.min(window.innerHeight*0.7,startH-(e.clientY-startY)));bottom.style.height=h+'px';bottom.classList.remove('collapsed')};
958
+ const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp)};
959
+ document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);
960
+ });
961
+ })();
962
+
963
+ // Tab in textareas
964
+ document.querySelectorAll('textarea').forEach(ta=>{
965
+ ta.addEventListener('keydown',function(e){if(e.key==='Tab'){e.preventDefault();const s=this.selectionStart;this.value=this.value.substring(0,s)+' '+this.value.substring(this.selectionEnd);this.selectionStart=this.selectionEnd=s+2}});
966
+ });
967
+
968
+ // ─── Helpers ───
969
+ function $(id){return document.getElementById(id)}
970
+ function renderTable(r){
971
+ if(!r.columns||!r.rows)return'';let h='<div class="table-wrap"><table><thead><tr>';
972
+ for(const c of r.columns)h+='<th>'+esc(c)+'</th>';h+='</tr></thead><tbody>';
973
+ for(const row of r.rows){h+='<tr>';for(const cell of row)h+='<td>'+esc(cell==null?'NULL':String(cell))+'</td>';h+='</tr>'}
974
+ return h+'</tbody></table></div>';
975
+ }
976
+ function esc(s){if(s==null)return'';return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
977
+ function ea(s){return String(s).replace(/'/g,"\\\\'").replace(/"/g,'&quot;')}
978
+
979
+ loadTopology();connectWs();
980
+ })();
981
+ </script>
982
+ </body>
983
+ </html>`;
984
+ }