agentp 0.10.0 → 0.11.1
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 +23 -6
- package/bin/agentp +93 -33
- package/bin/ocmux +56 -41
- package/bin/tgagentp +921 -251
- package/docs/specification.md +391 -0
- package/lib/ocmux.js +113 -2
- package/lib/opencode.js +60 -21
- package/package.json +9 -2
- package/AGENTS.md +0 -182
- package/CHANGELOG.md +0 -36
- package/devto-article.md +0 -121
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
|
@@ -163,6 +163,7 @@ Subcommands:
|
|
|
163
163
|
- `--GIT` resolves `dir` to the nearest parent with a `.git` directory only; errors if none is found.
|
|
164
164
|
- `--print-logs` passes `--print-logs` to `opencode serve`, which prints server logs to stderr in the server tmux pane.
|
|
165
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).
|
|
166
167
|
- **`list`** — List all running servers with their directories, URLs, and status.
|
|
167
168
|
|
|
168
169
|
Options:
|
|
@@ -239,11 +240,27 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
|
|
|
239
240
|
| `/cancel` | Cancel the current AI response for the active server |
|
|
240
241
|
| `/think [on\|off\|switch]` | Toggle forwarding of model thinking messages to the chat |
|
|
241
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 |
|
|
242
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 |
|
|
243
256
|
| `/allow` | Approve a permission request once |
|
|
244
257
|
| `/reject` | Deny a permission request |
|
|
245
258
|
| `/always` | Approve and remember for the session |
|
|
246
|
-
| `/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`).
|
|
247
264
|
|
|
248
265
|
### Per-server state
|
|
249
266
|
|
|
@@ -274,9 +291,9 @@ tgagentp starts a tiny HTTP server on `127.0.0.1` that accepts `POST /send` requ
|
|
|
274
291
|
- Port is randomly assigned by default; overridable via `TGAGENTP_PORT`.
|
|
275
292
|
- Port is written to `/tmp/tgagentp-port` for agentp discovery.
|
|
276
293
|
- Authentication reuses `OPENCODE_SERVER_PASSWORD`.
|
|
277
|
-
- Messages for the active server are delivered immediately.
|
|
278
|
-
- Messages for non-active servers are queued
|
|
279
|
-
-
|
|
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.
|
|
280
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.
|
|
281
298
|
|
|
282
299
|
### Logging
|
|
@@ -296,9 +313,9 @@ To capture everything (info + errors) to a log file:
|
|
|
296
313
|
tgagentp 2>/var/log/tgagentp.log
|
|
297
314
|
```
|
|
298
315
|
|
|
299
|
-
###
|
|
316
|
+
### State persistence
|
|
300
317
|
|
|
301
|
-
|
|
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.
|
|
302
319
|
|
|
303
320
|
### Environment variables
|
|
304
321
|
|
package/bin/agentp
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
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,
|
|
@@ -90,30 +91,39 @@ async function main() {
|
|
|
90
91
|
console.error('Pipe prompt text to an OpenCode session and get the assistant response on stdout.');
|
|
91
92
|
console.error('');
|
|
92
93
|
console.error('Options:');
|
|
93
|
-
console.error(' --version
|
|
94
|
-
console.error(' --qa
|
|
95
|
-
console.error(' --tg
|
|
96
|
-
console.error(' --no-tg
|
|
97
|
-
console.error(' --flush
|
|
98
|
-
console.error(' --getLast
|
|
99
|
-
console.error(' --help
|
|
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');
|
|
100
101
|
console.error('');
|
|
101
102
|
console.error('By default, --qa auto-detects tgagentp; standalone mode implies --no-tg.');
|
|
102
103
|
console.error('--tg: error if tgagentp is unavailable; auto mode: silently degrade.');
|
|
103
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('');
|
|
104
112
|
console.error('Arguments:');
|
|
105
113
|
console.error(' url OpenCode server URL or port number (default: 4096)');
|
|
106
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.');
|
|
107
116
|
console.error('');
|
|
108
117
|
console.error('See also: tgagentp -- Telegram bridge (tgagentp --help), ocmux -- manage OpenCode tmux sessions (ocmux --help)');
|
|
109
118
|
console.error('');
|
|
110
119
|
console.error('Examples:');
|
|
111
120
|
console.error(' printf "Summarize this file" | agentp');
|
|
112
|
-
console.error(' cat prompt.txt | agentp
|
|
121
|
+
console.error(' cat prompt.txt | agentp $(ocmux) # talk to current project\'s server');
|
|
113
122
|
console.error(' # Vim/Neovim filter, preserving prompt + answer:');
|
|
114
|
-
console.error(" :'<,'>!agentp --qa");
|
|
123
|
+
console.error(" :'<,'>!agentp --qa $(ocmux)");
|
|
115
124
|
console.error(' # Retrieve last 3 assistant answers:');
|
|
116
125
|
console.error(' agentp --getLast 3');
|
|
126
|
+
console.error(' agentp --getLast 3 --qa # full QA pairs with rulers');
|
|
117
127
|
console.error(' agentp --getLast 1 4096');
|
|
118
128
|
process.exit(0);
|
|
119
129
|
} else if (args[i] === '--qa') {
|
|
@@ -137,35 +147,77 @@ async function main() {
|
|
|
137
147
|
const sorted = sessions
|
|
138
148
|
.filter(s => s.time && s.time.updated)
|
|
139
149
|
.sort((a, b) => b.time.updated - a.time.updated);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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 = '';
|
|
143
184
|
try {
|
|
144
|
-
const
|
|
145
|
-
|
|
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');
|
|
146
193
|
} catch {
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
}
|
|
154
214
|
}
|
|
155
|
-
|
|
156
|
-
.
|
|
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.');
|
|
215
|
+
if (!output) {
|
|
216
|
+
console.error(formatAsQa ? 'No complete prompt-answer pairs found in session history.' : 'No assistant answers found in session history.');
|
|
164
217
|
process.exit(1);
|
|
165
218
|
}
|
|
166
|
-
|
|
167
|
-
process.stdout.write(
|
|
168
|
-
if (lastN.length > 0 && !lastN[lastN.length - 1].endsWith('\n')) process.stdout.write('\n');
|
|
219
|
+
process.stdout.write(output);
|
|
220
|
+
if (!output.endsWith('\n')) process.stdout.write('\n');
|
|
169
221
|
} catch (err) {
|
|
170
222
|
console.error('Error retrieving session history:', err.message);
|
|
171
223
|
process.exit(1);
|
|
@@ -179,6 +231,14 @@ async function main() {
|
|
|
179
231
|
serverBase = args[i].replace(/\/+$/, '');
|
|
180
232
|
} else if (/^\d+$/.test(args[i])) {
|
|
181
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);
|
|
182
242
|
}
|
|
183
243
|
}
|
|
184
244
|
|
package/bin/ocmux
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const crypto = require('crypto');
|
|
6
5
|
const readline = require('readline');
|
|
7
|
-
const { spawnSync
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
8
7
|
const os = require('os');
|
|
9
8
|
const {
|
|
9
|
+
SESSION,
|
|
10
10
|
readState,
|
|
11
11
|
statefileFor,
|
|
12
12
|
tuiPaneId,
|
|
@@ -15,6 +15,13 @@ const {
|
|
|
15
15
|
activeWindowIndex,
|
|
16
16
|
paneCount,
|
|
17
17
|
listServers,
|
|
18
|
+
ensureSession,
|
|
19
|
+
pinWindowName,
|
|
20
|
+
sleep,
|
|
21
|
+
hashDir,
|
|
22
|
+
logfileFor,
|
|
23
|
+
URL_RE,
|
|
24
|
+
resurrectServer,
|
|
18
25
|
} = require('../lib/ocmux');
|
|
19
26
|
|
|
20
27
|
// ---------------------------------------------------------------------------
|
|
@@ -23,10 +30,8 @@ const {
|
|
|
23
30
|
|
|
24
31
|
const { version } = require('../package.json');
|
|
25
32
|
|
|
26
|
-
const SESSION = 'Opencode';
|
|
27
33
|
const SCRIPT = path.basename(process.argv[1] || 'ocmux');
|
|
28
34
|
const HOME = os.homedir();
|
|
29
|
-
const URL_RE = /opencode server listening on (http:\/\/[^\s]*)/;
|
|
30
35
|
|
|
31
36
|
// ---------------------------------------------------------------------------
|
|
32
37
|
// Helpers
|
|
@@ -41,18 +46,6 @@ function tmux(args) {
|
|
|
41
46
|
return result;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
function sleep(seconds) {
|
|
45
|
-
execSync(`sleep ${seconds}`, { stdio: 'ignore' });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function hashDir(dir) {
|
|
49
|
-
return crypto.createHash('md5').update(dir).digest('hex').slice(0, 12);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function logfileFor(dir) {
|
|
53
|
-
return `/tmp/opencode-serve-${hashDir(dir)}.log`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
49
|
function findParentStatefile(startDir) {
|
|
57
50
|
let dir = path.resolve(startDir);
|
|
58
51
|
while (true) {
|
|
@@ -150,20 +143,7 @@ function ensureTuiZoomed(windowIndex) {
|
|
|
150
143
|
if (paneId) tmux(['resize-pane', '-Z', '-t', paneId]);
|
|
151
144
|
}
|
|
152
145
|
|
|
153
|
-
function pinWindowName(windowIndex) {
|
|
154
|
-
tmux(['set-window-option', '-t', `${SESSION}:${windowIndex}`, 'automatic-rename', 'off']);
|
|
155
|
-
}
|
|
156
146
|
|
|
157
|
-
function ensureSession() {
|
|
158
|
-
const r = tmux(['has-session', '-t', SESSION]);
|
|
159
|
-
if (r.status !== 0) {
|
|
160
|
-
const rc = tmux(['new-session', '-d', '-s', SESSION]);
|
|
161
|
-
if (rc.status !== 0) {
|
|
162
|
-
console.error('Error: failed to create tmux session');
|
|
163
|
-
process.exit(1);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
147
|
|
|
168
148
|
function outputUrl(url, dir, longMode) {
|
|
169
149
|
if (longMode) {
|
|
@@ -185,17 +165,21 @@ function printHelp() {
|
|
|
185
165
|
console.error('that session is used.');
|
|
186
166
|
console.error('');
|
|
187
167
|
console.error('Subcommands:');
|
|
188
|
-
console.error(' serve
|
|
189
|
-
console.error('
|
|
190
|
-
console.error('
|
|
191
|
-
console.error('
|
|
192
|
-
console.error('
|
|
193
|
-
console.error('
|
|
194
|
-
console.error('
|
|
195
|
-
console.error('
|
|
196
|
-
console.error(' kill
|
|
197
|
-
console.error('
|
|
198
|
-
console.error('
|
|
168
|
+
console.error(' serve Create a server in <directory> (default: $PWD) and attach a TUI.');
|
|
169
|
+
console.error(' Aliased as "new" for backwards compatibility.');
|
|
170
|
+
console.error(' Errors if one already exists there.');
|
|
171
|
+
console.error(' Warns if a parent directory already has a server.');
|
|
172
|
+
console.error(' --git Use the nearest parent with a .git entry (file or dir).');
|
|
173
|
+
console.error(' --GIT Use the nearest parent with a .git directory only.');
|
|
174
|
+
console.error(' Refuses to create above an existing .ocmux.json found while searching.');
|
|
175
|
+
console.error(' --print-logs Pass --print-logs to opencode serve (prints server logs to stderr).');
|
|
176
|
+
console.error(' kill Kill the server found upward from <directory>.');
|
|
177
|
+
console.error(' Removes its tmux window and state file.');
|
|
178
|
+
console.error(' resurrect Recover a dead server: kills the old window, removes the state file,');
|
|
179
|
+
console.error(' then creates a fresh server + TUI in the same directory.');
|
|
180
|
+
console.error(' Works even if no tmux window exists (stale state file).');
|
|
181
|
+
console.error(' --print-logs Pass --print-logs to the new opencode serve.');
|
|
182
|
+
console.error(' list List all running servers with their directories and URLs.');
|
|
199
183
|
console.error('');
|
|
200
184
|
console.error(' -- Treat the next argument as a directory even if it matches');
|
|
201
185
|
console.error(' a subcommand name (e.g. "ocmux -- kill").');
|
|
@@ -207,6 +191,7 @@ function printHelp() {
|
|
|
207
191
|
console.error(' -h Show this help');
|
|
208
192
|
console.error('');
|
|
209
193
|
console.error('See also: agentp -- pipe prompts into OpenCode from stdin');
|
|
194
|
+
console.error(' tgagentp -- Telegram bridge for OpenCode (tgagentp --help)');
|
|
210
195
|
process.exit(0);
|
|
211
196
|
}
|
|
212
197
|
|
|
@@ -428,6 +413,30 @@ function cmdKill(statefile) {
|
|
|
428
413
|
console.error('Done.');
|
|
429
414
|
}
|
|
430
415
|
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
// Subcommand: resurrect — kill dead server and create a fresh one
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
function cmdResurrect(targetDir, longMode, printLogs) {
|
|
421
|
+
const sf = findParentStatefile(targetDir);
|
|
422
|
+
if (!sf) {
|
|
423
|
+
console.error(`Error: no .ocmux.json found in ${targetDir} or any of its parents.`);
|
|
424
|
+
console.error("Run 'ocmux serve' to create a new server.");
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const serverDir = path.dirname(sf);
|
|
429
|
+
console.error(`Resurrecting server for ${serverDir}...`);
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
const { url } = resurrectServer(serverDir, printLogs);
|
|
433
|
+
outputUrl(url, serverDir, longMode);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
console.error(`Error: ${err.message}`);
|
|
436
|
+
process.exit(1);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
431
440
|
// ---------------------------------------------------------------------------
|
|
432
441
|
// Argument parsing
|
|
433
442
|
// ---------------------------------------------------------------------------
|
|
@@ -489,7 +498,7 @@ function main() {
|
|
|
489
498
|
process.exit(1);
|
|
490
499
|
}
|
|
491
500
|
targetDir = resolveDir(arg);
|
|
492
|
-
} else if (arg === 'serve' || arg === 'new' || arg === 'kill' || arg === 'list') {
|
|
501
|
+
} else if (arg === 'serve' || arg === 'new' || arg === 'kill' || arg === 'list' || arg === 'resurrect') {
|
|
493
502
|
if (subcommand) {
|
|
494
503
|
console.error(`Error: multiple subcommands specified: '${subcommand}' and '${arg}'`);
|
|
495
504
|
process.exit(1);
|
|
@@ -537,6 +546,12 @@ function main() {
|
|
|
537
546
|
process.exit(1);
|
|
538
547
|
}
|
|
539
548
|
cmdKill(sf);
|
|
549
|
+
} else if (subcommand === 'resurrect') {
|
|
550
|
+
if (gitMode) {
|
|
551
|
+
console.error("Error: '--git' and '--GIT' are only valid with 'serve'.");
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
cmdResurrect(targetDir, longMode, printLogs);
|
|
540
555
|
} else if (subcommand === 'serve' || subcommand === 'new') {
|
|
541
556
|
if (gitMode) {
|
|
542
557
|
const flagLabel = gitMode === 'root' ? '--GIT' : '--git';
|