@mrciphersmith/keryx 0.3.62 → 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 +1764 -394
  3. package/dist/core.js +15 -0
  4. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -9600,6 +9600,9 @@ function renderMetaprojectGitignoreBlock() {
9600
9600
  # entries rather than inheriting coverage that does not exist.
9601
9601
  .metaproject/data/learning/observations/
9602
9602
  .metaproject/data/learning/candidates/
9603
+ # Recommendation journal (flow 392): what the human chose and why they deviated.
9604
+ # Personal runtime state, written 0o600.
9605
+ .metaproject/data/decisions/
9603
9606
  `;
9604
9607
  }
9605
9608
  function escapeRegExp2(value) {
@@ -20104,7 +20107,7 @@ var package_default;
20104
20107
  var init_package = __esm(() => {
20105
20108
  package_default = {
20106
20109
  name: "@mrciphersmith/keryx",
20107
- version: "0.3.62",
20110
+ version: "0.3.64",
20108
20111
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
20109
20112
  private: false,
20110
20113
  publishConfig: {
@@ -30909,6 +30912,9 @@ function summarizeSubmittedLine(line) {
30909
30912
  function isZeroWidth(cp) {
30910
30913
  return cp >= 768 && cp <= 879 || cp >= 8203 && cp <= 8207 || cp >= 65024 && cp <= 65039 || cp >= 8288 && cp <= 8292;
30911
30914
  }
30915
+ function isEmojiModifier(cp) {
30916
+ return cp >= 127995 && cp <= 127999;
30917
+ }
30912
30918
  function codePointWidth(cp) {
30913
30919
  for (const [lo, hi] of WIDE_RANGES) {
30914
30920
  if (cp >= lo && cp <= hi) {
@@ -30919,11 +30925,17 @@ function codePointWidth(cp) {
30919
30925
  }
30920
30926
  function visualWidth(text) {
30921
30927
  let width = 0;
30928
+ let previous = 0;
30922
30929
  for (const ch of text) {
30923
30930
  const codePoint = ch.codePointAt(0) ?? 0;
30931
+ const joined = previous === ZERO_WIDTH_JOINER;
30932
+ previous = codePoint;
30924
30933
  if (isZeroWidth(codePoint)) {
30925
30934
  continue;
30926
30935
  }
30936
+ if (joined && codePointWidth(codePoint) === 2 || isEmojiModifier(codePoint)) {
30937
+ continue;
30938
+ }
30927
30939
  width += codePointWidth(codePoint);
30928
30940
  }
30929
30941
  return width;
@@ -30936,7 +30948,7 @@ function hugWidth(text, chrome) {
30936
30948
  }
30937
30949
  return widest + chrome;
30938
30950
  }
30939
- 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;
30940
30952
  var init_md_blocks = __esm(() => {
30941
30953
  FENCE_LINE = /^[ \t]{0,3}(```|~~~)(.*)\r?$/;
30942
30954
  HUNK_HEADER = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/;
@@ -31184,8 +31196,48 @@ var init_md_blocks = __esm(() => {
31184
31196
  [65072, 65135],
31185
31197
  [65280, 65376],
31186
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],
31187
31237
  [127744, 128591],
31238
+ [128640, 128767],
31188
31239
  [129280, 129535],
31240
+ [129536, 129791],
31189
31241
  [131072, 262141]
31190
31242
  ];
31191
31243
  });
@@ -100458,6 +100510,264 @@ var init_live_render = __esm(() => {
100458
100510
  ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
100459
100511
  });
100460
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
+
100461
100771
  // src/tui/reasoning-display.ts
100462
100772
  function parseThinkDisplayMode(raw) {
100463
100773
  if (raw === undefined) {
@@ -100517,7 +100827,7 @@ var init_reasoning_display = __esm(() => {
100517
100827
  });
100518
100828
 
100519
100829
  // src/tui/debug-log.ts
100520
- import path228 from "path";
100830
+ import path229 from "path";
100521
100831
  function isDebugEnabled() {
100522
100832
  return activeRun !== undefined;
100523
100833
  }
@@ -100545,11 +100855,11 @@ function startDebugRun(opts = {}) {
100545
100855
  const dir = ensureKeryxSubdir(["debug", name], opts.configDir);
100546
100856
  const run = {
100547
100857
  dir,
100548
- shellLog: path228.join(dir, "shell.ndjson"),
100549
- watcherLog: path228.join(dir, "watcher.ndjson")
100858
+ shellLog: path229.join(dir, "shell.ndjson"),
100859
+ watcherLog: path229.join(dir, "watcher.ndjson")
100550
100860
  };
100551
100861
  try {
100552
- writeOwnerOnlyFile(path228.join(path228.dirname(dir), "latest.txt"), `${dir}
100862
+ writeOwnerOnlyFile(path229.join(path229.dirname(dir), "latest.txt"), `${dir}
100553
100863
  `);
100554
100864
  } catch {}
100555
100865
  activeRun = run;
@@ -100615,7 +100925,7 @@ var init_debug_log = __esm(() => {
100615
100925
 
100616
100926
  // src/tui/theme.ts
100617
100927
  import { existsSync as existsSync39 } from "fs";
100618
- import path229 from "path";
100928
+ import path230 from "path";
100619
100929
  function hexChannels(hex) {
100620
100930
  return [1, 3, 5].map((offset) => Number.parseInt(hex.slice(offset, offset + 2), 16));
100621
100931
  }
@@ -100753,7 +101063,7 @@ function onThemeChange(listener) {
100753
101063
  };
100754
101064
  }
100755
101065
  function tuiConfigPath(dir) {
100756
- return path229.join(keryxConfigDir(dir), "tui.json");
101066
+ return path230.join(keryxConfigDir(dir), "tui.json");
100757
101067
  }
100758
101068
  function loadPersistedThemeId(dir) {
100759
101069
  try {
@@ -101149,6 +101459,17 @@ function buildSettingsRows(state) {
101149
101459
  usage: "/external-agents [on|off]",
101150
101460
  actions: toggleActions("/external-agents", state.externalAgents.on)
101151
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
+ },
101152
101473
  {
101153
101474
  id: "jevprofile",
101154
101475
  group: "External",
@@ -101187,6 +101508,7 @@ var SETTING_GROUPS, onOff = (value) => value ? "on" : "off", EXTERNAL_PRIVACY_DE
101187
101508
  var init_settings_model = __esm(() => {
101188
101509
  init_agent();
101189
101510
  init_permission_mode();
101511
+ init_rendering_mode();
101190
101512
  init_reasoning_display();
101191
101513
  init_theme();
101192
101514
  SETTING_GROUPS = ["Safety", "Routing", "Display", "External"];
@@ -101306,7 +101628,7 @@ var init_jev_profile = __esm(() => {
101306
101628
 
101307
101629
  // src/commands/review-jev-profile.ts
101308
101630
  import { readFile as readFile106 } from "fs/promises";
101309
- import path230 from "path";
101631
+ import path231 from "path";
101310
101632
  function rejectUnknownJevProfileFlags(args) {
101311
101633
  const unknown = args.filter((arg) => arg.startsWith("--") && !JEV_PROFILE_FLAGS.includes(arg.split("=")[0]));
101312
101634
  if (unknown.length > 0) {
@@ -101315,7 +101637,7 @@ function rejectUnknownJevProfileFlags(args) {
101315
101637
  }
101316
101638
  async function readRawConfig(cwd) {
101317
101639
  try {
101318
- 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"));
101319
101641
  } catch {
101320
101642
  return;
101321
101643
  }
@@ -101339,7 +101661,7 @@ async function runJevProfile(args) {
101339
101661
  return;
101340
101662
  }
101341
101663
  const merged = mergeRecommendedJevProfile(raw);
101342
- 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)}
101343
101665
  `);
101344
101666
  const current = currentJevProfile(merged);
101345
101667
  if (args.includes("--json")) {
@@ -101354,13 +101676,13 @@ async function readJevProfileForShell(cwd) {
101354
101676
  async function toggleJevProfileKeyForShell(cwd, key) {
101355
101677
  const raw = await readRawConfig(cwd);
101356
101678
  const merged = toggleJevProfileKey(raw, key);
101357
- 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)}
101358
101680
  `);
101359
101681
  }
101360
101682
  async function applyRecommendedJevProfileForShell(cwd) {
101361
101683
  const raw = await readRawConfig(cwd);
101362
101684
  const merged = mergeRecommendedJevProfile(raw);
101363
- 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)}
101364
101686
  `);
101365
101687
  }
101366
101688
  var JEV_PROFILE_FLAGS;
@@ -101382,14 +101704,14 @@ import {
101382
101704
  openSync as openSync4,
101383
101705
  realpathSync as realpathSync16,
101384
101706
  renameSync as renameSync4,
101385
- statSync as statSync14,
101386
- unlinkSync as unlinkSync4,
101707
+ statSync as statSync15,
101708
+ unlinkSync as unlinkSync5,
101387
101709
  writeFileSync as writeFileSync7
101388
101710
  } from "fs";
101389
- import path231 from "path";
101711
+ import path232 from "path";
101390
101712
  import { randomUUID as randomUUID28 } from "crypto";
101391
101713
  function projectRegistryPath(dir) {
101392
- return path231.join(keryxConfigDir(dir), "projects.json");
101714
+ return path232.join(keryxConfigDir(dir), "projects.json");
101393
101715
  }
101394
101716
  function withRegistryLock(dir, fn, onWaiting) {
101395
101717
  return withFileLock2(`${projectRegistryPath(dir)}.lock`, fn, {
@@ -101405,7 +101727,7 @@ function validEntry(candidate) {
101405
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";
101406
101728
  }
101407
101729
  function projectIdentity(projectPath) {
101408
- const absolute = path231.resolve(projectPath);
101730
+ const absolute = path232.resolve(projectPath);
101409
101731
  try {
101410
101732
  return realpathSync16.native(absolute);
101411
101733
  } catch {
@@ -101521,7 +101843,7 @@ function saveProjectRegistry(registry, dir, onWarn) {
101521
101843
  return true;
101522
101844
  } catch {
101523
101845
  try {
101524
- unlinkSync4(temp);
101846
+ unlinkSync5(temp);
101525
101847
  } catch {}
101526
101848
  return false;
101527
101849
  }
@@ -101560,7 +101882,7 @@ function registerProject(projectPath, options = {}) {
101560
101882
  const entry = {
101561
101883
  projectId: randomUUID28(),
101562
101884
  path: absolute,
101563
- displayName: options.displayName ?? path231.basename(absolute),
101885
+ displayName: options.displayName ?? path232.basename(absolute),
101564
101886
  state: "active",
101565
101887
  registeredAt: now,
101566
101888
  lastSeenAt: now
@@ -101597,7 +101919,7 @@ function forgetProject(projectId, dir, onWarn) {
101597
101919
  return outcome ?? "write-failed";
101598
101920
  }
101599
101921
  function isInitializedProject(projectPath) {
101600
- return statSync14(path231.join(projectPath, ".metaproject"), { throwIfNoEntry: false })?.isDirectory() === true;
101922
+ return statSync15(path232.join(projectPath, ".metaproject"), { throwIfNoEntry: false })?.isDirectory() === true;
101601
101923
  }
101602
101924
  function listProjects(dir, onWarn) {
101603
101925
  const registry = loadProjectRegistry(dir, onWarn);
@@ -101651,9 +101973,9 @@ var init_project_registry = __esm(() => {
101651
101973
  });
101652
101974
 
101653
101975
  // src/lib/permission-mode-config.ts
101654
- import path232 from "path";
101976
+ import path233 from "path";
101655
101977
  function permissionModeConfigPath(dir) {
101656
- return path232.join(keryxConfigDir(dir), "permission-mode.json");
101978
+ return path233.join(keryxConfigDir(dir), "permission-mode.json");
101657
101979
  }
101658
101980
  function withRegistryLock2(dir, fn) {
101659
101981
  return withFileLock2(`${permissionModeConfigPath(dir)}.lock`, fn, {
@@ -101760,10 +102082,10 @@ var init_jev_edit_guard = __esm(() => {
101760
102082
 
101761
102083
  // src/review/jev-edit-guard-config.ts
101762
102084
  import { readFile as readFile107 } from "fs/promises";
101763
- import path233 from "path";
102085
+ import path234 from "path";
101764
102086
  async function readReviewJevBlock(cwd) {
101765
102087
  try {
101766
- 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"));
101767
102089
  if (typeof parsed !== "object" || parsed === null)
101768
102090
  return;
101769
102091
  const review = parsed["review"];
@@ -101808,7 +102130,7 @@ async function readJevEditGuardConfig(cwd, opts) {
101808
102130
  return { enabled, threshold, maxCalls };
101809
102131
  }
101810
102132
  async function writeJevEditGuardEnabled(cwd, enabled) {
101811
- const file = path233.join(cwd, REVIEW_GATE_CONFIG_PATH);
102133
+ const file = path234.join(cwd, REVIEW_GATE_CONFIG_PATH);
101812
102134
  let root = {};
101813
102135
  if (await pathExists(file)) {
101814
102136
  try {
@@ -101842,6 +102164,14 @@ function readReasoning2(sessionOverride, globalEffort) {
101842
102164
  const afterRestart = describeReasoningEffortSource({ globalEffort });
101843
102165
  return resolved.source === "session" && afterRestart.source === "env" ? { ...resolved, envWinsOnRestart: true } : resolved;
101844
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
+ }
101845
102175
  async function orElse(read, fallback) {
101846
102176
  try {
101847
102177
  return await read();
@@ -101873,7 +102203,8 @@ async function loadSettingsSnapshot(cwd, live, configDir) {
101873
102203
  on: RECOMMENDED_JEV_PROFILE.filter((entry) => jevProfile[entry.key] === true).length,
101874
102204
  total: RECOMMENDED_JEV_PROFILE.length
101875
102205
  },
101876
- 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)
101877
102208
  };
101878
102209
  }
101879
102210
  var init_settings_state = __esm(() => {
@@ -101883,6 +102214,8 @@ var init_settings_state = __esm(() => {
101883
102214
  init_external_switch();
101884
102215
  init_permission_mode_config();
101885
102216
  init_shell_config();
102217
+ init_rendering_config();
102218
+ init_rendering_mode();
101886
102219
  init_jev_edit_guard_config();
101887
102220
  init_jev_profile();
101888
102221
  init_reasoning_display();
@@ -101938,7 +102271,7 @@ class NextStepSuggestionGate {
101938
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: .";
101939
102272
 
101940
102273
  // src/lib/version-check.ts
101941
- import path234 from "path";
102274
+ import path235 from "path";
101942
102275
  function formatVersionUpdateAdvisory(result) {
101943
102276
  if (result.status !== "update-available")
101944
102277
  return;
@@ -102125,7 +102458,7 @@ async function checkVersion(options) {
102125
102458
  const now = options.now ?? Date.now;
102126
102459
  const timestamp = now();
102127
102460
  const configDir = ensureKeryxConfigDir(options.cacheDir);
102128
- const cacheFile = path234.join(configDir, "version-check.json");
102461
+ const cacheFile = path235.join(configDir, "version-check.json");
102129
102462
  const cache = parseCache(cacheFile);
102130
102463
  if (cache?.latestVersion !== undefined && cache.successAt !== undefined && timestamp - cache.successAt >= 0 && timestamp - cache.successAt < SUCCESS_CACHE_TTL_MS) {
102131
102464
  return resultFor(options.currentVersion, current, cache.latestVersion, "cache");
@@ -102453,7 +102786,7 @@ var init_schemas2 = __esm(() => {
102453
102786
  var init_types7 = () => {};
102454
102787
 
102455
102788
  // src/standard/profiles.ts
102456
- import path235 from "path";
102789
+ import path236 from "path";
102457
102790
  import { readFile as readFile108 } from "fs/promises";
102458
102791
  function namedEntrypoints(manifest) {
102459
102792
  const agentEntrypoints = manifest.agentEntrypoints;
@@ -102491,7 +102824,7 @@ function enabledModuleKeys(manifest) {
102491
102824
  }
102492
102825
  async function rootEntrypointLinksIndex(cwd, manifest) {
102493
102826
  for (const { path: name } of namedEntrypoints(manifest)) {
102494
- const filePath = path235.join(cwd, name);
102827
+ const filePath = path236.join(cwd, name);
102495
102828
  if (!await pathExists(filePath)) {
102496
102829
  continue;
102497
102830
  }
@@ -102503,16 +102836,16 @@ async function rootEntrypointLinksIndex(cwd, manifest) {
102503
102836
  return false;
102504
102837
  }
102505
102838
  async function minimalSatisfied(cwd) {
102506
- const metaprojectRoot = path235.join(cwd, ".metaproject");
102839
+ const metaprojectRoot = path236.join(cwd, ".metaproject");
102507
102840
  const requiredFiles = ["index.md", "README.md", "metaproject.json"];
102508
102841
  const requiredDirs = ["modules", "rules", "skills", "data"];
102509
102842
  for (const file of requiredFiles) {
102510
- if (!await pathExists(path235.join(metaprojectRoot, file))) {
102843
+ if (!await pathExists(path236.join(metaprojectRoot, file))) {
102511
102844
  return false;
102512
102845
  }
102513
102846
  }
102514
102847
  for (const dir of requiredDirs) {
102515
- if (!await pathExists(path235.join(metaprojectRoot, dir))) {
102848
+ if (!await pathExists(path236.join(metaprojectRoot, dir))) {
102516
102849
  return false;
102517
102850
  }
102518
102851
  }
@@ -102525,7 +102858,7 @@ async function ciSatisfied(cwd, manifest) {
102525
102858
  return false;
102526
102859
  }
102527
102860
  for (const key of ciModules) {
102528
- if (await pathExists(path235.join(cwd, ".metaproject", "data", key, "artifacts"))) {
102861
+ if (await pathExists(path236.join(cwd, ".metaproject", "data", key, "artifacts"))) {
102529
102862
  return true;
102530
102863
  }
102531
102864
  }
@@ -102538,7 +102871,7 @@ async function evaluateProfiles(cwd, manifest) {
102538
102871
  satisfied.push("minimal");
102539
102872
  }
102540
102873
  const hasAgentModule = AGENT_MODULES.some((key) => new Set(enabledModuleKeys(manifest)).has(key));
102541
- 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"));
102542
102875
  if (agent) {
102543
102876
  satisfied.push("agent");
102544
102877
  }
@@ -102571,7 +102904,7 @@ var init_profiles2 = __esm(() => {
102571
102904
  });
102572
102905
 
102573
102906
  // src/standard/validate.ts
102574
- import path236 from "path";
102907
+ import path237 from "path";
102575
102908
  import { readFile as readFile109 } from "fs/promises";
102576
102909
  function isPlainObject14(value) {
102577
102910
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -102700,20 +103033,20 @@ function issue(code, message, fix) {
102700
103033
  async function validateWorkspace2(cwd) {
102701
103034
  const errors = [];
102702
103035
  const warnings = [];
102703
- const metaprojectRoot = path236.join(cwd, ".metaproject");
103036
+ const metaprojectRoot = path237.join(cwd, ".metaproject");
102704
103037
  const requiredFiles = ["index.md", "README.md", "metaproject.json"];
102705
103038
  for (const file of requiredFiles) {
102706
- if (!await pathExists(path236.join(metaprojectRoot, file))) {
103039
+ if (!await pathExists(path237.join(metaprojectRoot, file))) {
102707
103040
  errors.push(issue("missing-required-file", `Required file .metaproject/${file} is missing`, `Run \`keryx init\` to scaffold .metaproject/${file}.`));
102708
103041
  }
102709
103042
  }
102710
103043
  const requiredDirs = ["modules", "rules", "skills", "data"];
102711
103044
  for (const dir of requiredDirs) {
102712
- if (!await pathExists(path236.join(metaprojectRoot, dir))) {
103045
+ if (!await pathExists(path237.join(metaprojectRoot, dir))) {
102713
103046
  errors.push(issue("missing-required-dir", `Required directory .metaproject/${dir}/ is missing`, `Run \`keryx init\` to create .metaproject/${dir}/.`));
102714
103047
  }
102715
103048
  }
102716
- const manifestPath = path236.join(metaprojectRoot, "metaproject.json");
103049
+ const manifestPath = path237.join(metaprojectRoot, "metaproject.json");
102717
103050
  if (!await pathExists(manifestPath)) {
102718
103051
  return { ok: errors.length === 0, errors, warnings };
102719
103052
  }
@@ -102745,7 +103078,7 @@ async function validateWorkspace2(cwd) {
102745
103078
  if (typeof value !== "string") {
102746
103079
  continue;
102747
103080
  }
102748
- if (!await pathExists(path236.join(cwd, value))) {
103081
+ if (!await pathExists(path237.join(cwd, value))) {
102749
103082
  errors.push(issue("missing-declared-path", `Declared path paths.${key} -> ${value} does not exist`, `Create ${value} or remove paths.${key} from metaproject.json.`));
102750
103083
  }
102751
103084
  }
@@ -102755,7 +103088,7 @@ async function validateWorkspace2(cwd) {
102755
103088
  continue;
102756
103089
  }
102757
103090
  if (typeof entry.manifest === "string") {
102758
- if (!await pathExists(path236.join(cwd, entry.manifest))) {
103091
+ if (!await pathExists(path237.join(cwd, entry.manifest))) {
102759
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.`));
102760
103093
  }
102761
103094
  }
@@ -102764,14 +103097,14 @@ async function validateWorkspace2(cwd) {
102764
103097
  if (typeof value !== "string") {
102765
103098
  continue;
102766
103099
  }
102767
- if (!await pathExists(path236.join(cwd, value))) {
103100
+ if (!await pathExists(path237.join(cwd, value))) {
102768
103101
  const sink = field === "data" ? warnings : errors;
102769
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.`));
102770
103103
  }
102771
103104
  }
102772
103105
  }
102773
103106
  for (const { path: name, scope } of namedEntrypoints(manifest)) {
102774
- const filePath = path236.join(cwd, name);
103107
+ const filePath = path237.join(cwd, name);
102775
103108
  if (!await pathExists(filePath)) {
102776
103109
  continue;
102777
103110
  }
@@ -103436,6 +103769,12 @@ var init_help_groups = __esm(() => {
103436
103769
  group: "Automation",
103437
103770
  summary: "Connect, test or disconnect Telegram for this machine: bot token, one-time code, group. [status]."
103438
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
+ },
103439
103778
  {
103440
103779
  kind: "cli",
103441
103780
  name: "acp",
@@ -103628,6 +103967,12 @@ var init_help_groups = __esm(() => {
103628
103967
  group: "Maintenance and diagnostics",
103629
103968
  summary: "Bound stores that grow without bound (gdctx raw/artifacts, owner write-conflict sidecars)."
103630
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
+ },
103631
103976
  {
103632
103977
  kind: "cli",
103633
103978
  name: "forgetting",
@@ -103644,7 +103989,7 @@ var init_help_groups = __esm(() => {
103644
103989
  });
103645
103990
 
103646
103991
  // src/standard/service.ts
103647
- import path237 from "path";
103992
+ import path238 from "path";
103648
103993
  import { readFile as readFile110 } from "fs/promises";
103649
103994
  async function runValidate(cwd) {
103650
103995
  return validateWorkspace2(cwd);
@@ -103653,7 +103998,7 @@ async function runDoctor2(cwd) {
103653
103998
  return validateWorkspace2(cwd);
103654
103999
  }
103655
104000
  async function readManifest2(cwd) {
103656
- const manifestPath = path237.join(cwd, ".metaproject", "metaproject.json");
104001
+ const manifestPath = path238.join(cwd, ".metaproject", "metaproject.json");
103657
104002
  if (!await pathExists(manifestPath)) {
103658
104003
  throw new Error("Metaproject is not initialized (missing .metaproject/metaproject.json). Run `keryx init` first.");
103659
104004
  }
@@ -103680,7 +104025,7 @@ var init_service14 = __esm(() => {
103680
104025
 
103681
104026
  // src/rules/entrypoint-inspection.ts
103682
104027
  import { readFile as readFile111 } from "fs/promises";
103683
- import path238 from "path";
104028
+ import path239 from "path";
103684
104029
  async function inspectEntrypoints(projectRoot, agentEntrypoints, options = {}) {
103685
104030
  const git = await resolveGitCommonDir(projectRoot) !== undefined;
103686
104031
  const resolved = await resolveProjectEntrypoints(projectRoot, agentEntrypoints);
@@ -103765,14 +104110,14 @@ async function inspectEntrypoints(projectRoot, agentEntrypoints, options = {}) {
103765
104110
  codex = { entry, mode: "skip" };
103766
104111
  continue;
103767
104112
  }
103768
- const sourceExists = await pathExists(path238.join(projectRoot, entry.source));
104113
+ const sourceExists = await pathExists(path239.join(projectRoot, entry.source));
103769
104114
  const state = await codexOverrideState(projectRoot, entry);
103770
104115
  const bytes = await codexOverrideByteSize(projectRoot, entry);
103771
104116
  codex = { entry, mode: "override", sourceExists, state, ...bytes === undefined ? {} : { bytes } };
103772
104117
  localTargets.push({ path: entry.path, kind: "codex", exists: state !== "missing", expected: sourceExists });
103773
104118
  }
103774
104119
  if (targets.claudeSettings.scope === "local") {
103775
- const exists = await pathExists(path238.join(projectRoot, ...hooksTarget.split("/")));
104120
+ const exists = await pathExists(path239.join(projectRoot, ...hooksTarget.split("/")));
103776
104121
  localTargets.push({ path: hooksTarget, kind: "claudeSettings", exists, expected: options.hooksExpected === true });
103777
104122
  }
103778
104123
  const existing = localTargets.filter((target) => target.exists);
@@ -103901,14 +104246,14 @@ function codexPreviewLines(codex) {
103901
104246
  }
103902
104247
  async function readText3(projectRoot, relativePath) {
103903
104248
  try {
103904
- return await readFile111(path238.join(projectRoot, ...relativePath.split("/")), "utf8");
104249
+ return await readFile111(path239.join(projectRoot, ...relativePath.split("/")), "utf8");
103905
104250
  } catch {
103906
104251
  return;
103907
104252
  }
103908
104253
  }
103909
104254
  function displayPath2(projectRoot, target) {
103910
- const relative = path238.relative(projectRoot, target);
103911
- 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("/");
103912
104257
  }
103913
104258
  function isRecord10(value) {
103914
104259
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -103932,7 +104277,7 @@ var init_entrypoint_inspection = __esm(() => {
103932
104277
  import { readdir as readdir35, readFile as readFile112 } from "fs/promises";
103933
104278
  import { existsSync as existsSync41 } from "fs";
103934
104279
  import { homedir as homedir10, tmpdir as tmpdir10 } from "os";
103935
- import path239 from "path";
104280
+ import path240 from "path";
103936
104281
  async function registeredWorktrees(cwd) {
103937
104282
  if (!Bun.which("git"))
103938
104283
  return new Set;
@@ -103945,7 +104290,7 @@ async function registeredWorktrees(cwd) {
103945
104290
  for (const line of stdout.split(`
103946
104291
  `)) {
103947
104292
  if (line.startsWith("worktree ")) {
103948
- paths.add(path239.resolve(line.slice("worktree ".length).trim()));
104293
+ paths.add(path240.resolve(line.slice("worktree ".length).trim()));
103949
104294
  }
103950
104295
  }
103951
104296
  return paths;
@@ -103955,7 +104300,7 @@ async function registeredWorktrees(cwd) {
103955
104300
  }
103956
104301
  async function checkWorktrees(cwd) {
103957
104302
  const mainRoot = await resolveMainCheckoutRoot(cwd) ?? cwd;
103958
- const worktreesDir = path239.join(mainRoot, ".claude", "worktrees");
104303
+ const worktreesDir = path240.join(mainRoot, ".claude", "worktrees");
103959
104304
  let entries;
103960
104305
  try {
103961
104306
  entries = (await readdir35(worktreesDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
@@ -103967,8 +104312,8 @@ async function checkWorktrees(cwd) {
103967
104312
  }
103968
104313
  const registered = await registeredWorktrees(cwd);
103969
104314
  const stale = entries.filter((name) => {
103970
- const full = path239.resolve(worktreesDir, name);
103971
- 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);
103972
104317
  });
103973
104318
  if (stale.length === 0) {
103974
104319
  return {
@@ -104134,7 +104479,7 @@ async function checkEntrypoints(cwd) {
104134
104479
  const projectRoot = resolveProjectRoot2(cwd);
104135
104480
  let manifest;
104136
104481
  try {
104137
- 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"));
104138
104483
  } catch {
104139
104484
  manifest = undefined;
104140
104485
  }
@@ -104289,22 +104634,22 @@ async function checkWikiFreshness(cwd) {
104289
104634
  };
104290
104635
  }
104291
104636
  function isExcludedProjectRootCandidate(dir) {
104292
- return dir === path239.parse(dir).root || dir === "/tmp" || dir === tmpdir10();
104637
+ return dir === path240.parse(dir).root || dir === "/tmp" || dir === tmpdir10();
104293
104638
  }
104294
104639
  function findMetaprojectUpward(cwd, ceiling) {
104295
- let current = path239.resolve(cwd);
104296
- const resolvedCeiling = path239.resolve(ceiling);
104640
+ let current = path240.resolve(cwd);
104641
+ const resolvedCeiling = path240.resolve(ceiling);
104297
104642
  for (;; ) {
104298
104643
  if (isExcludedProjectRootCandidate(current)) {
104299
104644
  return;
104300
104645
  }
104301
- if (existsSync41(path239.join(current, ".metaproject"))) {
104646
+ if (existsSync41(path240.join(current, ".metaproject"))) {
104302
104647
  return current;
104303
104648
  }
104304
104649
  if (current === resolvedCeiling) {
104305
104650
  return;
104306
104651
  }
104307
- const parent = path239.dirname(current);
104652
+ const parent = path240.dirname(current);
104308
104653
  if (parent === current) {
104309
104654
  return;
104310
104655
  }
@@ -104318,7 +104663,7 @@ async function isKeryxProject(cwd, overrides = {}) {
104318
104663
  return true;
104319
104664
  }
104320
104665
  const mainRoot = await resolveMainCheckoutRoot(cwd);
104321
- return mainRoot !== undefined && existsSync41(path239.join(mainRoot, ".metaproject"));
104666
+ return mainRoot !== undefined && existsSync41(path240.join(mainRoot, ".metaproject"));
104322
104667
  }
104323
104668
  async function buildDoctorReport(cwd, env = process.env, overrides = {}) {
104324
104669
  const globalChecks = await Promise.all([
@@ -104406,7 +104751,7 @@ var init_doctor2 = __esm(() => {
104406
104751
  });
104407
104752
 
104408
104753
  // src/commands/goal-command.ts
104409
- import path240 from "path";
104754
+ import path241 from "path";
104410
104755
  import { readFile as readFile113 } from "fs/promises";
104411
104756
  function parseGoalArgs(rest) {
104412
104757
  const trimmed = rest.trim();
@@ -104479,7 +104824,7 @@ function getFlowService() {
104479
104824
  async function autoProvisionFlow(cwd, goalText) {
104480
104825
  const service = getFlowService();
104481
104826
  const result = await service.init({ cwd, title: goalText });
104482
- const acFile = path240.join(cwd, result.dir, "acceptance-criteria.md");
104827
+ const acFile = path241.join(cwd, result.dir, "acceptance-criteria.md");
104483
104828
  await writeFileAtomic(acFile, [
104484
104829
  "# Acceptance Criteria",
104485
104830
  "",
@@ -107517,7 +107862,7 @@ __export(exports_flow, {
107517
107862
  signerIdentityArgs: () => signerIdentityArgs
107518
107863
  });
107519
107864
  import { randomInt } from "crypto";
107520
- import path241 from "path";
107865
+ import path242 from "path";
107521
107866
  function parseTaskKind(raw) {
107522
107867
  if (raw === undefined) {
107523
107868
  return;
@@ -108432,7 +108777,7 @@ async function runSchema(args) {
108432
108777
  `;
108433
108778
  const out = optionValue(args, "--out");
108434
108779
  if (out) {
108435
- const target = path241.isAbsolute(out) ? out : path241.join(process.cwd(), out);
108780
+ const target = path242.isAbsolute(out) ? out : path242.join(process.cwd(), out);
108436
108781
  await writeFileAtomic(target, json);
108437
108782
  console.log(` ${style.green(symbols.ok)} Wrote flow-state schema ${style.cyan(symbols.arrow)} ${out}`);
108438
108783
  return;
@@ -108724,7 +109069,7 @@ var init_lifecycle_flag = __esm(() => {
108724
109069
  // src/sac/catch-up.ts
108725
109070
  import { randomUUID as randomUUID30 } from "crypto";
108726
109071
  import { readdir as readdir36, stat as stat24 } from "fs/promises";
108727
- import path242 from "path";
109072
+ import path243 from "path";
108728
109073
  async function buildCatchUp(input) {
108729
109074
  const [proposals, sessionCategories, lifecycleFlagsAll, unreviewedPathsAll] = await Promise.all([
108730
109075
  collectProposals(input.cwd, input.workspaceId),
@@ -108843,7 +109188,7 @@ async function readExternalUnboundCandidates(cwd) {
108843
109188
  }
108844
109189
  return groups;
108845
109190
  }, []));
108846
- const metaPath = path242.join(extDir, `${id}.json`);
109191
+ const metaPath = path243.join(extDir, `${id}.json`);
108847
109192
  if (await pathExists(unboundDismissedReceiptPath(metaPath)))
108848
109193
  continue;
108849
109194
  candidates.push({
@@ -108857,7 +109202,7 @@ async function readExternalUnboundCandidates(cwd) {
108857
109202
  return candidates;
108858
109203
  }
108859
109204
  async function readNewestUnboundCandidateForExternal(cwd, externalSessionId) {
108860
- const evidenceDir = path242.join(externalSlatesDir(cwd), externalSessionId);
109205
+ const evidenceDir = path243.join(externalSlatesDir(cwd), externalSessionId);
108861
109206
  let entries;
108862
109207
  try {
108863
109208
  entries = (await readdir36(evidenceDir)).filter((name) => name.endsWith("-unbound-candidate.json"));
@@ -108866,7 +109211,7 @@ async function readNewestUnboundCandidateForExternal(cwd, externalSessionId) {
108866
109211
  }
108867
109212
  entries.sort();
108868
109213
  for (let i = entries.length - 1;i >= 0; i--) {
108869
- const evidencePath = path242.join(evidenceDir, entries[i]);
109214
+ const evidencePath = path243.join(evidenceDir, entries[i]);
108870
109215
  if (await pathExists(unboundDismissedReceiptPath(evidencePath)))
108871
109216
  continue;
108872
109217
  const result = readConfigFile(evidencePath);
@@ -108907,7 +109252,7 @@ async function collectSessionCategories(cwd) {
108907
109252
  }
108908
109253
  async function collectReceiptTargets(cwd, owner) {
108909
109254
  const targets = new Set;
108910
- const workspacesDir = path242.join(cwd, ".metaproject", "workspaces");
109255
+ const workspacesDir = path243.join(cwd, ".metaproject", "workspaces");
108911
109256
  let workspaceIds;
108912
109257
  try {
108913
109258
  workspaceIds = (await readdir36(workspacesDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
@@ -108915,7 +109260,7 @@ async function collectReceiptTargets(cwd, owner) {
108915
109260
  return targets;
108916
109261
  }
108917
109262
  for (const workspaceId of workspaceIds) {
108918
- const receiptsDir = path242.join(workspacesDir, workspaceId, `${owner}-write-receipts`);
109263
+ const receiptsDir = path243.join(workspacesDir, workspaceId, `${owner}-write-receipts`);
108919
109264
  let files;
108920
109265
  try {
108921
109266
  files = (await readdir36(receiptsDir)).filter((name) => name.endsWith(".json"));
@@ -108923,7 +109268,7 @@ async function collectReceiptTargets(cwd, owner) {
108923
109268
  continue;
108924
109269
  }
108925
109270
  for (const file of files) {
108926
- const result = readConfigFile(path242.join(receiptsDir, file));
109271
+ const result = readConfigFile(path243.join(receiptsDir, file));
108927
109272
  if (!result.ok)
108928
109273
  continue;
108929
109274
  try {
@@ -108966,7 +109311,7 @@ async function findSkillFiles(dir) {
108966
109311
  }
108967
109312
  const found = [];
108968
109313
  for (const entry of entries) {
108969
- const full = path242.join(dir, entry.name);
109314
+ const full = path243.join(dir, entry.name);
108970
109315
  if (entry.isDirectory()) {
108971
109316
  found.push(...await findSkillFiles(full));
108972
109317
  } else if (entry.isFile() && entry.name === "SKILL.md") {
@@ -109025,10 +109370,10 @@ async function detectUnreviewedSacPathChanges(cwd, sessions) {
109025
109370
  changedAt: attribution.changedAt
109026
109371
  });
109027
109372
  }
109028
- const sacSkillsDir = path242.join(cwd, ".metaproject", "project-skills", "sac");
109373
+ const sacSkillsDir = path243.join(cwd, ".metaproject", "project-skills", "sac");
109029
109374
  const skillFiles = await findSkillFiles(sacSkillsDir);
109030
109375
  for (const absolutePath of skillFiles) {
109031
- 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("/");
109032
109377
  if (skillReceipts.has(targetRef))
109033
109378
  continue;
109034
109379
  const attribution = await attributeToSession(absolutePath, scoped);
@@ -109046,12 +109391,12 @@ async function detectUnreviewedSacPathChanges(cwd, sessions) {
109046
109391
  return items;
109047
109392
  }
109048
109393
  async function isSlateEngaged(dir) {
109049
- if (await pathExists(path242.join(dir, "slate.json")))
109394
+ if (await pathExists(path243.join(dir, "slate.json")))
109050
109395
  return true;
109051
- if (await pathExists(path242.join(dir, "terminal-state.json")))
109396
+ if (await pathExists(path243.join(dir, "terminal-state.json")))
109052
109397
  return true;
109053
109398
  try {
109054
- const entries = await readdir36(path242.join(dir, "slate-archive"));
109399
+ const entries = await readdir36(path243.join(dir, "slate-archive"));
109055
109400
  return entries.length > 0;
109056
109401
  } catch {
109057
109402
  return false;
@@ -109076,7 +109421,7 @@ async function safeReadSlate(dir) {
109076
109421
  }
109077
109422
  }
109078
109423
  async function readTerminalState(dir) {
109079
- const result = readConfigFile(path242.join(dir, "terminal-state.json"));
109424
+ const result = readConfigFile(path243.join(dir, "terminal-state.json"));
109080
109425
  if (!result.ok) {
109081
109426
  return;
109082
109427
  }
@@ -109100,7 +109445,7 @@ async function resolveUnboundCandidateTarget(cwd, target) {
109100
109445
  if (session === undefined) {
109101
109446
  throw new Error(`No session or evidence path matches "${target}"`);
109102
109447
  }
109103
- const archiveDir = path242.join(sessionDir(session.projectPath, session.id), "slate-archive");
109448
+ const archiveDir = path243.join(sessionDir(session.projectPath, session.id), "slate-archive");
109104
109449
  let entries;
109105
109450
  try {
109106
109451
  entries = (await readdir36(archiveDir)).filter((name) => name.endsWith("-unbound-candidate.json"));
@@ -109110,7 +109455,7 @@ async function resolveUnboundCandidateTarget(cwd, target) {
109110
109455
  if (entries.length === 0)
109111
109456
  throw new Error(`No unbound-candidate artifacts for session ${session.id}`);
109112
109457
  entries.sort();
109113
- return path242.join(archiveDir, entries[entries.length - 1]);
109458
+ return path243.join(archiveDir, entries[entries.length - 1]);
109114
109459
  }
109115
109460
  async function dismissUnboundByTarget(cwd, target, reason) {
109116
109461
  const evidencePath = await resolveUnboundCandidateTarget(cwd, target);
@@ -109136,7 +109481,7 @@ async function dismissUnboundCandidate(candidatePath, reason) {
109136
109481
  return { removed: candidatePath, receipt: receiptPath };
109137
109482
  }
109138
109483
  async function readNewestUnboundCandidate(dir) {
109139
- const archiveDir = path242.join(dir, "slate-archive");
109484
+ const archiveDir = path243.join(dir, "slate-archive");
109140
109485
  let entries;
109141
109486
  try {
109142
109487
  entries = (await readdir36(archiveDir)).filter((name) => name.endsWith("-unbound-candidate.json"));
@@ -109145,7 +109490,7 @@ async function readNewestUnboundCandidate(dir) {
109145
109490
  }
109146
109491
  entries.sort();
109147
109492
  for (let i = entries.length - 1;i >= 0; i--) {
109148
- const evidencePath = path242.join(archiveDir, entries[i]);
109493
+ const evidencePath = path243.join(archiveDir, entries[i]);
109149
109494
  if (await pathExists(unboundDismissedReceiptPath(evidencePath)))
109150
109495
  continue;
109151
109496
  const result = readConfigFile(evidencePath);
@@ -109175,7 +109520,7 @@ function isFailureOutcome2(g) {
109175
109520
  return g.outcome === "error" || g.outcome === "no_credential" || g.outcome === "conflict";
109176
109521
  }
109177
109522
  async function readNewestWrapUpOutcome(dir) {
109178
- const archiveDir = path242.join(dir, "slate-archive");
109523
+ const archiveDir = path243.join(dir, "slate-archive");
109179
109524
  let entries;
109180
109525
  try {
109181
109526
  entries = (await readdir36(archiveDir)).filter((name) => name.endsWith("-wrap-up-outcome.json"));
@@ -109184,7 +109529,7 @@ async function readNewestWrapUpOutcome(dir) {
109184
109529
  }
109185
109530
  entries.sort();
109186
109531
  for (let i = entries.length - 1;i >= 0; i--) {
109187
- const evidencePath = path242.join(archiveDir, entries[i]);
109532
+ const evidencePath = path243.join(archiveDir, entries[i]);
109188
109533
  const result = readConfigFile(evidencePath);
109189
109534
  if (!result.ok) {
109190
109535
  continue;
@@ -109934,12 +110279,12 @@ var init_extract3 = __esm(() => {
109934
110279
  // src/product/corpus.ts
109935
110280
  import { createHash as createHash62 } from "crypto";
109936
110281
  import { readdir as readdir37, readFile as readFile115 } from "fs/promises";
109937
- import path243 from "path";
110282
+ import path244 from "path";
109938
110283
  function flowsDir(cwd) {
109939
- return path243.join(cwd, ".metaproject", "flows");
110284
+ return path244.join(cwd, ".metaproject", "flows");
109940
110285
  }
109941
110286
  function requirementsDir(cwd) {
109942
- return path243.join(cwd, "docs", "requirements");
110287
+ return path244.join(cwd, "docs", "requirements");
109943
110288
  }
109944
110289
  async function readOptional(file) {
109945
110290
  try {
@@ -109992,23 +110337,23 @@ async function corpusFingerprint(cwd) {
109992
110337
  for (const name of await listFlowPackages(cwd)) {
109993
110338
  lines.push(`package\x00flow\x00${name}`);
109994
110339
  for (const file of FLOW_SOURCE_FILES) {
109995
- const content = await readBytes(path243.join(flowsDir(cwd), name, file));
110340
+ const content = await readBytes(path244.join(flowsDir(cwd), name, file));
109996
110341
  lines.push(`file\x00${FLOW_PREFIX}${name}/${file}\x00${digestOf4(content)}`);
109997
110342
  }
109998
110343
  }
109999
110344
  for (const name of await requirementPackages(cwd)) {
110000
110345
  lines.push(`package\x00docpack\x00${name}`);
110001
- const dir = path243.join(requirementsDir(cwd), name);
110346
+ const dir = path244.join(requirementsDir(cwd), name);
110002
110347
  for (const candidate of PRIMARY_DOCS) {
110003
- const content = await readBytes(path243.join(dir, candidate));
110348
+ const content = await readBytes(path244.join(dir, candidate));
110004
110349
  if (content === null)
110005
110350
  continue;
110006
110351
  lines.push(`file\x00docs/requirements/${name}/${candidate}\x00${digestOf4(content)}`);
110007
110352
  break;
110008
110353
  }
110009
- const readme = await readBytes(path243.join(dir, "README.md"));
110354
+ const readme = await readBytes(path244.join(dir, "README.md"));
110010
110355
  lines.push(`file\x00docs/requirements/${name}/README.md\x00${digestOf4(readme)}`);
110011
- const specification = await readBytes(path243.join(dir, "specification.md"));
110356
+ const specification = await readBytes(path244.join(dir, "specification.md"));
110012
110357
  lines.push(`file\x00docs/requirements/${name}/specification.md\x00${digestOf4(specification)}`);
110013
110358
  }
110014
110359
  return createHash62("sha256").update([...lines].sort().join(`
@@ -110050,19 +110395,19 @@ async function buildIntentIndex(cwd) {
110050
110395
  const intents = [];
110051
110396
  const failures = [];
110052
110397
  for (const name of await listFlowPackages(cwd)) {
110053
- const dir = path243.join(flowsDir(cwd), name);
110398
+ const dir = path244.join(flowsDir(cwd), name);
110054
110399
  const repoPath = `${FLOW_PREFIX}${name}`;
110055
110400
  try {
110056
- 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`);
110057
110402
  if (flowJson === null) {
110058
110403
  failures.push(`${repoPath}: no flow.json`);
110059
110404
  continue;
110060
110405
  }
110061
- 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`);
110062
110407
  intents.push(extractFlowIntent({
110063
110408
  flowJson,
110064
- description: await readSource(path243.join(dir, "description.md"), `${repoPath}/description.md`),
110065
- 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`),
110066
110411
  journal
110067
110412
  }, repoPath));
110068
110413
  const problem = observationProblem(journal);
@@ -110073,17 +110418,17 @@ async function buildIntentIndex(cwd) {
110073
110418
  }
110074
110419
  }
110075
110420
  for (const name of await requirementPackages(cwd)) {
110076
- const dir = path243.join(requirementsDir(cwd), name);
110421
+ const dir = path244.join(requirementsDir(cwd), name);
110077
110422
  const repoPath = `docs/requirements/${name}`;
110078
110423
  try {
110079
110424
  let primary = null;
110080
110425
  for (const candidate of PRIMARY_DOCS) {
110081
- primary = await readSource(path243.join(dir, candidate), `${repoPath}/${candidate}`);
110426
+ primary = await readSource(path244.join(dir, candidate), `${repoPath}/${candidate}`);
110082
110427
  if (primary !== null)
110083
110428
  break;
110084
110429
  }
110085
- const specification = await readSource(path243.join(dir, "specification.md"), `${repoPath}/specification.md`);
110086
- 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`);
110087
110432
  const source = { name, primary, specification, readme };
110088
110433
  intents.push(extractDocpackIntent(source, repoPath));
110089
110434
  const problem = docpackObservationProblem(source);
@@ -110143,12 +110488,12 @@ var init_by_origin = __esm(() => {
110143
110488
 
110144
110489
  // src/product/store.ts
110145
110490
  import { readFile as readFile116 } from "fs/promises";
110146
- import path244 from "path";
110491
+ import path245 from "path";
110147
110492
  function productDataRoot(cwd) {
110148
- return path244.join(cwd, ".metaproject", "data", "product");
110493
+ return path245.join(cwd, ".metaproject", "data", "product");
110149
110494
  }
110150
110495
  function indexPath(cwd) {
110151
- return path244.join(productDataRoot(cwd), "index.json");
110496
+ return path245.join(productDataRoot(cwd), "index.json");
110152
110497
  }
110153
110498
  function serializeIndex(index) {
110154
110499
  return `${JSON.stringify(index, null, 2)}
@@ -111639,9 +111984,9 @@ var init_repair = __esm(() => {
111639
111984
 
111640
111985
  // src/review/managed.ts
111641
111986
  import { mkdir as mkdir55, readFile as readFile117, readdir as readdir38, realpath as realpath18, rm as rm16, stat as stat25 } from "fs/promises";
111642
- import path245 from "path";
111987
+ import path246 from "path";
111643
111988
  function reviewsRoot(cwd) {
111644
- return path245.join(cwd, ".metaproject", "reviews");
111989
+ return path246.join(cwd, ".metaproject", "reviews");
111645
111990
  }
111646
111991
  async function resolveGitHead2(cwd) {
111647
111992
  try {
@@ -111774,26 +112119,26 @@ async function createManagedReviewPackage(input) {
111774
112119
  throw new Error(`Refusing to record findings that do not satisfy review-finding.schema.json: ${contractErrors.map((error) => `${error.path} ${error.message}`).join("; ")}`);
111775
112120
  }
111776
112121
  await mkdir55(packageDir, { recursive: true });
111777
- 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)}
111778
112123
  `);
111779
- await writeFileAtomic(path245.join(packageDir, "scope.md"), renderScope(input, flowMatch, at, verification, carried, caps, externalReclaims, scopeB.record, filterStats));
111780
- await writeFileAtomic(path245.join(packageDir, "coverage.md"), renderCoverage(coverage));
111781
- await writeFileAtomic(path245.join(packageDir, "report.md"), renderReport2(report, input.mode));
111782
- 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)}
111783
112128
  `);
111784
- await writeFileAtomic(path245.join(packageDir, "learning.md"), renderLearning(findings));
111785
- 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));
111786
112131
  await persistScreenRadius(packageDir, scopeB.resolved);
111787
112132
  const reviewNotes = await writeReviewNotes(findings, {
111788
112133
  cwd: input.cwd,
111789
112134
  reviewId,
111790
112135
  head,
111791
- packagePath: path245.relative(input.cwd, packageDir),
112136
+ packagePath: path246.relative(input.cwd, packageDir),
111792
112137
  ...input.now === undefined ? {} : { now: input.now }
111793
112138
  });
111794
112139
  return {
111795
112140
  reviewId,
111796
- path: path245.relative(input.cwd, packageDir),
112141
+ path: path246.relative(input.cwd, packageDir),
111797
112142
  manifest,
111798
112143
  verification: verification.counts,
111799
112144
  verificationRejections: verification.rejections,
@@ -111810,7 +112155,7 @@ async function screenScopeBFindings(input, packageDir, findings) {
111810
112155
  const scopeB = findings.filter((finding) => isBlastRadiusScopedFinding(finding, reviewers));
111811
112156
  if (resolved === undefined) {
111812
112157
  if (scopeB.length > 0) {
111813
- 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.`);
111814
112159
  }
111815
112160
  return { retained: [...findings], record: { source: "none", scopeBFindings: 0 }, resolved: undefined };
111816
112161
  }
@@ -111827,13 +112172,13 @@ async function screenScopeBFindings(input, packageDir, findings) {
111827
112172
  };
111828
112173
  }
111829
112174
  function handoffBlastRadiusPath(packageDir) {
111830
- return path245.join(path245.dirname(packageDir), BLAST_RADIUS_ARTIFACT);
112175
+ return path246.join(path246.dirname(packageDir), BLAST_RADIUS_ARTIFACT);
111831
112176
  }
111832
112177
  async function resolveScreenRadius(input, packageDir) {
111833
112178
  if (input.blastRadius !== undefined) {
111834
112179
  return { radius: input.blastRadius, source: "input" };
111835
112180
  }
111836
- const inPackage = path245.join(packageDir, BLAST_RADIUS_ARTIFACT);
112181
+ const inPackage = path246.join(packageDir, BLAST_RADIUS_ARTIFACT);
111837
112182
  const fromPackage = await readBlastRadiusRecordFile(inPackage);
111838
112183
  if (fromPackage !== undefined) {
111839
112184
  return { radius: fromPackage, source: "package", file: inPackage };
@@ -111859,7 +112204,7 @@ async function persistScreenRadius(packageDir, resolved) {
111859
112204
  if (resolved === undefined) {
111860
112205
  return;
111861
112206
  }
111862
- const inPackage = path245.join(packageDir, BLAST_RADIUS_ARTIFACT);
112207
+ const inPackage = path246.join(packageDir, BLAST_RADIUS_ARTIFACT);
111863
112208
  if (resolved.source !== "package") {
111864
112209
  await writeFileAtomic(inPackage, `${JSON.stringify(resolved.radius, null, 2)}
111865
112210
  `);
@@ -111890,12 +112235,12 @@ function fromVerifierRefutations(findings) {
111890
112235
  }));
111891
112236
  }
111892
112237
  async function getManagedReviewStatus(cwd, ref) {
111893
- 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");
111894
112239
  return JSON.parse(await readFile117(manifestPath, "utf8"));
111895
112240
  }
111896
112241
  async function completeManagedReview(cwd, ref, options = {}) {
111897
112242
  const packageDir = await resolveReviewPackagePath(cwd, ref);
111898
- const manifestPath = path245.join(packageDir, "manifest.json");
112243
+ const manifestPath = path246.join(packageDir, "manifest.json");
111899
112244
  const manifest = JSON.parse(await readFile117(manifestPath, "utf8"));
111900
112245
  const missing = await missingArtifacts(packageDir);
111901
112246
  if (missing.length > 0) {
@@ -111916,12 +112261,12 @@ async function completeManagedReview(cwd, ref, options = {}) {
111916
112261
  }
111917
112262
  await writeFileAtomic(manifestPath, `${JSON.stringify(updated, null, 2)}
111918
112263
  `);
111919
- 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"));
111920
112265
  const reviewNotes = await writeReviewNotes(recorded, {
111921
112266
  cwd,
111922
112267
  reviewId: updated.reviewId,
111923
112268
  head: updated.target.head ?? null,
111924
- packagePath: path245.relative(cwd, packageDir),
112269
+ packagePath: path246.relative(cwd, packageDir),
111925
112270
  ...options.now === undefined ? {} : { now: options.now }
111926
112271
  });
111927
112272
  return { manifest: updated, reviewNotes, dispositions: tallyDispositions(recorded) };
@@ -111987,16 +112332,16 @@ async function resolvePackageFlow(input) {
111987
112332
  }
111988
112333
  function packagePath(cwd, mode, reviewId, flowMatch) {
111989
112334
  if ((mode === "attach-review" || mode === "ingest") && flowMatch) {
111990
- return path245.join(flowsRoot(cwd), flowMatch.dir, "reviews", reviewId);
112335
+ return path246.join(flowsRoot(cwd), flowMatch.dir, "reviews", reviewId);
111991
112336
  }
111992
112337
  if (mode === "attach-review") {
111993
112338
  throw new Error("attach-review requires an explicit or matched flow");
111994
112339
  }
111995
- return path245.join(reviewsRoot(cwd), reviewId);
112340
+ return path246.join(reviewsRoot(cwd), reviewId);
111996
112341
  }
111997
112342
  function buildManifest(args) {
111998
112343
  const crossFamily = args.input.crossFamilyReview;
111999
- 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));
112000
112345
  const manifest = {
112001
112346
  schemaVersion: 1,
112002
112347
  reviewId: args.reviewId,
@@ -112043,7 +112388,7 @@ async function readReport(input) {
112043
112388
  return input.reportText;
112044
112389
  }
112045
112390
  if (input.reportPath) {
112046
- return readFile117(path245.resolve(input.cwd, input.reportPath), "utf8");
112391
+ return readFile117(path246.resolve(input.cwd, input.reportPath), "utf8");
112047
112392
  }
112048
112393
  if (input.mode === "ingest") {
112049
112394
  throw new Error("ingest requires --report or reportText");
@@ -112054,7 +112399,7 @@ No reviewer findings recorded yet.
112054
112399
  `;
112055
112400
  }
112056
112401
  function reportLabel(input) {
112057
- 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";
112058
112403
  }
112059
112404
  async function normalizeFindings(args) {
112060
112405
  if (args.source !== undefined) {
@@ -112109,16 +112454,16 @@ async function anchorFindings(findings, input, head) {
112109
112454
  }
112110
112455
  }
112111
112456
  async function containedRelativePath(cwd, relative) {
112112
- const root = await realpath18(cwd).catch(() => path245.resolve(cwd));
112113
- const resolved = path245.resolve(root, relative);
112457
+ const root = await realpath18(cwd).catch(() => path246.resolve(cwd));
112458
+ const resolved = path246.resolve(root, relative);
112114
112459
  const real = await realpath18(resolved).catch(() => null);
112115
- if (real === null || real !== root && !real.startsWith(`${root}${path245.sep}`)) {
112460
+ if (real === null || real !== root && !real.startsWith(`${root}${path246.sep}`)) {
112116
112461
  return null;
112117
112462
  }
112118
112463
  return real;
112119
112464
  }
112120
112465
  async function readFileAtCommit(cwd, commit, absolute) {
112121
- 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);
112122
112467
  try {
112123
112468
  const proc = Bun.spawn(["git", "show", `${commit}:${relative}`], {
112124
112469
  cwd,
@@ -112137,10 +112482,10 @@ async function readFileAtCommit(cwd, commit, absolute) {
112137
112482
  }
112138
112483
  function readFromWorkingTree(input) {
112139
112484
  return async (relative) => {
112140
- const root = await realpath18(input.cwd).catch(() => path245.resolve(input.cwd));
112141
- 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);
112142
112487
  const real = await realpath18(resolved).catch(() => null);
112143
- if (real === null || real !== root && !real.startsWith(`${root}${path245.sep}`)) {
112488
+ if (real === null || real !== root && !real.startsWith(`${root}${path246.sep}`)) {
112144
112489
  return null;
112145
112490
  }
112146
112491
  try {
@@ -112272,7 +112617,7 @@ function refutedClassification(state) {
112272
112617
  return state === "dismissed-incorrect" ? "false_positive" : "out_of_scope";
112273
112618
  }
112274
112619
  async function recordDispositions(packageDir, records) {
112275
- const findingsPath = path245.join(packageDir, "findings.json");
112620
+ const findingsPath = path246.join(packageDir, "findings.json");
112276
112621
  const findings = JSON.parse(await readFile117(findingsPath, "utf8"));
112277
112622
  const schema = await loadSchema("review-finding");
112278
112623
  const dispositionSchema = schema.properties?.["disposition"];
@@ -112599,7 +112944,7 @@ function upsertPreFilterScopeBlock(text, block) {
112599
112944
  ${body}`;
112600
112945
  }
112601
112946
  async function readPreFilterScopeBlock(packageDir) {
112602
- const scopePath = path245.join(packageDir, "scope.md");
112947
+ const scopePath = path246.join(packageDir, "scope.md");
112603
112948
  if (!await pathExists(scopePath)) {
112604
112949
  return;
112605
112950
  }
@@ -112773,39 +113118,39 @@ function matchReason(flow, dir, kind, ref) {
112773
113118
  async function missingArtifacts(packageDir) {
112774
113119
  const missing = [];
112775
113120
  for (const artifact of ["scope.md", "coverage.md", "report.md", "findings.json", "learning.md", "decisions.md"]) {
112776
- if (!await pathExists(path245.join(packageDir, artifact))) {
113121
+ if (!await pathExists(path246.join(packageDir, artifact))) {
112777
113122
  missing.push(artifact);
112778
113123
  }
112779
113124
  }
112780
113125
  return missing;
112781
113126
  }
112782
113127
  async function resolveReviewPackagePath(cwd, ref) {
112783
- const absolute = path245.resolve(cwd, ref);
112784
- if (await pathExists(path245.join(absolute, "manifest.json"))) {
113128
+ const absolute = path246.resolve(cwd, ref);
113129
+ if (await pathExists(path246.join(absolute, "manifest.json"))) {
112785
113130
  return absolute;
112786
113131
  }
112787
113132
  if (await pathExists(absolute) && absolute.endsWith("manifest.json")) {
112788
- return path245.dirname(absolute);
113133
+ return path246.dirname(absolute);
112789
113134
  }
112790
- const standalone = path245.join(reviewsRoot(cwd), ref);
112791
- 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"))) {
112792
113137
  return standalone;
112793
113138
  }
112794
113139
  for (const flowDir of await listFlowDirs(cwd)) {
112795
- const reviewsDir = path245.join(flowsRoot(cwd), flowDir, "reviews");
113140
+ const reviewsDir = path246.join(flowsRoot(cwd), flowDir, "reviews");
112796
113141
  if (!await pathExists(reviewsDir)) {
112797
113142
  continue;
112798
113143
  }
112799
113144
  for (const entry of await readdir38(reviewsDir, { withFileTypes: true })) {
112800
113145
  if (entry.isDirectory() && entry.name === ref) {
112801
- return path245.join(reviewsDir, entry.name);
113146
+ return path246.join(reviewsDir, entry.name);
112802
113147
  }
112803
113148
  }
112804
113149
  }
112805
113150
  throw new Error(`Managed review package not found: ${ref}`);
112806
113151
  }
112807
113152
  async function loadDocpackSchema(cwd) {
112808
- 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");
112809
113154
  if (!await pathExists(schemaPath)) {
112810
113155
  return null;
112811
113156
  }
@@ -112911,7 +113256,7 @@ function checkPullOpen(facts) {
112911
113256
 
112912
113257
  // src/review/bot/packages.ts
112913
113258
  import { readdir as readdir39, readFile as readFile118 } from "fs/promises";
112914
- import path246 from "path";
113259
+ import path247 from "path";
112915
113260
  async function subdirectories2(dir) {
112916
113261
  try {
112917
113262
  const entries = await readdir39(dir, { withFileTypes: true });
@@ -112934,12 +113279,12 @@ function pullFromTarget(target) {
112934
113279
  }
112935
113280
  async function readPackage(dir) {
112936
113281
  try {
112937
- 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"));
112938
113283
  if (manifest.target?.kind !== "pr")
112939
113284
  return null;
112940
113285
  let findings = [];
112941
113286
  try {
112942
- 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"));
112943
113288
  if (Array.isArray(parsed))
112944
113289
  findings = parsed;
112945
113290
  } catch {
@@ -112954,16 +113299,16 @@ async function readPackage(dir) {
112954
113299
  function candidateDirs2(cwd) {
112955
113300
  return (async () => {
112956
113301
  const dirs = [];
112957
- const reviews = path246.join(cwd, ".metaproject", "reviews");
113302
+ const reviews = path247.join(cwd, ".metaproject", "reviews");
112958
113303
  for (const name of await subdirectories2(reviews)) {
112959
113304
  if (name !== "bot")
112960
- dirs.push(path246.join(reviews, name));
113305
+ dirs.push(path247.join(reviews, name));
112961
113306
  }
112962
- const flows = path246.join(cwd, ".metaproject", "flows");
113307
+ const flows = path247.join(cwd, ".metaproject", "flows");
112963
113308
  for (const flow of await subdirectories2(flows)) {
112964
- const nested = path246.join(flows, flow, "reviews");
113309
+ const nested = path247.join(flows, flow, "reviews");
112965
113310
  for (const name of await subdirectories2(nested))
112966
- dirs.push(path246.join(nested, name));
113311
+ dirs.push(path247.join(nested, name));
112967
113312
  }
112968
113313
  return dirs;
112969
113314
  })();
@@ -112980,7 +113325,7 @@ async function listReviewPackages(cwd) {
112980
113325
  }
112981
113326
  async function loadReviewPackage(cwd, reviewId) {
112982
113327
  for (const dir of await candidateDirs2(cwd)) {
112983
- if (path246.basename(dir) !== reviewId)
113328
+ if (path247.basename(dir) !== reviewId)
112984
113329
  continue;
112985
113330
  const listed = await readPackage(dir);
112986
113331
  if (listed !== null && listed.reviewId === reviewId)
@@ -112991,7 +113336,7 @@ async function loadReviewPackage(cwd, reviewId) {
112991
113336
  var init_packages = () => {};
112992
113337
 
112993
113338
  // src/review/bot/run.ts
112994
- import path247 from "path";
113339
+ import path248 from "path";
112995
113340
  function capDiff(diff, maxBytes) {
112996
113341
  const totalBytes = Buffer.byteLength(diff);
112997
113342
  if (totalBytes <= maxBytes)
@@ -113036,7 +113381,7 @@ function parseVerifierVerdict2(reply) {
113036
113381
  return unverifiable;
113037
113382
  }
113038
113383
  function botStatePath(cwd, repo, number) {
113039
- 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`);
113040
113385
  }
113041
113386
  function emptyBotState(repo, number) {
113042
113387
  return { schemaVersion: 1, repo, number, reviews: [], pull: null };
@@ -113659,9 +114004,9 @@ var init_reviews_inspector = __esm(() => {
113659
114004
 
113660
114005
  // src/lib/serve-config.ts
113661
114006
  import { existsSync as existsSync42 } from "fs";
113662
- import path248 from "path";
114007
+ import path249 from "path";
113663
114008
  function serveConfigPath(dir) {
113664
- return path248.join(keryxConfigDir(dir), "serve.json");
114009
+ return path249.join(keryxConfigDir(dir), "serve.json");
113665
114010
  }
113666
114011
  function parseIpv4(value) {
113667
114012
  const parts = value.split(".");
@@ -113787,7 +114132,7 @@ function projectServeConfig(value, onDrop) {
113787
114132
  if (!isRecord12(value)) {
113788
114133
  return null;
113789
114134
  }
113790
- reportUndeclared(value, CONFIG_KEYS, "", onDrop);
114135
+ reportUndeclared(value, CONFIG_KEYS2, "", onDrop);
113791
114136
  if (value.schemaVersion !== SERVE_CONFIG_SCHEMA_VERSION) {
113792
114137
  return null;
113793
114138
  }
@@ -113959,10 +114304,10 @@ function saveServeConfig(config, dir, onWarn) {
113959
114304
  return false;
113960
114305
  }
113961
114306
  }
113962
- 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;
113963
114308
  var init_serve_config = __esm(() => {
113964
114309
  init_config_dir();
113965
- CONFIG_KEYS = [
114310
+ CONFIG_KEYS2 = [
113966
114311
  "schemaVersion",
113967
114312
  "enabled",
113968
114313
  "bind",
@@ -114098,25 +114443,6 @@ var init_protocol2 = __esm(() => {
114098
114443
  MESSAGE_STATES = ["reading", "working", "done", "failed"];
114099
114444
  });
114100
114445
 
114101
- // src/remote/paths.ts
114102
- import path249 from "path";
114103
- function remoteDirPath(dir) {
114104
- return path249.join(keryxConfigDir(dir), REMOTE_DIRNAME);
114105
- }
114106
- function ensureRemoteDir(dir, ...sub) {
114107
- return ensureKeryxSubdir([REMOTE_DIRNAME, ...sub], dir);
114108
- }
114109
- function botTokenPath(dir) {
114110
- return path249.join(remoteDirPath(dir), BOT_TOKEN_FILE);
114111
- }
114112
- function remoteConfigPath(dir) {
114113
- return path249.join(remoteDirPath(dir), REMOTE_CONFIG_FILE);
114114
- }
114115
- 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";
114116
- var init_paths4 = __esm(() => {
114117
- init_config_dir();
114118
- });
114119
-
114120
114446
  // src/remote/endpoint.ts
114121
114447
  import path250 from "path";
114122
114448
  function endpointPath(dir) {
@@ -114155,8 +114481,8 @@ import {
114155
114481
  fsyncSync as fsyncSync2,
114156
114482
  openSync as openSync5,
114157
114483
  renameSync as renameSync5,
114158
- statSync as statSync15,
114159
- unlinkSync as unlinkSync5,
114484
+ statSync as statSync16,
114485
+ unlinkSync as unlinkSync6,
114160
114486
  writeFileSync as writeFileSync8
114161
114487
  } from "fs";
114162
114488
  import path251 from "path";
@@ -114190,7 +114516,7 @@ function isGroupOrOtherAccessible(file) {
114190
114516
  if (process.platform === "win32") {
114191
114517
  return false;
114192
114518
  }
114193
- const mode = statSync15(file, { throwIfNoEntry: false })?.mode;
114519
+ const mode = statSync16(file, { throwIfNoEntry: false })?.mode;
114194
114520
  return mode !== undefined && (mode & 63) !== 0;
114195
114521
  }
114196
114522
  function readServeCredential(dir) {
@@ -114247,7 +114573,7 @@ function writeStore(store, dir) {
114247
114573
  return true;
114248
114574
  } catch {
114249
114575
  try {
114250
- unlinkSync5(temp);
114576
+ unlinkSync6(temp);
114251
114577
  } catch {}
114252
114578
  return false;
114253
114579
  }
@@ -114323,7 +114649,7 @@ var init_serve_credential = __esm(() => {
114323
114649
 
114324
114650
  // src/remote/shell-token.ts
114325
114651
  import { createHash as createHash64, createHmac as createHmac5, randomBytes as randomBytes12 } from "crypto";
114326
- import { statSync as statSync16 } from "fs";
114652
+ import { statSync as statSync17 } from "fs";
114327
114653
  import path252 from "path";
114328
114654
  function shellTokenPath(dir) {
114329
114655
  return path252.join(remoteDirPath(dir), SHELL_TOKEN_FILE);
@@ -114331,7 +114657,7 @@ function shellTokenPath(dir) {
114331
114657
  function readValidated(file) {
114332
114658
  if (process.platform !== "win32") {
114333
114659
  try {
114334
- const mode = statSync16(file).mode & 511;
114660
+ const mode = statSync17(file).mode & 511;
114335
114661
  if ((mode & 63) !== 0) {
114336
114662
  return { ok: false, reason: `shell token file ${file} is accessible to other users (mode ${mode.toString(8)}); run: chmod 600 ${file}` };
114337
114663
  }
@@ -114344,7 +114670,7 @@ function readValidated(file) {
114344
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})` };
114345
114671
  }
114346
114672
  const token = read.text.trim();
114347
- if (!TOKEN_SHAPE.test(token)) {
114673
+ if (!TOKEN_SHAPE2.test(token)) {
114348
114674
  return { ok: false, reason: `shell token file ${file} does not hold a shell token; delete it and restart \`keryx serve\`` };
114349
114675
  }
114350
114676
  return { ok: true, value: token };
@@ -114409,12 +114735,12 @@ function createShellTokenVerifier(token) {
114409
114735
  return constantTimeEqual(createHash64("sha256").update(presented, "utf8").digest(), expected);
114410
114736
  };
114411
114737
  }
114412
- 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;
114413
114739
  var init_shell_token = __esm(() => {
114414
114740
  init_config_dir();
114415
114741
  init_serve_credential();
114416
114742
  init_paths4();
114417
- TOKEN_SHAPE = /^[A-Za-z0-9_-]{32,128}$/;
114743
+ TOKEN_SHAPE2 = /^[A-Za-z0-9_-]{32,128}$/;
114418
114744
  NONCE_SHAPE = /^[A-Za-z0-9_-]{22,64}$/;
114419
114745
  MAC_SHAPE = /^[A-Za-z0-9_-]{43}$/;
114420
114746
  });
@@ -115862,162 +116188,6 @@ var init_shell_bridge = __esm(() => {
115862
116188
  init_protocol2();
115863
116189
  });
115864
116190
 
115865
- // src/remote/config.ts
115866
- import { statSync as statSync17, unlinkSync as unlinkSync6 } from "fs";
115867
- function isSafeInt(value) {
115868
- return typeof value === "number" && Number.isSafeInteger(value);
115869
- }
115870
- function parseRemoteConfig(value) {
115871
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
115872
- return { ok: false, reason: "remote config must be a JSON object" };
115873
- }
115874
- const doc = value;
115875
- for (const key of Object.keys(doc)) {
115876
- if (!CONFIG_KEYS2.has(key)) {
115877
- const shown = /^[A-Za-z_][A-Za-z0-9_]{0,30}$/.test(key) ? key : "<not shown>";
115878
- return { ok: false, reason: `remote config has an unknown key "${shown}" (the schema is closed)` };
115879
- }
115880
- }
115881
- if (doc.schemaVersion !== REMOTE_CONFIG_SCHEMA_VERSION) {
115882
- return { ok: false, reason: `remote config schemaVersion must be ${REMOTE_CONFIG_SCHEMA_VERSION}` };
115883
- }
115884
- if (!isSafeInt(doc.chatId) || doc.chatId === 0) {
115885
- return { ok: false, reason: "remote config chatId must be a non-zero integer (the supergroup id)" };
115886
- }
115887
- const ids = doc.allowedUserIds;
115888
- if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => isSafeInt(id) && id > 0)) {
115889
- return { ok: false, reason: "remote config allowedUserIds must be a non-empty array of positive integers" };
115890
- }
115891
- const timeout = (key, fallback) => {
115892
- const raw = doc[key];
115893
- if (raw === undefined) {
115894
- return { ok: true, value: fallback };
115895
- }
115896
- if (!isSafeInt(raw) || raw < 1 || raw > MAX_TIMEOUT_MS3) {
115897
- return { ok: false, reason: `remote config ${key} must be an integer from 1 to ${MAX_TIMEOUT_MS3} (milliseconds)` };
115898
- }
115899
- return { ok: true, value: raw };
115900
- };
115901
- const orphan = timeout("orphanMs", DEFAULT_ORPHAN_MS);
115902
- if (!orphan.ok) {
115903
- return orphan;
115904
- }
115905
- const run = timeout("runTimeoutMs", DEFAULT_RUN_TIMEOUT_MS);
115906
- if (!run.ok) {
115907
- return run;
115908
- }
115909
- return {
115910
- ok: true,
115911
- value: {
115912
- schemaVersion: REMOTE_CONFIG_SCHEMA_VERSION,
115913
- chatId: doc.chatId,
115914
- allowedUserIds: [...new Set(ids)],
115915
- orphanMs: orphan.value,
115916
- runTimeoutMs: run.value
115917
- }
115918
- };
115919
- }
115920
- function loadRemoteConfig(dir) {
115921
- const file = remoteConfigPath(dir);
115922
- const read = readConfigFile(file);
115923
- if (!read.ok) {
115924
- return {
115925
- ok: false,
115926
- 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})`
115927
- };
115928
- }
115929
- let parsed;
115930
- try {
115931
- parsed = JSON.parse(read.text);
115932
- } catch {
115933
- return { ok: false, reason: `remote-control config at ${file} is not valid JSON` };
115934
- }
115935
- return parseRemoteConfig(parsed);
115936
- }
115937
- function saveRemoteConfig(config, dir) {
115938
- const checked = parseRemoteConfig(config);
115939
- if (!checked.ok) {
115940
- return checked;
115941
- }
115942
- ensureRemoteDir(dir);
115943
- writeOwnerOnlyFileAtomic(remoteConfigPath(dir), `${JSON.stringify(checked.value, null, 2)}
115944
- `);
115945
- return checked;
115946
- }
115947
- function loadBotToken(dir) {
115948
- const file = botTokenPath(dir);
115949
- let mode;
115950
- try {
115951
- const stats = statSync17(file);
115952
- if (!stats.isFile()) {
115953
- return { ok: false, reason: `bot token path ${file} is not a regular file` };
115954
- }
115955
- mode = stats.mode & 511;
115956
- } catch {
115957
- return {
115958
- ok: false,
115959
- reason: `no bot token file at ${file}; put the token on one line in it and run: chmod 600 ${file}`
115960
- };
115961
- }
115962
- if (process.platform !== "win32" && (mode & 63) !== 0) {
115963
- return {
115964
- ok: false,
115965
- reason: `bot token file ${file} is accessible to other users (mode ${mode.toString(8)}); run: chmod 600 ${file}`
115966
- };
115967
- }
115968
- const read = readConfigFile(file);
115969
- if (!read.ok) {
115970
- return { ok: false, reason: `bot token file ${file} cannot be read (${read.reason})` };
115971
- }
115972
- const token = read.text.trim();
115973
- if (!TOKEN_SHAPE2.test(token)) {
115974
- return {
115975
- ok: false,
115976
- reason: `bot token file ${file} does not hold a bot token (expected <digits>:<secret> on a single line)`
115977
- };
115978
- }
115979
- return { ok: true, value: token };
115980
- }
115981
- function saveBotToken(token, dir) {
115982
- const trimmed = token.trim();
115983
- if (!TOKEN_SHAPE2.test(trimmed)) {
115984
- return { ok: false, reason: "that does not look like a bot token (expected <digits>:<secret>, as BotFather gives it)" };
115985
- }
115986
- const file = botTokenPath(dir);
115987
- try {
115988
- ensureRemoteDir(dir);
115989
- writeOwnerOnlyFileAtomic(file, `${trimmed}
115990
- `);
115991
- } catch {
115992
- return { ok: false, reason: `could not write the bot token file at ${file}` };
115993
- }
115994
- return { ok: true, value: true };
115995
- }
115996
- function removeFile2(file) {
115997
- try {
115998
- unlinkSync6(file);
115999
- return true;
116000
- } catch (error) {
116001
- return error.code === "ENOENT";
116002
- }
116003
- }
116004
- function removeBotToken(dir) {
116005
- return removeFile2(botTokenPath(dir));
116006
- }
116007
- function removeRemoteConfig(dir) {
116008
- return removeFile2(remoteConfigPath(dir));
116009
- }
116010
- var REMOTE_CONFIG_SCHEMA_VERSION = 1, DEFAULT_ORPHAN_MS, DEFAULT_RUN_TIMEOUT_MS, MAX_TIMEOUT_MS3, CONFIG_KEYS2, TOKEN_SHAPE2;
116011
- var init_config11 = __esm(() => {
116012
- init_config_dir();
116013
- init_paths4();
116014
- DEFAULT_ORPHAN_MS = 10 * 60000;
116015
- DEFAULT_RUN_TIMEOUT_MS = 30 * 60000;
116016
- MAX_TIMEOUT_MS3 = 7 * 24 * 60 * 60000;
116017
- CONFIG_KEYS2 = new Set(["schemaVersion", "chatId", "allowedUserIds", "orphanMs", "runTimeoutMs"]);
116018
- TOKEN_SHAPE2 = /^\d{5,}:[A-Za-z0-9_-]{20,}$/;
116019
- });
116020
-
116021
116191
  // src/remote/channels-client.ts
116022
116192
  function fileExists(file) {
116023
116193
  const read = readConfigFile(file);
@@ -116589,6 +116759,7 @@ async function loadChannelsSnapshot(client) {
116589
116759
  configured: telegram.state !== "not-connected" || local.tokenFile || local.configFile,
116590
116760
  machine,
116591
116761
  sessions: telegram.sessions,
116762
+ ...telegram.rendering !== undefined ? { rendering: telegram.rendering } : {},
116592
116763
  ...telegram.reason !== undefined ? { reason: telegram.reason } : {}
116593
116764
  };
116594
116765
  }
@@ -116619,6 +116790,19 @@ function projectChannelsRow(snapshot, width) {
116619
116790
  return { text: fit2("off", width), role: "muted", action: "open" };
116620
116791
  }
116621
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
+ }
116622
116806
  function channelsStatusLines(snapshot) {
116623
116807
  const machine = snapshot.machine === undefined ? [] : [`Machine: ${snapshot.machine}`];
116624
116808
  switch (snapshot.state) {
@@ -116642,6 +116826,7 @@ function channelsStatusLines(snapshot) {
116642
116826
  "Telegram: connected",
116643
116827
  ...machine,
116644
116828
  `Sessions in Telegram: ${snapshot.sessions ?? 0}`,
116829
+ ...renderingLines(snapshot.rendering),
116645
116830
  "Test sends one message to the General topic. Disconnect deletes the topics and erases the token.",
116646
116831
  "Sessions join through /remote-control in each shell."
116647
116832
  ];
@@ -116650,6 +116835,7 @@ function channelsStatusLines(snapshot) {
116650
116835
  "Telegram: configured, but not running",
116651
116836
  ...machine,
116652
116837
  ...snapshot.reason === undefined ? [] : [`Why: ${snapshot.reason}`],
116838
+ ...renderingLines(snapshot.rendering),
116653
116839
  "Retry starts it again from the saved token and config. Disconnect erases them. Test works once Telegram is running."
116654
116840
  ];
116655
116841
  case "serve-down":
@@ -117322,7 +117508,7 @@ function mountChannelsPanel(otui, renderer, parent, options) {
117322
117508
  }
117323
117509
  };
117324
117510
  }
117325
- 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) => {
117326
117512
  event.preventDefault?.();
117327
117513
  event.stopPropagation?.();
117328
117514
  }, CHANNELS_ROW_POLL_MS = 1e4;
@@ -117334,6 +117520,12 @@ var init_channels_surface = __esm(() => {
117334
117520
  init_theme_repaint();
117335
117521
  init_theme_text();
117336
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
+ };
117337
117529
  CHANNELS_FOOTER = [
117338
117530
  { key: "c", label: "connect" },
117339
117531
  { key: "t", label: "test" },
@@ -117394,19 +117586,19 @@ function formatRemoteStatusLines(status) {
117394
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>.`);
117395
117587
  return lines;
117396
117588
  }
117397
- function clock(at) {
117589
+ function clock2(at) {
117398
117590
  return new Date(at).toISOString().slice(11, 19);
117399
117591
  }
117400
117592
  function formatRemoteEventLines(status) {
117401
117593
  if (status.events.length === 0)
117402
117594
  return ["No remote events yet."];
117403
- 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}`);
117404
117596
  }
117405
117597
  function formatRemoteCommandLines(status) {
117406
117598
  const commands = status.events.filter((event) => event.kind === "command");
117407
117599
  if (commands.length === 0)
117408
117600
  return ["No remote commands yet."];
117409
- 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}`);
117410
117602
  }
117411
117603
  function renderRemoteControlText(status) {
117412
117604
  return `${[...formatRemoteStatusLines(status), "", "Recent events:", ...formatRemoteEventLines(status)].join(`
@@ -117802,6 +117994,11 @@ var init_agent_commands = __esm(() => {
117802
117994
  description: "Connect, test or disconnect Telegram for this machine: /channels [status]",
117803
117995
  modes: AGENT_ONLY
117804
117996
  },
117997
+ {
117998
+ name: "/rendering",
117999
+ description: "How Telegram replies are written: /rendering [auto|rich|html|plain]",
118000
+ modes: AGENT_ONLY
118001
+ },
117805
118002
  {
117806
118003
  name: "/product",
117807
118004
  description: "Intents closed in code that nobody looked back at, with their outcome criteria",
@@ -137858,6 +138055,9 @@ async function runSettingsCommand(command, handlers) {
137858
138055
  case "/external-agents":
137859
138056
  handlers.onSystem(await handlers.externalAgents(arg));
137860
138057
  return;
138058
+ case "/rendering":
138059
+ handlers.onSystem(handlers.rendering(arg));
138060
+ return;
137861
138061
  case "/reasoning":
137862
138062
  handlers.reasoning(arg);
137863
138063
  return;
@@ -152857,6 +153057,7 @@ ${formatThemeList(getThemeId())}`);
152857
153057
  route: setRoutingEnabled,
152858
153058
  external: setExternalEnabled,
152859
153059
  externalAgents: (arg) => runExternalAgentsCommand(arg, sessionCwd),
153060
+ rendering: (arg) => runRenderingCommand(arg),
152860
153061
  reasoning: runReasoningCommand,
152861
153062
  think: runThinkCommand,
152862
153063
  theme: runThemeCommand,
@@ -153949,6 +154150,11 @@ ${formatThemeList(getThemeId())}`);
153949
154150
  showEditGuard();
153950
154151
  return;
153951
154152
  }
154153
+ if (isRenderingCommand(command.name)) {
154154
+ const arg = line.trim().split(/\s+/).slice(1).join(" ");
154155
+ io.onSystem?.(runRenderingCommand(arg));
154156
+ return;
154157
+ }
153952
154158
  if (isExternalAgentsCommand(command.name)) {
153953
154159
  const arg = line.trim().split(/\s+/).slice(1).join(" ");
153954
154160
  trackCommandWork(() => runExternalAgentsCommand(arg, opts.session?.cwd ?? process.cwd()).then((text) => io.onSystem?.(text)));
@@ -154788,6 +154994,7 @@ var init_tui_shell = __esm(() => {
154788
154994
  init_turn_guard();
154789
154995
  init_routing_classifier_source();
154790
154996
  init_external_command();
154997
+ init_rendering_command();
154791
154998
  init_external_switch();
154792
154999
  init_external_providers();
154793
155000
  init_jev_client();
@@ -157363,6 +157570,8 @@ ${GUTTER}${turnSeparator()}
157363
157570
  agentIo.onSystem?.(readlineRemoteControlText(line));
157364
157571
  } else if (isChannelsCommand(command)) {
157365
157572
  agentIo.onSystem?.(readlineChannelsText(line, await loadChannelsSnapshot(new ChannelsClient)));
157573
+ } else if (isRenderingCommand(command)) {
157574
+ agentIo.onSystem?.(runRenderingCommand(rest));
157366
157575
  } else if (isSessionInfoCommand(command)) {
157367
157576
  const cwd = sessionCwd;
157368
157577
  const [workspaces, flows] = await Promise.all([
@@ -158485,6 +158694,7 @@ var init_shell = __esm(() => {
158485
158694
  init_statusbar();
158486
158695
  init_metaproject_state();
158487
158696
  init_live_render();
158697
+ init_rendering_command();
158488
158698
  init_settings_model();
158489
158699
  init_settings_state();
158490
158700
  init_theme();
@@ -158565,6 +158775,7 @@ var init_shell = __esm(() => {
158565
158775
  "/reviews",
158566
158776
  "/remote-control",
158567
158777
  "/channels",
158778
+ "/rendering",
158568
158779
  "/theme",
158569
158780
  "/settings",
158570
158781
  "/mode",
@@ -181413,6 +181624,25 @@ var init_command_registry = __esm(() => {
181413
181624
  "does not touch content already relayed to an agent, exported copies, or git history"
181414
181625
  ]
181415
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
+ },
181416
181646
  {
181417
181647
  module: "bus",
181418
181648
  command: "bus list",
@@ -192707,6 +192937,24 @@ function createHttpBotApi(options) {
192707
192937
  }
192708
192938
  return call("sendMessage", body);
192709
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
+ },
192710
192958
  async editMessageReplyMarkup(params) {
192711
192959
  await call("editMessageReplyMarkup", {
192712
192960
  chat_id: params.chatId,
@@ -192787,6 +193035,187 @@ var init_bot_api_http = __esm(() => {
192787
193035
  init_types11();
192788
193036
  });
192789
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
+
192790
193219
  // src/remote/format.ts
192791
193220
  function fenceOpening(line) {
192792
193221
  const match = FENCE_LINE2.exec(line);
@@ -192834,13 +193263,40 @@ function cutAt(text, max) {
192834
193263
  }
192835
193264
  function toPieces(text, capacity) {
192836
193265
  const pieces = [];
193266
+ const raws = text.split(/(?<=\n)/);
193267
+ const bare = raws.map((raw) => raw.endsWith(`
193268
+ `) ? raw.slice(0, -1) : raw);
192837
193269
  let fence;
192838
- for (const raw of text.split(/(?<=\n)/)) {
192839
- const line = raw.endsWith(`
192840
- `) ? 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];
192841
193273
  const before = fence;
192842
193274
  if (fence === undefined) {
192843
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
+ }
192844
193300
  } else if (closesFence(line, fence)) {
192845
193301
  fence = undefined;
192846
193302
  }
@@ -192848,24 +193304,33 @@ function toPieces(text, capacity) {
192848
193304
  let rest = raw;
192849
193305
  while (rest.length > room) {
192850
193306
  const cut = cutAt(rest, room);
192851
- pieces.push({ text: rest.slice(0, cut), fence: before ?? fence });
193307
+ pieces.push({ text: rest.slice(0, cut), fence: before ?? fence, cost: cut });
192852
193308
  rest = rest.slice(cut);
192853
193309
  }
192854
193310
  if (rest.length > 0) {
192855
- 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 });
192856
193314
  }
192857
193315
  }
192858
193316
  return pieces;
192859
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
+ }
192860
193322
  function pack(text, capacity) {
192861
193323
  const parts = [];
192862
193324
  let current = "";
193325
+ let used = 0;
192863
193326
  let hasContent = false;
192864
193327
  let open;
192865
- const finish = () => {
193328
+ let lastTable;
193329
+ const finish = (next) => {
192866
193330
  let body = current.trimEnd();
192867
193331
  if (!hasContent || body.length === 0) {
192868
193332
  current = "";
193333
+ used = 0;
192869
193334
  hasContent = false;
192870
193335
  return;
192871
193336
  }
@@ -192876,6 +193341,11 @@ ${open.marker}`;
192876
193341
  parts.push(body);
192877
193342
  current = open === undefined ? "" : `${open.header}
192878
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
+ }
192879
193349
  hasContent = false;
192880
193350
  };
192881
193351
  for (const piece of toPieces(text, capacity)) {
@@ -192883,15 +193353,17 @@ ${open.marker}`;
192883
193353
  continue;
192884
193354
  }
192885
193355
  const closing = piece.fence === undefined ? 0 : piece.fence.marker.length + 1;
192886
- if (hasContent && current.length + piece.text.length + closing > capacity) {
192887
- finish();
193356
+ if (hasContent && used + piece.cost + closing > capacity) {
193357
+ finish(piece);
192888
193358
  if (piece.text.trim().length === 0) {
192889
193359
  continue;
192890
193360
  }
192891
193361
  }
192892
193362
  current += piece.text;
193363
+ used += piece.cost;
192893
193364
  hasContent = true;
192894
193365
  open = piece.fence;
193366
+ lastTable = piece.table;
192895
193367
  }
192896
193368
  finish();
192897
193369
  return parts;
@@ -192905,7 +193377,7 @@ function formatReply(text, limit = TELEGRAM_MAX_TEXT) {
192905
193377
  if (text.trim().length === 0) {
192906
193378
  return [];
192907
193379
  }
192908
- if (text.length <= limit) {
193380
+ if (renderedLength(text) <= limit) {
192909
193381
  return [text];
192910
193382
  }
192911
193383
  for (let digits = 1;digits <= 3; digits += 1) {
@@ -192926,10 +193398,101 @@ ${part}`);
192926
193398
  }
192927
193399
  var MIN_LIMIT = 64, MAX_FENCE_MARKER = 16, MAX_FENCE_INFO = 40, FENCE_LINE2;
192928
193400
  var init_format2 = __esm(() => {
193401
+ init_format_blocks();
193402
+ init_format_table();
192929
193403
  init_types11();
192930
193404
  FENCE_LINE2 = /^ {0,3}(`{3,}|~{3,})(.*)$/;
192931
193405
  });
192932
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
+
192933
193496
  // src/remote/format-html.ts
192934
193497
  function escapeHtml2(text) {
192935
193498
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -193063,7 +193626,8 @@ function link(text, index, context) {
193063
193626
  if (!HTTP_URL.test(url)) {
193064
193627
  return;
193065
193628
  }
193066
- 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 };
193067
193631
  }
193068
193632
  function renderInline2(text, context) {
193069
193633
  let out = "";
@@ -193175,7 +193739,19 @@ function renderTelegramHtml(part) {
193175
193739
  }
193176
193740
  continue;
193177
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
+ }
193178
193750
  index += 1;
193751
+ if (RULE.test(line)) {
193752
+ push(RULE_GLYPH.repeat(RULE_LENGTH), false);
193753
+ continue;
193754
+ }
193179
193755
  const heading = HEADING2.exec(line);
193180
193756
  if (heading !== null && heading[1].trim().length > 0) {
193181
193757
  push(`<b>${renderInline2(heading[1], { ...OUTSIDE, bold: true })}</b>`, false);
@@ -193183,6 +193759,13 @@ function renderTelegramHtml(part) {
193183
193759
  }
193184
193760
  const bullet = BULLET.exec(line);
193185
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
+ }
193186
193769
  push(`${bullet[1]}\u2022 ${renderInline2(bullet[2], OUTSIDE)}`, false);
193187
193770
  continue;
193188
193771
  }
@@ -193190,6 +193773,93 @@ function renderTelegramHtml(part) {
193190
193773
  }
193191
193774
  return out;
193192
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
+ }
193193
193863
  function isEntityParseError(error) {
193194
193864
  return isBotApiError(error) && error.kind === "rejected" && error.status === 400 && /can't parse entities/i.test(error.message);
193195
193865
  }
@@ -193204,7 +193874,7 @@ async function sendHtml(api, params, onFallback) {
193204
193874
  try {
193205
193875
  onFallback?.(error);
193206
193876
  } catch {}
193207
- return api.sendMessage(plain);
193877
+ return api.sendMessage({ ...plain, text: renderPlainText(plain.text) });
193208
193878
  }
193209
193879
  }
193210
193880
  async function editHtml(api, params, onFallback) {
@@ -193217,15 +193887,19 @@ async function editHtml(api, params, onFallback) {
193217
193887
  try {
193218
193888
  onFallback?.(error);
193219
193889
  } catch {}
193220
- await api.editMessageText(params);
193890
+ await api.editMessageText({ ...params, text: renderPlainText(params.text) });
193221
193891
  }
193222
193892
  }
193223
193893
  function isNotModified(error) {
193224
193894
  return isBotApiError(error) && error.kind === "rejected" && /message is not modified/i.test(error.message);
193225
193895
  }
193226
- 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;
193227
193897
  var init_format_html = __esm(() => {
193228
193898
  init_format2();
193899
+ init_format_blocks();
193900
+ init_format_link();
193901
+ init_format_plain();
193902
+ init_format_table();
193229
193903
  init_types11();
193230
193904
  OUTSIDE = { bold: false, italic: false, strike: false, link: false };
193231
193905
  WORD = /[\p{L}\p{N}_]/u;
@@ -193236,10 +193910,9 @@ var init_format_html = __esm(() => {
193236
193910
  HTTP_URL = /^https?:\/\/[^\s<>"'`]+$/i;
193237
193911
  LANGUAGE = /^[A-Za-z0-9_+#.-]{1,32}$/;
193238
193912
  TAGS2 = { bold: "b", italic: "i", strike: "s" };
193239
- HEADING2 = /^ {0,3}#{1,6}[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/;
193240
- BULLET = /^([ \t]*)[-*][ \t]+(.*)$/;
193241
- QUOTE = /^ {0,3}>(?: (.*))?$/;
193242
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;
193243
193916
  });
193244
193917
 
193245
193918
  // src/remote/durable-log.ts
@@ -193556,6 +194229,512 @@ var init_journal3 = __esm(() => {
193556
194229
  init_paths4();
193557
194230
  });
193558
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
+
193559
194738
  // src/remote/outbound-queue.ts
193560
194739
  import { randomUUID as randomUUID51 } from "crypto";
193561
194740
  import path363 from "path";
@@ -193582,6 +194761,7 @@ class OutboundQueue {
193582
194761
  log;
193583
194762
  onDrop;
193584
194763
  onFallback;
194764
+ rendering;
193585
194765
  retryDelayMs;
193586
194766
  sentHooks = new Map;
193587
194767
  blockedUntil = 0;
@@ -193595,6 +194775,7 @@ class OutboundQueue {
193595
194775
  this.onFallback = options.onFallback ?? (() => {
193596
194776
  return;
193597
194777
  });
194778
+ this.rendering = options.rendering ?? new RenderingState({ now: options.now });
193598
194779
  this.retryDelayMs = options.retryDelayMs ?? 5000;
193599
194780
  const directory = ensureRemoteDir(options.dir);
193600
194781
  this.log = new DurableLog({
@@ -193683,17 +194864,17 @@ class OutboundQueue {
193683
194864
  return { sent, dropped, remaining: this.log.size, retryInMs: wait, stoppedBy: "rate limited" };
193684
194865
  }
193685
194866
  try {
193686
- const result = await sendHtml(this.api, {
194867
+ const result = await sendRendered(this.api, {
193687
194868
  chatId: entry.chatId,
193688
194869
  text: entry.text,
193689
194870
  ...entry.threadId === undefined ? {} : { messageThreadId: entry.threadId },
193690
194871
  ...entry.keyboard === undefined ? {} : { inlineKeyboard: entry.keyboard }
193691
- }, () => this.onFallback(entry));
194872
+ }, { state: this.rendering, onFallback: (fallback) => this.onFallback(entry, fallback) });
193692
194873
  this.log.ack(entry.id);
193693
194874
  sent += 1;
193694
194875
  this.notifySent(entry, result.message_id);
193695
194876
  } catch (error) {
193696
- const description = describe(error);
194877
+ const description = describe2(error);
193697
194878
  if (isBotApiError(error) && error.kind === "rate-limited") {
193698
194879
  const retryMs = Math.max(1, error.retryAfterSec ?? 1) * 1000;
193699
194880
  this.blockedUntil = this.now() + retryMs;
@@ -193710,7 +194891,7 @@ class OutboundQueue {
193710
194891
  }
193711
194892
  }
193712
194893
  }
193713
- function describe(error) {
194894
+ function describe2(error) {
193714
194895
  return redactSensitiveText(error instanceof Error ? error.message : String(error));
193715
194896
  }
193716
194897
  var OUTBOUND_MAX_ENTRIES = 200;
@@ -193719,7 +194900,7 @@ var init_outbound_queue = __esm(() => {
193719
194900
  init_durable_log();
193720
194901
  init_paths4();
193721
194902
  init_format2();
193722
- init_format_html();
194903
+ init_rendering();
193723
194904
  init_types11();
193724
194905
  });
193725
194906
 
@@ -193829,7 +195010,7 @@ class UpdatePoller {
193829
195010
  continue;
193830
195011
  }
193831
195012
  failures += 1;
193832
- this.set({ state: "running", reason: describe2(error) });
195013
+ this.set({ state: "running", reason: describe3(error) });
193833
195014
  await this.sleep(this.backoffMs[Math.min(failures, this.backoffMs.length) - 1] ?? 1000, signal);
193834
195015
  continue;
193835
195016
  }
@@ -193849,13 +195030,13 @@ class UpdatePoller {
193849
195030
  }
193850
195031
  } catch (error) {
193851
195032
  failures += 1;
193852
- this.set({ state: "running", reason: describe2(error) });
195033
+ this.set({ state: "running", reason: describe3(error) });
193853
195034
  await this.sleep(this.backoffMs[Math.min(failures, this.backoffMs.length) - 1] ?? 1000, signal);
193854
195035
  }
193855
195036
  }
193856
195037
  }
193857
195038
  }
193858
- function describe2(error) {
195039
+ function describe3(error) {
193859
195040
  return redactSensitiveText(error instanceof Error ? error.message : String(error));
193860
195041
  }
193861
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;
@@ -193866,7 +195047,7 @@ var init_poller = __esm(() => {
193866
195047
 
193867
195048
  // src/remote/service-topics.ts
193868
195049
  import path364 from "path";
193869
- function parse(value) {
195050
+ function parse2(value) {
193870
195051
  if (typeof value !== "object" || value === null)
193871
195052
  return;
193872
195053
  const raw = value;
@@ -193893,7 +195074,7 @@ class ServiceTopics {
193893
195074
  if (!Array.isArray(records))
193894
195075
  return;
193895
195076
  for (const candidate of records) {
193896
- const record = parse(candidate);
195077
+ const record = parse2(candidate);
193897
195078
  if (record !== undefined)
193898
195079
  this.byName.set(nameKey(record.name), record);
193899
195080
  }
@@ -194077,6 +195258,7 @@ class RemoteHub {
194077
195258
  registry;
194078
195259
  inbound;
194079
195260
  outbound;
195261
+ rendering;
194080
195262
  serviceTopics;
194081
195263
  droppedReasons = new Map;
194082
195264
  journal;
@@ -194115,15 +195297,23 @@ class RemoteHub {
194115
195297
  };
194116
195298
  this.registry = new SessionRegistry({ ...options.dir === undefined ? {} : { dir: options.dir }, now: this.now });
194117
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
+ });
194118
195307
  this.outbound = new OutboundQueue({
194119
195308
  api: this.api,
195309
+ rendering: this.rendering,
194120
195310
  ...options.dir === undefined ? {} : { dir: options.dir },
194121
195311
  now: this.now,
194122
195312
  onDrop: (entry, reason) => {
194123
195313
  this.droppedReasons.set(entry.id, reason);
194124
195314
  this.event("outbound-dropped", `${reason}${entry.threadId === undefined ? "" : ` (topic ${entry.threadId})`}`);
194125
195315
  },
194126
- 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")
194127
195317
  });
194128
195318
  this.journal = new RejectedJournal({ ...options.dir === undefined ? {} : { dir: options.dir }, now: this.now });
194129
195319
  this.offset = new PollerState(options.dir === undefined ? {} : { dir: options.dir });
@@ -194264,7 +195454,13 @@ class RemoteHub {
194264
195454
  });
194265
195455
  }
194266
195456
  async sendGeneral(text) {
194267
- 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}`);
194268
195464
  }
194269
195465
  heartbeat(sessionId) {
194270
195466
  return this.serial(async () => {
@@ -194383,7 +195579,7 @@ class RemoteHub {
194383
195579
  return "gone";
194384
195580
  }
194385
195581
  try {
194386
- 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") });
194387
195583
  return "edited";
194388
195584
  } catch (error) {
194389
195585
  if (isNotModified(error)) {
@@ -194796,11 +195992,14 @@ var realTimers, MAX_EVENTS = 50, ANSWER_CALLBACK_TIMEOUT_MS = 5000;
194796
195992
  var init_hub = __esm(() => {
194797
195993
  init_service2();
194798
195994
  init_lease();
195995
+ init_config11();
194799
195996
  init_format_html();
194800
195997
  init_inbound();
194801
195998
  init_journal3();
194802
195999
  init_naming();
194803
196000
  init_outbound_queue();
196001
+ init_rendering();
196002
+ init_rendering_mode();
194804
196003
  init_command_gateway();
194805
196004
  init_message_state();
194806
196005
  init_poller();
@@ -195815,10 +197014,10 @@ function newPairingCode() {
195815
197014
  }
195816
197015
  return code;
195817
197016
  }
195818
- function describe3(error) {
197017
+ function describe4(error) {
195819
197018
  return redactSensitiveText(error instanceof Error ? error.message : String(error));
195820
197019
  }
195821
- function normalise3(text) {
197020
+ function normalise4(text) {
195822
197021
  return text.toUpperCase().replace(/[^A-Z0-9]/g, "");
195823
197022
  }
195824
197023
 
@@ -195876,7 +197075,7 @@ class Pairing {
195876
197075
  if (isBotApiError(error) && error.kind === "rejected") {
195877
197076
  return { ok: false, kind: "rejected", reason: "Telegram does not accept this bot token; check it with BotFather and try again." };
195878
197077
  }
195879
- 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)}` };
195880
197079
  }
195881
197080
  return { ok: true, pairing: new Pairing(options, bot) };
195882
197081
  }
@@ -196003,7 +197202,7 @@ class Pairing {
196003
197202
  return;
196004
197203
  }
196005
197204
  const sent = message.text.trim().replace(/^\/start(?:@\w+)?\s+/i, "");
196006
- if (this.code === undefined || normalise3(sent) !== this.code) {
197205
+ if (this.code === undefined || normalise4(sent) !== this.code) {
196007
197206
  return;
196008
197207
  }
196009
197208
  this.userId = message.from.id;
@@ -196112,7 +197311,7 @@ class Pairing {
196112
197311
  problems.push("Give the bot the Manage Topics right in the group's administrator settings.");
196113
197312
  }
196114
197313
  } catch (error) {
196115
- problems.push(`could not inspect the group: ${describe3(error)}`);
197314
+ problems.push(`could not inspect the group: ${describe4(error)}`);
196116
197315
  }
196117
197316
  this.expireIfDue();
196118
197317
  if (FINAL.has(this.state)) {
@@ -196137,7 +197336,7 @@ var init_pairing = __esm(() => {
196137
197336
  });
196138
197337
 
196139
197338
  // src/remote/channels.ts
196140
- function describe4(error) {
197339
+ function describe5(error) {
196141
197340
  return redactSensitiveText(error instanceof Error ? error.message : String(error));
196142
197341
  }
196143
197342
 
@@ -196200,11 +197399,25 @@ class ChannelsController {
196200
197399
  }
196201
197400
  return { state: "not-connected" };
196202
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
+ }
196203
197410
  status() {
196204
197411
  const { state, reason } = this.state();
197412
+ const rendering = this.rendering();
196205
197413
  return ok2({
196206
197414
  machine: this.host.machine,
196207
- 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
+ }
196208
197421
  });
196209
197422
  }
196210
197423
  async dropPairing() {
@@ -196300,7 +197513,7 @@ class ChannelsController {
196300
197513
  try {
196301
197514
  await hub.sendGeneral(`Keryx test message from ${this.host.machine}: Telegram is connected.`);
196302
197515
  } catch (error) {
196303
- 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)}`);
196304
197517
  }
196305
197518
  return ok2({ delivered: true, machine: this.host.machine });
196306
197519
  }
@@ -196326,6 +197539,7 @@ var init_channels = __esm(() => {
196326
197539
  init_http_surface();
196327
197540
  init_pairing();
196328
197541
  init_paths4();
197542
+ init_rendering_mode();
196329
197543
  });
196330
197544
 
196331
197545
  // src/remote/poll-lock.ts
@@ -203117,6 +204331,156 @@ var init_retention2 = __esm(() => {
203117
204331
  init_sweep();
203118
204332
  });
203119
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
+
203120
204484
  // src/commands/forgetting.ts
203121
204485
  async function forgettingCommand(args) {
203122
204486
  const command = args[0];
@@ -204904,7 +206268,7 @@ function buildHeader(entry, size) {
204904
206268
  chk.copy(header, 148);
204905
206269
  return { ok: true, value: header };
204906
206270
  }
204907
- function pad4(buf) {
206271
+ function pad5(buf) {
204908
206272
  const rem = buf.length % BLOCK;
204909
206273
  if (rem === 0)
204910
206274
  return buf;
@@ -204939,7 +206303,7 @@ function buildBundleArchive(files, manifestBytes) {
204939
206303
  return header;
204940
206304
  chunks.push(header.value);
204941
206305
  if (entry.bytes && entry.bytes.length > 0) {
204942
- chunks.push(pad4(entry.bytes));
206306
+ chunks.push(pad5(entry.bytes));
204943
206307
  }
204944
206308
  }
204945
206309
  chunks.push(Buffer.alloc(BLOCK * 2));
@@ -209141,6 +210505,9 @@ var VERSION3, CLI_ROUTES, MCP_KNOWN_SUBCOMMANDS, USAGE_BODY = `Usage:
209141
210505
  keryx retention sweep [--apply] [--target <id>]... [--max-age-days <n>] [--max-bytes <n>] [--json]
209142
210506
  Bound gdctx raw/artifacts logs and owner write-conflict
209143
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
209144
210511
  keryx forgetting trail [--limit <n>] [--json]
209145
210512
  keryx forgetting lookup "<ref-or-path>" [--layer <layer>] [--search] [--json]
209146
210513
  Read the deletion trail: what was removed, when, at
@@ -209238,6 +210605,7 @@ Commands:
209238
210605
  metrics Provenance-aware execution observability: run records, baselines, benchmarks
209239
210606
  workspace Shared Agent Context: workspaces, FWK reads, propose/review (module sac)
209240
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
209241
210609
  forgetting Read the deletion trail \u2014 was this removed, or did it never exist?
209242
210610
  trigger Fire one declared project trigger (git hook, cron line, CI job) \u2014 one pass, one exit code
209243
210611
  schedule Scheduled agent tasks in the background: create (with confirmation), list, pause, resume, remove
@@ -209295,6 +210663,7 @@ var init_cli_registry = __esm(() => {
209295
210663
  init_external2();
209296
210664
  init_auth();
209297
210665
  init_retention2();
210666
+ init_remote();
209298
210667
  init_forgetting();
209299
210668
  init_trigger();
209300
210669
  init_schedule2();
@@ -209363,6 +210732,7 @@ var init_cli_registry = __esm(() => {
209363
210732
  version: versionCommand,
209364
210733
  workspace: workspaceCommand,
209365
210734
  retention: retentionCommand,
210735
+ remote: remoteCommand,
209366
210736
  forgetting: forgettingCommand,
209367
210737
  trigger: triggerCommand,
209368
210738
  schedule: scheduleCommand,