agentp 0.11.5 → 0.11.7
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 +4 -0
- package/bin/ocmux +33 -0
- package/bin/tgagentp +268 -16
- package/lib/ocmux.js +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,10 @@ It is designed for prompt-driven workflows where you want to do things like:
|
|
|
18
18
|
- capture output in files or pipe to other tools
|
|
19
19
|
- **drive prompts directly from editors** like Vim/Neovim
|
|
20
20
|
|
|
21
|
+
## Author's note
|
|
22
|
+
|
|
23
|
+
These tools are built for my own daily workflow. They are heavily AI-assisted — including the tests — and I review things before shipping, but the real test is using them every day. Bugs happen; I value a working feature more than a flawless one. MIT license, no warranty. Issues, suggestions, and PRs are welcome.
|
|
24
|
+
|
|
21
25
|
## Install
|
|
22
26
|
|
|
23
27
|
From npm:
|
package/bin/ocmux
CHANGED
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
logfileFor,
|
|
23
23
|
URL_RE,
|
|
24
24
|
resurrectServer,
|
|
25
|
+
getActiveServer,
|
|
25
26
|
} = require('../lib/ocmux');
|
|
26
27
|
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
@@ -186,6 +187,7 @@ function printHelp() {
|
|
|
186
187
|
console.error('');
|
|
187
188
|
console.error('Options:');
|
|
188
189
|
console.error(' -l Long output: append " → <dir>" after the URL (serve/default).');
|
|
190
|
+
console.error(' --last Print the URL of the active tmux window (last used server).');
|
|
189
191
|
console.error(' --print-logs Pass --print-logs to opencode serve (prints server logs to stderr).');
|
|
190
192
|
console.error(' --version Show version');
|
|
191
193
|
console.error(' -h Show this help');
|
|
@@ -445,12 +447,15 @@ function main() {
|
|
|
445
447
|
let longMode = false;
|
|
446
448
|
let gitMode = null;
|
|
447
449
|
let printLogs = false;
|
|
450
|
+
let lastMode = false;
|
|
448
451
|
const positional = [];
|
|
449
452
|
let dirIsForced = false;
|
|
450
453
|
|
|
451
454
|
for (const arg of process.argv.slice(2)) {
|
|
452
455
|
if (arg === '-l') {
|
|
453
456
|
longMode = true;
|
|
457
|
+
} else if (arg === '--last') {
|
|
458
|
+
lastMode = true;
|
|
454
459
|
} else if (arg === '--print-logs') {
|
|
455
460
|
printLogs = true;
|
|
456
461
|
} else if (arg === '--git') {
|
|
@@ -516,6 +521,34 @@ function main() {
|
|
|
516
521
|
if (!targetDir) targetDir = process.cwd();
|
|
517
522
|
|
|
518
523
|
// Dispatch
|
|
524
|
+
if (lastMode) {
|
|
525
|
+
if (gitMode) {
|
|
526
|
+
console.error("Error: '--last' cannot be combined with '--git' or '--GIT'.");
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
if (printLogs) {
|
|
530
|
+
console.error("Error: '--last' cannot be combined with '--print-logs'.");
|
|
531
|
+
process.exit(1);
|
|
532
|
+
}
|
|
533
|
+
if (subcommand) {
|
|
534
|
+
console.error(`Error: '--last' cannot be combined with subcommand '${subcommand}'.`);
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
if (targetDir !== process.cwd()) {
|
|
538
|
+
console.error("Error: '--last' does not accept a directory path.");
|
|
539
|
+
process.exit(1);
|
|
540
|
+
}
|
|
541
|
+
ensureSession();
|
|
542
|
+
const url = getActiveServer();
|
|
543
|
+
if (!url) {
|
|
544
|
+
console.error('Error: no active opencode server found in tmux.');
|
|
545
|
+
console.error("Run 'ocmux serve' to create a server, or switch to an existing one.");
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
outputUrl(url, null, longMode);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
519
552
|
if (subcommand === 'list') {
|
|
520
553
|
if (gitMode) {
|
|
521
554
|
console.error("Error: '--git' and '--GIT' are only valid with 'serve'.");
|
package/bin/tgagentp
CHANGED
|
@@ -22,6 +22,27 @@ const { listServers, activateServer, resurrectServer, statefileFor, readState, t
|
|
|
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');
|
|
25
|
+
const { spawnSync } = require('child_process');
|
|
26
|
+
|
|
27
|
+
const TGAGENTP_ROOT = process.env.TGAGENTP_ROOT;
|
|
28
|
+
let tgagentpRootValid = false;
|
|
29
|
+
if (TGAGENTP_ROOT) {
|
|
30
|
+
try {
|
|
31
|
+
const stats = fs.statSync(TGAGENTP_ROOT);
|
|
32
|
+
if (!stats.isDirectory()) {
|
|
33
|
+
log.error(`TGAGENTP_ROOT is not a directory: ${TGAGENTP_ROOT} — /serve and /new disabled`);
|
|
34
|
+
} else {
|
|
35
|
+
try {
|
|
36
|
+
fs.accessSync(TGAGENTP_ROOT, fs.constants.W_OK);
|
|
37
|
+
tgagentpRootValid = true;
|
|
38
|
+
} catch {
|
|
39
|
+
log.error(`TGAGENTP_ROOT is not writable: ${TGAGENTP_ROOT} — /serve and /new disabled`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
log.error(`TGAGENTP_ROOT does not exist: ${TGAGENTP_ROOT} — /serve and /new disabled`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
25
46
|
|
|
26
47
|
// Persist chat↔server directory mappings across restarts
|
|
27
48
|
function saveConnection(chatId, threadId, dir) {
|
|
@@ -362,8 +383,30 @@ async function cmdHelp(token, chatId, args, chatState) {
|
|
|
362
383
|
'Max 100 messages / 100KB. Oldest messages are dropped first.');
|
|
363
384
|
return;
|
|
364
385
|
}
|
|
386
|
+
if (topic === 'serve') {
|
|
387
|
+
const hint = TGAGENTP_ROOT ? '' :
|
|
388
|
+
'\nℹ️ Currently disabled. Set the `TGAGENTP_ROOT` environment variable to a writable directory to enable `/serve` and `/new`.';
|
|
389
|
+
await sendLongMessage(token, chatState,
|
|
390
|
+
'`/serve <path>` ─ start a server in an existing directory\n\n' +
|
|
391
|
+
'The path is relative to `TGAGENTP_ROOT`.\n' +
|
|
392
|
+
'Example: `/serve projects/my-app`\n\n' +
|
|
393
|
+
'Requires the `TGAGENTP_ROOT` environment variable to be set to a writable directory.' +
|
|
394
|
+
hint);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (topic === 'new') {
|
|
398
|
+
const hint = TGAGENTP_ROOT ? '' :
|
|
399
|
+
'\nℹ️ Currently disabled. Set the `TGAGENTP_ROOT` environment variable to a writable directory to enable `/serve` and `/new`.';
|
|
400
|
+
await sendLongMessage(token, chatState,
|
|
401
|
+
'`/new <path>` ─ create a directory, initialize git, and start a server\n\n' +
|
|
402
|
+
'The path is relative to `TGAGENTP_ROOT`.\n' +
|
|
403
|
+
'Example: `/new projects/my-app`\n\n' +
|
|
404
|
+
'Requires the `TGAGENTP_ROOT` environment variable to be set to a writable directory.' +
|
|
405
|
+
hint);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
365
408
|
await sendLongMessage(token, chatState,
|
|
366
|
-
'Unknown topic. Try `/help servers`, `/help sessions`, `/help agents`, `/help models`, `/help allow`, `/help think`, `/help record`, `/help queue`, `/help flush`, `/help note`, `/help comment`, `/help disconnect`, or just `/help`.');
|
|
409
|
+
'Unknown topic. Try `/help servers`, `/help sessions`, `/help agents`, `/help models`, `/help allow`, `/help think`, `/help record`, `/help queue`, `/help flush`, `/help note`, `/help comment`, `/help disconnect`, `/help serve`, `/help new`, or just `/help`.');
|
|
367
410
|
return;
|
|
368
411
|
}
|
|
369
412
|
await sendLongMessage(token, chatState,
|
|
@@ -390,7 +433,11 @@ async function cmdHelp(token, chatId, args, chatState) {
|
|
|
390
433
|
'`/comment <text>` ─ save a comment in the chat (not forwarded to the agent)\n' +
|
|
391
434
|
'`/disconnect` ─ disconnect this chat from its server\n' +
|
|
392
435
|
'`/allow` / `/reject` / `/always` ─ respond to permission requests\n' +
|
|
393
|
-
'`/shutdown` ─ stop tgagentp (only with `--dev` flag)\n
|
|
436
|
+
'`/shutdown` ─ stop tgagentp (only with `--dev` flag)\n' +
|
|
437
|
+
(tgagentpRootValid
|
|
438
|
+
? '`/serve <path>` ─ start a server in an existing directory\n' +
|
|
439
|
+
'`/new <path>` ─ create a directory, init git, and start a server\n\n'
|
|
440
|
+
: 'ℹ️ Set `TGAGENTP_ROOT` to a writable directory to enable `/serve` and `/new`.\n\n') +
|
|
394
441
|
'Everything else is forwarded to the active server as `[Telegram] <text>`.');
|
|
395
442
|
}
|
|
396
443
|
|
|
@@ -1059,6 +1106,7 @@ async function cmdAnswer(token, chatId, args, chatState, setServerForChat, setAc
|
|
|
1059
1106
|
async function cmdThink(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
1060
1107
|
const st = getState(chatState.serverBase);
|
|
1061
1108
|
const sub = args && args.length > 0 ? args[0].toLowerCase() : 'switch';
|
|
1109
|
+
const wasOn = st.thinkingEnabled;
|
|
1062
1110
|
if (sub === 'on') {
|
|
1063
1111
|
st.thinkingEnabled = true;
|
|
1064
1112
|
await sendLongMessage(token, chatState, '💭 Thinking messages **on** (forwarded to chat).');
|
|
@@ -1072,6 +1120,13 @@ async function cmdThink(token, chatId, args, chatState, setServerForChat, setAct
|
|
|
1072
1120
|
} else {
|
|
1073
1121
|
await sendLongMessage(token, chatState, 'Usage: `/think [on|off|switch]` (default: switch).');
|
|
1074
1122
|
}
|
|
1123
|
+
// When toggled on, flush any thinking text already buffered by the current request
|
|
1124
|
+
if (!wasOn && st.thinkingEnabled && st._thinkingBuf) {
|
|
1125
|
+
const buf = st._thinkingBuf;
|
|
1126
|
+
st._thinkingBuf = '';
|
|
1127
|
+
const html = '💭 ' + buf.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1128
|
+
sendLongMessage(token, chatState, html, null, chatState.threadId).catch(() => {});
|
|
1129
|
+
}
|
|
1075
1130
|
}
|
|
1076
1131
|
|
|
1077
1132
|
async function cmdQueue(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat, replyTo) {
|
|
@@ -1308,6 +1363,133 @@ async function cmdDisconnect(token, chatId, args, chatState) {
|
|
|
1308
1363
|
log.info(`[chat ${chatId}] Disconnected from server: ${url}`);
|
|
1309
1364
|
}
|
|
1310
1365
|
|
|
1366
|
+
async function cmdServe(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
1367
|
+
if (!tgagentpRootValid) {
|
|
1368
|
+
const msg = TGAGENTP_ROOT
|
|
1369
|
+
? '❌ `TGAGENTP_ROOT` is not a valid writable directory.\nCheck the logs for details.'
|
|
1370
|
+
: '❌ `TGAGENTP_ROOT` environment variable is not set.\nSet it to a writable directory to enable `/serve` and `/new` commands.';
|
|
1371
|
+
await sendLongMessage(token, chatState, msg);
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
const relPath = args.join(' ').trim();
|
|
1375
|
+
if (!relPath) {
|
|
1376
|
+
await sendLongMessage(token, chatState,
|
|
1377
|
+
'Usage: `/serve <path>`\n\n' +
|
|
1378
|
+
'Run `ocmux serve` on a directory under `TGAGENTP_ROOT`.\n' +
|
|
1379
|
+
'The path is relative to the root directory.');
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
const targetDir = path.resolve(TGAGENTP_ROOT, relPath);
|
|
1383
|
+
if (!targetDir.startsWith(TGAGENTP_ROOT)) {
|
|
1384
|
+
await sendLongMessage(token, chatState,
|
|
1385
|
+
'❌ Path must be within `TGAGENTP_ROOT`.\n' +
|
|
1386
|
+
`Requested: \`${relPath}\``);
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
try {
|
|
1390
|
+
const stats = fs.statSync(targetDir);
|
|
1391
|
+
if (!stats.isDirectory()) {
|
|
1392
|
+
await sendLongMessage(token, chatState,
|
|
1393
|
+
`❌ Not a directory: \`${relPath}\``);
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
} catch {
|
|
1397
|
+
await sendLongMessage(token, chatState,
|
|
1398
|
+
`❌ Directory does not exist: \`${relPath}\``);
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
const sf = statefileFor(targetDir);
|
|
1402
|
+
if (fs.existsSync(sf)) {
|
|
1403
|
+
await sendLongMessage(token, chatState,
|
|
1404
|
+
`⚠️ Server already exists for \`${relPath}\`.\n` +
|
|
1405
|
+
'Use `/servers switch <name>` to connect to it.');
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
await sendLongMessage(token, chatState,
|
|
1409
|
+
`🚀 Starting server for \`${relPath}\`...`);
|
|
1410
|
+
const result = spawnSync('ocmux', ['serve', targetDir], { encoding: 'utf8', timeout: 15000 });
|
|
1411
|
+
if (result.status !== 0) {
|
|
1412
|
+
const err = result.stderr || result.stdout || 'unknown error';
|
|
1413
|
+
await sendLongMessage(token, chatState,
|
|
1414
|
+
`❌ Failed to start server:\n\`\`\`\n${err}\n\`\`\``);
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
const url = (result.stdout || '').trim();
|
|
1418
|
+
if (!url) {
|
|
1419
|
+
await sendLongMessage(token, chatState,
|
|
1420
|
+
'❌ Server started but did not return a URL.');
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
await setServerForChat(chatState, url);
|
|
1424
|
+
await sendLongMessage(token, chatState,
|
|
1425
|
+
`✅ Server started.\nURL: \`${url}\`\nDir: \`${targetDir}\``);
|
|
1426
|
+
log.info(`[chat ${chatId}] Served ${targetDir} → ${url}`);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
async function cmdNew(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
1430
|
+
if (!tgagentpRootValid) {
|
|
1431
|
+
const msg = TGAGENTP_ROOT
|
|
1432
|
+
? '❌ `TGAGENTP_ROOT` is not a valid writable directory.\nCheck the logs for details.'
|
|
1433
|
+
: '❌ `TGAGENTP_ROOT` environment variable is not set.\nSet it to a writable directory to enable `/serve` and `/new` commands.';
|
|
1434
|
+
await sendLongMessage(token, chatState, msg);
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
const relPath = args.join(' ').trim();
|
|
1438
|
+
if (!relPath) {
|
|
1439
|
+
await sendLongMessage(token, chatState,
|
|
1440
|
+
'Usage: `/new <path>`\n\n' +
|
|
1441
|
+
'Create a new directory under `TGAGENTP_ROOT`, initialize git, and start a server.\n' +
|
|
1442
|
+
'The path is relative to the root directory.');
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
const targetDir = path.resolve(TGAGENTP_ROOT, relPath);
|
|
1446
|
+
if (!targetDir.startsWith(TGAGENTP_ROOT)) {
|
|
1447
|
+
await sendLongMessage(token, chatState,
|
|
1448
|
+
'❌ Path must be within `TGAGENTP_ROOT`.\n' +
|
|
1449
|
+
`Requested: \`${relPath}\``);
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
if (fs.existsSync(targetDir)) {
|
|
1453
|
+
await sendLongMessage(token, chatState,
|
|
1454
|
+
`❌ Directory already exists: \`${relPath}\`.\n` +
|
|
1455
|
+
'Use `/serve` to start a server in an existing directory.');
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
try {
|
|
1459
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
1460
|
+
} catch (err) {
|
|
1461
|
+
await sendLongMessage(token, chatState,
|
|
1462
|
+
`❌ Failed to create directory: ${err.message}`);
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
const gitResult = spawnSync('git', ['init'], { cwd: targetDir, encoding: 'utf8', timeout: 10000 });
|
|
1466
|
+
if (gitResult.status !== 0) {
|
|
1467
|
+
const err = gitResult.stderr || gitResult.stdout || 'unknown error';
|
|
1468
|
+
await sendLongMessage(token, chatState,
|
|
1469
|
+
`❌ Failed to initialize git:\n\`\`\`\n${err}\n\`\`\``);
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
await sendLongMessage(token, chatState,
|
|
1473
|
+
`🚀 Starting server for \`${relPath}\`...`);
|
|
1474
|
+
const result = spawnSync('ocmux', ['serve', targetDir], { encoding: 'utf8', timeout: 15000 });
|
|
1475
|
+
if (result.status !== 0) {
|
|
1476
|
+
const err = result.stderr || result.stdout || 'unknown error';
|
|
1477
|
+
await sendLongMessage(token, chatState,
|
|
1478
|
+
`❌ Failed to start server:\n\`\`\`\n${err}\n\`\`\``);
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
const url = (result.stdout || '').trim();
|
|
1482
|
+
if (!url) {
|
|
1483
|
+
await sendLongMessage(token, chatState,
|
|
1484
|
+
'❌ Server started but did not return a URL.');
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
await setServerForChat(chatState, url);
|
|
1488
|
+
await sendLongMessage(token, chatState,
|
|
1489
|
+
`✅ Directory created and server started.\nURL: \`${url}\`\nDir: \`${targetDir}\``);
|
|
1490
|
+
log.info(`[chat ${chatId}] Created ${targetDir} → ${url}`);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1311
1493
|
const COMMANDS = {
|
|
1312
1494
|
'help': cmdHelp,
|
|
1313
1495
|
'servers': cmdServers,
|
|
@@ -1327,6 +1509,8 @@ const COMMANDS = {
|
|
|
1327
1509
|
'reject': cmdReject,
|
|
1328
1510
|
'always': cmdAlways,
|
|
1329
1511
|
'answer': cmdAnswer,
|
|
1512
|
+
'serve': cmdServe,
|
|
1513
|
+
'new': cmdNew,
|
|
1330
1514
|
};
|
|
1331
1515
|
|
|
1332
1516
|
// --------------------------------------------------------------------------
|
|
@@ -1335,7 +1519,7 @@ const COMMANDS = {
|
|
|
1335
1519
|
|
|
1336
1520
|
const serverState = {};
|
|
1337
1521
|
function getState(url) {
|
|
1338
|
-
return serverState[url] = serverState[url] || { busy: false, serverDead: false, pendingResponse: null, activeSessionId: null, activeSessionName: null, activeAgentName: null, taskId: 0, pendingPermission: null, pendingQuestion: null, thinkingEnabled: false, messageQueue: [] };
|
|
1522
|
+
return serverState[url] = serverState[url] || { busy: false, serverDead: false, pendingResponse: null, activeSessionId: null, activeSessionName: null, activeAgentName: null, taskId: 0, pendingPermission: null, pendingQuestion: null, thinkingEnabled: false, _thinkingBuf: '', messageQueue: [] };
|
|
1339
1523
|
}
|
|
1340
1524
|
|
|
1341
1525
|
function sessionLabel(st) {
|
|
@@ -1563,7 +1747,6 @@ async function main() {
|
|
|
1563
1747
|
let answer;
|
|
1564
1748
|
if (useSession) {
|
|
1565
1749
|
const notifiedPerms = new Set();
|
|
1566
|
-
let thinkingBuf = '';
|
|
1567
1750
|
let sseConnected;
|
|
1568
1751
|
const onConnected = Promise.race([
|
|
1569
1752
|
new Promise((resolve) => { sseConnected = resolve; }),
|
|
@@ -1573,14 +1756,16 @@ async function main() {
|
|
|
1573
1756
|
onText: null,
|
|
1574
1757
|
onConnected: sseConnected,
|
|
1575
1758
|
onThinking: (chunk) => {
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
if (
|
|
1579
|
-
|
|
1759
|
+
const st2 = getState(url);
|
|
1760
|
+
st2._thinkingBuf += chunk;
|
|
1761
|
+
if (!st2.thinkingEnabled) return;
|
|
1762
|
+
const buf = st2._thinkingBuf;
|
|
1763
|
+
if (buf.length >= 50 || chunk.endsWith('\n')) {
|
|
1764
|
+
st2._thinkingBuf = '';
|
|
1765
|
+
const html = '💭 ' + buf.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1580
1766
|
sendLongMessage(token, chatState, html, replyTo, threadId)
|
|
1581
1767
|
.then(() => {})
|
|
1582
1768
|
.catch(() => {});
|
|
1583
|
-
thinkingBuf = '';
|
|
1584
1769
|
}
|
|
1585
1770
|
},
|
|
1586
1771
|
onPermission: (perm) => {
|
|
@@ -1628,10 +1813,10 @@ async function main() {
|
|
|
1628
1813
|
}, cancelRef).catch(() => {});
|
|
1629
1814
|
await onConnected;
|
|
1630
1815
|
answer = await sendToSession(url, sessionId, `[Telegram] ${text}`, st.activeAgentName, sessionCancelRef);
|
|
1631
|
-
if (
|
|
1632
|
-
const html = '💭 ' +
|
|
1816
|
+
if (st._thinkingBuf) {
|
|
1817
|
+
const html = '💭 ' + st._thinkingBuf.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1633
1818
|
sendLongMessage(token, chatState, html, replyTo, threadId).catch(() => {});
|
|
1634
|
-
|
|
1819
|
+
st._thinkingBuf = '';
|
|
1635
1820
|
}
|
|
1636
1821
|
// Keep the SSE listener alive so subsequent permission/question events
|
|
1637
1822
|
// that arrive after sendToSession returns are still forwarded to Telegram.
|
|
@@ -1691,6 +1876,7 @@ async function main() {
|
|
|
1691
1876
|
try { await sendLongMessage(token, chatState, msg, replyTo, threadId); } catch (_) {}
|
|
1692
1877
|
}
|
|
1693
1878
|
} finally {
|
|
1879
|
+
st._thinkingBuf = '';
|
|
1694
1880
|
st.pendingPermission = null;
|
|
1695
1881
|
// Only clear the reference if the SSE listener was destroyed or finished.
|
|
1696
1882
|
// If it is still running, keep the reference so the next message call
|
|
@@ -1813,6 +1999,18 @@ async function main() {
|
|
|
1813
1999
|
const intro = 'The following information is for your awareness. Reply with only "Ack" and do not take any action:';
|
|
1814
2000
|
const noteText = intro + '\n\n' + text;
|
|
1815
2001
|
if (st.busy) {
|
|
2002
|
+
// Experimental: try async injection instead of queuing
|
|
2003
|
+
if (st.activeSessionId) {
|
|
2004
|
+
try {
|
|
2005
|
+
log.debug(` → /note: attempting async injection into session ${st.activeSessionId}`);
|
|
2006
|
+
await sendToSessionAsync(url, st.activeSessionId, noteText, st.activeAgentName);
|
|
2007
|
+
await sendLongMessage(token, chatState, '💉 Note injected into ongoing session.');
|
|
2008
|
+
log.debug(` → /note: async injection succeeded`);
|
|
2009
|
+
return;
|
|
2010
|
+
} catch (err) {
|
|
2011
|
+
log.debug(` → /note: async injection failed (${err.message}), falling back to queue`);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
1816
2014
|
if (!st.messageQueue) st.messageQueue = [];
|
|
1817
2015
|
st.messageQueue.push({ text: noteText, replyTo: null });
|
|
1818
2016
|
await sendLongMessage(token, chatState, `⏳ Server is busy. Note queued (#${st.messageQueue.length}).`);
|
|
@@ -2084,7 +2282,25 @@ async function main() {
|
|
|
2084
2282
|
|
|
2085
2283
|
if (typeof msg.text !== 'string') continue;
|
|
2086
2284
|
|
|
2087
|
-
|
|
2285
|
+
let text = msg.text;
|
|
2286
|
+
|
|
2287
|
+
// !! wildcard: replace with the previous user message text
|
|
2288
|
+
if (text.includes('!!')) {
|
|
2289
|
+
const prevMsg = msg.reply_to_message;
|
|
2290
|
+
if (prevMsg && prevMsg.text) {
|
|
2291
|
+
text = text.replace(/!!/g, prevMsg.text);
|
|
2292
|
+
} else {
|
|
2293
|
+
// Fallback: look up the last user message from the ring buffer
|
|
2294
|
+
const ring = getChatState(chatId, threadId).ring;
|
|
2295
|
+
const lastUserMsg = ring.messages.slice().reverse().find(m => m.role === 'user');
|
|
2296
|
+
if (lastUserMsg) {
|
|
2297
|
+
text = text.replace(/!!/g, lastUserMsg.text);
|
|
2298
|
+
} else {
|
|
2299
|
+
await sendLongMessage(token, cs, '⚠️ No previous message found to replace !!');
|
|
2300
|
+
continue;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2088
2304
|
|
|
2089
2305
|
log.debug(`[chat ${chatId}] ${from}: ${text}`);
|
|
2090
2306
|
msgLog('in', chatId, from, text);
|
|
@@ -2092,6 +2308,42 @@ async function main() {
|
|
|
2092
2308
|
// Build reply context if this message is a Telegram reply
|
|
2093
2309
|
const replyContext = quoteReply(msg);
|
|
2094
2310
|
|
|
2311
|
+
// Download on reply: if the user replies to a file message, download it
|
|
2312
|
+
if (msg.reply_to_message && (msg.reply_to_message.document || msg.reply_to_message.photo)) {
|
|
2313
|
+
if (!cs.serverBase) {
|
|
2314
|
+
await sendLongMessage(token, cs, '🔌 Not connected to any server. Cannot download file.');
|
|
2315
|
+
} else {
|
|
2316
|
+
const projectDir = serverDir(cs.serverBase);
|
|
2317
|
+
if (!projectDir) {
|
|
2318
|
+
await sendLongMessage(token, cs, '❌ Could not find project directory for this server.');
|
|
2319
|
+
} else {
|
|
2320
|
+
try {
|
|
2321
|
+
const replyMsg = msg.reply_to_message;
|
|
2322
|
+
let fileId, fileName, fileSize;
|
|
2323
|
+
if (replyMsg.document) {
|
|
2324
|
+
fileId = replyMsg.document.file_id;
|
|
2325
|
+
fileName = replyMsg.document.file_name || 'download.bin';
|
|
2326
|
+
fileSize = replyMsg.document.file_size || 0;
|
|
2327
|
+
} else if (replyMsg.photo) {
|
|
2328
|
+
const largest = replyMsg.photo[replyMsg.photo.length - 1];
|
|
2329
|
+
fileId = largest.file_id;
|
|
2330
|
+
fileName = 'photo.jpg';
|
|
2331
|
+
fileSize = largest.file_size || 0;
|
|
2332
|
+
}
|
|
2333
|
+
const fileData = await downloadTelegramFile(token, fileId);
|
|
2334
|
+
ensureSharedDir(projectDir);
|
|
2335
|
+
const relativePath = saveUploadedFile(projectDir, fileName, fileData);
|
|
2336
|
+
const sizeStr = fileSize > 0 ? ` (${formatFileSize(fileSize)})` : '';
|
|
2337
|
+
await sendLongMessage(token, cs, `📎 File downloaded: \`${relativePath}\`${sizeStr}`);
|
|
2338
|
+
} catch (err) {
|
|
2339
|
+
log.error(`Download on reply error: ${err.message}`);
|
|
2340
|
+
await sendLongMessage(token, cs, `❌ File download failed: ${err.message}`);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
continue;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2095
2347
|
// Intercept //commands (raw TUI passthrough via tmux send-keys)
|
|
2096
2348
|
if (text.startsWith('//')) {
|
|
2097
2349
|
const tuiCmd = text.slice(1);
|
|
@@ -2140,8 +2392,8 @@ async function main() {
|
|
|
2140
2392
|
const cmdArgs = parts.slice(1);
|
|
2141
2393
|
const handler = cmds[cmd];
|
|
2142
2394
|
if (handler) {
|
|
2143
|
-
// Disconnected guard: only /help, /servers, /force-switch, /start, /comment, and /
|
|
2144
|
-
if (!cs.serverBase && cmd !== 'help' && cmd !== 'servers' && cmd !== 'force-switch' && cmd !== 'start' && cmd !== 'comment' && cmd !== 'shutdown') {
|
|
2395
|
+
// Disconnected guard: only /help, /servers, /force-switch, /start, /comment, /shutdown, /serve, and /new work without a server
|
|
2396
|
+
if (!cs.serverBase && cmd !== 'help' && cmd !== 'servers' && cmd !== 'force-switch' && cmd !== 'start' && cmd !== 'comment' && cmd !== 'shutdown' && cmd !== 'serve' && cmd !== 'new') {
|
|
2145
2397
|
await sendLongMessage(token, cs,
|
|
2146
2398
|
'🔌 Not connected to any server.\n\n' +
|
|
2147
2399
|
'Use `/servers` to see available servers and `/servers switch <name>` to connect.');
|
|
@@ -2155,7 +2407,7 @@ async function main() {
|
|
|
2155
2407
|
}
|
|
2156
2408
|
} else {
|
|
2157
2409
|
await sendLongMessage(token, cs,
|
|
2158
|
-
`Unknown command: /${cmd}\n\nAvailable commands: /help, /status, /servers, /sessions, /agents, /models, /think, /record, /queue, /flush, /note, /comment, /disconnect`);
|
|
2410
|
+
`Unknown command: /${cmd}\n\nAvailable commands: /help, /status, /servers, /sessions, /agents, /models, /think, /record, /queue, /flush, /note, /comment, /disconnect, /serve, /new`);
|
|
2159
2411
|
}
|
|
2160
2412
|
continue;
|
|
2161
2413
|
}
|
package/lib/ocmux.js
CHANGED
|
@@ -92,6 +92,18 @@ function activeWindowIndex() {
|
|
|
92
92
|
return null;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// Get the URL of the active tmux window (the one currently selected in the Opencode session).
|
|
96
|
+
function getActiveServer() {
|
|
97
|
+
const idx = activeWindowIndex();
|
|
98
|
+
if (idx == null) return null;
|
|
99
|
+
const dir = windowNameByIndex(idx);
|
|
100
|
+
if (!dir) return null;
|
|
101
|
+
const sf = statefileFor(dir);
|
|
102
|
+
if (!fs.existsSync(sf)) return null;
|
|
103
|
+
const state = readState(sf);
|
|
104
|
+
return state && state.url ? state.url : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
95
107
|
function paneCount(windowIndex) {
|
|
96
108
|
const r = _tmux(['list-panes', '-t', `${SESSION}:${windowIndex}`, '-F', '#{pane_id}']);
|
|
97
109
|
if (r.status !== 0) return 0;
|
|
@@ -259,6 +271,7 @@ module.exports = {
|
|
|
259
271
|
paneCount,
|
|
260
272
|
listServers,
|
|
261
273
|
activateServer,
|
|
274
|
+
getActiveServer,
|
|
262
275
|
hashDir,
|
|
263
276
|
logfileFor,
|
|
264
277
|
sleep,
|