agentp 0.9.0 → 0.10.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/AGENTS.md +7 -2
- package/CHANGELOG.md +16 -4
- package/README.md +31 -2
- package/bin/agentp +92 -13
- package/bin/ocmux +1 -1
- package/bin/tgagentp +116 -9
- package/devto-article.md +121 -0
- package/lib/opencode.js +29 -1
- package/package.json +1 -1
- package/debugging_tg_integration.md +0 -5594
- package/tgagentp.log +0 -2156
package/AGENTS.md
CHANGED
|
@@ -86,6 +86,8 @@ These endpoints are consumed but not documented elsewhere in the repo:
|
|
|
86
86
|
- Messages for the active server are delivered immediately; others are queued per-server.
|
|
87
87
|
- Debounced Telegram notification (configurable via `TGAGENTP_DEBOUNCE_MS`, default 5000ms).
|
|
88
88
|
- Queued messages are flushed on `/servers switch`.
|
|
89
|
+
- Gateway response includes `{ buffered: [...] }` — recorded conversation messages (see `/record` command) flushed on each POST.
|
|
90
|
+
- agentp `--qa` prepends the buffered context to stdout; `--flush` bypasses prepending but still triggers the flush.
|
|
89
91
|
|
|
90
92
|
## OpenCode tmux session model (ocmux)
|
|
91
93
|
|
|
@@ -103,7 +105,7 @@ These endpoints are consumed but not documented elsewhere in the repo:
|
|
|
103
105
|
|---|---|---|
|
|
104
106
|
| `npm link` | local dev install |
|
|
105
107
|
| `npm install -g .` | alternative local install |
|
|
106
|
-
| `agentp [--qa] [--tg|--no-tg] [port]` | pipe stdin → opencode session, stream answer to stdout (uses session API); `--tg` forwards answer to Telegram |
|
|
108
|
+
| `agentp [--qa] [--tg|--no-tg] [--flush] [--getLast n] [port]` | pipe stdin → opencode session, stream answer to stdout (uses session API); `--tg` forwards answer to Telegram; `--flush` clears recorded buffer; `--getLast n` retrieves last n answers |
|
|
107
109
|
| `tgagentp [port]` | bridge Telegram bot ↔ opencode TUI (needs `TELEGRAM_BOT_TOKEN`) |
|
|
108
110
|
| `tgagentp --dev` | enable `/shutdown` command for remote restart |
|
|
109
111
|
| `ocmux serve [--print-logs] [dir]` | start opencode serve in a tmux window (primary verb) |
|
|
@@ -116,7 +118,7 @@ These endpoints are consumed but not documented elsewhere in the repo:
|
|
|
116
118
|
|
|
117
119
|
### Pending
|
|
118
120
|
|
|
119
|
-
|
|
121
|
+
*(none)*
|
|
120
122
|
|
|
121
123
|
### In Progress
|
|
122
124
|
|
|
@@ -124,6 +126,9 @@ These endpoints are consumed but not documented elsewhere in the repo:
|
|
|
124
126
|
|
|
125
127
|
### Done
|
|
126
128
|
|
|
129
|
+
- `/queue` command: queue messages when server is busy, auto-sent after current task finishes preserves replyTo chain — 0.10.0
|
|
130
|
+
- `agentp --getLast n` — retrieve last n answers from session history — 0.10.0
|
|
131
|
+
- `/record` command with ring buffer (100 msgs / 100KB), gateway returns `{ buffered }`, agentp `--qa` prepends recorded context, `--flush` flushes without prepending — 0.10.0
|
|
127
132
|
- agentp `--qa` full context forwarding to Telegram (rulers, prompt, answer) — 0.9.0
|
|
128
133
|
- agentp resilience: 5s HTTP timeout, pre-send gate check, post-send warning (not hard error) for `--tg`; auto mode silently degrades — 0.9.0
|
|
129
134
|
- tgagentp: `lockedChatId` set at startup to fix race condition with agentp gateway — 0.9.0
|
package/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [
|
|
5
|
+
## [0.10.0] - 2026-06-04
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
7
|
+
- `/queue` command: queue messages when server is busy, auto-sent after current task finishes, preserves replyTo chain.
|
|
8
|
+
- `/record` command with ring buffer (100 msgs / 100KB), `/record stop` to clear; gateway returns `{ buffered }` JSON.
|
|
9
|
+
- agentp `--flush`: flush tgagentp's recorded buffer without prepending to stdout.
|
|
10
|
+
- agentp `--getLast n`: retrieve last n assistant answers from session history.
|
|
11
|
+
- agentp `--qa` prepends recorded Telegram context (with rulers) to stdout.
|
|
12
|
+
- `getSession()` in lib/opencode: `GET /session/:id` with fallback to session list.
|
|
13
|
+
- `makeRequest` optional timeout parameter (used only for getSession; all other calls wait indefinitely).
|
|
14
|
+
- Minimum 3-backtick code fence for reply quoting.
|
|
15
|
+
|
|
16
|
+
## [0.9.0] - 2026-06-04
|
|
17
|
+
|
|
18
|
+
- agentp `--tg`/`--no-tg` flags: gateway forwards answer to Telegram; auto mode silently degrades.
|
|
19
|
+
- Agentp resilience: 5s HTTP timeout, pre-send gate check, post-send warning.
|
|
20
|
+
- Full `--qa` output (rulers + prompt + answer) forwarded to Telegram.
|
|
21
|
+
- Rulers changed from `—` to `─`, shortened to 17 chars.
|
|
10
22
|
|
|
11
23
|
## [0.8.0] - 2026-06-03
|
|
12
24
|
|
package/README.md
CHANGED
|
@@ -55,10 +55,13 @@ Options:
|
|
|
55
55
|
- `--qa`: print the original prompt and answer with labels (useful when used as a filter)
|
|
56
56
|
- `--tg`: forward the answer to Telegram via tgagentp gateway (error if unreachable)
|
|
57
57
|
- `--no-tg`: do not forward to Telegram
|
|
58
|
+
- `--flush`: flush tgagentp's recorded buffer without prepending it to output
|
|
59
|
+
- `--getLast <n>`: retrieve last n assistant answers from session history
|
|
58
60
|
- `--version`: show version
|
|
59
61
|
- `--help`: show help message
|
|
60
62
|
|
|
61
|
-
By default, `--qa`
|
|
63
|
+
By default, `--qa` auto-detects tgagentp (silently degrades if unavailable); standalone mode implies `--no-tg`.
|
|
64
|
+
With `--tg`, errors if tgagentp is unavailable.
|
|
62
65
|
|
|
63
66
|
Arguments:
|
|
64
67
|
|
|
@@ -124,6 +127,25 @@ From Vim/Neovim, forward the answer to your Telegram:
|
|
|
124
127
|
(Works as long as `tgagentp` is running. The answer appears both in the editor
|
|
125
128
|
and in your Telegram chat.)
|
|
126
129
|
|
|
130
|
+
From Vim/Neovim, flush the recorded buffer without prepending context:
|
|
131
|
+
|
|
132
|
+
```vim
|
|
133
|
+
:'<,'>!agentp --qa --flush
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Useful when you've finished a conversation thread and want to reset the recorded
|
|
137
|
+
context for a new topic.
|
|
138
|
+
|
|
139
|
+
Retrieve the last 3 assistant answers from session history:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
agentp --getLast 3
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Useful to grab recent answers without sending a new prompt.
|
|
146
|
+
|
|
147
|
+
## ocmux
|
|
148
|
+
|
|
127
149
|
## ocmux
|
|
128
150
|
|
|
129
151
|
Manage OpenCode server + TUI in tmux for project directories.
|
|
@@ -174,6 +196,10 @@ Notes:
|
|
|
174
196
|
5. With `--tg` (or by default when `--qa` is given), forwards the answer to Telegram
|
|
175
197
|
via the agentp gateway. The answer appears in your Telegram chat as if tgagentp
|
|
176
198
|
itself had processed the request.
|
|
199
|
+
6. If tgagentp's [/record](#tgagentp) feature was active, the gateway response includes
|
|
200
|
+
the recorded conversation buffer. In `--qa` mode, agentp prepends this buffer (with
|
|
201
|
+
rulers) to its stdout so OpenCode receives the full Telegram context. Use `--flush`
|
|
202
|
+
to clear the buffer without prepending.
|
|
177
203
|
|
|
178
204
|
The session API ensures the request is processed even when no TUI is attached, and returns the full answer in a single HTTP response.
|
|
179
205
|
|
|
@@ -201,7 +227,7 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
|
|
|
201
227
|
|
|
202
228
|
| Command | Action |
|
|
203
229
|
|---|---|
|
|
204
|
-
| `/help [topic]` | Show general help or help for a topic (`servers`, `sessions`, `agents`, `models`, `allow`, `think`) |
|
|
230
|
+
| `/help [topic]` | Show general help or help for a topic (`servers`, `sessions`, `agents`, `models`, `allow`, `think`, `record`, `queue`) |
|
|
205
231
|
| `/servers` | List all running ocmux-served projects with URL + status (✅ idle / ⏳ busy) |
|
|
206
232
|
| `/servers switch <name>` | Switch active server; matches by full path, basename, or substring |
|
|
207
233
|
| `/sessions` | List recent sessions for the current server (max 50, with date headings) |
|
|
@@ -212,6 +238,8 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
|
|
|
212
238
|
| `/status` | Show current server path, URL, busy status, and active session |
|
|
213
239
|
| `/cancel` | Cancel the current AI response for the active server |
|
|
214
240
|
| `/think [on\|off\|switch]` | Toggle forwarding of model thinking messages to the chat |
|
|
241
|
+
| `/record [stop]` | Toggle recording of Telegram conversation for agentp context; `/record stop` clears and stops |
|
|
242
|
+
| `/queue <message>` | Queue a message when the server is busy; auto-sent when current task finishes |
|
|
215
243
|
| `/allow` | Approve a permission request once |
|
|
216
244
|
| `/reject` | Deny a permission request |
|
|
217
245
|
| `/always` | Approve and remember for the session |
|
|
@@ -249,6 +277,7 @@ tgagentp starts a tiny HTTP server on `127.0.0.1` that accepts `POST /send` requ
|
|
|
249
277
|
- Messages for the active server are delivered immediately.
|
|
250
278
|
- Messages for non-active servers are queued and delivered on `/servers switch`.
|
|
251
279
|
- Debounced Telegram notification on queue (configurable via `TGAGENTP_DEBOUNCE_MS`).
|
|
280
|
+
- When [/record](#tgagentp) is active, the gateway response includes the recorded conversation buffer. `agentp --qa` prepends this buffer (with rulers) to its stdout so the full Telegram context is available to OpenCode. Use `agentp --qa --flush` to flush the buffer without prepending.
|
|
252
281
|
|
|
253
282
|
### Logging
|
|
254
283
|
|
package/bin/agentp
CHANGED
|
@@ -8,6 +8,7 @@ const {
|
|
|
8
8
|
sendToSession,
|
|
9
9
|
listSessions,
|
|
10
10
|
createSession,
|
|
11
|
+
getSession,
|
|
11
12
|
} = require('../lib/opencode');
|
|
12
13
|
|
|
13
14
|
const TGAGENTP_PORT_FILE = '/tmp/tgagentp-port';
|
|
@@ -35,8 +36,16 @@ function notifyAgentpGateway(port, server, text) {
|
|
|
35
36
|
let data = '';
|
|
36
37
|
res.on('data', (chunk) => { data += chunk; });
|
|
37
38
|
res.on('end', () => {
|
|
38
|
-
if (res.statusCode === 200)
|
|
39
|
-
|
|
39
|
+
if (res.statusCode === 200) {
|
|
40
|
+
try {
|
|
41
|
+
const result = JSON.parse(data);
|
|
42
|
+
resolve(result.buffered || []);
|
|
43
|
+
} catch {
|
|
44
|
+
resolve([]);
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
reject(new Error(`status ${res.statusCode}: ${data}`));
|
|
48
|
+
}
|
|
40
49
|
});
|
|
41
50
|
});
|
|
42
51
|
req.setTimeout(5000, () => {
|
|
@@ -66,6 +75,7 @@ const FINAL_ROW = ' —————————————————\n';
|
|
|
66
75
|
async function main() {
|
|
67
76
|
let serverBase = 'http://localhost:4096';
|
|
68
77
|
let qaMode = false;
|
|
78
|
+
let flushMode = false;
|
|
69
79
|
// tgMode: false = disabled, 'auto' = auto-detect, true = explicit --tg
|
|
70
80
|
let tgMode = null;
|
|
71
81
|
|
|
@@ -84,6 +94,8 @@ async function main() {
|
|
|
84
94
|
console.error(' --qa Print the original prompt and answer with labels (useful as a filter)');
|
|
85
95
|
console.error(' --tg Forward the answer to Telegram via agentp gateway (error if unavailable)');
|
|
86
96
|
console.error(' --no-tg Do not forward to Telegram');
|
|
97
|
+
console.error(' --flush Flush tgagentp\'s recorded buffer without prepending it to output');
|
|
98
|
+
console.error(' --getLast Retrieve last N assistant answers from session history');
|
|
87
99
|
console.error(' --help Show this help message');
|
|
88
100
|
console.error('');
|
|
89
101
|
console.error('By default, --qa auto-detects tgagentp; standalone mode implies --no-tg.');
|
|
@@ -100,9 +112,65 @@ async function main() {
|
|
|
100
112
|
console.error(' cat prompt.txt | agentp 4096');
|
|
101
113
|
console.error(' # Vim/Neovim filter, preserving prompt + answer:');
|
|
102
114
|
console.error(" :'<,'>!agentp --qa");
|
|
115
|
+
console.error(' # Retrieve last 3 assistant answers:');
|
|
116
|
+
console.error(' agentp --getLast 3');
|
|
117
|
+
console.error(' agentp --getLast 1 4096');
|
|
103
118
|
process.exit(0);
|
|
104
119
|
} else if (args[i] === '--qa') {
|
|
105
120
|
qaMode = true;
|
|
121
|
+
} else if (args[i] === '--flush') {
|
|
122
|
+
flushMode = true;
|
|
123
|
+
} else if (args[i] === '--getLast') {
|
|
124
|
+
i++;
|
|
125
|
+
const nStr = args[i];
|
|
126
|
+
if (!nStr || !/^\d+$/.test(nStr)) {
|
|
127
|
+
console.error('Error: --getLast requires a number (e.g. --getLast 5)');
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
const n = parseInt(nStr, 10);
|
|
131
|
+
try {
|
|
132
|
+
const sessions = await listSessions(serverBase);
|
|
133
|
+
if (!sessions || sessions.length === 0) {
|
|
134
|
+
console.error('No sessions found.');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const sorted = sessions
|
|
138
|
+
.filter(s => s.time && s.time.updated)
|
|
139
|
+
.sort((a, b) => b.time.updated - a.time.updated);
|
|
140
|
+
// Try to extract messages; getSession may return data with
|
|
141
|
+
// messages, or the sessions list may already contain them
|
|
142
|
+
let rawMessages = [];
|
|
143
|
+
try {
|
|
144
|
+
const session = await getSession(serverBase, sorted[0].id);
|
|
145
|
+
rawMessages = session.messages || session.history || session.conversation || [];
|
|
146
|
+
} catch {
|
|
147
|
+
// If the dedicated endpoint fails, the list data may have messages
|
|
148
|
+
rawMessages = sorted[0].messages || sorted[0].history || sorted[0].conversation || [];
|
|
149
|
+
}
|
|
150
|
+
if (rawMessages.length === 0) {
|
|
151
|
+
console.error('No message history available for the most recent session.');
|
|
152
|
+
console.error('The OpenCode server may not expose message history via the HTTP API.');
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
const assistantTexts = rawMessages
|
|
156
|
+
.filter(m => m.role === 'assistant')
|
|
157
|
+
.map(m => {
|
|
158
|
+
if (m.parts) return m.parts.filter(p => p.type === 'text').map(p => p.text).join('');
|
|
159
|
+
return m.content || m.text || '';
|
|
160
|
+
})
|
|
161
|
+
.filter(t => t.trim());
|
|
162
|
+
if (assistantTexts.length === 0) {
|
|
163
|
+
console.error('No assistant answers found in session history.');
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
const lastN = assistantTexts.slice(-n);
|
|
167
|
+
process.stdout.write(lastN.join('\n'));
|
|
168
|
+
if (lastN.length > 0 && !lastN[lastN.length - 1].endsWith('\n')) process.stdout.write('\n');
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error('Error retrieving session history:', err.message);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
106
174
|
} else if (args[i] === '--tg') {
|
|
107
175
|
tgMode = true;
|
|
108
176
|
} else if (args[i] === '--no-tg') {
|
|
@@ -172,28 +240,39 @@ async function main() {
|
|
|
172
240
|
process.exit(1);
|
|
173
241
|
}
|
|
174
242
|
|
|
175
|
-
// Build
|
|
176
|
-
let
|
|
243
|
+
// Build the message to send to Telegram (QA pair with rulers)
|
|
244
|
+
let tgMessage = answer;
|
|
177
245
|
if (qaMode) {
|
|
178
|
-
|
|
179
|
-
if (!answer.endsWith('\n'))
|
|
180
|
-
|
|
181
|
-
process.stdout.write(tgText);
|
|
182
|
-
} else {
|
|
183
|
-
process.stdout.write(answer);
|
|
184
|
-
if (!answer.endsWith('\n')) process.stdout.write('\n');
|
|
246
|
+
tgMessage = HUMAN_ROW + promptText + AGENT_ROW + answer;
|
|
247
|
+
if (!answer.endsWith('\n')) tgMessage += '\n';
|
|
248
|
+
tgMessage += FINAL_ROW;
|
|
185
249
|
}
|
|
186
250
|
|
|
187
|
-
// Post-send: notify tgagentp
|
|
251
|
+
// Post-send: notify tgagentp, get recorded context back
|
|
252
|
+
let buffered = [];
|
|
188
253
|
let tgError = null;
|
|
189
254
|
if (tgPort) {
|
|
190
255
|
try {
|
|
191
|
-
await notifyAgentpGateway(tgPort, serverBase,
|
|
256
|
+
buffered = await notifyAgentpGateway(tgPort, serverBase, tgMessage);
|
|
192
257
|
} catch (err) {
|
|
193
258
|
tgError = err;
|
|
194
259
|
}
|
|
195
260
|
}
|
|
196
261
|
|
|
262
|
+
// Build stdout output (prepend recorded context if any, unless --flush)
|
|
263
|
+
if (qaMode) {
|
|
264
|
+
if (!flushMode && buffered.length > 0) {
|
|
265
|
+
for (const msg of buffered) {
|
|
266
|
+
const row = msg.role === 'user' ? HUMAN_ROW : AGENT_ROW;
|
|
267
|
+
process.stdout.write(row + msg.text + '\n');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
process.stdout.write(tgMessage);
|
|
271
|
+
} else {
|
|
272
|
+
process.stdout.write(tgMessage);
|
|
273
|
+
if (!tgMessage.endsWith('\n')) process.stdout.write('\n');
|
|
274
|
+
}
|
|
275
|
+
|
|
197
276
|
// Warnings at the end (stderr)
|
|
198
277
|
if (tgError && tgMode === true) {
|
|
199
278
|
process.stderr.write('\n');
|
package/bin/ocmux
CHANGED
|
@@ -192,7 +192,7 @@ function printHelp() {
|
|
|
192
192
|
console.error(' --git Use the nearest parent with a .git entry (file or dir).');
|
|
193
193
|
console.error(' --GIT Use the nearest parent with a .git directory only.');
|
|
194
194
|
console.error(' Refuses to create above an existing .ocmux.json found while searching.');
|
|
195
|
-
console.error(
|
|
195
|
+
console.error(' --print-logs Pass --print-logs to opencode serve (prints server logs to stderr).');
|
|
196
196
|
console.error(' kill Kill the server found upward from <directory>.');
|
|
197
197
|
console.error(' Removes its tmux window and state file.');
|
|
198
198
|
console.error(' list List all running servers with their directories and URLs.');
|
package/bin/tgagentp
CHANGED
|
@@ -49,6 +49,32 @@ function flushAgentpQueue(server, token, chatId) {
|
|
|
49
49
|
.catch(() => {});
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// /record — ring buffer for Telegram conversation context
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
const MAX_RECORDED_MSGS = 100;
|
|
57
|
+
const MAX_RECORDED_BYTES = 100 * 1024;
|
|
58
|
+
|
|
59
|
+
const recorder = { active: false, messages: [], bytes: 0 };
|
|
60
|
+
|
|
61
|
+
function bufferMessage(role, text) {
|
|
62
|
+
if (!recorder.active) return;
|
|
63
|
+
recorder.messages.push({ role, text });
|
|
64
|
+
recorder.bytes += Buffer.byteLength(text, 'utf8');
|
|
65
|
+
while (recorder.messages.length > MAX_RECORDED_MSGS || recorder.bytes > MAX_RECORDED_BYTES) {
|
|
66
|
+
const oldest = recorder.messages.shift();
|
|
67
|
+
recorder.bytes -= Buffer.byteLength(oldest.text, 'utf8');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function flushRecorded() {
|
|
72
|
+
if (!recorder.active) return [];
|
|
73
|
+
const msgs = recorder.messages.splice(0);
|
|
74
|
+
recorder.bytes = 0;
|
|
75
|
+
return msgs;
|
|
76
|
+
}
|
|
77
|
+
|
|
52
78
|
// ---------------------------------------------------------------------------
|
|
53
79
|
// Telegram Bot API helpers
|
|
54
80
|
// ---------------------------------------------------------------------------
|
|
@@ -226,7 +252,7 @@ function quoteReply(msg) {
|
|
|
226
252
|
if (rm.text[i] === '`') { cur++; maxBackticks = Math.max(maxBackticks, cur); }
|
|
227
253
|
else cur = 0;
|
|
228
254
|
}
|
|
229
|
-
const fence = '`'.repeat(maxBackticks + 1);
|
|
255
|
+
const fence = '`'.repeat(Math.max(3, maxBackticks + 1));
|
|
230
256
|
return `In response to ${replyFrom} (${senderType}):\n\n${fence}\n${rm.text}\n${fence}\n\n`;
|
|
231
257
|
}
|
|
232
258
|
|
|
@@ -307,8 +333,28 @@ async function cmdHelp(token, chatId, args) {
|
|
|
307
333
|
'Can also be enabled at startup with `--think` flag.');
|
|
308
334
|
return;
|
|
309
335
|
}
|
|
336
|
+
if (topic === 'queue') {
|
|
337
|
+
await sendLongMessage(token, chatId,
|
|
338
|
+
'`/queue <message>` ─ queue a message when the server is busy\n\n' +
|
|
339
|
+
'When the server is processing a previous message, queuing allows you to\n' +
|
|
340
|
+
'continue working without waiting. Once the current task finishes, the next\n' +
|
|
341
|
+
'queued message is automatically sent and the answer will reply to the\n' +
|
|
342
|
+
'original queued message.');
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (topic === 'record') {
|
|
346
|
+
await sendLongMessage(token, chatId,
|
|
347
|
+
'`/record` ─ toggle recording of messages for agentp context\n' +
|
|
348
|
+
'`/record stop` ─ stop recording and clear buffer\n\n' +
|
|
349
|
+
'When recording is active, all user and assistant messages are buffered in memory\n' +
|
|
350
|
+
'(max 100 messages / 100KB). The next `agentp` invocation retrieves the buffered\n' +
|
|
351
|
+
'conversation and prepends it to its `--qa` stdout output, so OpenCode can\n' +
|
|
352
|
+
'see the full conversation context.\n\n' +
|
|
353
|
+
'Buffered messages are NOT sent back to Telegram (they are already there).');
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
310
356
|
await sendLongMessage(token, chatId,
|
|
311
|
-
'Unknown topic. Try `/help servers`, `/help sessions`, `/help session`, `/help agents`, `/help models`, `/help allow`, `/help think`, or just `/help`.');
|
|
357
|
+
'Unknown topic. Try `/help servers`, `/help sessions`, `/help session`, `/help agents`, `/help models`, `/help allow`, `/help think`, `/help record`, `/help queue`, or just `/help`.');
|
|
312
358
|
return;
|
|
313
359
|
}
|
|
314
360
|
await sendLongMessage(token, chatId,
|
|
@@ -327,6 +373,8 @@ async function cmdHelp(token, chatId, args) {
|
|
|
327
373
|
'`/agents switch <name>` ─ switch active agent\n' +
|
|
328
374
|
'`/models` ─ list providers and models\n' +
|
|
329
375
|
'`/think` ─ toggle thinking message forwarding\n' +
|
|
376
|
+
'`/record` ─ record Telegram conversation for agentp context\n' +
|
|
377
|
+
'`/queue <message>` ─ queue a message when server is busy\n' +
|
|
330
378
|
'`/allow` / `/reject` / `/always` ─ respond to permission requests\n' +
|
|
331
379
|
'`/shutdown` ─ stop tgagentp (only with `--dev` flag)\n\n' +
|
|
332
380
|
'Everything else is forwarded to the active server as `[Telegram] <text>`.');
|
|
@@ -740,10 +788,13 @@ function startAgentpGateway(token, chatIdResolver, serverBaseResolver) {
|
|
|
740
788
|
res.end('Bad Request: text and server are required');
|
|
741
789
|
return;
|
|
742
790
|
}
|
|
743
|
-
res.writeHead(200);
|
|
744
|
-
res.end('OK');
|
|
745
|
-
|
|
746
791
|
const chatId = chatIdResolver();
|
|
792
|
+
|
|
793
|
+
// Return recorded conversation buffer before forwarding
|
|
794
|
+
const buffered = flushRecorded();
|
|
795
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
796
|
+
res.end(JSON.stringify({ buffered }));
|
|
797
|
+
|
|
747
798
|
if (!chatId) return;
|
|
748
799
|
const activeUrl = normalizeUrl(serverBaseResolver());
|
|
749
800
|
const targetUrl = normalizeUrl(server);
|
|
@@ -854,6 +905,39 @@ async function cmdThink(token, chatId, args, setServer, setActiveSession, server
|
|
|
854
905
|
}
|
|
855
906
|
}
|
|
856
907
|
|
|
908
|
+
async function cmdQueue(token, chatId, args, _, __, serverBase, replyTo) {
|
|
909
|
+
const st = getState(serverBase);
|
|
910
|
+
if (!st.busy) {
|
|
911
|
+
await sendLongMessage(token, chatId, 'Server is not busy. No need to queue — send your message directly.');
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const text = args.join(' ').trim();
|
|
915
|
+
if (!text) {
|
|
916
|
+
await sendLongMessage(token, chatId, 'Usage: `/queue <message>` — queue a message for when the server is idle.');
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (!st.messageQueue) st.messageQueue = [];
|
|
920
|
+
st.messageQueue.push({ text, replyTo });
|
|
921
|
+
await sendLongMessage(token, chatId, `📥 Queued. ${st.messageQueue.length} message(s) in queue. Will be sent when the current task finishes.`);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
async function cmdRecord(token, chatId, args) {
|
|
925
|
+
if (args.length > 0 && args[0].toLowerCase() === 'stop') {
|
|
926
|
+
recorder.active = false;
|
|
927
|
+
recorder.messages = [];
|
|
928
|
+
recorder.bytes = 0;
|
|
929
|
+
await sendLongMessage(token, chatId, '⏹ Recording stopped. Buffer cleared.');
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
recorder.active = !recorder.active;
|
|
933
|
+
if (!recorder.active) {
|
|
934
|
+
recorder.messages = [];
|
|
935
|
+
recorder.bytes = 0;
|
|
936
|
+
}
|
|
937
|
+
await sendLongMessage(token, chatId,
|
|
938
|
+
recorder.active ? '🔴 Recording started. Messages will be cached for agentp.' : '⏹ Recording stopped. Buffer cleared.');
|
|
939
|
+
}
|
|
940
|
+
|
|
857
941
|
const COMMANDS = {
|
|
858
942
|
'help': cmdHelp,
|
|
859
943
|
'servers': cmdServers,
|
|
@@ -862,6 +946,8 @@ const COMMANDS = {
|
|
|
862
946
|
'agents': cmdAgents,
|
|
863
947
|
'models': cmdModels,
|
|
864
948
|
'think': cmdThink,
|
|
949
|
+
'record': cmdRecord,
|
|
950
|
+
'queue': cmdQueue,
|
|
865
951
|
'allow': cmdAllow,
|
|
866
952
|
'reject': cmdReject,
|
|
867
953
|
'always': cmdAlways,
|
|
@@ -873,7 +959,7 @@ const COMMANDS = {
|
|
|
873
959
|
|
|
874
960
|
const serverState = {};
|
|
875
961
|
function getState(url) {
|
|
876
|
-
return serverState[url] = serverState[url] || { busy: false, pendingResponse: null, activeSessionId: null, activeSessionName: null, activeAgentName: null, taskId: 0, pendingPermission: null, thinkingEnabled: false };
|
|
962
|
+
return serverState[url] = serverState[url] || { busy: false, pendingResponse: null, activeSessionId: null, activeSessionName: null, activeAgentName: null, taskId: 0, pendingPermission: null, thinkingEnabled: false, messageQueue: [] };
|
|
877
963
|
}
|
|
878
964
|
|
|
879
965
|
function sessionLabel(st) {
|
|
@@ -1113,11 +1199,14 @@ async function main() {
|
|
|
1113
1199
|
st.pendingPermission = null;
|
|
1114
1200
|
if (url === serverBase) {
|
|
1115
1201
|
if (answer && answer.trim()) {
|
|
1116
|
-
|
|
1202
|
+
const trimmed = answer.trim();
|
|
1203
|
+
await sendLongMessage(token, chatId, trimmed, replyTo);
|
|
1204
|
+
bufferMessage('assistant', trimmed);
|
|
1117
1205
|
}
|
|
1118
1206
|
} else {
|
|
1119
1207
|
getState(url).pendingResponse = answer;
|
|
1120
1208
|
await sendLongMessage(token, chatId, `✅ Server \`${url}\` finished processing. Switch back to see the response.`, replyTo);
|
|
1209
|
+
if (answer && answer.trim()) bufferMessage('assistant', answer.trim());
|
|
1121
1210
|
}
|
|
1122
1211
|
} catch (err) {
|
|
1123
1212
|
// Clean up SSE listener if it's still active
|
|
@@ -1137,6 +1226,21 @@ async function main() {
|
|
|
1137
1226
|
if (st._sessionReq === sessionCancelRef) st._sessionReq = null;
|
|
1138
1227
|
if (!isCancelled()) {
|
|
1139
1228
|
getState(url).busy = false;
|
|
1229
|
+
// Drain message queue for this server
|
|
1230
|
+
const q = st.messageQueue || [];
|
|
1231
|
+
st.messageQueue = [];
|
|
1232
|
+
if (q.length > 0) {
|
|
1233
|
+
const next = q[0];
|
|
1234
|
+
// Re-queue remaining items (if any) for the next round
|
|
1235
|
+
st.messageQueue = q.slice(1);
|
|
1236
|
+
const nextText = next.text;
|
|
1237
|
+
const nextReplyTo = next.replyTo;
|
|
1238
|
+
// Mark busy again and process
|
|
1239
|
+
getState(url).busy = true;
|
|
1240
|
+
const nextTaskId = ++st.taskId;
|
|
1241
|
+
log.debug(` → processing queued message (task ${nextTaskId})`);
|
|
1242
|
+
processMessageAsync(chatId, nextText, url, !!st.activeSessionId, st.activeSessionId, nextTaskId, nextReplyTo);
|
|
1243
|
+
}
|
|
1140
1244
|
}
|
|
1141
1245
|
}
|
|
1142
1246
|
}
|
|
@@ -1285,15 +1389,18 @@ async function main() {
|
|
|
1285
1389
|
}
|
|
1286
1390
|
} else {
|
|
1287
1391
|
await sendLongMessage(token, chatId,
|
|
1288
|
-
`Unknown command: /${cmd}\n\nAvailable commands: /help, /status, /servers, /sessions, /agents, /models`);
|
|
1392
|
+
`Unknown command: /${cmd}\n\nAvailable commands: /help, /status, /servers, /sessions, /agents, /models, /think, /record, /queue`);
|
|
1289
1393
|
}
|
|
1290
1394
|
continue;
|
|
1291
1395
|
}
|
|
1292
1396
|
|
|
1397
|
+
// Buffer user messages for /record
|
|
1398
|
+
bufferMessage('user', replyContext + text);
|
|
1399
|
+
|
|
1293
1400
|
// Non-command message ─ process in background
|
|
1294
1401
|
const st = getState(serverBase);
|
|
1295
1402
|
if (st.busy) {
|
|
1296
|
-
await sendLongMessage(token, chatId, '⏳ Server is busy
|
|
1403
|
+
await sendLongMessage(token, chatId, '⏳ Server is busy. Use `/cancel` to cancel or `/queue <message>` to queue your message.');
|
|
1297
1404
|
continue;
|
|
1298
1405
|
}
|
|
1299
1406
|
st.busy = true;
|
package/devto-article.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Agentp: Turn OpenCode Into a Headless AI Engine for Your Editor, Terminal, and Telegram
|
|
2
|
+
|
|
3
|
+
OpenCode is great, but its TUI locks you in. You type in one window, watch it stream, and that's it. What if you could:
|
|
4
|
+
|
|
5
|
+
- Pipe a prompt straight from Vim and replace your selection with the answer?
|
|
6
|
+
- Queue messages from Telegram while it's busy and get replies threaded?
|
|
7
|
+
- Keep a permanent tmux server per project and switch between them?
|
|
8
|
+
- Pull the last N answers from session history without sending a new prompt?
|
|
9
|
+
- Record your Telegram conversation and inject it as context into your next `agentp` call?
|
|
10
|
+
|
|
11
|
+
That's what **agentp** — a set of three zero-dependency Node.js CLI tools — does.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## The Three Tools
|
|
16
|
+
|
|
17
|
+
### `agentp` — The Pipe
|
|
18
|
+
|
|
19
|
+
Stdin in, answer out. That's the core loop.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
printf "Summarize this file" | agentp
|
|
23
|
+
cat prompt.txt | agentp 4096
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
From Vim/Neovim:
|
|
27
|
+
|
|
28
|
+
```vim
|
|
29
|
+
:'<,'>!agentp --qa
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This replaces your visual selection with the answer. `--qa` preserves the prompt + answer with labels, so you keep the full context.
|
|
33
|
+
|
|
34
|
+
### `ocmux` — The Project Manager
|
|
35
|
+
|
|
36
|
+
Each project gets its own tmux window with a dedicated `opencode serve` + TUI pane. Auto-restarts dead panes, pins window names, stores state in `.ocmux.json` (discovered upward like `.git`).
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
ocmux serve ~/projects/myapp # create
|
|
40
|
+
ocmux list # list all
|
|
41
|
+
ocmux ~/projects/myapp # switch
|
|
42
|
+
ocmux kill ~/projects/myapp # remove
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### `tgagentp` — The Telegram Bridge
|
|
46
|
+
|
|
47
|
+
A Telegram bot that routes messages to your OpenCode servers. Multi-server aware, slash-commands for everything.
|
|
48
|
+
|
|
49
|
+
**Slash commands:**
|
|
50
|
+
|
|
51
|
+
| Command | What it does |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `/servers` | List/switch between ocmux projects |
|
|
54
|
+
| `/sessions` | List/switch/create/rename sessions |
|
|
55
|
+
| `/agents` | List/switch active agent |
|
|
56
|
+
| `/models` | List providers and models |
|
|
57
|
+
| `/queue <msg>` | Queue message when busy — auto-sent after current task finishes (replies chain!) |
|
|
58
|
+
| `/record` | Record conversation for `agentp` context injection |
|
|
59
|
+
| `/think` | Toggle real-time thinking message forwarding |
|
|
60
|
+
| `/cancel` | Abort the running prompt |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## The Agentp Gateway
|
|
65
|
+
|
|
66
|
+
The killer integration: `agentp` and `tgagentp` talk to each other through a tiny HTTP gateway.
|
|
67
|
+
|
|
68
|
+
- **`agentp --tg`** forwards the answer to your Telegram chat after every pipe.
|
|
69
|
+
- **`agentp --qa`** auto-detects tgagentp and sends the full QA pair (rulers + prompt + answer).
|
|
70
|
+
- **`/record`** buffers the Telegram conversation. On the next `agentp` call, the gateway returns the buffer, and `--qa` prepends it to stdout with rulers — so OpenCode sees the full Telegram thread as context.
|
|
71
|
+
- **`agentp --flush`** clears the buffer without prepending.
|
|
72
|
+
- **`agentp --getLast 5`** retrieves the last 5 assistant answers from session history.
|
|
73
|
+
|
|
74
|
+
```vim
|
|
75
|
+
:'<,'>!agentp --qa --tg " answer in editor + Telegram
|
|
76
|
+
:'<,'>!agentp --qa --flush " flush recorded buffer
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
All of this with **zero npm dependencies** — just Node.js 18+ stdlib.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Why This Setup Works
|
|
84
|
+
|
|
85
|
+
1. **Session API, not TUI hooks** — messages go through `POST /session/:id/message`, which returns synchronously. No fragile TUI scraping.
|
|
86
|
+
2. **Per-project tmux isolation** — each project gets its own `opencode serve` process. Switch with a single command.
|
|
87
|
+
3. **Graceful degradation** — `--tg` in auto mode silently skips Telegram if tgagentp isn't running. Explicit `--tg` errors pre-send, warns post-send.
|
|
88
|
+
4. **10s HTTP timeout on every request** — nothing ever hangs.
|
|
89
|
+
5. **Async processing** — tgagentp never blocks the polling loop. Commands stay responsive even while a prompt runs.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Quick Start
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npm install -g agentp
|
|
97
|
+
|
|
98
|
+
# Start a project server
|
|
99
|
+
ocmux serve ~/projects/myapp
|
|
100
|
+
|
|
101
|
+
# Pipe a prompt
|
|
102
|
+
printf "Explain this codebase" | agentp
|
|
103
|
+
|
|
104
|
+
# With Telegram (set up a bot with @BotFather first)
|
|
105
|
+
export TELEGRAM_BOT_TOKEN="your-token"
|
|
106
|
+
tgagentp
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
If your OpenCode server is password-protected, set `OPENCODE_SERVER_PASSWORD`.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Links
|
|
114
|
+
|
|
115
|
+
- **GitHub:** [github.com/bitifet/agentp](https://github.com/bitifet/agentp)
|
|
116
|
+
- **npm:** [npmjs.com/package/agentp](https://www.npmjs.com/package/agentp)
|
|
117
|
+
- **OpenCode:** [github.com/anthropics/claude-code](https://github.com/anthropics/claude-code)
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
*Feedback? Issues? PRs welcome.*
|