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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Three binaries, all registered
4
4
 
5
- - `bin/agentp` (Node.js) — registered in `package.json` `"bin"` — pipes stdin → opencode TUI
5
+ - `bin/agentp` (Node.js) — registered in `package.json` `"bin"` — pipes stdin → opencode session
6
6
  - `bin/ocmux` (Node.js) — registered in `package.json` `"bin"` — manages opencode tmux sessions
7
7
  - `bin/tgagentp` (Node.js) — registered in `package.json` `"bin"` — bridges Telegram bot ↔ opencode TUI
8
8
 
@@ -37,11 +37,14 @@ Extracted from `bin/agentp`; used by both `agentp` and `tgagentp`:
37
37
  - `listenForFinalAnswer(server, onText?, cancelRef?)` — SSE event stream listener; `cancelRef` allows true server-side abort via `req.destroy()`
38
38
  - `listSessions(server, directory?)` — `GET /session`; returns parsed JSON array; optional `?directory=` filter
39
39
  - `createSession(server, title?)` — `POST /session`; returns the created `Session` object; optional `title`
40
- - `updateSession(server, sessionId, title)` — `PATCH /session/:id`; returns the updated `Session` object
41
- - `sendToSession(server, sessionId, text, agent?)` — `POST /session/:id/message`; returns concatenated text parts from response; optional `agent` to handle the message
40
+ - `updateSession(server, sessionId, title, agent?)` — `PATCH /session/:id`; returns the updated `Session` object; optional `agent` to set the session's agent
41
+ - `sendToSession(server, sessionId, text, agent?, cancelRef?)` — `POST /session/:id/message`; returns concatenated text parts from response; optional `agent`, optional `cancelRef` for aborting the request
42
+ - `sendToSessionAsync(server, sessionId, text, agent?)` — `POST /session/:id/prompt_async`; returns 204; non-blocking, answer delivered via SSE
43
+ - `respondToPermission(server, sessionId, permissionId, response)` — `POST /session/:id/permissions/:id`
42
44
  - `selectSession(server, sessionId)` — `POST /session/:id/select`; tells the TUI to navigate to the given session (silently ignores 404 if the endpoint isn't available in older opencode versions)
43
45
  - `listAgents(server)` — `GET /agent`; returns parsed JSON array of agent objects
44
46
  - `listProviders(server)` — `GET /provider`; returns parsed JSON array of provider objects
47
+ - `listenForSessionEvents(server, sessionId, callbacks, cancelRef?)` — SSE listener for session events (text parts, permissions, thinking, session.idle)
45
48
 
46
49
  ## Shared `lib/ocmux.js`
47
50
 
@@ -67,17 +70,30 @@ These endpoints are consumed but not documented elsewhere in the repo:
67
70
  - `GET /event` (SSE stream)
68
71
  - `GET /session` — list sessions (optional `?directory=` filter)
69
72
  - `POST /session` — create a new session (optional `{ title? }` body)
70
- - `PATCH /session/:id` — update session properties (`{ title }`)
73
+ - `PATCH /session/:id` — update session properties (`{ title }`, optional `{ agent }`)
71
74
  - `POST /session/:id/select` — tell the TUI to navigate to a session (only with TUI attached)
72
75
  - `POST /session/:id/message` — send message to session (returns parts synchronously; optional `agent` field)
76
+ - `POST /session/:id/prompt_async` — send message to session (returns 204; answer via SSE)
73
77
  - `GET /agent` — list available agents
74
78
  - `GET /provider` — list connected providers/models
75
79
 
80
+ ## Agentp gateway (tgagentp ↔ agentp interop)
81
+
82
+ - tgagentp starts a tiny HTTP server (POST /send) on `127.0.0.1`, port from `TGAGENTP_PORT` (default 0 = random).
83
+ - Port is written to `/tmp/tgagentp-port` for agentp discovery.
84
+ - Validates `Authorization` header against `OPENCODE_SERVER_PASSWORD`.
85
+ - agentp `--tg` reads the port file and POSTs `{ text, server }` to the gateway.
86
+ - Messages for the active server are delivered immediately; others are queued per-server.
87
+ - Debounced Telegram notification (configurable via `TGAGENTP_DEBOUNCE_MS`, default 5000ms).
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.
91
+
76
92
  ## OpenCode tmux session model (ocmux)
77
93
 
78
94
  - Single tmux session named `"Opencode"`
79
95
  - Windows named by full project directory path
80
- - Server pane runs `opencode serve --port 0` (via send-keys → interactive shell for env var support), TUI pane runs `opencode attach --continue`
96
+ - Server pane runs `opencode serve --port 0` (via send-keys → interactive shell for env var support), TUI pane runs `opencode attach --continue`; `ocmux serve --print-logs` adds `--print-logs` to the serve command
81
97
  - Log file per project: `/tmp/opencode-serve-<hashDir(dir)>.log` — captured via `tee`, read by ocmux polling loop
82
98
  - Per-project `.ocmux.json` with upward directory search (git-like)
83
99
  - Auto-restarts dead TUI panes on switch (respawn-pane or split-window)
@@ -89,10 +105,10 @@ These endpoints are consumed but not documented elsewhere in the repo:
89
105
  |---|---|---|
90
106
  | `npm link` | local dev install |
91
107
  | `npm install -g .` | alternative local install |
92
- | `agentp [--qa] [port]` | pipe stdin → opencode TUI, stream answer to stdout |
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 |
93
109
  | `tgagentp [port]` | bridge Telegram bot ↔ opencode TUI (needs `TELEGRAM_BOT_TOKEN`) |
94
110
  | `tgagentp --dev` | enable `/shutdown` command for remote restart |
95
- | `ocmux serve [dir]` | start opencode serve in a tmux window (primary verb) |
111
+ | `ocmux serve [--print-logs] [dir]` | start opencode serve in a tmux window (primary verb) |
96
112
  | `ocmux new [dir]` | alias for `serve` (backwards compat, to be removed in 1.0) |
97
113
  | `ocmux kill [dir]` | kill server + tmux window |
98
114
  | `ocmux list [-l]` | list running servers |
@@ -102,7 +118,7 @@ These endpoints are consumed but not documented elsewhere in the repo:
102
118
 
103
119
  ### Pending
104
120
 
105
- - `--print-logs` flag for `ocmux serve`
121
+ *(none)*
106
122
 
107
123
  ### In Progress
108
124
 
@@ -110,12 +126,23 @@ These endpoints are consumed but not documented elsewhere in the repo:
110
126
 
111
127
  ### Done
112
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
132
+ - agentp `--qa` full context forwarding to Telegram (rulers, prompt, answer) — 0.9.0
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
134
+ - tgagentp: `lockedChatId` set at startup to fix race condition with agentp gateway — 0.9.0
135
+ - tgagentp: server error handler + try-catch in gateway handler (no crash on socket errors) — 0.9.0
136
+ - agentp `--tg`/`--no-tg` — gateway forwards answer to Telegram via tgagentp HTTP server; per-server queue with debounced notifications; auto-flush on server switch
137
+ - agentp session API migration — uses `POST /session/:id/message` instead of TUI endpoints
138
+ - ocmux `--print-logs` flag — pass-through to `opencode serve --print-logs` (prints server logs to stderr in the server pane)
139
+ - /agents lists only primary agents with ▶ active marker
140
+ - /agents switch calls `updateSession(agent)` to persist agent on session + `selectSession` to refresh TUI
113
141
  - Logging rebalance: stdout for info, stderr for errors/debug/trace, `--verbose` flag, `2>/dev/null` default usage
114
142
  - `/shutdown force` — refuses shutdown when busy unless `force` flag is given; goodbye message includes state
115
143
  - `/think [on|off|switch]` — toggle real-time forwarding of model thinking messages to the chat; `--think` CLI flag to start enabled
116
144
  - Event-driven permission handling (`POST /session/:id/prompt_async` + SSE listener + `/allow`, `/reject`, `/always` commands) — included in v0.7.0
117
145
  - TUI navigation on `/sessions switch` and `/sessions new` — `POST /tui/select-session` now works in opencode 1.15.13 (was: tried second, never reached)
118
- - /agents switch <name> — switch active agent on the active session
119
146
  - /sessions new [name] — create a new session, TUI follows via /session/:id/select
120
147
  - /sessions rename <name> — rename the current active session
121
148
  - Merge /session into /sessions (keep /session as alias)
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
@@ -53,9 +53,16 @@ agentp [options] [url]
53
53
  Options:
54
54
 
55
55
  - `--qa`: print the original prompt and answer with labels (useful when used as a filter)
56
+ - `--tg`: forward the answer to Telegram via tgagentp gateway (error if unreachable)
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
56
60
  - `--version`: show version
57
61
  - `--help`: show help message
58
62
 
63
+ By default, `--qa` auto-detects tgagentp (silently degrades if unavailable); standalone mode implies `--no-tg`.
64
+ With `--tg`, errors if tgagentp is unavailable.
65
+
59
66
  Arguments:
60
67
 
61
68
  - `url`: OpenCode TUI server URL or port number (defaults to `4096`). Examples: `4096`, `http://localhost:4096`, `http://192.168.1.50:4096`
@@ -111,6 +118,34 @@ From Vim/Neovim, send the current visual selection and keep the prompt with the
111
118
  :'<,'>!agentp --qa
112
119
  ```
113
120
 
121
+ From Vim/Neovim, forward the answer to your Telegram:
122
+
123
+ ```vim
124
+ :'<,'>!agentp --qa --tg
125
+ ```
126
+
127
+ (Works as long as `tgagentp` is running. The answer appears both in the editor
128
+ and in your Telegram chat.)
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
+
114
149
  ## ocmux
115
150
 
116
151
  Manage OpenCode server + TUI in tmux for project directories.
@@ -123,9 +158,10 @@ Without arguments, searches upward from `<directory>` (default: `$PWD`) for `.oc
123
158
 
124
159
  Subcommands:
125
160
 
126
- - **`serve [--git|--GIT] [dir]`** — Create a server in `dir` (default: `$PWD`) and attach a TUI pane. Aliased as `new` for backwards compatibility (to be removed in 1.0). Errors if one already exists there. Warns if a parent directory already has a server.
161
+ - **`serve [--git|--GIT] [--print-logs] [dir]`** — Create a server in `dir` (default: `$PWD`) and attach a TUI pane. Aliased as `new` for backwards compatibility (to be removed in 1.0). Errors if one already exists there. Warns if a parent directory already has a server.
127
162
  - `--git` resolves `dir` to the nearest parent with a `.git` entry (file or dir); errors if none is found.
128
163
  - `--GIT` resolves `dir` to the nearest parent with a `.git` directory only; errors if none is found.
164
+ - `--print-logs` passes `--print-logs` to `opencode serve`, which prints server logs to stderr in the server tmux pane.
129
165
  - **`kill [dir]`** — Kill the server found upward from `dir`. Removes its tmux window and state file.
130
166
  - **`list`** — List all running servers with their directories, URLs, and status.
131
167
 
@@ -153,11 +189,19 @@ Notes:
153
189
 
154
190
  ## How agentp Works
155
191
 
156
- 1. Clears the current TUI prompt input.
157
- 2. Appends each line from stdin to the TUI prompt.
158
- 3. Submits the prompt.
159
- 4. Listens to the event stream and prints assistant text parts.
160
- 5. Stops when the session reaches idle state.
192
+ 1. Reads all stdin into a single prompt string.
193
+ 2. Finds the most recently updated session, or creates one named `agentp`.
194
+ 3. Sends the prompt via the session API (`POST /session/:id/message`).
195
+ 4. Prints the assistant answer to stdout.
196
+ 5. With `--tg` (or by default when `--qa` is given), forwards the answer to Telegram
197
+ via the agentp gateway. The answer appears in your Telegram chat as if tgagentp
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.
203
+
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.
161
205
 
162
206
  Operational hint:
163
207
 
@@ -183,16 +227,22 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
183
227
 
184
228
  | Command | Action |
185
229
  |---|---|
186
- | `/help [topic]` | Show general help or help for a topic (`servers`, `sessions`, `agents`, `models`) |
230
+ | `/help [topic]` | Show general help or help for a topic (`servers`, `sessions`, `agents`, `models`, `allow`, `think`, `record`, `queue`) |
187
231
  | `/servers` | List all running ocmux-served projects with URL + status (✅ idle / ⏳ busy) |
188
232
  | `/servers switch <name>` | Switch active server; matches by full path, basename, or substring |
189
233
  | `/sessions` | List recent sessions for the current server (max 50, with date headings) |
190
234
  | `/sessions switch <number\|name>` | Switch active session by position or partial name match |
191
- | `/agents` | List available agents (name, mode, model, description) |
235
+ | `/agents` | List primary agents (▶ marker for the active one) |
236
+ | `/agents switch <name>` | Switch active agent — persists on session and refreshes TUI |
192
237
  | `/models` | List connected providers with model counts, context limits, and costs |
193
238
  | `/status` | Show current server path, URL, busy status, and active session |
194
239
  | `/cancel` | Cancel the current AI response for the active server |
195
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 |
243
+ | `/allow` | Approve a permission request once |
244
+ | `/reject` | Deny a permission request |
245
+ | `/always` | Approve and remember for the session |
196
246
  | `/shutdown [force]` | (requires `--dev`) Stop tgagentp; refuses if busy unless `force` is given |
197
247
 
198
248
  ### Per-server state
@@ -217,6 +267,18 @@ tgagentp
217
267
  - `--think`: start with thinking messages enabled (default: off)
218
268
  - `--dev`: enable `/shutdown` command for remote restart (run via `while true; do tgagentp --dev; done`)
219
269
 
270
+ ### Agentp gateway
271
+
272
+ tgagentp starts a tiny HTTP server on `127.0.0.1` that accepts `POST /send` requests from `agentp --tg`. This enables cross-tool interoperability: answers obtained through `agentp` (from scripts, editors, or pipes) are forwarded to your Telegram chat.
273
+
274
+ - Port is randomly assigned by default; overridable via `TGAGENTP_PORT`.
275
+ - Port is written to `/tmp/tgagentp-port` for agentp discovery.
276
+ - Authentication reuses `OPENCODE_SERVER_PASSWORD`.
277
+ - Messages for the active server are delivered immediately.
278
+ - Messages for non-active servers are queued and delivered on `/servers switch`.
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.
281
+
220
282
  ### Logging
221
283
 
222
284
  Informational messages (startup, discovery, session switches) go to **stdout**. Errors, warnings, and trace/debug messages go to **stderr**. With `--verbose`, detailed
@@ -243,9 +305,11 @@ A startup greeting is sent to the last known chat on boot — includes /status-s
243
305
  | Variable | Description |
244
306
  |---|---|
245
307
  | `TELEGRAM_BOT_TOKEN` | **Required.** Telegram bot token from @BotFather. |
246
- | `OPENCODE_SERVER_PASSWORD` | Optional. OpenCode server HTTP Basic Auth password. |
308
+ | `OPENCODE_SERVER_PASSWORD` | Optional. OpenCode server HTTP Basic Auth password (also used for agentp gateway auth). |
247
309
  | `OPENCODE_SERVER_USERNAME` | Optional. OpenCode server username (default: `opencode`). |
248
310
  | `TGAGENTP_ALLOWED_CHAT_IDS` | Optional. Comma-separated Telegram chat IDs that are allowed to use the bot. |
311
+ | `TGAGENTP_PORT` | Optional. Agentp gateway listen port (default: `0` = random). |
312
+ | `TGAGENTP_DEBOUNCE_MS` | Optional. Debounce interval for queued-message notifications (default: `5000`). |
249
313
 
250
314
  ## License
251
315
 
package/bin/agentp CHANGED
@@ -1,39 +1,83 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const http = require('http');
4
+ const fs = require('fs');
3
5
  const readline = require('readline');
4
6
  const { version } = require('../package.json');
5
7
  const {
6
- clearPrompt,
7
- appendPrompt,
8
- submitPrompt,
9
- listenForFinalAnswer,
8
+ sendToSession,
9
+ listSessions,
10
+ createSession,
11
+ getSession,
10
12
  } = require('../lib/opencode');
11
13
 
12
- async function sendPrompt(server) {
13
- let promptText = '';
14
+ const TGAGENTP_PORT_FILE = '/tmp/tgagentp-port';
14
15
 
15
- await clearPrompt(server);
16
+ function notifyAgentpGateway(port, server, text) {
17
+ return new Promise((resolve, reject) => {
18
+ const body = JSON.stringify({ text, server });
19
+ const options = {
20
+ hostname: '127.0.0.1',
21
+ port,
22
+ path: '/send',
23
+ method: 'POST',
24
+ headers: {
25
+ 'Content-Type': 'application/json',
26
+ 'Content-Length': Buffer.byteLength(body),
27
+ },
28
+ };
29
+ const password = process.env.OPENCODE_SERVER_PASSWORD;
30
+ if (password) {
31
+ const username = process.env.OPENCODE_SERVER_USERNAME || 'opencode';
32
+ options.headers['Authorization'] = 'Basic ' +
33
+ Buffer.from(`${username}:${password}`).toString('base64');
34
+ }
35
+ const req = http.request(options, (res) => {
36
+ let data = '';
37
+ res.on('data', (chunk) => { data += chunk; });
38
+ res.on('end', () => {
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
+ }
49
+ });
50
+ });
51
+ req.setTimeout(5000, () => {
52
+ req.destroy();
53
+ reject(new Error('Connection timed out'));
54
+ });
55
+ req.on('error', reject);
56
+ req.write(body);
57
+ req.end();
58
+ });
59
+ }
16
60
 
61
+ async function readAllStdin() {
62
+ let text = '';
17
63
  const rl = readline.createInterface({ input: process.stdin });
18
-
19
64
  for await (const line of rl) {
20
- const promptLine = line + '\n';
21
- promptText += promptLine;
22
- await appendPrompt(server, promptLine);
65
+ text += line + '\n';
23
66
  }
24
-
25
67
  rl.close();
26
- await submitPrompt(server);
27
- return promptText;
68
+ return text;
28
69
  }
29
70
 
30
- const HUMAN_ROW = '👤:  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄\n'
31
- const AGENT_ROW = '🤖:  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄\n'
32
- const FINAL_ROW = ' ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄\n';
71
+ const HUMAN_ROW = '👤: —————————————————\n'
72
+ const AGENT_ROW = '🤖: —————————————————\n'
73
+ const FINAL_ROW = ' —————————————————\n';
33
74
 
34
75
  async function main() {
35
76
  let serverBase = 'http://localhost:4096';
36
77
  let qaMode = false;
78
+ let flushMode = false;
79
+ // tgMode: false = disabled, 'auto' = auto-detect, true = explicit --tg
80
+ let tgMode = null;
37
81
 
38
82
  const args = process.argv.slice(2);
39
83
  for (let i = 0; i < args.length; i++) {
@@ -43,27 +87,94 @@ async function main() {
43
87
  } else if (args[i] === '--help') {
44
88
  console.error('Usage: agentp [options] [url]');
45
89
  console.error('');
46
- console.error('Pipe prompt text to an OpenCode TUI session and stream the assistant response to stdout.');
90
+ console.error('Pipe prompt text to an OpenCode session and get the assistant response on stdout.');
47
91
  console.error('');
48
92
  console.error('Options:');
49
93
  console.error(' --version Show version');
50
94
  console.error(' --qa Print the original prompt and answer with labels (useful as a filter)');
95
+ console.error(' --tg Forward the answer to Telegram via agentp gateway (error if unavailable)');
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');
51
99
  console.error(' --help Show this help message');
52
100
  console.error('');
101
+ console.error('By default, --qa auto-detects tgagentp; standalone mode implies --no-tg.');
102
+ console.error('--tg: error if tgagentp is unavailable; auto mode: silently degrade.');
103
+ console.error('');
53
104
  console.error('Arguments:');
54
- console.error(' url OpenCode TUI server URL or port number (default: 4096)');
105
+ console.error(' url OpenCode server URL or port number (default: 4096)');
55
106
  console.error(' Examples: 4096, http://localhost:4096, http://192.168.1.50:4096');
56
107
  console.error('');
57
- console.error('See also: ocmux -- manage OpenCode tmux sessions (ocmux --help)');
108
+ console.error('See also: tgagentp -- Telegram bridge (tgagentp --help), ocmux -- manage OpenCode tmux sessions (ocmux --help)');
58
109
  console.error('');
59
110
  console.error('Examples:');
60
111
  console.error(' printf "Summarize this file" | agentp');
61
112
  console.error(' cat prompt.txt | agentp 4096');
62
113
  console.error(' # Vim/Neovim filter, preserving prompt + answer:');
63
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');
64
118
  process.exit(0);
65
119
  } else if (args[i] === '--qa') {
66
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;
174
+ } else if (args[i] === '--tg') {
175
+ tgMode = true;
176
+ } else if (args[i] === '--no-tg') {
177
+ tgMode = false;
67
178
  } else if (args[i].includes('://')) {
68
179
  serverBase = args[i].replace(/\/+$/, '');
69
180
  } else if (/^\d+$/.test(args[i])) {
@@ -71,20 +182,102 @@ async function main() {
71
182
  }
72
183
  }
73
184
 
74
- const promptText = await sendPrompt(serverBase);
185
+ // Resolve default: --qa → auto-detect, standalone → disabled
186
+ if (tgMode === null) {
187
+ tgMode = qaMode ? 'auto' : false;
188
+ }
189
+
190
+ // Pre-send: probe tgagentp gateway if enabled
191
+ let tgPort = null;
192
+ if (tgMode) {
193
+ try {
194
+ tgPort = fs.readFileSync(TGAGENTP_PORT_FILE, 'utf8').trim();
195
+ if (!tgPort || !/^\d+$/.test(tgPort)) tgPort = null;
196
+ } catch {
197
+ tgPort = null;
198
+ }
199
+ if (!tgPort && tgMode === true) {
200
+ console.error('Error: tgagentp gateway not found. Is tgagentp running?');
201
+ process.exit(1);
202
+ }
203
+ // auto mode: tgPort stays null → silently skip tgagentp
204
+ }
205
+
206
+ const promptText = await readAllStdin();
207
+
208
+ // Find most recent session or create one
209
+ let sessions;
210
+ try {
211
+ sessions = await listSessions(serverBase);
212
+ } catch (err) {
213
+ console.error('Error connecting to server:', err.message);
214
+ process.exit(1);
215
+ }
216
+
217
+ let sessionId = null;
218
+ if (sessions && sessions.length > 0) {
219
+ const sorted = sessions
220
+ .filter(s => s.time && s.time.updated)
221
+ .sort((a, b) => b.time.updated - a.time.updated);
222
+ if (sorted.length > 0) sessionId = sorted[0].id;
223
+ }
224
+
225
+ if (!sessionId) {
226
+ try {
227
+ const session = await createSession(serverBase, 'agentp');
228
+ sessionId = session.id;
229
+ } catch (err) {
230
+ console.error('Error creating session:', err.message);
231
+ process.exit(1);
232
+ }
233
+ }
234
+
235
+ let answer;
236
+ try {
237
+ answer = await sendToSession(serverBase, sessionId, promptText);
238
+ } catch (err) {
239
+ console.error('Error sending prompt:', err.message);
240
+ process.exit(1);
241
+ }
75
242
 
243
+ // Build the message to send to Telegram (QA pair with rulers)
244
+ let tgMessage = answer;
76
245
  if (qaMode) {
77
- process.stdout.write(HUMAN_ROW);
78
- process.stdout.write(promptText);
79
- process.stdout.write(AGENT_ROW);
246
+ tgMessage = HUMAN_ROW + promptText + AGENT_ROW + answer;
247
+ if (!answer.endsWith('\n')) tgMessage += '\n';
248
+ tgMessage += FINAL_ROW;
80
249
  }
81
250
 
82
- await listenForFinalAnswer(serverBase, (chunk) => process.stdout.write(chunk));
83
- process.stdout.write('\n');
251
+ // Post-send: notify tgagentp, get recorded context back
252
+ let buffered = [];
253
+ let tgError = null;
254
+ if (tgPort) {
255
+ try {
256
+ buffered = await notifyAgentpGateway(tgPort, serverBase, tgMessage);
257
+ } catch (err) {
258
+ tgError = err;
259
+ }
260
+ }
261
+
262
+ // Build stdout output (prepend recorded context if any, unless --flush)
84
263
  if (qaMode) {
85
- process.stdout.write(FINAL_ROW);
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');
86
274
  }
87
275
 
276
+ // Warnings at the end (stderr)
277
+ if (tgError && tgMode === true) {
278
+ process.stderr.write('\n');
279
+ process.stderr.write(`⚠️ Telegram notification failed: ${tgError.message}\n`);
280
+ }
88
281
  if (!process.env.OPENCODE_SERVER_PASSWORD) {
89
282
  process.stderr.write('\n');
90
283
  process.stderr.write('🔓 ⚠️ Server is not password-protected! Set OPENCODE_SERVER_PASSWORD for better security.\n');