squadrant 0.9.2 → 0.10.0

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.
@@ -185,22 +185,22 @@ var MINIMAL_TEMPLATE = [
185
185
  ``
186
186
  ].join("\n");
187
187
  function ensureSocketAutomation(opts = {}) {
188
- const path13 = opts.path ?? defaultCmuxConfigPath();
189
- if (!existsSync(path13)) {
190
- mkdirSync(dirname(path13), { recursive: true });
191
- writeFileSync(path13, MINIMAL_TEMPLATE);
192
- return { path: path13, changed: true, alreadySet: false };
188
+ const path14 = opts.path ?? defaultCmuxConfigPath();
189
+ if (!existsSync(path14)) {
190
+ mkdirSync(dirname(path14), { recursive: true });
191
+ writeFileSync(path14, MINIMAL_TEMPLATE);
192
+ return { path: path14, changed: true, alreadySet: false };
193
193
  }
194
- const text = readFileSync(path13, "utf-8");
194
+ const text = readFileSync(path14, "utf-8");
195
195
  const current = parse(text)?.automation?.socketControlMode;
196
196
  if (current === AUTOMATION_MODE) {
197
- return { path: path13, changed: false, alreadySet: true };
197
+ return { path: path14, changed: false, alreadySet: true };
198
198
  }
199
199
  const edits = modify(text, [...SOCKET_CONTROL_MODE_PATH], AUTOMATION_MODE, {
200
200
  formattingOptions: { insertSpaces: true, tabSize: 2 }
201
201
  });
202
- writeFileSync(path13, applyEdits(text, edits));
203
- return { path: path13, changed: true, alreadySet: false };
202
+ writeFileSync(path14, applyEdits(text, edits));
203
+ return { path: path14, changed: true, alreadySet: false };
204
204
  }
205
205
 
206
206
  // packages/shared/dist/lib/cmux-probe.js
@@ -322,15 +322,15 @@ function sleep(ms) {
322
322
  function defaultStatePath() {
323
323
  return join4(homedir3(), ".config", "squadrant", "state", "cmux-autoconfig.json");
324
324
  }
325
- function readState(path13) {
325
+ function readState(path14) {
326
326
  try {
327
- return JSON.parse(readFileSync4(path13, "utf-8"));
327
+ return JSON.parse(readFileSync4(path14, "utf-8"));
328
328
  } catch {
329
329
  return {};
330
330
  }
331
331
  }
332
332
  async function ensureCmuxAutoConfig(opts = {}) {
333
- const statePath = opts.statePath ?? defaultStatePath();
333
+ const statePath2 = opts.statePath ?? defaultStatePath();
334
334
  const ensureConfig = opts.ensureConfig ?? ensureSocketAutomation;
335
335
  const probe = opts.probe ?? probeCmuxDaemonDirect;
336
336
  const cfg = ensureConfig({ path: opts.configPath });
@@ -338,15 +338,15 @@ async function ensureCmuxAutoConfig(opts = {}) {
338
338
  const needsRestart = verdict === "denied";
339
339
  let promptedThisRun = false;
340
340
  if (needsRestart) {
341
- const already = readState(statePath).promptedRestart === true;
341
+ const already = readState(statePath2).promptedRestart === true;
342
342
  if (!already) {
343
- mkdirSync2(dirname2(statePath), { recursive: true });
344
- writeFileSync3(statePath, JSON.stringify({ promptedRestart: true }));
343
+ mkdirSync2(dirname2(statePath2), { recursive: true });
344
+ writeFileSync3(statePath2, JSON.stringify({ promptedRestart: true }));
345
345
  promptedThisRun = true;
346
346
  }
347
347
  } else if (verdict === "reachable") {
348
- if (existsSync4(statePath))
349
- rmSync2(statePath, { force: true });
348
+ if (existsSync4(statePath2))
349
+ rmSync2(statePath2, { force: true });
350
350
  }
351
351
  return {
352
352
  configPath: cfg.path,
@@ -936,27 +936,39 @@ function withProjectLock(project, fn) {
936
936
  projectLocks.set(project, next.catch(() => void 0));
937
937
  return next;
938
938
  }
939
- async function appendToMailbox(opts) {
940
- return withProjectLock(opts.project, async () => {
941
- const dir = inboxDir(opts.stateRoot);
939
+ function appendEntry(stateRoot, project, build) {
940
+ return withProjectLock(project, async () => {
941
+ const dir = inboxDir(stateRoot);
942
942
  await fs6.mkdir(dir, { recursive: true });
943
- const file = logPath(opts.stateRoot, opts.project);
944
- const lastSeq = await readMaxSeq(opts.stateRoot, opts.project);
943
+ const file = logPath(stateRoot, project);
944
+ const lastSeq = await readMaxSeq(stateRoot, project);
945
945
  const seq = lastSeq + 1;
946
- const entry = {
947
- seq,
948
- ts: (/* @__PURE__ */ new Date()).toISOString(),
949
- taskId: opts.taskRecord.id,
950
- ...opts.taskRecord.name !== void 0 ? { name: opts.taskRecord.name } : {},
951
- kind: opts.event.type,
952
- provider: opts.taskRecord.provider,
953
- payload: extractPayload(opts.event),
954
- message: opts.message ?? null
955
- };
946
+ const entry = build(seq);
956
947
  await fs6.appendFile(file, JSON.stringify(entry) + "\n", { encoding: "utf-8" });
957
948
  return seq;
958
949
  });
959
950
  }
951
+ async function appendToMailbox(opts) {
952
+ return appendEntry(opts.stateRoot, opts.project, (seq) => ({
953
+ seq,
954
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
955
+ taskId: opts.taskRecord.id,
956
+ ...opts.taskRecord.name !== void 0 ? { name: opts.taskRecord.name } : {},
957
+ kind: opts.event.type,
958
+ provider: opts.taskRecord.provider,
959
+ payload: extractPayload(opts.event),
960
+ message: opts.message ?? null
961
+ }));
962
+ }
963
+ async function appendCaptainMessage(opts) {
964
+ await appendEntry(opts.stateRoot, opts.project, (seq) => ({
965
+ seq,
966
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
967
+ kind: "captain.message",
968
+ payload: { source: opts.source },
969
+ message: opts.text
970
+ }));
971
+ }
960
972
  function cursorPath(stateRoot, project, subscriber) {
961
973
  return join5(inboxDir(stateRoot), `${project}.${subscriber}.cursor`);
962
974
  }
@@ -1241,6 +1253,36 @@ function isDaemonSocketLive(sockPath, timeoutMs = 500) {
1241
1253
  });
1242
1254
  });
1243
1255
  }
1256
+ function sendRequest(sockPath, msg, timeoutMs = 5e3) {
1257
+ return new Promise((resolve2, reject) => {
1258
+ const conn = createConnection(sockPath);
1259
+ const dec = createDecoder();
1260
+ const timer = setTimeout(() => {
1261
+ conn.destroy();
1262
+ reject(new Error("control plane unavailable: request timed out"));
1263
+ }, timeoutMs);
1264
+ conn.setEncoding("utf-8");
1265
+ conn.on("connect", () => conn.write(encodeMsg({ ...msg, _v: PROTOCOL_VERSION })));
1266
+ conn.on("data", (chunk) => {
1267
+ for (const m of dec.push(chunk)) {
1268
+ clearTimeout(timer);
1269
+ conn.destroy();
1270
+ if (m._v !== void 0 && m._v !== PROTOCOL_VERSION) {
1271
+ reject(new Error(`squadrantd protocol v${m._v}, this client expects v${PROTOCOL_VERSION} \u2014 upgrade squadrantd or this CLI`));
1272
+ } else if (m.ok) {
1273
+ resolve2(m.reply);
1274
+ } else {
1275
+ reject(new Error(m.error));
1276
+ }
1277
+ return;
1278
+ }
1279
+ });
1280
+ conn.on("error", () => {
1281
+ clearTimeout(timer);
1282
+ reject(new Error("control plane unavailable: cannot reach squadrantd socket"));
1283
+ });
1284
+ });
1285
+ }
1244
1286
  function encodeFrame(f) {
1245
1287
  return JSON.stringify(f) + "\n";
1246
1288
  }
@@ -1508,6 +1550,7 @@ function buildContext(opts) {
1508
1550
  codexDriver: null,
1509
1551
  opencodeBridge: null,
1510
1552
  cmuxEventsBridge: null,
1553
+ telegramBridge: void 0,
1511
1554
  broadcast: () => {
1512
1555
  },
1513
1556
  schedulePromotion: () => {
@@ -2035,10 +2078,10 @@ function distBuiltAt() {
2035
2078
  return 0;
2036
2079
  }
2037
2080
  }
2038
- function gatherLogStats(path13, now, windowMs) {
2081
+ function gatherLogStats(path14, now, windowMs) {
2039
2082
  let sizeBytes = 0;
2040
2083
  try {
2041
- sizeBytes = statSync2(path13).size;
2084
+ sizeBytes = statSync2(path14).size;
2042
2085
  } catch {
2043
2086
  return { errorCount: 0, sizeBytes: 0, windowMs };
2044
2087
  }
@@ -2049,7 +2092,7 @@ function gatherLogStats(path13, now, windowMs) {
2049
2092
  const len = sizeBytes - start;
2050
2093
  let text = "";
2051
2094
  try {
2052
- const fd = openSync2(path13, "r");
2095
+ const fd = openSync2(path14, "r");
2053
2096
  try {
2054
2097
  const buf = Buffer.alloc(len);
2055
2098
  readSync(fd, buf, 0, len, start);
@@ -2126,7 +2169,11 @@ function startDaemon(ctx, opts, pkgVersion) {
2126
2169
  const { daemonCmux } = ctx;
2127
2170
  const probes = createProbes(ctx);
2128
2171
  const { defaultNotify, deliveryTick: initialDeliveryTick } = createDelivery(ctx, daemonCmux);
2129
- const notify = opts.notify ?? defaultNotify;
2172
+ const baseNotify = opts.notify ?? defaultNotify;
2173
+ const notify = ctx.telegramBridge ? async (args) => {
2174
+ await baseNotify(args);
2175
+ ctx.telegramBridge.pushLifecycle(args.project, args.event);
2176
+ } : baseNotify;
2130
2177
  const surfaceProbe = buildSurfaceProbe(ctx, probes, daemonCmux);
2131
2178
  const ingest = (project) => (e) => void ctx.d.handle({ kind: "event", project, event: e });
2132
2179
  const d = createDaemon({
@@ -2252,6 +2299,13 @@ function startDaemon(ctx, opts, pkgVersion) {
2252
2299
  log(`cmux events bridge start failed: ${e.message}`);
2253
2300
  }
2254
2301
  }
2302
+ if (ctx.telegramBridge) {
2303
+ try {
2304
+ ctx.telegramBridge.start();
2305
+ } catch (e) {
2306
+ log(`telegram bridge start failed: ${e.message}`);
2307
+ }
2308
+ }
2255
2309
  const autoConfigSafe = !!opts.runCmuxAutoConfig || !process.env.VITEST;
2256
2310
  if (autoConfigSafe) {
2257
2311
  try {
@@ -2341,6 +2395,10 @@ function startDaemon(ctx, opts, pkgVersion) {
2341
2395
  ctx.cmuxEventsBridge.stop();
2342
2396
  } catch {
2343
2397
  }
2398
+ try {
2399
+ ctx.telegramBridge?.stop();
2400
+ } catch {
2401
+ }
2344
2402
  try {
2345
2403
  ctx.codexDriver.stop?.();
2346
2404
  } catch {
@@ -2365,6 +2423,404 @@ import path6 from "path";
2365
2423
  // packages/core/dist/crew-lifecycle.js
2366
2424
  import { exec as nodeExec } from "child_process";
2367
2425
 
2426
+ // packages/core/dist/telegram/auth.js
2427
+ function isControlEnabled(cfg) {
2428
+ return cfg.remoteControl === true;
2429
+ }
2430
+ function isAuthorized(fromId, cfg) {
2431
+ if (fromId === void 0)
2432
+ return false;
2433
+ return Array.isArray(cfg.users) && cfg.users.includes(fromId);
2434
+ }
2435
+
2436
+ // packages/core/dist/telegram/commands.js
2437
+ var WRITABLE_CONFIG_KEYS = ["defaults.effort"];
2438
+ var EFFORT_MODES = /* @__PURE__ */ new Set(["max", "balance", "low"]);
2439
+ function ok(name, argv) {
2440
+ return { kind: "ok", name, argv };
2441
+ }
2442
+ function usage(name, message) {
2443
+ return { kind: "usage", name, message };
2444
+ }
2445
+ var REGISTRY = {
2446
+ status: { usage: "/status", build: () => ok("status", ["status"]) },
2447
+ projects: { usage: "/projects", build: () => ok("projects", ["projects", "list"]) },
2448
+ crews: {
2449
+ usage: "/crews <project>",
2450
+ build: (a) => a[0] ? ok("crews", ["crew", "list", a[0]]) : usage("crews", "usage: /crews <project>")
2451
+ },
2452
+ launch: {
2453
+ usage: "/launch <project>",
2454
+ build: (a) => a[0] ? ok("launch", ["launch", a[0]]) : usage("launch", "usage: /launch <project>")
2455
+ },
2456
+ effort: {
2457
+ usage: "/effort [max|balance|low]",
2458
+ build: (a) => {
2459
+ if (a.length === 0)
2460
+ return ok("effort", ["effort"]);
2461
+ if (!EFFORT_MODES.has(a[0]))
2462
+ return usage("effort", "usage: /effort [max|balance|low]");
2463
+ return ok("effort", ["effort", a[0]]);
2464
+ }
2465
+ },
2466
+ config: {
2467
+ usage: "/config get <key> | /config set <key> <value>",
2468
+ build: (a) => {
2469
+ const sub = a[0];
2470
+ if (sub === "get") {
2471
+ const key = a[1];
2472
+ if (!key)
2473
+ return usage("config", "usage: /config get <key>");
2474
+ return ok("config", ["config", "get", key]);
2475
+ }
2476
+ if (sub === "set") {
2477
+ const key = a[1];
2478
+ const value = a.slice(2).join(" ");
2479
+ if (!key || value === "")
2480
+ return usage("config", "usage: /config set <key> <value>");
2481
+ if (!WRITABLE_CONFIG_KEYS.includes(key)) {
2482
+ return {
2483
+ kind: "denied",
2484
+ message: `\u26D4 '${key}' is not writable over Telegram. Allowed: ${WRITABLE_CONFIG_KEYS.join(", ")}`
2485
+ };
2486
+ }
2487
+ return ok("config", ["config", "set", key, value]);
2488
+ }
2489
+ return usage("config", "usage: /config get <key> | /config set <key> <value>");
2490
+ }
2491
+ },
2492
+ spawn: {
2493
+ usage: "/spawn <project> <task...>",
2494
+ build: (a) => {
2495
+ const project = a[0];
2496
+ const task = a.slice(1).join(" ");
2497
+ if (!project || task === "")
2498
+ return usage("spawn", "usage: /spawn <project> <task...>");
2499
+ return ok("spawn", ["crew", "spawn", project, task]);
2500
+ }
2501
+ }
2502
+ };
2503
+ function helpText() {
2504
+ const lines = Object.values(REGISTRY).map((e) => ` ${e.usage}`);
2505
+ return ["Available commands:", ...lines, " /help"].join("\n");
2506
+ }
2507
+ function parseCommand(text) {
2508
+ const trimmed = text.trim();
2509
+ if (!trimmed.startsWith("/")) {
2510
+ return { kind: "unknown", message: "unknown command \u2014 send /help" };
2511
+ }
2512
+ const tokens = trimmed.slice(1).split(/\s+/).filter((t) => t.length > 0);
2513
+ const name = (tokens[0] ?? "").toLowerCase();
2514
+ const args = tokens.slice(1);
2515
+ if (name === "help") {
2516
+ return { kind: "usage", name: "help", message: helpText() };
2517
+ }
2518
+ const entry = REGISTRY[name];
2519
+ if (!entry) {
2520
+ return { kind: "unknown", message: `unknown command '/${name}' \u2014 send /help` };
2521
+ }
2522
+ return entry.build(args);
2523
+ }
2524
+
2525
+ // packages/core/dist/telegram/ensure-captain.js
2526
+ var DEFAULT_WARMUP_TIMEOUT_MS = 12e4;
2527
+ var DEFAULT_POLL_MS = 1e3;
2528
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
2529
+ function createEnsureCaptainAlive(deps) {
2530
+ const warmupTimeoutMs = deps.warmupTimeoutMs ?? DEFAULT_WARMUP_TIMEOUT_MS;
2531
+ const pollMs = deps.pollMs ?? DEFAULT_POLL_MS;
2532
+ const sleep3 = deps.sleep ?? defaultSleep;
2533
+ const now = deps.now ?? (() => Date.now());
2534
+ const inFlight = /* @__PURE__ */ new Map();
2535
+ async function run(project) {
2536
+ if (await deps.isAlive(project))
2537
+ return "alive";
2538
+ await deps.launch(project);
2539
+ const deadline = now() + warmupTimeoutMs;
2540
+ while (now() < deadline) {
2541
+ if (await deps.isAlive(project))
2542
+ return "launched";
2543
+ await sleep3(pollMs);
2544
+ }
2545
+ return "timeout";
2546
+ }
2547
+ return function ensure(project) {
2548
+ const existing = inFlight.get(project);
2549
+ if (existing)
2550
+ return existing;
2551
+ const p = run(project).finally(() => inFlight.delete(project));
2552
+ inFlight.set(project, p);
2553
+ return p;
2554
+ };
2555
+ }
2556
+
2557
+ // packages/core/dist/telegram/format.js
2558
+ function topicName(project) {
2559
+ return project;
2560
+ }
2561
+ function formatLifecycle(project, ev) {
2562
+ switch (ev.type) {
2563
+ case "task.done":
2564
+ return `\u2705 [${project}] CREW DONE \xB7 ${ev.id}` + (ev.message ? `
2565
+ ${ev.message}` : "");
2566
+ case "task.blocked":
2567
+ return `\u{1F6A7} [${project}] CREW BLOCKED \xB7 ${ev.id}
2568
+ ${ev.question}`;
2569
+ case "task.idle":
2570
+ return `\u{1F4A4} [${project}] CREW IDLE \xB7 ${ev.id}`;
2571
+ default:
2572
+ return `\u2139\uFE0F [${project}] ${ev.type} \xB7 ${ev.id}`;
2573
+ }
2574
+ }
2575
+ function formatInbound(text) {
2576
+ return `\u{1F4E9} [from Telegram] ${text}`;
2577
+ }
2578
+
2579
+ // packages/core/dist/telegram/state.js
2580
+ import fs8 from "fs";
2581
+ import path7 from "path";
2582
+ function statePath(stateRoot) {
2583
+ return path7.join(stateRoot, "telegram-state.json");
2584
+ }
2585
+ function topicKey(project, scope = "project") {
2586
+ return `${project}::${scope}`;
2587
+ }
2588
+ function loadState(stateRoot) {
2589
+ try {
2590
+ const raw = fs8.readFileSync(statePath(stateRoot), "utf-8");
2591
+ const data = JSON.parse(raw);
2592
+ return {
2593
+ offset: typeof data.offset === "number" ? data.offset : 0,
2594
+ topics: data.topics ?? {}
2595
+ };
2596
+ } catch {
2597
+ return { offset: 0, topics: {} };
2598
+ }
2599
+ }
2600
+ function saveState(stateRoot, s) {
2601
+ fs8.mkdirSync(stateRoot, { recursive: true });
2602
+ fs8.writeFileSync(statePath(stateRoot), JSON.stringify(s, null, 2) + "\n");
2603
+ }
2604
+ function setTopic(stateRoot, project, topicId, scope = "project") {
2605
+ const s = loadState(stateRoot);
2606
+ s.topics[topicKey(project, scope)] = topicId;
2607
+ saveState(stateRoot, s);
2608
+ }
2609
+ function findProjectByThread(stateRoot, threadId) {
2610
+ const s = loadState(stateRoot);
2611
+ for (const [key, id] of Object.entries(s.topics)) {
2612
+ if (id !== threadId)
2613
+ continue;
2614
+ const sep2 = key.indexOf("::");
2615
+ if (sep2 === -1)
2616
+ continue;
2617
+ return { project: key.slice(0, sep2), scope: key.slice(sep2 + 2) };
2618
+ }
2619
+ return null;
2620
+ }
2621
+
2622
+ // packages/core/dist/telegram/client.js
2623
+ function createTelegramClient(opts) {
2624
+ const fetchImpl = opts.fetch ?? fetch;
2625
+ const base = `https://api.telegram.org/bot${opts.token}`;
2626
+ async function call(method, body) {
2627
+ const res = await fetchImpl(`${base}/${method}`, {
2628
+ method: "POST",
2629
+ headers: { "content-type": "application/json" },
2630
+ body: JSON.stringify(body)
2631
+ });
2632
+ const json = await res.json();
2633
+ if (!res.ok || !json.ok) {
2634
+ const code = json.error_code ?? res.status;
2635
+ const desc = json.description ?? "unknown error";
2636
+ throw new Error(`telegram ${method} failed (${code}): ${desc}`);
2637
+ }
2638
+ return json.result;
2639
+ }
2640
+ return {
2641
+ async getMe() {
2642
+ const r = await call("getMe", {});
2643
+ return { id: r.id, username: r.username };
2644
+ },
2645
+ getUpdates(offset, timeoutSec = 50) {
2646
+ return call("getUpdates", { offset, timeout: timeoutSec });
2647
+ },
2648
+ async sendMessage(chatId, threadId, text) {
2649
+ const body = { chat_id: chatId, text };
2650
+ if (threadId !== void 0)
2651
+ body.message_thread_id = threadId;
2652
+ await call("sendMessage", body);
2653
+ },
2654
+ async createForumTopic(chatId, name) {
2655
+ const r = await call("createForumTopic", { chat_id: chatId, name });
2656
+ return r.message_thread_id;
2657
+ }
2658
+ };
2659
+ }
2660
+
2661
+ // packages/core/dist/telegram/bridge.js
2662
+ var LONG_POLL_SEC = 50;
2663
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
2664
+ function createTelegramBridge(opts) {
2665
+ const { cfg, stateRoot, client, appendCaptainMessage: appendCaptainMessage2, log, ensureCaptainAlive, runCommand, sendReply } = opts;
2666
+ const pollMs = cfg.pollMs ?? 1e3;
2667
+ let running = false;
2668
+ function persistOffset(next) {
2669
+ const s = loadState(stateRoot);
2670
+ s.offset = next;
2671
+ saveState(stateRoot, s);
2672
+ }
2673
+ async function deliverOutbound(project, ev) {
2674
+ let threadId = loadState(stateRoot).topics[topicKey(project)];
2675
+ if (threadId === void 0) {
2676
+ threadId = await client.createForumTopic(cfg.supergroupId, topicName(project));
2677
+ setTopic(stateRoot, project, threadId);
2678
+ }
2679
+ await client.sendMessage(cfg.supergroupId, threadId, formatLifecycle(project, ev));
2680
+ }
2681
+ async function reply(threadId, text) {
2682
+ if (!sendReply)
2683
+ return;
2684
+ try {
2685
+ await sendReply(threadId, text);
2686
+ } catch (e) {
2687
+ log(`telegram reply failed: ${e.message}`);
2688
+ }
2689
+ }
2690
+ async function handleGeneral(text, fromId) {
2691
+ if (!text.startsWith("/")) {
2692
+ await reply(void 0, "Send /help for commands.");
2693
+ return;
2694
+ }
2695
+ if (!isControlEnabled(cfg) || !isAuthorized(fromId, cfg)) {
2696
+ await reply(void 0, "\u26D4 not authorized");
2697
+ return;
2698
+ }
2699
+ const parsed = parseCommand(text);
2700
+ if (parsed.kind !== "ok") {
2701
+ await reply(void 0, parsed.message);
2702
+ return;
2703
+ }
2704
+ try {
2705
+ const out = runCommand ? await runCommand(parsed.argv) : "(command runner unavailable)";
2706
+ await reply(void 0, out);
2707
+ } catch (e) {
2708
+ await reply(void 0, `\u26A0\uFE0F command failed: ${e.message}`);
2709
+ log(`telegram command failed argv=${JSON.stringify(parsed.argv)}: ${e.message}`);
2710
+ }
2711
+ }
2712
+ async function handleProjectTopic(text, threadId, fromId) {
2713
+ const resolved = findProjectByThread(stateRoot, threadId);
2714
+ if (!resolved)
2715
+ return;
2716
+ if (ensureCaptainAlive && isControlEnabled(cfg) && isAuthorized(fromId, cfg)) {
2717
+ try {
2718
+ const r = await ensureCaptainAlive(resolved.project);
2719
+ if (r === "timeout")
2720
+ await reply(threadId, "\u26A0\uFE0F captain didn't warm up; message queued.");
2721
+ } catch (e) {
2722
+ log(`telegram auto-launch failed project=${resolved.project}: ${e.message}`);
2723
+ }
2724
+ }
2725
+ await appendCaptainMessage2({ stateRoot, project: resolved.project, text: formatInbound(text), source: "telegram" });
2726
+ }
2727
+ async function handleUpdate(u) {
2728
+ const m = u.message;
2729
+ if (!m || m.text === void 0)
2730
+ return;
2731
+ if (!cfg.chats.includes(m.chat.id))
2732
+ return;
2733
+ if (m.message_thread_id === void 0) {
2734
+ await handleGeneral(m.text, m.from?.id);
2735
+ return;
2736
+ }
2737
+ await handleProjectTopic(m.text, m.message_thread_id, m.from?.id);
2738
+ }
2739
+ async function pollLoop() {
2740
+ while (running) {
2741
+ try {
2742
+ const offset = loadState(stateRoot).offset;
2743
+ const updates = await client.getUpdates(offset, LONG_POLL_SEC);
2744
+ for (const u of updates) {
2745
+ await handleUpdate(u);
2746
+ persistOffset(u.update_id + 1);
2747
+ }
2748
+ } catch (e) {
2749
+ log(`telegram inbound poll failed: ${e.message}`);
2750
+ }
2751
+ if (running)
2752
+ await sleep2(pollMs);
2753
+ }
2754
+ }
2755
+ return {
2756
+ start() {
2757
+ if (running)
2758
+ return;
2759
+ running = true;
2760
+ void pollLoop();
2761
+ },
2762
+ stop() {
2763
+ running = false;
2764
+ },
2765
+ pushLifecycle(project, ev) {
2766
+ void deliverOutbound(project, ev).catch((e) => {
2767
+ log(`telegram outbound failed project=${project}: ${e.message}`);
2768
+ });
2769
+ }
2770
+ };
2771
+ }
2772
+
2773
+ // packages/core/dist/telegram/setup.js
2774
+ import fs9 from "fs";
2775
+
2776
+ // packages/cli/src/control/telegram-control.ts
2777
+ import { execFile } from "child_process";
2778
+ import { promisify } from "util";
2779
+ var pExecFile = promisify(execFile);
2780
+ var MAX_OUTPUT = 3500;
2781
+ function capOutput(stdout, stderr, max = MAX_OUTPUT) {
2782
+ const out = stdout.trim();
2783
+ const err = stderr.trim();
2784
+ let combined = out;
2785
+ if (err) combined = combined ? `${combined}
2786
+ [stderr] ${err}` : `[stderr] ${err}`;
2787
+ if (!combined) combined = "(no output)";
2788
+ if (combined.length > max) combined = combined.slice(0, max) + "\n\u2026[truncated]";
2789
+ return combined;
2790
+ }
2791
+ var COMMAND_TIMEOUT_MS = 6e4;
2792
+ function createRunCommand(cliBin) {
2793
+ return async (argv) => {
2794
+ try {
2795
+ const { stdout, stderr } = await pExecFile(
2796
+ process.execPath,
2797
+ [cliBin, ...argv],
2798
+ { timeout: COMMAND_TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024 }
2799
+ );
2800
+ return capOutput(stdout ?? "", stderr ?? "");
2801
+ } catch (e) {
2802
+ const err = e;
2803
+ return capOutput(err.stdout ?? "", err.stderr ?? err.message ?? "command failed");
2804
+ }
2805
+ };
2806
+ }
2807
+ function createIsCaptainAlive(sock) {
2808
+ return async (project) => {
2809
+ try {
2810
+ const health = await sendRequest(sock, { kind: "health", project }, 5e3);
2811
+ const captain = health?.find((h) => h.kind === "captain" && h.project === project);
2812
+ return captain != null && captain.state !== "gone" && captain.state !== "unknown";
2813
+ } catch {
2814
+ return false;
2815
+ }
2816
+ };
2817
+ }
2818
+ function createLaunch(cliBin) {
2819
+ return async (project) => {
2820
+ await pExecFile(process.execPath, [cliBin, "launch", project], { timeout: 3e4 });
2821
+ };
2822
+ }
2823
+
2368
2824
  // packages/agents/dist/drivers/claude.js
2369
2825
  import { execSync as execSync2 } from "child_process";
2370
2826
 
@@ -2378,27 +2834,27 @@ import { execSync as execSync4 } from "child_process";
2378
2834
  import { execSync as execSync5 } from "child_process";
2379
2835
 
2380
2836
  // packages/agents/dist/drivers/launch-cmd.js
2381
- import fs8 from "fs";
2382
- import path7 from "path";
2837
+ import fs10 from "fs";
2838
+ import path8 from "path";
2383
2839
 
2384
2840
  // packages/agents/dist/projection/cursor.js
2385
2841
  import { mkdir, readFile, writeFile } from "fs/promises";
2386
- import path8 from "path";
2842
+ import path9 from "path";
2387
2843
  import os2 from "os";
2388
2844
 
2389
2845
  // packages/agents/dist/projection/codex.js
2390
2846
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
2391
- import path9 from "path";
2847
+ import path10 from "path";
2392
2848
  import os3 from "os";
2393
2849
 
2394
2850
  // packages/agents/dist/projection/gemini.js
2395
2851
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
2396
- import path10 from "path";
2852
+ import path11 from "path";
2397
2853
  import os4 from "os";
2398
2854
 
2399
2855
  // packages/agents/dist/projection/opencode.js
2400
2856
  import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
2401
- import path11 from "path";
2857
+ import path12 from "path";
2402
2858
  import os5 from "os";
2403
2859
 
2404
2860
  // packages/agents/dist/codex/app-server-client.js
@@ -2979,7 +3435,7 @@ var OpencodeSseBridge = class {
2979
3435
  }
2980
3436
  async run(taskId, port, ac) {
2981
3437
  const fetchImpl = this.deps.fetchImpl ?? fetch;
2982
- const sleep2 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
3438
+ const sleep3 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
2983
3439
  const reconnectMs = this.deps.reconnectMs ?? 500;
2984
3440
  const maxBoot = this.deps.maxBootAttempts ?? 60;
2985
3441
  const url = `http://127.0.0.1:${port}/event`;
@@ -3007,7 +3463,7 @@ var OpencodeSseBridge = class {
3007
3463
  return;
3008
3464
  }
3009
3465
  }
3010
- await sleep2(reconnectMs);
3466
+ await sleep3(reconnectMs);
3011
3467
  }
3012
3468
  }
3013
3469
  this.controllers.delete(taskId);
@@ -3243,7 +3699,7 @@ function runHeadless(opts) {
3243
3699
  }
3244
3700
 
3245
3701
  // packages/workspaces/dist/runtimes/cmux.js
3246
- import { execFile, execFileSync as execFileSync4 } from "child_process";
3702
+ import { execFile as execFile2, execFileSync as execFileSync4 } from "child_process";
3247
3703
  var CMUX_TIMEOUT = 15e3;
3248
3704
  var CmuxTimeoutError = class extends Error {
3249
3705
  constructor(cmd) {
@@ -3253,7 +3709,7 @@ var CmuxTimeoutError = class extends Error {
3253
3709
  };
3254
3710
  function cmux(args) {
3255
3711
  return new Promise((resolve2, reject) => {
3256
- execFile(
3712
+ execFile2(
3257
3713
  resolveCmuxBin(),
3258
3714
  args,
3259
3715
  // CMUX_QUIET=1 silences cmux 0.64's one-time deprecation hints (e.g. the
@@ -3615,9 +4071,9 @@ function createCmuxDriver() {
3615
4071
  import { execFileSync as execFileSync5, execSync as execSync7 } from "child_process";
3616
4072
 
3617
4073
  // packages/workspaces/dist/workspaces/obsidian.js
3618
- import fs9 from "fs/promises";
4074
+ import fs11 from "fs/promises";
3619
4075
  import { existsSync as existsSync8 } from "fs";
3620
- import path12 from "path";
4076
+ import path13 from "path";
3621
4077
 
3622
4078
  // packages/workspaces/dist/cmux/events-bridge.js
3623
4079
  import { spawn as nodeSpawn2 } from "child_process";
@@ -3661,7 +4117,7 @@ var CmuxEventsBridge = class {
3661
4117
  async run() {
3662
4118
  const spawnImpl = this.deps.spawnImpl ?? ((bin2, args2) => nodeSpawn2(bin2, args2, { stdio: ["ignore", "pipe", "ignore"] }));
3663
4119
  const bin = this.deps.cmuxBin ?? resolveCmuxBin();
3664
- const sleep2 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
4120
+ const sleep3 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
3665
4121
  const reconnectMs = this.deps.reconnectMs ?? 1e3;
3666
4122
  const args = [
3667
4123
  "events",
@@ -3681,7 +4137,7 @@ var CmuxEventsBridge = class {
3681
4137
  this.deps.log?.(`cmux events spawn failed: ${e.message}`);
3682
4138
  if (this.deps.stopAfterFirstRun)
3683
4139
  return;
3684
- await sleep2(reconnectMs);
4140
+ await sleep3(reconnectMs);
3685
4141
  continue;
3686
4142
  }
3687
4143
  this.child = child;
@@ -3704,7 +4160,7 @@ var CmuxEventsBridge = class {
3704
4160
  this.child = null;
3705
4161
  if (this.stopped || this.deps.stopAfterFirstRun)
3706
4162
  break;
3707
- await sleep2(reconnectMs);
4163
+ await sleep3(reconnectMs);
3708
4164
  }
3709
4165
  }
3710
4166
  onData(chunk) {
@@ -3811,6 +4267,8 @@ import net from "net";
3811
4267
 
3812
4268
  // packages/cli/src/control/squadrantd.ts
3813
4269
  var SELF_PATH2 = fileURLToPath3(import.meta.url);
4270
+ var CLI_BIN = join13(dirname5(SELF_PATH2), "index.js");
4271
+ var DAEMON_SOCK = join13(homedir8(), ".config", "squadrant", "squadrant.sock");
3814
4272
  function readPkgVersion() {
3815
4273
  try {
3816
4274
  const pkgPath = join13(dirname5(SELF_PATH2), "..", "package.json");
@@ -3820,6 +4278,30 @@ function readPkgVersion() {
3820
4278
  }
3821
4279
  }
3822
4280
  var PKG_VERSION = readPkgVersion();
4281
+ function buildTelegramBridge(cfg, stateRoot, log) {
4282
+ const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
4283
+ if (!token) {
4284
+ log("telegram: config present but no botToken / TELEGRAM_BOT_TOKEN set \u2014 bridge disabled");
4285
+ return void 0;
4286
+ }
4287
+ const client = createTelegramClient({ token });
4288
+ const ensureCaptainAlive = createEnsureCaptainAlive({
4289
+ isAlive: createIsCaptainAlive(DAEMON_SOCK),
4290
+ launch: createLaunch(CLI_BIN)
4291
+ });
4292
+ const runCommand = createRunCommand(CLI_BIN);
4293
+ const sendReply = (threadId, text) => client.sendMessage(cfg.supergroupId, threadId, text);
4294
+ return createTelegramBridge({
4295
+ cfg,
4296
+ stateRoot,
4297
+ client,
4298
+ appendCaptainMessage,
4299
+ log,
4300
+ ensureCaptainAlive,
4301
+ runCommand,
4302
+ sendReply
4303
+ });
4304
+ }
3823
4305
  function startSquadrantd(opts = {}) {
3824
4306
  const ctx = buildContext(opts);
3825
4307
  const { stateRoot, store, log, spawn: spawn2, writeResult, inFlightHeadlessIds, activeHeadlessKills } = ctx;
@@ -3876,6 +4358,8 @@ function startSquadrantd(opts = {}) {
3876
4358
  ctx.codexDriver = codexDriver;
3877
4359
  ctx.opencodeBridge = opencodeBridge;
3878
4360
  ctx.cmuxEventsBridge = cmuxEventsBridge;
4361
+ const tgCfg = loadConfig().telegram;
4362
+ ctx.telegramBridge = opts.telegramBridge ?? (tgCfg && !process.env.VITEST ? buildTelegramBridge(tgCfg, stateRoot, log) : void 0);
3879
4363
  ctx.daemonCmux = opts.daemonCmux ?? (opts.makeDaemonCmux ?? (() => new DaemonCmux(createCmuxDriver())))();
3880
4364
  const launchHeadless = opts.launchHeadless ?? (async (rec) => {
3881
4365
  const ingest = (e) => void ctx.d.handle({ kind: "event", project: rec.project, event: e });