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.
- package/README.md +44 -0
- package/dist/index.js +649 -193
- package/dist/index.js.map +1 -1
- package/dist/squadrantd.js +535 -51
- package/dist/squadrantd.js.map +1 -1
- package/package.json +4 -3
- package/plugin/skills/captain-ops/SKILL.md +69 -0
package/dist/squadrantd.js
CHANGED
|
@@ -185,22 +185,22 @@ var MINIMAL_TEMPLATE = [
|
|
|
185
185
|
``
|
|
186
186
|
].join("\n");
|
|
187
187
|
function ensureSocketAutomation(opts = {}) {
|
|
188
|
-
const
|
|
189
|
-
if (!existsSync(
|
|
190
|
-
mkdirSync(dirname(
|
|
191
|
-
writeFileSync(
|
|
192
|
-
return { path:
|
|
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(
|
|
194
|
+
const text = readFileSync(path14, "utf-8");
|
|
195
195
|
const current = parse(text)?.automation?.socketControlMode;
|
|
196
196
|
if (current === AUTOMATION_MODE) {
|
|
197
|
-
return { path:
|
|
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(
|
|
203
|
-
return { path:
|
|
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(
|
|
325
|
+
function readState(path14) {
|
|
326
326
|
try {
|
|
327
|
-
return JSON.parse(readFileSync4(
|
|
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
|
|
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(
|
|
341
|
+
const already = readState(statePath2).promptedRestart === true;
|
|
342
342
|
if (!already) {
|
|
343
|
-
mkdirSync2(dirname2(
|
|
344
|
-
writeFileSync3(
|
|
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(
|
|
349
|
-
rmSync2(
|
|
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
|
-
|
|
940
|
-
return withProjectLock(
|
|
941
|
-
const dir = inboxDir(
|
|
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(
|
|
944
|
-
const lastSeq = await readMaxSeq(
|
|
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(
|
|
2081
|
+
function gatherLogStats(path14, now, windowMs) {
|
|
2039
2082
|
let sizeBytes = 0;
|
|
2040
2083
|
try {
|
|
2041
|
-
sizeBytes = statSync2(
|
|
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(
|
|
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
|
|
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
|
|
2382
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
4074
|
+
import fs11 from "fs/promises";
|
|
3619
4075
|
import { existsSync as existsSync8 } from "fs";
|
|
3620
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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 });
|