bosun 0.37.0 → 0.37.2
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/.env.example +4 -1
- package/agent-tool-config.mjs +338 -0
- package/bosun-skills.mjs +59 -4
- package/bosun.schema.json +1 -1
- package/desktop/launch.mjs +18 -0
- package/desktop/main.mjs +52 -13
- package/fleet-coordinator.mjs +34 -1
- package/kanban-adapter.mjs +30 -3
- package/library-manager.mjs +66 -0
- package/maintenance.mjs +30 -5
- package/monitor.mjs +56 -0
- package/package.json +4 -1
- package/setup-web-server.mjs +73 -12
- package/setup.mjs +3 -3
- package/ui/app.js +40 -3
- package/ui/components/session-list.js +25 -7
- package/ui/components/workspace-switcher.js +48 -1
- package/ui/demo.html +176 -0
- package/ui/modules/mic-track-registry.js +83 -0
- package/ui/modules/settings-schema.js +4 -1
- package/ui/modules/state.js +25 -0
- package/ui/modules/streaming.js +1 -1
- package/ui/modules/voice-barge-in.js +27 -0
- package/ui/modules/voice-client-sdk.js +268 -42
- package/ui/modules/voice-client.js +665 -61
- package/ui/modules/voice-overlay.js +829 -47
- package/ui/setup.html +151 -9
- package/ui/styles.css +258 -0
- package/ui/tabs/chat.js +11 -0
- package/ui/tabs/library.js +890 -15
- package/ui/tabs/settings.js +51 -11
- package/ui/tabs/telemetry.js +327 -105
- package/ui/tabs/workflows.js +86 -0
- package/ui-server.mjs +1201 -107
- package/voice-action-dispatcher.mjs +81 -0
- package/voice-agents-sdk.mjs +2 -2
- package/voice-relay.mjs +131 -14
- package/voice-tools.mjs +475 -9
- package/workflow-engine.mjs +54 -0
- package/workflow-nodes.mjs +177 -28
- package/workflow-templates/github.mjs +205 -94
- package/workflow-templates/task-batch.mjs +247 -0
- package/workflow-templates.mjs +15 -0
package/ui-server.mjs
CHANGED
|
@@ -70,7 +70,25 @@ import {
|
|
|
70
70
|
matchAgentProfile,
|
|
71
71
|
loadManifest,
|
|
72
72
|
getManifestPath,
|
|
73
|
+
scaffoldAgentProfiles,
|
|
73
74
|
} from "./library-manager.mjs";
|
|
75
|
+
import {
|
|
76
|
+
listCatalog,
|
|
77
|
+
getCatalogEntry,
|
|
78
|
+
installMcpServer,
|
|
79
|
+
uninstallMcpServer,
|
|
80
|
+
listInstalledMcpServers,
|
|
81
|
+
getInstalledMcpServer,
|
|
82
|
+
} from "./mcp-registry.mjs";
|
|
83
|
+
import {
|
|
84
|
+
loadToolConfig,
|
|
85
|
+
saveToolConfig,
|
|
86
|
+
getAgentToolConfig,
|
|
87
|
+
setAgentToolConfig,
|
|
88
|
+
getEffectiveTools,
|
|
89
|
+
listAvailableTools,
|
|
90
|
+
DEFAULT_BUILTIN_TOOLS,
|
|
91
|
+
} from "./agent-tool-config.mjs";
|
|
74
92
|
import {
|
|
75
93
|
loadSharedWorkspaceRegistry,
|
|
76
94
|
sweepExpiredLeases,
|
|
@@ -144,7 +162,7 @@ const repoRoot = resolveRepoRoot();
|
|
|
144
162
|
const uiRootPreferred = resolve(__dirname, "ui");
|
|
145
163
|
const uiRootFallback = resolve(__dirname, "site", "ui");
|
|
146
164
|
const uiRoot = existsSync(uiRootPreferred) ? uiRootPreferred : uiRootFallback;
|
|
147
|
-
|
|
165
|
+
const libraryInitAttemptedRoots = new Set();
|
|
148
166
|
const MAX_VISION_FRAME_BYTES = Math.max(
|
|
149
167
|
128_000,
|
|
150
168
|
Number.parseInt(process.env.VISION_FRAME_MAX_BYTES || "", 10) || 2_000_000,
|
|
@@ -191,21 +209,238 @@ function parseVisionFrameDataUrl(dataUrl) {
|
|
|
191
209
|
return { ok: true, mimeType, base64Data, approxBytes, raw };
|
|
192
210
|
}
|
|
193
211
|
|
|
194
|
-
function ensureLibraryInitialized() {
|
|
195
|
-
|
|
196
|
-
|
|
212
|
+
function ensureLibraryInitialized(rootDir = repoRoot) {
|
|
213
|
+
const normalizedRoot = normalizeCandidatePath(rootDir) || repoRoot;
|
|
214
|
+
if (libraryInitAttemptedRoots.has(normalizedRoot)) return;
|
|
215
|
+
libraryInitAttemptedRoots.add(normalizedRoot);
|
|
197
216
|
try {
|
|
198
|
-
const manifestPath = getManifestPath(
|
|
199
|
-
const manifest = loadManifest(
|
|
217
|
+
const manifestPath = getManifestPath(normalizedRoot);
|
|
218
|
+
const manifest = loadManifest(normalizedRoot);
|
|
200
219
|
if (!existsSync(manifestPath) || !Array.isArray(manifest?.entries) || manifest.entries.length === 0) {
|
|
201
|
-
const result = initLibrary(
|
|
220
|
+
const result = initLibrary(normalizedRoot);
|
|
202
221
|
const count = result?.manifest?.entries?.length ?? 0;
|
|
203
222
|
if (count > 0) {
|
|
204
|
-
console.log(`[ui] Library initialized (${count} entries).`);
|
|
223
|
+
console.log(`[ui] Library initialized (${count} entries) at ${normalizedRoot}.`);
|
|
205
224
|
}
|
|
206
225
|
}
|
|
226
|
+
const scaffoldResult = scaffoldAgentProfiles(normalizedRoot);
|
|
227
|
+
if (Array.isArray(scaffoldResult?.written) && scaffoldResult.written.length > 0) {
|
|
228
|
+
rebuildManifest(normalizedRoot);
|
|
229
|
+
}
|
|
207
230
|
} catch (err) {
|
|
208
|
-
console.warn(`[ui] Library init failed: ${err.message}`);
|
|
231
|
+
console.warn(`[ui] Library init failed for ${normalizedRoot}: ${err.message}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const VOICE_TOOL_ID_MAP = Object.freeze({
|
|
236
|
+
"search-files": ["search_code", "list_directory"],
|
|
237
|
+
"read-file": ["read_file_content"],
|
|
238
|
+
"edit-file": ["delegate_to_agent", "run_workspace_command"],
|
|
239
|
+
"run-command": ["run_command", "run_workspace_command"],
|
|
240
|
+
"web-search": ["ask_agent_context"],
|
|
241
|
+
"code-search": ["search_code", "get_workspace_context"],
|
|
242
|
+
"git-operations": ["run_workspace_command", "get_pr_status"],
|
|
243
|
+
"create-task": ["create_task"],
|
|
244
|
+
"delegate-task": ["delegate_to_agent", "ask_agent_context", "poll_background_session"],
|
|
245
|
+
"fetch-url": ["run_workspace_command"],
|
|
246
|
+
"list-directory": ["list_directory"],
|
|
247
|
+
"grep-search": ["search_code"],
|
|
248
|
+
"task-management": ["list_tasks", "get_task", "search_tasks", "get_task_stats", "delete_task", "comment_on_task"],
|
|
249
|
+
"notifications": ["dispatch_action"],
|
|
250
|
+
"vision-analysis": ["query_live_view"],
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const BUILTIN_TOOL_ID_SET = new Set(
|
|
254
|
+
(Array.isArray(DEFAULT_BUILTIN_TOOLS) ? DEFAULT_BUILTIN_TOOLS : [])
|
|
255
|
+
.map((tool) => String(tool?.id || "").trim())
|
|
256
|
+
.filter(Boolean),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
function mapToolConfigIdsToVoiceToolNames(enabledIds = []) {
|
|
260
|
+
const resolved = new Set();
|
|
261
|
+
for (const rawId of Array.isArray(enabledIds) ? enabledIds : []) {
|
|
262
|
+
const id = String(rawId || "").trim();
|
|
263
|
+
if (!id) continue;
|
|
264
|
+
const mapped = VOICE_TOOL_ID_MAP[id];
|
|
265
|
+
if (Array.isArray(mapped) && mapped.length > 0) {
|
|
266
|
+
for (const toolName of mapped) resolved.add(String(toolName || "").trim());
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
// If this is a known built-in tool id without a runtime mapping, skip it
|
|
270
|
+
// instead of treating the id as a voice runtime tool name.
|
|
271
|
+
if (BUILTIN_TOOL_ID_SET.has(id)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
resolved.add(id);
|
|
275
|
+
}
|
|
276
|
+
return resolved;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function isVoiceAgentProfileEntry(entry, profile) {
|
|
280
|
+
if (!entry || entry.type !== "agent") return false;
|
|
281
|
+
const id = String(entry.id || "").trim().toLowerCase();
|
|
282
|
+
const tags = Array.isArray(entry.tags) ? entry.tags.map((t) => String(t || "").trim().toLowerCase()) : [];
|
|
283
|
+
const profileType = String(profile?.agentType || "").trim().toLowerCase();
|
|
284
|
+
if (profileType === "voice") return true;
|
|
285
|
+
if (id.startsWith("voice-agent")) return true;
|
|
286
|
+
if (tags.includes("voice") || tags.includes("audio-agent") || tags.includes("realtime")) return true;
|
|
287
|
+
if (profile && typeof profile === "object" && profile.voiceAgent === true) return true;
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function resolveAgentProfileType(entry, profile) {
|
|
292
|
+
const explicit = String(profile?.agentType || "").trim().toLowerCase();
|
|
293
|
+
if (explicit === "voice" || explicit === "task" || explicit === "chat") return explicit;
|
|
294
|
+
if (isVoiceAgentProfileEntry(entry, profile)) return "voice";
|
|
295
|
+
return "task";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function resolveVoiceLibraryRoot(callContext = {}) {
|
|
299
|
+
const sessionId = String(callContext?.sessionId || "").trim();
|
|
300
|
+
if (!sessionId) return repoRoot;
|
|
301
|
+
try {
|
|
302
|
+
const tracker = getSessionTracker();
|
|
303
|
+
const session = tracker.getSessionById
|
|
304
|
+
? tracker.getSessionById(sessionId)
|
|
305
|
+
: (tracker.getSession ? tracker.getSession(sessionId) : null);
|
|
306
|
+
const workspaceDir = String(
|
|
307
|
+
session?.workspaceDir
|
|
308
|
+
|| session?.metadata?.workspaceDir
|
|
309
|
+
|| "",
|
|
310
|
+
).trim();
|
|
311
|
+
if (workspaceDir) return workspaceDir;
|
|
312
|
+
} catch {
|
|
313
|
+
// best effort
|
|
314
|
+
}
|
|
315
|
+
return repoRoot;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function listVoiceAgentProfiles(rootDir = repoRoot) {
|
|
319
|
+
const libraryRoot = rootDir || repoRoot;
|
|
320
|
+
ensureLibraryInitialized(libraryRoot);
|
|
321
|
+
const entries = listEntries(libraryRoot, { type: "agent" }) || [];
|
|
322
|
+
const profiles = [];
|
|
323
|
+
for (const entry of entries) {
|
|
324
|
+
const profile = getEntryContent(libraryRoot, entry);
|
|
325
|
+
if (!isVoiceAgentProfileEntry(entry, profile)) continue;
|
|
326
|
+
profiles.push({
|
|
327
|
+
id: entry.id,
|
|
328
|
+
name: entry.name || entry.id,
|
|
329
|
+
description: entry.description || "",
|
|
330
|
+
tags: Array.isArray(entry.tags) ? entry.tags : [],
|
|
331
|
+
model: String(profile?.model || "").trim() || null,
|
|
332
|
+
voicePersona: String(profile?.voicePersona || "").trim() || "neutral",
|
|
333
|
+
voiceInstructions: String(profile?.voiceInstructions || "").trim() || "",
|
|
334
|
+
skills: Array.isArray(profile?.skills) ? profile.skills.map((s) => String(s || "").trim()).filter(Boolean) : [],
|
|
335
|
+
promptOverride: String(profile?.promptOverride || "").trim() || null,
|
|
336
|
+
agentType: resolveAgentProfileType(entry, profile),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
profiles.sort((a, b) => {
|
|
341
|
+
const rank = (id) => {
|
|
342
|
+
if (id === "voice-agent-female") return 0;
|
|
343
|
+
if (id === "voice-agent-male") return 1;
|
|
344
|
+
if (id === "voice-agent") return 2;
|
|
345
|
+
return 3;
|
|
346
|
+
};
|
|
347
|
+
const ra = rank(a.id);
|
|
348
|
+
const rb = rank(b.id);
|
|
349
|
+
if (ra !== rb) return ra - rb;
|
|
350
|
+
return String(a.name || a.id).localeCompare(String(b.name || b.id));
|
|
351
|
+
});
|
|
352
|
+
return profiles;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function resolveActiveVoiceAgent(rootDir = repoRoot, requestedAgentId = "") {
|
|
356
|
+
const agents = listVoiceAgentProfiles(rootDir);
|
|
357
|
+
if (agents.length === 0) return { agents: [], selected: null };
|
|
358
|
+
const requested = String(requestedAgentId || "").trim();
|
|
359
|
+
const selected = agents.find((a) => a.id === requested)
|
|
360
|
+
|| agents.find((a) => a.id === "voice-agent-female")
|
|
361
|
+
|| agents[0];
|
|
362
|
+
return { agents, selected };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function applyVoiceAgentToolFilters(allTools, toolConfig = null) {
|
|
366
|
+
const tools = Array.isArray(allTools) ? [...allTools] : [];
|
|
367
|
+
if (!toolConfig || typeof toolConfig !== "object") return tools;
|
|
368
|
+
|
|
369
|
+
const disabledBuiltinIds = Array.isArray(toolConfig.disabledBuiltinTools)
|
|
370
|
+
? toolConfig.disabledBuiltinTools
|
|
371
|
+
: [];
|
|
372
|
+
const enabledIds = Array.isArray(toolConfig.enabledTools) ? toolConfig.enabledTools : null;
|
|
373
|
+
const enabledServers = Array.isArray(toolConfig.enabledMcpServers) ? toolConfig.enabledMcpServers : [];
|
|
374
|
+
|
|
375
|
+
const disabledNames = mapToolConfigIdsToVoiceToolNames(disabledBuiltinIds);
|
|
376
|
+
const enabledNames = enabledIds ? mapToolConfigIdsToVoiceToolNames(enabledIds) : null;
|
|
377
|
+
|
|
378
|
+
return tools.filter((tool) => {
|
|
379
|
+
const name = String(tool?.name || "").trim();
|
|
380
|
+
if (!name) return false;
|
|
381
|
+
if (name === "invoke_mcp_tool" && enabledServers.length === 0) return false;
|
|
382
|
+
if (enabledNames && enabledNames.size > 0) {
|
|
383
|
+
return enabledNames.has(name);
|
|
384
|
+
}
|
|
385
|
+
if (disabledNames.has(name)) return false;
|
|
386
|
+
return true;
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function buildVoiceToolCapabilityPrompt(tools = [], toolConfig = null, selectedVoiceAgent = null) {
|
|
391
|
+
const runtimeTools = Array.isArray(tools) ? tools : [];
|
|
392
|
+
const enabledServers = Array.isArray(toolConfig?.enabledMcpServers)
|
|
393
|
+
? toolConfig.enabledMcpServers
|
|
394
|
+
: [];
|
|
395
|
+
const toolLines = runtimeTools
|
|
396
|
+
.slice(0, 48)
|
|
397
|
+
.map((tool) => {
|
|
398
|
+
const name = String(tool?.name || "").trim();
|
|
399
|
+
if (!name) return null;
|
|
400
|
+
const description = String(tool?.description || "").replace(/\s+/g, " ").trim();
|
|
401
|
+
return description ? `- ${name}: ${description}` : `- ${name}`;
|
|
402
|
+
})
|
|
403
|
+
.filter(Boolean);
|
|
404
|
+
const skills = Array.isArray(selectedVoiceAgent?.skills)
|
|
405
|
+
? selectedVoiceAgent.skills.map((skill) => String(skill || "").trim()).filter(Boolean)
|
|
406
|
+
: [];
|
|
407
|
+
const agentName = String(selectedVoiceAgent?.name || selectedVoiceAgent?.id || "Voice Agent").trim();
|
|
408
|
+
|
|
409
|
+
return [
|
|
410
|
+
"",
|
|
411
|
+
"## Active Voice Agent Capability Contract",
|
|
412
|
+
`Agent profile: ${agentName}.`,
|
|
413
|
+
"You can execute tools directly in this voice session. Do not claim you cannot use tools or must rely on chat-only help.",
|
|
414
|
+
"When the user asks for action or facts, call the most relevant tool first, then report results briefly.",
|
|
415
|
+
toolLines.length > 0
|
|
416
|
+
? "Enabled runtime tools:\n" + toolLines.join("\n")
|
|
417
|
+
: "Enabled runtime tools: none (tool calls unavailable for this profile).",
|
|
418
|
+
enabledServers.length > 0
|
|
419
|
+
? `Enabled MCP servers (for invoke_mcp_tool): ${enabledServers.join(", ")}.`
|
|
420
|
+
: "Enabled MCP servers: none.",
|
|
421
|
+
skills.length > 0
|
|
422
|
+
? `Voice agent skills: ${skills.join(", ")}.`
|
|
423
|
+
: "Voice agent skills: none specified.",
|
|
424
|
+
"",
|
|
425
|
+
"If you need the current tool list, call get_admin_help.",
|
|
426
|
+
].join("\n");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function listBosunRuntimeTools(context = {}) {
|
|
430
|
+
try {
|
|
431
|
+
const { getVoiceToolDefinitions } = await import("./voice-relay.mjs");
|
|
432
|
+
const defs = await getVoiceToolDefinitions({ delegateOnly: false, context });
|
|
433
|
+
return (Array.isArray(defs) ? defs : [])
|
|
434
|
+
.map((tool) => ({
|
|
435
|
+
id: String(tool?.name || "").trim(),
|
|
436
|
+
name: String(tool?.name || "").trim(),
|
|
437
|
+
description: String(tool?.description || "").trim(),
|
|
438
|
+
category: "Bosun",
|
|
439
|
+
}))
|
|
440
|
+
.filter((tool) => Boolean(tool.id))
|
|
441
|
+
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
442
|
+
} catch {
|
|
443
|
+
return [];
|
|
209
444
|
}
|
|
210
445
|
}
|
|
211
446
|
|
|
@@ -214,10 +449,27 @@ let _wfEngine;
|
|
|
214
449
|
let _wfNodes;
|
|
215
450
|
let _wfTemplates;
|
|
216
451
|
let _wfServicesReady = false;
|
|
452
|
+
let _wfServices = null;
|
|
217
453
|
let _wfRecommendedInstalled = false;
|
|
454
|
+
const _wfRecommendedInstalledByWorkspace = new Set();
|
|
455
|
+
const _wfEngineByWorkspace = new Map();
|
|
218
456
|
let _wfInitPromise = null;
|
|
219
457
|
let _wfInitDone = false;
|
|
220
458
|
let _wfLoadedBase = null;
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Test-only: inject a mock workflow engine module and pre-seed the per-workspace
|
|
462
|
+
* engine cache so that dispatchWorkflowEvent uses the specified mock.
|
|
463
|
+
*/
|
|
464
|
+
let _testDefaultEngine = null;
|
|
465
|
+
export function _testInjectWorkflowEngine(mockModule, mockEngine) {
|
|
466
|
+
_wfEngine = mockModule;
|
|
467
|
+
_wfInitDone = true;
|
|
468
|
+
_wfInitPromise = null;
|
|
469
|
+
_testDefaultEngine = mockEngine || null;
|
|
470
|
+
_wfEngineByWorkspace.clear();
|
|
471
|
+
}
|
|
472
|
+
|
|
221
473
|
let workflowEventDedupWindowMs = (() => {
|
|
222
474
|
const parsed = Number.parseInt(process.env.WORKFLOW_EVENT_DEDUP_WINDOW_MS || "15000", 10);
|
|
223
475
|
if (!Number.isFinite(parsed) || parsed <= 0) return 15_000;
|
|
@@ -424,6 +676,7 @@ async function getWorkflowEngineModule() {
|
|
|
424
676
|
meeting: meetingService,
|
|
425
677
|
prompts: promptBundle?.prompts || null,
|
|
426
678
|
};
|
|
679
|
+
_wfServices = services;
|
|
427
680
|
_wfEngine.getWorkflowEngine({ services });
|
|
428
681
|
_wfServicesReady = true;
|
|
429
682
|
|
|
@@ -516,6 +769,100 @@ async function getWorkflowEngineModule() {
|
|
|
516
769
|
return _wfEngine;
|
|
517
770
|
}
|
|
518
771
|
|
|
772
|
+
function getWorkflowWorkspaceKey(workspaceDir = "") {
|
|
773
|
+
const normalized = normalizeCandidatePath(workspaceDir) || repoRoot;
|
|
774
|
+
if (process.platform === "win32") {
|
|
775
|
+
return normalized.toLowerCase();
|
|
776
|
+
}
|
|
777
|
+
return normalized;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function getWorkflowStoragePaths(workspaceDir = "") {
|
|
781
|
+
const root = normalizeCandidatePath(workspaceDir) || repoRoot;
|
|
782
|
+
return {
|
|
783
|
+
workspaceRoot: root,
|
|
784
|
+
workflowDir: resolve(root, ".bosun", "workflows"),
|
|
785
|
+
runsDir: resolve(root, ".bosun", "workflow-runs"),
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function maybeBootstrapWorkspaceWorkflowTemplates(engine, workspaceKey, workspaceLabel) {
|
|
790
|
+
if (!engine || !_wfTemplates) return;
|
|
791
|
+
if (_wfRecommendedInstalledByWorkspace.has(workspaceKey)) return;
|
|
792
|
+
try {
|
|
793
|
+
const selection = resolveWorkflowBootstrapSelection(_wfTemplates);
|
|
794
|
+
let result = { installed: [], skipped: [], errors: [] };
|
|
795
|
+
if (selection.enabled) {
|
|
796
|
+
if (
|
|
797
|
+
Array.isArray(selection.templateIds) &&
|
|
798
|
+
selection.templateIds.length > 0 &&
|
|
799
|
+
typeof _wfTemplates.installTemplateSet === "function"
|
|
800
|
+
) {
|
|
801
|
+
result = _wfTemplates.installTemplateSet(engine, selection.templateIds);
|
|
802
|
+
} else if (
|
|
803
|
+
selection.source === "recommended" &&
|
|
804
|
+
typeof _wfTemplates.installRecommendedTemplates === "function"
|
|
805
|
+
) {
|
|
806
|
+
result = _wfTemplates.installRecommendedTemplates(engine);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (typeof _wfTemplates.reconcileInstalledTemplates === "function") {
|
|
810
|
+
_wfTemplates.reconcileInstalledTemplates(engine, {
|
|
811
|
+
autoUpdateUnmodified: true,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
if (result.installed.length) {
|
|
815
|
+
console.log(
|
|
816
|
+
`[workflows] Installed ${result.installed.length} default workflow templates for workspace ${workspaceLabel}`,
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
} catch (err) {
|
|
820
|
+
console.warn(
|
|
821
|
+
`[workflows] Default template install failed for workspace ${workspaceLabel}: ${err.message}`,
|
|
822
|
+
);
|
|
823
|
+
} finally {
|
|
824
|
+
_wfRecommendedInstalledByWorkspace.add(workspaceKey);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
async function getWorkflowRequestContext(reqUrl) {
|
|
829
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(reqUrl, { allowAll: false });
|
|
830
|
+
if (!workspaceContext) {
|
|
831
|
+
return { ok: false, status: 400, error: "Unknown workspace. Set a valid workspace query value." };
|
|
832
|
+
}
|
|
833
|
+
const wfMod = await getWorkflowEngineModule();
|
|
834
|
+
if (!wfMod?.WorkflowEngine) {
|
|
835
|
+
return { ok: false, status: 503, error: "Workflow engine not available" };
|
|
836
|
+
}
|
|
837
|
+
const paths = getWorkflowStoragePaths(workspaceContext.workspaceDir);
|
|
838
|
+
const workspaceKey = getWorkflowWorkspaceKey(paths.workspaceRoot);
|
|
839
|
+
let engine = _wfEngineByWorkspace.get(workspaceKey) || null;
|
|
840
|
+
if (!engine) {
|
|
841
|
+
if (_testDefaultEngine) {
|
|
842
|
+
engine = _testDefaultEngine;
|
|
843
|
+
} else {
|
|
844
|
+
engine = new wfMod.WorkflowEngine({
|
|
845
|
+
workflowDir: paths.workflowDir,
|
|
846
|
+
runsDir: paths.runsDir,
|
|
847
|
+
services: _wfServices || {},
|
|
848
|
+
});
|
|
849
|
+
engine.load();
|
|
850
|
+
}
|
|
851
|
+
_wfEngineByWorkspace.set(workspaceKey, engine);
|
|
852
|
+
}
|
|
853
|
+
maybeBootstrapWorkspaceWorkflowTemplates(
|
|
854
|
+
engine,
|
|
855
|
+
workspaceKey,
|
|
856
|
+
workspaceContext.workspaceId || workspaceKey,
|
|
857
|
+
);
|
|
858
|
+
return {
|
|
859
|
+
ok: true,
|
|
860
|
+
wfMod,
|
|
861
|
+
engine,
|
|
862
|
+
workspaceContext: { ...workspaceContext, workspaceDir: paths.workspaceRoot },
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
519
866
|
function allowWorkflowEvent(dedupKey, windowMs = workflowEventDedupWindowMs) {
|
|
520
867
|
if (!dedupKey) return true;
|
|
521
868
|
const now = Date.now();
|
|
@@ -559,10 +906,11 @@ async function dispatchWorkflowEvent(eventType, eventData = {}, opts = {}) {
|
|
|
559
906
|
return false;
|
|
560
907
|
}
|
|
561
908
|
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
909
|
+
const wfCtx = await getWorkflowRequestContext(
|
|
910
|
+
new URL(`http://localhost?workspace=active`),
|
|
911
|
+
);
|
|
912
|
+
if (!wfCtx?.ok) return false;
|
|
913
|
+
const engine = wfCtx.engine;
|
|
566
914
|
if (!engine?.evaluateTriggers || !engine?.execute) return false;
|
|
567
915
|
|
|
568
916
|
const payload = buildWorkflowEventPayload(eventType, eventData, "ui-server");
|
|
@@ -1278,6 +1626,79 @@ function resolveActiveWorkspaceExecutionContext() {
|
|
|
1278
1626
|
};
|
|
1279
1627
|
}
|
|
1280
1628
|
|
|
1629
|
+
function resolveWorkspaceContextById(workspaceId = "") {
|
|
1630
|
+
const requestedId = String(workspaceId || "").trim().toLowerCase();
|
|
1631
|
+
if (!requestedId) return resolveActiveWorkspaceExecutionContext();
|
|
1632
|
+
const configDir = resolveUiConfigDir();
|
|
1633
|
+
if (!configDir) return null;
|
|
1634
|
+
const listed = listManagedWorkspaces(configDir, { repoRoot });
|
|
1635
|
+
const workspace = listed.find(
|
|
1636
|
+
(entry) => String(entry?.id || "").trim().toLowerCase() === requestedId,
|
|
1637
|
+
);
|
|
1638
|
+
if (!workspace) return null;
|
|
1639
|
+
const id = String(workspace.id || "").trim();
|
|
1640
|
+
return {
|
|
1641
|
+
workspaceId: id,
|
|
1642
|
+
workspaceDir: pickWorkspaceRepoDir(workspace) || repoRoot,
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function resolveWorkspaceContextFromRequest(reqUrl, opts = {}) {
|
|
1647
|
+
const allowAll = opts.allowAll !== false;
|
|
1648
|
+
const workspaceRaw = String(reqUrl?.searchParams?.get("workspace") || "").trim();
|
|
1649
|
+
const workspaceKey = workspaceRaw.toLowerCase();
|
|
1650
|
+
if (allowAll && (workspaceKey === "all" || workspaceKey === "*")) {
|
|
1651
|
+
return {
|
|
1652
|
+
allWorkspaces: true,
|
|
1653
|
+
workspaceId: "",
|
|
1654
|
+
workspaceDir: repoRoot,
|
|
1655
|
+
workspaceFilter: "",
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
if (!workspaceKey || workspaceKey === "active") {
|
|
1659
|
+
const active = resolveActiveWorkspaceExecutionContext();
|
|
1660
|
+
return {
|
|
1661
|
+
allWorkspaces: false,
|
|
1662
|
+
workspaceId: String(active.workspaceId || "").trim(),
|
|
1663
|
+
workspaceDir: normalizeCandidatePath(active.workspaceDir) || repoRoot,
|
|
1664
|
+
workspaceFilter: String(active.workspaceId || "").trim().toLowerCase(),
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
const explicit = resolveWorkspaceContextById(workspaceKey);
|
|
1668
|
+
if (!explicit) return null;
|
|
1669
|
+
return {
|
|
1670
|
+
allWorkspaces: false,
|
|
1671
|
+
workspaceId: String(explicit.workspaceId || "").trim(),
|
|
1672
|
+
workspaceDir: normalizeCandidatePath(explicit.workspaceDir) || repoRoot,
|
|
1673
|
+
workspaceFilter: String(explicit.workspaceId || "").trim().toLowerCase(),
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function resolveSessionWorkspaceMeta(session) {
|
|
1678
|
+
const metadata =
|
|
1679
|
+
session && typeof session.metadata === "object" && session.metadata
|
|
1680
|
+
? session.metadata
|
|
1681
|
+
: null;
|
|
1682
|
+
return {
|
|
1683
|
+
workspaceId: String(metadata?.workspaceId || "").trim().toLowerCase(),
|
|
1684
|
+
workspaceDir: normalizeCandidatePath(metadata?.workspaceDir),
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
function sessionMatchesWorkspaceContext(session, workspaceContext) {
|
|
1689
|
+
if (!session) return false;
|
|
1690
|
+
if (!workspaceContext || workspaceContext.allWorkspaces) return true;
|
|
1691
|
+
const sessionWorkspace = resolveSessionWorkspaceMeta(session);
|
|
1692
|
+
if (sessionWorkspace.workspaceId) {
|
|
1693
|
+
return sessionWorkspace.workspaceId === String(workspaceContext.workspaceFilter || "").trim().toLowerCase();
|
|
1694
|
+
}
|
|
1695
|
+
const activeWorkspaceDir = normalizeCandidatePath(workspaceContext.workspaceDir);
|
|
1696
|
+
if (sessionWorkspace.workspaceDir && activeWorkspaceDir) {
|
|
1697
|
+
return sessionWorkspace.workspaceDir === activeWorkspaceDir;
|
|
1698
|
+
}
|
|
1699
|
+
return !workspaceContext.workspaceFilter;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1281
1702
|
function resolveSessionWorkspaceDir(session = null) {
|
|
1282
1703
|
const metadata =
|
|
1283
1704
|
session && typeof session.metadata === "object" && session.metadata
|
|
@@ -5617,6 +6038,128 @@ function summarizeTelemetry(metrics, days) {
|
|
|
5617
6038
|
};
|
|
5618
6039
|
}
|
|
5619
6040
|
|
|
6041
|
+
// ── Usage Analytics ─────────────────────────────────────────────────────────
|
|
6042
|
+
|
|
6043
|
+
/**
|
|
6044
|
+
* Build comprehensive usage analytics from agent-work-stream.jsonl.
|
|
6045
|
+
* Aggregates agent runs, skill invocations, MCP tool calls, and daily trends.
|
|
6046
|
+
*
|
|
6047
|
+
* @param {number} [days] - Look-back window in days; 0 = all time.
|
|
6048
|
+
* @returns {Promise<Object>}
|
|
6049
|
+
*/
|
|
6050
|
+
async function buildUsageAnalytics(days) {
|
|
6051
|
+
const logDir = resolveAgentWorkLogDir();
|
|
6052
|
+
const streamPath = resolve(logDir, "agent-work-stream.jsonl");
|
|
6053
|
+
const events = await readJsonlTail(streamPath, 100_000);
|
|
6054
|
+
|
|
6055
|
+
const cutoff = days ? Date.now() - days * 24 * 60 * 60 * 1000 : 0;
|
|
6056
|
+
|
|
6057
|
+
let agentRuns = 0;
|
|
6058
|
+
let skillInvocations = 0;
|
|
6059
|
+
let mcpToolCalls = 0;
|
|
6060
|
+
let oldestTs = Infinity;
|
|
6061
|
+
let newestTs = 0;
|
|
6062
|
+
|
|
6063
|
+
/** @type {Map<string,number>} */
|
|
6064
|
+
const agents = new Map();
|
|
6065
|
+
/** @type {Map<string,number>} */
|
|
6066
|
+
const skills = new Map();
|
|
6067
|
+
/** @type {Map<string,number>} */
|
|
6068
|
+
const mcpTools = new Map();
|
|
6069
|
+
|
|
6070
|
+
/** dailyAgents[date][executor] = count */
|
|
6071
|
+
const dailyAgents = {};
|
|
6072
|
+
/** dailySkills[date][skill] = count */
|
|
6073
|
+
const dailySkills = {};
|
|
6074
|
+
/** dailyMcp[date][tool] = count */
|
|
6075
|
+
const dailyMcp = {};
|
|
6076
|
+
|
|
6077
|
+
const allDates = new Set();
|
|
6078
|
+
|
|
6079
|
+
for (const e of events) {
|
|
6080
|
+
const ts = Date.parse(e.timestamp || "");
|
|
6081
|
+
if (!Number.isFinite(ts)) continue;
|
|
6082
|
+
if (cutoff && ts < cutoff) continue;
|
|
6083
|
+
if (ts < oldestTs) oldestTs = ts;
|
|
6084
|
+
if (ts > newestTs) newestTs = ts;
|
|
6085
|
+
const day = (e.timestamp || "").slice(0, 10);
|
|
6086
|
+
if (day) allDates.add(day);
|
|
6087
|
+
|
|
6088
|
+
if (e.event_type === "session_start") {
|
|
6089
|
+
agentRuns++;
|
|
6090
|
+
const exec = e.executor || "unknown";
|
|
6091
|
+
agents.set(exec, (agents.get(exec) || 0) + 1);
|
|
6092
|
+
if (day) {
|
|
6093
|
+
(dailyAgents[day] = dailyAgents[day] || {})[exec] =
|
|
6094
|
+
(dailyAgents[day][exec] || 0) + 1;
|
|
6095
|
+
}
|
|
6096
|
+
} else if (e.event_type === "skill_invoke") {
|
|
6097
|
+
skillInvocations++;
|
|
6098
|
+
const skill = e.data?.skill_name || e.skill_name || "unknown";
|
|
6099
|
+
skills.set(skill, (skills.get(skill) || 0) + 1);
|
|
6100
|
+
if (day) {
|
|
6101
|
+
(dailySkills[day] = dailySkills[day] || {})[skill] =
|
|
6102
|
+
(dailySkills[day][skill] || 0) + 1;
|
|
6103
|
+
}
|
|
6104
|
+
} else if (e.event_type === "tool_call") {
|
|
6105
|
+
mcpToolCalls++;
|
|
6106
|
+
const tool = e.data?.tool_name || e.tool_name || "unknown";
|
|
6107
|
+
mcpTools.set(tool, (mcpTools.get(tool) || 0) + 1);
|
|
6108
|
+
if (day) {
|
|
6109
|
+
(dailyMcp[day] = dailyMcp[day] || {})[tool] =
|
|
6110
|
+
(dailyMcp[day][tool] || 0) + 1;
|
|
6111
|
+
}
|
|
6112
|
+
}
|
|
6113
|
+
}
|
|
6114
|
+
|
|
6115
|
+
const sortedDates = [...allDates].sort();
|
|
6116
|
+
const dayCount = sortedDates.length || 1;
|
|
6117
|
+
const total = agentRuns + skillInvocations + mcpToolCalls;
|
|
6118
|
+
const avgPerDay = Math.round(total / dayCount);
|
|
6119
|
+
|
|
6120
|
+
const topAgents = [...agents.entries()]
|
|
6121
|
+
.sort((a, b) => b[1] - a[1])
|
|
6122
|
+
.slice(0, 8)
|
|
6123
|
+
.map(([name, count]) => ({ name, count }));
|
|
6124
|
+
const topSkills = [...skills.entries()]
|
|
6125
|
+
.sort((a, b) => b[1] - a[1])
|
|
6126
|
+
.slice(0, 8)
|
|
6127
|
+
.map(([name, count]) => ({ name, count }));
|
|
6128
|
+
const topMcpTools = [...mcpTools.entries()]
|
|
6129
|
+
.sort((a, b) => b[1] - a[1])
|
|
6130
|
+
.slice(0, 8)
|
|
6131
|
+
.map(([name, count]) => ({ name, count }));
|
|
6132
|
+
|
|
6133
|
+
// Build trend series for top-6 items per category
|
|
6134
|
+
const topAgentNames = topAgents.slice(0, 6).map((a) => a.name);
|
|
6135
|
+
const topSkillNames = topSkills.slice(0, 6).map((s) => s.name);
|
|
6136
|
+
const topMcpNames = topMcpTools.slice(0, 6).map((t) => t.name);
|
|
6137
|
+
|
|
6138
|
+
const trend = { dates: sortedDates, agents: {}, skills: {}, mcpTools: {} };
|
|
6139
|
+
for (const name of topAgentNames) {
|
|
6140
|
+
trend.agents[name] = sortedDates.map((d) => dailyAgents[d]?.[name] || 0);
|
|
6141
|
+
}
|
|
6142
|
+
for (const name of topSkillNames) {
|
|
6143
|
+
trend.skills[name] = sortedDates.map((d) => dailySkills[d]?.[name] || 0);
|
|
6144
|
+
}
|
|
6145
|
+
for (const name of topMcpNames) {
|
|
6146
|
+
trend.mcpTools[name] = sortedDates.map((d) => dailyMcp[d]?.[name] || 0);
|
|
6147
|
+
}
|
|
6148
|
+
|
|
6149
|
+
return {
|
|
6150
|
+
agentRuns,
|
|
6151
|
+
skillInvocations,
|
|
6152
|
+
mcpToolCalls,
|
|
6153
|
+
avgPerDay,
|
|
6154
|
+
lastActiveAt: newestTs < Infinity && newestTs > 0 ? new Date(newestTs).toISOString() : null,
|
|
6155
|
+
sinceAt: oldestTs < Infinity ? new Date(oldestTs).toISOString() : null,
|
|
6156
|
+
topAgents,
|
|
6157
|
+
topSkills,
|
|
6158
|
+
topMcpTools,
|
|
6159
|
+
trend,
|
|
6160
|
+
};
|
|
6161
|
+
}
|
|
6162
|
+
|
|
5620
6163
|
function resolveAgentWorkLogDir() {
|
|
5621
6164
|
const candidates = [
|
|
5622
6165
|
resolve(repoRoot, ".cache", "agent-work-logs"),
|
|
@@ -5925,7 +6468,15 @@ async function handleApi(req, res, url) {
|
|
|
5925
6468
|
if (path === "/api/tasks") {
|
|
5926
6469
|
const status = url.searchParams.get("status") || "";
|
|
5927
6470
|
const projectId = url.searchParams.get("project") || "";
|
|
5928
|
-
const
|
|
6471
|
+
const workspaceQueryRaw = String(url.searchParams.get("workspace") || "").trim();
|
|
6472
|
+
let workspaceFilter = workspaceQueryRaw.toLowerCase();
|
|
6473
|
+
if (!workspaceFilter || workspaceFilter === "active") {
|
|
6474
|
+
const activeWorkspace = getActiveManagedWorkspace(resolveUiConfigDir());
|
|
6475
|
+
workspaceFilter = String(activeWorkspace?.id || "").trim().toLowerCase();
|
|
6476
|
+
}
|
|
6477
|
+
if (workspaceFilter === "*" || workspaceFilter === "all") {
|
|
6478
|
+
workspaceFilter = "";
|
|
6479
|
+
}
|
|
5929
6480
|
const repositoryFilter = (url.searchParams.get("repository") || "").trim().toLowerCase();
|
|
5930
6481
|
const page = Math.max(0, Number(url.searchParams.get("page") || "0"));
|
|
5931
6482
|
const pageSize = Math.min(
|
|
@@ -6528,14 +7079,34 @@ async function handleApi(req, res, url) {
|
|
|
6528
7079
|
|
|
6529
7080
|
if (path === "/api/library") {
|
|
6530
7081
|
try {
|
|
6531
|
-
|
|
7082
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: false });
|
|
7083
|
+
if (!workspaceContext) {
|
|
7084
|
+
jsonResponse(res, 400, { ok: false, error: "Unknown workspace" });
|
|
7085
|
+
return;
|
|
7086
|
+
}
|
|
7087
|
+
const libraryRoot = workspaceContext.workspaceDir || repoRoot;
|
|
7088
|
+
ensureLibraryInitialized(libraryRoot);
|
|
6532
7089
|
const typeRaw = (url.searchParams.get("type") || "").trim();
|
|
7090
|
+
const agentTypeRaw = String(url.searchParams.get("agentType") || "").trim().toLowerCase();
|
|
6533
7091
|
const search = (url.searchParams.get("search") || "").trim();
|
|
6534
7092
|
const type = typeRaw && typeRaw !== "all" ? typeRaw : "";
|
|
6535
|
-
|
|
7093
|
+
let data = listEntries(libraryRoot, {
|
|
6536
7094
|
type: type || undefined,
|
|
6537
7095
|
search: search || undefined,
|
|
6538
7096
|
});
|
|
7097
|
+
data = data.map((entry) => {
|
|
7098
|
+
if (entry?.type !== "agent") return entry;
|
|
7099
|
+
const profile = getEntryContent(libraryRoot, entry);
|
|
7100
|
+
return {
|
|
7101
|
+
...entry,
|
|
7102
|
+
agentType: resolveAgentProfileType(entry, profile),
|
|
7103
|
+
};
|
|
7104
|
+
});
|
|
7105
|
+
if (type === "agent" && (agentTypeRaw === "voice" || agentTypeRaw === "task" || agentTypeRaw === "chat")) {
|
|
7106
|
+
data = data.filter((entry) => {
|
|
7107
|
+
return String(entry?.agentType || "").trim().toLowerCase() === agentTypeRaw;
|
|
7108
|
+
});
|
|
7109
|
+
}
|
|
6539
7110
|
jsonResponse(res, 200, { ok: true, data });
|
|
6540
7111
|
} catch (err) {
|
|
6541
7112
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
@@ -6545,19 +7116,25 @@ async function handleApi(req, res, url) {
|
|
|
6545
7116
|
|
|
6546
7117
|
if (path === "/api/library/entry") {
|
|
6547
7118
|
try {
|
|
6548
|
-
|
|
7119
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: false });
|
|
7120
|
+
if (!workspaceContext) {
|
|
7121
|
+
jsonResponse(res, 400, { ok: false, error: "Unknown workspace" });
|
|
7122
|
+
return;
|
|
7123
|
+
}
|
|
7124
|
+
const libraryRoot = workspaceContext.workspaceDir || repoRoot;
|
|
7125
|
+
ensureLibraryInitialized(libraryRoot);
|
|
6549
7126
|
if (req.method === "GET") {
|
|
6550
7127
|
const id = (url.searchParams.get("id") || "").trim();
|
|
6551
7128
|
if (!id) {
|
|
6552
7129
|
jsonResponse(res, 400, { ok: false, error: "id required" });
|
|
6553
7130
|
return;
|
|
6554
7131
|
}
|
|
6555
|
-
const entry = getEntry(
|
|
7132
|
+
const entry = getEntry(libraryRoot, id);
|
|
6556
7133
|
if (!entry) {
|
|
6557
7134
|
jsonResponse(res, 404, { ok: false, error: "not found" });
|
|
6558
7135
|
return;
|
|
6559
7136
|
}
|
|
6560
|
-
const content = getEntryContent(
|
|
7137
|
+
const content = getEntryContent(libraryRoot, entry);
|
|
6561
7138
|
jsonResponse(res, 200, { ok: true, data: { ...entry, content } });
|
|
6562
7139
|
return;
|
|
6563
7140
|
}
|
|
@@ -6565,7 +7142,7 @@ async function handleApi(req, res, url) {
|
|
|
6565
7142
|
if (req.method === "POST") {
|
|
6566
7143
|
const body = await readJsonBody(req);
|
|
6567
7144
|
const { content, ...entryData } = body || {};
|
|
6568
|
-
const entry = upsertEntry(
|
|
7145
|
+
const entry = upsertEntry(libraryRoot, entryData, content);
|
|
6569
7146
|
jsonResponse(res, 200, { ok: true, data: entry });
|
|
6570
7147
|
return;
|
|
6571
7148
|
}
|
|
@@ -6577,7 +7154,7 @@ async function handleApi(req, res, url) {
|
|
|
6577
7154
|
jsonResponse(res, 400, { ok: false, error: "id required" });
|
|
6578
7155
|
return;
|
|
6579
7156
|
}
|
|
6580
|
-
const deleted = deleteEntry(
|
|
7157
|
+
const deleted = deleteEntry(libraryRoot, id, { deleteFile: Boolean(body?.deleteFile) });
|
|
6581
7158
|
if (!deleted) {
|
|
6582
7159
|
jsonResponse(res, 404, { ok: false, error: "not found" });
|
|
6583
7160
|
return;
|
|
@@ -6595,8 +7172,14 @@ async function handleApi(req, res, url) {
|
|
|
6595
7172
|
|
|
6596
7173
|
if (path === "/api/library/scopes") {
|
|
6597
7174
|
try {
|
|
6598
|
-
|
|
6599
|
-
|
|
7175
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: false });
|
|
7176
|
+
if (!workspaceContext) {
|
|
7177
|
+
jsonResponse(res, 400, { ok: false, error: "Unknown workspace" });
|
|
7178
|
+
return;
|
|
7179
|
+
}
|
|
7180
|
+
const libraryRoot = workspaceContext.workspaceDir || repoRoot;
|
|
7181
|
+
ensureLibraryInitialized(libraryRoot);
|
|
7182
|
+
const result = detectScopes(libraryRoot);
|
|
6600
7183
|
jsonResponse(res, 200, { ok: true, data: result?.scopes || [] });
|
|
6601
7184
|
} catch (err) {
|
|
6602
7185
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
@@ -6606,7 +7189,13 @@ async function handleApi(req, res, url) {
|
|
|
6606
7189
|
|
|
6607
7190
|
if (path === "/api/library/init" && req.method === "POST") {
|
|
6608
7191
|
try {
|
|
6609
|
-
const
|
|
7192
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: false });
|
|
7193
|
+
if (!workspaceContext) {
|
|
7194
|
+
jsonResponse(res, 400, { ok: false, error: "Unknown workspace" });
|
|
7195
|
+
return;
|
|
7196
|
+
}
|
|
7197
|
+
const libraryRoot = workspaceContext.workspaceDir || repoRoot;
|
|
7198
|
+
const result = initLibrary(libraryRoot);
|
|
6610
7199
|
const entriesCount = result?.manifest?.entries?.length ?? 0;
|
|
6611
7200
|
const scaffoldedCount = result?.scaffolded?.written?.length ?? 0;
|
|
6612
7201
|
jsonResponse(res, 200, {
|
|
@@ -6621,7 +7210,13 @@ async function handleApi(req, res, url) {
|
|
|
6621
7210
|
|
|
6622
7211
|
if (path === "/api/library/rebuild" && req.method === "POST") {
|
|
6623
7212
|
try {
|
|
6624
|
-
const
|
|
7213
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: false });
|
|
7214
|
+
if (!workspaceContext) {
|
|
7215
|
+
jsonResponse(res, 400, { ok: false, error: "Unknown workspace" });
|
|
7216
|
+
return;
|
|
7217
|
+
}
|
|
7218
|
+
const libraryRoot = workspaceContext.workspaceDir || repoRoot;
|
|
7219
|
+
const result = rebuildManifest(libraryRoot);
|
|
6625
7220
|
jsonResponse(res, 200, {
|
|
6626
7221
|
ok: true,
|
|
6627
7222
|
data: {
|
|
@@ -6638,9 +7233,15 @@ async function handleApi(req, res, url) {
|
|
|
6638
7233
|
|
|
6639
7234
|
if (path === "/api/library/match-profile") {
|
|
6640
7235
|
try {
|
|
6641
|
-
|
|
7236
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: false });
|
|
7237
|
+
if (!workspaceContext) {
|
|
7238
|
+
jsonResponse(res, 400, { ok: false, error: "Unknown workspace" });
|
|
7239
|
+
return;
|
|
7240
|
+
}
|
|
7241
|
+
const libraryRoot = workspaceContext.workspaceDir || repoRoot;
|
|
7242
|
+
ensureLibraryInitialized(libraryRoot);
|
|
6642
7243
|
const title = (url.searchParams.get("title") || "").trim();
|
|
6643
|
-
const match = matchAgentProfile(
|
|
7244
|
+
const match = matchAgentProfile(libraryRoot, title);
|
|
6644
7245
|
jsonResponse(res, 200, { ok: true, data: match || null });
|
|
6645
7246
|
} catch (err) {
|
|
6646
7247
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
@@ -6648,6 +7249,186 @@ async function handleApi(req, res, url) {
|
|
|
6648
7249
|
return;
|
|
6649
7250
|
}
|
|
6650
7251
|
|
|
7252
|
+
// ── MCP Server Management API ─────────────────────────────────────────────
|
|
7253
|
+
|
|
7254
|
+
if (path === "/api/mcp/catalog") {
|
|
7255
|
+
try {
|
|
7256
|
+
const tagsRaw = (url.searchParams.get("tags") || "").trim();
|
|
7257
|
+
const tags = tagsRaw ? tagsRaw.split(",").map((t) => t.trim()) : undefined;
|
|
7258
|
+
const catalog = listCatalog({ tags });
|
|
7259
|
+
jsonResponse(res, 200, { ok: true, data: catalog });
|
|
7260
|
+
} catch (err) {
|
|
7261
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
7262
|
+
}
|
|
7263
|
+
return;
|
|
7264
|
+
}
|
|
7265
|
+
|
|
7266
|
+
if (path === "/api/mcp/installed") {
|
|
7267
|
+
try {
|
|
7268
|
+
const servers = await listInstalledMcpServers(repoRoot);
|
|
7269
|
+
jsonResponse(res, 200, { ok: true, data: servers });
|
|
7270
|
+
} catch (err) {
|
|
7271
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
7272
|
+
}
|
|
7273
|
+
return;
|
|
7274
|
+
}
|
|
7275
|
+
|
|
7276
|
+
if (path === "/api/mcp/install" && req.method === "POST") {
|
|
7277
|
+
try {
|
|
7278
|
+
const body = await readJsonBody(req);
|
|
7279
|
+
if (!body?.id && !body?.serverDef) {
|
|
7280
|
+
jsonResponse(res, 400, { ok: false, error: "id (catalog) or serverDef (custom) required" });
|
|
7281
|
+
return;
|
|
7282
|
+
}
|
|
7283
|
+
const result = await installMcpServer(
|
|
7284
|
+
repoRoot,
|
|
7285
|
+
body.serverDef || body.id,
|
|
7286
|
+
{ envOverrides: body.envOverrides },
|
|
7287
|
+
);
|
|
7288
|
+
jsonResponse(res, 200, { ok: true, data: result });
|
|
7289
|
+
broadcastUiEvent(["library"], "invalidate", { reason: "mcp-installed", id: result?.id });
|
|
7290
|
+
} catch (err) {
|
|
7291
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
7292
|
+
}
|
|
7293
|
+
return;
|
|
7294
|
+
}
|
|
7295
|
+
|
|
7296
|
+
if (path === "/api/mcp/uninstall" && req.method === "POST") {
|
|
7297
|
+
try {
|
|
7298
|
+
const body = await readJsonBody(req);
|
|
7299
|
+
if (!body?.id) {
|
|
7300
|
+
jsonResponse(res, 400, { ok: false, error: "id required" });
|
|
7301
|
+
return;
|
|
7302
|
+
}
|
|
7303
|
+
const removed = await uninstallMcpServer(repoRoot, body.id);
|
|
7304
|
+
jsonResponse(res, 200, { ok: true, removed });
|
|
7305
|
+
broadcastUiEvent(["library"], "invalidate", { reason: "mcp-uninstalled", id: body.id });
|
|
7306
|
+
} catch (err) {
|
|
7307
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
7308
|
+
}
|
|
7309
|
+
return;
|
|
7310
|
+
}
|
|
7311
|
+
|
|
7312
|
+
if (path === "/api/mcp/configure" && req.method === "POST") {
|
|
7313
|
+
try {
|
|
7314
|
+
const body = await readJsonBody(req);
|
|
7315
|
+
if (!body?.id) {
|
|
7316
|
+
jsonResponse(res, 400, { ok: false, error: "id required" });
|
|
7317
|
+
return;
|
|
7318
|
+
}
|
|
7319
|
+
const existing = await getInstalledMcpServer(repoRoot, body.id);
|
|
7320
|
+
if (!existing) {
|
|
7321
|
+
jsonResponse(res, 404, { ok: false, error: "MCP server not installed" });
|
|
7322
|
+
return;
|
|
7323
|
+
}
|
|
7324
|
+
// Re-install with updated env
|
|
7325
|
+
const updated = await installMcpServer(repoRoot, {
|
|
7326
|
+
...existing.serverConfig,
|
|
7327
|
+
id: body.id,
|
|
7328
|
+
name: existing.name,
|
|
7329
|
+
description: existing.description,
|
|
7330
|
+
tags: existing.tags,
|
|
7331
|
+
}, { envOverrides: body.env });
|
|
7332
|
+
jsonResponse(res, 200, { ok: true, data: updated });
|
|
7333
|
+
} catch (err) {
|
|
7334
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
7335
|
+
}
|
|
7336
|
+
return;
|
|
7337
|
+
}
|
|
7338
|
+
|
|
7339
|
+
// ── Agent Tool Configuration API ──────────────────────────────────────────
|
|
7340
|
+
|
|
7341
|
+
if (path === "/api/agent-tools/available") {
|
|
7342
|
+
try {
|
|
7343
|
+
const available = await listAvailableTools(repoRoot);
|
|
7344
|
+
const bosunTools = await listBosunRuntimeTools({});
|
|
7345
|
+
jsonResponse(res, 200, {
|
|
7346
|
+
ok: true,
|
|
7347
|
+
data: {
|
|
7348
|
+
...available,
|
|
7349
|
+
bosunTools,
|
|
7350
|
+
},
|
|
7351
|
+
});
|
|
7352
|
+
} catch (err) {
|
|
7353
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
7354
|
+
}
|
|
7355
|
+
return;
|
|
7356
|
+
}
|
|
7357
|
+
|
|
7358
|
+
if (path === "/api/agent-tools/config") {
|
|
7359
|
+
try {
|
|
7360
|
+
if (req.method === "GET") {
|
|
7361
|
+
const agentId = (url.searchParams.get("agentId") || "").trim();
|
|
7362
|
+
if (!agentId) {
|
|
7363
|
+
// Return full config
|
|
7364
|
+
const config = loadToolConfig(repoRoot);
|
|
7365
|
+
jsonResponse(res, 200, { ok: true, data: config });
|
|
7366
|
+
return;
|
|
7367
|
+
}
|
|
7368
|
+
const effective = getEffectiveTools(repoRoot, agentId);
|
|
7369
|
+
const bosunTools = await listBosunRuntimeTools({});
|
|
7370
|
+
const raw = getAgentToolConfig(repoRoot, agentId);
|
|
7371
|
+
const enabledSet = Array.isArray(raw?.enabledTools) && raw.enabledTools.length > 0
|
|
7372
|
+
? new Set(raw.enabledTools.map((id) => String(id || "").trim()).filter(Boolean))
|
|
7373
|
+
: null;
|
|
7374
|
+
const bosunToolIdSet = new Set(
|
|
7375
|
+
bosunTools.map((tool) => String(tool?.id || "").trim()).filter(Boolean),
|
|
7376
|
+
);
|
|
7377
|
+
const hasBosunAllowlist = Boolean(
|
|
7378
|
+
enabledSet && [...enabledSet].some((id) => bosunToolIdSet.has(id)),
|
|
7379
|
+
);
|
|
7380
|
+
const effectiveBosunTools = bosunTools.map((tool) => ({
|
|
7381
|
+
...tool,
|
|
7382
|
+
enabled: hasBosunAllowlist ? enabledSet.has(tool.id) : true,
|
|
7383
|
+
}));
|
|
7384
|
+
jsonResponse(res, 200, {
|
|
7385
|
+
ok: true,
|
|
7386
|
+
data: {
|
|
7387
|
+
...effective,
|
|
7388
|
+
bosunTools: effectiveBosunTools,
|
|
7389
|
+
enabledTools: raw?.enabledTools ?? null,
|
|
7390
|
+
},
|
|
7391
|
+
});
|
|
7392
|
+
return;
|
|
7393
|
+
}
|
|
7394
|
+
|
|
7395
|
+
if (req.method === "POST") {
|
|
7396
|
+
const body = await readJsonBody(req);
|
|
7397
|
+
if (!body?.agentId) {
|
|
7398
|
+
jsonResponse(res, 400, { ok: false, error: "agentId required" });
|
|
7399
|
+
return;
|
|
7400
|
+
}
|
|
7401
|
+
const result = setAgentToolConfig(repoRoot, body.agentId, {
|
|
7402
|
+
enabledTools: body.enabledTools,
|
|
7403
|
+
enabledMcpServers: body.enabledMcpServers,
|
|
7404
|
+
disabledBuiltinTools: body.disabledBuiltinTools,
|
|
7405
|
+
});
|
|
7406
|
+
jsonResponse(res, 200, { ok: true, ...result });
|
|
7407
|
+
broadcastUiEvent(["library"], "invalidate", { reason: "agent-tools-updated", agentId: body.agentId });
|
|
7408
|
+
return;
|
|
7409
|
+
}
|
|
7410
|
+
|
|
7411
|
+
jsonResponse(res, 405, { ok: false, error: "method not allowed" });
|
|
7412
|
+
} catch (err) {
|
|
7413
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
7414
|
+
}
|
|
7415
|
+
return;
|
|
7416
|
+
}
|
|
7417
|
+
|
|
7418
|
+
if (path === "/api/agent-tools/defaults") {
|
|
7419
|
+
try {
|
|
7420
|
+
jsonResponse(res, 200, {
|
|
7421
|
+
ok: true,
|
|
7422
|
+
data: {
|
|
7423
|
+
builtinTools: [...DEFAULT_BUILTIN_TOOLS],
|
|
7424
|
+
},
|
|
7425
|
+
});
|
|
7426
|
+
} catch (err) {
|
|
7427
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
7428
|
+
}
|
|
7429
|
+
return;
|
|
7430
|
+
}
|
|
7431
|
+
|
|
6651
7432
|
if (path === "/api/logs") {
|
|
6652
7433
|
const lines = Math.min(
|
|
6653
7434
|
1000,
|
|
@@ -6724,7 +7505,7 @@ async function handleApi(req, res, url) {
|
|
|
6724
7505
|
ok: true,
|
|
6725
7506
|
activeId: String(active?.id || wsId),
|
|
6726
7507
|
});
|
|
6727
|
-
broadcastUiEvent(["workspaces", "tasks", "overview"], "invalidate", {
|
|
7508
|
+
broadcastUiEvent(["workspaces", "tasks", "overview", "sessions", "workflows", "library"], "invalidate", {
|
|
6728
7509
|
reason: "workspace-switched",
|
|
6729
7510
|
workspaceId: wsId,
|
|
6730
7511
|
});
|
|
@@ -7151,6 +7932,17 @@ async function handleApi(req, res, url) {
|
|
|
7151
7932
|
return;
|
|
7152
7933
|
}
|
|
7153
7934
|
|
|
7935
|
+
if (path === "/api/analytics/usage") {
|
|
7936
|
+
try {
|
|
7937
|
+
const days = Number(url.searchParams.get("days") || "30");
|
|
7938
|
+
const data = await buildUsageAnalytics(days || 0);
|
|
7939
|
+
jsonResponse(res, 200, { ok: true, data });
|
|
7940
|
+
} catch (err) {
|
|
7941
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
7942
|
+
}
|
|
7943
|
+
return;
|
|
7944
|
+
}
|
|
7945
|
+
|
|
7154
7946
|
if (path === "/api/agent-logs/context") {
|
|
7155
7947
|
try {
|
|
7156
7948
|
const query = url.searchParams.get("query") || "";
|
|
@@ -7718,14 +8510,14 @@ async function handleApi(req, res, url) {
|
|
|
7718
8510
|
* Workflow API endpoints
|
|
7719
8511
|
* ═══════════════════════════════════════════════════════════ */
|
|
7720
8512
|
|
|
7721
|
-
// Use module-scope getWorkflowEngineModule() for cross-request caching.
|
|
7722
|
-
const getWorkflowEngine = getWorkflowEngineModule;
|
|
7723
|
-
|
|
7724
8513
|
if (path === "/api/workflows") {
|
|
7725
8514
|
try {
|
|
7726
|
-
const
|
|
7727
|
-
if (!
|
|
7728
|
-
|
|
8515
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8516
|
+
if (!wfCtx.ok) {
|
|
8517
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8518
|
+
return;
|
|
8519
|
+
}
|
|
8520
|
+
const engine = wfCtx.engine;
|
|
7729
8521
|
const all = engine.list();
|
|
7730
8522
|
jsonResponse(res, 200, { ok: true, workflows: all.map(w => ({
|
|
7731
8523
|
id: w.id, name: w.name, description: w.description, category: w.category,
|
|
@@ -7742,9 +8534,12 @@ async function handleApi(req, res, url) {
|
|
|
7742
8534
|
if (path === "/api/workflows/save") {
|
|
7743
8535
|
try {
|
|
7744
8536
|
const body = await readJsonBody(req);
|
|
7745
|
-
const
|
|
7746
|
-
if (!
|
|
7747
|
-
|
|
8537
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8538
|
+
if (!wfCtx.ok) {
|
|
8539
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8540
|
+
return;
|
|
8541
|
+
}
|
|
8542
|
+
const engine = wfCtx.engine;
|
|
7748
8543
|
if (typeof _wfTemplates?.applyWorkflowTemplateState === "function") {
|
|
7749
8544
|
_wfTemplates.applyWorkflowTemplateState(body);
|
|
7750
8545
|
}
|
|
@@ -7758,8 +8553,11 @@ async function handleApi(req, res, url) {
|
|
|
7758
8553
|
|
|
7759
8554
|
if (path === "/api/workflows/templates") {
|
|
7760
8555
|
try {
|
|
7761
|
-
const
|
|
7762
|
-
if (!
|
|
8556
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8557
|
+
if (!wfCtx.ok) {
|
|
8558
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8559
|
+
return;
|
|
8560
|
+
}
|
|
7763
8561
|
const tplMod = _wfTemplates;
|
|
7764
8562
|
const list = tplMod.listTemplates();
|
|
7765
8563
|
jsonResponse(res, 200, { ok: true, templates: list });
|
|
@@ -7772,10 +8570,13 @@ async function handleApi(req, res, url) {
|
|
|
7772
8570
|
if (path === "/api/workflows/install-template") {
|
|
7773
8571
|
try {
|
|
7774
8572
|
const body = await readJsonBody(req);
|
|
7775
|
-
const
|
|
7776
|
-
if (!
|
|
8573
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8574
|
+
if (!wfCtx.ok) {
|
|
8575
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8576
|
+
return;
|
|
8577
|
+
}
|
|
7777
8578
|
const tplMod = _wfTemplates;
|
|
7778
|
-
const engine =
|
|
8579
|
+
const engine = wfCtx.engine;
|
|
7779
8580
|
const wf = await tplMod.installTemplate(body.templateId, engine, body.overrides);
|
|
7780
8581
|
jsonResponse(res, 200, { ok: true, workflow: wf });
|
|
7781
8582
|
} catch (err) {
|
|
@@ -7786,9 +8587,12 @@ async function handleApi(req, res, url) {
|
|
|
7786
8587
|
|
|
7787
8588
|
if (path === "/api/workflows/template-updates") {
|
|
7788
8589
|
try {
|
|
7789
|
-
const
|
|
7790
|
-
if (!
|
|
7791
|
-
|
|
8590
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8591
|
+
if (!wfCtx.ok) {
|
|
8592
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8593
|
+
return;
|
|
8594
|
+
}
|
|
8595
|
+
const engine = wfCtx.engine;
|
|
7792
8596
|
if (typeof _wfTemplates?.reconcileInstalledTemplates === "function") {
|
|
7793
8597
|
_wfTemplates.reconcileInstalledTemplates(engine, {
|
|
7794
8598
|
autoUpdateUnmodified: true,
|
|
@@ -7820,9 +8624,12 @@ async function handleApi(req, res, url) {
|
|
|
7820
8624
|
|
|
7821
8625
|
if (path.startsWith("/api/workflows/") && path.endsWith("/template-update")) {
|
|
7822
8626
|
try {
|
|
7823
|
-
const
|
|
7824
|
-
if (!
|
|
7825
|
-
|
|
8627
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8628
|
+
if (!wfCtx.ok) {
|
|
8629
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8630
|
+
return;
|
|
8631
|
+
}
|
|
8632
|
+
const engine = wfCtx.engine;
|
|
7826
8633
|
const workflowId = decodeURIComponent(path.split("/")[3] || "");
|
|
7827
8634
|
if (!workflowId) {
|
|
7828
8635
|
jsonResponse(res, 400, { ok: false, error: "Missing workflow id" });
|
|
@@ -7845,8 +8652,12 @@ async function handleApi(req, res, url) {
|
|
|
7845
8652
|
|
|
7846
8653
|
if (path === "/api/workflows/node-types") {
|
|
7847
8654
|
try {
|
|
7848
|
-
const
|
|
7849
|
-
if (!
|
|
8655
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8656
|
+
if (!wfCtx.ok) {
|
|
8657
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8658
|
+
return;
|
|
8659
|
+
}
|
|
8660
|
+
const wfMod = wfCtx.wfMod;
|
|
7850
8661
|
const types = wfMod.listNodeTypes();
|
|
7851
8662
|
jsonResponse(res, 200, { ok: true, nodeTypes: types.map(nt => ({
|
|
7852
8663
|
type: nt.type,
|
|
@@ -7862,9 +8673,12 @@ async function handleApi(req, res, url) {
|
|
|
7862
8673
|
|
|
7863
8674
|
if (path === "/api/workflows/runs") {
|
|
7864
8675
|
try {
|
|
7865
|
-
const
|
|
7866
|
-
if (!
|
|
7867
|
-
|
|
8676
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8677
|
+
if (!wfCtx.ok) {
|
|
8678
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8679
|
+
return;
|
|
8680
|
+
}
|
|
8681
|
+
const engine = wfCtx.engine;
|
|
7868
8682
|
const rawLimit = Number(url.searchParams.get("limit"));
|
|
7869
8683
|
const limit = Number.isFinite(rawLimit) && rawLimit > 0
|
|
7870
8684
|
? Math.min(rawLimit, 500)
|
|
@@ -7879,9 +8693,12 @@ async function handleApi(req, res, url) {
|
|
|
7879
8693
|
|
|
7880
8694
|
if (path.startsWith("/api/workflows/runs/")) {
|
|
7881
8695
|
try {
|
|
7882
|
-
const
|
|
7883
|
-
if (!
|
|
7884
|
-
|
|
8696
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8697
|
+
if (!wfCtx.ok) {
|
|
8698
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8699
|
+
return;
|
|
8700
|
+
}
|
|
8701
|
+
const engine = wfCtx.engine;
|
|
7885
8702
|
const subPath = path.replace("/api/workflows/runs/", "");
|
|
7886
8703
|
const segments = subPath.split("/").map(decodeURIComponent);
|
|
7887
8704
|
const runId = (segments[0] || "").trim();
|
|
@@ -7963,9 +8780,12 @@ async function handleApi(req, res, url) {
|
|
|
7963
8780
|
const action = segments[1] || "";
|
|
7964
8781
|
|
|
7965
8782
|
try {
|
|
7966
|
-
const
|
|
7967
|
-
if (!
|
|
7968
|
-
|
|
8783
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
8784
|
+
if (!wfCtx.ok) {
|
|
8785
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
8786
|
+
return;
|
|
8787
|
+
}
|
|
8788
|
+
const engine = wfCtx.engine;
|
|
7969
8789
|
|
|
7970
8790
|
if (action === "execute" && req.method === "POST") {
|
|
7971
8791
|
const body = await readJsonBody(req);
|
|
@@ -8850,11 +9670,20 @@ async function handleApi(req, res, url) {
|
|
|
8850
9670
|
if (path === "/api/sessions" && req.method === "GET") {
|
|
8851
9671
|
try {
|
|
8852
9672
|
const tracker = getSessionTracker();
|
|
9673
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: true });
|
|
9674
|
+
if (!workspaceContext) {
|
|
9675
|
+
jsonResponse(res, 400, { ok: false, error: "Unknown workspace" });
|
|
9676
|
+
return;
|
|
9677
|
+
}
|
|
8853
9678
|
let sessions = tracker.listAllSessions();
|
|
8854
9679
|
const typeFilter = url.searchParams.get("type");
|
|
8855
9680
|
const statusFilter = url.searchParams.get("status");
|
|
8856
9681
|
if (typeFilter) sessions = sessions.filter((s) => s.type === typeFilter);
|
|
8857
9682
|
if (statusFilter) sessions = sessions.filter((s) => s.status === statusFilter);
|
|
9683
|
+
sessions = sessions.filter((session) => {
|
|
9684
|
+
const detailed = tracker.getSessionById(session.id) || session;
|
|
9685
|
+
return sessionMatchesWorkspaceContext(detailed, workspaceContext);
|
|
9686
|
+
});
|
|
8858
9687
|
jsonResponse(res, 200, { ok: true, sessions });
|
|
8859
9688
|
} catch (err) {
|
|
8860
9689
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
@@ -8900,10 +9729,24 @@ async function handleApi(req, res, url) {
|
|
|
8900
9729
|
if (sessionMatch) {
|
|
8901
9730
|
const sessionId = decodeURIComponent(sessionMatch[1]);
|
|
8902
9731
|
const action = sessionMatch[2] || null;
|
|
9732
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: false });
|
|
9733
|
+
if (!workspaceContext) {
|
|
9734
|
+
jsonResponse(res, 400, { ok: false, error: "Unknown workspace" });
|
|
9735
|
+
return;
|
|
9736
|
+
}
|
|
9737
|
+
const tracker = getSessionTracker();
|
|
9738
|
+
const getScopedSession = () => {
|
|
9739
|
+
const session = tracker.getSessionById(sessionId);
|
|
9740
|
+
if (!session) return null;
|
|
9741
|
+
return sessionMatchesWorkspaceContext(session, workspaceContext) ? session : null;
|
|
9742
|
+
};
|
|
8903
9743
|
|
|
8904
9744
|
if (!action && req.method === "GET") {
|
|
8905
9745
|
try {
|
|
8906
|
-
|
|
9746
|
+
if (!getScopedSession()) {
|
|
9747
|
+
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
9748
|
+
return;
|
|
9749
|
+
}
|
|
8907
9750
|
const session = tracker.getSessionMessages(sessionId);
|
|
8908
9751
|
if (!session) {
|
|
8909
9752
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
@@ -8944,8 +9787,7 @@ async function handleApi(req, res, url) {
|
|
|
8944
9787
|
|
|
8945
9788
|
if (action === "attachments" && req.method === "POST") {
|
|
8946
9789
|
try {
|
|
8947
|
-
const
|
|
8948
|
-
const session = tracker.getSessionById(sessionId);
|
|
9790
|
+
const session = getScopedSession();
|
|
8949
9791
|
if (!session) {
|
|
8950
9792
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
8951
9793
|
return;
|
|
@@ -8997,8 +9839,7 @@ async function handleApi(req, res, url) {
|
|
|
8997
9839
|
|
|
8998
9840
|
if (action === "stop" && req.method === "POST") {
|
|
8999
9841
|
try {
|
|
9000
|
-
const
|
|
9001
|
-
const session = tracker.getSessionById(sessionId);
|
|
9842
|
+
const session = getScopedSession();
|
|
9002
9843
|
if (!session) {
|
|
9003
9844
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
9004
9845
|
return;
|
|
@@ -9033,8 +9874,7 @@ async function handleApi(req, res, url) {
|
|
|
9033
9874
|
|
|
9034
9875
|
if (action === "message" && req.method === "POST") {
|
|
9035
9876
|
try {
|
|
9036
|
-
const
|
|
9037
|
-
const session = tracker.getSessionById(sessionId);
|
|
9877
|
+
const session = getScopedSession();
|
|
9038
9878
|
if (!session) {
|
|
9039
9879
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
9040
9880
|
return;
|
|
@@ -9188,8 +10028,7 @@ async function handleApi(req, res, url) {
|
|
|
9188
10028
|
|
|
9189
10029
|
if (action === "message/edit" && req.method === "POST") {
|
|
9190
10030
|
try {
|
|
9191
|
-
const
|
|
9192
|
-
const session = tracker.getSessionById(sessionId);
|
|
10031
|
+
const session = getScopedSession();
|
|
9193
10032
|
if (!session) {
|
|
9194
10033
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
9195
10034
|
return;
|
|
@@ -9226,8 +10065,7 @@ async function handleApi(req, res, url) {
|
|
|
9226
10065
|
|
|
9227
10066
|
if (action === "archive" && req.method === "POST") {
|
|
9228
10067
|
try {
|
|
9229
|
-
const
|
|
9230
|
-
const session = tracker.getSessionById(sessionId);
|
|
10068
|
+
const session = getScopedSession();
|
|
9231
10069
|
if (!session) {
|
|
9232
10070
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
9233
10071
|
return;
|
|
@@ -9246,8 +10084,7 @@ async function handleApi(req, res, url) {
|
|
|
9246
10084
|
|
|
9247
10085
|
if (action === "resume" && req.method === "POST") {
|
|
9248
10086
|
try {
|
|
9249
|
-
const
|
|
9250
|
-
const session = tracker.getSessionById(sessionId);
|
|
10087
|
+
const session = getScopedSession();
|
|
9251
10088
|
if (!session) {
|
|
9252
10089
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
9253
10090
|
return;
|
|
@@ -9263,8 +10100,7 @@ async function handleApi(req, res, url) {
|
|
|
9263
10100
|
|
|
9264
10101
|
if (action === "delete" && req.method === "POST") {
|
|
9265
10102
|
try {
|
|
9266
|
-
const
|
|
9267
|
-
const session = tracker.getSessionById(sessionId);
|
|
10103
|
+
const session = getScopedSession();
|
|
9268
10104
|
if (!session) {
|
|
9269
10105
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
9270
10106
|
return;
|
|
@@ -9283,8 +10119,7 @@ async function handleApi(req, res, url) {
|
|
|
9283
10119
|
|
|
9284
10120
|
if (action === "rename" && req.method === "POST") {
|
|
9285
10121
|
try {
|
|
9286
|
-
const
|
|
9287
|
-
const session = tracker.getSessionById(sessionId);
|
|
10122
|
+
const session = getScopedSession();
|
|
9288
10123
|
if (!session) {
|
|
9289
10124
|
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
9290
10125
|
return;
|
|
@@ -9306,8 +10141,7 @@ async function handleApi(req, res, url) {
|
|
|
9306
10141
|
|
|
9307
10142
|
if (action === "diff" && req.method === "GET") {
|
|
9308
10143
|
try {
|
|
9309
|
-
const
|
|
9310
|
-
const session = tracker.getSessionById(sessionId);
|
|
10144
|
+
const session = getScopedSession();
|
|
9311
10145
|
if (!session) {
|
|
9312
10146
|
jsonResponse(res, 200, {
|
|
9313
10147
|
ok: true,
|
|
@@ -9500,9 +10334,10 @@ async function handleApi(req, res, url) {
|
|
|
9500
10334
|
// Respect it as final and issue the probe directly.
|
|
9501
10335
|
testUrl = base;
|
|
9502
10336
|
} else {
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
10337
|
+
// Always use /openai/models — works on both classic Azure OpenAI and
|
|
10338
|
+
// Azure AI Foundry (Global Standard) where /openai/deployments/{name}
|
|
10339
|
+
// returns 404.
|
|
10340
|
+
testUrl = `${base}/openai/models?api-version=2024-10-21`;
|
|
9506
10341
|
}
|
|
9507
10342
|
if (apiKey) headers["api-key"] = apiKey;
|
|
9508
10343
|
else {
|
|
@@ -9560,8 +10395,37 @@ async function handleApi(req, res, url) {
|
|
|
9560
10395
|
clearTimeout(timer);
|
|
9561
10396
|
const latencyMs = Date.now() - start;
|
|
9562
10397
|
if (resp.ok || resp.status === 200) {
|
|
9563
|
-
//
|
|
9564
|
-
|
|
10398
|
+
// Credentials verified via /openai/models. If a deployment name was
|
|
10399
|
+
// provided, do a secondary check to confirm the deployment exists.
|
|
10400
|
+
if (provider === "azure" && deployment) {
|
|
10401
|
+
const dep = String(deployment).trim();
|
|
10402
|
+
try {
|
|
10403
|
+
let base2 = String(azureEndpoint || "").replace(/\/+$/, "");
|
|
10404
|
+
try { const u2 = new URL(base2); base2 = `${u2.protocol}//${u2.host}`; } catch { /* keep */ }
|
|
10405
|
+
const depUrl = `${base2}/openai/deployments/${encodeURIComponent(dep)}/chat/completions?api-version=2024-10-21`;
|
|
10406
|
+
const depCtrl = new AbortController();
|
|
10407
|
+
const depTimer = setTimeout(() => depCtrl.abort(), 8_000);
|
|
10408
|
+
const depResp = await fetch(depUrl, {
|
|
10409
|
+
method: "POST",
|
|
10410
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
10411
|
+
body: JSON.stringify({ messages: [{ role: "user", content: "test" }], max_tokens: 1 }),
|
|
10412
|
+
signal: depCtrl.signal,
|
|
10413
|
+
});
|
|
10414
|
+
clearTimeout(depTimer);
|
|
10415
|
+
// 200 = chat model works, 400 = realtime model (expected), both confirm deployment exists
|
|
10416
|
+
if (depResp.ok || depResp.status === 400) {
|
|
10417
|
+
jsonResponse(res, 200, { ok: true, latencyMs, deployment: dep });
|
|
10418
|
+
} else if (depResp.status === 404) {
|
|
10419
|
+
jsonResponse(res, 200, { ok: false, error: `Credentials valid but deployment "${dep}" not found — check the deployment name in Azure AI Foundry`, latencyMs });
|
|
10420
|
+
} else {
|
|
10421
|
+
jsonResponse(res, 200, { ok: true, latencyMs, warning: `Credentials valid. Could not verify deployment "${dep}" (HTTP ${depResp.status})` });
|
|
10422
|
+
}
|
|
10423
|
+
} catch {
|
|
10424
|
+
jsonResponse(res, 200, { ok: true, latencyMs, warning: `Credentials valid. Could not verify deployment "${dep}" (timeout)` });
|
|
10425
|
+
}
|
|
10426
|
+
} else {
|
|
10427
|
+
jsonResponse(res, 200, { ok: true, latencyMs });
|
|
10428
|
+
}
|
|
9565
10429
|
} else {
|
|
9566
10430
|
const text = await resp.text().catch(() => "");
|
|
9567
10431
|
let errMsg = `HTTP ${resp.status}`;
|
|
@@ -9570,9 +10434,13 @@ async function handleApi(req, res, url) {
|
|
|
9570
10434
|
const missing = String(errMsg || "").match(/Missing scopes?:\s*([A-Za-z0-9._:\s-]+)/i)?.[1]?.trim() || "required scopes";
|
|
9571
10435
|
errMsg = `OpenAI Connected Account token is missing scopes (${missing}). Sign out and reconnect OpenAI in Connected Accounts. Also verify role access: org Owner/Reader, project Owner/Member, and workspace RBAC API/dashboard permissions.`;
|
|
9572
10436
|
}
|
|
9573
|
-
//
|
|
9574
|
-
if (
|
|
9575
|
-
|
|
10437
|
+
// Azure-specific: provide helpful messages for common errors
|
|
10438
|
+
if (provider === "azure") {
|
|
10439
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
10440
|
+
errMsg = `Authentication failed (HTTP ${resp.status}) — check API key and endpoint URL`;
|
|
10441
|
+
} else if (resp.status === 404) {
|
|
10442
|
+
errMsg = `Endpoint not found (HTTP 404) — check the Azure endpoint URL. Use https://<resource>.openai.azure.com`;
|
|
10443
|
+
}
|
|
9576
10444
|
}
|
|
9577
10445
|
jsonResponse(res, 200, { ok: false, error: errMsg, latencyMs });
|
|
9578
10446
|
}
|
|
@@ -9799,10 +10667,33 @@ async function handleApi(req, res, url) {
|
|
|
9799
10667
|
return;
|
|
9800
10668
|
}
|
|
9801
10669
|
|
|
10670
|
+
// GET /api/voice/agents
|
|
10671
|
+
if (path === "/api/voice/agents" && req.method === "GET") {
|
|
10672
|
+
try {
|
|
10673
|
+
const callContext = {
|
|
10674
|
+
sessionId: String(url.searchParams.get("sessionId") || "").trim() || undefined,
|
|
10675
|
+
};
|
|
10676
|
+
const libraryRoot = resolveVoiceLibraryRoot(callContext);
|
|
10677
|
+
const { agents, selected } = resolveActiveVoiceAgent(
|
|
10678
|
+
libraryRoot,
|
|
10679
|
+
String(url.searchParams.get("voiceAgentId") || "").trim(),
|
|
10680
|
+
);
|
|
10681
|
+
jsonResponse(res, 200, {
|
|
10682
|
+
ok: true,
|
|
10683
|
+
agents,
|
|
10684
|
+
defaultAgentId: selected?.id || null,
|
|
10685
|
+
});
|
|
10686
|
+
} catch (err) {
|
|
10687
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
10688
|
+
}
|
|
10689
|
+
return;
|
|
10690
|
+
}
|
|
10691
|
+
|
|
9802
10692
|
// POST /api/voice/token
|
|
9803
10693
|
if (path === "/api/voice/token" && req.method === "POST") {
|
|
9804
10694
|
try {
|
|
9805
10695
|
const body = await readJsonBody(req).catch(() => ({}));
|
|
10696
|
+
const requestedVoiceAgentId = String(body?.voiceAgentId || "").trim();
|
|
9806
10697
|
const callContext = {
|
|
9807
10698
|
sessionId: String(body?.sessionId || "").trim() || undefined,
|
|
9808
10699
|
executor: String(body?.executor || "").trim() || undefined,
|
|
@@ -9810,18 +10701,56 @@ async function handleApi(req, res, url) {
|
|
|
9810
10701
|
model: String(body?.model || "").trim() || undefined,
|
|
9811
10702
|
authSource: String(authResult?.source || "").trim() || undefined,
|
|
9812
10703
|
};
|
|
10704
|
+
const libraryRoot = resolveVoiceLibraryRoot(callContext);
|
|
10705
|
+
const { selected: selectedVoiceAgent } = resolveActiveVoiceAgent(
|
|
10706
|
+
libraryRoot,
|
|
10707
|
+
requestedVoiceAgentId,
|
|
10708
|
+
);
|
|
10709
|
+
const activeVoiceAgentId = selectedVoiceAgent?.id || "voice-agent";
|
|
9813
10710
|
const { createEphemeralToken, getVoiceToolDefinitions, getVoiceConfig, isPrivilegedVoiceContext } = await import("./voice-relay.mjs");
|
|
9814
10711
|
const privileged = isPrivilegedVoiceContext(callContext);
|
|
9815
10712
|
const delegateOnly =
|
|
9816
10713
|
body?.delegateOnly === true && !privileged;
|
|
9817
|
-
|
|
9818
|
-
|
|
10714
|
+
let tools = await getVoiceToolDefinitions({ delegateOnly, context: callContext });
|
|
10715
|
+
|
|
10716
|
+
const voiceToolCfg = getAgentToolConfig(libraryRoot, activeVoiceAgentId);
|
|
10717
|
+
tools = applyVoiceAgentToolFilters(tools, voiceToolCfg);
|
|
10718
|
+
const capabilityPrompt = buildVoiceToolCapabilityPrompt(
|
|
10719
|
+
tools,
|
|
10720
|
+
voiceToolCfg,
|
|
10721
|
+
selectedVoiceAgent,
|
|
10722
|
+
);
|
|
10723
|
+
|
|
10724
|
+
const voiceCallContext = {
|
|
10725
|
+
...callContext,
|
|
10726
|
+
voiceAgentId: activeVoiceAgentId,
|
|
10727
|
+
voiceAgentName: selectedVoiceAgent?.name || undefined,
|
|
10728
|
+
voiceAgentInstructions: selectedVoiceAgent?.voiceInstructions || undefined,
|
|
10729
|
+
voiceAgentSkills: Array.isArray(selectedVoiceAgent?.skills) ? selectedVoiceAgent.skills : undefined,
|
|
10730
|
+
voiceToolCapabilityPrompt: capabilityPrompt,
|
|
10731
|
+
enabledMcpServers: Array.isArray(voiceToolCfg?.enabledMcpServers)
|
|
10732
|
+
? voiceToolCfg.enabledMcpServers
|
|
10733
|
+
: undefined,
|
|
10734
|
+
};
|
|
10735
|
+
const tokenData = await createEphemeralToken(tools, voiceCallContext);
|
|
10736
|
+
tokenData.voiceAgentId = activeVoiceAgentId;
|
|
10737
|
+
tokenData.voiceAgentName = selectedVoiceAgent?.name || null;
|
|
10738
|
+
tokenData.voiceAgentSkills = Array.isArray(selectedVoiceAgent?.skills) ? selectedVoiceAgent.skills : [];
|
|
10739
|
+
tokenData.enabledMcpServers = Array.isArray(voiceToolCfg?.enabledMcpServers)
|
|
10740
|
+
? voiceToolCfg.enabledMcpServers
|
|
10741
|
+
: [];
|
|
10742
|
+
tokenData.tools = Array.isArray(tools) ? tools : [];
|
|
9819
10743
|
|
|
9820
10744
|
// When client requests sdkMode, include extra fields for @openai/agents SDK
|
|
9821
10745
|
if (body?.sdkMode === true) {
|
|
9822
10746
|
const voiceCfg = getVoiceConfig();
|
|
9823
|
-
tokenData.instructions = voiceCfg.instructions ||
|
|
9824
|
-
|
|
10747
|
+
tokenData.instructions = [voiceCfg.instructions || "", capabilityPrompt]
|
|
10748
|
+
.filter(Boolean)
|
|
10749
|
+
.join("\n\n")
|
|
10750
|
+
.trim() || undefined;
|
|
10751
|
+
if (selectedVoiceAgent?.voiceInstructions) {
|
|
10752
|
+
tokenData.instructions = `${tokenData.instructions || ""}\n\n${selectedVoiceAgent.voiceInstructions}`.trim();
|
|
10753
|
+
}
|
|
9825
10754
|
if (tokenData.provider === "azure") {
|
|
9826
10755
|
tokenData.azureEndpoint = voiceCfg.azureEndpoint || undefined;
|
|
9827
10756
|
tokenData.azureDeployment = voiceCfg.azureDeployment || undefined;
|
|
@@ -9850,6 +10779,7 @@ async function handleApi(req, res, url) {
|
|
|
9850
10779
|
executor: String(body?.executor || "").trim() || undefined,
|
|
9851
10780
|
mode: String(body?.mode || "").trim() || undefined,
|
|
9852
10781
|
model: String(body?.model || "").trim() || undefined,
|
|
10782
|
+
voiceAgentId: String(body?.voiceAgentId || "").trim() || undefined,
|
|
9853
10783
|
};
|
|
9854
10784
|
const options = {
|
|
9855
10785
|
voiceId: String(body?.voiceId || "").trim() || undefined,
|
|
@@ -9864,9 +10794,55 @@ async function handleApi(req, res, url) {
|
|
|
9864
10794
|
return;
|
|
9865
10795
|
}
|
|
9866
10796
|
|
|
9867
|
-
//
|
|
9868
|
-
if (path === "/api/
|
|
10797
|
+
// GET /api/agents/tools
|
|
10798
|
+
if (path === "/api/agents/tools" && req.method === "GET") {
|
|
9869
10799
|
try {
|
|
10800
|
+
const { getVoiceToolDefinitions, getAllowedVoiceTools } = await import("./voice-relay.mjs");
|
|
10801
|
+
const requestedVoiceAgentId = String(url.searchParams.get("voiceAgentId") || "").trim();
|
|
10802
|
+
const context = {
|
|
10803
|
+
sessionId: String(url.searchParams.get("sessionId") || "").trim() || undefined,
|
|
10804
|
+
executor: String(url.searchParams.get("executor") || "").trim() || undefined,
|
|
10805
|
+
mode: String(url.searchParams.get("mode") || "").trim() || undefined,
|
|
10806
|
+
model: String(url.searchParams.get("model") || "").trim() || undefined,
|
|
10807
|
+
authSource: String(authResult?.source || "").trim() || undefined,
|
|
10808
|
+
};
|
|
10809
|
+
const allTools = await getVoiceToolDefinitions({ delegateOnly: false, context });
|
|
10810
|
+
const allowed = await getAllowedVoiceTools(context);
|
|
10811
|
+
const allowedTools = allowed instanceof Set ? allowed : null;
|
|
10812
|
+
let tools = allowedTools
|
|
10813
|
+
? (Array.isArray(allTools) ? allTools : []).filter((tool) => allowedTools.has(String(tool?.name || "").trim()))
|
|
10814
|
+
: allTools;
|
|
10815
|
+
const libraryRoot = resolveVoiceLibraryRoot(context);
|
|
10816
|
+
const { selected: selectedVoiceAgent } = resolveActiveVoiceAgent(
|
|
10817
|
+
libraryRoot,
|
|
10818
|
+
requestedVoiceAgentId,
|
|
10819
|
+
);
|
|
10820
|
+
const activeVoiceAgentId = selectedVoiceAgent?.id || "voice-agent";
|
|
10821
|
+
const voiceToolCfg = getAgentToolConfig(libraryRoot, activeVoiceAgentId);
|
|
10822
|
+
tools = applyVoiceAgentToolFilters(tools, voiceToolCfg);
|
|
10823
|
+
jsonResponse(res, 200, {
|
|
10824
|
+
ok: true,
|
|
10825
|
+
tools: Array.isArray(tools) ? tools : [],
|
|
10826
|
+
allowedTools: allowedTools ? Array.from(allowedTools.values()).sort() : null,
|
|
10827
|
+
totalTools: Array.isArray(tools) ? tools.length : 0,
|
|
10828
|
+
voiceAgentId: activeVoiceAgentId,
|
|
10829
|
+
});
|
|
10830
|
+
} catch (err) {
|
|
10831
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
10832
|
+
}
|
|
10833
|
+
return;
|
|
10834
|
+
}
|
|
10835
|
+
|
|
10836
|
+
// POST /api/voice/tool
|
|
10837
|
+
// POST /api/agents/tool
|
|
10838
|
+
if ((path === "/api/voice/tool" || path === "/api/agents/tool") && req.method === "POST") {
|
|
10839
|
+
try {
|
|
10840
|
+
const isVoiceToolRoute = path === "/api/voice/tool";
|
|
10841
|
+
const eventSource = isVoiceToolRoute ? "voice" : "agent";
|
|
10842
|
+
const startedEventType = isVoiceToolRoute ? "voice_tool_started" : "agent_tool_started";
|
|
10843
|
+
const errorEventType = isVoiceToolRoute ? "voice_tool_error" : "agent_tool_error";
|
|
10844
|
+
const completeEventType = isVoiceToolRoute ? "voice_tool_complete_summary" : "agent_tool_complete_summary";
|
|
10845
|
+
const eventLabel = isVoiceToolRoute ? "Voice Action" : "Agent Tool";
|
|
9870
10846
|
const body = await readJsonBody(req);
|
|
9871
10847
|
const {
|
|
9872
10848
|
toolName,
|
|
@@ -9875,10 +10851,15 @@ async function handleApi(req, res, url) {
|
|
|
9875
10851
|
executor,
|
|
9876
10852
|
mode,
|
|
9877
10853
|
model,
|
|
10854
|
+
voiceAgentId,
|
|
9878
10855
|
} = body || {};
|
|
9879
10856
|
const normalizedToolName = String(toolName || "").trim();
|
|
9880
10857
|
if (!normalizedToolName) {
|
|
9881
|
-
|
|
10858
|
+
if (isVoiceToolRoute) {
|
|
10859
|
+
jsonResponse(res, 400, { error: "toolName required" });
|
|
10860
|
+
} else {
|
|
10861
|
+
jsonResponse(res, 400, { ok: false, error: "toolName required" });
|
|
10862
|
+
}
|
|
9882
10863
|
return;
|
|
9883
10864
|
}
|
|
9884
10865
|
const summarizeToolArgs = (value) => {
|
|
@@ -9908,8 +10889,16 @@ async function handleApi(req, res, url) {
|
|
|
9908
10889
|
mode: String(mode || "").trim() || undefined,
|
|
9909
10890
|
model: String(model || "").trim() || undefined,
|
|
9910
10891
|
authSource: String(authResult?.source || "").trim() || undefined,
|
|
10892
|
+
voiceAgentId: String(voiceAgentId || "").trim() || undefined,
|
|
9911
10893
|
};
|
|
9912
10894
|
const normalizedArgs = normalizeVoiceToolArgs(normalizedToolName, args || {});
|
|
10895
|
+
const libraryRoot = resolveVoiceLibraryRoot(context);
|
|
10896
|
+
const { selected: selectedVoiceAgent } = resolveActiveVoiceAgent(
|
|
10897
|
+
libraryRoot,
|
|
10898
|
+
context.voiceAgentId || "",
|
|
10899
|
+
);
|
|
10900
|
+
const activeVoiceAgentId = selectedVoiceAgent?.id || "voice-agent";
|
|
10901
|
+
const voiceToolCfg = getAgentToolConfig(libraryRoot, activeVoiceAgentId);
|
|
9913
10902
|
let tracker = null;
|
|
9914
10903
|
let session = null;
|
|
9915
10904
|
if (context.sessionId) {
|
|
@@ -9928,11 +10917,11 @@ async function handleApi(req, res, url) {
|
|
|
9928
10917
|
}
|
|
9929
10918
|
tracker.recordEvent(session?.id || context.sessionId, {
|
|
9930
10919
|
role: "system",
|
|
9931
|
-
content: `[
|
|
10920
|
+
content: `[${eventLabel} Started] ${normalizedToolName}\nArgs: ${summarizeToolArgs(normalizedArgs)}`,
|
|
9932
10921
|
timestamp: new Date().toISOString(),
|
|
9933
10922
|
meta: {
|
|
9934
|
-
source:
|
|
9935
|
-
eventType:
|
|
10923
|
+
source: eventSource,
|
|
10924
|
+
eventType: startedEventType,
|
|
9936
10925
|
toolName: normalizedToolName,
|
|
9937
10926
|
},
|
|
9938
10927
|
});
|
|
@@ -9941,9 +10930,43 @@ async function handleApi(req, res, url) {
|
|
|
9941
10930
|
// Session-bound calls are validated against the allowed tool set
|
|
9942
10931
|
const allowed = await getAllowedVoiceTools(context);
|
|
9943
10932
|
if (!allowed.has(normalizedToolName)) {
|
|
9944
|
-
|
|
9945
|
-
|
|
9946
|
-
|
|
10933
|
+
const deniedMessage = `Tool "${normalizedToolName}" is not allowed for session-bound calls`;
|
|
10934
|
+
if (isVoiceToolRoute) {
|
|
10935
|
+
jsonResponse(res, 400, { error: deniedMessage });
|
|
10936
|
+
} else {
|
|
10937
|
+
jsonResponse(res, 400, { ok: false, error: deniedMessage });
|
|
10938
|
+
}
|
|
10939
|
+
return;
|
|
10940
|
+
}
|
|
10941
|
+
}
|
|
10942
|
+
const toolEnabledForAgent =
|
|
10943
|
+
applyVoiceAgentToolFilters([{ name: normalizedToolName }], voiceToolCfg).length > 0;
|
|
10944
|
+
if (!toolEnabledForAgent) {
|
|
10945
|
+
const deniedMessage = `Tool "${normalizedToolName}" is not enabled for voice agent "${activeVoiceAgentId}"`;
|
|
10946
|
+
if (isVoiceToolRoute) {
|
|
10947
|
+
jsonResponse(res, 403, { error: deniedMessage });
|
|
10948
|
+
} else {
|
|
10949
|
+
jsonResponse(res, 403, { ok: false, error: deniedMessage });
|
|
10950
|
+
}
|
|
10951
|
+
return;
|
|
10952
|
+
}
|
|
10953
|
+
if (
|
|
10954
|
+
normalizedToolName === "invoke_mcp_tool"
|
|
10955
|
+
&& Array.isArray(voiceToolCfg?.enabledMcpServers)
|
|
10956
|
+
&& voiceToolCfg.enabledMcpServers.length > 0
|
|
10957
|
+
) {
|
|
10958
|
+
const requestedServer = String(
|
|
10959
|
+
normalizedArgs?.server
|
|
10960
|
+
|| normalizedArgs?.serverId
|
|
10961
|
+
|| "",
|
|
10962
|
+
).trim();
|
|
10963
|
+
if (requestedServer && !voiceToolCfg.enabledMcpServers.includes(requestedServer)) {
|
|
10964
|
+
const deniedMessage = `MCP server "${requestedServer}" is not enabled for voice agent "${activeVoiceAgentId}"`;
|
|
10965
|
+
if (isVoiceToolRoute) {
|
|
10966
|
+
jsonResponse(res, 403, { error: deniedMessage });
|
|
10967
|
+
} else {
|
|
10968
|
+
jsonResponse(res, 403, { ok: false, error: deniedMessage });
|
|
10969
|
+
}
|
|
9947
10970
|
return;
|
|
9948
10971
|
}
|
|
9949
10972
|
}
|
|
@@ -9952,31 +10975,43 @@ async function handleApi(req, res, url) {
|
|
|
9952
10975
|
if (result?.error) {
|
|
9953
10976
|
tracker.recordEvent(session?.id || context.sessionId, {
|
|
9954
10977
|
role: "system",
|
|
9955
|
-
content: `[
|
|
10978
|
+
content: `[${eventLabel} Error] ${normalizedToolName}\nError: ${String(result.error || "Unknown error")}`,
|
|
9956
10979
|
timestamp: new Date().toISOString(),
|
|
9957
10980
|
meta: {
|
|
9958
|
-
source:
|
|
9959
|
-
eventType:
|
|
10981
|
+
source: eventSource,
|
|
10982
|
+
eventType: errorEventType,
|
|
9960
10983
|
toolName: normalizedToolName,
|
|
9961
10984
|
},
|
|
9962
10985
|
});
|
|
9963
10986
|
} else {
|
|
9964
10987
|
tracker.recordEvent(session?.id || context.sessionId, {
|
|
9965
10988
|
role: "system",
|
|
9966
|
-
content: `[
|
|
10989
|
+
content: `[${eventLabel} Complete] ${normalizedToolName}\nSummary: ${summarizeToolResult(result?.result)}`,
|
|
9967
10990
|
timestamp: new Date().toISOString(),
|
|
9968
10991
|
meta: {
|
|
9969
|
-
source:
|
|
9970
|
-
eventType:
|
|
10992
|
+
source: eventSource,
|
|
10993
|
+
eventType: completeEventType,
|
|
9971
10994
|
toolName: normalizedToolName,
|
|
9972
10995
|
},
|
|
9973
10996
|
});
|
|
9974
10997
|
}
|
|
9975
10998
|
}
|
|
9976
10999
|
|
|
9977
|
-
|
|
11000
|
+
if (isVoiceToolRoute) {
|
|
11001
|
+
jsonResponse(res, 200, result);
|
|
11002
|
+
} else {
|
|
11003
|
+
jsonResponse(res, 200, {
|
|
11004
|
+
ok: !result?.error,
|
|
11005
|
+
toolName: normalizedToolName,
|
|
11006
|
+
...result,
|
|
11007
|
+
});
|
|
11008
|
+
}
|
|
9978
11009
|
} catch (err) {
|
|
9979
|
-
|
|
11010
|
+
if (path === "/api/voice/tool") {
|
|
11011
|
+
jsonResponse(res, 500, { error: err.message });
|
|
11012
|
+
} else {
|
|
11013
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
11014
|
+
}
|
|
9980
11015
|
}
|
|
9981
11016
|
return;
|
|
9982
11017
|
}
|
|
@@ -10669,6 +11704,7 @@ export async function startTelegramUiServer(options = {}) {
|
|
|
10669
11704
|
executor,
|
|
10670
11705
|
mode,
|
|
10671
11706
|
model,
|
|
11707
|
+
voiceAgentId,
|
|
10672
11708
|
} = message;
|
|
10673
11709
|
const normalizedToolName = String(toolName || "").trim();
|
|
10674
11710
|
if (!normalizedToolName) {
|
|
@@ -10690,6 +11726,7 @@ export async function startTelegramUiServer(options = {}) {
|
|
|
10690
11726
|
mode: String(mode || "").trim() || undefined,
|
|
10691
11727
|
model: String(model || "").trim() || undefined,
|
|
10692
11728
|
authSource: String(socket.__authSource || "").trim() || undefined,
|
|
11729
|
+
voiceAgentId: String(voiceAgentId || "").trim() || undefined,
|
|
10693
11730
|
};
|
|
10694
11731
|
const normalizedArgs = relay.normalizeVoiceToolArgs(normalizedToolName, args || {});
|
|
10695
11732
|
relay.getAllowedVoiceTools(context).then((allowed) => {
|
|
@@ -10702,6 +11739,44 @@ export async function startTelegramUiServer(options = {}) {
|
|
|
10702
11739
|
});
|
|
10703
11740
|
return;
|
|
10704
11741
|
}
|
|
11742
|
+
const libraryRoot = resolveVoiceLibraryRoot(context);
|
|
11743
|
+
const { selected: selectedVoiceAgent } = resolveActiveVoiceAgent(
|
|
11744
|
+
libraryRoot,
|
|
11745
|
+
context.voiceAgentId || "",
|
|
11746
|
+
);
|
|
11747
|
+
const activeVoiceAgentId = selectedVoiceAgent?.id || "voice-agent";
|
|
11748
|
+
const voiceToolCfg = getAgentToolConfig(libraryRoot, activeVoiceAgentId);
|
|
11749
|
+
const toolEnabledForAgent =
|
|
11750
|
+
applyVoiceAgentToolFilters([{ name: normalizedToolName }], voiceToolCfg).length > 0;
|
|
11751
|
+
if (!toolEnabledForAgent) {
|
|
11752
|
+
sendWsMessage(socket, {
|
|
11753
|
+
type: "voice-tool-result",
|
|
11754
|
+
callId,
|
|
11755
|
+
error: `Tool "${normalizedToolName}" is not enabled for voice agent "${activeVoiceAgentId}"`,
|
|
11756
|
+
ts: Date.now(),
|
|
11757
|
+
});
|
|
11758
|
+
return;
|
|
11759
|
+
}
|
|
11760
|
+
if (
|
|
11761
|
+
normalizedToolName === "invoke_mcp_tool"
|
|
11762
|
+
&& Array.isArray(voiceToolCfg?.enabledMcpServers)
|
|
11763
|
+
&& voiceToolCfg.enabledMcpServers.length > 0
|
|
11764
|
+
) {
|
|
11765
|
+
const requestedServer = String(
|
|
11766
|
+
normalizedArgs?.server
|
|
11767
|
+
|| normalizedArgs?.serverId
|
|
11768
|
+
|| "",
|
|
11769
|
+
).trim();
|
|
11770
|
+
if (requestedServer && !voiceToolCfg.enabledMcpServers.includes(requestedServer)) {
|
|
11771
|
+
sendWsMessage(socket, {
|
|
11772
|
+
type: "voice-tool-result",
|
|
11773
|
+
callId,
|
|
11774
|
+
error: `MCP server "${requestedServer}" is not enabled for voice agent "${activeVoiceAgentId}"`,
|
|
11775
|
+
ts: Date.now(),
|
|
11776
|
+
});
|
|
11777
|
+
return;
|
|
11778
|
+
}
|
|
11779
|
+
}
|
|
10705
11780
|
// Tool is allowed — execute it
|
|
10706
11781
|
relay.executeVoiceTool(normalizedToolName, normalizedArgs, context).then((result) => {
|
|
10707
11782
|
sendWsMessage(socket, {
|
|
@@ -10744,8 +11819,27 @@ export async function startTelegramUiServer(options = {}) {
|
|
|
10744
11819
|
mode: String(mode || "").trim() || undefined,
|
|
10745
11820
|
model: String(model || "").trim() || undefined,
|
|
10746
11821
|
authSource: String(socket.__authSource || "").trim() || undefined,
|
|
11822
|
+
voiceAgentId: String(voiceAgentId || "").trim() || undefined,
|
|
10747
11823
|
};
|
|
10748
11824
|
const normalizedArgs = relay.normalizeVoiceToolArgs(normalizedToolName, args || {});
|
|
11825
|
+
const libraryRoot = resolveVoiceLibraryRoot(context);
|
|
11826
|
+
const { selected: selectedVoiceAgent } = resolveActiveVoiceAgent(
|
|
11827
|
+
libraryRoot,
|
|
11828
|
+
context.voiceAgentId || "",
|
|
11829
|
+
);
|
|
11830
|
+
const activeVoiceAgentId = selectedVoiceAgent?.id || "voice-agent";
|
|
11831
|
+
const voiceToolCfg = getAgentToolConfig(libraryRoot, activeVoiceAgentId);
|
|
11832
|
+
const toolEnabledForAgent =
|
|
11833
|
+
applyVoiceAgentToolFilters([{ name: normalizedToolName }], voiceToolCfg).length > 0;
|
|
11834
|
+
if (!toolEnabledForAgent) {
|
|
11835
|
+
sendWsMessage(socket, {
|
|
11836
|
+
type: "voice-tool-result",
|
|
11837
|
+
callId,
|
|
11838
|
+
error: `Tool "${normalizedToolName}" is not enabled for voice agent "${activeVoiceAgentId}"`,
|
|
11839
|
+
ts: Date.now(),
|
|
11840
|
+
});
|
|
11841
|
+
return;
|
|
11842
|
+
}
|
|
10749
11843
|
const result = await relay.executeVoiceTool(normalizedToolName, normalizedArgs, context);
|
|
10750
11844
|
sendWsMessage(socket, {
|
|
10751
11845
|
type: "voice-tool-result",
|