clisbot 0.1.29 → 0.1.32

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 (2) hide show
  1. package/dist/main.js +1866 -572
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -4,25 +4,43 @@ var __getProtoOf = Object.getPrototypeOf;
4
4
  var __defProp = Object.defineProperty;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ function __accessProp(key) {
8
+ return this[key];
9
+ }
10
+ var __toESMCache_node;
11
+ var __toESMCache_esm;
7
12
  var __toESM = (mod, isNodeMode, target) => {
13
+ var canCache = mod != null && typeof mod === "object";
14
+ if (canCache) {
15
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
16
+ var cached = cache.get(mod);
17
+ if (cached)
18
+ return cached;
19
+ }
8
20
  target = mod != null ? __create(__getProtoOf(mod)) : {};
9
21
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
22
  for (let key of __getOwnPropNames(mod))
11
23
  if (!__hasOwnProp.call(to, key))
12
24
  __defProp(to, key, {
13
- get: () => mod[key],
25
+ get: __accessProp.bind(mod, key),
14
26
  enumerable: true
15
27
  });
28
+ if (canCache)
29
+ cache.set(mod, to);
16
30
  return to;
17
31
  };
18
32
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
33
+ var __returnValue = (v) => v;
34
+ function __exportSetter(name, newValue) {
35
+ this[name] = __returnValue.bind(null, newValue);
36
+ }
19
37
  var __export = (target, all) => {
20
38
  for (var name in all)
21
39
  __defProp(target, name, {
22
40
  get: all[name],
23
41
  enumerable: true,
24
42
  configurable: true,
25
- set: (newValue) => all[name] = () => newValue
43
+ set: __exportSetter.bind(all, name)
26
44
  });
27
45
  };
28
46
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
@@ -62543,6 +62561,421 @@ function prependAttachmentMentions(text, attachmentPaths) {
62543
62561
  return normalizedText ? `${mentions} ${normalizedText}` : mentions;
62544
62562
  }
62545
62563
 
62564
+ // src/agents/commands.ts
62565
+ function parseAgentCommand(text, options = {}) {
62566
+ const normalized = text.trim();
62567
+ const commandPrefixes = options.commandPrefixes ?? {
62568
+ slash: ["::", "\\"],
62569
+ bash: ["!"]
62570
+ };
62571
+ const bashPrefix = findMatchingPrefix(normalized, commandPrefixes.bash);
62572
+ if (bashPrefix) {
62573
+ const command2 = normalized.slice(bashPrefix.length).trim();
62574
+ return {
62575
+ type: "bash",
62576
+ command: command2,
62577
+ source: "shortcut"
62578
+ };
62579
+ }
62580
+ const slashPrefix = findMatchingPrefix(normalized, ["/", ...commandPrefixes.slash]);
62581
+ if (!slashPrefix) {
62582
+ return null;
62583
+ }
62584
+ const withoutSlash = normalized.slice(slashPrefix.length).trim();
62585
+ if (!withoutSlash) {
62586
+ return {
62587
+ type: "control",
62588
+ name: "help"
62589
+ };
62590
+ }
62591
+ const [command] = withoutSlash.split(/\s+/, 1);
62592
+ const lowered = normalizeSlashCommandName(command, options.botUsername);
62593
+ if (lowered === "start") {
62594
+ return {
62595
+ type: "control",
62596
+ name: "start"
62597
+ };
62598
+ }
62599
+ if (lowered === "status") {
62600
+ return {
62601
+ type: "control",
62602
+ name: "status"
62603
+ };
62604
+ }
62605
+ if (lowered === "help") {
62606
+ return {
62607
+ type: "control",
62608
+ name: "help"
62609
+ };
62610
+ }
62611
+ if (lowered === "whoami") {
62612
+ return {
62613
+ type: "control",
62614
+ name: "whoami"
62615
+ };
62616
+ }
62617
+ if (lowered === "transcript") {
62618
+ return {
62619
+ type: "control",
62620
+ name: "transcript"
62621
+ };
62622
+ }
62623
+ if (lowered === "attach") {
62624
+ return {
62625
+ type: "control",
62626
+ name: "attach"
62627
+ };
62628
+ }
62629
+ if (lowered === "detach") {
62630
+ return {
62631
+ type: "control",
62632
+ name: "detach"
62633
+ };
62634
+ }
62635
+ if (lowered === "watch") {
62636
+ const parsed = parseWatchCommand(withoutSlash.slice(command.length).trim());
62637
+ if (parsed) {
62638
+ return {
62639
+ type: "control",
62640
+ name: "watch",
62641
+ intervalMs: parsed.intervalMs,
62642
+ durationMs: parsed.durationMs
62643
+ };
62644
+ }
62645
+ return {
62646
+ type: "control",
62647
+ name: "help"
62648
+ };
62649
+ }
62650
+ if (lowered === "stop") {
62651
+ return {
62652
+ type: "control",
62653
+ name: "stop"
62654
+ };
62655
+ }
62656
+ if (lowered === "nudge") {
62657
+ return {
62658
+ type: "control",
62659
+ name: "nudge"
62660
+ };
62661
+ }
62662
+ if (lowered === "followup") {
62663
+ const action = withoutSlash.slice(command.length).trim().toLowerCase();
62664
+ if (!action || action === "status") {
62665
+ return {
62666
+ type: "control",
62667
+ name: "followup",
62668
+ action: "status"
62669
+ };
62670
+ }
62671
+ if (action === "auto") {
62672
+ return {
62673
+ type: "control",
62674
+ name: "followup",
62675
+ action: "auto",
62676
+ mode: "auto"
62677
+ };
62678
+ }
62679
+ if (action === "mention-only") {
62680
+ return {
62681
+ type: "control",
62682
+ name: "followup",
62683
+ action: "mention-only",
62684
+ mode: "mention-only"
62685
+ };
62686
+ }
62687
+ if (action === "pause") {
62688
+ return {
62689
+ type: "control",
62690
+ name: "followup",
62691
+ action: "pause",
62692
+ mode: "paused"
62693
+ };
62694
+ }
62695
+ if (action === "resume") {
62696
+ return {
62697
+ type: "control",
62698
+ name: "followup",
62699
+ action: "resume"
62700
+ };
62701
+ }
62702
+ return {
62703
+ type: "control",
62704
+ name: "followup",
62705
+ action: "status"
62706
+ };
62707
+ }
62708
+ if (lowered === "responsemode") {
62709
+ const action = withoutSlash.slice(command.length).trim().toLowerCase();
62710
+ if (!action || action === "status") {
62711
+ return {
62712
+ type: "control",
62713
+ name: "responsemode",
62714
+ action: "status"
62715
+ };
62716
+ }
62717
+ if (action === "capture-pane" || action === "message-tool") {
62718
+ return {
62719
+ type: "control",
62720
+ name: "responsemode",
62721
+ action,
62722
+ responseMode: action
62723
+ };
62724
+ }
62725
+ return {
62726
+ type: "control",
62727
+ name: "responsemode",
62728
+ action: "status"
62729
+ };
62730
+ }
62731
+ if (lowered === "streaming") {
62732
+ const action = withoutSlash.slice(command.length).trim().toLowerCase();
62733
+ if (!action || action === "status") {
62734
+ return {
62735
+ type: "control",
62736
+ name: "streaming",
62737
+ action: "status"
62738
+ };
62739
+ }
62740
+ if (action === "on") {
62741
+ return {
62742
+ type: "control",
62743
+ name: "streaming",
62744
+ action: "on",
62745
+ streaming: "all"
62746
+ };
62747
+ }
62748
+ if (action === "off" || action === "latest" || action === "all") {
62749
+ return {
62750
+ type: "control",
62751
+ name: "streaming",
62752
+ action,
62753
+ streaming: action
62754
+ };
62755
+ }
62756
+ return {
62757
+ type: "control",
62758
+ name: "streaming",
62759
+ action: "status"
62760
+ };
62761
+ }
62762
+ if (lowered === "additionalmessagemode") {
62763
+ const action = withoutSlash.slice(command.length).trim().toLowerCase();
62764
+ if (!action || action === "status") {
62765
+ return {
62766
+ type: "control",
62767
+ name: "additionalmessagemode",
62768
+ action: "status"
62769
+ };
62770
+ }
62771
+ if (action === "queue" || action === "steer") {
62772
+ return {
62773
+ type: "control",
62774
+ name: "additionalmessagemode",
62775
+ action,
62776
+ additionalMessageMode: action
62777
+ };
62778
+ }
62779
+ return {
62780
+ type: "control",
62781
+ name: "additionalmessagemode",
62782
+ action: "status"
62783
+ };
62784
+ }
62785
+ if (lowered === "bash") {
62786
+ return {
62787
+ type: "bash",
62788
+ command: withoutSlash.slice(command.length).trim(),
62789
+ source: "slash"
62790
+ };
62791
+ }
62792
+ if (lowered === "loop") {
62793
+ const loopText = withoutSlash.slice(command.length).trim();
62794
+ const loweredLoopText = loopText.toLowerCase();
62795
+ if (!loweredLoopText || loweredLoopText === "help") {
62796
+ return {
62797
+ type: "control",
62798
+ name: "loop-help"
62799
+ };
62800
+ }
62801
+ if (loweredLoopText === "status") {
62802
+ return {
62803
+ type: "loop-control",
62804
+ action: "status"
62805
+ };
62806
+ }
62807
+ if (loweredLoopText === "cancel" || loweredLoopText.startsWith("cancel ")) {
62808
+ const cancelArgs = loopText.slice("cancel".length).trim();
62809
+ if (hasLoopFlag(cancelArgs, LOOP_FORCE_FLAG)) {
62810
+ return {
62811
+ type: "loop-error",
62812
+ message: `Use \`/loop cancel --all ${LOOP_APP_FLAG}\` for app-wide cancellation.`
62813
+ };
62814
+ }
62815
+ const all = hasLoopFlag(cancelArgs, LOOP_ALL_FLAG);
62816
+ const app = hasLoopFlag(cancelArgs, LOOP_APP_FLAG);
62817
+ if (app && !all) {
62818
+ return {
62819
+ type: "loop-error",
62820
+ message: `\`${LOOP_APP_FLAG}\` only works with \`/loop cancel ${LOOP_ALL_FLAG}\`.`
62821
+ };
62822
+ }
62823
+ const loopId = cancelArgs.split(/\s+/).map((token) => token.trim()).find((token) => token && token !== LOOP_ALL_FLAG && token !== LOOP_APP_FLAG);
62824
+ return {
62825
+ type: "loop-control",
62826
+ action: "cancel",
62827
+ loopId: loopId || undefined,
62828
+ all,
62829
+ app
62830
+ };
62831
+ }
62832
+ const parsed = parseLoopSlashCommand(loopText);
62833
+ if ("error" in parsed) {
62834
+ return {
62835
+ type: "loop-error",
62836
+ message: parsed.error
62837
+ };
62838
+ }
62839
+ return {
62840
+ type: "loop",
62841
+ params: parsed
62842
+ };
62843
+ }
62844
+ if (lowered === "queue" || lowered === "q") {
62845
+ const queueText = withoutSlash.slice(command.length).trim();
62846
+ const normalizedQueueText = queueText.toLowerCase();
62847
+ if (lowered === "queue") {
62848
+ if (normalizedQueueText === "help") {
62849
+ return {
62850
+ type: "control",
62851
+ name: "queue-help"
62852
+ };
62853
+ }
62854
+ if (normalizedQueueText === "list") {
62855
+ return {
62856
+ type: "control",
62857
+ name: "queue-list"
62858
+ };
62859
+ }
62860
+ if (normalizedQueueText === "clear") {
62861
+ return {
62862
+ type: "control",
62863
+ name: "queue-clear"
62864
+ };
62865
+ }
62866
+ }
62867
+ return {
62868
+ type: "queue",
62869
+ text: queueText
62870
+ };
62871
+ }
62872
+ if (lowered === "queue-list" || lowered === "queuelist") {
62873
+ return {
62874
+ type: "control",
62875
+ name: "queue-list"
62876
+ };
62877
+ }
62878
+ if (lowered === "queue-clear" || lowered === "queueclear") {
62879
+ return {
62880
+ type: "control",
62881
+ name: "queue-clear"
62882
+ };
62883
+ }
62884
+ if (lowered === "steer" || lowered === "s") {
62885
+ return {
62886
+ type: "steer",
62887
+ text: withoutSlash.slice(command.length).trim()
62888
+ };
62889
+ }
62890
+ return {
62891
+ type: "native",
62892
+ text: normalized
62893
+ };
62894
+ }
62895
+ function findMatchingPrefix(text, prefixes) {
62896
+ return [...prefixes].sort((left, right) => right.length - left.length).find((prefix) => prefix.length > 0 && text.startsWith(prefix));
62897
+ }
62898
+ function normalizeSlashCommandName(command, botUsername) {
62899
+ const lowered = command?.toLowerCase() ?? "";
62900
+ const normalizedBotUsername = (botUsername ?? "").trim().toLowerCase().replace(/^@/, "");
62901
+ if (!normalizedBotUsername) {
62902
+ return lowered;
62903
+ }
62904
+ const suffix = `@${normalizedBotUsername}`;
62905
+ if (!lowered.endsWith(suffix)) {
62906
+ return lowered;
62907
+ }
62908
+ return lowered.slice(0, lowered.length - suffix.length);
62909
+ }
62910
+ function renderAgentControlSlashHelp() {
62911
+ return [
62912
+ "Slash commands",
62913
+ "",
62914
+ "- `/start`: show onboarding help for the current surface",
62915
+ "- `/status`: show the current route status and operator setup commands",
62916
+ "- `/help`: show available control slash commands",
62917
+ "- `/whoami`: show the current platform, route, and sender identity details",
62918
+ "- `/transcript`: show the current conversation session transcript when the route verbose policy allows it",
62919
+ "- `/attach`: attach this thread to the active run and resume live updates when it is still processing",
62920
+ "- `/detach`: stop live updates for this thread, switch to sparse progress updates, and still allow final settlement here",
62921
+ "- `/watch every 30s [for 10m]`: post the latest state on an interval until the run settles or the watch window ends",
62922
+ "- `/stop`: send Escape to interrupt the current conversation session",
62923
+ "- `/nudge`: send one extra Enter to the current tmux session without resending the prompt text",
62924
+ "- `/followup status`: show the current conversation follow-up policy",
62925
+ "- `/followup auto`: allow natural follow-up after the bot has replied in-thread",
62926
+ "- `/followup mention-only`: require explicit mention for each later turn",
62927
+ "- `/followup pause`: stop passive follow-up until the next explicit mention",
62928
+ "- `/followup resume`: clear the runtime override and restore config defaults",
62929
+ "- `/streaming status|on|off|latest|all`: show or change streaming mode for this surface",
62930
+ "- `/responsemode status`: show the configured response mode for this surface",
62931
+ "- `/responsemode capture-pane`: settle replies from captured pane output for this surface",
62932
+ "- `/responsemode message-tool`: expect the agent to reply through `clisbot message send` for this surface",
62933
+ "- `/additionalmessagemode status`: show how extra messages behave while a run is already active",
62934
+ "- `/additionalmessagemode steer`: send later user messages straight into the active session",
62935
+ "- `/additionalmessagemode queue`: queue later user messages behind the active run for this surface",
62936
+ "- `/queue <message>` or `\\q <message>`: enqueue a later message behind the active run and let clisbot deliver it in order",
62937
+ "- `/queue help`: show queue-specific help and examples",
62938
+ "- `/steer <message>` or `\\s <message>`: inject a steering message into the active run immediately",
62939
+ "- `/queue list`: show queued messages that have not started yet",
62940
+ "- `/queue clear`: clear queued messages that have not started yet",
62941
+ "- `/loop help`: show loop-specific help and syntax examples",
62942
+ ...renderLoopHelpLines(),
62943
+ "- `/bash` followed by a shell command: requires `shellExecute` on the resolved agent role",
62944
+ "- shortcut prefixes such as `!` run bash only when the resolved agent role allows `shellExecute`",
62945
+ "",
62946
+ "Other slash commands are forwarded to the agent unchanged."
62947
+ ].join(`
62948
+ `);
62949
+ }
62950
+ function renderQueueHelpLines() {
62951
+ return [
62952
+ "- `/queue <message>` or `\\q <message>`: enqueue one later message behind the active run",
62953
+ "- `/queue list`: show queued messages that have not started yet",
62954
+ "- `/queue clear`: clear queued messages that have not started yet",
62955
+ "- `/queue help`: show this queue help again",
62956
+ "- `/steer <message>` or `\\s <message>`: inject an immediate steering message instead of queueing"
62957
+ ];
62958
+ }
62959
+ function parseWatchCommand(raw) {
62960
+ const match = raw.match(/^every\s+(\S+)(?:\s+for\s+(\S+))?$/i);
62961
+ if (!match) {
62962
+ return null;
62963
+ }
62964
+ const intervalMs = parseCommandDurationMs(match[1] ?? "");
62965
+ if (!intervalMs) {
62966
+ return null;
62967
+ }
62968
+ const durationToken = match[2];
62969
+ const parsedDurationMs = durationToken ? parseCommandDurationMs(durationToken) : null;
62970
+ if (durationToken && !parsedDurationMs) {
62971
+ return null;
62972
+ }
62973
+ return {
62974
+ intervalMs,
62975
+ durationMs: parsedDurationMs ?? undefined
62976
+ };
62977
+ }
62978
+
62546
62979
  // src/agents/agent-service.ts
62547
62980
  import { randomUUID as randomUUID4 } from "node:crypto";
62548
62981
 
@@ -62604,6 +63037,7 @@ class SessionStore {
62604
63037
  followUp: existing?.followUp,
62605
63038
  runtime: existing?.runtime,
62606
63039
  intervalLoops: existing?.intervalLoops,
63040
+ recentConversation: existing?.recentConversation,
62607
63041
  updatedAt: Date.now()
62608
63042
  });
62609
63043
  }
@@ -62647,6 +63081,85 @@ class SessionStore {
62647
63081
  }
62648
63082
  }
62649
63083
 
63084
+ // src/shared/recent-message-context.ts
63085
+ var RECENT_CONVERSATION_MESSAGE_LIMIT = 5;
63086
+ function normalizeMessage(message) {
63087
+ return {
63088
+ marker: message.marker.trim(),
63089
+ text: message.text?.trim() || undefined,
63090
+ senderId: message.senderId?.trim() || undefined,
63091
+ senderName: message.senderName?.trim() || undefined
63092
+ };
63093
+ }
63094
+ function normalizeReplayLine(text) {
63095
+ return text.replace(/\s+/g, " ").trim();
63096
+ }
63097
+ function renderSenderLabel(message) {
63098
+ return message.senderName ?? message.senderId;
63099
+ }
63100
+ function appendRecentConversationMessage(state, message) {
63101
+ const normalized = normalizeMessage(message);
63102
+ if (!normalized.marker) {
63103
+ return state ?? { messages: [] };
63104
+ }
63105
+ const currentMessages = state?.messages ?? [];
63106
+ const nextMessages = [
63107
+ ...currentMessages.filter((entry) => entry.marker !== normalized.marker),
63108
+ normalized
63109
+ ].slice(-RECENT_CONVERSATION_MESSAGE_LIMIT);
63110
+ return {
63111
+ lastProcessedMarker: state?.lastProcessedMarker,
63112
+ messages: nextMessages
63113
+ };
63114
+ }
63115
+ function markRecentConversationProcessed(state, marker) {
63116
+ const normalizedMarker = marker.trim();
63117
+ return {
63118
+ lastProcessedMarker: normalizedMarker || state?.lastProcessedMarker,
63119
+ messages: state?.messages ?? []
63120
+ };
63121
+ }
63122
+ function collectRecentConversationReplayMessages(state, params = {}) {
63123
+ const messages = state?.messages ?? [];
63124
+ if (messages.length === 0) {
63125
+ return [];
63126
+ }
63127
+ const excludeMarker = params.excludeMarker?.trim();
63128
+ const pending = [];
63129
+ for (let index = messages.length - 1;index >= 0; index -= 1) {
63130
+ const message = messages[index];
63131
+ if (state?.lastProcessedMarker && message.marker === state.lastProcessedMarker) {
63132
+ break;
63133
+ }
63134
+ if (excludeMarker && message.marker === excludeMarker) {
63135
+ continue;
63136
+ }
63137
+ pending.push(message);
63138
+ }
63139
+ return pending.reverse();
63140
+ }
63141
+ function prependRecentConversationContext(params) {
63142
+ const replayLines = params.recentMessages.map((message) => {
63143
+ const text = normalizeReplayLine(message.text ?? "");
63144
+ if (!text) {
63145
+ return "";
63146
+ }
63147
+ const sender = renderSenderLabel(message);
63148
+ return sender ? `- ${sender}: ${text}` : `- ${text}`;
63149
+ }).filter(Boolean);
63150
+ if (replayLines.length === 0) {
63151
+ return params.currentText;
63152
+ }
63153
+ return [
63154
+ "Before answering, catch up on these newer messages from this conversation that were not processed yet:",
63155
+ ...replayLines,
63156
+ "",
63157
+ "Current message:",
63158
+ params.currentText
63159
+ ].join(`
63160
+ `);
63161
+ }
63162
+
62650
63163
  // src/agents/session-state.ts
62651
63164
  class AgentSessionState {
62652
63165
  sessionStore;
@@ -62665,7 +63178,8 @@ class AgentSessionState {
62665
63178
  followUp: existing?.followUp,
62666
63179
  runnerCommand: params.runnerCommand ?? existing?.runnerCommand ?? resolved.runner.command,
62667
63180
  runtime: params.runtime ?? existing?.runtime,
62668
- intervalLoops: existing?.intervalLoops
63181
+ intervalLoops: existing?.intervalLoops,
63182
+ recentConversation: existing?.recentConversation
62669
63183
  }));
62670
63184
  }
62671
63185
  async clearSessionIdEntry(resolved, params = {}) {
@@ -62676,7 +63190,8 @@ class AgentSessionState {
62676
63190
  runtime: {
62677
63191
  state: "idle"
62678
63192
  },
62679
- intervalLoops: existing?.intervalLoops
63193
+ intervalLoops: existing?.intervalLoops,
63194
+ recentConversation: existing?.recentConversation
62680
63195
  }));
62681
63196
  }
62682
63197
  async setSessionRuntime(resolved, runtime) {
@@ -62685,7 +63200,8 @@ class AgentSessionState {
62685
63200
  followUp: existing?.followUp,
62686
63201
  runnerCommand: existing?.runnerCommand ?? resolved.runner.command,
62687
63202
  runtime,
62688
- intervalLoops: existing?.intervalLoops
63203
+ intervalLoops: existing?.intervalLoops,
63204
+ recentConversation: existing?.recentConversation
62689
63205
  }));
62690
63206
  }
62691
63207
  async getConversationFollowUpState(target) {
@@ -62733,7 +63249,8 @@ class AgentSessionState {
62733
63249
  followUp: existing?.followUp,
62734
63250
  runnerCommand: existing?.runnerCommand ?? resolved.runner.command,
62735
63251
  runtime: existing?.runtime,
62736
- intervalLoops: [...(existing?.intervalLoops ?? []).filter((item) => item.id !== loop.id), loop]
63252
+ intervalLoops: [...(existing?.intervalLoops ?? []).filter((item) => item.id !== loop.id), loop],
63253
+ recentConversation: existing?.recentConversation
62737
63254
  }));
62738
63255
  }
62739
63256
  async replaceIntervalLoopIfPresent(resolved, loop) {
@@ -62753,6 +63270,7 @@ class AgentSessionState {
62753
63270
  followUp: existing?.followUp,
62754
63271
  runtime: existing?.runtime,
62755
63272
  intervalLoops: currentLoops.map((item) => item.id === loop.id ? loop : item),
63273
+ recentConversation: existing?.recentConversation,
62756
63274
  updatedAt: Date.now()
62757
63275
  };
62758
63276
  });
@@ -62764,7 +63282,8 @@ class AgentSessionState {
62764
63282
  followUp: existing?.followUp,
62765
63283
  runnerCommand: existing?.runnerCommand ?? resolved.runner.command,
62766
63284
  runtime: existing?.runtime,
62767
- intervalLoops: (existing?.intervalLoops ?? []).filter((item) => item.id !== loopId)
63285
+ intervalLoops: (existing?.intervalLoops ?? []).filter((item) => item.id !== loopId),
63286
+ recentConversation: existing?.recentConversation
62768
63287
  }));
62769
63288
  }
62770
63289
  async clearIntervalLoops(resolved) {
@@ -62773,7 +63292,8 @@ class AgentSessionState {
62773
63292
  followUp: existing?.followUp,
62774
63293
  runnerCommand: existing?.runnerCommand ?? resolved.runner.command,
62775
63294
  runtime: existing?.runtime,
62776
- intervalLoops: []
63295
+ intervalLoops: [],
63296
+ recentConversation: existing?.recentConversation
62777
63297
  }));
62778
63298
  }
62779
63299
  async removeIntervalLoopById(loopId) {
@@ -62826,7 +63346,8 @@ class AgentSessionState {
62826
63346
  overrideMode: mode
62827
63347
  },
62828
63348
  runnerCommand: existing?.runnerCommand ?? resolved.runner.command,
62829
- intervalLoops: existing?.intervalLoops
63349
+ intervalLoops: existing?.intervalLoops,
63350
+ recentConversation: existing?.recentConversation
62830
63351
  }));
62831
63352
  }
62832
63353
  async resetConversationFollowUpMode(resolved) {
@@ -62837,7 +63358,8 @@ class AgentSessionState {
62837
63358
  overrideMode: undefined
62838
63359
  } : undefined,
62839
63360
  runnerCommand: existing?.runnerCommand ?? resolved.runner.command,
62840
- intervalLoops: existing?.intervalLoops
63361
+ intervalLoops: existing?.intervalLoops,
63362
+ recentConversation: existing?.recentConversation
62841
63363
  }));
62842
63364
  }
62843
63365
  async reactivateConversationFollowUp(resolved) {
@@ -62868,7 +63390,32 @@ class AgentSessionState {
62868
63390
  } : {}
62869
63391
  } : {}
62870
63392
  } : existing?.runtime,
62871
- intervalLoops: existing?.intervalLoops
63393
+ intervalLoops: existing?.intervalLoops,
63394
+ recentConversation: existing?.recentConversation
63395
+ }));
63396
+ }
63397
+ async appendRecentConversationMessage(resolved, message) {
63398
+ return this.upsertSessionEntry(resolved, (existing) => ({
63399
+ sessionId: existing?.sessionId,
63400
+ followUp: existing?.followUp,
63401
+ runnerCommand: existing?.runnerCommand ?? resolved.runner.command,
63402
+ runtime: existing?.runtime,
63403
+ intervalLoops: existing?.intervalLoops,
63404
+ recentConversation: appendRecentConversationMessage(existing?.recentConversation, message)
63405
+ }));
63406
+ }
63407
+ async getRecentConversationReplayMessages(target, params = {}) {
63408
+ const entry = await this.sessionStore.get(target.sessionKey);
63409
+ return collectRecentConversationReplayMessages(entry?.recentConversation, params);
63410
+ }
63411
+ async markRecentConversationProcessed(resolved, marker) {
63412
+ return this.upsertSessionEntry(resolved, (existing) => ({
63413
+ sessionId: existing?.sessionId,
63414
+ followUp: existing?.followUp,
63415
+ runnerCommand: existing?.runnerCommand ?? resolved.runner.command,
63416
+ runtime: existing?.runtime,
63417
+ intervalLoops: existing?.intervalLoops,
63418
+ recentConversation: markRecentConversationProcessed(existing?.recentConversation, marker)
62872
63419
  }));
62873
63420
  }
62874
63421
  async upsertSessionEntry(resolved, update) {
@@ -62883,6 +63430,7 @@ class AgentSessionState {
62883
63430
  followUp: next.followUp,
62884
63431
  runtime: next.runtime ?? existing?.runtime,
62885
63432
  intervalLoops: next.intervalLoops ?? existing?.intervalLoops,
63433
+ recentConversation: next.recentConversation ?? existing?.recentConversation,
62886
63434
  updatedAt: Date.now()
62887
63435
  };
62888
63436
  });
@@ -62901,6 +63449,7 @@ You are operating inside clisbot.
62901
63449
  {{delivery_intro}}
62902
63450
  {{reply_command}}
62903
63451
  {{reply_rules}}
63452
+ {{reply_style_hint}}
62904
63453
  ${CONFIGURATION_GUIDANCE}{{protected_control_suffix}}
62905
63454
  </system>
62906
63455
 
@@ -62943,14 +63492,21 @@ var FINAL_RULE_REQUIRED = "send exactly 1 final user-facing response";
62943
63492
  var FINAL_RULE_OPTIONAL = "final response is optional";
62944
63493
  var EMPTY_REPLY_COMMAND = "";
62945
63494
  var EMPTY_REPLY_RULES = "";
63495
+ var EMPTY_REPLY_STYLE_HINT = "";
62946
63496
  var SLACK_REPLY_COMMAND_BASE = `{{command}} message send \\
62947
63497
  --channel slack \\
62948
63498
  {{account_clause}} --target channel:{{channel_id}} \\
62949
- {{thread_clause}}`;
63499
+ {{thread_clause}} --input md \\
63500
+ --render blocks \\
63501
+ `;
62950
63502
  var TELEGRAM_REPLY_COMMAND_BASE = `{{command}} message send \\
62951
63503
  --channel telegram \\
62952
63504
  {{account_clause}} --target {{chat_id}} \\
62953
- {{thread_clause}}`;
63505
+ {{thread_clause}} --input md \\
63506
+ --render native \\
63507
+ `;
63508
+ var SLACK_REPLY_STYLE_HINT = "Put readable hierarchical Markdown in the --message body.";
63509
+ var TELEGRAM_REPLY_STYLE_HINT = "Put readable hierarchical Markdown in the --message body.";
62954
63510
  var ACCOUNT_CLAUSE = " --account {{account_id}} \\\n";
62955
63511
  var EMPTY_ACCOUNT_CLAUSE = "";
62956
63512
  var SLACK_THREAD_CLAUSE = " --thread-id {{thread_ts}} \\\n";
@@ -62991,6 +63547,7 @@ function buildChannelPromptText(params) {
62991
63547
  delivery_intro: promptParts.deliveryIntro,
62992
63548
  reply_command: promptParts.replyCommand,
62993
63549
  reply_rules: promptParts.replyRules,
63550
+ reply_style_hint: promptParts.replyStyleHint,
62994
63551
  protected_control_suffix: renderProtectedControlSuffix(params.protectedControlMutationRule),
62995
63552
  message_body: params.text
62996
63553
  });
@@ -63001,7 +63558,8 @@ function renderMessagePromptParts(params) {
63001
63558
  return {
63002
63559
  deliveryIntro: DELIVERY_INTRO_CAPTURE_PANE,
63003
63560
  replyCommand: EMPTY_REPLY_COMMAND,
63004
- replyRules: EMPTY_REPLY_RULES
63561
+ replyRules: EMPTY_REPLY_RULES,
63562
+ replyStyleHint: EMPTY_REPLY_STYLE_HINT
63005
63563
  };
63006
63564
  }
63007
63565
  const allowProgress = (params.streaming ?? "off") === "off";
@@ -63025,9 +63583,13 @@ function renderMessagePromptParts(params) {
63025
63583
  max_progress_messages: String(params.config.maxProgressMessages)
63026
63584
  }),
63027
63585
  final_rule_line: finalRuleLine
63028
- })
63586
+ }),
63587
+ replyStyleHint: buildReplyStyleHint(params.identity)
63029
63588
  };
63030
63589
  }
63590
+ function buildReplyStyleHint(identity) {
63591
+ return identity.platform === "slack" ? SLACK_REPLY_STYLE_HINT : TELEGRAM_REPLY_STYLE_HINT;
63592
+ }
63031
63593
  function renderProtectedControlSuffix(rule) {
63032
63594
  if (!rule) {
63033
63595
  return "";
@@ -64027,6 +64589,17 @@ function extractScrolledAppend(previous, current) {
64027
64589
  }
64028
64590
  return "";
64029
64591
  }
64592
+ function deriveRunningInteractionText(previousSnapshot, currentSnapshot) {
64593
+ const previous = cleanRunningInteractionSnapshot(previousSnapshot);
64594
+ const current = cleanRunningInteractionSnapshot(currentSnapshot);
64595
+ if (!current || current === previous) {
64596
+ return "";
64597
+ }
64598
+ if (!previous) {
64599
+ return current;
64600
+ }
64601
+ return extractScrolledAppend(previous, current);
64602
+ }
64030
64603
  function deriveRunningInteractionSnapshot(currentSnapshot) {
64031
64604
  return cleanRunningInteractionSnapshot(currentSnapshot);
64032
64605
  }
@@ -64382,6 +64955,8 @@ var PASTE_SETTLE_POLL_INTERVAL_MS = 40;
64382
64955
  var PASTE_SETTLE_QUIET_WINDOW_MS = 60;
64383
64956
  var PASTE_SETTLE_MULTILINE_MAX_WAIT_MS = 800;
64384
64957
  var PASTE_SETTLE_SINGLE_LINE_MAX_WAIT_MS = 80;
64958
+ var PASTE_CAPTURE_REVALIDATE_POLL_INTERVAL_MS = 40;
64959
+ var PASTE_CAPTURE_REVALIDATE_MAX_WAIT_MS = 160;
64385
64960
  var SUBMIT_CONFIRM_POLL_INTERVAL_MS = 40;
64386
64961
  var SUBMIT_CONFIRM_MAX_WAIT_MS = 160;
64387
64962
  var TMUX_MISSING_TARGET_PATTERN = /(?:no current target|can't find pane|can't find window)/i;
@@ -64398,14 +64973,35 @@ class TmuxBootstrapSessionLostError extends Error {
64398
64973
  }
64399
64974
  async function submitTmuxSessionInput(params) {
64400
64975
  const prePasteState = await params.tmux.getPaneState(params.sessionName);
64976
+ const captureLines = estimatePasteCaptureLines(params.text);
64977
+ const prePasteSnapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, captureLines));
64401
64978
  await params.tmux.sendLiteral(params.sessionName, params.text);
64402
- const preSubmitState = await waitForPanePasteSettlement({
64979
+ const pasteSettlement = await waitForPanePasteSettlement({
64403
64980
  tmux: params.tmux,
64404
64981
  sessionName: params.sessionName,
64405
64982
  baseline: prePasteState,
64406
64983
  text: params.text,
64407
64984
  minDelayMs: params.promptSubmitDelayMs
64408
64985
  });
64986
+ let preSubmitState = pasteSettlement.state;
64987
+ if (!pasteSettlement.visible) {
64988
+ logLatencyDebug("tmux-paste-retry", params.timingContext, {
64989
+ sessionName: params.sessionName
64990
+ });
64991
+ const snapshotConfirmed = await waitForPanePasteSnapshotConfirmation({
64992
+ tmux: params.tmux,
64993
+ sessionName: params.sessionName,
64994
+ baselineSnapshot: prePasteSnapshot,
64995
+ captureLines
64996
+ });
64997
+ if (!snapshotConfirmed) {
64998
+ logLatencyDebug("tmux-paste-unconfirmed", params.timingContext, {
64999
+ sessionName: params.sessionName
65000
+ });
65001
+ throw new Error("tmux paste was not confirmed before Enter. The pane state did not change, so clisbot did not treat the prompt as visibly delivered.");
65002
+ }
65003
+ preSubmitState = await params.tmux.getPaneState(params.sessionName);
65004
+ }
64409
65005
  await params.tmux.sendKey(params.sessionName, "Enter");
64410
65006
  if (await waitForPaneSubmitConfirmation({
64411
65007
  tmux: params.tmux,
@@ -64609,11 +65205,17 @@ async function waitForPanePasteSettlement(params) {
64609
65205
  const deadline = Date.now() + (shouldWaitForVisiblePaste(params.text) ? PASTE_SETTLE_MULTILINE_MAX_WAIT_MS : PASTE_SETTLE_SINGLE_LINE_MAX_WAIT_MS);
64610
65206
  while (true) {
64611
65207
  if (sawChange && Date.now() - lastChangeAt >= PASTE_SETTLE_QUIET_WINDOW_MS) {
64612
- return currentState;
65208
+ return {
65209
+ visible: true,
65210
+ state: currentState
65211
+ };
64613
65212
  }
64614
65213
  const remainingMs = deadline - Date.now();
64615
65214
  if (remainingMs <= 0) {
64616
- return currentState;
65215
+ return {
65216
+ visible: sawChange,
65217
+ state: currentState
65218
+ };
64617
65219
  }
64618
65220
  await sleep(Math.min(PASTE_SETTLE_POLL_INTERVAL_MS, remainingMs));
64619
65221
  const nextState = await params.tmux.getPaneState(params.sessionName);
@@ -64626,6 +65228,24 @@ async function waitForPanePasteSettlement(params) {
64626
65228
  }
64627
65229
  }
64628
65230
  }
65231
+ async function waitForPanePasteSnapshotConfirmation(params) {
65232
+ const deadline = Date.now() + PASTE_CAPTURE_REVALIDATE_MAX_WAIT_MS;
65233
+ while (true) {
65234
+ const snapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, params.captureLines));
65235
+ if (snapshot !== params.baselineSnapshot) {
65236
+ return true;
65237
+ }
65238
+ const remainingMs = deadline - Date.now();
65239
+ if (remainingMs <= 0) {
65240
+ return false;
65241
+ }
65242
+ await sleep(Math.min(PASTE_CAPTURE_REVALIDATE_POLL_INTERVAL_MS, remainingMs));
65243
+ }
65244
+ }
65245
+ function estimatePasteCaptureLines(text) {
65246
+ return Math.max(40, Math.min(160, text.split(`
65247
+ `).length + 24));
65248
+ }
64629
65249
  function hasPaneStateChanged(left, right) {
64630
65250
  return left.cursorX !== right.cursorX || left.cursorY !== right.cursorY || left.historySize !== right.historySize;
64631
65251
  }
@@ -64914,6 +65534,12 @@ class RunnerService {
64914
65534
  nextAllowFreshRetry: false
64915
65535
  });
64916
65536
  }
65537
+ async retryAfterStartupTimeout(target, resolved, allowFreshRetry) {
65538
+ return this.retryFreshStartWithClearedSessionId(target, resolved, {
65539
+ allowRetry: allowFreshRetry,
65540
+ nextAllowFreshRetry: false
65541
+ });
65542
+ }
64917
65543
  async abortUnreadySession(resolved, reason, snapshot) {
64918
65544
  await this.tmux.killSession(resolved.sessionName);
64919
65545
  throw new Error(`${reason}${summarizeSnapshot(snapshot)}`);
@@ -65058,6 +65684,10 @@ class RunnerService {
65058
65684
  await this.abortUnreadySession(resolved, bootstrapResult.message, bootstrapResult.snapshot);
65059
65685
  }
65060
65686
  if (bootstrapResult.status === "timeout" && resolved.runner.startupReadyPattern) {
65687
+ const retried = await this.retryAfterStartupTimeout(target, resolved, options.allowFreshRetry);
65688
+ if (retried) {
65689
+ return retried;
65690
+ }
65061
65691
  await this.abortUnreadySession(resolved, `Runner session "${resolved.sessionName}" did not reach the configured ready state within ${resolved.runner.startupDelayMs}ms.`, bootstrapResult.snapshot);
65062
65692
  }
65063
65693
  await this.finalizeSessionStartup(resolved, {
@@ -65322,7 +65952,7 @@ async function monitorTmuxRun(params) {
65322
65952
  await sleep(sawActivity ? params.updateIntervalMs : Math.min(params.updateIntervalMs, FIRST_OUTPUT_POLL_INTERVAL_MS));
65323
65953
  const snapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, params.captureLines));
65324
65954
  const now = Date.now();
65325
- const runningSnapshot = deriveRunningInteractionSnapshot(snapshot);
65955
+ const runningSnapshot = params.initialSnapshot ? deriveRunningInteractionText(params.initialSnapshot, snapshot) : deriveRunningInteractionSnapshot(snapshot);
65326
65956
  previousSnapshot = snapshot;
65327
65957
  if (runningSnapshot && runningSnapshot !== previousRunningSnapshot) {
65328
65958
  previousRunningSnapshot = runningSnapshot;
@@ -66057,6 +66687,15 @@ class AgentService {
66057
66687
  async recordConversationReply(target, kind = "reply", source = "channel") {
66058
66688
  return this.sessionState.recordConversationReply(this.resolveTarget(target), kind, source);
66059
66689
  }
66690
+ async appendRecentConversationMessage(target, message) {
66691
+ return this.sessionState.appendRecentConversationMessage(this.resolveTarget(target), message);
66692
+ }
66693
+ async getRecentConversationReplayMessages(target, params = {}) {
66694
+ return this.sessionState.getRecentConversationReplayMessages(target, params);
66695
+ }
66696
+ async markRecentConversationProcessed(target, marker) {
66697
+ return this.sessionState.markRecentConversationProcessed(this.resolveTarget(target), marker);
66698
+ }
66060
66699
  async runShellCommand(target, command) {
66061
66700
  return this.queue.enqueue(`${target.sessionKey}:bash`, async () => this.runnerSessions.runShellCommand(target, command)).result;
66062
66701
  }
@@ -66566,421 +67205,6 @@ class AgentService {
66566
67205
  }
66567
67206
  }
66568
67207
 
66569
- // src/agents/commands.ts
66570
- function parseAgentCommand(text, options = {}) {
66571
- const normalized = text.trim();
66572
- const commandPrefixes = options.commandPrefixes ?? {
66573
- slash: ["::", "\\"],
66574
- bash: ["!"]
66575
- };
66576
- const bashPrefix = findMatchingPrefix(normalized, commandPrefixes.bash);
66577
- if (bashPrefix) {
66578
- const command2 = normalized.slice(bashPrefix.length).trim();
66579
- return {
66580
- type: "bash",
66581
- command: command2,
66582
- source: "shortcut"
66583
- };
66584
- }
66585
- const slashPrefix = findMatchingPrefix(normalized, ["/", ...commandPrefixes.slash]);
66586
- if (!slashPrefix) {
66587
- return null;
66588
- }
66589
- const withoutSlash = normalized.slice(slashPrefix.length).trim();
66590
- if (!withoutSlash) {
66591
- return {
66592
- type: "control",
66593
- name: "help"
66594
- };
66595
- }
66596
- const [command] = withoutSlash.split(/\s+/, 1);
66597
- const lowered = normalizeSlashCommandName(command, options.botUsername);
66598
- if (lowered === "start") {
66599
- return {
66600
- type: "control",
66601
- name: "start"
66602
- };
66603
- }
66604
- if (lowered === "status") {
66605
- return {
66606
- type: "control",
66607
- name: "status"
66608
- };
66609
- }
66610
- if (lowered === "help") {
66611
- return {
66612
- type: "control",
66613
- name: "help"
66614
- };
66615
- }
66616
- if (lowered === "whoami") {
66617
- return {
66618
- type: "control",
66619
- name: "whoami"
66620
- };
66621
- }
66622
- if (lowered === "transcript") {
66623
- return {
66624
- type: "control",
66625
- name: "transcript"
66626
- };
66627
- }
66628
- if (lowered === "attach") {
66629
- return {
66630
- type: "control",
66631
- name: "attach"
66632
- };
66633
- }
66634
- if (lowered === "detach") {
66635
- return {
66636
- type: "control",
66637
- name: "detach"
66638
- };
66639
- }
66640
- if (lowered === "watch") {
66641
- const parsed = parseWatchCommand(withoutSlash.slice(command.length).trim());
66642
- if (parsed) {
66643
- return {
66644
- type: "control",
66645
- name: "watch",
66646
- intervalMs: parsed.intervalMs,
66647
- durationMs: parsed.durationMs
66648
- };
66649
- }
66650
- return {
66651
- type: "control",
66652
- name: "help"
66653
- };
66654
- }
66655
- if (lowered === "stop") {
66656
- return {
66657
- type: "control",
66658
- name: "stop"
66659
- };
66660
- }
66661
- if (lowered === "nudge") {
66662
- return {
66663
- type: "control",
66664
- name: "nudge"
66665
- };
66666
- }
66667
- if (lowered === "followup") {
66668
- const action = withoutSlash.slice(command.length).trim().toLowerCase();
66669
- if (!action || action === "status") {
66670
- return {
66671
- type: "control",
66672
- name: "followup",
66673
- action: "status"
66674
- };
66675
- }
66676
- if (action === "auto") {
66677
- return {
66678
- type: "control",
66679
- name: "followup",
66680
- action: "auto",
66681
- mode: "auto"
66682
- };
66683
- }
66684
- if (action === "mention-only") {
66685
- return {
66686
- type: "control",
66687
- name: "followup",
66688
- action: "mention-only",
66689
- mode: "mention-only"
66690
- };
66691
- }
66692
- if (action === "pause") {
66693
- return {
66694
- type: "control",
66695
- name: "followup",
66696
- action: "pause",
66697
- mode: "paused"
66698
- };
66699
- }
66700
- if (action === "resume") {
66701
- return {
66702
- type: "control",
66703
- name: "followup",
66704
- action: "resume"
66705
- };
66706
- }
66707
- return {
66708
- type: "control",
66709
- name: "followup",
66710
- action: "status"
66711
- };
66712
- }
66713
- if (lowered === "responsemode") {
66714
- const action = withoutSlash.slice(command.length).trim().toLowerCase();
66715
- if (!action || action === "status") {
66716
- return {
66717
- type: "control",
66718
- name: "responsemode",
66719
- action: "status"
66720
- };
66721
- }
66722
- if (action === "capture-pane" || action === "message-tool") {
66723
- return {
66724
- type: "control",
66725
- name: "responsemode",
66726
- action,
66727
- responseMode: action
66728
- };
66729
- }
66730
- return {
66731
- type: "control",
66732
- name: "responsemode",
66733
- action: "status"
66734
- };
66735
- }
66736
- if (lowered === "streaming") {
66737
- const action = withoutSlash.slice(command.length).trim().toLowerCase();
66738
- if (!action || action === "status") {
66739
- return {
66740
- type: "control",
66741
- name: "streaming",
66742
- action: "status"
66743
- };
66744
- }
66745
- if (action === "on") {
66746
- return {
66747
- type: "control",
66748
- name: "streaming",
66749
- action: "on",
66750
- streaming: "all"
66751
- };
66752
- }
66753
- if (action === "off" || action === "latest" || action === "all") {
66754
- return {
66755
- type: "control",
66756
- name: "streaming",
66757
- action,
66758
- streaming: action
66759
- };
66760
- }
66761
- return {
66762
- type: "control",
66763
- name: "streaming",
66764
- action: "status"
66765
- };
66766
- }
66767
- if (lowered === "additionalmessagemode") {
66768
- const action = withoutSlash.slice(command.length).trim().toLowerCase();
66769
- if (!action || action === "status") {
66770
- return {
66771
- type: "control",
66772
- name: "additionalmessagemode",
66773
- action: "status"
66774
- };
66775
- }
66776
- if (action === "queue" || action === "steer") {
66777
- return {
66778
- type: "control",
66779
- name: "additionalmessagemode",
66780
- action,
66781
- additionalMessageMode: action
66782
- };
66783
- }
66784
- return {
66785
- type: "control",
66786
- name: "additionalmessagemode",
66787
- action: "status"
66788
- };
66789
- }
66790
- if (lowered === "bash") {
66791
- return {
66792
- type: "bash",
66793
- command: withoutSlash.slice(command.length).trim(),
66794
- source: "slash"
66795
- };
66796
- }
66797
- if (lowered === "loop") {
66798
- const loopText = withoutSlash.slice(command.length).trim();
66799
- const loweredLoopText = loopText.toLowerCase();
66800
- if (!loweredLoopText || loweredLoopText === "help") {
66801
- return {
66802
- type: "control",
66803
- name: "loop-help"
66804
- };
66805
- }
66806
- if (loweredLoopText === "status") {
66807
- return {
66808
- type: "loop-control",
66809
- action: "status"
66810
- };
66811
- }
66812
- if (loweredLoopText === "cancel" || loweredLoopText.startsWith("cancel ")) {
66813
- const cancelArgs = loopText.slice("cancel".length).trim();
66814
- if (hasLoopFlag(cancelArgs, LOOP_FORCE_FLAG)) {
66815
- return {
66816
- type: "loop-error",
66817
- message: `Use \`/loop cancel --all ${LOOP_APP_FLAG}\` for app-wide cancellation.`
66818
- };
66819
- }
66820
- const all = hasLoopFlag(cancelArgs, LOOP_ALL_FLAG);
66821
- const app = hasLoopFlag(cancelArgs, LOOP_APP_FLAG);
66822
- if (app && !all) {
66823
- return {
66824
- type: "loop-error",
66825
- message: `\`${LOOP_APP_FLAG}\` only works with \`/loop cancel ${LOOP_ALL_FLAG}\`.`
66826
- };
66827
- }
66828
- const loopId = cancelArgs.split(/\s+/).map((token) => token.trim()).find((token) => token && token !== LOOP_ALL_FLAG && token !== LOOP_APP_FLAG);
66829
- return {
66830
- type: "loop-control",
66831
- action: "cancel",
66832
- loopId: loopId || undefined,
66833
- all,
66834
- app
66835
- };
66836
- }
66837
- const parsed = parseLoopSlashCommand(loopText);
66838
- if ("error" in parsed) {
66839
- return {
66840
- type: "loop-error",
66841
- message: parsed.error
66842
- };
66843
- }
66844
- return {
66845
- type: "loop",
66846
- params: parsed
66847
- };
66848
- }
66849
- if (lowered === "queue" || lowered === "q") {
66850
- const queueText = withoutSlash.slice(command.length).trim();
66851
- const normalizedQueueText = queueText.toLowerCase();
66852
- if (lowered === "queue") {
66853
- if (normalizedQueueText === "help") {
66854
- return {
66855
- type: "control",
66856
- name: "queue-help"
66857
- };
66858
- }
66859
- if (normalizedQueueText === "list") {
66860
- return {
66861
- type: "control",
66862
- name: "queue-list"
66863
- };
66864
- }
66865
- if (normalizedQueueText === "clear") {
66866
- return {
66867
- type: "control",
66868
- name: "queue-clear"
66869
- };
66870
- }
66871
- }
66872
- return {
66873
- type: "queue",
66874
- text: queueText
66875
- };
66876
- }
66877
- if (lowered === "queue-list" || lowered === "queuelist") {
66878
- return {
66879
- type: "control",
66880
- name: "queue-list"
66881
- };
66882
- }
66883
- if (lowered === "queue-clear" || lowered === "queueclear") {
66884
- return {
66885
- type: "control",
66886
- name: "queue-clear"
66887
- };
66888
- }
66889
- if (lowered === "steer" || lowered === "s") {
66890
- return {
66891
- type: "steer",
66892
- text: withoutSlash.slice(command.length).trim()
66893
- };
66894
- }
66895
- return {
66896
- type: "native",
66897
- text: normalized
66898
- };
66899
- }
66900
- function findMatchingPrefix(text, prefixes) {
66901
- return [...prefixes].sort((left, right) => right.length - left.length).find((prefix) => prefix.length > 0 && text.startsWith(prefix));
66902
- }
66903
- function normalizeSlashCommandName(command, botUsername) {
66904
- const lowered = command?.toLowerCase() ?? "";
66905
- const normalizedBotUsername = (botUsername ?? "").trim().toLowerCase().replace(/^@/, "");
66906
- if (!normalizedBotUsername) {
66907
- return lowered;
66908
- }
66909
- const suffix = `@${normalizedBotUsername}`;
66910
- if (!lowered.endsWith(suffix)) {
66911
- return lowered;
66912
- }
66913
- return lowered.slice(0, lowered.length - suffix.length);
66914
- }
66915
- function renderAgentControlSlashHelp() {
66916
- return [
66917
- "Slash commands",
66918
- "",
66919
- "- `/start`: show onboarding help for the current surface",
66920
- "- `/status`: show the current route status and operator setup commands",
66921
- "- `/help`: show available control slash commands",
66922
- "- `/whoami`: show the current platform, route, and sender identity details",
66923
- "- `/transcript`: show the current conversation session transcript when the route verbose policy allows it",
66924
- "- `/attach`: attach this thread to the active run and resume live updates when it is still processing",
66925
- "- `/detach`: stop live updates for this thread, switch to sparse progress updates, and still allow final settlement here",
66926
- "- `/watch every 30s [for 10m]`: post the latest state on an interval until the run settles or the watch window ends",
66927
- "- `/stop`: send Escape to interrupt the current conversation session",
66928
- "- `/nudge`: send one extra Enter to the current tmux session without resending the prompt text",
66929
- "- `/followup status`: show the current conversation follow-up policy",
66930
- "- `/followup auto`: allow natural follow-up after the bot has replied in-thread",
66931
- "- `/followup mention-only`: require explicit mention for each later turn",
66932
- "- `/followup pause`: stop passive follow-up until the next explicit mention",
66933
- "- `/followup resume`: clear the runtime override and restore config defaults",
66934
- "- `/streaming status|on|off|latest|all`: show or change streaming mode for this surface",
66935
- "- `/responsemode status`: show the configured response mode for this surface",
66936
- "- `/responsemode capture-pane`: settle replies from captured pane output for this surface",
66937
- "- `/responsemode message-tool`: expect the agent to reply through `clisbot message send` for this surface",
66938
- "- `/additionalmessagemode status`: show how extra messages behave while a run is already active",
66939
- "- `/additionalmessagemode steer`: send later user messages straight into the active session",
66940
- "- `/additionalmessagemode queue`: queue later user messages behind the active run for this surface",
66941
- "- `/queue <message>` or `\\q <message>`: enqueue a later message behind the active run and let clisbot deliver it in order",
66942
- "- `/queue help`: show queue-specific help and examples",
66943
- "- `/steer <message>` or `\\s <message>`: inject a steering message into the active run immediately",
66944
- "- `/queue list`: show queued messages that have not started yet",
66945
- "- `/queue clear`: clear queued messages that have not started yet",
66946
- "- `/loop help`: show loop-specific help and syntax examples",
66947
- ...renderLoopHelpLines(),
66948
- "- `/bash` followed by a shell command: requires `shellExecute` on the resolved agent role",
66949
- "- shortcut prefixes such as `!` run bash only when the resolved agent role allows `shellExecute`",
66950
- "",
66951
- "Other slash commands are forwarded to the agent unchanged."
66952
- ].join(`
66953
- `);
66954
- }
66955
- function renderQueueHelpLines() {
66956
- return [
66957
- "- `/queue <message>` or `\\q <message>`: enqueue one later message behind the active run",
66958
- "- `/queue list`: show queued messages that have not started yet",
66959
- "- `/queue clear`: clear queued messages that have not started yet",
66960
- "- `/queue help`: show this queue help again",
66961
- "- `/steer <message>` or `\\s <message>`: inject an immediate steering message instead of queueing"
66962
- ];
66963
- }
66964
- function parseWatchCommand(raw) {
66965
- const match = raw.match(/^every\s+(\S+)(?:\s+for\s+(\S+))?$/i);
66966
- if (!match) {
66967
- return null;
66968
- }
66969
- const intervalMs = parseCommandDurationMs(match[1] ?? "");
66970
- if (!intervalMs) {
66971
- return null;
66972
- }
66973
- const durationToken = match[2];
66974
- const parsedDurationMs = durationToken ? parseCommandDurationMs(durationToken) : null;
66975
- if (durationToken && !parsedDurationMs) {
66976
- return null;
66977
- }
66978
- return {
66979
- intervalMs,
66980
- durationMs: parsedDurationMs ?? undefined
66981
- };
66982
- }
66983
-
66984
67208
  // src/channels/rendering.ts
66985
67209
  function buildRenderedMessageState(params) {
66986
67210
  const body = params.snapshot.trim() || (params.status === "completed" || params.status === "timeout" || params.status === "detached" ? params.previousState?.body ?? "" : "");
@@ -67706,6 +67930,9 @@ async function executePromptDelivery(params) {
67706
67930
  }
67707
67931
  });
67708
67932
  queueStartPending = positionAhead > 0 && (params.queueStartMode ?? "none") !== "none";
67933
+ if (params.onPromptAccepted) {
67934
+ await params.onPromptAccepted();
67935
+ }
67709
67936
  if (previewEnabled) {
67710
67937
  const placeholderText = buildInitialPlaceholderText(positionAhead);
67711
67938
  const postedNew2 = await renderResponseText(placeholderText);
@@ -68375,9 +68602,12 @@ ${escapeCodeFence(shellResult.output)}
68375
68602
  return interactionResult;
68376
68603
  }
68377
68604
  await params.agentService.submitSessionInput(params.sessionTarget, buildSteeringPromptText({
68378
- text: explicitSteerMessage,
68605
+ text: params.transformSessionInputText?.(explicitSteerMessage) ?? explicitSteerMessage,
68379
68606
  protectedControlMutationRule: params.protectedControlMutationRule
68380
68607
  }));
68608
+ if (params.onPromptAccepted) {
68609
+ await params.onPromptAccepted();
68610
+ }
68381
68611
  await params.postText("Steered.");
68382
68612
  await params.agentService.recordConversationReply(params.sessionTarget);
68383
68613
  return {
@@ -68387,9 +68617,12 @@ ${escapeCodeFence(shellResult.output)}
68387
68617
  if (!forceQueuedDelivery && params.route.additionalMessageMode === "steer") {
68388
68618
  if (sessionBusy && canSteerActiveRun) {
68389
68619
  await params.agentService.submitSessionInput(params.sessionTarget, buildSteeringPromptText({
68390
- text: params.text,
68620
+ text: params.transformSessionInputText?.(params.text) ?? params.text,
68391
68621
  protectedControlMutationRule: params.protectedControlMutationRule
68392
68622
  }));
68623
+ if (params.onPromptAccepted) {
68624
+ await params.onPromptAccepted();
68625
+ }
68393
68626
  return {
68394
68627
  processingIndicatorLifecycle: "active-run"
68395
68628
  };
@@ -68408,7 +68641,8 @@ ${escapeCodeFence(shellResult.output)}
68408
68641
  reconcileText: params.reconcileText,
68409
68642
  observerId,
68410
68643
  timingContext: params.timingContext,
68411
- forceQueuedDelivery
68644
+ forceQueuedDelivery,
68645
+ onPromptAccepted: params.onPromptAccepted
68412
68646
  });
68413
68647
  return interactionResult;
68414
68648
  }
@@ -68463,6 +68697,16 @@ function isTelegramSenderAllowed(params) {
68463
68697
  return false;
68464
68698
  }
68465
68699
 
68700
+ // src/channels/mention-follow-up.ts
68701
+ function buildMentionOnlyFollowUpPrompt(params) {
68702
+ const scope = params.threaded ? "this thread" : params.conversationKind === "dm" ? "this conversation" : "the recent conversation here";
68703
+ return [
68704
+ "The user explicitly mentioned you without any additional text.",
68705
+ `Review the recent context in ${scope} and respond to the latest unresolved request.`,
68706
+ "If the next step is still unclear, ask one short clarifying question."
68707
+ ].join(" ");
68708
+ }
68709
+
68466
68710
  // src/auth/resolve.ts
68467
68711
  function mergeRoleDefinitions(inherited, override) {
68468
68712
  return {
@@ -69219,13 +69463,21 @@ function canUseImplicitSlackFollowUp(params) {
69219
69463
  return params.conversationKind !== "dm" && typeof params.event.thread_ts === "string" && params.event.thread_ts.length > 0;
69220
69464
  }
69221
69465
  function hasBotMention(text, botUserId) {
69222
- if (!text) {
69466
+ const mentions = extractSlackMentionedUserIds(text);
69467
+ if (botUserId) {
69468
+ return mentions.includes(botUserId);
69469
+ }
69470
+ return mentions.length > 0;
69471
+ }
69472
+ function hasForeignSlackUserMention(text, botUserId) {
69473
+ const mentions = extractSlackMentionedUserIds(text);
69474
+ if (mentions.length === 0) {
69223
69475
  return false;
69224
69476
  }
69225
- if (botUserId) {
69226
- return text.includes(`<@${botUserId}>`);
69477
+ if (!botUserId) {
69478
+ return true;
69227
69479
  }
69228
- return /<@[^>]+>/.test(text);
69480
+ return !mentions.includes(botUserId);
69229
69481
  }
69230
69482
  function stripBotMention(text, botUserId) {
69231
69483
  if (!botUserId) {
@@ -69241,6 +69493,12 @@ function resolveSlackDirectReplyThreadTs(params) {
69241
69493
  const messageTs = (params.messageTs ?? "").trim();
69242
69494
  return messageTs || undefined;
69243
69495
  }
69496
+ function extractSlackMentionedUserIds(text) {
69497
+ if (!text) {
69498
+ return [];
69499
+ }
69500
+ return [...text.matchAll(/<@([A-Z0-9_]+)(?:\|[^>]+)?>/gi)].map((match) => match[1]?.trim()).filter((value) => Boolean(value));
69501
+ }
69244
69502
 
69245
69503
  // src/channels/slack/reactions.ts
69246
69504
  function normalizeSlackReactionName(value) {
@@ -69343,7 +69601,7 @@ function isSlackCommandLikeMessage(params) {
69343
69601
  }) !== null;
69344
69602
  }
69345
69603
  function renderSlackRouteChoiceMessage(params) {
69346
- const botHandle = params.botLabel?.trim() ? `@${params.botLabel.trim()}` : "@<botname>";
69604
+ const botReference = params.botLabel?.trim() ? `mention this bot (${params.botLabel.trim()})` : "mention this bot";
69347
69605
  return [
69348
69606
  "clisbot: this Slack channel is not configured yet.",
69349
69607
  "",
@@ -69351,19 +69609,34 @@ function renderSlackRouteChoiceMessage(params) {
69351
69609
  `- \`clisbot channels add slack-channel ${params.channelId}\``,
69352
69610
  `- \`clisbot channels add slack-channel ${params.channelId} --agent <id>\``,
69353
69611
  "",
69354
- `After that, mention \`${botHandle} \\start\` or \`${botHandle} \\status\` here.`
69612
+ `After that, ${botReference} and send \`\\start\` or \`\\status\` here.`
69355
69613
  ].join(`
69356
69614
  `);
69357
69615
  }
69358
69616
  function renderSlackMentionRequiredMessage(botLabel) {
69359
- const botHandle = botLabel?.trim() ? `@${botLabel.trim()}` : "@<botname>";
69617
+ const botReference = botLabel?.trim() ? `mention this bot (${botLabel.trim()})` : "mention this bot";
69360
69618
  return [
69361
69619
  "clisbot: this Slack channel requires a bot mention for new commands.",
69362
- `Try \`${botHandle} \\start\` or \`${botHandle} \\status\` here.`,
69620
+ `Try ${botReference} and send \`\\start\` or \`\\status\` here.`,
69363
69621
  "After the bot replies in a thread, normal follow-up messages there can continue according to the follow-up policy."
69364
69622
  ].join(`
69365
69623
  `);
69366
69624
  }
69625
+ function shouldSendSlackMentionRequiredGuidance(params) {
69626
+ return params.conversationKind === "dm" && params.isCommandLike;
69627
+ }
69628
+ function shouldGuideUnroutedSlackEvent(params) {
69629
+ if (params.isBotOriginated) {
69630
+ return false;
69631
+ }
69632
+ if (!params.isCommandLike) {
69633
+ return false;
69634
+ }
69635
+ if (params.conversationKind === "dm") {
69636
+ return true;
69637
+ }
69638
+ return params.wasMentioned;
69639
+ }
69367
69640
  async function sendSlackGuidanceOnce(params) {
69368
69641
  if (!params.eventId) {
69369
69642
  await params.send();
@@ -70100,6 +70373,14 @@ class SlackSocketService {
70100
70373
  conversationKind: params.conversationKind,
70101
70374
  replyToMode: params.route.replyToMode
70102
70375
  });
70376
+ if (hasForeignSlackUserMention(event.text ?? "", this.botUserId)) {
70377
+ debugSlackEvent("drop-foreign-mention", {
70378
+ eventId,
70379
+ channelId
70380
+ });
70381
+ await this.processedEventsStore.markCompleted(eventId);
70382
+ return;
70383
+ }
70103
70384
  const explicitMention = params.wasMentioned || hasBotMention(event.text ?? "", this.botUserId);
70104
70385
  const followUpState = await this.agentService.getConversationFollowUpState(sessionTarget);
70105
70386
  const effectiveFollowUpMode = resolveFollowUpMode({
@@ -70115,12 +70396,27 @@ class SlackSocketService {
70115
70396
  lastBotReplyAt: followUpState.lastBotReplyAt,
70116
70397
  directReplyToBot: isImplicitBotThreadReply(event, this.botUserId)
70117
70398
  });
70399
+ const rawText = explicitMention ? stripBotMention(event.text ?? "", this.botUserId) : `${event.text ?? ""}`.trim();
70400
+ const recentMessageMarker = messageTs?.trim();
70401
+ if (recentMessageMarker && (rawText || explicitMention)) {
70402
+ await this.agentService.appendRecentConversationMessage(sessionTarget, {
70403
+ marker: recentMessageMarker,
70404
+ text: parseAgentCommand(rawText, {
70405
+ commandPrefixes: params.route.commandPrefixes
70406
+ }) ? "" : rawText,
70407
+ senderId: typeof event.user === "string" ? event.user.trim().toUpperCase() : undefined
70408
+ });
70409
+ }
70118
70410
  if (requiresMention && !wasMentioned) {
70119
- if (isSlackCommandLikeMessage({
70411
+ const isCommandLike = isSlackCommandLikeMessage({
70120
70412
  text: event.text ?? "",
70121
70413
  botUserId: this.botUserId,
70122
70414
  botUsername: this.botLabel,
70123
70415
  commandPrefixes: params.route.commandPrefixes
70416
+ });
70417
+ if (shouldSendSlackMentionRequiredGuidance({
70418
+ conversationKind: params.conversationKind,
70419
+ isCommandLike
70124
70420
  })) {
70125
70421
  try {
70126
70422
  await postSlackText(this.app.client, {
@@ -70137,6 +70433,7 @@ class SlackSocketService {
70137
70433
  channelId,
70138
70434
  requiresMention,
70139
70435
  explicitMention,
70436
+ isCommandLike,
70140
70437
  effectiveFollowUpMode
70141
70438
  });
70142
70439
  await this.processedEventsStore.markCompleted(eventId);
@@ -70145,7 +70442,10 @@ class SlackSocketService {
70145
70442
  if (explicitMention && followUpState.overrideMode === "paused") {
70146
70443
  await this.agentService.reactivateConversationFollowUp(sessionTarget);
70147
70444
  }
70148
- const rawText = explicitMention ? stripBotMention(event.text ?? "", this.botUserId) : `${event.text ?? ""}`.trim();
70445
+ const effectivePromptText = rawText || (explicitMention ? buildMentionOnlyFollowUpPrompt({
70446
+ conversationKind: params.conversationKind,
70447
+ threaded: Boolean(threadTs)
70448
+ }) : "");
70149
70449
  const attachmentPaths = await resolveSlackAttachmentPaths({
70150
70450
  client: this.app.client,
70151
70451
  event,
@@ -70157,12 +70457,19 @@ class SlackSocketService {
70157
70457
  sessionKey: sessionTarget.sessionKey,
70158
70458
  messageId: messageTs ?? threadTs ?? `${Date.now()}`
70159
70459
  });
70160
- const text = prependAttachmentMentions(rawText, attachmentPaths);
70460
+ const text = prependAttachmentMentions(effectivePromptText, attachmentPaths);
70161
70461
  if (!text) {
70162
70462
  debugSlackEvent("drop-empty-text", { eventId, channelId });
70163
70463
  await this.processedEventsStore.markCompleted(eventId);
70164
70464
  return;
70165
70465
  }
70466
+ const recentConversationReplay = recentMessageMarker ? await this.agentService.getRecentConversationReplayMessages(sessionTarget, {
70467
+ excludeMarker: recentMessageMarker
70468
+ }) : [];
70469
+ const enrichPromptText = (nextText) => prependRecentConversationContext({
70470
+ currentText: nextText,
70471
+ recentMessages: recentConversationReplay
70472
+ });
70166
70473
  debugSlackEvent("process-message", {
70167
70474
  eventId,
70168
70475
  channelId,
@@ -70197,7 +70504,7 @@ class SlackSocketService {
70197
70504
  });
70198
70505
  const protectedControlMutationRule = auth.mayManageProtectedResources ? undefined : DEFAULT_PROTECTED_CONTROL_RULE;
70199
70506
  const agentPromptText = buildAgentPromptText({
70200
- text,
70507
+ text: enrichPromptText(text),
70201
70508
  identity,
70202
70509
  config: this.loadedConfig.raw.channels.slack.agentPrompt,
70203
70510
  cliTool,
@@ -70250,7 +70557,7 @@ class SlackSocketService {
70250
70557
  text,
70251
70558
  agentPromptText,
70252
70559
  agentPromptBuilder: (nextText) => buildAgentPromptText({
70253
- text: nextText,
70560
+ text: enrichPromptText(nextText),
70254
70561
  identity,
70255
70562
  config: this.loadedConfig.raw.channels.slack.agentPrompt,
70256
70563
  cliTool,
@@ -70259,6 +70566,10 @@ class SlackSocketService {
70259
70566
  protectedControlMutationRule
70260
70567
  }),
70261
70568
  protectedControlMutationRule,
70569
+ transformSessionInputText: enrichPromptText,
70570
+ onPromptAccepted: recentMessageMarker ? async () => {
70571
+ await this.agentService.markRecentConversationProcessed(sessionTarget, recentMessageMarker);
70572
+ } : undefined,
70262
70573
  route: params.route,
70263
70574
  maxChars: this.getSlackMaxChars(params.route.agentId),
70264
70575
  timingContext,
@@ -70323,6 +70634,13 @@ class SlackSocketService {
70323
70634
  const resolvedRoute = resolveSlackConversationRoute(this.loadedConfig, normalizedEvent, { accountId: this.accountId });
70324
70635
  const route = resolvedRoute.route;
70325
70636
  if (!route) {
70637
+ if (isBotOriginatedSlackEvent(normalizedEvent) && !this.loadedConfig.raw.channels.slack.allowBots) {
70638
+ debugSlackEvent("drop-unrouted-bot-mention", {
70639
+ eventId: body.event_id,
70640
+ allowBots: this.loadedConfig.raw.channels.slack.allowBots
70641
+ });
70642
+ return;
70643
+ }
70326
70644
  try {
70327
70645
  await this.maybeGuideUnroutedSlackEvent({
70328
70646
  eventId: body.event_id,
@@ -70358,11 +70676,16 @@ class SlackSocketService {
70358
70676
  const resolvedRoute = resolveSlackConversationRoute(this.loadedConfig, normalizedEvent, { accountId: this.accountId });
70359
70677
  const route = resolvedRoute.route;
70360
70678
  if (!route) {
70361
- const shouldGuide = isSlackCommandLikeMessage({
70362
- text: normalizedEvent.text ?? "",
70363
- botUserId: this.botUserId,
70364
- botUsername: this.botLabel,
70365
- commandPrefixes: this.loadedConfig.raw.channels.slack.commandPrefixes
70679
+ const shouldGuide = shouldGuideUnroutedSlackEvent({
70680
+ conversationKind: resolvedRoute.conversationKind,
70681
+ isCommandLike: isSlackCommandLikeMessage({
70682
+ text: normalizedEvent.text ?? "",
70683
+ botUserId: this.botUserId,
70684
+ botUsername: this.botLabel,
70685
+ commandPrefixes: this.loadedConfig.raw.channels.slack.commandPrefixes
70686
+ }),
70687
+ wasMentioned: hasBotMention(normalizedEvent.text ?? "", this.botUserId),
70688
+ isBotOriginated: isBotOriginatedSlackEvent(normalizedEvent) && !this.loadedConfig.raw.channels.slack.allowBots
70366
70689
  });
70367
70690
  if (shouldGuide) {
70368
70691
  try {
@@ -70433,6 +70756,399 @@ class SlackSocketService {
70433
70756
  var import_bolt = __toESM(require_dist7(), 1);
70434
70757
  import { basename } from "node:path";
70435
70758
  import { readFile as readFile2 } from "node:fs/promises";
70759
+
70760
+ // src/channels/slack/content.ts
70761
+ var SLACK_MAX_BLOCKS = 50;
70762
+ function normalizeMarkdownLinks(text) {
70763
+ return text.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_match, label, href) => {
70764
+ const trimmedHref = href.trim();
70765
+ if (!trimmedHref) {
70766
+ return label;
70767
+ }
70768
+ return `<${trimmedHref}|${label}>`;
70769
+ });
70770
+ }
70771
+ function renderInlineMarkdownToSlackMrkdwn(text) {
70772
+ return normalizeMarkdownLinks(text).replace(/~~([^~]+)~~/g, "~$1~").replace(/\*\*([^*\n][\s\S]*?[^*\n])\*\*/g, "*$1*");
70773
+ }
70774
+ function stripMarkdownInline(text) {
70775
+ return text.replace(/`([^`\n]+)`/g, "$1").replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, "$1").replace(/\*\*([^*\n][\s\S]*?[^*\n])\*\*/g, "$1").replace(/\*([^*\n][\s\S]*?[^*\n])\*/g, "$1").replace(/~~([^~]+)~~/g, "$1").trim();
70776
+ }
70777
+ function normalizeSlackHeaderText(text) {
70778
+ const normalized = stripMarkdownInline(text);
70779
+ if (!normalized) {
70780
+ return "Untitled";
70781
+ }
70782
+ return normalized.slice(0, 150);
70783
+ }
70784
+ function buildSlackBlocksFallbackText(blocks) {
70785
+ for (const block of blocks) {
70786
+ if (typeof block !== "object" || !block) {
70787
+ continue;
70788
+ }
70789
+ const text = block.text?.text;
70790
+ if (typeof text === "string" && text.trim()) {
70791
+ return stripMarkdownInline(text).replace(/\s+/g, " ").trim();
70792
+ }
70793
+ const elements = block.elements;
70794
+ if (!Array.isArray(elements)) {
70795
+ continue;
70796
+ }
70797
+ for (const element of elements) {
70798
+ if (typeof element?.text === "string" && element.text.trim()) {
70799
+ return stripMarkdownInline(element.text).replace(/\s+/g, " ").trim();
70800
+ }
70801
+ }
70802
+ const rows = block.rows;
70803
+ if (!Array.isArray(rows) || rows.length === 0) {
70804
+ continue;
70805
+ }
70806
+ const firstRow = rows[0];
70807
+ if (!Array.isArray(firstRow)) {
70808
+ continue;
70809
+ }
70810
+ const rowText = firstRow.map((cell) => {
70811
+ if (!cell || typeof cell !== "object") {
70812
+ return "";
70813
+ }
70814
+ const rawText = cell.text;
70815
+ return typeof rawText === "string" ? stripMarkdownInline(rawText) : "";
70816
+ }).filter(Boolean).join(" | ").trim();
70817
+ if (rowText) {
70818
+ return rowText;
70819
+ }
70820
+ }
70821
+ return "Shared a Block Kit message";
70822
+ }
70823
+ function validateSlackBlocksArray(raw) {
70824
+ if (!Array.isArray(raw)) {
70825
+ throw new Error("Slack blocks input must be a JSON array");
70826
+ }
70827
+ if (raw.length === 0) {
70828
+ throw new Error("Slack blocks input cannot be empty");
70829
+ }
70830
+ if (raw.length > SLACK_MAX_BLOCKS) {
70831
+ throw new Error(`Slack blocks cannot exceed ${SLACK_MAX_BLOCKS} items`);
70832
+ }
70833
+ for (const block of raw) {
70834
+ if (!block || typeof block !== "object" || Array.isArray(block)) {
70835
+ throw new Error("Each Slack block must be an object");
70836
+ }
70837
+ const type = block.type;
70838
+ if (typeof type !== "string" || !type.trim()) {
70839
+ throw new Error("Each Slack block must include a non-empty string type");
70840
+ }
70841
+ }
70842
+ return raw;
70843
+ }
70844
+ function parseSlackBlocksInput(text) {
70845
+ let parsed;
70846
+ try {
70847
+ parsed = JSON.parse(text);
70848
+ } catch {
70849
+ throw new Error("Slack blocks input must be valid JSON");
70850
+ }
70851
+ return validateSlackBlocksArray(parsed);
70852
+ }
70853
+ function renderMarkdownToSlackMrkdwn(markdown) {
70854
+ const normalized = markdown.replaceAll(`\r
70855
+ `, `
70856
+ `).replaceAll("\r", `
70857
+ `).trim();
70858
+ if (!normalized) {
70859
+ return "";
70860
+ }
70861
+ return normalized.split(`
70862
+ `).map((line) => {
70863
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
70864
+ if (headingMatch) {
70865
+ const level = headingMatch[1]?.length ?? 1;
70866
+ const content = renderInlineMarkdownToSlackMrkdwn(headingMatch[2] ?? "");
70867
+ if (level <= 3) {
70868
+ return `*${content}*`;
70869
+ }
70870
+ return content;
70871
+ }
70872
+ const bulletMatch = line.match(/^\s*[-*+]\s+(.+)$/);
70873
+ if (bulletMatch) {
70874
+ return `• ${renderInlineMarkdownToSlackMrkdwn(bulletMatch[1] ?? "")}`;
70875
+ }
70876
+ const orderedMatch = line.match(/^\s*(\d+)\.\s+(.+)$/);
70877
+ if (orderedMatch) {
70878
+ return `${orderedMatch[1]}. ${renderInlineMarkdownToSlackMrkdwn(orderedMatch[2] ?? "")}`;
70879
+ }
70880
+ return renderInlineMarkdownToSlackMrkdwn(line);
70881
+ }).join(`
70882
+ `);
70883
+ }
70884
+ function splitMarkdownTableCells(line) {
70885
+ return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
70886
+ }
70887
+ function isMarkdownTableSeparator(line) {
70888
+ const cells = splitMarkdownTableCells(line);
70889
+ return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
70890
+ }
70891
+ function isMarkdownTableLine(line) {
70892
+ const trimmed = line.trim();
70893
+ return trimmed.includes("|") && splitMarkdownTableCells(trimmed).length >= 2;
70894
+ }
70895
+ function renderSlackTableRow(headers, row) {
70896
+ if (headers.length === 2 && row.length >= 2) {
70897
+ return `*${renderInlineMarkdownToSlackMrkdwn(row[0] ?? "")}*: ${renderInlineMarkdownToSlackMrkdwn(row[1] ?? "")}`;
70898
+ }
70899
+ return headers.map((header, index) => {
70900
+ const value = row[index] ?? "";
70901
+ return `*${renderInlineMarkdownToSlackMrkdwn(header)}:* ${renderInlineMarkdownToSlackMrkdwn(value)}`;
70902
+ }).join(" • ");
70903
+ }
70904
+ function normalizeSlackTableCellText(text) {
70905
+ return stripMarkdownInline(text).slice(0, 3000);
70906
+ }
70907
+ function buildSlackTableCell(text) {
70908
+ return {
70909
+ type: "raw_text",
70910
+ text: normalizeSlackTableCellText(text)
70911
+ };
70912
+ }
70913
+ function renderMarkdownTableToNativeSlackBlock(headers, rows) {
70914
+ if (headers.length === 0 || rows.length === 0) {
70915
+ return null;
70916
+ }
70917
+ return {
70918
+ type: "table",
70919
+ column_settings: headers.map((_header, index) => ({
70920
+ is_wrapped: index === 0
70921
+ })),
70922
+ rows: [headers, ...rows].map((row) => row.map((cell) => buildSlackTableCell(cell)))
70923
+ };
70924
+ }
70925
+ function renderMarkdownTableToFallbackSlackBlock(headers, rows) {
70926
+ if (headers.length === 0 || rows.length === 0) {
70927
+ return null;
70928
+ }
70929
+ const text = rows.map((row) => renderSlackTableRow(headers, row)).filter(Boolean).join(`
70930
+ `);
70931
+ if (!text.trim()) {
70932
+ return null;
70933
+ }
70934
+ return {
70935
+ type: "section",
70936
+ text: {
70937
+ type: "mrkdwn",
70938
+ text
70939
+ }
70940
+ };
70941
+ }
70942
+ function renderMarkdownToSlackBlocks(markdown) {
70943
+ const normalized = markdown.replaceAll(`\r
70944
+ `, `
70945
+ `).replaceAll("\r", `
70946
+ `).trim();
70947
+ if (!normalized) {
70948
+ return [];
70949
+ }
70950
+ const blocks = [];
70951
+ const lines = normalized.split(`
70952
+ `);
70953
+ const firstHeadingLineIndex = lines.findIndex((line) => /^(#{1,6})\s+.+$/.test(line));
70954
+ const paragraph = [];
70955
+ const codeLines = [];
70956
+ let inCodeFence = false;
70957
+ let hasVisibleContent = false;
70958
+ let hasSeenHeading = false;
70959
+ let majorHeadingCount = 0;
70960
+ let hasNativeTableBlock = false;
70961
+ const pushBlock = (block) => {
70962
+ blocks.push(block);
70963
+ hasVisibleContent = true;
70964
+ };
70965
+ const pushDividerIfNeeded = () => {
70966
+ if (!hasVisibleContent) {
70967
+ return;
70968
+ }
70969
+ const lastBlock = blocks[blocks.length - 1];
70970
+ if (lastBlock?.type === "divider") {
70971
+ return;
70972
+ }
70973
+ blocks.push({ type: "divider" });
70974
+ };
70975
+ const flushParagraph = () => {
70976
+ if (paragraph.length === 0) {
70977
+ return;
70978
+ }
70979
+ const text = paragraph.join(`
70980
+ `).trim();
70981
+ if (text) {
70982
+ const shouldRenderAsPreamble = !hasSeenHeading && firstHeadingLineIndex > 0 && blocks.length === 0;
70983
+ pushBlock({
70984
+ ...shouldRenderAsPreamble ? {
70985
+ type: "context",
70986
+ elements: [
70987
+ {
70988
+ type: "mrkdwn",
70989
+ text
70990
+ }
70991
+ ]
70992
+ } : {
70993
+ type: "section",
70994
+ text: {
70995
+ type: "mrkdwn",
70996
+ text
70997
+ }
70998
+ }
70999
+ });
71000
+ }
71001
+ paragraph.length = 0;
71002
+ };
71003
+ const flushCodeFence = () => {
71004
+ if (codeLines.length === 0) {
71005
+ return;
71006
+ }
71007
+ pushBlock({
71008
+ type: "section",
71009
+ text: {
71010
+ type: "mrkdwn",
71011
+ text: `\`\`\`
71012
+ ${codeLines.join(`
71013
+ `)}
71014
+ \`\`\``
71015
+ }
71016
+ });
71017
+ codeLines.length = 0;
71018
+ };
71019
+ for (let lineIndex = 0;lineIndex < lines.length; lineIndex += 1) {
71020
+ const line = lines[lineIndex] ?? "";
71021
+ const fenceMatch = line.match(/^```([^\n`]*)$/);
71022
+ if (fenceMatch) {
71023
+ if (inCodeFence) {
71024
+ flushCodeFence();
71025
+ inCodeFence = false;
71026
+ } else {
71027
+ flushParagraph();
71028
+ inCodeFence = true;
71029
+ }
71030
+ continue;
71031
+ }
71032
+ if (inCodeFence) {
71033
+ codeLines.push(line);
71034
+ continue;
71035
+ }
71036
+ if (line.trim().length === 0) {
71037
+ flushParagraph();
71038
+ continue;
71039
+ }
71040
+ const nextLine = lines[lineIndex + 1] ?? "";
71041
+ if (isMarkdownTableLine(line) && isMarkdownTableSeparator(nextLine)) {
71042
+ flushParagraph();
71043
+ const headers = splitMarkdownTableCells(line);
71044
+ const rows = [];
71045
+ lineIndex += 2;
71046
+ while (lineIndex < lines.length) {
71047
+ const tableLine = lines[lineIndex] ?? "";
71048
+ if (!tableLine.trim() || !isMarkdownTableLine(tableLine)) {
71049
+ lineIndex -= 1;
71050
+ break;
71051
+ }
71052
+ rows.push(splitMarkdownTableCells(tableLine));
71053
+ lineIndex += 1;
71054
+ }
71055
+ const tableBlock = !hasNativeTableBlock ? renderMarkdownTableToNativeSlackBlock(headers, rows) : renderMarkdownTableToFallbackSlackBlock(headers, rows);
71056
+ if (tableBlock) {
71057
+ pushBlock(tableBlock);
71058
+ if (tableBlock.type === "table") {
71059
+ hasNativeTableBlock = true;
71060
+ }
71061
+ }
71062
+ continue;
71063
+ }
71064
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
71065
+ if (headingMatch) {
71066
+ flushParagraph();
71067
+ hasSeenHeading = true;
71068
+ const level = headingMatch[1]?.length ?? 1;
71069
+ const content = headingMatch[2] ?? "";
71070
+ if (level <= 2) {
71071
+ if (majorHeadingCount > 0) {
71072
+ pushDividerIfNeeded();
71073
+ }
71074
+ pushBlock({
71075
+ type: "header",
71076
+ text: {
71077
+ type: "plain_text",
71078
+ text: normalizeSlackHeaderText(content)
71079
+ }
71080
+ });
71081
+ majorHeadingCount += 1;
71082
+ } else if (level === 3) {
71083
+ pushBlock({
71084
+ type: "section",
71085
+ text: {
71086
+ type: "mrkdwn",
71087
+ text: `*${renderInlineMarkdownToSlackMrkdwn(content)}*`
71088
+ }
71089
+ });
71090
+ } else {
71091
+ paragraph.push(`*${renderInlineMarkdownToSlackMrkdwn(content)}*`);
71092
+ }
71093
+ continue;
71094
+ }
71095
+ const bulletMatch = line.match(/^\s*[-*+]\s+(.+)$/);
71096
+ if (bulletMatch) {
71097
+ paragraph.push(`• ${renderInlineMarkdownToSlackMrkdwn(bulletMatch[1] ?? "")}`);
71098
+ continue;
71099
+ }
71100
+ const orderedMatch = line.match(/^\s*(\d+)\.\s+(.+)$/);
71101
+ if (orderedMatch) {
71102
+ paragraph.push(`${orderedMatch[1]}. ${renderInlineMarkdownToSlackMrkdwn(orderedMatch[2] ?? "")}`);
71103
+ continue;
71104
+ }
71105
+ paragraph.push(renderInlineMarkdownToSlackMrkdwn(line));
71106
+ }
71107
+ if (inCodeFence) {
71108
+ flushCodeFence();
71109
+ }
71110
+ flushParagraph();
71111
+ return blocks;
71112
+ }
71113
+ function resolveSlackMessageContent(params) {
71114
+ const { text, inputFormat, renderMode } = params;
71115
+ if (inputFormat === "blocks") {
71116
+ if (renderMode !== "none" && renderMode !== "blocks") {
71117
+ throw new Error("Slack blocks input supports only --render none or --render blocks");
71118
+ }
71119
+ const blocks = parseSlackBlocksInput(text);
71120
+ return {
71121
+ text: buildSlackBlocksFallbackText(blocks),
71122
+ blocks,
71123
+ apiText: "​"
71124
+ };
71125
+ }
71126
+ if (inputFormat === "html") {
71127
+ throw new Error("Slack does not support HTML input; use --input md, mrkdwn, plain, or blocks");
71128
+ }
71129
+ if (renderMode === "blocks") {
71130
+ const blocks = renderMarkdownToSlackBlocks(text);
71131
+ return {
71132
+ text: buildSlackBlocksFallbackText(blocks),
71133
+ blocks,
71134
+ apiText: "​"
71135
+ };
71136
+ }
71137
+ if (renderMode === "html") {
71138
+ throw new Error("Slack does not support --render html");
71139
+ }
71140
+ if (inputFormat === "mrkdwn" || renderMode === "mrkdwn") {
71141
+ return { text };
71142
+ }
71143
+ if (inputFormat === "md" || renderMode === "native") {
71144
+ return {
71145
+ text: renderMarkdownToSlackMrkdwn(text)
71146
+ };
71147
+ }
71148
+ return { text };
71149
+ }
71150
+
71151
+ // src/channels/slack/message-actions.ts
70436
71152
  var { WebClient } = import_bolt.webApi;
70437
71153
  function createSlackClient(botToken) {
70438
71154
  return new WebClient(botToken);
@@ -70500,13 +71216,18 @@ async function loadSlackMedia(media) {
70500
71216
  async function sendSlackMessage(params) {
70501
71217
  const client = createSlackClient(params.botToken);
70502
71218
  const target = await resolveSlackTarget(client, params.target, params.threadId, params.replyTo);
71219
+ const resolvedMessage = resolveSlackMessageContent({
71220
+ text: params.message ?? "",
71221
+ inputFormat: params.inputFormat ?? "md",
71222
+ renderMode: params.renderMode ?? "native"
71223
+ });
70503
71224
  if (params.media) {
70504
71225
  const media = await loadSlackMedia(params.media);
70505
71226
  const filesClient = client.files;
70506
71227
  await filesClient.uploadV2({
70507
71228
  channel_id: target.channelId,
70508
71229
  thread_ts: target.threadTs,
70509
- initial_comment: params.message,
71230
+ initial_comment: resolvedMessage.text,
70510
71231
  filename: media.filename,
70511
71232
  file: media.data
70512
71233
  });
@@ -70518,10 +71239,20 @@ async function sendSlackMessage(params) {
70518
71239
  filename: media.filename
70519
71240
  };
70520
71241
  }
70521
- const posted = await postSlackText(client, {
71242
+ const posted = resolvedMessage.blocks ? [
71243
+ {
71244
+ text: resolvedMessage.text,
71245
+ ts: (await client.chat.postMessage({
71246
+ channel: target.channelId,
71247
+ thread_ts: target.threadTs,
71248
+ text: resolvedMessage.apiText ?? resolvedMessage.text,
71249
+ blocks: resolvedMessage.blocks
71250
+ })).ts ?? ""
71251
+ }
71252
+ ].filter((entry) => entry.ts) : await postSlackText(client, {
70522
71253
  channel: target.channelId,
70523
71254
  threadTs: target.threadTs,
70524
- text: params.message ?? ""
71255
+ text: resolvedMessage.text
70525
71256
  });
70526
71257
  return {
70527
71258
  ok: true,
@@ -70583,10 +71314,16 @@ async function editSlackMessage(params) {
70583
71314
  }
70584
71315
  const client = createSlackClient(params.botToken);
70585
71316
  const target = await resolveSlackTarget(client, params.target);
71317
+ const resolvedMessage = resolveSlackMessageContent({
71318
+ text: params.message,
71319
+ inputFormat: params.inputFormat ?? "md",
71320
+ renderMode: params.renderMode ?? "native"
71321
+ });
70586
71322
  await client.chat.update({
70587
71323
  channel: target.channelId,
70588
71324
  ts: params.messageId,
70589
- text: params.message
71325
+ text: resolvedMessage.apiText ?? resolvedMessage.text,
71326
+ ...resolvedMessage.blocks ? { blocks: resolvedMessage.blocks } : {}
70590
71327
  });
70591
71328
  return { ok: true };
70592
71329
  }
@@ -70780,7 +71517,9 @@ var slackChannelPlugin = {
70780
71517
  limit: command.limit,
70781
71518
  query: command.query,
70782
71519
  pollQuestion: command.pollQuestion,
70783
- pollOptions: command.pollOptions
71520
+ pollOptions: command.pollOptions,
71521
+ inputFormat: command.inputFormat,
71522
+ renderMode: command.renderMode
70784
71523
  };
70785
71524
  switch (command.action) {
70786
71525
  case "send":
@@ -70924,8 +71663,18 @@ function hasTelegramBotMention(text, botUsername) {
70924
71663
  if (!text || !normalizedBotUsername) {
70925
71664
  return false;
70926
71665
  }
70927
- const pattern = new RegExp(`(^|\\s)@${escapeRegExp2(normalizedBotUsername)}\\b`, "i");
70928
- return pattern.test(text);
71666
+ return extractTelegramMentionTargets(text).includes(normalizedBotUsername.toLowerCase());
71667
+ }
71668
+ function hasForeignTelegramMention(text, botUsername) {
71669
+ const mentions = extractTelegramMentionTargets(text);
71670
+ if (mentions.length === 0) {
71671
+ return false;
71672
+ }
71673
+ const normalizedBotUsername = (botUsername ?? "").trim().replace(/^@/, "").toLowerCase();
71674
+ if (!normalizedBotUsername) {
71675
+ return true;
71676
+ }
71677
+ return !mentions.includes(normalizedBotUsername);
70929
71678
  }
70930
71679
  function stripTelegramBotMention(text, botUsername) {
70931
71680
  const normalizedBotUsername = (botUsername ?? "").trim().replace(/^@/, "");
@@ -70944,6 +71693,27 @@ function isReplyToTelegramBot(message, botUserId) {
70944
71693
  function escapeRegExp2(raw) {
70945
71694
  return raw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
70946
71695
  }
71696
+ function extractTelegramMentionTargets(text) {
71697
+ if (!text) {
71698
+ return [];
71699
+ }
71700
+ const matches = new Set;
71701
+ const mentionPattern = /(^|\s)@([A-Za-z0-9_]{2,32})\b/g;
71702
+ const slashCommandTargetPattern = /(^|\s)\/[A-Za-z0-9_]+@([A-Za-z0-9_]{2,32})\b/g;
71703
+ for (const match of text.matchAll(mentionPattern)) {
71704
+ const username = match[2]?.trim().toLowerCase();
71705
+ if (username) {
71706
+ matches.add(username);
71707
+ }
71708
+ }
71709
+ for (const match of text.matchAll(slashCommandTargetPattern)) {
71710
+ const username = match[2]?.trim().toLowerCase();
71711
+ if (username) {
71712
+ matches.add(username);
71713
+ }
71714
+ }
71715
+ return [...matches];
71716
+ }
70947
71717
 
70948
71718
  // src/channels/telegram/session-routing.ts
70949
71719
  function resolveTelegramConversationTarget(params) {
@@ -71075,6 +71845,20 @@ async function paceTelegramEdit(params) {
71075
71845
  function recordTelegramEdit(params) {
71076
71846
  lastTelegramEditAtByMessage.set(getTelegramEditKey(params), Date.now());
71077
71847
  }
71848
+ function buildTelegramTextPayload(params) {
71849
+ if (params.wireFormat !== "html") {
71850
+ return {
71851
+ text: params.text
71852
+ };
71853
+ }
71854
+ return {
71855
+ text: params.text,
71856
+ parseMode: "HTML"
71857
+ };
71858
+ }
71859
+ function isTelegramHtmlParseError(error) {
71860
+ return error instanceof TelegramApiError && error.errorCode === 400 && /can't parse entities|unsupported start tag|unexpected end tag|entity beginning/i.test(error.description);
71861
+ }
71078
71862
  function splitTelegramText(text, maxChars = 3900) {
71079
71863
  if (!text) {
71080
71864
  return [];
@@ -71109,83 +71893,136 @@ function splitTelegramText(text, maxChars = 3900) {
71109
71893
  }
71110
71894
  async function postTelegramText(params) {
71111
71895
  const posted = [];
71112
- const chunks = splitTelegramText(params.text);
71113
- for (const chunk of chunks) {
71114
- const response = await callTelegramApi(params.token, "sendMessage", {
71115
- chat_id: params.chatId,
71116
- text: chunk,
71117
- ...params.topicId != null && !params.omitThreadId ? { message_thread_id: params.topicId } : {}
71118
- });
71119
- posted.push({
71120
- text: chunk,
71121
- messageId: response.message_id
71122
- });
71896
+ const rawChunks = splitTelegramText(params.text);
71897
+ try {
71898
+ for (const chunk of rawChunks) {
71899
+ const payload = buildTelegramTextPayload({
71900
+ text: chunk,
71901
+ wireFormat: params.wireFormat
71902
+ });
71903
+ const response = await callTelegramApi(params.token, "sendMessage", {
71904
+ chat_id: params.chatId,
71905
+ text: payload.text,
71906
+ ...payload.parseMode ? { parse_mode: payload.parseMode } : {},
71907
+ ...params.topicId != null && !params.omitThreadId ? { message_thread_id: params.topicId } : {}
71908
+ });
71909
+ posted.push({
71910
+ text: payload.text,
71911
+ messageId: response.message_id
71912
+ });
71913
+ }
71914
+ } catch (error) {
71915
+ if (params.wireFormat !== "html" || !isTelegramHtmlParseError(error)) {
71916
+ throw error;
71917
+ }
71918
+ for (const chunk of posted) {
71919
+ await callTelegramApi(params.token, "deleteMessage", {
71920
+ chat_id: params.chatId,
71921
+ message_id: chunk.messageId
71922
+ });
71923
+ lastTelegramEditAtByMessage.delete(getTelegramEditKey({
71924
+ token: params.token,
71925
+ chatId: params.chatId,
71926
+ messageId: chunk.messageId
71927
+ }));
71928
+ }
71929
+ posted.length = 0;
71930
+ for (const chunk of rawChunks) {
71931
+ const response = await callTelegramApi(params.token, "sendMessage", {
71932
+ chat_id: params.chatId,
71933
+ text: chunk,
71934
+ ...params.topicId != null && !params.omitThreadId ? { message_thread_id: params.topicId } : {}
71935
+ });
71936
+ posted.push({
71937
+ text: chunk,
71938
+ messageId: response.message_id
71939
+ });
71940
+ }
71123
71941
  }
71124
71942
  return posted;
71125
71943
  }
71126
71944
  async function reconcileTelegramText(params) {
71127
- const nextTexts = splitTelegramText(params.text);
71128
- const reconciled = [];
71129
- const sharedCount = Math.min(params.chunks.length, nextTexts.length);
71130
- for (let index = 0;index < sharedCount; index += 1) {
71131
- const existingChunk = params.chunks[index];
71132
- const nextText = nextTexts[index];
71133
- if (!existingChunk || !nextText) {
71134
- continue;
71135
- }
71136
- if (existingChunk.text !== nextText) {
71137
- await paceTelegramEdit({
71138
- token: params.token,
71139
- chatId: params.chatId,
71945
+ const rawNextTexts = splitTelegramText(params.text);
71946
+ const reconcileWithTexts = async (nextTexts, parseMode) => {
71947
+ const reconciled = [];
71948
+ const sharedCount = Math.min(params.chunks.length, nextTexts.length);
71949
+ for (let index = 0;index < sharedCount; index += 1) {
71950
+ const existingChunk = params.chunks[index];
71951
+ const nextText = nextTexts[index];
71952
+ if (!existingChunk || !nextText) {
71953
+ continue;
71954
+ }
71955
+ if (existingChunk.text !== nextText) {
71956
+ await paceTelegramEdit({
71957
+ token: params.token,
71958
+ chatId: params.chatId,
71959
+ messageId: existingChunk.messageId
71960
+ });
71961
+ await callTelegramApi(params.token, "editMessageText", {
71962
+ chat_id: params.chatId,
71963
+ message_id: existingChunk.messageId,
71964
+ text: nextText,
71965
+ ...parseMode ? { parse_mode: parseMode } : {}
71966
+ });
71967
+ recordTelegramEdit({
71968
+ token: params.token,
71969
+ chatId: params.chatId,
71970
+ messageId: existingChunk.messageId
71971
+ });
71972
+ }
71973
+ reconciled.push({
71974
+ text: nextText,
71140
71975
  messageId: existingChunk.messageId
71141
71976
  });
71142
- await callTelegramApi(params.token, "editMessageText", {
71977
+ }
71978
+ for (let index = sharedCount;index < nextTexts.length; index += 1) {
71979
+ const nextText = nextTexts[index];
71980
+ if (!nextText) {
71981
+ continue;
71982
+ }
71983
+ const response = await callTelegramApi(params.token, "sendMessage", {
71143
71984
  chat_id: params.chatId,
71144
- message_id: existingChunk.messageId,
71145
- text: nextText
71985
+ text: nextText,
71986
+ ...parseMode ? { parse_mode: parseMode } : {},
71987
+ ...params.topicId != null && !params.omitThreadId ? { message_thread_id: params.topicId } : {}
71146
71988
  });
71147
- recordTelegramEdit({
71148
- token: params.token,
71149
- chatId: params.chatId,
71150
- messageId: existingChunk.messageId
71989
+ reconciled.push({
71990
+ text: nextText,
71991
+ messageId: response.message_id
71151
71992
  });
71152
71993
  }
71153
- reconciled.push({
71154
- text: nextText,
71155
- messageId: existingChunk.messageId
71156
- });
71157
- }
71158
- for (let index = sharedCount;index < nextTexts.length; index += 1) {
71159
- const nextText = nextTexts[index];
71160
- if (!nextText) {
71161
- continue;
71994
+ for (let index = nextTexts.length;index < params.chunks.length; index += 1) {
71995
+ const staleChunk = params.chunks[index];
71996
+ if (!staleChunk) {
71997
+ continue;
71998
+ }
71999
+ await callTelegramApi(params.token, "deleteMessage", {
72000
+ chat_id: params.chatId,
72001
+ message_id: staleChunk.messageId
72002
+ });
72003
+ lastTelegramEditAtByMessage.delete(getTelegramEditKey({
72004
+ token: params.token,
72005
+ chatId: params.chatId,
72006
+ messageId: staleChunk.messageId
72007
+ }));
71162
72008
  }
71163
- const response = await callTelegramApi(params.token, "sendMessage", {
71164
- chat_id: params.chatId,
71165
- text: nextText,
71166
- ...params.topicId != null && !params.omitThreadId ? { message_thread_id: params.topicId } : {}
71167
- });
71168
- reconciled.push({
71169
- text: nextText,
71170
- messageId: response.message_id
71171
- });
72009
+ return reconciled;
72010
+ };
72011
+ if (params.wireFormat !== "html") {
72012
+ return await reconcileWithTexts(rawNextTexts);
71172
72013
  }
71173
- for (let index = nextTexts.length;index < params.chunks.length; index += 1) {
71174
- const staleChunk = params.chunks[index];
71175
- if (!staleChunk) {
71176
- continue;
72014
+ try {
72015
+ const renderedTexts = rawNextTexts.map((text) => buildTelegramTextPayload({
72016
+ text,
72017
+ wireFormat: params.wireFormat
72018
+ }).text);
72019
+ return await reconcileWithTexts(renderedTexts, "HTML");
72020
+ } catch (error) {
72021
+ if (!isTelegramHtmlParseError(error)) {
72022
+ throw error;
71177
72023
  }
71178
- await callTelegramApi(params.token, "deleteMessage", {
71179
- chat_id: params.chatId,
71180
- message_id: staleChunk.messageId
71181
- });
71182
- lastTelegramEditAtByMessage.delete(getTelegramEditKey({
71183
- token: params.token,
71184
- chatId: params.chatId,
71185
- messageId: staleChunk.messageId
71186
- }));
72024
+ return await reconcileWithTexts(rawNextTexts);
71187
72025
  }
71188
- return reconciled;
71189
72026
  }
71190
72027
  function shouldOmitTelegramThreadId(topicId) {
71191
72028
  return topicId === 1;
@@ -71194,6 +72031,235 @@ function getTelegramMaxChars(maxMessageChars) {
71194
72031
  return Math.min(maxMessageChars, 3900);
71195
72032
  }
71196
72033
 
72034
+ // src/channels/telegram/html-safe.ts
72035
+ var TOKEN_PREFIX = "\x00TGH";
72036
+ function escapeHtml(text) {
72037
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
72038
+ }
72039
+ function sanitizeTelegramHref(rawHref) {
72040
+ const href = rawHref.trim();
72041
+ if (!href) {
72042
+ return null;
72043
+ }
72044
+ const lowered = href.toLowerCase();
72045
+ if (lowered.startsWith("http://") || lowered.startsWith("https://") || lowered.startsWith("tg://") || lowered.startsWith("mailto:")) {
72046
+ return escapeHtml(href);
72047
+ }
72048
+ return null;
72049
+ }
72050
+ function storeToken(tokens, value) {
72051
+ const token = `${TOKEN_PREFIX}${tokens.length};\x00`;
72052
+ tokens.push(value);
72053
+ return token;
72054
+ }
72055
+ function restoreTokens(text, tokens) {
72056
+ let restored = text;
72057
+ for (let index = 0;index < tokens.length; index += 1) {
72058
+ restored = restored.replaceAll(`${TOKEN_PREFIX}${index};\x00`, tokens[index] ?? "");
72059
+ }
72060
+ return restored;
72061
+ }
72062
+ function applyInlineFormatting(text) {
72063
+ return text.replaceAll(/~~([^~]+)~~/g, "<s>$1</s>").replaceAll(/\*\*([^*\n][\s\S]*?[^*\n])\*\*/g, "<b>$1</b>").replaceAll(/\*([^*\n][\s\S]*?[^*\n])\*/g, "<i>$1</i>");
72064
+ }
72065
+ function renderInlineMarkdownToTelegramHtml(text) {
72066
+ const tokens = [];
72067
+ let working = text;
72068
+ working = working.replaceAll(/`([^`\n]+)`/g, (_match, code) => storeToken(tokens, `<code>${escapeHtml(code)}</code>`));
72069
+ working = working.replaceAll(/\[([^\]]+)\]\(([^)\s]+)\)/g, (match, label, href) => {
72070
+ const safeHref = sanitizeTelegramHref(href);
72071
+ if (!safeHref) {
72072
+ return match;
72073
+ }
72074
+ return storeToken(tokens, `<a href="${safeHref}">${escapeHtml(label)}</a>`);
72075
+ });
72076
+ working = escapeHtml(working);
72077
+ working = applyInlineFormatting(working);
72078
+ return restoreTokens(working, tokens);
72079
+ }
72080
+ function renderHeadingLine(level, content) {
72081
+ const renderedContent = renderInlineMarkdownToTelegramHtml(content);
72082
+ if (level <= 2) {
72083
+ return {
72084
+ kind: "section-heading",
72085
+ text: `<b>${renderedContent}</b>`
72086
+ };
72087
+ }
72088
+ if (level === 3) {
72089
+ return {
72090
+ kind: "subsection-heading",
72091
+ text: `<b>${renderedContent}</b>`
72092
+ };
72093
+ }
72094
+ return {
72095
+ kind: "text",
72096
+ text: renderedContent
72097
+ };
72098
+ }
72099
+ function renderMarkdownLine(line) {
72100
+ if (line.trim().length === 0) {
72101
+ return { kind: "blank", text: "" };
72102
+ }
72103
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
72104
+ if (headingMatch) {
72105
+ return renderHeadingLine(headingMatch[1]?.length ?? 1, headingMatch[2] ?? "");
72106
+ }
72107
+ const bulletMatch = line.match(/^\s*[-*+]\s+(.+)$/);
72108
+ if (bulletMatch) {
72109
+ return {
72110
+ kind: "text",
72111
+ text: `• ${renderInlineMarkdownToTelegramHtml(bulletMatch[1] ?? "")}`
72112
+ };
72113
+ }
72114
+ const orderedMatch = line.match(/^\s*(\d+)\.\s+(.+)$/);
72115
+ if (orderedMatch) {
72116
+ return {
72117
+ kind: "text",
72118
+ text: `${orderedMatch[1]}. ${renderInlineMarkdownToTelegramHtml(orderedMatch[2] ?? "")}`
72119
+ };
72120
+ }
72121
+ const quoteMatch = line.match(/^\s*>\s?(.*)$/);
72122
+ if (quoteMatch) {
72123
+ return {
72124
+ kind: "text",
72125
+ text: `&gt; ${renderInlineMarkdownToTelegramHtml(quoteMatch[1] ?? "")}`
72126
+ };
72127
+ }
72128
+ return {
72129
+ kind: "text",
72130
+ text: renderInlineMarkdownToTelegramHtml(line)
72131
+ };
72132
+ }
72133
+ function renderMarkdownTextBlock(text) {
72134
+ const lines = text.split(`
72135
+ `).map(renderMarkdownLine);
72136
+ const rendered = [];
72137
+ const ensureBlankSeparator = () => {
72138
+ if (rendered.length === 0 || rendered[rendered.length - 1] === "") {
72139
+ return;
72140
+ }
72141
+ rendered.push("");
72142
+ };
72143
+ const hasLaterVisibleLine = (startIndex) => lines.slice(startIndex + 1).some((line) => line.kind !== "blank");
72144
+ for (let index = 0;index < lines.length; index += 1) {
72145
+ const line = lines[index];
72146
+ if (!line) {
72147
+ continue;
72148
+ }
72149
+ if (line.kind === "blank") {
72150
+ ensureBlankSeparator();
72151
+ continue;
72152
+ }
72153
+ if (line.kind === "section-heading") {
72154
+ ensureBlankSeparator();
72155
+ rendered.push(line.text);
72156
+ if (hasLaterVisibleLine(index)) {
72157
+ rendered.push("");
72158
+ }
72159
+ continue;
72160
+ }
72161
+ if (line.kind === "subsection-heading") {
72162
+ rendered.push(line.text);
72163
+ continue;
72164
+ }
72165
+ rendered.push(line.text);
72166
+ }
72167
+ while (rendered[rendered.length - 1] === "") {
72168
+ rendered.pop();
72169
+ }
72170
+ return rendered.join(`
72171
+ `);
72172
+ }
72173
+ function renderCodeFence(language, code) {
72174
+ const trimmedLanguage = language.trim();
72175
+ const safeLanguage = /^[a-z0-9_+-]+$/i.test(trimmedLanguage) ? trimmedLanguage : "";
72176
+ const escapedCode = escapeHtml(code.replace(/\n$/, ""));
72177
+ if (safeLanguage) {
72178
+ return `<pre><code class="language-${safeLanguage}">${escapedCode}</code></pre>`;
72179
+ }
72180
+ return `<pre><code>${escapedCode}</code></pre>`;
72181
+ }
72182
+ function renderTelegramHtmlSafeFromMarkdown(markdown) {
72183
+ const normalized = markdown.replaceAll(`\r
72184
+ `, `
72185
+ `).replaceAll("\r", `
72186
+ `).trim();
72187
+ if (!normalized) {
72188
+ return "";
72189
+ }
72190
+ const segments = [];
72191
+ const codeFencePattern = /```([^\n`]*)\n([\s\S]*?)```/g;
72192
+ let cursor = 0;
72193
+ for (const match of normalized.matchAll(codeFencePattern)) {
72194
+ const matchText = match[0];
72195
+ const matchIndex = match.index ?? -1;
72196
+ if (matchIndex < 0) {
72197
+ continue;
72198
+ }
72199
+ const textBefore = normalized.slice(cursor, matchIndex);
72200
+ if (textBefore) {
72201
+ segments.push(renderMarkdownTextBlock(textBefore));
72202
+ }
72203
+ segments.push(renderCodeFence(match[1] ?? "", match[2] ?? ""));
72204
+ cursor = matchIndex + matchText.length;
72205
+ }
72206
+ const tail = normalized.slice(cursor);
72207
+ if (tail) {
72208
+ const unmatchedFenceMatch = tail.match(/([\s\S]*?)```([^\n`]*)\n([\s\S]*)$/);
72209
+ if (unmatchedFenceMatch) {
72210
+ const textBeforeFence = unmatchedFenceMatch[1] ?? "";
72211
+ const language = unmatchedFenceMatch[2] ?? "";
72212
+ const codeTail = unmatchedFenceMatch[3] ?? "";
72213
+ if (textBeforeFence) {
72214
+ segments.push(renderMarkdownTextBlock(textBeforeFence));
72215
+ }
72216
+ segments.push(renderCodeFence(language, codeTail));
72217
+ } else {
72218
+ segments.push(renderMarkdownTextBlock(tail));
72219
+ }
72220
+ }
72221
+ return segments.filter((segment) => segment.length > 0).join(`
72222
+ `).replaceAll(/\n{3,}/g, `
72223
+
72224
+ `).trim();
72225
+ }
72226
+
72227
+ // src/channels/telegram/content.ts
72228
+ function resolveTelegramMessageContent(params) {
72229
+ const { text, inputFormat, renderMode } = params;
72230
+ if (inputFormat === "blocks" || renderMode === "blocks") {
72231
+ throw new Error("Telegram does not support block payloads");
72232
+ }
72233
+ if (inputFormat === "mrkdwn" || renderMode === "mrkdwn") {
72234
+ throw new Error("Telegram does not support Slack mrkdwn payloads");
72235
+ }
72236
+ if (inputFormat === "html") {
72237
+ if (renderMode !== "none" && renderMode !== "html" && renderMode !== "native") {
72238
+ throw new Error("Telegram HTML input supports only --render none, html, or native");
72239
+ }
72240
+ return {
72241
+ text,
72242
+ wireFormat: "html"
72243
+ };
72244
+ }
72245
+ if (renderMode === "html") {
72246
+ return {
72247
+ text: renderTelegramHtmlSafeFromMarkdown(text),
72248
+ wireFormat: "html"
72249
+ };
72250
+ }
72251
+ if (renderMode === "none" || inputFormat === "plain") {
72252
+ return {
72253
+ text,
72254
+ wireFormat: "text"
72255
+ };
72256
+ }
72257
+ return {
72258
+ text: renderTelegramHtmlSafeFromMarkdown(text),
72259
+ wireFormat: "html"
72260
+ };
72261
+ }
72262
+
71197
72263
  // src/channels/telegram/attachments.ts
71198
72264
  import { basename as basename2 } from "node:path";
71199
72265
  function pickTelegramPhoto(photo) {
@@ -71463,12 +72529,18 @@ class TelegramPollingService {
71463
72529
  return;
71464
72530
  }
71465
72531
  const topicId = binding.topicId ? Number(binding.topicId) : undefined;
72532
+ const renderedNotification = resolveTelegramMessageContent({
72533
+ text,
72534
+ inputFormat: "md",
72535
+ renderMode: "native"
72536
+ });
71466
72537
  await postTelegramText({
71467
72538
  token: this.accountConfig.botToken,
71468
72539
  chatId,
71469
- text,
72540
+ text: renderedNotification.text,
71470
72541
  topicId: Number.isFinite(topicId) ? topicId : undefined,
71471
- omitThreadId: shouldOmitTelegramThreadId(Number.isFinite(topicId) ? topicId : undefined)
72542
+ omitThreadId: shouldOmitTelegramThreadId(Number.isFinite(topicId) ? topicId : undefined),
72543
+ wireFormat: renderedNotification.wireFormat
71472
72544
  });
71473
72545
  }
71474
72546
  });
@@ -71699,6 +72771,10 @@ class TelegramPollingService {
71699
72771
  }
71700
72772
  }
71701
72773
  }
72774
+ if (hasForeignTelegramMention(rawText, this.botUsername)) {
72775
+ await this.processedEventsStore.markCompleted(eventId);
72776
+ return;
72777
+ }
71702
72778
  const explicitMention = hasTelegramBotMention(rawText, this.botUsername) || Boolean(slashCommand && rawText.startsWith("/"));
71703
72779
  const followUpState = await this.agentService.getConversationFollowUpState(routeInfo.sessionTarget);
71704
72780
  const effectiveFollowUpMode = resolveFollowUpMode({
@@ -71712,6 +72788,16 @@ class TelegramPollingService {
71712
72788
  lastBotReplyAt: followUpState.lastBotReplyAt,
71713
72789
  directReplyToBot: isReplyToTelegramBot(message, this.botUserId)
71714
72790
  });
72791
+ const textBody = explicitMention ? stripTelegramBotMention(rawText, this.botUsername) : rawText;
72792
+ const recentMessageMarker = String(message.message_id);
72793
+ if (rawText || explicitMention || slashCommand) {
72794
+ await this.agentService.appendRecentConversationMessage(routeInfo.sessionTarget, {
72795
+ marker: recentMessageMarker,
72796
+ text: slashCommand ? "" : textBody,
72797
+ senderId: message.from?.id != null ? String(message.from.id).trim() : undefined,
72798
+ senderName: [message.from?.first_name, message.from?.last_name].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").trim() || message.from?.username?.trim() || undefined
72799
+ });
72800
+ }
71715
72801
  if (routeInfo.route.requireMention && !wasMentioned) {
71716
72802
  await this.processedEventsStore.markCompleted(eventId);
71717
72803
  return;
@@ -71719,7 +72805,10 @@ class TelegramPollingService {
71719
72805
  if (explicitMention && followUpState.overrideMode === "paused") {
71720
72806
  await this.agentService.reactivateConversationFollowUp(routeInfo.sessionTarget);
71721
72807
  }
71722
- const textBody = explicitMention ? stripTelegramBotMention(rawText, this.botUsername) : rawText;
72808
+ const effectivePromptText = textBody || (explicitMention ? buildMentionOnlyFollowUpPrompt({
72809
+ conversationKind: routeInfo.conversationKind === "topic" ? "group" : routeInfo.conversationKind,
72810
+ threaded: routeInfo.topicId != null || message.message_thread_id != null && Number.isFinite(message.message_thread_id)
72811
+ }) : "");
71723
72812
  const attachmentPaths = await resolveTelegramAttachmentPaths({
71724
72813
  message,
71725
72814
  botToken: this.accountConfig.botToken,
@@ -71727,11 +72816,18 @@ class TelegramPollingService {
71727
72816
  sessionKey: routeInfo.sessionTarget.sessionKey,
71728
72817
  messageId: String(message.message_id)
71729
72818
  });
71730
- const text = prependAttachmentMentions(textBody, attachmentPaths);
72819
+ const text = prependAttachmentMentions(effectivePromptText, attachmentPaths);
71731
72820
  if (!text) {
71732
72821
  await this.processedEventsStore.markCompleted(eventId);
71733
72822
  return;
71734
72823
  }
72824
+ const recentConversationReplay = await this.agentService.getRecentConversationReplayMessages(routeInfo.sessionTarget, {
72825
+ excludeMarker: recentMessageMarker
72826
+ });
72827
+ const enrichPromptText = (nextText) => prependRecentConversationContext({
72828
+ currentText: nextText,
72829
+ recentMessages: recentConversationReplay
72830
+ });
71735
72831
  await this.processedEventsStore.markProcessing(eventId);
71736
72832
  await this.activityStore.record({
71737
72833
  agentId: routeInfo.route.agentId,
@@ -71759,7 +72855,7 @@ class TelegramPollingService {
71759
72855
  });
71760
72856
  const protectedControlMutationRule = auth.mayManageProtectedResources ? undefined : DEFAULT_PROTECTED_CONTROL_RULE;
71761
72857
  const agentPromptText = buildAgentPromptText({
71762
- text,
72858
+ text: enrichPromptText(text),
71763
72859
  identity,
71764
72860
  config: this.loadedConfig.raw.channels.telegram.agentPrompt,
71765
72861
  cliTool,
@@ -71802,7 +72898,7 @@ class TelegramPollingService {
71802
72898
  text,
71803
72899
  agentPromptText,
71804
72900
  agentPromptBuilder: (nextText) => buildAgentPromptText({
71805
- text: nextText,
72901
+ text: enrichPromptText(nextText),
71806
72902
  identity,
71807
72903
  config: this.loadedConfig.raw.channels.telegram.agentPrompt,
71808
72904
  cliTool,
@@ -71811,27 +72907,43 @@ class TelegramPollingService {
71811
72907
  protectedControlMutationRule
71812
72908
  }),
71813
72909
  protectedControlMutationRule,
72910
+ transformSessionInputText: enrichPromptText,
72911
+ onPromptAccepted: async () => {
72912
+ await this.agentService.markRecentConversationProcessed(routeInfo.sessionTarget, recentMessageMarker);
72913
+ },
71814
72914
  route: routeInfo.route,
71815
72915
  maxChars: this.getTelegramMaxChars(routeInfo.route.agentId),
71816
72916
  timingContext,
71817
72917
  postText: async (nextText) => {
72918
+ const renderedReply = resolveTelegramMessageContent({
72919
+ text: nextText,
72920
+ inputFormat: "md",
72921
+ renderMode: "native"
72922
+ });
71818
72923
  responseChunks = await postTelegramText({
71819
72924
  token: this.accountConfig.botToken,
71820
72925
  chatId: message.chat.id,
71821
- text: nextText,
72926
+ text: renderedReply.text,
71822
72927
  topicId: routeInfo.topicId,
71823
- omitThreadId: shouldOmitTelegramThreadId(routeInfo.topicId)
72928
+ omitThreadId: shouldOmitTelegramThreadId(routeInfo.topicId),
72929
+ wireFormat: renderedReply.wireFormat
71824
72930
  });
71825
72931
  return responseChunks;
71826
72932
  },
71827
72933
  reconcileText: async (chunks, nextText) => {
72934
+ const renderedReply = resolveTelegramMessageContent({
72935
+ text: nextText,
72936
+ inputFormat: "md",
72937
+ renderMode: "native"
72938
+ });
71828
72939
  responseChunks = await reconcileTelegramText({
71829
72940
  token: this.accountConfig.botToken,
71830
72941
  chatId: message.chat.id,
71831
72942
  chunks,
71832
- text: nextText,
72943
+ text: renderedReply.text,
71833
72944
  topicId: routeInfo.topicId,
71834
- omitThreadId: shouldOmitTelegramThreadId(routeInfo.topicId)
72945
+ omitThreadId: shouldOmitTelegramThreadId(routeInfo.topicId),
72946
+ wireFormat: renderedReply.wireFormat
71835
72947
  });
71836
72948
  return responseChunks;
71837
72949
  }
@@ -71932,6 +73044,23 @@ function resolveRouteAndTarget(params) {
71932
73044
  // src/channels/telegram/message-actions.ts
71933
73045
  import { basename as basename3, extname as extname2 } from "node:path";
71934
73046
  import { readFile as readFile3 } from "node:fs/promises";
73047
+ function isTelegramHtmlParseError2(error) {
73048
+ return error instanceof TelegramApiError && error.errorCode === 400 && /can't parse entities|unsupported start tag|unexpected end tag|entity beginning/i.test(error.description);
73049
+ }
73050
+ function buildTelegramMessagePayload(params) {
73051
+ if (!params.text) {
73052
+ return null;
73053
+ }
73054
+ const resolved = resolveTelegramMessageContent({
73055
+ text: params.text,
73056
+ inputFormat: params.inputFormat ?? "md",
73057
+ renderMode: params.renderMode ?? "native"
73058
+ });
73059
+ return {
73060
+ text: resolved.text,
73061
+ parse_mode: resolved.wireFormat === "html" ? "HTML" : undefined
73062
+ };
73063
+ }
71935
73064
  function parseTelegramChatId(raw) {
71936
73065
  const value = raw.trim();
71937
73066
  if (value.startsWith("@")) {
@@ -71944,7 +73073,11 @@ function parseTelegramChatId(raw) {
71944
73073
  throw new Error(`Invalid Telegram target: ${raw}`);
71945
73074
  }
71946
73075
  function parseTelegramThreadId(threadId) {
71947
- const parsed = Number(threadId ?? "");
73076
+ const raw = threadId?.trim();
73077
+ if (!raw) {
73078
+ return;
73079
+ }
73080
+ const parsed = Number(raw);
71948
73081
  return Number.isFinite(parsed) ? parsed : undefined;
71949
73082
  }
71950
73083
  async function loadTelegramMedia(media) {
@@ -71999,6 +73132,11 @@ async function sendTelegramMessage(params) {
71999
73132
  const chatId = parseTelegramChatId(params.target);
72000
73133
  const threadId = parseTelegramThreadId(params.threadId);
72001
73134
  const replyTo = params.replyTo ? Number(params.replyTo) : undefined;
73135
+ const formattedMessage = buildTelegramMessagePayload({
73136
+ text: params.message,
73137
+ inputFormat: params.inputFormat,
73138
+ renderMode: params.renderMode
73139
+ });
72002
73140
  if (params.media) {
72003
73141
  const media = await loadTelegramMedia(params.media);
72004
73142
  const kind = inferTelegramMediaKind(media.filename, params.forceDocument);
@@ -72018,35 +73156,75 @@ async function sendTelegramMessage(params) {
72018
73156
  };
72019
73157
  const method = methodByKind[kind];
72020
73158
  const fileField = fieldByKind[kind];
72021
- const payload = {
73159
+ const payload2 = {
72022
73160
  chat_id: chatId,
72023
- ...params.message ? { caption: params.message } : {},
73161
+ ...formattedMessage ? { caption: formattedMessage.text } : {},
73162
+ ...formattedMessage?.parse_mode ? { parse_mode: formattedMessage.parse_mode } : {},
72024
73163
  ...threadId != null && threadId !== 1 ? { message_thread_id: threadId } : {},
72025
73164
  ...replyTo != null ? { reply_to_message_id: replyTo } : {},
72026
73165
  ...params.silent ? { disable_notification: true } : {}
72027
73166
  };
72028
- if (media.remoteUrl) {
72029
- return await callTelegramApi(params.botToken, method, {
72030
- ...payload,
72031
- [fileField]: media.remoteUrl
73167
+ try {
73168
+ if (media.remoteUrl) {
73169
+ return await callTelegramApi(params.botToken, method, {
73170
+ ...payload2,
73171
+ [fileField]: media.remoteUrl
73172
+ });
73173
+ }
73174
+ return await callTelegramMultipartApi({
73175
+ token: params.botToken,
73176
+ method,
73177
+ payload: payload2,
73178
+ fileField,
73179
+ file: media.file,
73180
+ filename: media.filename
73181
+ });
73182
+ } catch (error) {
73183
+ if (!formattedMessage || !isTelegramHtmlParseError2(error)) {
73184
+ throw error;
73185
+ }
73186
+ const plainPayload = {
73187
+ ...payload2,
73188
+ caption: params.message ?? ""
73189
+ };
73190
+ delete plainPayload.parse_mode;
73191
+ if (media.remoteUrl) {
73192
+ return await callTelegramApi(params.botToken, method, {
73193
+ ...plainPayload,
73194
+ [fileField]: media.remoteUrl
73195
+ });
73196
+ }
73197
+ return await callTelegramMultipartApi({
73198
+ token: params.botToken,
73199
+ method,
73200
+ payload: plainPayload,
73201
+ fileField,
73202
+ file: media.file,
73203
+ filename: media.filename
72032
73204
  });
72033
73205
  }
72034
- return await callTelegramMultipartApi({
72035
- token: params.botToken,
72036
- method,
72037
- payload,
72038
- fileField,
72039
- file: media.file,
72040
- filename: media.filename
72041
- });
72042
73206
  }
72043
- return await callTelegramApi(params.botToken, "sendMessage", {
73207
+ const payload = {
72044
73208
  chat_id: chatId,
72045
- text: params.message ?? "",
73209
+ text: formattedMessage?.text ?? params.message ?? "",
73210
+ ...formattedMessage?.parse_mode ? { parse_mode: formattedMessage.parse_mode } : {},
72046
73211
  ...threadId != null && threadId !== 1 ? { message_thread_id: threadId } : {},
72047
73212
  ...replyTo != null ? { reply_to_message_id: replyTo } : {},
72048
73213
  ...params.silent ? { disable_notification: true } : {}
72049
- });
73214
+ };
73215
+ try {
73216
+ return await callTelegramApi(params.botToken, "sendMessage", payload);
73217
+ } catch (error) {
73218
+ if (!formattedMessage || !isTelegramHtmlParseError2(error)) {
73219
+ throw error;
73220
+ }
73221
+ const plainPayload = {
73222
+ ...payload,
73223
+ text: params.message ?? ""
73224
+ };
73225
+ delete plainPayload.parse_mode;
73226
+ return await callTelegramApi(params.botToken, "sendMessage", plainPayload);
73227
+ }
72050
73228
  }
72051
73229
  async function sendTelegramPoll(params) {
72052
73230
  if (!params.pollQuestion || !params.pollOptions?.length) {
@@ -72064,11 +73242,30 @@ async function editTelegramMessage(params) {
72064
73242
  if (!params.messageId || !params.message) {
72065
73243
  throw new Error("--message-id and --message are required");
72066
73244
  }
72067
- return await callTelegramApi(params.botToken, "editMessageText", {
73245
+ const formattedMessage = buildTelegramMessagePayload({
73246
+ text: params.message,
73247
+ inputFormat: params.inputFormat,
73248
+ renderMode: params.renderMode
73249
+ });
73250
+ const payload = {
72068
73251
  chat_id: parseTelegramChatId(params.target),
72069
73252
  message_id: Number(params.messageId),
72070
- text: params.message
72071
- });
73253
+ text: formattedMessage?.text ?? params.message,
73254
+ ...formattedMessage?.parse_mode ? { parse_mode: formattedMessage.parse_mode } : {}
73255
+ };
73256
+ try {
73257
+ return await callTelegramApi(params.botToken, "editMessageText", payload);
73258
+ } catch (error) {
73259
+ if (!formattedMessage || !isTelegramHtmlParseError2(error)) {
73260
+ throw error;
73261
+ }
73262
+ const plainPayload = {
73263
+ ...payload,
73264
+ text: params.message
73265
+ };
73266
+ delete plainPayload.parse_mode;
73267
+ return await callTelegramApi(params.botToken, "editMessageText", plainPayload);
73268
+ }
72072
73269
  }
72073
73270
  async function deleteTelegramMessageAction(params) {
72074
73271
  if (!params.messageId) {
@@ -72186,7 +73383,9 @@ var telegramChannelPlugin = {
72186
73383
  pollQuestion: command.pollQuestion,
72187
73384
  pollOptions: command.pollOptions,
72188
73385
  forceDocument: command.forceDocument,
72189
- silent: command.silent
73386
+ silent: command.silent,
73387
+ inputFormat: command.inputFormat,
73388
+ renderMode: command.renderMode
72190
73389
  };
72191
73390
  switch (command.action) {
72192
73391
  case "send":
@@ -72387,6 +73586,7 @@ function buildOwnerAlertCommand(params) {
72387
73586
  account: params.accountId,
72388
73587
  target: params.platform === "slack" ? `user:${params.userId}` : params.userId,
72389
73588
  message: params.message,
73589
+ messageFile: undefined,
72390
73590
  media: undefined,
72391
73591
  messageId: undefined,
72392
73592
  emoji: undefined,
@@ -72401,7 +73601,9 @@ function buildOwnerAlertCommand(params) {
72401
73601
  silent: false,
72402
73602
  progress: false,
72403
73603
  final: false,
72404
- json: false
73604
+ json: false,
73605
+ inputFormat: "md",
73606
+ renderMode: "native"
72405
73607
  };
72406
73608
  }
72407
73609
  async function sendOwnerAlert(params) {
@@ -74573,6 +75775,47 @@ async function runLoopsCli(args) {
74573
75775
  throw new Error(renderLoopsHelp());
74574
75776
  }
74575
75777
 
75778
+ // src/control/message-cli.ts
75779
+ import { readFile as readFile4 } from "node:fs/promises";
75780
+
75781
+ // src/channels/message-format.ts
75782
+ var DEFAULT_MESSAGE_INPUT_FORMAT = "md";
75783
+ var DEFAULT_MESSAGE_RENDER_MODE = "native";
75784
+ var MESSAGE_INPUT_FORMATS = [
75785
+ "plain",
75786
+ "md",
75787
+ "html",
75788
+ "mrkdwn",
75789
+ "blocks"
75790
+ ];
75791
+ var MESSAGE_RENDER_MODES = [
75792
+ "native",
75793
+ "none",
75794
+ "html",
75795
+ "mrkdwn",
75796
+ "blocks"
75797
+ ];
75798
+ function parseMessageInputFormat(raw) {
75799
+ const normalized = raw?.trim().toLowerCase();
75800
+ if (!normalized) {
75801
+ return DEFAULT_MESSAGE_INPUT_FORMAT;
75802
+ }
75803
+ if (MESSAGE_INPUT_FORMATS.includes(normalized)) {
75804
+ return normalized;
75805
+ }
75806
+ throw new Error(`--input must be one of: ${MESSAGE_INPUT_FORMATS.join(", ")}`);
75807
+ }
75808
+ function parseMessageRenderMode(raw) {
75809
+ const normalized = raw?.trim().toLowerCase();
75810
+ if (!normalized) {
75811
+ return DEFAULT_MESSAGE_RENDER_MODE;
75812
+ }
75813
+ if (MESSAGE_RENDER_MODES.includes(normalized)) {
75814
+ return normalized;
75815
+ }
75816
+ throw new Error(`--render must be one of: ${MESSAGE_RENDER_MODES.join(", ")}`);
75817
+ }
75818
+
74576
75819
  // src/control/message-cli.ts
74577
75820
  function getConfigPath() {
74578
75821
  return process.env.CLISBOT_CONFIG_PATH;
@@ -74604,6 +75847,14 @@ function parseOptionValue3(args, name) {
74604
75847
  const values = parseRepeatedOption3(args, name);
74605
75848
  return values.length > 0 ? values.at(-1) : undefined;
74606
75849
  }
75850
+ function parseMessageBodyFileOption(args) {
75851
+ const bodyFileValues = parseRepeatedOption3(args, "--body-file");
75852
+ const messageFileValues = parseRepeatedOption3(args, "--message-file");
75853
+ if (bodyFileValues.length > 0 && messageFileValues.length > 0) {
75854
+ throw new Error("--body-file and --message-file are aliases; use only one");
75855
+ }
75856
+ return bodyFileValues.at(-1) ?? messageFileValues.at(-1);
75857
+ }
74607
75858
  function parseIntegerOption(args, name) {
74608
75859
  const raw = parseOptionValue3(args, name);
74609
75860
  if (!raw) {
@@ -74644,6 +75895,7 @@ function parseMessageCommand(args) {
74644
75895
  account: parseOptionValue3(rest, "--account"),
74645
75896
  target: parseOptionValue3(rest, "--target"),
74646
75897
  message: parseOptionValue3(rest, "--message") ?? parseOptionValue3(rest, "-m"),
75898
+ messageFile: parseMessageBodyFileOption(rest),
74647
75899
  media: parseOptionValue3(rest, "--media"),
74648
75900
  messageId: parseOptionValue3(rest, "--message-id"),
74649
75901
  emoji: parseOptionValue3(rest, "--emoji"),
@@ -74658,7 +75910,9 @@ function parseMessageCommand(args) {
74658
75910
  silent: hasFlag4(rest, "--silent"),
74659
75911
  progress: hasFlag4(rest, "--progress"),
74660
75912
  final: hasFlag4(rest, "--final"),
74661
- json: hasFlag4(rest, "--json")
75913
+ json: hasFlag4(rest, "--json"),
75914
+ inputFormat: parseMessageInputFormat(parseOptionValue3(rest, "--input")),
75915
+ renderMode: parseMessageRenderMode(parseOptionValue3(rest, "--render"))
74662
75916
  };
74663
75917
  }
74664
75918
  function renderMessageHelp() {
@@ -74666,20 +75920,59 @@ function renderMessageHelp() {
74666
75920
  "clisbot message",
74667
75921
  "",
74668
75922
  "Usage:",
74669
- " clisbot message send --channel <slack|telegram> --target <dest> --message <text> [--account <id>] [--media <path-or-url>] [--reply-to <id>] [--thread-id <id>] [--force-document] [--silent] [--progress|--final]",
75923
+ " clisbot message send --channel <slack|telegram> --target <dest> [--message <text> | --body-file <path>] [--input <plain|md|html|mrkdwn|blocks>] [--render <native|none|html|mrkdwn|blocks>] [--account <id>] [--media <path-or-url>] [--reply-to <id>] [--thread-id <id>] [--force-document] [--silent] [--progress|--final]",
74670
75924
  " clisbot message poll --channel <slack|telegram> --target <dest> --poll-question <text> --poll-option <value> [--poll-option <value>] [--account <id>] [--thread-id <id>] [--silent]",
74671
75925
  " clisbot message react --channel <slack|telegram> --target <dest> --message-id <id> --emoji <emoji> [--account <id>] [--remove]",
74672
75926
  " clisbot message reactions --channel <slack|telegram> --target <dest> --message-id <id> [--account <id>]",
74673
75927
  " clisbot message read --channel <slack|telegram> --target <dest> [--account <id>] [--limit <n>]",
74674
- " clisbot message edit --channel <slack|telegram> --target <dest> --message-id <id> --message <text> [--account <id>]",
75928
+ " clisbot message edit --channel <slack|telegram> --target <dest> --message-id <id> [--message <text> | --body-file <path>] [--input <plain|md|html|mrkdwn|blocks>] [--render <native|none|html|mrkdwn|blocks>] [--account <id>]",
74675
75929
  " clisbot message delete --channel <slack|telegram> --target <dest> --message-id <id> [--account <id>]",
74676
75930
  " clisbot message pin --channel <slack|telegram> --target <dest> --message-id <id> [--account <id>]",
74677
75931
  " clisbot message unpin --channel <slack|telegram> --target <dest> [--message-id <id>] [--account <id>]",
74678
75932
  " clisbot message pins --channel <slack|telegram> --target <dest> [--account <id>]",
74679
- " clisbot message search --channel <slack|telegram> --target <dest> --query <text> [--account <id>] [--limit <n>]"
75933
+ " clisbot message search --channel <slack|telegram> --target <dest> --query <text> [--account <id>] [--limit <n>]",
75934
+ "",
75935
+ "Send/Edit Content Options:",
75936
+ " --message <text> Inline message body",
75937
+ " --body-file <path> Read the message body from a file",
75938
+ " Alias: --message-file (compat only)",
75939
+ " --input <plain|md|html|mrkdwn|blocks>",
75940
+ " Input content format. Default: md",
75941
+ " --render <native|none|html|mrkdwn|blocks>",
75942
+ " Output rendering mode. Default: native",
75943
+ "",
75944
+ "Render Rules:",
75945
+ " native Channel-owned default rendering",
75946
+ " - Telegram: Markdown/plain -> safe HTML",
75947
+ " - Slack: Markdown/plain -> mrkdwn",
75948
+ " none Content is already destination-native",
75949
+ " - Telegram: use with --input html",
75950
+ " - Slack: use with --input mrkdwn or blocks",
75951
+ " blocks Slack only. Render Markdown into Block Kit",
75952
+ " html Telegram only",
75953
+ " mrkdwn Slack only",
75954
+ "",
75955
+ "Examples:",
75956
+ ' clisbot message send --channel telegram --target -1001234567890 --thread-id 42 --message "## Status"',
75957
+ ' clisbot message send --channel telegram --target -1001234567890 --thread-id 42 --input html --render none --message "<b>Status</b>"',
75958
+ ' clisbot message send --channel slack --target channel:C1234567890 --thread-id 1712345678.123456 --message "## Status"',
75959
+ ' clisbot message send --channel slack --target channel:C1234567890 --thread-id 1712345678.123456 --input mrkdwn --render none --message "*Status*"',
75960
+ " clisbot message send --channel slack --target channel:C1234567890 --thread-id 1712345678.123456 --input blocks --render none --body-file ./reply-blocks.json"
74680
75961
  ].join(`
74681
75962
  `);
74682
75963
  }
75964
+ async function resolveCommandMessage(command) {
75965
+ if (!command.messageFile) {
75966
+ return command;
75967
+ }
75968
+ if (command.message) {
75969
+ throw new Error("--message cannot be used together with --body-file or --message-file");
75970
+ }
75971
+ return {
75972
+ ...command,
75973
+ message: await readFile4(command.messageFile, "utf8")
75974
+ };
75975
+ }
74683
75976
  function assertTarget(command) {
74684
75977
  if (!command.target) {
74685
75978
  throw new Error("--target is required");
@@ -74691,21 +75984,22 @@ async function runMessageCli(args, dependencies = defaultMessageCliDependencies)
74691
75984
  dependencies.print(renderMessageHelp());
74692
75985
  return;
74693
75986
  }
74694
- if (command.progress && command.final) {
75987
+ const resolvedCommand = await resolveCommandMessage(command);
75988
+ if (resolvedCommand.progress && resolvedCommand.final) {
74695
75989
  throw new Error("--progress and --final cannot be used together");
74696
75990
  }
74697
- assertTarget(command);
75991
+ assertTarget(resolvedCommand);
74698
75992
  const loadedConfig = await dependencies.loadConfig(getConfigPath(), {
74699
- materializeChannels: [command.channel]
75993
+ materializeChannels: [resolvedCommand.channel]
74700
75994
  });
74701
- const plugin = dependencies.plugins.find((entry) => entry.id === command.channel);
75995
+ const plugin = dependencies.plugins.find((entry) => entry.id === resolvedCommand.channel);
74702
75996
  if (!plugin) {
74703
- throw new Error(`Unsupported message channel: ${command.channel}`);
75997
+ throw new Error(`Unsupported message channel: ${resolvedCommand.channel}`);
74704
75998
  }
74705
- const execution = await plugin.runMessageCommand(loadedConfig, command);
74706
- const replyTarget = command.action === "send" || command.action === "poll" ? plugin.resolveMessageReplyTarget({
75999
+ const execution = await plugin.runMessageCommand(loadedConfig, resolvedCommand);
76000
+ const replyTarget = resolvedCommand.action === "send" || resolvedCommand.action === "poll" ? plugin.resolveMessageReplyTarget({
74707
76001
  loadedConfig,
74708
- command,
76002
+ command: resolvedCommand,
74709
76003
  accountId: execution.accountId
74710
76004
  }) : null;
74711
76005
  if (replyTarget) {
@@ -75768,7 +77062,7 @@ import { statSync as statSync5, watch } from "node:fs";
75768
77062
  import { basename as basename4, dirname as dirname15 } from "node:path";
75769
77063
 
75770
77064
  // src/channels/processed-events-store.ts
75771
- import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
77065
+ import { mkdir as mkdir2, readFile as readFile5, writeFile as writeFile2 } from "node:fs/promises";
75772
77066
  var DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
75773
77067
  var PROCESSING_STALE_MS = 30 * 60 * 1000;
75774
77068
 
@@ -75786,7 +77080,7 @@ class ProcessedEventsStore {
75786
77080
  return;
75787
77081
  }
75788
77082
  try {
75789
- const text = await readFile4(this.filePath, "utf8");
77083
+ const text = await readFile5(this.filePath, "utf8");
75790
77084
  this.document = JSON.parse(text);
75791
77085
  } catch {
75792
77086
  this.document = { events: {} };