squadrant 0.21.0 → 0.22.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/dist/index.js +1612 -368
- package/dist/index.js.map +1 -1
- package/dist/squadrantd.js +929 -192
- package/dist/squadrantd.js.map +1 -1
- package/package.json +1 -1
package/dist/squadrantd.js
CHANGED
|
@@ -135,12 +135,7 @@ function getDefaultConfig() {
|
|
|
135
135
|
// resolver fallback is the safety net).
|
|
136
136
|
controlChannel: { claude: "on", opencode: "on" },
|
|
137
137
|
crewRouting: {
|
|
138
|
-
rules:
|
|
139
|
-
{ tier: "extreme", match: "redesign|architect|rewrite|from scratch|deep reasoning", agent: "claude", model: "opus" },
|
|
140
|
-
{ tier: "hard", match: "refactor|migrate|implement|feature|daemon|control-plane", agent: "claude", model: "sonnet" },
|
|
141
|
-
{ tier: "mobile", match: "mobile|ios|swift|android|kotlin|react native", agent: "codex" },
|
|
142
|
-
{ tier: "daily", match: "typo|rename|bump|docs|comment|lint|format", agent: "opencode" }
|
|
143
|
-
]
|
|
138
|
+
rules: DEFAULT_CREW_ROUTING_RULES.map((r) => ({ ...r }))
|
|
144
139
|
}
|
|
145
140
|
},
|
|
146
141
|
metrics: {
|
|
@@ -182,7 +177,7 @@ function saveConfig(config, configPath = DEFAULT_CONFIG_PATH) {
|
|
|
182
177
|
function resolveHome(p) {
|
|
183
178
|
return p.startsWith("~") ? p.replace("~", os.homedir()) : p;
|
|
184
179
|
}
|
|
185
|
-
var GATE_MODES, GATE_POLICIES, DEFAULT_CONFIG_PATH, CONFIG_DIR, DAEMON_SOCK_PATH;
|
|
180
|
+
var GATE_MODES, GATE_POLICIES, DEFAULT_CONFIG_PATH, CONFIG_DIR, DAEMON_SOCK_PATH, DEFAULT_CREW_ROUTING_RULES;
|
|
186
181
|
var init_config = __esm({
|
|
187
182
|
"packages/shared/dist/config.js"() {
|
|
188
183
|
init_config_io();
|
|
@@ -191,6 +186,19 @@ var init_config = __esm({
|
|
|
191
186
|
DEFAULT_CONFIG_PATH = process.env.SQUADRANT_CONFIG || path2.join(os.homedir(), ".config", "squadrant", "config.json");
|
|
192
187
|
CONFIG_DIR = path2.dirname(DEFAULT_CONFIG_PATH);
|
|
193
188
|
DAEMON_SOCK_PATH = path2.join(CONFIG_DIR, "squadrant.sock");
|
|
189
|
+
DEFAULT_CREW_ROUTING_RULES = [
|
|
190
|
+
{ tier: "extreme", match: "redesign|architect|rewrite|from scratch|deep reasoning", agent: "claude", model: "opus" },
|
|
191
|
+
{ tier: "hard", match: "refactor|migrate|implement|feature|daemon|control-plane", agent: "claude", model: "sonnet" },
|
|
192
|
+
{ tier: "mobile", match: "mobile|ios|swift|android|kotlin|react native", agent: "codex" },
|
|
193
|
+
{ tier: "daily", match: "typo|rename|bump|docs|comment|lint|format", agent: "opencode" }
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// packages/shared/dist/provider-preset.js
|
|
199
|
+
var init_provider_preset = __esm({
|
|
200
|
+
"packages/shared/dist/provider-preset.js"() {
|
|
201
|
+
init_config();
|
|
194
202
|
}
|
|
195
203
|
});
|
|
196
204
|
|
|
@@ -790,6 +798,7 @@ var init_daemon_keys = __esm({
|
|
|
790
798
|
var init_dist = __esm({
|
|
791
799
|
"packages/shared/dist/index.js"() {
|
|
792
800
|
init_config();
|
|
801
|
+
init_provider_preset();
|
|
793
802
|
init_project_config();
|
|
794
803
|
init_effort();
|
|
795
804
|
init_router_model();
|
|
@@ -2027,8 +2036,8 @@ function reconcileLiveness(prev, next) {
|
|
|
2027
2036
|
if (!prev)
|
|
2028
2037
|
return next;
|
|
2029
2038
|
if (next.source === "scan") {
|
|
2030
|
-
const
|
|
2031
|
-
return { ...prev, pidAlive, lastSeenAt: Math.max(prev.lastSeenAt, next.lastSeenAt) };
|
|
2039
|
+
const pidAlive2 = next.lastSeenAt >= prev.lastSeenAt ? next.pidAlive : prev.pidAlive;
|
|
2040
|
+
return { ...prev, pidAlive: pidAlive2, lastSeenAt: Math.max(prev.lastSeenAt, next.lastSeenAt) };
|
|
2032
2041
|
}
|
|
2033
2042
|
if (next.startedAt >= prev.startedAt || next.lastState === "end")
|
|
2034
2043
|
return next;
|
|
@@ -2774,6 +2783,68 @@ function createDirectSurfaceLivenessProbe(cmux2, getCaptainTitle) {
|
|
|
2774
2783
|
}
|
|
2775
2784
|
};
|
|
2776
2785
|
}
|
|
2786
|
+
function hasModalOptionList(screen) {
|
|
2787
|
+
if (!screen)
|
|
2788
|
+
return false;
|
|
2789
|
+
const lines = screen.split(/\r?\n/);
|
|
2790
|
+
let bottomHR = -1;
|
|
2791
|
+
let topHR = -1;
|
|
2792
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
2793
|
+
if (HR_RE.test(lines[i])) {
|
|
2794
|
+
if (bottomHR === -1)
|
|
2795
|
+
bottomHR = i;
|
|
2796
|
+
else {
|
|
2797
|
+
topHR = i;
|
|
2798
|
+
break;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
if (topHR === -1)
|
|
2803
|
+
return false;
|
|
2804
|
+
return lines.slice(topHR + 1, bottomHR).some((l) => /^\s*\d+\.\s/.test(l));
|
|
2805
|
+
}
|
|
2806
|
+
function classifyCaptainPane(screen, opts = {}) {
|
|
2807
|
+
if (screen == null || screen.trim() === "")
|
|
2808
|
+
return { status: "unreadable" };
|
|
2809
|
+
if (hasModalOptionList(screen))
|
|
2810
|
+
return { status: "locked" };
|
|
2811
|
+
if (WORKING_RE.test(screen))
|
|
2812
|
+
return { status: "mid-turn" };
|
|
2813
|
+
const idleAfterMs = opts.idleAfterMs ?? 0;
|
|
2814
|
+
const stale = opts.outputAgoMs !== void 0 && opts.outputAgoMs >= idleAfterMs;
|
|
2815
|
+
return stale ? { status: "no-session" } : { status: "mid-turn" };
|
|
2816
|
+
}
|
|
2817
|
+
function screenHash(screen) {
|
|
2818
|
+
let h = 2166136261;
|
|
2819
|
+
for (let i = 0; i < screen.length; i++) {
|
|
2820
|
+
h ^= screen.charCodeAt(i);
|
|
2821
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
2822
|
+
}
|
|
2823
|
+
return h.toString(16).padStart(8, "0");
|
|
2824
|
+
}
|
|
2825
|
+
function createCaptainPaneReader(cmux2, getCaptainTitle, opts = {}) {
|
|
2826
|
+
const idleAfterMs = opts.idleAfterMs ?? PANE_IDLE_AFTER_MS;
|
|
2827
|
+
return async (project) => {
|
|
2828
|
+
try {
|
|
2829
|
+
const wsId = await cmux2.findWorkspaceId(getCaptainTitle(project));
|
|
2830
|
+
if (!wsId)
|
|
2831
|
+
return { status: "no-session" };
|
|
2832
|
+
const surfaces = await cmux2.listSurfaces(wsId);
|
|
2833
|
+
const want = getCaptainTitle(project);
|
|
2834
|
+
const pane = surfaces.find((s) => s.title === want) ?? surfaces[0];
|
|
2835
|
+
if (!pane)
|
|
2836
|
+
return { status: "no-session" };
|
|
2837
|
+
const screen = await cmux2.readPaneScreen(pane);
|
|
2838
|
+
if (screen == null)
|
|
2839
|
+
return { status: "unreadable" };
|
|
2840
|
+
const outputAgoMs = opts.noteScreen?.(project, screenHash(screen));
|
|
2841
|
+
return classifyCaptainPane(screen, { outputAgoMs, idleAfterMs });
|
|
2842
|
+
} catch (e) {
|
|
2843
|
+
opts.log?.(`captain pane read failed project=${project}: ${e.message}`);
|
|
2844
|
+
return { status: "unreadable" };
|
|
2845
|
+
}
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2777
2848
|
function createDirectCrewPaneReader(cmux2, getCaptainTitle) {
|
|
2778
2849
|
return async (rec) => {
|
|
2779
2850
|
try {
|
|
@@ -2796,11 +2867,14 @@ function createDirectCrewPaneReader(cmux2, getCaptainTitle) {
|
|
|
2796
2867
|
}
|
|
2797
2868
|
};
|
|
2798
2869
|
}
|
|
2799
|
-
var TAIL_LINES;
|
|
2870
|
+
var TAIL_LINES, HR_RE, WORKING_RE, PANE_IDLE_AFTER_MS;
|
|
2800
2871
|
var init_crew_pane_reader = __esm({
|
|
2801
2872
|
"packages/core/dist/crew-pane-reader.js"() {
|
|
2802
2873
|
init_dist();
|
|
2803
2874
|
TAIL_LINES = 25;
|
|
2875
|
+
HR_RE = /^\s*─{10,}\s*$/;
|
|
2876
|
+
WORKING_RE = /esc to interrupt|\btokens?\b.*\b(used|left)\b|↓\s*[\d.]+\s*k?\s*tokens?\b|\(\d+m?\s*\d+s\b/i;
|
|
2877
|
+
PANE_IDLE_AFTER_MS = 10 * 6e4;
|
|
2804
2878
|
}
|
|
2805
2879
|
});
|
|
2806
2880
|
|
|
@@ -3471,6 +3545,11 @@ function assertBackendUsable(o) {
|
|
|
3471
3545
|
function shouldBuildRouterService(router, isVitest) {
|
|
3472
3546
|
return !!router && !isVitest;
|
|
3473
3547
|
}
|
|
3548
|
+
function claudeEnvShadowsRouter(claudeEnv) {
|
|
3549
|
+
if (!claudeEnv)
|
|
3550
|
+
return [];
|
|
3551
|
+
return Object.keys(claudeEnv).filter((key) => key.startsWith("ANTHROPIC_"));
|
|
3552
|
+
}
|
|
3474
3553
|
var init_router_resolution = __esm({
|
|
3475
3554
|
"packages/core/dist/router-resolution.js"() {
|
|
3476
3555
|
init_dist();
|
|
@@ -3497,6 +3576,14 @@ function buildRouterEnv(creds, model) {
|
|
|
3497
3576
|
env.ANTHROPIC_MODEL = model;
|
|
3498
3577
|
return env;
|
|
3499
3578
|
}
|
|
3579
|
+
function mergeClaudeEnvRouterSettings(routerEnv, claudeEnv) {
|
|
3580
|
+
const merged = { ...routerEnv };
|
|
3581
|
+
for (const [key, value] of Object.entries(claudeEnv ?? {})) {
|
|
3582
|
+
if (!key.startsWith("ANTHROPIC_"))
|
|
3583
|
+
merged[key] = value;
|
|
3584
|
+
}
|
|
3585
|
+
return merged;
|
|
3586
|
+
}
|
|
3500
3587
|
function formatCustomHeaders(headers) {
|
|
3501
3588
|
const entries = Object.entries(headers ?? {});
|
|
3502
3589
|
if (entries.length === 0)
|
|
@@ -3810,10 +3897,18 @@ async function runCrewSpawn(input, config, deps) {
|
|
|
3810
3897
|
if (agentName === "claude") {
|
|
3811
3898
|
let routerEnv = {};
|
|
3812
3899
|
if (backend !== "native") {
|
|
3900
|
+
if (!deps.writeRouterSettings) {
|
|
3901
|
+
throw new Error(`backend '${backend}' requires a per-spawn --settings writer to outrank defaults.claudeEnv, but the spawn path has none \u2014 refusing to launch a routed session that would silently bypass the router shim`);
|
|
3902
|
+
}
|
|
3813
3903
|
if (!deps.routerCredentials) {
|
|
3814
3904
|
throw new Error(`backend '${backend}' requires router credentials, but the spawn path has no daemon credentials provider`);
|
|
3815
3905
|
}
|
|
3816
3906
|
routerEnv = buildRouterEnv(await deps.routerCredentials({ project: input.project, backend }), crewModel);
|
|
3907
|
+
const shadowed = claudeEnvShadowsRouter(config.defaults.claudeEnv);
|
|
3908
|
+
if (shadowed.length > 0) {
|
|
3909
|
+
process.stderr.write(`\u26A0\uFE0F backend '${backend}' selected but defaults.claudeEnv sets ${shadowed.join(", ")} \u2014 these would shadow the router shim. Overriding via a per-spawn --settings env block (outranks ~/.claude/settings.json).
|
|
3910
|
+
`);
|
|
3911
|
+
}
|
|
3817
3912
|
}
|
|
3818
3913
|
ensureSocksDir();
|
|
3819
3914
|
const messagingSocketPath = path9.join(CC_SOCKS_DIR, `squadrant-${randomUUID3()}.sock`);
|
|
@@ -3826,6 +3921,12 @@ async function runCrewSpawn(input, config, deps) {
|
|
|
3826
3921
|
name,
|
|
3827
3922
|
messagingSocketPath
|
|
3828
3923
|
});
|
|
3924
|
+
const routerSettingsPath = Object.keys(routerEnv).length > 0 ? deps.writeRouterSettings({
|
|
3925
|
+
stateRoot: STATE_ROOT,
|
|
3926
|
+
project: input.project,
|
|
3927
|
+
taskId: rec.id,
|
|
3928
|
+
env: mergeClaudeEnvRouterSettings(routerEnv, config.defaults.claudeEnv)
|
|
3929
|
+
}) : void 0;
|
|
3829
3930
|
let hooksInstalled = false;
|
|
3830
3931
|
try {
|
|
3831
3932
|
deps.writeSettingsLocal(spawnCwd);
|
|
@@ -3841,18 +3942,25 @@ async function runCrewSpawn(input, config, deps) {
|
|
|
3841
3942
|
messagingSocketPath,
|
|
3842
3943
|
// Permission mode is config-driven so squadrant can default crews to 'auto'
|
|
3843
3944
|
// or keep the semi-automatic 'acceptEdits' gate. Falls back to 'acceptEdits'.
|
|
3844
|
-
|
|
3945
|
+
// #772: a router-backed crew MUST use 'default' — 'auto' makes the built-in
|
|
3946
|
+
// Sonnet-5 classifier own PermissionRequest and the gate yields, which is
|
|
3947
|
+
// exactly the fail-closed dead end U7 exists to fix. The rubric owns the
|
|
3948
|
+
// permission mode for a routed spawn; the operator's config cannot override
|
|
3949
|
+
// it back into the broken path.
|
|
3950
|
+
permissionMode: backend !== "native" ? "default" : config.defaults.permissions?.crew ?? "acceptEdits",
|
|
3845
3951
|
// #708: self-describing name so ListAgents/the registry can tell this
|
|
3846
3952
|
// crew apart from an unrelated session instead of an auto-derived cwd
|
|
3847
3953
|
// basename (only the claude driver reads this — other agents ignore it).
|
|
3848
3954
|
sessionName: crewSessionName(input.project, name),
|
|
3849
3955
|
...crewModel ? { model: crewModel } : {},
|
|
3850
|
-
...crewThinking ? { thinking: crewThinking } : {}
|
|
3956
|
+
...crewThinking ? { thinking: crewThinking } : {},
|
|
3957
|
+
...routerSettingsPath ? { settingsPath: routerSettingsPath } : {}
|
|
3851
3958
|
});
|
|
3852
3959
|
const direction2 = input.direction ?? "tab";
|
|
3853
3960
|
const title2 = titleFor(input.project, name);
|
|
3854
3961
|
const pane2 = await deps.runtime.newPane({ workspaceId: captain.id, direction: direction2, title: title2 });
|
|
3855
|
-
const
|
|
3962
|
+
const gateEnv = backend !== "native" ? " SQUADRANT_GATE=on" : "";
|
|
3963
|
+
const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}${gateEnv}`;
|
|
3856
3964
|
const routerPrefix = Object.keys(routerEnv).length > 0 ? ` ${renderEnvAssignments(routerEnv)}` : "";
|
|
3857
3965
|
await deps.runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix}${routerPrefix} ${niceCrewCommand(cliCommand2)}`);
|
|
3858
3966
|
const preLaunchScreen = await deps.runtime.readPaneScreen(pane2) ?? "";
|
|
@@ -4631,8 +4739,9 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
|
|
|
4631
4739
|
if (mode !== "off")
|
|
4632
4740
|
throw new DeferDelivery(null, "no-channel");
|
|
4633
4741
|
}
|
|
4742
|
+
const paneOpts = { ...sendOpts, agent };
|
|
4634
4743
|
try {
|
|
4635
|
-
return await cmux2.send(surface, text,
|
|
4744
|
+
return await cmux2.send(surface, text, paneOpts);
|
|
4636
4745
|
} catch (e) {
|
|
4637
4746
|
if (!(e instanceof DeferDelivery) || e.reason !== "probe-failed")
|
|
4638
4747
|
throw e;
|
|
@@ -4644,7 +4753,7 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
|
|
|
4644
4753
|
}
|
|
4645
4754
|
log(`delivery project=${project}: probe-failed on ${surface.workspaceId}/${surface.surfaceId} \u2014 re-resolved to ${next.workspaceId}/${next.surfaceId}, retrying`);
|
|
4646
4755
|
surface = next;
|
|
4647
|
-
return cmux2.send(next, text,
|
|
4756
|
+
return cmux2.send(next, text, paneOpts);
|
|
4648
4757
|
}
|
|
4649
4758
|
});
|
|
4650
4759
|
if ("delivered" in result) {
|
|
@@ -5575,16 +5684,26 @@ function createRunCommand(cliBin) {
|
|
|
5575
5684
|
}
|
|
5576
5685
|
};
|
|
5577
5686
|
}
|
|
5687
|
+
function captainAliveStateFromHealth(rows, project) {
|
|
5688
|
+
const row = rows.find((h) => h.kind === "captain" && h.project === project);
|
|
5689
|
+
if (!row)
|
|
5690
|
+
return "unknown";
|
|
5691
|
+
if (row.state === "alive")
|
|
5692
|
+
return "alive";
|
|
5693
|
+
if (row.state === "gone" || row.state === "stopped")
|
|
5694
|
+
return "dead";
|
|
5695
|
+
return "unknown";
|
|
5696
|
+
}
|
|
5578
5697
|
function isCaptainAliveFromHealth(rows, project) {
|
|
5579
|
-
return rows
|
|
5698
|
+
return captainAliveStateFromHealth(rows, project) === "alive";
|
|
5580
5699
|
}
|
|
5581
5700
|
function createIsCaptainAlive(sock) {
|
|
5582
5701
|
return async (project) => {
|
|
5583
5702
|
try {
|
|
5584
5703
|
const health = await sendRequest(sock, { kind: "health", project }, 5e3);
|
|
5585
|
-
return
|
|
5704
|
+
return captainAliveStateFromHealth(health ?? [], project);
|
|
5586
5705
|
} catch {
|
|
5587
|
-
return
|
|
5706
|
+
return "unknown";
|
|
5588
5707
|
}
|
|
5589
5708
|
};
|
|
5590
5709
|
}
|
|
@@ -5621,16 +5740,19 @@ function createEnsureCaptainAlive(deps) {
|
|
|
5621
5740
|
const now = deps.now ?? (() => Date.now());
|
|
5622
5741
|
const inFlight = /* @__PURE__ */ new Map();
|
|
5623
5742
|
async function run(project) {
|
|
5624
|
-
|
|
5743
|
+
const first = await deps.isAlive(project);
|
|
5744
|
+
if (first === "alive")
|
|
5625
5745
|
return "alive";
|
|
5626
5746
|
await deps.launch(project);
|
|
5627
5747
|
const deadline = now() + warmupTimeoutMs;
|
|
5748
|
+
let last = first;
|
|
5628
5749
|
while (now() < deadline) {
|
|
5629
|
-
|
|
5750
|
+
last = await deps.isAlive(project);
|
|
5751
|
+
if (last === "alive")
|
|
5630
5752
|
return "launched";
|
|
5631
5753
|
await sleep3(pollMs);
|
|
5632
5754
|
}
|
|
5633
|
-
return "timeout";
|
|
5755
|
+
return last === "dead" ? "timeout" : "unknown";
|
|
5634
5756
|
}
|
|
5635
5757
|
return function ensure(project) {
|
|
5636
5758
|
const existing = inFlight.get(project);
|
|
@@ -5685,6 +5807,26 @@ ${ev.question}`;
|
|
|
5685
5807
|
function formatInbound(text) {
|
|
5686
5808
|
return `\u{1F4E9} [from Telegram] ${text}`;
|
|
5687
5809
|
}
|
|
5810
|
+
function mediaKind(m) {
|
|
5811
|
+
for (const [field, name] of MEDIA_KINDS) {
|
|
5812
|
+
if (m[field] !== void 0)
|
|
5813
|
+
return name;
|
|
5814
|
+
}
|
|
5815
|
+
return void 0;
|
|
5816
|
+
}
|
|
5817
|
+
function mediaMarker(kind) {
|
|
5818
|
+
return `[${kind} attached - not forwarded]`;
|
|
5819
|
+
}
|
|
5820
|
+
function inboundBody(text, kind) {
|
|
5821
|
+
if (kind === void 0)
|
|
5822
|
+
return text ?? "";
|
|
5823
|
+
const marker = mediaMarker(kind);
|
|
5824
|
+
return text ? `${text}
|
|
5825
|
+
${marker}` : marker;
|
|
5826
|
+
}
|
|
5827
|
+
function formatMediaReceipt(kind, hasCaption) {
|
|
5828
|
+
return hasCaption ? `\u{1F4CE} ${kind} not forwarded \u2014 your caption reached the captain` : `\u{1F4CE} ${kind} not forwarded \u2014 the captain was told it arrived, nothing else was sent`;
|
|
5829
|
+
}
|
|
5688
5830
|
function formatUsageLine(u) {
|
|
5689
5831
|
const reqs = u.requests === 1 ? "1 req" : `${u.requests} reqs`;
|
|
5690
5832
|
const named = Object.keys(u.models).filter((m) => m !== "unknown").sort((a, b) => u.models[b].costUsd - u.models[a].costUsd);
|
|
@@ -5696,8 +5838,19 @@ function maskToken(token) {
|
|
|
5696
5838
|
return token;
|
|
5697
5839
|
return "*".repeat(token.length - 4) + token.slice(-4);
|
|
5698
5840
|
}
|
|
5841
|
+
var MEDIA_KINDS;
|
|
5699
5842
|
var init_format = __esm({
|
|
5700
5843
|
"packages/core/dist/telegram/format.js"() {
|
|
5844
|
+
MEDIA_KINDS = [
|
|
5845
|
+
["photo", "photo"],
|
|
5846
|
+
["voice", "voice message"],
|
|
5847
|
+
["video_note", "video message"],
|
|
5848
|
+
["video", "video"],
|
|
5849
|
+
["audio", "audio file"],
|
|
5850
|
+
["document", "document"],
|
|
5851
|
+
["animation", "animation"],
|
|
5852
|
+
["sticker", "sticker"]
|
|
5853
|
+
];
|
|
5701
5854
|
}
|
|
5702
5855
|
});
|
|
5703
5856
|
|
|
@@ -5720,6 +5873,8 @@ function loadState(stateRoot) {
|
|
|
5720
5873
|
};
|
|
5721
5874
|
if (typeof data.lastUserId === "number")
|
|
5722
5875
|
result.lastUserId = data.lastUserId;
|
|
5876
|
+
if (data.pending && Object.keys(data.pending).length > 0)
|
|
5877
|
+
result.pending = data.pending;
|
|
5723
5878
|
return result;
|
|
5724
5879
|
} catch {
|
|
5725
5880
|
return { offset: 0, topics: {}, notify: {} };
|
|
@@ -5733,6 +5888,19 @@ function setTopic(stateRoot, project, topicId, scope = "project") {
|
|
|
5733
5888
|
s.topics[topicKey(project, scope)] = topicId;
|
|
5734
5889
|
saveState(stateRoot, s);
|
|
5735
5890
|
}
|
|
5891
|
+
function pruneTopics(stateRoot, keep) {
|
|
5892
|
+
const s = loadState(stateRoot);
|
|
5893
|
+
const removed = Object.keys(s.topics).filter((key) => {
|
|
5894
|
+
const sep3 = key.indexOf("::");
|
|
5895
|
+
return !keep(sep3 === -1 ? key : key.slice(0, sep3));
|
|
5896
|
+
});
|
|
5897
|
+
if (removed.length === 0)
|
|
5898
|
+
return [];
|
|
5899
|
+
for (const key of removed)
|
|
5900
|
+
delete s.topics[key];
|
|
5901
|
+
saveState(stateRoot, s);
|
|
5902
|
+
return removed;
|
|
5903
|
+
}
|
|
5736
5904
|
function isNotifyActive(stateRoot, project) {
|
|
5737
5905
|
return loadState(stateRoot).notify[project] === true;
|
|
5738
5906
|
}
|
|
@@ -5746,6 +5914,42 @@ function setNotify(stateRoot, project, active) {
|
|
|
5746
5914
|
s.notify[project] = active;
|
|
5747
5915
|
saveState(stateRoot, s);
|
|
5748
5916
|
}
|
|
5917
|
+
function loadPending(stateRoot) {
|
|
5918
|
+
return loadState(stateRoot).pending ?? {};
|
|
5919
|
+
}
|
|
5920
|
+
function setPending(stateRoot, project, p) {
|
|
5921
|
+
const s = loadState(stateRoot);
|
|
5922
|
+
s.pending = { ...s.pending ?? {}, [project]: p };
|
|
5923
|
+
saveState(stateRoot, s);
|
|
5924
|
+
}
|
|
5925
|
+
function clearPending(stateRoot, project) {
|
|
5926
|
+
const s = loadState(stateRoot);
|
|
5927
|
+
if (!s.pending || !(project in s.pending))
|
|
5928
|
+
return;
|
|
5929
|
+
delete s.pending[project];
|
|
5930
|
+
if (Object.keys(s.pending).length === 0)
|
|
5931
|
+
delete s.pending;
|
|
5932
|
+
saveState(stateRoot, s);
|
|
5933
|
+
}
|
|
5934
|
+
function markPendingWarned(stateRoot, project, at) {
|
|
5935
|
+
const s = loadState(stateRoot);
|
|
5936
|
+
const p = s.pending?.[project];
|
|
5937
|
+
if (!p)
|
|
5938
|
+
return;
|
|
5939
|
+
s.pending[project] = { ...p, warnedAt: at };
|
|
5940
|
+
saveState(stateRoot, s);
|
|
5941
|
+
}
|
|
5942
|
+
function notePaneScreen(stateRoot, project, hash, at) {
|
|
5943
|
+
const s = loadState(stateRoot);
|
|
5944
|
+
const p = s.pending?.[project];
|
|
5945
|
+
if (!p)
|
|
5946
|
+
return void 0;
|
|
5947
|
+
if (p.paneHash === hash && p.paneChangedAt !== void 0)
|
|
5948
|
+
return at - p.paneChangedAt;
|
|
5949
|
+
s.pending[project] = { ...p, paneHash: hash, paneChangedAt: at };
|
|
5950
|
+
saveState(stateRoot, s);
|
|
5951
|
+
return void 0;
|
|
5952
|
+
}
|
|
5749
5953
|
function findProjectByThread(stateRoot, threadId) {
|
|
5750
5954
|
const s = loadState(stateRoot);
|
|
5751
5955
|
for (const [key, id] of Object.entries(s.topics)) {
|
|
@@ -5768,17 +5972,18 @@ var init_state = __esm({
|
|
|
5768
5972
|
function createTelegramClient(opts) {
|
|
5769
5973
|
const fetchImpl = opts.fetch ?? fetch;
|
|
5770
5974
|
const base = `https://api.telegram.org/bot${opts.token}`;
|
|
5771
|
-
async function call(method, body) {
|
|
5975
|
+
async function call(method, body, signal) {
|
|
5772
5976
|
const res = await fetchImpl(`${base}/${method}`, {
|
|
5773
5977
|
method: "POST",
|
|
5774
5978
|
headers: { "content-type": "application/json" },
|
|
5775
|
-
body: JSON.stringify(body)
|
|
5979
|
+
body: JSON.stringify(body),
|
|
5980
|
+
...signal ? { signal } : {}
|
|
5776
5981
|
});
|
|
5777
5982
|
const json = await res.json();
|
|
5778
5983
|
if (!res.ok || !json.ok) {
|
|
5779
5984
|
const code = json.error_code ?? res.status;
|
|
5780
5985
|
const desc = json.description ?? "unknown error";
|
|
5781
|
-
throw new
|
|
5986
|
+
throw new TelegramApiError(code, `telegram ${method} failed (${code}): ${desc}`, json.parameters?.retry_after);
|
|
5782
5987
|
}
|
|
5783
5988
|
return json.result;
|
|
5784
5989
|
}
|
|
@@ -5787,8 +5992,8 @@ function createTelegramClient(opts) {
|
|
|
5787
5992
|
const r = await call("getMe", {});
|
|
5788
5993
|
return { id: r.id, username: r.username };
|
|
5789
5994
|
},
|
|
5790
|
-
getUpdates(offset, timeoutSec = 50) {
|
|
5791
|
-
return call("getUpdates", { offset, timeout: timeoutSec });
|
|
5995
|
+
getUpdates(offset, timeoutSec = 50, signal) {
|
|
5996
|
+
return call("getUpdates", { offset, timeout: timeoutSec }, signal);
|
|
5792
5997
|
},
|
|
5793
5998
|
async sendMessage(chatId, threadId, text, replyMarkup) {
|
|
5794
5999
|
const body = { chat_id: chatId, text };
|
|
@@ -5811,6 +6016,9 @@ function createTelegramClient(opts) {
|
|
|
5811
6016
|
const r = await call("createForumTopic", { chat_id: chatId, name });
|
|
5812
6017
|
return r.message_thread_id;
|
|
5813
6018
|
},
|
|
6019
|
+
async deleteForumTopic(chatId, threadId) {
|
|
6020
|
+
await call("deleteForumTopic", { chat_id: chatId, message_thread_id: threadId });
|
|
6021
|
+
},
|
|
5814
6022
|
async setMyCommands(commands) {
|
|
5815
6023
|
await call("setMyCommands", { commands });
|
|
5816
6024
|
},
|
|
@@ -5819,11 +6027,241 @@ function createTelegramClient(opts) {
|
|
|
5819
6027
|
if (threadId !== void 0)
|
|
5820
6028
|
body.message_thread_id = threadId;
|
|
5821
6029
|
await call("sendChatAction", body);
|
|
6030
|
+
},
|
|
6031
|
+
async setMessageReaction(chatId, messageId, emoji) {
|
|
6032
|
+
await call("setMessageReaction", {
|
|
6033
|
+
chat_id: chatId,
|
|
6034
|
+
message_id: messageId,
|
|
6035
|
+
reaction: [{ type: "emoji", emoji }]
|
|
6036
|
+
});
|
|
5822
6037
|
}
|
|
5823
6038
|
};
|
|
5824
6039
|
}
|
|
6040
|
+
var TelegramApiError;
|
|
5825
6041
|
var init_client = __esm({
|
|
5826
6042
|
"packages/core/dist/telegram/client.js"() {
|
|
6043
|
+
TelegramApiError = class extends Error {
|
|
6044
|
+
code;
|
|
6045
|
+
retryAfterSec;
|
|
6046
|
+
constructor(code, message, retryAfterSec) {
|
|
6047
|
+
super(message);
|
|
6048
|
+
this.code = code;
|
|
6049
|
+
this.retryAfterSec = retryAfterSec;
|
|
6050
|
+
this.name = "TelegramApiError";
|
|
6051
|
+
}
|
|
6052
|
+
};
|
|
6053
|
+
}
|
|
6054
|
+
});
|
|
6055
|
+
|
|
6056
|
+
// packages/core/dist/telegram/inbound-lifecycle.js
|
|
6057
|
+
function defaultSchedule(fn, ms) {
|
|
6058
|
+
const t = setTimeout(fn, ms);
|
|
6059
|
+
t.unref?.();
|
|
6060
|
+
return () => clearTimeout(t);
|
|
6061
|
+
}
|
|
6062
|
+
function formatWatchdogWarning(project, read, minutes) {
|
|
6063
|
+
const head = `\u23F1\uFE0F [${project}] no reply for ${minutes} minutes`;
|
|
6064
|
+
switch (read.status) {
|
|
6065
|
+
case "locked":
|
|
6066
|
+
return `${head} \u2014 the captain is LOCKED on a prompt, waiting for a human. Answer it in the pane (or with \`squadrant crew answer\`) and it will continue.`;
|
|
6067
|
+
case "mid-turn":
|
|
6068
|
+
return `${head} \u2014 the captain is still working (a turn is in flight), just slow. No action needed yet.`;
|
|
6069
|
+
case "no-session":
|
|
6070
|
+
return `${head} \u2014 the captain looks GONE (no live session in its pane). Run \`squadrant launch ${project}\`.`;
|
|
6071
|
+
case "unreadable":
|
|
6072
|
+
return `${head} \u2014 couldn't read the captain's pane to classify it. Check it manually.`;
|
|
6073
|
+
}
|
|
6074
|
+
}
|
|
6075
|
+
function createInboundLifecycle(opts) {
|
|
6076
|
+
const { stateRoot, cfg, sendChatAction, sendReply, readPane, log } = opts;
|
|
6077
|
+
const now = opts.now ?? Date.now;
|
|
6078
|
+
const schedule = opts.schedule ?? defaultSchedule;
|
|
6079
|
+
const typingMs = opts.typingMs ?? TYPING_KEEPALIVE_MS;
|
|
6080
|
+
const watchdogMs = opts.watchdogMs ?? WATCHDOG_MS;
|
|
6081
|
+
const warnAtMs = watchdogMs + (opts.watchdogMs === void 0 ? WATCHDOG_GRACE_MS : 0);
|
|
6082
|
+
const active = /* @__PURE__ */ new Set();
|
|
6083
|
+
let running = false;
|
|
6084
|
+
function issue(project, threadId) {
|
|
6085
|
+
void Promise.resolve(sendChatAction(cfg.supergroupId, threadId, "typing")).catch((e) => log(`telegram typing keep-alive failed project=${project}: ${e.message}`));
|
|
6086
|
+
}
|
|
6087
|
+
function drain(project) {
|
|
6088
|
+
active.add(project);
|
|
6089
|
+
const tick = () => {
|
|
6090
|
+
if (!running || !active.has(project))
|
|
6091
|
+
return;
|
|
6092
|
+
const p = loadPending(stateRoot)[project];
|
|
6093
|
+
if (!p) {
|
|
6094
|
+
active.delete(project);
|
|
6095
|
+
return;
|
|
6096
|
+
}
|
|
6097
|
+
if (now() - p.startedAt >= warnAtMs && p.warnedAt === void 0) {
|
|
6098
|
+
void warn(project, p.threadId);
|
|
6099
|
+
}
|
|
6100
|
+
issue(project, p.threadId);
|
|
6101
|
+
schedule(tick, typingMs);
|
|
6102
|
+
};
|
|
6103
|
+
schedule(tick, typingMs);
|
|
6104
|
+
}
|
|
6105
|
+
async function warn(project, threadId) {
|
|
6106
|
+
markPendingWarned(stateRoot, project, now());
|
|
6107
|
+
let read = { status: "unreadable" };
|
|
6108
|
+
try {
|
|
6109
|
+
if (readPane)
|
|
6110
|
+
read = await readPane(project);
|
|
6111
|
+
} catch (e) {
|
|
6112
|
+
log(`telegram watchdog pane read failed project=${project}: ${e.message}`);
|
|
6113
|
+
}
|
|
6114
|
+
try {
|
|
6115
|
+
await sendReply(threadId, formatWatchdogWarning(project, read, Math.floor(watchdogMs / 6e4)));
|
|
6116
|
+
} catch (e) {
|
|
6117
|
+
log(`telegram watchdog warning failed project=${project}: ${e.message}`);
|
|
6118
|
+
}
|
|
6119
|
+
}
|
|
6120
|
+
return {
|
|
6121
|
+
start() {
|
|
6122
|
+
running = true;
|
|
6123
|
+
for (const [project, p] of Object.entries(loadPending(stateRoot))) {
|
|
6124
|
+
void (async () => {
|
|
6125
|
+
let read = { status: "unreadable" };
|
|
6126
|
+
try {
|
|
6127
|
+
if (readPane)
|
|
6128
|
+
read = await readPane(project);
|
|
6129
|
+
} catch (e) {
|
|
6130
|
+
log(`telegram resume pane read failed project=${project}: ${e.message}`);
|
|
6131
|
+
}
|
|
6132
|
+
const still = loadPending(stateRoot)[project];
|
|
6133
|
+
if (!still || still.startedAt !== p.startedAt)
|
|
6134
|
+
return;
|
|
6135
|
+
if (read.status === "no-session") {
|
|
6136
|
+
clearPending(stateRoot, project);
|
|
6137
|
+
log(`telegram typing lifecycle cleared for ${project}: captain session gone on boot`);
|
|
6138
|
+
return;
|
|
6139
|
+
}
|
|
6140
|
+
log(`telegram typing lifecycle resumed for ${project} (thread ${p.threadId})`);
|
|
6141
|
+
issue(project, p.threadId);
|
|
6142
|
+
drain(project);
|
|
6143
|
+
})();
|
|
6144
|
+
}
|
|
6145
|
+
},
|
|
6146
|
+
stop() {
|
|
6147
|
+
running = false;
|
|
6148
|
+
active.clear();
|
|
6149
|
+
},
|
|
6150
|
+
begin(project, threadId) {
|
|
6151
|
+
setPending(stateRoot, project, { threadId, startedAt: now() });
|
|
6152
|
+
issue(project, threadId);
|
|
6153
|
+
if (running && !active.has(project))
|
|
6154
|
+
drain(project);
|
|
6155
|
+
},
|
|
6156
|
+
pending() {
|
|
6157
|
+
return loadPending(stateRoot);
|
|
6158
|
+
}
|
|
6159
|
+
};
|
|
6160
|
+
}
|
|
6161
|
+
var TYPING_KEEPALIVE_MS, WATCHDOG_MS, WATCHDOG_GRACE_MS;
|
|
6162
|
+
var init_inbound_lifecycle = __esm({
|
|
6163
|
+
"packages/core/dist/telegram/inbound-lifecycle.js"() {
|
|
6164
|
+
init_state();
|
|
6165
|
+
TYPING_KEEPALIVE_MS = 4e3;
|
|
6166
|
+
WATCHDOG_MS = 15 * 6e4;
|
|
6167
|
+
WATCHDOG_GRACE_MS = 1;
|
|
6168
|
+
}
|
|
6169
|
+
});
|
|
6170
|
+
|
|
6171
|
+
// packages/core/dist/telegram/notify.js
|
|
6172
|
+
function runTelegramStatus(opts) {
|
|
6173
|
+
const tg = opts.config.telegram;
|
|
6174
|
+
const env = opts.env ?? process.env;
|
|
6175
|
+
const tokenSet = !!(tg?.botToken ?? env.TELEGRAM_BOT_TOKEN);
|
|
6176
|
+
const links = Object.entries(loadState(opts.stateRoot).topics).map(([key, topicId]) => {
|
|
6177
|
+
const sep3 = key.indexOf("::");
|
|
6178
|
+
return { project: key.slice(0, sep3), scope: key.slice(sep3 + 2), topicId };
|
|
6179
|
+
});
|
|
6180
|
+
return { tokenSet, supergroupId: tg?.supergroupId ?? null, links };
|
|
6181
|
+
}
|
|
6182
|
+
function runTelegramNotifySet(opts) {
|
|
6183
|
+
setNotify(opts.stateRoot, opts.project, opts.active);
|
|
6184
|
+
}
|
|
6185
|
+
function runTelegramNotifyPref(args) {
|
|
6186
|
+
const { project, dimension, value, root } = args;
|
|
6187
|
+
if (dimension === "crew") {
|
|
6188
|
+
if (!["all", "alert_only", "done_only", "none"].includes(value))
|
|
6189
|
+
return { ok: false, message: "crew must be all|alert_only|done_only|none" };
|
|
6190
|
+
saveProjectOverride(project, { telegram: { notify: { crew: value } } }, root);
|
|
6191
|
+
return { ok: true };
|
|
6192
|
+
}
|
|
6193
|
+
if (value !== "on" && value !== "off")
|
|
6194
|
+
return { ok: false, message: "cap must be on|off" };
|
|
6195
|
+
saveProjectOverride(project, { telegram: { notify: { cap: value === "on" } } }, root);
|
|
6196
|
+
return { ok: true };
|
|
6197
|
+
}
|
|
6198
|
+
function runTelegramNotifyStatus(opts) {
|
|
6199
|
+
const s = loadState(opts.stateRoot);
|
|
6200
|
+
const projects = /* @__PURE__ */ new Set();
|
|
6201
|
+
for (const key of Object.keys(s.topics)) {
|
|
6202
|
+
const sep3 = key.indexOf("::");
|
|
6203
|
+
projects.add(sep3 === -1 ? key : key.slice(0, sep3));
|
|
6204
|
+
}
|
|
6205
|
+
for (const p of Object.keys(s.notify))
|
|
6206
|
+
projects.add(p);
|
|
6207
|
+
return [...projects].map((project) => ({ project, active: s.notify[project] === true }));
|
|
6208
|
+
}
|
|
6209
|
+
function capAllowed(project, globalNotify, root) {
|
|
6210
|
+
return resolveNotify(globalNotify, loadProjectOverride(project, root)).cap;
|
|
6211
|
+
}
|
|
6212
|
+
function confirmationText(project, before, after, dim) {
|
|
6213
|
+
if (dim === "active")
|
|
6214
|
+
return `\u{1F515} ${project} \u2014 all notifications muted here. Unmute: squadrant telegram notify ${project} on`;
|
|
6215
|
+
if (dim === "cap")
|
|
6216
|
+
return `\u{1F515} ${project} \u2014 captain messages muted here. Re-enable: squadrant telegram notify ${project} cap on`;
|
|
6217
|
+
return `\u{1F515} ${project} \u2014 crew notifications now '${after.crew}' (was '${before.crew}'). Re-enable: squadrant telegram notify ${project} crew ${before.crew}`;
|
|
6218
|
+
}
|
|
6219
|
+
async function runNotifyConfirmation(opts) {
|
|
6220
|
+
const { quieter, dim } = isQuieter(opts.before, opts.after);
|
|
6221
|
+
if (!quieter || dim === null)
|
|
6222
|
+
return false;
|
|
6223
|
+
const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
|
|
6224
|
+
if (topicId === void 0)
|
|
6225
|
+
return false;
|
|
6226
|
+
const text = confirmationText(opts.project, opts.before, opts.after, dim);
|
|
6227
|
+
try {
|
|
6228
|
+
await opts.client.sendMessage(opts.cfg.supergroupId, topicId, text);
|
|
6229
|
+
return true;
|
|
6230
|
+
} catch {
|
|
6231
|
+
console.warn(`[squadrant] mute-confirmation send failed for ${opts.project} \u2014 notification preference was still saved`);
|
|
6232
|
+
return false;
|
|
6233
|
+
}
|
|
6234
|
+
}
|
|
6235
|
+
async function runTelegramSend(opts) {
|
|
6236
|
+
const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
|
|
6237
|
+
if (topicId === void 0) {
|
|
6238
|
+
throw new Error(`project "${opts.project}" is not linked \u2014 run: squadrant telegram link ${opts.project}`);
|
|
6239
|
+
}
|
|
6240
|
+
await opts.client.sendMessage(opts.cfg.supergroupId, topicId, opts.message);
|
|
6241
|
+
clearPending(opts.stateRoot, opts.project);
|
|
6242
|
+
return { chatId: opts.cfg.supergroupId, topicId };
|
|
6243
|
+
}
|
|
6244
|
+
async function runTelegramLink(opts) {
|
|
6245
|
+
const existing = loadState(opts.stateRoot).topics[topicKey(opts.project)];
|
|
6246
|
+
if (existing !== void 0)
|
|
6247
|
+
return { topicId: existing, created: false };
|
|
6248
|
+
const topicId = await opts.client.createForumTopic(opts.cfg.supergroupId, topicName(opts.project));
|
|
6249
|
+
const winner = loadState(opts.stateRoot).topics[topicKey(opts.project)];
|
|
6250
|
+
if (winner !== void 0) {
|
|
6251
|
+
try {
|
|
6252
|
+
await opts.client.deleteForumTopic?.(opts.cfg.supergroupId, topicId);
|
|
6253
|
+
} catch {
|
|
6254
|
+
}
|
|
6255
|
+
return { topicId: winner, created: false };
|
|
6256
|
+
}
|
|
6257
|
+
setTopic(opts.stateRoot, opts.project, topicId);
|
|
6258
|
+
return { topicId, created: true };
|
|
6259
|
+
}
|
|
6260
|
+
var init_notify = __esm({
|
|
6261
|
+
"packages/core/dist/telegram/notify.js"() {
|
|
6262
|
+
init_dist();
|
|
6263
|
+
init_state();
|
|
6264
|
+
init_format();
|
|
5827
6265
|
}
|
|
5828
6266
|
});
|
|
5829
6267
|
|
|
@@ -5911,8 +6349,10 @@ var init_tiers = __esm({
|
|
|
5911
6349
|
});
|
|
5912
6350
|
|
|
5913
6351
|
// packages/core/dist/telegram/bridge.js
|
|
6352
|
+
import fs11 from "fs";
|
|
5914
6353
|
import os5 from "os";
|
|
5915
6354
|
import path12 from "path";
|
|
6355
|
+
import crypto2 from "crypto";
|
|
5916
6356
|
function formatInboundReceipt(project, outcome) {
|
|
5917
6357
|
if (!outcome)
|
|
5918
6358
|
return void 0;
|
|
@@ -5928,6 +6368,85 @@ function formatInboundReceipt(project, outcome) {
|
|
|
5928
6368
|
return void 0;
|
|
5929
6369
|
}
|
|
5930
6370
|
}
|
|
6371
|
+
function telegramPollLockPath(token) {
|
|
6372
|
+
const hash = crypto2.createHash("sha256").update(token).digest("hex").slice(0, 16);
|
|
6373
|
+
return path12.join(os5.tmpdir(), `squadrant-telegram-poll-${hash}.lock`);
|
|
6374
|
+
}
|
|
6375
|
+
function pidAlive(pid) {
|
|
6376
|
+
try {
|
|
6377
|
+
process.kill(pid, 0);
|
|
6378
|
+
return true;
|
|
6379
|
+
} catch (e) {
|
|
6380
|
+
return e.code === "EPERM";
|
|
6381
|
+
}
|
|
6382
|
+
}
|
|
6383
|
+
function lockIsHeld(file) {
|
|
6384
|
+
let pid;
|
|
6385
|
+
let ageMs;
|
|
6386
|
+
try {
|
|
6387
|
+
const parsed = Number.parseInt(fs11.readFileSync(file, "utf8").trim(), 10);
|
|
6388
|
+
pid = Number.isFinite(parsed) ? parsed : void 0;
|
|
6389
|
+
ageMs = Date.now() - fs11.statSync(file).mtimeMs;
|
|
6390
|
+
} catch {
|
|
6391
|
+
return false;
|
|
6392
|
+
}
|
|
6393
|
+
if (pid !== void 0)
|
|
6394
|
+
return pidAlive(pid);
|
|
6395
|
+
return ageMs < POLL_LOCK_STALE_MS;
|
|
6396
|
+
}
|
|
6397
|
+
function acquirePollLock(token) {
|
|
6398
|
+
const file = telegramPollLockPath(token);
|
|
6399
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
6400
|
+
try {
|
|
6401
|
+
const fd = fs11.openSync(file, "wx");
|
|
6402
|
+
fs11.writeSync(fd, String(process.pid));
|
|
6403
|
+
fs11.closeSync(fd);
|
|
6404
|
+
return () => {
|
|
6405
|
+
try {
|
|
6406
|
+
fs11.unlinkSync(file);
|
|
6407
|
+
} catch {
|
|
6408
|
+
}
|
|
6409
|
+
};
|
|
6410
|
+
} catch (e) {
|
|
6411
|
+
if (e.code !== "EEXIST")
|
|
6412
|
+
return null;
|
|
6413
|
+
if (attempt === 0 && !lockIsHeld(file)) {
|
|
6414
|
+
try {
|
|
6415
|
+
fs11.unlinkSync(file);
|
|
6416
|
+
} catch {
|
|
6417
|
+
}
|
|
6418
|
+
continue;
|
|
6419
|
+
}
|
|
6420
|
+
return null;
|
|
6421
|
+
}
|
|
6422
|
+
}
|
|
6423
|
+
return null;
|
|
6424
|
+
}
|
|
6425
|
+
function defaultSchedule2(fn, ms) {
|
|
6426
|
+
const t = setTimeout(fn, ms);
|
|
6427
|
+
t.unref?.();
|
|
6428
|
+
return () => clearTimeout(t);
|
|
6429
|
+
}
|
|
6430
|
+
function foreignPollLockHolder(token, selfPid = process.pid) {
|
|
6431
|
+
const file = telegramPollLockPath(token);
|
|
6432
|
+
let pid;
|
|
6433
|
+
try {
|
|
6434
|
+
const parsed = Number.parseInt(fs11.readFileSync(file, "utf8").trim(), 10);
|
|
6435
|
+
if (!Number.isFinite(parsed))
|
|
6436
|
+
return null;
|
|
6437
|
+
pid = parsed;
|
|
6438
|
+
} catch {
|
|
6439
|
+
return null;
|
|
6440
|
+
}
|
|
6441
|
+
if (pid === selfPid)
|
|
6442
|
+
return null;
|
|
6443
|
+
return pidAlive(pid) ? pid : null;
|
|
6444
|
+
}
|
|
6445
|
+
function pollBackoffMs(err, pollMs) {
|
|
6446
|
+
if (err instanceof TelegramApiError && err.retryAfterSec !== void 0)
|
|
6447
|
+
return err.retryAfterSec * 1e3;
|
|
6448
|
+
return pollMs;
|
|
6449
|
+
}
|
|
5931
6450
|
function parseNotifyPref(text) {
|
|
5932
6451
|
const parts = text.trim().split(/\s+/);
|
|
5933
6452
|
if (stripBotMention(parts[0] ?? "").toLowerCase() !== "/notify")
|
|
@@ -5954,9 +6473,17 @@ function notifyToggle(text) {
|
|
|
5954
6473
|
}
|
|
5955
6474
|
function createTelegramBridge(opts) {
|
|
5956
6475
|
const { cfg, stateRoot, client, appendCaptainMessage: appendCaptainMessage2, log, ensureCaptainAlive, runCommand, sendReply } = opts;
|
|
6476
|
+
const lifecycle = opts.lifecycle;
|
|
5957
6477
|
const configRoot = opts.configRoot ?? path12.join(os5.homedir(), ".config", "squadrant");
|
|
5958
6478
|
const pollMs = cfg.pollMs ?? 1e3;
|
|
6479
|
+
const schedule = opts.schedule ?? defaultSchedule2;
|
|
6480
|
+
const lockToken = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
6481
|
+
let releasePollLock = null;
|
|
6482
|
+
let pollAbort = null;
|
|
5959
6483
|
let running = false;
|
|
6484
|
+
let pollStopped = false;
|
|
6485
|
+
let selfHealCancel = null;
|
|
6486
|
+
let conflictCount = 0;
|
|
5960
6487
|
let lastSuccessfulPollAt = null;
|
|
5961
6488
|
let lastError = null;
|
|
5962
6489
|
let lastErrorAt = null;
|
|
@@ -5966,12 +6493,8 @@ function createTelegramBridge(opts) {
|
|
|
5966
6493
|
saveState(stateRoot, s);
|
|
5967
6494
|
}
|
|
5968
6495
|
async function sendToTopic(project, text) {
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
threadId = await client.createForumTopic(cfg.supergroupId, topicName(project));
|
|
5972
|
-
setTopic(stateRoot, project, threadId);
|
|
5973
|
-
}
|
|
5974
|
-
await client.sendMessage(cfg.supergroupId, threadId, text);
|
|
6496
|
+
const { topicId } = await runTelegramLink({ project, cfg, client, stateRoot });
|
|
6497
|
+
await client.sendMessage(cfg.supergroupId, topicId, text);
|
|
5975
6498
|
}
|
|
5976
6499
|
async function deliverOutbound(project, ev) {
|
|
5977
6500
|
const resolved = resolveNotify(cfg.notify, loadProjectOverride(project, configRoot));
|
|
@@ -6158,10 +6681,13 @@ ${formatUsageLine(usage2)}`;
|
|
|
6158
6681
|
}
|
|
6159
6682
|
await runChannelCommand(text, fromId, void 0);
|
|
6160
6683
|
}
|
|
6161
|
-
async function handleProjectTopic(text, threadId, fromId) {
|
|
6684
|
+
async function handleProjectTopic(text, threadId, fromId, messageId, media) {
|
|
6162
6685
|
const resolved = findProjectByThread(stateRoot, threadId);
|
|
6163
|
-
if (!resolved)
|
|
6686
|
+
if (!resolved) {
|
|
6687
|
+
log(`telegram inbound in unbound topic thread=${threadId} \u2014 no project linked, message dropped`);
|
|
6688
|
+
await reply(threadId, "\u26A0\uFE0F this topic isn't linked to a project \u2014 run: squadrant telegram link <project>");
|
|
6164
6689
|
return;
|
|
6690
|
+
}
|
|
6165
6691
|
if (isBareSpawn(text)) {
|
|
6166
6692
|
if (!isControlEnabled(cfg) || !isAuthorized(fromId, cfg)) {
|
|
6167
6693
|
await reply(threadId, "\u26D4 not authorized");
|
|
@@ -6211,21 +6737,16 @@ ${formatUsageLine(usage2)}`;
|
|
|
6211
6737
|
await runChannelCommand(text, fromId, threadId);
|
|
6212
6738
|
return;
|
|
6213
6739
|
}
|
|
6214
|
-
|
|
6215
|
-
log(`telegram
|
|
6216
|
-
}
|
|
6740
|
+
if (messageId !== void 0) {
|
|
6741
|
+
void Promise.resolve(client.setMessageReaction?.(cfg.supergroupId, messageId, "\u{1F44D}")).catch((e) => log(`telegram stage-1 reaction failed project=${resolved.project}: ${e.message}`));
|
|
6742
|
+
}
|
|
6217
6743
|
setNotify(stateRoot, resolved.project, true);
|
|
6218
6744
|
if (ensureCaptainAlive && isControlEnabled(cfg) && isAuthorized(fromId, cfg)) {
|
|
6219
|
-
|
|
6220
|
-
const r = await ensureCaptainAlive(resolved.project);
|
|
6745
|
+
void ensureCaptainAlive(resolved.project).then((r) => {
|
|
6221
6746
|
if (r === "timeout") {
|
|
6222
|
-
|
|
6223
|
-
} else {
|
|
6224
|
-
await reply(threadId, `\u{1F4E8} delivered to ${resolved.project} captain`);
|
|
6747
|
+
log(`telegram auto-launch: warmup timed out project=${resolved.project} \u2014 delivery verdict (not this probe) decides the receipt`);
|
|
6225
6748
|
}
|
|
6226
|
-
}
|
|
6227
|
-
log(`telegram auto-launch failed project=${resolved.project}: ${e.message}`);
|
|
6228
|
-
}
|
|
6749
|
+
}).catch((e) => log(`telegram auto-launch failed project=${resolved.project}: ${e.message}`));
|
|
6229
6750
|
}
|
|
6230
6751
|
let handled = false;
|
|
6231
6752
|
let outcome;
|
|
@@ -6234,13 +6755,22 @@ ${formatUsageLine(usage2)}`;
|
|
|
6234
6755
|
handled = res.handled;
|
|
6235
6756
|
outcome = res.outcome;
|
|
6236
6757
|
}
|
|
6758
|
+
const deliveryHeld = outcome?.status === "held";
|
|
6237
6759
|
if (!handled) {
|
|
6238
6760
|
await appendCaptainMessage2({ stateRoot, project: resolved.project, text: formatInbound(text), source: "telegram" });
|
|
6761
|
+
outcome = void 0;
|
|
6239
6762
|
}
|
|
6240
6763
|
const receipt = formatInboundReceipt(resolved.project, outcome);
|
|
6241
6764
|
if (receipt && sendReply) {
|
|
6242
6765
|
await sendReply(threadId, receipt).catch((e) => log(`telegram receipt failed project=${resolved.project}: ${e.message}`));
|
|
6243
6766
|
}
|
|
6767
|
+
if (media) {
|
|
6768
|
+
await reply(threadId, formatMediaReceipt(media.kind, media.hasCaption));
|
|
6769
|
+
}
|
|
6770
|
+
if (deliveryHeld)
|
|
6771
|
+
return;
|
|
6772
|
+
await reply(threadId, CAPTAIN_RECEIVED_ACK);
|
|
6773
|
+
lifecycle?.begin(resolved.project, threadId);
|
|
6244
6774
|
}
|
|
6245
6775
|
async function handleUpdate(u) {
|
|
6246
6776
|
if (u.callback_query) {
|
|
@@ -6248,7 +6778,11 @@ ${formatUsageLine(usage2)}`;
|
|
|
6248
6778
|
return;
|
|
6249
6779
|
}
|
|
6250
6780
|
const m = u.message;
|
|
6251
|
-
if (!m
|
|
6781
|
+
if (!m)
|
|
6782
|
+
return;
|
|
6783
|
+
const kind = mediaKind(m);
|
|
6784
|
+
const text = m.text ?? m.caption;
|
|
6785
|
+
if (text === void 0 && kind === void 0)
|
|
6252
6786
|
return;
|
|
6253
6787
|
if (!cfg.chats.includes(m.chat.id))
|
|
6254
6788
|
return;
|
|
@@ -6262,7 +6796,7 @@ ${formatUsageLine(usage2)}`;
|
|
|
6262
6796
|
await reply(threadId, "\u26D4 not authorized");
|
|
6263
6797
|
return;
|
|
6264
6798
|
}
|
|
6265
|
-
const task =
|
|
6799
|
+
const task = (text ?? "").trim();
|
|
6266
6800
|
if (!task) {
|
|
6267
6801
|
await reply(threadId, "spawn cancelled \u2014 empty task");
|
|
6268
6802
|
return;
|
|
@@ -6273,39 +6807,122 @@ ${formatUsageLine(usage2)}`;
|
|
|
6273
6807
|
return;
|
|
6274
6808
|
}
|
|
6275
6809
|
if (m.message_thread_id === void 0) {
|
|
6810
|
+
if (m.text === void 0)
|
|
6811
|
+
return;
|
|
6276
6812
|
await handleGeneral(m.text, m.from?.id);
|
|
6277
6813
|
return;
|
|
6278
6814
|
}
|
|
6279
|
-
await handleProjectTopic(
|
|
6815
|
+
await handleProjectTopic(inboundBody(text, kind), m.message_thread_id, m.from?.id, m.message_id, kind === void 0 ? void 0 : { kind, hasCaption: text !== void 0 });
|
|
6816
|
+
}
|
|
6817
|
+
function acquirePollLockOrLog() {
|
|
6818
|
+
if (!lockToken)
|
|
6819
|
+
return true;
|
|
6820
|
+
releasePollLock = acquirePollLock(lockToken);
|
|
6821
|
+
if (releasePollLock)
|
|
6822
|
+
return true;
|
|
6823
|
+
const holder = foreignPollLockHolder(lockToken);
|
|
6824
|
+
log(`telegram inbound poll disabled: another consumer already polls this bot token (single-consumer guard, #830)${holder !== null ? ` \u2014 lock held by pid ${holder}` : ""}`);
|
|
6825
|
+
return false;
|
|
6826
|
+
}
|
|
6827
|
+
function scheduleSelfHeal() {
|
|
6828
|
+
selfHealCancel?.();
|
|
6829
|
+
selfHealCancel = schedule(() => {
|
|
6830
|
+
selfHealCancel = null;
|
|
6831
|
+
if (pollStopped || running)
|
|
6832
|
+
return;
|
|
6833
|
+
if (!acquirePollLockOrLog()) {
|
|
6834
|
+
scheduleSelfHeal();
|
|
6835
|
+
return;
|
|
6836
|
+
}
|
|
6837
|
+
running = true;
|
|
6838
|
+
conflictCount = 0;
|
|
6839
|
+
log("telegram inbound poll re-attempting after a 409 stop (#850)");
|
|
6840
|
+
void pollLoop();
|
|
6841
|
+
}, CONFLICT_409_SELF_HEAL_MS);
|
|
6280
6842
|
}
|
|
6281
6843
|
async function pollLoop() {
|
|
6282
6844
|
while (running) {
|
|
6845
|
+
const ac = new AbortController();
|
|
6846
|
+
pollAbort = ac;
|
|
6847
|
+
let waitMs = pollMs;
|
|
6283
6848
|
try {
|
|
6284
6849
|
const offset = loadState(stateRoot).offset;
|
|
6285
|
-
const updates = await client.getUpdates(offset, LONG_POLL_SEC);
|
|
6850
|
+
const updates = await client.getUpdates(offset, LONG_POLL_SEC, ac.signal);
|
|
6851
|
+
conflictCount = 0;
|
|
6286
6852
|
for (const u of updates) {
|
|
6287
6853
|
await handleUpdate(u);
|
|
6288
6854
|
persistOffset(u.update_id + 1);
|
|
6289
6855
|
}
|
|
6290
6856
|
lastSuccessfulPollAt = Date.now();
|
|
6291
6857
|
} catch (e) {
|
|
6292
|
-
|
|
6858
|
+
if (ac.signal.aborted)
|
|
6859
|
+
break;
|
|
6860
|
+
const msg = e.message;
|
|
6861
|
+
lastError = msg;
|
|
6293
6862
|
lastErrorAt = Date.now();
|
|
6294
|
-
|
|
6863
|
+
if (e instanceof TelegramApiError && e.code === 409) {
|
|
6864
|
+
conflictCount += 1;
|
|
6865
|
+
if (conflictCount >= CONFLICT_409_TERMINAL_AFTER) {
|
|
6866
|
+
log(`telegram inbound poll stopped: repeated 409 conflict (another getUpdates consumer holds this token) \u2014 ${msg}`);
|
|
6867
|
+
break;
|
|
6868
|
+
}
|
|
6869
|
+
} else {
|
|
6870
|
+
log(`telegram inbound poll failed: ${msg}`);
|
|
6871
|
+
}
|
|
6872
|
+
waitMs = pollBackoffMs(e, pollMs);
|
|
6873
|
+
} finally {
|
|
6874
|
+
pollAbort = null;
|
|
6295
6875
|
}
|
|
6296
6876
|
if (running)
|
|
6297
|
-
await sleep2(
|
|
6877
|
+
await sleep2(waitMs);
|
|
6298
6878
|
}
|
|
6879
|
+
running = false;
|
|
6880
|
+
releasePollLock?.();
|
|
6881
|
+
releasePollLock = null;
|
|
6882
|
+
if (!pollStopped)
|
|
6883
|
+
scheduleSelfHeal();
|
|
6299
6884
|
}
|
|
6300
6885
|
return {
|
|
6301
6886
|
start() {
|
|
6302
6887
|
if (running)
|
|
6303
6888
|
return;
|
|
6889
|
+
pollStopped = false;
|
|
6890
|
+
selfHealCancel?.();
|
|
6891
|
+
selfHealCancel = null;
|
|
6892
|
+
if (!acquirePollLockOrLog())
|
|
6893
|
+
return;
|
|
6894
|
+
const known = opts.registeredProjects?.();
|
|
6895
|
+
if (known !== void 0) {
|
|
6896
|
+
try {
|
|
6897
|
+
const removed = pruneTopics(stateRoot, (p) => known.includes(p));
|
|
6898
|
+
if (removed.length > 0)
|
|
6899
|
+
log(`telegram registry pruned ${removed.length} stale topic link(s): ${removed.join(", ")}`);
|
|
6900
|
+
} catch (e) {
|
|
6901
|
+
log(`telegram registry prune failed: ${e.message}`);
|
|
6902
|
+
}
|
|
6903
|
+
}
|
|
6304
6904
|
running = true;
|
|
6905
|
+
conflictCount = 0;
|
|
6906
|
+
try {
|
|
6907
|
+
lifecycle?.start();
|
|
6908
|
+
} catch (e) {
|
|
6909
|
+
log(`telegram lifecycle start failed: ${e.message}`);
|
|
6910
|
+
}
|
|
6305
6911
|
void pollLoop();
|
|
6306
6912
|
},
|
|
6307
6913
|
stop() {
|
|
6308
6914
|
running = false;
|
|
6915
|
+
pollStopped = true;
|
|
6916
|
+
selfHealCancel?.();
|
|
6917
|
+
selfHealCancel = null;
|
|
6918
|
+
try {
|
|
6919
|
+
lifecycle?.stop();
|
|
6920
|
+
} catch (e) {
|
|
6921
|
+
log(`telegram lifecycle stop failed: ${e.message}`);
|
|
6922
|
+
}
|
|
6923
|
+
pollAbort?.abort();
|
|
6924
|
+
releasePollLock?.();
|
|
6925
|
+
releasePollLock = null;
|
|
6309
6926
|
},
|
|
6310
6927
|
pushLifecycle(project, ev) {
|
|
6311
6928
|
void deliverOutbound(project, ev).catch((e) => {
|
|
@@ -6322,17 +6939,23 @@ ${formatUsageLine(usage2)}`;
|
|
|
6322
6939
|
}
|
|
6323
6940
|
};
|
|
6324
6941
|
}
|
|
6325
|
-
var LONG_POLL_SEC, sleep2, CREW_TIERS, RECOGNIZED_CHANNEL_COMMANDS;
|
|
6942
|
+
var CAPTAIN_RECEIVED_ACK, LONG_POLL_SEC, POLL_LOCK_STALE_MS, CONFLICT_409_TERMINAL_AFTER, CONFLICT_409_SELF_HEAL_MS, sleep2, CREW_TIERS, RECOGNIZED_CHANNEL_COMMANDS;
|
|
6326
6943
|
var init_bridge = __esm({
|
|
6327
6944
|
"packages/core/dist/telegram/bridge.js"() {
|
|
6328
6945
|
init_dist();
|
|
6946
|
+
init_client();
|
|
6329
6947
|
init_auth();
|
|
6330
6948
|
init_commands();
|
|
6331
6949
|
init_format();
|
|
6950
|
+
init_notify();
|
|
6332
6951
|
init_panels();
|
|
6333
6952
|
init_state();
|
|
6334
6953
|
init_tiers();
|
|
6954
|
+
CAPTAIN_RECEIVED_ACK = "\u2705 captain received";
|
|
6335
6955
|
LONG_POLL_SEC = 50;
|
|
6956
|
+
POLL_LOCK_STALE_MS = 2 * 6e4;
|
|
6957
|
+
CONFLICT_409_TERMINAL_AFTER = 3;
|
|
6958
|
+
CONFLICT_409_SELF_HEAL_MS = 5 * 6e4;
|
|
6336
6959
|
sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6337
6960
|
CREW_TIERS = ["all", "alert_only", "done_only", "none"];
|
|
6338
6961
|
RECOGNIZED_CHANNEL_COMMANDS = /* @__PURE__ */ new Set(["status", "projects", "crews", "launch", "effort", "spawn"]);
|
|
@@ -6377,7 +7000,7 @@ var init_restart_daemon = __esm({
|
|
|
6377
7000
|
});
|
|
6378
7001
|
|
|
6379
7002
|
// packages/core/dist/telegram/setup.js
|
|
6380
|
-
import
|
|
7003
|
+
import fs12 from "fs";
|
|
6381
7004
|
function resolveSetupGroup(existingSupergroupId, opts) {
|
|
6382
7005
|
if (existingSupergroupId !== void 0 && !opts.redetect)
|
|
6383
7006
|
return "reuse";
|
|
@@ -6428,7 +7051,7 @@ function writeTelegramConfig(configPath, opts) {
|
|
|
6428
7051
|
let config;
|
|
6429
7052
|
let raw = null;
|
|
6430
7053
|
try {
|
|
6431
|
-
raw =
|
|
7054
|
+
raw = fs12.readFileSync(configPath, "utf-8");
|
|
6432
7055
|
} catch (err) {
|
|
6433
7056
|
if (err.code !== "ENOENT") {
|
|
6434
7057
|
throw new Error(`refusing to overwrite unreadable config at ${configPath}: ${String(err)}`);
|
|
@@ -6467,94 +7090,6 @@ var init_setup = __esm({
|
|
|
6467
7090
|
}
|
|
6468
7091
|
});
|
|
6469
7092
|
|
|
6470
|
-
// packages/core/dist/telegram/notify.js
|
|
6471
|
-
function runTelegramStatus(opts) {
|
|
6472
|
-
const tg = opts.config.telegram;
|
|
6473
|
-
const env = opts.env ?? process.env;
|
|
6474
|
-
const tokenSet = !!(tg?.botToken ?? env.TELEGRAM_BOT_TOKEN);
|
|
6475
|
-
const links = Object.entries(loadState(opts.stateRoot).topics).map(([key, topicId]) => {
|
|
6476
|
-
const sep3 = key.indexOf("::");
|
|
6477
|
-
return { project: key.slice(0, sep3), scope: key.slice(sep3 + 2), topicId };
|
|
6478
|
-
});
|
|
6479
|
-
return { tokenSet, supergroupId: tg?.supergroupId ?? null, links };
|
|
6480
|
-
}
|
|
6481
|
-
function runTelegramNotifySet(opts) {
|
|
6482
|
-
setNotify(opts.stateRoot, opts.project, opts.active);
|
|
6483
|
-
}
|
|
6484
|
-
function runTelegramNotifyPref(args) {
|
|
6485
|
-
const { project, dimension, value, root } = args;
|
|
6486
|
-
if (dimension === "crew") {
|
|
6487
|
-
if (!["all", "alert_only", "done_only", "none"].includes(value))
|
|
6488
|
-
return { ok: false, message: "crew must be all|alert_only|done_only|none" };
|
|
6489
|
-
saveProjectOverride(project, { telegram: { notify: { crew: value } } }, root);
|
|
6490
|
-
return { ok: true };
|
|
6491
|
-
}
|
|
6492
|
-
if (value !== "on" && value !== "off")
|
|
6493
|
-
return { ok: false, message: "cap must be on|off" };
|
|
6494
|
-
saveProjectOverride(project, { telegram: { notify: { cap: value === "on" } } }, root);
|
|
6495
|
-
return { ok: true };
|
|
6496
|
-
}
|
|
6497
|
-
function runTelegramNotifyStatus(opts) {
|
|
6498
|
-
const s = loadState(opts.stateRoot);
|
|
6499
|
-
const projects = /* @__PURE__ */ new Set();
|
|
6500
|
-
for (const key of Object.keys(s.topics)) {
|
|
6501
|
-
const sep3 = key.indexOf("::");
|
|
6502
|
-
projects.add(sep3 === -1 ? key : key.slice(0, sep3));
|
|
6503
|
-
}
|
|
6504
|
-
for (const p of Object.keys(s.notify))
|
|
6505
|
-
projects.add(p);
|
|
6506
|
-
return [...projects].map((project) => ({ project, active: s.notify[project] === true }));
|
|
6507
|
-
}
|
|
6508
|
-
function capAllowed(project, globalNotify, root) {
|
|
6509
|
-
return resolveNotify(globalNotify, loadProjectOverride(project, root)).cap;
|
|
6510
|
-
}
|
|
6511
|
-
function confirmationText(project, before, after, dim) {
|
|
6512
|
-
if (dim === "active")
|
|
6513
|
-
return `\u{1F515} ${project} \u2014 all notifications muted here. Unmute: squadrant telegram notify ${project} on`;
|
|
6514
|
-
if (dim === "cap")
|
|
6515
|
-
return `\u{1F515} ${project} \u2014 captain messages muted here. Re-enable: squadrant telegram notify ${project} cap on`;
|
|
6516
|
-
return `\u{1F515} ${project} \u2014 crew notifications now '${after.crew}' (was '${before.crew}'). Re-enable: squadrant telegram notify ${project} crew ${before.crew}`;
|
|
6517
|
-
}
|
|
6518
|
-
async function runNotifyConfirmation(opts) {
|
|
6519
|
-
const { quieter, dim } = isQuieter(opts.before, opts.after);
|
|
6520
|
-
if (!quieter || dim === null)
|
|
6521
|
-
return false;
|
|
6522
|
-
const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
|
|
6523
|
-
if (topicId === void 0)
|
|
6524
|
-
return false;
|
|
6525
|
-
const text = confirmationText(opts.project, opts.before, opts.after, dim);
|
|
6526
|
-
try {
|
|
6527
|
-
await opts.client.sendMessage(opts.cfg.supergroupId, topicId, text);
|
|
6528
|
-
return true;
|
|
6529
|
-
} catch {
|
|
6530
|
-
console.warn(`[squadrant] mute-confirmation send failed for ${opts.project} \u2014 notification preference was still saved`);
|
|
6531
|
-
return false;
|
|
6532
|
-
}
|
|
6533
|
-
}
|
|
6534
|
-
async function runTelegramSend(opts) {
|
|
6535
|
-
const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
|
|
6536
|
-
if (topicId === void 0) {
|
|
6537
|
-
throw new Error(`project "${opts.project}" is not linked \u2014 run: squadrant telegram link ${opts.project}`);
|
|
6538
|
-
}
|
|
6539
|
-
await opts.client.sendMessage(opts.cfg.supergroupId, topicId, opts.message);
|
|
6540
|
-
return { chatId: opts.cfg.supergroupId, topicId };
|
|
6541
|
-
}
|
|
6542
|
-
async function runTelegramLink(opts) {
|
|
6543
|
-
const existing = loadState(opts.stateRoot).topics[topicKey(opts.project)];
|
|
6544
|
-
if (existing !== void 0)
|
|
6545
|
-
return { topicId: existing, created: false };
|
|
6546
|
-
const topicId = await opts.client.createForumTopic(opts.cfg.supergroupId, topicName(opts.project));
|
|
6547
|
-
setTopic(opts.stateRoot, opts.project, topicId);
|
|
6548
|
-
return { topicId, created: true };
|
|
6549
|
-
}
|
|
6550
|
-
var init_notify = __esm({
|
|
6551
|
-
"packages/core/dist/telegram/notify.js"() {
|
|
6552
|
-
init_dist();
|
|
6553
|
-
init_state();
|
|
6554
|
-
init_format();
|
|
6555
|
-
}
|
|
6556
|
-
});
|
|
6557
|
-
|
|
6558
7093
|
// packages/core/dist/telegram/index.js
|
|
6559
7094
|
var init_telegram = __esm({
|
|
6560
7095
|
"packages/core/dist/telegram/index.js"() {
|
|
@@ -6566,6 +7101,7 @@ var init_telegram = __esm({
|
|
|
6566
7101
|
init_format();
|
|
6567
7102
|
init_state();
|
|
6568
7103
|
init_client();
|
|
7104
|
+
init_inbound_lifecycle();
|
|
6569
7105
|
init_bridge();
|
|
6570
7106
|
init_setup();
|
|
6571
7107
|
init_notify();
|
|
@@ -6760,6 +7296,26 @@ function writeJson(res, status, body) {
|
|
|
6760
7296
|
res.writeHead(status, { "content-type": "application/json" });
|
|
6761
7297
|
res.end(JSON.stringify(body));
|
|
6762
7298
|
}
|
|
7299
|
+
function pathnameOf(url) {
|
|
7300
|
+
if (!url)
|
|
7301
|
+
return "";
|
|
7302
|
+
const q = url.indexOf("?");
|
|
7303
|
+
return q === -1 ? url : url.slice(0, q);
|
|
7304
|
+
}
|
|
7305
|
+
function modelsList(ids) {
|
|
7306
|
+
const data = ids.map((id) => ({
|
|
7307
|
+
type: "model",
|
|
7308
|
+
id,
|
|
7309
|
+
display_name: id,
|
|
7310
|
+
created_at: "1970-01-01T00:00:00Z"
|
|
7311
|
+
}));
|
|
7312
|
+
return {
|
|
7313
|
+
data,
|
|
7314
|
+
has_more: false,
|
|
7315
|
+
first_id: ids[0] ?? null,
|
|
7316
|
+
last_id: ids[ids.length - 1] ?? null
|
|
7317
|
+
};
|
|
7318
|
+
}
|
|
6763
7319
|
function buildUpstreamHeaders(req, upstream, stream) {
|
|
6764
7320
|
const headers = {
|
|
6765
7321
|
"content-type": "application/json",
|
|
@@ -6788,6 +7344,7 @@ function createRouterShim(opts) {
|
|
|
6788
7344
|
let lastError;
|
|
6789
7345
|
let upstreamReachable = true;
|
|
6790
7346
|
const upstreamUrl = joinUrl(opts.upstream.baseUrl, "/v1/messages");
|
|
7347
|
+
const advertisedModels = new Set(opts.models ?? []);
|
|
6791
7348
|
function emitUsage(u) {
|
|
6792
7349
|
try {
|
|
6793
7350
|
opts.onUsage?.(u);
|
|
@@ -6817,6 +7374,8 @@ function createRouterShim(opts) {
|
|
|
6817
7374
|
const body = parsed;
|
|
6818
7375
|
const wantsStream = body.stream === true;
|
|
6819
7376
|
const model = typeof body.model === "string" ? body.model : void 0;
|
|
7377
|
+
if (model)
|
|
7378
|
+
advertisedModels.add(model);
|
|
6820
7379
|
const sanitized = sanitizeRequest(body, opts.upstream);
|
|
6821
7380
|
let upstreamRes;
|
|
6822
7381
|
try {
|
|
@@ -6889,8 +7448,20 @@ function createRouterShim(opts) {
|
|
|
6889
7448
|
emitUsage(usage2);
|
|
6890
7449
|
writeJson(res, upstreamRes.status, upstreamBody);
|
|
6891
7450
|
}
|
|
7451
|
+
async function handleCountTokens(req, res) {
|
|
7452
|
+
let text;
|
|
7453
|
+
try {
|
|
7454
|
+
text = await readBody(req);
|
|
7455
|
+
} catch {
|
|
7456
|
+
const e = anthropicError(400, "invalid_request_error", "malformed request body");
|
|
7457
|
+
writeJson(res, e.status, e.body);
|
|
7458
|
+
return;
|
|
7459
|
+
}
|
|
7460
|
+
writeJson(res, 200, { input_tokens: Math.max(1, Math.ceil(text.length / 4)) });
|
|
7461
|
+
}
|
|
6892
7462
|
function handler(req, res) {
|
|
6893
|
-
|
|
7463
|
+
const path23 = pathnameOf(req.url);
|
|
7464
|
+
if (req.method === "POST" && path23 === "/v1/messages") {
|
|
6894
7465
|
void handleMessages(req, res).catch((err) => {
|
|
6895
7466
|
log(`router internal error: ${err instanceof Error ? err.message : String(err)}`);
|
|
6896
7467
|
if (!res.headersSent) {
|
|
@@ -6902,7 +7473,18 @@ function createRouterShim(opts) {
|
|
|
6902
7473
|
});
|
|
6903
7474
|
return;
|
|
6904
7475
|
}
|
|
6905
|
-
if (req.method === "
|
|
7476
|
+
if (req.method === "POST" && path23 === "/v1/messages/count_tokens") {
|
|
7477
|
+
void handleCountTokens(req, res).catch(() => {
|
|
7478
|
+
if (!res.headersSent)
|
|
7479
|
+
writeJson(res, 502, anthropicError(502, "api_error", "router internal error").body);
|
|
7480
|
+
});
|
|
7481
|
+
return;
|
|
7482
|
+
}
|
|
7483
|
+
if (req.method === "GET" && path23 === "/v1/models") {
|
|
7484
|
+
writeJson(res, 200, modelsList([...advertisedModels]));
|
|
7485
|
+
return;
|
|
7486
|
+
}
|
|
7487
|
+
if (req.method === "GET" && path23 === "/healthz") {
|
|
6906
7488
|
writeJson(res, 200, { ready: server?.listening === true, upstreamReachable, lastError });
|
|
6907
7489
|
return;
|
|
6908
7490
|
}
|
|
@@ -7027,12 +7609,17 @@ function createRouterService(router, projects, deps = {}) {
|
|
|
7027
7609
|
tokenByProject.set(p, t);
|
|
7028
7610
|
projectTokens.set(t, p);
|
|
7029
7611
|
}
|
|
7612
|
+
const models = [
|
|
7613
|
+
...deps.models ?? [],
|
|
7614
|
+
...Object.values(router.models ?? {}).map((m) => m.upstream)
|
|
7615
|
+
];
|
|
7030
7616
|
const ledger = createUsageLedger();
|
|
7031
7617
|
const shim = createRouterShim({
|
|
7032
7618
|
upstream,
|
|
7033
7619
|
projectTokens,
|
|
7034
7620
|
port: router.port ?? 0,
|
|
7035
7621
|
log,
|
|
7622
|
+
models,
|
|
7036
7623
|
onUsage: (u) => ledger.record(u),
|
|
7037
7624
|
...deps.fetch ? { fetch: deps.fetch } : {}
|
|
7038
7625
|
});
|
|
@@ -7095,12 +7682,53 @@ var init_router = __esm({
|
|
|
7095
7682
|
}
|
|
7096
7683
|
});
|
|
7097
7684
|
|
|
7685
|
+
// packages/core/dist/captain-router.js
|
|
7686
|
+
async function prepareCaptainRoute(o) {
|
|
7687
|
+
assertBackendUsable({ backend: o.backend, agent: o.agentName, router: o.config.defaults.router });
|
|
7688
|
+
if (o.backend === "native") {
|
|
7689
|
+
return { backend: "native", model: o.model, permissionMode: o.configuredPermissionMode, env: {} };
|
|
7690
|
+
}
|
|
7691
|
+
if (!o.deps.routerCredentials) {
|
|
7692
|
+
throw new Error(`captain launch: backend '${o.backend}' requires router credentials from the daemon, but this launch path has no credentials provider`);
|
|
7693
|
+
}
|
|
7694
|
+
if (!o.deps.writeRouterSettings) {
|
|
7695
|
+
throw new Error(`captain launch: backend '${o.backend}' requires a per-spawn --settings writer to outrank defaults.claudeEnv, but this launch path has none \u2014 refusing to launch a routed captain that would silently bypass the router shim`);
|
|
7696
|
+
}
|
|
7697
|
+
const model = resolveRouterModel(o.model, o.agentName, o.config.defaults.router);
|
|
7698
|
+
const creds = await o.deps.routerCredentials({ project: o.project, backend: o.backend });
|
|
7699
|
+
const routerEnv = buildRouterEnv(creds, model);
|
|
7700
|
+
const shadowed = claudeEnvShadowsRouter(o.config.defaults.claudeEnv);
|
|
7701
|
+
if (shadowed.length > 0) {
|
|
7702
|
+
o.warn?.(`backend '${o.backend}' selected but defaults.claudeEnv sets ${shadowed.join(", ")} \u2014 these would shadow the router shim. Overriding via a per-spawn --settings env block (outranks ~/.claude/settings.json).`);
|
|
7703
|
+
}
|
|
7704
|
+
const settingsPath = o.deps.writeRouterSettings({
|
|
7705
|
+
stateRoot: o.stateRoot,
|
|
7706
|
+
project: o.project,
|
|
7707
|
+
taskId: "captain",
|
|
7708
|
+
env: mergeClaudeEnvRouterSettings(routerEnv, o.config.defaults.claudeEnv)
|
|
7709
|
+
});
|
|
7710
|
+
return {
|
|
7711
|
+
backend: o.backend,
|
|
7712
|
+
model,
|
|
7713
|
+
permissionMode: "default",
|
|
7714
|
+
env: { ...routerEnv, SQUADRANT_GATE: "on" },
|
|
7715
|
+
settingsPath
|
|
7716
|
+
};
|
|
7717
|
+
}
|
|
7718
|
+
var init_captain_router = __esm({
|
|
7719
|
+
"packages/core/dist/captain-router.js"() {
|
|
7720
|
+
init_dist();
|
|
7721
|
+
init_router_resolution();
|
|
7722
|
+
init_env();
|
|
7723
|
+
}
|
|
7724
|
+
});
|
|
7725
|
+
|
|
7098
7726
|
// packages/core/dist/permission-gate.js
|
|
7099
7727
|
import { createHash } from "crypto";
|
|
7100
7728
|
import { mkdirSync as mkdirSync7, readFileSync as readFileSync11, renameSync as renameSync4, writeFileSync as writeFileSync10, unlinkSync as unlinkSync4 } from "fs";
|
|
7101
7729
|
import { dirname as dirname5, join as join15 } from "path";
|
|
7102
7730
|
function isGateSession(env) {
|
|
7103
|
-
return Boolean(env.SQUADRANT_CREW_TASK_ID) || env[SIDE_SESSION_ENV] === "1";
|
|
7731
|
+
return Boolean(env.SQUADRANT_CREW_TASK_ID) || env[SIDE_SESSION_ENV] === "1" || env.SQUADRANT_ROLE === "captain";
|
|
7104
7732
|
}
|
|
7105
7733
|
function resolveGateMode(env, gate) {
|
|
7106
7734
|
const fromEnv = env.SQUADRANT_GATE;
|
|
@@ -7296,7 +7924,15 @@ async function callClassifier(o) {
|
|
|
7296
7924
|
return "ask";
|
|
7297
7925
|
}
|
|
7298
7926
|
const data = await res.json();
|
|
7927
|
+
if (data?.stop_reason === "max_tokens") {
|
|
7928
|
+
o.log?.("gate: classifier exhausted max_tokens before a verdict \u2014 asking");
|
|
7929
|
+
return "ask";
|
|
7930
|
+
}
|
|
7299
7931
|
const text = Array.isArray(data?.content) ? data.content.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join(" ") : "";
|
|
7932
|
+
if (!text.trim()) {
|
|
7933
|
+
o.log?.("gate: classifier returned no text verdict \u2014 asking");
|
|
7934
|
+
return "ask";
|
|
7935
|
+
}
|
|
7300
7936
|
return parseClassifierVerdict(text);
|
|
7301
7937
|
} catch (e) {
|
|
7302
7938
|
o.log?.(`gate: classifier unavailable (${e.message}) \u2014 asking`);
|
|
@@ -7315,7 +7951,7 @@ ${toolPayload}`).digest("hex");
|
|
|
7315
7951
|
}
|
|
7316
7952
|
async function evaluatePermissionRequest(input) {
|
|
7317
7953
|
if (!isGateSession(input.env)) {
|
|
7318
|
-
return { decision: "yield", reason: "not a squadrant crew/side session" };
|
|
7954
|
+
return { decision: "yield", reason: "not a squadrant crew/side/captain session" };
|
|
7319
7955
|
}
|
|
7320
7956
|
const gate = input.config.defaults.gate;
|
|
7321
7957
|
const mode = resolveGateMode(input.env, gate);
|
|
@@ -7401,7 +8037,7 @@ var init_permission_gate = __esm({
|
|
|
7401
8037
|
DEFAULT_CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
7402
8038
|
DEFAULT_CACHE_MAX_ENTRIES = 500;
|
|
7403
8039
|
DEFAULT_CLASSIFIER_TIMEOUT_MS = 5e3;
|
|
7404
|
-
CLASSIFIER_MAX_TOKENS =
|
|
8040
|
+
CLASSIFIER_MAX_TOKENS = 256;
|
|
7405
8041
|
MAX_PAYLOAD_CHARS = 2e3;
|
|
7406
8042
|
DEFAULT_GATE_DENY_RULES = [
|
|
7407
8043
|
// Flag-order-agnostic: both recursive and force must be present, in any order,
|
|
@@ -7731,7 +8367,7 @@ var init_launch_workspace = __esm({
|
|
|
7731
8367
|
});
|
|
7732
8368
|
|
|
7733
8369
|
// packages/core/dist/side-session.js
|
|
7734
|
-
import
|
|
8370
|
+
import fs13 from "fs";
|
|
7735
8371
|
function sideTitleFor(project, name) {
|
|
7736
8372
|
return `\u{1F5D2} ${project}:${name}`;
|
|
7737
8373
|
}
|
|
@@ -7833,7 +8469,7 @@ async function runSideClose(runtime, workspaceId, project, name, projPath, workt
|
|
|
7833
8469
|
await runtime.closePane(pane);
|
|
7834
8470
|
if (projPath) {
|
|
7835
8471
|
const wtPath = worktreePath(projPath, worktreeDir, project, name);
|
|
7836
|
-
if (
|
|
8472
|
+
if (fs13.existsSync(wtPath)) {
|
|
7837
8473
|
try {
|
|
7838
8474
|
removeWorktree(projPath, wtPath);
|
|
7839
8475
|
} catch (e) {
|
|
@@ -8270,8 +8906,10 @@ __export(dist_exports, {
|
|
|
8270
8906
|
AGENT_BINS: () => AGENT_BINS,
|
|
8271
8907
|
ANTHROPIC_ONLY_REQUEST_FIELDS: () => ANTHROPIC_ONLY_REQUEST_FIELDS,
|
|
8272
8908
|
BOT_COMMANDS: () => BOT_COMMANDS,
|
|
8909
|
+
CAPTAIN_RECEIVED_ACK: () => CAPTAIN_RECEIVED_ACK,
|
|
8273
8910
|
CC_SOCKS_DIR: () => CC_SOCKS_DIR,
|
|
8274
8911
|
CMUX_PRESERVE_CLAUDE_AUTH_ENV: () => CMUX_PRESERVE_CLAUDE_AUTH_ENV,
|
|
8912
|
+
CONFLICT_409_SELF_HEAL_MS: () => CONFLICT_409_SELF_HEAL_MS,
|
|
8275
8913
|
CREW_GONE_MS: () => CREW_GONE_MS,
|
|
8276
8914
|
CREW_STALE_MS: () => CREW_STALE_MS,
|
|
8277
8915
|
CaptainDelivery: () => CaptainDelivery,
|
|
@@ -8291,6 +8929,7 @@ __export(dist_exports, {
|
|
|
8291
8929
|
LivenessRegistry: () => LivenessRegistry,
|
|
8292
8930
|
MONITOR_STALL_BUDGET_MS: () => MONITOR_STALL_BUDGET_MS,
|
|
8293
8931
|
OPERATOR_INITIATED_COMMANDS: () => OPERATOR_INITIATED_COMMANDS,
|
|
8932
|
+
PANE_IDLE_AFTER_MS: () => PANE_IDLE_AFTER_MS,
|
|
8294
8933
|
PROBE_QUIET_MS: () => PROBE_QUIET_MS,
|
|
8295
8934
|
PROTOCOL_VERSION: () => PROTOCOL_VERSION,
|
|
8296
8935
|
READ_ONLY_CREW_SUBCOMMANDS: () => READ_ONLY_CREW_SUBCOMMANDS,
|
|
@@ -8303,6 +8942,10 @@ __export(dist_exports, {
|
|
|
8303
8942
|
TERMINAL_RECORD_KEEP_PER_PROJECT: () => TERMINAL_RECORD_KEEP_PER_PROJECT,
|
|
8304
8943
|
TERMINAL_RECORD_TTL_MS: () => TERMINAL_RECORD_TTL_MS,
|
|
8305
8944
|
TOOL_STALL_BUDGET_MS: () => TOOL_STALL_BUDGET_MS,
|
|
8945
|
+
TYPING_KEEPALIVE_MS: () => TYPING_KEEPALIVE_MS,
|
|
8946
|
+
TelegramApiError: () => TelegramApiError,
|
|
8947
|
+
WATCHDOG_GRACE_MS: () => WATCHDOG_GRACE_MS,
|
|
8948
|
+
WATCHDOG_MS: () => WATCHDOG_MS,
|
|
8306
8949
|
WORK_ITEM_TTL_MS: () => WORK_ITEM_TTL_MS,
|
|
8307
8950
|
WRITABLE_CONFIG_KEYS: () => WRITABLE_CONFIG_KEYS,
|
|
8308
8951
|
_resetRestartInFlightForTest: () => _resetRestartInFlightForTest,
|
|
@@ -8324,14 +8967,19 @@ __export(dist_exports, {
|
|
|
8324
8967
|
callClassifier: () => callClassifier,
|
|
8325
8968
|
capAllowed: () => capAllowed,
|
|
8326
8969
|
capOutput: () => capOutput,
|
|
8970
|
+
captainAliveStateFromHealth: () => captainAliveStateFromHealth,
|
|
8327
8971
|
captainRecordPath: () => captainRecordPath,
|
|
8328
8972
|
captainSocketPath: () => captainSocketPath,
|
|
8329
8973
|
checkFact: () => checkFact,
|
|
8974
|
+
classifyCaptainPane: () => classifyCaptainPane,
|
|
8330
8975
|
classifyHealth: () => classifyHealth,
|
|
8976
|
+
claudeEnvShadowsRouter: () => claudeEnvShadowsRouter,
|
|
8977
|
+
clearPending: () => clearPending,
|
|
8331
8978
|
closeWorkItem: () => closeWorkItem,
|
|
8332
8979
|
computeTemplateHash: () => computeTemplateHash,
|
|
8333
8980
|
consumeExitMarker: () => consumeExitMarker,
|
|
8334
8981
|
createAttach: () => createAttach,
|
|
8982
|
+
createCaptainPaneReader: () => createCaptainPaneReader,
|
|
8335
8983
|
createCrewPaneReader: () => createCrewPaneReader,
|
|
8336
8984
|
createDaemon: () => createDaemon,
|
|
8337
8985
|
createDecoder: () => createDecoder,
|
|
@@ -8340,6 +8988,7 @@ __export(dist_exports, {
|
|
|
8340
8988
|
createDirectSurfaceLivenessProbe: () => createDirectSurfaceLivenessProbe,
|
|
8341
8989
|
createEnsureCaptainAlive: () => createEnsureCaptainAlive,
|
|
8342
8990
|
createEventsSource: () => createEventsSource,
|
|
8991
|
+
createInboundLifecycle: () => createInboundLifecycle,
|
|
8343
8992
|
createInteractiveProbe: () => createInteractiveProbe,
|
|
8344
8993
|
createIsCaptainAlive: () => createIsCaptainAlive,
|
|
8345
8994
|
createLaunch: () => createLaunch,
|
|
@@ -8395,12 +9044,16 @@ __export(dist_exports, {
|
|
|
8395
9044
|
formatInbound: () => formatInbound,
|
|
8396
9045
|
formatInboundReceipt: () => formatInboundReceipt,
|
|
8397
9046
|
formatLifecycle: () => formatLifecycle,
|
|
9047
|
+
formatMediaReceipt: () => formatMediaReceipt,
|
|
8398
9048
|
formatPermissionDecision: () => formatPermissionDecision,
|
|
8399
9049
|
formatUsageLine: () => formatUsageLine,
|
|
9050
|
+
formatWatchdogWarning: () => formatWatchdogWarning,
|
|
8400
9051
|
freshTrace: () => freshTrace,
|
|
8401
9052
|
gateCacheKey: () => gateCacheKey,
|
|
8402
9053
|
getDaemonPid: () => getDaemonPid,
|
|
9054
|
+
hasModalOptionList: () => hasModalOptionList,
|
|
8403
9055
|
healCmdFor: () => healCmdFor,
|
|
9056
|
+
inboundBody: () => inboundBody,
|
|
8404
9057
|
isAuthorized: () => isAuthorized,
|
|
8405
9058
|
isBareSpawn: () => isBareSpawn,
|
|
8406
9059
|
isCaptainAlive: () => isCaptainAlive,
|
|
@@ -8422,17 +9075,23 @@ __export(dist_exports, {
|
|
|
8422
9075
|
kickstartArgv: () => kickstartArgv,
|
|
8423
9076
|
launchOneWorkspace: () => launchOneWorkspace,
|
|
8424
9077
|
listSessions: () => listSessions,
|
|
9078
|
+
loadPending: () => loadPending,
|
|
8425
9079
|
loadSessions: () => loadSessions,
|
|
8426
9080
|
loadState: () => loadState,
|
|
8427
9081
|
mailboxStats: () => mailboxStats,
|
|
8428
9082
|
makeGate: () => makeGate,
|
|
9083
|
+
markPendingWarned: () => markPendingWarned,
|
|
8429
9084
|
maskToken: () => maskToken,
|
|
8430
9085
|
matchTier1Deny: () => matchTier1Deny,
|
|
9086
|
+
mediaKind: () => mediaKind,
|
|
9087
|
+
mediaMarker: () => mediaMarker,
|
|
9088
|
+
mergeClaudeEnvRouterSettings: () => mergeClaudeEnvRouterSettings,
|
|
8431
9089
|
mintToken: () => mintToken,
|
|
8432
9090
|
nameFromTitle: () => nameFromTitle,
|
|
8433
9091
|
newestSessionInDirectory: () => newestSessionInDirectory,
|
|
8434
9092
|
nextAutoName: () => nextAutoName,
|
|
8435
9093
|
niceCrewCommand: () => niceCrewCommand,
|
|
9094
|
+
notePaneScreen: () => notePaneScreen,
|
|
8436
9095
|
notifyToggle: () => notifyToggle,
|
|
8437
9096
|
parseBearer: () => parseBearer,
|
|
8438
9097
|
parseClassifierVerdict: () => parseClassifierVerdict,
|
|
@@ -8441,12 +9100,15 @@ __export(dist_exports, {
|
|
|
8441
9100
|
parseNotifyPref: () => parseNotifyPref,
|
|
8442
9101
|
parseProgramArgs: () => parseProgramArgs,
|
|
8443
9102
|
plistPath: () => plistPath,
|
|
9103
|
+
pollBackoffMs: () => pollBackoffMs,
|
|
8444
9104
|
pollNewestSessionInDirectory: () => pollNewestSessionInDirectory,
|
|
9105
|
+
prepareCaptainRoute: () => prepareCaptainRoute,
|
|
8445
9106
|
printForeignInstallError: () => printForeignInstallError,
|
|
8446
9107
|
printVersionUpgradeNotice: () => printVersionUpgradeNotice,
|
|
8447
9108
|
probeCaptainChannel: () => probeCaptainChannel,
|
|
8448
9109
|
programArgsBlock: () => programArgsBlock,
|
|
8449
9110
|
projectHealth: () => projectHealth,
|
|
9111
|
+
pruneTopics: () => pruneTopics,
|
|
8450
9112
|
purgeExpiredWorkItems: () => purgeExpiredWorkItems,
|
|
8451
9113
|
readCaptainAddress: () => readCaptainAddress,
|
|
8452
9114
|
readCursor: () => readCursor,
|
|
@@ -8513,9 +9175,11 @@ __export(dist_exports, {
|
|
|
8513
9175
|
saveSessions: () => saveSessions,
|
|
8514
9176
|
saveState: () => saveState,
|
|
8515
9177
|
screenHasSplashMarker: () => screenHasSplashMarker,
|
|
9178
|
+
screenHash: () => screenHash,
|
|
8516
9179
|
sendRequest: () => sendRequest,
|
|
8517
9180
|
setLastUserId: () => setLastUserId,
|
|
8518
9181
|
setNotify: () => setNotify,
|
|
9182
|
+
setPending: () => setPending,
|
|
8519
9183
|
setTopic: () => setTopic,
|
|
8520
9184
|
shellQuote: () => shellQuote,
|
|
8521
9185
|
shouldBuildRouterService: () => shouldBuildRouterService,
|
|
@@ -8528,6 +9192,7 @@ __export(dist_exports, {
|
|
|
8528
9192
|
startServer: () => startServer,
|
|
8529
9193
|
stripBotMention: () => stripBotMention,
|
|
8530
9194
|
surfaceVerdict: () => surfaceVerdict,
|
|
9195
|
+
telegramPollLockPath: () => telegramPollLockPath,
|
|
8531
9196
|
timeoutGate: () => timeoutGate,
|
|
8532
9197
|
titleFor: () => titleFor,
|
|
8533
9198
|
toControlEvent: () => toControlEvent,
|
|
@@ -8574,6 +9239,7 @@ var init_dist2 = __esm({
|
|
|
8574
9239
|
init_router();
|
|
8575
9240
|
init_crew_routing();
|
|
8576
9241
|
init_router_resolution();
|
|
9242
|
+
init_captain_router();
|
|
8577
9243
|
init_permission_gate();
|
|
8578
9244
|
init_restart_daemon();
|
|
8579
9245
|
init_group_dispatch();
|
|
@@ -8620,7 +9286,7 @@ import { readdirSync as readdirSync4, readFileSync as readFileSync12 } from "fs"
|
|
|
8620
9286
|
import { join as join18 } from "path";
|
|
8621
9287
|
|
|
8622
9288
|
// packages/agents/dist/claude/registry.js
|
|
8623
|
-
import
|
|
9289
|
+
import fs14 from "fs";
|
|
8624
9290
|
import { join as join17 } from "path";
|
|
8625
9291
|
import { homedir as homedir7 } from "os";
|
|
8626
9292
|
var CLAUDE_SESSIONS_DIR = join17(homedir7(), ".claude", "sessions");
|
|
@@ -8665,11 +9331,11 @@ function toLifecycleSnapshot(entry, taskId, alive, now) {
|
|
|
8665
9331
|
function readClaudeStatusBySocketPath(socketPath) {
|
|
8666
9332
|
let files;
|
|
8667
9333
|
try {
|
|
8668
|
-
files =
|
|
9334
|
+
files = fs14.readdirSync(CLAUDE_SESSIONS_DIR);
|
|
8669
9335
|
} catch {
|
|
8670
9336
|
return void 0;
|
|
8671
9337
|
}
|
|
8672
|
-
const entries = parseRegistryDir(files, (name) =>
|
|
9338
|
+
const entries = parseRegistryDir(files, (name) => fs14.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
|
|
8673
9339
|
const entry = entries.find((e) => e.messagingSocketPath === socketPath);
|
|
8674
9340
|
if (!entry)
|
|
8675
9341
|
return void 0;
|
|
@@ -8692,7 +9358,7 @@ import { homedir as homedir8 } from "os";
|
|
|
8692
9358
|
var SQUADRANT_STATE_DIR = join19(homedir8(), ".config", "squadrant", "state");
|
|
8693
9359
|
|
|
8694
9360
|
// packages/agents/dist/drivers/launch-cmd.js
|
|
8695
|
-
import
|
|
9361
|
+
import fs15 from "fs";
|
|
8696
9362
|
import path13 from "path";
|
|
8697
9363
|
|
|
8698
9364
|
// packages/agents/dist/projection/cursor.js
|
|
@@ -8716,7 +9382,7 @@ import path17 from "path";
|
|
|
8716
9382
|
import os9 from "os";
|
|
8717
9383
|
|
|
8718
9384
|
// packages/agents/dist/projection/opencode-skills.js
|
|
8719
|
-
import
|
|
9385
|
+
import fs16 from "fs";
|
|
8720
9386
|
import os10 from "os";
|
|
8721
9387
|
import path18 from "path";
|
|
8722
9388
|
var OPENCODE_SKILL_MARKER = ".squadrant-managed";
|
|
@@ -8738,62 +9404,62 @@ function parseSkillName(raw) {
|
|
|
8738
9404
|
}
|
|
8739
9405
|
function listSourceSkills(sourceSkillsDir) {
|
|
8740
9406
|
const out = /* @__PURE__ */ new Map();
|
|
8741
|
-
if (!
|
|
9407
|
+
if (!fs16.existsSync(sourceSkillsDir))
|
|
8742
9408
|
return out;
|
|
8743
|
-
for (const entry of
|
|
9409
|
+
for (const entry of fs16.readdirSync(sourceSkillsDir, { withFileTypes: true })) {
|
|
8744
9410
|
if (!entry.isDirectory())
|
|
8745
9411
|
continue;
|
|
8746
9412
|
const skillPath = path18.join(sourceSkillsDir, entry.name, "SKILL.md");
|
|
8747
|
-
if (!
|
|
9413
|
+
if (!fs16.existsSync(skillPath))
|
|
8748
9414
|
continue;
|
|
8749
|
-
const name = parseSkillName(
|
|
9415
|
+
const name = parseSkillName(fs16.readFileSync(skillPath, "utf-8"));
|
|
8750
9416
|
if (name && SAFE_NAME.test(name))
|
|
8751
9417
|
out.set(name, skillPath);
|
|
8752
9418
|
}
|
|
8753
9419
|
return out;
|
|
8754
9420
|
}
|
|
8755
9421
|
function hasMarker(dir) {
|
|
8756
|
-
return
|
|
9422
|
+
return fs16.existsSync(path18.join(dir, OPENCODE_SKILL_MARKER));
|
|
8757
9423
|
}
|
|
8758
9424
|
function syncOpencodeSkills(opts) {
|
|
8759
9425
|
const result = { written: [], unchanged: [], removed: [], skipped: [] };
|
|
8760
9426
|
const wanted = listSourceSkills(opts.sourceSkillsDir);
|
|
8761
|
-
if (wanted.size === 0 && !
|
|
9427
|
+
if (wanted.size === 0 && !fs16.existsSync(opts.sourceSkillsDir))
|
|
8762
9428
|
return result;
|
|
8763
9429
|
if (!opts.dryRun)
|
|
8764
|
-
|
|
9430
|
+
fs16.mkdirSync(opts.skillsRoot, { recursive: true });
|
|
8765
9431
|
for (const [name, srcPath] of wanted) {
|
|
8766
9432
|
const targetDir = path18.join(opts.skillsRoot, name);
|
|
8767
9433
|
const targetSkill = path18.join(targetDir, "SKILL.md");
|
|
8768
9434
|
const managed = hasMarker(targetDir);
|
|
8769
|
-
if (
|
|
9435
|
+
if (fs16.existsSync(targetSkill) && !managed) {
|
|
8770
9436
|
result.skipped.push({ name, reason: "foreign-collision" });
|
|
8771
9437
|
continue;
|
|
8772
9438
|
}
|
|
8773
|
-
const current =
|
|
8774
|
-
if (current && current.equals(
|
|
9439
|
+
const current = fs16.existsSync(targetSkill) ? fs16.readFileSync(targetSkill) : null;
|
|
9440
|
+
if (current && current.equals(fs16.readFileSync(srcPath))) {
|
|
8775
9441
|
if (!managed && !opts.dryRun) {
|
|
8776
|
-
|
|
9442
|
+
fs16.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
|
|
8777
9443
|
}
|
|
8778
9444
|
result.unchanged.push(name);
|
|
8779
9445
|
continue;
|
|
8780
9446
|
}
|
|
8781
9447
|
if (!opts.dryRun) {
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
9448
|
+
fs16.mkdirSync(targetDir, { recursive: true });
|
|
9449
|
+
fs16.copyFileSync(srcPath, targetSkill);
|
|
9450
|
+
fs16.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
|
|
8785
9451
|
}
|
|
8786
9452
|
result.written.push(name);
|
|
8787
9453
|
}
|
|
8788
|
-
if (
|
|
8789
|
-
for (const entry of
|
|
9454
|
+
if (fs16.existsSync(opts.skillsRoot)) {
|
|
9455
|
+
for (const entry of fs16.readdirSync(opts.skillsRoot, { withFileTypes: true })) {
|
|
8790
9456
|
if (!entry.isDirectory() || wanted.has(entry.name))
|
|
8791
9457
|
continue;
|
|
8792
9458
|
const dir = path18.join(opts.skillsRoot, entry.name);
|
|
8793
9459
|
if (!hasMarker(dir))
|
|
8794
9460
|
continue;
|
|
8795
9461
|
if (!opts.dryRun)
|
|
8796
|
-
|
|
9462
|
+
fs16.rmSync(dir, { recursive: true, force: true });
|
|
8797
9463
|
result.removed.push(entry.name);
|
|
8798
9464
|
}
|
|
8799
9465
|
}
|
|
@@ -9967,6 +10633,9 @@ var ClaudePeerChannel = class {
|
|
|
9967
10633
|
return { status: "accepted", via: this.name, confirmed: true };
|
|
9968
10634
|
}
|
|
9969
10635
|
}
|
|
10636
|
+
if (statusBefore === "busy" || statusBefore === "shell") {
|
|
10637
|
+
return { status: "queued", via: this.name };
|
|
10638
|
+
}
|
|
9970
10639
|
if (statusBefore === "idle") {
|
|
9971
10640
|
await this.deps.sleep(window);
|
|
9972
10641
|
const afterInfo = this.deps.statusFor(taskId);
|
|
@@ -10105,7 +10774,7 @@ var ClaudeReceiptListener = class {
|
|
|
10105
10774
|
};
|
|
10106
10775
|
|
|
10107
10776
|
// packages/agents/dist/claude/api-key-approval.js
|
|
10108
|
-
import
|
|
10777
|
+
import fs17 from "fs";
|
|
10109
10778
|
import os12 from "os";
|
|
10110
10779
|
import path20 from "path";
|
|
10111
10780
|
|
|
@@ -10347,11 +11016,11 @@ function parseDraftFromScreen(screen) {
|
|
|
10347
11016
|
if (!screen)
|
|
10348
11017
|
return null;
|
|
10349
11018
|
const lines = screen.split(/\r?\n/);
|
|
10350
|
-
const
|
|
11019
|
+
const HR_RE2 = /^\s*─{10,}\s*$/;
|
|
10351
11020
|
let bottomHR = -1;
|
|
10352
11021
|
let topHR = -1;
|
|
10353
11022
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
10354
|
-
if (
|
|
11023
|
+
if (HR_RE2.test(lines[i])) {
|
|
10355
11024
|
if (bottomHR === -1) {
|
|
10356
11025
|
bottomHR = i;
|
|
10357
11026
|
} else {
|
|
@@ -10389,11 +11058,11 @@ function hasCCInputBox(screen) {
|
|
|
10389
11058
|
if (!screen)
|
|
10390
11059
|
return false;
|
|
10391
11060
|
const lines = screen.split(/\r?\n/);
|
|
10392
|
-
const
|
|
11061
|
+
const HR_RE2 = /^\s*─{10,}\s*$/;
|
|
10393
11062
|
let bottomHR = -1;
|
|
10394
11063
|
let topHR = -1;
|
|
10395
11064
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
10396
|
-
if (
|
|
11065
|
+
if (HR_RE2.test(lines[i])) {
|
|
10397
11066
|
if (bottomHR === -1)
|
|
10398
11067
|
bottomHR = i;
|
|
10399
11068
|
else {
|
|
@@ -10406,15 +11075,15 @@ function hasCCInputBox(screen) {
|
|
|
10406
11075
|
return false;
|
|
10407
11076
|
return lines.slice(topHR + 1, bottomHR).some((l) => /[>❯]/.test(l));
|
|
10408
11077
|
}
|
|
10409
|
-
function
|
|
11078
|
+
function hasModalOptionList2(screen) {
|
|
10410
11079
|
if (!screen)
|
|
10411
11080
|
return false;
|
|
10412
11081
|
const lines = screen.split(/\r?\n/);
|
|
10413
|
-
const
|
|
11082
|
+
const HR_RE2 = /^\s*─{10,}\s*$/;
|
|
10414
11083
|
let bottomHR = -1;
|
|
10415
11084
|
let topHR = -1;
|
|
10416
11085
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
10417
|
-
if (
|
|
11086
|
+
if (HR_RE2.test(lines[i])) {
|
|
10418
11087
|
if (bottomHR === -1)
|
|
10419
11088
|
bottomHR = i;
|
|
10420
11089
|
else {
|
|
@@ -10431,11 +11100,11 @@ function readInputBoxRaw(screen, opts) {
|
|
|
10431
11100
|
if (!screen)
|
|
10432
11101
|
return null;
|
|
10433
11102
|
const lines = screen.split(/\r?\n/);
|
|
10434
|
-
const
|
|
11103
|
+
const HR_RE2 = /^\s*─{10,}\s*$/;
|
|
10435
11104
|
let bottomHR = -1;
|
|
10436
11105
|
let topHR = -1;
|
|
10437
11106
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
10438
|
-
if (
|
|
11107
|
+
if (HR_RE2.test(lines[i])) {
|
|
10439
11108
|
if (bottomHR === -1)
|
|
10440
11109
|
bottomHR = i;
|
|
10441
11110
|
else {
|
|
@@ -10465,6 +11134,34 @@ function classifyStartupSurface(screen) {
|
|
|
10465
11134
|
return "idle";
|
|
10466
11135
|
return "loading";
|
|
10467
11136
|
}
|
|
11137
|
+
var OC_BOX_BOTTOM_RE = /^\s*╹[▀]{6,}\s*$/;
|
|
11138
|
+
var OC_BOX_SIDE_RE = /^\s*┃/;
|
|
11139
|
+
var OC_BOX_STATUS_RE = /^Build\s*·/i;
|
|
11140
|
+
var OC_BUSY_RE = /esc\s+interrupt/i;
|
|
11141
|
+
function parseOpencodeDraftFromScreen(screen) {
|
|
11142
|
+
if (!screen)
|
|
11143
|
+
return null;
|
|
11144
|
+
if (OC_BUSY_RE.test(screen))
|
|
11145
|
+
return null;
|
|
11146
|
+
const lines = screen.split(/\r?\n/);
|
|
11147
|
+
let bottom = -1;
|
|
11148
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
11149
|
+
if (OC_BOX_BOTTOM_RE.test(lines[i])) {
|
|
11150
|
+
bottom = i;
|
|
11151
|
+
break;
|
|
11152
|
+
}
|
|
11153
|
+
}
|
|
11154
|
+
if (bottom === -1)
|
|
11155
|
+
return null;
|
|
11156
|
+
const content = [];
|
|
11157
|
+
for (let i = bottom - 1; i >= 0 && OC_BOX_SIDE_RE.test(lines[i]); i--) {
|
|
11158
|
+
content.push(lines[i].replace(/^\s*┃\s*/, "").replace(/[\s┃│]+$/, ""));
|
|
11159
|
+
}
|
|
11160
|
+
if (content.length === 0)
|
|
11161
|
+
return null;
|
|
11162
|
+
const hasDraft = content.some((l) => l !== "" && !OC_BOX_STATUS_RE.test(l) && !screenHasSplashMarker(l, "Ask anything"));
|
|
11163
|
+
return hasDraft ? null : "";
|
|
11164
|
+
}
|
|
10468
11165
|
function sendDebugEnabled() {
|
|
10469
11166
|
return !!process.env.SQUADRANT_DEBUG_SEND;
|
|
10470
11167
|
}
|
|
@@ -10680,10 +11377,10 @@ function createCmuxDriver() {
|
|
|
10680
11377
|
`);
|
|
10681
11378
|
throw new DeferDelivery(null, "probe-failed");
|
|
10682
11379
|
}
|
|
10683
|
-
const draft = parseDraftFromScreen(screen);
|
|
11380
|
+
const draft = opts?.agent === "opencode" ? parseOpencodeDraftFromScreen(screen) : parseDraftFromScreen(screen);
|
|
10684
11381
|
if (draft === null)
|
|
10685
11382
|
throw new DeferDelivery(null, "no-box");
|
|
10686
|
-
if (
|
|
11383
|
+
if (hasModalOptionList2(screen))
|
|
10687
11384
|
throw new DeferDelivery(null, "modal");
|
|
10688
11385
|
if (draft === "") {
|
|
10689
11386
|
await deliver();
|
|
@@ -10877,7 +11574,7 @@ var NotifierRegistry = class {
|
|
|
10877
11574
|
};
|
|
10878
11575
|
|
|
10879
11576
|
// packages/workspaces/dist/workspaces/obsidian.js
|
|
10880
|
-
import
|
|
11577
|
+
import fs18 from "fs/promises";
|
|
10881
11578
|
import { existsSync as existsSync11 } from "fs";
|
|
10882
11579
|
import path21 from "path";
|
|
10883
11580
|
|
|
@@ -11673,7 +12370,7 @@ async function settleInputBox(runtime, pane) {
|
|
|
11673
12370
|
}
|
|
11674
12371
|
async function confirmedSendToPane(runtime, pane, message) {
|
|
11675
12372
|
const preSendScreen = await runtime.readPaneScreen(pane) ?? "";
|
|
11676
|
-
if (
|
|
12373
|
+
if (hasModalOptionList2(preSendScreen)) {
|
|
11677
12374
|
return { delivered: false, blockedByModal: true };
|
|
11678
12375
|
}
|
|
11679
12376
|
await runtime.pasteToPane(pane, message);
|
|
@@ -11776,7 +12473,7 @@ async function maybeBroadcastDaemonRestart(opts) {
|
|
|
11776
12473
|
|
|
11777
12474
|
// packages/cli/src/lib/captain-channel-factory.ts
|
|
11778
12475
|
import { createServer as createServer4, connect as netConnect } from "net";
|
|
11779
|
-
import
|
|
12476
|
+
import fs19 from "fs";
|
|
11780
12477
|
import { join as join26 } from "path";
|
|
11781
12478
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
11782
12479
|
import chalk2 from "chalk";
|
|
@@ -11785,15 +12482,15 @@ var shared;
|
|
|
11785
12482
|
var registryEntryPath = () => join26(CLAUDE_SESSIONS_DIR, `${process.pid}.json`);
|
|
11786
12483
|
function unregisterSenderIdentity() {
|
|
11787
12484
|
try {
|
|
11788
|
-
|
|
12485
|
+
fs19.unlinkSync(registryEntryPath());
|
|
11789
12486
|
} catch {
|
|
11790
12487
|
}
|
|
11791
12488
|
}
|
|
11792
12489
|
function registerSenderIdentity(socketPath) {
|
|
11793
12490
|
try {
|
|
11794
12491
|
unregisterSenderIdentity();
|
|
11795
|
-
|
|
11796
|
-
|
|
12492
|
+
fs19.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
|
|
12493
|
+
fs19.writeFileSync(
|
|
11797
12494
|
registryEntryPath(),
|
|
11798
12495
|
JSON.stringify({
|
|
11799
12496
|
pid: process.pid,
|
|
@@ -11819,7 +12516,7 @@ async function sharedReceiptListener() {
|
|
|
11819
12516
|
// must never be the reason we refuse to start.
|
|
11820
12517
|
unlinkStale: (p) => {
|
|
11821
12518
|
try {
|
|
11822
|
-
|
|
12519
|
+
fs19.unlinkSync(p);
|
|
11823
12520
|
} catch {
|
|
11824
12521
|
}
|
|
11825
12522
|
},
|
|
@@ -11916,6 +12613,33 @@ function buildTelegramBridge(cfg, stateRoot, log, deliverInbound, usageFor) {
|
|
|
11916
12613
|
});
|
|
11917
12614
|
const runCommand = createRunCommand(CLI_BIN);
|
|
11918
12615
|
const sendReply = (threadId, text, replyMarkup) => client.sendMessage(cfg.supergroupId, threadId, text, replyMarkup);
|
|
12616
|
+
const lifecycle = createInboundLifecycle({
|
|
12617
|
+
stateRoot,
|
|
12618
|
+
cfg: { supergroupId: cfg.supergroupId },
|
|
12619
|
+
sendChatAction: (chatId, threadId, action) => client.sendChatAction(chatId, threadId, action),
|
|
12620
|
+
sendReply: async (threadId, text) => {
|
|
12621
|
+
await client.sendMessage(cfg.supergroupId, threadId, text);
|
|
12622
|
+
},
|
|
12623
|
+
readPane: createCaptainPaneReader(
|
|
12624
|
+
new DaemonCmux(createCmuxDriver()),
|
|
12625
|
+
(project) => {
|
|
12626
|
+
try {
|
|
12627
|
+
return loadConfig().projects[project]?.captainName ?? `${project}-captain`;
|
|
12628
|
+
} catch {
|
|
12629
|
+
return `${project}-captain`;
|
|
12630
|
+
}
|
|
12631
|
+
},
|
|
12632
|
+
{
|
|
12633
|
+
log,
|
|
12634
|
+
// #839 staleness, measured for real: cmux has no pane activity
|
|
12635
|
+
// timestamp, so the reader hashes the screen and asks the persisted
|
|
12636
|
+
// pending store how long that exact screen has been up. Persisted, so a
|
|
12637
|
+
// daemon restart cannot hand a dead captain a fresh 15 minutes.
|
|
12638
|
+
noteScreen: (project, hash) => notePaneScreen(stateRoot, project, hash, Date.now())
|
|
12639
|
+
}
|
|
12640
|
+
),
|
|
12641
|
+
log
|
|
12642
|
+
});
|
|
11919
12643
|
return createTelegramBridge({
|
|
11920
12644
|
cfg,
|
|
11921
12645
|
stateRoot,
|
|
@@ -11927,7 +12651,20 @@ function buildTelegramBridge(cfg, stateRoot, log, deliverInbound, usageFor) {
|
|
|
11927
12651
|
runCommand,
|
|
11928
12652
|
sendReply,
|
|
11929
12653
|
deliverInbound,
|
|
11930
|
-
usageFor
|
|
12654
|
+
usageFor,
|
|
12655
|
+
lifecycle,
|
|
12656
|
+
// #321 registry prune. Read the file directly rather than via loadConfig():
|
|
12657
|
+
// loadConfig swallows every failure and returns defaults, so an unreadable
|
|
12658
|
+
// config would look like "no projects registered" and the prune would delete
|
|
12659
|
+
// every link on the machine. `undefined` correctly means "don't prune".
|
|
12660
|
+
registeredProjects: () => {
|
|
12661
|
+
try {
|
|
12662
|
+
const parsed = JSON.parse(readFileSync19(join27(dirname6(stateRoot), "config.json"), "utf8"));
|
|
12663
|
+
return Object.keys(parsed.projects ?? {});
|
|
12664
|
+
} catch {
|
|
12665
|
+
return void 0;
|
|
12666
|
+
}
|
|
12667
|
+
}
|
|
11931
12668
|
});
|
|
11932
12669
|
}
|
|
11933
12670
|
function buildNotifyFault(log) {
|