bosun 0.34.2 → 0.34.4
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/agent-pool.mjs +30 -10
- package/bosun.schema.json +5 -0
- package/cli.mjs +121 -1
- package/desktop/main.mjs +37 -1
- package/git-commit-helpers.mjs +83 -0
- package/kanban-adapter.mjs +97 -11
- package/monitor.mjs +179 -39
- package/package.json +2 -1
- package/setup-web-server.mjs +32 -7
- package/task-executor.mjs +7 -10
- package/task-store.mjs +43 -10
- package/telegram-bot.mjs +96 -23
- package/ui/modules/settings-schema.js +6 -0
- package/ui/tabs/control.js +2 -1
- package/ui/tabs/settings.js +21 -3
- package/ui-server.mjs +106 -12
- package/workflow-nodes.mjs +109 -2
- package/workspace-manager.mjs +18 -6
package/telegram-bot.mjs
CHANGED
|
@@ -118,6 +118,37 @@ import {
|
|
|
118
118
|
} from "./presence.mjs";
|
|
119
119
|
|
|
120
120
|
const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
|
121
|
+
|
|
122
|
+
function isWslInteropRuntime() {
|
|
123
|
+
return Boolean(
|
|
124
|
+
process.env.WSL_DISTRO_NAME
|
|
125
|
+
|| process.env.WSL_INTEROP
|
|
126
|
+
|| (process.platform === "win32"
|
|
127
|
+
&& String(process.env.HOME || "")
|
|
128
|
+
.trim()
|
|
129
|
+
.startsWith("/home/")),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveTelegramConfigDir() {
|
|
134
|
+
if (process.env.BOSUN_HOME) return resolve(process.env.BOSUN_HOME);
|
|
135
|
+
if (process.env.BOSUN_DIR) return resolve(process.env.BOSUN_DIR);
|
|
136
|
+
|
|
137
|
+
const preferWindowsDirs = process.platform === "win32" && !isWslInteropRuntime();
|
|
138
|
+
const baseDir = preferWindowsDirs
|
|
139
|
+
? process.env.APPDATA
|
|
140
|
+
|| process.env.LOCALAPPDATA
|
|
141
|
+
|| process.env.USERPROFILE
|
|
142
|
+
|| process.env.HOME
|
|
143
|
+
|| homedir()
|
|
144
|
+
: process.env.HOME
|
|
145
|
+
|| process.env.XDG_CONFIG_HOME
|
|
146
|
+
|| process.env.USERPROFILE
|
|
147
|
+
|| process.env.APPDATA
|
|
148
|
+
|| process.env.LOCALAPPDATA
|
|
149
|
+
|| homedir();
|
|
150
|
+
return resolve(baseDir, "bosun");
|
|
151
|
+
}
|
|
121
152
|
const repoRoot = resolveRepoRoot();
|
|
122
153
|
const BosunDir = __dirname;
|
|
123
154
|
const statusPath = resolve(repoRoot, ".cache", "ve-orchestrator-status.json");
|
|
@@ -329,6 +360,23 @@ function delayMs(ms) {
|
|
|
329
360
|
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
|
330
361
|
}
|
|
331
362
|
|
|
363
|
+
const SAFE_DETACH_PREFIX = "[telegram-bot] async-detach";
|
|
364
|
+
|
|
365
|
+
function safeDetach(label, taskOrPromise) {
|
|
366
|
+
const tag = label
|
|
367
|
+
? `${SAFE_DETACH_PREFIX}:${String(label)}`
|
|
368
|
+
: SAFE_DETACH_PREFIX;
|
|
369
|
+
try {
|
|
370
|
+
const result =
|
|
371
|
+
typeof taskOrPromise === "function" ? taskOrPromise() : taskOrPromise;
|
|
372
|
+
Promise.resolve(result).catch((err) => {
|
|
373
|
+
console.warn(`${tag} failed: ${err?.message || err}`);
|
|
374
|
+
});
|
|
375
|
+
} catch (err) {
|
|
376
|
+
console.warn(`${tag} failed: ${err?.message || err}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
332
380
|
function shouldUseCurlPrimary() {
|
|
333
381
|
return (
|
|
334
382
|
TELEGRAM_CURL_FALLBACK &&
|
|
@@ -1088,6 +1136,25 @@ async function bumpStickyMenu(chatId) {
|
|
|
1088
1136
|
});
|
|
1089
1137
|
}
|
|
1090
1138
|
|
|
1139
|
+
async function refreshStickyMenu(chatId, screenId = "home", params = {}) {
|
|
1140
|
+
const state = stickyMenuState.get(chatId);
|
|
1141
|
+
if (state?.messageId) {
|
|
1142
|
+
try {
|
|
1143
|
+
await deleteDirect(chatId, state.messageId);
|
|
1144
|
+
} catch {
|
|
1145
|
+
/* best effort */
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
const timer = stickyMenuTimers.get(chatId);
|
|
1149
|
+
if (timer) {
|
|
1150
|
+
clearTimeout(timer);
|
|
1151
|
+
stickyMenuTimers.delete(chatId);
|
|
1152
|
+
}
|
|
1153
|
+
stickyMenuState.delete(chatId);
|
|
1154
|
+
clearPendingUiInput(chatId);
|
|
1155
|
+
await showUiScreen(chatId, null, screenId, params, { sticky: true });
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1091
1158
|
// ── Telegram API Helpers ─────────────────────────────────────────────────────
|
|
1092
1159
|
|
|
1093
1160
|
/**
|
|
@@ -2257,7 +2324,7 @@ async function handleUpdate(update) {
|
|
|
2257
2324
|
// Free-text agent task runs in a separate queue so polling isn't blocked.
|
|
2258
2325
|
// If agent is already busy, handle immediately so follow-ups can be queued.
|
|
2259
2326
|
if (isPrimaryBusy()) {
|
|
2260
|
-
|
|
2327
|
+
safeDetach("free-text", () => handleFreeText(text, chatId));
|
|
2261
2328
|
return;
|
|
2262
2329
|
}
|
|
2263
2330
|
enqueueAgentTask(() => handleFreeText(text, chatId));
|
|
@@ -2432,7 +2499,7 @@ async function cmdWorkspace(chatId, text) {
|
|
|
2432
2499
|
.trim();
|
|
2433
2500
|
const [sub, ...rest] = raw ? raw.split(/\s+/) : [];
|
|
2434
2501
|
const subcmd = String(sub || "list").toLowerCase();
|
|
2435
|
-
const configDir =
|
|
2502
|
+
const configDir = resolveTelegramConfigDir();
|
|
2436
2503
|
|
|
2437
2504
|
try {
|
|
2438
2505
|
if (subcmd === "scan") {
|
|
@@ -3951,13 +4018,13 @@ function uiNavRow(parent) {
|
|
|
3951
4018
|
if (!parent) {
|
|
3952
4019
|
return [
|
|
3953
4020
|
uiButton("🏠 Home", uiGoAction("home")),
|
|
3954
|
-
uiButton("
|
|
4021
|
+
uiButton("❌ Close", "cb:close_menu"),
|
|
3955
4022
|
];
|
|
3956
4023
|
}
|
|
3957
4024
|
return [
|
|
3958
4025
|
uiButton("⬅️ Back", uiGoAction(parent)),
|
|
3959
4026
|
uiButton("🏠 Home", uiGoAction("home")),
|
|
3960
|
-
uiButton("
|
|
4027
|
+
uiButton("❌ Close", "cb:close_menu"),
|
|
3961
4028
|
];
|
|
3962
4029
|
}
|
|
3963
4030
|
|
|
@@ -4191,7 +4258,7 @@ Object.assign(UI_SCREENS, {
|
|
|
4191
4258
|
text: "📱 Open Control Center",
|
|
4192
4259
|
web_app: { url: telegramWebAppUrl },
|
|
4193
4260
|
},
|
|
4194
|
-
uiButton("
|
|
4261
|
+
uiButton("❌", "cb:close_menu"),
|
|
4195
4262
|
]);
|
|
4196
4263
|
if (telegramUiUrl) {
|
|
4197
4264
|
rows.unshift([
|
|
@@ -4201,10 +4268,10 @@ Object.assign(UI_SCREENS, {
|
|
|
4201
4268
|
} else if (telegramUiUrl) {
|
|
4202
4269
|
rows.unshift([
|
|
4203
4270
|
{ text: "🌐 Open Control Center", url: getBrowserUiUrl() || telegramUiUrl },
|
|
4204
|
-
uiButton("
|
|
4271
|
+
uiButton("❌", "cb:close_menu"),
|
|
4205
4272
|
]);
|
|
4206
4273
|
} else {
|
|
4207
|
-
rows.unshift([uiButton("
|
|
4274
|
+
rows.unshift([uiButton("❌ Close Menu", "cb:close_menu")]);
|
|
4208
4275
|
}
|
|
4209
4276
|
return buildKeyboard(rows);
|
|
4210
4277
|
},
|
|
@@ -5038,7 +5105,7 @@ Object.assign(UI_SCREENS, {
|
|
|
5038
5105
|
body: () => "Choose a local workspace to set active for task routing.",
|
|
5039
5106
|
keyboard: async (ctx) => {
|
|
5040
5107
|
const page = parsePageParam(ctx.params?.page);
|
|
5041
|
-
const configDir =
|
|
5108
|
+
const configDir = resolveTelegramConfigDir();
|
|
5042
5109
|
const workspaces = listLocalWorkspaces(configDir);
|
|
5043
5110
|
const active = getActiveLocalWorkspace(configDir);
|
|
5044
5111
|
if (!workspaces.length) {
|
|
@@ -5302,7 +5369,7 @@ Object.assign(UI_SCREENS, {
|
|
|
5302
5369
|
body: () => "Browse, manage, and interact with your managed workspaces.",
|
|
5303
5370
|
keyboard: async (ctx) => {
|
|
5304
5371
|
const page = parsePageParam(ctx.params?.page);
|
|
5305
|
-
const configDir =
|
|
5372
|
+
const configDir = resolveTelegramConfigDir();
|
|
5306
5373
|
const workspaces = listLocalWorkspaces(configDir);
|
|
5307
5374
|
const active = getActiveLocalWorkspace(configDir);
|
|
5308
5375
|
if (!workspaces.length) {
|
|
@@ -6060,7 +6127,7 @@ async function cmdApp(chatId) {
|
|
|
6060
6127
|
async function cmdMenu(chatId) {
|
|
6061
6128
|
syncUiUrlsFromServer();
|
|
6062
6129
|
if (telegramApiReachable !== false) {
|
|
6063
|
-
|
|
6130
|
+
safeDetach("menu-button-refresh", refreshMenuButton);
|
|
6064
6131
|
}
|
|
6065
6132
|
clearPendingUiInput(chatId);
|
|
6066
6133
|
await showUiScreen(chatId, null, "home", {}, { sticky: true });
|
|
@@ -6774,10 +6841,10 @@ async function cmdStartTask(chatId, args) {
|
|
|
6774
6841
|
);
|
|
6775
6842
|
return;
|
|
6776
6843
|
}
|
|
6777
|
-
|
|
6844
|
+
safeDetach("manual-start", () => executor.executeTask(task, {
|
|
6778
6845
|
sdk: sdk || undefined,
|
|
6779
6846
|
model: model || undefined,
|
|
6780
|
-
});
|
|
6847
|
+
}));
|
|
6781
6848
|
await sendReply(
|
|
6782
6849
|
chatId,
|
|
6783
6850
|
`✅ Manual start queued for ${task.title || task.id}.` +
|
|
@@ -9089,7 +9156,7 @@ async function cmdBackground(chatId, args) {
|
|
|
9089
9156
|
chatId,
|
|
9090
9157
|
`🛰️ Background task queued: "${task.slice(0, 80)}${task.length > 80 ? "…" : ""}"`,
|
|
9091
9158
|
);
|
|
9092
|
-
|
|
9159
|
+
safeDetach("background-free-text", () => handleFreeText(task, chatId, { background: true, isolated: true }));
|
|
9093
9160
|
return;
|
|
9094
9161
|
}
|
|
9095
9162
|
|
|
@@ -9448,11 +9515,11 @@ async function handleFreeText(text, chatId, options = {}) {
|
|
|
9448
9515
|
const elapsed = now - lastEditAt;
|
|
9449
9516
|
if (elapsed >= EDIT_THROTTLE_MS) {
|
|
9450
9517
|
editPending = true;
|
|
9451
|
-
|
|
9518
|
+
safeDetach("agent-edit", doEdit);
|
|
9452
9519
|
} else {
|
|
9453
9520
|
editPending = true;
|
|
9454
9521
|
if (editTimer) clearTimeout(editTimer);
|
|
9455
|
-
editTimer = setTimeout(() =>
|
|
9522
|
+
editTimer = setTimeout(() => safeDetach("agent-edit", doEdit), EDIT_THROTTLE_MS - elapsed);
|
|
9456
9523
|
}
|
|
9457
9524
|
};
|
|
9458
9525
|
|
|
@@ -9796,8 +9863,8 @@ function startPresenceLoop() {
|
|
|
9796
9863
|
);
|
|
9797
9864
|
}
|
|
9798
9865
|
};
|
|
9799
|
-
setTimeout(() =>
|
|
9800
|
-
setInterval(() =>
|
|
9866
|
+
setTimeout(() => safeDetach("presence-heartbeat", sendPresence), intervalMs);
|
|
9867
|
+
setInterval(() => safeDetach("presence-heartbeat", sendPresence), intervalMs);
|
|
9801
9868
|
}
|
|
9802
9869
|
|
|
9803
9870
|
function hasPresenceChanged(prev, curr) {
|
|
@@ -10439,6 +10506,11 @@ export async function startTelegramBot() {
|
|
|
10439
10506
|
if (miniAppEnabled || miniAppPort > 0) {
|
|
10440
10507
|
try {
|
|
10441
10508
|
await startTelegramUiServer({
|
|
10509
|
+
// Background monitor/bot runtime should not keep opening browser tabs.
|
|
10510
|
+
// Set BOSUN_UI_AUTO_OPEN_BROWSER=1 to opt-in.
|
|
10511
|
+
skipAutoOpen: !["1", "true", "yes", "on"].includes(
|
|
10512
|
+
String(process.env.BOSUN_UI_AUTO_OPEN_BROWSER || "").toLowerCase(),
|
|
10513
|
+
),
|
|
10442
10514
|
dependencies: {
|
|
10443
10515
|
execPrimaryPrompt,
|
|
10444
10516
|
getInternalExecutor: _getInternalExecutor,
|
|
@@ -10446,7 +10518,7 @@ export async function startTelegramBot() {
|
|
|
10446
10518
|
getAgentEventBus: _getAgentEventBus,
|
|
10447
10519
|
handleUiCommand: handleUiCommand,
|
|
10448
10520
|
getSyncEngine: _getSyncEngine,
|
|
10449
|
-
configDir:
|
|
10521
|
+
configDir: resolveTelegramConfigDir(),
|
|
10450
10522
|
onProjectSyncAlert: async (alert) => {
|
|
10451
10523
|
if (!_sendTelegramMessage) return;
|
|
10452
10524
|
const text = String(alert?.message || "Project sync alert");
|
|
@@ -10467,16 +10539,16 @@ export async function startTelegramBot() {
|
|
|
10467
10539
|
|
|
10468
10540
|
// Immediately sync the menu button, then periodically refresh
|
|
10469
10541
|
if (reachable) {
|
|
10470
|
-
|
|
10542
|
+
safeDetach("menu-button-refresh", refreshMenuButton);
|
|
10471
10543
|
}
|
|
10472
10544
|
if (reachable && !menuButtonRefreshTimer) {
|
|
10473
|
-
menuButtonRefreshTimer = setInterval(() =>
|
|
10545
|
+
menuButtonRefreshTimer = setInterval(() => safeDetach("menu-button-refresh", refreshMenuButton), MENU_BUTTON_REFRESH_MS);
|
|
10474
10546
|
}
|
|
10475
10547
|
|
|
10476
10548
|
// React immediately when the tunnel URL changes (e.g. after restart)
|
|
10477
10549
|
onTunnelUrlChange((url) => {
|
|
10478
10550
|
console.log(`[telegram-bot] tunnel URL changed: ${url} — refreshing menu button`);
|
|
10479
|
-
|
|
10551
|
+
safeDetach("menu-button-refresh", refreshMenuButton);
|
|
10480
10552
|
});
|
|
10481
10553
|
|
|
10482
10554
|
// Notify about firewall issues if detected (24h cooldown)
|
|
@@ -10588,8 +10660,9 @@ export async function startTelegramBot() {
|
|
|
10588
10660
|
} else {
|
|
10589
10661
|
await sendDirect(
|
|
10590
10662
|
telegramChatId,
|
|
10591
|
-
`🤖 Bosun primary agent online (${getPrimaryAgentName()}).\n\nType /menu for the control center or send any message to chat with the agent
|
|
10663
|
+
`🤖 Bosun primary agent online (${getPrimaryAgentName()}).\n\nType /menu for the control center or send any message to chat with the agent.\n\nRefreshing control center menu below…`,
|
|
10592
10664
|
);
|
|
10665
|
+
await refreshStickyMenu(telegramChatId, "home", {});
|
|
10593
10666
|
|
|
10594
10667
|
// ── SECURITY: Alert when ALLOW_UNSAFE is enabled (especially with tunnel) ──
|
|
10595
10668
|
const _isUnsafe = ["1", "true", "yes"].includes(
|
|
@@ -10691,7 +10764,7 @@ export function stopTelegramBot(options = {}) {
|
|
|
10691
10764
|
}
|
|
10692
10765
|
stopBatchFlushLoop();
|
|
10693
10766
|
}
|
|
10694
|
-
|
|
10767
|
+
safeDetach("poll-lock-release", releaseTelegramPollLock);
|
|
10695
10768
|
stopTelegramUiServer();
|
|
10696
10769
|
if (menuButtonRefreshTimer) {
|
|
10697
10770
|
clearInterval(menuButtonRefreshTimer);
|
|
@@ -131,6 +131,12 @@ export const SETTINGS_SCHEMA = [
|
|
|
131
131
|
|
|
132
132
|
// ── GitHub / Git ─────────────────────────────────────────
|
|
133
133
|
{ key: "GITHUB_TOKEN", label: "GitHub Token", category: "github", type: "secret", sensitive: true, description: "Personal access token or fine-grained token for GitHub API. Required for GitHub kanban backend." },
|
|
134
|
+
{ key: "BOSUN_GITHUB_CLIENT_ID", label: "GitHub OAuth Client ID", category: "github", type: "string", description: "OAuth client ID used for GitHub device flow sign-in in the Bosun portal." },
|
|
135
|
+
{ key: "BOSUN_GITHUB_CLIENT_SECRET", label: "GitHub OAuth Client Secret", category: "github", type: "secret", sensitive: true, description: "OAuth client secret used for GitHub web callback exchange.", advanced: true },
|
|
136
|
+
{ key: "BOSUN_GITHUB_APP_ID", label: "GitHub App ID", category: "github", type: "string", description: "Numeric Bosun GitHub App ID used for installation token auth.", advanced: true },
|
|
137
|
+
{ key: "BOSUN_GITHUB_PRIVATE_KEY_PATH", label: "GitHub App Private Key", category: "github", type: "string", description: "Absolute path to the Bosun GitHub App .pem private key.", advanced: true },
|
|
138
|
+
{ key: "BOSUN_GITHUB_WEBHOOK_SECRET", label: "GitHub App Webhook Secret", category: "github", type: "secret", sensitive: true, description: "Webhook HMAC secret for validating GitHub App webhook deliveries.", advanced: true },
|
|
139
|
+
{ key: "BOSUN_GITHUB_USER_TOKEN", label: "GitHub User Token Override", category: "github", type: "secret", sensitive: true, description: "Optional direct OAuth user token override (skips auth-state lookup).", advanced: true },
|
|
134
140
|
{ key: "GITHUB_REPOSITORY", label: "Repository", category: "github", type: "string", description: "GitHub repository in owner/repo format. Auto-detected from git remote if not set.", validate: "^[\\w.-]+/[\\w.-]+$" },
|
|
135
141
|
{ key: "GITHUB_PROJECT_MODE", label: "Project Mode", category: "github", type: "select", defaultVal: "issues", options: ["issues", "kanban"], description: "Use GitHub Issues directly, or GitHub Projects v2 kanban board." },
|
|
136
142
|
{ key: "GITHUB_PROJECT_NUMBER", label: "Project Number", category: "github", type: "number", min: 1, description: "GitHub Projects v2 number. Required when project mode is 'kanban'." },
|
package/ui/tabs/control.js
CHANGED
|
@@ -868,11 +868,12 @@ export function ControlTab() {
|
|
|
868
868
|
<div class="card-subtitle mt-sm">Kanban</div>
|
|
869
869
|
<${SegmentedControl}
|
|
870
870
|
options=${[
|
|
871
|
+
{ value: "internal", label: "Internal" },
|
|
871
872
|
{ value: "vk", label: "VK" },
|
|
872
873
|
{ value: "github", label: "GitHub" },
|
|
873
874
|
{ value: "jira", label: "Jira" },
|
|
874
875
|
]}
|
|
875
|
-
value=${config?.kanbanBackend || "
|
|
876
|
+
value=${config?.kanbanBackend || "internal"}
|
|
876
877
|
onChange=${(v) => updateConfig("kanban", v)}
|
|
877
878
|
/>
|
|
878
879
|
${regions.length > 1 && html`
|
package/ui/tabs/settings.js
CHANGED
|
@@ -715,11 +715,29 @@ function ServerConfigMode() {
|
|
|
715
715
|
method: "POST",
|
|
716
716
|
body: JSON.stringify({ changes }),
|
|
717
717
|
});
|
|
718
|
-
} catch (
|
|
719
|
-
|
|
718
|
+
} catch (error_) {
|
|
719
|
+
const message = String(error_?.message || "");
|
|
720
|
+
const shouldTryLegacy =
|
|
721
|
+
/Request failed \((404|405|501)\)/.test(message)
|
|
722
|
+
|| /Failed to fetch|NetworkError|Load failed/i.test(message);
|
|
723
|
+
|
|
724
|
+
if (!shouldTryLegacy) throw error_;
|
|
725
|
+
|
|
726
|
+
const legacyKeyMap = {
|
|
727
|
+
INTERNAL_EXECUTOR_SDK: "sdk",
|
|
728
|
+
KANBAN_BACKEND: "kanban",
|
|
729
|
+
EXECUTOR_REGIONS: "region",
|
|
730
|
+
};
|
|
731
|
+
const entries = Object.entries(changes);
|
|
732
|
+
if (entries.length !== 1) throw error_;
|
|
733
|
+
|
|
734
|
+
const [envKey, value] = entries[0];
|
|
735
|
+
const legacyKey = legacyKeyMap[envKey];
|
|
736
|
+
if (!legacyKey) throw error_;
|
|
737
|
+
|
|
720
738
|
res = await apiFetch("/api/config/update", {
|
|
721
739
|
method: "POST",
|
|
722
|
-
body: JSON.stringify(
|
|
740
|
+
body: JSON.stringify({ key: legacyKey, value }),
|
|
723
741
|
});
|
|
724
742
|
}
|
|
725
743
|
if (res?.ok || (res && typeof res === "object" && !Array.isArray(res))) {
|
package/ui-server.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { open, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
6
|
import { get as httpsGet } from "node:https";
|
|
7
7
|
import { createServer as createHttpsServer } from "node:https";
|
|
8
|
-
import { networkInterfaces } from "node:os";
|
|
8
|
+
import { networkInterfaces, homedir } from "node:os";
|
|
9
9
|
import { connect as netConnect } from "node:net";
|
|
10
10
|
import { resolve, extname, dirname, basename, relative } from "node:path";
|
|
11
11
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -27,6 +27,8 @@ function getLocalLanIp() {
|
|
|
27
27
|
import { WebSocketServer } from "ws";
|
|
28
28
|
import {
|
|
29
29
|
getKanbanAdapter,
|
|
30
|
+
getKanbanBackendName,
|
|
31
|
+
setKanbanBackend,
|
|
30
32
|
markTaskIgnored,
|
|
31
33
|
unmarkTaskIgnored,
|
|
32
34
|
} from "./kanban-adapter.mjs";
|
|
@@ -1249,9 +1251,38 @@ let uiDeps = {};
|
|
|
1249
1251
|
* Ensures the directory exists.
|
|
1250
1252
|
*/
|
|
1251
1253
|
function resolveUiConfigDir() {
|
|
1254
|
+
if (process.env.BOSUN_CONFIG_PATH) {
|
|
1255
|
+
const fromConfigPath = dirname(resolve(process.env.BOSUN_CONFIG_PATH));
|
|
1256
|
+
try { mkdirSync(fromConfigPath, { recursive: true }); } catch { /* ok */ }
|
|
1257
|
+
if (!uiDeps.configDir) uiDeps.configDir = fromConfigPath;
|
|
1258
|
+
return fromConfigPath;
|
|
1259
|
+
}
|
|
1260
|
+
const isWslInteropRuntime = Boolean(
|
|
1261
|
+
process.env.WSL_DISTRO_NAME
|
|
1262
|
+
|| process.env.WSL_INTEROP
|
|
1263
|
+
|| (process.platform === "win32"
|
|
1264
|
+
&& String(process.env.HOME || "")
|
|
1265
|
+
.trim()
|
|
1266
|
+
.startsWith("/home/")),
|
|
1267
|
+
);
|
|
1268
|
+
const preferWindowsDirs = process.platform === "win32" && !isWslInteropRuntime;
|
|
1269
|
+
const baseDir = preferWindowsDirs
|
|
1270
|
+
? process.env.APPDATA
|
|
1271
|
+
|| process.env.LOCALAPPDATA
|
|
1272
|
+
|| process.env.USERPROFILE
|
|
1273
|
+
|| process.env.HOME
|
|
1274
|
+
|| homedir()
|
|
1275
|
+
: process.env.HOME
|
|
1276
|
+
|| process.env.XDG_CONFIG_HOME
|
|
1277
|
+
|| process.env.USERPROFILE
|
|
1278
|
+
|| process.env.APPDATA
|
|
1279
|
+
|| process.env.LOCALAPPDATA
|
|
1280
|
+
|| homedir();
|
|
1281
|
+
|
|
1252
1282
|
const dir = uiDeps.configDir
|
|
1283
|
+
|| process.env.BOSUN_HOME
|
|
1253
1284
|
|| process.env.BOSUN_DIR
|
|
1254
|
-
|| resolve(
|
|
1285
|
+
|| resolve(baseDir, "bosun");
|
|
1255
1286
|
if (dir) {
|
|
1256
1287
|
try { mkdirSync(dir, { recursive: true }); } catch { /* ok */ }
|
|
1257
1288
|
// Cache it so subsequent calls don't re-resolve
|
|
@@ -1308,6 +1339,8 @@ const SETTINGS_KNOWN_KEYS = [
|
|
|
1308
1339
|
"BOSUN_PROMPT_PLANNER",
|
|
1309
1340
|
"GITHUB_TOKEN", "GITHUB_REPOSITORY", "GITHUB_PROJECT_MODE",
|
|
1310
1341
|
"GITHUB_PROJECT_NUMBER", "GITHUB_DEFAULT_ASSIGNEE", "GITHUB_AUTO_ASSIGN_CREATOR",
|
|
1342
|
+
"BOSUN_GITHUB_APP_ID", "BOSUN_GITHUB_PRIVATE_KEY_PATH", "BOSUN_GITHUB_CLIENT_ID", "BOSUN_GITHUB_CLIENT_SECRET",
|
|
1343
|
+
"BOSUN_GITHUB_WEBHOOK_SECRET", "BOSUN_GITHUB_USER_TOKEN",
|
|
1311
1344
|
"GITHUB_PROJECT_WEBHOOK_PATH", "GITHUB_PROJECT_WEBHOOK_SECRET", "GITHUB_PROJECT_WEBHOOK_REQUIRE_SIGNATURE",
|
|
1312
1345
|
"GITHUB_PROJECT_SYNC_ALERT_FAILURE_THRESHOLD", "GITHUB_PROJECT_SYNC_RATE_LIMIT_ALERT_THRESHOLD",
|
|
1313
1346
|
"VK_TARGET_BRANCH", "CODEX_ANALYZE_MERGE_STRATEGY", "DEPENDABOT_AUTO_MERGE",
|
|
@@ -1342,6 +1375,7 @@ const SETTINGS_SENSITIVE_KEYS = new Set([
|
|
|
1342
1375
|
"TELEGRAM_BOT_TOKEN", "TELEGRAM_CHAT_ID", "GITHUB_TOKEN",
|
|
1343
1376
|
"OPENAI_API_KEY", "AZURE_OPENAI_API_KEY", "CODEX_MODEL_PROFILE_XL_API_KEY", "CODEX_MODEL_PROFILE_M_API_KEY",
|
|
1344
1377
|
"ANTHROPIC_API_KEY", "COPILOT_CLI_TOKEN", "GITHUB_PROJECT_WEBHOOK_SECRET",
|
|
1378
|
+
"BOSUN_GITHUB_CLIENT_SECRET", "BOSUN_GITHUB_WEBHOOK_SECRET", "BOSUN_GITHUB_USER_TOKEN",
|
|
1345
1379
|
"CLOUDFLARE_TUNNEL_CREDENTIALS",
|
|
1346
1380
|
]);
|
|
1347
1381
|
|
|
@@ -1504,11 +1538,7 @@ function validateConfigSchemaChanges(changes) {
|
|
|
1504
1538
|
fieldErrors[envKey] = err.message || "Invalid value";
|
|
1505
1539
|
}
|
|
1506
1540
|
}
|
|
1507
|
-
if (Object.keys(fieldErrors).length === 0) {
|
|
1508
|
-
for (const envKey of pathMap.values()) {
|
|
1509
|
-
fieldErrors[envKey] = "Invalid value (config schema)";
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1541
|
+
if (Object.keys(fieldErrors).length === 0) return {};
|
|
1512
1542
|
return fieldErrors;
|
|
1513
1543
|
} catch {
|
|
1514
1544
|
return {};
|
|
@@ -5758,6 +5788,16 @@ async function handleApi(req, res, url) {
|
|
|
5758
5788
|
const regionEnv = (process.env.EXECUTOR_REGIONS || "").trim();
|
|
5759
5789
|
const regions = regionEnv ? regionEnv.split(",").map((r) => r.trim()).filter(Boolean) : ["auto"];
|
|
5760
5790
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, "package.json"), "utf8"));
|
|
5791
|
+
let runtimeKanbanBackend = "internal";
|
|
5792
|
+
try {
|
|
5793
|
+
runtimeKanbanBackend = String(
|
|
5794
|
+
getKanbanBackendName() || process.env.KANBAN_BACKEND || "internal",
|
|
5795
|
+
).trim().toLowerCase();
|
|
5796
|
+
} catch {
|
|
5797
|
+
runtimeKanbanBackend = String(
|
|
5798
|
+
process.env.KANBAN_BACKEND || "internal",
|
|
5799
|
+
).trim().toLowerCase();
|
|
5800
|
+
}
|
|
5761
5801
|
jsonResponse(res, 200, {
|
|
5762
5802
|
ok: true,
|
|
5763
5803
|
version: pkg.version,
|
|
@@ -5769,7 +5809,7 @@ async function handleApi(req, res, url) {
|
|
|
5769
5809
|
wsEnabled: true,
|
|
5770
5810
|
authRequired: !isAllowUnsafe(),
|
|
5771
5811
|
sdk: process.env.EXECUTOR_SDK || "auto",
|
|
5772
|
-
kanbanBackend:
|
|
5812
|
+
kanbanBackend: runtimeKanbanBackend,
|
|
5773
5813
|
regions,
|
|
5774
5814
|
});
|
|
5775
5815
|
return;
|
|
@@ -5790,6 +5830,13 @@ async function handleApi(req, res, url) {
|
|
|
5790
5830
|
return;
|
|
5791
5831
|
}
|
|
5792
5832
|
process.env[envKey] = value;
|
|
5833
|
+
if (envKey === "KANBAN_BACKEND") {
|
|
5834
|
+
try {
|
|
5835
|
+
setKanbanBackend(String(value).trim().toLowerCase());
|
|
5836
|
+
} catch (err) {
|
|
5837
|
+
console.warn(`[config] failed to switch kanban backend: ${err.message}`);
|
|
5838
|
+
}
|
|
5839
|
+
}
|
|
5793
5840
|
// Also send chat command for backward compat
|
|
5794
5841
|
const cmdMap = { sdk: `/sdk ${value}`, kanban: `/kanban ${value}`, region: `/region ${value}` };
|
|
5795
5842
|
const handler = uiDeps.handleUiCommand;
|
|
@@ -5895,6 +5942,15 @@ async function handleApi(req, res, url) {
|
|
|
5895
5942
|
process.env[key] = strVal;
|
|
5896
5943
|
strChanges[key] = strVal;
|
|
5897
5944
|
}
|
|
5945
|
+
if (Object.prototype.hasOwnProperty.call(strChanges, "KANBAN_BACKEND")) {
|
|
5946
|
+
try {
|
|
5947
|
+
setKanbanBackend(
|
|
5948
|
+
String(strChanges.KANBAN_BACKEND).trim().toLowerCase(),
|
|
5949
|
+
);
|
|
5950
|
+
} catch (err) {
|
|
5951
|
+
console.warn(`[settings] failed to switch kanban backend: ${err.message}`);
|
|
5952
|
+
}
|
|
5953
|
+
}
|
|
5898
5954
|
// Write to .env file
|
|
5899
5955
|
const updated = updateEnvFile(strChanges);
|
|
5900
5956
|
const configUpdate = updateConfigFile(changes);
|
|
@@ -6136,7 +6192,7 @@ async function handleApi(req, res, url) {
|
|
|
6136
6192
|
jsonResponse(res, 404, { ok: false, error: "Task not found." });
|
|
6137
6193
|
return;
|
|
6138
6194
|
}
|
|
6139
|
-
executor.executeTask(task).catch((error) => {
|
|
6195
|
+
executor.executeTask(task, { force: true }).catch((error) => {
|
|
6140
6196
|
console.warn(
|
|
6141
6197
|
`[telegram-ui] dispatch failed for ${taskId}: ${error.message}`,
|
|
6142
6198
|
);
|
|
@@ -6782,7 +6838,43 @@ export async function startTelegramUiServer(options = {}) {
|
|
|
6782
6838
|
|
|
6783
6839
|
const rawPort = options.port ?? getDefaultPort();
|
|
6784
6840
|
const port = Number(rawPort);
|
|
6785
|
-
|
|
6841
|
+
const isTestRun =
|
|
6842
|
+
Boolean(process.env.VITEST) ||
|
|
6843
|
+
process.env.NODE_ENV === "test" ||
|
|
6844
|
+
Boolean(process.env.JEST_WORKER_ID);
|
|
6845
|
+
const allowEphemeralPort =
|
|
6846
|
+
options.allowEphemeralPort === true ||
|
|
6847
|
+
process.env.BOSUN_UI_ALLOW_EPHEMERAL_PORT === "1" ||
|
|
6848
|
+
isTestRun;
|
|
6849
|
+
const portSource =
|
|
6850
|
+
options.port != null
|
|
6851
|
+
? "options.port"
|
|
6852
|
+
: process.env.TELEGRAM_UI_PORT
|
|
6853
|
+
? "env.TELEGRAM_UI_PORT"
|
|
6854
|
+
: "default(0)";
|
|
6855
|
+
|
|
6856
|
+
if (!Number.isFinite(port) || port < 0) {
|
|
6857
|
+
console.warn(
|
|
6858
|
+
`[telegram-ui] invalid ui port: raw=${String(rawPort)} source=${portSource}`,
|
|
6859
|
+
);
|
|
6860
|
+
return null;
|
|
6861
|
+
}
|
|
6862
|
+
if (port === 0 && !allowEphemeralPort) {
|
|
6863
|
+
console.warn(
|
|
6864
|
+
`[telegram-ui] refusing ephemeral ui port (resolved 0 from ${portSource}); ` +
|
|
6865
|
+
`set TELEGRAM_UI_PORT (for example 4400) or set BOSUN_UI_ALLOW_EPHEMERAL_PORT=1`,
|
|
6866
|
+
);
|
|
6867
|
+
return null;
|
|
6868
|
+
}
|
|
6869
|
+
|
|
6870
|
+
const autoOpenEnabled = ["1", "true", "yes", "on"].includes(
|
|
6871
|
+
String(process.env.BOSUN_UI_AUTO_OPEN_BROWSER || "")
|
|
6872
|
+
.trim()
|
|
6873
|
+
.toLowerCase(),
|
|
6874
|
+
);
|
|
6875
|
+
console.log(
|
|
6876
|
+
`[telegram-ui] startup config: port=${port} source=${portSource} autoOpen=${autoOpenEnabled ? "enabled" : "disabled"}`,
|
|
6877
|
+
);
|
|
6786
6878
|
|
|
6787
6879
|
injectUiDependencies(options.dependencies || {});
|
|
6788
6880
|
|
|
@@ -7042,12 +7134,14 @@ export async function startTelegramUiServer(options = {}) {
|
|
|
7042
7134
|
// - skip when caller passes skipAutoOpen
|
|
7043
7135
|
// - skip during Vitest / Jest test runs (avoids opening 20+ tabs during `npm test`)
|
|
7044
7136
|
// - only open ONCE per process (singleton guard — prevents loops on server restart)
|
|
7045
|
-
const
|
|
7137
|
+
const isTestRunRuntime =
|
|
7138
|
+
process.env.VITEST || process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID;
|
|
7046
7139
|
if (
|
|
7140
|
+
autoOpenEnabled &&
|
|
7047
7141
|
process.env.BOSUN_DESKTOP !== "1" &&
|
|
7048
7142
|
!options.skipAutoOpen &&
|
|
7049
7143
|
!_browserOpened &&
|
|
7050
|
-
!
|
|
7144
|
+
!isTestRunRuntime
|
|
7051
7145
|
) {
|
|
7052
7146
|
_browserOpened = true;
|
|
7053
7147
|
const openUrl = `${protocol}://${lanIp}:${actualPort}/?token=${sessionToken}`;
|
package/workflow-nodes.mjs
CHANGED
|
@@ -26,6 +26,76 @@ import { randomUUID } from "node:crypto";
|
|
|
26
26
|
const TAG = "[workflow-nodes]";
|
|
27
27
|
const PORTABLE_WORKTREE_COUNT_COMMAND = "node -e \"const cp=require('node:child_process');const wt=cp.execSync('git worktree list --porcelain',{encoding:'utf8'});const count=(wt.match(/^worktree /gm)||[]).length;process.stdout.write(String(count)+'\\\\n');\"";
|
|
28
28
|
const PORTABLE_PRUNE_AND_COUNT_WORKTREES_COMMAND = "node -e \"const cp=require('node:child_process');cp.execSync('git worktree prune',{stdio:'ignore'});const wt=cp.execSync('git worktree list --porcelain',{encoding:'utf8'});const count=(wt.match(/^worktree /gm)||[]).length;process.stdout.write(String(count)+'\\\\n');\"";
|
|
29
|
+
const WORKFLOW_AGENT_HEARTBEAT_MS = (() => {
|
|
30
|
+
const raw = Number(process.env.WORKFLOW_AGENT_HEARTBEAT_MS || 30000);
|
|
31
|
+
if (!Number.isFinite(raw)) return 30000;
|
|
32
|
+
return Math.max(5000, Math.min(120000, Math.trunc(raw)));
|
|
33
|
+
})();
|
|
34
|
+
|
|
35
|
+
function trimLogText(value, max = 180) {
|
|
36
|
+
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
37
|
+
if (!text) return "";
|
|
38
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function summarizeAgentStreamEvent(event) {
|
|
42
|
+
const type = String(event?.type || "").trim();
|
|
43
|
+
if (!type) return "";
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
type === "response.output_text.delta" ||
|
|
47
|
+
type === "response.output_text.done" ||
|
|
48
|
+
type === "item.updated"
|
|
49
|
+
) {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (type === "tool_call") {
|
|
54
|
+
return `Tool call: ${event?.tool_name || event?.data?.tool_name || "unknown"}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (type === "tool_result") {
|
|
58
|
+
const name = event?.tool_name || event?.data?.tool_name || "unknown";
|
|
59
|
+
return `Tool result: ${name}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (type === "error") {
|
|
63
|
+
return `Agent error: ${trimLogText(event?.error || event?.message || "unknown error", 220)}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const messageText = trimLogText(
|
|
67
|
+
event?.message?.content ||
|
|
68
|
+
event?.message?.text ||
|
|
69
|
+
event?.content ||
|
|
70
|
+
event?.text ||
|
|
71
|
+
event?.data?.content ||
|
|
72
|
+
event?.data?.text ||
|
|
73
|
+
"",
|
|
74
|
+
220,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (messageText) {
|
|
78
|
+
if (
|
|
79
|
+
type === "agent_message" ||
|
|
80
|
+
type === "assistant_message" ||
|
|
81
|
+
type === "message" ||
|
|
82
|
+
type === "item.completed"
|
|
83
|
+
) {
|
|
84
|
+
return `Agent: ${messageText}`;
|
|
85
|
+
}
|
|
86
|
+
return `${type}: ${messageText}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (
|
|
90
|
+
type === "turn.complete" ||
|
|
91
|
+
type === "session.completed" ||
|
|
92
|
+
type === "response.completed"
|
|
93
|
+
) {
|
|
94
|
+
return `Agent event: ${type}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
29
99
|
|
|
30
100
|
function normalizeLegacyWorkflowCommand(command) {
|
|
31
101
|
let normalized = String(command || "");
|
|
@@ -460,8 +530,45 @@ registerNodeType("action.run_agent", {
|
|
|
460
530
|
// Use the engine's service injection to call agent pool
|
|
461
531
|
const agentPool = engine.services?.agentPool;
|
|
462
532
|
if (agentPool?.launchEphemeralThread) {
|
|
463
|
-
|
|
464
|
-
|
|
533
|
+
let streamEventCount = 0;
|
|
534
|
+
let lastStreamLog = "";
|
|
535
|
+
const startedAt = Date.now();
|
|
536
|
+
const launchExtra = {};
|
|
537
|
+
if (sdk && sdk !== "auto") launchExtra.sdk = sdk;
|
|
538
|
+
|
|
539
|
+
launchExtra.onEvent = (event) => {
|
|
540
|
+
try {
|
|
541
|
+
const line = summarizeAgentStreamEvent(event);
|
|
542
|
+
if (!line || line === lastStreamLog) return;
|
|
543
|
+
lastStreamLog = line;
|
|
544
|
+
streamEventCount += 1;
|
|
545
|
+
ctx.log(node.id, line);
|
|
546
|
+
} catch {
|
|
547
|
+
// Stream callbacks must never crash workflow execution.
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const heartbeat = setInterval(() => {
|
|
552
|
+
const elapsedSec = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
|
|
553
|
+
ctx.log(node.id, `Agent still running (${elapsedSec}s elapsed)`);
|
|
554
|
+
}, WORKFLOW_AGENT_HEARTBEAT_MS);
|
|
555
|
+
|
|
556
|
+
let result;
|
|
557
|
+
try {
|
|
558
|
+
result = await agentPool.launchEphemeralThread(
|
|
559
|
+
finalPrompt,
|
|
560
|
+
cwd,
|
|
561
|
+
timeoutMs,
|
|
562
|
+
launchExtra,
|
|
563
|
+
);
|
|
564
|
+
} finally {
|
|
565
|
+
clearInterval(heartbeat);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
ctx.log(
|
|
569
|
+
node.id,
|
|
570
|
+
`Agent completed: success=${result.success} streamEvents=${streamEventCount}`,
|
|
571
|
+
);
|
|
465
572
|
|
|
466
573
|
// Propagate session/thread IDs for downstream chaining
|
|
467
574
|
const threadId = result.threadId || result.sessionId || null;
|