agentp 0.11.10 → 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 +14 -7
- package/bin/tgagentp +674 -334
- package/lib/ocmux.js +27 -9
- package/lib/opencode.js +12 -3
- package/package.json +1 -1
package/bin/tgagentp
CHANGED
|
@@ -4,7 +4,7 @@ const http = require('http');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { version } = require('../package.json');
|
|
7
|
-
const { getAuthHeaders, sendText, listenForFinalAnswer, listSessions, createSession, updateSession, selectSession, sendToSession, sendToSessionAsync, respondToPermission, respondToQuestion, listenForSessionEvents, listAgents, listProviders, isServerAlive } = require('../lib/opencode.js');
|
|
7
|
+
const { getAuthHeaders, makeRequest, buildJsonRequest, sendText, listenForFinalAnswer, listSessions, createSession, updateSession, selectSession, sendToSession, sendToSessionAsync, respondToPermission, respondToQuestion, listenForSessionEvents, listAgents, listProviders, isServerAlive } = require('../lib/opencode.js');
|
|
8
8
|
|
|
9
9
|
// Logger: info→stdout, error→stderr (always), debug→stderr (--verbose only)
|
|
10
10
|
const log = {
|
|
@@ -264,53 +264,103 @@ 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
|
|
268
|
-
'
|
|
269
|
-
'
|
|
270
|
-
'
|
|
271
|
-
'
|
|
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
|
-
'`/
|
|
278
|
-
'`/
|
|
279
|
-
'`/
|
|
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 === '
|
|
303
|
+
if (topic === 'servers') {
|
|
304
|
+
await sendLongMessage(token, chatState,
|
|
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') {
|
|
285
313
|
await sendLongMessage(token, chatState,
|
|
286
|
-
'`/
|
|
287
|
-
'`/
|
|
288
|
-
'
|
|
289
|
-
'
|
|
290
|
-
'`while true; do tgagentp --dev; done`');
|
|
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
|
-
'`/
|
|
297
|
-
'Agent switching requires an active session (`/
|
|
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
|
|
303
|
-
'`/
|
|
304
|
-
'`/
|
|
305
|
-
'`/sessions
|
|
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') {
|
|
310
344
|
await sendLongMessage(token, chatState,
|
|
311
|
-
'`/models` ─ list
|
|
312
|
-
'Shows provider
|
|
313
|
-
'
|
|
345
|
+
'`/models` ─ list connected providers.\n' +
|
|
346
|
+
' • Shows provider names with model count: `**OpenAI** (*5 models*)`\n' +
|
|
347
|
+
' • The current provider is marked with ▶\n' +
|
|
348
|
+
' • Includes hint to drill down: `/models <provider>`\n\n' +
|
|
349
|
+
'`/models <provider>` ─ list all models for a matching provider.\n' +
|
|
350
|
+
' • Partial name/ID matching supported\n' +
|
|
351
|
+
' • Current model marked with ▶\n' +
|
|
352
|
+
' • Shows context limit and input/output costs in parentheses\n\n' +
|
|
353
|
+
'Use `/model <providerID/modelID>` or `/model <modelID>` to switch the active session model.');
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (topic === 'model') {
|
|
357
|
+
await sendLongMessage(token, chatState,
|
|
358
|
+
'`/model <providerID/modelID>` ─ switch the active session to a specific model.\n' +
|
|
359
|
+
'`/model <modelID>` ─ search all connected providers for a model by name.\n\n' +
|
|
360
|
+
'Examples:\n' +
|
|
361
|
+
' `/model opencode/big-pickle` ─ explicit provider/model\n' +
|
|
362
|
+
' `/model gpt-4` ─ search across all providers\n\n' +
|
|
363
|
+
'Use `/models` to see available providers and models.');
|
|
314
364
|
return;
|
|
315
365
|
}
|
|
316
366
|
if (topic === 'allow' || topic === 'reject' || topic === 'always' || topic === 'answer') {
|
|
@@ -374,7 +424,7 @@ async function cmdHelp(token, chatId, args, chatState) {
|
|
|
374
424
|
if (topic === 'disconnect') {
|
|
375
425
|
await sendLongMessage(token, chatState,
|
|
376
426
|
'`/disconnect` ─ disconnect this chat from its server\n\n' +
|
|
377
|
-
'Clears the server connection for this chat. Use `/
|
|
427
|
+
'Clears the server connection for this chat. Use `/server <name>` to reconnect.');
|
|
378
428
|
return;
|
|
379
429
|
}
|
|
380
430
|
if (topic === 'status') {
|
|
@@ -437,7 +487,7 @@ async function cmdHelp(token, chatId, args, chatState) {
|
|
|
437
487
|
return;
|
|
438
488
|
}
|
|
439
489
|
await sendLongMessage(token, chatState,
|
|
440
|
-
'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`.');
|
|
441
491
|
return;
|
|
442
492
|
}
|
|
443
493
|
await sendLongMessage(token, chatState,
|
|
@@ -446,16 +496,16 @@ async function cmdHelp(token, chatId, args, chatState) {
|
|
|
446
496
|
'`/status` ─ show current server and session\n' +
|
|
447
497
|
'`/cancel` ─ cancel the running prompt\n' +
|
|
448
498
|
'`/servers` ─ list running opencode servers\n' +
|
|
449
|
-
'`/
|
|
450
|
-
'`/force
|
|
451
|
-
'`/sessions` ─ list
|
|
452
|
-
'`/
|
|
453
|
-
'`/
|
|
454
|
-
'`/
|
|
455
|
-
'`/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' +
|
|
456
505
|
'`/agents` ─ list available agents\n' +
|
|
457
|
-
'`/
|
|
506
|
+
'`/agent <name>` ─ switch active agent\n' +
|
|
458
507
|
'`/models` ─ list providers and models\n' +
|
|
508
|
+
'`/model <provider/model>` ─ switch session model\n' +
|
|
459
509
|
'`/think` ─ toggle thinking message forwarding\n' +
|
|
460
510
|
'`/record` ─ record Telegram conversation for agentp context\n' +
|
|
461
511
|
'`/queue <message>` ─ queue a message when server is busy\n' +
|
|
@@ -473,90 +523,18 @@ async function cmdHelp(token, chatId, args, chatState) {
|
|
|
473
523
|
'Everything else is forwarded to the active server as `[Telegram] <text>`.');
|
|
474
524
|
}
|
|
475
525
|
|
|
476
|
-
async function cmdForceSwitch(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
477
|
-
return cmdServers(token, chatId, ['switch', '--force', ...args], chatState, setServerForChat, setActiveSessionForChat);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
526
|
async function cmdServers(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
481
|
-
|
|
482
|
-
if (args.length > 0
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
'Usage: `/servers switch <name>`\n' +
|
|
489
|
-
'To force-switch (take over a server from another chat), use `/force-switch <name>`.\n' +
|
|
490
|
-
'`<name>` can be a full directory path or a basename.');
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
const name = switchArgs.join(' ');
|
|
494
|
-
const servers = listServers();
|
|
495
|
-
if (servers.length === 0) {
|
|
496
|
-
await sendLongMessage(token, chatState, 'No servers running.');
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Phase 1: case-insensitive exact match on dir or basename
|
|
501
|
-
let matches = servers.filter(s => s.dir.toLowerCase() === name.toLowerCase());
|
|
502
|
-
if (matches.length === 0) {
|
|
503
|
-
matches = servers.filter(s => path.basename(s.dir).toLowerCase() === name.toLowerCase());
|
|
504
|
-
}
|
|
505
|
-
// Fallback: case-insensitive substring match
|
|
506
|
-
if (matches.length === 0) {
|
|
507
|
-
const lowerName = name.toLowerCase();
|
|
508
|
-
matches = servers.filter(s => s.dir.toLowerCase().includes(lowerName) || path.basename(s.dir).toLowerCase().includes(lowerName));
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
if (matches.length === 1) {
|
|
512
|
-
// Phase 1 found a single match — use it directly
|
|
513
|
-
} else if (matches.length === 0) {
|
|
514
|
-
await sendLongMessage(token, chatState, `No server found matching "${name}".`);
|
|
515
|
-
return;
|
|
516
|
-
} else {
|
|
517
|
-
// Phase 2: case-sensitive exact match to disambiguate
|
|
518
|
-
let exact = servers.filter(s => s.dir === name);
|
|
519
|
-
if (exact.length === 0) {
|
|
520
|
-
exact = servers.filter(s => path.basename(s.dir) === name);
|
|
521
|
-
}
|
|
522
|
-
if (exact.length === 1) {
|
|
523
|
-
matches = exact;
|
|
524
|
-
} else {
|
|
525
|
-
const list = matches.map(s => ` ${s.url} → ${s.dir}`).join('\n');
|
|
526
|
-
await sendLongMessage(token, chatState,
|
|
527
|
-
`Multiple matches for "${name}":\n${list}\n\nPlease be more specific.`);
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
const target = matches[0];
|
|
532
|
-
const existing = serverOwners[target.url];
|
|
533
|
-
if (existing && existing.chatId !== chatState.chatId && !force) {
|
|
534
|
-
await sendLongMessage(token, chatState,
|
|
535
|
-
`⚠️ Server \`${target.url}\` is already connected to another chat.\n\n` +
|
|
536
|
-
`Use \`/servers force-switch ${name}\` to take over (the other chat will be disconnected).`);
|
|
537
|
-
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);
|
|
538
534
|
}
|
|
539
|
-
if (
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
if (force && existing && existing.chatId !== chatState.chatId) {
|
|
543
|
-
// Reset the old owner's server and notify them
|
|
544
|
-
const oldState = getChatState(existing.chatId, existing.threadId);
|
|
545
|
-
oldState.serverBase = null;
|
|
546
|
-
sendLongMessage(token, oldState,
|
|
547
|
-
`⚠️ Server \`${target.url}\` has been taken over by another chat.\n\n` +
|
|
548
|
-
`Use \`/servers\` to see available servers and reconnect.`).catch(() => {});
|
|
549
|
-
}
|
|
550
|
-
const pending = await setServerForChat(chatState, target.url);
|
|
551
|
-
activateServer(target.dir, target.index, target.url);
|
|
552
|
-
let msg = `Switched to server: ${target.url} → ${target.dir}`;
|
|
553
|
-
if (pending && pending.trim()) {
|
|
554
|
-
await sendLongMessage(token, chatState, msg);
|
|
555
|
-
await sendLongMessage(token, chatState, pending.trim());
|
|
556
|
-
} else {
|
|
557
|
-
await sendLongMessage(token, chatState, msg);
|
|
535
|
+
if (sub === 'force-switch') {
|
|
536
|
+
return cmdServer(token, chatId, ['--force', ...args.slice(1)], chatState, setServerForChat, setActiveSessionForChat);
|
|
558
537
|
}
|
|
559
|
-
return;
|
|
560
538
|
}
|
|
561
539
|
|
|
562
540
|
// Default: list servers (alive + dead)
|
|
@@ -634,106 +612,19 @@ async function discoverActiveSession(server) {
|
|
|
634
612
|
|
|
635
613
|
async function cmdSessions(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
636
614
|
const server = chatState.serverBase;
|
|
615
|
+
// Backward compatibility: redirect old subcommands to singular commands
|
|
637
616
|
if (args.length > 0) {
|
|
638
617
|
const sub = args[0].toLowerCase();
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const title = args.slice(1).join(' ') || undefined;
|
|
642
|
-
try {
|
|
643
|
-
const session = await createSession(server, title);
|
|
644
|
-
const name = session.title || session.slug || session.id;
|
|
645
|
-
setActiveSessionForChat(chatState, session.id, name);
|
|
646
|
-
selectSession(server, session.id);
|
|
647
|
-
await sendLongMessage(token, chatState,
|
|
648
|
-
`Created session: \`${name}\`\nIt is now the active session.`);
|
|
649
|
-
} catch (err) {
|
|
650
|
-
await sendLongMessage(token, chatState, `Error creating session: ${err.message}`);
|
|
651
|
-
}
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
if (sub === 'rename') {
|
|
656
|
-
if (args.length < 2) {
|
|
657
|
-
await sendLongMessage(token, chatState, 'Usage: `/sessions rename <name>`');
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
const st = getState(server);
|
|
661
|
-
if (!st.activeSessionId) {
|
|
662
|
-
await sendLongMessage(token, chatState, 'No active session. Use `/sessions switch` or `/sessions new` first.');
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
const newTitle = args.slice(1).join(' ');
|
|
666
|
-
try {
|
|
667
|
-
const session = await updateSession(server, st.activeSessionId, newTitle);
|
|
668
|
-
const name = session.title || session.slug || session.id;
|
|
669
|
-
await sendLongMessage(token, chatState,
|
|
670
|
-
`Renamed session to: \`${name}\``);
|
|
671
|
-
} catch (err) {
|
|
672
|
-
await sendLongMessage(token, chatState, `Error renaming session: ${err.message}`);
|
|
673
|
-
}
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
if (sub === 'switch') {
|
|
678
|
-
if (args.length < 2) {
|
|
679
|
-
await sendLongMessage(token, chatState,
|
|
680
|
-
'Usage: `/sessions switch <number-or-name>`\n' +
|
|
681
|
-
'Use `/sessions` to see numbered list. You can switch by number or by name (case-insensitive partial match).');
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
const query = args.slice(1).join(' ');
|
|
685
|
-
try {
|
|
686
|
-
const projectDir = serverDir(server);
|
|
687
|
-
const sessions = await listSessions(server, projectDir);
|
|
688
|
-
const activeId = getState(server).activeSessionId;
|
|
689
|
-
const filtered = sessions.filter(s =>
|
|
690
|
-
(s.agent || s.directory || s.id === activeId) && (!projectDir || s.directory === projectDir));
|
|
691
|
-
const sorted = sortedSessions(filtered, 50);
|
|
692
|
-
|
|
693
|
-
// Try by position number first
|
|
694
|
-
const num = parseInt(query, 10);
|
|
695
|
-
if (/^\d+$/.test(query) && num >= 1 && num <= sorted.length) {
|
|
696
|
-
const target = sorted[num - 1];
|
|
697
|
-
setActiveSessionForChat(chatState, target.id, sessionName(target));
|
|
698
|
-
selectSession(server, target.id);
|
|
699
|
-
await sendLongMessage(token, chatState,
|
|
700
|
-
`Switched to session #${num}: \`${sessionName(target)}\`\nSubsequent messages will be sent to this session.`);
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// Try by name (case-insensitive partial match)
|
|
705
|
-
const q = query.toLowerCase();
|
|
706
|
-
const matches = sorted.filter(s => sessionName(s).toLowerCase().includes(q));
|
|
707
|
-
if (matches.length === 0) {
|
|
708
|
-
await sendLongMessage(token, chatState,
|
|
709
|
-
`No session matching "${query}". Use /sessions to list active sessions.`);
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
if (matches.length > 1) {
|
|
713
|
-
const list = matches.map(s => ` \`${sessionName(s)}\``).join('\n');
|
|
714
|
-
await sendLongMessage(token, chatState,
|
|
715
|
-
`Multiple matches for "${query}":\n${list}\n\nPlease be more specific or use a number.`);
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
const target = matches[0];
|
|
719
|
-
setActiveSessionForChat(chatState, target.id);
|
|
720
|
-
selectSession(server, target.id);
|
|
721
|
-
await sendLongMessage(token, chatState,
|
|
722
|
-
`Switched to session: \`${sessionName(target)}\`\nSubsequent messages will be sent to this session.`);
|
|
723
|
-
} catch (err) {
|
|
724
|
-
await sendLongMessage(token, chatState, `Error switching session: ${err.message}`);
|
|
725
|
-
}
|
|
726
|
-
return;
|
|
618
|
+
if (sub === 'switch' || sub === 'new' || sub === 'rename') {
|
|
619
|
+
return cmdSession(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat);
|
|
727
620
|
}
|
|
728
|
-
|
|
729
|
-
// Unknown subcommand
|
|
730
621
|
await sendLongMessage(token, chatState,
|
|
731
622
|
`Unknown subcommand: /sessions ${sub}\n\n` +
|
|
732
623
|
'Usage:\n' +
|
|
733
624
|
' `/sessions` ─ list sessions\n' +
|
|
734
|
-
' `/
|
|
735
|
-
' `/
|
|
736
|
-
' `/
|
|
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');
|
|
737
628
|
return;
|
|
738
629
|
}
|
|
739
630
|
|
|
@@ -807,48 +698,9 @@ async function cmdSessions(token, chatId, args, chatState, setServerForChat, set
|
|
|
807
698
|
|
|
808
699
|
async function cmdAgents(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
809
700
|
const server = chatState.serverBase;
|
|
701
|
+
// Backward compatibility: redirect /agents switch to /agent
|
|
810
702
|
if (args.length > 0 && args[0].toLowerCase() === 'switch') {
|
|
811
|
-
|
|
812
|
-
await sendLongMessage(token, chatState,
|
|
813
|
-
'Usage: `/agents switch <name>`\n' +
|
|
814
|
-
'Use `/agents` to list available agents.');
|
|
815
|
-
return;
|
|
816
|
-
}
|
|
817
|
-
const name = args.slice(1).join(' ');
|
|
818
|
-
try {
|
|
819
|
-
const agents = await listAgents(server);
|
|
820
|
-
if (!agents || agents.length === 0) {
|
|
821
|
-
await sendLongMessage(token, chatState, 'No agents available.');
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
let matches = agents.filter(a => a.name === name);
|
|
825
|
-
if (matches.length === 0) {
|
|
826
|
-
matches = agents.filter(a => a.name.toLowerCase().includes(name.toLowerCase()));
|
|
827
|
-
}
|
|
828
|
-
if (matches.length === 0) {
|
|
829
|
-
await sendLongMessage(token, chatState, `No agent matching "${name}".`);
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
if (matches.length > 1) {
|
|
833
|
-
const list = matches.map(a => ` \`${a.name}\``).join('\n');
|
|
834
|
-
await sendLongMessage(token, chatState,
|
|
835
|
-
`Multiple matches for "${name}":\n${list}\n\nPlease be more specific.`);
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
const target = matches[0];
|
|
839
|
-
getState(server).activeAgentName = target.name;
|
|
840
|
-
const st = getState(server);
|
|
841
|
-
if (st.activeSessionId) {
|
|
842
|
-
updateSession(server, st.activeSessionId, undefined, target.name);
|
|
843
|
-
selectSession(server, st.activeSessionId);
|
|
844
|
-
}
|
|
845
|
-
const model = target.model ? ` ─ \`${target.model.providerID}/${target.model.modelID}\`` : '';
|
|
846
|
-
await sendLongMessage(token, chatState,
|
|
847
|
-
`Switched to agent: **${target.name}**${model}\nSubsequent messages will use this agent.`);
|
|
848
|
-
} catch (err) {
|
|
849
|
-
await sendLongMessage(token, chatState, `Error switching agent: ${err.message}`);
|
|
850
|
-
}
|
|
851
|
-
return;
|
|
703
|
+
return cmdAgent(token, chatId, args.slice(1), chatState, setServerForChat, setActiveSessionForChat);
|
|
852
704
|
}
|
|
853
705
|
|
|
854
706
|
try {
|
|
@@ -888,27 +740,124 @@ async function cmdModels(token, chatId, args, chatState, setServerForChat, setAc
|
|
|
888
740
|
return;
|
|
889
741
|
}
|
|
890
742
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
743
|
+
// Determine current model: session model first (the real in-use model,
|
|
744
|
+
// including manual /model TUI switches), then fall back to the agent's
|
|
745
|
+
// configured default model.
|
|
746
|
+
let currentProviderID = null;
|
|
747
|
+
let currentModelID = null;
|
|
748
|
+
const st = getState(server);
|
|
749
|
+
let activeSid = st.activeSessionId;
|
|
750
|
+
log.info(`[cmdModels] activeSessionId=${activeSid} activeAgentName=${st.activeAgentName}`);
|
|
751
|
+
// Auto-discover active session if not set
|
|
752
|
+
if (!activeSid) {
|
|
753
|
+
try {
|
|
754
|
+
const discovered = await discoverActiveSession(server);
|
|
755
|
+
if (discovered) {
|
|
756
|
+
st.activeSessionId = discovered.id;
|
|
757
|
+
st.activeSessionName = discovered.name;
|
|
758
|
+
activeSid = discovered.id;
|
|
759
|
+
log.info(`[cmdModels] auto-discovered session: ${discovered.name || discovered.id}`);
|
|
760
|
+
}
|
|
761
|
+
} catch (err) {
|
|
762
|
+
log.info(`[cmdModels] auto-discover error: ${err.message}`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (activeSid) {
|
|
766
|
+
try {
|
|
767
|
+
const sessions = await listSessions(server);
|
|
768
|
+
const session = sessions.find(s => String(s.id) === String(activeSid));
|
|
769
|
+
if (session && session.model) {
|
|
770
|
+
const m = session.model;
|
|
771
|
+
log.info(`[cmdModels] session model: ${JSON.stringify(m)}`);
|
|
772
|
+
if (typeof m === 'object' && m.providerID) {
|
|
773
|
+
currentProviderID = m.providerID;
|
|
774
|
+
currentModelID = m.modelID || m.id;
|
|
775
|
+
} else if (typeof m === 'string') {
|
|
776
|
+
const parts = m.split('/');
|
|
777
|
+
if (parts.length === 2) {
|
|
778
|
+
currentProviderID = parts[0];
|
|
779
|
+
currentModelID = parts[1];
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
} else {
|
|
783
|
+
log.info(`[cmdModels] session found=${!!session}, has model=${session && !!session.model}`);
|
|
784
|
+
}
|
|
785
|
+
} catch (err) {
|
|
786
|
+
log.info(`[cmdModels] listSessions error: ${err.message}`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
// Fallback: active agent's configured model
|
|
790
|
+
if (!currentModelID) {
|
|
791
|
+
const activeAgentName = st.activeAgentName;
|
|
792
|
+
if (activeAgentName) {
|
|
793
|
+
try {
|
|
794
|
+
const agents = await listAgents(server);
|
|
795
|
+
const activeAgent = agents.find(a => a.name === activeAgentName);
|
|
796
|
+
if (activeAgent && activeAgent.model) {
|
|
797
|
+
const am = activeAgent.model;
|
|
798
|
+
log.info(`[cmdModels] agent model: ${JSON.stringify(am)}`);
|
|
799
|
+
currentProviderID = am.providerID;
|
|
800
|
+
currentModelID = am.modelID || am.id;
|
|
801
|
+
} else {
|
|
802
|
+
log.info(`[cmdModels] agent found=${!!activeAgent}, has model=${activeAgent && !!activeAgent.model}`);
|
|
803
|
+
}
|
|
804
|
+
} catch (err) {
|
|
805
|
+
log.info(`[cmdModels] listAgents error: ${err.message}`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
log.info(`[cmdModels] result: provider=${currentProviderID} model=${currentModelID}`);
|
|
896
810
|
|
|
811
|
+
// /models <provider> — list all models for a matching provider
|
|
812
|
+
if (args.length > 0) {
|
|
813
|
+
const query = args.join(' ');
|
|
814
|
+
const matches = filtered.filter(p =>
|
|
815
|
+
p.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
816
|
+
p.id.toLowerCase().includes(query.toLowerCase())
|
|
817
|
+
);
|
|
818
|
+
if (matches.length === 0) {
|
|
819
|
+
await sendLongMessage(token, chatState, `No provider matching "${query}".`);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (matches.length > 1) {
|
|
823
|
+
const list = matches.map(p => ' `' + p.name + '`').join('\n');
|
|
824
|
+
await sendLongMessage(token, chatState,
|
|
825
|
+
`Multiple matches for "${query}":\n${list}\n\nPlease be more specific.`);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const provider = matches[0];
|
|
897
829
|
const modelEntries = Object.entries(provider.models || {});
|
|
898
|
-
if (modelEntries.length === 0)
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
const ctx = model.limit ? Math.round(model.limit.context / 1000) + 'K' : '';
|
|
903
|
-
const cost = model.cost ? ` $${model.cost.input}/${model.cost.output}` : '';
|
|
904
|
-
lines.push(' `' + id + '`' + (ctx ? ' ' + ctx : '') + cost);
|
|
830
|
+
if (modelEntries.length === 0) {
|
|
831
|
+
await sendLongMessage(token, chatState,
|
|
832
|
+
'**' + provider.name + '** has no models configured.');
|
|
833
|
+
return;
|
|
905
834
|
}
|
|
906
|
-
|
|
907
|
-
|
|
835
|
+
const lines = ['**' + provider.name + '** models:'];
|
|
836
|
+
for (const [id, model] of modelEntries) {
|
|
837
|
+
const isCurrent = currentProviderID === provider.id && currentModelID === id;
|
|
838
|
+
const bullet = isCurrent ? '\u25b6 ' : '\u00b7 ';
|
|
839
|
+
const extras = [];
|
|
840
|
+
if (model.limit) extras.push(Math.round(model.limit.context / 1000) + 'K ctx');
|
|
841
|
+
if (model.cost) extras.push(`$${model.cost.input}/$${model.cost.output}`);
|
|
842
|
+
const suffix = extras.length > 0 ? ` (*${extras.join(', ')}*)` : '';
|
|
843
|
+
lines.push(bullet + '`' + id + '`' + suffix);
|
|
908
844
|
}
|
|
845
|
+
await sendLongMessage(token, chatState, lines.join('\n'));
|
|
846
|
+
return;
|
|
909
847
|
}
|
|
910
848
|
|
|
911
|
-
|
|
849
|
+
// Default: list providers with model count per provider
|
|
850
|
+
const lines = ['**Connected providers:**'];
|
|
851
|
+
for (const provider of filtered) {
|
|
852
|
+
const modelCount = Object.keys(provider.models || {}).length;
|
|
853
|
+
const isCurrentProvider = currentProviderID === provider.id;
|
|
854
|
+
const bullet = isCurrentProvider ? '\u25b6 ' : '\u00b7 ';
|
|
855
|
+
lines.push(bullet + '**' + provider.name + '**' +
|
|
856
|
+
(modelCount > 0 ? ` (*${modelCount} models*)` : ' *No models*'));
|
|
857
|
+
}
|
|
858
|
+
lines.push('');
|
|
859
|
+
lines.push('\u{1f4a1} Use `/models <provider>` to list the models it provides.');
|
|
860
|
+
await sendLongMessage(token, chatState, lines.join('\n'));
|
|
912
861
|
} catch (err) {
|
|
913
862
|
await sendLongMessage(token, chatState, `Error listing models: ${err.message}`);
|
|
914
863
|
}
|
|
@@ -1525,7 +1474,7 @@ async function cmdServe(token, chatId, args, chatState, setServerForChat, setAct
|
|
|
1525
1474
|
} else {
|
|
1526
1475
|
await sendLongMessage(token, chatState,
|
|
1527
1476
|
`⚠️ Server already exists for \`${relPath}\`.\n` +
|
|
1528
|
-
'Use `/
|
|
1477
|
+
'Use `/server <name>` to connect to it.');
|
|
1529
1478
|
}
|
|
1530
1479
|
return;
|
|
1531
1480
|
}
|
|
@@ -1614,6 +1563,319 @@ async function cmdNew(token, chatId, args, chatState, setServerForChat, setActiv
|
|
|
1614
1563
|
log.info(`[chat ${chatId}] Created ${targetDir} → ${url}`);
|
|
1615
1564
|
}
|
|
1616
1565
|
|
|
1566
|
+
async function cmdModel(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
1567
|
+
const server = chatState.serverBase;
|
|
1568
|
+
const st = getState(server);
|
|
1569
|
+
// Auto-discover active session if not set
|
|
1570
|
+
if (!st.activeSessionId) {
|
|
1571
|
+
try {
|
|
1572
|
+
const discovered = await discoverActiveSession(server);
|
|
1573
|
+
if (discovered) {
|
|
1574
|
+
st.activeSessionId = discovered.id;
|
|
1575
|
+
st.activeSessionName = discovered.name;
|
|
1576
|
+
}
|
|
1577
|
+
} catch (err) {
|
|
1578
|
+
log.info(`[cmdModel] auto-discover error: ${err.message}`);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
if (args.length === 0) {
|
|
1582
|
+
await sendLongMessage(token, chatState,
|
|
1583
|
+
'Usage: `/model <providerID/modelID>` or `/model <modelID>` to search.\n' +
|
|
1584
|
+
'Example: `/model opencode/big-pickle` or `/model gpt-4`');
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
if (!st.activeSessionId) {
|
|
1588
|
+
await sendLongMessage(token, chatState, 'No active session. Use `/sessions` to select one.');
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
const query = args[0];
|
|
1592
|
+
let providerID, modelID;
|
|
1593
|
+
|
|
1594
|
+
// Try "providerID/modelID" format first
|
|
1595
|
+
if (query.includes('/')) {
|
|
1596
|
+
const parts = query.split('/');
|
|
1597
|
+
const rawProviderID = parts[0];
|
|
1598
|
+
modelID = parts[1];
|
|
1599
|
+
providerID = rawProviderID;
|
|
1600
|
+
|
|
1601
|
+
// Validate provider ID exists, or try partial match (e.g. "go" → "opencode-go")
|
|
1602
|
+
try {
|
|
1603
|
+
const result = await listProviders(server);
|
|
1604
|
+
const all = result.all || [];
|
|
1605
|
+
const connectedIds = result.connected || [];
|
|
1606
|
+
const connected = all.filter(p => connectedIds.includes(p.id));
|
|
1607
|
+
const exists = connected.some(p => p.id === rawProviderID);
|
|
1608
|
+
if (!exists) {
|
|
1609
|
+
const match = connected.find(p =>
|
|
1610
|
+
(p.name && p.name.toLowerCase().includes(rawProviderID.toLowerCase())) ||
|
|
1611
|
+
(p.id && p.id.toLowerCase().includes(rawProviderID.toLowerCase()))
|
|
1612
|
+
);
|
|
1613
|
+
if (match) {
|
|
1614
|
+
providerID = match.id;
|
|
1615
|
+
log.info(`[cmdModel] resolved "${rawProviderID}" → "${match.id}"`);
|
|
1616
|
+
} else {
|
|
1617
|
+
await sendLongMessage(token, chatState,
|
|
1618
|
+
`Provider "${rawProviderID}" not found. Use \`/models\` to list available providers.`);
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
// Verify model exists under this provider
|
|
1623
|
+
const provider = all.find(p => p.id === providerID);
|
|
1624
|
+
if (!provider || !provider.models || !provider.models[modelID]) {
|
|
1625
|
+
const availableModels = provider && provider.models ? Object.keys(provider.models).join(', ') : 'unknown';
|
|
1626
|
+
log.info(`[cmdModel] model "${modelID}" not found in ${providerID}, available: ${availableModels}`);
|
|
1627
|
+
await sendLongMessage(token, chatState,
|
|
1628
|
+
`Model "${modelID}" not found under provider \`${providerID}\`. Use \`/models\` to list available models.`);
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
} catch (err) {
|
|
1632
|
+
log.info(`[cmdModel] provider validation error: ${err.message}`);
|
|
1633
|
+
// Continue with what we have — may still work
|
|
1634
|
+
}
|
|
1635
|
+
} else {
|
|
1636
|
+
// Search across all connected providers to resolve to providerID/modelID
|
|
1637
|
+
try {
|
|
1638
|
+
const result = await listProviders(server);
|
|
1639
|
+
const all = result.all || [];
|
|
1640
|
+
const connectedIds = result.connected || [];
|
|
1641
|
+
const connected = all.filter(p => connectedIds.includes(p.id));
|
|
1642
|
+
for (const provider of connected) {
|
|
1643
|
+
const models = Object.keys(provider.models || {});
|
|
1644
|
+
const match = models.find(m => m.toLowerCase() === query.toLowerCase());
|
|
1645
|
+
if (match) {
|
|
1646
|
+
providerID = provider.id;
|
|
1647
|
+
modelID = match;
|
|
1648
|
+
break;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
} catch (err) {
|
|
1652
|
+
log.info(`[cmdModel] listProviders error: ${err.message}`);
|
|
1653
|
+
}
|
|
1654
|
+
if (!modelID) {
|
|
1655
|
+
await sendLongMessage(token, chatState,
|
|
1656
|
+
`No model matching "${query}" found in any connected provider. Use \`/models\` to list available models.`);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// Send an async no-reply message with the desired model.
|
|
1662
|
+
// This triggers createUserMessage which fires ModelSwitched when the model differs,
|
|
1663
|
+
// persisting it as the session's current model.
|
|
1664
|
+
try {
|
|
1665
|
+
await sendToSessionAsync(server, st.activeSessionId, '.', null, { providerID, modelID }, true);
|
|
1666
|
+
const label = providerID ? `\`${providerID}/${modelID}\`` : `\`${modelID}\``;
|
|
1667
|
+
await sendLongMessage(token, chatState,
|
|
1668
|
+
`✅ Switched to model ${label}.`);
|
|
1669
|
+
log.info(`[chat ${chatId}] Switched to model ${providerID || '?'}/${modelID}`);
|
|
1670
|
+
} catch (err) {
|
|
1671
|
+
await sendLongMessage(token, chatState, `Error switching model: ${err.message}`);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
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
|
+
|
|
1617
1879
|
async function cmdMarkdown(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat, replyTo, replyToMsgId) {
|
|
1618
1880
|
let entry = null;
|
|
1619
1881
|
if (replyToMsgId) {
|
|
@@ -1637,11 +1899,13 @@ async function cmdMarkdown(token, chatId, args, chatState, setServerForChat, set
|
|
|
1637
1899
|
const COMMANDS = {
|
|
1638
1900
|
'help': cmdHelp,
|
|
1639
1901
|
'servers': cmdServers,
|
|
1640
|
-
'
|
|
1902
|
+
'server': cmdServer,
|
|
1641
1903
|
'sessions': cmdSessions,
|
|
1642
|
-
'session':
|
|
1904
|
+
'session': cmdSession,
|
|
1643
1905
|
'agents': cmdAgents,
|
|
1906
|
+
'agent': cmdAgent,
|
|
1644
1907
|
'models': cmdModels,
|
|
1908
|
+
'model': cmdModel,
|
|
1645
1909
|
'think': cmdThink,
|
|
1646
1910
|
'record': cmdRecord,
|
|
1647
1911
|
'resurrect': cmdResurrect,
|
|
@@ -1851,6 +2115,7 @@ async function main() {
|
|
|
1851
2115
|
if (srvDiscovered) {
|
|
1852
2116
|
getState(url).activeSessionId = srvDiscovered.id;
|
|
1853
2117
|
getState(url).activeSessionName = srvDiscovered.name;
|
|
2118
|
+
startSessionMonitor(url, srvDiscovered.id);
|
|
1854
2119
|
log.info(` → session: ${srvDiscovered.name || srvDiscovered.id}`);
|
|
1855
2120
|
}
|
|
1856
2121
|
const defaultSrvAgent = srvAgents.find(a => a.mode === 'primary') || srvAgents[0];
|
|
@@ -1872,7 +2137,10 @@ async function main() {
|
|
|
1872
2137
|
getState(server).activeSessionId = id;
|
|
1873
2138
|
if (name) getState(server).activeSessionName = name;
|
|
1874
2139
|
log.info(`[chat ${chatState.chatId}] Switched to session: ${name || id}`);
|
|
1875
|
-
if (id !== prevId)
|
|
2140
|
+
if (id !== prevId) {
|
|
2141
|
+
sendGreeting(chatState);
|
|
2142
|
+
startSessionMonitor(server, id);
|
|
2143
|
+
}
|
|
1876
2144
|
};
|
|
1877
2145
|
|
|
1878
2146
|
const TELEGRAM_GREETING_BODY = (sessionName) =>
|
|
@@ -1891,6 +2159,90 @@ async function main() {
|
|
|
1891
2159
|
'For more details about a command:\n' +
|
|
1892
2160
|
'[Telegram]{"command":"help","topic":"<upload|download|help>"}';
|
|
1893
2161
|
|
|
2162
|
+
/**
|
|
2163
|
+
* Start a persistent SSE session monitor that catches question.asked,
|
|
2164
|
+
* permission_asked, and permission_replied events in the background,
|
|
2165
|
+
* even when the AI was triggered by agentp (--tg) rather than by tgagentp.
|
|
2166
|
+
* Automatically restarted when the session or server changes.
|
|
2167
|
+
*/
|
|
2168
|
+
function startSessionMonitor(server, sessionId) {
|
|
2169
|
+
const st = getState(server);
|
|
2170
|
+
if (st._monitorRef && st._monitorRef.current) {
|
|
2171
|
+
st._monitorRef.current.destroy();
|
|
2172
|
+
st._monitorRef.current = null;
|
|
2173
|
+
}
|
|
2174
|
+
const monRef = { current: null };
|
|
2175
|
+
st._monitorRef = monRef;
|
|
2176
|
+
st._monitorStarted = true;
|
|
2177
|
+
|
|
2178
|
+
const notifiedPerms = new Set();
|
|
2179
|
+
|
|
2180
|
+
listenForSessionEvents(server, sessionId, {
|
|
2181
|
+
onText: null,
|
|
2182
|
+
onConnected: null,
|
|
2183
|
+
onThinking: null,
|
|
2184
|
+
onPermission: (perm) => {
|
|
2185
|
+
const st2 = getState(server);
|
|
2186
|
+
const permDump = JSON.stringify(perm);
|
|
2187
|
+
log.debug(` [monitor] -> permission_asked callback: ${permDump}`);
|
|
2188
|
+
st2.pendingPermission = { server, sessionId, permissionId: perm.id, type: perm.permission || 'unspecified', pattern: perm.patterns || perm.pattern, title: perm.title };
|
|
2189
|
+
const ownerChat = Object.entries(serverOwners).find(([url]) => normalizeUrl(url) === normalizeUrl(server))?.[1];
|
|
2190
|
+
if (ownerChat && !notifiedPerms.has(perm.id)) {
|
|
2191
|
+
notifiedPerms.add(perm.id);
|
|
2192
|
+
const toolStr = perm.permission || 'unspecified';
|
|
2193
|
+
const patternArr = perm.patterns || (perm.pattern ? [perm.pattern] : []);
|
|
2194
|
+
const patternStr = patternArr.join(', ');
|
|
2195
|
+
const msg = '\u{1f510}**Permission needed**\n'
|
|
2196
|
+
+ '**Tool:** `' + toolStr + '`\n'
|
|
2197
|
+
+ (patternStr ? '**Target:** `' + patternStr + '`\n' : '')
|
|
2198
|
+
+ (perm.title ? '**Detail:** ' + perm.title + '\n' : '')
|
|
2199
|
+
+ 'Use `/allow` to approve once, `/always` to always allow, or `/reject` to deny.';
|
|
2200
|
+
log.debug(` [monitor] -> sending permission notification to chat ${ownerChat.chatId}`);
|
|
2201
|
+
sendLongMessage(token, { chatId: ownerChat.chatId, threadId: ownerChat.threadId }, msg)
|
|
2202
|
+
.then(() => log.debug(` [monitor] -> permission notification sent OK`))
|
|
2203
|
+
.catch((err) => log.debug(` [monitor] -> permission notification failed: ${err.message}`));
|
|
2204
|
+
} else if (ownerChat) {
|
|
2205
|
+
log.debug(` [monitor] -> permission ${perm.id} already notified, updating pendingPermission only`);
|
|
2206
|
+
} else {
|
|
2207
|
+
log.debug(` [monitor] -> no owner chat found for server ${server}, skipping notification`);
|
|
2208
|
+
}
|
|
2209
|
+
},
|
|
2210
|
+
onPermissionReplied: () => {
|
|
2211
|
+
log.debug(` [monitor] -> permission_replied: clearing pendingPermission`);
|
|
2212
|
+
getState(server).pendingPermission = null;
|
|
2213
|
+
},
|
|
2214
|
+
onQuestion: (q) => {
|
|
2215
|
+
const st2 = getState(server);
|
|
2216
|
+
const qDump = JSON.stringify(q);
|
|
2217
|
+
log.debug(` [monitor] -> question_asked callback: ${qDump}`);
|
|
2218
|
+
st2.pendingQuestion = { server, sessionId, questionId: q.id, question: q.question || 'Unknown question', options: q.options || [] };
|
|
2219
|
+
const ownerChat = Object.entries(serverOwners).find(([url]) => normalizeUrl(url) === normalizeUrl(server))?.[1];
|
|
2220
|
+
if (ownerChat) {
|
|
2221
|
+
const options = (q.options || []).map((opt, i) => `${i + 1}. ${opt.text || opt}`).join('\n');
|
|
2222
|
+
const msg = '\u2753**Question from the AI**\n\n'
|
|
2223
|
+
+ (q.question || 'Choose an option:') + '\n\n'
|
|
2224
|
+
+ (options ? options + '\n\n' : '')
|
|
2225
|
+
+ 'Use `/answer <number>` to respond.';
|
|
2226
|
+
log.debug(` [monitor] -> sending question notification to chat ${ownerChat.chatId}`);
|
|
2227
|
+
sendLongMessage(token, { chatId: ownerChat.chatId, threadId: ownerChat.threadId }, msg)
|
|
2228
|
+
.then(() => log.debug(` [monitor] -> question notification sent OK`))
|
|
2229
|
+
.catch((err) => log.debug(` [monitor] -> question notification failed: ${err.message}`));
|
|
2230
|
+
} else {
|
|
2231
|
+
log.debug(` [monitor] -> no owner chat found for server ${server}, skipping question notification`);
|
|
2232
|
+
}
|
|
2233
|
+
},
|
|
2234
|
+
}, monRef).then(() => {
|
|
2235
|
+
// Normal completion (90s safety timeout or stream ended)
|
|
2236
|
+
}, (err) => {
|
|
2237
|
+
log.debug(` [monitor] SSE listener for session ${sessionId} ended: ${err.message}`);
|
|
2238
|
+
}).finally(() => {
|
|
2239
|
+
if (getState(server)._monitorRef === monRef) {
|
|
2240
|
+
getState(server)._monitorRef = null;
|
|
2241
|
+
getState(server)._monitorStarted = false;
|
|
2242
|
+
}
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
|
|
1894
2246
|
async function sendGreeting(chatState) {
|
|
1895
2247
|
const st = getState(chatState.serverBase);
|
|
1896
2248
|
if (!st.activeSessionId) {
|
|
@@ -1928,10 +2280,17 @@ async function main() {
|
|
|
1928
2280
|
if (st._sessionReq && st._sessionReq.current) { st._sessionReq.current.destroy(); st._sessionReq.current = null; }
|
|
1929
2281
|
st._cancelRef = cancelRef;
|
|
1930
2282
|
st._sessionReq = sessionCancelRef;
|
|
2283
|
+
|
|
2284
|
+
// Ensure the persistent session monitor is running so question/permission
|
|
2285
|
+
// events from any source (agentp, tgagentp, TUI) are caught, not just
|
|
2286
|
+
// those triggered by this message's per-request SSE listener.
|
|
2287
|
+
if (useSession && sessionId && !st._monitorStarted) {
|
|
2288
|
+
startSessionMonitor(url, sessionId);
|
|
2289
|
+
}
|
|
2290
|
+
|
|
1931
2291
|
try {
|
|
1932
2292
|
let answer;
|
|
1933
2293
|
if (useSession) {
|
|
1934
|
-
const notifiedPerms = new Set();
|
|
1935
2294
|
let sseConnected;
|
|
1936
2295
|
const onConnected = Promise.race([
|
|
1937
2296
|
new Promise((resolve) => { sseConnected = resolve; }),
|
|
@@ -1953,48 +2312,9 @@ async function main() {
|
|
|
1953
2312
|
.catch(() => {});
|
|
1954
2313
|
}
|
|
1955
2314
|
},
|
|
1956
|
-
onPermission
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
log.debug(` → permission_asked callback: ${permDump}`);
|
|
1960
|
-
st2.pendingPermission = { server: url, sessionId, permissionId: perm.id, type: perm.permission || 'unspecified', pattern: perm.patterns || perm.pattern, title: perm.title };
|
|
1961
|
-
if (!notifiedPerms.has(perm.id)) {
|
|
1962
|
-
notifiedPerms.add(perm.id);
|
|
1963
|
-
const toolStr = perm.permission || 'unspecified';
|
|
1964
|
-
const patternArr = perm.patterns || (perm.pattern ? [perm.pattern] : []);
|
|
1965
|
-
const patternStr = patternArr.join(', ');
|
|
1966
|
-
const msg = '🔐 **Permission needed**\n'
|
|
1967
|
-
+ '**Tool:** `' + toolStr + '`\n'
|
|
1968
|
-
+ (patternStr ? '**Target:** `' + patternStr + '`\n' : '')
|
|
1969
|
-
+ (perm.title ? '**Detail:** ' + perm.title + '\n' : '')
|
|
1970
|
-
+ 'Use `/allow` to approve once, `/always` to always allow, or `/reject` to deny.';
|
|
1971
|
-
log.debug(` → sending permission notification to chat ${chatId}`);
|
|
1972
|
-
sendLongMessage(token, chatState, msg, replyTo, threadId)
|
|
1973
|
-
.then(() => log.debug(` → permission notification sent OK`))
|
|
1974
|
-
.catch((err) => log.debug(` → permission notification failed: ${err.message}`));
|
|
1975
|
-
} else {
|
|
1976
|
-
log.debug(` → permission ${perm.id} already notified, updating pendingPermission only`);
|
|
1977
|
-
}
|
|
1978
|
-
},
|
|
1979
|
-
onPermissionReplied: () => {
|
|
1980
|
-
log.debug(` → permission_replied: clearing pendingPermission`);
|
|
1981
|
-
getState(url).pendingPermission = null;
|
|
1982
|
-
},
|
|
1983
|
-
onQuestion: (q) => {
|
|
1984
|
-
const st2 = getState(url);
|
|
1985
|
-
const qDump = JSON.stringify(q);
|
|
1986
|
-
log.debug(` → question_asked callback: ${qDump}`);
|
|
1987
|
-
st2.pendingQuestion = { server: url, sessionId, questionId: q.id, question: q.question || 'Unknown question', options: q.options || [] };
|
|
1988
|
-
const options = (q.options || []).map((opt, i) => `${i + 1}. ${opt.text || opt}`).join('\n');
|
|
1989
|
-
const msg = '❓ **Question from the AI**\n\n'
|
|
1990
|
-
+ (q.question || 'Choose an option:') + '\n\n'
|
|
1991
|
-
+ (options ? options + '\n\n' : '')
|
|
1992
|
-
+ 'Use `/answer <number>` to respond.';
|
|
1993
|
-
log.debug(` → sending question notification to chat ${chatId}`);
|
|
1994
|
-
sendLongMessage(token, chatState, msg, replyTo, threadId)
|
|
1995
|
-
.then(() => log.debug(` → question notification sent OK`))
|
|
1996
|
-
.catch((err) => log.debug(` → question notification failed: ${err.message}`));
|
|
1997
|
-
},
|
|
2315
|
+
// onPermission, onPermissionReplied, and onQuestion are handled by
|
|
2316
|
+
// the persistent session monitor (startSessionMonitor) so they are
|
|
2317
|
+
// caught even when agentp (--tg) triggers the AI response.
|
|
1998
2318
|
}, cancelRef).catch(() => {});
|
|
1999
2319
|
await onConnected;
|
|
2000
2320
|
answer = await sendToSession(url, sessionId, `[Telegram] ${text}`, st.activeAgentName, sessionCancelRef);
|
|
@@ -2003,8 +2323,8 @@ async function main() {
|
|
|
2003
2323
|
sendLongMessage(token, chatState, html, replyTo, threadId).catch(() => {});
|
|
2004
2324
|
st._thinkingBuf = '';
|
|
2005
2325
|
}
|
|
2006
|
-
// Keep the SSE listener alive
|
|
2007
|
-
//
|
|
2326
|
+
// Keep the per-request SSE listener alive for streaming text events.
|
|
2327
|
+
// Questions and permissions are handled by the persistent monitor.
|
|
2008
2328
|
} else {
|
|
2009
2329
|
await sendText(url, `[Telegram] ${text}`);
|
|
2010
2330
|
answer = await listenForFinalAnswer(url, null, cancelRef);
|
|
@@ -2059,7 +2379,7 @@ async function main() {
|
|
|
2059
2379
|
? `⚠️ Server \`${url}\` is unreachable. The message has been requeued and will be sent when the server is available again.\n\n` +
|
|
2060
2380
|
`**Options:**\n` +
|
|
2061
2381
|
`• Restart it: \`/resurrect\`\n` +
|
|
2062
|
-
`• Switch to another server: \`/
|
|
2382
|
+
`• Switch to another server: \`/server <name>\`\n` +
|
|
2063
2383
|
`• Flush the queue: \`/flush\``
|
|
2064
2384
|
: `Error: ${err.message}`;
|
|
2065
2385
|
try { await sendLongMessage(token, chatState, msg, replyTo, threadId); } catch (_) {}
|
|
@@ -2143,24 +2463,32 @@ async function main() {
|
|
|
2143
2463
|
// Handle /shutdown clear — wipe saved connections before exiting
|
|
2144
2464
|
if (args && args.length > 0 && args[0].toLowerCase() === 'clear') {
|
|
2145
2465
|
clearConnections();
|
|
2146
|
-
await sendLongMessage(token, cs, '
|
|
2147
|
-
log.
|
|
2148
|
-
try { await getUpdates(token, offset, 1); } catch (_) {}
|
|
2466
|
+
await sendLongMessage(token, cs, '\u{1f9f9} Connection state cleared.').catch(() => {});
|
|
2467
|
+
log.info('/shutdown clear: exiting');
|
|
2149
2468
|
process.exit(0);
|
|
2150
2469
|
}
|
|
2151
2470
|
|
|
2152
|
-
|
|
2471
|
+
// Parse numeric exit code from args (default 0). Uses regex /^\d+$/
|
|
2472
|
+
// to robustly match only pure-digit arguments.
|
|
2473
|
+
let exitCode = 0;
|
|
2474
|
+
for (const arg of (args || [])) {
|
|
2475
|
+
if (/^\d+$/.test(arg)) {
|
|
2476
|
+
exitCode = parseInt(arg, 10);
|
|
2477
|
+
break;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
const isForce = args && args.some(a => a.toLowerCase() === 'force');
|
|
2153
2482
|
if (st.busy && !isForce) {
|
|
2154
2483
|
await sendLongMessage(token, cs,
|
|
2155
|
-
'
|
|
2156
|
-
'Use `/shutdown force` to shut down anyway (in-flight response will be lost).');
|
|
2484
|
+
'\u23f3 Server is busy processing a message.\n' +
|
|
2485
|
+
'Use `/shutdown force` to shut down anyway (in-flight response will be lost).').catch(() => {});
|
|
2157
2486
|
return;
|
|
2158
2487
|
}
|
|
2159
|
-
const status = st.busy ? '
|
|
2160
|
-
await sendLongMessage(token, cs,
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
process.exit(0);
|
|
2488
|
+
const status = st.busy ? '\u23f3 busy (forced)' : '\u2705 idle';
|
|
2489
|
+
await sendLongMessage(token, cs, `\u{1f44b} Shutting down (state: ${status}, exit ${exitCode})...`).catch(() => {});
|
|
2490
|
+
log.info(`/shutdown: exiting with code ${exitCode}`);
|
|
2491
|
+
process.exit(exitCode);
|
|
2164
2492
|
};
|
|
2165
2493
|
|
|
2166
2494
|
const cmdNote = async (token, chatId, cmdArgs, chatState) => {
|
|
@@ -2289,7 +2617,7 @@ async function main() {
|
|
|
2289
2617
|
await sendLongMessage(token, cs,
|
|
2290
2618
|
'🔄 Bot started. Previously connected to `' + dir + '`.\n' +
|
|
2291
2619
|
'⚠️ The server at `' + state.url + '` is not reachable.\n' +
|
|
2292
|
-
'Use `/resurrect` to restart it, or `/
|
|
2620
|
+
'Use `/resurrect` to restart it, or `/server <name>` to connect elsewhere.');
|
|
2293
2621
|
} catch (_) {}
|
|
2294
2622
|
}
|
|
2295
2623
|
}
|
|
@@ -2335,6 +2663,18 @@ async function main() {
|
|
|
2335
2663
|
// the key changes — new messages from newId will hit a new state)
|
|
2336
2664
|
}
|
|
2337
2665
|
|
|
2666
|
+
// Drain stale updates from previous sessions before entering the poll loop.
|
|
2667
|
+
// Using timeout=1 to return immediately without long-polling.
|
|
2668
|
+
try {
|
|
2669
|
+
const drainResult = await getUpdates(token, 0, 1);
|
|
2670
|
+
if (drainResult && drainResult.ok && drainResult.result) {
|
|
2671
|
+
for (const u of drainResult.result) {
|
|
2672
|
+
offset = Math.max(offset, u.update_id + 1);
|
|
2673
|
+
}
|
|
2674
|
+
log.info(`Drained ${drainResult.result.length} stale update(s), offset ${offset}`);
|
|
2675
|
+
}
|
|
2676
|
+
} catch (_) {}
|
|
2677
|
+
|
|
2338
2678
|
// Poll for updates and forward each text message to OpenCode, sending the answer back.
|
|
2339
2679
|
while (true) {
|
|
2340
2680
|
let result;
|
|
@@ -2426,7 +2766,7 @@ async function main() {
|
|
|
2426
2766
|
`Your file upload has been queued (#${qlen}). It will be sent when the server becomes available.\n\n` +
|
|
2427
2767
|
`**Options:**\n` +
|
|
2428
2768
|
`• Restart it: \`/resurrect\`\n` +
|
|
2429
|
-
`• Switch to another server: \`/
|
|
2769
|
+
`• Switch to another server: \`/server <name>\`\n` +
|
|
2430
2770
|
`• Flush the queue: \`/flush\``);
|
|
2431
2771
|
continue;
|
|
2432
2772
|
}
|
|
@@ -2576,11 +2916,11 @@ async function main() {
|
|
|
2576
2916
|
const cmdArgs = parts.slice(1);
|
|
2577
2917
|
const handler = cmds[cmd];
|
|
2578
2918
|
if (handler) {
|
|
2579
|
-
// Disconnected guard:
|
|
2580
|
-
if (!cs.serverBase && cmd !== 'help' && cmd !== 'servers' && cmd !== '
|
|
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') {
|
|
2581
2921
|
await sendLongMessage(token, cs,
|
|
2582
2922
|
'🔌 Not connected to any server.\n\n' +
|
|
2583
|
-
'Use `/servers` to see available servers and `/
|
|
2923
|
+
'Use `/servers` to see available servers and `/server <name>` to connect.');
|
|
2584
2924
|
continue;
|
|
2585
2925
|
}
|
|
2586
2926
|
try {
|
|
@@ -2592,7 +2932,7 @@ async function main() {
|
|
|
2592
2932
|
}
|
|
2593
2933
|
} else {
|
|
2594
2934
|
await sendLongMessage(token, cs,
|
|
2595
|
-
`Unknown command: /${cmd}\n\nAvailable commands: /help, /status, /servers, /sessions, /agents, /models, /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`);
|
|
2596
2936
|
}
|
|
2597
2937
|
continue;
|
|
2598
2938
|
}
|
|
@@ -2601,7 +2941,7 @@ async function main() {
|
|
|
2601
2941
|
if (!cs.serverBase) {
|
|
2602
2942
|
await sendLongMessage(token, cs,
|
|
2603
2943
|
'🔌 Not connected to any server.\n\n' +
|
|
2604
|
-
'Use `/servers` to see available servers and `/
|
|
2944
|
+
'Use `/servers` to see available servers and `/server <name>` to connect.');
|
|
2605
2945
|
continue;
|
|
2606
2946
|
}
|
|
2607
2947
|
|
|
@@ -2628,7 +2968,7 @@ async function main() {
|
|
|
2628
2968
|
const suggestions = [];
|
|
2629
2969
|
suggestions.push(`• Restart it: \`/resurrect\``);
|
|
2630
2970
|
if (servers.length > 0) {
|
|
2631
|
-
suggestions.push(`• Switch to another server: \`/
|
|
2971
|
+
suggestions.push(`• Switch to another server: \`/server <name>\``);
|
|
2632
2972
|
}
|
|
2633
2973
|
suggestions.push(`• Flush the queue: \`/flush\``);
|
|
2634
2974
|
await sendLongMessage(token, cs,
|