@mrciphersmith/keryx 0.3.63 → 0.3.64

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 (4) hide show
  1. package/README.md +10 -2
  2. package/dist/cli.js +1761 -394
  3. package/dist/core.js +12 -0
  4. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -20107,7 +20107,7 @@ var package_default;
20107
20107
  var init_package = __esm(() => {
20108
20108
  package_default = {
20109
20109
  name: "@mrciphersmith/keryx",
20110
- version: "0.3.63",
20110
+ version: "0.3.64",
20111
20111
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
20112
20112
  private: false,
20113
20113
  publishConfig: {
@@ -30912,6 +30912,9 @@ function summarizeSubmittedLine(line) {
30912
30912
  function isZeroWidth(cp) {
30913
30913
  return cp >= 768 && cp <= 879 || cp >= 8203 && cp <= 8207 || cp >= 65024 && cp <= 65039 || cp >= 8288 && cp <= 8292;
30914
30914
  }
30915
+ function isEmojiModifier(cp) {
30916
+ return cp >= 127995 && cp <= 127999;
30917
+ }
30915
30918
  function codePointWidth(cp) {
30916
30919
  for (const [lo, hi] of WIDE_RANGES) {
30917
30920
  if (cp >= lo && cp <= hi) {
@@ -30922,11 +30925,17 @@ function codePointWidth(cp) {
30922
30925
  }
30923
30926
  function visualWidth(text) {
30924
30927
  let width = 0;
30928
+ let previous = 0;
30925
30929
  for (const ch of text) {
30926
30930
  const codePoint = ch.codePointAt(0) ?? 0;
30931
+ const joined = previous === ZERO_WIDTH_JOINER;
30932
+ previous = codePoint;
30927
30933
  if (isZeroWidth(codePoint)) {
30928
30934
  continue;
30929
30935
  }
30936
+ if (joined && codePointWidth(codePoint) === 2 || isEmojiModifier(codePoint)) {
30937
+ continue;
30938
+ }
30930
30939
  width += codePointWidth(codePoint);
30931
30940
  }
30932
30941
  return width;
@@ -30939,7 +30948,7 @@ function hugWidth(text, chrome) {
30939
30948
  }
30940
30949
  return widest + chrome;
30941
30950
  }
30942
- var FENCE_LINE, HUNK_HEADER, MARKDOWN_LANGS, DIFF_LANGS, CODE_LANG_ALIASES, GENERIC_KEYWORDS, CODE_LANG_KEYWORDS, CODE_WORD_OR_NUMBER, WIDE_RANGES;
30951
+ var FENCE_LINE, HUNK_HEADER, MARKDOWN_LANGS, DIFF_LANGS, CODE_LANG_ALIASES, GENERIC_KEYWORDS, CODE_LANG_KEYWORDS, CODE_WORD_OR_NUMBER, WIDE_RANGES, ZERO_WIDTH_JOINER = 8205;
30943
30952
  var init_md_blocks = __esm(() => {
30944
30953
  FENCE_LINE = /^[ \t]{0,3}(```|~~~)(.*)\r?$/;
30945
30954
  HUNK_HEADER = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/;
@@ -31187,8 +31196,48 @@ var init_md_blocks = __esm(() => {
31187
31196
  [65072, 65135],
31188
31197
  [65280, 65376],
31189
31198
  [65504, 65510],
31199
+ [8986, 8987],
31200
+ [9193, 9196],
31201
+ [9200, 9200],
31202
+ [9203, 9203],
31203
+ [9725, 9726],
31204
+ [9748, 9749],
31205
+ [9800, 9811],
31206
+ [9855, 9855],
31207
+ [9875, 9875],
31208
+ [9889, 9889],
31209
+ [9898, 9899],
31210
+ [9917, 9918],
31211
+ [9924, 9925],
31212
+ [9934, 9934],
31213
+ [9940, 9940],
31214
+ [9962, 9962],
31215
+ [9970, 9971],
31216
+ [9973, 9973],
31217
+ [9978, 9978],
31218
+ [9981, 9981],
31219
+ [9989, 9989],
31220
+ [9994, 9995],
31221
+ [10024, 10024],
31222
+ [10060, 10060],
31223
+ [10062, 10062],
31224
+ [10067, 10069],
31225
+ [10071, 10071],
31226
+ [10133, 10135],
31227
+ [10160, 10160],
31228
+ [10175, 10175],
31229
+ [11035, 11036],
31230
+ [11088, 11088],
31231
+ [11093, 11093],
31232
+ [126980, 126980],
31233
+ [127183, 127183],
31234
+ [127374, 127374],
31235
+ [127377, 127386],
31236
+ [127488, 127743],
31190
31237
  [127744, 128591],
31238
+ [128640, 128767],
31191
31239
  [129280, 129535],
31240
+ [129536, 129791],
31192
31241
  [131072, 262141]
31193
31242
  ];
31194
31243
  });
@@ -100461,6 +100510,264 @@ var init_live_render = __esm(() => {
100461
100510
  ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
100462
100511
  });
100463
100512
 
100513
+ // src/remote/paths.ts
100514
+ import path228 from "path";
100515
+ function remoteDirPath(dir) {
100516
+ return path228.join(keryxConfigDir(dir), REMOTE_DIRNAME);
100517
+ }
100518
+ function ensureRemoteDir(dir, ...sub) {
100519
+ return ensureKeryxSubdir([REMOTE_DIRNAME, ...sub], dir);
100520
+ }
100521
+ function botTokenPath(dir) {
100522
+ return path228.join(remoteDirPath(dir), BOT_TOKEN_FILE);
100523
+ }
100524
+ function remoteConfigPath(dir) {
100525
+ return path228.join(remoteDirPath(dir), REMOTE_CONFIG_FILE);
100526
+ }
100527
+ var REMOTE_DIRNAME = "remote", BOT_TOKEN_FILE = "bot-token", REMOTE_CONFIG_FILE = "config.json", REGISTRY_FILE = "sessions.json", SERVICE_TOPICS_FILE = "service-topics.json", POLLER_STATE_FILE = "poller-state.json", OUTBOUND_FILE = "outbound.jsonl", REJECTED_JOURNAL_FILE = "rejected.jsonl", INBOUND_DIRNAME = "inbound";
100528
+ var init_paths4 = __esm(() => {
100529
+ init_config_dir();
100530
+ });
100531
+
100532
+ // src/remote/rendering-mode.ts
100533
+ function isRenderMode(value) {
100534
+ return typeof value === "string" && RENDER_MODES.includes(value);
100535
+ }
100536
+ var RENDER_MODES, DEFAULT_RENDER_MODE = "auto", RENDER_MODE_CHOICES;
100537
+ var init_rendering_mode = __esm(() => {
100538
+ RENDER_MODES = ["auto", "rich", "html", "plain"];
100539
+ RENDER_MODE_CHOICES = `one of ${RENDER_MODES.join(", ")}`;
100540
+ });
100541
+
100542
+ // src/remote/config.ts
100543
+ import { statSync as statSync14, unlinkSync as unlinkSync4 } from "fs";
100544
+ function isSafeInt(value) {
100545
+ return typeof value === "number" && Number.isSafeInteger(value);
100546
+ }
100547
+ function parseRemoteConfig(value) {
100548
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
100549
+ return { ok: false, reason: "remote config must be a JSON object" };
100550
+ }
100551
+ const doc = value;
100552
+ for (const key of Object.keys(doc)) {
100553
+ if (!CONFIG_KEYS.has(key)) {
100554
+ const shown = /^[A-Za-z_][A-Za-z0-9_]{0,30}$/.test(key) ? key : "<not shown>";
100555
+ return { ok: false, reason: `remote config has an unknown key "${shown}" (the schema is closed)` };
100556
+ }
100557
+ }
100558
+ if (doc.schemaVersion !== REMOTE_CONFIG_SCHEMA_VERSION) {
100559
+ return { ok: false, reason: `remote config schemaVersion must be ${REMOTE_CONFIG_SCHEMA_VERSION}` };
100560
+ }
100561
+ if (!isSafeInt(doc.chatId) || doc.chatId === 0) {
100562
+ return { ok: false, reason: "remote config chatId must be a non-zero integer (the supergroup id)" };
100563
+ }
100564
+ const ids = doc.allowedUserIds;
100565
+ if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => isSafeInt(id) && id > 0)) {
100566
+ return { ok: false, reason: "remote config allowedUserIds must be a non-empty array of positive integers" };
100567
+ }
100568
+ const timeout = (key, fallback) => {
100569
+ const raw = doc[key];
100570
+ if (raw === undefined) {
100571
+ return { ok: true, value: fallback };
100572
+ }
100573
+ if (!isSafeInt(raw) || raw < 1 || raw > MAX_TIMEOUT_MS3) {
100574
+ return { ok: false, reason: `remote config ${key} must be an integer from 1 to ${MAX_TIMEOUT_MS3} (milliseconds)` };
100575
+ }
100576
+ return { ok: true, value: raw };
100577
+ };
100578
+ const orphan = timeout("orphanMs", DEFAULT_ORPHAN_MS);
100579
+ if (!orphan.ok) {
100580
+ return orphan;
100581
+ }
100582
+ const run = timeout("runTimeoutMs", DEFAULT_RUN_TIMEOUT_MS);
100583
+ if (!run.ok) {
100584
+ return run;
100585
+ }
100586
+ if (doc.rendering !== undefined && !isRenderMode(doc.rendering)) {
100587
+ return { ok: false, reason: `remote config rendering must be ${RENDER_MODE_CHOICES}` };
100588
+ }
100589
+ return {
100590
+ ok: true,
100591
+ value: {
100592
+ schemaVersion: REMOTE_CONFIG_SCHEMA_VERSION,
100593
+ chatId: doc.chatId,
100594
+ allowedUserIds: [...new Set(ids)],
100595
+ orphanMs: orphan.value,
100596
+ runTimeoutMs: run.value,
100597
+ ...doc.rendering === undefined ? {} : { rendering: doc.rendering }
100598
+ }
100599
+ };
100600
+ }
100601
+ function loadRemoteConfig(dir) {
100602
+ const file = remoteConfigPath(dir);
100603
+ const read = readConfigFile(file);
100604
+ if (!read.ok) {
100605
+ return {
100606
+ ok: false,
100607
+ reason: read.reason === "absent" ? `no remote-control config at ${file}; create it with chatId and allowedUserIds` : `remote-control config at ${file} cannot be read (${read.reason})`
100608
+ };
100609
+ }
100610
+ let parsed;
100611
+ try {
100612
+ parsed = JSON.parse(read.text);
100613
+ } catch {
100614
+ return { ok: false, reason: `remote-control config at ${file} is not valid JSON` };
100615
+ }
100616
+ return parseRemoteConfig(parsed);
100617
+ }
100618
+ function saveRemoteConfig(config, dir) {
100619
+ const checked = parseRemoteConfig(config);
100620
+ if (!checked.ok) {
100621
+ return checked;
100622
+ }
100623
+ ensureRemoteDir(dir);
100624
+ writeOwnerOnlyFileAtomic(remoteConfigPath(dir), `${JSON.stringify(checked.value, null, 2)}
100625
+ `);
100626
+ return checked;
100627
+ }
100628
+ function loadBotToken(dir) {
100629
+ const file = botTokenPath(dir);
100630
+ let mode;
100631
+ try {
100632
+ const stats = statSync14(file);
100633
+ if (!stats.isFile()) {
100634
+ return { ok: false, reason: `bot token path ${file} is not a regular file` };
100635
+ }
100636
+ mode = stats.mode & 511;
100637
+ } catch {
100638
+ return {
100639
+ ok: false,
100640
+ reason: `no bot token file at ${file}; put the token on one line in it and run: chmod 600 ${file}`
100641
+ };
100642
+ }
100643
+ if (process.platform !== "win32" && (mode & 63) !== 0) {
100644
+ return {
100645
+ ok: false,
100646
+ reason: `bot token file ${file} is accessible to other users (mode ${mode.toString(8)}); run: chmod 600 ${file}`
100647
+ };
100648
+ }
100649
+ const read = readConfigFile(file);
100650
+ if (!read.ok) {
100651
+ return { ok: false, reason: `bot token file ${file} cannot be read (${read.reason})` };
100652
+ }
100653
+ const token = read.text.trim();
100654
+ if (!TOKEN_SHAPE.test(token)) {
100655
+ return {
100656
+ ok: false,
100657
+ reason: `bot token file ${file} does not hold a bot token (expected <digits>:<secret> on a single line)`
100658
+ };
100659
+ }
100660
+ return { ok: true, value: token };
100661
+ }
100662
+ function saveBotToken(token, dir) {
100663
+ const trimmed = token.trim();
100664
+ if (!TOKEN_SHAPE.test(trimmed)) {
100665
+ return { ok: false, reason: "that does not look like a bot token (expected <digits>:<secret>, as BotFather gives it)" };
100666
+ }
100667
+ const file = botTokenPath(dir);
100668
+ try {
100669
+ ensureRemoteDir(dir);
100670
+ writeOwnerOnlyFileAtomic(file, `${trimmed}
100671
+ `);
100672
+ } catch {
100673
+ return { ok: false, reason: `could not write the bot token file at ${file}` };
100674
+ }
100675
+ return { ok: true, value: true };
100676
+ }
100677
+ function removeFile2(file) {
100678
+ try {
100679
+ unlinkSync4(file);
100680
+ return true;
100681
+ } catch (error) {
100682
+ return error.code === "ENOENT";
100683
+ }
100684
+ }
100685
+ function removeBotToken(dir) {
100686
+ return removeFile2(botTokenPath(dir));
100687
+ }
100688
+ function removeRemoteConfig(dir) {
100689
+ return removeFile2(remoteConfigPath(dir));
100690
+ }
100691
+ var REMOTE_CONFIG_SCHEMA_VERSION = 1, DEFAULT_ORPHAN_MS, DEFAULT_RUN_TIMEOUT_MS, MAX_TIMEOUT_MS3, CONFIG_KEYS, TOKEN_SHAPE;
100692
+ var init_config11 = __esm(() => {
100693
+ init_config_dir();
100694
+ init_paths4();
100695
+ init_rendering_mode();
100696
+ DEFAULT_ORPHAN_MS = 10 * 60000;
100697
+ DEFAULT_RUN_TIMEOUT_MS = 30 * 60000;
100698
+ MAX_TIMEOUT_MS3 = 7 * 24 * 60 * 60000;
100699
+ CONFIG_KEYS = new Set(["schemaVersion", "chatId", "allowedUserIds", "orphanMs", "runTimeoutMs", "rendering"]);
100700
+ TOKEN_SHAPE = /^\d{5,}:[A-Za-z0-9_-]{20,}$/;
100701
+ });
100702
+
100703
+ // src/remote/rendering-config.ts
100704
+ function readRenderingSetting(dir) {
100705
+ const loaded = loadRemoteConfig(dir);
100706
+ if (!loaded.ok) {
100707
+ return { mode: DEFAULT_RENDER_MODE, source: "default", saveable: false };
100708
+ }
100709
+ const mode = loaded.value.rendering;
100710
+ return mode === undefined ? { mode: DEFAULT_RENDER_MODE, source: "default", saveable: true } : { mode, source: "config", saveable: true };
100711
+ }
100712
+ function writeRenderingMode(raw, dir) {
100713
+ const wanted = raw.trim().toLowerCase();
100714
+ if (!isRenderMode(wanted)) {
100715
+ return { ok: false, reason: `rendering must be ${RENDER_MODE_CHOICES}` };
100716
+ }
100717
+ const loaded = loadRemoteConfig(dir);
100718
+ if (!loaded.ok) {
100719
+ return { ok: false, reason: `remote control is not set up on this machine, so the mode cannot be saved yet (${loaded.reason})` };
100720
+ }
100721
+ const saved = saveRemoteConfig({ ...loaded.value, rendering: wanted }, dir);
100722
+ return saved.ok ? { ok: true, value: wanted } : saved;
100723
+ }
100724
+ var init_rendering_config = __esm(() => {
100725
+ init_config11();
100726
+ init_rendering_mode();
100727
+ });
100728
+
100729
+ // src/tui/rendering-command.ts
100730
+ function isRenderingCommand(line) {
100731
+ const token = line.trim().split(/\s+/)[0] ?? "";
100732
+ return token === RENDERING_COMMAND;
100733
+ }
100734
+ function runRenderingCommand(argText, dir) {
100735
+ const arg = argText.trim();
100736
+ try {
100737
+ if (arg.length > 0) {
100738
+ const saved = writeRenderingMode(arg, dir);
100739
+ return saved.ok ? `Telegram rendering: ${saved.value} (${RENDERING_MEANING[saved.value]}). Saved; it applies to the next message.
100740
+ ` : `${RENDERING_COMMAND}: ${saved.reason}
100741
+ `;
100742
+ }
100743
+ const current = readRenderingSetting(dir);
100744
+ const where = current.source === "config" ? "saved in the remote config" : current.saveable ? `default (${DEFAULT_RENDER_MODE})` : "default, remote control is not set up yet";
100745
+ return [
100746
+ `Telegram rendering: ${current.mode} (${where})`,
100747
+ ...RENDER_MODES.map((mode) => ` ${mode === current.mode ? "*" : " "} ${mode}: ${RENDERING_MEANING[mode]}`),
100748
+ RENDERING_FALLBACK_NOTE,
100749
+ `Usage: ${RENDERING_COMMAND} [${RENDER_MODES.join("|")}]`,
100750
+ ""
100751
+ ].join(`
100752
+ `);
100753
+ } catch (error) {
100754
+ return `${RENDERING_COMMAND}: ${error instanceof Error ? error.message : String(error)}
100755
+ `;
100756
+ }
100757
+ }
100758
+ var RENDERING_COMMAND = "/rendering", RENDERING_SUMMARY, RENDERING_MEANING, RENDERING_FALLBACK_NOTE = "Whatever the mode, a refused message is resent once one step down: rich, then HTML, then plain text.";
100759
+ var init_rendering_command = __esm(() => {
100760
+ init_rendering_config();
100761
+ init_rendering_mode();
100762
+ RENDERING_SUMMARY = `How Telegram replies are written: ${RENDERING_COMMAND} [${RENDER_MODES.join("|")}]`;
100763
+ RENDERING_MEANING = {
100764
+ auto: "a reply with a table goes as a rich message, any other reply as HTML",
100765
+ rich: "every reply goes as a rich message",
100766
+ html: "every reply goes as Telegram HTML",
100767
+ plain: "every reply goes as plain text, a table as aligned lines"
100768
+ };
100769
+ });
100770
+
100464
100771
  // src/tui/reasoning-display.ts
100465
100772
  function parseThinkDisplayMode(raw) {
100466
100773
  if (raw === undefined) {
@@ -100520,7 +100827,7 @@ var init_reasoning_display = __esm(() => {
100520
100827
  });
100521
100828
 
100522
100829
  // src/tui/debug-log.ts
100523
- import path228 from "path";
100830
+ import path229 from "path";
100524
100831
  function isDebugEnabled() {
100525
100832
  return activeRun !== undefined;
100526
100833
  }
@@ -100548,11 +100855,11 @@ function startDebugRun(opts = {}) {
100548
100855
  const dir = ensureKeryxSubdir(["debug", name], opts.configDir);
100549
100856
  const run = {
100550
100857
  dir,
100551
- shellLog: path228.join(dir, "shell.ndjson"),
100552
- watcherLog: path228.join(dir, "watcher.ndjson")
100858
+ shellLog: path229.join(dir, "shell.ndjson"),
100859
+ watcherLog: path229.join(dir, "watcher.ndjson")
100553
100860
  };
100554
100861
  try {
100555
- writeOwnerOnlyFile(path228.join(path228.dirname(dir), "latest.txt"), `${dir}
100862
+ writeOwnerOnlyFile(path229.join(path229.dirname(dir), "latest.txt"), `${dir}
100556
100863
  `);
100557
100864
  } catch {}
100558
100865
  activeRun = run;
@@ -100618,7 +100925,7 @@ var init_debug_log = __esm(() => {
100618
100925
 
100619
100926
  // src/tui/theme.ts
100620
100927
  import { existsSync as existsSync39 } from "fs";
100621
- import path229 from "path";
100928
+ import path230 from "path";
100622
100929
  function hexChannels(hex) {
100623
100930
  return [1, 3, 5].map((offset) => Number.parseInt(hex.slice(offset, offset + 2), 16));
100624
100931
  }
@@ -100756,7 +101063,7 @@ function onThemeChange(listener) {
100756
101063
  };
100757
101064
  }
100758
101065
  function tuiConfigPath(dir) {
100759
- return path229.join(keryxConfigDir(dir), "tui.json");
101066
+ return path230.join(keryxConfigDir(dir), "tui.json");
100760
101067
  }
100761
101068
  function loadPersistedThemeId(dir) {
100762
101069
  try {
@@ -101152,6 +101459,17 @@ function buildSettingsRows(state) {
101152
101459
  usage: "/external-agents [on|off]",
101153
101460
  actions: toggleActions("/external-agents", state.externalAgents.on)
101154
101461
  },
101462
+ {
101463
+ id: "rendering",
101464
+ group: "External",
101465
+ label: "Telegram rendering",
101466
+ value: state.rendering.mode,
101467
+ scope: "saved",
101468
+ ...state.rendering.saveable ? {} : { detail: "remote control is not set up yet" },
101469
+ command: "/rendering",
101470
+ usage: `/rendering [${RENDER_MODES.join("|")}]`,
101471
+ actions: RENDER_MODES.map((mode) => action(mode, `/rendering ${mode}`, mode === state.rendering.mode))
101472
+ },
101155
101473
  {
101156
101474
  id: "jevprofile",
101157
101475
  group: "External",
@@ -101190,6 +101508,7 @@ var SETTING_GROUPS, onOff = (value) => value ? "on" : "off", EXTERNAL_PRIVACY_DE
101190
101508
  var init_settings_model = __esm(() => {
101191
101509
  init_agent();
101192
101510
  init_permission_mode();
101511
+ init_rendering_mode();
101193
101512
  init_reasoning_display();
101194
101513
  init_theme();
101195
101514
  SETTING_GROUPS = ["Safety", "Routing", "Display", "External"];
@@ -101309,7 +101628,7 @@ var init_jev_profile = __esm(() => {
101309
101628
 
101310
101629
  // src/commands/review-jev-profile.ts
101311
101630
  import { readFile as readFile106 } from "fs/promises";
101312
- import path230 from "path";
101631
+ import path231 from "path";
101313
101632
  function rejectUnknownJevProfileFlags(args) {
101314
101633
  const unknown = args.filter((arg) => arg.startsWith("--") && !JEV_PROFILE_FLAGS.includes(arg.split("=")[0]));
101315
101634
  if (unknown.length > 0) {
@@ -101318,7 +101637,7 @@ function rejectUnknownJevProfileFlags(args) {
101318
101637
  }
101319
101638
  async function readRawConfig(cwd) {
101320
101639
  try {
101321
- return JSON.parse(await readFile106(path230.join(cwd, REVIEW_GATE_CONFIG_PATH), "utf8"));
101640
+ return JSON.parse(await readFile106(path231.join(cwd, REVIEW_GATE_CONFIG_PATH), "utf8"));
101322
101641
  } catch {
101323
101642
  return;
101324
101643
  }
@@ -101342,7 +101661,7 @@ async function runJevProfile(args) {
101342
101661
  return;
101343
101662
  }
101344
101663
  const merged = mergeRecommendedJevProfile(raw);
101345
- await writeFileAtomic(path230.join(cwd, REVIEW_GATE_CONFIG_PATH), `${JSON.stringify(merged, null, 2)}
101664
+ await writeFileAtomic(path231.join(cwd, REVIEW_GATE_CONFIG_PATH), `${JSON.stringify(merged, null, 2)}
101346
101665
  `);
101347
101666
  const current = currentJevProfile(merged);
101348
101667
  if (args.includes("--json")) {
@@ -101357,13 +101676,13 @@ async function readJevProfileForShell(cwd) {
101357
101676
  async function toggleJevProfileKeyForShell(cwd, key) {
101358
101677
  const raw = await readRawConfig(cwd);
101359
101678
  const merged = toggleJevProfileKey(raw, key);
101360
- await writeFileAtomic(path230.join(cwd, REVIEW_GATE_CONFIG_PATH), `${JSON.stringify(merged, null, 2)}
101679
+ await writeFileAtomic(path231.join(cwd, REVIEW_GATE_CONFIG_PATH), `${JSON.stringify(merged, null, 2)}
101361
101680
  `);
101362
101681
  }
101363
101682
  async function applyRecommendedJevProfileForShell(cwd) {
101364
101683
  const raw = await readRawConfig(cwd);
101365
101684
  const merged = mergeRecommendedJevProfile(raw);
101366
- await writeFileAtomic(path230.join(cwd, REVIEW_GATE_CONFIG_PATH), `${JSON.stringify(merged, null, 2)}
101685
+ await writeFileAtomic(path231.join(cwd, REVIEW_GATE_CONFIG_PATH), `${JSON.stringify(merged, null, 2)}
101367
101686
  `);
101368
101687
  }
101369
101688
  var JEV_PROFILE_FLAGS;
@@ -101385,14 +101704,14 @@ import {
101385
101704
  openSync as openSync4,
101386
101705
  realpathSync as realpathSync16,
101387
101706
  renameSync as renameSync4,
101388
- statSync as statSync14,
101389
- unlinkSync as unlinkSync4,
101707
+ statSync as statSync15,
101708
+ unlinkSync as unlinkSync5,
101390
101709
  writeFileSync as writeFileSync7
101391
101710
  } from "fs";
101392
- import path231 from "path";
101711
+ import path232 from "path";
101393
101712
  import { randomUUID as randomUUID28 } from "crypto";
101394
101713
  function projectRegistryPath(dir) {
101395
- return path231.join(keryxConfigDir(dir), "projects.json");
101714
+ return path232.join(keryxConfigDir(dir), "projects.json");
101396
101715
  }
101397
101716
  function withRegistryLock(dir, fn, onWaiting) {
101398
101717
  return withFileLock2(`${projectRegistryPath(dir)}.lock`, fn, {
@@ -101408,7 +101727,7 @@ function validEntry(candidate) {
101408
101727
  return typeof entry.path === "string" && entry.path.length > 0 && typeof entry.projectId === "string" && entry.projectId.length > 0 && typeof entry.displayName === "string" && (entry.state === "active" || entry.state === "missing") && typeof entry.registeredAt === "string";
101409
101728
  }
101410
101729
  function projectIdentity(projectPath) {
101411
- const absolute = path231.resolve(projectPath);
101730
+ const absolute = path232.resolve(projectPath);
101412
101731
  try {
101413
101732
  return realpathSync16.native(absolute);
101414
101733
  } catch {
@@ -101524,7 +101843,7 @@ function saveProjectRegistry(registry, dir, onWarn) {
101524
101843
  return true;
101525
101844
  } catch {
101526
101845
  try {
101527
- unlinkSync4(temp);
101846
+ unlinkSync5(temp);
101528
101847
  } catch {}
101529
101848
  return false;
101530
101849
  }
@@ -101563,7 +101882,7 @@ function registerProject(projectPath, options = {}) {
101563
101882
  const entry = {
101564
101883
  projectId: randomUUID28(),
101565
101884
  path: absolute,
101566
- displayName: options.displayName ?? path231.basename(absolute),
101885
+ displayName: options.displayName ?? path232.basename(absolute),
101567
101886
  state: "active",
101568
101887
  registeredAt: now,
101569
101888
  lastSeenAt: now
@@ -101600,7 +101919,7 @@ function forgetProject(projectId, dir, onWarn) {
101600
101919
  return outcome ?? "write-failed";
101601
101920
  }
101602
101921
  function isInitializedProject(projectPath) {
101603
- return statSync14(path231.join(projectPath, ".metaproject"), { throwIfNoEntry: false })?.isDirectory() === true;
101922
+ return statSync15(path232.join(projectPath, ".metaproject"), { throwIfNoEntry: false })?.isDirectory() === true;
101604
101923
  }
101605
101924
  function listProjects(dir, onWarn) {
101606
101925
  const registry = loadProjectRegistry(dir, onWarn);
@@ -101654,9 +101973,9 @@ var init_project_registry = __esm(() => {
101654
101973
  });
101655
101974
 
101656
101975
  // src/lib/permission-mode-config.ts
101657
- import path232 from "path";
101976
+ import path233 from "path";
101658
101977
  function permissionModeConfigPath(dir) {
101659
- return path232.join(keryxConfigDir(dir), "permission-mode.json");
101978
+ return path233.join(keryxConfigDir(dir), "permission-mode.json");
101660
101979
  }
101661
101980
  function withRegistryLock2(dir, fn) {
101662
101981
  return withFileLock2(`${permissionModeConfigPath(dir)}.lock`, fn, {
@@ -101763,10 +102082,10 @@ var init_jev_edit_guard = __esm(() => {
101763
102082
 
101764
102083
  // src/review/jev-edit-guard-config.ts
101765
102084
  import { readFile as readFile107 } from "fs/promises";
101766
- import path233 from "path";
102085
+ import path234 from "path";
101767
102086
  async function readReviewJevBlock(cwd) {
101768
102087
  try {
101769
- const parsed = JSON.parse(await readFile107(path233.join(cwd, REVIEW_GATE_CONFIG_PATH), "utf8"));
102088
+ const parsed = JSON.parse(await readFile107(path234.join(cwd, REVIEW_GATE_CONFIG_PATH), "utf8"));
101770
102089
  if (typeof parsed !== "object" || parsed === null)
101771
102090
  return;
101772
102091
  const review = parsed["review"];
@@ -101811,7 +102130,7 @@ async function readJevEditGuardConfig(cwd, opts) {
101811
102130
  return { enabled, threshold, maxCalls };
101812
102131
  }
101813
102132
  async function writeJevEditGuardEnabled(cwd, enabled) {
101814
- const file = path233.join(cwd, REVIEW_GATE_CONFIG_PATH);
102133
+ const file = path234.join(cwd, REVIEW_GATE_CONFIG_PATH);
101815
102134
  let root = {};
101816
102135
  if (await pathExists(file)) {
101817
102136
  try {
@@ -101845,6 +102164,14 @@ function readReasoning2(sessionOverride, globalEffort) {
101845
102164
  const afterRestart = describeReasoningEffortSource({ globalEffort });
101846
102165
  return resolved.source === "session" && afterRestart.source === "env" ? { ...resolved, envWinsOnRestart: true } : resolved;
101847
102166
  }
102167
+ function readRendering(dir) {
102168
+ try {
102169
+ const { mode, saveable } = readRenderingSetting(dir);
102170
+ return { mode, saveable };
102171
+ } catch {
102172
+ return { mode: DEFAULT_RENDER_MODE, saveable: false };
102173
+ }
102174
+ }
101848
102175
  async function orElse(read, fallback) {
101849
102176
  try {
101850
102177
  return await read();
@@ -101876,7 +102203,8 @@ async function loadSettingsSnapshot(cwd, live, configDir) {
101876
102203
  on: RECOMMENDED_JEV_PROFILE.filter((entry) => jevProfile[entry.key] === true).length,
101877
102204
  total: RECOMMENDED_JEV_PROFILE.length
101878
102205
  },
101879
- externalAgents: agents.ok ? { on: true } : { on: false, reason: agents.reason }
102206
+ externalAgents: agents.ok ? { on: true } : { on: false, reason: agents.reason },
102207
+ rendering: readRendering(configDir)
101880
102208
  };
101881
102209
  }
101882
102210
  var init_settings_state = __esm(() => {
@@ -101886,6 +102214,8 @@ var init_settings_state = __esm(() => {
101886
102214
  init_external_switch();
101887
102215
  init_permission_mode_config();
101888
102216
  init_shell_config();
102217
+ init_rendering_config();
102218
+ init_rendering_mode();
101889
102219
  init_jev_edit_guard_config();
101890
102220
  init_jev_profile();
101891
102221
  init_reasoning_display();
@@ -101941,7 +102271,7 @@ class NextStepSuggestionGate {
101941
102271
  var NEXT_STEP_SUGGESTION_MAX_LENGTH = 80, NEXT_STEP_SUGGESTION_MAX_WORDS = 20, NEXT_STEP_SUGGESTION_TIMEOUT_MS = 8000, NEXT_STEP_USER_TAIL = 800, NEXT_STEP_ASSISTANT_TAIL = 3000, NEXT_STEP_SYSTEM_PROMPT = "You are the next-step advisor of a coding assistant terminal. Based on the user's last request and the assistant's final reply, propose ONE short follow-up the user could do next: imperative, no quotes, no markdown, at most 80 characters. If nothing useful exists, reply with exactly one dot: .";
101942
102272
 
101943
102273
  // src/lib/version-check.ts
101944
- import path234 from "path";
102274
+ import path235 from "path";
101945
102275
  function formatVersionUpdateAdvisory(result) {
101946
102276
  if (result.status !== "update-available")
101947
102277
  return;
@@ -102128,7 +102458,7 @@ async function checkVersion(options) {
102128
102458
  const now = options.now ?? Date.now;
102129
102459
  const timestamp = now();
102130
102460
  const configDir = ensureKeryxConfigDir(options.cacheDir);
102131
- const cacheFile = path234.join(configDir, "version-check.json");
102461
+ const cacheFile = path235.join(configDir, "version-check.json");
102132
102462
  const cache = parseCache(cacheFile);
102133
102463
  if (cache?.latestVersion !== undefined && cache.successAt !== undefined && timestamp - cache.successAt >= 0 && timestamp - cache.successAt < SUCCESS_CACHE_TTL_MS) {
102134
102464
  return resultFor(options.currentVersion, current, cache.latestVersion, "cache");
@@ -102456,7 +102786,7 @@ var init_schemas2 = __esm(() => {
102456
102786
  var init_types7 = () => {};
102457
102787
 
102458
102788
  // src/standard/profiles.ts
102459
- import path235 from "path";
102789
+ import path236 from "path";
102460
102790
  import { readFile as readFile108 } from "fs/promises";
102461
102791
  function namedEntrypoints(manifest) {
102462
102792
  const agentEntrypoints = manifest.agentEntrypoints;
@@ -102494,7 +102824,7 @@ function enabledModuleKeys(manifest) {
102494
102824
  }
102495
102825
  async function rootEntrypointLinksIndex(cwd, manifest) {
102496
102826
  for (const { path: name } of namedEntrypoints(manifest)) {
102497
- const filePath = path235.join(cwd, name);
102827
+ const filePath = path236.join(cwd, name);
102498
102828
  if (!await pathExists(filePath)) {
102499
102829
  continue;
102500
102830
  }
@@ -102506,16 +102836,16 @@ async function rootEntrypointLinksIndex(cwd, manifest) {
102506
102836
  return false;
102507
102837
  }
102508
102838
  async function minimalSatisfied(cwd) {
102509
- const metaprojectRoot = path235.join(cwd, ".metaproject");
102839
+ const metaprojectRoot = path236.join(cwd, ".metaproject");
102510
102840
  const requiredFiles = ["index.md", "README.md", "metaproject.json"];
102511
102841
  const requiredDirs = ["modules", "rules", "skills", "data"];
102512
102842
  for (const file of requiredFiles) {
102513
- if (!await pathExists(path235.join(metaprojectRoot, file))) {
102843
+ if (!await pathExists(path236.join(metaprojectRoot, file))) {
102514
102844
  return false;
102515
102845
  }
102516
102846
  }
102517
102847
  for (const dir of requiredDirs) {
102518
- if (!await pathExists(path235.join(metaprojectRoot, dir))) {
102848
+ if (!await pathExists(path236.join(metaprojectRoot, dir))) {
102519
102849
  return false;
102520
102850
  }
102521
102851
  }
@@ -102528,7 +102858,7 @@ async function ciSatisfied(cwd, manifest) {
102528
102858
  return false;
102529
102859
  }
102530
102860
  for (const key of ciModules) {
102531
- if (await pathExists(path235.join(cwd, ".metaproject", "data", key, "artifacts"))) {
102861
+ if (await pathExists(path236.join(cwd, ".metaproject", "data", key, "artifacts"))) {
102532
102862
  return true;
102533
102863
  }
102534
102864
  }
@@ -102541,7 +102871,7 @@ async function evaluateProfiles(cwd, manifest) {
102541
102871
  satisfied.push("minimal");
102542
102872
  }
102543
102873
  const hasAgentModule = AGENT_MODULES.some((key) => new Set(enabledModuleKeys(manifest)).has(key));
102544
- const agent = minimal && hasAgentModule && await rootEntrypointLinksIndex(cwd, manifest) && await pathExists(path235.join(cwd, ".metaproject", "rules"));
102874
+ const agent = minimal && hasAgentModule && await rootEntrypointLinksIndex(cwd, manifest) && await pathExists(path236.join(cwd, ".metaproject", "rules"));
102545
102875
  if (agent) {
102546
102876
  satisfied.push("agent");
102547
102877
  }
@@ -102574,7 +102904,7 @@ var init_profiles2 = __esm(() => {
102574
102904
  });
102575
102905
 
102576
102906
  // src/standard/validate.ts
102577
- import path236 from "path";
102907
+ import path237 from "path";
102578
102908
  import { readFile as readFile109 } from "fs/promises";
102579
102909
  function isPlainObject14(value) {
102580
102910
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -102703,20 +103033,20 @@ function issue(code, message, fix) {
102703
103033
  async function validateWorkspace2(cwd) {
102704
103034
  const errors = [];
102705
103035
  const warnings = [];
102706
- const metaprojectRoot = path236.join(cwd, ".metaproject");
103036
+ const metaprojectRoot = path237.join(cwd, ".metaproject");
102707
103037
  const requiredFiles = ["index.md", "README.md", "metaproject.json"];
102708
103038
  for (const file of requiredFiles) {
102709
- if (!await pathExists(path236.join(metaprojectRoot, file))) {
103039
+ if (!await pathExists(path237.join(metaprojectRoot, file))) {
102710
103040
  errors.push(issue("missing-required-file", `Required file .metaproject/${file} is missing`, `Run \`keryx init\` to scaffold .metaproject/${file}.`));
102711
103041
  }
102712
103042
  }
102713
103043
  const requiredDirs = ["modules", "rules", "skills", "data"];
102714
103044
  for (const dir of requiredDirs) {
102715
- if (!await pathExists(path236.join(metaprojectRoot, dir))) {
103045
+ if (!await pathExists(path237.join(metaprojectRoot, dir))) {
102716
103046
  errors.push(issue("missing-required-dir", `Required directory .metaproject/${dir}/ is missing`, `Run \`keryx init\` to create .metaproject/${dir}/.`));
102717
103047
  }
102718
103048
  }
102719
- const manifestPath = path236.join(metaprojectRoot, "metaproject.json");
103049
+ const manifestPath = path237.join(metaprojectRoot, "metaproject.json");
102720
103050
  if (!await pathExists(manifestPath)) {
102721
103051
  return { ok: errors.length === 0, errors, warnings };
102722
103052
  }
@@ -102748,7 +103078,7 @@ async function validateWorkspace2(cwd) {
102748
103078
  if (typeof value !== "string") {
102749
103079
  continue;
102750
103080
  }
102751
- if (!await pathExists(path236.join(cwd, value))) {
103081
+ if (!await pathExists(path237.join(cwd, value))) {
102752
103082
  errors.push(issue("missing-declared-path", `Declared path paths.${key} -> ${value} does not exist`, `Create ${value} or remove paths.${key} from metaproject.json.`));
102753
103083
  }
102754
103084
  }
@@ -102758,7 +103088,7 @@ async function validateWorkspace2(cwd) {
102758
103088
  continue;
102759
103089
  }
102760
103090
  if (typeof entry.manifest === "string") {
102761
- if (!await pathExists(path236.join(cwd, entry.manifest))) {
103091
+ if (!await pathExists(path237.join(cwd, entry.manifest))) {
102762
103092
  errors.push(issue("missing-module-manifest", `Enabled module "${key}" manifest ${entry.manifest} is missing`, `Create ${entry.manifest} or run \`keryx update\` to regenerate module manifests.`));
102763
103093
  }
102764
103094
  }
@@ -102767,14 +103097,14 @@ async function validateWorkspace2(cwd) {
102767
103097
  if (typeof value !== "string") {
102768
103098
  continue;
102769
103099
  }
102770
- if (!await pathExists(path236.join(cwd, value))) {
103100
+ if (!await pathExists(path237.join(cwd, value))) {
102771
103101
  const sink = field === "data" ? warnings : errors;
102772
103102
  sink.push(issue("missing-module-path", `Module "${key}" declares ${String(field)} ${value}, which does not exist`, field === "data" ? `Run the module (e.g. \`keryx ${key} ...\`) to generate ${value}, or update modules.${key}.data in metaproject.json.` : `Create ${value} or update modules.${key}.${String(field)} in metaproject.json.`));
102773
103103
  }
102774
103104
  }
102775
103105
  }
102776
103106
  for (const { path: name, scope } of namedEntrypoints(manifest)) {
102777
- const filePath = path236.join(cwd, name);
103107
+ const filePath = path237.join(cwd, name);
102778
103108
  if (!await pathExists(filePath)) {
102779
103109
  continue;
102780
103110
  }
@@ -103439,6 +103769,12 @@ var init_help_groups = __esm(() => {
103439
103769
  group: "Automation",
103440
103770
  summary: "Connect, test or disconnect Telegram for this machine: bot token, one-time code, group. [status]."
103441
103771
  },
103772
+ {
103773
+ kind: "slash",
103774
+ name: "/rendering",
103775
+ group: "Automation",
103776
+ summary: "How Telegram replies are written: tables and lists as rich messages or HTML, with a fallback. [auto|rich|html|plain]."
103777
+ },
103442
103778
  {
103443
103779
  kind: "cli",
103444
103780
  name: "acp",
@@ -103631,6 +103967,12 @@ var init_help_groups = __esm(() => {
103631
103967
  group: "Maintenance and diagnostics",
103632
103968
  summary: "Bound stores that grow without bound (gdctx raw/artifacts, owner write-conflict sidecars)."
103633
103969
  },
103970
+ {
103971
+ kind: "cli",
103972
+ name: "remote",
103973
+ group: "Automation",
103974
+ summary: "Preview how Telegram replies are rendered (tables, lists, rules) in every mode, with no network: format-sample."
103975
+ },
103634
103976
  {
103635
103977
  kind: "cli",
103636
103978
  name: "forgetting",
@@ -103647,7 +103989,7 @@ var init_help_groups = __esm(() => {
103647
103989
  });
103648
103990
 
103649
103991
  // src/standard/service.ts
103650
- import path237 from "path";
103992
+ import path238 from "path";
103651
103993
  import { readFile as readFile110 } from "fs/promises";
103652
103994
  async function runValidate(cwd) {
103653
103995
  return validateWorkspace2(cwd);
@@ -103656,7 +103998,7 @@ async function runDoctor2(cwd) {
103656
103998
  return validateWorkspace2(cwd);
103657
103999
  }
103658
104000
  async function readManifest2(cwd) {
103659
- const manifestPath = path237.join(cwd, ".metaproject", "metaproject.json");
104001
+ const manifestPath = path238.join(cwd, ".metaproject", "metaproject.json");
103660
104002
  if (!await pathExists(manifestPath)) {
103661
104003
  throw new Error("Metaproject is not initialized (missing .metaproject/metaproject.json). Run `keryx init` first.");
103662
104004
  }
@@ -103683,7 +104025,7 @@ var init_service14 = __esm(() => {
103683
104025
 
103684
104026
  // src/rules/entrypoint-inspection.ts
103685
104027
  import { readFile as readFile111 } from "fs/promises";
103686
- import path238 from "path";
104028
+ import path239 from "path";
103687
104029
  async function inspectEntrypoints(projectRoot, agentEntrypoints, options = {}) {
103688
104030
  const git = await resolveGitCommonDir(projectRoot) !== undefined;
103689
104031
  const resolved = await resolveProjectEntrypoints(projectRoot, agentEntrypoints);
@@ -103768,14 +104110,14 @@ async function inspectEntrypoints(projectRoot, agentEntrypoints, options = {}) {
103768
104110
  codex = { entry, mode: "skip" };
103769
104111
  continue;
103770
104112
  }
103771
- const sourceExists = await pathExists(path238.join(projectRoot, entry.source));
104113
+ const sourceExists = await pathExists(path239.join(projectRoot, entry.source));
103772
104114
  const state = await codexOverrideState(projectRoot, entry);
103773
104115
  const bytes = await codexOverrideByteSize(projectRoot, entry);
103774
104116
  codex = { entry, mode: "override", sourceExists, state, ...bytes === undefined ? {} : { bytes } };
103775
104117
  localTargets.push({ path: entry.path, kind: "codex", exists: state !== "missing", expected: sourceExists });
103776
104118
  }
103777
104119
  if (targets.claudeSettings.scope === "local") {
103778
- const exists = await pathExists(path238.join(projectRoot, ...hooksTarget.split("/")));
104120
+ const exists = await pathExists(path239.join(projectRoot, ...hooksTarget.split("/")));
103779
104121
  localTargets.push({ path: hooksTarget, kind: "claudeSettings", exists, expected: options.hooksExpected === true });
103780
104122
  }
103781
104123
  const existing = localTargets.filter((target) => target.exists);
@@ -103904,14 +104246,14 @@ function codexPreviewLines(codex) {
103904
104246
  }
103905
104247
  async function readText3(projectRoot, relativePath) {
103906
104248
  try {
103907
- return await readFile111(path238.join(projectRoot, ...relativePath.split("/")), "utf8");
104249
+ return await readFile111(path239.join(projectRoot, ...relativePath.split("/")), "utf8");
103908
104250
  } catch {
103909
104251
  return;
103910
104252
  }
103911
104253
  }
103912
104254
  function displayPath2(projectRoot, target) {
103913
- const relative = path238.relative(projectRoot, target);
103914
- return relative.startsWith("..") || path238.isAbsolute(relative) ? target : relative.split(path238.sep).join("/");
104255
+ const relative = path239.relative(projectRoot, target);
104256
+ return relative.startsWith("..") || path239.isAbsolute(relative) ? target : relative.split(path239.sep).join("/");
103915
104257
  }
103916
104258
  function isRecord10(value) {
103917
104259
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -103935,7 +104277,7 @@ var init_entrypoint_inspection = __esm(() => {
103935
104277
  import { readdir as readdir35, readFile as readFile112 } from "fs/promises";
103936
104278
  import { existsSync as existsSync41 } from "fs";
103937
104279
  import { homedir as homedir10, tmpdir as tmpdir10 } from "os";
103938
- import path239 from "path";
104280
+ import path240 from "path";
103939
104281
  async function registeredWorktrees(cwd) {
103940
104282
  if (!Bun.which("git"))
103941
104283
  return new Set;
@@ -103948,7 +104290,7 @@ async function registeredWorktrees(cwd) {
103948
104290
  for (const line of stdout.split(`
103949
104291
  `)) {
103950
104292
  if (line.startsWith("worktree ")) {
103951
- paths.add(path239.resolve(line.slice("worktree ".length).trim()));
104293
+ paths.add(path240.resolve(line.slice("worktree ".length).trim()));
103952
104294
  }
103953
104295
  }
103954
104296
  return paths;
@@ -103958,7 +104300,7 @@ async function registeredWorktrees(cwd) {
103958
104300
  }
103959
104301
  async function checkWorktrees(cwd) {
103960
104302
  const mainRoot = await resolveMainCheckoutRoot(cwd) ?? cwd;
103961
- const worktreesDir = path239.join(mainRoot, ".claude", "worktrees");
104303
+ const worktreesDir = path240.join(mainRoot, ".claude", "worktrees");
103962
104304
  let entries;
103963
104305
  try {
103964
104306
  entries = (await readdir35(worktreesDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
@@ -103970,8 +104312,8 @@ async function checkWorktrees(cwd) {
103970
104312
  }
103971
104313
  const registered = await registeredWorktrees(cwd);
103972
104314
  const stale = entries.filter((name) => {
103973
- const full = path239.resolve(worktreesDir, name);
103974
- return !existsSync41(path239.join(full, ".git")) || !registered.has(full);
104315
+ const full = path240.resolve(worktreesDir, name);
104316
+ return !existsSync41(path240.join(full, ".git")) || !registered.has(full);
103975
104317
  });
103976
104318
  if (stale.length === 0) {
103977
104319
  return {
@@ -104137,7 +104479,7 @@ async function checkEntrypoints(cwd) {
104137
104479
  const projectRoot = resolveProjectRoot2(cwd);
104138
104480
  let manifest;
104139
104481
  try {
104140
- manifest = JSON.parse(await readFile112(path239.join(projectRoot, ".metaproject", "metaproject.json"), "utf8"));
104482
+ manifest = JSON.parse(await readFile112(path240.join(projectRoot, ".metaproject", "metaproject.json"), "utf8"));
104141
104483
  } catch {
104142
104484
  manifest = undefined;
104143
104485
  }
@@ -104292,22 +104634,22 @@ async function checkWikiFreshness(cwd) {
104292
104634
  };
104293
104635
  }
104294
104636
  function isExcludedProjectRootCandidate(dir) {
104295
- return dir === path239.parse(dir).root || dir === "/tmp" || dir === tmpdir10();
104637
+ return dir === path240.parse(dir).root || dir === "/tmp" || dir === tmpdir10();
104296
104638
  }
104297
104639
  function findMetaprojectUpward(cwd, ceiling) {
104298
- let current = path239.resolve(cwd);
104299
- const resolvedCeiling = path239.resolve(ceiling);
104640
+ let current = path240.resolve(cwd);
104641
+ const resolvedCeiling = path240.resolve(ceiling);
104300
104642
  for (;; ) {
104301
104643
  if (isExcludedProjectRootCandidate(current)) {
104302
104644
  return;
104303
104645
  }
104304
- if (existsSync41(path239.join(current, ".metaproject"))) {
104646
+ if (existsSync41(path240.join(current, ".metaproject"))) {
104305
104647
  return current;
104306
104648
  }
104307
104649
  if (current === resolvedCeiling) {
104308
104650
  return;
104309
104651
  }
104310
- const parent = path239.dirname(current);
104652
+ const parent = path240.dirname(current);
104311
104653
  if (parent === current) {
104312
104654
  return;
104313
104655
  }
@@ -104321,7 +104663,7 @@ async function isKeryxProject(cwd, overrides = {}) {
104321
104663
  return true;
104322
104664
  }
104323
104665
  const mainRoot = await resolveMainCheckoutRoot(cwd);
104324
- return mainRoot !== undefined && existsSync41(path239.join(mainRoot, ".metaproject"));
104666
+ return mainRoot !== undefined && existsSync41(path240.join(mainRoot, ".metaproject"));
104325
104667
  }
104326
104668
  async function buildDoctorReport(cwd, env = process.env, overrides = {}) {
104327
104669
  const globalChecks = await Promise.all([
@@ -104409,7 +104751,7 @@ var init_doctor2 = __esm(() => {
104409
104751
  });
104410
104752
 
104411
104753
  // src/commands/goal-command.ts
104412
- import path240 from "path";
104754
+ import path241 from "path";
104413
104755
  import { readFile as readFile113 } from "fs/promises";
104414
104756
  function parseGoalArgs(rest) {
104415
104757
  const trimmed = rest.trim();
@@ -104482,7 +104824,7 @@ function getFlowService() {
104482
104824
  async function autoProvisionFlow(cwd, goalText) {
104483
104825
  const service = getFlowService();
104484
104826
  const result = await service.init({ cwd, title: goalText });
104485
- const acFile = path240.join(cwd, result.dir, "acceptance-criteria.md");
104827
+ const acFile = path241.join(cwd, result.dir, "acceptance-criteria.md");
104486
104828
  await writeFileAtomic(acFile, [
104487
104829
  "# Acceptance Criteria",
104488
104830
  "",
@@ -107520,7 +107862,7 @@ __export(exports_flow, {
107520
107862
  signerIdentityArgs: () => signerIdentityArgs
107521
107863
  });
107522
107864
  import { randomInt } from "crypto";
107523
- import path241 from "path";
107865
+ import path242 from "path";
107524
107866
  function parseTaskKind(raw) {
107525
107867
  if (raw === undefined) {
107526
107868
  return;
@@ -108435,7 +108777,7 @@ async function runSchema(args) {
108435
108777
  `;
108436
108778
  const out = optionValue(args, "--out");
108437
108779
  if (out) {
108438
- const target = path241.isAbsolute(out) ? out : path241.join(process.cwd(), out);
108780
+ const target = path242.isAbsolute(out) ? out : path242.join(process.cwd(), out);
108439
108781
  await writeFileAtomic(target, json);
108440
108782
  console.log(` ${style.green(symbols.ok)} Wrote flow-state schema ${style.cyan(symbols.arrow)} ${out}`);
108441
108783
  return;
@@ -108727,7 +109069,7 @@ var init_lifecycle_flag = __esm(() => {
108727
109069
  // src/sac/catch-up.ts
108728
109070
  import { randomUUID as randomUUID30 } from "crypto";
108729
109071
  import { readdir as readdir36, stat as stat24 } from "fs/promises";
108730
- import path242 from "path";
109072
+ import path243 from "path";
108731
109073
  async function buildCatchUp(input) {
108732
109074
  const [proposals, sessionCategories, lifecycleFlagsAll, unreviewedPathsAll] = await Promise.all([
108733
109075
  collectProposals(input.cwd, input.workspaceId),
@@ -108846,7 +109188,7 @@ async function readExternalUnboundCandidates(cwd) {
108846
109188
  }
108847
109189
  return groups;
108848
109190
  }, []));
108849
- const metaPath = path242.join(extDir, `${id}.json`);
109191
+ const metaPath = path243.join(extDir, `${id}.json`);
108850
109192
  if (await pathExists(unboundDismissedReceiptPath(metaPath)))
108851
109193
  continue;
108852
109194
  candidates.push({
@@ -108860,7 +109202,7 @@ async function readExternalUnboundCandidates(cwd) {
108860
109202
  return candidates;
108861
109203
  }
108862
109204
  async function readNewestUnboundCandidateForExternal(cwd, externalSessionId) {
108863
- const evidenceDir = path242.join(externalSlatesDir(cwd), externalSessionId);
109205
+ const evidenceDir = path243.join(externalSlatesDir(cwd), externalSessionId);
108864
109206
  let entries;
108865
109207
  try {
108866
109208
  entries = (await readdir36(evidenceDir)).filter((name) => name.endsWith("-unbound-candidate.json"));
@@ -108869,7 +109211,7 @@ async function readNewestUnboundCandidateForExternal(cwd, externalSessionId) {
108869
109211
  }
108870
109212
  entries.sort();
108871
109213
  for (let i = entries.length - 1;i >= 0; i--) {
108872
- const evidencePath = path242.join(evidenceDir, entries[i]);
109214
+ const evidencePath = path243.join(evidenceDir, entries[i]);
108873
109215
  if (await pathExists(unboundDismissedReceiptPath(evidencePath)))
108874
109216
  continue;
108875
109217
  const result = readConfigFile(evidencePath);
@@ -108910,7 +109252,7 @@ async function collectSessionCategories(cwd) {
108910
109252
  }
108911
109253
  async function collectReceiptTargets(cwd, owner) {
108912
109254
  const targets = new Set;
108913
- const workspacesDir = path242.join(cwd, ".metaproject", "workspaces");
109255
+ const workspacesDir = path243.join(cwd, ".metaproject", "workspaces");
108914
109256
  let workspaceIds;
108915
109257
  try {
108916
109258
  workspaceIds = (await readdir36(workspacesDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
@@ -108918,7 +109260,7 @@ async function collectReceiptTargets(cwd, owner) {
108918
109260
  return targets;
108919
109261
  }
108920
109262
  for (const workspaceId of workspaceIds) {
108921
- const receiptsDir = path242.join(workspacesDir, workspaceId, `${owner}-write-receipts`);
109263
+ const receiptsDir = path243.join(workspacesDir, workspaceId, `${owner}-write-receipts`);
108922
109264
  let files;
108923
109265
  try {
108924
109266
  files = (await readdir36(receiptsDir)).filter((name) => name.endsWith(".json"));
@@ -108926,7 +109268,7 @@ async function collectReceiptTargets(cwd, owner) {
108926
109268
  continue;
108927
109269
  }
108928
109270
  for (const file of files) {
108929
- const result = readConfigFile(path242.join(receiptsDir, file));
109271
+ const result = readConfigFile(path243.join(receiptsDir, file));
108930
109272
  if (!result.ok)
108931
109273
  continue;
108932
109274
  try {
@@ -108969,7 +109311,7 @@ async function findSkillFiles(dir) {
108969
109311
  }
108970
109312
  const found = [];
108971
109313
  for (const entry of entries) {
108972
- const full = path242.join(dir, entry.name);
109314
+ const full = path243.join(dir, entry.name);
108973
109315
  if (entry.isDirectory()) {
108974
109316
  found.push(...await findSkillFiles(full));
108975
109317
  } else if (entry.isFile() && entry.name === "SKILL.md") {
@@ -109028,10 +109370,10 @@ async function detectUnreviewedSacPathChanges(cwd, sessions) {
109028
109370
  changedAt: attribution.changedAt
109029
109371
  });
109030
109372
  }
109031
- const sacSkillsDir = path242.join(cwd, ".metaproject", "project-skills", "sac");
109373
+ const sacSkillsDir = path243.join(cwd, ".metaproject", "project-skills", "sac");
109032
109374
  const skillFiles = await findSkillFiles(sacSkillsDir);
109033
109375
  for (const absolutePath of skillFiles) {
109034
- const targetRef = path242.relative(path242.join(cwd, ".metaproject"), absolutePath).split(path242.sep).join("/");
109376
+ const targetRef = path243.relative(path243.join(cwd, ".metaproject"), absolutePath).split(path243.sep).join("/");
109035
109377
  if (skillReceipts.has(targetRef))
109036
109378
  continue;
109037
109379
  const attribution = await attributeToSession(absolutePath, scoped);
@@ -109049,12 +109391,12 @@ async function detectUnreviewedSacPathChanges(cwd, sessions) {
109049
109391
  return items;
109050
109392
  }
109051
109393
  async function isSlateEngaged(dir) {
109052
- if (await pathExists(path242.join(dir, "slate.json")))
109394
+ if (await pathExists(path243.join(dir, "slate.json")))
109053
109395
  return true;
109054
- if (await pathExists(path242.join(dir, "terminal-state.json")))
109396
+ if (await pathExists(path243.join(dir, "terminal-state.json")))
109055
109397
  return true;
109056
109398
  try {
109057
- const entries = await readdir36(path242.join(dir, "slate-archive"));
109399
+ const entries = await readdir36(path243.join(dir, "slate-archive"));
109058
109400
  return entries.length > 0;
109059
109401
  } catch {
109060
109402
  return false;
@@ -109079,7 +109421,7 @@ async function safeReadSlate(dir) {
109079
109421
  }
109080
109422
  }
109081
109423
  async function readTerminalState(dir) {
109082
- const result = readConfigFile(path242.join(dir, "terminal-state.json"));
109424
+ const result = readConfigFile(path243.join(dir, "terminal-state.json"));
109083
109425
  if (!result.ok) {
109084
109426
  return;
109085
109427
  }
@@ -109103,7 +109445,7 @@ async function resolveUnboundCandidateTarget(cwd, target) {
109103
109445
  if (session === undefined) {
109104
109446
  throw new Error(`No session or evidence path matches "${target}"`);
109105
109447
  }
109106
- const archiveDir = path242.join(sessionDir(session.projectPath, session.id), "slate-archive");
109448
+ const archiveDir = path243.join(sessionDir(session.projectPath, session.id), "slate-archive");
109107
109449
  let entries;
109108
109450
  try {
109109
109451
  entries = (await readdir36(archiveDir)).filter((name) => name.endsWith("-unbound-candidate.json"));
@@ -109113,7 +109455,7 @@ async function resolveUnboundCandidateTarget(cwd, target) {
109113
109455
  if (entries.length === 0)
109114
109456
  throw new Error(`No unbound-candidate artifacts for session ${session.id}`);
109115
109457
  entries.sort();
109116
- return path242.join(archiveDir, entries[entries.length - 1]);
109458
+ return path243.join(archiveDir, entries[entries.length - 1]);
109117
109459
  }
109118
109460
  async function dismissUnboundByTarget(cwd, target, reason) {
109119
109461
  const evidencePath = await resolveUnboundCandidateTarget(cwd, target);
@@ -109139,7 +109481,7 @@ async function dismissUnboundCandidate(candidatePath, reason) {
109139
109481
  return { removed: candidatePath, receipt: receiptPath };
109140
109482
  }
109141
109483
  async function readNewestUnboundCandidate(dir) {
109142
- const archiveDir = path242.join(dir, "slate-archive");
109484
+ const archiveDir = path243.join(dir, "slate-archive");
109143
109485
  let entries;
109144
109486
  try {
109145
109487
  entries = (await readdir36(archiveDir)).filter((name) => name.endsWith("-unbound-candidate.json"));
@@ -109148,7 +109490,7 @@ async function readNewestUnboundCandidate(dir) {
109148
109490
  }
109149
109491
  entries.sort();
109150
109492
  for (let i = entries.length - 1;i >= 0; i--) {
109151
- const evidencePath = path242.join(archiveDir, entries[i]);
109493
+ const evidencePath = path243.join(archiveDir, entries[i]);
109152
109494
  if (await pathExists(unboundDismissedReceiptPath(evidencePath)))
109153
109495
  continue;
109154
109496
  const result = readConfigFile(evidencePath);
@@ -109178,7 +109520,7 @@ function isFailureOutcome2(g) {
109178
109520
  return g.outcome === "error" || g.outcome === "no_credential" || g.outcome === "conflict";
109179
109521
  }
109180
109522
  async function readNewestWrapUpOutcome(dir) {
109181
- const archiveDir = path242.join(dir, "slate-archive");
109523
+ const archiveDir = path243.join(dir, "slate-archive");
109182
109524
  let entries;
109183
109525
  try {
109184
109526
  entries = (await readdir36(archiveDir)).filter((name) => name.endsWith("-wrap-up-outcome.json"));
@@ -109187,7 +109529,7 @@ async function readNewestWrapUpOutcome(dir) {
109187
109529
  }
109188
109530
  entries.sort();
109189
109531
  for (let i = entries.length - 1;i >= 0; i--) {
109190
- const evidencePath = path242.join(archiveDir, entries[i]);
109532
+ const evidencePath = path243.join(archiveDir, entries[i]);
109191
109533
  const result = readConfigFile(evidencePath);
109192
109534
  if (!result.ok) {
109193
109535
  continue;
@@ -109937,12 +110279,12 @@ var init_extract3 = __esm(() => {
109937
110279
  // src/product/corpus.ts
109938
110280
  import { createHash as createHash62 } from "crypto";
109939
110281
  import { readdir as readdir37, readFile as readFile115 } from "fs/promises";
109940
- import path243 from "path";
110282
+ import path244 from "path";
109941
110283
  function flowsDir(cwd) {
109942
- return path243.join(cwd, ".metaproject", "flows");
110284
+ return path244.join(cwd, ".metaproject", "flows");
109943
110285
  }
109944
110286
  function requirementsDir(cwd) {
109945
- return path243.join(cwd, "docs", "requirements");
110287
+ return path244.join(cwd, "docs", "requirements");
109946
110288
  }
109947
110289
  async function readOptional(file) {
109948
110290
  try {
@@ -109995,23 +110337,23 @@ async function corpusFingerprint(cwd) {
109995
110337
  for (const name of await listFlowPackages(cwd)) {
109996
110338
  lines.push(`package\x00flow\x00${name}`);
109997
110339
  for (const file of FLOW_SOURCE_FILES) {
109998
- const content = await readBytes(path243.join(flowsDir(cwd), name, file));
110340
+ const content = await readBytes(path244.join(flowsDir(cwd), name, file));
109999
110341
  lines.push(`file\x00${FLOW_PREFIX}${name}/${file}\x00${digestOf4(content)}`);
110000
110342
  }
110001
110343
  }
110002
110344
  for (const name of await requirementPackages(cwd)) {
110003
110345
  lines.push(`package\x00docpack\x00${name}`);
110004
- const dir = path243.join(requirementsDir(cwd), name);
110346
+ const dir = path244.join(requirementsDir(cwd), name);
110005
110347
  for (const candidate of PRIMARY_DOCS) {
110006
- const content = await readBytes(path243.join(dir, candidate));
110348
+ const content = await readBytes(path244.join(dir, candidate));
110007
110349
  if (content === null)
110008
110350
  continue;
110009
110351
  lines.push(`file\x00docs/requirements/${name}/${candidate}\x00${digestOf4(content)}`);
110010
110352
  break;
110011
110353
  }
110012
- const readme = await readBytes(path243.join(dir, "README.md"));
110354
+ const readme = await readBytes(path244.join(dir, "README.md"));
110013
110355
  lines.push(`file\x00docs/requirements/${name}/README.md\x00${digestOf4(readme)}`);
110014
- const specification = await readBytes(path243.join(dir, "specification.md"));
110356
+ const specification = await readBytes(path244.join(dir, "specification.md"));
110015
110357
  lines.push(`file\x00docs/requirements/${name}/specification.md\x00${digestOf4(specification)}`);
110016
110358
  }
110017
110359
  return createHash62("sha256").update([...lines].sort().join(`
@@ -110053,19 +110395,19 @@ async function buildIntentIndex(cwd) {
110053
110395
  const intents = [];
110054
110396
  const failures = [];
110055
110397
  for (const name of await listFlowPackages(cwd)) {
110056
- const dir = path243.join(flowsDir(cwd), name);
110398
+ const dir = path244.join(flowsDir(cwd), name);
110057
110399
  const repoPath = `${FLOW_PREFIX}${name}`;
110058
110400
  try {
110059
- const flowJson = await readSource(path243.join(dir, "flow.json"), `${repoPath}/flow.json`);
110401
+ const flowJson = await readSource(path244.join(dir, "flow.json"), `${repoPath}/flow.json`);
110060
110402
  if (flowJson === null) {
110061
110403
  failures.push(`${repoPath}: no flow.json`);
110062
110404
  continue;
110063
110405
  }
110064
- const journal = await readSource(path243.join(dir, "journal.md"), `${repoPath}/journal.md`);
110406
+ const journal = await readSource(path244.join(dir, "journal.md"), `${repoPath}/journal.md`);
110065
110407
  intents.push(extractFlowIntent({
110066
110408
  flowJson,
110067
- description: await readSource(path243.join(dir, "description.md"), `${repoPath}/description.md`),
110068
- criteria: await readSource(path243.join(dir, "acceptance-criteria.md"), `${repoPath}/acceptance-criteria.md`),
110409
+ description: await readSource(path244.join(dir, "description.md"), `${repoPath}/description.md`),
110410
+ criteria: await readSource(path244.join(dir, "acceptance-criteria.md"), `${repoPath}/acceptance-criteria.md`),
110069
110411
  journal
110070
110412
  }, repoPath));
110071
110413
  const problem = observationProblem(journal);
@@ -110076,17 +110418,17 @@ async function buildIntentIndex(cwd) {
110076
110418
  }
110077
110419
  }
110078
110420
  for (const name of await requirementPackages(cwd)) {
110079
- const dir = path243.join(requirementsDir(cwd), name);
110421
+ const dir = path244.join(requirementsDir(cwd), name);
110080
110422
  const repoPath = `docs/requirements/${name}`;
110081
110423
  try {
110082
110424
  let primary = null;
110083
110425
  for (const candidate of PRIMARY_DOCS) {
110084
- primary = await readSource(path243.join(dir, candidate), `${repoPath}/${candidate}`);
110426
+ primary = await readSource(path244.join(dir, candidate), `${repoPath}/${candidate}`);
110085
110427
  if (primary !== null)
110086
110428
  break;
110087
110429
  }
110088
- const specification = await readSource(path243.join(dir, "specification.md"), `${repoPath}/specification.md`);
110089
- const readme = await readSource(path243.join(dir, "README.md"), `${repoPath}/README.md`);
110430
+ const specification = await readSource(path244.join(dir, "specification.md"), `${repoPath}/specification.md`);
110431
+ const readme = await readSource(path244.join(dir, "README.md"), `${repoPath}/README.md`);
110090
110432
  const source = { name, primary, specification, readme };
110091
110433
  intents.push(extractDocpackIntent(source, repoPath));
110092
110434
  const problem = docpackObservationProblem(source);
@@ -110146,12 +110488,12 @@ var init_by_origin = __esm(() => {
110146
110488
 
110147
110489
  // src/product/store.ts
110148
110490
  import { readFile as readFile116 } from "fs/promises";
110149
- import path244 from "path";
110491
+ import path245 from "path";
110150
110492
  function productDataRoot(cwd) {
110151
- return path244.join(cwd, ".metaproject", "data", "product");
110493
+ return path245.join(cwd, ".metaproject", "data", "product");
110152
110494
  }
110153
110495
  function indexPath(cwd) {
110154
- return path244.join(productDataRoot(cwd), "index.json");
110496
+ return path245.join(productDataRoot(cwd), "index.json");
110155
110497
  }
110156
110498
  function serializeIndex(index) {
110157
110499
  return `${JSON.stringify(index, null, 2)}
@@ -111642,9 +111984,9 @@ var init_repair = __esm(() => {
111642
111984
 
111643
111985
  // src/review/managed.ts
111644
111986
  import { mkdir as mkdir55, readFile as readFile117, readdir as readdir38, realpath as realpath18, rm as rm16, stat as stat25 } from "fs/promises";
111645
- import path245 from "path";
111987
+ import path246 from "path";
111646
111988
  function reviewsRoot(cwd) {
111647
- return path245.join(cwd, ".metaproject", "reviews");
111989
+ return path246.join(cwd, ".metaproject", "reviews");
111648
111990
  }
111649
111991
  async function resolveGitHead2(cwd) {
111650
111992
  try {
@@ -111777,26 +112119,26 @@ async function createManagedReviewPackage(input) {
111777
112119
  throw new Error(`Refusing to record findings that do not satisfy review-finding.schema.json: ${contractErrors.map((error) => `${error.path} ${error.message}`).join("; ")}`);
111778
112120
  }
111779
112121
  await mkdir55(packageDir, { recursive: true });
111780
- await writeFileAtomic(path245.join(packageDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
112122
+ await writeFileAtomic(path246.join(packageDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
111781
112123
  `);
111782
- await writeFileAtomic(path245.join(packageDir, "scope.md"), renderScope(input, flowMatch, at, verification, carried, caps, externalReclaims, scopeB.record, filterStats));
111783
- await writeFileAtomic(path245.join(packageDir, "coverage.md"), renderCoverage(coverage));
111784
- await writeFileAtomic(path245.join(packageDir, "report.md"), renderReport2(report, input.mode));
111785
- await writeFileAtomic(path245.join(packageDir, "findings.json"), `${JSON.stringify(findings.map(toContractFinding), null, 2)}
112124
+ await writeFileAtomic(path246.join(packageDir, "scope.md"), renderScope(input, flowMatch, at, verification, carried, caps, externalReclaims, scopeB.record, filterStats));
112125
+ await writeFileAtomic(path246.join(packageDir, "coverage.md"), renderCoverage(coverage));
112126
+ await writeFileAtomic(path246.join(packageDir, "report.md"), renderReport2(report, input.mode));
112127
+ await writeFileAtomic(path246.join(packageDir, "findings.json"), `${JSON.stringify(findings.map(toContractFinding), null, 2)}
111786
112128
  `);
111787
- await writeFileAtomic(path245.join(packageDir, "learning.md"), renderLearning(findings));
111788
- await writeFileAtomic(path245.join(packageDir, "decisions.md"), renderDecisions(findings));
112129
+ await writeFileAtomic(path246.join(packageDir, "learning.md"), renderLearning(findings));
112130
+ await writeFileAtomic(path246.join(packageDir, "decisions.md"), renderDecisions(findings));
111789
112131
  await persistScreenRadius(packageDir, scopeB.resolved);
111790
112132
  const reviewNotes = await writeReviewNotes(findings, {
111791
112133
  cwd: input.cwd,
111792
112134
  reviewId,
111793
112135
  head,
111794
- packagePath: path245.relative(input.cwd, packageDir),
112136
+ packagePath: path246.relative(input.cwd, packageDir),
111795
112137
  ...input.now === undefined ? {} : { now: input.now }
111796
112138
  });
111797
112139
  return {
111798
112140
  reviewId,
111799
- path: path245.relative(input.cwd, packageDir),
112141
+ path: path246.relative(input.cwd, packageDir),
111800
112142
  manifest,
111801
112143
  verification: verification.counts,
111802
112144
  verificationRejections: verification.rejections,
@@ -111813,7 +112155,7 @@ async function screenScopeBFindings(input, packageDir, findings) {
111813
112155
  const scopeB = findings.filter((finding) => isBlastRadiusScopedFinding(finding, reviewers));
111814
112156
  if (resolved === undefined) {
111815
112157
  if (scopeB.length > 0) {
111816
- throw new Error(`Refusing to record ${scopeB.length} finding(s) raised under scope B (${scopeB.map((finding) => `${finding.id} by ${finding.reviewer}`).join(", ")}) with no blast-radius record: AC3 is enforced in code, and the screen cannot be applied without the computed set the round was dispatched over. Two ways out, and the first is the one that always works: (1) compute the set with \`keryx review blast-radius --json > <file>\` and pass \`--blast-radius <file>\` to this ingest; (2) write that record to \`${path245.relative(input.cwd, handoffBlastRadiusPath(packageDir))}\` and ingest again \u2014 the ingest reads it from there and consumes it. Do NOT write it to \`${path245.relative(input.cwd, path245.join(packageDir, BLAST_RADIUS_ARTIFACT))}\`: a round that names no --review-id has not allocated that directory yet, so creating it makes the next ingest take the NEXT free name and find nothing there either.`);
112158
+ throw new Error(`Refusing to record ${scopeB.length} finding(s) raised under scope B (${scopeB.map((finding) => `${finding.id} by ${finding.reviewer}`).join(", ")}) with no blast-radius record: AC3 is enforced in code, and the screen cannot be applied without the computed set the round was dispatched over. Two ways out, and the first is the one that always works: (1) compute the set with \`keryx review blast-radius --json > <file>\` and pass \`--blast-radius <file>\` to this ingest; (2) write that record to \`${path246.relative(input.cwd, handoffBlastRadiusPath(packageDir))}\` and ingest again \u2014 the ingest reads it from there and consumes it. Do NOT write it to \`${path246.relative(input.cwd, path246.join(packageDir, BLAST_RADIUS_ARTIFACT))}\`: a round that names no --review-id has not allocated that directory yet, so creating it makes the next ingest take the NEXT free name and find nothing there either.`);
111817
112159
  }
111818
112160
  return { retained: [...findings], record: { source: "none", scopeBFindings: 0 }, resolved: undefined };
111819
112161
  }
@@ -111830,13 +112172,13 @@ async function screenScopeBFindings(input, packageDir, findings) {
111830
112172
  };
111831
112173
  }
111832
112174
  function handoffBlastRadiusPath(packageDir) {
111833
- return path245.join(path245.dirname(packageDir), BLAST_RADIUS_ARTIFACT);
112175
+ return path246.join(path246.dirname(packageDir), BLAST_RADIUS_ARTIFACT);
111834
112176
  }
111835
112177
  async function resolveScreenRadius(input, packageDir) {
111836
112178
  if (input.blastRadius !== undefined) {
111837
112179
  return { radius: input.blastRadius, source: "input" };
111838
112180
  }
111839
- const inPackage = path245.join(packageDir, BLAST_RADIUS_ARTIFACT);
112181
+ const inPackage = path246.join(packageDir, BLAST_RADIUS_ARTIFACT);
111840
112182
  const fromPackage = await readBlastRadiusRecordFile(inPackage);
111841
112183
  if (fromPackage !== undefined) {
111842
112184
  return { radius: fromPackage, source: "package", file: inPackage };
@@ -111862,7 +112204,7 @@ async function persistScreenRadius(packageDir, resolved) {
111862
112204
  if (resolved === undefined) {
111863
112205
  return;
111864
112206
  }
111865
- const inPackage = path245.join(packageDir, BLAST_RADIUS_ARTIFACT);
112207
+ const inPackage = path246.join(packageDir, BLAST_RADIUS_ARTIFACT);
111866
112208
  if (resolved.source !== "package") {
111867
112209
  await writeFileAtomic(inPackage, `${JSON.stringify(resolved.radius, null, 2)}
111868
112210
  `);
@@ -111893,12 +112235,12 @@ function fromVerifierRefutations(findings) {
111893
112235
  }));
111894
112236
  }
111895
112237
  async function getManagedReviewStatus(cwd, ref) {
111896
- const manifestPath = ref.endsWith("manifest.json") ? path245.resolve(cwd, ref) : path245.join(await resolveReviewPackagePath(cwd, ref), "manifest.json");
112238
+ const manifestPath = ref.endsWith("manifest.json") ? path246.resolve(cwd, ref) : path246.join(await resolveReviewPackagePath(cwd, ref), "manifest.json");
111897
112239
  return JSON.parse(await readFile117(manifestPath, "utf8"));
111898
112240
  }
111899
112241
  async function completeManagedReview(cwd, ref, options = {}) {
111900
112242
  const packageDir = await resolveReviewPackagePath(cwd, ref);
111901
- const manifestPath = path245.join(packageDir, "manifest.json");
112243
+ const manifestPath = path246.join(packageDir, "manifest.json");
111902
112244
  const manifest = JSON.parse(await readFile117(manifestPath, "utf8"));
111903
112245
  const missing = await missingArtifacts(packageDir);
111904
112246
  if (missing.length > 0) {
@@ -111919,12 +112261,12 @@ async function completeManagedReview(cwd, ref, options = {}) {
111919
112261
  }
111920
112262
  await writeFileAtomic(manifestPath, `${JSON.stringify(updated, null, 2)}
111921
112263
  `);
111922
- const recorded = JSON.parse(await readFile117(path245.join(packageDir, "findings.json"), "utf8"));
112264
+ const recorded = JSON.parse(await readFile117(path246.join(packageDir, "findings.json"), "utf8"));
111923
112265
  const reviewNotes = await writeReviewNotes(recorded, {
111924
112266
  cwd,
111925
112267
  reviewId: updated.reviewId,
111926
112268
  head: updated.target.head ?? null,
111927
- packagePath: path245.relative(cwd, packageDir),
112269
+ packagePath: path246.relative(cwd, packageDir),
111928
112270
  ...options.now === undefined ? {} : { now: options.now }
111929
112271
  });
111930
112272
  return { manifest: updated, reviewNotes, dispositions: tallyDispositions(recorded) };
@@ -111990,16 +112332,16 @@ async function resolvePackageFlow(input) {
111990
112332
  }
111991
112333
  function packagePath(cwd, mode, reviewId, flowMatch) {
111992
112334
  if ((mode === "attach-review" || mode === "ingest") && flowMatch) {
111993
- return path245.join(flowsRoot(cwd), flowMatch.dir, "reviews", reviewId);
112335
+ return path246.join(flowsRoot(cwd), flowMatch.dir, "reviews", reviewId);
111994
112336
  }
111995
112337
  if (mode === "attach-review") {
111996
112338
  throw new Error("attach-review requires an explicit or matched flow");
111997
112339
  }
111998
- return path245.join(reviewsRoot(cwd), reviewId);
112340
+ return path246.join(reviewsRoot(cwd), reviewId);
111999
112341
  }
112000
112342
  function buildManifest(args) {
112001
112343
  const crossFamily = args.input.crossFamilyReview;
112002
- const artifactPath = (name) => path245.relative(args.input.cwd, path245.join(args.packageDir, name));
112344
+ const artifactPath = (name) => path246.relative(args.input.cwd, path246.join(args.packageDir, name));
112003
112345
  const manifest = {
112004
112346
  schemaVersion: 1,
112005
112347
  reviewId: args.reviewId,
@@ -112046,7 +112388,7 @@ async function readReport(input) {
112046
112388
  return input.reportText;
112047
112389
  }
112048
112390
  if (input.reportPath) {
112049
- return readFile117(path245.resolve(input.cwd, input.reportPath), "utf8");
112391
+ return readFile117(path246.resolve(input.cwd, input.reportPath), "utf8");
112050
112392
  }
112051
112393
  if (input.mode === "ingest") {
112052
112394
  throw new Error("ingest requires --report or reportText");
@@ -112057,7 +112399,7 @@ No reviewer findings recorded yet.
112057
112399
  `;
112058
112400
  }
112059
112401
  function reportLabel(input) {
112060
- return input.reportPath ? path245.resolve(input.cwd, input.reportPath) : "the report text passed to keryx review";
112402
+ return input.reportPath ? path246.resolve(input.cwd, input.reportPath) : "the report text passed to keryx review";
112061
112403
  }
112062
112404
  async function normalizeFindings(args) {
112063
112405
  if (args.source !== undefined) {
@@ -112112,16 +112454,16 @@ async function anchorFindings(findings, input, head) {
112112
112454
  }
112113
112455
  }
112114
112456
  async function containedRelativePath(cwd, relative) {
112115
- const root = await realpath18(cwd).catch(() => path245.resolve(cwd));
112116
- const resolved = path245.resolve(root, relative);
112457
+ const root = await realpath18(cwd).catch(() => path246.resolve(cwd));
112458
+ const resolved = path246.resolve(root, relative);
112117
112459
  const real = await realpath18(resolved).catch(() => null);
112118
- if (real === null || real !== root && !real.startsWith(`${root}${path245.sep}`)) {
112460
+ if (real === null || real !== root && !real.startsWith(`${root}${path246.sep}`)) {
112119
112461
  return null;
112120
112462
  }
112121
112463
  return real;
112122
112464
  }
112123
112465
  async function readFileAtCommit(cwd, commit, absolute) {
112124
- const relative = path245.relative(await realpath18(cwd).catch(() => path245.resolve(cwd)), absolute);
112466
+ const relative = path246.relative(await realpath18(cwd).catch(() => path246.resolve(cwd)), absolute);
112125
112467
  try {
112126
112468
  const proc = Bun.spawn(["git", "show", `${commit}:${relative}`], {
112127
112469
  cwd,
@@ -112140,10 +112482,10 @@ async function readFileAtCommit(cwd, commit, absolute) {
112140
112482
  }
112141
112483
  function readFromWorkingTree(input) {
112142
112484
  return async (relative) => {
112143
- const root = await realpath18(input.cwd).catch(() => path245.resolve(input.cwd));
112144
- const resolved = path245.resolve(root, relative);
112485
+ const root = await realpath18(input.cwd).catch(() => path246.resolve(input.cwd));
112486
+ const resolved = path246.resolve(root, relative);
112145
112487
  const real = await realpath18(resolved).catch(() => null);
112146
- if (real === null || real !== root && !real.startsWith(`${root}${path245.sep}`)) {
112488
+ if (real === null || real !== root && !real.startsWith(`${root}${path246.sep}`)) {
112147
112489
  return null;
112148
112490
  }
112149
112491
  try {
@@ -112275,7 +112617,7 @@ function refutedClassification(state) {
112275
112617
  return state === "dismissed-incorrect" ? "false_positive" : "out_of_scope";
112276
112618
  }
112277
112619
  async function recordDispositions(packageDir, records) {
112278
- const findingsPath = path245.join(packageDir, "findings.json");
112620
+ const findingsPath = path246.join(packageDir, "findings.json");
112279
112621
  const findings = JSON.parse(await readFile117(findingsPath, "utf8"));
112280
112622
  const schema = await loadSchema("review-finding");
112281
112623
  const dispositionSchema = schema.properties?.["disposition"];
@@ -112602,7 +112944,7 @@ function upsertPreFilterScopeBlock(text, block) {
112602
112944
  ${body}`;
112603
112945
  }
112604
112946
  async function readPreFilterScopeBlock(packageDir) {
112605
- const scopePath = path245.join(packageDir, "scope.md");
112947
+ const scopePath = path246.join(packageDir, "scope.md");
112606
112948
  if (!await pathExists(scopePath)) {
112607
112949
  return;
112608
112950
  }
@@ -112776,39 +113118,39 @@ function matchReason(flow, dir, kind, ref) {
112776
113118
  async function missingArtifacts(packageDir) {
112777
113119
  const missing = [];
112778
113120
  for (const artifact of ["scope.md", "coverage.md", "report.md", "findings.json", "learning.md", "decisions.md"]) {
112779
- if (!await pathExists(path245.join(packageDir, artifact))) {
113121
+ if (!await pathExists(path246.join(packageDir, artifact))) {
112780
113122
  missing.push(artifact);
112781
113123
  }
112782
113124
  }
112783
113125
  return missing;
112784
113126
  }
112785
113127
  async function resolveReviewPackagePath(cwd, ref) {
112786
- const absolute = path245.resolve(cwd, ref);
112787
- if (await pathExists(path245.join(absolute, "manifest.json"))) {
113128
+ const absolute = path246.resolve(cwd, ref);
113129
+ if (await pathExists(path246.join(absolute, "manifest.json"))) {
112788
113130
  return absolute;
112789
113131
  }
112790
113132
  if (await pathExists(absolute) && absolute.endsWith("manifest.json")) {
112791
- return path245.dirname(absolute);
113133
+ return path246.dirname(absolute);
112792
113134
  }
112793
- const standalone = path245.join(reviewsRoot(cwd), ref);
112794
- if (await pathExists(path245.join(standalone, "manifest.json"))) {
113135
+ const standalone = path246.join(reviewsRoot(cwd), ref);
113136
+ if (await pathExists(path246.join(standalone, "manifest.json"))) {
112795
113137
  return standalone;
112796
113138
  }
112797
113139
  for (const flowDir of await listFlowDirs(cwd)) {
112798
- const reviewsDir = path245.join(flowsRoot(cwd), flowDir, "reviews");
113140
+ const reviewsDir = path246.join(flowsRoot(cwd), flowDir, "reviews");
112799
113141
  if (!await pathExists(reviewsDir)) {
112800
113142
  continue;
112801
113143
  }
112802
113144
  for (const entry of await readdir38(reviewsDir, { withFileTypes: true })) {
112803
113145
  if (entry.isDirectory() && entry.name === ref) {
112804
- return path245.join(reviewsDir, entry.name);
113146
+ return path246.join(reviewsDir, entry.name);
112805
113147
  }
112806
113148
  }
112807
113149
  }
112808
113150
  throw new Error(`Managed review package not found: ${ref}`);
112809
113151
  }
112810
113152
  async function loadDocpackSchema(cwd) {
112811
- const schemaPath = path245.join(cwd, "docs", "requirements", "managed-review-feedback-loop", "schemas", "managed-review-package.schema.json");
113153
+ const schemaPath = path246.join(cwd, "docs", "requirements", "managed-review-feedback-loop", "schemas", "managed-review-package.schema.json");
112812
113154
  if (!await pathExists(schemaPath)) {
112813
113155
  return null;
112814
113156
  }
@@ -112914,7 +113256,7 @@ function checkPullOpen(facts) {
112914
113256
 
112915
113257
  // src/review/bot/packages.ts
112916
113258
  import { readdir as readdir39, readFile as readFile118 } from "fs/promises";
112917
- import path246 from "path";
113259
+ import path247 from "path";
112918
113260
  async function subdirectories2(dir) {
112919
113261
  try {
112920
113262
  const entries = await readdir39(dir, { withFileTypes: true });
@@ -112937,12 +113279,12 @@ function pullFromTarget(target) {
112937
113279
  }
112938
113280
  async function readPackage(dir) {
112939
113281
  try {
112940
- const manifest = JSON.parse(await readFile118(path246.join(dir, "manifest.json"), "utf8"));
113282
+ const manifest = JSON.parse(await readFile118(path247.join(dir, "manifest.json"), "utf8"));
112941
113283
  if (manifest.target?.kind !== "pr")
112942
113284
  return null;
112943
113285
  let findings = [];
112944
113286
  try {
112945
- const parsed = JSON.parse(await readFile118(path246.join(dir, "findings.json"), "utf8"));
113287
+ const parsed = JSON.parse(await readFile118(path247.join(dir, "findings.json"), "utf8"));
112946
113288
  if (Array.isArray(parsed))
112947
113289
  findings = parsed;
112948
113290
  } catch {
@@ -112957,16 +113299,16 @@ async function readPackage(dir) {
112957
113299
  function candidateDirs2(cwd) {
112958
113300
  return (async () => {
112959
113301
  const dirs = [];
112960
- const reviews = path246.join(cwd, ".metaproject", "reviews");
113302
+ const reviews = path247.join(cwd, ".metaproject", "reviews");
112961
113303
  for (const name of await subdirectories2(reviews)) {
112962
113304
  if (name !== "bot")
112963
- dirs.push(path246.join(reviews, name));
113305
+ dirs.push(path247.join(reviews, name));
112964
113306
  }
112965
- const flows = path246.join(cwd, ".metaproject", "flows");
113307
+ const flows = path247.join(cwd, ".metaproject", "flows");
112966
113308
  for (const flow of await subdirectories2(flows)) {
112967
- const nested = path246.join(flows, flow, "reviews");
113309
+ const nested = path247.join(flows, flow, "reviews");
112968
113310
  for (const name of await subdirectories2(nested))
112969
- dirs.push(path246.join(nested, name));
113311
+ dirs.push(path247.join(nested, name));
112970
113312
  }
112971
113313
  return dirs;
112972
113314
  })();
@@ -112983,7 +113325,7 @@ async function listReviewPackages(cwd) {
112983
113325
  }
112984
113326
  async function loadReviewPackage(cwd, reviewId) {
112985
113327
  for (const dir of await candidateDirs2(cwd)) {
112986
- if (path246.basename(dir) !== reviewId)
113328
+ if (path247.basename(dir) !== reviewId)
112987
113329
  continue;
112988
113330
  const listed = await readPackage(dir);
112989
113331
  if (listed !== null && listed.reviewId === reviewId)
@@ -112994,7 +113336,7 @@ async function loadReviewPackage(cwd, reviewId) {
112994
113336
  var init_packages = () => {};
112995
113337
 
112996
113338
  // src/review/bot/run.ts
112997
- import path247 from "path";
113339
+ import path248 from "path";
112998
113340
  function capDiff(diff, maxBytes) {
112999
113341
  const totalBytes = Buffer.byteLength(diff);
113000
113342
  if (totalBytes <= maxBytes)
@@ -113039,7 +113381,7 @@ function parseVerifierVerdict2(reply) {
113039
113381
  return unverifiable;
113040
113382
  }
113041
113383
  function botStatePath(cwd, repo, number) {
113042
- return path247.join(cwd, ".metaproject", "reviews", "bot", `${repo.replace(/\//g, "__")}__${number}.json`);
113384
+ return path248.join(cwd, ".metaproject", "reviews", "bot", `${repo.replace(/\//g, "__")}__${number}.json`);
113043
113385
  }
113044
113386
  function emptyBotState(repo, number) {
113045
113387
  return { schemaVersion: 1, repo, number, reviews: [], pull: null };
@@ -113662,9 +114004,9 @@ var init_reviews_inspector = __esm(() => {
113662
114004
 
113663
114005
  // src/lib/serve-config.ts
113664
114006
  import { existsSync as existsSync42 } from "fs";
113665
- import path248 from "path";
114007
+ import path249 from "path";
113666
114008
  function serveConfigPath(dir) {
113667
- return path248.join(keryxConfigDir(dir), "serve.json");
114009
+ return path249.join(keryxConfigDir(dir), "serve.json");
113668
114010
  }
113669
114011
  function parseIpv4(value) {
113670
114012
  const parts = value.split(".");
@@ -113790,7 +114132,7 @@ function projectServeConfig(value, onDrop) {
113790
114132
  if (!isRecord12(value)) {
113791
114133
  return null;
113792
114134
  }
113793
- reportUndeclared(value, CONFIG_KEYS, "", onDrop);
114135
+ reportUndeclared(value, CONFIG_KEYS2, "", onDrop);
113794
114136
  if (value.schemaVersion !== SERVE_CONFIG_SCHEMA_VERSION) {
113795
114137
  return null;
113796
114138
  }
@@ -113962,10 +114304,10 @@ function saveServeConfig(config, dir, onWarn) {
113962
114304
  return false;
113963
114305
  }
113964
114306
  }
113965
- var SERVE_CONFIG_SCHEMA_VERSION = "1.0.0", DEFAULT_SERVE_BIND_ADDRESS = "127.0.0.1", DEFAULT_SERVE_PORT = 7377, DEFAULT_SERVE_PROFILE = "remote-restricted", CONFIG_KEYS, BIND_KEYS, CREDENTIAL_REF_KEYS, APPROVAL_KEYS, BOUNDS_KEYS;
114307
+ var SERVE_CONFIG_SCHEMA_VERSION = "1.0.0", DEFAULT_SERVE_BIND_ADDRESS = "127.0.0.1", DEFAULT_SERVE_PORT = 7377, DEFAULT_SERVE_PROFILE = "remote-restricted", CONFIG_KEYS2, BIND_KEYS, CREDENTIAL_REF_KEYS, APPROVAL_KEYS, BOUNDS_KEYS;
113966
114308
  var init_serve_config = __esm(() => {
113967
114309
  init_config_dir();
113968
- CONFIG_KEYS = [
114310
+ CONFIG_KEYS2 = [
113969
114311
  "schemaVersion",
113970
114312
  "enabled",
113971
114313
  "bind",
@@ -114101,25 +114443,6 @@ var init_protocol2 = __esm(() => {
114101
114443
  MESSAGE_STATES = ["reading", "working", "done", "failed"];
114102
114444
  });
114103
114445
 
114104
- // src/remote/paths.ts
114105
- import path249 from "path";
114106
- function remoteDirPath(dir) {
114107
- return path249.join(keryxConfigDir(dir), REMOTE_DIRNAME);
114108
- }
114109
- function ensureRemoteDir(dir, ...sub) {
114110
- return ensureKeryxSubdir([REMOTE_DIRNAME, ...sub], dir);
114111
- }
114112
- function botTokenPath(dir) {
114113
- return path249.join(remoteDirPath(dir), BOT_TOKEN_FILE);
114114
- }
114115
- function remoteConfigPath(dir) {
114116
- return path249.join(remoteDirPath(dir), REMOTE_CONFIG_FILE);
114117
- }
114118
- var REMOTE_DIRNAME = "remote", BOT_TOKEN_FILE = "bot-token", REMOTE_CONFIG_FILE = "config.json", REGISTRY_FILE = "sessions.json", SERVICE_TOPICS_FILE = "service-topics.json", POLLER_STATE_FILE = "poller-state.json", OUTBOUND_FILE = "outbound.jsonl", REJECTED_JOURNAL_FILE = "rejected.jsonl", INBOUND_DIRNAME = "inbound";
114119
- var init_paths4 = __esm(() => {
114120
- init_config_dir();
114121
- });
114122
-
114123
114446
  // src/remote/endpoint.ts
114124
114447
  import path250 from "path";
114125
114448
  function endpointPath(dir) {
@@ -114158,8 +114481,8 @@ import {
114158
114481
  fsyncSync as fsyncSync2,
114159
114482
  openSync as openSync5,
114160
114483
  renameSync as renameSync5,
114161
- statSync as statSync15,
114162
- unlinkSync as unlinkSync5,
114484
+ statSync as statSync16,
114485
+ unlinkSync as unlinkSync6,
114163
114486
  writeFileSync as writeFileSync8
114164
114487
  } from "fs";
114165
114488
  import path251 from "path";
@@ -114193,7 +114516,7 @@ function isGroupOrOtherAccessible(file) {
114193
114516
  if (process.platform === "win32") {
114194
114517
  return false;
114195
114518
  }
114196
- const mode = statSync15(file, { throwIfNoEntry: false })?.mode;
114519
+ const mode = statSync16(file, { throwIfNoEntry: false })?.mode;
114197
114520
  return mode !== undefined && (mode & 63) !== 0;
114198
114521
  }
114199
114522
  function readServeCredential(dir) {
@@ -114250,7 +114573,7 @@ function writeStore(store, dir) {
114250
114573
  return true;
114251
114574
  } catch {
114252
114575
  try {
114253
- unlinkSync5(temp);
114576
+ unlinkSync6(temp);
114254
114577
  } catch {}
114255
114578
  return false;
114256
114579
  }
@@ -114326,7 +114649,7 @@ var init_serve_credential = __esm(() => {
114326
114649
 
114327
114650
  // src/remote/shell-token.ts
114328
114651
  import { createHash as createHash64, createHmac as createHmac5, randomBytes as randomBytes12 } from "crypto";
114329
- import { statSync as statSync16 } from "fs";
114652
+ import { statSync as statSync17 } from "fs";
114330
114653
  import path252 from "path";
114331
114654
  function shellTokenPath(dir) {
114332
114655
  return path252.join(remoteDirPath(dir), SHELL_TOKEN_FILE);
@@ -114334,7 +114657,7 @@ function shellTokenPath(dir) {
114334
114657
  function readValidated(file) {
114335
114658
  if (process.platform !== "win32") {
114336
114659
  try {
114337
- const mode = statSync16(file).mode & 511;
114660
+ const mode = statSync17(file).mode & 511;
114338
114661
  if ((mode & 63) !== 0) {
114339
114662
  return { ok: false, reason: `shell token file ${file} is accessible to other users (mode ${mode.toString(8)}); run: chmod 600 ${file}` };
114340
114663
  }
@@ -114347,7 +114670,7 @@ function readValidated(file) {
114347
114670
  return { ok: false, reason: read.reason === "absent" ? `no shell token at ${file}; is \`keryx serve\` running with remote control configured?` : `shell token file ${file} cannot be read (${read.reason})` };
114348
114671
  }
114349
114672
  const token = read.text.trim();
114350
- if (!TOKEN_SHAPE.test(token)) {
114673
+ if (!TOKEN_SHAPE2.test(token)) {
114351
114674
  return { ok: false, reason: `shell token file ${file} does not hold a shell token; delete it and restart \`keryx serve\`` };
114352
114675
  }
114353
114676
  return { ok: true, value: token };
@@ -114412,12 +114735,12 @@ function createShellTokenVerifier(token) {
114412
114735
  return constantTimeEqual(createHash64("sha256").update(presented, "utf8").digest(), expected);
114413
114736
  };
114414
114737
  }
114415
- var SHELL_TOKEN_FILE = "shell-token", TOKEN_SHAPE, SERVE_PROOF_HEADER = "x-keryx-serve-proof", DERIVED_BEARER_PREFIX = "ksp1.", AUTH_DOMAIN = "keryx-shell-auth-v1", PROOF_DOMAIN = "keryx-serve-proof-v1", NONCE_SHAPE, MAC_SHAPE;
114738
+ var SHELL_TOKEN_FILE = "shell-token", TOKEN_SHAPE2, SERVE_PROOF_HEADER = "x-keryx-serve-proof", DERIVED_BEARER_PREFIX = "ksp1.", AUTH_DOMAIN = "keryx-shell-auth-v1", PROOF_DOMAIN = "keryx-serve-proof-v1", NONCE_SHAPE, MAC_SHAPE;
114416
114739
  var init_shell_token = __esm(() => {
114417
114740
  init_config_dir();
114418
114741
  init_serve_credential();
114419
114742
  init_paths4();
114420
- TOKEN_SHAPE = /^[A-Za-z0-9_-]{32,128}$/;
114743
+ TOKEN_SHAPE2 = /^[A-Za-z0-9_-]{32,128}$/;
114421
114744
  NONCE_SHAPE = /^[A-Za-z0-9_-]{22,64}$/;
114422
114745
  MAC_SHAPE = /^[A-Za-z0-9_-]{43}$/;
114423
114746
  });
@@ -115865,162 +116188,6 @@ var init_shell_bridge = __esm(() => {
115865
116188
  init_protocol2();
115866
116189
  });
115867
116190
 
115868
- // src/remote/config.ts
115869
- import { statSync as statSync17, unlinkSync as unlinkSync6 } from "fs";
115870
- function isSafeInt(value) {
115871
- return typeof value === "number" && Number.isSafeInteger(value);
115872
- }
115873
- function parseRemoteConfig(value) {
115874
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
115875
- return { ok: false, reason: "remote config must be a JSON object" };
115876
- }
115877
- const doc = value;
115878
- for (const key of Object.keys(doc)) {
115879
- if (!CONFIG_KEYS2.has(key)) {
115880
- const shown = /^[A-Za-z_][A-Za-z0-9_]{0,30}$/.test(key) ? key : "<not shown>";
115881
- return { ok: false, reason: `remote config has an unknown key "${shown}" (the schema is closed)` };
115882
- }
115883
- }
115884
- if (doc.schemaVersion !== REMOTE_CONFIG_SCHEMA_VERSION) {
115885
- return { ok: false, reason: `remote config schemaVersion must be ${REMOTE_CONFIG_SCHEMA_VERSION}` };
115886
- }
115887
- if (!isSafeInt(doc.chatId) || doc.chatId === 0) {
115888
- return { ok: false, reason: "remote config chatId must be a non-zero integer (the supergroup id)" };
115889
- }
115890
- const ids = doc.allowedUserIds;
115891
- if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => isSafeInt(id) && id > 0)) {
115892
- return { ok: false, reason: "remote config allowedUserIds must be a non-empty array of positive integers" };
115893
- }
115894
- const timeout = (key, fallback) => {
115895
- const raw = doc[key];
115896
- if (raw === undefined) {
115897
- return { ok: true, value: fallback };
115898
- }
115899
- if (!isSafeInt(raw) || raw < 1 || raw > MAX_TIMEOUT_MS3) {
115900
- return { ok: false, reason: `remote config ${key} must be an integer from 1 to ${MAX_TIMEOUT_MS3} (milliseconds)` };
115901
- }
115902
- return { ok: true, value: raw };
115903
- };
115904
- const orphan = timeout("orphanMs", DEFAULT_ORPHAN_MS);
115905
- if (!orphan.ok) {
115906
- return orphan;
115907
- }
115908
- const run = timeout("runTimeoutMs", DEFAULT_RUN_TIMEOUT_MS);
115909
- if (!run.ok) {
115910
- return run;
115911
- }
115912
- return {
115913
- ok: true,
115914
- value: {
115915
- schemaVersion: REMOTE_CONFIG_SCHEMA_VERSION,
115916
- chatId: doc.chatId,
115917
- allowedUserIds: [...new Set(ids)],
115918
- orphanMs: orphan.value,
115919
- runTimeoutMs: run.value
115920
- }
115921
- };
115922
- }
115923
- function loadRemoteConfig(dir) {
115924
- const file = remoteConfigPath(dir);
115925
- const read = readConfigFile(file);
115926
- if (!read.ok) {
115927
- return {
115928
- ok: false,
115929
- reason: read.reason === "absent" ? `no remote-control config at ${file}; create it with chatId and allowedUserIds` : `remote-control config at ${file} cannot be read (${read.reason})`
115930
- };
115931
- }
115932
- let parsed;
115933
- try {
115934
- parsed = JSON.parse(read.text);
115935
- } catch {
115936
- return { ok: false, reason: `remote-control config at ${file} is not valid JSON` };
115937
- }
115938
- return parseRemoteConfig(parsed);
115939
- }
115940
- function saveRemoteConfig(config, dir) {
115941
- const checked = parseRemoteConfig(config);
115942
- if (!checked.ok) {
115943
- return checked;
115944
- }
115945
- ensureRemoteDir(dir);
115946
- writeOwnerOnlyFileAtomic(remoteConfigPath(dir), `${JSON.stringify(checked.value, null, 2)}
115947
- `);
115948
- return checked;
115949
- }
115950
- function loadBotToken(dir) {
115951
- const file = botTokenPath(dir);
115952
- let mode;
115953
- try {
115954
- const stats = statSync17(file);
115955
- if (!stats.isFile()) {
115956
- return { ok: false, reason: `bot token path ${file} is not a regular file` };
115957
- }
115958
- mode = stats.mode & 511;
115959
- } catch {
115960
- return {
115961
- ok: false,
115962
- reason: `no bot token file at ${file}; put the token on one line in it and run: chmod 600 ${file}`
115963
- };
115964
- }
115965
- if (process.platform !== "win32" && (mode & 63) !== 0) {
115966
- return {
115967
- ok: false,
115968
- reason: `bot token file ${file} is accessible to other users (mode ${mode.toString(8)}); run: chmod 600 ${file}`
115969
- };
115970
- }
115971
- const read = readConfigFile(file);
115972
- if (!read.ok) {
115973
- return { ok: false, reason: `bot token file ${file} cannot be read (${read.reason})` };
115974
- }
115975
- const token = read.text.trim();
115976
- if (!TOKEN_SHAPE2.test(token)) {
115977
- return {
115978
- ok: false,
115979
- reason: `bot token file ${file} does not hold a bot token (expected <digits>:<secret> on a single line)`
115980
- };
115981
- }
115982
- return { ok: true, value: token };
115983
- }
115984
- function saveBotToken(token, dir) {
115985
- const trimmed = token.trim();
115986
- if (!TOKEN_SHAPE2.test(trimmed)) {
115987
- return { ok: false, reason: "that does not look like a bot token (expected <digits>:<secret>, as BotFather gives it)" };
115988
- }
115989
- const file = botTokenPath(dir);
115990
- try {
115991
- ensureRemoteDir(dir);
115992
- writeOwnerOnlyFileAtomic(file, `${trimmed}
115993
- `);
115994
- } catch {
115995
- return { ok: false, reason: `could not write the bot token file at ${file}` };
115996
- }
115997
- return { ok: true, value: true };
115998
- }
115999
- function removeFile2(file) {
116000
- try {
116001
- unlinkSync6(file);
116002
- return true;
116003
- } catch (error) {
116004
- return error.code === "ENOENT";
116005
- }
116006
- }
116007
- function removeBotToken(dir) {
116008
- return removeFile2(botTokenPath(dir));
116009
- }
116010
- function removeRemoteConfig(dir) {
116011
- return removeFile2(remoteConfigPath(dir));
116012
- }
116013
- var REMOTE_CONFIG_SCHEMA_VERSION = 1, DEFAULT_ORPHAN_MS, DEFAULT_RUN_TIMEOUT_MS, MAX_TIMEOUT_MS3, CONFIG_KEYS2, TOKEN_SHAPE2;
116014
- var init_config11 = __esm(() => {
116015
- init_config_dir();
116016
- init_paths4();
116017
- DEFAULT_ORPHAN_MS = 10 * 60000;
116018
- DEFAULT_RUN_TIMEOUT_MS = 30 * 60000;
116019
- MAX_TIMEOUT_MS3 = 7 * 24 * 60 * 60000;
116020
- CONFIG_KEYS2 = new Set(["schemaVersion", "chatId", "allowedUserIds", "orphanMs", "runTimeoutMs"]);
116021
- TOKEN_SHAPE2 = /^\d{5,}:[A-Za-z0-9_-]{20,}$/;
116022
- });
116023
-
116024
116191
  // src/remote/channels-client.ts
116025
116192
  function fileExists(file) {
116026
116193
  const read = readConfigFile(file);
@@ -116592,6 +116759,7 @@ async function loadChannelsSnapshot(client) {
116592
116759
  configured: telegram.state !== "not-connected" || local.tokenFile || local.configFile,
116593
116760
  machine,
116594
116761
  sessions: telegram.sessions,
116762
+ ...telegram.rendering !== undefined ? { rendering: telegram.rendering } : {},
116595
116763
  ...telegram.reason !== undefined ? { reason: telegram.reason } : {}
116596
116764
  };
116597
116765
  }
@@ -116622,6 +116790,19 @@ function projectChannelsRow(snapshot, width) {
116622
116790
  return { text: fit2("off", width), role: "muted", action: "open" };
116623
116791
  }
116624
116792
  }
116793
+ function clock(at) {
116794
+ return `${new Date(at).toISOString().slice(11, 19)} UTC`;
116795
+ }
116796
+ function renderingLines(rendering) {
116797
+ if (rendering === undefined)
116798
+ return [];
116799
+ const last = rendering.lastFallback;
116800
+ const step = last?.step === "rich-to-html" ? "rich to HTML" : "HTML to plain text";
116801
+ return [
116802
+ `Rendering: ${rendering.mode} (${RENDERING_MEANING2[rendering.mode]}). Change it in /settings.`,
116803
+ last === undefined ? "Last fallback: none since keryx serve started." : `Last fallback: ${step} at ${clock(last.at)}: ${last.reason}`
116804
+ ];
116805
+ }
116625
116806
  function channelsStatusLines(snapshot) {
116626
116807
  const machine = snapshot.machine === undefined ? [] : [`Machine: ${snapshot.machine}`];
116627
116808
  switch (snapshot.state) {
@@ -116645,6 +116826,7 @@ function channelsStatusLines(snapshot) {
116645
116826
  "Telegram: connected",
116646
116827
  ...machine,
116647
116828
  `Sessions in Telegram: ${snapshot.sessions ?? 0}`,
116829
+ ...renderingLines(snapshot.rendering),
116648
116830
  "Test sends one message to the General topic. Disconnect deletes the topics and erases the token.",
116649
116831
  "Sessions join through /remote-control in each shell."
116650
116832
  ];
@@ -116653,6 +116835,7 @@ function channelsStatusLines(snapshot) {
116653
116835
  "Telegram: configured, but not running",
116654
116836
  ...machine,
116655
116837
  ...snapshot.reason === undefined ? [] : [`Why: ${snapshot.reason}`],
116838
+ ...renderingLines(snapshot.rendering),
116656
116839
  "Retry starts it again from the saved token and config. Disconnect erases them. Test works once Telegram is running."
116657
116840
  ];
116658
116841
  case "serve-down":
@@ -117325,7 +117508,7 @@ function mountChannelsPanel(otui, renderer, parent, options) {
117325
117508
  }
117326
117509
  };
117327
117510
  }
117328
- var CHANNELS_COMMAND = "/channels", CHANNELS_USAGE, CHANNELS_FOOTER, TOKEN_CHAR, TOKEN_MAX = 200, PAIRING_POLL_MS = 2000, swallow = (event) => {
117511
+ var CHANNELS_COMMAND = "/channels", CHANNELS_USAGE, RENDERING_MEANING2, CHANNELS_FOOTER, TOKEN_CHAR, TOKEN_MAX = 200, PAIRING_POLL_MS = 2000, swallow = (event) => {
117329
117512
  event.preventDefault?.();
117330
117513
  event.stopPropagation?.();
117331
117514
  }, CHANNELS_ROW_POLL_MS = 1e4;
@@ -117337,6 +117520,12 @@ var init_channels_surface = __esm(() => {
117337
117520
  init_theme_repaint();
117338
117521
  init_theme_text();
117339
117522
  CHANNELS_USAGE = `Usage: ${CHANNELS_COMMAND} [status]`;
117523
+ RENDERING_MEANING2 = {
117524
+ auto: "rich for a reply with a table, HTML otherwise",
117525
+ rich: "every reply as a rich message",
117526
+ html: "every reply as Telegram HTML",
117527
+ plain: "every reply as plain text"
117528
+ };
117340
117529
  CHANNELS_FOOTER = [
117341
117530
  { key: "c", label: "connect" },
117342
117531
  { key: "t", label: "test" },
@@ -117397,19 +117586,19 @@ function formatRemoteStatusLines(status) {
117397
117586
  lines.push(`Turn it off with ${REMOTE_CONTROL_COMMAND} off, or press o here. Rename: ${REMOTE_CONTROL_COMMAND} off, then ${REMOTE_CONTROL_COMMAND} <name>.`);
117398
117587
  return lines;
117399
117588
  }
117400
- function clock(at) {
117589
+ function clock2(at) {
117401
117590
  return new Date(at).toISOString().slice(11, 19);
117402
117591
  }
117403
117592
  function formatRemoteEventLines(status) {
117404
117593
  if (status.events.length === 0)
117405
117594
  return ["No remote events yet."];
117406
- return [...status.events].reverse().slice(0, REMOTE_EVENT_LIMIT).map((event) => `${clock(event.at)} ${event.kind.padEnd(8)} ${event.text}`);
117595
+ return [...status.events].reverse().slice(0, REMOTE_EVENT_LIMIT).map((event) => `${clock2(event.at)} ${event.kind.padEnd(8)} ${event.text}`);
117407
117596
  }
117408
117597
  function formatRemoteCommandLines(status) {
117409
117598
  const commands = status.events.filter((event) => event.kind === "command");
117410
117599
  if (commands.length === 0)
117411
117600
  return ["No remote commands yet."];
117412
- return [...commands].reverse().slice(0, REMOTE_EVENT_LIMIT).map((event) => `${clock(event.at)} ${event.text}`);
117601
+ return [...commands].reverse().slice(0, REMOTE_EVENT_LIMIT).map((event) => `${clock2(event.at)} ${event.text}`);
117413
117602
  }
117414
117603
  function renderRemoteControlText(status) {
117415
117604
  return `${[...formatRemoteStatusLines(status), "", "Recent events:", ...formatRemoteEventLines(status)].join(`
@@ -117805,6 +117994,11 @@ var init_agent_commands = __esm(() => {
117805
117994
  description: "Connect, test or disconnect Telegram for this machine: /channels [status]",
117806
117995
  modes: AGENT_ONLY
117807
117996
  },
117997
+ {
117998
+ name: "/rendering",
117999
+ description: "How Telegram replies are written: /rendering [auto|rich|html|plain]",
118000
+ modes: AGENT_ONLY
118001
+ },
117808
118002
  {
117809
118003
  name: "/product",
117810
118004
  description: "Intents closed in code that nobody looked back at, with their outcome criteria",
@@ -137861,6 +138055,9 @@ async function runSettingsCommand(command, handlers) {
137861
138055
  case "/external-agents":
137862
138056
  handlers.onSystem(await handlers.externalAgents(arg));
137863
138057
  return;
138058
+ case "/rendering":
138059
+ handlers.onSystem(handlers.rendering(arg));
138060
+ return;
137864
138061
  case "/reasoning":
137865
138062
  handlers.reasoning(arg);
137866
138063
  return;
@@ -152860,6 +153057,7 @@ ${formatThemeList(getThemeId())}`);
152860
153057
  route: setRoutingEnabled,
152861
153058
  external: setExternalEnabled,
152862
153059
  externalAgents: (arg) => runExternalAgentsCommand(arg, sessionCwd),
153060
+ rendering: (arg) => runRenderingCommand(arg),
152863
153061
  reasoning: runReasoningCommand,
152864
153062
  think: runThinkCommand,
152865
153063
  theme: runThemeCommand,
@@ -153952,6 +154150,11 @@ ${formatThemeList(getThemeId())}`);
153952
154150
  showEditGuard();
153953
154151
  return;
153954
154152
  }
154153
+ if (isRenderingCommand(command.name)) {
154154
+ const arg = line.trim().split(/\s+/).slice(1).join(" ");
154155
+ io.onSystem?.(runRenderingCommand(arg));
154156
+ return;
154157
+ }
153955
154158
  if (isExternalAgentsCommand(command.name)) {
153956
154159
  const arg = line.trim().split(/\s+/).slice(1).join(" ");
153957
154160
  trackCommandWork(() => runExternalAgentsCommand(arg, opts.session?.cwd ?? process.cwd()).then((text) => io.onSystem?.(text)));
@@ -154791,6 +154994,7 @@ var init_tui_shell = __esm(() => {
154791
154994
  init_turn_guard();
154792
154995
  init_routing_classifier_source();
154793
154996
  init_external_command();
154997
+ init_rendering_command();
154794
154998
  init_external_switch();
154795
154999
  init_external_providers();
154796
155000
  init_jev_client();
@@ -157366,6 +157570,8 @@ ${GUTTER}${turnSeparator()}
157366
157570
  agentIo.onSystem?.(readlineRemoteControlText(line));
157367
157571
  } else if (isChannelsCommand(command)) {
157368
157572
  agentIo.onSystem?.(readlineChannelsText(line, await loadChannelsSnapshot(new ChannelsClient)));
157573
+ } else if (isRenderingCommand(command)) {
157574
+ agentIo.onSystem?.(runRenderingCommand(rest));
157369
157575
  } else if (isSessionInfoCommand(command)) {
157370
157576
  const cwd = sessionCwd;
157371
157577
  const [workspaces, flows] = await Promise.all([
@@ -158488,6 +158694,7 @@ var init_shell = __esm(() => {
158488
158694
  init_statusbar();
158489
158695
  init_metaproject_state();
158490
158696
  init_live_render();
158697
+ init_rendering_command();
158491
158698
  init_settings_model();
158492
158699
  init_settings_state();
158493
158700
  init_theme();
@@ -158568,6 +158775,7 @@ var init_shell = __esm(() => {
158568
158775
  "/reviews",
158569
158776
  "/remote-control",
158570
158777
  "/channels",
158778
+ "/rendering",
158571
158779
  "/theme",
158572
158780
  "/settings",
158573
158781
  "/mode",
@@ -181416,6 +181624,25 @@ var init_command_registry = __esm(() => {
181416
181624
  "does not touch content already relayed to an agent, exported copies, or git history"
181417
181625
  ]
181418
181626
  },
181627
+ {
181628
+ module: "remote",
181629
+ command: "remote format-sample",
181630
+ summary: "Print the fixed sample reply (a table, lists, a rule) the way each Telegram rendering mode sends it. No network, no token.",
181631
+ intent: [
181632
+ "\u043A\u0430\u043A \u0432\u044B\u0433\u043B\u044F\u0434\u0438\u0442 \u043E\u0442\u0432\u0435\u0442 \u0432 \u0442\u0435\u043B\u0435\u0433\u0440\u0430\u043C\u0435",
181633
+ "remote format-sample",
181634
+ "preview telegram rendering",
181635
+ "how are tables rendered in telegram",
181636
+ "show the telegram sample reply"
181637
+ ],
181638
+ args: [
181639
+ { name: "mode", type: "string", required: false, desc: "only this mode: auto, rich, html or plain" },
181640
+ { name: "full", type: "bool", required: false, desc: "print the whole rich message instead of a count of its blocks" },
181641
+ { name: "json", type: "bool", required: false, desc: "structured sample source and per-mode parts" }
181642
+ ],
181643
+ json: true,
181644
+ read: true
181645
+ },
181419
181646
  {
181420
181647
  module: "bus",
181421
181648
  command: "bus list",
@@ -192710,6 +192937,24 @@ function createHttpBotApi(options) {
192710
192937
  }
192711
192938
  return call("sendMessage", body);
192712
192939
  },
192940
+ async sendRichMessage(params) {
192941
+ const body = { chat_id: params.chatId, rich_message: params.richMessage };
192942
+ if (params.messageThreadId !== undefined) {
192943
+ body.message_thread_id = params.messageThreadId;
192944
+ }
192945
+ if (params.inlineKeyboard !== undefined) {
192946
+ body.reply_markup = { inline_keyboard: params.inlineKeyboard };
192947
+ }
192948
+ return call("sendRichMessage", body);
192949
+ },
192950
+ async editRichMessage(params) {
192951
+ await call("editMessageText", {
192952
+ chat_id: params.chatId,
192953
+ message_id: params.messageId,
192954
+ rich_message: params.richMessage,
192955
+ reply_markup: { inline_keyboard: params.inlineKeyboard ?? [] }
192956
+ });
192957
+ },
192713
192958
  async editMessageReplyMarkup(params) {
192714
192959
  await call("editMessageReplyMarkup", {
192715
192960
  chat_id: params.chatId,
@@ -192790,6 +193035,187 @@ var init_bot_api_http = __esm(() => {
192790
193035
  init_types11();
192791
193036
  });
192792
193037
 
193038
+ // src/remote/format-blocks.ts
193039
+ var HEADING2, BULLET, ORDERED, QUOTE, RULE, TASK, UNCHECKED_BOX = "\u2610", CHECKED_BOX = "\u2611", RULE_GLYPH = "\u2500", RULE_LENGTH = 12;
193040
+ var init_format_blocks = __esm(() => {
193041
+ HEADING2 = /^ {0,3}#{1,6}[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/;
193042
+ BULLET = /^([ \t]*)[-*][ \t]+(.*)$/;
193043
+ ORDERED = /^([ \t]*)(\d{1,9})([.)])[ \t]+(.*)$/;
193044
+ QUOTE = /^ {0,3}>(?: (.*))?$/;
193045
+ RULE = /^ {0,3}-{3,}[ \t]*$/;
193046
+ TASK = /^\[([ xX])\](?:[ \t]+(.*))?$/;
193047
+ });
193048
+
193049
+ // src/remote/format-table.ts
193050
+ function splitRow(line) {
193051
+ const cells = [];
193052
+ let current = "";
193053
+ let sawPipe = false;
193054
+ for (let index = 0;index < line.length; index += 1) {
193055
+ const char = line[index];
193056
+ if (char === "\\" && line[index + 1] === "|") {
193057
+ current += "|";
193058
+ index += 1;
193059
+ continue;
193060
+ }
193061
+ if (char === "|") {
193062
+ sawPipe = true;
193063
+ cells.push(current);
193064
+ current = "";
193065
+ continue;
193066
+ }
193067
+ current += char;
193068
+ }
193069
+ if (!sawPipe) {
193070
+ return;
193071
+ }
193072
+ cells.push(current);
193073
+ if (cells[0]?.trim() === "") {
193074
+ cells.shift();
193075
+ }
193076
+ if (cells.length > 0 && cells[cells.length - 1]?.trim() === "") {
193077
+ cells.pop();
193078
+ }
193079
+ return cells.map((cell) => cell.trim());
193080
+ }
193081
+ function separatorAlign(line) {
193082
+ const cells = splitRow(line);
193083
+ if (cells === undefined || cells.length === 0) {
193084
+ return;
193085
+ }
193086
+ const align = [];
193087
+ for (const cell of cells) {
193088
+ const match = SEPARATOR_CELL.exec(cell);
193089
+ if (match === null) {
193090
+ return;
193091
+ }
193092
+ align.push(match[1] !== undefined && match[2] !== undefined ? "center" : match[2] !== undefined ? "right" : "left");
193093
+ }
193094
+ return align;
193095
+ }
193096
+ function endsTable(line) {
193097
+ return line.trim().length === 0 || !line.includes("|");
193098
+ }
193099
+ function fitRow(cells, width) {
193100
+ const row = cells.slice(0, width);
193101
+ while (row.length < width) {
193102
+ row.push("");
193103
+ }
193104
+ return row;
193105
+ }
193106
+ function tableAt(lines, start) {
193107
+ const headerLine = lines[start];
193108
+ const separatorLine = lines[start + 1];
193109
+ if (headerLine === undefined || separatorLine === undefined || endsTable(headerLine)) {
193110
+ return;
193111
+ }
193112
+ const header = splitRow(headerLine);
193113
+ const align = separatorAlign(separatorLine);
193114
+ if (header === undefined || align === undefined || header.length !== align.length || header.length > MAX_TABLE_COLUMNS) {
193115
+ return;
193116
+ }
193117
+ const rows = [];
193118
+ let end = start + 2;
193119
+ while (end < lines.length && !endsTable(lines[end])) {
193120
+ const cells = splitRow(lines[end]);
193121
+ if (cells === undefined) {
193122
+ break;
193123
+ }
193124
+ rows.push(fitRow(cells, header.length));
193125
+ end += 1;
193126
+ }
193127
+ return { table: { header, align, rows }, end };
193128
+ }
193129
+ function plainCell(text) {
193130
+ return text.replace(/\[([^\]]*)\]\((https?:\/\/[^\s)]*)\)/g, (_all, label, url) => label.trim().length > 0 ? `${label} (${url})` : url).replace(/(\*\*|__|~~)(.+?)\1/g, "$2").replace(/`+([^`]+)`+/g, "$1").replace(/<br\s*\/?>/gi, " ").trim();
193131
+ }
193132
+ function columnWidths(table) {
193133
+ return table.header.map((cell, column) => {
193134
+ let widest = visualWidth(plainCell(cell));
193135
+ for (const row of table.rows) {
193136
+ widest = Math.max(widest, visualWidth(plainCell(row[column])));
193137
+ }
193138
+ return widest;
193139
+ });
193140
+ }
193141
+ function pad4(text, width, align) {
193142
+ const gap = Math.max(0, width - visualWidth(text));
193143
+ if (align === "right") {
193144
+ return `${" ".repeat(gap)}${text}`;
193145
+ }
193146
+ if (align === "center") {
193147
+ const left = Math.floor(gap / 2);
193148
+ return `${" ".repeat(left)}${text}${" ".repeat(gap - left)}`;
193149
+ }
193150
+ return `${text}${" ".repeat(gap)}`;
193151
+ }
193152
+ function renderTableRow(cells, widths, align) {
193153
+ return cells.map((cell, column) => pad4(plainCell(cell), widths[column], align[column])).join(COLUMN_GAP).trimEnd();
193154
+ }
193155
+ function alignedCosts(table) {
193156
+ const widths = columnWidths(table);
193157
+ return { header: renderedRowCost(table.header, widths), rows: table.rows.map((row) => renderedRowCost(row, widths)) };
193158
+ }
193159
+ function tableLayout(table) {
193160
+ const costs = alignedCosts(table);
193161
+ return [costs.header, ...costs.rows].some((cost) => cost > MAX_TABLE_ROW_COST) ? "stacked" : "aligned";
193162
+ }
193163
+ function stackedRowLines(table, row) {
193164
+ const lines = [];
193165
+ row.forEach((cell, column) => {
193166
+ const text = plainCell(cell);
193167
+ if (text.length > 0) {
193168
+ lines.push(`${plainCell(table.header[column])}: ${text}`);
193169
+ }
193170
+ });
193171
+ return lines.length > 0 ? lines : ["(empty)"];
193172
+ }
193173
+ function renderStackedLines(table) {
193174
+ if (table.rows.length === 0) {
193175
+ return [table.header.map(plainCell).join(COLUMN_GAP).trim()];
193176
+ }
193177
+ return table.rows.flatMap((row, index) => index === 0 ? stackedRowLines(table, row) : ["", ...stackedRowLines(table, row)]);
193178
+ }
193179
+ function renderTableLines(table, widths) {
193180
+ if (widths === undefined && tableLayout(table) === "stacked") {
193181
+ return renderStackedLines(table);
193182
+ }
193183
+ const use = widths ?? columnWidths(table);
193184
+ return [table.header, ...table.rows].map((row) => renderTableRow(row, use, table.align));
193185
+ }
193186
+ function renderedRowCost(cells, widths) {
193187
+ let cost = 1;
193188
+ cells.forEach((cell, column) => {
193189
+ const text = plainCell(cell);
193190
+ cost += text.length + Math.max(0, widths[column] - visualWidth(text));
193191
+ if (column > 0) {
193192
+ cost += COLUMN_GAP.length;
193193
+ }
193194
+ });
193195
+ return cost;
193196
+ }
193197
+ function tableCosts(table) {
193198
+ const aligned = alignedCosts(table);
193199
+ if (tableLayout(table) === "aligned") {
193200
+ return aligned;
193201
+ }
193202
+ return {
193203
+ header: Math.min(aligned.header, MAX_TABLE_ROW_COST),
193204
+ rows: table.rows.map((row, index) => {
193205
+ let stacked = 1;
193206
+ stackedRowLines(table, row).forEach((line) => {
193207
+ stacked += line.length + 1;
193208
+ });
193209
+ return Math.max(stacked, Math.min(aligned.rows[index], MAX_TABLE_ROW_COST));
193210
+ })
193211
+ };
193212
+ }
193213
+ var MAX_TABLE_COLUMNS = 20, MAX_TABLE_ROW_COST = 1500, SEPARATOR_CELL, COLUMN_GAP = " \u2502 ";
193214
+ var init_format_table = __esm(() => {
193215
+ init_md_blocks();
193216
+ SEPARATOR_CELL = /^\s*(:)?-+(:)?\s*$/;
193217
+ });
193218
+
192793
193219
  // src/remote/format.ts
192794
193220
  function fenceOpening(line) {
192795
193221
  const match = FENCE_LINE2.exec(line);
@@ -192837,13 +193263,40 @@ function cutAt(text, max) {
192837
193263
  }
192838
193264
  function toPieces(text, capacity) {
192839
193265
  const pieces = [];
193266
+ const raws = text.split(/(?<=\n)/);
193267
+ const bare = raws.map((raw) => raw.endsWith(`
193268
+ `) ? raw.slice(0, -1) : raw);
192840
193269
  let fence;
192841
- for (const raw of text.split(/(?<=\n)/)) {
192842
- const line = raw.endsWith(`
192843
- `) ? raw.slice(0, -1) : raw;
193270
+ for (let at = 0;at < raws.length; at += 1) {
193271
+ const raw = raws[at];
193272
+ const line = bare[at];
192844
193273
  const before = fence;
192845
193274
  if (fence === undefined) {
192846
193275
  fence = openingFence(line);
193276
+ if (fence === undefined) {
193277
+ const found = tableAt(bare, at);
193278
+ if (found !== undefined) {
193279
+ const costs = tableCosts(found.table);
193280
+ const headerCost = costs.header;
193281
+ const widest = Math.max(...costs.rows, 0);
193282
+ if (headerCost + widest <= capacity) {
193283
+ const run = { headerSource: `${raw}${raws[at + 1]}`, headerCost };
193284
+ const rows = found.table.rows;
193285
+ const leadEnd = at + 2 + (rows.length > 0 ? 1 : 0);
193286
+ pieces.push({
193287
+ text: raws.slice(at, leadEnd).join(""),
193288
+ fence: undefined,
193289
+ cost: headerCost + (costs.rows[0] ?? 0),
193290
+ table: run
193291
+ });
193292
+ for (let row = leadEnd;row < found.end; row += 1) {
193293
+ pieces.push({ text: raws[row], fence: undefined, cost: costs.rows[row - at - 2], table: run });
193294
+ }
193295
+ at = found.end - 1;
193296
+ continue;
193297
+ }
193298
+ }
193299
+ }
192847
193300
  } else if (closesFence(line, fence)) {
192848
193301
  fence = undefined;
192849
193302
  }
@@ -192851,24 +193304,33 @@ function toPieces(text, capacity) {
192851
193304
  let rest = raw;
192852
193305
  while (rest.length > room) {
192853
193306
  const cut = cutAt(rest, room);
192854
- pieces.push({ text: rest.slice(0, cut), fence: before ?? fence });
193307
+ pieces.push({ text: rest.slice(0, cut), fence: before ?? fence, cost: cut });
192855
193308
  rest = rest.slice(cut);
192856
193309
  }
192857
193310
  if (rest.length > 0) {
192858
- pieces.push({ text: rest, fence });
193311
+ const rule = before === undefined && fence === undefined && RULE.test(line);
193312
+ pieces.push({ text: rest, fence, cost: rule ? Math.max(rest.length, RULE_LENGTH + (rest.endsWith(`
193313
+ `) ? 1 : 0)) : rest.length });
192859
193314
  }
192860
193315
  }
192861
193316
  return pieces;
192862
193317
  }
193318
+ function renderedLength(text) {
193319
+ return toPieces(text.replace(/\r\n?/g, `
193320
+ `), Number.POSITIVE_INFINITY).reduce((sum, piece) => sum + piece.cost, 0);
193321
+ }
192863
193322
  function pack(text, capacity) {
192864
193323
  const parts = [];
192865
193324
  let current = "";
193325
+ let used = 0;
192866
193326
  let hasContent = false;
192867
193327
  let open;
192868
- const finish = () => {
193328
+ let lastTable;
193329
+ const finish = (next) => {
192869
193330
  let body = current.trimEnd();
192870
193331
  if (!hasContent || body.length === 0) {
192871
193332
  current = "";
193333
+ used = 0;
192872
193334
  hasContent = false;
192873
193335
  return;
192874
193336
  }
@@ -192879,6 +193341,11 @@ ${open.marker}`;
192879
193341
  parts.push(body);
192880
193342
  current = open === undefined ? "" : `${open.header}
192881
193343
  `;
193344
+ used = open === undefined ? 0 : open.header.length + 1;
193345
+ if (open === undefined && next?.table !== undefined && next.table === lastTable) {
193346
+ current = next.table.headerSource;
193347
+ used = next.table.headerCost;
193348
+ }
192882
193349
  hasContent = false;
192883
193350
  };
192884
193351
  for (const piece of toPieces(text, capacity)) {
@@ -192886,15 +193353,17 @@ ${open.marker}`;
192886
193353
  continue;
192887
193354
  }
192888
193355
  const closing = piece.fence === undefined ? 0 : piece.fence.marker.length + 1;
192889
- if (hasContent && current.length + piece.text.length + closing > capacity) {
192890
- finish();
193356
+ if (hasContent && used + piece.cost + closing > capacity) {
193357
+ finish(piece);
192891
193358
  if (piece.text.trim().length === 0) {
192892
193359
  continue;
192893
193360
  }
192894
193361
  }
192895
193362
  current += piece.text;
193363
+ used += piece.cost;
192896
193364
  hasContent = true;
192897
193365
  open = piece.fence;
193366
+ lastTable = piece.table;
192898
193367
  }
192899
193368
  finish();
192900
193369
  return parts;
@@ -192908,7 +193377,7 @@ function formatReply(text, limit = TELEGRAM_MAX_TEXT) {
192908
193377
  if (text.trim().length === 0) {
192909
193378
  return [];
192910
193379
  }
192911
- if (text.length <= limit) {
193380
+ if (renderedLength(text) <= limit) {
192912
193381
  return [text];
192913
193382
  }
192914
193383
  for (let digits = 1;digits <= 3; digits += 1) {
@@ -192929,10 +193398,101 @@ ${part}`);
192929
193398
  }
192930
193399
  var MIN_LIMIT = 64, MAX_FENCE_MARKER = 16, MAX_FENCE_INFO = 40, FENCE_LINE2;
192931
193400
  var init_format2 = __esm(() => {
193401
+ init_format_blocks();
193402
+ init_format_table();
192932
193403
  init_types11();
192933
193404
  FENCE_LINE2 = /^ {0,3}(`{3,}|~{3,})(.*)$/;
192934
193405
  });
192935
193406
 
193407
+ // src/remote/format-link.ts
193408
+ function parse(text) {
193409
+ try {
193410
+ const url = new URL(text);
193411
+ return { host: normalise3(url.hostname), userinfo: url.username !== "" || url.password !== "" };
193412
+ } catch {
193413
+ const authority = /^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)/i.exec(text)?.[1] ?? text.split(/[/?#]/, 1)[0] ?? "";
193414
+ const at = authority.lastIndexOf("@");
193415
+ const host = (at < 0 ? authority : authority.slice(at + 1)).replace(/:\d*$/, "");
193416
+ return { host: normalise3(host), userinfo: at >= 0 };
193417
+ }
193418
+ }
193419
+ function normalise3(host) {
193420
+ return host.toLowerCase().replace(/\.$/, "").replace(/^www\./, "");
193421
+ }
193422
+ function bare(label) {
193423
+ return label.replace(/^[\s*_~`]+|[\s*_~`]+$/g, "");
193424
+ }
193425
+ function looksLikeAddress(label) {
193426
+ return SCHEME.test(label) || /^www\./i.test(label) || HOST_LIKE.test(label) && !FILE_EXTENSION.test(label);
193427
+ }
193428
+ function mismatchedHost(label, url) {
193429
+ const text = bare(label);
193430
+ if (!looksLikeAddress(text)) {
193431
+ return;
193432
+ }
193433
+ const target = parse(url);
193434
+ const shown = parse(SCHEME.test(text) ? text : `https://${text}`);
193435
+ if (target.host.length === 0) {
193436
+ return;
193437
+ }
193438
+ if (!target.userinfo && !shown.userinfo && shown.host === target.host) {
193439
+ return;
193440
+ }
193441
+ const room = Math.max(1, url.length - 1);
193442
+ return target.host.length > room ? `${target.host.slice(0, room - 1)}\u2026` : target.host;
193443
+ }
193444
+ function hostMarker(label, url) {
193445
+ const host = mismatchedHost(label, url);
193446
+ return host === undefined ? "" : ` (\u2192 ${host})`;
193447
+ }
193448
+ var SCHEME, HOST_LIKE, FILE_EXTENSION;
193449
+ var init_format_link = __esm(() => {
193450
+ SCHEME = /^https?:\/\//i;
193451
+ HOST_LIKE = /^[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)*\.\p{L}{2,}(?::\d+)?(?:[/?#]\S*)?$/u;
193452
+ FILE_EXTENSION = /\.(?:md|mdx|txt|json|jsonc|ya?ml|toml|lock|log|csv|tsv|xml|html?|css|scss|less|[cm]?[jt]sx?|py|rb|rs|go|java|kt|swift|sh|zsh|bash|sql|png|jpe?g|gif|svg|webp|pdf|zip|tgz|gz|ini|env|conf|cfg)(?:[?#]\S*)?$/i;
193453
+ });
193454
+
193455
+ // src/remote/format-plain.ts
193456
+ function renderPlainText(part) {
193457
+ if (!part.includes("|")) {
193458
+ return part;
193459
+ }
193460
+ const lines = part.split(`
193461
+ `);
193462
+ const bare = lines.map((entry) => entry.replace(/\r$/, ""));
193463
+ const out = [];
193464
+ let fence;
193465
+ let changed = false;
193466
+ let index = 0;
193467
+ while (index < lines.length) {
193468
+ const line = lines[index];
193469
+ if (fence !== undefined) {
193470
+ if (closesFence(line, fence)) {
193471
+ fence = undefined;
193472
+ }
193473
+ out.push(line);
193474
+ index += 1;
193475
+ continue;
193476
+ }
193477
+ fence = fenceOpening(line);
193478
+ const found = fence === undefined ? tableAt(bare, index) : undefined;
193479
+ if (found === undefined) {
193480
+ out.push(line);
193481
+ index += 1;
193482
+ continue;
193483
+ }
193484
+ out.push(...renderTableLines(found.table));
193485
+ changed = true;
193486
+ index = found.end;
193487
+ }
193488
+ return changed ? out.join(`
193489
+ `) : part;
193490
+ }
193491
+ var init_format_plain = __esm(() => {
193492
+ init_format2();
193493
+ init_format_table();
193494
+ });
193495
+
192936
193496
  // src/remote/format-html.ts
192937
193497
  function escapeHtml2(text) {
192938
193498
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -193066,7 +193626,8 @@ function link(text, index, context) {
193066
193626
  if (!HTTP_URL.test(url)) {
193067
193627
  return;
193068
193628
  }
193069
- return { html: `<a href="${escapeAttribute(url)}">${renderInline2(label, { ...context, link: true })}</a>`, end };
193629
+ const marker = escapeHtml2(hostMarker(label, url));
193630
+ return { html: `<a href="${escapeAttribute(url)}">${renderInline2(label, { ...context, link: true })}</a>${marker}`, end };
193070
193631
  }
193071
193632
  function renderInline2(text, context) {
193072
193633
  let out = "";
@@ -193178,7 +193739,19 @@ function renderTelegramHtml(part) {
193178
193739
  }
193179
193740
  continue;
193180
193741
  }
193742
+ const found = tableAt(lines, index);
193743
+ if (found !== undefined) {
193744
+ const text = escapeHtml2(renderTableLines(found.table).join(`
193745
+ `));
193746
+ push(tableLayout(found.table) === "aligned" ? `<pre>${text}</pre>` : text, true);
193747
+ index = found.end;
193748
+ continue;
193749
+ }
193181
193750
  index += 1;
193751
+ if (RULE.test(line)) {
193752
+ push(RULE_GLYPH.repeat(RULE_LENGTH), false);
193753
+ continue;
193754
+ }
193182
193755
  const heading = HEADING2.exec(line);
193183
193756
  if (heading !== null && heading[1].trim().length > 0) {
193184
193757
  push(`<b>${renderInline2(heading[1], { ...OUTSIDE, bold: true })}</b>`, false);
@@ -193186,6 +193759,13 @@ function renderTelegramHtml(part) {
193186
193759
  }
193187
193760
  const bullet = BULLET.exec(line);
193188
193761
  if (bullet !== null) {
193762
+ const task = TASK.exec(bullet[2]);
193763
+ if (task !== null) {
193764
+ const box = task[1] === " " ? UNCHECKED_BOX : CHECKED_BOX;
193765
+ const label = renderInline2(task[2] ?? "", OUTSIDE);
193766
+ push(`${bullet[1]}${box}${label.length > 0 ? ` ${label}` : ""}`, false);
193767
+ continue;
193768
+ }
193189
193769
  push(`${bullet[1]}\u2022 ${renderInline2(bullet[2], OUTSIDE)}`, false);
193190
193770
  continue;
193191
193771
  }
@@ -193193,6 +193773,93 @@ function renderTelegramHtml(part) {
193193
193773
  }
193194
193774
  return out;
193195
193775
  }
193776
+ function unescapeEntities(text) {
193777
+ return text.replace(/&(amp|lt|gt|quot|#\d{1,7}|#x[0-9a-f]{1,6});/gi, (_all, name) => {
193778
+ switch (name.toLowerCase()) {
193779
+ case "amp":
193780
+ return "&";
193781
+ case "lt":
193782
+ return "<";
193783
+ case "gt":
193784
+ return ">";
193785
+ case "quot":
193786
+ return '"';
193787
+ default:
193788
+ return String.fromCodePoint(name[1] === "x" || name[1] === "X" ? Number.parseInt(name.slice(2), 16) : Number.parseInt(name.slice(1), 10));
193789
+ }
193790
+ });
193791
+ }
193792
+ function checkTelegramHtml(html) {
193793
+ const stack = [];
193794
+ let text = "";
193795
+ let index = 0;
193796
+ const fail = (reason) => ({ ok: false, reason: `can't parse entities: ${reason} at byte offset ${index}` });
193797
+ while (index < html.length) {
193798
+ const char = html[index];
193799
+ if (char === "&") {
193800
+ const entity = ENTITY.exec(html.slice(index, index + 12));
193801
+ if (entity === null) {
193802
+ return fail("unescaped &");
193803
+ }
193804
+ text += unescapeEntities(entity[0]);
193805
+ index += entity[0].length;
193806
+ continue;
193807
+ }
193808
+ if (char === ">") {
193809
+ return fail("unescaped >");
193810
+ }
193811
+ if (char !== "<") {
193812
+ text += char;
193813
+ index += 1;
193814
+ continue;
193815
+ }
193816
+ const match = TAG.exec(html.slice(index));
193817
+ if (match === null) {
193818
+ return fail("unsupported start tag");
193819
+ }
193820
+ const closing = match[1] === "/";
193821
+ const name = match[2].toLowerCase();
193822
+ const attributes = match[3].trim();
193823
+ const top = stack[stack.length - 1];
193824
+ if (closing) {
193825
+ if (attributes.length > 0 || top === undefined || top.name !== name) {
193826
+ return fail(`can't find start tag for </${name}>`);
193827
+ }
193828
+ stack.pop();
193829
+ index += match[0].length;
193830
+ continue;
193831
+ }
193832
+ const known = SIMPLE_TAGS.has(name) || ["a", "code", "pre", "span", "blockquote"].includes(name);
193833
+ if (!known) {
193834
+ return fail(`unsupported start tag "${name}"`);
193835
+ }
193836
+ if (stack.some((entry) => entry.name === "code") || top?.name === "pre" && name !== "code") {
193837
+ return fail(`<${name}> inside code or pre`);
193838
+ }
193839
+ if (name === "code" || name === "pre") {
193840
+ const holder = stack.find((entry) => entry.name !== "blockquote" && !(name === "code" && entry === top && entry.name === "pre"));
193841
+ if (holder !== undefined) {
193842
+ return fail(`<${name}> inside <${holder.name}>`);
193843
+ }
193844
+ }
193845
+ if (name === "pre" && stack.some((entry) => entry.name !== "blockquote")) {
193846
+ return fail("pre inside another entity");
193847
+ }
193848
+ if (name === "blockquote" && stack.some((entry) => entry.name === "blockquote") || name === "a" && stack.some((entry) => entry.name === "a")) {
193849
+ return fail(`<${name}> nested in itself`);
193850
+ }
193851
+ const attributeOk = SIMPLE_TAGS.has(name) || name === "pre" ? attributes.length === 0 : name === "a" ? /^href="[^"<>]+"$/.test(attributes) : name === "code" ? attributes.length === 0 || /^class="language-[A-Za-z0-9_+#.-]+"$/.test(attributes) : name === "span" ? attributes === 'class="tg-spoiler"' : attributes.length === 0 || attributes === "expandable";
193852
+ if (!attributeOk) {
193853
+ return fail(`bad attributes on <${name}>`);
193854
+ }
193855
+ stack.push({ name });
193856
+ index += match[0].length;
193857
+ }
193858
+ if (stack.length > 0) {
193859
+ return fail(`can't find end tag for <${stack[stack.length - 1].name}>`);
193860
+ }
193861
+ return { ok: true, text };
193862
+ }
193196
193863
  function isEntityParseError(error) {
193197
193864
  return isBotApiError(error) && error.kind === "rejected" && error.status === 400 && /can't parse entities/i.test(error.message);
193198
193865
  }
@@ -193207,7 +193874,7 @@ async function sendHtml(api, params, onFallback) {
193207
193874
  try {
193208
193875
  onFallback?.(error);
193209
193876
  } catch {}
193210
- return api.sendMessage(plain);
193877
+ return api.sendMessage({ ...plain, text: renderPlainText(plain.text) });
193211
193878
  }
193212
193879
  }
193213
193880
  async function editHtml(api, params, onFallback) {
@@ -193220,15 +193887,19 @@ async function editHtml(api, params, onFallback) {
193220
193887
  try {
193221
193888
  onFallback?.(error);
193222
193889
  } catch {}
193223
- await api.editMessageText(params);
193890
+ await api.editMessageText({ ...params, text: renderPlainText(params.text) });
193224
193891
  }
193225
193892
  }
193226
193893
  function isNotModified(error) {
193227
193894
  return isBotApiError(error) && error.kind === "rejected" && /message is not modified/i.test(error.message);
193228
193895
  }
193229
- var OUTSIDE, WORD, ALNUM, SPACE, SINGLE_OPEN_BAD, SINGLE_CLOSE_BAD, HTTP_URL, LANGUAGE, EXPANDABLE_QUOTE_LINES = 8, TAGS2, HEADING2, BULLET, QUOTE, SIMPLE_TAGS;
193896
+ var OUTSIDE, WORD, ALNUM, SPACE, SINGLE_OPEN_BAD, SINGLE_CLOSE_BAD, HTTP_URL, LANGUAGE, EXPANDABLE_QUOTE_LINES = 8, TAGS2, SIMPLE_TAGS, TAG, ENTITY;
193230
193897
  var init_format_html = __esm(() => {
193231
193898
  init_format2();
193899
+ init_format_blocks();
193900
+ init_format_link();
193901
+ init_format_plain();
193902
+ init_format_table();
193232
193903
  init_types11();
193233
193904
  OUTSIDE = { bold: false, italic: false, strike: false, link: false };
193234
193905
  WORD = /[\p{L}\p{N}_]/u;
@@ -193239,10 +193910,9 @@ var init_format_html = __esm(() => {
193239
193910
  HTTP_URL = /^https?:\/\/[^\s<>"'`]+$/i;
193240
193911
  LANGUAGE = /^[A-Za-z0-9_+#.-]{1,32}$/;
193241
193912
  TAGS2 = { bold: "b", italic: "i", strike: "s" };
193242
- HEADING2 = /^ {0,3}#{1,6}[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/;
193243
- BULLET = /^([ \t]*)[-*][ \t]+(.*)$/;
193244
- QUOTE = /^ {0,3}>(?: (.*))?$/;
193245
193913
  SIMPLE_TAGS = new Set(["b", "strong", "i", "em", "u", "ins", "s", "strike", "del"]);
193914
+ TAG = /^<(\/?)([a-z][a-z-]*)((?:\s+[a-z-]+(?:="[^"<>]*")?)*)\s*>/i;
193915
+ ENTITY = /^&(?:amp|lt|gt|quot|#\d{1,7}|#x[0-9a-f]{1,6});/i;
193246
193916
  });
193247
193917
 
193248
193918
  // src/remote/durable-log.ts
@@ -193559,6 +194229,512 @@ var init_journal3 = __esm(() => {
193559
194229
  init_paths4();
193560
194230
  });
193561
194231
 
194232
+ // src/remote/rich-types.ts
194233
+ var RICH_LIMITS;
194234
+ var init_rich_types = __esm(() => {
194235
+ RICH_LIMITS = {
194236
+ characters: 32768,
194237
+ blocks: 500,
194238
+ nesting: 16,
194239
+ tableColumns: 20
194240
+ };
194241
+ });
194242
+
194243
+ // src/remote/format-rich.ts
194244
+ function unescape(text) {
194245
+ return text.replace(/&(amp|lt|gt|quot);/g, (_all, name) => ({ amp: "&", lt: "<", gt: ">", quot: '"' })[name]);
194246
+ }
194247
+ function collapse(children) {
194248
+ const merged = [];
194249
+ for (const child of children) {
194250
+ const last = merged[merged.length - 1];
194251
+ if (typeof child === "string" && typeof last === "string") {
194252
+ merged[merged.length - 1] = last + child;
194253
+ } else {
194254
+ merged.push(child);
194255
+ }
194256
+ }
194257
+ if (merged.length === 0) {
194258
+ return "";
194259
+ }
194260
+ return merged.length === 1 ? merged[0] : merged;
194261
+ }
194262
+ function htmlToRichText(html) {
194263
+ const root = { type: "bold", children: [] };
194264
+ const stack = [root];
194265
+ const token = /<(\/?)(b|i|s|code|a)(?: href="([^"]*)")?>|([^<]+)/g;
194266
+ for (let match = token.exec(html);match !== null; match = token.exec(html)) {
194267
+ const top = stack[stack.length - 1];
194268
+ if (match[4] !== undefined) {
194269
+ top.children.push(unescape(match[4]));
194270
+ } else if (match[1] === "/") {
194271
+ const closed = stack.pop();
194272
+ const text = collapse(closed.children);
194273
+ const node = closed.type === "url" ? { type: "url", text, url: closed.url } : { type: closed.type, text };
194274
+ stack[stack.length - 1].children.push(node);
194275
+ } else {
194276
+ stack.push({ type: TAG_TYPES[match[2]], ...match[3] === undefined ? {} : { url: unescape(match[3]) }, children: [] });
194277
+ }
194278
+ }
194279
+ return collapse(root.children);
194280
+ }
194281
+ function inline(text) {
194282
+ return htmlToRichText(renderInline2(text, OUTSIDE));
194283
+ }
194284
+ function isEmpty(text) {
194285
+ return text === "" || Array.isArray(text) && text.length === 0;
194286
+ }
194287
+ function indentOf2(whitespace) {
194288
+ return whitespace.replace(/\t/g, " ").length;
194289
+ }
194290
+ function listEntry(line) {
194291
+ const bullet = BULLET.exec(line);
194292
+ if (bullet !== null) {
194293
+ const task = TASK.exec(bullet[2]);
194294
+ return {
194295
+ indent: indentOf2(bullet[1]),
194296
+ ordered: false,
194297
+ ...task === null ? {} : { checkbox: task[1] === " " ? "open" : "done" },
194298
+ text: task === null ? bullet[2] : task[2] ?? ""
194299
+ };
194300
+ }
194301
+ const ordered = ORDERED.exec(line);
194302
+ if (ordered !== null) {
194303
+ return { indent: indentOf2(ordered[1]), ordered: true, number: Number.parseInt(ordered[2], 10), text: ordered[4] };
194304
+ }
194305
+ return;
194306
+ }
194307
+ function listItem(entry) {
194308
+ const text = inline(entry.text);
194309
+ return {
194310
+ blocks: [{ type: "paragraph", text: isEmpty(text) ? " " : text }],
194311
+ ...entry.checkbox === undefined ? {} : { has_checkbox: true, ...entry.checkbox === "done" ? { is_checked: true } : {} },
194312
+ ...entry.ordered ? { value: entry.number, type: "1" } : {}
194313
+ };
194314
+ }
194315
+ function buildLists(entries) {
194316
+ const root = [];
194317
+ const stack = [];
194318
+ for (const entry of entries) {
194319
+ while (stack.length > 0 && stack[stack.length - 1].indent > entry.indent) {
194320
+ stack.pop();
194321
+ }
194322
+ let top = stack[stack.length - 1];
194323
+ let parent;
194324
+ if (top !== undefined && top.indent === entry.indent && top.ordered !== entry.ordered) {
194325
+ parent = top.parent;
194326
+ stack.pop();
194327
+ top = stack[stack.length - 1];
194328
+ }
194329
+ if (top === undefined || top.indent < entry.indent || parent !== undefined) {
194330
+ const holder = parent ?? (top === undefined ? root : top.items[top.items.length - 1].blocks);
194331
+ const frame = { indent: entry.indent, ordered: entry.ordered, items: [], parent: holder };
194332
+ holder.push({ type: "list", items: frame.items });
194333
+ stack.push(frame);
194334
+ top = frame;
194335
+ }
194336
+ top.items.push(listItem(entry));
194337
+ }
194338
+ return root;
194339
+ }
194340
+ function countBlocks(blocks, depth) {
194341
+ let count = 0;
194342
+ let deepest = depth;
194343
+ for (const block of blocks) {
194344
+ count += 1;
194345
+ if (block.type === "list") {
194346
+ for (const item of block.items) {
194347
+ count += 1;
194348
+ const inner = countBlocks(item.blocks, depth + 2);
194349
+ count += inner.count;
194350
+ deepest = Math.max(deepest, inner.depth);
194351
+ }
194352
+ } else if (block.type === "blockquote") {
194353
+ const inner = countBlocks(block.blocks, depth + 1);
194354
+ count += inner.count;
194355
+ deepest = Math.max(deepest, inner.depth);
194356
+ } else if (block.type === "table") {
194357
+ count += block.cells.length;
194358
+ }
194359
+ }
194360
+ return { count, depth: deepest };
194361
+ }
194362
+ function containsTable(part) {
194363
+ const lines = part.replace(/\r\n?/g, `
194364
+ `).split(`
194365
+ `);
194366
+ let fence;
194367
+ for (let index = 0;index < lines.length; index += 1) {
194368
+ const line = lines[index];
194369
+ if (fence !== undefined) {
194370
+ if (closesFence(line, fence)) {
194371
+ fence = undefined;
194372
+ }
194373
+ continue;
194374
+ }
194375
+ fence = fenceOpening(line);
194376
+ if (fence === undefined && tableAt(lines, index) !== undefined) {
194377
+ return true;
194378
+ }
194379
+ }
194380
+ return false;
194381
+ }
194382
+ function tableBlock(table) {
194383
+ const cell = (text, column, header) => {
194384
+ const content = inline(text.replace(/<br\s*\/?>/gi, " ").trim());
194385
+ return {
194386
+ ...isEmpty(content) ? {} : { text: content },
194387
+ ...header ? { is_header: true } : {},
194388
+ align: table.align[column],
194389
+ valign: "top"
194390
+ };
194391
+ };
194392
+ return {
194393
+ type: "table",
194394
+ is_bordered: true,
194395
+ cells: [table.header.map((text, column) => cell(text, column, true)), ...table.rows.map((row) => row.map((text, column) => cell(text, column, false)))]
194396
+ };
194397
+ }
194398
+ function renderRichMessage(part) {
194399
+ const lines = part.replace(/\r\n?/g, `
194400
+ `).split(`
194401
+ `);
194402
+ const blocks = [];
194403
+ let index = 0;
194404
+ while (index < lines.length) {
194405
+ const line = lines[index];
194406
+ const fence = fenceOpening(line);
194407
+ if (fence !== undefined) {
194408
+ const body = [];
194409
+ let cursor = index + 1;
194410
+ while (cursor < lines.length && !closesFence(lines[cursor], fence)) {
194411
+ body.push(lines[cursor]);
194412
+ cursor += 1;
194413
+ }
194414
+ const closed = cursor < lines.length;
194415
+ const code = body.join(`
194416
+ `);
194417
+ if (code.trim().length === 0) {
194418
+ for (const raw of lines.slice(index, closed ? cursor + 1 : cursor)) {
194419
+ blocks.push({ type: "paragraph", text: raw });
194420
+ }
194421
+ } else {
194422
+ const language = fence.info.split(/\s+/)[0];
194423
+ blocks.push({ type: "pre", text: code, ...LANGUAGE2.test(language) ? { language } : {} });
194424
+ }
194425
+ index = closed ? cursor + 1 : cursor;
194426
+ continue;
194427
+ }
194428
+ if (QUOTE.test(line)) {
194429
+ const quoted = [];
194430
+ const start = index;
194431
+ while (index < lines.length) {
194432
+ const next = QUOTE.exec(lines[index]);
194433
+ if (next === null) {
194434
+ break;
194435
+ }
194436
+ quoted.push(next[1] ?? "");
194437
+ index += 1;
194438
+ }
194439
+ if (quoted.every((entry) => entry.trim().length === 0)) {
194440
+ for (const raw of lines.slice(start, index)) {
194441
+ blocks.push({ type: "paragraph", text: raw });
194442
+ }
194443
+ } else if (quoted.length >= EXPANDABLE_QUOTE_LINES2) {
194444
+ const text = [];
194445
+ quoted.forEach((entry, position) => {
194446
+ if (position > 0) {
194447
+ text.push(`
194448
+ `);
194449
+ }
194450
+ text.push(inline(entry));
194451
+ });
194452
+ blocks.push({ type: "expandable_blockquote", text });
194453
+ } else {
194454
+ blocks.push({
194455
+ type: "blockquote",
194456
+ blocks: quoted.flatMap((entry) => {
194457
+ const text = inline(entry);
194458
+ return isEmpty(text) ? [] : [{ type: "paragraph", text }];
194459
+ })
194460
+ });
194461
+ }
194462
+ continue;
194463
+ }
194464
+ const found = tableAt(lines, index);
194465
+ if (found !== undefined) {
194466
+ blocks.push(tableBlock(found.table));
194467
+ index = found.end;
194468
+ continue;
194469
+ }
194470
+ if (listEntry(line) !== undefined && !RULE.test(line)) {
194471
+ const entries = [];
194472
+ while (index < lines.length) {
194473
+ const entry = listEntry(lines[index]);
194474
+ if (entry === undefined || RULE.test(lines[index])) {
194475
+ break;
194476
+ }
194477
+ entries.push(entry);
194478
+ index += 1;
194479
+ }
194480
+ blocks.push(...buildLists(entries));
194481
+ continue;
194482
+ }
194483
+ index += 1;
194484
+ if (RULE.test(line)) {
194485
+ blocks.push({ type: "divider" });
194486
+ continue;
194487
+ }
194488
+ const heading = HEADING2.exec(line);
194489
+ if (heading !== null && heading[1].trim().length > 0) {
194490
+ const level = line.trimStart().match(/^#+/)[0].length;
194491
+ blocks.push({ type: "heading", text: inline(heading[1]), size: Math.min(level + 1, 6) });
194492
+ continue;
194493
+ }
194494
+ const text = inline(line);
194495
+ if (!isEmpty(text) && line.trim().length > 0) {
194496
+ blocks.push({ type: "paragraph", text });
194497
+ }
194498
+ }
194499
+ const measured = countBlocks(blocks, 1);
194500
+ if (blocks.length === 0) {
194501
+ throw new RichRenderError("nothing to send as a rich message");
194502
+ }
194503
+ if (measured.count > RICH_LIMITS.blocks) {
194504
+ throw new RichRenderError(`${measured.count} blocks, over the limit of ${RICH_LIMITS.blocks}`);
194505
+ }
194506
+ if (measured.depth > RICH_LIMITS.nesting) {
194507
+ throw new RichRenderError(`${measured.depth} nesting levels, over the limit of ${RICH_LIMITS.nesting}`);
194508
+ }
194509
+ return { blocks };
194510
+ }
194511
+ var RichRenderError, LANGUAGE2, EXPANDABLE_QUOTE_LINES2 = 8, TAG_TYPES;
194512
+ var init_format_rich = __esm(() => {
194513
+ init_format_blocks();
194514
+ init_format2();
194515
+ init_format_html();
194516
+ init_format_table();
194517
+ init_rich_types();
194518
+ RichRenderError = class RichRenderError extends Error {
194519
+ constructor(message) {
194520
+ super(message);
194521
+ this.name = "RichRenderError";
194522
+ }
194523
+ };
194524
+ LANGUAGE2 = /^[A-Za-z0-9_+#.-]{1,32}$/;
194525
+ TAG_TYPES = { b: "bold", i: "italic", s: "strikethrough", code: "code", a: "url" };
194526
+ });
194527
+
194528
+ // src/remote/rendering.ts
194529
+ function capReason(reason) {
194530
+ return reason.length > MAX_REASON_LENGTH2 ? `${reason.slice(0, MAX_REASON_LENGTH2 - 1)}\u2026` : reason;
194531
+ }
194532
+
194533
+ class RenderingState {
194534
+ now;
194535
+ readMode;
194536
+ last;
194537
+ pausedUntil = 0;
194538
+ pauseReason;
194539
+ serverKey;
194540
+ serverFailures = 0;
194541
+ constructor(options = {}) {
194542
+ this.now = options.now ?? Date.now;
194543
+ this.readMode = options.mode ?? (() => DEFAULT_RENDER_MODE);
194544
+ }
194545
+ mode() {
194546
+ try {
194547
+ return this.readMode();
194548
+ } catch {
194549
+ return DEFAULT_RENDER_MODE;
194550
+ }
194551
+ }
194552
+ lastFallback() {
194553
+ return this.last;
194554
+ }
194555
+ record(step, reason) {
194556
+ this.last = { step, reason: capReason(redactSensitiveText(reason)), at: this.now() };
194557
+ return this.last;
194558
+ }
194559
+ pauseRich(reason) {
194560
+ this.pausedUntil = this.now() + RICH_PAUSE_MS;
194561
+ this.pauseReason = capReason(redactSensitiveText(reason));
194562
+ }
194563
+ richServerFailure(key) {
194564
+ this.serverFailures = this.serverKey === key ? this.serverFailures + 1 : 1;
194565
+ this.serverKey = key;
194566
+ return this.serverFailures;
194567
+ }
194568
+ richServerReset() {
194569
+ this.serverKey = undefined;
194570
+ this.serverFailures = 0;
194571
+ }
194572
+ richPausedReason() {
194573
+ if (this.pausedUntil > this.now()) {
194574
+ return this.pauseReason ?? "rich messages are paused";
194575
+ }
194576
+ return;
194577
+ }
194578
+ snapshot() {
194579
+ return {
194580
+ mode: this.mode(),
194581
+ ...this.last === undefined ? {} : { lastFallback: this.last },
194582
+ ...this.pausedUntil > this.now() ? { richPausedUntil: this.pausedUntil } : {}
194583
+ };
194584
+ }
194585
+ }
194586
+ function firstStep(mode, text) {
194587
+ if (mode === "auto") {
194588
+ return containsTable(text) ? "rich" : "html";
194589
+ }
194590
+ return mode;
194591
+ }
194592
+ function descriptionOf(error) {
194593
+ return error.message.replace(/^\w+:\s*(?:\d{3}\s*)?/, "");
194594
+ }
194595
+ function describe(error) {
194596
+ if (!isBotApiError(error)) {
194597
+ return capReason(redactSensitiveText(error instanceof Error ? error.message : String(error)));
194598
+ }
194599
+ const text = redactSensitiveText(descriptionOf(error));
194600
+ const cut = text.search(/["\u201c\u00ab`]/);
194601
+ const head = (cut < 0 ? text : text.slice(0, cut)).replace(/[\s:,(]+$/, "");
194602
+ return capReason(error.status === undefined ? head : `${error.status} ${head}`);
194603
+ }
194604
+ function isRefusal(error) {
194605
+ return isBotApiError(error) && error.kind === "rejected";
194606
+ }
194607
+ function isStanding(error) {
194608
+ if (!isBotApiError(error) || error.kind !== "rejected") {
194609
+ return false;
194610
+ }
194611
+ const text = descriptionOf(error);
194612
+ if (error.status === 404 || error.status === 405) {
194613
+ return MENTIONS_RICH_METHOD.test(text) || !MENTIONS_CHAT.test(text);
194614
+ }
194615
+ return error.status === 403 && MENTIONS_RICH_METHOD.test(text);
194616
+ }
194617
+ function isChatScoped(error) {
194618
+ if (!isBotApiError(error) || error.kind !== "rejected" || isStanding(error)) {
194619
+ return false;
194620
+ }
194621
+ const text = descriptionOf(error);
194622
+ return !MENTIONS_RICH_METHOD.test(text) && (error.status === 403 || (error.status === 400 || error.status === 404) && MENTIONS_CHAT.test(text));
194623
+ }
194624
+ function fallTo(hooks, step, reason) {
194625
+ const recorded = hooks.state.record(step, reason);
194626
+ try {
194627
+ hooks.onFallback?.(recorded);
194628
+ } catch {}
194629
+ }
194630
+ async function sendRendered(api, params, hooks) {
194631
+ const { parseMode: _ignored, ...plain } = params;
194632
+ const step = firstStep(hooks.state.mode(), params.text);
194633
+ if (step === "plain") {
194634
+ return api.sendMessage({ ...plain, text: renderPlainText(params.text) });
194635
+ }
194636
+ if (step === "rich") {
194637
+ const paused = hooks.state.richPausedReason();
194638
+ if (paused !== undefined) {
194639
+ fallTo(hooks, "rich-to-html", `rich messages paused: ${paused}`);
194640
+ } else {
194641
+ try {
194642
+ if (api.sendRichMessage === undefined) {
194643
+ throw new RichUnavailable(MISSING_METHOD);
194644
+ }
194645
+ const sent = await api.sendRichMessage({
194646
+ chatId: params.chatId,
194647
+ richMessage: renderRichMessage(params.text),
194648
+ ...params.messageThreadId === undefined ? {} : { messageThreadId: params.messageThreadId },
194649
+ ...params.inlineKeyboard === undefined ? {} : { inlineKeyboard: params.inlineKeyboard }
194650
+ });
194651
+ hooks.state.richServerReset();
194652
+ return sent;
194653
+ } catch (error) {
194654
+ handleRichFailure(error, hooks, `send:${params.chatId}:${params.messageThreadId ?? ""}:${params.text}`);
194655
+ }
194656
+ }
194657
+ }
194658
+ return sendHtml(api, plain, (error) => fallTo(hooks, "html-to-plain", describe(error)));
194659
+ }
194660
+ async function editRendered(api, params, hooks) {
194661
+ const step = firstStep(hooks.state.mode(), params.text);
194662
+ if (step === "plain") {
194663
+ await api.editMessageText({ ...params, text: renderPlainText(params.text) });
194664
+ return;
194665
+ }
194666
+ if (step === "rich") {
194667
+ const paused = hooks.state.richPausedReason();
194668
+ if (paused !== undefined) {
194669
+ fallTo(hooks, "rich-to-html", `rich messages paused: ${paused}`);
194670
+ } else {
194671
+ try {
194672
+ if (api.editRichMessage === undefined) {
194673
+ throw new RichUnavailable(MISSING_METHOD);
194674
+ }
194675
+ await api.editRichMessage({
194676
+ chatId: params.chatId,
194677
+ messageId: params.messageId,
194678
+ richMessage: renderRichMessage(params.text),
194679
+ ...params.inlineKeyboard === undefined ? {} : { inlineKeyboard: params.inlineKeyboard }
194680
+ });
194681
+ hooks.state.richServerReset();
194682
+ return;
194683
+ } catch (error) {
194684
+ if (isNotModified(error)) {
194685
+ throw error;
194686
+ }
194687
+ handleRichFailure(error, hooks, `edit:${params.chatId}:${params.messageId}:${params.text}`);
194688
+ }
194689
+ }
194690
+ }
194691
+ await editHtml(api, params, (error) => fallTo(hooks, "html-to-plain", describe(error)));
194692
+ }
194693
+ function handleRichFailure(error, hooks, key) {
194694
+ if (error instanceof RichUnavailable) {
194695
+ hooks.state.pauseRich(error.message);
194696
+ fallTo(hooks, "rich-to-html", error.message);
194697
+ return;
194698
+ }
194699
+ if (error instanceof RichRenderError) {
194700
+ fallTo(hooks, "rich-to-html", describe(error));
194701
+ return;
194702
+ }
194703
+ if (isBotApiError(error) && error.kind === "server") {
194704
+ if (hooks.state.richServerFailure(key) < RICH_SERVER_ATTEMPTS) {
194705
+ throw error;
194706
+ }
194707
+ hooks.state.richServerReset();
194708
+ fallTo(hooks, "rich-to-html", `rich failed ${RICH_SERVER_ATTEMPTS} times: ${describe(error)}`);
194709
+ return;
194710
+ }
194711
+ if (!isRefusal(error)) {
194712
+ throw error;
194713
+ }
194714
+ if (isChatScoped(error)) {
194715
+ return;
194716
+ }
194717
+ const reason = describe(error);
194718
+ if (isStanding(error)) {
194719
+ hooks.state.pauseRich(reason);
194720
+ }
194721
+ fallTo(hooks, "rich-to-html", reason);
194722
+ }
194723
+ var RICH_PAUSE_MS, RICH_SERVER_ATTEMPTS = 3, MAX_REASON_LENGTH2 = 200, MENTIONS_RICH_METHOD, MENTIONS_CHAT, MISSING_METHOD = "this Bot API client has no rich message method", RichUnavailable;
194724
+ var init_rendering = __esm(() => {
194725
+ init_service2();
194726
+ init_format_html();
194727
+ init_format_rich();
194728
+ init_format_plain();
194729
+ init_rendering_mode();
194730
+ init_types11();
194731
+ RICH_PAUSE_MS = 10 * 60000;
194732
+ MENTIONS_RICH_METHOD = /rich|method|unknown parameter|not supported|unsupported/i;
194733
+ MENTIONS_CHAT = /chat|thread|topic|user|member|group|channel|blocked|kicked|deactivated|rights/i;
194734
+ RichUnavailable = class RichUnavailable extends Error {
194735
+ };
194736
+ });
194737
+
193562
194738
  // src/remote/outbound-queue.ts
193563
194739
  import { randomUUID as randomUUID51 } from "crypto";
193564
194740
  import path363 from "path";
@@ -193585,6 +194761,7 @@ class OutboundQueue {
193585
194761
  log;
193586
194762
  onDrop;
193587
194763
  onFallback;
194764
+ rendering;
193588
194765
  retryDelayMs;
193589
194766
  sentHooks = new Map;
193590
194767
  blockedUntil = 0;
@@ -193598,6 +194775,7 @@ class OutboundQueue {
193598
194775
  this.onFallback = options.onFallback ?? (() => {
193599
194776
  return;
193600
194777
  });
194778
+ this.rendering = options.rendering ?? new RenderingState({ now: options.now });
193601
194779
  this.retryDelayMs = options.retryDelayMs ?? 5000;
193602
194780
  const directory = ensureRemoteDir(options.dir);
193603
194781
  this.log = new DurableLog({
@@ -193686,17 +194864,17 @@ class OutboundQueue {
193686
194864
  return { sent, dropped, remaining: this.log.size, retryInMs: wait, stoppedBy: "rate limited" };
193687
194865
  }
193688
194866
  try {
193689
- const result = await sendHtml(this.api, {
194867
+ const result = await sendRendered(this.api, {
193690
194868
  chatId: entry.chatId,
193691
194869
  text: entry.text,
193692
194870
  ...entry.threadId === undefined ? {} : { messageThreadId: entry.threadId },
193693
194871
  ...entry.keyboard === undefined ? {} : { inlineKeyboard: entry.keyboard }
193694
- }, () => this.onFallback(entry));
194872
+ }, { state: this.rendering, onFallback: (fallback) => this.onFallback(entry, fallback) });
193695
194873
  this.log.ack(entry.id);
193696
194874
  sent += 1;
193697
194875
  this.notifySent(entry, result.message_id);
193698
194876
  } catch (error) {
193699
- const description = describe(error);
194877
+ const description = describe2(error);
193700
194878
  if (isBotApiError(error) && error.kind === "rate-limited") {
193701
194879
  const retryMs = Math.max(1, error.retryAfterSec ?? 1) * 1000;
193702
194880
  this.blockedUntil = this.now() + retryMs;
@@ -193713,7 +194891,7 @@ class OutboundQueue {
193713
194891
  }
193714
194892
  }
193715
194893
  }
193716
- function describe(error) {
194894
+ function describe2(error) {
193717
194895
  return redactSensitiveText(error instanceof Error ? error.message : String(error));
193718
194896
  }
193719
194897
  var OUTBOUND_MAX_ENTRIES = 200;
@@ -193722,7 +194900,7 @@ var init_outbound_queue = __esm(() => {
193722
194900
  init_durable_log();
193723
194901
  init_paths4();
193724
194902
  init_format2();
193725
- init_format_html();
194903
+ init_rendering();
193726
194904
  init_types11();
193727
194905
  });
193728
194906
 
@@ -193832,7 +195010,7 @@ class UpdatePoller {
193832
195010
  continue;
193833
195011
  }
193834
195012
  failures += 1;
193835
- this.set({ state: "running", reason: describe2(error) });
195013
+ this.set({ state: "running", reason: describe3(error) });
193836
195014
  await this.sleep(this.backoffMs[Math.min(failures, this.backoffMs.length) - 1] ?? 1000, signal);
193837
195015
  continue;
193838
195016
  }
@@ -193852,13 +195030,13 @@ class UpdatePoller {
193852
195030
  }
193853
195031
  } catch (error) {
193854
195032
  failures += 1;
193855
- this.set({ state: "running", reason: describe2(error) });
195033
+ this.set({ state: "running", reason: describe3(error) });
193856
195034
  await this.sleep(this.backoffMs[Math.min(failures, this.backoffMs.length) - 1] ?? 1000, signal);
193857
195035
  }
193858
195036
  }
193859
195037
  }
193860
195038
  }
193861
- function describe2(error) {
195039
+ function describe3(error) {
193862
195040
  return redactSensitiveText(error instanceof Error ? error.message : String(error));
193863
195041
  }
193864
195042
  var POLLER_CONFLICT_REASON = "another getUpdates poller already owns this bot token (HTTP 409 Conflict); only one `keryx serve` may poll a token, so this one will not poll. Stop the other instance or use a different bot.", SHORT_POLL_PAUSE_MS = 1000;
@@ -193869,7 +195047,7 @@ var init_poller = __esm(() => {
193869
195047
 
193870
195048
  // src/remote/service-topics.ts
193871
195049
  import path364 from "path";
193872
- function parse(value) {
195050
+ function parse2(value) {
193873
195051
  if (typeof value !== "object" || value === null)
193874
195052
  return;
193875
195053
  const raw = value;
@@ -193896,7 +195074,7 @@ class ServiceTopics {
193896
195074
  if (!Array.isArray(records))
193897
195075
  return;
193898
195076
  for (const candidate of records) {
193899
- const record = parse(candidate);
195077
+ const record = parse2(candidate);
193900
195078
  if (record !== undefined)
193901
195079
  this.byName.set(nameKey(record.name), record);
193902
195080
  }
@@ -194080,6 +195258,7 @@ class RemoteHub {
194080
195258
  registry;
194081
195259
  inbound;
194082
195260
  outbound;
195261
+ rendering;
194083
195262
  serviceTopics;
194084
195263
  droppedReasons = new Map;
194085
195264
  journal;
@@ -194118,15 +195297,23 @@ class RemoteHub {
194118
195297
  };
194119
195298
  this.registry = new SessionRegistry({ ...options.dir === undefined ? {} : { dir: options.dir }, now: this.now });
194120
195299
  this.inbound = new InboundQueues(options.dir === undefined ? {} : { dir: options.dir });
195300
+ this.rendering = new RenderingState({
195301
+ now: this.now,
195302
+ mode: options.renderingMode ?? (() => {
195303
+ const fresh = loadRemoteConfig(options.dir);
195304
+ return (fresh.ok ? fresh.value.rendering : undefined) ?? options.config.rendering ?? DEFAULT_RENDER_MODE;
195305
+ })
195306
+ });
194121
195307
  this.outbound = new OutboundQueue({
194122
195308
  api: this.api,
195309
+ rendering: this.rendering,
194123
195310
  ...options.dir === undefined ? {} : { dir: options.dir },
194124
195311
  now: this.now,
194125
195312
  onDrop: (entry, reason) => {
194126
195313
  this.droppedReasons.set(entry.id, reason);
194127
195314
  this.event("outbound-dropped", `${reason}${entry.threadId === undefined ? "" : ` (topic ${entry.threadId})`}`);
194128
195315
  },
194129
- onFallback: (entry) => this.event("format-fallback", `Telegram refused the formatting; sent as plain text${entry.threadId === undefined ? "" : ` (topic ${entry.threadId})`}`)
195316
+ onFallback: (entry, fallback) => this.fallbackEvent(fallback, entry.threadId === undefined ? "" : ` (topic ${entry.threadId})`, "sent")
194130
195317
  });
194131
195318
  this.journal = new RejectedJournal({ ...options.dir === undefined ? {} : { dir: options.dir }, now: this.now });
194132
195319
  this.offset = new PollerState(options.dir === undefined ? {} : { dir: options.dir });
@@ -194267,7 +195454,13 @@ class RemoteHub {
194267
195454
  });
194268
195455
  }
194269
195456
  async sendGeneral(text) {
194270
- await sendHtml(this.api, { chatId: this.config.chatId, text }, () => this.event("format-fallback", "Telegram refused the formatting; sent as plain text (General)"));
195457
+ await sendRendered(this.api, { chatId: this.config.chatId, text }, { state: this.rendering, onFallback: (fallback) => this.fallbackEvent(fallback, " (General)", "sent") });
195458
+ }
195459
+ renderingStatus() {
195460
+ return this.rendering.snapshot();
195461
+ }
195462
+ fallbackEvent(fallback, where, verb) {
195463
+ this.event("format-fallback", fallback.step === "html-to-plain" ? `Telegram refused the formatting; ${verb} as plain text${where}` : `Telegram refused the rich message (${fallback.reason}); ${verb} as HTML${where}`);
194271
195464
  }
194272
195465
  heartbeat(sessionId) {
194273
195466
  return this.serial(async () => {
@@ -194386,7 +195579,7 @@ class RemoteHub {
194386
195579
  return "gone";
194387
195580
  }
194388
195581
  try {
194389
- await editHtml(this.api, { chatId: record.chatId, messageId, text }, () => this.event("format-fallback", `Telegram refused the formatting; edited as plain text (topic ${record.threadId})`));
195582
+ await editRendered(this.api, { chatId: record.chatId, messageId, text }, { state: this.rendering, onFallback: (fallback) => this.fallbackEvent(fallback, ` (topic ${record.threadId})`, "edited") });
194390
195583
  return "edited";
194391
195584
  } catch (error) {
194392
195585
  if (isNotModified(error)) {
@@ -194799,11 +195992,14 @@ var realTimers, MAX_EVENTS = 50, ANSWER_CALLBACK_TIMEOUT_MS = 5000;
194799
195992
  var init_hub = __esm(() => {
194800
195993
  init_service2();
194801
195994
  init_lease();
195995
+ init_config11();
194802
195996
  init_format_html();
194803
195997
  init_inbound();
194804
195998
  init_journal3();
194805
195999
  init_naming();
194806
196000
  init_outbound_queue();
196001
+ init_rendering();
196002
+ init_rendering_mode();
194807
196003
  init_command_gateway();
194808
196004
  init_message_state();
194809
196005
  init_poller();
@@ -195818,10 +197014,10 @@ function newPairingCode() {
195818
197014
  }
195819
197015
  return code;
195820
197016
  }
195821
- function describe3(error) {
197017
+ function describe4(error) {
195822
197018
  return redactSensitiveText(error instanceof Error ? error.message : String(error));
195823
197019
  }
195824
- function normalise3(text) {
197020
+ function normalise4(text) {
195825
197021
  return text.toUpperCase().replace(/[^A-Z0-9]/g, "");
195826
197022
  }
195827
197023
 
@@ -195879,7 +197075,7 @@ class Pairing {
195879
197075
  if (isBotApiError(error) && error.kind === "rejected") {
195880
197076
  return { ok: false, kind: "rejected", reason: "Telegram does not accept this bot token; check it with BotFather and try again." };
195881
197077
  }
195882
- return { ok: false, kind: "unreachable", reason: `could not reach Telegram: ${describe3(error)}` };
197078
+ return { ok: false, kind: "unreachable", reason: `could not reach Telegram: ${describe4(error)}` };
195883
197079
  }
195884
197080
  return { ok: true, pairing: new Pairing(options, bot) };
195885
197081
  }
@@ -196006,7 +197202,7 @@ class Pairing {
196006
197202
  return;
196007
197203
  }
196008
197204
  const sent = message.text.trim().replace(/^\/start(?:@\w+)?\s+/i, "");
196009
- if (this.code === undefined || normalise3(sent) !== this.code) {
197205
+ if (this.code === undefined || normalise4(sent) !== this.code) {
196010
197206
  return;
196011
197207
  }
196012
197208
  this.userId = message.from.id;
@@ -196115,7 +197311,7 @@ class Pairing {
196115
197311
  problems.push("Give the bot the Manage Topics right in the group's administrator settings.");
196116
197312
  }
196117
197313
  } catch (error) {
196118
- problems.push(`could not inspect the group: ${describe3(error)}`);
197314
+ problems.push(`could not inspect the group: ${describe4(error)}`);
196119
197315
  }
196120
197316
  this.expireIfDue();
196121
197317
  if (FINAL.has(this.state)) {
@@ -196140,7 +197336,7 @@ var init_pairing = __esm(() => {
196140
197336
  });
196141
197337
 
196142
197338
  // src/remote/channels.ts
196143
- function describe4(error) {
197339
+ function describe5(error) {
196144
197340
  return redactSensitiveText(error instanceof Error ? error.message : String(error));
196145
197341
  }
196146
197342
 
@@ -196203,11 +197399,25 @@ class ChannelsController {
196203
197399
  }
196204
197400
  return { state: "not-connected" };
196205
197401
  }
197402
+ rendering() {
197403
+ const hub = this.host.hub();
197404
+ if (hub !== undefined && typeof hub.renderingStatus === "function") {
197405
+ return hub.renderingStatus();
197406
+ }
197407
+ const config = loadRemoteConfig(this.host.dir);
197408
+ return config.ok ? { mode: config.value.rendering ?? DEFAULT_RENDER_MODE } : undefined;
197409
+ }
196206
197410
  status() {
196207
197411
  const { state, reason } = this.state();
197412
+ const rendering = this.rendering();
196208
197413
  return ok2({
196209
197414
  machine: this.host.machine,
196210
- telegram: { state, ...reason === undefined ? {} : { reason }, sessions: this.host.hub()?.list().length ?? 0 }
197415
+ telegram: {
197416
+ state,
197417
+ ...reason === undefined ? {} : { reason },
197418
+ sessions: this.host.hub()?.list().length ?? 0,
197419
+ ...rendering === undefined ? {} : { rendering }
197420
+ }
196211
197421
  });
196212
197422
  }
196213
197423
  async dropPairing() {
@@ -196303,7 +197513,7 @@ class ChannelsController {
196303
197513
  try {
196304
197514
  await hub.sendGeneral(`Keryx test message from ${this.host.machine}: Telegram is connected.`);
196305
197515
  } catch (error) {
196306
- return fail4(502, "send-failed", `Telegram did not accept the test message: ${describe4(error)}`);
197516
+ return fail4(502, "send-failed", `Telegram did not accept the test message: ${describe5(error)}`);
196307
197517
  }
196308
197518
  return ok2({ delivered: true, machine: this.host.machine });
196309
197519
  }
@@ -196329,6 +197539,7 @@ var init_channels = __esm(() => {
196329
197539
  init_http_surface();
196330
197540
  init_pairing();
196331
197541
  init_paths4();
197542
+ init_rendering_mode();
196332
197543
  });
196333
197544
 
196334
197545
  // src/remote/poll-lock.ts
@@ -203120,6 +204331,156 @@ var init_retention2 = __esm(() => {
203120
204331
  init_sweep();
203121
204332
  });
203122
204333
 
204334
+ // src/remote/rendering-sample.ts
204335
+ function describeBlocks(message) {
204336
+ const counts = new Map;
204337
+ const walk = (blocks) => {
204338
+ for (const block of blocks) {
204339
+ counts.set(block.type, (counts.get(block.type) ?? 0) + 1);
204340
+ for (const item of block.items ?? []) {
204341
+ walk(item.blocks ?? []);
204342
+ }
204343
+ }
204344
+ };
204345
+ walk(message.blocks);
204346
+ return [...counts.entries()].map(([type, count]) => `${count} ${type}`).join(", ");
204347
+ }
204348
+ function htmlOrPlain(part) {
204349
+ const html = renderTelegramHtml(part);
204350
+ const checked = checkTelegramHtml(html);
204351
+ return checked.ok ? { kind: "html", text: checked.text } : { kind: "plain", text: renderPlainText(part), note: `HTML refused locally (${checked.reason}); plain text` };
204352
+ }
204353
+ function renderSample(mode, reply = SAMPLE_REPLY) {
204354
+ const pieces = formatReply(reply);
204355
+ const parts = pieces.map((piece, position) => {
204356
+ const base = { index: position + 1, total: pieces.length };
204357
+ const step = firstStep(mode, piece);
204358
+ if (step === "plain") {
204359
+ return { ...base, kind: "plain", text: renderPlainText(piece) };
204360
+ }
204361
+ if (step === "rich") {
204362
+ try {
204363
+ const richMessage = renderRichMessage(piece);
204364
+ return { ...base, kind: "rich", text: describeBlocks(richMessage), richMessage };
204365
+ } catch (error) {
204366
+ if (!(error instanceof RichRenderError)) {
204367
+ throw error;
204368
+ }
204369
+ return { ...base, ...htmlOrPlain(piece), note: `not renderable as a rich message (${error.message}); HTML instead` };
204370
+ }
204371
+ }
204372
+ return { ...base, ...htmlOrPlain(piece) };
204373
+ });
204374
+ return { mode, parts };
204375
+ }
204376
+ function renderAllSamples(reply = SAMPLE_REPLY) {
204377
+ return RENDER_MODES.map((mode) => renderSample(mode, reply));
204378
+ }
204379
+ function formatSampleText(renderings, options = {}) {
204380
+ const lines = ["Sample reply (source):", "", ...SAMPLE_REPLY.split(`
204381
+ `).map((line) => ` ${line}`), ""];
204382
+ for (const rendering of renderings) {
204383
+ lines.push(`=== ${MODE_HEADLINE[rendering.mode]} ===`);
204384
+ for (const part of rendering.parts) {
204385
+ lines.push(`--- part ${part.index}/${part.total}, sent as ${part.kind}${part.note === undefined ? "" : ` (${part.note})`} ---`);
204386
+ if (part.kind === "rich" && options.full === true) {
204387
+ lines.push(JSON.stringify(part.richMessage, null, 2));
204388
+ } else if (part.kind === "rich") {
204389
+ lines.push(`rich blocks: ${part.text}`);
204390
+ } else {
204391
+ lines.push(part.text);
204392
+ }
204393
+ }
204394
+ lines.push("");
204395
+ }
204396
+ return `${lines.join(`
204397
+ `)}
204398
+ `;
204399
+ }
204400
+ var SAMPLE_REPLY, MODE_HEADLINE;
204401
+ var init_rendering_sample = __esm(() => {
204402
+ init_format2();
204403
+ init_format_html();
204404
+ init_format_plain();
204405
+ init_format_rich();
204406
+ init_rendering();
204407
+ init_rendering_mode();
204408
+ SAMPLE_REPLY = [
204409
+ "## Release check",
204410
+ "",
204411
+ "Everything below is **one reply**: a table, lists, a rule and some `code`.",
204412
+ "",
204413
+ "| Step | Result | Time |",
204414
+ "|:-----|:------:|-----:|",
204415
+ "| build | `ok` | 41s |",
204416
+ "| unit tests | ok | 2m 08s |",
204417
+ "| smoke | **failed** | 12s |",
204418
+ "",
204419
+ "1. Fix the smoke test",
204420
+ "2. Re-run the release check",
204421
+ " - on the branch first",
204422
+ " - then on main",
204423
+ "",
204424
+ "- [x] changelog written",
204425
+ "- [ ] tag pushed",
204426
+ "",
204427
+ "---",
204428
+ "",
204429
+ "> The tag is pushed by the operator, not by the agent."
204430
+ ].join(`
204431
+ `);
204432
+ MODE_HEADLINE = {
204433
+ auto: "auto: rich where there is a table, otherwise HTML",
204434
+ rich: "rich: every reply as a rich message",
204435
+ html: "html: every reply as Telegram HTML",
204436
+ plain: "plain: every reply as plain text"
204437
+ };
204438
+ });
204439
+
204440
+ // src/commands/remote.ts
204441
+ function remoteCommand(args) {
204442
+ const [sub, ...rest] = args;
204443
+ if (sub === undefined || sub === "--help" || sub === "-h" || sub === "help") {
204444
+ console.log(REMOTE_USAGE);
204445
+ return;
204446
+ }
204447
+ if (sub !== "format-sample") {
204448
+ console.error(`keryx remote: unknown subcommand '${sub}'. Try \`keryx remote format-sample\`.`);
204449
+ process.exitCode = 1;
204450
+ return;
204451
+ }
204452
+ const wanted = optionValue(rest, "--mode");
204453
+ if (wanted !== undefined && !isRenderMode(wanted)) {
204454
+ console.error(`keryx remote format-sample: --mode must be ${RENDER_MODE_CHOICES}`);
204455
+ process.exitCode = 1;
204456
+ return;
204457
+ }
204458
+ const renderings = wanted === undefined ? renderAllSamples() : [renderSample(wanted)];
204459
+ if (rest.includes("--json")) {
204460
+ console.log(JSON.stringify({ sample: SAMPLE_REPLY, renderings }, null, 2));
204461
+ return;
204462
+ }
204463
+ process.stdout.write(formatSampleText(renderings, { full: rest.includes("--full") }));
204464
+ }
204465
+ var REMOTE_USAGE;
204466
+ var init_remote = __esm(() => {
204467
+ init_args();
204468
+ init_rendering_sample();
204469
+ init_rendering_mode();
204470
+ REMOTE_USAGE = `keryx remote format-sample \u2014 show the sample reply in every Telegram rendering mode (no network)
204471
+
204472
+ Usage:
204473
+ keryx remote format-sample [--mode ${RENDER_MODES.join("|")}] [--full] [--json]
204474
+
204475
+ --mode Only that mode. Without it all four are printed.
204476
+ --full Print the whole rich message (JSON) instead of a count of its blocks.
204477
+ --json Machine-readable: the sample source and, per mode, each part with how it is sent.
204478
+
204479
+ The mode in effect for a running session is set with \`/rendering\` in the shell, or the
204480
+ "Telegram rendering" row of \`/settings\`; \`/channels\` shows it together with the last fallback.
204481
+ `;
204482
+ });
204483
+
203123
204484
  // src/commands/forgetting.ts
203124
204485
  async function forgettingCommand(args) {
203125
204486
  const command = args[0];
@@ -204907,7 +206268,7 @@ function buildHeader(entry, size) {
204907
206268
  chk.copy(header, 148);
204908
206269
  return { ok: true, value: header };
204909
206270
  }
204910
- function pad4(buf) {
206271
+ function pad5(buf) {
204911
206272
  const rem = buf.length % BLOCK;
204912
206273
  if (rem === 0)
204913
206274
  return buf;
@@ -204942,7 +206303,7 @@ function buildBundleArchive(files, manifestBytes) {
204942
206303
  return header;
204943
206304
  chunks.push(header.value);
204944
206305
  if (entry.bytes && entry.bytes.length > 0) {
204945
- chunks.push(pad4(entry.bytes));
206306
+ chunks.push(pad5(entry.bytes));
204946
206307
  }
204947
206308
  }
204948
206309
  chunks.push(Buffer.alloc(BLOCK * 2));
@@ -209144,6 +210505,9 @@ var VERSION3, CLI_ROUTES, MCP_KNOWN_SUBCOMMANDS, USAGE_BODY = `Usage:
209144
210505
  keryx retention sweep [--apply] [--target <id>]... [--max-age-days <n>] [--max-bytes <n>] [--json]
209145
210506
  Bound gdctx raw/artifacts logs and owner write-conflict
209146
210507
  sidecars; dry run by default, --apply removes
210508
+ keryx remote format-sample [--mode auto|rich|html|plain] [--full] [--json]
210509
+ Print the sample reply in each Telegram rendering mode
210510
+ (tables, lists, rules); no network
209147
210511
  keryx forgetting trail [--limit <n>] [--json]
209148
210512
  keryx forgetting lookup "<ref-or-path>" [--layer <layer>] [--search] [--json]
209149
210513
  Read the deletion trail: what was removed, when, at
@@ -209241,6 +210605,7 @@ Commands:
209241
210605
  metrics Provenance-aware execution observability: run records, baselines, benchmarks
209242
210606
  workspace Shared Agent Context: workspaces, FWK reads, propose/review (module sac)
209243
210607
  retention Bound stores that grow without bound (gdctx raw/artifacts, owner write-conflict sidecars)
210608
+ remote Telegram remote control: preview how replies are rendered, with no network
209244
210609
  forgetting Read the deletion trail \u2014 was this removed, or did it never exist?
209245
210610
  trigger Fire one declared project trigger (git hook, cron line, CI job) \u2014 one pass, one exit code
209246
210611
  schedule Scheduled agent tasks in the background: create (with confirmation), list, pause, resume, remove
@@ -209298,6 +210663,7 @@ var init_cli_registry = __esm(() => {
209298
210663
  init_external2();
209299
210664
  init_auth();
209300
210665
  init_retention2();
210666
+ init_remote();
209301
210667
  init_forgetting();
209302
210668
  init_trigger();
209303
210669
  init_schedule2();
@@ -209366,6 +210732,7 @@ var init_cli_registry = __esm(() => {
209366
210732
  version: versionCommand,
209367
210733
  workspace: workspaceCommand,
209368
210734
  retention: retentionCommand,
210735
+ remote: remoteCommand,
209369
210736
  forgetting: forgettingCommand,
209370
210737
  trigger: triggerCommand,
209371
210738
  schedule: scheduleCommand,