agentp 0.11.8 → 0.11.10
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 +6 -15
- package/bin/ocmux +19 -3
- package/bin/tgagentp +77 -39
- package/lib/ocmux.js +55 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -233,29 +233,20 @@ 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 |
|
|
255
242
|
| `/answer <number>` | Respond to a question asked by the AI (structured multiple-choice) |
|
|
256
243
|
| `/markdown` | Send the original markdown of the last response as a `.md` file; reply to a message to get that specific response |
|
|
244
|
+
| `/agents list` | List available agents/models |
|
|
245
|
+
| `/agents switch <name>` | Switch the active agent for subsequent messages (synced to server via API) |
|
|
257
246
|
| `/shutdown [force\|clear]` | (requires `--dev`) Stop tgagentp; `clear` also wipes saved connections |
|
|
258
247
|
|
|
248
|
+
> **Note on model/agent switching:** Changing the model in the OpenCode TUI's prompt dropdown is a **local UI action** — it only takes effect server-side after a message is sent *through the TUI*. If you change the model in the TUI and then send a prompt via `agentp` or Telegram, the old model will still be used. Use `/agents switch <name>` from Telegram (or `tgagentp`) to change models — this explicitly calls the API and syncs correctly.
|
|
249
|
+
|
|
259
250
|
#### TUI Command Passthrough
|
|
260
251
|
|
|
261
252
|
Messages starting with `//` are forwarded to the OpenCode TUI as raw keystrokes (not text sent to the AI):
|
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
|
|
|
@@ -308,6 +309,8 @@ function cmdNew(targetDir, longMode, printLogs) {
|
|
|
308
309
|
}
|
|
309
310
|
|
|
310
311
|
function doNew(targetDir, longMode, printLogs) {
|
|
312
|
+
// Normalize to real path so tmux window names are consistent regardless of symlinks
|
|
313
|
+
try { targetDir = fs.realpathSync(targetDir); } catch {}
|
|
311
314
|
ensureSession();
|
|
312
315
|
const lf = logfileFor(targetDir);
|
|
313
316
|
|
|
@@ -332,7 +335,7 @@ function doNew(targetDir, longMode, printLogs) {
|
|
|
332
335
|
try { fs.truncateSync(lf); } catch { /* may not exist yet */ }
|
|
333
336
|
|
|
334
337
|
// Send the command to the interactive shell (like typing it manually)
|
|
335
|
-
const serveFlags = printLogs ? '--
|
|
338
|
+
const serveFlags = printLogs ? '--print-logs' : '';
|
|
336
339
|
tmux(['send-keys', '-t', `${SESSION}:${winIdx}.0`,
|
|
337
340
|
`opencode serve ${serveFlags} 2>&1 | tee '${lf}'`, 'Enter']);
|
|
338
341
|
|
|
@@ -600,13 +603,26 @@ function main() {
|
|
|
600
603
|
console.error("Error: '--print-logs' is only valid with 'serve'.");
|
|
601
604
|
process.exit(1);
|
|
602
605
|
}
|
|
603
|
-
// Default mode
|
|
604
|
-
|
|
606
|
+
// Default mode — find an existing server and switch to it
|
|
607
|
+
let sf = findParentStatefile(targetDir);
|
|
605
608
|
if (!sf) {
|
|
606
609
|
console.error(`Error: no opencode server found in ${targetDir} or any of its parents.`);
|
|
607
610
|
console.error("Run 'ocmux serve' to create a new server.");
|
|
608
611
|
process.exit(1);
|
|
609
612
|
}
|
|
613
|
+
|
|
614
|
+
// Skip stale state files whose tmux window no longer exists,
|
|
615
|
+
// and search from the parent directory instead.
|
|
616
|
+
const state = readState(sf);
|
|
617
|
+
if (state && state.window_index != null) {
|
|
618
|
+
const serverDir = path.dirname(sf);
|
|
619
|
+
const actualName = windowNameByIndex(state.window_index);
|
|
620
|
+
if (actualName !== serverDir && windowByDir(serverDir) == null) {
|
|
621
|
+
const parentSf = findParentStatefile(path.dirname(serverDir));
|
|
622
|
+
if (parentSf) sf = parentSf;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
610
626
|
ensureSession();
|
|
611
627
|
cmdSwitch(sf, longMode);
|
|
612
628
|
}
|
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
|
@@ -63,11 +63,16 @@ function tuiPaneId(windowIndex) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function windowByDir(dir) {
|
|
66
|
+
let realDir;
|
|
67
|
+
try { realDir = fs.realpathSync(dir); } catch { realDir = dir; }
|
|
66
68
|
const r = _tmux(['list-windows', '-t', SESSION, '-F', '#{window_index}\t#{window_name}']);
|
|
67
69
|
if (r.status !== 0) return null;
|
|
68
70
|
for (const line of r.stdout.trim().split('\n').filter(Boolean)) {
|
|
69
71
|
const [idx, name] = line.split('\t');
|
|
70
72
|
if (name === dir) return parseInt(idx, 10);
|
|
73
|
+
try {
|
|
74
|
+
if (fs.realpathSync(name) === realDir) return parseInt(idx, 10);
|
|
75
|
+
} catch {}
|
|
71
76
|
}
|
|
72
77
|
return null;
|
|
73
78
|
}
|
|
@@ -130,6 +135,37 @@ function listServers() {
|
|
|
130
135
|
return rows;
|
|
131
136
|
}
|
|
132
137
|
|
|
138
|
+
// Check the state of a server in a given directory.
|
|
139
|
+
// Returns null if no .ocmux.json exists, or an object with status.
|
|
140
|
+
function serverStatus(dir) {
|
|
141
|
+
const sf = statefileFor(dir);
|
|
142
|
+
const state = readState(sf);
|
|
143
|
+
if (!state || !state.url) return null;
|
|
144
|
+
const winIdx = windowByDir(dir);
|
|
145
|
+
const tui = winIdx != null ? tuiPaneId(winIdx) : null;
|
|
146
|
+
return {
|
|
147
|
+
dir,
|
|
148
|
+
url: state.url,
|
|
149
|
+
status: winIdx != null ? 'alive' : 'dead',
|
|
150
|
+
windowIndex: winIdx,
|
|
151
|
+
hasTui: tui != null,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// List dead servers by scanning known directories for orphaned .ocmux.json files.
|
|
156
|
+
// dirs: array of directories to check (e.g. from connections file).
|
|
157
|
+
// Returns entries with status 'dead' that are not already in listServers().
|
|
158
|
+
function listDeadServers(knownDirs) {
|
|
159
|
+
const alive = new Set(listServers().map(s => s.dir));
|
|
160
|
+
const dead = [];
|
|
161
|
+
for (const dir of knownDirs) {
|
|
162
|
+
if (alive.has(dir)) continue;
|
|
163
|
+
const status = serverStatus(dir);
|
|
164
|
+
if (status) dead.push(status);
|
|
165
|
+
}
|
|
166
|
+
return dead;
|
|
167
|
+
}
|
|
168
|
+
|
|
133
169
|
// Switch tmux focus to the given server window and ensure the TUI pane is alive and zoomed.
|
|
134
170
|
function activateServer(dir, index, url) {
|
|
135
171
|
const winIdx = index;
|
|
@@ -182,8 +218,10 @@ function activateServer(dir, index, url) {
|
|
|
182
218
|
|
|
183
219
|
// Recover a dead/crashed server: kill old window, remove state file,
|
|
184
220
|
// create fresh server + TUI in the same directory.
|
|
221
|
+
// Delegates to startServer for the actual server creation.
|
|
185
222
|
// Returns { url, dir } on success, throws on error.
|
|
186
223
|
function resurrectServer(dir, printLogs) {
|
|
224
|
+
try { dir = fs.realpathSync(dir); } catch {}
|
|
187
225
|
const sf = path.join(dir, '.ocmux.json');
|
|
188
226
|
const state = readState(sf);
|
|
189
227
|
|
|
@@ -209,7 +247,16 @@ function resurrectServer(dir, printLogs) {
|
|
|
209
247
|
// Remove old state file so we can create a fresh one
|
|
210
248
|
try { fs.unlinkSync(sf); } catch {}
|
|
211
249
|
|
|
212
|
-
//
|
|
250
|
+
// Start fresh server (same logic as ocmux serve)
|
|
251
|
+
return startServer(dir, printLogs);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Start a new opencode server in the given directory.
|
|
255
|
+
// Creates a tmux window, starts opencode serve, waits for the URL,
|
|
256
|
+
// and writes .ocmux.json. Returns { url, dir } on success.
|
|
257
|
+
function startServer(dir, printLogs) {
|
|
258
|
+
// Normalize to real path so tmux window names are consistent regardless of symlinks
|
|
259
|
+
try { dir = fs.realpathSync(dir); } catch {}
|
|
213
260
|
ensureSession();
|
|
214
261
|
const lf = logfileFor(dir);
|
|
215
262
|
|
|
@@ -223,16 +270,16 @@ function resurrectServer(dir, printLogs) {
|
|
|
223
270
|
|
|
224
271
|
try { fs.truncateSync(lf); } catch {}
|
|
225
272
|
|
|
226
|
-
const serveFlags = printLogs ? '--
|
|
273
|
+
const serveFlags = printLogs ? '--print-logs' : '';
|
|
227
274
|
_tmux(['send-keys', '-t', `${SESSION}:${winIdx}.0`,
|
|
228
|
-
|
|
275
|
+
'opencode serve' + (serveFlags ? ' ' + serveFlags : '') + ` 2>&1 | tee '${lf}'`, 'Enter']);
|
|
229
276
|
|
|
230
277
|
let url = null;
|
|
231
278
|
for (let i = 0; i < 50; i++) {
|
|
232
279
|
sleep(0.2);
|
|
233
280
|
const log = (() => { try { return fs.readFileSync(lf, 'utf8'); } catch { return ''; } })();
|
|
234
281
|
const m = log.match(URL_RE);
|
|
235
|
-
if (m) url = m[1];
|
|
282
|
+
if (m) { url = m[1]; break; }
|
|
236
283
|
}
|
|
237
284
|
|
|
238
285
|
if (!url) {
|
|
@@ -242,7 +289,7 @@ function resurrectServer(dir, printLogs) {
|
|
|
242
289
|
}
|
|
243
290
|
|
|
244
291
|
const newState = { url, logfile: lf, window_index: winIdx };
|
|
245
|
-
fs.writeFileSync(
|
|
292
|
+
fs.writeFileSync(path.join(dir, '.ocmux.json'), JSON.stringify(newState) + '\n');
|
|
246
293
|
|
|
247
294
|
const splitResult = _tmux(['split-window', '-v', '-P', '-F', '#{pane_id}', '-t', `${SESSION}:${winIdx}`, '-c', dir]);
|
|
248
295
|
const tuiPane = splitResult.stdout.trim();
|
|
@@ -278,4 +325,7 @@ module.exports = {
|
|
|
278
325
|
ensureSession,
|
|
279
326
|
pinWindowName,
|
|
280
327
|
resurrectServer,
|
|
328
|
+
startServer,
|
|
329
|
+
serverStatus,
|
|
330
|
+
listDeadServers,
|
|
281
331
|
};
|