agentp 0.11.6 → 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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/bin/tgagentp +201 -15
  3. 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/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
@@ -2206,8 +2392,8 @@ async function main() {
2206
2392
  const cmdArgs = parts.slice(1);
2207
2393
  const handler = cmds[cmd];
2208
2394
  if (handler) {
2209
- // Disconnected guard: only /help, /servers, /force-switch, /start, /comment, and /shutdown work without a server
2210
- 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') {
2211
2397
  await sendLongMessage(token, cs,
2212
2398
  '🔌 Not connected to any server.\n\n' +
2213
2399
  'Use `/servers` to see available servers and `/servers switch <name>` to connect.');
@@ -2221,7 +2407,7 @@ async function main() {
2221
2407
  }
2222
2408
  } else {
2223
2409
  await sendLongMessage(token, cs,
2224
- `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`);
2225
2411
  }
2226
2412
  continue;
2227
2413
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentp",
3
- "version": "0.11.6",
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",