clawgram 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/channel.js CHANGED
@@ -233,6 +233,20 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
233
233
  },
234
234
  capabilities: CHANNEL_CAPABILITIES,
235
235
  agentPrompt: {
236
+ // Core injects its `## Reactions` section only when this returns a
237
+ // level. Without it the prompt never mentions reactions, and the agent
238
+ // treats the `react` action as one more entry in a 106-property schema:
239
+ // she used it when asked outright in a DM and never once on her own.
240
+ reactionGuidance: ({ cfg, accountId }) => {
241
+ const resolvedAccountId = (0, helpers_1.resolveConfiguredAccountId)(cfg, accountId);
242
+ const account = resolvedAccountId
243
+ ? cfg?.channels?.[constants_1.CHANNEL_ID]?.accounts?.[resolvedAccountId]
244
+ : undefined;
245
+ const level = (0, reactions_1.resolveAgentReactionGuidance)(account?.reactionLevel);
246
+ // "clawgram" is our plugin id; the section reads "Reactions are
247
+ // enabled for <label>", and the label is the platform people see.
248
+ return level ? { level, channelLabel: "Telegram" } : undefined;
249
+ },
236
250
  messageToolHints: () => [
237
251
  "Use clawgram to send Telegram replies from the connected personal account.",
238
252
  "When replying in the current Telegram chat, omit `to`/`target` and clawgram will send to the current conversation automatically.",
@@ -781,7 +795,16 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
781
795
  // A suppressed silent reply legitimately delivers nothing, so
782
796
  // this fallback fires right after it. Without the same check
783
797
  // the token would be read back from the transcript and sent.
784
- const visibleFallbackText = fallbackText ? (0, helpers_1.stripSilentReplyToken)(fallbackText) : "";
798
+ //
799
+ // TTS markup needs the same treatment for the same reason:
800
+ // core strips it on the normal reply path, but this text comes
801
+ // straight out of the transcript. On 2026-08-08 a group got
802
+ // `[[tts:text]]Привет, Вася!…[[/tts:text]]` verbatim. The
803
+ // spoken words are kept — a synthesis that did not happen
804
+ // should degrade to readable text, not to markup.
805
+ const visibleFallbackText = fallbackText
806
+ ? (0, helpers_1.stripTtsDirectives)((0, helpers_1.stripSilentReplyToken)(fallbackText))
807
+ : "";
785
808
  if (fallbackText && !visibleFallbackText) {
786
809
  log?.info?.("clawgram skipping silent transcript fallback", {
787
810
  accountId,
@@ -1634,25 +1657,32 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
1634
1657
  }
1635
1658
  actionLog.info("clawgram outbound sendMedia", {
1636
1659
  accountId: ctx.accountId,
1637
- to: ctx.to,
1660
+ rawTo: ctx.to,
1638
1661
  replyToId: ctx.replyToId ?? null,
1639
1662
  threadId: ctx.threadId ?? null,
1640
1663
  filePath: ctx.filePath ?? null,
1641
1664
  mediaUrl: ctx.mediaUrl ?? null,
1642
1665
  hasText: Boolean(ctx.text),
1643
1666
  hasCaption: Boolean(ctx.caption),
1667
+ asVoice: ctx.audioAsVoice === true,
1644
1668
  });
1645
1669
  const file = ctx.filePath ?? ctx.mediaUrl;
1646
1670
  if (!file) {
1647
1671
  throw new Error("clawgram: sendMedia requires filePath or mediaUrl");
1648
1672
  }
1649
1673
  const messageThreadId = parseOptionalThreadId(ctx.threadId);
1674
+ // Same normalization `sendText` does two functions up. Without it the
1675
+ // channel prefix reaches peer resolution and the send throws — which is
1676
+ // exactly how a synthesized group reply died on 2026-08-08, silently
1677
+ // enough that the transcript fallback posted it as raw text instead.
1678
+ const target = (0, helpers_1.normalizeOutboundTarget)(ctx.to);
1650
1679
  const sent = await gram.sendMedia({
1651
- target: ctx.to,
1680
+ target,
1652
1681
  file,
1653
1682
  caption: ctx.caption ?? ctx.text,
1654
1683
  replyToMessageId: (0, helpers_1.resolveReplyToMessageIdForTarget)(ctx.to, ctx.replyToId),
1655
1684
  messageThreadId,
1685
+ asVoice: ctx.audioAsVoice === true,
1656
1686
  });
1657
1687
  actionLog.info("clawgram outbound sendMedia completed", {
1658
1688
  accountId: ctx.accountId,
package/dist/helpers.js CHANGED
@@ -16,6 +16,7 @@ exports.resolveActionTarget = resolveActionTarget;
16
16
  exports.resolveReplyToMessageIdForTarget = resolveReplyToMessageIdForTarget;
17
17
  exports.readMessageText = readMessageText;
18
18
  exports.readVoiceNoteFlag = readVoiceNoteFlag;
19
+ exports.stripTtsDirectives = stripTtsDirectives;
19
20
  exports.resolveAllowFrom = resolveAllowFrom;
20
21
  exports.resolveGroupPolicy = resolveGroupPolicy;
21
22
  exports.resolveGroups = resolveGroups;
@@ -194,6 +195,28 @@ function resolveReplyToMessageIdForTarget(rawTarget, replyToId) {
194
195
  }
195
196
  return undefined;
196
197
  }
198
+ /**
199
+ * Разметка синтеза речи, которая не должна доехать до человека.
200
+ *
201
+ * Core вырезает `[[tts:...]]` из видимого текста сам, но только на штатном
202
+ * пути ответа. Аварийный путь (`readLatestAssistantFallbackFromTranscript`)
203
+ * читает сырой текст из стенограммы, поэтому 2026-08-08 в групповой чат
204
+ * ушло `[[tts:text]]Привет, Вася!…[[/tts:text]]` как есть.
205
+ *
206
+ * Блок `[[tts:text]]…[[/tts:text]]` РАЗВОРАЧИВАЕТСЯ, а не удаляется: внутри
207
+ * лежит то, что агент собирался сказать. Если синтез не состоялся, человек
208
+ * должен получить эти слова текстом — деградация в читаемое, а не в мусор
209
+ * и не в пустоту.
210
+ */
211
+ const TTS_TEXT_BLOCK = /\[\[tts:text\]\]([\s\S]*?)\[\[\/tts:text\]\]/gi;
212
+ const TTS_DIRECTIVE = /\[\[\s*\/?\s*(?:tts:[^\]]*|audio_as_voice)\s*\]\]/gi;
213
+ function stripTtsDirectives(text) {
214
+ return text
215
+ .replace(TTS_TEXT_BLOCK, (_match, spoken) => spoken)
216
+ .replace(TTS_DIRECTIVE, "")
217
+ .replace(/[ \t]{2,}/g, " ")
218
+ .trim();
219
+ }
197
220
  /**
198
221
  * Whether an outbound file should become a Telegram voice message.
199
222
  *
package/dist/reactions.js CHANGED
@@ -11,7 +11,47 @@
11
11
  * tested without a Telegram connection.
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.resolveAgentReactionGuidance = resolveAgentReactionGuidance;
14
15
  exports.parseReactionParams = parseReactionParams;
16
+ /**
17
+ * How chatty the agent may be with reactions, as core understands it.
18
+ *
19
+ * Core injects a `## Reactions` section into the system prompt only when a
20
+ * channel returns a level from `agentPrompt.reactionGuidance`. Return nothing
21
+ * and the prompt never mentions reactions at all — which is what happened
22
+ * here until 2.8.0: the `react` action existed, the agent had it, and no line
23
+ * of the prompt suggested using it.
24
+ *
25
+ * Levels and fallbacks mirror the bundled Telegram channel so the two behave
26
+ * the same for the same config:
27
+ *
28
+ * - `off`, `ack` — no agent reactions (`ack` is the "seen it" emoji core sends
29
+ * by itself, which is a different feature);
30
+ * - `minimal` — react sparingly; the default when nothing is configured;
31
+ * - `extensive` — react whenever it feels natural.
32
+ *
33
+ * An invalid value yields no guidance rather than a guess: a typo turning a
34
+ * work chat chatty is worse than a typo turning it quiet.
35
+ */
36
+ function resolveAgentReactionGuidance(value) {
37
+ if (value === undefined || value === null) {
38
+ return "minimal";
39
+ }
40
+ if (typeof value !== "string") {
41
+ return undefined;
42
+ }
43
+ const level = value.trim();
44
+ if (!level) {
45
+ return "minimal";
46
+ }
47
+ if (level === "minimal" || level === "extensive") {
48
+ return level;
49
+ }
50
+ if (level === "off" || level === "ack") {
51
+ return undefined;
52
+ }
53
+ return undefined;
54
+ }
15
55
  /** Accepts the boolean and the string a JSON-ish caller may send for it. */
16
56
  function readBooleanFlag(value) {
17
57
  return value === true || value === "true";
@@ -2,7 +2,7 @@
2
2
  "id": "clawgram",
3
3
  "name": "Clawgram",
4
4
  "description": "Clawgram — personal Telegram (MTProto userbot) channel for OpenClaw. Your AI assistant reads and responds as you.",
5
- "version": "2.7.0",
5
+ "version": "2.8.0",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
@@ -326,6 +326,16 @@
326
326
  "type": "string",
327
327
  "description": "Where joins observed for this account are journalled. Defaults to a per-account file under the OpenClaw state directory."
328
328
  },
329
+ "reactionLevel": {
330
+ "type": "string",
331
+ "enum": [
332
+ "off",
333
+ "ack",
334
+ "minimal",
335
+ "extensive"
336
+ ],
337
+ "description": "How freely the agent may leave emoji reactions (2.8.0). Core injects its `## Reactions` prompt section only for `minimal` and `extensive`; `off` and `ack` leave the prompt silent about reactions, and the agent then effectively never reacts on its own. Absent means `minimal`. Mirrors the bundled Telegram channel's reactionLevel."
338
+ },
329
339
  "replyParseMode": {
330
340
  "type": "string",
331
341
  "enum": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawgram",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "Clawgram — personal Telegram (MTProto userbot) channel for OpenClaw. Your AI assistant reads and responds as you.",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {