agentp 0.9.0 → 0.11.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.
@@ -0,0 +1,139 @@
1
+ # Contributing to agentp
2
+
3
+ ## Introduction
4
+
5
+ agentp is a collection of three zero-dependency Node.js CLI tools that extend [OpenCode](https://opencode.ai) with per-project tmux server management (`ocmux`), a stdin-to-session pipe (`agentp`), and a Telegram bot bridge (`tgagentp`).
6
+
7
+ The project aims to stay **zero npm dependencies** — all tools use only the Node.js 18+ stdlib (`http`, `https`, `readline`, `url`, `child_process`, `fs`, `path`, `crypto`, `os`). PRs introducing new dependencies will not be accepted unless there is an exceptional justification.
8
+
9
+ ## Development Setup
10
+
11
+ ### Prerequisites
12
+
13
+ - Node.js >= 18
14
+ - npm (ships with Node.js)
15
+ - tmux (optional, only needed for `ocmux` and `tgagentp` features)
16
+
17
+ ### Local Install
18
+
19
+ ```bash
20
+ git clone <your-fork>
21
+ cd agentp
22
+ npm link # registers bin/agentp, bin/ocmux, bin/tgagentp globally
23
+ # or
24
+ npm install -g . # alternative
25
+ ```
26
+
27
+ After linking, all three binaries are available globally. Run `tgagentp --help` or refer to `README.md`.
28
+
29
+ ### Code Map
30
+
31
+ ```
32
+ agentp/
33
+ ├── bin/
34
+ │ ├── agentp — Stdin-to-OpenCode pipe
35
+ │ ├── ocmux — Tmux server manager
36
+ │ └── tgagentp — Telegram bot bridge
37
+ ├── lib/
38
+ │ ├── opencode.js — HTTP session API client (shared by agentp + tgagentp)
39
+ │ └── ocmux.js — Tmux management (shared by ocmux + tgagentp)
40
+ ├── tests/
41
+ │ ├── opencode.test.js — Unit tests for lib/opencode.js
42
+ │ └── ocmux.test.js — Unit tests for lib/ocmux.js
43
+ ├── docs/
44
+ │ └── specification.md — Technical architecture reference
45
+ ├── AGENTS.md — Development notes and TODO
46
+ ├── CONTRIBUTING.md — This file
47
+ └── package.json
48
+ ```
49
+
50
+ ## Coding Standards
51
+
52
+ ### Style
53
+
54
+ - **CommonJS** (`require` / `module.exports`) — no ES modules
55
+ - **No semicolons** — the project uses ASI (automatic semicolon insertion)
56
+ - **No comments** in production code — let the code speak; use descriptive variable/function names
57
+ - **2-space indentation**
58
+ - Single quotes for strings
59
+ - `const` over `let`; avoid `var`
60
+ - Arrow functions for callbacks and closures
61
+
62
+ ### Conventions
63
+
64
+ - Async functions: use `async/await`, avoid raw `.then()`
65
+ - Error handling: use try-catch at call sites; log errors via `log.error()`
66
+ - Logging: use the `log` helper (`log.info`, `log.error`, `log.debug`) — never `console.log`
67
+ - HTTP: use `lib/opencode.js` request helpers instead of raw `http.request`
68
+ - Tmux: use `lib/ocmux.js` helpers instead of raw `spawnSync`
69
+
70
+ ### Architecture Rules
71
+
72
+ 1. **Zero npm dependencies.** The `package.json` `"dependencies"` field must remain empty.
73
+ 2. **`bin/`** files are entry points — keep them thin. Business logic goes in `lib/`.
74
+ 3. **`bin/tgagentp`** is the largest file (~2000 lines). When adding new features, extract reusable logic into `lib/` when possible.
75
+ 4. **Shared state** (e.g., `chatStates`, `serverOwners`) is held in module-level variables in `bin/tgagentp` and `lib/ocmux.js`. Be mindful of mutation.
76
+ 5. **All external calls must be mockable.** `lib/opencode.js` tests mock `http.request`; `lib/ocmux.js` tests mock `child_process.spawnSync` and `fs.*`.
77
+
78
+ ## Running Tests
79
+
80
+ Tests use Node.js built-in test runner (`node:test`) — zero additional dependencies.
81
+
82
+ ```bash
83
+ # Run all tests
84
+ npm test
85
+
86
+ # Run a specific test file
87
+ node --test tests/opencode.test.js
88
+ node --test tests/ocmux.test.js
89
+
90
+ # Run with verbose output
91
+ node --test tests/opencode.test.js | bunyan # or just grep for results
92
+ ```
93
+
94
+ All external interfaces are mocked — tests run entirely in-process without touching the network, tmux, or the filesystem. They are safe to run alongside a live OpenCode instance.
95
+
96
+ ### Test Architecture
97
+
98
+ Tests are structured in phases (see `AGENTS.md` for the full plan):
99
+
100
+ | Phase | Module | Boundary Mocked |
101
+ |-------|--------|----------------|
102
+ | 1a | `lib/opencode.js` | `http.request` |
103
+ | 1b | `lib/ocmux.js` | `child_process.spawnSync`, `child_process.execSync`, `fs.*` |
104
+
105
+ Each test file uses `node:test`'s `mock` API in `before()`/`after()` hooks to install and tear down mocks. Tests within a describe block run serially (`concurrency: false`) when they share mocked state.
106
+
107
+ ### Adding Tests
108
+
109
+ 1. Place new tests in `tests/<module>.test.js`
110
+ 2. Use `describe`, `it`, `before`, `after` from `node:test`
111
+ 3. Use `node:assert` for assertions
112
+ 4. Mock all external boundaries (network, filesystem, subprocesses)
113
+ 5. Run the full suite before submitting a PR
114
+
115
+ ## Pull Request Process
116
+
117
+ 1. **Fork the repo** and create a feature branch from `main`.
118
+ 2. **Make your changes** following the coding standards above.
119
+ 3. **Run `npm test`** and ensure all tests pass.
120
+ 4. **Update documentation** if your change affects user-facing behavior:
121
+ - Help text in `bin/tgagentp` (the `cmdHelp` function)
122
+ - `docs/specification.md` for architecture changes
123
+ - Command table in `devto-article.md` (if adding/changing a slash command)
124
+ - `AGENTS.md` Done section (move items in/out as appropriate)
125
+ 5. **Commit with a descriptive message** following the existing style (e.g., `fix: ...`, `feat: ...`, `refactor: ...`, `docs: ...`).
126
+ 6. **Open a pull request** against `main`. Include a summary of the change and any testing instructions.
127
+
128
+ ### Review Process
129
+
130
+ - Maintainers review within a few business days
131
+ - Focus areas: mock correctness, zero-dependency rule, architectural consistency
132
+ - Large changes may be asked to split into smaller PRs
133
+ - All PRs must pass the test suite before merging
134
+
135
+ ## Getting Help
136
+
137
+ - Open an issue on GitHub for bugs or feature requests
138
+ - Tag questions with `question` label for general help
139
+ - For OpenCode-specific questions, refer to [opencode.ai](https://opencode.ai)
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.
@@ -141,6 +163,7 @@ Subcommands:
141
163
  - `--GIT` resolves `dir` to the nearest parent with a `.git` directory only; errors if none is found.
142
164
  - `--print-logs` passes `--print-logs` to `opencode serve`, which prints server logs to stderr in the server tmux pane.
143
165
  - **`kill [dir]`** — Kill the server found upward from `dir`. Removes its tmux window and state file.
166
+ - **`resurrect [--print-logs] [dir]`** — Recover a dead/crashed server: reads `.ocmux.json`, kills old tmux window, removes state file, then creates a fresh server + TUI in the same directory. Works even if no tmux window exists (stale state file).
144
167
  - **`list`** — List all running servers with their directories, URLs, and status.
145
168
 
146
169
  Options:
@@ -174,6 +197,10 @@ Notes:
174
197
  5. With `--tg` (or by default when `--qa` is given), forwards the answer to Telegram
175
198
  via the agentp gateway. The answer appears in your Telegram chat as if tgagentp
176
199
  itself had processed the request.
200
+ 6. If tgagentp's [/record](#tgagentp) feature was active, the gateway response includes
201
+ the recorded conversation buffer. In `--qa` mode, agentp prepends this buffer (with
202
+ rulers) to its stdout so OpenCode receives the full Telegram context. Use `--flush`
203
+ to clear the buffer without prepending.
177
204
 
178
205
  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
206
 
@@ -201,7 +228,7 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
201
228
 
202
229
  | Command | Action |
203
230
  |---|---|
204
- | `/help [topic]` | Show general help or help for a topic (`servers`, `sessions`, `agents`, `models`, `allow`, `think`) |
231
+ | `/help [topic]` | Show general help or help for a topic (`servers`, `sessions`, `agents`, `models`, `allow`, `think`, `record`, `queue`) |
205
232
  | `/servers` | List all running ocmux-served projects with URL + status (✅ idle / ⏳ busy) |
206
233
  | `/servers switch <name>` | Switch active server; matches by full path, basename, or substring |
207
234
  | `/sessions` | List recent sessions for the current server (max 50, with date headings) |
@@ -212,10 +239,28 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
212
239
  | `/status` | Show current server path, URL, busy status, and active session |
213
240
  | `/cancel` | Cancel the current AI response for the active server |
214
241
  | `/think [on\|off\|switch]` | Toggle forwarding of model thinking messages to the chat |
242
+ | `/record [stop]` | Toggle recording of Telegram conversation for agentp context; `/record stop` clears and stops |
243
+ | `/servers switch <name> [--force]` | Switch to a server; `--force` takes over from another chat |
244
+ | `/sessions` | List recent sessions for the current server (max 50, with date headings) |
245
+ | `/sessions switch <number\|name>` | Switch active session by position or partial name match |
246
+ | `/agents` | List primary agents (▶ marker for the active one) |
247
+ | `/agents switch <name>` | Switch active agent — persists on session and refreshes TUI |
248
+ | `/models` | List connected providers with model counts, context limits, and costs |
249
+ | `/status` | Show current server path, URL, busy status, and active session |
250
+ | `/cancel` | Cancel the current AI response for the active server |
251
+ | `/think [on\|off\|switch]` | Toggle forwarding of model thinking messages to the chat |
252
+ | `/record [stop]` | Toggle recording of Telegram conversation for agentp context; `/record stop` clears and stops |
253
+ | `/queue <message>` | Queue a message when the server is busy; auto-sent when current task finishes |
254
+ | `/flush` | Clear all queued messages (manual and auto-queued) |
255
+ | `/resurrect` | Recover a dead server — restart processes in the same directory, reconnect chat |
215
256
  | `/allow` | Approve a permission request once |
216
257
  | `/reject` | Deny a permission request |
217
258
  | `/always` | Approve and remember for the session |
218
- | `/shutdown [force]` | (requires `--dev`) Stop tgagentp; refuses if busy unless `force` is given |
259
+ | `/shutdown [force\|clear]` | (requires `--dev`) Stop tgagentp; `clear` also wipes saved connections |
260
+
261
+ ### Chat-server ownership
262
+
263
+ Each server can be owned by at most one chat at a time. New chats start disconnected. Use `/servers switch <name>` to connect; `--force` takes over and notifies the previous owner. Connections are persisted to `/tmp/tgagentp-connections.json` and restored automatically on restart (server URL is re-discovered from `.ocmux.json`).
219
264
 
220
265
  ### Per-server state
221
266
 
@@ -246,9 +291,10 @@ tgagentp starts a tiny HTTP server on `127.0.0.1` that accepts `POST /send` requ
246
291
  - Port is randomly assigned by default; overridable via `TGAGENTP_PORT`.
247
292
  - Port is written to `/tmp/tgagentp-port` for agentp discovery.
248
293
  - Authentication reuses `OPENCODE_SERVER_PASSWORD`.
249
- - Messages for the active server are delivered immediately.
250
- - Messages for non-active servers are queued and delivered on `/servers switch`.
251
- - Debounced Telegram notification on queue (configurable via `TGAGENTP_DEBOUNCE_MS`).
294
+ - Messages for the owning chat's active server are delivered immediately.
295
+ - Messages for non-active servers are queued per-server with debounced notifications (configurable via `TGAGENTP_DEBOUNCE_MS`); delivered on `/servers switch`.
296
+ - Server health detection pre-sends: if a server is unreachable, messages are auto-queued and delivered when it comes back. `/flush` clears all queues.
297
+ - 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
298
 
253
299
  ### Logging
254
300
 
@@ -267,9 +313,9 @@ To capture everything (info + errors) to a log file:
267
313
  tgagentp 2>/var/log/tgagentp.log
268
314
  ```
269
315
 
270
- ### Telemetry
316
+ ### State persistence
271
317
 
272
- A startup greeting is sent to the last known chat on boot — includes /status-style server info. The chat ID is persisted at `/tmp/tgagentp-startup-chat`.
318
+ Chat-to-server directory mappings are saved to `/tmp/tgagentp-connections.json` on every connection. On restart, tgagentp reads this file, discovers the server URL from each directory's `.ocmux.json`, and reconnects automatically with a welcome message. Use `/shutdown clear` (requires `--dev`) to wipe the saved state for a clean start.
273
319
 
274
320
  ### Environment variables
275
321
 
package/bin/agentp CHANGED
@@ -3,11 +3,13 @@
3
3
  const http = require('http');
4
4
  const fs = require('fs');
5
5
  const readline = require('readline');
6
+ const child_process = require('child_process');
6
7
  const { version } = require('../package.json');
7
8
  const {
8
9
  sendToSession,
9
10
  listSessions,
10
11
  createSession,
12
+ getSession,
11
13
  } = require('../lib/opencode');
12
14
 
13
15
  const TGAGENTP_PORT_FILE = '/tmp/tgagentp-port';
@@ -35,8 +37,16 @@ function notifyAgentpGateway(port, server, text) {
35
37
  let data = '';
36
38
  res.on('data', (chunk) => { data += chunk; });
37
39
  res.on('end', () => {
38
- if (res.statusCode === 200) resolve();
39
- else reject(new Error(`status ${res.statusCode}: ${data}`));
40
+ if (res.statusCode === 200) {
41
+ try {
42
+ const result = JSON.parse(data);
43
+ resolve(result.buffered || []);
44
+ } catch {
45
+ resolve([]);
46
+ }
47
+ } else {
48
+ reject(new Error(`status ${res.statusCode}: ${data}`));
49
+ }
40
50
  });
41
51
  });
42
52
  req.setTimeout(5000, () => {
@@ -66,6 +76,7 @@ const FINAL_ROW = ' —————————————————\n';
66
76
  async function main() {
67
77
  let serverBase = 'http://localhost:4096';
68
78
  let qaMode = false;
79
+ let flushMode = false;
69
80
  // tgMode: false = disabled, 'auto' = auto-detect, true = explicit --tg
70
81
  let tgMode = null;
71
82
 
@@ -80,29 +91,138 @@ async function main() {
80
91
  console.error('Pipe prompt text to an OpenCode session and get the assistant response on stdout.');
81
92
  console.error('');
82
93
  console.error('Options:');
83
- console.error(' --version Show version');
84
- console.error(' --qa Print the original prompt and answer with labels (useful as a filter)');
85
- console.error(' --tg Forward the answer to Telegram via agentp gateway (error if unavailable)');
86
- console.error(' --no-tg Do not forward to Telegram');
87
- console.error(' --help Show this help message');
94
+ console.error(' --version Show version');
95
+ console.error(' --qa Print the original prompt and answer with labels (useful as a filter)');
96
+ console.error(' --tg Forward the answer to Telegram via agentp gateway (error if unavailable)');
97
+ console.error(' --no-tg Do not forward to Telegram');
98
+ console.error(' --flush Flush tgagentp\'s recorded buffer without prepending it to output');
99
+ console.error(' --getLast Retrieve last N assistant answers (or QA pairs with --qa) from session history');
100
+ console.error(' --help Show this help message');
88
101
  console.error('');
89
102
  console.error('By default, --qa auto-detects tgagentp; standalone mode implies --no-tg.');
90
103
  console.error('--tg: error if tgagentp is unavailable; auto mode: silently degrade.');
91
104
  console.error('');
105
+ console.error('When --qa is combined with --getLast, the output includes full QA pairs');
106
+ console.error('(user prompt + assistant answer) with rulers, matching the recorded');
107
+ console.error('context replay format. Without --qa, only the assistant answers are shown.');
108
+ console.error('');
109
+ console.error('--flush clears the tgagentp recorded buffer and also prevents the prepended');
110
+ console.error('context from appearing in --qa output. Use it when you want fresh context.');
111
+ console.error('');
92
112
  console.error('Arguments:');
93
113
  console.error(' url OpenCode server URL or port number (default: 4096)');
94
114
  console.error(' Examples: 4096, http://localhost:4096, http://192.168.1.50:4096');
115
+ console.error(' Use `ocmux` (with no args) to get the URL of the current project\'s server.');
95
116
  console.error('');
96
117
  console.error('See also: tgagentp -- Telegram bridge (tgagentp --help), ocmux -- manage OpenCode tmux sessions (ocmux --help)');
97
118
  console.error('');
98
119
  console.error('Examples:');
99
120
  console.error(' printf "Summarize this file" | agentp');
100
- console.error(' cat prompt.txt | agentp 4096');
121
+ console.error(' cat prompt.txt | agentp $(ocmux) # talk to current project\'s server');
101
122
  console.error(' # Vim/Neovim filter, preserving prompt + answer:');
102
- console.error(" :'<,'>!agentp --qa");
123
+ console.error(" :'<,'>!agentp --qa $(ocmux)");
124
+ console.error(' # Retrieve last 3 assistant answers:');
125
+ console.error(' agentp --getLast 3');
126
+ console.error(' agentp --getLast 3 --qa # full QA pairs with rulers');
127
+ console.error(' agentp --getLast 1 4096');
103
128
  process.exit(0);
104
129
  } else if (args[i] === '--qa') {
105
130
  qaMode = true;
131
+ } else if (args[i] === '--flush') {
132
+ flushMode = true;
133
+ } else if (args[i] === '--getLast') {
134
+ i++;
135
+ const nStr = args[i];
136
+ if (!nStr || !/^\d+$/.test(nStr)) {
137
+ console.error('Error: --getLast requires a number (e.g. --getLast 5)');
138
+ process.exit(1);
139
+ }
140
+ const n = parseInt(nStr, 10);
141
+ try {
142
+ const sessions = await listSessions(serverBase);
143
+ if (!sessions || sessions.length === 0) {
144
+ console.error('No sessions found.');
145
+ process.exit(1);
146
+ }
147
+ const sorted = sessions
148
+ .filter(s => s.time && s.time.updated)
149
+ .sort((a, b) => b.time.updated - a.time.updated);
150
+ const sessionId = sorted[0].id;
151
+
152
+ function buildPairsFromMessages(messages) {
153
+ const pairs = [];
154
+ let cur = null;
155
+ for (const raw of messages) {
156
+ // Normalize: export gives {role, parts: [{type, text}]}, SQLite gives {role, text}
157
+ const m = raw.parts ? raw : { role: raw.role, parts: [{ type: 'text', text: raw.text }] };
158
+ const role = m.role;
159
+ const text = (m.parts || []).filter(p => p.type === 'text').map(p => p.text).join('').trim();
160
+ if (!text) continue;
161
+ if (role === 'user') {
162
+ if (cur) pairs.push(cur);
163
+ cur = { prompt: text, answer: '' };
164
+ } else if (role === 'assistant' && cur) {
165
+ if (cur.answer) cur.answer += '\n';
166
+ cur.answer += text;
167
+ }
168
+ }
169
+ // Don't push incomplete pair (no answer yet)
170
+ return pairs;
171
+ }
172
+
173
+ function formatPairs(pairs) {
174
+ return pairs.map(p =>
175
+ HUMAN_ROW + p.prompt + '\n' + AGENT_ROW + p.answer + '\n' + FINAL_ROW
176
+ ).join('\n');
177
+ }
178
+
179
+ const formatAsQa = qaMode || args.includes('--qa');
180
+ function extractAnswers(pairs) {
181
+ return pairs.map(p => p.answer);
182
+ }
183
+ let output = '';
184
+ try {
185
+ const raw = child_process.execFileSync('opencode', ['export', sessionId], { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
186
+ const jsonStart = raw.indexOf('{');
187
+ const body = jsonStart >= 0 ? raw.slice(jsonStart) : raw;
188
+ const exportData = JSON.parse(body);
189
+ const messages = exportData.messages || [];
190
+ const pairs = buildPairsFromMessages(messages);
191
+ const lastN = pairs.slice(-n);
192
+ output = formatAsQa ? formatPairs(lastN) : extractAnswers(lastN).join('\n');
193
+ } catch {
194
+ // export JSON is malformed (opencode bug);
195
+ // fall back to SQLite
196
+ const home = process.env.HOME || process.env.USERPROFILE || '';
197
+ const dbCandidates = [
198
+ `${home}/.local/share/opencode/opencode.db`,
199
+ `${home}/Library/Application Support/opencode/opencode.db`,
200
+ `${home}/.opencode/opencode.db`,
201
+ ];
202
+ let dbPath = null;
203
+ for (const p of dbCandidates) {
204
+ try { fs.accessSync(p); dbPath = p; break; } catch {}
205
+ }
206
+ if (dbPath) {
207
+ const sql = `SELECT m.data->>'role' as role, group_concat(p.data->>'text', '') as text FROM message m JOIN part p ON p.message_id = m.id WHERE m.session_id = '${sessionId.replace(/'/g, "''")}' AND p.data->>'type' = 'text' AND m.data->>'role' IN ('user','assistant') GROUP BY m.id ORDER BY m.time_created ASC`;
208
+ const out = child_process.execFileSync('sqlite3', ['-json', dbPath, sql], { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
209
+ const messages = JSON.parse(out);
210
+ const pairs = buildPairsFromMessages(messages);
211
+ const lastN = pairs.slice(-n);
212
+ output = formatAsQa ? formatPairs(lastN) : extractAnswers(lastN).join('\n');
213
+ }
214
+ }
215
+ if (!output) {
216
+ console.error(formatAsQa ? 'No complete prompt-answer pairs found in session history.' : 'No assistant answers found in session history.');
217
+ process.exit(1);
218
+ }
219
+ process.stdout.write(output);
220
+ if (!output.endsWith('\n')) process.stdout.write('\n');
221
+ } catch (err) {
222
+ console.error('Error retrieving session history:', err.message);
223
+ process.exit(1);
224
+ }
225
+ return;
106
226
  } else if (args[i] === '--tg') {
107
227
  tgMode = true;
108
228
  } else if (args[i] === '--no-tg') {
@@ -111,6 +231,14 @@ async function main() {
111
231
  serverBase = args[i].replace(/\/+$/, '');
112
232
  } else if (/^\d+$/.test(args[i])) {
113
233
  serverBase = 'http://localhost:' + args[i];
234
+ } else if (args[i].startsWith('--')) {
235
+ console.error(`Error: unknown option '${args[i]}'`);
236
+ console.error(`Usage: agentp [options] [url]`);
237
+ console.error(`Try 'agentp --help' for more information.`);
238
+ process.exit(1);
239
+ } else {
240
+ console.error(`Error: unexpected argument '${args[i]}'`);
241
+ process.exit(1);
114
242
  }
115
243
  }
116
244
 
@@ -172,28 +300,39 @@ async function main() {
172
300
  process.exit(1);
173
301
  }
174
302
 
175
- // Build full --qa output and send to stdout
176
- let tgText = answer;
303
+ // Build the message to send to Telegram (QA pair with rulers)
304
+ let tgMessage = answer;
177
305
  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');
306
+ tgMessage = HUMAN_ROW + promptText + AGENT_ROW + answer;
307
+ if (!answer.endsWith('\n')) tgMessage += '\n';
308
+ tgMessage += FINAL_ROW;
185
309
  }
186
310
 
187
- // Post-send: notify tgagentp if we have a port
311
+ // Post-send: notify tgagentp, get recorded context back
312
+ let buffered = [];
188
313
  let tgError = null;
189
314
  if (tgPort) {
190
315
  try {
191
- await notifyAgentpGateway(tgPort, serverBase, tgText);
316
+ buffered = await notifyAgentpGateway(tgPort, serverBase, tgMessage);
192
317
  } catch (err) {
193
318
  tgError = err;
194
319
  }
195
320
  }
196
321
 
322
+ // Build stdout output (prepend recorded context if any, unless --flush)
323
+ if (qaMode) {
324
+ if (!flushMode && buffered.length > 0) {
325
+ for (const msg of buffered) {
326
+ const row = msg.role === 'user' ? HUMAN_ROW : AGENT_ROW;
327
+ process.stdout.write(row + msg.text + '\n');
328
+ }
329
+ }
330
+ process.stdout.write(tgMessage);
331
+ } else {
332
+ process.stdout.write(tgMessage);
333
+ if (!tgMessage.endsWith('\n')) process.stdout.write('\n');
334
+ }
335
+
197
336
  // Warnings at the end (stderr)
198
337
  if (tgError && tgMode === true) {
199
338
  process.stderr.write('\n');