@lapage/codex-telegram-bridge 0.1.0 → 0.1.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/dist/text.js CHANGED
@@ -8,45 +8,6 @@ export function normalizeTerminalOutput(data) {
8
8
  function visibleText(data) {
9
9
  return normalizeTerminalOutput(data);
10
10
  }
11
- export function cleanPaneSnapshot(data) {
12
- const lines = paneLines(data);
13
- return collapseRepeatedLines(lines).join('\n').trim();
14
- }
15
- export function latestCompletedCodexResponse(data) {
16
- const lines = paneLines(data);
17
- if (isCodexWorking(data)) {
18
- return null;
19
- }
20
- return latestCodexResponse(data);
21
- }
22
- export function latestCodexResponse(data) {
23
- const lines = paneLines(data);
24
- const lastResponseBullet = findLastIndex(lines, (line) => {
25
- const trimmed = visibleText(line).trimStart();
26
- return trimmed.startsWith('• ') && !/^•\s*Working\s*\(/i.test(trimmed);
27
- });
28
- if (lastResponseBullet === -1) {
29
- return null;
30
- }
31
- const promptStart = findLastIndex(lines.slice(0, lastResponseBullet), (line) => isUserPromptLine(visibleText(line).trim()));
32
- const responseStart = promptStart === -1 ? lastResponseBullet : promptStart + 1;
33
- const response = [];
34
- for (const line of lines.slice(responseStart)) {
35
- const trimmed = visibleText(line).trim();
36
- if (response.length > 0 && isPromptOrStatusLine(trimmed)) {
37
- break;
38
- }
39
- if (isIgnorableResponseLine(trimmed)) {
40
- continue;
41
- }
42
- response.push(line);
43
- }
44
- const cleaned = collapseRepeatedLines(response).join('\n').trim();
45
- return cleaned || null;
46
- }
47
- export function isCodexWorking(data) {
48
- return paneLines(data).some((line) => /esc to interrupt|^\s*•\s*Working\s*\(/i.test(visibleText(line)));
49
- }
50
11
  export function chunkText(text, maxLength) {
51
12
  const chunks = [];
52
13
  let remaining = text;
@@ -70,11 +31,56 @@ export function formatTelegramMarkdown(text) {
70
31
  .replace(/\n{3,}/g, '\n\n')
71
32
  .trim();
72
33
  }
34
+ export function formatTelegramMarkdownChunks(text, maxLength) {
35
+ const lines = formatTelegramMarkdownLines(text);
36
+ const chunks = [];
37
+ let current = '';
38
+ for (const line of lines) {
39
+ const nextLine = current ? `\n\n${line}` : line;
40
+ if (current && current.length + nextLine.length > maxLength) {
41
+ chunks.push(current);
42
+ current = line;
43
+ continue;
44
+ }
45
+ current += nextLine;
46
+ }
47
+ if (current) {
48
+ chunks.push(current);
49
+ }
50
+ return chunks.flatMap((chunk) => chunk.length > maxLength ? hardSplitText(chunk, maxLength) : [chunk]);
51
+ }
73
52
  export function formatTelegramMarkdownLines(text) {
74
- return normalizeWrappedLines(text)
75
- .split('\n')
76
- .map(formatTelegramLine)
77
- .filter((line) => line.trim().length > 0);
53
+ const lines = normalizeTerminalOutput(text).split('\n');
54
+ const formatted = [];
55
+ let codeFenceLanguage = '';
56
+ let codeFenceLines = [];
57
+ for (const line of lines) {
58
+ const trimmed = visibleText(line).trim();
59
+ const fenceMatch = trimmed.match(/^```([a-zA-Z0-9_-]*)\s*$/);
60
+ if (fenceMatch && codeFenceLines.length === 0 && codeFenceLanguage === '') {
61
+ codeFenceLanguage = fenceMatch[1] || 'text';
62
+ codeFenceLines = [];
63
+ continue;
64
+ }
65
+ if (trimmed === '```' && codeFenceLanguage) {
66
+ formatted.push(formatCodeBlock(codeFenceLines.join('\n'), codeFenceLanguage));
67
+ codeFenceLanguage = '';
68
+ codeFenceLines = [];
69
+ continue;
70
+ }
71
+ if (codeFenceLanguage) {
72
+ codeFenceLines.push(line);
73
+ continue;
74
+ }
75
+ const formattedLine = formatTelegramLine(line);
76
+ if (formattedLine.trim().length > 0) {
77
+ formatted.push(formattedLine);
78
+ }
79
+ }
80
+ if (codeFenceLanguage) {
81
+ formatted.push(formatCodeBlock(codeFenceLines.join('\n'), codeFenceLanguage));
82
+ }
83
+ return formatted;
78
84
  }
79
85
  export function plainTelegramText(text) {
80
86
  return plainTelegramLines(text)
@@ -82,57 +88,24 @@ export function plainTelegramText(text) {
82
88
  .replace(/\n{3,}/g, '\n\n')
83
89
  .trim();
84
90
  }
91
+ export function safePlainTelegramText(text) {
92
+ return plainTelegramText(text)
93
+ .replace(/^```[a-zA-Z0-9_-]*\s*$/gm, '')
94
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
95
+ .replace(/`([^`]+)`/g, '$1')
96
+ .replace(/^\s*-\s+/gm, '• ')
97
+ .replace(/^\s{2,}•\s+/gm, ' ◦ ')
98
+ .trim();
99
+ }
100
+ export function safePlainTelegramChunks(text, maxLength) {
101
+ return chunkText(safePlainTelegramText(text), maxLength);
102
+ }
85
103
  export function plainTelegramLines(text) {
86
104
  return normalizeWrappedLines(text)
87
105
  .split('\n')
88
106
  .map((line) => line.trimEnd())
89
107
  .filter((line) => line.trim().length > 0);
90
108
  }
91
- function isDecorativeLine(line) {
92
- const trimmed = visibleText(line).trim();
93
- if (/^[╭╮╰╯│─┌┐└┘├┤┬┴┼╞╡═\s]+$/.test(trimmed)) {
94
- return true;
95
- }
96
- if (/^Tip: /.test(trimmed)) {
97
- return true;
98
- }
99
- if (/^model:\s+/.test(trimmed) || /^directory:\s+/.test(trimmed)) {
100
- return true;
101
- }
102
- if (/^>_ OpenAI Codex/.test(trimmed)) {
103
- return true;
104
- }
105
- return false;
106
- }
107
- function paneLines(data) {
108
- return normalizeTerminalOutput(data)
109
- .split('\n')
110
- .map((line) => line.trimEnd())
111
- .filter((line) => visibleText(line).trim().length > 0)
112
- .filter((line) => !isDecorativeLine(line))
113
- .filter((line) => !isIgnorablePaneLine(visibleText(line).trim()));
114
- }
115
- function isPromptOrStatusLine(trimmed) {
116
- return isUserPromptLine(trimmed)
117
- || /^agentic\s+/.test(trimmed)
118
- || /^•\s*Working\s*\(/i.test(trimmed)
119
- || /^─\s*Worked for\b/.test(trimmed);
120
- }
121
- function isUserPromptLine(trimmed) {
122
- return trimmed.startsWith('› ');
123
- }
124
- function isIgnorablePaneLine(trimmed) {
125
- return /^⚠ Model metadata for `agentic` not found/.test(trimmed)
126
- || /^can degrade performance and cause issues\.$/.test(trimmed)
127
- || /^fallback metadata;/.test(trimmed)
128
- || /^issues\.$/.test(trimmed)
129
- || /^─\s*Worked for\b/.test(trimmed)
130
- || /^agentic\s+/.test(trimmed)
131
- || /^› (Write tests for @filename|Summarize recent commits|Implement \{feature\}|Improve documentation in @filename)/.test(trimmed);
132
- }
133
- function isIgnorableResponseLine(trimmed) {
134
- return isIgnorablePaneLine(trimmed) || /^⚠ /.test(trimmed);
135
- }
136
109
  function normalizeWrappedLines(text) {
137
110
  const lines = text.split('\n');
138
111
  const normalized = [];
@@ -176,27 +149,16 @@ function shouldJoinSoftWrap(previous, current) {
176
149
  return !/[.!?:;)]$/.test(trimmedPrevious) && /^[a-z0-9(/]/i.test(trimmedCurrent);
177
150
  }
178
151
  function formatTelegramLine(line) {
179
- const style = lineStyle(line);
180
152
  const trimmed = visibleText(line).trim();
181
153
  if (!trimmed) {
182
154
  return '';
183
155
  }
184
- if (style.hasBold && /(?:^•\s+)?Ran\b/.test(trimmed)) {
185
- const command = trimmed.replace(/^•\s+Ran\s+/, '').replace(/^Ran\s+/, '');
186
- return blockQuote(`🔧 *Ran* ${inlineCode(command)}`);
187
- }
188
156
  if (trimmed.startsWith('• Ran ')) {
189
157
  return blockQuote(`🔧 *Ran* ${inlineCode(trimmed.slice('• Ran '.length))}`);
190
158
  }
191
- if (style.hasBold && /background terminal/i.test(trimmed)) {
192
- return blockQuote(`🔧 _${escapeMarkdownV2(trimmed.replace(/^•\s+/, ''))}_`);
193
- }
194
159
  if (trimmed.startsWith('• Waited for background terminal')) {
195
160
  return blockQuote(`🔧 _${escapeMarkdownV2(trimmed.slice(2))}_`);
196
161
  }
197
- if (style.hasDim && isThinkingLine(trimmed)) {
198
- return blockQuote(`🧠 _${escapeMarkdownV2(trimmed.slice(2))}_`);
199
- }
200
162
  if (trimmed.startsWith('↳ Interacted with background terminal')) {
201
163
  return blockQuote(`🔧 _${escapeMarkdownV2(trimmed)}_`);
202
164
  }
@@ -210,33 +172,70 @@ function formatTelegramLine(line) {
210
172
  return `• ${escapeMarkdownV2(trimmed.slice(2))}`;
211
173
  }
212
174
  if (trimmed.startsWith('- ')) {
213
- return ` ◦ ${escapeMarkdownV2(trimmed.slice(2))}`;
175
+ return `• ${formatInlineMarkdown(trimmed.slice(2))}`;
214
176
  }
215
177
  if (/^\s{2,}\S/.test(visibleText(line))) {
216
178
  return ` ${escapeMarkdownV2(trimmed)}`;
217
179
  }
218
- return escapeMarkdownV2(trimmed);
180
+ return formatInlineMarkdown(trimmed);
181
+ }
182
+ function hardSplitText(text, maxLength) {
183
+ const chunks = [];
184
+ let remaining = text;
185
+ while (remaining.length > maxLength) {
186
+ chunks.push(remaining.slice(0, maxLength));
187
+ remaining = remaining.slice(maxLength);
188
+ }
189
+ if (remaining) {
190
+ chunks.push(remaining);
191
+ }
192
+ return chunks;
219
193
  }
220
194
  function inlineCode(text) {
221
195
  return `\`${text.replace(/[`\\]/g, '\\$&')}\``;
222
196
  }
223
- function lineStyle(line) {
224
- const sgrMatches = line.match(/\x1b\[[0-9;]*m/g) ?? [];
225
- let hasDim = false;
226
- let hasBold = false;
227
- for (const match of sgrMatches) {
228
- const codes = match.slice(2, -1).split(';').map((code) => Number(code || 0));
229
- if (codes.includes(0)) {
230
- continue;
197
+ function formatCodeBlock(text, language) {
198
+ return `\`\`\`${escapeCodeFenceLanguage(language)}\n${escapePreText(text)}\n\`\`\``;
199
+ }
200
+ function escapeCodeFenceLanguage(language) {
201
+ return language.replace(/[^a-zA-Z0-9_-]/g, '') || 'text';
202
+ }
203
+ function escapePreText(text) {
204
+ return text.replace(/[`\\]/g, '\\$&');
205
+ }
206
+ function formatInlineMarkdown(text) {
207
+ const segments = splitInlineMarkdown(text);
208
+ return segments.map((segment) => {
209
+ if (segment.type === 'code') {
210
+ return inlineCode(segment.text);
211
+ }
212
+ if (segment.type === 'bold') {
213
+ return `*${escapeMarkdownV2(segment.text)}*`;
214
+ }
215
+ return escapeMarkdownV2(segment.text);
216
+ }).join('');
217
+ }
218
+ function splitInlineMarkdown(text) {
219
+ const segments = [];
220
+ const pattern = /`([^`]+)`|\*\*([^*]+)\*\*/g;
221
+ let index = 0;
222
+ let match;
223
+ while ((match = pattern.exec(text))) {
224
+ if (match.index > index) {
225
+ segments.push({ type: 'plain', text: text.slice(index, match.index) });
231
226
  }
232
- if (codes.includes(1)) {
233
- hasBold = true;
227
+ if (match[1] !== undefined) {
228
+ segments.push({ type: 'code', text: match[1] });
234
229
  }
235
- if (codes.includes(2)) {
236
- hasDim = true;
230
+ else if (match[2] !== undefined) {
231
+ segments.push({ type: 'bold', text: match[2] });
237
232
  }
233
+ index = pattern.lastIndex;
238
234
  }
239
- return { hasDim, hasBold };
235
+ if (index < text.length) {
236
+ segments.push({ type: 'plain', text: text.slice(index) });
237
+ }
238
+ return segments;
240
239
  }
241
240
  function blockQuote(markdown) {
242
241
  return markdown
@@ -244,29 +243,6 @@ function blockQuote(markdown) {
244
243
  .map((line) => `> ${line}`)
245
244
  .join('\n');
246
245
  }
247
- function isThinkingLine(trimmed) {
248
- return /^•\s+I(?:’|'|`)?m\s+(thinking|considering|checking|looking|trying|wondering|deciding|figuring|reasoning|planning)\b/i.test(trimmed)
249
- || /^•\s+I\s+need\s+to\s+think\b/i.test(trimmed)
250
- || /^•\s+I\s+need\s+to\s+(provide|answer|decide|figure|check|inspect|verify)\b/i.test(trimmed)
251
- || /^•\s+Let\s+me\s+think\b/i.test(trimmed);
252
- }
253
- function findLastIndex(items, predicate) {
254
- for (let index = items.length - 1; index >= 0; index -= 1) {
255
- if (predicate(items[index])) {
256
- return index;
257
- }
258
- }
259
- return -1;
260
- }
261
- function collapseRepeatedLines(lines) {
262
- const collapsed = [];
263
- for (const line of lines) {
264
- if (collapsed[collapsed.length - 1] !== line) {
265
- collapsed.push(line);
266
- }
267
- }
268
- return collapsed;
269
- }
270
246
  function escapeMarkdownV2(text) {
271
247
  return text.replace(/([_*.\[\]()~`>#+\-=|{}!\\])/g, '\\$1');
272
248
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lapage/codex-telegram-bridge",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Private Telegram bridge for controlling Codex CLI on a home PC or server without opening inbound ports.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,7 +24,7 @@
24
24
  "codex-cli",
25
25
  "telegram",
26
26
  "telegram-bot",
27
- "tmux",
27
+ "app-server",
28
28
  "remote-control",
29
29
  "agent"
30
30
  ],
package/dist/tmux.js DELETED
@@ -1,25 +0,0 @@
1
- import { execFile } from 'node:child_process';
2
- import { promisify } from 'node:util';
3
- const execFileAsync = promisify(execFile);
4
- export async function runTmux(args) {
5
- const { stdout } = await execFileAsync('tmux', args, { env: process.env });
6
- return stdout;
7
- }
8
- export async function tmuxSessionExists(session) {
9
- try {
10
- await runTmux(['has-session', '-t', session]);
11
- return true;
12
- }
13
- catch {
14
- return false;
15
- }
16
- }
17
- export async function capturePane(session, rows) {
18
- return runTmux(['capture-pane', '-p', '-t', session, '-S', `-${rows}`]);
19
- }
20
- export function shellCommand(parts) {
21
- return parts.map(shellQuote).join(' ');
22
- }
23
- function shellQuote(value) {
24
- return `'${value.replace(/'/g, `'\\''`)}'`;
25
- }