@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/.env.example +21 -9
- package/README.md +20 -46
- package/dist/codex-session.js +212 -39
- package/dist/config.js +2 -12
- package/dist/env.js +2 -9
- package/dist/telegram-bridge.js +290 -150
- package/dist/text.js +113 -137
- package/package.json +2 -2
- package/dist/tmux.js +0 -25
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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
|
|
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
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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 (
|
|
233
|
-
|
|
227
|
+
if (match[1] !== undefined) {
|
|
228
|
+
segments.push({ type: 'code', text: match[1] });
|
|
234
229
|
}
|
|
235
|
-
if (
|
|
236
|
-
|
|
230
|
+
else if (match[2] !== undefined) {
|
|
231
|
+
segments.push({ type: 'bold', text: match[2] });
|
|
237
232
|
}
|
|
233
|
+
index = pattern.lastIndex;
|
|
238
234
|
}
|
|
239
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
}
|