squadrant 0.10.0 → 0.11.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.
@@ -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 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 };
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(path14, "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: path14, 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(path14, applyEdits(text, edits));
203
- return { path: path14, 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,9 +364,9 @@ function sleep(ms) {
322
364
  function defaultStatePath() {
323
365
  return join4(homedir3(), ".config", "squadrant", "state", "cmux-autoconfig.json");
324
366
  }
325
- function readState(path14) {
367
+ function readState(path16) {
326
368
  try {
327
- return JSON.parse(readFileSync4(path14, "utf-8"));
369
+ return JSON.parse(readFileSync4(path16, "utf-8"));
328
370
  } catch {
329
371
  return {};
330
372
  }
@@ -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");
@@ -939,12 +981,12 @@ function withProjectLock(project, fn) {
939
981
  function appendEntry(stateRoot, project, build) {
940
982
  return withProjectLock(project, async () => {
941
983
  const dir = inboxDir(stateRoot);
942
- await fs6.mkdir(dir, { recursive: true });
984
+ await fs7.mkdir(dir, { recursive: true });
943
985
  const file = logPath(stateRoot, project);
944
986
  const lastSeq = await readMaxSeq(stateRoot, project);
945
987
  const seq = lastSeq + 1;
946
988
  const entry = build(seq);
947
- await fs6.appendFile(file, JSON.stringify(entry) + "\n", { encoding: "utf-8" });
989
+ await fs7.appendFile(file, JSON.stringify(entry) + "\n", { encoding: "utf-8" });
948
990
  return seq;
949
991
  });
950
992
  }
@@ -975,7 +1017,7 @@ function cursorPath(stateRoot, project, subscriber) {
975
1017
  async function readCursor(opts) {
976
1018
  let buf;
977
1019
  try {
978
- 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");
979
1021
  } catch (e) {
980
1022
  if (e.code === "ENOENT")
981
1023
  return null;
@@ -990,7 +1032,7 @@ async function readCursor(opts) {
990
1032
  }
991
1033
  }
992
1034
  async function writeCursor(opts) {
993
- await fs6.mkdir(inboxDir(opts.stateRoot), { recursive: true });
1035
+ await fs7.mkdir(inboxDir(opts.stateRoot), { recursive: true });
994
1036
  const dest = cursorPath(opts.stateRoot, opts.project, opts.subscriber);
995
1037
  const tmp = `${dest}.${process.pid}.${randomUUID()}.tmp`;
996
1038
  const data = {
@@ -998,7 +1040,7 @@ async function writeCursor(opts) {
998
1040
  subscriber: opts.subscriber,
999
1041
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1000
1042
  };
1001
- const handle = await fs6.open(tmp, "w");
1043
+ const handle = await fs7.open(tmp, "w");
1002
1044
  try {
1003
1045
  await handle.writeFile(JSON.stringify(data), { encoding: "utf-8" });
1004
1046
  await handle.sync();
@@ -1006,9 +1048,9 @@ async function writeCursor(opts) {
1006
1048
  await handle.close();
1007
1049
  }
1008
1050
  try {
1009
- await fs6.rename(tmp, dest);
1051
+ await fs7.rename(tmp, dest);
1010
1052
  } catch (e) {
1011
- await fs6.unlink(tmp).catch(() => {
1053
+ await fs7.unlink(tmp).catch(() => {
1012
1054
  });
1013
1055
  throw e;
1014
1056
  }
@@ -1019,7 +1061,7 @@ async function* readFromCursor(opts) {
1019
1061
  for (const file of files) {
1020
1062
  let buf;
1021
1063
  try {
1022
- buf = await fs6.readFile(file, "utf-8");
1064
+ buf = await fs7.readFile(file, "utf-8");
1023
1065
  } catch (e) {
1024
1066
  if (e.code === "ENOENT")
1025
1067
  continue;
@@ -1043,7 +1085,7 @@ async function mailboxStats(stateRoot, project) {
1043
1085
  const file = logPath(stateRoot, project);
1044
1086
  let sizeBytes = 0;
1045
1087
  try {
1046
- sizeBytes = (await fs6.stat(file)).size;
1088
+ sizeBytes = (await fs7.stat(file)).size;
1047
1089
  } catch (e) {
1048
1090
  if (e.code !== "ENOENT")
1049
1091
  throw e;
@@ -1058,7 +1100,7 @@ async function mailboxStats(stateRoot, project) {
1058
1100
  }
1059
1101
  async function oldestEntryAgeMs(file) {
1060
1102
  try {
1061
- const buf = await fs6.readFile(file, "utf-8");
1103
+ const buf = await fs7.readFile(file, "utf-8");
1062
1104
  const firstLine = buf.split("\n").find((l) => l.trim());
1063
1105
  if (!firstLine)
1064
1106
  return 0;
@@ -1073,7 +1115,7 @@ async function rotateIfNeeded(opts) {
1073
1115
  const file = logPath(opts.stateRoot, opts.project);
1074
1116
  let size = 0;
1075
1117
  try {
1076
- size = (await fs6.stat(file)).size;
1118
+ size = (await fs7.stat(file)).size;
1077
1119
  } catch (e) {
1078
1120
  if (e.code === "ENOENT")
1079
1121
  return { rotated: false };
@@ -1089,22 +1131,22 @@ async function rotateIfNeeded(opts) {
1089
1131
  const dst = `${file}.${n + 1}`;
1090
1132
  if (n + 1 > opts.keepCount) {
1091
1133
  try {
1092
- await fs6.unlink(src);
1134
+ await fs7.unlink(src);
1093
1135
  } catch (e) {
1094
1136
  if (e.code !== "ENOENT")
1095
1137
  throw e;
1096
1138
  }
1097
1139
  } else {
1098
1140
  try {
1099
- await fs6.rename(src, dst);
1141
+ await fs7.rename(src, dst);
1100
1142
  } catch (e) {
1101
1143
  if (e.code !== "ENOENT")
1102
1144
  throw e;
1103
1145
  }
1104
1146
  }
1105
1147
  }
1106
- await fs6.rename(file, `${file}.1`);
1107
- await fs6.writeFile(file, "", { encoding: "utf-8" });
1148
+ await fs7.rename(file, `${file}.1`);
1149
+ await fs7.writeFile(file, "", { encoding: "utf-8" });
1108
1150
  return { rotated: true, from: file, to: `${file}.1` };
1109
1151
  });
1110
1152
  }
@@ -2078,10 +2120,10 @@ function distBuiltAt() {
2078
2120
  return 0;
2079
2121
  }
2080
2122
  }
2081
- function gatherLogStats(path14, now, windowMs) {
2123
+ function gatherLogStats(path16, now, windowMs) {
2082
2124
  let sizeBytes = 0;
2083
2125
  try {
2084
- sizeBytes = statSync2(path14).size;
2126
+ sizeBytes = statSync2(path16).size;
2085
2127
  } catch {
2086
2128
  return { errorCount: 0, sizeBytes: 0, windowMs };
2087
2129
  }
@@ -2092,7 +2134,7 @@ function gatherLogStats(path14, now, windowMs) {
2092
2134
  const len = sizeBytes - start;
2093
2135
  let text = "";
2094
2136
  try {
2095
- const fd = openSync2(path14, "r");
2137
+ const fd = openSync2(path16, "r");
2096
2138
  try {
2097
2139
  const buf = Buffer.alloc(len);
2098
2140
  readSync(fd, buf, 0, len, start);
@@ -2417,8 +2459,8 @@ function startDaemon(ctx, opts, pkgVersion) {
2417
2459
 
2418
2460
  // packages/core/dist/session-freshness.js
2419
2461
  import crypto from "crypto";
2420
- import fs7 from "fs";
2421
- import path6 from "path";
2462
+ import fs8 from "fs";
2463
+ import path7 from "path";
2422
2464
 
2423
2465
  // packages/core/dist/crew-lifecycle.js
2424
2466
  import { exec as nodeExec } from "child_process";
@@ -2498,19 +2540,30 @@ var REGISTRY = {
2498
2540
  return usage("spawn", "usage: /spawn <project> <task...>");
2499
2541
  return ok("spawn", ["crew", "spawn", project, task]);
2500
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>")
2501
2551
  }
2502
2552
  };
2503
2553
  function helpText() {
2504
2554
  const lines = Object.values(REGISTRY).map((e) => ` ${e.usage}`);
2505
2555
  return ["Available commands:", ...lines, " /help"].join("\n");
2506
2556
  }
2557
+ function stripBotMention(token) {
2558
+ return token.split("@")[0];
2559
+ }
2507
2560
  function parseCommand(text) {
2508
2561
  const trimmed = text.trim();
2509
2562
  if (!trimmed.startsWith("/")) {
2510
2563
  return { kind: "unknown", message: "unknown command \u2014 send /help" };
2511
2564
  }
2512
2565
  const tokens = trimmed.slice(1).split(/\s+/).filter((t) => t.length > 0);
2513
- const name = (tokens[0] ?? "").toLowerCase();
2566
+ const name = stripBotMention(tokens[0] ?? "").toLowerCase();
2514
2567
  const args = tokens.slice(1);
2515
2568
  if (name === "help") {
2516
2569
  return { kind: "usage", name: "help", message: helpText() };
@@ -2568,6 +2621,17 @@ ${ev.message}` : "");
2568
2621
  ${ev.question}`;
2569
2622
  case "task.idle":
2570
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}`;
2571
2635
  default:
2572
2636
  return `\u2139\uFE0F [${project}] ${ev.type} \xB7 ${ev.id}`;
2573
2637
  }
@@ -2577,35 +2641,49 @@ function formatInbound(text) {
2577
2641
  }
2578
2642
 
2579
2643
  // packages/core/dist/telegram/state.js
2580
- import fs8 from "fs";
2581
- import path7 from "path";
2644
+ import fs9 from "fs";
2645
+ import path8 from "path";
2582
2646
  function statePath(stateRoot) {
2583
- return path7.join(stateRoot, "telegram-state.json");
2647
+ return path8.join(stateRoot, "telegram-state.json");
2584
2648
  }
2585
2649
  function topicKey(project, scope = "project") {
2586
2650
  return `${project}::${scope}`;
2587
2651
  }
2588
2652
  function loadState(stateRoot) {
2589
2653
  try {
2590
- const raw = fs8.readFileSync(statePath(stateRoot), "utf-8");
2654
+ const raw = fs9.readFileSync(statePath(stateRoot), "utf-8");
2591
2655
  const data = JSON.parse(raw);
2592
- return {
2656
+ const result = {
2593
2657
  offset: typeof data.offset === "number" ? data.offset : 0,
2594
- topics: data.topics ?? {}
2658
+ topics: data.topics ?? {},
2659
+ notify: data.notify ?? {}
2595
2660
  };
2661
+ if (typeof data.lastUserId === "number")
2662
+ result.lastUserId = data.lastUserId;
2663
+ return result;
2596
2664
  } catch {
2597
- return { offset: 0, topics: {} };
2665
+ return { offset: 0, topics: {}, notify: {} };
2598
2666
  }
2599
2667
  }
2600
2668
  function saveState(stateRoot, s) {
2601
- fs8.mkdirSync(stateRoot, { recursive: true });
2602
- fs8.writeFileSync(statePath(stateRoot), JSON.stringify(s, null, 2) + "\n");
2669
+ fs9.mkdirSync(stateRoot, { recursive: true });
2670
+ fs9.writeFileSync(statePath(stateRoot), JSON.stringify(s, null, 2) + "\n");
2603
2671
  }
2604
2672
  function setTopic(stateRoot, project, topicId, scope = "project") {
2605
2673
  const s = loadState(stateRoot);
2606
2674
  s.topics[topicKey(project, scope)] = topicId;
2607
2675
  saveState(stateRoot, s);
2608
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
+ }
2609
2687
  function findProjectByThread(stateRoot, threadId) {
2610
2688
  const s = loadState(stateRoot);
2611
2689
  for (const [key, id] of Object.entries(s.topics)) {
@@ -2645,24 +2723,141 @@ function createTelegramClient(opts) {
2645
2723
  getUpdates(offset, timeoutSec = 50) {
2646
2724
  return call("getUpdates", { offset, timeout: timeoutSec });
2647
2725
  },
2648
- async sendMessage(chatId, threadId, text) {
2726
+ async sendMessage(chatId, threadId, text, replyMarkup) {
2649
2727
  const body = { chat_id: chatId, text };
2650
2728
  if (threadId !== void 0)
2651
2729
  body.message_thread_id = threadId;
2730
+ if (replyMarkup !== void 0)
2731
+ body.reply_markup = replyMarkup;
2652
2732
  await call("sendMessage", body);
2653
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
+ },
2654
2743
  async createForumTopic(chatId, name) {
2655
2744
  const r = await call("createForumTopic", { chat_id: chatId, name });
2656
2745
  return r.message_thread_id;
2746
+ },
2747
+ async setMyCommands(commands) {
2748
+ await call("setMyCommands", { commands });
2657
2749
  }
2658
2750
  };
2659
2751
  }
2660
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
+
2661
2829
  // packages/core/dist/telegram/bridge.js
2662
2830
  var LONG_POLL_SEC = 50;
2663
2831
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
2832
+ var CREW_TIERS = ["all", "alert_only", "done_only", "none"];
2833
+ var RECOGNIZED_CHANNEL_COMMANDS = /* @__PURE__ */ new Set(["status", "projects", "crews", "launch", "effort", "spawn"]);
2834
+ function parseNotifyPref(text) {
2835
+ const parts = text.trim().split(/\s+/);
2836
+ if (stripBotMention(parts[0] ?? "").toLowerCase() !== "/notify")
2837
+ return null;
2838
+ const dimension = parts[1]?.toLowerCase();
2839
+ if ((dimension === "crew" || dimension === "cap") && parts[2])
2840
+ return { dimension, value: parts[2].toLowerCase() };
2841
+ return null;
2842
+ }
2843
+ function isBareSpawn(text) {
2844
+ const trimmed = text.trim();
2845
+ if (!trimmed.startsWith("/"))
2846
+ return false;
2847
+ const tokens = trimmed.slice(1).split(/\s+/).filter((t) => t.length > 0);
2848
+ return stripBotMention(tokens[0] ?? "").toLowerCase() === "spawn" && tokens.length === 1;
2849
+ }
2850
+ function notifyToggle(text) {
2851
+ const first = stripBotMention(text.trim().split(/\s+/)[0] ?? "").toLowerCase();
2852
+ if (first === "/unmute")
2853
+ return true;
2854
+ if (first === "/mute")
2855
+ return false;
2856
+ return null;
2857
+ }
2664
2858
  function createTelegramBridge(opts) {
2665
2859
  const { cfg, stateRoot, client, appendCaptainMessage: appendCaptainMessage2, log, ensureCaptainAlive, runCommand, sendReply } = opts;
2860
+ const configRoot = opts.configRoot ?? path9.join(os3.homedir(), ".config", "squadrant");
2666
2861
  const pollMs = cfg.pollMs ?? 1e3;
2667
2862
  let running = false;
2668
2863
  function persistOffset(next) {
@@ -2671,6 +2866,13 @@ function createTelegramBridge(opts) {
2671
2866
  saveState(stateRoot, s);
2672
2867
  }
2673
2868
  async function deliverOutbound(project, ev) {
2869
+ const resolved = resolveNotify(cfg.notify, loadProjectOverride(project, configRoot));
2870
+ const live = loadState(stateRoot).notify[project];
2871
+ const active = live ?? resolved.active;
2872
+ if (!active)
2873
+ return;
2874
+ if (!tierIncludes(resolved.crew, ev.type))
2875
+ return;
2674
2876
  let threadId = loadState(stateRoot).topics[topicKey(project)];
2675
2877
  if (threadId === void 0) {
2676
2878
  threadId = await client.createForumTopic(cfg.supergroupId, topicName(project));
@@ -2678,41 +2880,225 @@ function createTelegramBridge(opts) {
2678
2880
  }
2679
2881
  await client.sendMessage(cfg.supergroupId, threadId, formatLifecycle(project, ev));
2680
2882
  }
2681
- async function reply(threadId, text) {
2883
+ function resolveLiveNotify(project) {
2884
+ const resolved = resolveNotify(cfg.notify, loadProjectOverride(project, configRoot));
2885
+ const live = loadState(stateRoot).notify[project];
2886
+ return { ...resolved, active: live ?? resolved.active };
2887
+ }
2888
+ async function editMarkup(chatId, messageId, markup) {
2889
+ try {
2890
+ await client.editMessageReplyMarkup(chatId, messageId, markup);
2891
+ } catch (e) {
2892
+ const msg = e.message;
2893
+ if (!/not modified/i.test(msg))
2894
+ throw e;
2895
+ }
2896
+ }
2897
+ async function handleCallback(cq) {
2898
+ try {
2899
+ if (!cq.data || !cq.message) {
2900
+ await client.answerCallbackQuery(cq.id);
2901
+ return;
2902
+ }
2903
+ if (!isControlEnabled(cfg) || !isAuthorized(cq.from?.id, cfg)) {
2904
+ await client.answerCallbackQuery(cq.id, "\u26D4 not authorized");
2905
+ return;
2906
+ }
2907
+ const action = parseCallback(cq.data);
2908
+ if (!action) {
2909
+ await client.answerCallbackQuery(cq.id);
2910
+ return;
2911
+ }
2912
+ const chatId = cq.message.chat.id;
2913
+ const messageId = cq.message.message_id;
2914
+ if (action.t === "notify") {
2915
+ const resolved = findProjectByThread(stateRoot, cq.message.message_thread_id ?? -1);
2916
+ if (!resolved) {
2917
+ await client.answerCallbackQuery(cq.id, "no project for this topic");
2918
+ return;
2919
+ }
2920
+ const project2 = resolved.project;
2921
+ if (action.dim === "active") {
2922
+ setNotify(stateRoot, project2, action.val === "on");
2923
+ } else if (action.dim === "cap") {
2924
+ saveProjectOverride(project2, { telegram: { notify: { cap: action.val === "on" } } }, configRoot);
2925
+ } else {
2926
+ saveProjectOverride(project2, { telegram: { notify: { crew: action.val } } }, configRoot);
2927
+ }
2928
+ await client.answerCallbackQuery(cq.id, `\u2705 ${action.dim} = ${action.val}`);
2929
+ await editMarkup(chatId, messageId, notifyPanel(resolveLiveNotify(project2)));
2930
+ return;
2931
+ }
2932
+ if (action.t === "effort") {
2933
+ if (runCommand)
2934
+ await runCommand(["effort", action.mode]);
2935
+ await client.answerCallbackQuery(cq.id, `\u2705 effort = ${action.mode}`);
2936
+ await editMarkup(chatId, messageId, effortPanel(action.mode));
2937
+ return;
2938
+ }
2939
+ if (action.t === "spawn") {
2940
+ await reply(cq.message.message_thread_id, buildSpawnPrompt(action.project), { force_reply: true, selective: true });
2941
+ await client.answerCallbackQuery(cq.id);
2942
+ return;
2943
+ }
2944
+ const { action: act, project } = action;
2945
+ if (act === "cr") {
2946
+ const out = runCommand ? await runCommand(["crew", "list", project]) : "(command runner unavailable)";
2947
+ await client.answerCallbackQuery(cq.id);
2948
+ await reply(void 0, out);
2949
+ } else if (act === "lc") {
2950
+ if (runCommand)
2951
+ await runCommand(["launch", project]);
2952
+ await client.answerCallbackQuery(cq.id, `launching ${project}`);
2953
+ } else if (act === "mu") {
2954
+ setNotify(stateRoot, project, false);
2955
+ await client.answerCallbackQuery(cq.id, `\u{1F515} muted ${project}`);
2956
+ } else {
2957
+ setNotify(stateRoot, project, true);
2958
+ await client.answerCallbackQuery(cq.id, `\u{1F514} unmuted ${project}`);
2959
+ }
2960
+ } catch (e) {
2961
+ log(`telegram callback failed data=${cq.data}: ${e.message}`);
2962
+ try {
2963
+ await client.answerCallbackQuery(cq.id, "\u26A0\uFE0F failed");
2964
+ } catch {
2965
+ }
2966
+ }
2967
+ }
2968
+ async function reply(threadId, text, replyMarkup) {
2682
2969
  if (!sendReply)
2683
2970
  return;
2684
2971
  try {
2685
- await sendReply(threadId, text);
2972
+ if (replyMarkup !== void 0)
2973
+ await sendReply(threadId, text, replyMarkup);
2974
+ else
2975
+ await sendReply(threadId, text);
2686
2976
  } catch (e) {
2687
2977
  log(`telegram reply failed: ${e.message}`);
2688
2978
  }
2689
2979
  }
2690
- async function handleGeneral(text, fromId) {
2691
- if (!text.startsWith("/")) {
2692
- await reply(void 0, "Send /help for commands.");
2980
+ function currentEffort() {
2981
+ try {
2982
+ return loadConfig(path9.join(configRoot, "config.json")).defaults.effort ?? "balance";
2983
+ } catch {
2984
+ return "balance";
2985
+ }
2986
+ }
2987
+ function projectNames() {
2988
+ try {
2989
+ return Object.keys(loadConfig(path9.join(configRoot, "config.json")).projects);
2990
+ } catch {
2991
+ return [];
2992
+ }
2993
+ }
2994
+ async function replySpawnPicker(threadId) {
2995
+ const projects = projectNames();
2996
+ if (projects.length === 0) {
2997
+ await reply(threadId, "no projects registered");
2693
2998
  return;
2694
2999
  }
3000
+ await reply(threadId, "Pick a project to spawn a crew on:", spawnPicker(projects));
3001
+ }
3002
+ async function runChannelCommand(text, fromId, threadId) {
2695
3003
  if (!isControlEnabled(cfg) || !isAuthorized(fromId, cfg)) {
2696
- await reply(void 0, "\u26D4 not authorized");
3004
+ await reply(threadId, "\u26D4 not authorized");
3005
+ return;
3006
+ }
3007
+ const tokens = text.trim().slice(1).split(/\s+/).filter((t) => t.length > 0);
3008
+ const name = stripBotMention(tokens[0] ?? "").toLowerCase();
3009
+ const noArg = tokens.length === 1;
3010
+ if (noArg && name === "effort") {
3011
+ await reply(threadId, "Effort mode:", effortPanel(currentEffort()));
3012
+ return;
3013
+ }
3014
+ if (noArg && name === "spawn") {
3015
+ await replySpawnPicker(threadId);
3016
+ return;
3017
+ }
3018
+ const PICKERS = { crews: "cr", launch: "lc", mute: "mu", unmute: "um" };
3019
+ if (noArg && name in PICKERS) {
3020
+ const projects = projectNames();
3021
+ if (projects.length === 0) {
3022
+ await reply(threadId, "no projects registered");
3023
+ return;
3024
+ }
3025
+ await reply(threadId, `Pick a project:`, projectPicker(PICKERS[name], projects));
2697
3026
  return;
2698
3027
  }
2699
3028
  const parsed = parseCommand(text);
2700
3029
  if (parsed.kind !== "ok") {
2701
- await reply(void 0, parsed.message);
3030
+ await reply(threadId, parsed.message);
2702
3031
  return;
2703
3032
  }
2704
3033
  try {
2705
3034
  const out = runCommand ? await runCommand(parsed.argv) : "(command runner unavailable)";
2706
- await reply(void 0, out);
3035
+ await reply(threadId, out);
2707
3036
  } catch (e) {
2708
- await reply(void 0, `\u26A0\uFE0F command failed: ${e.message}`);
3037
+ await reply(threadId, `\u26A0\uFE0F command failed: ${e.message}`);
2709
3038
  log(`telegram command failed argv=${JSON.stringify(parsed.argv)}: ${e.message}`);
2710
3039
  }
2711
3040
  }
3041
+ async function handleGeneral(text, fromId) {
3042
+ if (!text.startsWith("/")) {
3043
+ await reply(void 0, "Send /help for commands.");
3044
+ return;
3045
+ }
3046
+ await runChannelCommand(text, fromId, void 0);
3047
+ }
2712
3048
  async function handleProjectTopic(text, threadId, fromId) {
2713
3049
  const resolved = findProjectByThread(stateRoot, threadId);
2714
3050
  if (!resolved)
2715
3051
  return;
3052
+ if (isBareSpawn(text)) {
3053
+ if (!isControlEnabled(cfg) || !isAuthorized(fromId, cfg)) {
3054
+ await reply(threadId, "\u26D4 not authorized");
3055
+ return;
3056
+ }
3057
+ await replySpawnPicker(threadId);
3058
+ return;
3059
+ }
3060
+ const toggle = notifyToggle(text);
3061
+ if (toggle !== null) {
3062
+ if (!isControlEnabled(cfg) || !isAuthorized(fromId, cfg)) {
3063
+ await reply(threadId, "\u26D4 not authorized");
3064
+ return;
3065
+ }
3066
+ setNotify(stateRoot, resolved.project, toggle);
3067
+ await reply(threadId, toggle ? `\u{1F514} ${resolved.project} notifications ON` : `\u{1F515} ${resolved.project} notifications OFF`);
3068
+ return;
3069
+ }
3070
+ if (stripBotMention(text.trim().split(/\s+/)[0] ?? "").toLowerCase() === "/notify") {
3071
+ if (!isControlEnabled(cfg) || !isAuthorized(fromId, cfg)) {
3072
+ await reply(threadId, "\u26D4 not authorized");
3073
+ return;
3074
+ }
3075
+ const pref = parseNotifyPref(text);
3076
+ if (pref === null) {
3077
+ await reply(threadId, `\u{1F514} ${resolved.project} notifications`, notifyPanel(resolveLiveNotify(resolved.project)));
3078
+ return;
3079
+ }
3080
+ if (pref.dimension === "crew") {
3081
+ if (!CREW_TIERS.includes(pref.value)) {
3082
+ await reply(threadId, "crew must be all|alert_only|done_only|none");
3083
+ return;
3084
+ }
3085
+ saveProjectOverride(resolved.project, { telegram: { notify: { crew: pref.value } } }, configRoot);
3086
+ } else {
3087
+ if (pref.value !== "on" && pref.value !== "off") {
3088
+ await reply(threadId, "cap must be on|off");
3089
+ return;
3090
+ }
3091
+ saveProjectOverride(resolved.project, { telegram: { notify: { cap: pref.value === "on" } } }, configRoot);
3092
+ }
3093
+ await reply(threadId, `\u2705 ${pref.dimension} = ${pref.value}`);
3094
+ return;
3095
+ }
3096
+ const firstTok = stripBotMention(text.trim().split(/\s+/)[0] ?? "").toLowerCase();
3097
+ if (firstTok.startsWith("/") && RECOGNIZED_CHANNEL_COMMANDS.has(firstTok.slice(1))) {
3098
+ await runChannelCommand(text, fromId, threadId);
3099
+ return;
3100
+ }
3101
+ setNotify(stateRoot, resolved.project, true);
2716
3102
  if (ensureCaptainAlive && isControlEnabled(cfg) && isAuthorized(fromId, cfg)) {
2717
3103
  try {
2718
3104
  const r = await ensureCaptainAlive(resolved.project);
@@ -2725,11 +3111,35 @@ function createTelegramBridge(opts) {
2725
3111
  await appendCaptainMessage2({ stateRoot, project: resolved.project, text: formatInbound(text), source: "telegram" });
2726
3112
  }
2727
3113
  async function handleUpdate(u) {
3114
+ if (u.callback_query) {
3115
+ await handleCallback(u.callback_query);
3116
+ return;
3117
+ }
2728
3118
  const m = u.message;
2729
3119
  if (!m || m.text === void 0)
2730
3120
  return;
2731
3121
  if (!cfg.chats.includes(m.chat.id))
2732
3122
  return;
3123
+ if (m.from?.id !== void 0 && loadState(stateRoot).lastUserId !== m.from.id) {
3124
+ setLastUserId(stateRoot, m.from.id);
3125
+ }
3126
+ const spawnProject = parseSpawnPrompt(m.reply_to_message?.text);
3127
+ if (spawnProject) {
3128
+ const threadId = m.message_thread_id;
3129
+ if (!isControlEnabled(cfg) || !isAuthorized(m.from?.id, cfg)) {
3130
+ await reply(threadId, "\u26D4 not authorized");
3131
+ return;
3132
+ }
3133
+ const task = m.text.trim();
3134
+ if (!task) {
3135
+ await reply(threadId, "spawn cancelled \u2014 empty task");
3136
+ return;
3137
+ }
3138
+ if (runCommand)
3139
+ await runCommand(["crew", "spawn", spawnProject, task]);
3140
+ await reply(threadId, `\u{1F195} spawning a crew on ${spawnProject}\u2026`);
3141
+ return;
3142
+ }
2733
3143
  if (m.message_thread_id === void 0) {
2734
3144
  await handleGeneral(m.text, m.from?.id);
2735
3145
  return;
@@ -2771,7 +3181,7 @@ function createTelegramBridge(opts) {
2771
3181
  }
2772
3182
 
2773
3183
  // packages/core/dist/telegram/setup.js
2774
- import fs9 from "fs";
3184
+ import fs10 from "fs";
2775
3185
 
2776
3186
  // packages/cli/src/control/telegram-control.ts
2777
3187
  import { execFile } from "child_process";
@@ -2834,28 +3244,28 @@ import { execSync as execSync4 } from "child_process";
2834
3244
  import { execSync as execSync5 } from "child_process";
2835
3245
 
2836
3246
  // packages/agents/dist/drivers/launch-cmd.js
2837
- import fs10 from "fs";
2838
- import path8 from "path";
3247
+ import fs11 from "fs";
3248
+ import path10 from "path";
2839
3249
 
2840
3250
  // packages/agents/dist/projection/cursor.js
2841
3251
  import { mkdir, readFile, writeFile } from "fs/promises";
2842
- import path9 from "path";
2843
- import os2 from "os";
3252
+ import path11 from "path";
3253
+ import os4 from "os";
2844
3254
 
2845
3255
  // packages/agents/dist/projection/codex.js
2846
3256
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
2847
- import path10 from "path";
2848
- import os3 from "os";
3257
+ import path12 from "path";
3258
+ import os5 from "os";
2849
3259
 
2850
3260
  // packages/agents/dist/projection/gemini.js
2851
3261
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
2852
- import path11 from "path";
2853
- import os4 from "os";
3262
+ import path13 from "path";
3263
+ import os6 from "os";
2854
3264
 
2855
3265
  // packages/agents/dist/projection/opencode.js
2856
3266
  import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
2857
- import path12 from "path";
2858
- import os5 from "os";
3267
+ import path14 from "path";
3268
+ import os7 from "os";
2859
3269
 
2860
3270
  // packages/agents/dist/codex/app-server-client.js
2861
3271
  import { EventEmitter } from "events";
@@ -4071,9 +4481,9 @@ function createCmuxDriver() {
4071
4481
  import { execFileSync as execFileSync5, execSync as execSync7 } from "child_process";
4072
4482
 
4073
4483
  // packages/workspaces/dist/workspaces/obsidian.js
4074
- import fs11 from "fs/promises";
4484
+ import fs12 from "fs/promises";
4075
4485
  import { existsSync as existsSync8 } from "fs";
4076
- import path13 from "path";
4486
+ import path15 from "path";
4077
4487
 
4078
4488
  // packages/workspaces/dist/cmux/events-bridge.js
4079
4489
  import { spawn as nodeSpawn2 } from "child_process";
@@ -4290,10 +4700,11 @@ function buildTelegramBridge(cfg, stateRoot, log) {
4290
4700
  launch: createLaunch(CLI_BIN)
4291
4701
  });
4292
4702
  const runCommand = createRunCommand(CLI_BIN);
4293
- const sendReply = (threadId, text) => client.sendMessage(cfg.supergroupId, threadId, text);
4703
+ const sendReply = (threadId, text, replyMarkup) => client.sendMessage(cfg.supergroupId, threadId, text, replyMarkup);
4294
4704
  return createTelegramBridge({
4295
4705
  cfg,
4296
4706
  stateRoot,
4707
+ configRoot: dirname5(stateRoot),
4297
4708
  client,
4298
4709
  appendCaptainMessage,
4299
4710
  log,