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 +2 -15
- package/bin/ocmux +2 -1
- package/bin/tgagentp +77 -39
- package/lib/ocmux.js +47 -5
- package/package.json +1 -1
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
|
|
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
|
-
| `/
|
|
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 ? '--
|
|
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
|
-
|
|
565
|
-
|
|
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 =
|
|
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 ? '
|
|
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
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
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
|
-
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
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
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
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
|
-
`•
|
|
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
|
-
'
|
|
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
|
-
`•
|
|
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 /
|
|
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(`•
|
|
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
|
-
//
|
|
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 ? '--
|
|
265
|
+
const serveFlags = printLogs ? '--print-logs' : '';
|
|
227
266
|
_tmux(['send-keys', '-t', `${SESSION}:${winIdx}.0`,
|
|
228
|
-
|
|
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(
|
|
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
|
};
|