agentp 0.8.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 +36 -9
- package/CHANGELOG.md +16 -4
- package/README.md +73 -9
- package/bin/agentp +220 -27
- package/bin/ocmux +24 -6
- package/bin/tgagentp +332 -74
- package/devto-article.md +121 -0
- package/lib/opencode.js +73 -9
- package/package.json +1 -1
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.*
|
package/lib/opencode.js
CHANGED
|
@@ -11,7 +11,7 @@ function getAuthHeaders() {
|
|
|
11
11
|
return { 'Authorization': `Basic ${encoded}` };
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function makeRequest(options, data) {
|
|
14
|
+
function makeRequest(options, data, cancelRef, timeoutMs) {
|
|
15
15
|
return new Promise((resolve, reject) => {
|
|
16
16
|
const req = http.request(options, (res) => {
|
|
17
17
|
let body = '';
|
|
@@ -24,7 +24,14 @@ function makeRequest(options, data) {
|
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
26
|
});
|
|
27
|
+
if (timeoutMs) {
|
|
28
|
+
req.setTimeout(timeoutMs, () => {
|
|
29
|
+
req.destroy();
|
|
30
|
+
reject(new Error('Request timed out'));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
27
33
|
req.on('error', reject);
|
|
34
|
+
if (cancelRef) cancelRef.current = req;
|
|
28
35
|
if (data) req.write(data);
|
|
29
36
|
req.end();
|
|
30
37
|
});
|
|
@@ -181,15 +188,16 @@ async function listSessions(server, directory) {
|
|
|
181
188
|
return JSON.parse(body);
|
|
182
189
|
}
|
|
183
190
|
|
|
184
|
-
// Send a message directly to a specific session via the session API.
|
|
191
|
+
// Send a message directly to a specific session via the synchronous session API.
|
|
185
192
|
// Returns the collected text from all text parts in the response.
|
|
186
193
|
// Optionally specify an agent to handle the message.
|
|
187
|
-
|
|
194
|
+
// Optional cancelRef allows aborting the HTTP request (for /cancel support).
|
|
195
|
+
async function sendToSession(server, sessionId, text, agent, cancelRef) {
|
|
188
196
|
const url = `${server}/session/${encodeURIComponent(sessionId)}/message`;
|
|
189
197
|
const bodyObj = { parts: [{ type: 'text', text }] };
|
|
190
198
|
if (agent) bodyObj.agent = agent;
|
|
191
199
|
const body = JSON.stringify(bodyObj);
|
|
192
|
-
const { status, body: responseBody } = await makeRequest(buildJsonRequest(url, 'POST', body), body);
|
|
200
|
+
const { status, body: responseBody } = await makeRequest(buildJsonRequest(url, 'POST', body), body, cancelRef);
|
|
193
201
|
if (status !== 200) throw new Error(`Failed to send to session: ${status}`);
|
|
194
202
|
const result = JSON.parse(responseBody);
|
|
195
203
|
const texts = (result.parts || [])
|
|
@@ -209,9 +217,12 @@ async function createSession(server, title) {
|
|
|
209
217
|
}
|
|
210
218
|
|
|
211
219
|
// Update a session's properties (e.g. title).
|
|
212
|
-
async function updateSession(server, sessionId, title) {
|
|
220
|
+
async function updateSession(server, sessionId, title, agent) {
|
|
221
|
+
const bodyObj = {};
|
|
222
|
+
if (title != null) bodyObj.title = title;
|
|
223
|
+
if (agent != null) bodyObj.agent = agent;
|
|
213
224
|
const url = `${server}/session/${encodeURIComponent(sessionId)}`;
|
|
214
|
-
const body = JSON.stringify(
|
|
225
|
+
const body = JSON.stringify(bodyObj);
|
|
215
226
|
const { status, body: responseBody } = await makeRequest(buildJsonRequest(url, 'PATCH', body), body);
|
|
216
227
|
if (status !== 200) throw new Error(`Failed to update session: ${status}`);
|
|
217
228
|
return JSON.parse(responseBody);
|
|
@@ -238,6 +249,27 @@ async function selectSession(server, sessionId) {
|
|
|
238
249
|
}
|
|
239
250
|
}
|
|
240
251
|
|
|
252
|
+
// Get a single session by ID, including its message history.
|
|
253
|
+
// Tries GET /session/:id first; falls back to filtering the sessions list.
|
|
254
|
+
async function getSession(server, sessionId) {
|
|
255
|
+
// Try dedicated endpoint first (newer opencode versions)
|
|
256
|
+
try {
|
|
257
|
+
const url = `${server}/session/${encodeURIComponent(sessionId)}`;
|
|
258
|
+
const { status, body } = await makeRequest(buildJsonRequest(url, 'GET'), null, null, 10000);
|
|
259
|
+
if (status === 200) {
|
|
260
|
+
const parsed = JSON.parse(body);
|
|
261
|
+
if (parsed) return parsed;
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
// Fall through
|
|
265
|
+
}
|
|
266
|
+
// Fallback: get all sessions and find the one we need
|
|
267
|
+
const sessions = await listSessions(server);
|
|
268
|
+
const match = sessions.find(s => String(s.id) === String(sessionId));
|
|
269
|
+
if (match) return match;
|
|
270
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
271
|
+
}
|
|
272
|
+
|
|
241
273
|
// List all agents from the OpenCode server.
|
|
242
274
|
async function listAgents(server) {
|
|
243
275
|
const url = `${server}/agent`;
|
|
@@ -276,7 +308,7 @@ async function respondToPermission(server, sessionId, permissionId, response, re
|
|
|
276
308
|
|
|
277
309
|
// Listen for session events via SSE, collecting assistant text parts and detecting
|
|
278
310
|
// permission requests. Resolves with the full collected text when the session goes idle.
|
|
279
|
-
// callbacks: { onText(chunk), onPermission(permission) }
|
|
311
|
+
// callbacks: { onText(chunk), onPermission(permission), onThinking(chunk), onConnected() }
|
|
280
312
|
// cancelRef — allows aborting the SSE stream via req.destroy()
|
|
281
313
|
function listenForSessionEvents(server, sessionId, callbacks, cancelRef) {
|
|
282
314
|
process.stderr.write(` [SSE] connecting to ${server}/event for session ${sessionId}\n`);
|
|
@@ -297,9 +329,22 @@ function listenForSessionEvents(server, sessionId, callbacks, cancelRef) {
|
|
|
297
329
|
return;
|
|
298
330
|
}
|
|
299
331
|
process.stderr.write(` [SSE] connected for session ${sessionId}\n`);
|
|
332
|
+
if (callbacks.onConnected) callbacks.onConnected();
|
|
300
333
|
let buffer = '';
|
|
301
334
|
const userMessageIDs = new Set();
|
|
302
335
|
let collected = '';
|
|
336
|
+
let resolved = false;
|
|
337
|
+
|
|
338
|
+
// Safety timeout: resolve with whatever we have after 90s
|
|
339
|
+
const safetyTimer = setTimeout(() => {
|
|
340
|
+
if (resolved) return;
|
|
341
|
+
resolved = true;
|
|
342
|
+
process.stderr.write(` [SSE] safety timeout (90s) for session ${sessionId}, collected ${collected.length} chars\n`);
|
|
343
|
+
cleanup();
|
|
344
|
+
req.destroy();
|
|
345
|
+
res.destroy();
|
|
346
|
+
resolve(collected);
|
|
347
|
+
}, 90000);
|
|
303
348
|
|
|
304
349
|
res.on('data', (chunk) => {
|
|
305
350
|
buffer += chunk.toString();
|
|
@@ -325,8 +370,8 @@ function listenForSessionEvents(server, sessionId, callbacks, cancelRef) {
|
|
|
325
370
|
process.stderr.write(` [SSE] event type=${event.type} sessionID=${props.sessionID} waitSession=${sessionId}\n`);
|
|
326
371
|
}
|
|
327
372
|
|
|
328
|
-
// Filter by sessionID when the event carries one
|
|
329
|
-
if (props.sessionID && props.sessionID !== sessionId) continue;
|
|
373
|
+
// Filter by sessionID when the event carries one (string-compare to handle type mismatches)
|
|
374
|
+
if (props.sessionID && String(props.sessionID) !== String(sessionId)) continue;
|
|
330
375
|
|
|
331
376
|
if (event.type === 'message.updated' && props.info?.role === 'user') {
|
|
332
377
|
userMessageIDs.add(props.info.id);
|
|
@@ -355,6 +400,9 @@ function listenForSessionEvents(server, sessionId, callbacks, cancelRef) {
|
|
|
355
400
|
|
|
356
401
|
// Completion signals
|
|
357
402
|
if (event.type === 'session.idle') {
|
|
403
|
+
clearTimeout(safetyTimer);
|
|
404
|
+
if (resolved) return;
|
|
405
|
+
resolved = true;
|
|
358
406
|
cleanup();
|
|
359
407
|
req.destroy();
|
|
360
408
|
res.destroy();
|
|
@@ -362,6 +410,9 @@ function listenForSessionEvents(server, sessionId, callbacks, cancelRef) {
|
|
|
362
410
|
return;
|
|
363
411
|
}
|
|
364
412
|
if (event.type === 'session.status' && props.status?.type === 'idle') {
|
|
413
|
+
clearTimeout(safetyTimer);
|
|
414
|
+
if (resolved) return;
|
|
415
|
+
resolved = true;
|
|
365
416
|
cleanup();
|
|
366
417
|
req.destroy();
|
|
367
418
|
res.destroy();
|
|
@@ -390,9 +441,21 @@ function listenForSessionEvents(server, sessionId, callbacks, cancelRef) {
|
|
|
390
441
|
});
|
|
391
442
|
|
|
392
443
|
res.on('end', () => {
|
|
444
|
+
clearTimeout(safetyTimer);
|
|
445
|
+
if (resolved) return;
|
|
446
|
+
resolved = true;
|
|
393
447
|
cleanup();
|
|
394
448
|
resolve(collected);
|
|
395
449
|
});
|
|
450
|
+
|
|
451
|
+
res.on('error', (err) => {
|
|
452
|
+
clearTimeout(safetyTimer);
|
|
453
|
+
if (resolved) return;
|
|
454
|
+
resolved = true;
|
|
455
|
+
cleanup();
|
|
456
|
+
process.stderr.write(` [SSE] response stream error for session ${sessionId}: ${err.message}\n`);
|
|
457
|
+
resolve(collected); // resolve with what we have
|
|
458
|
+
});
|
|
396
459
|
});
|
|
397
460
|
|
|
398
461
|
const cleanup = () => {
|
|
@@ -419,6 +482,7 @@ module.exports = {
|
|
|
419
482
|
sendText,
|
|
420
483
|
listenForFinalAnswer,
|
|
421
484
|
listSessions,
|
|
485
|
+
getSession,
|
|
422
486
|
createSession,
|
|
423
487
|
updateSession,
|
|
424
488
|
selectSession,
|