switchroom 0.18.13 → 0.18.15

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.
Files changed (47) hide show
  1. package/dist/agent-scheduler/index.js +49 -9
  2. package/dist/auth-broker/index.js +152 -46
  3. package/dist/cli/autoaccept-poll.js +23 -0
  4. package/dist/cli/drive-write-pretool.mjs +24 -1
  5. package/dist/cli/foreground-hog-pretool.mjs +264 -0
  6. package/dist/cli/notion-write-pretool.mjs +0 -1
  7. package/dist/cli/switchroom.js +1185 -1072
  8. package/dist/host-control/main.js +53 -52
  9. package/dist/vault/approvals/kernel-server.js +16 -13
  10. package/dist/vault/broker/server.js +672 -669
  11. package/package.json +1 -1
  12. package/profiles/coding/CLAUDE.md.hbs +2 -0
  13. package/profiles/default/CLAUDE.md.hbs +2 -0
  14. package/skills/switchroom-architecture/telegram.md +0 -1
  15. package/telegram-plugin/auth-snapshot-format.ts +37 -5
  16. package/telegram-plugin/auto-fallback-fleet.ts +29 -1
  17. package/telegram-plugin/bridge/bridge.ts +2 -0
  18. package/telegram-plugin/dist/bridge/bridge.js +23 -0
  19. package/telegram-plugin/dist/gateway/gateway.js +765 -67
  20. package/telegram-plugin/dist/server.js +24 -1
  21. package/telegram-plugin/gateway/auth-broker-client.ts +1 -0
  22. package/telegram-plugin/gateway/auth-command.ts +14 -0
  23. package/telegram-plugin/gateway/forward-origin.ts +235 -0
  24. package/telegram-plugin/gateway/gateway.ts +270 -10
  25. package/telegram-plugin/gateway/throttle-tier-wiring.ts +268 -0
  26. package/telegram-plugin/history.ts +55 -6
  27. package/telegram-plugin/model-unavailable.ts +234 -2
  28. package/telegram-plugin/render/rich-render.ts +40 -32
  29. package/telegram-plugin/runtime-metrics.ts +31 -0
  30. package/telegram-plugin/session-tail.ts +14 -2
  31. package/telegram-plugin/stream-controller.ts +3 -2
  32. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +72 -0
  33. package/telegram-plugin/tests/forward-origin.test.ts +309 -0
  34. package/telegram-plugin/tests/history.test.ts +157 -0
  35. package/telegram-plugin/tests/model-unavailable.test.ts +187 -0
  36. package/telegram-plugin/tests/operator-events-session-tail.test.ts +55 -0
  37. package/telegram-plugin/tests/render/render-outbound-chunks.test.ts +6 -4
  38. package/telegram-plugin/tests/render/rich-render.test.ts +41 -22
  39. package/telegram-plugin/tests/runtime-metrics.test.ts +24 -0
  40. package/telegram-plugin/tests/single-mode-stream-reply.test.ts +5 -3
  41. package/telegram-plugin/tests/status-accent.test.ts +5 -3
  42. package/telegram-plugin/tests/stream-controller-chunk-cap.test.ts +20 -20
  43. package/telegram-plugin/tests/stream-reply-handler.test.ts +5 -2
  44. package/telegram-plugin/tests/throttle-tier-wiring.test.ts +290 -0
  45. package/telegram-plugin/tests/throttle-tier.test.ts +454 -0
  46. package/telegram-plugin/throttle-tier.ts +323 -0
  47. package/telegram-plugin/uat/scenarios/jtbd-rich-formatting-render-dm.test.ts +8 -7
@@ -0,0 +1,264 @@
1
+ // src/cli/foreground-hog-pretool.ts
2
+ import { readFileSync } from "node:fs";
3
+ function gateEnabledFromEnv(env = process.env) {
4
+ const v = env.SWITCHROOM_FOREGROUND_HOG_GATE;
5
+ if (v == null)
6
+ return true;
7
+ const t = v.trim().toLowerCase();
8
+ return !(t === "0" || t === "false" || t === "off" || t === "no");
9
+ }
10
+ function blankQuotedAndHeredocs(src) {
11
+ const out = src.split("");
12
+ const n = src.length;
13
+ const pending = [];
14
+ let i = 0;
15
+ while (i < n) {
16
+ const ch = src[i];
17
+ if (ch === "\\") {
18
+ out[i] = " ";
19
+ if (i + 1 < n)
20
+ out[i + 1] = " ";
21
+ i += 2;
22
+ continue;
23
+ }
24
+ if (ch === "'") {
25
+ let j = i + 1;
26
+ while (j < n && src[j] !== "'")
27
+ j++;
28
+ if (j >= n)
29
+ return null;
30
+ for (let k = i;k <= j; k++)
31
+ out[k] = " ";
32
+ i = j + 1;
33
+ continue;
34
+ }
35
+ if (ch === '"') {
36
+ let j = i + 1;
37
+ while (j < n && src[j] !== '"') {
38
+ j += src[j] === "\\" ? 2 : 1;
39
+ }
40
+ if (j >= n)
41
+ return null;
42
+ for (let k = i;k <= j && k < n; k++)
43
+ out[k] = " ";
44
+ i = j + 1;
45
+ continue;
46
+ }
47
+ if (ch === "<" && src[i + 1] === "<") {
48
+ if (src[i + 2] === "<") {
49
+ i += 3;
50
+ continue;
51
+ }
52
+ let j = i + 2;
53
+ let stripTabs = false;
54
+ if (src[j] === "-") {
55
+ stripTabs = true;
56
+ j++;
57
+ }
58
+ while (j < n && (src[j] === " " || src[j] === "\t"))
59
+ j++;
60
+ let quote = "";
61
+ if (src[j] === "'" || src[j] === '"') {
62
+ quote = src[j];
63
+ j++;
64
+ }
65
+ let tag = "";
66
+ while (j < n && /[A-Za-z0-9_]/.test(src[j])) {
67
+ tag += src[j];
68
+ j++;
69
+ }
70
+ if (quote) {
71
+ if (src[j] !== quote)
72
+ return null;
73
+ j++;
74
+ }
75
+ if (!tag)
76
+ return null;
77
+ pending.push({ tag, stripTabs });
78
+ i = j;
79
+ continue;
80
+ }
81
+ if (ch === `
82
+ ` && pending.length > 0) {
83
+ let j = i + 1;
84
+ while (pending.length > 0) {
85
+ const { tag, stripTabs } = pending.shift();
86
+ let found = false;
87
+ while (j <= n) {
88
+ let eol = src.indexOf(`
89
+ `, j);
90
+ if (eol === -1)
91
+ eol = n;
92
+ const line = src.slice(j, eol);
93
+ const cmp = stripTabs ? line.replace(/^\t+/, "") : line;
94
+ if (cmp === tag) {
95
+ found = true;
96
+ j = eol + 1;
97
+ break;
98
+ }
99
+ for (let k = j;k < eol; k++)
100
+ out[k] = " ";
101
+ if (eol === n) {
102
+ j = n + 1;
103
+ break;
104
+ }
105
+ j = eol + 1;
106
+ }
107
+ if (!found)
108
+ return null;
109
+ }
110
+ i = j;
111
+ continue;
112
+ }
113
+ i++;
114
+ }
115
+ return out.join("");
116
+ }
117
+ var LOOP_SLEEP = /\b(?:while|until|for)\b[\s\S]*?\bdo\b[\s\S]*?\bsleep\b[\s\S]*?\bdone\b/;
118
+ function sleepSeconds(token) {
119
+ const m = /^(\d+(?:\.\d+)?)([smhd]?)$/.exec(token);
120
+ if (!m)
121
+ return null;
122
+ const mult = { "": 1, s: 1, m: 60, h: 3600, d: 86400 }[m[2]];
123
+ return Number(m[1]) * mult;
124
+ }
125
+ function hasFollowFlag(args, allowCapitalF) {
126
+ return args.some((a) => a === "--follow" || a.startsWith("--follow=") || /^-[a-zA-Z0-9]+$/.test(a) && (a.includes("f") || allowCapitalF && a.includes("F")));
127
+ }
128
+ function foregroundSegments(command) {
129
+ const cleaned = command.replace(/\d*>&\d*/g, " ");
130
+ const out = [];
131
+ const sep = /&&|\|\||[;|\n&]/g;
132
+ let last = 0;
133
+ let m;
134
+ const push = (seg, backgrounded) => {
135
+ const t = seg.trim();
136
+ if (t && !backgrounded)
137
+ out.push(t);
138
+ };
139
+ while ((m = sep.exec(cleaned)) !== null) {
140
+ push(cleaned.slice(last, m.index), m[0] === "&");
141
+ last = m.index + m[0].length;
142
+ }
143
+ push(cleaned.slice(last), false);
144
+ return out;
145
+ }
146
+ function commandTokens(segment) {
147
+ const SKIP = new Set([
148
+ "if",
149
+ "elif",
150
+ "then",
151
+ "else",
152
+ "do",
153
+ "time",
154
+ "nohup",
155
+ "sudo",
156
+ "command",
157
+ "exec"
158
+ ]);
159
+ const tokens = segment.split(/\s+/);
160
+ const out = [];
161
+ let started = false;
162
+ for (const rawTok of tokens) {
163
+ const tok = rawTok.replace(/^[({]+/, "");
164
+ if (!tok)
165
+ continue;
166
+ if (!started) {
167
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(tok))
168
+ continue;
169
+ if (SKIP.has(tok))
170
+ continue;
171
+ started = true;
172
+ }
173
+ out.push(tok);
174
+ }
175
+ return out;
176
+ }
177
+ function matchSegment(segment) {
178
+ const tokens = commandTokens(segment);
179
+ const cmd = tokens[0];
180
+ if (!cmd)
181
+ return null;
182
+ const args = tokens.slice(1);
183
+ if (cmd === "tail" && hasFollowFlag(args, true))
184
+ return "tail -f/--follow";
185
+ if (cmd === "watch")
186
+ return "watch";
187
+ if (cmd === "sleep") {
188
+ const secs = sleepSeconds(args[0] ?? "");
189
+ if (secs !== null && secs > 30)
190
+ return `sleep ${args[0]} (> 30s foreground)`;
191
+ }
192
+ if (cmd === "gh") {
193
+ const positionals = args.filter((a) => !a.startsWith("-"));
194
+ if (positionals[0] === "pr" && positionals[1] === "checks" && args.includes("--watch"))
195
+ return "gh pr checks --watch";
196
+ if (positionals[0] === "run" && positionals[1] === "watch")
197
+ return "gh run watch";
198
+ }
199
+ if (cmd === "docker" && args[0] === "logs" && hasFollowFlag(args.slice(1), false))
200
+ return "docker logs -f/--follow";
201
+ if (cmd === "kubectl" && args[0] === "logs" && hasFollowFlag(args.slice(1), false))
202
+ return "kubectl logs -f/--follow";
203
+ if (cmd === "journalctl" && hasFollowFlag(args, false))
204
+ return "journalctl -f";
205
+ return null;
206
+ }
207
+ function detectForegroundHog(command) {
208
+ const blanked = blankQuotedAndHeredocs(command);
209
+ if (blanked === null)
210
+ return null;
211
+ if (LOOP_SLEEP.test(blanked))
212
+ return "sleep in a while/until/for loop";
213
+ for (const segment of foregroundSegments(blanked)) {
214
+ const hit = matchSegment(segment);
215
+ if (hit)
216
+ return hit;
217
+ }
218
+ return null;
219
+ }
220
+ function denyMessage(pattern) {
221
+ return `Foreground-hog gate: \`${pattern}\` would hold the session foreground for its full duration. ` + "Re-run this exact command with `run_in_background: true` and act on the completion notification, " + "or use a bounded alternative (e.g. `tail -n 200` instead of `tail -f`).";
222
+ }
223
+ function readStdin() {
224
+ try {
225
+ return readFileSync(0, "utf8");
226
+ } catch {
227
+ return "";
228
+ }
229
+ }
230
+ function allow() {
231
+ process.exit(0);
232
+ }
233
+ function block(reason) {
234
+ process.stdout.write(JSON.stringify({ decision: "block", reason }));
235
+ process.exit(0);
236
+ }
237
+ function main() {
238
+ if (!gateEnabledFromEnv())
239
+ allow();
240
+ const raw = readStdin().trim();
241
+ if (!raw)
242
+ allow();
243
+ let event;
244
+ try {
245
+ event = JSON.parse(raw);
246
+ } catch {
247
+ allow();
248
+ }
249
+ if (event.tool_name !== "Bash")
250
+ allow();
251
+ const input = event.tool_input;
252
+ if (!input || typeof input !== "object")
253
+ allow();
254
+ const { run_in_background: runInBackground, command } = input;
255
+ if (runInBackground === true)
256
+ allow();
257
+ if (typeof command !== "string" || command.length === 0)
258
+ allow();
259
+ const pattern = detectForegroundHog(command);
260
+ if (pattern)
261
+ block(denyMessage(pattern));
262
+ allow();
263
+ }
264
+ main();
@@ -11945,7 +11945,6 @@ var TelegramChannelSchema = exports_external.object({
11945
11945
  enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
11946
11946
  plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' \u2014 the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
11947
11947
  format: exports_external.enum(["html", "markdownv2", "text"]).optional().describe("Default reply format passed to the plugin"),
11948
- rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
11949
11948
  stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI \u2014 " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events \u2014 stable " + "order, per-tool status emojis, fires only on semantic transitions."),
11950
11949
  stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive in-place stream edits " + "during a turn. Lower = more responsive stream, higher = fewer API " + "calls. Floored at 250 by draft-stream itself. Default 400 ms for DMs " + "and 1000 ms for groups/forums (respects Telegram's ~1 edit/sec/message " + "practical ceiling). Override per-agent if a particular agent needs " + "snappier or quieter streaming."),
11951
11950
  clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message \u2014 Reading X, Searching the web for Y, \u2026) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) \u2014 no post-then-delete. Per-agent " + "override; cascades defaults \u2192 profile \u2192 agent (per-key)."),