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.
@@ -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; 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`).
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 and delivered on `/servers switch`.
279
- - 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.
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
- ### Telemetry
316
+ ### State persistence
300
317
 
301
- 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.
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 Show version');
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');
99
- 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');
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 4096');
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
- // Try to extract messages; getSession may return data with
141
- // messages, or the sessions list may already contain them
142
- let rawMessages = [];
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 session = await getSession(serverBase, sorted[0].id);
145
- rawMessages = session.messages || session.history || session.conversation || [];
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
- // 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);
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
- 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.');
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
- 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');
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, execSync } = require('child_process');
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 Create a server in <directory> (default: $PWD) and attach a TUI.');
189
- console.error(' Aliased as "new" for backwards compatibility.');
190
- console.error(' Errors if one already exists there.');
191
- console.error(' Warns if a parent directory already has a server.');
192
- console.error(' --git Use the nearest parent with a .git entry (file or dir).');
193
- console.error(' --GIT Use the nearest parent with a .git directory only.');
194
- console.error(' Refuses to create above an existing .ocmux.json found while searching.');
195
- console.error(' --print-logs Pass --print-logs to opencode serve (prints server logs to stderr).');
196
- console.error(' kill Kill the server found upward from <directory>.');
197
- console.error(' Removes its tmux window and state file.');
198
- console.error(' list List all running servers with their directories and URLs.');
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';