switchroom 0.17.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/switchroom.js +3 -2
- package/dist/host-control/main.js +1 -1
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +164 -63
- package/telegram-plugin/format.ts +119 -17
- package/telegram-plugin/gateway/approvals-commands.ts +6 -2
- package/telegram-plugin/gateway/gateway.ts +17 -3
- package/telegram-plugin/gateway/ms365-write-approval.test.ts +13 -0
- package/telegram-plugin/gateway/ms365-write-approval.ts +5 -1
- package/telegram-plugin/gateway/vault-request-access-card.ts +5 -1
- package/telegram-plugin/tests/format-consistency.test.ts +79 -0
- package/telegram-plugin/tests/vault-request-access-card.test.ts +17 -0
- package/telegram-plugin/tests/welcome-text.test.ts +64 -0
- package/telegram-plugin/welcome-text.ts +13 -9
- package/vendor/hindsight-memory/CHANGELOG.md +42 -0
- package/vendor/hindsight-memory/scripts/lib/content.py +39 -3
- package/vendor/hindsight-memory/scripts/retain.py +71 -10
- package/vendor/hindsight-memory/scripts/tests/test_recall_context_slice.py +126 -0
- package/vendor/hindsight-memory/scripts/tests/test_retain_window.py +261 -0
- package/vendor/hindsight-memory/tests/test_content.py +105 -0
package/dist/cli/switchroom.js
CHANGED
|
@@ -25806,6 +25806,7 @@ function renderHindsightSettingsOverrides(raw, additionalBanks) {
|
|
|
25806
25806
|
return null;
|
|
25807
25807
|
}
|
|
25808
25808
|
settings.retainEveryNTurns = 1;
|
|
25809
|
+
settings.retainMode = "chunked";
|
|
25809
25810
|
settings.recallMaxMemories = 8;
|
|
25810
25811
|
settings.recallMinOverlap = 0.1;
|
|
25811
25812
|
settings.recallTypes = ["world", "experience", "observation"];
|
|
@@ -63728,8 +63729,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
63728
63729
|
import { dirname, join } from "node:path";
|
|
63729
63730
|
|
|
63730
63731
|
// src/build-info.ts
|
|
63731
|
-
var VERSION = "0.17.
|
|
63732
|
-
var COMMIT_SHA = "
|
|
63732
|
+
var VERSION = "0.17.1";
|
|
63733
|
+
var COMMIT_SHA = "d7162c0";
|
|
63733
63734
|
|
|
63734
63735
|
// src/cli/resolve-version.ts
|
|
63735
63736
|
function readPackageVersion() {
|
|
@@ -22649,7 +22649,7 @@ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:f
|
|
|
22649
22649
|
import { dirname as dirname4, join as join2 } from "node:path";
|
|
22650
22650
|
|
|
22651
22651
|
// src/build-info.ts
|
|
22652
|
-
var VERSION = "0.17.
|
|
22652
|
+
var VERSION = "0.17.1";
|
|
22653
22653
|
|
|
22654
22654
|
// src/cli/resolve-version.ts
|
|
22655
22655
|
function readPackageVersion() {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
3
|
"//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
|
|
4
|
-
"version": "0.17.
|
|
4
|
+
"version": "0.17.1",
|
|
5
5
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -6713,6 +6713,77 @@ function escapeMarkdown(text) {
|
|
|
6713
6713
|
function codeSpanSafe(s) {
|
|
6714
6714
|
return s.replace(/`/g, "`\u200b");
|
|
6715
6715
|
}
|
|
6716
|
+
function maskCodeRegions(text, nonce) {
|
|
6717
|
+
const FENCE_MASK_PH = `\x00RMF${nonce}_`;
|
|
6718
|
+
const INLINE_MASK_PH = `\x00RMI${nonce}_`;
|
|
6719
|
+
const codeMasks = [];
|
|
6720
|
+
const masked = text.replace(/```[\s\S]*?```/g, (m) => {
|
|
6721
|
+
const idx = codeMasks.length;
|
|
6722
|
+
codeMasks.push(m);
|
|
6723
|
+
return `${FENCE_MASK_PH}${idx}\x00`;
|
|
6724
|
+
}).replace(/`[^`\n]+`/g, (m) => {
|
|
6725
|
+
const idx = codeMasks.length;
|
|
6726
|
+
codeMasks.push(m);
|
|
6727
|
+
return `${INLINE_MASK_PH}${idx}\x00`;
|
|
6728
|
+
});
|
|
6729
|
+
const escNonce = nonce.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6730
|
+
const anyMaskRe = new RegExp(`\x00RM[FI]${escNonce}_(\\d+)\x00`, "g");
|
|
6731
|
+
const restore = (s) => s.replace(anyMaskRe, (_m, idx) => codeMasks[Number(idx)] ?? _m);
|
|
6732
|
+
const stripPlaceholders = (s) => s.replace(anyMaskRe, "");
|
|
6733
|
+
return { masked, restore, placeholder: FENCE_MASK_PH, stripPlaceholders };
|
|
6734
|
+
}
|
|
6735
|
+
function hardenCardBreaks(text) {
|
|
6736
|
+
if (!text.includes(`
|
|
6737
|
+
`))
|
|
6738
|
+
return text;
|
|
6739
|
+
const nonce = Math.random().toString(36).slice(2);
|
|
6740
|
+
const { masked, restore, placeholder } = maskCodeRegions(text, nonce);
|
|
6741
|
+
const out = masked.replace(/\n{3,}/g, `
|
|
6742
|
+
|
|
6743
|
+
`);
|
|
6744
|
+
const isBlockConstructLine = (line) => isListItemLine(line) || isTableRowLine(line) || isTableDelimiterLine(line) || isBlockquoteLine(line) || isHeadingLine(line) || isFenceOpenLine(line, placeholder);
|
|
6745
|
+
const lines = out.split(`
|
|
6746
|
+
`);
|
|
6747
|
+
const pieces = [];
|
|
6748
|
+
for (let i = 0;i < lines.length; i++) {
|
|
6749
|
+
let line = lines[i];
|
|
6750
|
+
const isLast = i === lines.length - 1;
|
|
6751
|
+
const next = isLast ? "" : lines[i + 1];
|
|
6752
|
+
const promote = !isLast && line.trim() !== "" && next.trim() !== "" && !isBlockConstructLine(line) && !isBlockConstructLine(next);
|
|
6753
|
+
if (promote) {
|
|
6754
|
+
line = line.replace(/[ \t\r]+$/, "");
|
|
6755
|
+
}
|
|
6756
|
+
pieces.push(line);
|
|
6757
|
+
if (isLast)
|
|
6758
|
+
break;
|
|
6759
|
+
pieces.push(promote ? `
|
|
6760
|
+
` : `
|
|
6761
|
+
`);
|
|
6762
|
+
}
|
|
6763
|
+
return restore(pieces.join(""));
|
|
6764
|
+
}
|
|
6765
|
+
function isListItemLine(line) {
|
|
6766
|
+
const t = line.trimStart();
|
|
6767
|
+
return /^[-*+]\s/.test(t) || /^\d+[.)]\s/.test(t);
|
|
6768
|
+
}
|
|
6769
|
+
function isTableRowLine(line) {
|
|
6770
|
+
return /^\s*\|/.test(line);
|
|
6771
|
+
}
|
|
6772
|
+
function isTableDelimiterLine(line) {
|
|
6773
|
+
return /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?\s*$/.test(line);
|
|
6774
|
+
}
|
|
6775
|
+
function isFenceOpenLine(line, placeholder) {
|
|
6776
|
+
const t = line.trimStart();
|
|
6777
|
+
if (placeholder != null && placeholder.length > 0 && t.startsWith(placeholder))
|
|
6778
|
+
return true;
|
|
6779
|
+
return t.startsWith("```");
|
|
6780
|
+
}
|
|
6781
|
+
function isBlockquoteLine(line) {
|
|
6782
|
+
return line.trimStart().startsWith(">");
|
|
6783
|
+
}
|
|
6784
|
+
function isHeadingLine(line) {
|
|
6785
|
+
return /^#{1,6}\s/.test(line.trimStart());
|
|
6786
|
+
}
|
|
6716
6787
|
var RICH_MESSAGE_MAX_CHARS = 32768;
|
|
6717
6788
|
|
|
6718
6789
|
// text-voice-scrub.ts
|
|
@@ -32074,11 +32145,11 @@ function registerApprovalsCommands(bot, opts) {
|
|
|
32074
32145
|
return `\`${d.id.slice(0, 8)}\` ` + `${escapeMarkdown(d.agent_unit)} \u2192 ` + `\`${codeSpanSafe(d.scope)}\` ` + `(${escapeMarkdown(d.action)}, ${ttl}) ` + `\u00b7 /approvals revoke ${escapeMarkdown(d.id)}`;
|
|
32075
32146
|
}).join(`
|
|
32076
32147
|
`);
|
|
32077
|
-
await ctx.replyWithRichMessage(richMessage(`**Active approvals**
|
|
32148
|
+
await ctx.replyWithRichMessage(richMessage(hardenCardBreaks(`**Active approvals**
|
|
32078
32149
|
|
|
32079
32150
|
${summary}
|
|
32080
32151
|
|
|
32081
|
-
${detail}`));
|
|
32152
|
+
${detail}`)));
|
|
32082
32153
|
return;
|
|
32083
32154
|
}
|
|
32084
32155
|
if (sub === "revoke") {
|
|
@@ -35303,8 +35374,8 @@ function renderVaultRequestAccessCard(req) {
|
|
|
35303
35374
|
}
|
|
35304
35375
|
lines.push("");
|
|
35305
35376
|
lines.push(`_Tap Approve to mint a scoped grant token (same flow as \`switchroom vault grant\`). Tap Deny to refuse \u2014 the agent will receive a denial result._`);
|
|
35306
|
-
return lines.join(`
|
|
35307
|
-
`);
|
|
35377
|
+
return hardenCardBreaks(lines.join(`
|
|
35378
|
+
`));
|
|
35308
35379
|
}
|
|
35309
35380
|
|
|
35310
35381
|
// gateway/permission-card-store.ts
|
|
@@ -45062,26 +45133,29 @@ function repairEscapedWhitespace(text) {
|
|
|
45062
45133
|
return text;
|
|
45063
45134
|
const nonce = Math.random().toString(36).slice(2);
|
|
45064
45135
|
const BACKSLASH_PH = `\x00BK${nonce}_`;
|
|
45065
|
-
const { masked, restore: restore2 } =
|
|
45136
|
+
const { masked, restore: restore2 } = maskCodeRegions2(text, nonce);
|
|
45066
45137
|
const unescaped = masked.replace(/\\\\/g, BACKSLASH_PH).replace(/\\n/g, `
|
|
45067
45138
|
`).replace(/\\r/g, "\r").replace(/\\t/g, "\t").replace(/\\"/g, '"').replace(new RegExp(BACKSLASH_PH.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), "\\");
|
|
45068
45139
|
return restore2(unescaped);
|
|
45069
45140
|
}
|
|
45070
|
-
function
|
|
45071
|
-
const
|
|
45141
|
+
function maskCodeRegions2(text, nonce) {
|
|
45142
|
+
const FENCE_MASK_PH = `\x00RMF${nonce}_`;
|
|
45143
|
+
const INLINE_MASK_PH = `\x00RMI${nonce}_`;
|
|
45072
45144
|
const codeMasks = [];
|
|
45073
45145
|
const masked = text.replace(/```[\s\S]*?```/g, (m) => {
|
|
45074
45146
|
const idx = codeMasks.length;
|
|
45075
45147
|
codeMasks.push(m);
|
|
45076
|
-
return `${
|
|
45148
|
+
return `${FENCE_MASK_PH}${idx}\x00`;
|
|
45077
45149
|
}).replace(/`[^`\n]+`/g, (m) => {
|
|
45078
45150
|
const idx = codeMasks.length;
|
|
45079
45151
|
codeMasks.push(m);
|
|
45080
|
-
return `${
|
|
45152
|
+
return `${INLINE_MASK_PH}${idx}\x00`;
|
|
45081
45153
|
});
|
|
45082
|
-
const
|
|
45083
|
-
const
|
|
45084
|
-
|
|
45154
|
+
const escNonce = nonce.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
45155
|
+
const anyMaskRe = new RegExp(`\x00RM[FI]${escNonce}_(\\d+)\x00`, "g");
|
|
45156
|
+
const restore2 = (s) => s.replace(anyMaskRe, (_m, idx) => codeMasks[Number(idx)] ?? _m);
|
|
45157
|
+
const stripPlaceholders = (s) => s.replace(anyMaskRe, "");
|
|
45158
|
+
return { masked, restore: restore2, placeholder: FENCE_MASK_PH, stripPlaceholders };
|
|
45085
45159
|
}
|
|
45086
45160
|
function splitCollapsedInlineBullets(text) {
|
|
45087
45161
|
if (!/[\u2022\u00b7]/.test(text))
|
|
@@ -45103,7 +45177,7 @@ function normalizeParagraphBreaks(text) {
|
|
|
45103
45177
|
`) && !/[\u2022\u00b7]/.test(text))
|
|
45104
45178
|
return text;
|
|
45105
45179
|
const nonce = Math.random().toString(36).slice(2);
|
|
45106
|
-
const { masked: maskedRaw, restore: restore2, placeholder } =
|
|
45180
|
+
const { masked: maskedRaw, restore: restore2, placeholder } = maskCodeRegions2(text, nonce);
|
|
45107
45181
|
const masked = splitCollapsedInlineBullets(maskedRaw);
|
|
45108
45182
|
let out = masked.replace(/\n[ \t\r]+\n(?:[ \t\r]*\n)*/g, `
|
|
45109
45183
|
|
|
@@ -45132,6 +45206,36 @@ function normalizeParagraphBreaks(text) {
|
|
|
45132
45206
|
out = ensureBlockBoundaries(out, placeholder);
|
|
45133
45207
|
return restore2(out);
|
|
45134
45208
|
}
|
|
45209
|
+
function hardenCardBreaks2(text) {
|
|
45210
|
+
if (!text.includes(`
|
|
45211
|
+
`))
|
|
45212
|
+
return text;
|
|
45213
|
+
const nonce = Math.random().toString(36).slice(2);
|
|
45214
|
+
const { masked, restore: restore2, placeholder } = maskCodeRegions2(text, nonce);
|
|
45215
|
+
const out = masked.replace(/\n{3,}/g, `
|
|
45216
|
+
|
|
45217
|
+
`);
|
|
45218
|
+
const isBlockConstructLine = (line) => isListItemLine2(line) || isTableRowLine2(line) || isTableDelimiterLine2(line) || isBlockquoteLine2(line) || isHeadingLine2(line) || isFenceOpenLine2(line, placeholder);
|
|
45219
|
+
const lines = out.split(`
|
|
45220
|
+
`);
|
|
45221
|
+
const pieces = [];
|
|
45222
|
+
for (let i = 0;i < lines.length; i++) {
|
|
45223
|
+
let line = lines[i];
|
|
45224
|
+
const isLast = i === lines.length - 1;
|
|
45225
|
+
const next = isLast ? "" : lines[i + 1];
|
|
45226
|
+
const promote = !isLast && line.trim() !== "" && next.trim() !== "" && !isBlockConstructLine(line) && !isBlockConstructLine(next);
|
|
45227
|
+
if (promote) {
|
|
45228
|
+
line = line.replace(/[ \t\r]+$/, "");
|
|
45229
|
+
}
|
|
45230
|
+
pieces.push(line);
|
|
45231
|
+
if (isLast)
|
|
45232
|
+
break;
|
|
45233
|
+
pieces.push(promote ? `
|
|
45234
|
+
` : `
|
|
45235
|
+
`);
|
|
45236
|
+
}
|
|
45237
|
+
return restore2(pieces.join(""));
|
|
45238
|
+
}
|
|
45135
45239
|
var PARAGRAPH_SPACER = "\u00a0";
|
|
45136
45240
|
function addParagraphSpacers(text) {
|
|
45137
45241
|
if (!text.includes(`
|
|
@@ -45139,7 +45243,7 @@ function addParagraphSpacers(text) {
|
|
|
45139
45243
|
`))
|
|
45140
45244
|
return text;
|
|
45141
45245
|
const nonce = Math.random().toString(36).slice(2);
|
|
45142
|
-
const { masked, restore: restore2, placeholder } =
|
|
45246
|
+
const { masked, restore: restore2, placeholder } = maskCodeRegions2(text, nonce);
|
|
45143
45247
|
if (!masked.includes(`
|
|
45144
45248
|
|
|
45145
45249
|
`))
|
|
@@ -45150,15 +45254,15 @@ function addParagraphSpacers(text) {
|
|
|
45150
45254
|
const blockKind = (line) => {
|
|
45151
45255
|
if (asciiTrim(line) === spacerLine)
|
|
45152
45256
|
return "spacer";
|
|
45153
|
-
if (
|
|
45257
|
+
if (isFenceOpenLine2(line, placeholder))
|
|
45154
45258
|
return "fence";
|
|
45155
|
-
if (
|
|
45259
|
+
if (isListItemLine2(line))
|
|
45156
45260
|
return "list";
|
|
45157
|
-
if (
|
|
45261
|
+
if (isTableRowLine2(line) || isTableDelimiterLine2(line))
|
|
45158
45262
|
return "table";
|
|
45159
|
-
if (
|
|
45263
|
+
if (isBlockquoteLine2(line))
|
|
45160
45264
|
return "quote";
|
|
45161
|
-
if (
|
|
45265
|
+
if (isHeadingLine2(line))
|
|
45162
45266
|
return "heading";
|
|
45163
45267
|
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line.trimStart()))
|
|
45164
45268
|
return "divider";
|
|
@@ -45224,7 +45328,7 @@ function normalizePunctuation(text) {
|
|
|
45224
45328
|
if (!/[\u2014\u2013\u2022\u00b7]/.test(text))
|
|
45225
45329
|
return text;
|
|
45226
45330
|
const nonce = Math.random().toString(36).slice(2);
|
|
45227
|
-
const { masked, restore: restore2 } =
|
|
45331
|
+
const { masked, restore: restore2 } = maskCodeRegions2(text, nonce);
|
|
45228
45332
|
let out = masked.replace(/(\S)[ \t][\u2014\u2013][ \t](?=(\S))/g, (_m, a, b) => /\d/.test(a) && /\d/.test(b) ? `${a}-` : `${a}, `).replace(/(\w)\u2014(?=(\w))/g, (_m, a, b) => /\d/.test(a) && /\d/.test(b) ? `${a}-` : `${a}, `).replace(/(\w)\u2013(?=\w)/g, "$1-");
|
|
45229
45333
|
out = out.split(`
|
|
45230
45334
|
`).map((line) => line.replace(/^([ \t]*)[\u2022\u00b7][ \t]+/, "$1- ")).join(`
|
|
@@ -45244,9 +45348,8 @@ function stripExcessBold(text) {
|
|
|
45244
45348
|
if (!text.includes("**"))
|
|
45245
45349
|
return text;
|
|
45246
45350
|
const nonce = Math.random().toString(36).slice(2);
|
|
45247
|
-
const { masked, restore: restore2, placeholder } =
|
|
45248
|
-
const
|
|
45249
|
-
const visible = masked.replace(placeholderRe, "");
|
|
45351
|
+
const { masked, restore: restore2, placeholder, stripPlaceholders } = maskCodeRegions2(text, nonce);
|
|
45352
|
+
const visible = stripPlaceholders(masked);
|
|
45250
45353
|
if (visible.length < 100)
|
|
45251
45354
|
return restore2(masked);
|
|
45252
45355
|
let boldChars = 0;
|
|
@@ -45261,7 +45364,7 @@ function stripExcessBold(text) {
|
|
|
45261
45364
|
`).filter((l) => l.trim() !== "");
|
|
45262
45365
|
if (lines.length === 0 || !block.includes("**"))
|
|
45263
45366
|
return block;
|
|
45264
|
-
const listLines = lines.filter((l) =>
|
|
45367
|
+
const listLines = lines.filter((l) => isListItemLine2(l));
|
|
45265
45368
|
if (listLines.length >= 2 && listLines.length === lines.length) {
|
|
45266
45369
|
if (lines.every((l) => isFullyBolded(listItemContent(l))))
|
|
45267
45370
|
return unbold(block);
|
|
@@ -45284,26 +45387,26 @@ function stripExcessBold(text) {
|
|
|
45284
45387
|
`) + rebuilt[i];
|
|
45285
45388
|
return restore2(out);
|
|
45286
45389
|
}
|
|
45287
|
-
function
|
|
45390
|
+
function isListItemLine2(line) {
|
|
45288
45391
|
const t = line.trimStart();
|
|
45289
45392
|
return /^[-*+]\s/.test(t) || /^\d+[.)]\s/.test(t);
|
|
45290
45393
|
}
|
|
45291
|
-
function
|
|
45394
|
+
function isTableRowLine2(line) {
|
|
45292
45395
|
return /^\s*\|/.test(line);
|
|
45293
45396
|
}
|
|
45294
|
-
function
|
|
45397
|
+
function isTableDelimiterLine2(line) {
|
|
45295
45398
|
return /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?\s*$/.test(line);
|
|
45296
45399
|
}
|
|
45297
|
-
function
|
|
45400
|
+
function isFenceOpenLine2(line, placeholder) {
|
|
45298
45401
|
const t = line.trimStart();
|
|
45299
45402
|
if (placeholder != null && placeholder.length > 0 && t.startsWith(placeholder))
|
|
45300
45403
|
return true;
|
|
45301
45404
|
return t.startsWith("```");
|
|
45302
45405
|
}
|
|
45303
|
-
function
|
|
45406
|
+
function isBlockquoteLine2(line) {
|
|
45304
45407
|
return line.trimStart().startsWith(">");
|
|
45305
45408
|
}
|
|
45306
|
-
function
|
|
45409
|
+
function isHeadingLine2(line) {
|
|
45307
45410
|
return /^#{1,6}\s/.test(line.trimStart());
|
|
45308
45411
|
}
|
|
45309
45412
|
function ensureBlockBoundaries(text, placeholder) {
|
|
@@ -45320,24 +45423,24 @@ function ensureBlockBoundaries(text, placeholder) {
|
|
|
45320
45423
|
const curBlank = line.trim() === "";
|
|
45321
45424
|
if (prevNonBlank && !curBlank) {
|
|
45322
45425
|
const next = i + 1 < lines.length ? lines[i + 1] : "";
|
|
45323
|
-
const prevIsTable =
|
|
45324
|
-
const startsTableHere = !prevIsTable && (line.includes("|") &&
|
|
45325
|
-
const startsFence =
|
|
45326
|
-
const startsQuote =
|
|
45327
|
-
const startsHeading =
|
|
45328
|
-
const startsList =
|
|
45426
|
+
const prevIsTable = isTableRowLine2(prev);
|
|
45427
|
+
const startsTableHere = !prevIsTable && (line.includes("|") && isTableDelimiterLine2(next) || isTableRowLine2(line) && isTableDelimiterLine2(next));
|
|
45428
|
+
const startsFence = isFenceOpenLine2(line, placeholder) && !isFenceOpenLine2(prev, placeholder);
|
|
45429
|
+
const startsQuote = isBlockquoteLine2(line) && !isBlockquoteLine2(prev);
|
|
45430
|
+
const startsHeading = isHeadingLine2(line) && !isHeadingLine2(prev);
|
|
45431
|
+
const startsList = isListItemLine2(line) && !isListItemLine2(prev);
|
|
45329
45432
|
if (startsTableHere || startsFence || startsQuote || startsHeading || startsList) {
|
|
45330
45433
|
result.push("");
|
|
45331
45434
|
}
|
|
45332
45435
|
}
|
|
45333
|
-
if (prevNonBlank && !curBlank &&
|
|
45436
|
+
if (prevNonBlank && !curBlank && isFenceOpenLine2(prev, placeholder)) {
|
|
45334
45437
|
const alreadySeparated = result.length > 0 && result[result.length - 1].trim() === "";
|
|
45335
|
-
const curIsBlockStart =
|
|
45438
|
+
const curIsBlockStart = isFenceOpenLine2(line, placeholder) || isBlockquoteLine2(line) || isHeadingLine2(line) || isTableRowLine2(line) || isTableDelimiterLine2(line);
|
|
45336
45439
|
if (!alreadySeparated && !curIsBlockStart) {
|
|
45337
45440
|
result.push("");
|
|
45338
45441
|
}
|
|
45339
45442
|
}
|
|
45340
|
-
if (prevNonBlank && !curBlank &&
|
|
45443
|
+
if (prevNonBlank && !curBlank && isListItemLine2(prev) && !isListItemLine2(line)) {
|
|
45341
45444
|
const isIndentedContinuation = /^(\t| {4,})\S/.test(line);
|
|
45342
45445
|
const alreadySeparated = result.length > 0 && result[result.length - 1].trim() === "";
|
|
45343
45446
|
if (!isIndentedContinuation && !alreadySeparated) {
|
|
@@ -45355,7 +45458,7 @@ function isMarkerLine(line, placeholder) {
|
|
|
45355
45458
|
const t = line.trimStart();
|
|
45356
45459
|
if (placeholder != null && placeholder.length > 0 && t.startsWith(placeholder))
|
|
45357
45460
|
return true;
|
|
45358
|
-
return /^[-*+]\s/.test(t) || /^\d+[.)]\s/.test(t) || t.startsWith(">") || /^#{1,6}\s/.test(t) ||
|
|
45461
|
+
return /^[-*+]\s/.test(t) || /^\d+[.)]\s/.test(t) || t.startsWith(">") || /^#{1,6}\s/.test(t) || isTableRowLine2(line) || isTableDelimiterLine2(line) || t.startsWith("```") || /^(-{3,}|\*{3,}|_{3,})\s*$/.test(t);
|
|
45359
45462
|
}
|
|
45360
45463
|
function shouldPromoteBreak(prev, next, placeholder) {
|
|
45361
45464
|
if (isMarkerLine(prev, placeholder) || isMarkerLine(next, placeholder))
|
|
@@ -45786,7 +45889,7 @@ function formatAgentLine(meta) {
|
|
|
45786
45889
|
function startText(agentName3, dmDisabled) {
|
|
45787
45890
|
if (dmDisabled)
|
|
45788
45891
|
return "This bot isn't accepting new connections.";
|
|
45789
|
-
return [
|
|
45892
|
+
return stackCardLines([
|
|
45790
45893
|
`**Switchroom** \u2014 Telegram on your Claude Pro or Max subscription.`,
|
|
45791
45894
|
``,
|
|
45792
45895
|
`This bot is the **${escapeHtml(agentName3)}** agent. Pair first, then send messages here and they reach the agent; replies and reactions come back.`,
|
|
@@ -45796,11 +45899,10 @@ function startText(agentName3, dmDisabled) {
|
|
|
45796
45899
|
`2. In Claude Code: \`/telegram:access pair <code>\``,
|
|
45797
45900
|
``,
|
|
45798
45901
|
`After pairing, try \`/status\` or \`/commands\`.`
|
|
45799
|
-
]
|
|
45800
|
-
`);
|
|
45902
|
+
]);
|
|
45801
45903
|
}
|
|
45802
45904
|
function helpText(agentName3) {
|
|
45803
|
-
return [
|
|
45905
|
+
return stackCardLines([
|
|
45804
45906
|
`**Switchroom** \u2014 your Pro/Max subscription, wired to Telegram.`,
|
|
45805
45907
|
``,
|
|
45806
45908
|
`This bot is the **${escapeHtml(agentName3)}** agent. Text and photos route through to it; replies, reactions and progress cards come back.`,
|
|
@@ -45811,8 +45913,7 @@ function helpText(agentName3) {
|
|
|
45811
45913
|
`\`/status\` \u2014 agent, model, auth`,
|
|
45812
45914
|
`\`/vault audit <agent>\` \u2014 admin: review agent's vault access + one-tap [\uD83D\uDD13 Allow] on recent denials`,
|
|
45813
45915
|
`\`/commands\` \u2014 full command list`
|
|
45814
|
-
]
|
|
45815
|
-
`);
|
|
45916
|
+
]);
|
|
45816
45917
|
}
|
|
45817
45918
|
var STATUS_DOT = {
|
|
45818
45919
|
ok: "\uD83D\uDFE2",
|
|
@@ -45858,13 +45959,14 @@ function statusPairedText(params) {
|
|
|
45858
45959
|
if (audit.memoryBank)
|
|
45859
45960
|
lines.push(`**Memory** ${escapeHtml(audit.memoryBank)}`);
|
|
45860
45961
|
}
|
|
45861
|
-
return lines
|
|
45862
|
-
`);
|
|
45962
|
+
return stackCardLines(lines);
|
|
45863
45963
|
}
|
|
45864
45964
|
function statusPendingText(code) {
|
|
45865
|
-
return
|
|
45866
|
-
|
|
45867
|
-
|
|
45965
|
+
return stackCardLines([
|
|
45966
|
+
`Pending pairing \u2014 run in Claude Code:`,
|
|
45967
|
+
``,
|
|
45968
|
+
`\`/telegram:access pair ${code}\``
|
|
45969
|
+
]);
|
|
45868
45970
|
}
|
|
45869
45971
|
function statusUnpairedText() {
|
|
45870
45972
|
return "Not paired. Send me a message to get a pairing code.";
|
|
@@ -45895,7 +45997,7 @@ var TELEGRAM_MENU_COMMANDS = [
|
|
|
45895
45997
|
var TELEGRAM_BASE_COMMANDS = TELEGRAM_MENU_COMMANDS.slice(0, 3);
|
|
45896
45998
|
var TELEGRAM_SWITCHROOM_COMMANDS = TELEGRAM_MENU_COMMANDS.slice(3);
|
|
45897
45999
|
function switchroomHelpText(agentName3) {
|
|
45898
|
-
return [
|
|
46000
|
+
return stackCardLines([
|
|
45899
46001
|
`**Switchroom bot** \u2014 commands for the **${escapeHtml(agentName3)}** agent.`,
|
|
45900
46002
|
``,
|
|
45901
46003
|
`**Session & approvals**`,
|
|
@@ -45945,8 +46047,7 @@ function switchroomHelpText(agentName3) {
|
|
|
45945
46047
|
`\`/commands\` \u2014 this help`,
|
|
45946
46048
|
``,
|
|
45947
46049
|
`_Tip: \`/update\` shows the plan; \`/update apply\` executes it; \`/restart\` bounces a stuck agent; \`/version\` checks what's running._`
|
|
45948
|
-
]
|
|
45949
|
-
`);
|
|
46050
|
+
]);
|
|
45950
46051
|
}
|
|
45951
46052
|
function restartAckText(agentName3) {
|
|
45952
46053
|
return `\uD83D\uDD04 Restarting **${escapeHtml(agentName3)}**\u2026`;
|
|
@@ -51577,8 +51678,8 @@ function buildMs365CardText(p) {
|
|
|
51577
51678
|
}
|
|
51578
51679
|
lines.push("");
|
|
51579
51680
|
lines.push("\u26a0\ufe0f Weak attestation (RFC \u00a78 v1): operator should click through to verify the actual change before approving. Structural diff coming v1.5.");
|
|
51580
|
-
return lines.join(`
|
|
51581
|
-
`);
|
|
51681
|
+
return hardenCardBreaks(lines.join(`
|
|
51682
|
+
`));
|
|
51582
51683
|
}
|
|
51583
51684
|
function truncate3(s, n) {
|
|
51584
51685
|
if (s.length <= n)
|
|
@@ -58663,10 +58764,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
58663
58764
|
}
|
|
58664
58765
|
|
|
58665
58766
|
// ../src/build-info.ts
|
|
58666
|
-
var VERSION = "0.17.
|
|
58667
|
-
var COMMIT_SHA = "
|
|
58668
|
-
var COMMIT_DATE = "2026-07-
|
|
58669
|
-
var LATEST_PR =
|
|
58767
|
+
var VERSION = "0.17.1";
|
|
58768
|
+
var COMMIT_SHA = "d7162c0";
|
|
58769
|
+
var COMMIT_DATE = "2026-07-05T05:44:23Z";
|
|
58770
|
+
var LATEST_PR = 2831;
|
|
58670
58771
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
58671
58772
|
|
|
58672
58773
|
// gateway/boot-version.ts
|
|
@@ -64735,8 +64836,8 @@ function renderVaultRequestSaveCard(req, agentSlug) {
|
|
|
64735
64836
|
}
|
|
64736
64837
|
lines.push("");
|
|
64737
64838
|
lines.push(`_Tap Save to write to the host vault, Rename to change the key name, or Discard to drop it. The value is held in this chat's gateway memory until you decide._`);
|
|
64738
|
-
return lines.join(`
|
|
64739
|
-
`);
|
|
64839
|
+
return hardenCardBreaks2(lines.join(`
|
|
64840
|
+
`));
|
|
64740
64841
|
}
|
|
64741
64842
|
async function executeVaultRequestSave(args) {
|
|
64742
64843
|
const chat_id = String(args.chat_id ?? "");
|
|
@@ -67343,7 +67444,7 @@ async function switchroomReply(ctx, text2, options = {}) {
|
|
|
67343
67444
|
...options.reply_markup ? { reply_markup: options.reply_markup } : {}
|
|
67344
67445
|
};
|
|
67345
67446
|
if (options.html) {
|
|
67346
|
-
await ctx.replyWithRichMessage(richMessage2(text2), replyOpts);
|
|
67447
|
+
await ctx.replyWithRichMessage(richMessage2(hardenCardBreaks2(text2)), replyOpts);
|
|
67347
67448
|
} else {
|
|
67348
67449
|
await ctx.reply(text2, replyOpts);
|
|
67349
67450
|
}
|
|
@@ -132,8 +132,18 @@ export function repairEscapedWhitespace(text: string): string {
|
|
|
132
132
|
interface MaskedCode {
|
|
133
133
|
masked: string
|
|
134
134
|
restore: (s: string) => string
|
|
135
|
-
/**
|
|
135
|
+
/**
|
|
136
|
+
* The placeholder prefix injected for FENCED-BLOCK masks only. A masked
|
|
137
|
+
* fenced block occupies a whole line, so `isFenceOpenLine` / `isMarkerLine`
|
|
138
|
+
* treat a line that STARTS with this prefix as a block construct. INLINE
|
|
139
|
+
* code spans get a DISTINCT prefix (see maskCodeRegions) that deliberately
|
|
140
|
+
* does NOT start with this one — so a line that merely opens with an inline
|
|
141
|
+
* span (e.g. `\`key\` = \`value\``) reads as ordinary prose and still gets
|
|
142
|
+
* its line break hardened.
|
|
143
|
+
*/
|
|
136
144
|
placeholder: string
|
|
145
|
+
/** Remove EVERY mask (fenced + inline) — used to measure visible length. */
|
|
146
|
+
stripPlaceholders: (s: string) => string
|
|
137
147
|
}
|
|
138
148
|
|
|
139
149
|
/**
|
|
@@ -144,31 +154,40 @@ interface MaskedCode {
|
|
|
144
154
|
* Fenced blocks are extracted FIRST and only when CLOSED (matching ```), so an
|
|
145
155
|
* unclosed fence is left intact rather than misparsed by the inline pass. Inline
|
|
146
156
|
* spans use `[^\`\n]+` — the same definition the chunker treats as code.
|
|
157
|
+
*
|
|
158
|
+
* Fenced and inline masks carry DISTINCT prefixes (`\x00RMF…` vs `\x00RMI…`).
|
|
159
|
+
* This matters because the fenced prefix is what the block-structure predicates
|
|
160
|
+
* (`isFenceOpenLine`, `isMarkerLine`) use to recognise a standalone masked code
|
|
161
|
+
* block. Sharing one prefix (the pre-fix bug) made a line that merely STARTS
|
|
162
|
+
* with an inline code span look like a fenced block, so its lone `\n` was never
|
|
163
|
+
* hardened and the card collapsed into one run-on line (real victim:
|
|
164
|
+
* `/vault get` rendering `\`key\` = \`value\``).
|
|
147
165
|
*/
|
|
148
166
|
function maskCodeRegions(text: string, nonce: string): MaskedCode {
|
|
149
|
-
const
|
|
167
|
+
const FENCE_MASK_PH = `\x00RMF${nonce}_`
|
|
168
|
+
const INLINE_MASK_PH = `\x00RMI${nonce}_`
|
|
150
169
|
const codeMasks: string[] = []
|
|
151
170
|
|
|
152
171
|
const masked = text
|
|
153
172
|
.replace(/```[\s\S]*?```/g, (m) => {
|
|
154
173
|
const idx = codeMasks.length
|
|
155
174
|
codeMasks.push(m)
|
|
156
|
-
return `${
|
|
175
|
+
return `${FENCE_MASK_PH}${idx}\x00`
|
|
157
176
|
})
|
|
158
177
|
.replace(/`[^`\n]+`/g, (m) => {
|
|
159
178
|
const idx = codeMasks.length
|
|
160
179
|
codeMasks.push(m)
|
|
161
|
-
return `${
|
|
180
|
+
return `${INLINE_MASK_PH}${idx}\x00`
|
|
162
181
|
})
|
|
163
182
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
)
|
|
183
|
+
// Restore / strip match EITHER prefix, keyed on the shared index space.
|
|
184
|
+
const escNonce = nonce.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
185
|
+
const anyMaskRe = new RegExp(`\x00RM[FI]${escNonce}_(\\d+)\x00`, 'g')
|
|
168
186
|
const restore = (s: string): string =>
|
|
169
|
-
s.replace(
|
|
187
|
+
s.replace(anyMaskRe, (_m, idx) => codeMasks[Number(idx)] ?? _m)
|
|
188
|
+
const stripPlaceholders = (s: string): string => s.replace(anyMaskRe, '')
|
|
170
189
|
|
|
171
|
-
return { masked, restore, placeholder:
|
|
190
|
+
return { masked, restore, placeholder: FENCE_MASK_PH, stripPlaceholders }
|
|
172
191
|
}
|
|
173
192
|
|
|
174
193
|
// ---------------------------------------------------------------------------
|
|
@@ -344,6 +363,92 @@ export function normalizeParagraphBreaks(text: string): string {
|
|
|
344
363
|
return restore(out)
|
|
345
364
|
}
|
|
346
365
|
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
// Card line-break hardener — for DETERMINISTIC command/card bodies
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Harden the lone `\n` line breaks of a DETERMINISTIC card body into GFM hard
|
|
372
|
+
* breaks (` \n`, two trailing spaces) so every field lands on its own line
|
|
373
|
+
* under Telegram's Bot API 10.1 rich-message (GFM) renderer.
|
|
374
|
+
*
|
|
375
|
+
* Why this exists (the run-on-blob bug): the rich path (#2669) renders a lone
|
|
376
|
+
* `\n` between two non-blank lines as a *soft* break — the two lines collapse
|
|
377
|
+
* onto the same visual line with a space between them. Agent PROSE is repaired
|
|
378
|
+
* on the reply path by `normalizeParagraphBreaks`, but the ~98 slash-command
|
|
379
|
+
* card replies dispatched through `switchroomReply(…, { html: true })` are sent
|
|
380
|
+
* as RAW markdown with no normalization. Their builders stack short labelled
|
|
381
|
+
* fields (`**5h window** …`, `**Model** …`, `Auth: ✓ Max …`) joined by a single
|
|
382
|
+
* `\n`, so the whole card renders as one run-on blob.
|
|
383
|
+
*
|
|
384
|
+
* A deterministic card is NOT free prose — every newline its builder emits is
|
|
385
|
+
* an INTENDED line break. So this hardener promotes UNCONDITIONALLY (no
|
|
386
|
+
* sentence-terminal-punctuation gate, unlike `normalizeParagraphBreaks`) with
|
|
387
|
+
* one exception: a line that participates in a genuine GFM block construct
|
|
388
|
+
* (list / table / blockquote / heading / fenced code) keeps its single `\n` so
|
|
389
|
+
* its native stacking / contiguity survives — a monospace table inside a ```
|
|
390
|
+
* fence is never touched (it is code-masked AND the fence lines are excluded).
|
|
391
|
+
* Real `\n\n` paragraph gaps (a builder's block separators) are preserved.
|
|
392
|
+
*
|
|
393
|
+
* This is the string-level sibling of `stackCardLines` (card-format.ts), which
|
|
394
|
+
* does the same promotion from a pre-split `string[]` of guaranteed
|
|
395
|
+
* single-line, non-block entries. Use `hardenCardBreaks` where the card body is
|
|
396
|
+
* already an assembled string (e.g. the `switchroomReply` chokepoint) and may
|
|
397
|
+
* legitimately contain GFM block constructs.
|
|
398
|
+
*
|
|
399
|
+
* Runs on code-masked text and is idempotent — a break already hardened to
|
|
400
|
+
* ` \n` re-hardens to the same ` \n`.
|
|
401
|
+
*/
|
|
402
|
+
export function hardenCardBreaks(text: string): string {
|
|
403
|
+
if (!text.includes('\n')) return text
|
|
404
|
+
|
|
405
|
+
const nonce = Math.random().toString(36).slice(2)
|
|
406
|
+
const { masked, restore, placeholder } = maskCodeRegions(text, nonce)
|
|
407
|
+
|
|
408
|
+
// Collapse 3+ newline runs to a single clean `\n\n` gap (mirrors
|
|
409
|
+
// normalizeParagraphBreaks step 1) so a stray extra blank line never becomes
|
|
410
|
+
// an oversized gap. A genuine one-blank-line `\n\n` block gap is preserved.
|
|
411
|
+
const out = masked.replace(/\n{3,}/g, '\n\n')
|
|
412
|
+
|
|
413
|
+
// A line participating in a GFM block construct whose single-`\n` contiguity
|
|
414
|
+
// must survive (its interior must NOT get a hard break).
|
|
415
|
+
const isBlockConstructLine = (line: string): boolean =>
|
|
416
|
+
isListItemLine(line) ||
|
|
417
|
+
isTableRowLine(line) ||
|
|
418
|
+
isTableDelimiterLine(line) ||
|
|
419
|
+
isBlockquoteLine(line) ||
|
|
420
|
+
isHeadingLine(line) ||
|
|
421
|
+
isFenceOpenLine(line, placeholder)
|
|
422
|
+
|
|
423
|
+
const lines = out.split('\n')
|
|
424
|
+
const pieces: string[] = []
|
|
425
|
+
for (let i = 0; i < lines.length; i++) {
|
|
426
|
+
let line = lines[i]
|
|
427
|
+
const isLast = i === lines.length - 1
|
|
428
|
+
const next = isLast ? '' : lines[i + 1]
|
|
429
|
+
// Promote only between two non-blank content lines where NEITHER is a GFM
|
|
430
|
+
// block-construct line (so lists / tables / quotes / headings / fences keep
|
|
431
|
+
// their native single-`\n` stacking). A blank current/next line is a `\n\n`
|
|
432
|
+
// paragraph gap — never promote across it.
|
|
433
|
+
const promote =
|
|
434
|
+
!isLast &&
|
|
435
|
+
line.trim() !== '' &&
|
|
436
|
+
next.trim() !== '' &&
|
|
437
|
+
!isBlockConstructLine(line) &&
|
|
438
|
+
!isBlockConstructLine(next)
|
|
439
|
+
if (promote) {
|
|
440
|
+
// Strip trailing whitespace so a re-run emits exactly one ` \n` (never
|
|
441
|
+
// accumulate spaces). Include `\r` for CRLF sources.
|
|
442
|
+
line = line.replace(/[ \t\r]+$/, '')
|
|
443
|
+
}
|
|
444
|
+
pieces.push(line)
|
|
445
|
+
if (isLast) break
|
|
446
|
+
pieces.push(promote ? ' \n' : '\n')
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return restore(pieces.join(''))
|
|
450
|
+
}
|
|
451
|
+
|
|
347
452
|
// ---------------------------------------------------------------------------
|
|
348
453
|
// Paragraph spacers — restore a VISIBLE blank line between prose paragraphs
|
|
349
454
|
// ---------------------------------------------------------------------------
|
|
@@ -623,14 +728,11 @@ export function stripExcessBold(text: string): string {
|
|
|
623
728
|
if (!text.includes('**')) return text
|
|
624
729
|
|
|
625
730
|
const nonce = Math.random().toString(36).slice(2)
|
|
626
|
-
const { masked, restore, placeholder } = maskCodeRegions(text, nonce)
|
|
731
|
+
const { masked, restore, placeholder, stripPlaceholders } = maskCodeRegions(text, nonce)
|
|
627
732
|
|
|
628
|
-
// Non-code character budget: masked text with
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
'g',
|
|
632
|
-
)
|
|
633
|
-
const visible = masked.replace(placeholderRe, '')
|
|
733
|
+
// Non-code character budget: masked text with BOTH fenced + inline masks
|
|
734
|
+
// removed (stripPlaceholders handles the two distinct prefixes).
|
|
735
|
+
const visible = stripPlaceholders(masked)
|
|
634
736
|
if (visible.length < 100) return restore(masked)
|
|
635
737
|
|
|
636
738
|
let boldChars = 0
|