bosun 0.36.0 → 0.36.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 +98 -16
- package/README.md +27 -0
- package/agent-event-bus.mjs +5 -5
- package/agent-pool.mjs +129 -12
- package/agent-prompts.mjs +7 -1
- package/agent-sdk.mjs +13 -2
- package/agent-supervisor.mjs +2 -2
- package/agent-work-report.mjs +1 -1
- package/anomaly-detector.mjs +6 -6
- package/autofix.mjs +15 -15
- package/bosun-skills.mjs +4 -4
- package/bosun.schema.json +160 -4
- package/claude-shell.mjs +11 -11
- package/cli.mjs +21 -21
- package/codex-config.mjs +19 -19
- package/codex-shell.mjs +180 -29
- package/config-doctor.mjs +27 -2
- package/config.mjs +60 -7
- package/copilot-shell.mjs +4 -4
- package/error-detector.mjs +1 -1
- package/fleet-coordinator.mjs +2 -2
- package/gemini-shell.mjs +692 -0
- package/github-oauth-portal.mjs +1 -1
- package/github-reconciler.mjs +2 -2
- package/kanban-adapter.mjs +741 -168
- package/merge-strategy.mjs +25 -25
- package/monitor.mjs +123 -105
- package/opencode-shell.mjs +22 -22
- package/package.json +7 -1
- package/postinstall.mjs +22 -22
- package/pr-cleanup-daemon.mjs +6 -6
- package/prepublish-check.mjs +4 -4
- package/presence.mjs +2 -2
- package/primary-agent.mjs +85 -7
- package/publish.mjs +1 -1
- package/review-agent.mjs +1 -1
- package/session-tracker.mjs +11 -0
- package/setup-web-server.mjs +429 -21
- package/setup.mjs +367 -12
- package/shared-knowledge.mjs +1 -1
- package/startup-service.mjs +9 -9
- package/stream-resilience.mjs +58 -4
- package/sync-engine.mjs +2 -2
- package/task-assessment.mjs +9 -9
- package/task-cli.mjs +1 -1
- package/task-complexity.mjs +71 -2
- package/task-context.mjs +1 -2
- package/task-executor.mjs +104 -41
- package/telegram-bot.mjs +825 -494
- package/telegram-sentinel.mjs +28 -28
- package/ui/app.js +256 -23
- package/ui/app.monolith.js +1 -1
- package/ui/components/agent-selector.js +4 -3
- package/ui/components/chat-view.js +101 -28
- package/ui/components/diff-viewer.js +3 -3
- package/ui/components/kanban-board.js +3 -3
- package/ui/components/session-list.js +255 -35
- package/ui/components/workspace-switcher.js +3 -3
- package/ui/demo.html +209 -194
- package/ui/index.html +3 -3
- package/ui/modules/icon-utils.js +206 -142
- package/ui/modules/icons.js +2 -27
- package/ui/modules/settings-schema.js +29 -5
- package/ui/modules/streaming.js +30 -2
- package/ui/modules/vision-stream.js +275 -0
- package/ui/modules/voice-client.js +102 -9
- package/ui/modules/voice-fallback.js +62 -6
- package/ui/modules/voice-overlay.js +594 -59
- package/ui/modules/voice.js +31 -38
- package/ui/setup.html +284 -34
- package/ui/styles/components.css +47 -0
- package/ui/styles/sessions.css +75 -0
- package/ui/tabs/agents.js +73 -43
- package/ui/tabs/chat.js +37 -40
- package/ui/tabs/control.js +2 -2
- package/ui/tabs/dashboard.js +1 -1
- package/ui/tabs/infra.js +10 -10
- package/ui/tabs/library.js +8 -8
- package/ui/tabs/logs.js +10 -10
- package/ui/tabs/settings.js +20 -20
- package/ui/tabs/tasks.js +76 -47
- package/ui-server.mjs +1761 -124
- package/update-check.mjs +13 -13
- package/ve-kanban.mjs +1 -1
- package/whatsapp-channel.mjs +5 -5
- package/workflow-engine.mjs +20 -1
- package/workflow-nodes.mjs +904 -4
- package/workflow-templates/agents.mjs +321 -7
- package/workflow-templates/ci-cd.mjs +6 -6
- package/workflow-templates/github.mjs +156 -84
- package/workflow-templates/planning.mjs +8 -8
- package/workflow-templates/reliability.mjs +8 -8
- package/workflow-templates/security.mjs +3 -3
- package/workflow-templates.mjs +15 -9
- package/workspace-manager.mjs +85 -1
- package/workspace-monitor.mjs +2 -2
- package/workspace-registry.mjs +2 -2
- package/worktree-manager.mjs +1 -1
package/setup-web-server.mjs
CHANGED
|
@@ -36,8 +36,8 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
36
36
|
// 2. Node module resolution via createRequire — handles global install hoisting
|
|
37
37
|
// 3. CDN redirect — last resort
|
|
38
38
|
const _require = createRequire(import.meta.url);
|
|
39
|
-
const uiRootPreferred = resolve(__dirname, "
|
|
40
|
-
const uiRootFallback = resolve(__dirname, "ui");
|
|
39
|
+
const uiRootPreferred = resolve(__dirname, "ui");
|
|
40
|
+
const uiRootFallback = resolve(__dirname, "site", "ui");
|
|
41
41
|
const uiRoot = existsSync(uiRootPreferred) ? uiRootPreferred : uiRootFallback;
|
|
42
42
|
const BUNDLED_VENDOR_DIR = resolve(uiRoot, "vendor");
|
|
43
43
|
|
|
@@ -134,12 +134,27 @@ const MODELS = {
|
|
|
134
134
|
{ value: "claude-sonnet-4", label: "claude-sonnet-4" },
|
|
135
135
|
{ value: "claude-haiku-4.5", label: "claude-haiku-4.5" },
|
|
136
136
|
],
|
|
137
|
+
gemini: [
|
|
138
|
+
{ value: "gemini-2.5-pro", label: "gemini-2.5-pro", recommended: true },
|
|
139
|
+
{ value: "gemini-2.5-flash", label: "gemini-2.5-flash" },
|
|
140
|
+
{ value: "gemini-2.0-flash", label: "gemini-2.0-flash" },
|
|
141
|
+
{ value: "gemini-1.5-pro", label: "gemini-1.5-pro" },
|
|
142
|
+
{ value: "gemini-1.5-flash", label: "gemini-1.5-flash" },
|
|
143
|
+
],
|
|
144
|
+
opencode: [
|
|
145
|
+
{ value: "gpt-5.3-codex", label: "gpt-5.3-codex", recommended: true },
|
|
146
|
+
{ value: "gpt-5.2-codex", label: "gpt-5.2-codex" },
|
|
147
|
+
{ value: "claude-opus-4.6", label: "claude-opus-4.6" },
|
|
148
|
+
{ value: "gemini-2.5-pro", label: "gemini-2.5-pro" },
|
|
149
|
+
],
|
|
137
150
|
};
|
|
138
151
|
|
|
139
152
|
const EXECUTOR_TYPES = [
|
|
140
153
|
{ value: "COPILOT", label: "GitHub Copilot (recommended)", recommended: true },
|
|
141
154
|
{ value: "CODEX", label: "OpenAI Codex CLI" },
|
|
142
155
|
{ value: "CLAUDE_CODE", label: "Claude Code" },
|
|
156
|
+
{ value: "GEMINI", label: "Google Gemini" },
|
|
157
|
+
{ value: "OPENCODE", label: "OpenCode (local server)" },
|
|
143
158
|
];
|
|
144
159
|
|
|
145
160
|
const KANBAN_BACKENDS = [
|
|
@@ -178,6 +193,195 @@ function normalizeWorkflowTemplateIds(rawValue, fallback = []) {
|
|
|
178
193
|
return Array.isArray(fallback) ? [...fallback] : [];
|
|
179
194
|
}
|
|
180
195
|
|
|
196
|
+
function normalizeWorkspaceId(value, fallback = "workspace") {
|
|
197
|
+
const normalized = String(value || "")
|
|
198
|
+
.trim()
|
|
199
|
+
.toLowerCase()
|
|
200
|
+
.replace(/[^a-z0-9_-]/g, "-")
|
|
201
|
+
.replace(/-+/g, "-")
|
|
202
|
+
.replace(/^-|-$/g, "");
|
|
203
|
+
return normalized || fallback;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function extractRepoNameFromText(text) {
|
|
207
|
+
const raw = String(text || "").trim();
|
|
208
|
+
if (!raw) return "";
|
|
209
|
+
if (/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/i.test(raw)) {
|
|
210
|
+
return raw.split("/").pop() || "";
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const parsed = new URL(raw);
|
|
214
|
+
const pathname = String(parsed.pathname || "")
|
|
215
|
+
.replace(/\.git$/i, "")
|
|
216
|
+
.replace(/^\/+|\/+$/g, "");
|
|
217
|
+
return pathname.split("/").pop() || "";
|
|
218
|
+
} catch {
|
|
219
|
+
const cleaned = raw
|
|
220
|
+
.replace(/\\/g, "/")
|
|
221
|
+
.replace(/\.git$/i, "")
|
|
222
|
+
.replace(/^\/+|\/+$/g, "");
|
|
223
|
+
return cleaned.split("/").pop() || "";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normalizeRepoSlug(text) {
|
|
228
|
+
const raw = String(text || "").trim();
|
|
229
|
+
if (!raw) return "";
|
|
230
|
+
if (/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/i.test(raw)) return raw;
|
|
231
|
+
const fromUrl = raw.match(/github\.com[:/]([a-z0-9_.-]+\/[a-z0-9_.-]+)(?:\.git)?/i);
|
|
232
|
+
if (fromUrl?.[1]) return fromUrl[1];
|
|
233
|
+
return "";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function normalizeRepoConfigEntry(repo, index = 0) {
|
|
237
|
+
const raw = typeof repo === "string" ? { slug: repo } : (repo && typeof repo === "object" ? repo : null);
|
|
238
|
+
if (!raw) return null;
|
|
239
|
+
const slug = normalizeRepoSlug(raw.slug || raw.url || raw.path || raw.name || "");
|
|
240
|
+
const url =
|
|
241
|
+
String(raw.url || "").trim() ||
|
|
242
|
+
(slug ? `https://github.com/${slug}.git` : "");
|
|
243
|
+
const name = String(raw.name || raw.id || "").trim() || extractRepoNameFromText(slug || url || raw.path || "");
|
|
244
|
+
if (!name && !slug && !url) return null;
|
|
245
|
+
return {
|
|
246
|
+
name: name || extractRepoNameFromText(url || slug) || `repo-${index + 1}`,
|
|
247
|
+
slug,
|
|
248
|
+
url,
|
|
249
|
+
primary: Boolean(raw.primary) || index === 0,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function normalizeWorkspaceConfigEntry(workspace, index = 0) {
|
|
254
|
+
if (!workspace || typeof workspace !== "object") return null;
|
|
255
|
+
const fallbackId = `workspace-${index + 1}`;
|
|
256
|
+
const id = normalizeWorkspaceId(
|
|
257
|
+
workspace.id || workspace.name || fallbackId,
|
|
258
|
+
fallbackId,
|
|
259
|
+
);
|
|
260
|
+
const name = String(workspace.name || workspace.id || id).trim() || id;
|
|
261
|
+
const repos = Array.isArray(workspace.repos)
|
|
262
|
+
? workspace.repos
|
|
263
|
+
.map((repo, repoIndex) => normalizeRepoConfigEntry(repo, repoIndex))
|
|
264
|
+
.filter(Boolean)
|
|
265
|
+
: [];
|
|
266
|
+
let activeRepo = String(workspace.activeRepo || "").trim();
|
|
267
|
+
if (!activeRepo && repos[0]?.name) activeRepo = repos[0].name;
|
|
268
|
+
if (activeRepo && !repos.some((repo) => repo.name === activeRepo)) {
|
|
269
|
+
activeRepo = repos[0]?.name || "";
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
id,
|
|
273
|
+
name,
|
|
274
|
+
repos,
|
|
275
|
+
createdAt: workspace.createdAt || new Date().toISOString(),
|
|
276
|
+
activeRepo: activeRepo || null,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function normalizeWorkspaceConfigList(workspaces) {
|
|
281
|
+
if (!Array.isArray(workspaces)) return [];
|
|
282
|
+
const byId = new Map();
|
|
283
|
+
for (let i = 0; i < workspaces.length; i += 1) {
|
|
284
|
+
const normalized = normalizeWorkspaceConfigEntry(workspaces[i], i);
|
|
285
|
+
if (!normalized) continue;
|
|
286
|
+
const existing = byId.get(normalized.id);
|
|
287
|
+
if (!existing) {
|
|
288
|
+
byId.set(normalized.id, normalized);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
// Merge with precedence to latest entry while preserving earlier repo order.
|
|
292
|
+
const repoMap = new Map();
|
|
293
|
+
for (const repo of existing.repos || []) repoMap.set(repo.name, repo);
|
|
294
|
+
for (const repo of normalized.repos || []) repoMap.set(repo.name, repo);
|
|
295
|
+
byId.set(normalized.id, {
|
|
296
|
+
...existing,
|
|
297
|
+
...normalized,
|
|
298
|
+
repos: [...repoMap.values()],
|
|
299
|
+
activeRepo:
|
|
300
|
+
normalized.activeRepo ||
|
|
301
|
+
existing.activeRepo ||
|
|
302
|
+
[...repoMap.values()][0]?.name ||
|
|
303
|
+
null,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return [...byId.values()];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function readExistingBosunConfig(bosunHome) {
|
|
310
|
+
const configPath = resolve(bosunHome, "bosun.config.json");
|
|
311
|
+
if (!existsSync(configPath)) return {};
|
|
312
|
+
try {
|
|
313
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
314
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
315
|
+
} catch {
|
|
316
|
+
return {};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function resolveSetupWorkspaceAndRepoConfig(existingConfig = {}, configJson = {}, env = {}) {
|
|
321
|
+
const normalizedIncomingRepos = Array.isArray(configJson.repos)
|
|
322
|
+
? configJson.repos
|
|
323
|
+
.map((repo, idx) => normalizeRepoConfigEntry(repo, idx))
|
|
324
|
+
.filter(Boolean)
|
|
325
|
+
: [];
|
|
326
|
+
const normalizedExistingRepos = Array.isArray(existingConfig.repos)
|
|
327
|
+
? existingConfig.repos
|
|
328
|
+
.map((repo, idx) => normalizeRepoConfigEntry(repo, idx))
|
|
329
|
+
.filter(Boolean)
|
|
330
|
+
: [];
|
|
331
|
+
const repos =
|
|
332
|
+
normalizedIncomingRepos.length > 0
|
|
333
|
+
? normalizedIncomingRepos
|
|
334
|
+
: normalizedExistingRepos;
|
|
335
|
+
|
|
336
|
+
const normalizedIncomingWorkspaces = normalizeWorkspaceConfigList(configJson.workspaces);
|
|
337
|
+
const normalizedExistingWorkspaces = normalizeWorkspaceConfigList(existingConfig.workspaces);
|
|
338
|
+
let workspaces = normalizedIncomingWorkspaces.length > 0
|
|
339
|
+
? normalizedIncomingWorkspaces
|
|
340
|
+
: normalizedExistingWorkspaces;
|
|
341
|
+
|
|
342
|
+
// Bootstrap a single default workspace when repos exist but no workspace
|
|
343
|
+
// layout was provided, so the UI and workspace APIs stay consistent.
|
|
344
|
+
if (workspaces.length === 0 && repos.length > 0) {
|
|
345
|
+
const defaultWorkspaceId = normalizeWorkspaceId(
|
|
346
|
+
configJson.projectName || env.projectName || existingConfig.projectName || "default",
|
|
347
|
+
"default",
|
|
348
|
+
);
|
|
349
|
+
workspaces = [
|
|
350
|
+
{
|
|
351
|
+
id: defaultWorkspaceId,
|
|
352
|
+
name:
|
|
353
|
+
configJson.projectName ||
|
|
354
|
+
env.projectName ||
|
|
355
|
+
existingConfig.projectName ||
|
|
356
|
+
"Default Workspace",
|
|
357
|
+
repos: repos.map((repo, idx) => ({
|
|
358
|
+
...repo,
|
|
359
|
+
primary: repo.primary === true || idx === 0,
|
|
360
|
+
})),
|
|
361
|
+
createdAt: new Date().toISOString(),
|
|
362
|
+
activeRepo: repos[0]?.name || null,
|
|
363
|
+
},
|
|
364
|
+
];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let activeWorkspace = "";
|
|
368
|
+
if (workspaces.length > 0) {
|
|
369
|
+
const requestedActiveWorkspace = normalizeWorkspaceId(
|
|
370
|
+
configJson.activeWorkspace || existingConfig.activeWorkspace || workspaces[0]?.id,
|
|
371
|
+
workspaces[0]?.id || "default",
|
|
372
|
+
);
|
|
373
|
+
activeWorkspace = workspaces.some((ws) => ws.id === requestedActiveWorkspace)
|
|
374
|
+
? requestedActiveWorkspace
|
|
375
|
+
: workspaces[0].id;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
repos,
|
|
380
|
+
workspaces,
|
|
381
|
+
activeWorkspace,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
181
385
|
function buildStableSetupDefaults({
|
|
182
386
|
projectName,
|
|
183
387
|
slug,
|
|
@@ -230,6 +434,18 @@ function buildStableSetupDefaults({
|
|
|
230
434
|
workflowRunStuckThresholdMs: 300000,
|
|
231
435
|
workflowMaxPersistedRuns: 200,
|
|
232
436
|
workflowMaxConcurrentBranches: 8,
|
|
437
|
+
voiceEnabled: true,
|
|
438
|
+
voiceProvider: "auto",
|
|
439
|
+
voiceModel: "gpt-4o-realtime-preview-2024-12-17",
|
|
440
|
+
voiceVisionModel: "gpt-4.1-mini",
|
|
441
|
+
voiceId: "alloy",
|
|
442
|
+
voiceTurnDetection: "server_vad",
|
|
443
|
+
voiceFallbackMode: "browser",
|
|
444
|
+
voiceDelegateExecutor: "codex-sdk",
|
|
445
|
+
openaiRealtimeApiKey: "",
|
|
446
|
+
azureOpenaiRealtimeEndpoint: "",
|
|
447
|
+
azureOpenaiRealtimeApiKey: "",
|
|
448
|
+
azureOpenaiRealtimeDeployment: "gpt-4o-realtime-preview",
|
|
233
449
|
copilotEnableAllMcpTools: false,
|
|
234
450
|
// Backward-compatible fields consumed by older setup UI revisions.
|
|
235
451
|
distribution: "primary-only",
|
|
@@ -330,8 +546,33 @@ function applyTelegramMiniAppSetupEnv(envMap, env, sourceEnv = process.env) {
|
|
|
330
546
|
env?.telegramUiTunnel ||
|
|
331
547
|
env?.TELEGRAM_UI_TUNNEL ||
|
|
332
548
|
sourceEnv.TELEGRAM_UI_TUNNEL ||
|
|
333
|
-
"
|
|
334
|
-
envMap.TELEGRAM_UI_TUNNEL = String(tunnelRaw).trim() || "
|
|
549
|
+
"named";
|
|
550
|
+
envMap.TELEGRAM_UI_TUNNEL = String(tunnelRaw).trim() || "named";
|
|
551
|
+
|
|
552
|
+
const quickFallbackRaw =
|
|
553
|
+
env?.telegramUiAllowQuickTunnelFallback ??
|
|
554
|
+
env?.TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK ??
|
|
555
|
+
sourceEnv.TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK;
|
|
556
|
+
envMap.TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK = toBooleanEnvString(
|
|
557
|
+
quickFallbackRaw,
|
|
558
|
+
false,
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
const fallbackAuthRaw =
|
|
562
|
+
env?.telegramUiFallbackAuthEnabled ??
|
|
563
|
+
env?.TELEGRAM_UI_FALLBACK_AUTH_ENABLED ??
|
|
564
|
+
sourceEnv.TELEGRAM_UI_FALLBACK_AUTH_ENABLED;
|
|
565
|
+
envMap.TELEGRAM_UI_FALLBACK_AUTH_ENABLED = toBooleanEnvString(
|
|
566
|
+
fallbackAuthRaw,
|
|
567
|
+
true,
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
const hostnamePolicyRaw =
|
|
571
|
+
env?.cloudflareUsernameHostnamePolicy ||
|
|
572
|
+
env?.CLOUDFLARE_USERNAME_HOSTNAME_POLICY ||
|
|
573
|
+
sourceEnv.CLOUDFLARE_USERNAME_HOSTNAME_POLICY ||
|
|
574
|
+
"per-user-fixed";
|
|
575
|
+
envMap.CLOUDFLARE_USERNAME_HOSTNAME_POLICY = String(hostnamePolicyRaw).trim() || "per-user-fixed";
|
|
335
576
|
|
|
336
577
|
const unsafeRaw =
|
|
337
578
|
env?.telegramUiAllowUnsafe ??
|
|
@@ -589,6 +830,122 @@ function applyNonBlockingSetupEnvDefaults(envMap, env = {}, sourceEnv = process.
|
|
|
589
830
|
["workspace-write", "danger-full-access", "read-only"],
|
|
590
831
|
"workspace-write",
|
|
591
832
|
);
|
|
833
|
+
envMap.VOICE_ENABLED = toBooleanEnvString(
|
|
834
|
+
pickNonEmptyValue(
|
|
835
|
+
env.voiceEnabled,
|
|
836
|
+
env.VOICE_ENABLED,
|
|
837
|
+
envMap.VOICE_ENABLED,
|
|
838
|
+
sourceEnv.VOICE_ENABLED,
|
|
839
|
+
),
|
|
840
|
+
true,
|
|
841
|
+
);
|
|
842
|
+
envMap.VOICE_PROVIDER = normalizeEnumValue(
|
|
843
|
+
pickNonEmptyValue(
|
|
844
|
+
env.voiceProvider,
|
|
845
|
+
env.VOICE_PROVIDER,
|
|
846
|
+
envMap.VOICE_PROVIDER,
|
|
847
|
+
sourceEnv.VOICE_PROVIDER,
|
|
848
|
+
),
|
|
849
|
+
["auto", "openai", "azure", "claude", "gemini", "fallback"],
|
|
850
|
+
"auto",
|
|
851
|
+
);
|
|
852
|
+
envMap.VOICE_MODEL = String(
|
|
853
|
+
pickNonEmptyValue(
|
|
854
|
+
env.voiceModel,
|
|
855
|
+
env.VOICE_MODEL,
|
|
856
|
+
envMap.VOICE_MODEL,
|
|
857
|
+
sourceEnv.VOICE_MODEL,
|
|
858
|
+
) || "gpt-4o-realtime-preview-2024-12-17",
|
|
859
|
+
).trim() || "gpt-4o-realtime-preview-2024-12-17";
|
|
860
|
+
envMap.VOICE_VISION_MODEL = String(
|
|
861
|
+
pickNonEmptyValue(
|
|
862
|
+
env.voiceVisionModel,
|
|
863
|
+
env.VOICE_VISION_MODEL,
|
|
864
|
+
envMap.VOICE_VISION_MODEL,
|
|
865
|
+
sourceEnv.VOICE_VISION_MODEL,
|
|
866
|
+
) || "gpt-4.1-mini",
|
|
867
|
+
).trim() || "gpt-4.1-mini";
|
|
868
|
+
envMap.VOICE_ID = normalizeEnumValue(
|
|
869
|
+
pickNonEmptyValue(
|
|
870
|
+
env.voiceId,
|
|
871
|
+
env.VOICE_ID,
|
|
872
|
+
envMap.VOICE_ID,
|
|
873
|
+
sourceEnv.VOICE_ID,
|
|
874
|
+
),
|
|
875
|
+
["alloy", "ash", "ballad", "coral", "echo", "fable", "onyx", "nova", "sage", "shimmer", "verse"],
|
|
876
|
+
"alloy",
|
|
877
|
+
);
|
|
878
|
+
envMap.VOICE_TURN_DETECTION = normalizeEnumValue(
|
|
879
|
+
pickNonEmptyValue(
|
|
880
|
+
env.voiceTurnDetection,
|
|
881
|
+
env.VOICE_TURN_DETECTION,
|
|
882
|
+
envMap.VOICE_TURN_DETECTION,
|
|
883
|
+
sourceEnv.VOICE_TURN_DETECTION,
|
|
884
|
+
),
|
|
885
|
+
["server_vad", "semantic_vad", "none"],
|
|
886
|
+
"server_vad",
|
|
887
|
+
);
|
|
888
|
+
envMap.VOICE_FALLBACK_MODE = normalizeEnumValue(
|
|
889
|
+
pickNonEmptyValue(
|
|
890
|
+
env.voiceFallbackMode,
|
|
891
|
+
env.VOICE_FALLBACK_MODE,
|
|
892
|
+
envMap.VOICE_FALLBACK_MODE,
|
|
893
|
+
sourceEnv.VOICE_FALLBACK_MODE,
|
|
894
|
+
),
|
|
895
|
+
["browser", "disabled"],
|
|
896
|
+
"browser",
|
|
897
|
+
);
|
|
898
|
+
envMap.VOICE_DELEGATE_EXECUTOR = normalizeEnumValue(
|
|
899
|
+
pickNonEmptyValue(
|
|
900
|
+
env.voiceDelegateExecutor,
|
|
901
|
+
env.VOICE_DELEGATE_EXECUTOR,
|
|
902
|
+
envMap.VOICE_DELEGATE_EXECUTOR,
|
|
903
|
+
sourceEnv.VOICE_DELEGATE_EXECUTOR,
|
|
904
|
+
),
|
|
905
|
+
["codex-sdk", "copilot-sdk", "claude-sdk", "gemini-sdk", "opencode-sdk"],
|
|
906
|
+
"codex-sdk",
|
|
907
|
+
);
|
|
908
|
+
|
|
909
|
+
const openaiRealtimeApiKey = pickNonEmptyValue(
|
|
910
|
+
env.openaiRealtimeApiKey,
|
|
911
|
+
env.OPENAI_REALTIME_API_KEY,
|
|
912
|
+
envMap.OPENAI_REALTIME_API_KEY,
|
|
913
|
+
sourceEnv.OPENAI_REALTIME_API_KEY,
|
|
914
|
+
);
|
|
915
|
+
if (openaiRealtimeApiKey !== undefined) {
|
|
916
|
+
envMap.OPENAI_REALTIME_API_KEY = String(openaiRealtimeApiKey).trim();
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const azureRealtimeEndpoint = pickNonEmptyValue(
|
|
920
|
+
env.azureOpenaiRealtimeEndpoint,
|
|
921
|
+
env.AZURE_OPENAI_REALTIME_ENDPOINT,
|
|
922
|
+
envMap.AZURE_OPENAI_REALTIME_ENDPOINT,
|
|
923
|
+
sourceEnv.AZURE_OPENAI_REALTIME_ENDPOINT,
|
|
924
|
+
sourceEnv.AZURE_OPENAI_ENDPOINT,
|
|
925
|
+
);
|
|
926
|
+
if (azureRealtimeEndpoint !== undefined) {
|
|
927
|
+
envMap.AZURE_OPENAI_REALTIME_ENDPOINT = String(azureRealtimeEndpoint).trim();
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const azureRealtimeApiKey = pickNonEmptyValue(
|
|
931
|
+
env.azureOpenaiRealtimeApiKey,
|
|
932
|
+
env.AZURE_OPENAI_REALTIME_API_KEY,
|
|
933
|
+
envMap.AZURE_OPENAI_REALTIME_API_KEY,
|
|
934
|
+
sourceEnv.AZURE_OPENAI_REALTIME_API_KEY,
|
|
935
|
+
sourceEnv.AZURE_OPENAI_API_KEY,
|
|
936
|
+
);
|
|
937
|
+
if (azureRealtimeApiKey !== undefined) {
|
|
938
|
+
envMap.AZURE_OPENAI_REALTIME_API_KEY = String(azureRealtimeApiKey).trim();
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
envMap.AZURE_OPENAI_REALTIME_DEPLOYMENT = String(
|
|
942
|
+
pickNonEmptyValue(
|
|
943
|
+
env.azureOpenaiRealtimeDeployment,
|
|
944
|
+
env.AZURE_OPENAI_REALTIME_DEPLOYMENT,
|
|
945
|
+
envMap.AZURE_OPENAI_REALTIME_DEPLOYMENT,
|
|
946
|
+
sourceEnv.AZURE_OPENAI_REALTIME_DEPLOYMENT,
|
|
947
|
+
) || "gpt-4o-realtime-preview",
|
|
948
|
+
).trim() || "gpt-4o-realtime-preview";
|
|
592
949
|
|
|
593
950
|
envMap.CONTAINER_ENABLED = toBooleanEnvString(
|
|
594
951
|
pickNonEmptyValue(env.containerEnabled, envMap.CONTAINER_ENABLED, sourceEnv.CONTAINER_ENABLED),
|
|
@@ -962,8 +1319,20 @@ async function handleModelsProbe(body) {
|
|
|
962
1319
|
|
|
963
1320
|
// Copilot and Claude Code use OAuth — we can't probe their model lists from
|
|
964
1321
|
// the server side. Return the static list with a note.
|
|
965
|
-
if (
|
|
966
|
-
|
|
1322
|
+
if (
|
|
1323
|
+
executor === "COPILOT" ||
|
|
1324
|
+
executor === "CLAUDE_CODE" ||
|
|
1325
|
+
executor === "GEMINI" ||
|
|
1326
|
+
executor === "OPENCODE"
|
|
1327
|
+
) {
|
|
1328
|
+
const key =
|
|
1329
|
+
executor === "COPILOT"
|
|
1330
|
+
? "copilot"
|
|
1331
|
+
: executor === "CLAUDE_CODE"
|
|
1332
|
+
? "claude"
|
|
1333
|
+
: executor === "GEMINI"
|
|
1334
|
+
? "gemini"
|
|
1335
|
+
: "opencode";
|
|
967
1336
|
return {
|
|
968
1337
|
ok: true,
|
|
969
1338
|
models: MODELS[key] || [],
|
|
@@ -1073,6 +1442,7 @@ function handleApply(body) {
|
|
|
1073
1442
|
// Resolve home + workspaces dirs — prefer what the user chose in the wizard
|
|
1074
1443
|
const bosunHome = env.bosunHome ? resolve(env.bosunHome) : resolveConfigDir();
|
|
1075
1444
|
const workspacesDir = env.workspacesDir ? resolve(env.workspacesDir) : resolveWorkspacesDir(bosunHome);
|
|
1445
|
+
const existingConfig = readExistingBosunConfig(bosunHome);
|
|
1076
1446
|
|
|
1077
1447
|
// ── Create directory scaffold ───────────────────────────────────────────
|
|
1078
1448
|
mkdirSync(bosunHome, { recursive: true });
|
|
@@ -1264,6 +1634,13 @@ function handleApply(body) {
|
|
|
1264
1634
|
if (ex.baseUrl) envMap.ANTHROPIC_BASE_URL = ex.baseUrl;
|
|
1265
1635
|
// Note: Anthropic does not have a native multi-profile env-var system;
|
|
1266
1636
|
// only a single key/endpoint is supported for this executor type.
|
|
1637
|
+
} else if (type === "GEMINI" || type === "GOOGLE_GEMINI") {
|
|
1638
|
+
if (ex.apiKey) {
|
|
1639
|
+
envMap.GEMINI_API_KEY = ex.apiKey;
|
|
1640
|
+
}
|
|
1641
|
+
if (ex.baseUrl) {
|
|
1642
|
+
envMap.GEMINI_BASE_URL = ex.baseUrl;
|
|
1643
|
+
}
|
|
1267
1644
|
}
|
|
1268
1645
|
// COPILOT uses gh auth — no API key env vars needed.
|
|
1269
1646
|
}
|
|
@@ -1280,17 +1657,28 @@ function handleApply(body) {
|
|
|
1280
1657
|
|
|
1281
1658
|
// ── Build bosun.config.json ─────────────────────────────────────────────
|
|
1282
1659
|
const config = {
|
|
1660
|
+
...existingConfig,
|
|
1283
1661
|
projectName: configJson.projectName || env.projectName || "my-project",
|
|
1284
1662
|
bosunHome,
|
|
1285
1663
|
workspacesDir,
|
|
1286
|
-
executors:
|
|
1664
|
+
executors:
|
|
1665
|
+
Array.isArray(configJson.executors)
|
|
1666
|
+
? configJson.executors
|
|
1667
|
+
: Array.isArray(existingConfig.executors)
|
|
1668
|
+
? existingConfig.executors
|
|
1669
|
+
: [],
|
|
1287
1670
|
failover: configJson.failover || {
|
|
1671
|
+
...(existingConfig.failover || {}),
|
|
1288
1672
|
strategy: env.failoverStrategy || "next-in-line",
|
|
1289
1673
|
maxRetries: Number(env.maxRetries) || 3,
|
|
1290
1674
|
cooldownMinutes: Number(env.failoverCooldownMinutes) || 5,
|
|
1291
1675
|
disableOnConsecutiveFailures: Number(env.failoverDisableOnConsecutive) || 3,
|
|
1292
1676
|
},
|
|
1293
|
-
distribution:
|
|
1677
|
+
distribution:
|
|
1678
|
+
configJson.distribution ||
|
|
1679
|
+
existingConfig.distribution ||
|
|
1680
|
+
env.executorDistribution ||
|
|
1681
|
+
"primary-only",
|
|
1294
1682
|
};
|
|
1295
1683
|
|
|
1296
1684
|
const workflowProfile = normalizeWorkflowProfile(
|
|
@@ -1345,16 +1733,33 @@ function handleApply(body) {
|
|
|
1345
1733
|
if (configJson.primaryAgent) config.primaryAgent = configJson.primaryAgent;
|
|
1346
1734
|
if (configJson.projectRequirementsProfile) config.projectRequirementsProfile = configJson.projectRequirementsProfile;
|
|
1347
1735
|
if (configJson.internalReplenish) config.internalReplenish = configJson.internalReplenish;
|
|
1348
|
-
if (configJson.repos?.length) config.repos = configJson.repos;
|
|
1349
1736
|
if (configJson.kanban) config.kanban = configJson.kanban;
|
|
1350
|
-
|
|
1737
|
+
|
|
1738
|
+
const workspaceConfig = resolveSetupWorkspaceAndRepoConfig(
|
|
1739
|
+
existingConfig,
|
|
1740
|
+
configJson,
|
|
1741
|
+
env,
|
|
1742
|
+
);
|
|
1743
|
+
if (workspaceConfig.repos.length > 0) {
|
|
1744
|
+
config.repos = workspaceConfig.repos;
|
|
1745
|
+
} else {
|
|
1746
|
+
delete config.repos;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
if (workspaceConfig.workspaces.length > 0) {
|
|
1750
|
+
config.workspaces = workspaceConfig.workspaces;
|
|
1751
|
+
config.activeWorkspace = workspaceConfig.activeWorkspace;
|
|
1752
|
+
} else {
|
|
1753
|
+
delete config.workspaces;
|
|
1754
|
+
delete config.activeWorkspace;
|
|
1755
|
+
}
|
|
1351
1756
|
|
|
1352
1757
|
const configPath = resolve(bosunHome, "bosun.config.json");
|
|
1353
1758
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
1354
1759
|
|
|
1355
1760
|
// ── Trust the BOSUN_HOME in every agent CLI ─────────────────────────────
|
|
1356
1761
|
// Without this, running a codex agent from bosunHome is rejected with:
|
|
1357
|
-
// "
|
|
1762
|
+
// ":alert: Project config.toml files are disabled…"
|
|
1358
1763
|
// We also register bosunHome with Claude Code so it won't prompt for
|
|
1359
1764
|
// permission when accessing the workspace directories.
|
|
1360
1765
|
|
|
@@ -1362,7 +1767,7 @@ function handleApply(body) {
|
|
|
1362
1767
|
try {
|
|
1363
1768
|
const trustedResult = ensureTrustedProjects([bosunHome, workspacesDir]);
|
|
1364
1769
|
if (trustedResult.added.length > 0) {
|
|
1365
|
-
console.log("
|
|
1770
|
+
console.log(" :check: Codex: trusted bosun home directory:", trustedResult.added.join(", "));
|
|
1366
1771
|
}
|
|
1367
1772
|
} catch (err) {
|
|
1368
1773
|
console.warn("[setup] could not update codex trusted_projects:", err.message);
|
|
@@ -1373,7 +1778,7 @@ function handleApply(body) {
|
|
|
1373
1778
|
const claudeResult = ensureClaudeAdditionalDirectory(bosunHome);
|
|
1374
1779
|
const claudeWs = ensureClaudeAdditionalDirectory(workspacesDir);
|
|
1375
1780
|
if (claudeResult.added || claudeWs.added) {
|
|
1376
|
-
console.log("
|
|
1781
|
+
console.log(" :check: Claude: added bosun directories to additionalDirectories");
|
|
1377
1782
|
}
|
|
1378
1783
|
} catch (err) {
|
|
1379
1784
|
console.warn("[setup] could not update claude settings:", err.message);
|
|
@@ -1521,7 +1926,7 @@ async function handleRequest(req, res) {
|
|
|
1521
1926
|
jsonResponse(res, 200, { ok: true, message: "Setup complete" });
|
|
1522
1927
|
// Shut down server after response is sent
|
|
1523
1928
|
setTimeout(() => {
|
|
1524
|
-
console.log("\n
|
|
1929
|
+
console.log("\n :check: Setup complete — shutting down wizard server.\n");
|
|
1525
1930
|
if (callbackServer) callbackServer.close();
|
|
1526
1931
|
server.close();
|
|
1527
1932
|
process.exit(0);
|
|
@@ -1646,7 +2051,7 @@ async function startCallbackCatcher(setupPort) {
|
|
|
1646
2051
|
</head>
|
|
1647
2052
|
<body>
|
|
1648
2053
|
<div class="card">
|
|
1649
|
-
<div class="logo"
|
|
2054
|
+
<div class="logo">:rocket:</div>
|
|
1650
2055
|
<h1>Bosun GitHub App Setup</h1>
|
|
1651
2056
|
<p>Bosun needs to be running on your machine before you complete the GitHub Marketplace installation.</p>
|
|
1652
2057
|
<div class="step"><strong>Step 1:</strong> Open a terminal and run:<br><br><code>bosun --setup</code></div>
|
|
@@ -1677,7 +2082,7 @@ async function startCallbackCatcher(setupPort) {
|
|
|
1677
2082
|
</head>
|
|
1678
2083
|
<body>
|
|
1679
2084
|
<div class="card">
|
|
1680
|
-
<div class="icon"
|
|
2085
|
+
<div class="icon">:check:</div>
|
|
1681
2086
|
<h1>GitHub App Authorized!</h1>
|
|
1682
2087
|
<p>Redirecting you to the Bosun setup wizard…</p>
|
|
1683
2088
|
</div>
|
|
@@ -1742,14 +2147,14 @@ async function startCallbackCatcher(setupPort) {
|
|
|
1742
2147
|
|
|
1743
2148
|
try {
|
|
1744
2149
|
await tryListen(callbackServer, CALLBACK_PORT);
|
|
1745
|
-
console.log(`
|
|
2150
|
+
console.log(` :server: GitHub OAuth callback listener: http://127.0.0.1:${CALLBACK_PORT}/github/callback`);
|
|
1746
2151
|
console.log(` ↳ Keep this terminal open while installing from GitHub Marketplace.\n`);
|
|
1747
2152
|
} catch (err) {
|
|
1748
2153
|
if (err.code === "EADDRINUSE") {
|
|
1749
2154
|
// Another Bosun instance (or the main UI server) is already on this port — that's fine.
|
|
1750
|
-
console.log(`
|
|
2155
|
+
console.log(` :help: Port ${CALLBACK_PORT} is already in use (main Bosun server may be running).\n`);
|
|
1751
2156
|
} else {
|
|
1752
|
-
console.warn(`
|
|
2157
|
+
console.warn(` :alert: Could not start OAuth callback listener on port ${CALLBACK_PORT}: ${err.message}`);
|
|
1753
2158
|
}
|
|
1754
2159
|
callbackServer = null;
|
|
1755
2160
|
}
|
|
@@ -1807,7 +2212,7 @@ export async function startSetupServer(options = {}) {
|
|
|
1807
2212
|
actualPort = await tryListen(server, 0);
|
|
1808
2213
|
break;
|
|
1809
2214
|
} catch (e) {
|
|
1810
|
-
console.error(`
|
|
2215
|
+
console.error(` :close: Could not start setup server: ${e.message}`);
|
|
1811
2216
|
process.exit(1);
|
|
1812
2217
|
}
|
|
1813
2218
|
}
|
|
@@ -1824,7 +2229,7 @@ export async function startSetupServer(options = {}) {
|
|
|
1824
2229
|
console.log(`
|
|
1825
2230
|
┌──────────────────────────────────────────────────┐
|
|
1826
2231
|
│ │
|
|
1827
|
-
│
|
|
2232
|
+
│ :rocket: Bosun Setup Wizard v${version.padEnd(25)}│
|
|
1828
2233
|
│ │
|
|
1829
2234
|
│ Open in your browser: │
|
|
1830
2235
|
│ ${url.padEnd(45)}│
|
|
@@ -1855,6 +2260,9 @@ export {
|
|
|
1855
2260
|
applyTelegramMiniAppSetupEnv,
|
|
1856
2261
|
applyNonBlockingSetupEnvDefaults,
|
|
1857
2262
|
normalizeTelegramUiPort,
|
|
2263
|
+
normalizeRepoConfigEntry,
|
|
2264
|
+
normalizeWorkspaceConfigList,
|
|
2265
|
+
resolveSetupWorkspaceAndRepoConfig,
|
|
1858
2266
|
};
|
|
1859
2267
|
|
|
1860
2268
|
// Entry point when run directly
|