bosun 0.36.2 → 0.36.3

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/ui-server.mjs CHANGED
@@ -1253,6 +1253,85 @@ async function applySharedStateToTasks(tasks) {
1253
1253
  });
1254
1254
  }
1255
1255
 
1256
+ function normalizeCandidatePath(input) {
1257
+ if (!input) return "";
1258
+ const raw = String(input).trim();
1259
+ if (!raw) return "";
1260
+ try {
1261
+ return resolve(raw);
1262
+ } catch {
1263
+ return "";
1264
+ }
1265
+ }
1266
+
1267
+ function pickWorkspaceRepoDir(workspace) {
1268
+ if (!workspace || typeof workspace !== "object") return "";
1269
+ const repos = Array.isArray(workspace.repos) ? workspace.repos : [];
1270
+ const activeRepoName = String(workspace.activeRepo || "").trim();
1271
+ const selectedRepo =
1272
+ (activeRepoName
1273
+ ? repos.find((repo) => String(repo?.name || "").trim() === activeRepoName)
1274
+ : null) ||
1275
+ repos.find((repo) => repo?.primary) ||
1276
+ repos[0] ||
1277
+ null;
1278
+
1279
+ const candidates = [];
1280
+ const selectedRepoPath = normalizeCandidatePath(selectedRepo?.path);
1281
+ if (selectedRepoPath) candidates.push(selectedRepoPath);
1282
+ const workspacePath = normalizeCandidatePath(workspace.path);
1283
+ if (workspacePath && selectedRepo?.name) {
1284
+ const joined = normalizeCandidatePath(resolve(workspacePath, String(selectedRepo.name)));
1285
+ if (joined) candidates.push(joined);
1286
+ }
1287
+ if (workspacePath) candidates.push(workspacePath);
1288
+
1289
+ for (const candidate of candidates) {
1290
+ if (!candidate || !existsSync(candidate)) continue;
1291
+ if (existsSync(resolve(candidate, ".git"))) return candidate;
1292
+ }
1293
+ for (const candidate of candidates) {
1294
+ if (candidate && existsSync(candidate)) return candidate;
1295
+ }
1296
+ return "";
1297
+ }
1298
+
1299
+ function resolveActiveWorkspaceExecutionContext() {
1300
+ const fallback = { workspaceId: "", workspaceDir: repoRoot };
1301
+ const configDir = resolveUiConfigDir();
1302
+ if (!configDir) return fallback;
1303
+
1304
+ const listed = listManagedWorkspaces(configDir, { repoRoot });
1305
+ const active = getActiveManagedWorkspace(configDir);
1306
+ const activeId = String(active?.id || "").trim();
1307
+ const workspace =
1308
+ (activeId
1309
+ ? listed.find((entry) => String(entry?.id || "") === activeId)
1310
+ : null) ||
1311
+ active ||
1312
+ listed[0] ||
1313
+ null;
1314
+ if (!workspace) return fallback;
1315
+
1316
+ const workspaceId = String(workspace.id || "").trim();
1317
+ const workspaceDir = pickWorkspaceRepoDir(workspace) || fallback.workspaceDir;
1318
+ return {
1319
+ workspaceId,
1320
+ workspaceDir,
1321
+ };
1322
+ }
1323
+
1324
+ function resolveSessionWorkspaceDir(session = null) {
1325
+ const metadata =
1326
+ session && typeof session.metadata === "object" && session.metadata
1327
+ ? session.metadata
1328
+ : null;
1329
+ const explicit = normalizeCandidatePath(metadata?.workspaceDir);
1330
+ if (explicit && existsSync(explicit)) return explicit;
1331
+ const context = resolveActiveWorkspaceExecutionContext();
1332
+ return context.workspaceDir || repoRoot;
1333
+ }
1334
+
1256
1335
  function normalizeWorktreePath(input) {
1257
1336
  if (!input) return "";
1258
1337
  try {
@@ -2359,7 +2438,11 @@ const SETTINGS_SENSITIVE_KEYS = new Set([
2359
2438
  "CLOUDFLARE_TUNNEL_CREDENTIALS", "CLOUDFLARE_API_TOKEN",
2360
2439
  ]);
2361
2440
 
2362
- const SETTINGS_KNOWN_SET = new Set(SETTINGS_KNOWN_KEYS);
2441
+ const SETTINGS_SCHEMA_KEYS = SETTINGS_SCHEMA
2442
+ .map((def) => String(def?.key || "").trim())
2443
+ .filter((key) => key && !key.startsWith("_"));
2444
+ const SETTINGS_KNOWN_SET = new Set([...SETTINGS_KNOWN_KEYS, ...SETTINGS_SCHEMA_KEYS]);
2445
+ const SETTINGS_EFFECTIVE_KEYS = Array.from(SETTINGS_KNOWN_SET);
2363
2446
  let _settingsLastUpdateTime = 0;
2364
2447
  const ASYNC_UI_COMMAND_BASES = new Set(["/plan"]);
2365
2448
 
@@ -2411,7 +2494,7 @@ function buildSettingsResponseData() {
2411
2494
  );
2412
2495
  const { configData } = readConfigDocument();
2413
2496
 
2414
- for (const key of SETTINGS_KNOWN_KEYS) {
2497
+ for (const key of SETTINGS_EFFECTIVE_KEYS) {
2415
2498
  const def = defsByKey.get(key);
2416
2499
  let rawValue = process.env[key];
2417
2500
  let source = hasSettingValue(rawValue) ? "env" : "unset";
@@ -2447,7 +2530,7 @@ function buildSettingsResponseData() {
2447
2530
  }
2448
2531
 
2449
2532
  const displayValue = toSettingsDisplayValue(def, rawValue);
2450
- if (SETTINGS_SENSITIVE_KEYS.has(key)) {
2533
+ if (SETTINGS_SENSITIVE_KEYS.has(key) || def?.sensitive) {
2451
2534
  data[key] = displayValue ? "••••••" : "";
2452
2535
  } else {
2453
2536
  data[key] = displayValue;
@@ -8490,9 +8573,28 @@ async function handleApi(req, res, url) {
8490
8573
  }
8491
8574
  const args = (body?.args || "").trim();
8492
8575
  const adapter = (body?.adapter || "").trim() || undefined;
8493
- const result = await execSdkCommand(command, args, adapter);
8576
+ const requestedSessionId = String(body?.sessionId || "").trim();
8577
+ const tracker = getSessionTracker();
8578
+ const commandSession = requestedSessionId
8579
+ ? tracker.getSessionById(requestedSessionId)
8580
+ : null;
8581
+ const commandCwd = resolveSessionWorkspaceDir(commandSession);
8582
+ const runSdkCommand =
8583
+ typeof uiDeps.execSdkCommand === "function"
8584
+ ? uiDeps.execSdkCommand
8585
+ : execSdkCommand;
8586
+ const result = await runSdkCommand(command, args, adapter, {
8587
+ cwd: commandCwd,
8588
+ sessionId: requestedSessionId || undefined,
8589
+ });
8494
8590
  const parsed = typeof result === "string" ? result : JSON.stringify(result);
8495
- jsonResponse(res, 200, { ok: true, result: parsed, command, adapter: adapter || getPrimaryAgentName() });
8591
+ jsonResponse(res, 200, {
8592
+ ok: true,
8593
+ result: parsed,
8594
+ command,
8595
+ adapter: adapter || getPrimaryAgentName(),
8596
+ sessionId: requestedSessionId || null,
8597
+ });
8496
8598
  broadcastUiEvent(["agents", "sessions"], "invalidate", {
8497
8599
  reason: "sdk-command-executed",
8498
8600
  command,
@@ -8659,6 +8761,13 @@ async function handleApi(req, res, url) {
8659
8761
  const body = await readJsonBody(req);
8660
8762
  const type = body?.type || "manual";
8661
8763
  const id = `${type}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
8764
+ const workspaceContext = resolveActiveWorkspaceExecutionContext();
8765
+ const requestedWorkspaceId = String(body?.workspaceId || "").trim();
8766
+ const requestedWorkspaceDir = normalizeCandidatePath(body?.workspaceDir);
8767
+ const resolvedWorkspaceId =
8768
+ requestedWorkspaceId || workspaceContext.workspaceId;
8769
+ const resolvedWorkspaceDir =
8770
+ requestedWorkspaceDir || workspaceContext.workspaceDir || repoRoot;
8662
8771
  const tracker = getSessionTracker();
8663
8772
  const session = tracker.createSession({
8664
8773
  id,
@@ -8668,6 +8777,8 @@ async function handleApi(req, res, url) {
8668
8777
  agent: body?.agent || getPrimaryAgentName(),
8669
8778
  mode: body?.mode || getAgentMode(),
8670
8779
  model: body?.model || undefined,
8780
+ ...(resolvedWorkspaceId ? { workspaceId: resolvedWorkspaceId } : {}),
8781
+ ...(resolvedWorkspaceDir ? { workspaceDir: resolvedWorkspaceDir } : {}),
8671
8782
  },
8672
8783
  });
8673
8784
  jsonResponse(res, 200, { ok: true, session: { id: session.id, type: session.type, status: session.status, metadata: session.metadata } });
@@ -8813,6 +8924,7 @@ async function handleApi(req, res, url) {
8813
8924
  exec = await resolveExecPrimaryPrompt();
8814
8925
  }
8815
8926
  if (exec) {
8927
+ const sessionWorkspaceDir = resolveSessionWorkspaceDir(session);
8816
8928
  // Don't record user event here — execPrimaryPrompt records it
8817
8929
  // Respond immediately so the UI doesn't block on agent execution
8818
8930
  jsonResponse(res, 200, { ok: true, messageId });
@@ -8853,6 +8965,7 @@ async function handleApi(req, res, url) {
8853
8965
  sessionType: "primary",
8854
8966
  mode: messageMode,
8855
8967
  model: messageModel,
8968
+ cwd: sessionWorkspaceDir,
8856
8969
  persistent: true,
8857
8970
  sendRawEvents: true,
8858
8971
  attachments,
@@ -8893,6 +9006,44 @@ async function handleApi(req, res, url) {
8893
9006
  return;
8894
9007
  }
8895
9008
 
9009
+ if (action === "message/edit" && req.method === "POST") {
9010
+ try {
9011
+ const tracker = getSessionTracker();
9012
+ const session = tracker.getSessionById(sessionId);
9013
+ if (!session) {
9014
+ jsonResponse(res, 404, { ok: false, error: "Session not found" });
9015
+ return;
9016
+ }
9017
+ const body = await readJsonBody(req);
9018
+ const content = String(body?.content || "").trim();
9019
+ if (!content) {
9020
+ jsonResponse(res, 400, { ok: false, error: "content is required" });
9021
+ return;
9022
+ }
9023
+
9024
+ const edited = tracker.editUserMessage(sessionId, {
9025
+ messageId: body?.messageId,
9026
+ timestamp: body?.timestamp,
9027
+ previousContent: body?.previousContent,
9028
+ content,
9029
+ });
9030
+ if (!edited?.ok) {
9031
+ const status = edited?.error === "Message not found" ? 404 : 400;
9032
+ jsonResponse(res, status, { ok: false, error: edited?.error || "edit failed" });
9033
+ return;
9034
+ }
9035
+
9036
+ jsonResponse(res, 200, { ok: true, message: edited.message });
9037
+ broadcastUiEvent(["sessions"], "invalidate", {
9038
+ reason: "session-message-edited",
9039
+ sessionId,
9040
+ });
9041
+ } catch (err) {
9042
+ jsonResponse(res, 500, { ok: false, error: err.message });
9043
+ }
9044
+ return;
9045
+ }
9046
+
8896
9047
  if (action === "archive" && req.method === "POST") {
8897
9048
  try {
8898
9049
  const tracker = getSessionTracker();
@@ -9421,14 +9572,18 @@ export async function startTelegramUiServer(options = {}) {
9421
9572
  shouldReusePersistedPort
9422
9573
  ? persistedPort
9423
9574
  : configuredPort;
9575
+ const hasExplicitEnvPort =
9576
+ Number.isFinite(Number(process.env.TELEGRAM_UI_PORT || "")) &&
9577
+ Number(process.env.TELEGRAM_UI_PORT || "") > 0;
9424
9578
  const portSource =
9425
9579
  shouldReusePersistedPort
9426
9580
  ? "cache.ui-last-port"
9427
9581
  : options.port != null
9428
9582
  ? "options.port"
9429
- : process.env.TELEGRAM_UI_PORT
9583
+ : hasExplicitEnvPort
9430
9584
  ? "env.TELEGRAM_UI_PORT"
9431
9585
  : `default(${DEFAULT_TELEGRAM_UI_PORT})`;
9586
+ const usingFallbackDefaultPort = portSource.startsWith("default(");
9432
9587
 
9433
9588
  if (!Number.isFinite(port) || port < 0) {
9434
9589
  console.warn(
@@ -9745,7 +9900,8 @@ export async function startTelegramUiServer(options = {}) {
9745
9900
  // Reuse a recent session token when possible so browser sessions survive restarts.
9746
9901
  ensureSessionToken();
9747
9902
 
9748
- const listenHost = options.host || DEFAULT_HOST;
9903
+ const host = options.host || DEFAULT_HOST;
9904
+ const maxPortFallbackAttempts = usingFallbackDefaultPort ? 20 : 0;
9749
9905
  const listenOnce = (targetPort) =>
9750
9906
  new Promise((resolveReady, rejectReady) => {
9751
9907
  const onError = (err) => {
@@ -9758,20 +9914,38 @@ export async function startTelegramUiServer(options = {}) {
9758
9914
  };
9759
9915
  uiServer.once("error", onError);
9760
9916
  uiServer.once("listening", onListening);
9761
- uiServer.listen(targetPort, listenHost);
9917
+ uiServer.listen(targetPort, host);
9762
9918
  });
9919
+ let listenPort = port;
9920
+ for (let attempt = 0; ; attempt += 1) {
9921
+ try {
9922
+ await listenOnce(listenPort);
9923
+ break;
9924
+ } catch (err) {
9925
+ const isAddrInUse = err?.code === "EADDRINUSE";
9926
+ const canRetryPortIncrement =
9927
+ isAddrInUse &&
9928
+ attempt < maxPortFallbackAttempts &&
9929
+ listenPort > 0;
9930
+ if (canRetryPortIncrement) {
9931
+ const nextPort = listenPort + 1;
9932
+ console.warn(
9933
+ `[telegram-ui] port ${listenPort} in use; retrying on ${nextPort} (attempt ${attempt + 1}/${maxPortFallbackAttempts})`,
9934
+ );
9935
+ listenPort = nextPort;
9936
+ continue;
9937
+ }
9763
9938
 
9764
- try {
9765
- await listenOnce(port);
9766
- } catch (err) {
9767
- const code = String(err?.code || "").toUpperCase();
9768
- const canRetryWithEphemeral =
9769
- allowEphemeralPort && port > 0 && (code === "EADDRINUSE" || code === "EACCES");
9770
- if (!canRetryWithEphemeral) throw err;
9771
- console.warn(
9772
- `[telegram-ui] failed to bind ${listenHost}:${port} (${code || "unknown"}); retrying with ephemeral port`,
9773
- );
9774
- await listenOnce(0);
9939
+ const code = String(err?.code || "").toUpperCase();
9940
+ const canRetryWithEphemeral =
9941
+ allowEphemeralPort && listenPort > 0 && (code === "EADDRINUSE" || code === "EACCES");
9942
+ if (!canRetryWithEphemeral) throw err;
9943
+ console.warn(
9944
+ `[telegram-ui] failed to bind ${host}:${listenPort} (${code || "unknown"}); retrying with ephemeral port`,
9945
+ );
9946
+ await listenOnce(0);
9947
+ break;
9948
+ }
9775
9949
  }
9776
9950
  } catch (err) {
9777
9951
  releaseUiInstanceLock();
package/update-check.mjs CHANGED
@@ -529,6 +529,12 @@ let parentPid = null;
529
529
  let parentCheckInterval = null;
530
530
  let cleanupHandlersRegistered = false;
531
531
 
532
+ function isSuppressedStreamNoiseError(err) {
533
+ const msg = String(err?.message || err || "");
534
+ if (!msg) return false;
535
+ return msg.includes("setRawMode EIO") || msg.includes("read EIO");
536
+ }
537
+
532
538
  /**
533
539
  * Start a background polling loop that checks for updates every `intervalMs`
534
540
  * (default 10 min). When a newer version is found, it:
@@ -800,17 +806,17 @@ function registerCleanupHandlers() {
800
806
  stopAutoUpdateLoop();
801
807
  });
802
808
 
803
- // Handle uncaught exceptions (last resort)
804
- const originalUncaughtException = process.listeners("uncaughtException");
809
+ // Handle uncaught exceptions (last resort). Do not manually re-emit previous
810
+ // listeners: Node invokes all uncaughtException handlers automatically.
805
811
  process.on("uncaughtException", (err) => {
812
+ if (isSuppressedStreamNoiseError(err)) {
813
+ console.log(
814
+ `[auto-update] suppressed stream noise (uncaughtException): ${err?.message || err}`,
815
+ );
816
+ return;
817
+ }
806
818
  console.error(`[auto-update] Uncaught exception, cleaning up:`, err);
807
819
  stopAutoUpdateLoop();
808
- // Re-emit for other handlers
809
- if (originalUncaughtException.length > 0) {
810
- for (const handler of originalUncaughtException) {
811
- handler(err);
812
- }
813
- }
814
820
  });
815
821
  }
816
822
 
@@ -829,22 +835,44 @@ function printUpdateNotice(current, latest) {
829
835
 
830
836
  function promptConfirm(question) {
831
837
  return new Promise((res) => {
838
+ let settled = false;
839
+ const settle = (value) => {
840
+ if (settled) return;
841
+ settled = true;
842
+ res(Boolean(value));
843
+ };
832
844
  const rl = createInterface({
833
845
  input: process.stdin,
834
846
  output: process.stdout,
835
- terminal: process.stdin.isTTY && process.stdout.isTTY,
847
+ // Keep readline out of raw-mode transitions; they can throw EIO during
848
+ // terminal teardown in daemonized/non-interactive contexts.
849
+ terminal: false,
850
+ });
851
+ rl.on("error", (err) => {
852
+ if (isSuppressedStreamNoiseError(err)) {
853
+ console.log(
854
+ `[auto-update] suppressed stream noise (readline): ${err?.message || err}`,
855
+ );
856
+ settle(false);
857
+ return;
858
+ }
859
+ console.warn(`[auto-update] prompt failed: ${err?.message || err}`);
860
+ settle(false);
836
861
  });
837
862
  rl.question(question, (answer) => {
838
863
  try {
839
864
  rl.close();
840
865
  } catch (err) {
841
- const msg = err?.message || String(err || "");
842
- if (!msg.includes("setRawMode EIO")) {
843
- throw err;
866
+ if (isSuppressedStreamNoiseError(err)) {
867
+ console.log(
868
+ `[auto-update] suppressed stream noise (readline close): ${err?.message || err}`,
869
+ );
870
+ } else {
871
+ console.warn(`[auto-update] prompt close failed: ${err?.message || err}`);
844
872
  }
845
873
  }
846
874
  const a = answer.trim().toLowerCase();
847
- res(!a || a === "y" || a === "yes");
875
+ settle(!a || a === "y" || a === "yes");
848
876
  });
849
877
  });
850
878
  }