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 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\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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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
- if (!getState(url).thinkingEnabled) { thinkingBuf = ''; return; }
1577
- thinkingBuf += chunk;
1578
- if (thinkingBuf.length >= 50 || chunk.endsWith('\n')) {
1579
- const html = '💭 ' + thinkingBuf.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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 (thinkingBuf) {
1632
- const html = '💭 ' + thinkingBuf.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1816
+ if (st._thinkingBuf) {
1817
+ const html = '💭 ' + st._thinkingBuf.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1633
1818
  sendLongMessage(token, chatState, html, replyTo, threadId).catch(() => {});
1634
- thinkingBuf = '';
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
- const text = msg.text;
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 /shutdown work without a server
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentp",
3
- "version": "0.11.5",
3
+ "version": "0.11.7",
4
4
  "description": "Pipe prompts into a running OpenCode TUI session and stream the final answer",
5
5
  "bin": {
6
6
  "agentp": "bin/agentp",