@s0nderlabs/anima-plugin-telegram 0.22.0 → 0.22.1
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/package.json +2 -2
- package/src/markdown.test.ts +39 -0
- package/src/markdown.ts +50 -1
- package/src/progress.ts +22 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@s0nderlabs/anima-plugin-telegram",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Telegram gateway plugin for anima — long-poll bot, debounced dispatch, reactions, allowlist",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"test": "bun test"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@s0nderlabs/anima-core": "0.22.
|
|
42
|
+
"@s0nderlabs/anima-core": "0.22.1",
|
|
43
43
|
"grammy": "^1.42.0",
|
|
44
44
|
"zod": "^3.23.8"
|
|
45
45
|
}
|
package/src/markdown.test.ts
CHANGED
|
@@ -4,8 +4,47 @@ import {
|
|
|
4
4
|
formatMarkdownV2,
|
|
5
5
|
isMarkdownParseError,
|
|
6
6
|
stripMarkdownV2,
|
|
7
|
+
wrapGfmTablesInCodeBlocks,
|
|
7
8
|
} from './markdown'
|
|
8
9
|
|
|
10
|
+
describe('wrapGfmTablesInCodeBlocks (v0.22.1)', () => {
|
|
11
|
+
it('wraps a basic 3x3 GFM table in fences', () => {
|
|
12
|
+
const input = `Here you go:
|
|
13
|
+
|
|
14
|
+
| Mode | Behavior | Modal |
|
|
15
|
+
|--------|----------|-------|
|
|
16
|
+
| yolo | auto | no |
|
|
17
|
+
| prompt | approve | yes |
|
|
18
|
+
| strict | deny | no |
|
|
19
|
+
|
|
20
|
+
That's the table.`
|
|
21
|
+
const out = wrapGfmTablesInCodeBlocks(input)
|
|
22
|
+
expect(out).toContain('```\n| Mode')
|
|
23
|
+
expect(out).toContain('| strict | deny | no |\n```')
|
|
24
|
+
expect(out).toContain("That's the table.")
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('passes through text with no tables unchanged', () => {
|
|
28
|
+
expect(wrapGfmTablesInCodeBlocks('just prose')).toBe('just prose')
|
|
29
|
+
expect(wrapGfmTablesInCodeBlocks('| not | a table without separator')).toBe(
|
|
30
|
+
'| not | a table without separator',
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('handles alignment colons in separator row', () => {
|
|
35
|
+
const input = `| a | b |
|
|
36
|
+
|:--|--:|
|
|
37
|
+
| 1 | 2 |`
|
|
38
|
+
const out = wrapGfmTablesInCodeBlocks(input)
|
|
39
|
+
expect(out.startsWith('```')).toBe(true)
|
|
40
|
+
expect(out.endsWith('```')).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('keeps non-table pipes intact', () => {
|
|
44
|
+
expect(wrapGfmTablesInCodeBlocks('use | as separator')).toBe('use | as separator')
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
9
48
|
describe('escapeMarkdownV2', () => {
|
|
10
49
|
it('escapes all reserved characters', () => {
|
|
11
50
|
expect(escapeMarkdownV2('a_b*c')).toBe('a\\_b\\*c')
|
package/src/markdown.ts
CHANGED
|
@@ -64,6 +64,13 @@ export function isMarkdownParseError(err: unknown): boolean {
|
|
|
64
64
|
export function formatMarkdownV2(content: string): string {
|
|
65
65
|
if (!content) return content
|
|
66
66
|
|
|
67
|
+
// GFM tables don't render in MarkdownV2 — pipes show literally and columns
|
|
68
|
+
// misalign. Wrap detected table blocks in ``` fences so TG renders them
|
|
69
|
+
// monospace + preserves column alignment. Detection: a line starting with
|
|
70
|
+
// `|` followed by a separator row (`|---|---|`) makes the start of a table;
|
|
71
|
+
// contiguous `|...|` lines are pulled in until the first non-table line.
|
|
72
|
+
const wrapped = wrapGfmTablesInCodeBlocks(content)
|
|
73
|
+
|
|
67
74
|
const placeholders: string[] = []
|
|
68
75
|
const ph = (value: string): string => {
|
|
69
76
|
const key = `\x00PH${placeholders.length}\x00`
|
|
@@ -71,7 +78,7 @@ export function formatMarkdownV2(content: string): string {
|
|
|
71
78
|
return key
|
|
72
79
|
}
|
|
73
80
|
|
|
74
|
-
let text =
|
|
81
|
+
let text = wrapped
|
|
75
82
|
|
|
76
83
|
text = text.replace(/```(?:[^\n]*\n)?[\s\S]*?```/g, raw => {
|
|
77
84
|
const newlineIdx = raw.indexOf('\n', 3)
|
|
@@ -145,6 +152,48 @@ function escapeStrayParens(text: string): string {
|
|
|
145
152
|
.join('')
|
|
146
153
|
}
|
|
147
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Detect GFM table blocks in the brain's reply and wrap them in ``` fences.
|
|
157
|
+
* TG MarkdownV2 doesn't render tables; without the fence, pipes show literally
|
|
158
|
+
* and columns drift. With the fence, TG renders monospace + preserves the
|
|
159
|
+
* brain's space padding so columns line up.
|
|
160
|
+
*
|
|
161
|
+
* Table boundary: a `|...|` row immediately followed by a separator row
|
|
162
|
+
* `|---|---|` (or `:---:`, `---|---`, etc.) is the start. Contiguous `|...|`
|
|
163
|
+
* data rows are pulled into the same block. First non-pipe line ends it.
|
|
164
|
+
*/
|
|
165
|
+
const TABLE_SEPARATOR_RE = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/
|
|
166
|
+
const TABLE_ROW_RE = /^\s*\|.+\|?\s*$/
|
|
167
|
+
|
|
168
|
+
export function wrapGfmTablesInCodeBlocks(text: string): string {
|
|
169
|
+
const lines = text.split('\n')
|
|
170
|
+
const out: string[] = []
|
|
171
|
+
let i = 0
|
|
172
|
+
while (i < lines.length) {
|
|
173
|
+
const line = lines[i] ?? ''
|
|
174
|
+
if (
|
|
175
|
+
TABLE_ROW_RE.test(line) &&
|
|
176
|
+
i + 1 < lines.length &&
|
|
177
|
+
TABLE_SEPARATOR_RE.test(lines[i + 1] ?? '')
|
|
178
|
+
) {
|
|
179
|
+
const block: string[] = [line, lines[i + 1] ?? '']
|
|
180
|
+
let j = i + 2
|
|
181
|
+
while (j < lines.length && TABLE_ROW_RE.test(lines[j] ?? '')) {
|
|
182
|
+
block.push(lines[j] ?? '')
|
|
183
|
+
j += 1
|
|
184
|
+
}
|
|
185
|
+
out.push('```')
|
|
186
|
+
out.push(...block)
|
|
187
|
+
out.push('```')
|
|
188
|
+
i = j
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
out.push(line)
|
|
192
|
+
i += 1
|
|
193
|
+
}
|
|
194
|
+
return out.join('\n')
|
|
195
|
+
}
|
|
196
|
+
|
|
148
197
|
function isInsideLinkUrl(seg: string, closeIdx: number): boolean {
|
|
149
198
|
let depth = 0
|
|
150
199
|
for (let j = closeIdx - 1; j >= Math.max(closeIdx - 2000, 0); j--) {
|
package/src/progress.ts
CHANGED
|
@@ -94,6 +94,15 @@ export class ProgressTracker {
|
|
|
94
94
|
private canEdit = true
|
|
95
95
|
private pendingTimer: ReturnType<typeof setTimeout> | null = null
|
|
96
96
|
private finalized = false
|
|
97
|
+
/**
|
|
98
|
+
* Serialize all flush operations so the start-event's sendMessage finishes
|
|
99
|
+
* (assigning messageId) before any end-event's flush runs. Without this
|
|
100
|
+
* lock, fast tools that fire start+end within ~5ms (e.g. strict-deny path)
|
|
101
|
+
* would race two parallel sendMessage calls, producing a duplicate "tool
|
|
102
|
+
* starting" message followed by a separate "tool ended ✗" message instead
|
|
103
|
+
* of one in-place edit. v0.22.1 fix.
|
|
104
|
+
*/
|
|
105
|
+
private flushLock: Promise<void> = Promise.resolve()
|
|
97
106
|
|
|
98
107
|
constructor(
|
|
99
108
|
private readonly bot: Bot,
|
|
@@ -118,7 +127,13 @@ export class ProgressTracker {
|
|
|
118
127
|
if (idx == null || this.lines[idx] == null) return
|
|
119
128
|
this.lines[idx] = `${this.lines[idx]} ${ev.ok === false ? '✗' : '✓'}`
|
|
120
129
|
}
|
|
121
|
-
|
|
130
|
+
// Serialize: subsequent flushes wait for any in-flight sendMessage to
|
|
131
|
+
// resolve so the second flush sees the assigned messageId and routes to
|
|
132
|
+
// editMessageText, not a second sendMessage. v0.22.1 fix for fast-tool
|
|
133
|
+
// double-message regression.
|
|
134
|
+
const previous = this.flushLock
|
|
135
|
+
this.flushLock = previous.then(() => this.flush()).catch(() => {})
|
|
136
|
+
await this.flushLock
|
|
122
137
|
}
|
|
123
138
|
|
|
124
139
|
/**
|
|
@@ -131,7 +146,12 @@ export class ProgressTracker {
|
|
|
131
146
|
clearTimeout(this.pendingTimer)
|
|
132
147
|
this.pendingTimer = null
|
|
133
148
|
}
|
|
134
|
-
|
|
149
|
+
// Serialize through the lock so finalize doesn't race a still-flying
|
|
150
|
+
// push from the brain. Important for fast tools where end-event flush
|
|
151
|
+
// and finalize() race the same scratch message edit.
|
|
152
|
+
const previous = this.flushLock
|
|
153
|
+
this.flushLock = previous.then(() => this.flush(true)).catch(() => {})
|
|
154
|
+
await this.flushLock
|
|
135
155
|
this.finalized = true
|
|
136
156
|
}
|
|
137
157
|
|