switchroom 0.14.26 → 0.14.28

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 (27) hide show
  1. package/dist/cli/switchroom.js +20 -4
  2. package/dist/host-control/main.js +2 -2
  3. package/package.json +1 -1
  4. package/telegram-plugin/bridge/bridge.ts +15 -0
  5. package/telegram-plugin/card-format.ts +65 -0
  6. package/telegram-plugin/dist/bridge/bridge.js +14 -0
  7. package/telegram-plugin/dist/gateway/gateway.js +2201 -1748
  8. package/telegram-plugin/dist/server.js +14 -0
  9. package/telegram-plugin/gateway/gateway.ts +458 -13
  10. package/telegram-plugin/gateway/worker-feed-dispatch.ts +1 -1
  11. package/telegram-plugin/history.ts +16 -4
  12. package/telegram-plugin/permission-title.ts +48 -0
  13. package/telegram-plugin/secret-detect/patterns.ts +8 -0
  14. package/telegram-plugin/secret-detect/redact.ts +76 -0
  15. package/telegram-plugin/tests/card-format.test.ts +96 -0
  16. package/telegram-plugin/tests/gateway-outbound-redact.test.ts +80 -0
  17. package/telegram-plugin/tests/gateway-request-secret.test.ts +78 -0
  18. package/telegram-plugin/tests/history.test.ts +59 -0
  19. package/telegram-plugin/tests/permission-title.test.ts +68 -0
  20. package/telegram-plugin/tests/permission-verdict-resume-guard.test.ts +35 -0
  21. package/telegram-plugin/tests/secret-detect-sanctum.test.ts +115 -0
  22. package/telegram-plugin/tests/worker-activity-feed.test.ts +110 -51
  23. package/telegram-plugin/uat/assertions.ts +8 -6
  24. package/telegram-plugin/uat/feed-matcher.test.ts +14 -8
  25. package/telegram-plugin/uat/scenarios/jtbd-request-secret-dm.test.ts +101 -0
  26. package/telegram-plugin/uat/scenarios/jtbd-worker-activity-feed-dm.test.ts +17 -6
  27. package/telegram-plugin/worker-activity-feed.ts +84 -46
@@ -49420,8 +49420,8 @@ var {
49420
49420
  } = import__.default;
49421
49421
 
49422
49422
  // src/build-info.ts
49423
- var VERSION = "0.14.26";
49424
- var COMMIT_SHA = "7ef6e93c";
49423
+ var VERSION = "0.14.28";
49424
+ var COMMIT_SHA = "cb64351f";
49425
49425
 
49426
49426
  // src/cli/agent.ts
49427
49427
  init_source();
@@ -50220,6 +50220,22 @@ a flood. Going quiet mid-work is fine \u2014 going quiet *instead* of
50220
50220
  acknowledging, or *instead* of an update at a real milestone, is the
50221
50221
  black box this exists to prevent.
50222
50222
 
50223
+ ### Secrets \u2014 never ask for a chat paste
50224
+
50225
+ Never ask the user to paste a secret \u2014 an API key, token, password, or
50226
+ private key \u2014 as a normal chat message. It would persist in plaintext
50227
+ before anything can scrub it. Instead:
50228
+
50229
+ - Need a credential that isn't in the vault? Call
50230
+ \`request_secret(key, reason)\`. The operator gets a secure card, provides
50231
+ the value once, and it goes straight to the vault \u2014 you only ever see
50232
+ \`vault:<key>\`. After calling it, end your turn and wait for the reply.
50233
+ - The user *handed* you a value to keep? Use \`vault_request_save\`.
50234
+ - The key exists but you hit \`VAULT-BROKER-DENIED\`? Use \`vault_request_access\`.
50235
+
50236
+ Reference stored secrets only as \`vault:<key>\` \u2014 never echo a raw secret
50237
+ value back into chat.
50238
+
50223
50239
  ### Formatting \u2014 make it scannable
50224
50240
 
50225
50241
  \`reply\` and \`stream_reply\` render Markdown as Telegram HTML for you, so
@@ -73962,6 +73978,7 @@ var ANCHORED_PATTERNS = [
73962
73978
  { rule_id: "npm_token", regex: /\b(npm_[A-Za-z0-9]{10,})\b/g, captureIndex: 1, slugHint: "npm_token" },
73963
73979
  { rule_id: "telegram_bot_token_prefixed", regex: /\bbot(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "telegram_bot_token" },
73964
73980
  { rule_id: "telegram_bot_token", regex: /\b(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "telegram_bot_token" },
73981
+ { rule_id: "laravel_sanctum_token", regex: /\b(\d+\|[A-Za-z0-9]{40,})\b/g, captureIndex: 1, slugHint: "api_token" },
73965
73982
  { rule_id: "aws_access_key", regex: /\b(AKIA[0-9A-Z]{16})\b/g, captureIndex: 1, slugHint: "aws_access_key" },
73966
73983
  { rule_id: "jwt", regex: /\b(eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b/g, captureIndex: 1, slugHint: "jwt" }
73967
73984
  ];
@@ -74216,7 +74233,7 @@ function dropOverlaps(hits) {
74216
74233
  return out;
74217
74234
  }
74218
74235
 
74219
- // src/secret-detect/redact.ts
74236
+ // telegram-plugin/secret-detect/redact.ts
74220
74237
  var REDACTED_MARKER = "[REDACTED]";
74221
74238
  function redact(text) {
74222
74239
  if (!text || text.length === 0)
@@ -74239,7 +74256,6 @@ function redactedMarker(ruleId) {
74239
74256
  }
74240
74257
  return `[REDACTED:${trimmed}]`;
74241
74258
  }
74242
-
74243
74259
  // src/issues/store.ts
74244
74260
  var ISSUES_FILE = "issues.jsonl";
74245
74261
  var ISSUES_LOCK = "issues.lock";
@@ -19866,6 +19866,7 @@ var ANCHORED_PATTERNS = [
19866
19866
  { rule_id: "npm_token", regex: /\b(npm_[A-Za-z0-9]{10,})\b/g, captureIndex: 1, slugHint: "npm_token" },
19867
19867
  { rule_id: "telegram_bot_token_prefixed", regex: /\bbot(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "telegram_bot_token" },
19868
19868
  { rule_id: "telegram_bot_token", regex: /\b(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "telegram_bot_token" },
19869
+ { rule_id: "laravel_sanctum_token", regex: /\b(\d+\|[A-Za-z0-9]{40,})\b/g, captureIndex: 1, slugHint: "api_token" },
19869
19870
  { rule_id: "aws_access_key", regex: /\b(AKIA[0-9A-Z]{16})\b/g, captureIndex: 1, slugHint: "aws_access_key" },
19870
19871
  { rule_id: "jwt", regex: /\b(eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b/g, captureIndex: 1, slugHint: "jwt" }
19871
19872
  ];
@@ -20120,7 +20121,7 @@ function dropOverlaps(hits) {
20120
20121
  return out;
20121
20122
  }
20122
20123
 
20123
- // src/secret-detect/redact.ts
20124
+ // telegram-plugin/secret-detect/redact.ts
20124
20125
  var REDACTED_MARKER = "[REDACTED]";
20125
20126
  function redact(text) {
20126
20127
  if (!text || text.length === 0)
@@ -20143,7 +20144,6 @@ function redactedMarker(ruleId) {
20143
20144
  }
20144
20145
  return `[REDACTED:${trimmed}]`;
20145
20146
  }
20146
-
20147
20147
  // src/cli/install-detect.ts
20148
20148
  import * as fs from "node:fs";
20149
20149
  import * as path from "node:path";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.26",
3
+ "version": "0.14.28",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -438,6 +438,21 @@ const TOOL_SCHEMAS = [
438
438
  required: ['chat_id', 'key'],
439
439
  },
440
440
  },
441
+ {
442
+ name: 'request_secret',
443
+ description:
444
+ 'Ask the operator to PROVIDE a secret you do not have, securely — NEVER ask the user to paste a token/key/password as a normal chat message. Use this when you need a credential that is not in the vault (a `vault:<key>` reference is missing/empty, or you know an upcoming task needs one you lack). Renders a Telegram card with [Provide securely] [Decline]; on tap, the operator sends the value once and the gateway DELETES their message instantly and writes it straight to the vault — the raw value is never echoed back to you. You receive only `vault:<key>`. This is the sibling of `vault_request_save` (use that when the user already handed YOU a value to store) and `vault_request_access` (use that when the key exists but you lack read access). After firing this tool, END YOUR TURN cleanly — a fresh inbound message arrives once the operator provides (or declines) the secret. Do NOT call this for keys you already have, and do NOT spam (one open request per key; the operator sees every card).',
445
+ inputSchema: {
446
+ type: 'object',
447
+ properties: {
448
+ chat_id: { type: 'string', description: 'Chat to render the card in (use the chat_id of the user message that triggered the workflow).' },
449
+ key: { type: 'string', description: 'Vault key to store the provided secret under. Use lowercase namespaced snake_case, e.g. `coolify/api-token`.' },
450
+ reason: { type: 'string', description: 'REQUIRED in practice — one-line human-readable rationale rendered on the card (e.g. "to trigger a redeploy on Coolify"). Omitting it makes the operator more likely to Decline.' },
451
+ message_thread_id: { type: 'string', description: 'Forum topic thread ID. Auto-applied from the last inbound message if not specified.' },
452
+ },
453
+ required: ['chat_id', 'key'],
454
+ },
455
+ },
441
456
  ]
442
457
 
443
458
  mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }))
@@ -60,3 +60,68 @@ export function escapeHtml(s: string): string {
60
60
  export function truncate(s: string, n: number): string {
61
61
  return s.length > n ? s.slice(0, n - 1) + '…' : s;
62
62
  }
63
+
64
+ /**
65
+ * Strip Markdown markup from a single line, leaving plain prose.
66
+ *
67
+ * Worker narration is authored as Markdown — the model writes `**bold**`,
68
+ * `` `code` ``, `- bullets`, `# headings`. The status cards render Telegram
69
+ * HTML, which does NOT interpret Markdown, so an unstripped `**` shows up as
70
+ * two literal asterisks (the #94-class "half-done" look). Strip the markup
71
+ * here so the card reads as clean prose.
72
+ *
73
+ * Run this BEFORE `truncate` + `escapeHtml`: clean → measure → escape. (The
74
+ * stripper never touches `<`/`>`/`&`, so escaping stays the last step.)
75
+ */
76
+ export function stripMarkdown(s: string): string {
77
+ let out = s;
78
+ // Inline + leftover code spans → bare text.
79
+ out = out.replace(/`+/g, '');
80
+ // Links / images: [text](url) and ![alt](url) → the label.
81
+ out = out.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1');
82
+ // Paired bold / emphasis runs (longest marker first).
83
+ out = out.replace(/\*\*(.+?)\*\*/g, '$1');
84
+ out = out.replace(/__(.+?)__/g, '$1');
85
+ out = out.replace(/\*(.+?)\*/g, '$1');
86
+ out = out.replace(/(?<![A-Za-z0-9])_(.+?)_(?![A-Za-z0-9])/g, '$1');
87
+ // Leading block markup: heading, blockquote, bullet, ordered item.
88
+ // `gm` so the markers are stripped on EVERY line, not just the string
89
+ // start — a multi-line summary like "Done.\n\n## Summary\n…" must not
90
+ // leak a raw `## Summary` when rendered as a single card step.
91
+ out = out.replace(/^\s{0,3}#{1,6}\s+/gm, '');
92
+ out = out.replace(/^\s{0,3}>\s?/gm, '');
93
+ out = out.replace(/^\s{0,3}[-*+]\s+/gm, '');
94
+ out = out.replace(/^\s{0,3}\d+[.)]\s+/gm, '');
95
+ // Residual unpaired bold markers (a lone `*` is left alone so `3 * 4`
96
+ // survives; only the doubled form is markup-by-construction).
97
+ out = out.replace(/\*\*/g, '');
98
+ return out.trim();
99
+ }
100
+
101
+ /** True for a whole-line horizontal rule: `---`, `___`, `***` (3+ of one). */
102
+ function isRuleLine(s: string): boolean {
103
+ return /^\s*([-_*])\1{2,}\s*$/.test(s);
104
+ }
105
+
106
+ /**
107
+ * Clean a worker's multi-line result/narration into a single plain-text
108
+ * paragraph for a card's finished body. Drops fenced code blocks and
109
+ * horizontal rules, strips per-line Markdown, then space-joins what's left.
110
+ * Output is plain text — the caller still truncates + escapes before
111
+ * interpolating into HTML.
112
+ */
113
+ export function cleanWorkerResultParagraph(s: string): string {
114
+ const kept: string[] = [];
115
+ let inFence = false;
116
+ for (const raw of s.split('\n')) {
117
+ if (/^\s*```/.test(raw)) {
118
+ inFence = !inFence;
119
+ continue;
120
+ }
121
+ if (inFence) continue;
122
+ if (isRuleLine(raw)) continue;
123
+ const cleaned = stripMarkdown(raw);
124
+ if (cleaned.length > 0) kept.push(cleaned);
125
+ }
126
+ return kept.join(' ').replace(/\s+/g, ' ').trim();
127
+ }
@@ -24914,6 +24914,20 @@ var TOOL_SCHEMAS = [
24914
24914
  },
24915
24915
  required: ["chat_id", "key"]
24916
24916
  }
24917
+ },
24918
+ {
24919
+ name: "request_secret",
24920
+ description: "Ask the operator to PROVIDE a secret you do not have, securely \u2014 NEVER ask the user to paste a token/key/password as a normal chat message. Use this when you need a credential that is not in the vault (a `vault:<key>` reference is missing/empty, or you know an upcoming task needs one you lack). Renders a Telegram card with [Provide securely] [Decline]; on tap, the operator sends the value once and the gateway DELETES their message instantly and writes it straight to the vault \u2014 the raw value is never echoed back to you. You receive only `vault:<key>`. This is the sibling of `vault_request_save` (use that when the user already handed YOU a value to store) and `vault_request_access` (use that when the key exists but you lack read access). After firing this tool, END YOUR TURN cleanly \u2014 a fresh inbound message arrives once the operator provides (or declines) the secret. Do NOT call this for keys you already have, and do NOT spam (one open request per key; the operator sees every card).",
24921
+ inputSchema: {
24922
+ type: "object",
24923
+ properties: {
24924
+ chat_id: { type: "string", description: "Chat to render the card in (use the chat_id of the user message that triggered the workflow)." },
24925
+ key: { type: "string", description: "Vault key to store the provided secret under. Use lowercase namespaced snake_case, e.g. `coolify/api-token`." },
24926
+ reason: { type: "string", description: 'REQUIRED in practice \u2014 one-line human-readable rationale rendered on the card (e.g. "to trigger a redeploy on Coolify"). Omitting it makes the operator more likely to Decline.' },
24927
+ message_thread_id: { type: "string", description: "Forum topic thread ID. Auto-applied from the last inbound message if not specified." }
24928
+ },
24929
+ required: ["chat_id", "key"]
24930
+ }
24917
24931
  }
24918
24932
  ];
24919
24933
  mcp.setRequestHandler(ListToolsRequestSchema2, async () => ({ tools: TOOL_SCHEMAS }));