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/.env.example
CHANGED
|
@@ -21,7 +21,6 @@ SHARED_STATE_STALE_THRESHOLD_MS=300000
|
|
|
21
21
|
# Maximum retry attempts before permanently ignoring a task (default: 3)
|
|
22
22
|
SHARED_STATE_MAX_RETRIES=3
|
|
23
23
|
# Task claim owner staleness threshold in milliseconds (default: 600000 = 10 minutes)
|
|
24
|
-
# Used by task-claims.mjs to detect stale local claims
|
|
25
24
|
TASK_CLAIM_OWNER_STALE_TTL_MS=600000
|
|
26
25
|
|
|
27
26
|
# ─── Project Identity ─────────────────────────────────────────────────────────
|
|
@@ -178,6 +177,10 @@ VOICE_VISION_MODEL=gpt-4.1-mini
|
|
|
178
177
|
# Gemini provider mode (Tier 2 voice fallback + Gemini vision)
|
|
179
178
|
# GEMINI_API_KEY=
|
|
180
179
|
# GOOGLE_API_KEY=
|
|
180
|
+
# Transcription model used for audio-to-text (default: gpt-4o-transcribe)
|
|
181
|
+
# VOICE_TRANSCRIPTION_MODEL=gpt-4o-mini-transcribe
|
|
182
|
+
# Enable/disable input audio transcription in realtime sessions (default: true)
|
|
183
|
+
# VOICE_TRANSCRIPTION_ENABLED=true
|
|
181
184
|
# Voice output persona
|
|
182
185
|
VOICE_ID=alloy
|
|
183
186
|
# server_vad | semantic_vad | none
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-tool-config.mjs — Per-Agent Tool Configuration Store
|
|
3
|
+
*
|
|
4
|
+
* Manages which tools and MCP servers are enabled for each agent profile.
|
|
5
|
+
* Persisted as `.bosun/agent-tools.json` alongside the library manifest.
|
|
6
|
+
*
|
|
7
|
+
* Schema:
|
|
8
|
+
* {
|
|
9
|
+
* "agents": {
|
|
10
|
+
* "<agentId>": {
|
|
11
|
+
* "enabledTools": ["tool1", "tool2"] | null, // null = all tools
|
|
12
|
+
* "enabledMcpServers": ["github", "context7"], // enabled MCP server IDs
|
|
13
|
+
* "disabledBuiltinTools": ["tool3"], // explicitly disabled builtins
|
|
14
|
+
* "updatedAt": "2026-01-01T00:00:00.000Z"
|
|
15
|
+
* }
|
|
16
|
+
* },
|
|
17
|
+
* "defaults": {
|
|
18
|
+
* "builtinTools": [...], // default tool list for all agents
|
|
19
|
+
* "updatedAt": "..."
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* EXPORTS:
|
|
24
|
+
* DEFAULT_BUILTIN_TOOLS — list of default built-in tools for voice/agents
|
|
25
|
+
* loadToolConfig(rootDir) — load the full config
|
|
26
|
+
* saveToolConfig(rootDir, cfg) — save the full config
|
|
27
|
+
* getAgentToolConfig(rootDir, agentId) — get config for one agent
|
|
28
|
+
* setAgentToolConfig(rootDir, agentId, config) — update config for one agent
|
|
29
|
+
* getEffectiveTools(rootDir, agentId) — compute final enabled tools list
|
|
30
|
+
* listAvailableTools(rootDir) — list all available tools (builtin + MCP)
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
34
|
+
import { resolve } from "node:path";
|
|
35
|
+
import { homedir } from "node:os";
|
|
36
|
+
|
|
37
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
const TAG = "[agent-tool-config]";
|
|
40
|
+
const CONFIG_FILE = "agent-tools.json";
|
|
41
|
+
|
|
42
|
+
function getBosunHome() {
|
|
43
|
+
return (
|
|
44
|
+
process.env.BOSUN_HOME ||
|
|
45
|
+
process.env.BOSUN_DIR ||
|
|
46
|
+
resolve(homedir(), ".bosun")
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Default built-in tools available to all voice agents and executors.
|
|
52
|
+
* Maps to common capabilities that voice/agent sessions can invoke.
|
|
53
|
+
*/
|
|
54
|
+
export const DEFAULT_BUILTIN_TOOLS = Object.freeze([
|
|
55
|
+
{
|
|
56
|
+
id: "search-files",
|
|
57
|
+
name: "Search Files",
|
|
58
|
+
description: "Search for files in the workspace by name or pattern",
|
|
59
|
+
category: "Built-In",
|
|
60
|
+
icon: ":search:",
|
|
61
|
+
default: true,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "read-file",
|
|
65
|
+
name: "Read File",
|
|
66
|
+
description: "Read contents of a file in the workspace",
|
|
67
|
+
category: "Built-In",
|
|
68
|
+
icon: ":file:",
|
|
69
|
+
default: true,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "edit-file",
|
|
73
|
+
name: "Edit File",
|
|
74
|
+
description: "Create or edit files in the workspace",
|
|
75
|
+
category: "Built-In",
|
|
76
|
+
icon: ":edit:",
|
|
77
|
+
default: true,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "run-command",
|
|
81
|
+
name: "Run Terminal Command",
|
|
82
|
+
description: "Execute shell commands in a terminal",
|
|
83
|
+
category: "Built-In",
|
|
84
|
+
icon: ":terminal:",
|
|
85
|
+
default: true,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "web-search",
|
|
89
|
+
name: "Web Search",
|
|
90
|
+
description: "Search the web for information",
|
|
91
|
+
category: "Built-In",
|
|
92
|
+
icon: ":globe:",
|
|
93
|
+
default: true,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "code-search",
|
|
97
|
+
name: "Semantic Code Search",
|
|
98
|
+
description: "Search codebase semantically for relevant code",
|
|
99
|
+
category: "Built-In",
|
|
100
|
+
icon: ":cpu:",
|
|
101
|
+
default: true,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "git-operations",
|
|
105
|
+
name: "Git Operations",
|
|
106
|
+
description: "Run git commands (commit, push, branch, etc.)",
|
|
107
|
+
category: "Built-In",
|
|
108
|
+
icon: ":git:",
|
|
109
|
+
default: true,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "create-task",
|
|
113
|
+
name: "Create Task",
|
|
114
|
+
description: "Create new tasks and issues",
|
|
115
|
+
category: "Built-In",
|
|
116
|
+
icon: ":check:",
|
|
117
|
+
default: true,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "delegate-task",
|
|
121
|
+
name: "Delegate to Agent",
|
|
122
|
+
description: "Delegate work to another agent executor",
|
|
123
|
+
category: "Built-In",
|
|
124
|
+
icon: ":bot:",
|
|
125
|
+
default: true,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "fetch-url",
|
|
129
|
+
name: "Fetch URL",
|
|
130
|
+
description: "Fetch content from a URL and convert for LLM usage",
|
|
131
|
+
category: "Built-In",
|
|
132
|
+
icon: ":link:",
|
|
133
|
+
default: true,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "list-directory",
|
|
137
|
+
name: "List Directory",
|
|
138
|
+
description: "List contents of a directory in the workspace",
|
|
139
|
+
category: "Built-In",
|
|
140
|
+
icon: ":folder:",
|
|
141
|
+
default: true,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: "grep-search",
|
|
145
|
+
name: "Text Search (Grep)",
|
|
146
|
+
description: "Search for exact text or regex patterns in files",
|
|
147
|
+
category: "Built-In",
|
|
148
|
+
icon: ":search:",
|
|
149
|
+
default: true,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "task-management",
|
|
153
|
+
name: "Task Management",
|
|
154
|
+
description: "Track and manage todo items and task status",
|
|
155
|
+
category: "Built-In",
|
|
156
|
+
icon: ":clipboard:",
|
|
157
|
+
default: true,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: "notifications",
|
|
161
|
+
name: "Send Notifications",
|
|
162
|
+
description: "Send notifications via Telegram, webhook, etc.",
|
|
163
|
+
category: "Built-In",
|
|
164
|
+
icon: ":bell:",
|
|
165
|
+
default: false,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "vision-analysis",
|
|
169
|
+
name: "Vision Analysis",
|
|
170
|
+
description: "Analyze images and screenshots",
|
|
171
|
+
category: "Built-In",
|
|
172
|
+
icon: ":eye:",
|
|
173
|
+
default: true,
|
|
174
|
+
},
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
// ── Config File I/O ───────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
function getConfigPath(rootDir) {
|
|
180
|
+
return resolve(rootDir || getBosunHome(), ".bosun", CONFIG_FILE);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Load the agent tool configuration.
|
|
185
|
+
* @param {string} [rootDir]
|
|
186
|
+
* @returns {{ agents: Object, defaults: Object }}
|
|
187
|
+
*/
|
|
188
|
+
export function loadToolConfig(rootDir) {
|
|
189
|
+
const configPath = getConfigPath(rootDir);
|
|
190
|
+
if (!existsSync(configPath)) {
|
|
191
|
+
return {
|
|
192
|
+
agents: {},
|
|
193
|
+
defaults: {
|
|
194
|
+
builtinTools: DEFAULT_BUILTIN_TOOLS.filter((t) => t.default).map((t) => t.id),
|
|
195
|
+
updatedAt: new Date().toISOString(),
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const raw = readFileSync(configPath, "utf8");
|
|
201
|
+
const parsed = JSON.parse(raw);
|
|
202
|
+
return {
|
|
203
|
+
agents: parsed.agents || {},
|
|
204
|
+
defaults: parsed.defaults || {
|
|
205
|
+
builtinTools: DEFAULT_BUILTIN_TOOLS.filter((t) => t.default).map((t) => t.id),
|
|
206
|
+
updatedAt: new Date().toISOString(),
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
} catch {
|
|
210
|
+
return {
|
|
211
|
+
agents: {},
|
|
212
|
+
defaults: {
|
|
213
|
+
builtinTools: DEFAULT_BUILTIN_TOOLS.filter((t) => t.default).map((t) => t.id),
|
|
214
|
+
updatedAt: new Date().toISOString(),
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Save the full tool configuration.
|
|
222
|
+
* @param {string} rootDir
|
|
223
|
+
* @param {{ agents: Object, defaults: Object }} config
|
|
224
|
+
*/
|
|
225
|
+
export function saveToolConfig(rootDir, config) {
|
|
226
|
+
const configPath = getConfigPath(rootDir);
|
|
227
|
+
const dir = resolve(configPath, "..");
|
|
228
|
+
mkdirSync(dir, { recursive: true });
|
|
229
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get tool configuration for a specific agent.
|
|
234
|
+
* @param {string} rootDir
|
|
235
|
+
* @param {string} agentId
|
|
236
|
+
* @returns {{ enabledTools: string[]|null, enabledMcpServers: string[], disabledBuiltinTools: string[] }}
|
|
237
|
+
*/
|
|
238
|
+
export function getAgentToolConfig(rootDir, agentId) {
|
|
239
|
+
const config = loadToolConfig(rootDir);
|
|
240
|
+
const agentConfig = config.agents[agentId];
|
|
241
|
+
if (!agentConfig) {
|
|
242
|
+
return {
|
|
243
|
+
enabledTools: null,
|
|
244
|
+
enabledMcpServers: [],
|
|
245
|
+
disabledBuiltinTools: [],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
enabledTools: agentConfig.enabledTools ?? null,
|
|
250
|
+
enabledMcpServers: agentConfig.enabledMcpServers || [],
|
|
251
|
+
disabledBuiltinTools: agentConfig.disabledBuiltinTools || [],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Update tool configuration for a specific agent.
|
|
257
|
+
* @param {string} rootDir
|
|
258
|
+
* @param {string} agentId
|
|
259
|
+
* @param {{ enabledTools?: string[]|null, enabledMcpServers?: string[], disabledBuiltinTools?: string[] }} update
|
|
260
|
+
* @returns {{ ok: boolean }}
|
|
261
|
+
*/
|
|
262
|
+
export function setAgentToolConfig(rootDir, agentId, update) {
|
|
263
|
+
const config = loadToolConfig(rootDir);
|
|
264
|
+
const existing = config.agents[agentId] || {};
|
|
265
|
+
config.agents[agentId] = {
|
|
266
|
+
...existing,
|
|
267
|
+
enabledTools: update.enabledTools !== undefined ? update.enabledTools : (existing.enabledTools ?? null),
|
|
268
|
+
enabledMcpServers: update.enabledMcpServers !== undefined ? update.enabledMcpServers : (existing.enabledMcpServers || []),
|
|
269
|
+
disabledBuiltinTools: update.disabledBuiltinTools !== undefined ? update.disabledBuiltinTools : (existing.disabledBuiltinTools || []),
|
|
270
|
+
updatedAt: new Date().toISOString(),
|
|
271
|
+
};
|
|
272
|
+
saveToolConfig(rootDir, config);
|
|
273
|
+
return { ok: true };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Compute the effective enabled tools for an agent.
|
|
278
|
+
* Merges builtin defaults with agent-specific overrides and MCP servers.
|
|
279
|
+
*
|
|
280
|
+
* @param {string} rootDir
|
|
281
|
+
* @param {string} agentId
|
|
282
|
+
* @returns {{ builtinTools: Array<{ id: string, name: string, enabled: boolean }>, mcpServers: string[] }}
|
|
283
|
+
*/
|
|
284
|
+
export function getEffectiveTools(rootDir, agentId) {
|
|
285
|
+
const config = loadToolConfig(rootDir);
|
|
286
|
+
const agentConfig = config.agents[agentId] || {};
|
|
287
|
+
const disabledSet = new Set(agentConfig.disabledBuiltinTools || []);
|
|
288
|
+
const defaultIds = new Set(config.defaults?.builtinTools || DEFAULT_BUILTIN_TOOLS.filter((t) => t.default).map((t) => t.id));
|
|
289
|
+
const builtinIdSet = new Set(DEFAULT_BUILTIN_TOOLS.map((tool) => tool.id));
|
|
290
|
+
const explicitEnabled = Array.isArray(agentConfig.enabledTools)
|
|
291
|
+
? agentConfig.enabledTools.map((id) => String(id || "").trim()).filter(Boolean)
|
|
292
|
+
: null;
|
|
293
|
+
const explicitBuiltinEnabled = explicitEnabled
|
|
294
|
+
? explicitEnabled.filter((id) => builtinIdSet.has(id))
|
|
295
|
+
: [];
|
|
296
|
+
const useBuiltinAllowlist = explicitBuiltinEnabled.length > 0;
|
|
297
|
+
const explicitBuiltinSet = new Set(explicitBuiltinEnabled);
|
|
298
|
+
|
|
299
|
+
const builtinTools = DEFAULT_BUILTIN_TOOLS.map((tool) => ({
|
|
300
|
+
...tool,
|
|
301
|
+
enabled: !disabledSet.has(tool.id) && (
|
|
302
|
+
useBuiltinAllowlist
|
|
303
|
+
? explicitBuiltinSet.has(tool.id)
|
|
304
|
+
: defaultIds.has(tool.id)
|
|
305
|
+
),
|
|
306
|
+
}));
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
builtinTools,
|
|
310
|
+
mcpServers: agentConfig.enabledMcpServers || [],
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* List all available tools (builtin + installed MCP servers).
|
|
316
|
+
* @param {string} rootDir
|
|
317
|
+
* @returns {{ builtinTools: Array<Object>, mcpServers: Array<Object> }}
|
|
318
|
+
*/
|
|
319
|
+
export async function listAvailableTools(rootDir) {
|
|
320
|
+
let mcpServers = [];
|
|
321
|
+
try {
|
|
322
|
+
const { listInstalledMcpServers } = await import("./mcp-registry.mjs");
|
|
323
|
+
mcpServers = await listInstalledMcpServers(rootDir);
|
|
324
|
+
} catch {
|
|
325
|
+
// MCP registry not available
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
builtinTools: [...DEFAULT_BUILTIN_TOOLS],
|
|
330
|
+
mcpServers: mcpServers.map((s) => ({
|
|
331
|
+
id: s.id,
|
|
332
|
+
name: s.name,
|
|
333
|
+
description: s.description || "",
|
|
334
|
+
tags: s.tags || [],
|
|
335
|
+
transport: s.meta?.transport || "stdio",
|
|
336
|
+
})),
|
|
337
|
+
};
|
|
338
|
+
}
|
package/bosun-skills.mjs
CHANGED
|
@@ -16,8 +16,44 @@
|
|
|
16
16
|
* scan quickly to decide which skill files to read.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import {
|
|
20
|
-
import { resolve, basename } from "node:path";
|
|
19
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { dirname, resolve, basename } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = dirname(__filename);
|
|
25
|
+
|
|
26
|
+
// ── Analytics stream path (same file task-executor writes to) ────────────────
|
|
27
|
+
const _SKILL_STREAM_PATH = resolve(
|
|
28
|
+
__dirname,
|
|
29
|
+
".cache",
|
|
30
|
+
"agent-work-logs",
|
|
31
|
+
"agent-work-stream.jsonl",
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Best-effort: emit a skill_invoke event to the agent work stream so usage
|
|
36
|
+
* analytics can track which skills are loaded per task.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} skillName
|
|
39
|
+
* @param {string} [skillTitle]
|
|
40
|
+
* @param {{ taskId?: string, executor?: string }} [opts]
|
|
41
|
+
*/
|
|
42
|
+
function emitSkillInvokeEvent(skillName, skillTitle, opts = {}) {
|
|
43
|
+
try {
|
|
44
|
+
const event = {
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
event_type: "skill_invoke",
|
|
47
|
+
data: { skill_name: skillName, skill_title: skillTitle || skillName },
|
|
48
|
+
...(opts.taskId ? { task_id: String(opts.taskId) } : {}),
|
|
49
|
+
...(opts.executor ? { executor: String(opts.executor) } : {}),
|
|
50
|
+
};
|
|
51
|
+
mkdirSync(dirname(_SKILL_STREAM_PATH), { recursive: true });
|
|
52
|
+
appendFileSync(_SKILL_STREAM_PATH, JSON.stringify(event) + "\n", "utf8");
|
|
53
|
+
} catch {
|
|
54
|
+
/* best effort — never let analytics crash skill loading */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
21
57
|
|
|
22
58
|
// ── Built-in skill definitions ────────────────────────────────────────────────
|
|
23
59
|
|
|
@@ -911,14 +947,25 @@ export function loadSkillsIndex(bosunHome) {
|
|
|
911
947
|
* @param {string} [taskDescription]
|
|
912
948
|
* @returns {Array<{filename:string,title:string,tags:string[],content:string}>}
|
|
913
949
|
*/
|
|
914
|
-
|
|
950
|
+
/**
|
|
951
|
+
* Find skills relevant to a given task by matching tags against the task title
|
|
952
|
+
* and description. Also emits `skill_invoke` analytics events for each matched
|
|
953
|
+
* skill so usage analytics can track skill popularity over time.
|
|
954
|
+
*
|
|
955
|
+
* @param {string} bosunHome
|
|
956
|
+
* @param {string} taskTitle
|
|
957
|
+
* @param {string} [taskDescription]
|
|
958
|
+
* @param {{ taskId?: string, executor?: string }} [opts] - Optional task context for analytics.
|
|
959
|
+
* @returns {Array<{filename:string,title:string,tags:string[],content:string}>}
|
|
960
|
+
*/
|
|
961
|
+
export function findRelevantSkills(bosunHome, taskTitle, taskDescription = "", opts = {}) {
|
|
915
962
|
const index = loadSkillsIndex(bosunHome);
|
|
916
963
|
if (!index?.skills?.length) return [];
|
|
917
964
|
|
|
918
965
|
const searchText = `${taskTitle} ${taskDescription}`.toLowerCase();
|
|
919
966
|
const skillsDir = getSkillsDir(bosunHome);
|
|
920
967
|
|
|
921
|
-
|
|
968
|
+
const matched = index.skills
|
|
922
969
|
.filter(({ tags }) =>
|
|
923
970
|
tags.some((tag) => searchText.includes(tag)),
|
|
924
971
|
)
|
|
@@ -930,4 +977,12 @@ export function findRelevantSkills(bosunHome, taskTitle, taskDescription = "") {
|
|
|
930
977
|
return { filename, title, tags, content };
|
|
931
978
|
})
|
|
932
979
|
.filter(({ content }) => !!content);
|
|
980
|
+
|
|
981
|
+
// Emit analytics events for each loaded skill
|
|
982
|
+
for (const skill of matched) {
|
|
983
|
+
const skillName = skill.filename.replace(/\.md$/i, "");
|
|
984
|
+
emitSkillInvokeEvent(skillName, skill.title, opts);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return matched;
|
|
933
988
|
}
|
package/bosun.schema.json
CHANGED
|
@@ -281,7 +281,7 @@
|
|
|
281
281
|
"turnDetection": {
|
|
282
282
|
"type": "string",
|
|
283
283
|
"enum": ["server_vad", "semantic_vad", "none"],
|
|
284
|
-
"default": "
|
|
284
|
+
"default": "semantic_vad",
|
|
285
285
|
"description": "Turn detection mode for voice activity detection"
|
|
286
286
|
},
|
|
287
287
|
"instructions": {
|
package/desktop/launch.mjs
CHANGED
|
@@ -17,6 +17,13 @@ const chromeSandbox = resolve(
|
|
|
17
17
|
|
|
18
18
|
process.title = "bosun-desktop-launcher";
|
|
19
19
|
|
|
20
|
+
function hasGuiEnvironment() {
|
|
21
|
+
if (process.platform !== "linux") return true;
|
|
22
|
+
if (process.env.DISPLAY || process.env.WAYLAND_DISPLAY) return true;
|
|
23
|
+
if (process.env.XDG_SESSION_TYPE && process.env.XDG_SESSION_TYPE !== "tty") return true;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
function shouldDisableSandbox() {
|
|
21
28
|
if (process.env.BOSUN_DESKTOP_DISABLE_SANDBOX === "1") return true;
|
|
22
29
|
if (process.platform !== "linux") return false;
|
|
@@ -49,6 +56,17 @@ function ensureElectronInstalled() {
|
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
function launch() {
|
|
59
|
+
if (!hasGuiEnvironment()) {
|
|
60
|
+
console.error(
|
|
61
|
+
[
|
|
62
|
+
"[desktop] No GUI display server detected.",
|
|
63
|
+
"Cannot launch Electron portal without DISPLAY/WAYLAND.",
|
|
64
|
+
"Run Bosun in daemon/web mode instead (for example: `bosun --daemon`).",
|
|
65
|
+
].join(" "),
|
|
66
|
+
);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
if (!ensureElectronInstalled()) {
|
|
53
71
|
process.exit(1);
|
|
54
72
|
}
|
package/desktop/main.mjs
CHANGED
|
@@ -89,6 +89,7 @@ let shuttingDown = false;
|
|
|
89
89
|
let uiServerStarted = false;
|
|
90
90
|
let uiOrigin = null;
|
|
91
91
|
let uiApi = null;
|
|
92
|
+
let desktopAuthHeaderBridgeInstalled = false;
|
|
92
93
|
let runtimeConfigLoaded = false;
|
|
93
94
|
/** True when the app is running as a persistent background / tray resident. */
|
|
94
95
|
let trayMode = false;
|
|
@@ -154,6 +155,38 @@ function isTrustedCaptureOrigin(originLike) {
|
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
function isTrustedDesktopRequestUrl(urlLike) {
|
|
159
|
+
return isTrustedCaptureOrigin(urlLike);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function installDesktopAuthHeaderBridge() {
|
|
163
|
+
if (desktopAuthHeaderBridgeInstalled) return;
|
|
164
|
+
const ses = session.defaultSession;
|
|
165
|
+
if (!ses) return;
|
|
166
|
+
ses.webRequest.onBeforeSendHeaders((details, callback) => {
|
|
167
|
+
try {
|
|
168
|
+
const desktopKey = String(process.env.BOSUN_DESKTOP_API_KEY || "").trim();
|
|
169
|
+
if (!desktopKey) {
|
|
170
|
+
callback({ requestHeaders: details.requestHeaders });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!isTrustedDesktopRequestUrl(details?.url || "")) {
|
|
174
|
+
callback({ requestHeaders: details.requestHeaders });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const headers = { ...(details.requestHeaders || {}) };
|
|
178
|
+
const existingAuth = String(headers.Authorization || headers.authorization || "").trim();
|
|
179
|
+
if (!existingAuth) {
|
|
180
|
+
headers.Authorization = `Bearer ${desktopKey}`;
|
|
181
|
+
}
|
|
182
|
+
callback({ requestHeaders: headers });
|
|
183
|
+
} catch {
|
|
184
|
+
callback({ requestHeaders: details.requestHeaders });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
desktopAuthHeaderBridgeInstalled = true;
|
|
188
|
+
}
|
|
189
|
+
|
|
157
190
|
function installDesktopMediaHandlers() {
|
|
158
191
|
const ses = session.defaultSession;
|
|
159
192
|
if (!ses) return;
|
|
@@ -583,20 +616,25 @@ async function fetchWorkspaces({ force = false } = {}) {
|
|
|
583
616
|
* @param {string} workspaceId
|
|
584
617
|
*/
|
|
585
618
|
async function switchWorkspace(workspaceId) {
|
|
586
|
-
if (!uiOrigin || !workspaceId) return;
|
|
619
|
+
if (!uiOrigin || !workspaceId) return { ok: false, error: "workspace unavailable" };
|
|
587
620
|
const body = JSON.stringify({ workspaceId });
|
|
588
621
|
const data = await uiServerRequest(`${uiOrigin}/api/workspaces/active`, {
|
|
589
622
|
method: "POST",
|
|
590
623
|
body,
|
|
591
624
|
});
|
|
592
|
-
if (data?.ok) {
|
|
593
|
-
|
|
594
|
-
|
|
625
|
+
if (!data?.ok) {
|
|
626
|
+
return {
|
|
627
|
+
ok: false,
|
|
628
|
+
error: String(data?.error || "workspace switch failed"),
|
|
629
|
+
};
|
|
595
630
|
}
|
|
631
|
+
_cachedActiveWorkspaceId = String(data?.activeId || workspaceId);
|
|
632
|
+
_workspaceCacheAt = 0; // force re-fetch next time
|
|
596
633
|
await fetchWorkspaces({ force: true });
|
|
597
634
|
Menu.setApplicationMenu(buildAppMenu());
|
|
598
635
|
refreshTrayMenu();
|
|
599
636
|
navigateMainWindow("/");
|
|
637
|
+
return { ok: true, activeId: _cachedActiveWorkspaceId };
|
|
600
638
|
}
|
|
601
639
|
|
|
602
640
|
/**
|
|
@@ -1265,14 +1303,15 @@ async function openFollowWindow(detail = {}) {
|
|
|
1265
1303
|
const win = await createFollowWindow();
|
|
1266
1304
|
const baseUiUrl = await buildUiUrl();
|
|
1267
1305
|
const target = buildFollowWindowUrl(baseUiUrl, detail);
|
|
1306
|
+
// Append a cache-buster timestamp so every Call press produces a unique URL.
|
|
1307
|
+
// Without this, a second Call press with the same parameters would match
|
|
1308
|
+
// followWindowLaunchSignature and skip loadURL — leaving the follow window
|
|
1309
|
+
// in its previous dead state (launch params already scrubbed, voice overlay
|
|
1310
|
+
// closed, useEffect([], []) already fired and won't re-run).
|
|
1311
|
+
target.searchParams.set("t", String(Date.now()));
|
|
1268
1312
|
const signature = target.toString();
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
await win.loadURL(signature);
|
|
1272
|
-
anchorFollowWindow(win);
|
|
1273
|
-
setWindowVisible(win);
|
|
1274
|
-
return;
|
|
1275
|
-
}
|
|
1313
|
+
followWindowLaunchSignature = signature;
|
|
1314
|
+
await win.loadURL(signature);
|
|
1276
1315
|
anchorFollowWindow(win);
|
|
1277
1316
|
setWindowVisible(win);
|
|
1278
1317
|
}
|
|
@@ -1727,8 +1766,7 @@ function registerDesktopIpc() {
|
|
|
1727
1766
|
*/
|
|
1728
1767
|
ipcMain.handle("bosun:workspaces:switch", async (_event, { workspaceId } = {}) => {
|
|
1729
1768
|
if (!workspaceId) return { ok: false, error: "workspaceId required" };
|
|
1730
|
-
|
|
1731
|
-
return { ok: true, activeId: workspaceId };
|
|
1769
|
+
return switchWorkspace(workspaceId);
|
|
1732
1770
|
});
|
|
1733
1771
|
}
|
|
1734
1772
|
|
|
@@ -1745,6 +1783,7 @@ async function bootstrap() {
|
|
|
1745
1783
|
}
|
|
1746
1784
|
callback(-3); // -3 = use Chromium default chain verification
|
|
1747
1785
|
});
|
|
1786
|
+
installDesktopAuthHeaderBridge();
|
|
1748
1787
|
installDesktopMediaHandlers();
|
|
1749
1788
|
|
|
1750
1789
|
if (process.env.ELECTRON_DISABLE_SANDBOX === "1") {
|
package/fleet-coordinator.mjs
CHANGED
|
@@ -36,6 +36,24 @@ import {
|
|
|
36
36
|
|
|
37
37
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
38
38
|
|
|
39
|
+
// ── Workflow Event Bridge ────────────────────────────────────────────────────
|
|
40
|
+
// Lazy-import queueWorkflowEvent from monitor.mjs — cached at module scope.
|
|
41
|
+
let _queueWorkflowEvent = null;
|
|
42
|
+
function emitFleetEvent(eventType, eventData = {}, opts = {}) {
|
|
43
|
+
if (!_queueWorkflowEvent) {
|
|
44
|
+
import("./monitor.mjs")
|
|
45
|
+
.then((mod) => {
|
|
46
|
+
if (typeof mod.queueWorkflowEvent === "function") {
|
|
47
|
+
_queueWorkflowEvent = mod.queueWorkflowEvent;
|
|
48
|
+
_queueWorkflowEvent(eventType, eventData, opts);
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
.catch(() => {});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
_queueWorkflowEvent(eventType, eventData, opts);
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
// ── Repo Fingerprinting ──────────────────────────────────────────────────────
|
|
40
58
|
|
|
41
59
|
function buildGitEnv() {
|
|
@@ -434,13 +452,21 @@ export function assignTasksToWorkstations(waves, peers, taskMap = new Map()) {
|
|
|
434
452
|
assignments.push(...waveAssignments);
|
|
435
453
|
}
|
|
436
454
|
|
|
437
|
-
|
|
455
|
+
const result = {
|
|
438
456
|
assignments,
|
|
439
457
|
totalTasks: assignments.length,
|
|
440
458
|
totalPeers: peers.length,
|
|
441
459
|
waveCount: waves.length,
|
|
442
460
|
createdAt: new Date().toISOString(),
|
|
443
461
|
};
|
|
462
|
+
|
|
463
|
+
emitFleetEvent("fleet.tasks_assigned", {
|
|
464
|
+
totalTasks: result.totalTasks,
|
|
465
|
+
totalPeers: result.totalPeers,
|
|
466
|
+
waveCount: result.waveCount,
|
|
467
|
+
}, { dedupKey: `fleet-assign-${result.createdAt}` });
|
|
468
|
+
|
|
469
|
+
return result;
|
|
444
470
|
}
|
|
445
471
|
|
|
446
472
|
// ── Backlog Depth Calculator ─────────────────────────────────────────────────
|
|
@@ -502,6 +528,10 @@ export function detectMaintenanceMode(status) {
|
|
|
502
528
|
|
|
503
529
|
// Maintenance mode: nothing to do AND nothing in progress
|
|
504
530
|
if (backlog === 0 && todo === 0 && running === 0 && review === 0) {
|
|
531
|
+
emitFleetEvent("fleet.maintenance_mode", {
|
|
532
|
+
isMaintenanceMode: true,
|
|
533
|
+
reason: "all tasks completed — no backlog, no active work",
|
|
534
|
+
});
|
|
505
535
|
return {
|
|
506
536
|
isMaintenanceMode: true,
|
|
507
537
|
reason: "all tasks completed — no backlog, no active work",
|
|
@@ -722,6 +752,9 @@ export function shouldAutoGenerateTasks({
|
|
|
722
752
|
*/
|
|
723
753
|
export function markAutoGenTriggered() {
|
|
724
754
|
lastAutoGenTimestamp = Date.now();
|
|
755
|
+
emitFleetEvent("fleet.auto_gen_triggered", {
|
|
756
|
+
triggeredAt: new Date(lastAutoGenTimestamp).toISOString(),
|
|
757
|
+
});
|
|
725
758
|
}
|
|
726
759
|
|
|
727
760
|
/**
|