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.
- package/CONTRIBUTING.md +139 -0
- package/README.md +54 -8
- package/bin/agentp +159 -20
- package/bin/ocmux +56 -41
- package/bin/tgagentp +971 -206
- package/docs/specification.md +391 -0
- package/lib/ocmux.js +113 -2
- package/lib/opencode.js +79 -12
- package/package.json +9 -2
- package/AGENTS.md +0 -177
- package/CHANGELOG.md +0 -24
- package/debugging_tg_integration.md +0 -5594
- package/tgagentp.log +0 -2156
package/CONTRIBUTING.md
ADDED
|
@@ -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`
|
|
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;
|
|
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
|
|
251
|
-
-
|
|
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
|
-
###
|
|
316
|
+
### State persistence
|
|
271
317
|
|
|
272
|
-
|
|
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)
|
|
39
|
-
|
|
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
|
|
84
|
-
console.error(' --qa
|
|
85
|
-
console.error(' --tg
|
|
86
|
-
console.error(' --no-tg
|
|
87
|
-
console.error(' --
|
|
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
|
|
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
|
|
176
|
-
let
|
|
303
|
+
// Build the message to send to Telegram (QA pair with rulers)
|
|
304
|
+
let tgMessage = answer;
|
|
177
305
|
if (qaMode) {
|
|
178
|
-
|
|
179
|
-
if (!answer.endsWith('\n'))
|
|
180
|
-
|
|
181
|
-
process.stdout.write(tgText);
|
|
182
|
-
} else {
|
|
183
|
-
process.stdout.write(answer);
|
|
184
|
-
if (!answer.endsWith('\n')) process.stdout.write('\n');
|
|
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
|
|
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,
|
|
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');
|