squadrant 0.9.2 → 0.11.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.
@@ -152,6 +152,48 @@ function saveConfig(config, configPath = DEFAULT_CONFIG_PATH) {
152
152
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
153
153
  }
154
154
 
155
+ // packages/shared/dist/project-config.js
156
+ import fs2 from "fs";
157
+ import os2 from "os";
158
+ import path2 from "path";
159
+ function defaultRoot() {
160
+ return path2.join(os2.homedir(), ".config", "squadrant");
161
+ }
162
+ function projectConfigPath(name, root = defaultRoot()) {
163
+ return path2.join(root, "projects", `${name}.json`);
164
+ }
165
+ function loadProjectOverride(name, root = defaultRoot()) {
166
+ try {
167
+ return JSON.parse(fs2.readFileSync(projectConfigPath(name, root), "utf-8"));
168
+ } catch {
169
+ return {};
170
+ }
171
+ }
172
+ function deepMerge(base, patch) {
173
+ if (patch === null || typeof patch !== "object" || Array.isArray(patch))
174
+ return patch ?? base;
175
+ const out = { ...base };
176
+ for (const [k, v] of Object.entries(patch)) {
177
+ out[k] = deepMerge(out[k], v);
178
+ }
179
+ return out;
180
+ }
181
+ function saveProjectOverride(name, patch, root = defaultRoot()) {
182
+ const merged = deepMerge(loadProjectOverride(name, root), patch);
183
+ const file = projectConfigPath(name, root);
184
+ fs2.mkdirSync(path2.dirname(file), { recursive: true });
185
+ fs2.writeFileSync(file, JSON.stringify(merged, null, 2) + "\n");
186
+ }
187
+ var DEFAULT_NOTIFY = { active: false, cap: true, crew: "alert_only" };
188
+ function resolveNotify(globalNotify, override) {
189
+ let n = { ...DEFAULT_NOTIFY };
190
+ if (globalNotify)
191
+ n = deepMerge(n, globalNotify);
192
+ if (override.telegram?.notify)
193
+ n = deepMerge(n, override.telegram.notify);
194
+ return n;
195
+ }
196
+
155
197
  // packages/shared/dist/types/control.js
156
198
  var TERMINAL_STATES = /* @__PURE__ */ new Set([
157
199
  "done",
@@ -185,22 +227,22 @@ var MINIMAL_TEMPLATE = [
185
227
  ``
186
228
  ].join("\n");
187
229
  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 };
230
+ const path16 = opts.path ?? defaultCmuxConfigPath();
231
+ if (!existsSync(path16)) {
232
+ mkdirSync(dirname(path16), { recursive: true });
233
+ writeFileSync(path16, MINIMAL_TEMPLATE);
234
+ return { path: path16, changed: true, alreadySet: false };
193
235
  }
194
- const text = readFileSync(path13, "utf-8");
236
+ const text = readFileSync(path16, "utf-8");
195
237
  const current = parse(text)?.automation?.socketControlMode;
196
238
  if (current === AUTOMATION_MODE) {
197
- return { path: path13, changed: false, alreadySet: true };
239
+ return { path: path16, changed: false, alreadySet: true };
198
240
  }
199
241
  const edits = modify(text, [...SOCKET_CONTROL_MODE_PATH], AUTOMATION_MODE, {
200
242
  formattingOptions: { insertSpaces: true, tabSize: 2 }
201
243
  });
202
- writeFileSync(path13, applyEdits(text, edits));
203
- return { path: path13, changed: true, alreadySet: false };
244
+ writeFileSync(path16, applyEdits(text, edits));
245
+ return { path: path16, changed: true, alreadySet: false };
204
246
  }
205
247
 
206
248
  // packages/shared/dist/lib/cmux-probe.js
@@ -322,15 +364,15 @@ function sleep(ms) {
322
364
  function defaultStatePath() {
323
365
  return join4(homedir3(), ".config", "squadrant", "state", "cmux-autoconfig.json");
324
366
  }
325
- function readState(path13) {
367
+ function readState(path16) {
326
368
  try {
327
- return JSON.parse(readFileSync4(path13, "utf-8"));
369
+ return JSON.parse(readFileSync4(path16, "utf-8"));
328
370
  } catch {
329
371
  return {};
330
372
  }
331
373
  }
332
374
  async function ensureCmuxAutoConfig(opts = {}) {
333
- const statePath = opts.statePath ?? defaultStatePath();
375
+ const statePath2 = opts.statePath ?? defaultStatePath();
334
376
  const ensureConfig = opts.ensureConfig ?? ensureSocketAutomation;
335
377
  const probe = opts.probe ?? probeCmuxDaemonDirect;
336
378
  const cfg = ensureConfig({ path: opts.configPath });
@@ -338,15 +380,15 @@ async function ensureCmuxAutoConfig(opts = {}) {
338
380
  const needsRestart = verdict === "denied";
339
381
  let promptedThisRun = false;
340
382
  if (needsRestart) {
341
- const already = readState(statePath).promptedRestart === true;
383
+ const already = readState(statePath2).promptedRestart === true;
342
384
  if (!already) {
343
- mkdirSync2(dirname2(statePath), { recursive: true });
344
- writeFileSync3(statePath, JSON.stringify({ promptedRestart: true }));
385
+ mkdirSync2(dirname2(statePath2), { recursive: true });
386
+ writeFileSync3(statePath2, JSON.stringify({ promptedRestart: true }));
345
387
  promptedThisRun = true;
346
388
  }
347
389
  } else if (verdict === "reachable") {
348
- if (existsSync4(statePath))
349
- rmSync2(statePath, { force: true });
390
+ if (existsSync4(statePath2))
391
+ rmSync2(statePath2, { force: true });
350
392
  }
351
393
  return {
352
394
  configPath: cfg.path,
@@ -373,14 +415,14 @@ var compatManifest = {
373
415
 
374
416
  // packages/shared/dist/lib/git-worktree.js
375
417
  import { execFileSync as execFileSync2 } from "child_process";
376
- import path2 from "path";
418
+ import path3 from "path";
377
419
 
378
420
  // packages/shared/dist/lib/resolve-text-input.js
379
- import fs2 from "fs";
421
+ import fs3 from "fs";
380
422
 
381
423
  // packages/shared/dist/lib/runtime-sync.js
382
- import fs3 from "fs";
383
- import path3 from "path";
424
+ import fs4 from "fs";
425
+ import path4 from "path";
384
426
 
385
427
  // packages/shared/dist/lib/tool-compat.js
386
428
  function parseSemVer(v) {
@@ -414,13 +456,13 @@ function checkToolCompat(name, rawVersion, entry) {
414
456
  }
415
457
 
416
458
  // packages/shared/dist/lib/canonical-source.js
417
- import fs4 from "fs";
418
- import path4 from "path";
459
+ import fs5 from "fs";
460
+ import path5 from "path";
419
461
 
420
462
  // packages/shared/dist/lib/daily-logs.js
421
463
  import { execSync } from "child_process";
422
- import fs5 from "fs";
423
- import path5 from "path";
464
+ import fs6 from "fs";
465
+ import path6 from "path";
424
466
  import matter from "gray-matter";
425
467
 
426
468
  // packages/core/dist/state-machine.js
@@ -871,7 +913,7 @@ function createDaemon(deps) {
871
913
  }
872
914
 
873
915
  // packages/core/dist/mailbox.js
874
- import { promises as fs6 } from "fs";
916
+ import { promises as fs7 } from "fs";
875
917
  import { join as join5 } from "path";
876
918
  import { randomUUID } from "crypto";
877
919
  function inboxDir(stateRoot) {
@@ -888,7 +930,7 @@ async function listRotatedOldestFirst(stateRoot, project) {
888
930
  const dir = inboxDir(stateRoot);
889
931
  let entries;
890
932
  try {
891
- entries = await fs6.readdir(dir);
933
+ entries = await fs7.readdir(dir);
892
934
  } catch {
893
935
  return [];
894
936
  }
@@ -897,7 +939,7 @@ async function listRotatedOldestFirst(stateRoot, project) {
897
939
  }
898
940
  async function readMaxSeqFromFile(file) {
899
941
  try {
900
- const buf = await fs6.readFile(file, "utf-8");
942
+ const buf = await fs7.readFile(file, "utf-8");
901
943
  if (!buf.trim())
902
944
  return 0;
903
945
  const lines = buf.trim().split("\n");
@@ -936,34 +978,46 @@ function withProjectLock(project, fn) {
936
978
  projectLocks.set(project, next.catch(() => void 0));
937
979
  return next;
938
980
  }
939
- async function appendToMailbox(opts) {
940
- return withProjectLock(opts.project, async () => {
941
- const dir = inboxDir(opts.stateRoot);
942
- await fs6.mkdir(dir, { recursive: true });
943
- const file = logPath(opts.stateRoot, opts.project);
944
- const lastSeq = await readMaxSeq(opts.stateRoot, opts.project);
981
+ function appendEntry(stateRoot, project, build) {
982
+ return withProjectLock(project, async () => {
983
+ const dir = inboxDir(stateRoot);
984
+ await fs7.mkdir(dir, { recursive: true });
985
+ const file = logPath(stateRoot, project);
986
+ const lastSeq = await readMaxSeq(stateRoot, project);
945
987
  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
- };
956
- await fs6.appendFile(file, JSON.stringify(entry) + "\n", { encoding: "utf-8" });
988
+ const entry = build(seq);
989
+ await fs7.appendFile(file, JSON.stringify(entry) + "\n", { encoding: "utf-8" });
957
990
  return seq;
958
991
  });
959
992
  }
993
+ async function appendToMailbox(opts) {
994
+ return appendEntry(opts.stateRoot, opts.project, (seq) => ({
995
+ seq,
996
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
997
+ taskId: opts.taskRecord.id,
998
+ ...opts.taskRecord.name !== void 0 ? { name: opts.taskRecord.name } : {},
999
+ kind: opts.event.type,
1000
+ provider: opts.taskRecord.provider,
1001
+ payload: extractPayload(opts.event),
1002
+ message: opts.message ?? null
1003
+ }));
1004
+ }
1005
+ async function appendCaptainMessage(opts) {
1006
+ await appendEntry(opts.stateRoot, opts.project, (seq) => ({
1007
+ seq,
1008
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1009
+ kind: "captain.message",
1010
+ payload: { source: opts.source },
1011
+ message: opts.text
1012
+ }));
1013
+ }
960
1014
  function cursorPath(stateRoot, project, subscriber) {
961
1015
  return join5(inboxDir(stateRoot), `${project}.${subscriber}.cursor`);
962
1016
  }
963
1017
  async function readCursor(opts) {
964
1018
  let buf;
965
1019
  try {
966
- buf = await fs6.readFile(cursorPath(opts.stateRoot, opts.project, opts.subscriber), "utf-8");
1020
+ buf = await fs7.readFile(cursorPath(opts.stateRoot, opts.project, opts.subscriber), "utf-8");
967
1021
  } catch (e) {
968
1022
  if (e.code === "ENOENT")
969
1023
  return null;
@@ -978,7 +1032,7 @@ async function readCursor(opts) {
978
1032
  }
979
1033
  }
980
1034
  async function writeCursor(opts) {
981
- await fs6.mkdir(inboxDir(opts.stateRoot), { recursive: true });
1035
+ await fs7.mkdir(inboxDir(opts.stateRoot), { recursive: true });
982
1036
  const dest = cursorPath(opts.stateRoot, opts.project, opts.subscriber);
983
1037
  const tmp = `${dest}.${process.pid}.${randomUUID()}.tmp`;
984
1038
  const data = {
@@ -986,7 +1040,7 @@ async function writeCursor(opts) {
986
1040
  subscriber: opts.subscriber,
987
1041
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
988
1042
  };
989
- const handle = await fs6.open(tmp, "w");
1043
+ const handle = await fs7.open(tmp, "w");
990
1044
  try {
991
1045
  await handle.writeFile(JSON.stringify(data), { encoding: "utf-8" });
992
1046
  await handle.sync();
@@ -994,9 +1048,9 @@ async function writeCursor(opts) {
994
1048
  await handle.close();
995
1049
  }
996
1050
  try {
997
- await fs6.rename(tmp, dest);
1051
+ await fs7.rename(tmp, dest);
998
1052
  } catch (e) {
999
- await fs6.unlink(tmp).catch(() => {
1053
+ await fs7.unlink(tmp).catch(() => {
1000
1054
  });
1001
1055
  throw e;
1002
1056
  }
@@ -1007,7 +1061,7 @@ async function* readFromCursor(opts) {
1007
1061
  for (const file of files) {
1008
1062
  let buf;
1009
1063
  try {
1010
- buf = await fs6.readFile(file, "utf-8");
1064
+ buf = await fs7.readFile(file, "utf-8");
1011
1065
  } catch (e) {
1012
1066
  if (e.code === "ENOENT")
1013
1067
  continue;
@@ -1031,7 +1085,7 @@ async function mailboxStats(stateRoot, project) {
1031
1085
  const file = logPath(stateRoot, project);
1032
1086
  let sizeBytes = 0;
1033
1087
  try {
1034
- sizeBytes = (await fs6.stat(file)).size;
1088
+ sizeBytes = (await fs7.stat(file)).size;
1035
1089
  } catch (e) {
1036
1090
  if (e.code !== "ENOENT")
1037
1091
  throw e;
@@ -1046,7 +1100,7 @@ async function mailboxStats(stateRoot, project) {
1046
1100
  }
1047
1101
  async function oldestEntryAgeMs(file) {
1048
1102
  try {
1049
- const buf = await fs6.readFile(file, "utf-8");
1103
+ const buf = await fs7.readFile(file, "utf-8");
1050
1104
  const firstLine = buf.split("\n").find((l) => l.trim());
1051
1105
  if (!firstLine)
1052
1106
  return 0;
@@ -1061,7 +1115,7 @@ async function rotateIfNeeded(opts) {
1061
1115
  const file = logPath(opts.stateRoot, opts.project);
1062
1116
  let size = 0;
1063
1117
  try {
1064
- size = (await fs6.stat(file)).size;
1118
+ size = (await fs7.stat(file)).size;
1065
1119
  } catch (e) {
1066
1120
  if (e.code === "ENOENT")
1067
1121
  return { rotated: false };
@@ -1077,22 +1131,22 @@ async function rotateIfNeeded(opts) {
1077
1131
  const dst = `${file}.${n + 1}`;
1078
1132
  if (n + 1 > opts.keepCount) {
1079
1133
  try {
1080
- await fs6.unlink(src);
1134
+ await fs7.unlink(src);
1081
1135
  } catch (e) {
1082
1136
  if (e.code !== "ENOENT")
1083
1137
  throw e;
1084
1138
  }
1085
1139
  } else {
1086
1140
  try {
1087
- await fs6.rename(src, dst);
1141
+ await fs7.rename(src, dst);
1088
1142
  } catch (e) {
1089
1143
  if (e.code !== "ENOENT")
1090
1144
  throw e;
1091
1145
  }
1092
1146
  }
1093
1147
  }
1094
- await fs6.rename(file, `${file}.1`);
1095
- await fs6.writeFile(file, "", { encoding: "utf-8" });
1148
+ await fs7.rename(file, `${file}.1`);
1149
+ await fs7.writeFile(file, "", { encoding: "utf-8" });
1096
1150
  return { rotated: true, from: file, to: `${file}.1` };
1097
1151
  });
1098
1152
  }
@@ -1241,6 +1295,36 @@ function isDaemonSocketLive(sockPath, timeoutMs = 500) {
1241
1295
  });
1242
1296
  });
1243
1297
  }
1298
+ function sendRequest(sockPath, msg, timeoutMs = 5e3) {
1299
+ return new Promise((resolve2, reject) => {
1300
+ const conn = createConnection(sockPath);
1301
+ const dec = createDecoder();
1302
+ const timer = setTimeout(() => {
1303
+ conn.destroy();
1304
+ reject(new Error("control plane unavailable: request timed out"));
1305
+ }, timeoutMs);
1306
+ conn.setEncoding("utf-8");
1307
+ conn.on("connect", () => conn.write(encodeMsg({ ...msg, _v: PROTOCOL_VERSION })));
1308
+ conn.on("data", (chunk) => {
1309
+ for (const m of dec.push(chunk)) {
1310
+ clearTimeout(timer);
1311
+ conn.destroy();
1312
+ if (m._v !== void 0 && m._v !== PROTOCOL_VERSION) {
1313
+ reject(new Error(`squadrantd protocol v${m._v}, this client expects v${PROTOCOL_VERSION} \u2014 upgrade squadrantd or this CLI`));
1314
+ } else if (m.ok) {
1315
+ resolve2(m.reply);
1316
+ } else {
1317
+ reject(new Error(m.error));
1318
+ }
1319
+ return;
1320
+ }
1321
+ });
1322
+ conn.on("error", () => {
1323
+ clearTimeout(timer);
1324
+ reject(new Error("control plane unavailable: cannot reach squadrantd socket"));
1325
+ });
1326
+ });
1327
+ }
1244
1328
  function encodeFrame(f) {
1245
1329
  return JSON.stringify(f) + "\n";
1246
1330
  }
@@ -1508,6 +1592,7 @@ function buildContext(opts) {
1508
1592
  codexDriver: null,
1509
1593
  opencodeBridge: null,
1510
1594
  cmuxEventsBridge: null,
1595
+ telegramBridge: void 0,
1511
1596
  broadcast: () => {
1512
1597
  },
1513
1598
  schedulePromotion: () => {
@@ -2035,10 +2120,10 @@ function distBuiltAt() {
2035
2120
  return 0;
2036
2121
  }
2037
2122
  }
2038
- function gatherLogStats(path13, now, windowMs) {
2123
+ function gatherLogStats(path16, now, windowMs) {
2039
2124
  let sizeBytes = 0;
2040
2125
  try {
2041
- sizeBytes = statSync2(path13).size;
2126
+ sizeBytes = statSync2(path16).size;
2042
2127
  } catch {
2043
2128
  return { errorCount: 0, sizeBytes: 0, windowMs };
2044
2129
  }
@@ -2049,7 +2134,7 @@ function gatherLogStats(path13, now, windowMs) {
2049
2134
  const len = sizeBytes - start;
2050
2135
  let text = "";
2051
2136
  try {
2052
- const fd = openSync2(path13, "r");
2137
+ const fd = openSync2(path16, "r");
2053
2138
  try {
2054
2139
  const buf = Buffer.alloc(len);
2055
2140
  readSync(fd, buf, 0, len, start);
@@ -2126,7 +2211,11 @@ function startDaemon(ctx, opts, pkgVersion) {
2126
2211
  const { daemonCmux } = ctx;
2127
2212
  const probes = createProbes(ctx);
2128
2213
  const { defaultNotify, deliveryTick: initialDeliveryTick } = createDelivery(ctx, daemonCmux);
2129
- const notify = opts.notify ?? defaultNotify;
2214
+ const baseNotify = opts.notify ?? defaultNotify;
2215
+ const notify = ctx.telegramBridge ? async (args) => {
2216
+ await baseNotify(args);
2217
+ ctx.telegramBridge.pushLifecycle(args.project, args.event);
2218
+ } : baseNotify;
2130
2219
  const surfaceProbe = buildSurfaceProbe(ctx, probes, daemonCmux);
2131
2220
  const ingest = (project) => (e) => void ctx.d.handle({ kind: "event", project, event: e });
2132
2221
  const d = createDaemon({
@@ -2252,6 +2341,13 @@ function startDaemon(ctx, opts, pkgVersion) {
2252
2341
  log(`cmux events bridge start failed: ${e.message}`);
2253
2342
  }
2254
2343
  }
2344
+ if (ctx.telegramBridge) {
2345
+ try {
2346
+ ctx.telegramBridge.start();
2347
+ } catch (e) {
2348
+ log(`telegram bridge start failed: ${e.message}`);
2349
+ }
2350
+ }
2255
2351
  const autoConfigSafe = !!opts.runCmuxAutoConfig || !process.env.VITEST;
2256
2352
  if (autoConfigSafe) {
2257
2353
  try {
@@ -2341,6 +2437,10 @@ function startDaemon(ctx, opts, pkgVersion) {
2341
2437
  ctx.cmuxEventsBridge.stop();
2342
2438
  } catch {
2343
2439
  }
2440
+ try {
2441
+ ctx.telegramBridge?.stop();
2442
+ } catch {
2443
+ }
2344
2444
  try {
2345
2445
  ctx.codexDriver.stop?.();
2346
2446
  } catch {
@@ -2359,12 +2459,769 @@ function startDaemon(ctx, opts, pkgVersion) {
2359
2459
 
2360
2460
  // packages/core/dist/session-freshness.js
2361
2461
  import crypto from "crypto";
2362
- import fs7 from "fs";
2363
- import path6 from "path";
2462
+ import fs8 from "fs";
2463
+ import path7 from "path";
2364
2464
 
2365
2465
  // packages/core/dist/crew-lifecycle.js
2366
2466
  import { exec as nodeExec } from "child_process";
2367
2467
 
2468
+ // packages/core/dist/telegram/auth.js
2469
+ function isControlEnabled(cfg) {
2470
+ return cfg.remoteControl === true;
2471
+ }
2472
+ function isAuthorized(fromId, cfg) {
2473
+ if (fromId === void 0)
2474
+ return false;
2475
+ return Array.isArray(cfg.users) && cfg.users.includes(fromId);
2476
+ }
2477
+
2478
+ // packages/core/dist/telegram/commands.js
2479
+ var WRITABLE_CONFIG_KEYS = ["defaults.effort"];
2480
+ var EFFORT_MODES = /* @__PURE__ */ new Set(["max", "balance", "low"]);
2481
+ function ok(name, argv) {
2482
+ return { kind: "ok", name, argv };
2483
+ }
2484
+ function usage(name, message) {
2485
+ return { kind: "usage", name, message };
2486
+ }
2487
+ var REGISTRY = {
2488
+ status: { usage: "/status", build: () => ok("status", ["status"]) },
2489
+ projects: { usage: "/projects", build: () => ok("projects", ["projects", "list"]) },
2490
+ crews: {
2491
+ usage: "/crews <project>",
2492
+ build: (a) => a[0] ? ok("crews", ["crew", "list", a[0]]) : usage("crews", "usage: /crews <project>")
2493
+ },
2494
+ launch: {
2495
+ usage: "/launch <project>",
2496
+ build: (a) => a[0] ? ok("launch", ["launch", a[0]]) : usage("launch", "usage: /launch <project>")
2497
+ },
2498
+ effort: {
2499
+ usage: "/effort [max|balance|low]",
2500
+ build: (a) => {
2501
+ if (a.length === 0)
2502
+ return ok("effort", ["effort"]);
2503
+ if (!EFFORT_MODES.has(a[0]))
2504
+ return usage("effort", "usage: /effort [max|balance|low]");
2505
+ return ok("effort", ["effort", a[0]]);
2506
+ }
2507
+ },
2508
+ config: {
2509
+ usage: "/config get <key> | /config set <key> <value>",
2510
+ build: (a) => {
2511
+ const sub = a[0];
2512
+ if (sub === "get") {
2513
+ const key = a[1];
2514
+ if (!key)
2515
+ return usage("config", "usage: /config get <key>");
2516
+ return ok("config", ["config", "get", key]);
2517
+ }
2518
+ if (sub === "set") {
2519
+ const key = a[1];
2520
+ const value = a.slice(2).join(" ");
2521
+ if (!key || value === "")
2522
+ return usage("config", "usage: /config set <key> <value>");
2523
+ if (!WRITABLE_CONFIG_KEYS.includes(key)) {
2524
+ return {
2525
+ kind: "denied",
2526
+ message: `\u26D4 '${key}' is not writable over Telegram. Allowed: ${WRITABLE_CONFIG_KEYS.join(", ")}`
2527
+ };
2528
+ }
2529
+ return ok("config", ["config", "set", key, value]);
2530
+ }
2531
+ return usage("config", "usage: /config get <key> | /config set <key> <value>");
2532
+ }
2533
+ },
2534
+ spawn: {
2535
+ usage: "/spawn <project> <task...>",
2536
+ build: (a) => {
2537
+ const project = a[0];
2538
+ const task = a.slice(1).join(" ");
2539
+ if (!project || task === "")
2540
+ return usage("spawn", "usage: /spawn <project> <task...>");
2541
+ return ok("spawn", ["crew", "spawn", project, task]);
2542
+ }
2543
+ },
2544
+ mute: {
2545
+ usage: "/mute <project>",
2546
+ build: (a) => a[0] ? ok("mute", ["telegram", "notify", a[0], "off"]) : usage("mute", "usage: /mute <project>")
2547
+ },
2548
+ unmute: {
2549
+ usage: "/unmute <project>",
2550
+ build: (a) => a[0] ? ok("unmute", ["telegram", "notify", a[0], "on"]) : usage("unmute", "usage: /unmute <project>")
2551
+ }
2552
+ };
2553
+ function helpText() {
2554
+ const lines = Object.values(REGISTRY).map((e) => ` ${e.usage}`);
2555
+ return ["Available commands:", ...lines, " /help"].join("\n");
2556
+ }
2557
+ function stripBotMention(token) {
2558
+ return token.split("@")[0];
2559
+ }
2560
+ function parseCommand(text) {
2561
+ const trimmed = text.trim();
2562
+ if (!trimmed.startsWith("/")) {
2563
+ return { kind: "unknown", message: "unknown command \u2014 send /help" };
2564
+ }
2565
+ const tokens = trimmed.slice(1).split(/\s+/).filter((t) => t.length > 0);
2566
+ const name = stripBotMention(tokens[0] ?? "").toLowerCase();
2567
+ const args = tokens.slice(1);
2568
+ if (name === "help") {
2569
+ return { kind: "usage", name: "help", message: helpText() };
2570
+ }
2571
+ const entry = REGISTRY[name];
2572
+ if (!entry) {
2573
+ return { kind: "unknown", message: `unknown command '/${name}' \u2014 send /help` };
2574
+ }
2575
+ return entry.build(args);
2576
+ }
2577
+
2578
+ // packages/core/dist/telegram/ensure-captain.js
2579
+ var DEFAULT_WARMUP_TIMEOUT_MS = 12e4;
2580
+ var DEFAULT_POLL_MS = 1e3;
2581
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
2582
+ function createEnsureCaptainAlive(deps) {
2583
+ const warmupTimeoutMs = deps.warmupTimeoutMs ?? DEFAULT_WARMUP_TIMEOUT_MS;
2584
+ const pollMs = deps.pollMs ?? DEFAULT_POLL_MS;
2585
+ const sleep3 = deps.sleep ?? defaultSleep;
2586
+ const now = deps.now ?? (() => Date.now());
2587
+ const inFlight = /* @__PURE__ */ new Map();
2588
+ async function run(project) {
2589
+ if (await deps.isAlive(project))
2590
+ return "alive";
2591
+ await deps.launch(project);
2592
+ const deadline = now() + warmupTimeoutMs;
2593
+ while (now() < deadline) {
2594
+ if (await deps.isAlive(project))
2595
+ return "launched";
2596
+ await sleep3(pollMs);
2597
+ }
2598
+ return "timeout";
2599
+ }
2600
+ return function ensure(project) {
2601
+ const existing = inFlight.get(project);
2602
+ if (existing)
2603
+ return existing;
2604
+ const p = run(project).finally(() => inFlight.delete(project));
2605
+ inFlight.set(project, p);
2606
+ return p;
2607
+ };
2608
+ }
2609
+
2610
+ // packages/core/dist/telegram/format.js
2611
+ function topicName(project) {
2612
+ return project;
2613
+ }
2614
+ function formatLifecycle(project, ev) {
2615
+ switch (ev.type) {
2616
+ case "task.done":
2617
+ return `\u2705 [${project}] CREW DONE \xB7 ${ev.id}` + (ev.message ? `
2618
+ ${ev.message}` : "");
2619
+ case "task.blocked":
2620
+ return `\u{1F6A7} [${project}] CREW BLOCKED \xB7 ${ev.id}
2621
+ ${ev.question}`;
2622
+ case "task.idle":
2623
+ return `\u{1F4A4} [${project}] CREW IDLE \xB7 ${ev.id}`;
2624
+ case "task.failed":
2625
+ return `\u274C [${project}] CREW FAILED \xB7 ${ev.id}
2626
+ ${ev.error}`;
2627
+ case "task.approval.requested":
2628
+ return `\u{1F510} [${project}] APPROVAL NEEDED \xB7 ${ev.id}
2629
+ ${ev.question}`;
2630
+ case "task.input.requested":
2631
+ return `\u2753 [${project}] INPUT NEEDED \xB7 ${ev.id}
2632
+ ${ev.question}`;
2633
+ case "task.timeout":
2634
+ return `\u23F1\uFE0F [${project}] CREW TIMEOUT \xB7 ${ev.id}`;
2635
+ default:
2636
+ return `\u2139\uFE0F [${project}] ${ev.type} \xB7 ${ev.id}`;
2637
+ }
2638
+ }
2639
+ function formatInbound(text) {
2640
+ return `\u{1F4E9} [from Telegram] ${text}`;
2641
+ }
2642
+
2643
+ // packages/core/dist/telegram/state.js
2644
+ import fs9 from "fs";
2645
+ import path8 from "path";
2646
+ function statePath(stateRoot) {
2647
+ return path8.join(stateRoot, "telegram-state.json");
2648
+ }
2649
+ function topicKey(project, scope = "project") {
2650
+ return `${project}::${scope}`;
2651
+ }
2652
+ function loadState(stateRoot) {
2653
+ try {
2654
+ const raw = fs9.readFileSync(statePath(stateRoot), "utf-8");
2655
+ const data = JSON.parse(raw);
2656
+ const result = {
2657
+ offset: typeof data.offset === "number" ? data.offset : 0,
2658
+ topics: data.topics ?? {},
2659
+ notify: data.notify ?? {}
2660
+ };
2661
+ if (typeof data.lastUserId === "number")
2662
+ result.lastUserId = data.lastUserId;
2663
+ return result;
2664
+ } catch {
2665
+ return { offset: 0, topics: {}, notify: {} };
2666
+ }
2667
+ }
2668
+ function saveState(stateRoot, s) {
2669
+ fs9.mkdirSync(stateRoot, { recursive: true });
2670
+ fs9.writeFileSync(statePath(stateRoot), JSON.stringify(s, null, 2) + "\n");
2671
+ }
2672
+ function setTopic(stateRoot, project, topicId, scope = "project") {
2673
+ const s = loadState(stateRoot);
2674
+ s.topics[topicKey(project, scope)] = topicId;
2675
+ saveState(stateRoot, s);
2676
+ }
2677
+ function setLastUserId(stateRoot, id) {
2678
+ const s = loadState(stateRoot);
2679
+ s.lastUserId = id;
2680
+ saveState(stateRoot, s);
2681
+ }
2682
+ function setNotify(stateRoot, project, active) {
2683
+ const s = loadState(stateRoot);
2684
+ s.notify[project] = active;
2685
+ saveState(stateRoot, s);
2686
+ }
2687
+ function findProjectByThread(stateRoot, threadId) {
2688
+ const s = loadState(stateRoot);
2689
+ for (const [key, id] of Object.entries(s.topics)) {
2690
+ if (id !== threadId)
2691
+ continue;
2692
+ const sep2 = key.indexOf("::");
2693
+ if (sep2 === -1)
2694
+ continue;
2695
+ return { project: key.slice(0, sep2), scope: key.slice(sep2 + 2) };
2696
+ }
2697
+ return null;
2698
+ }
2699
+
2700
+ // packages/core/dist/telegram/client.js
2701
+ function createTelegramClient(opts) {
2702
+ const fetchImpl = opts.fetch ?? fetch;
2703
+ const base = `https://api.telegram.org/bot${opts.token}`;
2704
+ async function call(method, body) {
2705
+ const res = await fetchImpl(`${base}/${method}`, {
2706
+ method: "POST",
2707
+ headers: { "content-type": "application/json" },
2708
+ body: JSON.stringify(body)
2709
+ });
2710
+ const json = await res.json();
2711
+ if (!res.ok || !json.ok) {
2712
+ const code = json.error_code ?? res.status;
2713
+ const desc = json.description ?? "unknown error";
2714
+ throw new Error(`telegram ${method} failed (${code}): ${desc}`);
2715
+ }
2716
+ return json.result;
2717
+ }
2718
+ return {
2719
+ async getMe() {
2720
+ const r = await call("getMe", {});
2721
+ return { id: r.id, username: r.username };
2722
+ },
2723
+ getUpdates(offset, timeoutSec = 50) {
2724
+ return call("getUpdates", { offset, timeout: timeoutSec });
2725
+ },
2726
+ async sendMessage(chatId, threadId, text, replyMarkup) {
2727
+ const body = { chat_id: chatId, text };
2728
+ if (threadId !== void 0)
2729
+ body.message_thread_id = threadId;
2730
+ if (replyMarkup !== void 0)
2731
+ body.reply_markup = replyMarkup;
2732
+ await call("sendMessage", body);
2733
+ },
2734
+ async answerCallbackQuery(callbackQueryId, text) {
2735
+ const body = { callback_query_id: callbackQueryId };
2736
+ if (text !== void 0)
2737
+ body.text = text;
2738
+ await call("answerCallbackQuery", body);
2739
+ },
2740
+ async editMessageReplyMarkup(chatId, messageId, replyMarkup) {
2741
+ await call("editMessageReplyMarkup", { chat_id: chatId, message_id: messageId, reply_markup: replyMarkup });
2742
+ },
2743
+ async createForumTopic(chatId, name) {
2744
+ const r = await call("createForumTopic", { chat_id: chatId, name });
2745
+ return r.message_thread_id;
2746
+ },
2747
+ async setMyCommands(commands) {
2748
+ await call("setMyCommands", { commands });
2749
+ }
2750
+ };
2751
+ }
2752
+
2753
+ // packages/core/dist/telegram/bridge.js
2754
+ import os3 from "os";
2755
+ import path9 from "path";
2756
+
2757
+ // packages/core/dist/telegram/panels.js
2758
+ var mark = (on, label) => on ? `\u2022 ${label}` : label;
2759
+ var TIERS = ["none", "alert_only", "all"];
2760
+ function notifyPanel(s) {
2761
+ return {
2762
+ inline_keyboard: [
2763
+ [{ text: `Captain: ${s.cap ? "ON" : "OFF"}`, callback_data: `n:cap:${s.cap ? "off" : "on"}` }],
2764
+ TIERS.map((t) => ({ text: mark(s.crew === t, `crew:${t}`), callback_data: `n:crew:${t}` })),
2765
+ [{ text: s.active ? "\u{1F515} Mute topic" : "\u{1F514} Unmute", callback_data: `n:active:${s.active ? "off" : "on"}` }]
2766
+ ]
2767
+ };
2768
+ }
2769
+ function effortPanel(current) {
2770
+ const modes = ["max", "balance", "low"];
2771
+ return {
2772
+ inline_keyboard: [modes.map((m) => ({ text: mark(current === m, m), callback_data: `e:${m}` }))]
2773
+ };
2774
+ }
2775
+ function projectPicker(action, projects) {
2776
+ return { inline_keyboard: projects.map((p) => [{ text: p, callback_data: `${action}:${p}` }]) };
2777
+ }
2778
+ var SPAWN_PROMPT_PREFIX = "\u{1F195} Reply with the task for a crew on: ";
2779
+ function buildSpawnPrompt(project) {
2780
+ return `${SPAWN_PROMPT_PREFIX}${project}`;
2781
+ }
2782
+ function parseSpawnPrompt(text) {
2783
+ if (!text || !text.startsWith(SPAWN_PROMPT_PREFIX))
2784
+ return null;
2785
+ const project = text.slice(SPAWN_PROMPT_PREFIX.length).trim();
2786
+ return project.length > 0 ? project : null;
2787
+ }
2788
+ function spawnPicker(projects) {
2789
+ return { inline_keyboard: projects.map((p) => [{ text: p, callback_data: `sp:${p}` }]) };
2790
+ }
2791
+ var PICK_ACTIONS = ["cr", "lc", "mu", "um"];
2792
+ function parseCallback(data) {
2793
+ const parts = data.split(":");
2794
+ if (parts[0] === "n" && (parts[1] === "cap" || parts[1] === "crew" || parts[1] === "active") && parts[2]) {
2795
+ return { t: "notify", dim: parts[1], val: parts[2] };
2796
+ }
2797
+ if (parts[0] === "e" && parts[1])
2798
+ return { t: "effort", mode: parts[1] };
2799
+ if (PICK_ACTIONS.includes(parts[0]) && parts[1]) {
2800
+ return { t: "pick", action: parts[0], project: parts.slice(1).join(":") };
2801
+ }
2802
+ if (parts[0] === "sp" && parts[1])
2803
+ return { t: "spawn", project: parts.slice(1).join(":") };
2804
+ return null;
2805
+ }
2806
+
2807
+ // packages/core/dist/telegram/tiers.js
2808
+ var DONE_ONLY = /* @__PURE__ */ new Set(["task.done", "task.failed"]);
2809
+ var ALERTS = /* @__PURE__ */ new Set([
2810
+ ...DONE_ONLY,
2811
+ "task.blocked",
2812
+ "task.approval.requested",
2813
+ "task.input.requested",
2814
+ "task.timeout"
2815
+ ]);
2816
+ function tierIncludes(tier, eventType) {
2817
+ switch (tier) {
2818
+ case "none":
2819
+ return false;
2820
+ case "done_only":
2821
+ return DONE_ONLY.has(eventType);
2822
+ case "alert_only":
2823
+ return ALERTS.has(eventType);
2824
+ case "all":
2825
+ return true;
2826
+ }
2827
+ }
2828
+
2829
+ // packages/core/dist/telegram/bridge.js
2830
+ var LONG_POLL_SEC = 50;
2831
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
2832
+ var CREW_TIERS = ["all", "alert_only", "done_only", "none"];
2833
+ function parseNotifyPref(text) {
2834
+ const parts = text.trim().split(/\s+/);
2835
+ if (stripBotMention(parts[0] ?? "").toLowerCase() !== "/notify")
2836
+ return null;
2837
+ const dimension = parts[1]?.toLowerCase();
2838
+ if ((dimension === "crew" || dimension === "cap") && parts[2])
2839
+ return { dimension, value: parts[2].toLowerCase() };
2840
+ return null;
2841
+ }
2842
+ function isBareSpawn(text) {
2843
+ const trimmed = text.trim();
2844
+ if (!trimmed.startsWith("/"))
2845
+ return false;
2846
+ const tokens = trimmed.slice(1).split(/\s+/).filter((t) => t.length > 0);
2847
+ return stripBotMention(tokens[0] ?? "").toLowerCase() === "spawn" && tokens.length === 1;
2848
+ }
2849
+ function notifyToggle(text) {
2850
+ const first = stripBotMention(text.trim().split(/\s+/)[0] ?? "").toLowerCase();
2851
+ if (first === "/unmute")
2852
+ return true;
2853
+ if (first === "/mute")
2854
+ return false;
2855
+ return null;
2856
+ }
2857
+ function createTelegramBridge(opts) {
2858
+ const { cfg, stateRoot, client, appendCaptainMessage: appendCaptainMessage2, log, ensureCaptainAlive, runCommand, sendReply } = opts;
2859
+ const configRoot = opts.configRoot ?? path9.join(os3.homedir(), ".config", "squadrant");
2860
+ const pollMs = cfg.pollMs ?? 1e3;
2861
+ let running = false;
2862
+ function persistOffset(next) {
2863
+ const s = loadState(stateRoot);
2864
+ s.offset = next;
2865
+ saveState(stateRoot, s);
2866
+ }
2867
+ async function deliverOutbound(project, ev) {
2868
+ const resolved = resolveNotify(cfg.notify, loadProjectOverride(project, configRoot));
2869
+ const live = loadState(stateRoot).notify[project];
2870
+ const active = live ?? resolved.active;
2871
+ if (!active)
2872
+ return;
2873
+ if (!tierIncludes(resolved.crew, ev.type))
2874
+ return;
2875
+ let threadId = loadState(stateRoot).topics[topicKey(project)];
2876
+ if (threadId === void 0) {
2877
+ threadId = await client.createForumTopic(cfg.supergroupId, topicName(project));
2878
+ setTopic(stateRoot, project, threadId);
2879
+ }
2880
+ await client.sendMessage(cfg.supergroupId, threadId, formatLifecycle(project, ev));
2881
+ }
2882
+ function resolveLiveNotify(project) {
2883
+ const resolved = resolveNotify(cfg.notify, loadProjectOverride(project, configRoot));
2884
+ const live = loadState(stateRoot).notify[project];
2885
+ return { ...resolved, active: live ?? resolved.active };
2886
+ }
2887
+ async function editMarkup(chatId, messageId, markup) {
2888
+ try {
2889
+ await client.editMessageReplyMarkup(chatId, messageId, markup);
2890
+ } catch (e) {
2891
+ const msg = e.message;
2892
+ if (!/not modified/i.test(msg))
2893
+ throw e;
2894
+ }
2895
+ }
2896
+ async function handleCallback(cq) {
2897
+ try {
2898
+ if (!cq.data || !cq.message) {
2899
+ await client.answerCallbackQuery(cq.id);
2900
+ return;
2901
+ }
2902
+ if (!isControlEnabled(cfg) || !isAuthorized(cq.from?.id, cfg)) {
2903
+ await client.answerCallbackQuery(cq.id, "\u26D4 not authorized");
2904
+ return;
2905
+ }
2906
+ const action = parseCallback(cq.data);
2907
+ if (!action) {
2908
+ await client.answerCallbackQuery(cq.id);
2909
+ return;
2910
+ }
2911
+ const chatId = cq.message.chat.id;
2912
+ const messageId = cq.message.message_id;
2913
+ if (action.t === "notify") {
2914
+ const resolved = findProjectByThread(stateRoot, cq.message.message_thread_id ?? -1);
2915
+ if (!resolved) {
2916
+ await client.answerCallbackQuery(cq.id, "no project for this topic");
2917
+ return;
2918
+ }
2919
+ const project2 = resolved.project;
2920
+ if (action.dim === "active") {
2921
+ setNotify(stateRoot, project2, action.val === "on");
2922
+ } else if (action.dim === "cap") {
2923
+ saveProjectOverride(project2, { telegram: { notify: { cap: action.val === "on" } } }, configRoot);
2924
+ } else {
2925
+ saveProjectOverride(project2, { telegram: { notify: { crew: action.val } } }, configRoot);
2926
+ }
2927
+ await client.answerCallbackQuery(cq.id, `\u2705 ${action.dim} = ${action.val}`);
2928
+ await editMarkup(chatId, messageId, notifyPanel(resolveLiveNotify(project2)));
2929
+ return;
2930
+ }
2931
+ if (action.t === "effort") {
2932
+ if (runCommand)
2933
+ await runCommand(["effort", action.mode]);
2934
+ await client.answerCallbackQuery(cq.id, `\u2705 effort = ${action.mode}`);
2935
+ await editMarkup(chatId, messageId, effortPanel(action.mode));
2936
+ return;
2937
+ }
2938
+ if (action.t === "spawn") {
2939
+ await reply(cq.message.message_thread_id, buildSpawnPrompt(action.project), { force_reply: true, selective: true });
2940
+ await client.answerCallbackQuery(cq.id);
2941
+ return;
2942
+ }
2943
+ const { action: act, project } = action;
2944
+ if (act === "cr") {
2945
+ const out = runCommand ? await runCommand(["crew", "list", project]) : "(command runner unavailable)";
2946
+ await client.answerCallbackQuery(cq.id);
2947
+ await reply(void 0, out);
2948
+ } else if (act === "lc") {
2949
+ if (runCommand)
2950
+ await runCommand(["launch", project]);
2951
+ await client.answerCallbackQuery(cq.id, `launching ${project}`);
2952
+ } else if (act === "mu") {
2953
+ setNotify(stateRoot, project, false);
2954
+ await client.answerCallbackQuery(cq.id, `\u{1F515} muted ${project}`);
2955
+ } else {
2956
+ setNotify(stateRoot, project, true);
2957
+ await client.answerCallbackQuery(cq.id, `\u{1F514} unmuted ${project}`);
2958
+ }
2959
+ } catch (e) {
2960
+ log(`telegram callback failed data=${cq.data}: ${e.message}`);
2961
+ try {
2962
+ await client.answerCallbackQuery(cq.id, "\u26A0\uFE0F failed");
2963
+ } catch {
2964
+ }
2965
+ }
2966
+ }
2967
+ async function reply(threadId, text, replyMarkup) {
2968
+ if (!sendReply)
2969
+ return;
2970
+ try {
2971
+ if (replyMarkup !== void 0)
2972
+ await sendReply(threadId, text, replyMarkup);
2973
+ else
2974
+ await sendReply(threadId, text);
2975
+ } catch (e) {
2976
+ log(`telegram reply failed: ${e.message}`);
2977
+ }
2978
+ }
2979
+ function currentEffort() {
2980
+ try {
2981
+ return loadConfig(path9.join(configRoot, "config.json")).defaults.effort ?? "balance";
2982
+ } catch {
2983
+ return "balance";
2984
+ }
2985
+ }
2986
+ function projectNames() {
2987
+ try {
2988
+ return Object.keys(loadConfig(path9.join(configRoot, "config.json")).projects);
2989
+ } catch {
2990
+ return [];
2991
+ }
2992
+ }
2993
+ async function replySpawnPicker(threadId) {
2994
+ const projects = projectNames();
2995
+ if (projects.length === 0) {
2996
+ await reply(threadId, "no projects registered");
2997
+ return;
2998
+ }
2999
+ await reply(threadId, "Pick a project to spawn a crew on:", spawnPicker(projects));
3000
+ }
3001
+ async function handleGeneral(text, fromId) {
3002
+ if (!text.startsWith("/")) {
3003
+ await reply(void 0, "Send /help for commands.");
3004
+ return;
3005
+ }
3006
+ if (!isControlEnabled(cfg) || !isAuthorized(fromId, cfg)) {
3007
+ await reply(void 0, "\u26D4 not authorized");
3008
+ return;
3009
+ }
3010
+ const tokens = text.trim().slice(1).split(/\s+/).filter((t) => t.length > 0);
3011
+ const name = stripBotMention(tokens[0] ?? "").toLowerCase();
3012
+ const noArg = tokens.length === 1;
3013
+ if (noArg && name === "effort") {
3014
+ await reply(void 0, "Effort mode:", effortPanel(currentEffort()));
3015
+ return;
3016
+ }
3017
+ if (noArg && name === "spawn") {
3018
+ await replySpawnPicker(void 0);
3019
+ return;
3020
+ }
3021
+ const PICKERS = { crews: "cr", launch: "lc", mute: "mu", unmute: "um" };
3022
+ if (noArg && name in PICKERS) {
3023
+ const projects = projectNames();
3024
+ if (projects.length === 0) {
3025
+ await reply(void 0, "no projects registered");
3026
+ return;
3027
+ }
3028
+ await reply(void 0, `Pick a project:`, projectPicker(PICKERS[name], projects));
3029
+ return;
3030
+ }
3031
+ const parsed = parseCommand(text);
3032
+ if (parsed.kind !== "ok") {
3033
+ await reply(void 0, parsed.message);
3034
+ return;
3035
+ }
3036
+ try {
3037
+ const out = runCommand ? await runCommand(parsed.argv) : "(command runner unavailable)";
3038
+ await reply(void 0, out);
3039
+ } catch (e) {
3040
+ await reply(void 0, `\u26A0\uFE0F command failed: ${e.message}`);
3041
+ log(`telegram command failed argv=${JSON.stringify(parsed.argv)}: ${e.message}`);
3042
+ }
3043
+ }
3044
+ async function handleProjectTopic(text, threadId, fromId) {
3045
+ const resolved = findProjectByThread(stateRoot, threadId);
3046
+ if (!resolved)
3047
+ return;
3048
+ if (isBareSpawn(text)) {
3049
+ if (!isControlEnabled(cfg) || !isAuthorized(fromId, cfg)) {
3050
+ await reply(threadId, "\u26D4 not authorized");
3051
+ return;
3052
+ }
3053
+ await replySpawnPicker(threadId);
3054
+ return;
3055
+ }
3056
+ const toggle = notifyToggle(text);
3057
+ if (toggle !== null) {
3058
+ if (!isControlEnabled(cfg) || !isAuthorized(fromId, cfg)) {
3059
+ await reply(threadId, "\u26D4 not authorized");
3060
+ return;
3061
+ }
3062
+ setNotify(stateRoot, resolved.project, toggle);
3063
+ await reply(threadId, toggle ? `\u{1F514} ${resolved.project} notifications ON` : `\u{1F515} ${resolved.project} notifications OFF`);
3064
+ return;
3065
+ }
3066
+ if (stripBotMention(text.trim().split(/\s+/)[0] ?? "").toLowerCase() === "/notify") {
3067
+ if (!isControlEnabled(cfg) || !isAuthorized(fromId, cfg)) {
3068
+ await reply(threadId, "\u26D4 not authorized");
3069
+ return;
3070
+ }
3071
+ const pref = parseNotifyPref(text);
3072
+ if (pref === null) {
3073
+ await reply(threadId, `\u{1F514} ${resolved.project} notifications`, notifyPanel(resolveLiveNotify(resolved.project)));
3074
+ return;
3075
+ }
3076
+ if (pref.dimension === "crew") {
3077
+ if (!CREW_TIERS.includes(pref.value)) {
3078
+ await reply(threadId, "crew must be all|alert_only|done_only|none");
3079
+ return;
3080
+ }
3081
+ saveProjectOverride(resolved.project, { telegram: { notify: { crew: pref.value } } }, configRoot);
3082
+ } else {
3083
+ if (pref.value !== "on" && pref.value !== "off") {
3084
+ await reply(threadId, "cap must be on|off");
3085
+ return;
3086
+ }
3087
+ saveProjectOverride(resolved.project, { telegram: { notify: { cap: pref.value === "on" } } }, configRoot);
3088
+ }
3089
+ await reply(threadId, `\u2705 ${pref.dimension} = ${pref.value}`);
3090
+ return;
3091
+ }
3092
+ setNotify(stateRoot, resolved.project, true);
3093
+ if (ensureCaptainAlive && isControlEnabled(cfg) && isAuthorized(fromId, cfg)) {
3094
+ try {
3095
+ const r = await ensureCaptainAlive(resolved.project);
3096
+ if (r === "timeout")
3097
+ await reply(threadId, "\u26A0\uFE0F captain didn't warm up; message queued.");
3098
+ } catch (e) {
3099
+ log(`telegram auto-launch failed project=${resolved.project}: ${e.message}`);
3100
+ }
3101
+ }
3102
+ await appendCaptainMessage2({ stateRoot, project: resolved.project, text: formatInbound(text), source: "telegram" });
3103
+ }
3104
+ async function handleUpdate(u) {
3105
+ if (u.callback_query) {
3106
+ await handleCallback(u.callback_query);
3107
+ return;
3108
+ }
3109
+ const m = u.message;
3110
+ if (!m || m.text === void 0)
3111
+ return;
3112
+ if (!cfg.chats.includes(m.chat.id))
3113
+ return;
3114
+ if (m.from?.id !== void 0 && loadState(stateRoot).lastUserId !== m.from.id) {
3115
+ setLastUserId(stateRoot, m.from.id);
3116
+ }
3117
+ const spawnProject = parseSpawnPrompt(m.reply_to_message?.text);
3118
+ if (spawnProject) {
3119
+ const threadId = m.message_thread_id;
3120
+ if (!isControlEnabled(cfg) || !isAuthorized(m.from?.id, cfg)) {
3121
+ await reply(threadId, "\u26D4 not authorized");
3122
+ return;
3123
+ }
3124
+ const task = m.text.trim();
3125
+ if (!task) {
3126
+ await reply(threadId, "spawn cancelled \u2014 empty task");
3127
+ return;
3128
+ }
3129
+ if (runCommand)
3130
+ await runCommand(["crew", "spawn", spawnProject, task]);
3131
+ await reply(threadId, `\u{1F195} spawning a crew on ${spawnProject}\u2026`);
3132
+ return;
3133
+ }
3134
+ if (m.message_thread_id === void 0) {
3135
+ await handleGeneral(m.text, m.from?.id);
3136
+ return;
3137
+ }
3138
+ await handleProjectTopic(m.text, m.message_thread_id, m.from?.id);
3139
+ }
3140
+ async function pollLoop() {
3141
+ while (running) {
3142
+ try {
3143
+ const offset = loadState(stateRoot).offset;
3144
+ const updates = await client.getUpdates(offset, LONG_POLL_SEC);
3145
+ for (const u of updates) {
3146
+ await handleUpdate(u);
3147
+ persistOffset(u.update_id + 1);
3148
+ }
3149
+ } catch (e) {
3150
+ log(`telegram inbound poll failed: ${e.message}`);
3151
+ }
3152
+ if (running)
3153
+ await sleep2(pollMs);
3154
+ }
3155
+ }
3156
+ return {
3157
+ start() {
3158
+ if (running)
3159
+ return;
3160
+ running = true;
3161
+ void pollLoop();
3162
+ },
3163
+ stop() {
3164
+ running = false;
3165
+ },
3166
+ pushLifecycle(project, ev) {
3167
+ void deliverOutbound(project, ev).catch((e) => {
3168
+ log(`telegram outbound failed project=${project}: ${e.message}`);
3169
+ });
3170
+ }
3171
+ };
3172
+ }
3173
+
3174
+ // packages/core/dist/telegram/setup.js
3175
+ import fs10 from "fs";
3176
+
3177
+ // packages/cli/src/control/telegram-control.ts
3178
+ import { execFile } from "child_process";
3179
+ import { promisify } from "util";
3180
+ var pExecFile = promisify(execFile);
3181
+ var MAX_OUTPUT = 3500;
3182
+ function capOutput(stdout, stderr, max = MAX_OUTPUT) {
3183
+ const out = stdout.trim();
3184
+ const err = stderr.trim();
3185
+ let combined = out;
3186
+ if (err) combined = combined ? `${combined}
3187
+ [stderr] ${err}` : `[stderr] ${err}`;
3188
+ if (!combined) combined = "(no output)";
3189
+ if (combined.length > max) combined = combined.slice(0, max) + "\n\u2026[truncated]";
3190
+ return combined;
3191
+ }
3192
+ var COMMAND_TIMEOUT_MS = 6e4;
3193
+ function createRunCommand(cliBin) {
3194
+ return async (argv) => {
3195
+ try {
3196
+ const { stdout, stderr } = await pExecFile(
3197
+ process.execPath,
3198
+ [cliBin, ...argv],
3199
+ { timeout: COMMAND_TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024 }
3200
+ );
3201
+ return capOutput(stdout ?? "", stderr ?? "");
3202
+ } catch (e) {
3203
+ const err = e;
3204
+ return capOutput(err.stdout ?? "", err.stderr ?? err.message ?? "command failed");
3205
+ }
3206
+ };
3207
+ }
3208
+ function createIsCaptainAlive(sock) {
3209
+ return async (project) => {
3210
+ try {
3211
+ const health = await sendRequest(sock, { kind: "health", project }, 5e3);
3212
+ const captain = health?.find((h) => h.kind === "captain" && h.project === project);
3213
+ return captain != null && captain.state !== "gone" && captain.state !== "unknown";
3214
+ } catch {
3215
+ return false;
3216
+ }
3217
+ };
3218
+ }
3219
+ function createLaunch(cliBin) {
3220
+ return async (project) => {
3221
+ await pExecFile(process.execPath, [cliBin, "launch", project], { timeout: 3e4 });
3222
+ };
3223
+ }
3224
+
2368
3225
  // packages/agents/dist/drivers/claude.js
2369
3226
  import { execSync as execSync2 } from "child_process";
2370
3227
 
@@ -2378,28 +3235,28 @@ import { execSync as execSync4 } from "child_process";
2378
3235
  import { execSync as execSync5 } from "child_process";
2379
3236
 
2380
3237
  // packages/agents/dist/drivers/launch-cmd.js
2381
- import fs8 from "fs";
2382
- import path7 from "path";
3238
+ import fs11 from "fs";
3239
+ import path10 from "path";
2383
3240
 
2384
3241
  // packages/agents/dist/projection/cursor.js
2385
3242
  import { mkdir, readFile, writeFile } from "fs/promises";
2386
- import path8 from "path";
2387
- import os2 from "os";
3243
+ import path11 from "path";
3244
+ import os4 from "os";
2388
3245
 
2389
3246
  // packages/agents/dist/projection/codex.js
2390
3247
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
2391
- import path9 from "path";
2392
- import os3 from "os";
3248
+ import path12 from "path";
3249
+ import os5 from "os";
2393
3250
 
2394
3251
  // packages/agents/dist/projection/gemini.js
2395
3252
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
2396
- import path10 from "path";
2397
- import os4 from "os";
3253
+ import path13 from "path";
3254
+ import os6 from "os";
2398
3255
 
2399
3256
  // packages/agents/dist/projection/opencode.js
2400
3257
  import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
2401
- import path11 from "path";
2402
- import os5 from "os";
3258
+ import path14 from "path";
3259
+ import os7 from "os";
2403
3260
 
2404
3261
  // packages/agents/dist/codex/app-server-client.js
2405
3262
  import { EventEmitter } from "events";
@@ -2979,7 +3836,7 @@ var OpencodeSseBridge = class {
2979
3836
  }
2980
3837
  async run(taskId, port, ac) {
2981
3838
  const fetchImpl = this.deps.fetchImpl ?? fetch;
2982
- const sleep2 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
3839
+ const sleep3 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
2983
3840
  const reconnectMs = this.deps.reconnectMs ?? 500;
2984
3841
  const maxBoot = this.deps.maxBootAttempts ?? 60;
2985
3842
  const url = `http://127.0.0.1:${port}/event`;
@@ -3007,7 +3864,7 @@ var OpencodeSseBridge = class {
3007
3864
  return;
3008
3865
  }
3009
3866
  }
3010
- await sleep2(reconnectMs);
3867
+ await sleep3(reconnectMs);
3011
3868
  }
3012
3869
  }
3013
3870
  this.controllers.delete(taskId);
@@ -3243,7 +4100,7 @@ function runHeadless(opts) {
3243
4100
  }
3244
4101
 
3245
4102
  // packages/workspaces/dist/runtimes/cmux.js
3246
- import { execFile, execFileSync as execFileSync4 } from "child_process";
4103
+ import { execFile as execFile2, execFileSync as execFileSync4 } from "child_process";
3247
4104
  var CMUX_TIMEOUT = 15e3;
3248
4105
  var CmuxTimeoutError = class extends Error {
3249
4106
  constructor(cmd) {
@@ -3253,7 +4110,7 @@ var CmuxTimeoutError = class extends Error {
3253
4110
  };
3254
4111
  function cmux(args) {
3255
4112
  return new Promise((resolve2, reject) => {
3256
- execFile(
4113
+ execFile2(
3257
4114
  resolveCmuxBin(),
3258
4115
  args,
3259
4116
  // CMUX_QUIET=1 silences cmux 0.64's one-time deprecation hints (e.g. the
@@ -3615,9 +4472,9 @@ function createCmuxDriver() {
3615
4472
  import { execFileSync as execFileSync5, execSync as execSync7 } from "child_process";
3616
4473
 
3617
4474
  // packages/workspaces/dist/workspaces/obsidian.js
3618
- import fs9 from "fs/promises";
4475
+ import fs12 from "fs/promises";
3619
4476
  import { existsSync as existsSync8 } from "fs";
3620
- import path12 from "path";
4477
+ import path15 from "path";
3621
4478
 
3622
4479
  // packages/workspaces/dist/cmux/events-bridge.js
3623
4480
  import { spawn as nodeSpawn2 } from "child_process";
@@ -3661,7 +4518,7 @@ var CmuxEventsBridge = class {
3661
4518
  async run() {
3662
4519
  const spawnImpl = this.deps.spawnImpl ?? ((bin2, args2) => nodeSpawn2(bin2, args2, { stdio: ["ignore", "pipe", "ignore"] }));
3663
4520
  const bin = this.deps.cmuxBin ?? resolveCmuxBin();
3664
- const sleep2 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
4521
+ const sleep3 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
3665
4522
  const reconnectMs = this.deps.reconnectMs ?? 1e3;
3666
4523
  const args = [
3667
4524
  "events",
@@ -3681,7 +4538,7 @@ var CmuxEventsBridge = class {
3681
4538
  this.deps.log?.(`cmux events spawn failed: ${e.message}`);
3682
4539
  if (this.deps.stopAfterFirstRun)
3683
4540
  return;
3684
- await sleep2(reconnectMs);
4541
+ await sleep3(reconnectMs);
3685
4542
  continue;
3686
4543
  }
3687
4544
  this.child = child;
@@ -3704,7 +4561,7 @@ var CmuxEventsBridge = class {
3704
4561
  this.child = null;
3705
4562
  if (this.stopped || this.deps.stopAfterFirstRun)
3706
4563
  break;
3707
- await sleep2(reconnectMs);
4564
+ await sleep3(reconnectMs);
3708
4565
  }
3709
4566
  }
3710
4567
  onData(chunk) {
@@ -3811,6 +4668,8 @@ import net from "net";
3811
4668
 
3812
4669
  // packages/cli/src/control/squadrantd.ts
3813
4670
  var SELF_PATH2 = fileURLToPath3(import.meta.url);
4671
+ var CLI_BIN = join13(dirname5(SELF_PATH2), "index.js");
4672
+ var DAEMON_SOCK = join13(homedir8(), ".config", "squadrant", "squadrant.sock");
3814
4673
  function readPkgVersion() {
3815
4674
  try {
3816
4675
  const pkgPath = join13(dirname5(SELF_PATH2), "..", "package.json");
@@ -3820,6 +4679,31 @@ function readPkgVersion() {
3820
4679
  }
3821
4680
  }
3822
4681
  var PKG_VERSION = readPkgVersion();
4682
+ function buildTelegramBridge(cfg, stateRoot, log) {
4683
+ const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
4684
+ if (!token) {
4685
+ log("telegram: config present but no botToken / TELEGRAM_BOT_TOKEN set \u2014 bridge disabled");
4686
+ return void 0;
4687
+ }
4688
+ const client = createTelegramClient({ token });
4689
+ const ensureCaptainAlive = createEnsureCaptainAlive({
4690
+ isAlive: createIsCaptainAlive(DAEMON_SOCK),
4691
+ launch: createLaunch(CLI_BIN)
4692
+ });
4693
+ const runCommand = createRunCommand(CLI_BIN);
4694
+ const sendReply = (threadId, text, replyMarkup) => client.sendMessage(cfg.supergroupId, threadId, text, replyMarkup);
4695
+ return createTelegramBridge({
4696
+ cfg,
4697
+ stateRoot,
4698
+ configRoot: dirname5(stateRoot),
4699
+ client,
4700
+ appendCaptainMessage,
4701
+ log,
4702
+ ensureCaptainAlive,
4703
+ runCommand,
4704
+ sendReply
4705
+ });
4706
+ }
3823
4707
  function startSquadrantd(opts = {}) {
3824
4708
  const ctx = buildContext(opts);
3825
4709
  const { stateRoot, store, log, spawn: spawn2, writeResult, inFlightHeadlessIds, activeHeadlessKills } = ctx;
@@ -3876,6 +4760,8 @@ function startSquadrantd(opts = {}) {
3876
4760
  ctx.codexDriver = codexDriver;
3877
4761
  ctx.opencodeBridge = opencodeBridge;
3878
4762
  ctx.cmuxEventsBridge = cmuxEventsBridge;
4763
+ const tgCfg = loadConfig().telegram;
4764
+ ctx.telegramBridge = opts.telegramBridge ?? (tgCfg && !process.env.VITEST ? buildTelegramBridge(tgCfg, stateRoot, log) : void 0);
3879
4765
  ctx.daemonCmux = opts.daemonCmux ?? (opts.makeDaemonCmux ?? (() => new DaemonCmux(createCmuxDriver())))();
3880
4766
  const launchHeadless = opts.launchHeadless ?? (async (rec) => {
3881
4767
  const ingest = (e) => void ctx.d.handle({ kind: "event", project: rec.project, event: e });