switchroom 0.18.25 → 0.18.26
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 +1 -1
- package/dist/host-control/main.js +1 -1
- package/package.json +2 -2
- package/telegram-plugin/dist/gateway/gateway.js +342 -6
- package/telegram-plugin/render/code-segments.ts +210 -0
- package/telegram-plugin/render/dollar-math-guard.ts +126 -0
- package/telegram-plugin/render/emphasis-guard.ts +158 -0
- package/telegram-plugin/render/inline-pairs-guard.ts +171 -0
- package/telegram-plugin/render/line-start-guard.ts +167 -0
- package/telegram-plugin/render/rich-render.ts +7 -0
- package/telegram-plugin/rich-send.ts +48 -2
- package/telegram-plugin/tests/render/dollar-math-guard.test.ts +162 -0
- package/telegram-plugin/tests/render/emphasis-guard.test.ts +205 -0
- package/telegram-plugin/tests/render/guard-composition.test.ts +138 -0
- package/telegram-plugin/tests/render/inline-pairs-guard.test.ts +171 -0
- package/telegram-plugin/tests/render/line-start-guard.test.ts +164 -0
package/dist/cli/switchroom.js
CHANGED
|
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
|
|
|
2120
2120
|
});
|
|
2121
2121
|
|
|
2122
2122
|
// src/build-info.ts
|
|
2123
|
-
var VERSION = "0.18.
|
|
2123
|
+
var VERSION = "0.18.26", COMMIT_SHA = "ceb7d1a9";
|
|
2124
2124
|
|
|
2125
2125
|
// src/cli/resolve-version.ts
|
|
2126
2126
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -26605,7 +26605,7 @@ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:f
|
|
|
26605
26605
|
import { dirname as dirname4, join as join7 } from "node:path";
|
|
26606
26606
|
|
|
26607
26607
|
// src/build-info.ts
|
|
26608
|
-
var VERSION = "0.18.
|
|
26608
|
+
var VERSION = "0.18.26";
|
|
26609
26609
|
|
|
26610
26610
|
// src/cli/resolve-version.ts
|
|
26611
26611
|
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.18.
|
|
4
|
+
"version": "0.18.26",
|
|
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": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"test:vitest": "vitest run",
|
|
29
29
|
"test:bun": "bun test src/vault/grants.test.ts src/vault/grants-db.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
|
|
30
30
|
"test:watch": "vitest",
|
|
31
|
-
"lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-web-subscription-honest.mjs",
|
|
31
|
+
"lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs",
|
|
32
32
|
"lint:tsc": "tsc --noEmit",
|
|
33
33
|
"lint:plugin-references": "node scripts/check-plugin-references.mjs",
|
|
34
34
|
"lint:bot-api-wrapping": "bash scripts/check-bot-api-wrapping.sh",
|
|
@@ -7525,9 +7525,329 @@ var init_approval_card = __esm(() => {
|
|
|
7525
7525
|
import_grammy3 = __toESM(require_mod2(), 1);
|
|
7526
7526
|
});
|
|
7527
7527
|
|
|
7528
|
+
// render/code-segments.ts
|
|
7529
|
+
function findClosingBackticks(text, from, runLen) {
|
|
7530
|
+
let i = from;
|
|
7531
|
+
while (i < text.length) {
|
|
7532
|
+
if (text[i] === "`") {
|
|
7533
|
+
let j = i;
|
|
7534
|
+
while (j < text.length && text[j] === "`")
|
|
7535
|
+
j++;
|
|
7536
|
+
if (j - i === runLen)
|
|
7537
|
+
return j;
|
|
7538
|
+
i = j;
|
|
7539
|
+
} else {
|
|
7540
|
+
i++;
|
|
7541
|
+
}
|
|
7542
|
+
}
|
|
7543
|
+
return -1;
|
|
7544
|
+
}
|
|
7545
|
+
function splitCodeSegments(text) {
|
|
7546
|
+
const out = [];
|
|
7547
|
+
let i = 0;
|
|
7548
|
+
let plainStart = 0;
|
|
7549
|
+
while (i < text.length) {
|
|
7550
|
+
if (text[i] === "`") {
|
|
7551
|
+
let j = i;
|
|
7552
|
+
while (j < text.length && text[j] === "`")
|
|
7553
|
+
j++;
|
|
7554
|
+
const runLen = j - i;
|
|
7555
|
+
const close = findClosingBackticks(text, j, runLen);
|
|
7556
|
+
if (close !== -1) {
|
|
7557
|
+
if (plainStart < i)
|
|
7558
|
+
out.push({ code: false, text: text.slice(plainStart, i) });
|
|
7559
|
+
out.push({ code: true, text: text.slice(i, close) });
|
|
7560
|
+
i = close;
|
|
7561
|
+
plainStart = close;
|
|
7562
|
+
continue;
|
|
7563
|
+
}
|
|
7564
|
+
}
|
|
7565
|
+
i++;
|
|
7566
|
+
}
|
|
7567
|
+
if (plainStart < text.length)
|
|
7568
|
+
out.push({ code: false, text: text.slice(plainStart) });
|
|
7569
|
+
return out;
|
|
7570
|
+
}
|
|
7571
|
+
function isTableDelimiterRow(line) {
|
|
7572
|
+
const t = line.trim();
|
|
7573
|
+
return t.length > 0 && /^[\s|:-]+$/.test(t) && t.includes("-") && t.includes("|");
|
|
7574
|
+
}
|
|
7575
|
+
function isTableCandidateLine(line) {
|
|
7576
|
+
return /^\s*\|/.test(line);
|
|
7577
|
+
}
|
|
7578
|
+
function findTableRanges(text) {
|
|
7579
|
+
const ranges = [];
|
|
7580
|
+
const lines = text.split(`
|
|
7581
|
+
`);
|
|
7582
|
+
let offset = 0;
|
|
7583
|
+
let runStart = -1;
|
|
7584
|
+
let runEnd = -1;
|
|
7585
|
+
let runHasDelim = false;
|
|
7586
|
+
let runLineCount = 0;
|
|
7587
|
+
const flush = () => {
|
|
7588
|
+
if (runStart !== -1 && runLineCount >= 2 && runHasDelim) {
|
|
7589
|
+
ranges.push([runStart, runEnd]);
|
|
7590
|
+
}
|
|
7591
|
+
runStart = -1;
|
|
7592
|
+
runEnd = -1;
|
|
7593
|
+
runHasDelim = false;
|
|
7594
|
+
runLineCount = 0;
|
|
7595
|
+
};
|
|
7596
|
+
for (let k = 0;k < lines.length; k++) {
|
|
7597
|
+
const line = lines[k];
|
|
7598
|
+
const lineLen = line.length + (k < lines.length - 1 ? 1 : 0);
|
|
7599
|
+
if (isTableCandidateLine(line)) {
|
|
7600
|
+
if (runStart === -1)
|
|
7601
|
+
runStart = offset;
|
|
7602
|
+
runEnd = offset + lineLen;
|
|
7603
|
+
runLineCount += 1;
|
|
7604
|
+
if (isTableDelimiterRow(line))
|
|
7605
|
+
runHasDelim = true;
|
|
7606
|
+
} else {
|
|
7607
|
+
flush();
|
|
7608
|
+
}
|
|
7609
|
+
offset += lineLen;
|
|
7610
|
+
}
|
|
7611
|
+
flush();
|
|
7612
|
+
return ranges;
|
|
7613
|
+
}
|
|
7614
|
+
function splitProseProtected(text) {
|
|
7615
|
+
const out = [];
|
|
7616
|
+
const tables = findTableRanges(text);
|
|
7617
|
+
let tIdx = 0;
|
|
7618
|
+
let i = 0;
|
|
7619
|
+
let plainStart = 0;
|
|
7620
|
+
const pushProtected = (from, to) => {
|
|
7621
|
+
if (plainStart < from)
|
|
7622
|
+
out.push({ code: false, text: text.slice(plainStart, from) });
|
|
7623
|
+
out.push({ code: true, text: text.slice(from, to) });
|
|
7624
|
+
plainStart = to;
|
|
7625
|
+
};
|
|
7626
|
+
while (i < text.length) {
|
|
7627
|
+
while (tIdx < tables.length && tables[tIdx][1] <= i)
|
|
7628
|
+
tIdx++;
|
|
7629
|
+
if (tIdx < tables.length && tables[tIdx][0] === i) {
|
|
7630
|
+
const [, end] = tables[tIdx];
|
|
7631
|
+
pushProtected(i, end);
|
|
7632
|
+
i = end;
|
|
7633
|
+
continue;
|
|
7634
|
+
}
|
|
7635
|
+
const ch = text[i];
|
|
7636
|
+
if (ch === "[") {
|
|
7637
|
+
const close = text.indexOf("]", i + 1);
|
|
7638
|
+
if (close !== -1 && text[close + 1] === "(") {
|
|
7639
|
+
const destClose = text.indexOf(")", close + 2);
|
|
7640
|
+
if (destClose !== -1) {
|
|
7641
|
+
pushProtected(close + 1, destClose + 1);
|
|
7642
|
+
i = destClose + 1;
|
|
7643
|
+
continue;
|
|
7644
|
+
}
|
|
7645
|
+
}
|
|
7646
|
+
i++;
|
|
7647
|
+
continue;
|
|
7648
|
+
}
|
|
7649
|
+
if ((ch === "h" || ch === "w") && (i === 0 || !/[A-Za-z0-9]/.test(text[i - 1]))) {
|
|
7650
|
+
const m = /^(?:https?:\/\/|www\.)[^\s<>()\[\]]+/.exec(text.slice(i));
|
|
7651
|
+
if (m) {
|
|
7652
|
+
const end = i + m[0].length;
|
|
7653
|
+
pushProtected(i, end);
|
|
7654
|
+
i = end;
|
|
7655
|
+
continue;
|
|
7656
|
+
}
|
|
7657
|
+
}
|
|
7658
|
+
i++;
|
|
7659
|
+
}
|
|
7660
|
+
if (plainStart < text.length)
|
|
7661
|
+
out.push({ code: false, text: text.slice(plainStart) });
|
|
7662
|
+
return out;
|
|
7663
|
+
}
|
|
7664
|
+
function splitProtectedSegments(text) {
|
|
7665
|
+
const out = [];
|
|
7666
|
+
for (const seg of splitCodeSegments(text)) {
|
|
7667
|
+
if (seg.code) {
|
|
7668
|
+
out.push(seg);
|
|
7669
|
+
continue;
|
|
7670
|
+
}
|
|
7671
|
+
for (const sub of splitProseProtected(seg.text))
|
|
7672
|
+
out.push(sub);
|
|
7673
|
+
}
|
|
7674
|
+
return out;
|
|
7675
|
+
}
|
|
7676
|
+
|
|
7677
|
+
// render/dollar-math-guard.ts
|
|
7678
|
+
function guardDollarMath(text) {
|
|
7679
|
+
if (!text.includes("$"))
|
|
7680
|
+
return text;
|
|
7681
|
+
const segments = splitProtectedSegments(text);
|
|
7682
|
+
let total = 0;
|
|
7683
|
+
let hasCurrencySignal = false;
|
|
7684
|
+
for (const seg of segments) {
|
|
7685
|
+
if (seg.code)
|
|
7686
|
+
continue;
|
|
7687
|
+
total += seg.text.match(ANY_DOLLAR)?.length ?? 0;
|
|
7688
|
+
if (!hasCurrencySignal && CURRENCY_SIGNAL.test(seg.text))
|
|
7689
|
+
hasCurrencySignal = true;
|
|
7690
|
+
}
|
|
7691
|
+
if (total < 2 || !hasCurrencySignal)
|
|
7692
|
+
return text;
|
|
7693
|
+
return segments.map((seg) => seg.code ? seg.text : seg.text.replace(UNESCAPED_DOLLAR, () => "\\$")).join("");
|
|
7694
|
+
}
|
|
7695
|
+
var ANY_DOLLAR, CURRENCY_SIGNAL, UNESCAPED_DOLLAR;
|
|
7696
|
+
var init_dollar_math_guard = __esm(() => {
|
|
7697
|
+
ANY_DOLLAR = /\$/g;
|
|
7698
|
+
CURRENCY_SIGNAL = /\$\.?\d|\d\s?\$/;
|
|
7699
|
+
UNESCAPED_DOLLAR = /(?<!\\)\$/g;
|
|
7700
|
+
});
|
|
7701
|
+
|
|
7702
|
+
// render/emphasis-guard.ts
|
|
7703
|
+
function guardAccidentalEmphasis(text) {
|
|
7704
|
+
if (!text.includes("_") && !text.includes("*"))
|
|
7705
|
+
return text;
|
|
7706
|
+
const segments = splitProtectedSegments(text);
|
|
7707
|
+
let hasIntraUnderscore = false;
|
|
7708
|
+
let hasIntraAsterisk = false;
|
|
7709
|
+
let underscoreCount = 0;
|
|
7710
|
+
let asteriskCount = 0;
|
|
7711
|
+
for (const seg of segments) {
|
|
7712
|
+
if (seg.code)
|
|
7713
|
+
continue;
|
|
7714
|
+
if (INTRA_WORD_UNDERSCORE.test(seg.text))
|
|
7715
|
+
hasIntraUnderscore = true;
|
|
7716
|
+
if (INTRA_WORD_ASTERISK.test(seg.text))
|
|
7717
|
+
hasIntraAsterisk = true;
|
|
7718
|
+
underscoreCount += seg.text.match(ANY_UNDERSCORE)?.length ?? 0;
|
|
7719
|
+
asteriskCount += seg.text.match(ANY_ASTERISK)?.length ?? 0;
|
|
7720
|
+
}
|
|
7721
|
+
INTRA_WORD_UNDERSCORE.lastIndex = 0;
|
|
7722
|
+
INTRA_WORD_ASTERISK.lastIndex = 0;
|
|
7723
|
+
const armUnderscore = hasIntraUnderscore && underscoreCount >= 2;
|
|
7724
|
+
const armAsterisk = hasIntraAsterisk && asteriskCount >= 2;
|
|
7725
|
+
if (!armUnderscore && !armAsterisk)
|
|
7726
|
+
return text;
|
|
7727
|
+
return segments.map((seg) => {
|
|
7728
|
+
if (seg.code)
|
|
7729
|
+
return seg.text;
|
|
7730
|
+
let out = seg.text;
|
|
7731
|
+
if (armUnderscore)
|
|
7732
|
+
out = out.replace(INTRA_WORD_UNDERSCORE, "\\_");
|
|
7733
|
+
if (armAsterisk)
|
|
7734
|
+
out = out.replace(INTRA_WORD_ASTERISK, "\\*");
|
|
7735
|
+
return out;
|
|
7736
|
+
}).join("");
|
|
7737
|
+
}
|
|
7738
|
+
var INTRA_WORD_UNDERSCORE, INTRA_WORD_ASTERISK, ANY_UNDERSCORE, ANY_ASTERISK;
|
|
7739
|
+
var init_emphasis_guard = __esm(() => {
|
|
7740
|
+
INTRA_WORD_UNDERSCORE = /(?<=[A-Za-z0-9])_(?=[A-Za-z0-9])/g;
|
|
7741
|
+
INTRA_WORD_ASTERISK = /(?<=[A-Za-z0-9])\*(?=[A-Za-z0-9])/g;
|
|
7742
|
+
ANY_UNDERSCORE = /(?<!\\)_/g;
|
|
7743
|
+
ANY_ASTERISK = /(?<!\\)\*/g;
|
|
7744
|
+
});
|
|
7745
|
+
|
|
7746
|
+
// render/line-start-guard.ts
|
|
7747
|
+
function escapeAccidentalLineStart(line) {
|
|
7748
|
+
const indent = /^ */.exec(line)[0];
|
|
7749
|
+
if (indent.length >= 4)
|
|
7750
|
+
return line;
|
|
7751
|
+
const rest = line.slice(indent.length);
|
|
7752
|
+
if (ACCIDENTAL_BLOCKQUOTE.test(rest)) {
|
|
7753
|
+
return indent + "\\" + rest;
|
|
7754
|
+
}
|
|
7755
|
+
const ol = ACCIDENTAL_ORDERED_LIST.exec(rest);
|
|
7756
|
+
if (ol) {
|
|
7757
|
+
const digits = ol[1];
|
|
7758
|
+
const delim = ol[2];
|
|
7759
|
+
return indent + digits + "\\" + delim + rest.slice(digits.length + 1);
|
|
7760
|
+
}
|
|
7761
|
+
return line;
|
|
7762
|
+
}
|
|
7763
|
+
function guardAccidentalBlockConstructs(text) {
|
|
7764
|
+
if (!text.includes(">") && !/\d{4,}[.)]/.test(text))
|
|
7765
|
+
return text;
|
|
7766
|
+
const segments = splitProtectedSegments(text);
|
|
7767
|
+
let out = "";
|
|
7768
|
+
let atLineStart = true;
|
|
7769
|
+
for (const seg of segments) {
|
|
7770
|
+
if (seg.code) {
|
|
7771
|
+
out += seg.text;
|
|
7772
|
+
atLineStart = seg.text.endsWith(`
|
|
7773
|
+
`);
|
|
7774
|
+
continue;
|
|
7775
|
+
}
|
|
7776
|
+
const lines = seg.text.split(`
|
|
7777
|
+
`);
|
|
7778
|
+
for (let k = 0;k < lines.length; k++) {
|
|
7779
|
+
const lineIsAtStart = k === 0 ? atLineStart : true;
|
|
7780
|
+
const processed = lineIsAtStart ? escapeAccidentalLineStart(lines[k]) : lines[k];
|
|
7781
|
+
out += processed;
|
|
7782
|
+
if (k < lines.length - 1)
|
|
7783
|
+
out += `
|
|
7784
|
+
`;
|
|
7785
|
+
}
|
|
7786
|
+
atLineStart = seg.text.endsWith(`
|
|
7787
|
+
`);
|
|
7788
|
+
}
|
|
7789
|
+
return out;
|
|
7790
|
+
}
|
|
7791
|
+
var ACCIDENTAL_BLOCKQUOTE, ACCIDENTAL_ORDERED_LIST;
|
|
7792
|
+
var init_line_start_guard = __esm(() => {
|
|
7793
|
+
ACCIDENTAL_BLOCKQUOTE = /^>[0-9=]/;
|
|
7794
|
+
ACCIDENTAL_ORDERED_LIST = /^(\d{4,})([.)])(\s|$)/;
|
|
7795
|
+
});
|
|
7796
|
+
|
|
7797
|
+
// render/inline-pairs-guard.ts
|
|
7798
|
+
function countMatches(text, re) {
|
|
7799
|
+
return text.match(re)?.length ?? 0;
|
|
7800
|
+
}
|
|
7801
|
+
function guardAccidentalInlinePairs(text) {
|
|
7802
|
+
if (!/[~=|]/.test(text))
|
|
7803
|
+
return text;
|
|
7804
|
+
const segments = splitProtectedSegments(text);
|
|
7805
|
+
let tildes = 0;
|
|
7806
|
+
let marks = 0;
|
|
7807
|
+
let spoilers = 0;
|
|
7808
|
+
for (const seg of segments) {
|
|
7809
|
+
if (seg.code)
|
|
7810
|
+
continue;
|
|
7811
|
+
tildes += countMatches(seg.text, TILDE_APPROX);
|
|
7812
|
+
marks += countMatches(seg.text, MARK_OP);
|
|
7813
|
+
spoilers += countMatches(seg.text, SPOILER_OP);
|
|
7814
|
+
}
|
|
7815
|
+
const armTilde = tildes >= 2;
|
|
7816
|
+
const armMark = marks >= 2;
|
|
7817
|
+
const armSpoiler = spoilers >= 2;
|
|
7818
|
+
if (!armTilde && !armMark && !armSpoiler)
|
|
7819
|
+
return text;
|
|
7820
|
+
return segments.map((seg) => {
|
|
7821
|
+
if (seg.code)
|
|
7822
|
+
return seg.text;
|
|
7823
|
+
let out = seg.text;
|
|
7824
|
+
if (armTilde)
|
|
7825
|
+
out = out.replace(TILDE_APPROX, () => "\\~");
|
|
7826
|
+
if (armMark)
|
|
7827
|
+
out = out.replace(MARK_OP, () => "\\=\\=");
|
|
7828
|
+
if (armSpoiler)
|
|
7829
|
+
out = out.replace(SPOILER_OP, () => "\\|\\|");
|
|
7830
|
+
return out;
|
|
7831
|
+
}).join("");
|
|
7832
|
+
}
|
|
7833
|
+
var TILDE_APPROX, MARK_OP, SPOILER_OP;
|
|
7834
|
+
var init_inline_pairs_guard = __esm(() => {
|
|
7835
|
+
TILDE_APPROX = /(?<!\\)~(?=\$?\.?\d)/g;
|
|
7836
|
+
MARK_OP = /(?<=\w)==(?=\w)/g;
|
|
7837
|
+
SPOILER_OP = /(?<=\w)\|\|(?=\w)/g;
|
|
7838
|
+
});
|
|
7839
|
+
|
|
7528
7840
|
// rich-send.ts
|
|
7841
|
+
function guardAccidentalFormatting(markdown) {
|
|
7842
|
+
let out = markdown;
|
|
7843
|
+
out = guardAccidentalEmphasis(out);
|
|
7844
|
+
out = guardAccidentalBlockConstructs(out);
|
|
7845
|
+
out = guardAccidentalInlinePairs(out);
|
|
7846
|
+
out = guardDollarMath(out);
|
|
7847
|
+
return out;
|
|
7848
|
+
}
|
|
7529
7849
|
function richMessage(markdown) {
|
|
7530
|
-
return { markdown };
|
|
7850
|
+
return { markdown: guardAccidentalFormatting(markdown) };
|
|
7531
7851
|
}
|
|
7532
7852
|
function isParseEntitiesError(err) {
|
|
7533
7853
|
if (!(err instanceof import_grammy4.GrammyError) || err.error_code !== 400)
|
|
@@ -7545,6 +7865,10 @@ function isLengthError(err) {
|
|
|
7545
7865
|
}
|
|
7546
7866
|
var import_grammy4;
|
|
7547
7867
|
var init_rich_send = __esm(() => {
|
|
7868
|
+
init_dollar_math_guard();
|
|
7869
|
+
init_emphasis_guard();
|
|
7870
|
+
init_line_start_guard();
|
|
7871
|
+
init_inline_pairs_guard();
|
|
7548
7872
|
import_grammy4 = __toESM(require_mod2(), 1);
|
|
7549
7873
|
});
|
|
7550
7874
|
|
|
@@ -67710,9 +68034,21 @@ function backOffOpenInline2(text4, cut) {
|
|
|
67710
68034
|
}
|
|
67711
68035
|
|
|
67712
68036
|
// rich-send.ts
|
|
68037
|
+
init_dollar_math_guard();
|
|
68038
|
+
init_emphasis_guard();
|
|
68039
|
+
init_line_start_guard();
|
|
68040
|
+
init_inline_pairs_guard();
|
|
67713
68041
|
var import_grammy8 = __toESM(require_mod2(), 1);
|
|
68042
|
+
function guardAccidentalFormatting2(markdown) {
|
|
68043
|
+
let out = markdown;
|
|
68044
|
+
out = guardAccidentalEmphasis(out);
|
|
68045
|
+
out = guardAccidentalBlockConstructs(out);
|
|
68046
|
+
out = guardAccidentalInlinePairs(out);
|
|
68047
|
+
out = guardDollarMath(out);
|
|
68048
|
+
return out;
|
|
68049
|
+
}
|
|
67714
68050
|
function richMessage2(markdown) {
|
|
67715
|
-
return { markdown };
|
|
68051
|
+
return { markdown: guardAccidentalFormatting2(markdown) };
|
|
67716
68052
|
}
|
|
67717
68053
|
|
|
67718
68054
|
// gateway/redelivery-decision.ts
|
|
@@ -83460,10 +83796,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
83460
83796
|
}
|
|
83461
83797
|
|
|
83462
83798
|
// ../src/build-info.ts
|
|
83463
|
-
var VERSION = "0.18.
|
|
83464
|
-
var COMMIT_SHA = "
|
|
83465
|
-
var COMMIT_DATE = "2026-07-
|
|
83466
|
-
var LATEST_PR =
|
|
83799
|
+
var VERSION = "0.18.26";
|
|
83800
|
+
var COMMIT_SHA = "ceb7d1a9";
|
|
83801
|
+
var COMMIT_DATE = "2026-07-15T11:05:52Z";
|
|
83802
|
+
var LATEST_PR = 3258;
|
|
83467
83803
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
83468
83804
|
|
|
83469
83805
|
// gateway/boot-version.ts
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Shared CommonMark-correct code-span / fenced-block splitter for the outbound
|
|
2
|
+
// formatting guards (#3252). Every guard that neutralises accidental Telegram
|
|
3
|
+
// markdown typesetting (dollar-math, emphasis, line-start block constructs,
|
|
4
|
+
// inline pairs) must skip content inside code spans and fenced code blocks
|
|
5
|
+
// verbatim. Rather than each guard forking its own copy of the splitter (the
|
|
6
|
+
// dollar guard shipped one, the emphasis + line-start guards each duplicated
|
|
7
|
+
// it), this is the ONE source of truth they all import.
|
|
8
|
+
|
|
9
|
+
/** A contiguous slice of rendered markdown, tagged as code (verbatim, never
|
|
10
|
+
* transformed) or prose (eligible for a guard's rewrite). */
|
|
11
|
+
export interface Segment {
|
|
12
|
+
code: boolean;
|
|
13
|
+
text: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** From index `from` (just past an opening run of `runLen` backticks), find the
|
|
17
|
+
* index immediately AFTER the matching closing run of exactly `runLen`
|
|
18
|
+
* backticks. Returns -1 when there is no matching close (a stray backtick), in
|
|
19
|
+
* which case the opener is treated as literal prose. Equal-length matching is
|
|
20
|
+
* the CommonMark rule for code spans and matches the balanced fences the
|
|
21
|
+
* renderer emits for code blocks. */
|
|
22
|
+
export function findClosingBackticks(text: string, from: number, runLen: number): number {
|
|
23
|
+
let i = from;
|
|
24
|
+
while (i < text.length) {
|
|
25
|
+
if (text[i] === "`") {
|
|
26
|
+
let j = i;
|
|
27
|
+
while (j < text.length && text[j] === "`") j++;
|
|
28
|
+
if (j - i === runLen) return j;
|
|
29
|
+
i = j;
|
|
30
|
+
} else {
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return -1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Split rendered markdown into alternating prose / code segments so a guard can
|
|
38
|
+
* skip code spans and fenced code blocks entirely. */
|
|
39
|
+
export function splitCodeSegments(text: string): Segment[] {
|
|
40
|
+
const out: Segment[] = [];
|
|
41
|
+
let i = 0;
|
|
42
|
+
let plainStart = 0;
|
|
43
|
+
while (i < text.length) {
|
|
44
|
+
if (text[i] === "`") {
|
|
45
|
+
let j = i;
|
|
46
|
+
while (j < text.length && text[j] === "`") j++;
|
|
47
|
+
const runLen = j - i;
|
|
48
|
+
const close = findClosingBackticks(text, j, runLen);
|
|
49
|
+
if (close !== -1) {
|
|
50
|
+
if (plainStart < i) out.push({ code: false, text: text.slice(plainStart, i) });
|
|
51
|
+
out.push({ code: true, text: text.slice(i, close) });
|
|
52
|
+
i = close;
|
|
53
|
+
plainStart = close;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
i++;
|
|
58
|
+
}
|
|
59
|
+
if (plainStart < text.length) out.push({ code: false, text: text.slice(plainStart) });
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Link / autolink / table protection (#3252 — link-aware guards)
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
//
|
|
67
|
+
// The four accidental-formatting guards must NOT escape inside content where
|
|
68
|
+
// the trigger chars (`_ * ~ = | $`) are STRUCTURAL rather than prose:
|
|
69
|
+
// • markdown link destinations — the `(url)` (and `(url "title")`) target of
|
|
70
|
+
// a `[label](url)` link. Underscores / tildes / `==` in a URL path or query
|
|
71
|
+
// are part of the address; a backslash there can 404 the link. The link
|
|
72
|
+
// LABEL (`[...]`) is rendered prose and stays guarded.
|
|
73
|
+
// • bare autolinked URLs — `http(s)://…` and `www.…` runs Telegram auto-links.
|
|
74
|
+
// • GFM table rows — a table's structural pipes / empty cells (`|a||b|`) must
|
|
75
|
+
// survive; escaping inside a real table row corrupts the table.
|
|
76
|
+
// Everything else is prose and stays fully guarded. This is the sibling of the
|
|
77
|
+
// code-span skip: a PROTECTED segment (`code: true`) is emitted verbatim.
|
|
78
|
+
//
|
|
79
|
+
// Deterministic and linear-time: one left-to-right char scan per prose chunk
|
|
80
|
+
// plus one line pre-pass for tables; no backtracking regexes.
|
|
81
|
+
|
|
82
|
+
/** A GFM table delimiter row: only pipes / colons / dashes / spaces, with at
|
|
83
|
+
* least one dash AND at least one pipe (a bare `---` is a thematic break, not a
|
|
84
|
+
* table). This is what distinguishes a real table from prose that merely starts
|
|
85
|
+
* with `|`. */
|
|
86
|
+
function isTableDelimiterRow(line: string): boolean {
|
|
87
|
+
const t = line.trim();
|
|
88
|
+
return t.length > 0 && /^[\s|:-]+$/.test(t) && t.includes("-") && t.includes("|");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A candidate table line: after optional leading spaces it begins with `|`. */
|
|
92
|
+
function isTableCandidateLine(line: string): boolean {
|
|
93
|
+
return /^\s*\|/.test(line);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Find the [start, end) char ranges (relative to `text`) of GFM table blocks —
|
|
97
|
+
* maximal runs of 2+ consecutive `|`-leading lines that contain a delimiter
|
|
98
|
+
* row. Each returned range spans whole lines INCLUDING their trailing newline,
|
|
99
|
+
* so a table becomes ONE protected segment. A lone `|a||b|` line with no
|
|
100
|
+
* delimiter row is NOT a real table → not protected → still guarded. */
|
|
101
|
+
function findTableRanges(text: string): Array<[number, number]> {
|
|
102
|
+
const ranges: Array<[number, number]> = [];
|
|
103
|
+
const lines = text.split("\n");
|
|
104
|
+
let offset = 0;
|
|
105
|
+
let runStart = -1;
|
|
106
|
+
let runEnd = -1;
|
|
107
|
+
let runHasDelim = false;
|
|
108
|
+
let runLineCount = 0;
|
|
109
|
+
const flush = () => {
|
|
110
|
+
if (runStart !== -1 && runLineCount >= 2 && runHasDelim) {
|
|
111
|
+
ranges.push([runStart, runEnd]);
|
|
112
|
+
}
|
|
113
|
+
runStart = -1;
|
|
114
|
+
runEnd = -1;
|
|
115
|
+
runHasDelim = false;
|
|
116
|
+
runLineCount = 0;
|
|
117
|
+
};
|
|
118
|
+
for (let k = 0; k < lines.length; k++) {
|
|
119
|
+
const line = lines[k];
|
|
120
|
+
// char length of this line including its trailing newline (except the last).
|
|
121
|
+
const lineLen = line.length + (k < lines.length - 1 ? 1 : 0);
|
|
122
|
+
if (isTableCandidateLine(line)) {
|
|
123
|
+
if (runStart === -1) runStart = offset;
|
|
124
|
+
runEnd = offset + lineLen;
|
|
125
|
+
runLineCount += 1;
|
|
126
|
+
if (isTableDelimiterRow(line)) runHasDelim = true;
|
|
127
|
+
} else {
|
|
128
|
+
flush();
|
|
129
|
+
}
|
|
130
|
+
offset += lineLen;
|
|
131
|
+
}
|
|
132
|
+
flush();
|
|
133
|
+
return ranges;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Protect links / autolinks / table rows within a single PROSE chunk, returning
|
|
137
|
+
* alternating prose / protected segments. */
|
|
138
|
+
function splitProseProtected(text: string): Segment[] {
|
|
139
|
+
const out: Segment[] = [];
|
|
140
|
+
const tables = findTableRanges(text);
|
|
141
|
+
let tIdx = 0;
|
|
142
|
+
let i = 0;
|
|
143
|
+
let plainStart = 0;
|
|
144
|
+
const pushProtected = (from: number, to: number) => {
|
|
145
|
+
if (plainStart < from) out.push({ code: false, text: text.slice(plainStart, from) });
|
|
146
|
+
out.push({ code: true, text: text.slice(from, to) });
|
|
147
|
+
plainStart = to;
|
|
148
|
+
};
|
|
149
|
+
while (i < text.length) {
|
|
150
|
+
// Advance the table pointer past any range we've already scanned past.
|
|
151
|
+
while (tIdx < tables.length && tables[tIdx][1] <= i) tIdx++;
|
|
152
|
+
// 1. Table block — protect whole lines verbatim.
|
|
153
|
+
if (tIdx < tables.length && tables[tIdx][0] === i) {
|
|
154
|
+
const [, end] = tables[tIdx];
|
|
155
|
+
pushProtected(i, end);
|
|
156
|
+
i = end;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const ch = text[i];
|
|
160
|
+
// 2. Markdown link destination: `[label](dest)` — protect only `(dest)`.
|
|
161
|
+
if (ch === "[") {
|
|
162
|
+
const close = text.indexOf("]", i + 1);
|
|
163
|
+
if (close !== -1 && text[close + 1] === "(") {
|
|
164
|
+
const destClose = text.indexOf(")", close + 2);
|
|
165
|
+
if (destClose !== -1) {
|
|
166
|
+
// Protect the `(dest)` span; the `[label]` before it stays prose.
|
|
167
|
+
pushProtected(close + 1, destClose + 1);
|
|
168
|
+
i = destClose + 1;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
i++;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
// 3. Bare autolink: `http(s)://…` or `www.…`, left-flanked by a boundary.
|
|
176
|
+
if (
|
|
177
|
+
(ch === "h" || ch === "w") &&
|
|
178
|
+
(i === 0 || !/[A-Za-z0-9]/.test(text[i - 1]))
|
|
179
|
+
) {
|
|
180
|
+
const m = /^(?:https?:\/\/|www\.)[^\s<>()\[\]]+/.exec(text.slice(i));
|
|
181
|
+
if (m) {
|
|
182
|
+
const end = i + m[0].length;
|
|
183
|
+
pushProtected(i, end);
|
|
184
|
+
i = end;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
i++;
|
|
189
|
+
}
|
|
190
|
+
if (plainStart < text.length) out.push({ code: false, text: text.slice(plainStart) });
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Split rendered markdown into prose / protected segments where a PROTECTED
|
|
195
|
+
* (`code: true`) segment is any content a guard must emit verbatim: code spans,
|
|
196
|
+
* fenced code blocks, markdown link destinations, bare autolinks, and GFM
|
|
197
|
+
* table rows. This is the link/table-aware superset of `splitCodeSegments` that
|
|
198
|
+
* all four #3252 guards route through. Prose segments (`code: false`) remain
|
|
199
|
+
* guardable (including a link's `[label]` text). Deterministic, linear-time. */
|
|
200
|
+
export function splitProtectedSegments(text: string): Segment[] {
|
|
201
|
+
const out: Segment[] = [];
|
|
202
|
+
for (const seg of splitCodeSegments(text)) {
|
|
203
|
+
if (seg.code) {
|
|
204
|
+
out.push(seg);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
for (const sub of splitProseProtected(seg.text)) out.push(sub);
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|