bosun 0.40.21 → 0.41.1

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.
Files changed (80) hide show
  1. package/.env.example +8 -0
  2. package/README.md +20 -0
  3. package/agent/agent-custom-tools.mjs +23 -5
  4. package/agent/agent-event-bus.mjs +248 -6
  5. package/agent/agent-pool.mjs +131 -30
  6. package/agent/agent-work-analyzer.mjs +8 -16
  7. package/agent/primary-agent.mjs +81 -7
  8. package/agent/retry-queue.mjs +164 -0
  9. package/bench/swebench/bosun-swebench.mjs +5 -0
  10. package/bosun.config.example.json +25 -0
  11. package/bosun.schema.json +825 -183
  12. package/cli.mjs +267 -8
  13. package/config/config-doctor.mjs +51 -2
  14. package/config/config.mjs +232 -5
  15. package/github/github-auth-manager.mjs +70 -19
  16. package/infra/library-manager.mjs +894 -60
  17. package/infra/monitor.mjs +701 -69
  18. package/infra/runtime-accumulator.mjs +376 -84
  19. package/infra/session-tracker.mjs +95 -28
  20. package/infra/test-runtime.mjs +267 -0
  21. package/lib/codebase-audit.mjs +133 -18
  22. package/package.json +30 -8
  23. package/server/setup-web-server.mjs +29 -1
  24. package/server/ui-server.mjs +1571 -49
  25. package/setup.mjs +27 -24
  26. package/shell/codex-shell.mjs +34 -3
  27. package/shell/copilot-shell.mjs +50 -8
  28. package/task/msg-hub.mjs +193 -0
  29. package/task/pipeline.mjs +544 -0
  30. package/task/task-claims.mjs +6 -10
  31. package/task/task-cli.mjs +38 -2
  32. package/task/task-executor-pipeline.mjs +143 -0
  33. package/task/task-executor.mjs +36 -27
  34. package/telegram/get-telegram-chat-id.mjs +57 -47
  35. package/ui/components/chat-view.js +18 -1
  36. package/ui/components/workspace-switcher.js +321 -9
  37. package/ui/demo-defaults.js +17830 -10433
  38. package/ui/demo.html +9 -1
  39. package/ui/modules/router.js +1 -1
  40. package/ui/modules/settings-schema.js +2 -0
  41. package/ui/modules/state.js +54 -57
  42. package/ui/modules/voice-client-sdk.js +376 -37
  43. package/ui/modules/voice-client.js +173 -33
  44. package/ui/setup.html +68 -2
  45. package/ui/styles/components.css +571 -1
  46. package/ui/styles.css +201 -1
  47. package/ui/tabs/dashboard.js +74 -0
  48. package/ui/tabs/library.js +410 -55
  49. package/ui/tabs/logs.js +10 -0
  50. package/ui/tabs/settings.js +178 -99
  51. package/ui/tabs/tasks.js +1083 -507
  52. package/ui/tabs/telemetry.js +34 -0
  53. package/ui/tabs/workflow-canvas-utils.mjs +38 -1
  54. package/ui/tabs/workflows.js +1275 -402
  55. package/voice/voice-agents-sdk.mjs +2 -2
  56. package/voice/voice-relay.mjs +28 -20
  57. package/workflow/declarative-workflows.mjs +145 -0
  58. package/workflow/msg-hub.mjs +237 -0
  59. package/workflow/pipeline-workflows.mjs +287 -0
  60. package/workflow/pipeline.mjs +828 -315
  61. package/workflow/project-detection.mjs +559 -0
  62. package/workflow/workflow-cli.mjs +128 -0
  63. package/workflow/workflow-contract.mjs +433 -232
  64. package/workflow/workflow-engine.mjs +510 -47
  65. package/workflow/workflow-nodes/custom-loader.mjs +251 -0
  66. package/workflow/workflow-nodes.mjs +2024 -184
  67. package/workflow/workflow-templates.mjs +118 -24
  68. package/workflow-templates/agents.mjs +20 -20
  69. package/workflow-templates/bosun-native.mjs +212 -2
  70. package/workflow-templates/code-quality.mjs +20 -14
  71. package/workflow-templates/continuation-loop.mjs +339 -0
  72. package/workflow-templates/github.mjs +516 -40
  73. package/workflow-templates/planning.mjs +446 -17
  74. package/workflow-templates/reliability.mjs +65 -12
  75. package/workflow-templates/task-batch.mjs +27 -10
  76. package/workflow-templates/task-execution.mjs +752 -0
  77. package/workflow-templates/task-lifecycle.mjs +117 -14
  78. package/workspace/context-cache.mjs +66 -18
  79. package/workspace/workspace-manager.mjs +153 -1
  80. package/workflow-templates/issue-continuation.mjs +0 -243
@@ -23,8 +23,6 @@ const SOURCE_TYPES = new Map([
23
23
 
24
24
  const SUMMARY_MARKERS = ["CLAUDE:SUMMARY", "BOSUN:SUMMARY"];
25
25
  const WARN_MARKERS = ["CLAUDE:WARN", "BOSUN:WARN"];
26
- const SUMMARY_LINE_RE = /^\s*(?:\/\/|#)\s*(?:CLAUDE|BOSUN):SUMMARY\b/i;
27
- const WARN_LINE_RE = /^\s*(?:\/\/|#)\s*(?:CLAUDE|BOSUN):WARN\b/i;
28
26
  const GENERATED_PATTERNS = [
29
27
  /^\.git(?:\/|$)/,
30
28
  /^node_modules(?:\/|$)/,
@@ -151,6 +149,101 @@ function getSourceType(pathValue) {
151
149
  return SOURCE_TYPES.get(extname(pathValue).toLowerCase()) || null;
152
150
  }
153
151
 
152
+ function parseImportSpecifiers(content, language) {
153
+ if (language !== "javascript" && language !== "typescript") return [];
154
+ const specs = new Set();
155
+ const importRegex = /import\s+(?:[^"'`]+?\s+from\s+)?["'`]([^"'`]+)["'`]/g;
156
+ const dynamicImportRegex = /import\(\s*["'`]([^"'`]+)["'`]\s*\)/g;
157
+ const requireRegex = /require\(\s*["'`]([^"'`]+)["'`]\s*\)/g;
158
+ for (const pattern of [importRegex, dynamicImportRegex, requireRegex]) {
159
+ let match;
160
+ while ((match = pattern.exec(content)) !== null) {
161
+ if (match[1]) specs.add(match[1]);
162
+ }
163
+ pattern.lastIndex = 0;
164
+ }
165
+ return [...specs];
166
+ }
167
+
168
+ function resolveLocalImportPath(repoRoot, fromFile, specifier) {
169
+ if (!specifier || !specifier.startsWith(".")) return "";
170
+ const fromDir = dirname(fromFile);
171
+ const absoluteBase = resolve(fromDir, specifier);
172
+ const candidates = [
173
+ absoluteBase,
174
+ `${absoluteBase}.js`,
175
+ `${absoluteBase}.mjs`,
176
+ `${absoluteBase}.cjs`,
177
+ `${absoluteBase}.ts`,
178
+ `${absoluteBase}.tsx`,
179
+ `${absoluteBase}.jsx`,
180
+ resolve(absoluteBase, "index.js"),
181
+ resolve(absoluteBase, "index.mjs"),
182
+ resolve(absoluteBase, "index.cjs"),
183
+ resolve(absoluteBase, "index.ts"),
184
+ resolve(absoluteBase, "index.tsx"),
185
+ resolve(absoluteBase, "index.jsx"),
186
+ ];
187
+ for (const candidate of candidates) {
188
+ const info = safeStat(candidate);
189
+ if (info?.isFile()) return toPosix(relative(repoRoot, candidate));
190
+ }
191
+ return "";
192
+ }
193
+
194
+ function findCycleMembers(edgesByFile) {
195
+ const states = new Map();
196
+ const stack = [];
197
+ const inCycles = new Set();
198
+
199
+ function visit(node) {
200
+ const state = states.get(node) || 0;
201
+ if (state === 2) return;
202
+ if (state === 1) {
203
+ const cycleStart = stack.lastIndexOf(node);
204
+ if (cycleStart >= 0) {
205
+ for (let index = cycleStart; index < stack.length; index += 1) inCycles.add(stack[index]);
206
+ inCycles.add(node);
207
+ }
208
+ return;
209
+ }
210
+ states.set(node, 1);
211
+ stack.push(node);
212
+ for (const dep of edgesByFile.get(node) || []) visit(dep);
213
+ stack.pop();
214
+ states.set(node, 2);
215
+ }
216
+
217
+ for (const node of edgesByFile.keys()) visit(node);
218
+ return inCycles;
219
+ }
220
+
221
+ function appendCircularDependencyWarnings(files, repoRoot, warningKinds) {
222
+ const sourceSet = new Set(files.map((file) => file.path));
223
+ const edgesByFile = new Map();
224
+ for (const file of files) {
225
+ if (file.language !== "javascript" && file.language !== "typescript") continue;
226
+ const deps = [];
227
+ for (const specifier of file.importSpecifiers || []) {
228
+ const resolved = resolveLocalImportPath(repoRoot, file.absolutePath, specifier);
229
+ if (resolved && sourceSet.has(resolved)) deps.push(resolved);
230
+ }
231
+ edgesByFile.set(file.path, deps);
232
+ }
233
+ const cycleMembers = findCycleMembers(edgesByFile);
234
+ for (const file of files) {
235
+ if (!cycleMembers.has(file.path)) continue;
236
+ if (file.warnings.some((warning) => warning.kind === "circular-deps")) continue;
237
+ file.warnings.push({
238
+ kind: "circular-deps",
239
+ text: "Module participates in circular dependency chains; avoid reordering imports or eager top-level side effects.",
240
+ functionName: "__module__",
241
+ lineIndex: file.firstFunctionLine,
242
+ });
243
+ warningKinds["circular-deps"] = (warningKinds["circular-deps"] || 0) + 1;
244
+ }
245
+ }
246
+
154
247
  function detectCategory(relPath) {
155
248
  if (/(^|\/)(tests?|__tests__|fixtures?|sandbox)(\/|$)|\.(test|spec)\./i.test(relPath)) return "test";
156
249
  if (/(^|\/)(config|configs)(\/|$)|(^|\/)(AGENTS|CLAUDE)\.md$/i.test(relPath)) return "config";
@@ -159,15 +252,20 @@ function detectCategory(relPath) {
159
252
  return "core";
160
253
  }
161
254
 
162
- function hasMarker(content, markers) {
163
- const matcher = markers === SUMMARY_MARKERS ? SUMMARY_LINE_RE : WARN_LINE_RE;
164
- return content.split(/\r?\n/).some((line) => matcher.test(line));
255
+ function isAnnotationLine(line, markers) {
256
+ const trimmed = String(line || "").trim();
257
+ if (!trimmed.startsWith("//") && !trimmed.startsWith("#")) return false;
258
+ return markers.some((marker) => new RegExp(`\\b${marker}\\b`).test(trimmed));
165
259
  }
166
260
 
167
- function extractAnnotationLines(content) {
261
+ function extractAnnotationLines(content, markers = SUMMARY_MARKERS) {
168
262
  const lines = content.split(/\r?\n/);
169
- const summaryLine = lines.find((line) => SUMMARY_LINE_RE.test(line)) || "";
170
- const warnLines = lines.filter((line) => WARN_LINE_RE.test(line));
263
+ return lines.filter((line) => isAnnotationLine(line, markers));
264
+ }
265
+
266
+ function collectAnnotations(content) {
267
+ const summaryLine = extractAnnotationLines(content, SUMMARY_MARKERS)[0] || "";
268
+ const warnLines = extractAnnotationLines(content, WARN_MARKERS);
171
269
  return { summaryLine, warnLines };
172
270
  }
173
271
 
@@ -210,7 +308,8 @@ export function scanRepository(rootDir, options = {}) {
210
308
  if (isGeneratedPath(relPath)) return;
211
309
 
212
310
  const content = readText(absolutePath);
213
- const annotations = extractAnnotationLines(content);
311
+ const contentLines = content.split(/\r?\n/);
312
+ const annotations = collectAnnotations(content);
214
313
  const functionMatches = findFunctionMatches(content, sourceType.language);
215
314
  const warnings = analyzeFileWarnings(content, sourceType.language, functionMatches);
216
315
  for (const warning of warnings) {
@@ -223,12 +322,14 @@ export function scanRepository(rootDir, options = {}) {
223
322
  language: sourceType.language,
224
323
  extension: extname(absolutePath).toLowerCase(),
225
324
  comment: sourceType.comment,
226
- lines: content === "" ? 0 : content.split(/\r?\n/).length,
325
+ lines: content === "" ? 0 : contentLines.length,
227
326
  category: detectCategory(relPath),
228
- hasSummary: hasMarker(content, SUMMARY_MARKERS),
229
- hasWarn: hasMarker(content, WARN_MARKERS),
327
+ hasSummary: annotations.summaryLine !== "",
328
+ hasWarn: annotations.warnLines.length > 0,
230
329
  summaryLine: annotations.summaryLine,
231
330
  warnLines: annotations.warnLines,
331
+ importSpecifiers: parseImportSpecifiers(content, sourceType.language),
332
+ firstFunctionLine: functionMatches[0]?.lineIndex ?? findInsertionIndex(contentLines),
232
333
  warnings,
233
334
  });
234
335
  }
@@ -261,6 +362,7 @@ export function scanRepository(rootDir, options = {}) {
261
362
  const relPath = toPosix(relative(repoRoot, targetDir));
262
363
  if (getSourceType(targetDir) && !isGeneratedPath(relPath)) addFile(targetDir);
263
364
  }
365
+ appendCircularDependencyWarnings(files, repoRoot, warningKinds);
264
366
 
265
367
  const result = {
266
368
  rootDir: repoRoot,
@@ -432,7 +534,7 @@ function hasNearbyWarn(lines, lineIndex) {
432
534
  const start = Math.max(0, lineIndex - 2);
433
535
  const end = Math.min(lines.length - 1, lineIndex + 1);
434
536
  for (let index = start; index <= end; index += 1) {
435
- if (WARN_LINE_RE.test(lines[index])) return true;
537
+ if (isAnnotationLine(lines[index], WARN_MARKERS)) return true;
436
538
  }
437
539
  return false;
438
540
  }
@@ -447,8 +549,9 @@ export function generateWarnings(rootDir, options = {}) {
447
549
  const inserts = [];
448
550
  for (const warning of file.warnings) {
449
551
  if (hasNearbyWarn(lines, warning.lineIndex)) continue;
552
+ const preferredIndex = Number.isInteger(warning.lineIndex) ? warning.lineIndex : findInsertionIndex(lines);
450
553
  inserts.push({
451
- index: warning.functionName ? warning.lineIndex : findInsertionIndex(lines),
554
+ index: warning.functionName ? preferredIndex : findInsertionIndex(lines),
452
555
  text: buildCommentLine(file.comment, "CLAUDE:WARN", warning.text),
453
556
  });
454
557
  }
@@ -618,7 +721,8 @@ function findStaleWarnings(content, language) {
618
721
  const matcher = patterns[language];
619
722
  if (!matcher) return stale;
620
723
  for (let index = 0; index < lines.length; index += 1) {
621
- if (!WARN_LINE_RE.test(lines[index])) continue;
724
+ if (!isAnnotationLine(lines[index], WARN_MARKERS)) continue;
725
+ if (/circular dependency/i.test(lines[index])) continue;
622
726
  const window = lines.slice(index + 1, index + 5).join("\n");
623
727
  if (!matcher.test(window)) stale.push(index + 1);
624
728
  }
@@ -666,9 +770,21 @@ export function runConformity(rootDir, options = {}) {
666
770
 
667
771
  export function migrateAnnotations(rootDir, options = {}) {
668
772
  const scan = scanRepository(rootDir, options);
773
+ const migrateLegacyAnnotationMarkers = (content) =>
774
+ content
775
+ .replace(/BOSUN:SUMMARY/g, "CLAUDE:SUMMARY")
776
+ .replace(/BOSUN:WARN/g, "CLAUDE:WARN")
777
+ .replace(
778
+ /^(\s*(?:\/\/|#)\s*)(?:LEGACY:)?SUMMARY\s*[:\-]\s*/gim,
779
+ "$1CLAUDE:SUMMARY ",
780
+ )
781
+ .replace(
782
+ /^(\s*(?:\/\/|#)\s*)(?:LEGACY:)?WARN(?:ING)?\s*[:\-]\s*/gim,
783
+ "$1CLAUDE:WARN ",
784
+ );
669
785
  const changed = updateFiles(
670
- scan.files.filter((file) => file.hasSummary || file.hasWarn),
671
- (file, content) => content.replace(/BOSUN:SUMMARY/g, "CLAUDE:SUMMARY").replace(/BOSUN:WARN/g, "CLAUDE:WARN"),
786
+ scan.files,
787
+ (file, content) => migrateLegacyAnnotationMarkers(content),
672
788
  options,
673
789
  );
674
790
  return {
@@ -804,4 +920,3 @@ export async function runAuditCli(argv, io = {}) {
804
920
  const shouldFail = command === "conformity" || Boolean(flags.ci);
805
921
  return { exitCode: shouldFail && result.ok === false ? 1 : 0, result };
806
922
  }
807
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.40.21",
3
+ "version": "0.41.1",
4
4
  "description": "Bosun Autonomous Engineering — manages AI agent executors with failover, extremely powerful workflow builder, and a massive amount of included default workflow templates for autonomous engineering, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -71,10 +71,17 @@
71
71
  "./container-runner": "./infra/container-runner.mjs",
72
72
  "./compat": "./compat.mjs",
73
73
  "./task-cli": "./task/task-cli.mjs",
74
+ "./task-pipeline": "./task/pipeline.mjs",
75
+ "./task-msg-hub": "./task/msg-hub.mjs",
76
+ "./workflow-cli": "./workflow/workflow-cli.mjs",
77
+ "./pipeline-workflows": "./workflow/pipeline-workflows.mjs",
74
78
  "./github-auth-manager": "./github/github-auth-manager.mjs",
75
79
  "./git-commit-helpers": "./git/git-commit-helpers.mjs",
76
80
  "./opencode-shell": "./shell/opencode-shell.mjs",
77
- "./context-indexer": "./workspace/context-indexer.mjs"
81
+ "./context-indexer": "./workspace/context-indexer.mjs",
82
+ "./msg-hub": "./workflow/msg-hub.mjs",
83
+ "./declarative-workflows": "./workflow/declarative-workflows.mjs",
84
+ "./pipeline": "./workflow/pipeline.mjs"
78
85
  },
79
86
  "bin": {
80
87
  "bosun": "cli.mjs",
@@ -106,8 +113,10 @@
106
113
  "pretest": "npm run syntax:check",
107
114
  "test": "node --max-old-space-size=4096 node_modules/vitest/vitest.mjs run --config vitest.config.mjs",
108
115
  "test:vitest": "node --max-old-space-size=4096 node_modules/vitest/vitest.mjs run --config vitest.config.mjs",
109
- "test:node": "node --test tests/*.node.test.mjs",
116
+ "test:node": "node --import ./tests/node-test-bootstrap.mjs --test tests/*.node.test.mjs",
110
117
  "test:all": "npm run test:vitest && npm run test:node",
118
+ "test:e2e": "npx playwright test server/playwright-ui-e2e.mjs",
119
+ "test:e2e:all": "npx playwright test server/playwright-ui-e2e.mjs server/playwright-ui-smoke.mjs",
111
120
  "test:voice-provider-smoke": "vitest run --config vitest.config.mjs tests/voice-provider-smoke.test.mjs",
112
121
  "check:native-call-parity": "vitest run --config vitest.config.mjs tests/voice-provider-smoke.test.mjs tests/native-call-parity-checklist.test.mjs",
113
122
  "test:watch": "vitest",
@@ -147,6 +156,7 @@
147
156
  "agent/agent-custom-tools.mjs",
148
157
  "agent/agent-endpoint.mjs",
149
158
  "agent/agent-event-bus.mjs",
159
+ "agent/retry-queue.mjs",
150
160
  "agent/agent-pool.mjs",
151
161
  "agent/agent-prompts.mjs",
152
162
  "agent/agent-sdk.mjs",
@@ -200,7 +210,8 @@
200
210
  "infra/library-manager.mjs",
201
211
  "infra/maintenance.mjs",
202
212
  "workflow/manual-flows.mjs",
203
- "workflow-templates/code-quality.mjs",
213
+ "workflow/pipeline-workflows.mjs",
214
+ "workflow/workflow-cli.mjs",
204
215
  "workflow/mcp-discovery-proxy.mjs",
205
216
  "workflow/mcp-workflow-adapter.mjs",
206
217
  "workflow/mcp-registry.mjs",
@@ -222,6 +233,7 @@
222
233
  "agent/review-agent.mjs",
223
234
  "git/sdk-conflict-resolver.mjs",
224
235
  "infra/session-tracker.mjs",
236
+ "infra/test-runtime.mjs",
225
237
  "setup.mjs",
226
238
  "server/setup-web-server.mjs",
227
239
  "shell/pwsh-runtime.mjs",
@@ -241,7 +253,10 @@
241
253
  "task/task-claims.mjs",
242
254
  "task/task-context.mjs",
243
255
  "task/task-attachments.mjs",
256
+ "task/pipeline.mjs",
257
+ "task/msg-hub.mjs",
244
258
  "task/task-executor.mjs",
259
+ "task/task-executor-pipeline.mjs",
245
260
  "task/task-store.mjs",
246
261
  "telegram/telegram-bot.mjs",
247
262
  "server/ui-server.mjs",
@@ -289,6 +304,8 @@
289
304
  "workflow/workflow-engine.mjs",
290
305
  "workflow/workflow-migration.mjs",
291
306
  "workflow/workflow-nodes.mjs",
307
+ "workflow/workflow-nodes/custom-loader.mjs",
308
+ "workflow/project-detection.mjs",
292
309
  "workflow/workflow-templates.mjs",
293
310
  "workflow/workflow-contract.mjs",
294
311
  "workflow/pipeline.mjs",
@@ -303,13 +320,18 @@
303
320
  "workflow-templates/research.mjs",
304
321
  "workflow-templates/security.mjs",
305
322
  "workflow-templates/task-batch.mjs",
323
+ "workflow-templates/task-execution.mjs",
306
324
  "workflow-templates/task-lifecycle.mjs",
307
325
  "workflow-templates/issue-continuation.mjs",
326
+ "workflow-templates/continuation-loop.mjs",
327
+ "workflow-templates/code-quality.mjs",
308
328
  "workflow-templates/bosun-native.mjs",
309
329
  "bosun-tui.mjs",
310
330
  "tui/",
311
331
  "tools/",
312
- "ui/vendor/"
332
+ "ui/vendor/",
333
+ "workflow/msg-hub.mjs",
334
+ "workflow/declarative-workflows.mjs"
313
335
  ],
314
336
  "dependencies": {
315
337
  "@anthropic-ai/claude-agent-sdk": "latest",
@@ -327,12 +349,12 @@
327
349
  "express-rate-limit": "^8.0.0",
328
350
  "hono": "^4.12.7",
329
351
  "htm": "3.1.1",
352
+ "ink": "^5.0.0",
353
+ "ink-text-input": "^6.0.0",
330
354
  "preact": "10.25.4",
331
355
  "qrcode-terminal": "^0.12.0",
332
356
  "vibe-kanban": "latest",
333
- "ws": "^8.19.0",
334
- "ink": "^5.0.0",
335
- "ink-text-input": "^6.0.0"
357
+ "ws": "^8.19.0"
336
358
  },
337
359
  "devDependencies": {
338
360
  "@emotion/react": "^11.14.0",
@@ -19,6 +19,7 @@ import { fileURLToPath } from "node:url";
19
19
  import { createRequire } from "node:module";
20
20
  import { execSync } from "node:child_process";
21
21
  import { homedir } from "node:os";
22
+ import { ensureTestRuntimeSandbox } from "../infra/test-runtime.mjs";
22
23
  import { scaffoldSkills } from "../agent/bosun-skills.mjs";
23
24
  import { ensureCodexConfig, ensureTrustedProjects } from "../shell/codex-config.mjs";
24
25
  import {
@@ -28,6 +29,7 @@ import {
28
29
  resolveWorkflowTemplateIds,
29
30
  normalizeTemplateOverridesById,
30
31
  } from "../workflow/workflow-templates.mjs";
32
+ import { discoverTelegramChats } from "../telegram/get-telegram-chat-id.mjs";
31
33
 
32
34
  const __dirname = dirname(fileURLToPath(import.meta.url));
33
35
 
@@ -1420,6 +1422,9 @@ function resolveConfigDir() {
1420
1422
  if (isExpectedHome) return cwd;
1421
1423
  }
1422
1424
 
1425
+ const sandbox = ensureTestRuntimeSandbox();
1426
+ if (sandbox?.configDir) return sandbox.configDir;
1427
+
1423
1428
  const preferWindowsDirs =
1424
1429
  process.platform === "win32" && !isWslInteropRuntime();
1425
1430
  const baseDir = preferWindowsDirs
@@ -1977,6 +1982,20 @@ function handleValidate(body) {
1977
1982
  return { ok: true, valid: Object.keys(errors).length === 0, errors };
1978
1983
  }
1979
1984
 
1985
+ async function handleTelegramChatIdLookup(body) {
1986
+ const token = String(body?.token || "").trim();
1987
+ if (!token) {
1988
+ return { ok: false, status: 400, error: "TELEGRAM_BOT_TOKEN is required" };
1989
+ }
1990
+
1991
+ try {
1992
+ const { chats, message } = await discoverTelegramChats(token);
1993
+ return { ok: true, status: 200, chats, message };
1994
+ } catch (err) {
1995
+ return { ok: false, status: 500, error: err.message || String(err) };
1996
+ }
1997
+ }
1998
+
1980
1999
  function handleApply(body) {
1981
2000
  try {
1982
2001
  const { env = {}, configJson = {} } = body || {};
@@ -2575,6 +2594,15 @@ async function handleRequest(req, res) {
2575
2594
  }
2576
2595
  jsonResponse(res, 200, handleValidate(await readBody(req)));
2577
2596
  return;
2597
+ case "telegram-chat-id": {
2598
+ if (req.method !== "POST") {
2599
+ jsonResponse(res, 405, { ok: false, error: "POST required" });
2600
+ return;
2601
+ }
2602
+ const result = await handleTelegramChatIdLookup(await readBody(req));
2603
+ jsonResponse(res, result.status, result);
2604
+ return;
2605
+ }
2578
2606
  case "apply":
2579
2607
  if (req.method !== "POST") {
2580
2608
  jsonResponse(res, 405, { ok: false, error: "POST required" });
@@ -2980,6 +3008,7 @@ export async function startSetupServer(options = {}) {
2980
3008
  export {
2981
3009
  applyTelegramMiniAppSetupEnv,
2982
3010
  applyNonBlockingSetupEnvDefaults,
3011
+ handleTelegramChatIdLookup,
2983
3012
  normalizeWorkflowTemplateOverrides,
2984
3013
  normalizeTelegramUiPort,
2985
3014
  normalizeRepoConfigEntry,
@@ -2995,4 +3024,3 @@ if (process.argv[1] && resolve(process.argv[1]) === resolve(__filename_setup_web
2995
3024
  process.exit(1);
2996
3025
  });
2997
3026
  }
2998
-