nexus-prime 3.2.2 → 3.3.0
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/agents/adapters/mcp.d.ts +1 -0
- package/dist/agents/adapters/mcp.d.ts.map +1 -1
- package/dist/agents/adapters/mcp.js +43 -1
- package/dist/agents/adapters/mcp.js.map +1 -1
- package/dist/dashboard/index.html +331 -46
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +305 -201
- package/dist/dashboard/server.js.map +1 -1
- package/dist/engines/client-registry.d.ts.map +1 -1
- package/dist/engines/client-registry.js +12 -1
- package/dist/engines/client-registry.js.map +1 -1
- package/package.json +1 -1
package/dist/dashboard/server.js
CHANGED
|
@@ -20,6 +20,7 @@ const REQUIRED_CAPABILITIES = {
|
|
|
20
20
|
};
|
|
21
21
|
export class DashboardServer {
|
|
22
22
|
server;
|
|
23
|
+
cachedDashboardHtml = null;
|
|
23
24
|
clients = new Set();
|
|
24
25
|
unsubscribeBus = null;
|
|
25
26
|
runtimeProvider;
|
|
@@ -108,220 +109,312 @@ export class DashboardServer {
|
|
|
108
109
|
console.error(`[Dashboard] Topology console listening at ${this.dashboardUrl}`);
|
|
109
110
|
}
|
|
110
111
|
async requestHandler(req, res) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
|
|
117
|
-
this.serveDashboard(res);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
if (req.method === 'GET' && url.pathname === '/stream') {
|
|
121
|
-
this.serveSSE(req, res);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
if (req.method === 'GET' && url.pathname === '/api/runs') {
|
|
125
|
-
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
126
|
-
this.respondJson(res, this.getRuntime()?.listRuns(limit) ?? []);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (req.method === 'GET' && url.pathname.startsWith('/api/runs/')) {
|
|
130
|
-
const runId = decodeURIComponent(url.pathname.replace('/api/runs/', ''));
|
|
131
|
-
const run = this.getRuntime()?.getRun(runId);
|
|
132
|
-
this.respondJson(res, run ?? { error: 'run-not-found', runId }, run ? 200 : 404);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (req.method === 'GET' && url.pathname === '/api/skills') {
|
|
136
|
-
this.respondJson(res, this.getRuntime()?.listSkills() ?? []);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (req.method === 'GET' && url.pathname === '/api/workflows') {
|
|
140
|
-
this.respondJson(res, this.getRuntime()?.listWorkflows() ?? []);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
if (req.method === 'GET' && url.pathname === '/api/backends') {
|
|
144
|
-
this.respondJson(res, this.getRuntime()?.getBackendCatalog() ?? {});
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (req.method === 'GET' && url.pathname === '/api/health') {
|
|
148
|
-
this.respondJson(res, this.collectHealth());
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
if (req.method === 'GET' && url.pathname === '/api/memory') {
|
|
152
|
-
const limit = parseInt(url.searchParams.get('limit') || '40', 10);
|
|
153
|
-
const tier = url.searchParams.get('tier') ?? undefined;
|
|
154
|
-
const tag = url.searchParams.get('tag') ?? undefined;
|
|
155
|
-
const linkedType = url.searchParams.get('linkedType') ?? undefined;
|
|
156
|
-
const recencyMs = url.searchParams.get('recencyMs');
|
|
157
|
-
const memory = this.getMemory();
|
|
158
|
-
this.respondJson(res, memory?.listSnapshots(limit, {
|
|
159
|
-
tier: tier,
|
|
160
|
-
tag,
|
|
161
|
-
linkedType: linkedType,
|
|
162
|
-
recencyMs: recencyMs ? parseInt(recencyMs, 10) : undefined,
|
|
163
|
-
}) ?? []);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
if (req.method === 'GET' && url.pathname.startsWith('/api/memory/') && url.pathname.endsWith('/network')) {
|
|
167
|
-
const id = decodeURIComponent(url.pathname.replace('/api/memory/', '').replace('/network', '').replace(/\/$/, ''));
|
|
168
|
-
const depth = parseInt(url.searchParams.get('depth') || '2', 10);
|
|
169
|
-
const limit = parseInt(url.searchParams.get('limit') || '18', 10);
|
|
170
|
-
const memory = this.getMemory();
|
|
171
|
-
this.respondJson(res, memory?.getNetworkSnapshot(id, depth, limit) ?? { focusId: id, nodes: [], links: [] });
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
if (req.method === 'GET' && url.pathname.startsWith('/api/memory/')) {
|
|
175
|
-
const id = decodeURIComponent(url.pathname.replace('/api/memory/', ''));
|
|
176
|
-
const memory = this.getMemory();
|
|
177
|
-
const detail = memory?.getDetail(id);
|
|
178
|
-
this.respondJson(res, detail ?? { error: 'memory-not-found', id }, detail ? 200 : 404);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
if (req.method === 'GET' && url.pathname === '/api/pod') {
|
|
182
|
-
const limit = parseInt(url.searchParams.get('limit') || '40', 10);
|
|
183
|
-
this.respondJson(res, podNetwork.getDashboardSnapshot(limit));
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
if (req.method === 'GET' && url.pathname.startsWith('/api/pod/')) {
|
|
187
|
-
const workerId = decodeURIComponent(url.pathname.replace('/api/pod/', ''));
|
|
188
|
-
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
189
|
-
this.respondJson(res, podNetwork.getWorker(workerId, limit));
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
if (req.method === 'GET' && url.pathname === '/api/clients') {
|
|
193
|
-
this.respondJson(res, this.getClientRegistry()?.listClients(this.getAdapters()) ?? []);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
if (req.method === 'GET' && url.pathname === '/api/events') {
|
|
197
|
-
const category = url.searchParams.get('category');
|
|
198
|
-
const type = url.searchParams.get('type');
|
|
199
|
-
const limit = parseInt(url.searchParams.get('limit') || '80', 10);
|
|
200
|
-
const events = this.getEventCards()
|
|
201
|
-
.filter((event) => !category || event.category === category)
|
|
202
|
-
.filter((event) => !type || event.type === type)
|
|
203
|
-
.slice(0, Math.max(limit, 1));
|
|
204
|
-
this.respondJson(res, events);
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
if (req.method === 'POST' && url.pathname === '/api/skills/deploy') {
|
|
208
|
-
const body = await this.readJsonBody(req);
|
|
209
|
-
const runtime = this.getRuntime();
|
|
210
|
-
const deployed = runtime?.deploySkill(String(body.skillId), body.scope);
|
|
211
|
-
if (deployed) {
|
|
212
|
-
nexusEventBus.emit('skill.deploy', {
|
|
213
|
-
skillId: deployed.skillId,
|
|
214
|
-
scope: deployed.scope,
|
|
215
|
-
status: deployed.rolloutStatus,
|
|
216
|
-
});
|
|
112
|
+
try {
|
|
113
|
+
const url = new URL(req.url || '/', this.dashboardUrl ?? this.buildUrl(this.activePort ?? DEFAULT_PORT));
|
|
114
|
+
if (req.method === 'OPTIONS') {
|
|
115
|
+
this.respondOptions(res);
|
|
116
|
+
return;
|
|
217
117
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (req.method === 'POST' && url.pathname === '/api/skills/revoke') {
|
|
222
|
-
const body = await this.readJsonBody(req);
|
|
223
|
-
const runtime = this.getRuntime();
|
|
224
|
-
const revoked = runtime?.revokeSkill(String(body.skillId));
|
|
225
|
-
if (revoked) {
|
|
226
|
-
nexusEventBus.emit('skill.revoke', {
|
|
227
|
-
skillId: revoked.skillId,
|
|
228
|
-
status: revoked.rolloutStatus,
|
|
229
|
-
});
|
|
118
|
+
if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
|
|
119
|
+
this.serveDashboard(res);
|
|
120
|
+
return;
|
|
230
121
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (req.method === 'POST' && url.pathname === '/api/workflows/deploy') {
|
|
235
|
-
const body = await this.readJsonBody(req);
|
|
236
|
-
const runtime = this.getRuntime();
|
|
237
|
-
const deployed = runtime?.deployWorkflow(String(body.workflowId), body.scope);
|
|
238
|
-
if (deployed) {
|
|
239
|
-
nexusEventBus.emit('workflow.deploy', {
|
|
240
|
-
workflowId: deployed.workflowId,
|
|
241
|
-
scope: deployed.scope,
|
|
242
|
-
status: deployed.rolloutStatus,
|
|
243
|
-
});
|
|
122
|
+
if (req.method === 'GET' && url.pathname === '/stream') {
|
|
123
|
+
this.serveSSE(req, res);
|
|
124
|
+
return;
|
|
244
125
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (req.method === 'POST' && url.pathname === '/api/workflows/run') {
|
|
249
|
-
const body = await this.readJsonBody(req);
|
|
250
|
-
const runtime = this.getRuntime();
|
|
251
|
-
if (!runtime) {
|
|
252
|
-
this.respondJson(res, { error: 'runtime-unavailable' }, 503);
|
|
126
|
+
if (req.method === 'GET' && url.pathname === '/api/runs') {
|
|
127
|
+
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
128
|
+
this.respondJson(res, this.getRuntime()?.listRuns(limit) ?? []);
|
|
253
129
|
return;
|
|
254
130
|
}
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
status: run.state,
|
|
261
|
-
});
|
|
262
|
-
this.respondJson(res, run);
|
|
131
|
+
if (req.method === 'GET' && url.pathname.startsWith('/api/runs/')) {
|
|
132
|
+
const runId = decodeURIComponent(url.pathname.replace('/api/runs/', ''));
|
|
133
|
+
const run = this.getRuntime()?.getRun(runId);
|
|
134
|
+
this.respondJson(res, run ?? { error: 'run-not-found', runId }, run ? 200 : 404);
|
|
135
|
+
return;
|
|
263
136
|
}
|
|
264
|
-
|
|
265
|
-
this.respondJson(res,
|
|
137
|
+
if (req.method === 'GET' && url.pathname === '/api/skills') {
|
|
138
|
+
this.respondJson(res, this.getRuntime()?.listSkills() ?? []);
|
|
139
|
+
return;
|
|
266
140
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (req.method === 'POST' && url.pathname === '/api/runtime/execute') {
|
|
270
|
-
const body = await this.readJsonBody(req);
|
|
271
|
-
const runtime = this.getRuntime();
|
|
272
|
-
if (!runtime) {
|
|
273
|
-
this.respondJson(res, { error: 'runtime-unavailable' }, 503);
|
|
141
|
+
if (req.method === 'GET' && url.pathname === '/api/workflows') {
|
|
142
|
+
this.respondJson(res, this.getRuntime()?.listWorkflows() ?? []);
|
|
274
143
|
return;
|
|
275
144
|
}
|
|
276
|
-
if (
|
|
277
|
-
this.respondJson(res,
|
|
145
|
+
if (req.method === 'GET' && url.pathname === '/api/backends') {
|
|
146
|
+
this.respondJson(res, this.getRuntime()?.getBackendCatalog() ?? {});
|
|
278
147
|
return;
|
|
279
148
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
action: 'runtime.execute',
|
|
283
|
-
status: run.state,
|
|
284
|
-
target: run.runId,
|
|
285
|
-
});
|
|
286
|
-
this.respondJson(res, run, 201);
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
if (req.method === 'POST' && url.pathname.startsWith('/api/clients/') && url.pathname.endsWith('/reconnect')) {
|
|
290
|
-
const clientId = decodeURIComponent(url.pathname.replace('/api/clients/', '').replace('/reconnect', '').replace(/\/$/, ''));
|
|
291
|
-
const registry = this.getClientRegistry();
|
|
292
|
-
if (!registry) {
|
|
293
|
-
this.respondJson(res, { error: 'client-registry-unavailable' }, 503);
|
|
149
|
+
if (req.method === 'GET' && url.pathname === '/api/health') {
|
|
150
|
+
this.respondJson(res, this.collectHealth());
|
|
294
151
|
return;
|
|
295
152
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
this.respondJson(res, { error: 'client-registry-unavailable' }, 503);
|
|
153
|
+
if (req.method === 'GET' && url.pathname === '/api/memory') {
|
|
154
|
+
const limit = parseInt(url.searchParams.get('limit') || '40', 10);
|
|
155
|
+
const tier = url.searchParams.get('tier') ?? undefined;
|
|
156
|
+
const tag = url.searchParams.get('tag') ?? undefined;
|
|
157
|
+
const linkedType = url.searchParams.get('linkedType') ?? undefined;
|
|
158
|
+
const recencyMs = url.searchParams.get('recencyMs');
|
|
159
|
+
const memory = this.getMemory();
|
|
160
|
+
this.respondJson(res, memory?.listSnapshots(limit, {
|
|
161
|
+
tier: tier,
|
|
162
|
+
tag,
|
|
163
|
+
linkedType: linkedType,
|
|
164
|
+
recencyMs: recencyMs ? parseInt(recencyMs, 10) : undefined,
|
|
165
|
+
}) ?? []);
|
|
310
166
|
return;
|
|
311
167
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
168
|
+
if (req.method === 'GET' && url.pathname.startsWith('/api/memory/') && url.pathname.endsWith('/network')) {
|
|
169
|
+
const id = decodeURIComponent(url.pathname.replace('/api/memory/', '').replace('/network', '').replace(/\/$/, ''));
|
|
170
|
+
const depth = parseInt(url.searchParams.get('depth') || '2', 10);
|
|
171
|
+
const limit = parseInt(url.searchParams.get('limit') || '18', 10);
|
|
172
|
+
const memory = this.getMemory();
|
|
173
|
+
this.respondJson(res, memory?.getNetworkSnapshot(id, depth, limit) ?? { focusId: id, nodes: [], links: [] });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (req.method === 'GET' && url.pathname.startsWith('/api/memory/')) {
|
|
177
|
+
const id = decodeURIComponent(url.pathname.replace('/api/memory/', ''));
|
|
178
|
+
const memory = this.getMemory();
|
|
179
|
+
const detail = memory?.getDetail(id);
|
|
180
|
+
this.respondJson(res, detail ?? { error: 'memory-not-found', id }, detail ? 200 : 404);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (req.method === 'GET' && url.pathname === '/api/pod') {
|
|
184
|
+
const limit = parseInt(url.searchParams.get('limit') || '40', 10);
|
|
185
|
+
this.respondJson(res, podNetwork.getDashboardSnapshot(limit));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (req.method === 'GET' && url.pathname.startsWith('/api/pod/')) {
|
|
189
|
+
const workerId = decodeURIComponent(url.pathname.replace('/api/pod/', ''));
|
|
190
|
+
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
191
|
+
this.respondJson(res, podNetwork.getWorker(workerId, limit));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (req.method === 'GET' && url.pathname === '/api/clients') {
|
|
195
|
+
this.respondJson(res, this.getClientRegistry()?.listClients(this.getAdapters()) ?? []);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (req.method === 'GET' && url.pathname === '/api/events') {
|
|
199
|
+
const category = url.searchParams.get('category');
|
|
200
|
+
const type = url.searchParams.get('type');
|
|
201
|
+
const limit = parseInt(url.searchParams.get('limit') || '80', 10);
|
|
202
|
+
const events = this.getEventCards()
|
|
203
|
+
.filter((event) => !category || event.category === category)
|
|
204
|
+
.filter((event) => !type || event.type === type)
|
|
205
|
+
.slice(0, Math.max(limit, 1));
|
|
206
|
+
this.respondJson(res, events);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (req.method === 'POST' && url.pathname === '/api/skills/deploy') {
|
|
210
|
+
const body = await this.readJsonBody(req);
|
|
211
|
+
const runtime = this.getRuntime();
|
|
212
|
+
const deployed = runtime?.deploySkill(String(body.skillId), body.scope);
|
|
213
|
+
if (deployed) {
|
|
214
|
+
nexusEventBus.emit('skill.deploy', {
|
|
215
|
+
skillId: deployed.skillId,
|
|
216
|
+
scope: deployed.scope,
|
|
217
|
+
status: deployed.rolloutStatus,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
this.respondJson(res, deployed ?? { error: 'skill-not-found' }, deployed ? 200 : 404);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (req.method === 'POST' && url.pathname === '/api/skills/revoke') {
|
|
224
|
+
const body = await this.readJsonBody(req);
|
|
225
|
+
const runtime = this.getRuntime();
|
|
226
|
+
const revoked = runtime?.revokeSkill(String(body.skillId));
|
|
227
|
+
if (revoked) {
|
|
228
|
+
nexusEventBus.emit('skill.revoke', {
|
|
229
|
+
skillId: revoked.skillId,
|
|
230
|
+
status: revoked.rolloutStatus,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
this.respondJson(res, revoked ?? { error: 'skill-not-found' }, revoked ? 200 : 404);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (req.method === 'POST' && url.pathname === '/api/skills/register') {
|
|
237
|
+
const body = await this.readJsonBody(req);
|
|
238
|
+
const runtime = this.getRuntime();
|
|
239
|
+
if (!runtime) {
|
|
240
|
+
this.respondJson(res, { error: 'runtime-unavailable' }, 503);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const riskClass = (['read', 'orchestrate', 'mutate'].includes(body.riskClass) ? body.riskClass : 'orchestrate');
|
|
245
|
+
const scope = (['session', 'worker', 'global'].includes(body.scope) ? body.scope : 'session');
|
|
246
|
+
const skill = runtime.generateSkill({
|
|
247
|
+
name: String(body.name || 'unnamed-skill'),
|
|
248
|
+
instructions: String(body.instructions || ''),
|
|
249
|
+
riskClass,
|
|
250
|
+
scope,
|
|
251
|
+
});
|
|
252
|
+
nexusEventBus.emit('skill.register', {
|
|
253
|
+
name: skill.name,
|
|
254
|
+
id: skill.skillId,
|
|
255
|
+
});
|
|
256
|
+
this.respondJson(res, skill);
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
this.respondJson(res, { error: err.message || 'register-failed' }, 400);
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (req.method === 'POST' && url.pathname === '/api/skills/seed') {
|
|
264
|
+
const runtime = this.getRuntime();
|
|
265
|
+
if (!runtime) {
|
|
266
|
+
this.respondJson(res, { error: 'runtime-unavailable' }, 503);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const defaultSkills = [
|
|
270
|
+
{ name: 'code-review-playbook', instructions: 'Guide structured code review: check for bugs, security issues, performance problems, readability concerns. Produce a checklist with severity ratings.', riskClass: 'read', scope: 'global' },
|
|
271
|
+
{ name: 'token-budget-guardian', instructions: 'Monitor token usage during sessions. Warn when context exceeds 70k tokens. Suggest pruning strategies and memory offloading when approaching limits.', riskClass: 'read', scope: 'global' },
|
|
272
|
+
{ name: 'session-handover', instructions: 'At session end, generate a comprehensive summary: files modified, decisions made, open questions, recommended next steps. Store as session-summary memory.', riskClass: 'orchestrate', scope: 'global' },
|
|
273
|
+
{ name: 'memory-hygiene', instructions: 'Review stored memories for staleness, duplicates, and contradictions. Suggest pruning candidates and consolidation opportunities. Never auto-delete.', riskClass: 'read', scope: 'global' },
|
|
274
|
+
{ name: 'test-first-guard', instructions: 'Before implementing features, ensure test files exist or are planned. Prompt for test strategy if missing. Verify tests pass after implementation.', riskClass: 'orchestrate', scope: 'session' },
|
|
275
|
+
{ name: 'commit-message-crafter', instructions: 'Generate concise, semantic commit messages following conventional commits format. Analyze staged changes to infer the correct type (feat/fix/chore/refactor).', riskClass: 'read', scope: 'global' },
|
|
276
|
+
];
|
|
277
|
+
const results = [];
|
|
278
|
+
for (const skill of defaultSkills) {
|
|
279
|
+
try {
|
|
280
|
+
const existing = runtime.listSkills().find((s) => s.name === skill.name);
|
|
281
|
+
if (!existing) {
|
|
282
|
+
const registered = runtime.generateSkill(skill);
|
|
283
|
+
results.push({ name: skill.name, status: 'created', skillId: registered.skillId });
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
results.push({ name: skill.name, status: 'exists', skillId: existing.skillId });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
results.push({ name: skill.name, status: 'failed' });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
this.respondJson(res, { seeded: results });
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (req.method === 'POST' && url.pathname === '/api/workflows/deploy') {
|
|
297
|
+
const body = await this.readJsonBody(req);
|
|
298
|
+
const runtime = this.getRuntime();
|
|
299
|
+
const deployed = runtime?.deployWorkflow(String(body.workflowId), body.scope);
|
|
300
|
+
if (deployed) {
|
|
301
|
+
nexusEventBus.emit('workflow.deploy', {
|
|
302
|
+
workflowId: deployed.workflowId,
|
|
303
|
+
scope: deployed.scope,
|
|
304
|
+
status: deployed.rolloutStatus,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
this.respondJson(res, deployed ?? { error: 'workflow-not-found' }, deployed ? 200 : 404);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (req.method === 'POST' && url.pathname === '/api/workflows/run') {
|
|
311
|
+
const body = await this.readJsonBody(req);
|
|
312
|
+
const runtime = this.getRuntime();
|
|
313
|
+
if (!runtime) {
|
|
314
|
+
this.respondJson(res, { error: 'runtime-unavailable' }, 503);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
const run = await runtime.runWorkflow(String(body.workflowId), body.goal ? String(body.goal) : undefined);
|
|
319
|
+
nexusEventBus.emit('workflow.run', {
|
|
320
|
+
workflowId: String(body.workflowId),
|
|
321
|
+
runId: run.runId,
|
|
322
|
+
status: run.state,
|
|
323
|
+
});
|
|
324
|
+
this.respondJson(res, run);
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
this.respondJson(res, { error: error instanceof Error ? error.message : 'workflow-run-failed' }, 400);
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (req.method === 'POST' && url.pathname === '/api/runtime/execute') {
|
|
332
|
+
const body = await this.readJsonBody(req);
|
|
333
|
+
const runtime = this.getRuntime();
|
|
334
|
+
if (!runtime) {
|
|
335
|
+
this.respondJson(res, { error: 'runtime-unavailable' }, 503);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (!body.goal || typeof body.goal !== 'string') {
|
|
339
|
+
this.respondJson(res, { error: 'goal-required' }, 400);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const run = await runtime.run(body);
|
|
343
|
+
nexusEventBus.emit('dashboard.action', {
|
|
344
|
+
action: 'runtime.execute',
|
|
345
|
+
status: run.state,
|
|
346
|
+
target: run.runId,
|
|
347
|
+
});
|
|
348
|
+
this.respondJson(res, run, 201);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (req.method === 'POST' && url.pathname.startsWith('/api/clients/') && url.pathname.endsWith('/reconnect')) {
|
|
352
|
+
const clientId = decodeURIComponent(url.pathname.replace('/api/clients/', '').replace('/reconnect', '').replace(/\/$/, ''));
|
|
353
|
+
const registry = this.getClientRegistry();
|
|
354
|
+
if (!registry) {
|
|
355
|
+
this.respondJson(res, { error: 'client-registry-unavailable' }, 503);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const client = registry.reconnect(clientId);
|
|
359
|
+
nexusEventBus.emit('dashboard.action', {
|
|
360
|
+
action: 'client.reconnect',
|
|
361
|
+
status: 'ok',
|
|
362
|
+
target: clientId,
|
|
363
|
+
});
|
|
364
|
+
this.respondJson(res, client);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (req.method === 'POST' && url.pathname.startsWith('/api/clients/') && url.pathname.endsWith('/clear')) {
|
|
368
|
+
const clientId = decodeURIComponent(url.pathname.replace('/api/clients/', '').replace('/clear', '').replace(/\/$/, ''));
|
|
369
|
+
const registry = this.getClientRegistry();
|
|
370
|
+
if (!registry) {
|
|
371
|
+
this.respondJson(res, { error: 'client-registry-unavailable' }, 503);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
registry.clear(clientId);
|
|
375
|
+
nexusEventBus.emit('dashboard.action', {
|
|
376
|
+
action: 'client.clear',
|
|
377
|
+
status: 'ok',
|
|
378
|
+
target: clientId,
|
|
379
|
+
});
|
|
380
|
+
this.respondJson(res, { ok: true, clientId });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
res.writeHead(404);
|
|
384
|
+
res.end('Not found');
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
if (!res.headersSent) {
|
|
388
|
+
if (error instanceof Error && error.message === 'Request body too large') {
|
|
389
|
+
res.writeHead(413, { 'Content-Type': 'text/plain' });
|
|
390
|
+
res.end('Request body too large');
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
394
|
+
res.end('Internal server error');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
320
397
|
}
|
|
321
|
-
res.writeHead(404);
|
|
322
|
-
res.end('Not found');
|
|
323
398
|
}
|
|
324
399
|
serveDashboard(res) {
|
|
400
|
+
const securityHeaders = {
|
|
401
|
+
'Content-Type': 'text/html',
|
|
402
|
+
'Content-Security-Policy': [
|
|
403
|
+
"default-src 'self'",
|
|
404
|
+
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
|
405
|
+
"font-src 'self' https://fonts.gstatic.com",
|
|
406
|
+
"script-src 'unsafe-inline'",
|
|
407
|
+
"connect-src 'self'",
|
|
408
|
+
"img-src 'self' data:",
|
|
409
|
+
].join('; '),
|
|
410
|
+
'X-Content-Type-Options': 'nosniff',
|
|
411
|
+
'X-Frame-Options': 'DENY',
|
|
412
|
+
};
|
|
413
|
+
if (this.cachedDashboardHtml) {
|
|
414
|
+
res.writeHead(200, securityHeaders);
|
|
415
|
+
res.end(this.cachedDashboardHtml);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
325
418
|
const htmlPath = path.join(__dirname, 'index.html');
|
|
326
419
|
fs.readFile(htmlPath, 'utf8', (err, data) => {
|
|
327
420
|
if (err) {
|
|
@@ -329,7 +422,8 @@ export class DashboardServer {
|
|
|
329
422
|
res.end('Error loading dashboard HTML');
|
|
330
423
|
return;
|
|
331
424
|
}
|
|
332
|
-
|
|
425
|
+
this.cachedDashboardHtml = data;
|
|
426
|
+
res.writeHead(200, securityHeaders);
|
|
333
427
|
res.end(data);
|
|
334
428
|
});
|
|
335
429
|
}
|
|
@@ -338,7 +432,7 @@ export class DashboardServer {
|
|
|
338
432
|
'Content-Type': 'text/event-stream',
|
|
339
433
|
'Cache-Control': 'no-cache',
|
|
340
434
|
Connection: 'keep-alive',
|
|
341
|
-
'Access-Control-Allow-Origin':
|
|
435
|
+
'Access-Control-Allow-Origin': this.getCorsOrigin(),
|
|
342
436
|
});
|
|
343
437
|
res.write('retry: 3000\n\n');
|
|
344
438
|
res.write(`event: bootstrap\ndata: ${JSON.stringify({ connected: true, timestamp: Date.now() })}\n\n`);
|
|
@@ -363,9 +457,12 @@ export class DashboardServer {
|
|
|
363
457
|
res.write(dataStr);
|
|
364
458
|
}
|
|
365
459
|
}
|
|
460
|
+
getCorsOrigin() {
|
|
461
|
+
return this.dashboardUrl || `http://${HOST}:${this.activePort || DEFAULT_PORT}`;
|
|
462
|
+
}
|
|
366
463
|
respondOptions(res) {
|
|
367
464
|
res.writeHead(204, {
|
|
368
|
-
'Access-Control-Allow-Origin':
|
|
465
|
+
'Access-Control-Allow-Origin': this.getCorsOrigin(),
|
|
369
466
|
'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
|
|
370
467
|
'Access-Control-Allow-Headers': 'Content-Type',
|
|
371
468
|
});
|
|
@@ -375,14 +472,21 @@ export class DashboardServer {
|
|
|
375
472
|
res.writeHead(statusCode, {
|
|
376
473
|
'Content-Type': 'application/json',
|
|
377
474
|
'Cache-Control': 'no-cache',
|
|
378
|
-
'Access-Control-Allow-Origin':
|
|
475
|
+
'Access-Control-Allow-Origin': this.getCorsOrigin(),
|
|
379
476
|
});
|
|
380
477
|
res.end(JSON.stringify(data, null, 2));
|
|
381
478
|
}
|
|
382
479
|
async readJsonBody(req) {
|
|
480
|
+
const MAX_BODY = 1024 * 1024; // 1MB
|
|
383
481
|
const chunks = [];
|
|
482
|
+
let totalLength = 0;
|
|
384
483
|
for await (const chunk of req) {
|
|
385
|
-
|
|
484
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
485
|
+
totalLength += buf.length;
|
|
486
|
+
if (totalLength > MAX_BODY) {
|
|
487
|
+
throw new Error('Request body too large');
|
|
488
|
+
}
|
|
489
|
+
chunks.push(buf);
|
|
386
490
|
}
|
|
387
491
|
if (!chunks.length)
|
|
388
492
|
return {};
|