agentp 2.0.1 → 2.1.0
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 +7 -5
- package/README.md +97 -23
- package/bin/agentp +3 -6
- package/bin/ocmux +564 -244
- package/bin/tgagentp +121 -130
- package/docs/specification.md +2 -1
- package/docs/specification_v2.md +188 -624
- package/lib/ocmux.js +26 -289
- package/lib/opencode.js +186 -43
- package/lib/tui-cmd.js +12 -5
- package/lib/tui-registry.js +313 -0
- package/package.json +1 -1
package/bin/tgagentp
CHANGED
|
@@ -18,12 +18,56 @@ 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 {
|
|
21
|
+
const { switchTui } = require('../lib/ocmux.js');
|
|
22
|
+
const projectState = require('../lib/project-state.js');
|
|
23
|
+
const tuiRegistry = require('../lib/tui-registry.js');
|
|
22
24
|
const { executeTuiCommand } = require('../lib/tui-cmd.js');
|
|
23
25
|
const { getUpdates, downloadTelegramFile, sendTelegramDocument } = require('../lib/telegram-api.js');
|
|
24
26
|
const { sendLongMessage } = require('../lib/telegram-format.js');
|
|
25
27
|
const { spawnSync } = require('child_process');
|
|
26
28
|
|
|
29
|
+
function readProject(dir) {
|
|
30
|
+
return projectState.readProjectState(projectState.statefileFor(dir));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Telegram's historical "servers" are project routes. With user-placed TUIs,
|
|
34
|
+
// the runtime registry is the only global list available without scanning the
|
|
35
|
+
// filesystem. Saved chat connections supplement it where a project is headless.
|
|
36
|
+
function listServers() {
|
|
37
|
+
const rows = [];
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
for (const instance of tuiRegistry.list()) {
|
|
40
|
+
if (!instance.directory || !instance.server) continue;
|
|
41
|
+
const key = `${instance.directory}\0${instance.server}`;
|
|
42
|
+
if (seen.has(key)) continue;
|
|
43
|
+
seen.add(key);
|
|
44
|
+
rows.push({
|
|
45
|
+
url: instance.server,
|
|
46
|
+
dir: instance.directory,
|
|
47
|
+
status: instance.shared ? 'shared' : 'project',
|
|
48
|
+
instance,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const saved = JSON.parse(fs.readFileSync(CONNECTIONS_FILE, 'utf8'));
|
|
53
|
+
for (const connection of saved.connections || []) {
|
|
54
|
+
const state = connection.dir ? readProject(connection.dir) : null;
|
|
55
|
+
if (!state || !state.server) continue;
|
|
56
|
+
const key = `${state.directory}\0${state.server}`;
|
|
57
|
+
if (seen.has(key)) continue;
|
|
58
|
+
seen.add(key);
|
|
59
|
+
rows.push({ url: state.server, dir: state.directory, status: 'headless', instance: null });
|
|
60
|
+
}
|
|
61
|
+
} catch {}
|
|
62
|
+
return rows;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function activateServer(dir, _unused, server) {
|
|
66
|
+
const state = readProject(dir);
|
|
67
|
+
if (!state || !state.session) return null;
|
|
68
|
+
return switchTui(dir, server || state.server, state.session);
|
|
69
|
+
}
|
|
70
|
+
|
|
27
71
|
const TGAGENTP_ROOT = process.env.TGAGENTP_ROOT;
|
|
28
72
|
let tgagentpRootValid = false;
|
|
29
73
|
if (TGAGENTP_ROOT) {
|
|
@@ -461,7 +505,7 @@ async function cmdHelp(token, chatId, args, chatState) {
|
|
|
461
505
|
const hint = TGAGENTP_ROOT ? '' :
|
|
462
506
|
'\nℹ️ Currently disabled. Set the `TGAGENTP_ROOT` environment variable to a writable directory to enable `/serve` and `/new`.';
|
|
463
507
|
await sendLongMessage(token, chatState,
|
|
464
|
-
'`/serve <path>` ─
|
|
508
|
+
'`/serve <path>` ─ initialize an existing project directory\n\n' +
|
|
465
509
|
'The path is relative to `TGAGENTP_ROOT`.\n' +
|
|
466
510
|
'Example: `/serve projects/my-app`\n\n' +
|
|
467
511
|
'Requires the `TGAGENTP_ROOT` environment variable to be set to a writable directory.' +
|
|
@@ -472,7 +516,7 @@ async function cmdHelp(token, chatId, args, chatState) {
|
|
|
472
516
|
const hint = TGAGENTP_ROOT ? '' :
|
|
473
517
|
'\nℹ️ Currently disabled. Set the `TGAGENTP_ROOT` environment variable to a writable directory to enable `/serve` and `/new`.';
|
|
474
518
|
await sendLongMessage(token, chatState,
|
|
475
|
-
'`/new <path>` ─ create a directory, initialize git, and
|
|
519
|
+
'`/new <path>` ─ create a directory, initialize git, and configure it\n\n' +
|
|
476
520
|
'The path is relative to `TGAGENTP_ROOT`.\n' +
|
|
477
521
|
'Example: `/new projects/my-app`\n\n' +
|
|
478
522
|
'Requires the `TGAGENTP_ROOT` environment variable to be set to a writable directory.' +
|
|
@@ -517,8 +561,8 @@ async function cmdHelp(token, chatId, args, chatState) {
|
|
|
517
561
|
'`/markdown` ─ send the original markdown of the last response as a `.md` file\n' +
|
|
518
562
|
'`/shutdown` ─ stop tgagentp (only with `--dev` flag)\n' +
|
|
519
563
|
(tgagentpRootValid
|
|
520
|
-
? '`/serve <path>` ─
|
|
521
|
-
'`/new <path>` ─ create a directory, init git, and
|
|
564
|
+
? '`/serve <path>` ─ initialize an existing project directory\n' +
|
|
565
|
+
'`/new <path>` ─ create a directory, init git, and configure it\n\n'
|
|
522
566
|
: 'ℹ️ Set `TGAGENTP_ROOT` to a writable directory to enable `/serve` and `/new`.\n\n') +
|
|
523
567
|
'Everything else is forwarded to the active server as `[Telegram] <text>`.');
|
|
524
568
|
}
|
|
@@ -537,21 +581,15 @@ async function cmdServers(token, chatId, args, chatState, setServerForChat, setA
|
|
|
537
581
|
}
|
|
538
582
|
}
|
|
539
583
|
|
|
540
|
-
// Default: list
|
|
584
|
+
// Default: list configured project routes known to the runtime registry or
|
|
585
|
+
// saved Telegram connections.
|
|
541
586
|
const servers = listServers();
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
try { savedData = JSON.parse(fs.readFileSync(CONNECTIONS_FILE, 'utf8')); } catch {}
|
|
545
|
-
const knownDirs = savedData && Array.isArray(savedData.connections)
|
|
546
|
-
? savedData.connections.map(c => c.dir).filter(Boolean) : [];
|
|
547
|
-
const dead = listDeadServers(knownDirs);
|
|
548
|
-
const all = [...servers, ...dead.map(d => ({ url: d.url, dir: d.dir, index: d.windowIndex, status: 'dead' }))];
|
|
549
|
-
if (all.length === 0) {
|
|
550
|
-
await sendLongMessage(token, chatState, 'No servers found.');
|
|
587
|
+
if (servers.length === 0) {
|
|
588
|
+
await sendLongMessage(token, chatState, 'No configured project routes found.');
|
|
551
589
|
return;
|
|
552
590
|
}
|
|
553
591
|
const activeUrl = chatState.serverBase ? normalizeUrl(chatState.serverBase) : null;
|
|
554
|
-
const lines =
|
|
592
|
+
const lines = servers.map(s => {
|
|
555
593
|
const normalizedUrl = normalizeUrl(s.url);
|
|
556
594
|
const isActive = activeUrl && normalizedUrl === activeUrl;
|
|
557
595
|
const isOwned = Object.keys(serverOwners).some(url => normalizeUrl(url) === normalizedUrl);
|
|
@@ -560,8 +598,8 @@ async function cmdServers(token, chatId, args, chatState, setServerForChat, setA
|
|
|
560
598
|
bullet = '▶ ';
|
|
561
599
|
} else if (isOwned) {
|
|
562
600
|
bullet = ' · ';
|
|
563
|
-
} else if (s.status === '
|
|
564
|
-
bullet = '
|
|
601
|
+
} else if (s.status === 'headless') {
|
|
602
|
+
bullet = '○ ';
|
|
565
603
|
} else {
|
|
566
604
|
bullet = '🔌 ';
|
|
567
605
|
}
|
|
@@ -1359,8 +1397,8 @@ async function cmdResurrect(token, chatId, args, chatState) {
|
|
|
1359
1397
|
return;
|
|
1360
1398
|
}
|
|
1361
1399
|
// Read the old URL from the state file
|
|
1362
|
-
const state =
|
|
1363
|
-
url = state ? state.
|
|
1400
|
+
const state = readProject(dir);
|
|
1401
|
+
url = state ? state.server : null;
|
|
1364
1402
|
} else {
|
|
1365
1403
|
url = chatState.serverBase;
|
|
1366
1404
|
if (!url) {
|
|
@@ -1382,48 +1420,19 @@ async function cmdResurrect(token, chatId, args, chatState) {
|
|
|
1382
1420
|
if (!dir) {
|
|
1383
1421
|
await sendLongMessage(token, chatState,
|
|
1384
1422
|
'Could not find the directory for this server.\n' +
|
|
1385
|
-
'
|
|
1423
|
+
'Specify a configured directory: `/resurrect <path>`.');
|
|
1386
1424
|
return;
|
|
1387
1425
|
}
|
|
1388
1426
|
}
|
|
1389
1427
|
|
|
1390
|
-
await
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
try {
|
|
1395
|
-
const { url: newUrl } = resurrectServer(dir, false);
|
|
1396
|
-
|
|
1397
|
-
// Update chat state to new URL
|
|
1398
|
-
chatState.serverBase = newUrl;
|
|
1399
|
-
|
|
1400
|
-
// Transfer relevant state from old URL to new URL
|
|
1401
|
-
const oldSt = getState(url);
|
|
1402
|
-
const newSt = getState(newUrl);
|
|
1403
|
-
newSt.activeSessionId = oldSt.activeSessionId;
|
|
1404
|
-
newSt.activeSessionName = oldSt.activeSessionName;
|
|
1405
|
-
newSt.activeAgentName = oldSt.activeAgentName;
|
|
1406
|
-
newSt.thinkingEnabled = oldSt.thinkingEnabled;
|
|
1407
|
-
|
|
1408
|
-
// Update serverOwners to point to the new URL
|
|
1409
|
-
const normOld = normalizeUrl(url);
|
|
1410
|
-
for (const [sfUrl, owner] of Object.entries(serverOwners)) {
|
|
1411
|
-
if (normalizeUrl(sfUrl) === normOld) {
|
|
1412
|
-
serverOwners[newUrl] = owner;
|
|
1413
|
-
delete serverOwners[sfUrl];
|
|
1414
|
-
break;
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
await sendLongMessage(token, chatState,
|
|
1419
|
-
`✅ Server resurrected.\n` +
|
|
1420
|
-
`New URL: \`${newUrl}\`\n\n` +
|
|
1421
|
-
`Use \`/status\` to check the session.`);
|
|
1422
|
-
} catch (err) {
|
|
1423
|
-
await sendLongMessage(token, chatState,
|
|
1424
|
-
`❌ Resurrection failed: ${err.message}\n` +
|
|
1425
|
-
`Try \`ocmux resurrect\` from the terminal.`);
|
|
1428
|
+
const alive = url ? await isServerAlive(url) : false;
|
|
1429
|
+
if (alive) {
|
|
1430
|
+
await sendLongMessage(token, chatState, `✅ Server is already reachable at \`${url}\`.`);
|
|
1431
|
+
return;
|
|
1426
1432
|
}
|
|
1433
|
+
await sendLongMessage(token, chatState,
|
|
1434
|
+
`❌ Server at \`${url || 'unknown URL'}\` is not reachable.\n` +
|
|
1435
|
+
'OpenCode servers are user-managed; start `opencode serve` externally, then use `/server <name>` again.');
|
|
1427
1436
|
}
|
|
1428
1437
|
|
|
1429
1438
|
async function cmdDisconnect(token, chatId, args, chatState) {
|
|
@@ -1481,22 +1490,15 @@ async function cmdServe(token, chatId, args, chatState, setServerForChat, setAct
|
|
|
1481
1490
|
`❌ Directory does not exist: \`${relPath}\``);
|
|
1482
1491
|
return;
|
|
1483
1492
|
}
|
|
1484
|
-
const sf = statefileFor(targetDir);
|
|
1493
|
+
const sf = projectState.statefileFor(targetDir);
|
|
1485
1494
|
if (fs.existsSync(sf)) {
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
`💀 A dead server exists at \`${relPath}\` (${status.url}).\n` +
|
|
1490
|
-
`Use \`/resurrect ${relPath}\` to restart it.`);
|
|
1491
|
-
} else {
|
|
1492
|
-
await sendLongMessage(token, chatState,
|
|
1493
|
-
`⚠️ Server already exists for \`${relPath}\`.\n` +
|
|
1494
|
-
'Use `/server <name>` to connect to it.');
|
|
1495
|
-
}
|
|
1495
|
+
await sendLongMessage(token, chatState,
|
|
1496
|
+
`⚠️ Project state already exists for \`${relPath}\`.\n` +
|
|
1497
|
+
'Use `/server <name>` to connect to it.');
|
|
1496
1498
|
return;
|
|
1497
1499
|
}
|
|
1498
1500
|
await sendLongMessage(token, chatState,
|
|
1499
|
-
`🚀
|
|
1501
|
+
`🚀 Initializing project \`${relPath}\`...`);
|
|
1500
1502
|
const result = spawnSync('ocmux', ['serve', targetDir], { encoding: 'utf8', timeout: 15000 });
|
|
1501
1503
|
if (result.status !== 0) {
|
|
1502
1504
|
const err = result.stderr || result.stdout || 'unknown error';
|
|
@@ -1504,15 +1506,16 @@ async function cmdServe(token, chatId, args, chatState, setServerForChat, setAct
|
|
|
1504
1506
|
`❌ Failed to start server:\n\`\`\`\n${err}\n\`\`\``);
|
|
1505
1507
|
return;
|
|
1506
1508
|
}
|
|
1507
|
-
const
|
|
1509
|
+
const state = readProject(targetDir);
|
|
1510
|
+
const url = state && state.server;
|
|
1508
1511
|
if (!url) {
|
|
1509
1512
|
await sendLongMessage(token, chatState,
|
|
1510
|
-
'❌
|
|
1513
|
+
'❌ Project initialized but its server URL was not recorded.');
|
|
1511
1514
|
return;
|
|
1512
1515
|
}
|
|
1513
|
-
await setServerForChat(chatState, url);
|
|
1516
|
+
await setServerForChat(chatState, url, targetDir);
|
|
1514
1517
|
await sendLongMessage(token, chatState,
|
|
1515
|
-
`✅
|
|
1518
|
+
`✅ Project initialized.\nURL: \`${url}\`\nDir: \`${targetDir}\``);
|
|
1516
1519
|
log.info(`[chat ${chatId}] Served ${targetDir} → ${url}`);
|
|
1517
1520
|
}
|
|
1518
1521
|
|
|
@@ -1528,7 +1531,7 @@ async function cmdNew(token, chatId, args, chatState, setServerForChat, setActiv
|
|
|
1528
1531
|
if (!relPath) {
|
|
1529
1532
|
await sendLongMessage(token, chatState,
|
|
1530
1533
|
'Usage: `/new <path>`\n\n' +
|
|
1531
|
-
'Create a new directory under `TGAGENTP_ROOT`, initialize git, and
|
|
1534
|
+
'Create a new directory under `TGAGENTP_ROOT`, initialize git, and configure it.\n' +
|
|
1532
1535
|
'The path is relative to the root directory.');
|
|
1533
1536
|
return;
|
|
1534
1537
|
}
|
|
@@ -1542,7 +1545,7 @@ async function cmdNew(token, chatId, args, chatState, setServerForChat, setActiv
|
|
|
1542
1545
|
if (fs.existsSync(targetDir)) {
|
|
1543
1546
|
await sendLongMessage(token, chatState,
|
|
1544
1547
|
`❌ Directory already exists: \`${relPath}\`.\n` +
|
|
1545
|
-
'Use `/serve` to
|
|
1548
|
+
'Use `/serve` to initialize an existing directory.');
|
|
1546
1549
|
return;
|
|
1547
1550
|
}
|
|
1548
1551
|
try {
|
|
@@ -1560,7 +1563,7 @@ async function cmdNew(token, chatId, args, chatState, setServerForChat, setActiv
|
|
|
1560
1563
|
return;
|
|
1561
1564
|
}
|
|
1562
1565
|
await sendLongMessage(token, chatState,
|
|
1563
|
-
`🚀
|
|
1566
|
+
`🚀 Creating project \`${relPath}\`...`);
|
|
1564
1567
|
const result = spawnSync('ocmux', ['serve', targetDir], { encoding: 'utf8', timeout: 15000 });
|
|
1565
1568
|
if (result.status !== 0) {
|
|
1566
1569
|
const err = result.stderr || result.stdout || 'unknown error';
|
|
@@ -1568,15 +1571,16 @@ async function cmdNew(token, chatId, args, chatState, setServerForChat, setActiv
|
|
|
1568
1571
|
`❌ Failed to start server:\n\`\`\`\n${err}\n\`\`\``);
|
|
1569
1572
|
return;
|
|
1570
1573
|
}
|
|
1571
|
-
const
|
|
1574
|
+
const state = readProject(targetDir);
|
|
1575
|
+
const url = state && state.server;
|
|
1572
1576
|
if (!url) {
|
|
1573
1577
|
await sendLongMessage(token, chatState,
|
|
1574
|
-
'❌
|
|
1578
|
+
'❌ Project initialized but its server URL was not recorded.');
|
|
1575
1579
|
return;
|
|
1576
1580
|
}
|
|
1577
|
-
await setServerForChat(chatState, url);
|
|
1581
|
+
await setServerForChat(chatState, url, targetDir);
|
|
1578
1582
|
await sendLongMessage(token, chatState,
|
|
1579
|
-
`✅ Directory created and
|
|
1583
|
+
`✅ Directory created and project initialized.\nURL: \`${url}\`\nDir: \`${targetDir}\``);
|
|
1580
1584
|
log.info(`[chat ${chatId}] Created ${targetDir} → ${url}`);
|
|
1581
1585
|
}
|
|
1582
1586
|
|
|
@@ -1752,8 +1756,7 @@ async function cmdServer(token, chatId, args, chatState, setServerForChat, setAc
|
|
|
1752
1756
|
`⚠️ Server \`${target.url}\` has been taken over by another chat.\n\n` +
|
|
1753
1757
|
`Use \`/servers\` to see available servers and reconnect.`).catch(() => {});
|
|
1754
1758
|
}
|
|
1755
|
-
const pending = await setServerForChat(chatState, target.url);
|
|
1756
|
-
activateServer(target.dir, target.index, target.url);
|
|
1759
|
+
const pending = await setServerForChat(chatState, target.url, target.dir);
|
|
1757
1760
|
let msg = `Switched to server: ${target.url} → ${target.dir}`;
|
|
1758
1761
|
if (pending && pending.trim()) {
|
|
1759
1762
|
await sendLongMessage(token, chatState, msg);
|
|
@@ -2081,7 +2084,7 @@ async function main() {
|
|
|
2081
2084
|
log.info('Waiting for Telegram messages... (Ctrl+C to stop)');
|
|
2082
2085
|
|
|
2083
2086
|
// Chat-aware server/session mutation callbacks (defined here for token access)
|
|
2084
|
-
const setServerForChat = async (chatState, url) => {
|
|
2087
|
+
const setServerForChat = async (chatState, url, projectDir) => {
|
|
2085
2088
|
const cs = chatState;
|
|
2086
2089
|
|
|
2087
2090
|
// Disconnect any other chat still connected to this server (one server → one chat)
|
|
@@ -2111,10 +2114,12 @@ async function main() {
|
|
|
2111
2114
|
// Send Telegram bridge awareness note to the new session
|
|
2112
2115
|
sendGreeting(cs).catch(() => {});
|
|
2113
2116
|
|
|
2114
|
-
//
|
|
2117
|
+
// Refresh the registered TUI route for this project.
|
|
2115
2118
|
const servers = listServers();
|
|
2116
2119
|
const normalized = normalizeUrl(url);
|
|
2117
|
-
const match =
|
|
2120
|
+
const match = projectDir
|
|
2121
|
+
? { dir: projectDir, url }
|
|
2122
|
+
: servers.find(s => normalizeUrl(s.url) === normalized);
|
|
2118
2123
|
if (match) {
|
|
2119
2124
|
activateServer(match.dir, match.index, match.url);
|
|
2120
2125
|
saveConnection(cs.chatId, cs.threadId, match.dir);
|
|
@@ -2153,6 +2158,15 @@ async function main() {
|
|
|
2153
2158
|
const prevId = getState(server).activeSessionId;
|
|
2154
2159
|
getState(server).activeSessionId = id;
|
|
2155
2160
|
if (name) getState(server).activeSessionName = name;
|
|
2161
|
+
const dir = serverDir(server);
|
|
2162
|
+
if (dir) {
|
|
2163
|
+
const state = readProject(dir);
|
|
2164
|
+
if (state) projectState.writeProjectState(state.file, { session: id });
|
|
2165
|
+
const result = switchTui(dir, server, id);
|
|
2166
|
+
if (!result.ok && result.reason === 'failed') {
|
|
2167
|
+
log.error(`Warning: session selected, but its ${result.kind} TUI could not be refreshed: ${result.error}`);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2156
2170
|
log.info(`[chat ${chatState.chatId}] Switched to session: ${name || id}`);
|
|
2157
2171
|
if (id !== prevId) {
|
|
2158
2172
|
sendGreeting(chatState);
|
|
@@ -2417,7 +2431,7 @@ async function main() {
|
|
|
2417
2431
|
const msg = isConnError
|
|
2418
2432
|
? `⚠️ Server \`${url}\` is unreachable. The message has been requeued and will be sent when the server is available again.\n\n` +
|
|
2419
2433
|
`**Options:**\n` +
|
|
2420
|
-
|
|
2434
|
+
'• Start the user-managed OpenCode server externally\n' +
|
|
2421
2435
|
`• Switch to another server: \`/server <name>\`\n` +
|
|
2422
2436
|
`• Flush the queue: \`/flush\``
|
|
2423
2437
|
: `Error: ${err.message}`;
|
|
@@ -2573,11 +2587,6 @@ async function main() {
|
|
|
2573
2587
|
return;
|
|
2574
2588
|
}
|
|
2575
2589
|
|
|
2576
|
-
// Auto-switch tmux window to this chat's server
|
|
2577
|
-
const srvList = listServers();
|
|
2578
|
-
const srvMatch = srvList.find(s => normalizeUrl(s.url) === normalizeUrl(url));
|
|
2579
|
-
if (srvMatch) activateServer(srvMatch.dir, srvMatch.index, srvMatch.url);
|
|
2580
|
-
|
|
2581
2590
|
st.busy = true;
|
|
2582
2591
|
const myTaskId = ++st.taskId;
|
|
2583
2592
|
log.debug(` → /note: starting background processing (task ${myTaskId})`);
|
|
@@ -2601,14 +2610,13 @@ async function main() {
|
|
|
2601
2610
|
for (const { chatId, threadId, dir } of savedData.connections) {
|
|
2602
2611
|
if (!dir) continue;
|
|
2603
2612
|
const cs = getChatState(chatId, threadId);
|
|
2604
|
-
const
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
cs.serverBase = state.url;
|
|
2613
|
+
const state = readProject(dir);
|
|
2614
|
+
if (state && state.server) {
|
|
2615
|
+
cs.serverBase = state.server;
|
|
2608
2616
|
// Prefer non-thread (private chat) as owner over topic threads
|
|
2609
|
-
const existing = serverOwners[state.
|
|
2617
|
+
const existing = serverOwners[state.server];
|
|
2610
2618
|
if (!existing || (!threadId && existing.threadId)) {
|
|
2611
|
-
serverOwners[state.
|
|
2619
|
+
serverOwners[state.server] = { chatId, threadId };
|
|
2612
2620
|
}
|
|
2613
2621
|
}
|
|
2614
2622
|
}
|
|
@@ -2631,32 +2639,31 @@ async function main() {
|
|
|
2631
2639
|
if (!dir) continue;
|
|
2632
2640
|
const cs = getChatState(chatId, threadId);
|
|
2633
2641
|
if (!cs.serverBase) continue;
|
|
2634
|
-
const
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
if (notifiedUrls.has(state.url)) {
|
|
2642
|
+
const state = readProject(dir);
|
|
2643
|
+
if (state && state.server) {
|
|
2644
|
+
if (notifiedUrls.has(state.server)) {
|
|
2638
2645
|
log.info(`[chat ${chatId}] Skipping startup notification for ${dir} (already notified for this URL)`);
|
|
2639
2646
|
continue;
|
|
2640
2647
|
}
|
|
2641
|
-
notifiedUrls.add(state.
|
|
2642
|
-
const alive = await isServerAlive(state.
|
|
2648
|
+
notifiedUrls.add(state.server);
|
|
2649
|
+
const alive = await isServerAlive(state.server);
|
|
2643
2650
|
if (alive) {
|
|
2644
2651
|
const lines = [
|
|
2645
2652
|
'✅ Bot started.',
|
|
2646
2653
|
'**Server:**',
|
|
2647
2654
|
' • **Path:** `' + dir + '`',
|
|
2648
|
-
' • **URL:** `' + state.
|
|
2655
|
+
' • **URL:** `' + state.server + '`',
|
|
2649
2656
|
' • **Status:** ✅ restored',
|
|
2650
2657
|
];
|
|
2651
2658
|
try { await sendLongMessage(token, cs, lines.join('\n')); } catch (_) {}
|
|
2652
2659
|
log.info(`Restored connection: chat ${chatId} → ${dir}`);
|
|
2653
2660
|
} else {
|
|
2654
|
-
log.info(`Saved connection for chat ${chatId} → ${dir}, but server ${state.
|
|
2661
|
+
log.info(`Saved connection for chat ${chatId} → ${dir}, but server ${state.server} is unreachable`);
|
|
2655
2662
|
try {
|
|
2656
2663
|
await sendLongMessage(token, cs,
|
|
2657
2664
|
'🔄 Bot started. Previously connected to `' + dir + '`.\n' +
|
|
2658
|
-
'⚠️ The server at `' + state.
|
|
2659
|
-
'
|
|
2665
|
+
'⚠️ The server at `' + state.server + '` is not reachable.\n' +
|
|
2666
|
+
'Start the user-managed OpenCode server externally, or use `/server <name>` to connect elsewhere.');
|
|
2660
2667
|
} catch (_) {}
|
|
2661
2668
|
}
|
|
2662
2669
|
}
|
|
@@ -2804,7 +2811,7 @@ async function main() {
|
|
|
2804
2811
|
`⚠️ Server \`${cs.serverBase}\` is not reachable.\n\n` +
|
|
2805
2812
|
`Your file upload has been queued (#${qlen}). It will be sent when the server becomes available.\n\n` +
|
|
2806
2813
|
`**Options:**\n` +
|
|
2807
|
-
|
|
2814
|
+
'• Start the user-managed OpenCode server externally\n' +
|
|
2808
2815
|
`• Switch to another server: \`/server <name>\`\n` +
|
|
2809
2816
|
`• Flush the queue: \`/flush\``);
|
|
2810
2817
|
continue;
|
|
@@ -2817,11 +2824,6 @@ async function main() {
|
|
|
2817
2824
|
continue;
|
|
2818
2825
|
}
|
|
2819
2826
|
|
|
2820
|
-
// Auto-switch tmux window
|
|
2821
|
-
const srvList = listServers();
|
|
2822
|
-
const srvMatch = srvList.find(s => normalizeUrl(s.url) === normalizeUrl(cs.serverBase));
|
|
2823
|
-
if (srvMatch) activateServer(srvMatch.dir, srvMatch.index, srvMatch.url);
|
|
2824
|
-
|
|
2825
2827
|
st.busy = true;
|
|
2826
2828
|
const myTaskId = ++st.taskId;
|
|
2827
2829
|
log.debug(` → processing file upload notification (task ${myTaskId})`);
|
|
@@ -2914,21 +2916,15 @@ async function main() {
|
|
|
2914
2916
|
await sendLongMessage(token, cs, '⏳ Server is busy. Use `/cancel` to cancel first.');
|
|
2915
2917
|
continue;
|
|
2916
2918
|
}
|
|
2917
|
-
const
|
|
2918
|
-
const
|
|
2919
|
-
if (!
|
|
2920
|
-
await sendLongMessage(token, cs, '❌
|
|
2921
|
-
continue;
|
|
2922
|
-
}
|
|
2923
|
-
activateServer(srvMatch.dir, srvMatch.index, srvMatch.url);
|
|
2924
|
-
const paneId = tuiPaneId(srvMatch.index);
|
|
2925
|
-
if (!paneId) {
|
|
2926
|
-
await sendLongMessage(token, cs, '❌ TUI pane not found.');
|
|
2919
|
+
const projectDir = serverDir(cs.serverBase);
|
|
2920
|
+
const target = projectDir ? tuiRegistry.resolve(projectDir) : null;
|
|
2921
|
+
if (!target) {
|
|
2922
|
+
await sendLongMessage(token, cs, '❌ No registered TUI is available for this project.');
|
|
2927
2923
|
continue;
|
|
2928
2924
|
}
|
|
2929
2925
|
st.busy = true;
|
|
2930
2926
|
try {
|
|
2931
|
-
const answer = await executeTuiCommand(
|
|
2927
|
+
const answer = await executeTuiCommand(target.instance, cs.serverBase, tuiCmd);
|
|
2932
2928
|
if (answer && answer.trim()) {
|
|
2933
2929
|
const cleaned = await processTelegramCommands(answer.trim(), cs.serverBase, token, cs);
|
|
2934
2930
|
if (cleaned) {
|
|
@@ -3005,7 +3001,7 @@ async function main() {
|
|
|
3005
3001
|
const qlen = st.messageQueue.length;
|
|
3006
3002
|
const servers = listServers();
|
|
3007
3003
|
const suggestions = [];
|
|
3008
|
-
suggestions.push(
|
|
3004
|
+
suggestions.push('• Start the user-managed OpenCode server externally');
|
|
3009
3005
|
if (servers.length > 0) {
|
|
3010
3006
|
suggestions.push(`• Switch to another server: \`/server <name>\``);
|
|
3011
3007
|
}
|
|
@@ -3022,11 +3018,6 @@ async function main() {
|
|
|
3022
3018
|
continue;
|
|
3023
3019
|
}
|
|
3024
3020
|
|
|
3025
|
-
// Auto-switch tmux window to this chat's server
|
|
3026
|
-
const srvList = listServers();
|
|
3027
|
-
const srvMatch = srvList.find(s => normalizeUrl(s.url) === normalizeUrl(cs.serverBase));
|
|
3028
|
-
if (srvMatch) activateServer(srvMatch.dir, srvMatch.index, srvMatch.url);
|
|
3029
|
-
|
|
3030
3021
|
st.busy = true;
|
|
3031
3022
|
const myTaskId = ++st.taskId;
|
|
3032
3023
|
log.debug(` → started background processing (task ${myTaskId})`);
|
package/docs/specification.md
CHANGED
|
@@ -9,7 +9,8 @@ See **[specification_v2.md](./specification_v2.md)** for the current design:
|
|
|
9
9
|
- a single, **user-managed** OpenCode v2 server (no per-project servers);
|
|
10
10
|
- the **project = directory / target = session** model with a v2 `.ocmux.json`
|
|
11
11
|
(`directory`, `session`, `server`, `annotations`);
|
|
12
|
-
- `ocmux` project/
|
|
12
|
+
- `ocmux` project/session routing, registered dedicated/shared TUIs, and its
|
|
13
|
+
interactive pickers;
|
|
13
14
|
- `agentp` resolution, deferred tickets (incl. `cancelled`) and `--qa` output;
|
|
14
15
|
- `lib/opencode.js` as the v2-only HTTP/SSE client.
|
|
15
16
|
|