agentp 0.11.8 → 0.11.9

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/README.md CHANGED
@@ -233,22 +233,9 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
233
233
  | Command | Action |
234
234
  |---|---|
235
235
  | `/help [topic]` | Show general help or help for a topic (`servers`, `sessions`, `agents`, `models`, `allow`, `think`, `record`, `queue`) |
236
- | `/servers` | List all running ocmux-served projects with URL + status (✅ idle / ⏳ busy) |
236
+ | `/servers` | List all ocmux-served projects (▶ active, 🔌 disconnected, 💀 dead) |
237
237
  | `/servers switch <name> [--force]` | Switch active server; matches by full path, basename, or substring; `--force` takes over from another chat |
238
- | `/serve <path>` | Start a server in an existing directory under `TGAGENTP_ROOT` |
239
- | `/new <path>` | Create a directory, init git, and start a server under `TGAGENTP_ROOT` |
240
- | `/sessions` | List recent sessions for the current server (max 50, with date headings) |
241
- | `/sessions switch <number\|name>` | Switch active session by position or partial name match |
242
- | `/agents` | List primary agents (▶ marker for the active one) |
243
- | `/agents switch <name>` | Switch active agent — persists on session and refreshes TUI |
244
- | `/models` | List connected providers with model counts, context limits, and costs |
245
- | `/status` | Show current server path, URL, busy status, and active session |
246
- | `/cancel` | Cancel the current AI response for the active server |
247
- | `/think [on\|off\|switch]` | Toggle forwarding of model thinking messages to the chat |
248
- | `/record [stop]` | Toggle recording of Telegram conversation for agentp context; `/record stop` clears and stops |
249
- | `/queue <message>` | Queue a message when the server is busy; auto-sent when current task finishes |
250
- | `/flush` | Clear all queued messages (manual and auto-queued) |
251
- | `/resurrect` | Recover a dead server — restart processes in the same directory, reconnect chat |
238
+ | `/resurrect [path]` | Restart a crashed server from its `.ocmux.json`; accepts optional directory path |
252
239
  | `/allow` | Approve a permission request once |
253
240
  | `/reject` | Deny a permission request |
254
241
  | `/always` | Approve and remember for the session |
package/bin/ocmux CHANGED
@@ -22,6 +22,7 @@ const {
22
22
  logfileFor,
23
23
  URL_RE,
24
24
  resurrectServer,
25
+ startServer,
25
26
  getActiveServer,
26
27
  } = require('../lib/ocmux');
27
28
 
@@ -332,7 +333,7 @@ function doNew(targetDir, longMode, printLogs) {
332
333
  try { fs.truncateSync(lf); } catch { /* may not exist yet */ }
333
334
 
334
335
  // Send the command to the interactive shell (like typing it manually)
335
- const serveFlags = printLogs ? '--port 0 --print-logs' : '--port 0';
336
+ const serveFlags = printLogs ? '--print-logs' : '';
336
337
  tmux(['send-keys', '-t', `${SESSION}:${winIdx}.0`,
337
338
  `opencode serve ${serveFlags} 2>&1 | tee '${lf}'`, 'Enter']);
338
339
 
package/bin/tgagentp CHANGED
@@ -18,7 +18,7 @@ const TGAGENTP_PORT = parseInt(process.env.TGAGENTP_PORT || '0', 10);
18
18
  const PORT_FILE = '/tmp/tgagentp-port';
19
19
  const TGAGENTP_DEBOUNCE_MS = parseInt(process.env.TGAGENTP_DEBOUNCE_MS || '5000', 10);
20
20
  const CONNECTIONS_FILE = '/tmp/tgagentp-connections.json';
21
- const { listServers, activateServer, resurrectServer, statefileFor, readState, tuiPaneId } = require('../lib/ocmux.js');
21
+ const { listServers, activateServer, resurrectServer, statefileFor, readState, tuiPaneId, serverStatus, listDeadServers } = require('../lib/ocmux.js');
22
22
  const { executeTuiCommand } = require('../lib/tui-cmd.js');
23
23
  const { getUpdates, downloadTelegramFile, sendTelegramDocument } = require('../lib/telegram-api.js');
24
24
  const { sendLongMessage } = require('../lib/telegram-format.js');
@@ -559,14 +559,21 @@ async function cmdServers(token, chatId, args, chatState, setServerForChat, setA
559
559
  return;
560
560
  }
561
561
 
562
- // Default: list servers
562
+ // Default: list servers (alive + dead)
563
563
  const servers = listServers();
564
- if (servers.length === 0) {
565
- await sendLongMessage(token, chatState, 'No servers running.');
564
+ // Also find dead servers from saved connections
565
+ let savedData;
566
+ try { savedData = JSON.parse(fs.readFileSync(CONNECTIONS_FILE, 'utf8')); } catch {}
567
+ const knownDirs = savedData && Array.isArray(savedData.connections)
568
+ ? savedData.connections.map(c => c.dir).filter(Boolean) : [];
569
+ const dead = listDeadServers(knownDirs);
570
+ const all = [...servers, ...dead.map(d => ({ url: d.url, dir: d.dir, index: d.windowIndex, status: 'dead' }))];
571
+ if (all.length === 0) {
572
+ await sendLongMessage(token, chatState, 'No servers found.');
566
573
  return;
567
574
  }
568
575
  const activeUrl = chatState.serverBase ? normalizeUrl(chatState.serverBase) : null;
569
- const lines = servers.map(s => {
576
+ const lines = all.map(s => {
570
577
  const normalizedUrl = normalizeUrl(s.url);
571
578
  const isActive = activeUrl && normalizedUrl === activeUrl;
572
579
  const isOwned = Object.keys(serverOwners).some(url => normalizeUrl(url) === normalizedUrl);
@@ -575,12 +582,14 @@ async function cmdServers(token, chatId, args, chatState, setServerForChat, setA
575
582
  bullet = '▶ ';
576
583
  } else if (isOwned) {
577
584
  bullet = ' · ';
585
+ } else if (s.status === 'dead') {
586
+ bullet = '💀 ';
578
587
  } else {
579
588
  bullet = '🔌 ';
580
589
  }
581
590
  return bullet + `${s.url} → ${s.dir} (${s.status})`;
582
591
  });
583
- const prefix = chatState.serverBase ? 'Active servers:' : 'Available servers:';
592
+ const prefix = chatState.serverBase ? 'All servers:' : 'Available servers:';
584
593
  await sendLongMessage(token, chatState, prefix + '\n' + lines.join('\n'));
585
594
  }
586
595
 
@@ -1363,31 +1372,53 @@ async function cmdComment(token, chatId, args, chatState) {
1363
1372
  }
1364
1373
 
1365
1374
  async function cmdResurrect(token, chatId, args, chatState) {
1366
- const url = chatState.serverBase;
1367
- if (!url) {
1368
- await sendLongMessage(token, chatState, '🔌 Not connected to any server. Nothing to resurrect.');
1369
- return;
1370
- }
1371
-
1372
- let dir = serverDir(url);
1373
- if (!dir) {
1374
- // Fallback: look up directory from saved connections
1375
- try {
1376
- const savedData = JSON.parse(fs.readFileSync(CONNECTIONS_FILE, 'utf8'));
1377
- if (savedData && Array.isArray(savedData.connections)) {
1378
- const match = savedData.connections.find(c =>
1379
- String(c.chatId) === String(chatId) && (c.threadId || null) === (chatState.threadId || null)
1380
- );
1381
- if (match && match.dir) dir = match.dir;
1375
+ let dir = null;
1376
+ let url = null;
1377
+ const dirArg = args && args.length > 0 ? args.join(' ') : null;
1378
+ if (dirArg) {
1379
+ // User specified a directory: resolve it
1380
+ if (TGAGENTP_ROOT) {
1381
+ const resolved = path.resolve(TGAGENTP_ROOT, dirArg);
1382
+ if (resolved.startsWith(TGAGENTP_ROOT) && fs.existsSync(resolved)) {
1383
+ dir = resolved;
1382
1384
  }
1383
- } catch {}
1384
- }
1385
- if (!dir) {
1386
- await sendLongMessage(token, chatState,
1387
- 'Could not find the directory for this server.\n' +
1388
- 'The tmux window may have been removed.\n' +
1389
- 'Use `ocmux resurrect` from the terminal to recover it.');
1390
- return;
1385
+ }
1386
+ if (!dir) {
1387
+ // Resolve against home or absolute path
1388
+ const resolved = path.resolve(dirArg);
1389
+ if (fs.existsSync(resolved)) dir = resolved;
1390
+ }
1391
+ if (!dir) {
1392
+ await sendLongMessage(token, chatState, `❌ Directory not found: \`${dirArg}\``);
1393
+ return;
1394
+ }
1395
+ // Read the old URL from the state file
1396
+ const state = readState(statefileFor(dir));
1397
+ url = state ? state.url : null;
1398
+ } else {
1399
+ url = chatState.serverBase;
1400
+ if (!url) {
1401
+ await sendLongMessage(token, chatState, '🔌 Not connected to any server. Specify a directory or switch to a server first.');
1402
+ return;
1403
+ }
1404
+ dir = serverDir(url);
1405
+ if (!dir) {
1406
+ try {
1407
+ const savedData = JSON.parse(fs.readFileSync(CONNECTIONS_FILE, 'utf8'));
1408
+ if (savedData && Array.isArray(savedData.connections)) {
1409
+ const match = savedData.connections.find(c =>
1410
+ String(c.chatId) === String(chatId) && (c.threadId || null) === (chatState.threadId || null)
1411
+ );
1412
+ if (match && match.dir) dir = match.dir;
1413
+ }
1414
+ } catch {}
1415
+ }
1416
+ if (!dir) {
1417
+ await sendLongMessage(token, chatState,
1418
+ 'Could not find the directory for this server.\n' +
1419
+ 'Use `ocmux resurrect` from the terminal or specify a directory: `/resurrect <path>`.');
1420
+ return;
1421
+ }
1391
1422
  }
1392
1423
 
1393
1424
  await sendLongMessage(token, chatState,
@@ -1486,9 +1517,16 @@ async function cmdServe(token, chatId, args, chatState, setServerForChat, setAct
1486
1517
  }
1487
1518
  const sf = statefileFor(targetDir);
1488
1519
  if (fs.existsSync(sf)) {
1489
- await sendLongMessage(token, chatState,
1490
- `⚠️ Server already exists for \`${relPath}\`.\n` +
1491
- 'Use `/servers switch <name>` to connect to it.');
1520
+ const status = serverStatus(targetDir);
1521
+ if (status && status.status === 'dead') {
1522
+ await sendLongMessage(token, chatState,
1523
+ `💀 A dead server exists at \`${relPath}\` (${status.url}).\n` +
1524
+ `Use \`/resurrect ${relPath}\` to restart it.`);
1525
+ } else {
1526
+ await sendLongMessage(token, chatState,
1527
+ `⚠️ Server already exists for \`${relPath}\`.\n` +
1528
+ 'Use `/servers switch <name>` to connect to it.');
1529
+ }
1492
1530
  return;
1493
1531
  }
1494
1532
  await sendLongMessage(token, chatState,
@@ -2020,7 +2058,7 @@ async function main() {
2020
2058
  const msg = isConnError
2021
2059
  ? `⚠️ Server \`${url}\` is unreachable. The message has been requeued and will be sent when the server is available again.\n\n` +
2022
2060
  `**Options:**\n` +
2023
- `• Start the server: \`ocmux serve\`\n` +
2061
+ `• Restart it: \`/resurrect\`\n` +
2024
2062
  `• Switch to another server: \`/servers switch <name>\`\n` +
2025
2063
  `• Flush the queue: \`/flush\``
2026
2064
  : `Error: ${err.message}`;
@@ -2251,7 +2289,7 @@ async function main() {
2251
2289
  await sendLongMessage(token, cs,
2252
2290
  '🔄 Bot started. Previously connected to `' + dir + '`.\n' +
2253
2291
  '⚠️ The server at `' + state.url + '` is not reachable.\n' +
2254
- 'Options: start the server with `ocmux serve`, or switch with `/servers switch <name>`.');
2292
+ 'Use `/resurrect` to restart it, or `/servers switch <name>` to connect elsewhere.');
2255
2293
  } catch (_) {}
2256
2294
  }
2257
2295
  }
@@ -2387,7 +2425,7 @@ async function main() {
2387
2425
  `⚠️ Server \`${cs.serverBase}\` is not reachable.\n\n` +
2388
2426
  `Your file upload has been queued (#${qlen}). It will be sent when the server becomes available.\n\n` +
2389
2427
  `**Options:**\n` +
2390
- `• Start the server: \`ocmux serve\`\n` +
2428
+ `• Restart it: \`/resurrect\`\n` +
2391
2429
  `• Switch to another server: \`/servers switch <name>\`\n` +
2392
2430
  `• Flush the queue: \`/flush\``);
2393
2431
  continue;
@@ -2538,8 +2576,8 @@ async function main() {
2538
2576
  const cmdArgs = parts.slice(1);
2539
2577
  const handler = cmds[cmd];
2540
2578
  if (handler) {
2541
- // Disconnected guard: only /help, /servers, /force-switch, /start, /comment, /shutdown, /serve, and /new work without a server
2542
- if (!cs.serverBase && cmd !== 'help' && cmd !== 'servers' && cmd !== 'force-switch' && cmd !== 'start' && cmd !== 'comment' && cmd !== 'shutdown' && cmd !== 'serve' && cmd !== 'new') {
2579
+ // Disconnected guard: only /help, /servers, /force-switch, /start, /comment, /shutdown, /serve, /new, and /resurrect work without a server
2580
+ if (!cs.serverBase && cmd !== 'help' && cmd !== 'servers' && cmd !== 'force-switch' && cmd !== 'start' && cmd !== 'comment' && cmd !== 'shutdown' && cmd !== 'serve' && cmd !== 'new' && cmd !== 'resurrect') {
2543
2581
  await sendLongMessage(token, cs,
2544
2582
  '🔌 Not connected to any server.\n\n' +
2545
2583
  'Use `/servers` to see available servers and `/servers switch <name>` to connect.');
@@ -2588,7 +2626,7 @@ async function main() {
2588
2626
  const qlen = st.messageQueue.length;
2589
2627
  const servers = listServers();
2590
2628
  const suggestions = [];
2591
- suggestions.push(`• Start the server: \`ocmux serve\``);
2629
+ suggestions.push(`• Restart it: \`/resurrect\``);
2592
2630
  if (servers.length > 0) {
2593
2631
  suggestions.push(`• Switch to another server: \`/servers switch <name>\``);
2594
2632
  }
package/lib/ocmux.js CHANGED
@@ -130,6 +130,37 @@ function listServers() {
130
130
  return rows;
131
131
  }
132
132
 
133
+ // Check the state of a server in a given directory.
134
+ // Returns null if no .ocmux.json exists, or an object with status.
135
+ function serverStatus(dir) {
136
+ const sf = statefileFor(dir);
137
+ const state = readState(sf);
138
+ if (!state || !state.url) return null;
139
+ const winIdx = windowByDir(dir);
140
+ const tui = winIdx != null ? tuiPaneId(winIdx) : null;
141
+ return {
142
+ dir,
143
+ url: state.url,
144
+ status: winIdx != null ? 'alive' : 'dead',
145
+ windowIndex: winIdx,
146
+ hasTui: tui != null,
147
+ };
148
+ }
149
+
150
+ // List dead servers by scanning known directories for orphaned .ocmux.json files.
151
+ // dirs: array of directories to check (e.g. from connections file).
152
+ // Returns entries with status 'dead' that are not already in listServers().
153
+ function listDeadServers(knownDirs) {
154
+ const alive = new Set(listServers().map(s => s.dir));
155
+ const dead = [];
156
+ for (const dir of knownDirs) {
157
+ if (alive.has(dir)) continue;
158
+ const status = serverStatus(dir);
159
+ if (status) dead.push(status);
160
+ }
161
+ return dead;
162
+ }
163
+
133
164
  // Switch tmux focus to the given server window and ensure the TUI pane is alive and zoomed.
134
165
  function activateServer(dir, index, url) {
135
166
  const winIdx = index;
@@ -182,6 +213,7 @@ function activateServer(dir, index, url) {
182
213
 
183
214
  // Recover a dead/crashed server: kill old window, remove state file,
184
215
  // create fresh server + TUI in the same directory.
216
+ // Delegates to startServer for the actual server creation.
185
217
  // Returns { url, dir } on success, throws on error.
186
218
  function resurrectServer(dir, printLogs) {
187
219
  const sf = path.join(dir, '.ocmux.json');
@@ -209,7 +241,14 @@ function resurrectServer(dir, printLogs) {
209
241
  // Remove old state file so we can create a fresh one
210
242
  try { fs.unlinkSync(sf); } catch {}
211
243
 
212
- // Create fresh server + TUI
244
+ // Start fresh server (same logic as ocmux serve)
245
+ return startServer(dir, printLogs);
246
+ }
247
+
248
+ // Start a new opencode server in the given directory.
249
+ // Creates a tmux window, starts opencode serve, waits for the URL,
250
+ // and writes .ocmux.json. Returns { url, dir } on success.
251
+ function startServer(dir, printLogs) {
213
252
  ensureSession();
214
253
  const lf = logfileFor(dir);
215
254
 
@@ -223,16 +262,16 @@ function resurrectServer(dir, printLogs) {
223
262
 
224
263
  try { fs.truncateSync(lf); } catch {}
225
264
 
226
- const serveFlags = printLogs ? '--port 0 --print-logs' : '--port 0';
265
+ const serveFlags = printLogs ? '--print-logs' : '';
227
266
  _tmux(['send-keys', '-t', `${SESSION}:${winIdx}.0`,
228
- `opencode serve ${serveFlags} 2>&1 | tee '${lf}'`, 'Enter']);
267
+ 'opencode serve' + (serveFlags ? ' ' + serveFlags : '') + ` 2>&1 | tee '${lf}'`, 'Enter']);
229
268
 
230
269
  let url = null;
231
270
  for (let i = 0; i < 50; i++) {
232
271
  sleep(0.2);
233
272
  const log = (() => { try { return fs.readFileSync(lf, 'utf8'); } catch { return ''; } })();
234
273
  const m = log.match(URL_RE);
235
- if (m) url = m[1];
274
+ if (m) { url = m[1]; break; }
236
275
  }
237
276
 
238
277
  if (!url) {
@@ -242,7 +281,7 @@ function resurrectServer(dir, printLogs) {
242
281
  }
243
282
 
244
283
  const newState = { url, logfile: lf, window_index: winIdx };
245
- fs.writeFileSync(sf, JSON.stringify(newState) + '\n');
284
+ fs.writeFileSync(path.join(dir, '.ocmux.json'), JSON.stringify(newState) + '\n');
246
285
 
247
286
  const splitResult = _tmux(['split-window', '-v', '-P', '-F', '#{pane_id}', '-t', `${SESSION}:${winIdx}`, '-c', dir]);
248
287
  const tuiPane = splitResult.stdout.trim();
@@ -278,4 +317,7 @@ module.exports = {
278
317
  ensureSession,
279
318
  pinWindowName,
280
319
  resurrectServer,
320
+ startServer,
321
+ serverStatus,
322
+ listDeadServers,
281
323
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentp",
3
- "version": "0.11.8",
3
+ "version": "0.11.9",
4
4
  "description": "Pipe prompts into a running OpenCode TUI session and stream the final answer",
5
5
  "bin": {
6
6
  "agentp": "bin/agentp",