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 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
- - `agentp --getLast n` — retrieve last n answers from session history
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
- ## [Unreleased]
5
+ ## [0.10.0] - 2026-06-04
6
6
 
7
- - Rename package and CLI command from `aprompt` to `agentp`.
8
- - Extract shared OpenCode client logic into `lib/opencode.js`.
9
- - Add `tgagentp` — bridge a Telegram bot chat with an OpenCode TUI session.
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` implies `--tg`; standalone mode implies `--no-tg`.
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) resolve();
39
- else reject(new Error(`status ${res.statusCode}: ${data}`));
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 full --qa output and send to stdout
176
- let tgText = answer;
243
+ // Build the message to send to Telegram (QA pair with rulers)
244
+ let tgMessage = answer;
177
245
  if (qaMode) {
178
- tgText = HUMAN_ROW + promptText + AGENT_ROW + answer;
179
- if (!answer.endsWith('\n')) tgText += '\n';
180
- tgText += FINAL_ROW;
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 if we have a port
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, tgText);
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( ' --print-logs Pass --print-logs to opencode serve (prints server logs to stderr).');
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
- await sendLongMessage(token, chatId, answer.trim(), replyTo);
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 processing a previous message. Please wait for it to finish, or use /cancel to cancel it.');
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;
@@ -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.*