agentp 0.11.10 → 0.11.11
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/bin/tgagentp +376 -77
- 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 = {
|
|
@@ -308,9 +308,25 @@ async function cmdHelp(token, chatId, args, chatState) {
|
|
|
308
308
|
}
|
|
309
309
|
if (topic === 'models') {
|
|
310
310
|
await sendLongMessage(token, chatState,
|
|
311
|
-
'`/models` ─ list
|
|
312
|
-
'Shows provider
|
|
313
|
-
'
|
|
311
|
+
'`/models` ─ list connected providers.\n' +
|
|
312
|
+
' • Shows provider names with model count: `**OpenAI** (*5 models*)`\n' +
|
|
313
|
+
' • The current provider is marked with ▶\n' +
|
|
314
|
+
' • Includes hint to drill down: `/models <provider>`\n\n' +
|
|
315
|
+
'`/models <provider>` ─ list all models for a matching provider.\n' +
|
|
316
|
+
' • Partial name/ID matching supported\n' +
|
|
317
|
+
' • Current model marked with ▶\n' +
|
|
318
|
+
' • Shows context limit and input/output costs in parentheses\n\n' +
|
|
319
|
+
'Use `/model <providerID/modelID>` or `/model <modelID>` to switch the active session model.');
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (topic === 'model') {
|
|
323
|
+
await sendLongMessage(token, chatState,
|
|
324
|
+
'`/model <providerID/modelID>` ─ switch the active session to a specific model.\n' +
|
|
325
|
+
'`/model <modelID>` ─ search all connected providers for a model by name.\n\n' +
|
|
326
|
+
'Examples:\n' +
|
|
327
|
+
' `/model opencode/big-pickle` ─ explicit provider/model\n' +
|
|
328
|
+
' `/model gpt-4` ─ search across all providers\n\n' +
|
|
329
|
+
'Use `/models` to see available providers and models.');
|
|
314
330
|
return;
|
|
315
331
|
}
|
|
316
332
|
if (topic === 'allow' || topic === 'reject' || topic === 'always' || topic === 'answer') {
|
|
@@ -888,27 +904,124 @@ async function cmdModels(token, chatId, args, chatState, setServerForChat, setAc
|
|
|
888
904
|
return;
|
|
889
905
|
}
|
|
890
906
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
907
|
+
// Determine current model: session model first (the real in-use model,
|
|
908
|
+
// including manual /model TUI switches), then fall back to the agent's
|
|
909
|
+
// configured default model.
|
|
910
|
+
let currentProviderID = null;
|
|
911
|
+
let currentModelID = null;
|
|
912
|
+
const st = getState(server);
|
|
913
|
+
let activeSid = st.activeSessionId;
|
|
914
|
+
log.info(`[cmdModels] activeSessionId=${activeSid} activeAgentName=${st.activeAgentName}`);
|
|
915
|
+
// Auto-discover active session if not set
|
|
916
|
+
if (!activeSid) {
|
|
917
|
+
try {
|
|
918
|
+
const discovered = await discoverActiveSession(server);
|
|
919
|
+
if (discovered) {
|
|
920
|
+
st.activeSessionId = discovered.id;
|
|
921
|
+
st.activeSessionName = discovered.name;
|
|
922
|
+
activeSid = discovered.id;
|
|
923
|
+
log.info(`[cmdModels] auto-discovered session: ${discovered.name || discovered.id}`);
|
|
924
|
+
}
|
|
925
|
+
} catch (err) {
|
|
926
|
+
log.info(`[cmdModels] auto-discover error: ${err.message}`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (activeSid) {
|
|
930
|
+
try {
|
|
931
|
+
const sessions = await listSessions(server);
|
|
932
|
+
const session = sessions.find(s => String(s.id) === String(activeSid));
|
|
933
|
+
if (session && session.model) {
|
|
934
|
+
const m = session.model;
|
|
935
|
+
log.info(`[cmdModels] session model: ${JSON.stringify(m)}`);
|
|
936
|
+
if (typeof m === 'object' && m.providerID) {
|
|
937
|
+
currentProviderID = m.providerID;
|
|
938
|
+
currentModelID = m.modelID || m.id;
|
|
939
|
+
} else if (typeof m === 'string') {
|
|
940
|
+
const parts = m.split('/');
|
|
941
|
+
if (parts.length === 2) {
|
|
942
|
+
currentProviderID = parts[0];
|
|
943
|
+
currentModelID = parts[1];
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
} else {
|
|
947
|
+
log.info(`[cmdModels] session found=${!!session}, has model=${session && !!session.model}`);
|
|
948
|
+
}
|
|
949
|
+
} catch (err) {
|
|
950
|
+
log.info(`[cmdModels] listSessions error: ${err.message}`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
// Fallback: active agent's configured model
|
|
954
|
+
if (!currentModelID) {
|
|
955
|
+
const activeAgentName = st.activeAgentName;
|
|
956
|
+
if (activeAgentName) {
|
|
957
|
+
try {
|
|
958
|
+
const agents = await listAgents(server);
|
|
959
|
+
const activeAgent = agents.find(a => a.name === activeAgentName);
|
|
960
|
+
if (activeAgent && activeAgent.model) {
|
|
961
|
+
const am = activeAgent.model;
|
|
962
|
+
log.info(`[cmdModels] agent model: ${JSON.stringify(am)}`);
|
|
963
|
+
currentProviderID = am.providerID;
|
|
964
|
+
currentModelID = am.modelID || am.id;
|
|
965
|
+
} else {
|
|
966
|
+
log.info(`[cmdModels] agent found=${!!activeAgent}, has model=${activeAgent && !!activeAgent.model}`);
|
|
967
|
+
}
|
|
968
|
+
} catch (err) {
|
|
969
|
+
log.info(`[cmdModels] listAgents error: ${err.message}`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
log.info(`[cmdModels] result: provider=${currentProviderID} model=${currentModelID}`);
|
|
896
974
|
|
|
975
|
+
// /models <provider> — list all models for a matching provider
|
|
976
|
+
if (args.length > 0) {
|
|
977
|
+
const query = args.join(' ');
|
|
978
|
+
const matches = filtered.filter(p =>
|
|
979
|
+
p.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
980
|
+
p.id.toLowerCase().includes(query.toLowerCase())
|
|
981
|
+
);
|
|
982
|
+
if (matches.length === 0) {
|
|
983
|
+
await sendLongMessage(token, chatState, `No provider matching "${query}".`);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (matches.length > 1) {
|
|
987
|
+
const list = matches.map(p => ' `' + p.name + '`').join('\n');
|
|
988
|
+
await sendLongMessage(token, chatState,
|
|
989
|
+
`Multiple matches for "${query}":\n${list}\n\nPlease be more specific.`);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
const provider = matches[0];
|
|
897
993
|
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);
|
|
994
|
+
if (modelEntries.length === 0) {
|
|
995
|
+
await sendLongMessage(token, chatState,
|
|
996
|
+
'**' + provider.name + '** has no models configured.');
|
|
997
|
+
return;
|
|
905
998
|
}
|
|
906
|
-
|
|
907
|
-
|
|
999
|
+
const lines = ['**' + provider.name + '** models:'];
|
|
1000
|
+
for (const [id, model] of modelEntries) {
|
|
1001
|
+
const isCurrent = currentProviderID === provider.id && currentModelID === id;
|
|
1002
|
+
const bullet = isCurrent ? '\u25b6 ' : '\u00b7 ';
|
|
1003
|
+
const extras = [];
|
|
1004
|
+
if (model.limit) extras.push(Math.round(model.limit.context / 1000) + 'K ctx');
|
|
1005
|
+
if (model.cost) extras.push(`$${model.cost.input}/$${model.cost.output}`);
|
|
1006
|
+
const suffix = extras.length > 0 ? ` (*${extras.join(', ')}*)` : '';
|
|
1007
|
+
lines.push(bullet + '`' + id + '`' + suffix);
|
|
908
1008
|
}
|
|
1009
|
+
await sendLongMessage(token, chatState, lines.join('\n'));
|
|
1010
|
+
return;
|
|
909
1011
|
}
|
|
910
1012
|
|
|
911
|
-
|
|
1013
|
+
// Default: list providers with model count per provider
|
|
1014
|
+
const lines = ['**Connected providers:**'];
|
|
1015
|
+
for (const provider of filtered) {
|
|
1016
|
+
const modelCount = Object.keys(provider.models || {}).length;
|
|
1017
|
+
const isCurrentProvider = currentProviderID === provider.id;
|
|
1018
|
+
const bullet = isCurrentProvider ? '\u25b6 ' : '\u00b7 ';
|
|
1019
|
+
lines.push(bullet + '**' + provider.name + '**' +
|
|
1020
|
+
(modelCount > 0 ? ` (*${modelCount} models*)` : ' *No models*'));
|
|
1021
|
+
}
|
|
1022
|
+
lines.push('');
|
|
1023
|
+
lines.push('\u{1f4a1} Use `/models <provider>` to list the models it provides.');
|
|
1024
|
+
await sendLongMessage(token, chatState, lines.join('\n'));
|
|
912
1025
|
} catch (err) {
|
|
913
1026
|
await sendLongMessage(token, chatState, `Error listing models: ${err.message}`);
|
|
914
1027
|
}
|
|
@@ -1614,6 +1727,115 @@ async function cmdNew(token, chatId, args, chatState, setServerForChat, setActiv
|
|
|
1614
1727
|
log.info(`[chat ${chatId}] Created ${targetDir} → ${url}`);
|
|
1615
1728
|
}
|
|
1616
1729
|
|
|
1730
|
+
async function cmdModel(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
1731
|
+
const server = chatState.serverBase;
|
|
1732
|
+
const st = getState(server);
|
|
1733
|
+
// Auto-discover active session if not set
|
|
1734
|
+
if (!st.activeSessionId) {
|
|
1735
|
+
try {
|
|
1736
|
+
const discovered = await discoverActiveSession(server);
|
|
1737
|
+
if (discovered) {
|
|
1738
|
+
st.activeSessionId = discovered.id;
|
|
1739
|
+
st.activeSessionName = discovered.name;
|
|
1740
|
+
}
|
|
1741
|
+
} catch (err) {
|
|
1742
|
+
log.info(`[cmdModel] auto-discover error: ${err.message}`);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
if (args.length === 0) {
|
|
1746
|
+
await sendLongMessage(token, chatState,
|
|
1747
|
+
'Usage: `/model <providerID/modelID>` or `/model <modelID>` to search.\n' +
|
|
1748
|
+
'Example: `/model opencode/big-pickle` or `/model gpt-4`');
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
if (!st.activeSessionId) {
|
|
1752
|
+
await sendLongMessage(token, chatState, 'No active session. Use `/sessions` to select one.');
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
const query = args[0];
|
|
1756
|
+
let providerID, modelID;
|
|
1757
|
+
|
|
1758
|
+
// Try "providerID/modelID" format first
|
|
1759
|
+
if (query.includes('/')) {
|
|
1760
|
+
const parts = query.split('/');
|
|
1761
|
+
const rawProviderID = parts[0];
|
|
1762
|
+
modelID = parts[1];
|
|
1763
|
+
providerID = rawProviderID;
|
|
1764
|
+
|
|
1765
|
+
// Validate provider ID exists, or try partial match (e.g. "go" → "opencode-go")
|
|
1766
|
+
try {
|
|
1767
|
+
const result = await listProviders(server);
|
|
1768
|
+
const all = result.all || [];
|
|
1769
|
+
const connectedIds = result.connected || [];
|
|
1770
|
+
const connected = all.filter(p => connectedIds.includes(p.id));
|
|
1771
|
+
const exists = connected.some(p => p.id === rawProviderID);
|
|
1772
|
+
if (!exists) {
|
|
1773
|
+
const match = connected.find(p =>
|
|
1774
|
+
(p.name && p.name.toLowerCase().includes(rawProviderID.toLowerCase())) ||
|
|
1775
|
+
(p.id && p.id.toLowerCase().includes(rawProviderID.toLowerCase()))
|
|
1776
|
+
);
|
|
1777
|
+
if (match) {
|
|
1778
|
+
providerID = match.id;
|
|
1779
|
+
log.info(`[cmdModel] resolved "${rawProviderID}" → "${match.id}"`);
|
|
1780
|
+
} else {
|
|
1781
|
+
await sendLongMessage(token, chatState,
|
|
1782
|
+
`Provider "${rawProviderID}" not found. Use \`/models\` to list available providers.`);
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
// Verify model exists under this provider
|
|
1787
|
+
const provider = all.find(p => p.id === providerID);
|
|
1788
|
+
if (!provider || !provider.models || !provider.models[modelID]) {
|
|
1789
|
+
const availableModels = provider && provider.models ? Object.keys(provider.models).join(', ') : 'unknown';
|
|
1790
|
+
log.info(`[cmdModel] model "${modelID}" not found in ${providerID}, available: ${availableModels}`);
|
|
1791
|
+
await sendLongMessage(token, chatState,
|
|
1792
|
+
`Model "${modelID}" not found under provider \`${providerID}\`. Use \`/models\` to list available models.`);
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
} catch (err) {
|
|
1796
|
+
log.info(`[cmdModel] provider validation error: ${err.message}`);
|
|
1797
|
+
// Continue with what we have — may still work
|
|
1798
|
+
}
|
|
1799
|
+
} else {
|
|
1800
|
+
// Search across all connected providers to resolve to providerID/modelID
|
|
1801
|
+
try {
|
|
1802
|
+
const result = await listProviders(server);
|
|
1803
|
+
const all = result.all || [];
|
|
1804
|
+
const connectedIds = result.connected || [];
|
|
1805
|
+
const connected = all.filter(p => connectedIds.includes(p.id));
|
|
1806
|
+
for (const provider of connected) {
|
|
1807
|
+
const models = Object.keys(provider.models || {});
|
|
1808
|
+
const match = models.find(m => m.toLowerCase() === query.toLowerCase());
|
|
1809
|
+
if (match) {
|
|
1810
|
+
providerID = provider.id;
|
|
1811
|
+
modelID = match;
|
|
1812
|
+
break;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
} catch (err) {
|
|
1816
|
+
log.info(`[cmdModel] listProviders error: ${err.message}`);
|
|
1817
|
+
}
|
|
1818
|
+
if (!modelID) {
|
|
1819
|
+
await sendLongMessage(token, chatState,
|
|
1820
|
+
`No model matching "${query}" found in any connected provider. Use \`/models\` to list available models.`);
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// Send an async no-reply message with the desired model.
|
|
1826
|
+
// This triggers createUserMessage which fires ModelSwitched when the model differs,
|
|
1827
|
+
// persisting it as the session's current model.
|
|
1828
|
+
try {
|
|
1829
|
+
await sendToSessionAsync(server, st.activeSessionId, '.', null, { providerID, modelID }, true);
|
|
1830
|
+
const label = providerID ? `\`${providerID}/${modelID}\`` : `\`${modelID}\``;
|
|
1831
|
+
await sendLongMessage(token, chatState,
|
|
1832
|
+
`✅ Switched to model ${label}.`);
|
|
1833
|
+
log.info(`[chat ${chatId}] Switched to model ${providerID || '?'}/${modelID}`);
|
|
1834
|
+
} catch (err) {
|
|
1835
|
+
await sendLongMessage(token, chatState, `Error switching model: ${err.message}`);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1617
1839
|
async function cmdMarkdown(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat, replyTo, replyToMsgId) {
|
|
1618
1840
|
let entry = null;
|
|
1619
1841
|
if (replyToMsgId) {
|
|
@@ -1642,6 +1864,7 @@ const COMMANDS = {
|
|
|
1642
1864
|
'session': cmdSessions,
|
|
1643
1865
|
'agents': cmdAgents,
|
|
1644
1866
|
'models': cmdModels,
|
|
1867
|
+
'model': cmdModel,
|
|
1645
1868
|
'think': cmdThink,
|
|
1646
1869
|
'record': cmdRecord,
|
|
1647
1870
|
'resurrect': cmdResurrect,
|
|
@@ -1851,6 +2074,7 @@ async function main() {
|
|
|
1851
2074
|
if (srvDiscovered) {
|
|
1852
2075
|
getState(url).activeSessionId = srvDiscovered.id;
|
|
1853
2076
|
getState(url).activeSessionName = srvDiscovered.name;
|
|
2077
|
+
startSessionMonitor(url, srvDiscovered.id);
|
|
1854
2078
|
log.info(` → session: ${srvDiscovered.name || srvDiscovered.id}`);
|
|
1855
2079
|
}
|
|
1856
2080
|
const defaultSrvAgent = srvAgents.find(a => a.mode === 'primary') || srvAgents[0];
|
|
@@ -1872,7 +2096,10 @@ async function main() {
|
|
|
1872
2096
|
getState(server).activeSessionId = id;
|
|
1873
2097
|
if (name) getState(server).activeSessionName = name;
|
|
1874
2098
|
log.info(`[chat ${chatState.chatId}] Switched to session: ${name || id}`);
|
|
1875
|
-
if (id !== prevId)
|
|
2099
|
+
if (id !== prevId) {
|
|
2100
|
+
sendGreeting(chatState);
|
|
2101
|
+
startSessionMonitor(server, id);
|
|
2102
|
+
}
|
|
1876
2103
|
};
|
|
1877
2104
|
|
|
1878
2105
|
const TELEGRAM_GREETING_BODY = (sessionName) =>
|
|
@@ -1891,6 +2118,90 @@ async function main() {
|
|
|
1891
2118
|
'For more details about a command:\n' +
|
|
1892
2119
|
'[Telegram]{"command":"help","topic":"<upload|download|help>"}';
|
|
1893
2120
|
|
|
2121
|
+
/**
|
|
2122
|
+
* Start a persistent SSE session monitor that catches question.asked,
|
|
2123
|
+
* permission_asked, and permission_replied events in the background,
|
|
2124
|
+
* even when the AI was triggered by agentp (--tg) rather than by tgagentp.
|
|
2125
|
+
* Automatically restarted when the session or server changes.
|
|
2126
|
+
*/
|
|
2127
|
+
function startSessionMonitor(server, sessionId) {
|
|
2128
|
+
const st = getState(server);
|
|
2129
|
+
if (st._monitorRef && st._monitorRef.current) {
|
|
2130
|
+
st._monitorRef.current.destroy();
|
|
2131
|
+
st._monitorRef.current = null;
|
|
2132
|
+
}
|
|
2133
|
+
const monRef = { current: null };
|
|
2134
|
+
st._monitorRef = monRef;
|
|
2135
|
+
st._monitorStarted = true;
|
|
2136
|
+
|
|
2137
|
+
const notifiedPerms = new Set();
|
|
2138
|
+
|
|
2139
|
+
listenForSessionEvents(server, sessionId, {
|
|
2140
|
+
onText: null,
|
|
2141
|
+
onConnected: null,
|
|
2142
|
+
onThinking: null,
|
|
2143
|
+
onPermission: (perm) => {
|
|
2144
|
+
const st2 = getState(server);
|
|
2145
|
+
const permDump = JSON.stringify(perm);
|
|
2146
|
+
log.debug(` [monitor] -> permission_asked callback: ${permDump}`);
|
|
2147
|
+
st2.pendingPermission = { server, sessionId, permissionId: perm.id, type: perm.permission || 'unspecified', pattern: perm.patterns || perm.pattern, title: perm.title };
|
|
2148
|
+
const ownerChat = Object.entries(serverOwners).find(([url]) => normalizeUrl(url) === normalizeUrl(server))?.[1];
|
|
2149
|
+
if (ownerChat && !notifiedPerms.has(perm.id)) {
|
|
2150
|
+
notifiedPerms.add(perm.id);
|
|
2151
|
+
const toolStr = perm.permission || 'unspecified';
|
|
2152
|
+
const patternArr = perm.patterns || (perm.pattern ? [perm.pattern] : []);
|
|
2153
|
+
const patternStr = patternArr.join(', ');
|
|
2154
|
+
const msg = '\u{1f510}**Permission needed**\n'
|
|
2155
|
+
+ '**Tool:** `' + toolStr + '`\n'
|
|
2156
|
+
+ (patternStr ? '**Target:** `' + patternStr + '`\n' : '')
|
|
2157
|
+
+ (perm.title ? '**Detail:** ' + perm.title + '\n' : '')
|
|
2158
|
+
+ 'Use `/allow` to approve once, `/always` to always allow, or `/reject` to deny.';
|
|
2159
|
+
log.debug(` [monitor] -> sending permission notification to chat ${ownerChat.chatId}`);
|
|
2160
|
+
sendLongMessage(token, { chatId: ownerChat.chatId, threadId: ownerChat.threadId }, msg)
|
|
2161
|
+
.then(() => log.debug(` [monitor] -> permission notification sent OK`))
|
|
2162
|
+
.catch((err) => log.debug(` [monitor] -> permission notification failed: ${err.message}`));
|
|
2163
|
+
} else if (ownerChat) {
|
|
2164
|
+
log.debug(` [monitor] -> permission ${perm.id} already notified, updating pendingPermission only`);
|
|
2165
|
+
} else {
|
|
2166
|
+
log.debug(` [monitor] -> no owner chat found for server ${server}, skipping notification`);
|
|
2167
|
+
}
|
|
2168
|
+
},
|
|
2169
|
+
onPermissionReplied: () => {
|
|
2170
|
+
log.debug(` [monitor] -> permission_replied: clearing pendingPermission`);
|
|
2171
|
+
getState(server).pendingPermission = null;
|
|
2172
|
+
},
|
|
2173
|
+
onQuestion: (q) => {
|
|
2174
|
+
const st2 = getState(server);
|
|
2175
|
+
const qDump = JSON.stringify(q);
|
|
2176
|
+
log.debug(` [monitor] -> question_asked callback: ${qDump}`);
|
|
2177
|
+
st2.pendingQuestion = { server, sessionId, questionId: q.id, question: q.question || 'Unknown question', options: q.options || [] };
|
|
2178
|
+
const ownerChat = Object.entries(serverOwners).find(([url]) => normalizeUrl(url) === normalizeUrl(server))?.[1];
|
|
2179
|
+
if (ownerChat) {
|
|
2180
|
+
const options = (q.options || []).map((opt, i) => `${i + 1}. ${opt.text || opt}`).join('\n');
|
|
2181
|
+
const msg = '\u2753**Question from the AI**\n\n'
|
|
2182
|
+
+ (q.question || 'Choose an option:') + '\n\n'
|
|
2183
|
+
+ (options ? options + '\n\n' : '')
|
|
2184
|
+
+ 'Use `/answer <number>` to respond.';
|
|
2185
|
+
log.debug(` [monitor] -> sending question notification to chat ${ownerChat.chatId}`);
|
|
2186
|
+
sendLongMessage(token, { chatId: ownerChat.chatId, threadId: ownerChat.threadId }, msg)
|
|
2187
|
+
.then(() => log.debug(` [monitor] -> question notification sent OK`))
|
|
2188
|
+
.catch((err) => log.debug(` [monitor] -> question notification failed: ${err.message}`));
|
|
2189
|
+
} else {
|
|
2190
|
+
log.debug(` [monitor] -> no owner chat found for server ${server}, skipping question notification`);
|
|
2191
|
+
}
|
|
2192
|
+
},
|
|
2193
|
+
}, monRef).then(() => {
|
|
2194
|
+
// Normal completion (90s safety timeout or stream ended)
|
|
2195
|
+
}, (err) => {
|
|
2196
|
+
log.debug(` [monitor] SSE listener for session ${sessionId} ended: ${err.message}`);
|
|
2197
|
+
}).finally(() => {
|
|
2198
|
+
if (getState(server)._monitorRef === monRef) {
|
|
2199
|
+
getState(server)._monitorRef = null;
|
|
2200
|
+
getState(server)._monitorStarted = false;
|
|
2201
|
+
}
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
|
|
1894
2205
|
async function sendGreeting(chatState) {
|
|
1895
2206
|
const st = getState(chatState.serverBase);
|
|
1896
2207
|
if (!st.activeSessionId) {
|
|
@@ -1928,10 +2239,17 @@ async function main() {
|
|
|
1928
2239
|
if (st._sessionReq && st._sessionReq.current) { st._sessionReq.current.destroy(); st._sessionReq.current = null; }
|
|
1929
2240
|
st._cancelRef = cancelRef;
|
|
1930
2241
|
st._sessionReq = sessionCancelRef;
|
|
2242
|
+
|
|
2243
|
+
// Ensure the persistent session monitor is running so question/permission
|
|
2244
|
+
// events from any source (agentp, tgagentp, TUI) are caught, not just
|
|
2245
|
+
// those triggered by this message's per-request SSE listener.
|
|
2246
|
+
if (useSession && sessionId && !st._monitorStarted) {
|
|
2247
|
+
startSessionMonitor(url, sessionId);
|
|
2248
|
+
}
|
|
2249
|
+
|
|
1931
2250
|
try {
|
|
1932
2251
|
let answer;
|
|
1933
2252
|
if (useSession) {
|
|
1934
|
-
const notifiedPerms = new Set();
|
|
1935
2253
|
let sseConnected;
|
|
1936
2254
|
const onConnected = Promise.race([
|
|
1937
2255
|
new Promise((resolve) => { sseConnected = resolve; }),
|
|
@@ -1953,48 +2271,9 @@ async function main() {
|
|
|
1953
2271
|
.catch(() => {});
|
|
1954
2272
|
}
|
|
1955
2273
|
},
|
|
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
|
-
},
|
|
2274
|
+
// onPermission, onPermissionReplied, and onQuestion are handled by
|
|
2275
|
+
// the persistent session monitor (startSessionMonitor) so they are
|
|
2276
|
+
// caught even when agentp (--tg) triggers the AI response.
|
|
1998
2277
|
}, cancelRef).catch(() => {});
|
|
1999
2278
|
await onConnected;
|
|
2000
2279
|
answer = await sendToSession(url, sessionId, `[Telegram] ${text}`, st.activeAgentName, sessionCancelRef);
|
|
@@ -2003,8 +2282,8 @@ async function main() {
|
|
|
2003
2282
|
sendLongMessage(token, chatState, html, replyTo, threadId).catch(() => {});
|
|
2004
2283
|
st._thinkingBuf = '';
|
|
2005
2284
|
}
|
|
2006
|
-
// Keep the SSE listener alive
|
|
2007
|
-
//
|
|
2285
|
+
// Keep the per-request SSE listener alive for streaming text events.
|
|
2286
|
+
// Questions and permissions are handled by the persistent monitor.
|
|
2008
2287
|
} else {
|
|
2009
2288
|
await sendText(url, `[Telegram] ${text}`);
|
|
2010
2289
|
answer = await listenForFinalAnswer(url, null, cancelRef);
|
|
@@ -2143,24 +2422,32 @@ async function main() {
|
|
|
2143
2422
|
// Handle /shutdown clear — wipe saved connections before exiting
|
|
2144
2423
|
if (args && args.length > 0 && args[0].toLowerCase() === 'clear') {
|
|
2145
2424
|
clearConnections();
|
|
2146
|
-
await sendLongMessage(token, cs, '
|
|
2147
|
-
log.
|
|
2148
|
-
try { await getUpdates(token, offset, 1); } catch (_) {}
|
|
2425
|
+
await sendLongMessage(token, cs, '\u{1f9f9} Connection state cleared.').catch(() => {});
|
|
2426
|
+
log.info('/shutdown clear: exiting');
|
|
2149
2427
|
process.exit(0);
|
|
2150
2428
|
}
|
|
2151
2429
|
|
|
2152
|
-
|
|
2430
|
+
// Parse numeric exit code from args (default 0). Uses regex /^\d+$/
|
|
2431
|
+
// to robustly match only pure-digit arguments.
|
|
2432
|
+
let exitCode = 0;
|
|
2433
|
+
for (const arg of (args || [])) {
|
|
2434
|
+
if (/^\d+$/.test(arg)) {
|
|
2435
|
+
exitCode = parseInt(arg, 10);
|
|
2436
|
+
break;
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
const isForce = args && args.some(a => a.toLowerCase() === 'force');
|
|
2153
2441
|
if (st.busy && !isForce) {
|
|
2154
2442
|
await sendLongMessage(token, cs,
|
|
2155
|
-
'
|
|
2156
|
-
'Use `/shutdown force` to shut down anyway (in-flight response will be lost).');
|
|
2443
|
+
'\u23f3 Server is busy processing a message.\n' +
|
|
2444
|
+
'Use `/shutdown force` to shut down anyway (in-flight response will be lost).').catch(() => {});
|
|
2157
2445
|
return;
|
|
2158
2446
|
}
|
|
2159
|
-
const status = st.busy ? '
|
|
2160
|
-
await sendLongMessage(token, cs,
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
process.exit(0);
|
|
2447
|
+
const status = st.busy ? '\u23f3 busy (forced)' : '\u2705 idle';
|
|
2448
|
+
await sendLongMessage(token, cs, `\u{1f44b} Shutting down (state: ${status}, exit ${exitCode})...`).catch(() => {});
|
|
2449
|
+
log.info(`/shutdown: exiting with code ${exitCode}`);
|
|
2450
|
+
process.exit(exitCode);
|
|
2164
2451
|
};
|
|
2165
2452
|
|
|
2166
2453
|
const cmdNote = async (token, chatId, cmdArgs, chatState) => {
|
|
@@ -2335,6 +2622,18 @@ async function main() {
|
|
|
2335
2622
|
// the key changes — new messages from newId will hit a new state)
|
|
2336
2623
|
}
|
|
2337
2624
|
|
|
2625
|
+
// Drain stale updates from previous sessions before entering the poll loop.
|
|
2626
|
+
// Using timeout=1 to return immediately without long-polling.
|
|
2627
|
+
try {
|
|
2628
|
+
const drainResult = await getUpdates(token, 0, 1);
|
|
2629
|
+
if (drainResult && drainResult.ok && drainResult.result) {
|
|
2630
|
+
for (const u of drainResult.result) {
|
|
2631
|
+
offset = Math.max(offset, u.update_id + 1);
|
|
2632
|
+
}
|
|
2633
|
+
log.info(`Drained ${drainResult.result.length} stale update(s), offset ${offset}`);
|
|
2634
|
+
}
|
|
2635
|
+
} catch (_) {}
|
|
2636
|
+
|
|
2338
2637
|
// Poll for updates and forward each text message to OpenCode, sending the answer back.
|
|
2339
2638
|
while (true) {
|
|
2340
2639
|
let result;
|
|
@@ -2592,7 +2891,7 @@ async function main() {
|
|
|
2592
2891
|
}
|
|
2593
2892
|
} else {
|
|
2594
2893
|
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`);
|
|
2894
|
+
`Unknown command: /${cmd}\n\nAvailable commands: /help, /status, /servers, /sessions, /agents, /models, /model, /think, /record, /queue, /flush, /note, /comment, /disconnect, /serve, /new, /markdown`);
|
|
2596
2895
|
}
|
|
2597
2896
|
continue;
|
|
2598
2897
|
}
|
package/lib/opencode.js
CHANGED
|
@@ -217,11 +217,17 @@ async function createSession(server, title) {
|
|
|
217
217
|
return JSON.parse(responseBody);
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
// Update a session's properties (e.g. title).
|
|
221
|
-
|
|
220
|
+
// Update a session's properties (e.g. title, agent, model).
|
|
221
|
+
// model can be a string "providerID/modelID" or an object { providerID, modelID }.
|
|
222
|
+
async function updateSession(server, sessionId, title, agent, model) {
|
|
222
223
|
const bodyObj = {};
|
|
223
224
|
if (title != null) bodyObj.title = title;
|
|
224
225
|
if (agent != null) bodyObj.agent = agent;
|
|
226
|
+
if (model != null) {
|
|
227
|
+
bodyObj.model = typeof model === 'string'
|
|
228
|
+
? model
|
|
229
|
+
: { providerID: model.providerID, id: model.modelID || model.id };
|
|
230
|
+
}
|
|
225
231
|
const url = `${server}/session/${encodeURIComponent(sessionId)}`;
|
|
226
232
|
const body = JSON.stringify(bodyObj);
|
|
227
233
|
const { status, body: responseBody } = await makeRequest(buildJsonRequest(url, 'PATCH', body), body);
|
|
@@ -297,10 +303,13 @@ async function listProviders(server) {
|
|
|
297
303
|
}
|
|
298
304
|
|
|
299
305
|
// Send a message asynchronously (non-blocking). Returns 204 No Content.
|
|
300
|
-
|
|
306
|
+
// Optionally specify a model and/or noReply (injects context without AI response).
|
|
307
|
+
async function sendToSessionAsync(server, sessionId, text, agent, model, noReply) {
|
|
301
308
|
const url = `${server}/session/${encodeURIComponent(sessionId)}/prompt_async`;
|
|
302
309
|
const bodyObj = { parts: [{ type: 'text', text }] };
|
|
303
310
|
if (agent) bodyObj.agent = agent;
|
|
311
|
+
if (model) bodyObj.model = typeof model === 'string' ? model : { providerID: model.providerID, modelID: model.modelID };
|
|
312
|
+
if (noReply) bodyObj.noReply = true;
|
|
304
313
|
const body = JSON.stringify(bodyObj);
|
|
305
314
|
const { status } = await makeRequest(buildJsonRequest(url, 'POST', body), body);
|
|
306
315
|
if (status !== 204) throw new Error(`Failed to send async message: ${status}`);
|