agentp 0.11.11 → 0.12.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/README.md CHANGED
@@ -231,21 +231,28 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
231
231
  ### Slash commands
232
232
 
233
233
  | Command | Action |
234
- |---|---|
234
+ |---|---|---|
235
235
  | `/help [topic]` | Show general help or help for a topic (`servers`, `sessions`, `agents`, `models`, `allow`, `think`, `record`, `queue`) |
236
236
  | `/servers` | List all ocmux-served projects (▶ active, 🔌 disconnected, 💀 dead) |
237
- | `/servers switch <name> [--force]` | Switch active server; matches by full path, basename, or substring; `--force` takes over from another chat |
237
+ | `/server <name>` | Switch active server; matches by full path, basename, or substring |
238
+ | `/server --force <name>` | Take over a server from another chat |
238
239
  | `/resurrect [path]` | Restart a crashed server from its `.ocmux.json`; accepts optional directory path |
240
+ | `/sessions` | List sessions (numbered, newest first) |
241
+ | `/session <name-or-number>` | Switch to a session by name or position |
242
+ | `/session new [name]` | Create a new session |
243
+ | `/session rename <name>` | Rename the active session |
244
+ | `/agents` | List available agents/models |
245
+ | `/agent <name>` | Switch the active agent for subsequent messages (synced to server via API) |
246
+ | `/models` | List providers and models (▶ marks current) |
247
+ | `/model <providerID/modelID>` | Switch session model |
239
248
  | `/allow` | Approve a permission request once |
240
249
  | `/reject` | Deny a permission request |
241
250
  | `/always` | Approve and remember for the session |
242
251
  | `/answer <number>` | Respond to a question asked by the AI (structured multiple-choice) |
243
252
  | `/markdown` | Send the original markdown of the last response as a `.md` file; reply to a message to get that specific response |
244
- | `/agents list` | List available agents/models |
245
- | `/agents switch <name>` | Switch the active agent for subsequent messages (synced to server via API) |
246
253
  | `/shutdown [force\|clear]` | (requires `--dev`) Stop tgagentp; `clear` also wipes saved connections |
247
254
 
248
- > **Note on model/agent switching:** Changing the model in the OpenCode TUI's prompt dropdown is a **local UI action** — it only takes effect server-side after a message is sent *through the TUI*. If you change the model in the TUI and then send a prompt via `agentp` or Telegram, the old model will still be used. Use `/agents switch <name>` from Telegram (or `tgagentp`) to change models — this explicitly calls the API and syncs correctly.
255
+ > **Note on model/agent switching:** Changing the model in the OpenCode TUI's prompt dropdown is a **local UI action** — it only takes effect server-side after a message is sent *through the TUI*. If you change the model in the TUI and then send a prompt via `agentp` or Telegram, the old model will still be used. Use `/agent <name>` or `/model <providerID/modelID>` from Telegram (or `tgagentp`) to change models — this explicitly calls the API and syncs correctly.
249
256
 
250
257
  #### TUI Command Passthrough
251
258
 
@@ -255,7 +262,7 @@ Messages starting with `//` are forwarded to the OpenCode TUI as raw keystrokes
255
262
 
256
263
  ### Chat-server ownership
257
264
 
258
- Each server can be owned by at most one chat at a time. New chats start disconnected. Use `/servers switch <name>` to connect; `--force` takes over and notifies the previous owner. Connections are persisted to `/tmp/tgagentp-connections.json` and restored automatically on restart (server URL is re-discovered from `.ocmux.json`).
265
+ Each server can be owned by at most one chat at a time. New chats start disconnected. Use `/server <name>` to connect; `--force` takes over and notifies the previous owner. Connections are persisted to `/tmp/tgagentp-connections.json` and restored automatically on restart (server URL is re-discovered from `.ocmux.json`).
259
266
 
260
267
  ### Per-server state
261
268
 
@@ -287,7 +294,7 @@ tgagentp starts a tiny HTTP server on `127.0.0.1` that accepts `POST /send` requ
287
294
  - Port is written to `/tmp/tgagentp-port` for agentp discovery.
288
295
  - Authentication reuses `OPENCODE_SERVER_PASSWORD`.
289
296
  - Messages for the owning chat's active server are delivered immediately.
290
- - Messages for non-active servers are queued per-server with debounced notifications (configurable via `TGAGENTP_DEBOUNCE_MS`); delivered on `/servers switch`.
297
+ - Messages for non-active servers are queued per-server with debounced notifications (configurable via `TGAGENTP_DEBOUNCE_MS`); delivered on `/server <name>`.
291
298
  - Server health detection pre-sends: if a server is unreachable, messages are auto-queued and delivered when it comes back. `/flush` clears all queues.
292
299
  - When [/record](#tgagentp) is active, the gateway response includes the recorded conversation buffer. `agentp --qa` prepends this buffer (with rulers) to its stdout so the full Telegram context is available to OpenCode. Use `agentp --qa --flush` to flush the buffer without prepending.
293
300
 
package/bin/tgagentp CHANGED
@@ -264,46 +264,80 @@ async function cmdHelp(token, chatId, args, chatState) {
264
264
  const topic = args[0].toLowerCase();
265
265
  if (topic === 'servers') {
266
266
  await sendLongMessage(token, chatState,
267
- '`/servers` ─ list all opencode servers (ocmux sessions)\n' +
268
- '`/servers switch <name>` ─ switch target server\n' +
269
- '`/force-switch <name>` ─ take over a server from another chat\n\n' +
270
- 'Name matching: case-insensitive first, then case-sensitive. ' +
271
- '`<name>` can be a full path or basename of the project directory.');
267
+ '`/servers` ─ list running servers\n' +
268
+ ' • Shows alive and dead servers\n' +
269
+ ' • Your server marked with ▶\n\n' +
270
+ '`/server <name>` ─ switch to a server by directory path or basename\n' +
271
+ '`/server --force <name>` ─ take over a server from another chat');
272
+ return;
273
+ }
274
+ if (topic === 'agents') {
275
+ await sendLongMessage(token, chatState,
276
+ '`/agents` ─ list primary agents\n' +
277
+ ' • Current agent marked with ▶\n' +
278
+ ' • Shows model and description\n\n' +
279
+ '`/agent <name>` ─ switch to an agent');
280
+ return;
281
+ }
282
+ if (topic === 'sessions') {
283
+ await sendLongMessage(token, chatState,
284
+ '`/sessions` ─ list sessions\n' +
285
+ ' • Primary-agent sessions shown by default (includes subagent sessions to maintain context)\n' +
286
+ ' • Active session marked with ▶\n' +
287
+ ' • Grouped by recency: Today, this week, older months\n\n' +
288
+ '`/session <name-or-number>` ─ switch to a session by name or position\n' +
289
+ '`/session new [name]` ─ create a new session\n' +
290
+ '`/session rename <name>` ─ rename the active session');
272
291
  return;
273
292
  }
274
293
  if (topic === 'sessions') {
275
294
  await sendLongMessage(token, chatState,
276
295
  '`/sessions` ─ list active sessions (numbered, newest first, max 50)\n' +
277
- '`/sessions switch <number-or-name>` ─ send messages to a specific session\n' +
278
- '`/sessions new [name]` ─ create a new session\n' +
279
- '`/sessions rename <name>` ─ rename the active session\n\n' +
296
+ '`/session <name-or-number>` ─ switch to a session by name or position\n' +
297
+ '`/session new [name]` ─ create a new session\n' +
298
+ '`/session rename <name>` ─ rename the active session\n\n' +
280
299
  'Switch by number (from the list) or by case-insensitive partial name match.\n' +
281
300
  'Use `/sessions` to find session numbers.');
282
301
  return;
283
302
  }
284
- if (topic === 'shutdown') {
303
+ if (topic === 'servers') {
285
304
  await sendLongMessage(token, chatState,
286
- '`/shutdown` ─ stop tgagentp process\n' +
287
- '`/shutdown force` ─ force shutdown even if busy (in-flight work will be lost)\n\n' +
288
- 'Only available when tgagentp is started with `--dev` flag.\n' +
289
- 'Useful when running tgagentp in a restart loop:\n' +
290
- '`while true; do tgagentp --dev; done`');
305
+ '`/servers` ─ list running servers\n' +
306
+ ' • Shows alive and dead servers\n' +
307
+ ' • Your server marked with ▶\n\n' +
308
+ '`/server <name>` ─ switch to a server by directory path or basename\n' +
309
+ '`/server --force <name>` ─ take over a server from another chat');
310
+ return;
311
+ }
312
+ if (topic === 'server') {
313
+ await sendLongMessage(token, chatState,
314
+ '`/server <name>` ─ switch to a server by directory path or basename.\n' +
315
+ '`/server --force <name>` ─ take over a server from another chat.\n\n' +
316
+ 'Matches by full directory path, basename, or case-insensitive substring.\n' +
317
+ 'Use `/servers` to see available servers.');
291
318
  return;
292
319
  }
293
320
  if (topic === 'agents') {
294
321
  await sendLongMessage(token, chatState,
295
322
  '`/agents` ─ list primary agents (with ▶ marker for the active one).\n' +
296
- '`/agents switch <name>` ─ switch the active agent. Subsequent messages will use this agent.\n\n' +
297
- 'Agent switching requires an active session (`/sessions switch` or `/sessions new`).');
323
+ '`/agent <name>` ─ switch the active agent. Subsequent messages will use this agent.\n\n' +
324
+ 'Agent switching requires an active session (`/session <name>` or `/session new`).');
325
+ return;
326
+ }
327
+ if (topic === 'agent') {
328
+ await sendLongMessage(token, chatState,
329
+ '`/agent <name>` ─ switch the active agent.\n\n' +
330
+ 'Matches by exact name first, then case-insensitive substring.\n' +
331
+ 'The agent is synced to the server session via API.\n' +
332
+ 'Use `/agents` to list available agents.');
298
333
  return;
299
334
  }
300
335
  if (topic === 'session') {
301
336
  await sendLongMessage(token, chatState,
302
- '`/session` is an alias for `/sessions`.\n\n' +
303
- '`/sessions` ─ list active sessions\n' +
304
- '`/sessions switch <number-or-name>` ─ switch to a session\n' +
305
- '`/sessions new [name]` ─ create a new session\n' +
306
- '`/sessions rename <name>` ─ rename the active session');
337
+ '`/session <name-or-number>` ─ switch to a session by name or position\n' +
338
+ '`/session new [name]` ─ create a new session\n' +
339
+ '`/session rename <name>` ─ rename the active session\n\n' +
340
+ 'Use `/sessions` to list available sessions with numbers.');
307
341
  return;
308
342
  }
309
343
  if (topic === 'models') {
@@ -390,7 +424,7 @@ async function cmdHelp(token, chatId, args, chatState) {
390
424
  if (topic === 'disconnect') {
391
425
  await sendLongMessage(token, chatState,
392
426
  '`/disconnect` ─ disconnect this chat from its server\n\n' +
393
- 'Clears the server connection for this chat. Use `/servers switch <name>` to reconnect.');
427
+ 'Clears the server connection for this chat. Use `/server <name>` to reconnect.');
394
428
  return;
395
429
  }
396
430
  if (topic === 'status') {
@@ -453,7 +487,7 @@ async function cmdHelp(token, chatId, args, chatState) {
453
487
  return;
454
488
  }
455
489
  await sendLongMessage(token, chatState,
456
- '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`, `/help markdown`, or just `/help`.');
490
+ 'Unknown topic. Try `/help servers`, `/help server`, `/help sessions`, `/help session`, `/help agents`, `/help agent`, `/help models`, `/help model`, `/help allow`, `/help think`, `/help record`, `/help queue`, `/help flush`, `/help note`, `/help comment`, `/help disconnect`, `/help serve`, `/help new`, `/help markdown`, or just `/help`.');
457
491
  return;
458
492
  }
459
493
  await sendLongMessage(token, chatState,
@@ -462,16 +496,16 @@ async function cmdHelp(token, chatId, args, chatState) {
462
496
  '`/status` ─ show current server and session\n' +
463
497
  '`/cancel` ─ cancel the running prompt\n' +
464
498
  '`/servers` ─ list running opencode servers\n' +
465
- '`/servers switch <name>` ─ switch to a different server\n' +
466
- '`/force-switch <name>` ─ take over a server from another chat\n' +
467
- '`/sessions` ─ list/create/switch/rename sessions\n' +
468
- '`/sessions switch <number-or-name>` ─ switch to a session\n' +
469
- '`/sessions new [name]` ─ create a new session\n' +
470
- '`/sessions rename <name>` ─ rename the active session\n' +
471
- '`/session` ─ alias for `/sessions`\n' +
499
+ '`/server <name>` ─ switch to a different server\n' +
500
+ '`/server --force <name>` ─ take over a server from another chat\n' +
501
+ '`/sessions` ─ list sessions\n' +
502
+ '`/session <name-or-number>` ─ switch to a session\n' +
503
+ '`/session new [name]` ─ create a new session\n' +
504
+ '`/session rename <name>` ─ rename the active session\n' +
472
505
  '`/agents` ─ list available agents\n' +
473
- '`/agents switch <name>` ─ switch active agent\n' +
506
+ '`/agent <name>` ─ switch active agent\n' +
474
507
  '`/models` ─ list providers and models\n' +
508
+ '`/model <provider/model>` ─ switch session model\n' +
475
509
  '`/think` ─ toggle thinking message forwarding\n' +
476
510
  '`/record` ─ record Telegram conversation for agentp context\n' +
477
511
  '`/queue <message>` ─ queue a message when server is busy\n' +
@@ -489,90 +523,18 @@ async function cmdHelp(token, chatId, args, chatState) {
489
523
  'Everything else is forwarded to the active server as `[Telegram] <text>`.');
490
524
  }
491
525
 
492
- async function cmdForceSwitch(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
493
- return cmdServers(token, chatId, ['switch', '--force', ...args], chatState, setServerForChat, setActiveSessionForChat);
494
- }
495
-
496
526
  async function cmdServers(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
497
- const isForce = args.length > 0 && args[0].toLowerCase() === 'force-switch';
498
- if (args.length > 0 && (args[0].toLowerCase() === 'switch' || isForce)) {
499
- const forceIdx = args.indexOf('--force');
500
- const force = forceIdx !== -1 || isForce;
501
- const switchArgs = args.slice(1).filter(a => a !== '--force');
502
- if (switchArgs.length < 1) {
503
- await sendLongMessage(token, chatState,
504
- 'Usage: `/servers switch <name>`\n' +
505
- 'To force-switch (take over a server from another chat), use `/force-switch <name>`.\n' +
506
- '`<name>` can be a full directory path or a basename.');
507
- return;
508
- }
509
- const name = switchArgs.join(' ');
510
- const servers = listServers();
511
- if (servers.length === 0) {
512
- await sendLongMessage(token, chatState, 'No servers running.');
513
- return;
514
- }
515
-
516
- // Phase 1: case-insensitive exact match on dir or basename
517
- let matches = servers.filter(s => s.dir.toLowerCase() === name.toLowerCase());
518
- if (matches.length === 0) {
519
- matches = servers.filter(s => path.basename(s.dir).toLowerCase() === name.toLowerCase());
520
- }
521
- // Fallback: case-insensitive substring match
522
- if (matches.length === 0) {
523
- const lowerName = name.toLowerCase();
524
- matches = servers.filter(s => s.dir.toLowerCase().includes(lowerName) || path.basename(s.dir).toLowerCase().includes(lowerName));
525
- }
526
-
527
- if (matches.length === 1) {
528
- // Phase 1 found a single match — use it directly
529
- } else if (matches.length === 0) {
530
- await sendLongMessage(token, chatState, `No server found matching "${name}".`);
531
- return;
532
- } else {
533
- // Phase 2: case-sensitive exact match to disambiguate
534
- let exact = servers.filter(s => s.dir === name);
535
- if (exact.length === 0) {
536
- exact = servers.filter(s => path.basename(s.dir) === name);
537
- }
538
- if (exact.length === 1) {
539
- matches = exact;
540
- } else {
541
- const list = matches.map(s => ` ${s.url} → ${s.dir}`).join('\n');
542
- await sendLongMessage(token, chatState,
543
- `Multiple matches for "${name}":\n${list}\n\nPlease be more specific.`);
544
- return;
545
- }
546
- }
547
- const target = matches[0];
548
- const existing = serverOwners[target.url];
549
- if (existing && existing.chatId !== chatState.chatId && !force) {
550
- await sendLongMessage(token, chatState,
551
- `⚠️ Server \`${target.url}\` is already connected to another chat.\n\n` +
552
- `Use \`/servers force-switch ${name}\` to take over (the other chat will be disconnected).`);
553
- return;
527
+ // Backward compatibility: redirect old subcommands to singular commands
528
+ if (args.length > 0) {
529
+ const sub = args[0].toLowerCase();
530
+ if (sub === 'switch') {
531
+ const rest = args.slice(1).filter(a => a !== '--force');
532
+ const force = args.includes('--force');
533
+ return cmdServer(token, chatId, force ? ['--force', ...rest] : rest, chatState, setServerForChat, setActiveSessionForChat);
554
534
  }
555
- if (existing && existing.chatId === chatState.chatId) {
556
- // Already connected — re-run setup to refresh state
557
- }
558
- if (force && existing && existing.chatId !== chatState.chatId) {
559
- // Reset the old owner's server and notify them
560
- const oldState = getChatState(existing.chatId, existing.threadId);
561
- oldState.serverBase = null;
562
- sendLongMessage(token, oldState,
563
- `⚠️ Server \`${target.url}\` has been taken over by another chat.\n\n` +
564
- `Use \`/servers\` to see available servers and reconnect.`).catch(() => {});
565
- }
566
- const pending = await setServerForChat(chatState, target.url);
567
- activateServer(target.dir, target.index, target.url);
568
- let msg = `Switched to server: ${target.url} → ${target.dir}`;
569
- if (pending && pending.trim()) {
570
- await sendLongMessage(token, chatState, msg);
571
- await sendLongMessage(token, chatState, pending.trim());
572
- } else {
573
- await sendLongMessage(token, chatState, msg);
535
+ if (sub === 'force-switch') {
536
+ return cmdServer(token, chatId, ['--force', ...args.slice(1)], chatState, setServerForChat, setActiveSessionForChat);
574
537
  }
575
- return;
576
538
  }
577
539
 
578
540
  // Default: list servers (alive + dead)
@@ -650,106 +612,19 @@ async function discoverActiveSession(server) {
650
612
 
651
613
  async function cmdSessions(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
652
614
  const server = chatState.serverBase;
615
+ // Backward compatibility: redirect old subcommands to singular commands
653
616
  if (args.length > 0) {
654
617
  const sub = args[0].toLowerCase();
655
-
656
- if (sub === 'new') {
657
- const title = args.slice(1).join(' ') || undefined;
658
- try {
659
- const session = await createSession(server, title);
660
- const name = session.title || session.slug || session.id;
661
- setActiveSessionForChat(chatState, session.id, name);
662
- selectSession(server, session.id);
663
- await sendLongMessage(token, chatState,
664
- `Created session: \`${name}\`\nIt is now the active session.`);
665
- } catch (err) {
666
- await sendLongMessage(token, chatState, `Error creating session: ${err.message}`);
667
- }
668
- return;
618
+ if (sub === 'switch' || sub === 'new' || sub === 'rename') {
619
+ return cmdSession(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat);
669
620
  }
670
-
671
- if (sub === 'rename') {
672
- if (args.length < 2) {
673
- await sendLongMessage(token, chatState, 'Usage: `/sessions rename <name>`');
674
- return;
675
- }
676
- const st = getState(server);
677
- if (!st.activeSessionId) {
678
- await sendLongMessage(token, chatState, 'No active session. Use `/sessions switch` or `/sessions new` first.');
679
- return;
680
- }
681
- const newTitle = args.slice(1).join(' ');
682
- try {
683
- const session = await updateSession(server, st.activeSessionId, newTitle);
684
- const name = session.title || session.slug || session.id;
685
- await sendLongMessage(token, chatState,
686
- `Renamed session to: \`${name}\``);
687
- } catch (err) {
688
- await sendLongMessage(token, chatState, `Error renaming session: ${err.message}`);
689
- }
690
- return;
691
- }
692
-
693
- if (sub === 'switch') {
694
- if (args.length < 2) {
695
- await sendLongMessage(token, chatState,
696
- 'Usage: `/sessions switch <number-or-name>`\n' +
697
- 'Use `/sessions` to see numbered list. You can switch by number or by name (case-insensitive partial match).');
698
- return;
699
- }
700
- const query = args.slice(1).join(' ');
701
- try {
702
- const projectDir = serverDir(server);
703
- const sessions = await listSessions(server, projectDir);
704
- const activeId = getState(server).activeSessionId;
705
- const filtered = sessions.filter(s =>
706
- (s.agent || s.directory || s.id === activeId) && (!projectDir || s.directory === projectDir));
707
- const sorted = sortedSessions(filtered, 50);
708
-
709
- // Try by position number first
710
- const num = parseInt(query, 10);
711
- if (/^\d+$/.test(query) && num >= 1 && num <= sorted.length) {
712
- const target = sorted[num - 1];
713
- setActiveSessionForChat(chatState, target.id, sessionName(target));
714
- selectSession(server, target.id);
715
- await sendLongMessage(token, chatState,
716
- `Switched to session #${num}: \`${sessionName(target)}\`\nSubsequent messages will be sent to this session.`);
717
- return;
718
- }
719
-
720
- // Try by name (case-insensitive partial match)
721
- const q = query.toLowerCase();
722
- const matches = sorted.filter(s => sessionName(s).toLowerCase().includes(q));
723
- if (matches.length === 0) {
724
- await sendLongMessage(token, chatState,
725
- `No session matching "${query}". Use /sessions to list active sessions.`);
726
- return;
727
- }
728
- if (matches.length > 1) {
729
- const list = matches.map(s => ` \`${sessionName(s)}\``).join('\n');
730
- await sendLongMessage(token, chatState,
731
- `Multiple matches for "${query}":\n${list}\n\nPlease be more specific or use a number.`);
732
- return;
733
- }
734
- const target = matches[0];
735
- setActiveSessionForChat(chatState, target.id);
736
- selectSession(server, target.id);
737
- await sendLongMessage(token, chatState,
738
- `Switched to session: \`${sessionName(target)}\`\nSubsequent messages will be sent to this session.`);
739
- } catch (err) {
740
- await sendLongMessage(token, chatState, `Error switching session: ${err.message}`);
741
- }
742
- return;
743
- }
744
-
745
- // Unknown subcommand
746
621
  await sendLongMessage(token, chatState,
747
622
  `Unknown subcommand: /sessions ${sub}\n\n` +
748
623
  'Usage:\n' +
749
624
  ' `/sessions` ─ list sessions\n' +
750
- ' `/sessions switch <number-or-name>` ─ switch to a session\n' +
751
- ' `/sessions new [name]` ─ create a new session\n' +
752
- ' `/sessions rename <name>` ─ rename the active session');
625
+ ' `/session <name-or-number>` ─ switch to a session\n' +
626
+ ' `/session new [name]` ─ create a new session\n' +
627
+ ' `/session rename <name>` ─ rename the active session');
753
628
  return;
754
629
  }
755
630
 
@@ -823,48 +698,9 @@ async function cmdSessions(token, chatId, args, chatState, setServerForChat, set
823
698
 
824
699
  async function cmdAgents(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
825
700
  const server = chatState.serverBase;
701
+ // Backward compatibility: redirect /agents switch to /agent
826
702
  if (args.length > 0 && args[0].toLowerCase() === 'switch') {
827
- if (args.length < 2) {
828
- await sendLongMessage(token, chatState,
829
- 'Usage: `/agents switch <name>`\n' +
830
- 'Use `/agents` to list available agents.');
831
- return;
832
- }
833
- const name = args.slice(1).join(' ');
834
- try {
835
- const agents = await listAgents(server);
836
- if (!agents || agents.length === 0) {
837
- await sendLongMessage(token, chatState, 'No agents available.');
838
- return;
839
- }
840
- let matches = agents.filter(a => a.name === name);
841
- if (matches.length === 0) {
842
- matches = agents.filter(a => a.name.toLowerCase().includes(name.toLowerCase()));
843
- }
844
- if (matches.length === 0) {
845
- await sendLongMessage(token, chatState, `No agent matching "${name}".`);
846
- return;
847
- }
848
- if (matches.length > 1) {
849
- const list = matches.map(a => ` \`${a.name}\``).join('\n');
850
- await sendLongMessage(token, chatState,
851
- `Multiple matches for "${name}":\n${list}\n\nPlease be more specific.`);
852
- return;
853
- }
854
- const target = matches[0];
855
- getState(server).activeAgentName = target.name;
856
- const st = getState(server);
857
- if (st.activeSessionId) {
858
- updateSession(server, st.activeSessionId, undefined, target.name);
859
- selectSession(server, st.activeSessionId);
860
- }
861
- const model = target.model ? ` ─ \`${target.model.providerID}/${target.model.modelID}\`` : '';
862
- await sendLongMessage(token, chatState,
863
- `Switched to agent: **${target.name}**${model}\nSubsequent messages will use this agent.`);
864
- } catch (err) {
865
- await sendLongMessage(token, chatState, `Error switching agent: ${err.message}`);
866
- }
867
- return;
703
+ return cmdAgent(token, chatId, args.slice(1), chatState, setServerForChat, setActiveSessionForChat);
868
704
  }
869
705
 
870
706
  try {
@@ -1638,7 +1474,7 @@ async function cmdServe(token, chatId, args, chatState, setServerForChat, setAct
1638
1474
  } else {
1639
1475
  await sendLongMessage(token, chatState,
1640
1476
  `⚠️ Server already exists for \`${relPath}\`.\n` +
1641
- 'Use `/servers switch <name>` to connect to it.');
1477
+ 'Use `/server <name>` to connect to it.');
1642
1478
  }
1643
1479
  return;
1644
1480
  }
@@ -1836,6 +1672,210 @@ async function cmdModel(token, chatId, args, chatState, setServerForChat, setAct
1836
1672
  }
1837
1673
  }
1838
1674
 
1675
+ async function cmdServer(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
1676
+ if (args.length === 0) {
1677
+ await sendLongMessage(token, chatState,
1678
+ 'Usage: `/server <name>`\n' +
1679
+ 'To force-switch (take over a server from another chat), use `/server --force <name>`.\n' +
1680
+ '`<name>` can be a full directory path or a basename.\n' +
1681
+ 'Use `/servers` to list available servers.');
1682
+ return;
1683
+ }
1684
+ const forceIdx = args.indexOf('--force');
1685
+ const force = forceIdx !== -1;
1686
+ const nameArgs = args.filter((a, i) => a !== '--force');
1687
+ if (nameArgs.length === 0) {
1688
+ await sendLongMessage(token, chatState,
1689
+ 'Usage: `/server <name>`\n' +
1690
+ 'To force-switch, use `/server --force <name>`.');
1691
+ return;
1692
+ }
1693
+ const name = nameArgs.join(' ');
1694
+ const servers = listServers();
1695
+ if (servers.length === 0) {
1696
+ await sendLongMessage(token, chatState, 'No servers running.');
1697
+ return;
1698
+ }
1699
+ let matches = servers.filter(s => s.dir.toLowerCase() === name.toLowerCase());
1700
+ if (matches.length === 0) {
1701
+ matches = servers.filter(s => path.basename(s.dir).toLowerCase() === name.toLowerCase());
1702
+ }
1703
+ if (matches.length === 0) {
1704
+ const lowerName = name.toLowerCase();
1705
+ matches = servers.filter(s => s.dir.toLowerCase().includes(lowerName) || path.basename(s.dir).toLowerCase().includes(lowerName));
1706
+ }
1707
+ if (matches.length === 0) {
1708
+ await sendLongMessage(token, chatState, `No server found matching "${name}".`);
1709
+ return;
1710
+ }
1711
+ if (matches.length > 1) {
1712
+ let exact = servers.filter(s => s.dir === name);
1713
+ if (exact.length === 0) exact = servers.filter(s => path.basename(s.dir) === name);
1714
+ if (exact.length === 1) {
1715
+ matches = exact;
1716
+ } else {
1717
+ const list = matches.map(s => ` ${s.url} → ${s.dir}`).join('\n');
1718
+ await sendLongMessage(token, chatState,
1719
+ `Multiple matches for "${name}":\n${list}\n\nPlease be more specific.`);
1720
+ return;
1721
+ }
1722
+ }
1723
+ const target = matches[0];
1724
+ const existing = serverOwners[target.url];
1725
+ if (existing && existing.chatId !== chatState.chatId && !force) {
1726
+ await sendLongMessage(token, chatState,
1727
+ `⚠️ Server \`${target.url}\` is already connected to another chat.\n\n` +
1728
+ `Use \`/server --force ${name}\` to take over (the other chat will be disconnected).`);
1729
+ return;
1730
+ }
1731
+ if (force && existing && existing.chatId !== chatState.chatId) {
1732
+ const oldState = getChatState(existing.chatId, existing.threadId);
1733
+ oldState.serverBase = null;
1734
+ sendLongMessage(token, oldState,
1735
+ `⚠️ Server \`${target.url}\` has been taken over by another chat.\n\n` +
1736
+ `Use \`/servers\` to see available servers and reconnect.`).catch(() => {});
1737
+ }
1738
+ const pending = await setServerForChat(chatState, target.url);
1739
+ activateServer(target.dir, target.index, target.url);
1740
+ let msg = `Switched to server: ${target.url} → ${target.dir}`;
1741
+ if (pending && pending.trim()) {
1742
+ await sendLongMessage(token, chatState, msg);
1743
+ await sendLongMessage(token, chatState, pending.trim());
1744
+ } else {
1745
+ await sendLongMessage(token, chatState, msg);
1746
+ }
1747
+ }
1748
+
1749
+ async function cmdSession(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
1750
+ const server = chatState.serverBase;
1751
+ if (args.length === 0) {
1752
+ await sendLongMessage(token, chatState,
1753
+ '`/session <name-or-number>` ─ switch to a session\n' +
1754
+ '`/session new [name]` ─ create a new session\n' +
1755
+ '`/session rename <name>` ─ rename the active session\n' +
1756
+ 'Use `/sessions` to list available sessions.');
1757
+ return;
1758
+ }
1759
+ const sub = args[0].toLowerCase();
1760
+ if (sub === 'new') {
1761
+ const title = args.slice(1).join(' ') || undefined;
1762
+ try {
1763
+ const session = await createSession(server, title);
1764
+ const name = session.title || session.slug || session.id;
1765
+ setActiveSessionForChat(chatState, session.id, name);
1766
+ selectSession(server, session.id);
1767
+ await sendLongMessage(token, chatState,
1768
+ `Created session: \`${name}\`\nIt is now the active session.`);
1769
+ } catch (err) {
1770
+ await sendLongMessage(token, chatState, `Error creating session: ${err.message}`);
1771
+ }
1772
+ return;
1773
+ }
1774
+ if (sub === 'rename') {
1775
+ if (args.length < 2) {
1776
+ await sendLongMessage(token, chatState, 'Usage: `/session rename <name>`');
1777
+ return;
1778
+ }
1779
+ const st = getState(server);
1780
+ if (!st.activeSessionId) {
1781
+ await sendLongMessage(token, chatState, 'No active session. Use `/session <name>` to select one first.');
1782
+ return;
1783
+ }
1784
+ const newTitle = args.slice(1).join(' ');
1785
+ try {
1786
+ const session = await updateSession(server, st.activeSessionId, newTitle);
1787
+ const name = session.title || session.slug || session.id;
1788
+ await sendLongMessage(token, chatState, `Renamed session to: \`${name}\``);
1789
+ } catch (err) {
1790
+ await sendLongMessage(token, chatState, `Error renaming session: ${err.message}`);
1791
+ }
1792
+ return;
1793
+ }
1794
+ // Switch to session by name or number
1795
+ const query = args.join(' ');
1796
+ try {
1797
+ const projectDir = serverDir(server);
1798
+ const sessions = await listSessions(server, projectDir);
1799
+ const activeId = getState(server).activeSessionId;
1800
+ const filtered = sessions.filter(s =>
1801
+ (s.agent || s.directory || s.id === activeId) && (!projectDir || s.directory === projectDir));
1802
+ const sorted = sortedSessions(filtered, 50);
1803
+ const num = parseInt(query, 10);
1804
+ if (/^\d+$/.test(query) && num >= 1 && num <= sorted.length) {
1805
+ const target = sorted[num - 1];
1806
+ setActiveSessionForChat(chatState, target.id, sessionName(target));
1807
+ selectSession(server, target.id);
1808
+ await sendLongMessage(token, chatState,
1809
+ `Switched to session #${num}: \`${sessionName(target)}\`\nSubsequent messages will be sent to this session.`);
1810
+ return;
1811
+ }
1812
+ const q = query.toLowerCase();
1813
+ const matches = sorted.filter(s => sessionName(s).toLowerCase().includes(q));
1814
+ if (matches.length === 0) {
1815
+ await sendLongMessage(token, chatState,
1816
+ `No session matching "${query}". Use /sessions to list active sessions.`);
1817
+ return;
1818
+ }
1819
+ if (matches.length > 1) {
1820
+ const list = matches.map(s => ` \`${sessionName(s)}\``).join('\n');
1821
+ await sendLongMessage(token, chatState,
1822
+ `Multiple matches for "${query}":\n${list}\n\nPlease be more specific or use a number.`);
1823
+ return;
1824
+ }
1825
+ const target = matches[0];
1826
+ setActiveSessionForChat(chatState, target.id);
1827
+ selectSession(server, target.id);
1828
+ await sendLongMessage(token, chatState,
1829
+ `Switched to session: \`${sessionName(target)}\`\nSubsequent messages will be sent to this session.`);
1830
+ } catch (err) {
1831
+ await sendLongMessage(token, chatState, `Error switching session: ${err.message}`);
1832
+ }
1833
+ }
1834
+
1835
+ async function cmdAgent(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
1836
+ const server = chatState.serverBase;
1837
+ if (args.length === 0) {
1838
+ await sendLongMessage(token, chatState,
1839
+ 'Usage: `/agent <name>`\n' +
1840
+ 'Use `/agents` to list available agents.');
1841
+ return;
1842
+ }
1843
+ const name = args.join(' ');
1844
+ try {
1845
+ const agents = await listAgents(server);
1846
+ if (!agents || agents.length === 0) {
1847
+ await sendLongMessage(token, chatState, 'No agents available.');
1848
+ return;
1849
+ }
1850
+ let matches = agents.filter(a => a.name === name);
1851
+ if (matches.length === 0) {
1852
+ matches = agents.filter(a => a.name.toLowerCase().includes(name.toLowerCase()));
1853
+ }
1854
+ if (matches.length === 0) {
1855
+ await sendLongMessage(token, chatState, `No agent matching "${name}".`);
1856
+ return;
1857
+ }
1858
+ if (matches.length > 1) {
1859
+ const list = matches.map(a => ` \`${a.name}\``).join('\n');
1860
+ await sendLongMessage(token, chatState,
1861
+ `Multiple matches for "${name}":\n${list}\n\nPlease be more specific.`);
1862
+ return;
1863
+ }
1864
+ const target = matches[0];
1865
+ getState(server).activeAgentName = target.name;
1866
+ const st = getState(server);
1867
+ if (st.activeSessionId) {
1868
+ updateSession(server, st.activeSessionId, undefined, target.name);
1869
+ selectSession(server, st.activeSessionId);
1870
+ }
1871
+ const model = target.model ? ` ─ \`${target.model.providerID}/${target.model.modelID}\`` : '';
1872
+ await sendLongMessage(token, chatState,
1873
+ `Switched to agent: **${target.name}**${model}\nSubsequent messages will use this agent.`);
1874
+ } catch (err) {
1875
+ await sendLongMessage(token, chatState, `Error switching agent: ${err.message}`);
1876
+ }
1877
+ }
1878
+
1839
1879
  async function cmdMarkdown(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat, replyTo, replyToMsgId) {
1840
1880
  let entry = null;
1841
1881
  if (replyToMsgId) {
@@ -1859,10 +1899,11 @@ async function cmdMarkdown(token, chatId, args, chatState, setServerForChat, set
1859
1899
  const COMMANDS = {
1860
1900
  'help': cmdHelp,
1861
1901
  'servers': cmdServers,
1862
- 'force-switch': cmdForceSwitch,
1902
+ 'server': cmdServer,
1863
1903
  'sessions': cmdSessions,
1864
- 'session': cmdSessions,
1904
+ 'session': cmdSession,
1865
1905
  'agents': cmdAgents,
1906
+ 'agent': cmdAgent,
1866
1907
  'models': cmdModels,
1867
1908
  'model': cmdModel,
1868
1909
  'think': cmdThink,
@@ -2338,7 +2379,7 @@ async function main() {
2338
2379
  ? `⚠️ Server \`${url}\` is unreachable. The message has been requeued and will be sent when the server is available again.\n\n` +
2339
2380
  `**Options:**\n` +
2340
2381
  `• Restart it: \`/resurrect\`\n` +
2341
- `• Switch to another server: \`/servers switch <name>\`\n` +
2382
+ `• Switch to another server: \`/server <name>\`\n` +
2342
2383
  `• Flush the queue: \`/flush\``
2343
2384
  : `Error: ${err.message}`;
2344
2385
  try { await sendLongMessage(token, chatState, msg, replyTo, threadId); } catch (_) {}
@@ -2576,7 +2617,7 @@ async function main() {
2576
2617
  await sendLongMessage(token, cs,
2577
2618
  '🔄 Bot started. Previously connected to `' + dir + '`.\n' +
2578
2619
  '⚠️ The server at `' + state.url + '` is not reachable.\n' +
2579
- 'Use `/resurrect` to restart it, or `/servers switch <name>` to connect elsewhere.');
2620
+ 'Use `/resurrect` to restart it, or `/server <name>` to connect elsewhere.');
2580
2621
  } catch (_) {}
2581
2622
  }
2582
2623
  }
@@ -2725,7 +2766,7 @@ async function main() {
2725
2766
  `Your file upload has been queued (#${qlen}). It will be sent when the server becomes available.\n\n` +
2726
2767
  `**Options:**\n` +
2727
2768
  `• Restart it: \`/resurrect\`\n` +
2728
- `• Switch to another server: \`/servers switch <name>\`\n` +
2769
+ `• Switch to another server: \`/server <name>\`\n` +
2729
2770
  `• Flush the queue: \`/flush\``);
2730
2771
  continue;
2731
2772
  }
@@ -2875,11 +2916,11 @@ async function main() {
2875
2916
  const cmdArgs = parts.slice(1);
2876
2917
  const handler = cmds[cmd];
2877
2918
  if (handler) {
2878
- // Disconnected guard: only /help, /servers, /force-switch, /start, /comment, /shutdown, /serve, /new, and /resurrect work without a server
2879
- if (!cs.serverBase && cmd !== 'help' && cmd !== 'servers' && cmd !== 'force-switch' && cmd !== 'start' && cmd !== 'comment' && cmd !== 'shutdown' && cmd !== 'serve' && cmd !== 'new' && cmd !== 'resurrect') {
2919
+ // Disconnected guard: /help, /servers, /server, /start, /comment, /shutdown, /serve, /new, and /resurrect work without a server
2920
+ if (!cs.serverBase && cmd !== 'help' && cmd !== 'servers' && cmd !== 'server' && cmd !== 'start' && cmd !== 'comment' && cmd !== 'shutdown' && cmd !== 'serve' && cmd !== 'new' && cmd !== 'resurrect') {
2880
2921
  await sendLongMessage(token, cs,
2881
2922
  '🔌 Not connected to any server.\n\n' +
2882
- 'Use `/servers` to see available servers and `/servers switch <name>` to connect.');
2923
+ 'Use `/servers` to see available servers and `/server <name>` to connect.');
2883
2924
  continue;
2884
2925
  }
2885
2926
  try {
@@ -2891,7 +2932,7 @@ async function main() {
2891
2932
  }
2892
2933
  } else {
2893
2934
  await sendLongMessage(token, cs,
2894
- `Unknown command: /${cmd}\n\nAvailable commands: /help, /status, /servers, /sessions, /agents, /models, /model, /think, /record, /queue, /flush, /note, /comment, /disconnect, /serve, /new, /markdown`);
2935
+ `Unknown command: /${cmd}\n\nAvailable commands: /help, /status, /servers, /server, /sessions, /session, /agents, /agent, /models, /model, /think, /record, /queue, /flush, /note, /comment, /disconnect, /serve, /new, /markdown`);
2895
2936
  }
2896
2937
  continue;
2897
2938
  }
@@ -2900,7 +2941,7 @@ async function main() {
2900
2941
  if (!cs.serverBase) {
2901
2942
  await sendLongMessage(token, cs,
2902
2943
  '🔌 Not connected to any server.\n\n' +
2903
- 'Use `/servers` to see available servers and `/servers switch <name>` to connect.');
2944
+ 'Use `/servers` to see available servers and `/server <name>` to connect.');
2904
2945
  continue;
2905
2946
  }
2906
2947
 
@@ -2927,7 +2968,7 @@ async function main() {
2927
2968
  const suggestions = [];
2928
2969
  suggestions.push(`• Restart it: \`/resurrect\``);
2929
2970
  if (servers.length > 0) {
2930
- suggestions.push(`• Switch to another server: \`/servers switch <name>\``);
2971
+ suggestions.push(`• Switch to another server: \`/server <name>\``);
2931
2972
  }
2932
2973
  suggestions.push(`• Flush the queue: \`/flush\``);
2933
2974
  await sendLongMessage(token, cs,
package/lib/ocmux.js CHANGED
@@ -233,13 +233,29 @@ function resurrectServer(dir, printLogs) {
233
233
  const actualName = windowNameByIndex(winIdx);
234
234
  if (actualName !== dir) {
235
235
  const found = windowByDir(dir);
236
- if (found != null) winIdx = found;
236
+ if (found == null) {
237
+ // No window exists for this directory — old server is already dead, skip kill
238
+ winIdx = null;
239
+ } else {
240
+ winIdx = found;
241
+ }
237
242
  }
238
243
  if (winIdx != null) {
239
244
  _tmux(['select-window', '-t', `${SESSION}:${winIdx}`]);
240
245
  _tmux(['send-keys', '-t', `${SESSION}:${winIdx}.0`, 'C-c']);
241
246
  sleep(0.3);
242
- _tmux(['kill-window', '-t', `${SESSION}:${winIdx}`]);
247
+ const killR = _tmux(['kill-window', '-t', `${SESSION}:${winIdx}`]);
248
+ if (killR.status !== 0) {
249
+ throw new Error(`Failed to kill old tmux window ${winIdx}: ${killR.stderr || 'unknown error'}`);
250
+ }
251
+ // Wait for window to actually disappear (up to 2s)
252
+ for (let i = 0; i < 10; i++) {
253
+ if (windowByDir(dir) == null) break;
254
+ sleep(0.2);
255
+ }
256
+ if (windowByDir(dir) != null) {
257
+ throw new Error(`Old tmux window for ${dir} still exists after kill`);
258
+ }
243
259
  }
244
260
  }
245
261
  }
@@ -260,11 +276,14 @@ function startServer(dir, printLogs) {
260
276
  ensureSession();
261
277
  const lf = logfileFor(dir);
262
278
 
263
- const rc = _tmux(['new-window', '-d', '-t', SESSION, '-n', dir, '-c', dir]);
264
- if (rc.status !== 0) throw new Error(`failed to create tmux window for ${dir}`);
265
-
266
- const winIdx = windowByDir(dir);
267
- if (winIdx == null) throw new Error('window was not created properly');
279
+ // Create a new tmux window and capture its index directly from the -P output
280
+ const newWinR = _tmux(['new-window', '-d', '-P', '-F', '#{window_index}', '-t', SESSION, '-n', dir, '-c', dir]);
281
+ if (newWinR.status !== 0) throw new Error(`failed to create tmux window for ${dir}`);
282
+ let winIdx = parseInt(newWinR.stdout.trim(), 10);
283
+ if (isNaN(winIdx)) {
284
+ winIdx = windowByDir(dir);
285
+ if (winIdx == null) throw new Error('could not determine new window index');
286
+ }
268
287
 
269
288
  pinWindowName(winIdx);
270
289
 
@@ -283,8 +302,7 @@ function startServer(dir, printLogs) {
283
302
  }
284
303
 
285
304
  if (!url) {
286
- const wi = windowByDir(dir);
287
- if (wi != null) _tmux(['kill-window', '-t', `${SESSION}:${wi}`]);
305
+ _tmux(['kill-window', '-t', `${SESSION}:${winIdx}`]);
288
306
  throw new Error('opencode server did not start within 10 seconds');
289
307
  }
290
308
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentp",
3
- "version": "0.11.11",
3
+ "version": "0.12.0",
4
4
  "description": "Pipe prompts into a running OpenCode TUI session and stream the final answer",
5
5
  "bin": {
6
6
  "agentp": "bin/agentp",