@bd7pil/ocrc 0.9.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/LICENSE +34 -0
- package/README.md +330 -0
- package/dist/cli/host-backends.js +216 -0
- package/dist/cli/host-backends.js.map +1 -0
- package/dist/cli/host.js +185 -0
- package/dist/cli/host.js.map +1 -0
- package/dist/cli/index.js +67 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.js +68 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/install.js +220 -0
- package/dist/cli/install.js.map +1 -0
- package/dist/cli/pair.js +11 -0
- package/dist/cli/pair.js.map +1 -0
- package/dist/cli/uninstall.js +75 -0
- package/dist/cli/uninstall.js.map +1 -0
- package/dist/connectivity/auth/cf-access.js +20 -0
- package/dist/connectivity/auth/cf-access.js.map +1 -0
- package/dist/connectivity/auth/index.js +2 -0
- package/dist/connectivity/auth/index.js.map +1 -0
- package/dist/connectivity/auth/select.js +19 -0
- package/dist/connectivity/auth/select.js.map +1 -0
- package/dist/connectivity/auth/token.js +157 -0
- package/dist/connectivity/auth/token.js.map +1 -0
- package/dist/connectivity/exposure/cloudflared.js +77 -0
- package/dist/connectivity/exposure/cloudflared.js.map +1 -0
- package/dist/connectivity/exposure/index.js +2 -0
- package/dist/connectivity/exposure/index.js.map +1 -0
- package/dist/connectivity/exposure/providers.js +49 -0
- package/dist/connectivity/exposure/providers.js.map +1 -0
- package/dist/connectivity/pairing.js +29 -0
- package/dist/connectivity/pairing.js.map +1 -0
- package/dist/core/agent/acp-backend.js +539 -0
- package/dist/core/agent/acp-backend.js.map +1 -0
- package/dist/core/agent/acp-connect.js +92 -0
- package/dist/core/agent/acp-connect.js.map +1 -0
- package/dist/core/agent/acp-normalizer.js +81 -0
- package/dist/core/agent/acp-normalizer.js.map +1 -0
- package/dist/core/agent/acp-replay.js +89 -0
- package/dist/core/agent/acp-replay.js.map +1 -0
- package/dist/core/agent/acp-store.js +129 -0
- package/dist/core/agent/acp-store.js.map +1 -0
- package/dist/core/agent/backend.js +2 -0
- package/dist/core/agent/backend.js.map +1 -0
- package/dist/core/agent/diff-util.js +70 -0
- package/dist/core/agent/diff-util.js.map +1 -0
- package/dist/core/agent/event.js +12 -0
- package/dist/core/agent/event.js.map +1 -0
- package/dist/core/agent/opencode-backend.js +598 -0
- package/dist/core/agent/opencode-backend.js.map +1 -0
- package/dist/core/agent/opencode-normalizer.js +69 -0
- package/dist/core/agent/opencode-normalizer.js.map +1 -0
- package/dist/core/agent/registry.js +55 -0
- package/dist/core/agent/registry.js.map +1 -0
- package/dist/core/agent/v2-backend.js +176 -0
- package/dist/core/agent/v2-backend.js.map +1 -0
- package/dist/core/card-bus.js +74 -0
- package/dist/core/card-bus.js.map +1 -0
- package/dist/core/channels.js +95 -0
- package/dist/core/channels.js.map +1 -0
- package/dist/core/history.js +91 -0
- package/dist/core/history.js.map +1 -0
- package/dist/core/list-files.js +50 -0
- package/dist/core/list-files.js.map +1 -0
- package/dist/core/opencode-events.js +29 -0
- package/dist/core/opencode-events.js.map +1 -0
- package/dist/core/primary-election.js +91 -0
- package/dist/core/primary-election.js.map +1 -0
- package/dist/core/push.js +152 -0
- package/dist/core/push.js.map +1 -0
- package/dist/core/relay.js +400 -0
- package/dist/core/relay.js.map +1 -0
- package/dist/core/scheduler.js +150 -0
- package/dist/core/scheduler.js.map +1 -0
- package/dist/core/state.js +222 -0
- package/dist/core/state.js.map +1 -0
- package/dist/core/stream-accumulator.js +76 -0
- package/dist/core/stream-accumulator.js.map +1 -0
- package/dist/core/structured-card.js +2 -0
- package/dist/core/structured-card.js.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/opencode/global-events.js +57 -0
- package/dist/opencode/global-events.js.map +1 -0
- package/dist/opencode/list-sessions.js +122 -0
- package/dist/opencode/list-sessions.js.map +1 -0
- package/dist/opencode/submit.js +39 -0
- package/dist/opencode/submit.js.map +1 -0
- package/dist/opencode/workspaces.js +51 -0
- package/dist/opencode/workspaces.js.map +1 -0
- package/dist/plugin/config.js +128 -0
- package/dist/plugin/config.js.map +1 -0
- package/dist/plugin/control-plane.js +133 -0
- package/dist/plugin/control-plane.js.map +1 -0
- package/dist/plugin/entry.js +409 -0
- package/dist/plugin/entry.js.map +1 -0
- package/dist/plugin/v2/event-map.js +193 -0
- package/dist/plugin/v2/event-map.js.map +1 -0
- package/dist/plugin/v2/types.js +15 -0
- package/dist/plugin/v2/types.js.map +1 -0
- package/dist/transport/interface.js +2 -0
- package/dist/transport/interface.js.map +1 -0
- package/dist/transport/telegram/esc.js +9 -0
- package/dist/transport/telegram/esc.js.map +1 -0
- package/dist/transport/telegram/handlers/info-commands.js +101 -0
- package/dist/transport/telegram/handlers/info-commands.js.map +1 -0
- package/dist/transport/telegram/handlers.js +1348 -0
- package/dist/transport/telegram/handlers.js.map +1 -0
- package/dist/transport/telegram/i18n/en.js +538 -0
- package/dist/transport/telegram/i18n/en.js.map +1 -0
- package/dist/transport/telegram/i18n/index.js +83 -0
- package/dist/transport/telegram/i18n/index.js.map +1 -0
- package/dist/transport/telegram/i18n/zh.js +538 -0
- package/dist/transport/telegram/i18n/zh.js.map +1 -0
- package/dist/transport/telegram/index.js +792 -0
- package/dist/transport/telegram/index.js.map +1 -0
- package/dist/transport/telegram/main-keyboard.js +36 -0
- package/dist/transport/telegram/main-keyboard.js.map +1 -0
- package/dist/transport/telegram/managers/abort-suppression-manager.js +33 -0
- package/dist/transport/telegram/managers/abort-suppression-manager.js.map +1 -0
- package/dist/transport/telegram/managers/external-input-suppression-manager.js +51 -0
- package/dist/transport/telegram/managers/external-input-suppression-manager.js.map +1 -0
- package/dist/transport/telegram/managers/interaction-manager.js +254 -0
- package/dist/transport/telegram/managers/interaction-manager.js.map +1 -0
- package/dist/transport/telegram/managers/permission-manager.js +242 -0
- package/dist/transport/telegram/managers/permission-manager.js.map +1 -0
- package/dist/transport/telegram/managers/question-manager.js +191 -0
- package/dist/transport/telegram/managers/question-manager.js.map +1 -0
- package/dist/transport/telegram/managers/rename-manager.js +56 -0
- package/dist/transport/telegram/managers/rename-manager.js.map +1 -0
- package/dist/transport/telegram/menus.js +77 -0
- package/dist/transport/telegram/menus.js.map +1 -0
- package/dist/transport/telegram/permission-flow.js +156 -0
- package/dist/transport/telegram/permission-flow.js.map +1 -0
- package/dist/transport/telegram/question-flow.js +224 -0
- package/dist/transport/telegram/question-flow.js.map +1 -0
- package/dist/transport/telegram/render/block-parser.js +365 -0
- package/dist/transport/telegram/render/block-parser.js.map +1 -0
- package/dist/transport/telegram/render/block-plain-text.js +87 -0
- package/dist/transport/telegram/render/block-plain-text.js.map +1 -0
- package/dist/transport/telegram/render/block-splitter.js +152 -0
- package/dist/transport/telegram/render/block-splitter.js.map +1 -0
- package/dist/transport/telegram/render/chunker.js +71 -0
- package/dist/transport/telegram/render/chunker.js.map +1 -0
- package/dist/transport/telegram/render/limits.js +13 -0
- package/dist/transport/telegram/render/limits.js.map +1 -0
- package/dist/transport/telegram/render/markdown-normalizer.js +102 -0
- package/dist/transport/telegram/render/markdown-normalizer.js.map +1 -0
- package/dist/transport/telegram/render/markdown-to-telegram-v2.js +116 -0
- package/dist/transport/telegram/render/markdown-to-telegram-v2.js.map +1 -0
- package/dist/transport/telegram/render/part-signature.js +14 -0
- package/dist/transport/telegram/render/part-signature.js.map +1 -0
- package/dist/transport/telegram/render/pipeline.js +23 -0
- package/dist/transport/telegram/render/pipeline.js.map +1 -0
- package/dist/transport/telegram/render/rich-blocks.js +311 -0
- package/dist/transport/telegram/render/rich-blocks.js.map +1 -0
- package/dist/transport/telegram/render/text-splitter.js +64 -0
- package/dist/transport/telegram/render/text-splitter.js.map +1 -0
- package/dist/transport/telegram/render/types.js +2 -0
- package/dist/transport/telegram/render/types.js.map +1 -0
- package/dist/transport/telegram/renderer.js +305 -0
- package/dist/transport/telegram/renderer.js.map +1 -0
- package/dist/transport/telegram/streaming/duration-formatter.js +68 -0
- package/dist/transport/telegram/streaming/duration-formatter.js.map +1 -0
- package/dist/transport/telegram/streaming/finalize-assistant-response.js +37 -0
- package/dist/transport/telegram/streaming/finalize-assistant-response.js.map +1 -0
- package/dist/transport/telegram/streaming/response-streamer.js +400 -0
- package/dist/transport/telegram/streaming/response-streamer.js.map +1 -0
- package/dist/transport/telegram/streaming/running-tool-tracker.js +150 -0
- package/dist/transport/telegram/streaming/running-tool-tracker.js.map +1 -0
- package/dist/transport/telegram/streaming/stream-throttle.js +57 -0
- package/dist/transport/telegram/streaming/stream-throttle.js.map +1 -0
- package/dist/transport/telegram/streaming/tool-call-streamer.js +390 -0
- package/dist/transport/telegram/streaming/tool-call-streamer.js.map +1 -0
- package/dist/transport/telegram/streaming-render.js +182 -0
- package/dist/transport/telegram/streaming-render.js.map +1 -0
- package/dist/transport/telegram/types/interaction.js +2 -0
- package/dist/transport/telegram/types/interaction.js.map +1 -0
- package/dist/transport/telegram/types/model.js +25 -0
- package/dist/transport/telegram/types/model.js.map +1 -0
- package/dist/transport/telegram/types/permission.js +2 -0
- package/dist/transport/telegram/types/permission.js.map +1 -0
- package/dist/transport/telegram/types/question.js +2 -0
- package/dist/transport/telegram/types/question.js.map +1 -0
- package/dist/transport/telegram/types/rename.js +2 -0
- package/dist/transport/telegram/types/rename.js.map +1 -0
- package/dist/transport/telegram/types/scheduled-task.js +23 -0
- package/dist/transport/telegram/types/scheduled-task.js.map +1 -0
- package/dist/transport/telegram/types/session.js +2 -0
- package/dist/transport/telegram/types/session.js.map +1 -0
- package/dist/transport/telegram/ui.js +28 -0
- package/dist/transport/telegram/ui.js.map +1 -0
- package/dist/transport/web/index.js +148 -0
- package/dist/transport/web/index.js.map +1 -0
- package/dist/transport/web/middleware/cf-access.js +104 -0
- package/dist/transport/web/middleware/cf-access.js.map +1 -0
- package/dist/transport/web/opencode-config.js +15 -0
- package/dist/transport/web/opencode-config.js.map +1 -0
- package/dist/transport/web/routes/abort.js +12 -0
- package/dist/transport/web/routes/abort.js.map +1 -0
- package/dist/transport/web/routes/approval.js +16 -0
- package/dist/transport/web/routes/approval.js.map +1 -0
- package/dist/transport/web/routes/catalog.js +11 -0
- package/dist/transport/web/routes/catalog.js.map +1 -0
- package/dist/transport/web/routes/channels.js +53 -0
- package/dist/transport/web/routes/channels.js.map +1 -0
- package/dist/transport/web/routes/commands.js +15 -0
- package/dist/transport/web/routes/commands.js.map +1 -0
- package/dist/transport/web/routes/context.js +17 -0
- package/dist/transport/web/routes/context.js.map +1 -0
- package/dist/transport/web/routes/controls.js +34 -0
- package/dist/transport/web/routes/controls.js.map +1 -0
- package/dist/transport/web/routes/create-session.js +21 -0
- package/dist/transport/web/routes/create-session.js.map +1 -0
- package/dist/transport/web/routes/diff.js +7 -0
- package/dist/transport/web/routes/diff.js.map +1 -0
- package/dist/transport/web/routes/files.js +22 -0
- package/dist/transport/web/routes/files.js.map +1 -0
- package/dist/transport/web/routes/logs.js +9 -0
- package/dist/transport/web/routes/logs.js.map +1 -0
- package/dist/transport/web/routes/m8.js +69 -0
- package/dist/transport/web/routes/m8.js.map +1 -0
- package/dist/transport/web/routes/mcp.js +7 -0
- package/dist/transport/web/routes/mcp.js.map +1 -0
- package/dist/transport/web/routes/message.js +32 -0
- package/dist/transport/web/routes/message.js.map +1 -0
- package/dist/transport/web/routes/overrides.js +20 -0
- package/dist/transport/web/routes/overrides.js.map +1 -0
- package/dist/transport/web/routes/question.js +32 -0
- package/dist/transport/web/routes/question.js.map +1 -0
- package/dist/transport/web/routes/rename.js +12 -0
- package/dist/transport/web/routes/rename.js.map +1 -0
- package/dist/transport/web/routes/schedules.js +28 -0
- package/dist/transport/web/routes/schedules.js.map +1 -0
- package/dist/transport/web/routes/session.js +20 -0
- package/dist/transport/web/routes/session.js.map +1 -0
- package/dist/transport/web/routes/sessions.js +25 -0
- package/dist/transport/web/routes/sessions.js.map +1 -0
- package/dist/transport/web/routes/subagents.js +11 -0
- package/dist/transport/web/routes/subagents.js.map +1 -0
- package/dist/transport/web/routes/suggestions.js +7 -0
- package/dist/transport/web/routes/suggestions.js.map +1 -0
- package/dist/transport/web/routes/todo.js +7 -0
- package/dist/transport/web/routes/todo.js.map +1 -0
- package/dist/transport/web/routes/version.js +5 -0
- package/dist/transport/web/routes/version.js.map +1 -0
- package/dist/transport/web/routes/workspaces.js +7 -0
- package/dist/transport/web/routes/workspaces.js.map +1 -0
- package/dist/transport/web/server.js +111 -0
- package/dist/transport/web/server.js.map +1 -0
- package/dist/transport/web/session-summary.js +46 -0
- package/dist/transport/web/session-summary.js.map +1 -0
- package/dist/transport/web/ws-hub.js +83 -0
- package/dist/transport/web/ws-hub.js.map +1 -0
- package/dist/utils/logger.js +105 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/markdown.js +129 -0
- package/dist/utils/markdown.js.map +1 -0
- package/dist/utils/paths.js +18 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/version.js +44 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +91 -0
- package/web/dist/_app/env.js +1 -0
- package/web/dist/_app/immutable/assets/0.DxN3RsuG.css +1 -0
- package/web/dist/_app/immutable/assets/2.BNuGfmZP.css +1 -0
- package/web/dist/_app/immutable/assets/3.CTDprntu.css +1 -0
- package/web/dist/_app/immutable/chunks/BUpwHpMJ.js +1 -0
- package/web/dist/_app/immutable/chunks/BjY4dxcB.js +2 -0
- package/web/dist/_app/immutable/chunks/C2FuqD3s.js +1 -0
- package/web/dist/_app/immutable/chunks/CUHGq08X.js +2 -0
- package/web/dist/_app/immutable/chunks/CWj6FrbW.js +1 -0
- package/web/dist/_app/immutable/chunks/ClvoqtGj.js +1 -0
- package/web/dist/_app/immutable/chunks/DIEDQ_hk.js +1 -0
- package/web/dist/_app/immutable/entry/app.BoRpcVMj.js +2 -0
- package/web/dist/_app/immutable/entry/start.Cj-PQNTq.js +1 -0
- package/web/dist/_app/immutable/nodes/0.y9YEms5p.js +3 -0
- package/web/dist/_app/immutable/nodes/1.CPhhiRM2.js +1 -0
- package/web/dist/_app/immutable/nodes/2.B_c84Mc9.js +1 -0
- package/web/dist/_app/immutable/nodes/3.ClTd0J3D.js +64 -0
- package/web/dist/_app/version.json +1 -0
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/favicon.png +0 -0
- package/web/dist/favicon.svg +17 -0
- package/web/dist/icon-192.png +0 -0
- package/web/dist/icon-512-maskable.png +0 -0
- package/web/dist/icon-512.png +0 -0
- package/web/dist/index.html +73 -0
- package/web/dist/manifest.webmanifest +28 -0
- package/web/dist/service-worker.js +1 -0
|
@@ -0,0 +1,1348 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { InlineKeyboard } from 'grammy';
|
|
3
|
+
import { inlineKeyboard, btn } from './ui.js';
|
|
4
|
+
import { createLogger } from '../../utils/logger.js';
|
|
5
|
+
import { getVersionInfo } from '../../utils/version.js';
|
|
6
|
+
import { registerInfoCommands } from './handlers/info-commands.js';
|
|
7
|
+
import { esc } from './esc.js';
|
|
8
|
+
const log = createLogger('handlers');
|
|
9
|
+
/** M9 ๅทฅไฝๅบ่ฎฟ้ฎ่ๅด: filter the workspace list per the tg-default bot scope. */
|
|
10
|
+
function scopeWorkspaces(deps, ws) {
|
|
11
|
+
const scope = deps.channels?.()?.workspaces;
|
|
12
|
+
if (!scope || scope.mode === 'all')
|
|
13
|
+
return ws;
|
|
14
|
+
const allowed = new Set(scope.dirs);
|
|
15
|
+
return ws.filter((w) => allowed.has(w.directory));
|
|
16
|
+
}
|
|
17
|
+
/** M9: is the given workspace allowed for the tg-default bot? */
|
|
18
|
+
function workspaceAllowed(deps, dir) {
|
|
19
|
+
const scope = deps.channels?.()?.workspaces;
|
|
20
|
+
if (!scope || scope.mode === 'all')
|
|
21
|
+
return true;
|
|
22
|
+
return scope.dirs.includes(dir);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parse a "providerID/modelID" string into the shape expected by state/relay.
|
|
26
|
+
* Returns undefined if the string lacks a slash. Preserves slashes inside modelID.
|
|
27
|
+
*/
|
|
28
|
+
function parseAgentModel(modelStr) {
|
|
29
|
+
const idx = modelStr.indexOf('/');
|
|
30
|
+
if (idx <= 0 || idx === modelStr.length - 1)
|
|
31
|
+
return undefined;
|
|
32
|
+
return {
|
|
33
|
+
providerID: modelStr.slice(0, idx),
|
|
34
|
+
modelID: modelStr.slice(idx + 1),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function shortPath(p) {
|
|
38
|
+
const cwd = process.cwd();
|
|
39
|
+
if (p.startsWith(cwd + '/'))
|
|
40
|
+
return p.slice(cwd.length + 1);
|
|
41
|
+
if (p.startsWith('/')) {
|
|
42
|
+
const parts = p.split('/');
|
|
43
|
+
if (parts.length > 3)
|
|
44
|
+
return 'โฆ/' + parts.slice(-3).join('/');
|
|
45
|
+
}
|
|
46
|
+
return p;
|
|
47
|
+
}
|
|
48
|
+
async function buildStatusCard(deps) {
|
|
49
|
+
const healthy = await deps.backend.ping();
|
|
50
|
+
let busyCount = 0;
|
|
51
|
+
let totalSessions = 0;
|
|
52
|
+
let totalCost = 0;
|
|
53
|
+
try {
|
|
54
|
+
const data = (await deps.backend.getSessionsStatus());
|
|
55
|
+
totalSessions = Object.keys(data).length;
|
|
56
|
+
busyCount = Object.values(data).filter((s) => s.type === 'busy').length;
|
|
57
|
+
for (const sid of Object.keys(data)) {
|
|
58
|
+
const c = deps.state.getSessionCost(sid);
|
|
59
|
+
if (c !== undefined)
|
|
60
|
+
totalCost += c;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
try {
|
|
65
|
+
totalSessions = (await deps.backend.listSessions()).length;
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
}
|
|
69
|
+
const pinnedSession = deps.state.getPinnedSessionId();
|
|
70
|
+
const lastSession = deps.state.getLastSessionId();
|
|
71
|
+
const tuiSession = deps.state.getTuiSelectedSession();
|
|
72
|
+
const effectiveSession = pinnedSession ?? lastSession;
|
|
73
|
+
const currentAgent = deps.state.getCurrentAgent();
|
|
74
|
+
const nextAgent = deps.state.getNextAgent();
|
|
75
|
+
const nextModel = deps.state.getNextModel();
|
|
76
|
+
const row = (label, value) => ` <b>${label}</b> ${value}`;
|
|
77
|
+
const sessionLabel = pinnedSession ? 'Bot ๐' : 'Bot';
|
|
78
|
+
const lines = [
|
|
79
|
+
`${healthy ? '๐ข' : '๐ด'} <b>opencode</b> ยท ${healthy ? 'healthy' : 'unreachable'}`,
|
|
80
|
+
'',
|
|
81
|
+
row('Sessions', `${totalSessions} ยท ${busyCount} busy`),
|
|
82
|
+
...(totalCost > 0 ? [row('Cost', `$${totalCost.toFixed(2)} today`)] : []),
|
|
83
|
+
...(effectiveSession
|
|
84
|
+
? [row(sessionLabel, `<code>โฆ${effectiveSession.slice(-8)}</code>${currentAgent ? ` ยท ${currentAgent}` : ''}`)]
|
|
85
|
+
: []),
|
|
86
|
+
...(tuiSession && tuiSession !== effectiveSession
|
|
87
|
+
? [row('TUI', `<code>โฆ${tuiSession.slice(-8)}</code>`)]
|
|
88
|
+
: []),
|
|
89
|
+
...((nextAgent || nextModel)
|
|
90
|
+
? ['',
|
|
91
|
+
` Next โบ ${nextAgent ? `<b>${nextAgent}</b>` : 'โ'} ยท ${nextModel ? `<code>${nextModel.modelID}</code>` : 'โ'}`]
|
|
92
|
+
: []),
|
|
93
|
+
];
|
|
94
|
+
const buttons = [
|
|
95
|
+
btn('๐ Refresh', 'status:refresh'),
|
|
96
|
+
];
|
|
97
|
+
if (deps.isGenerating()) {
|
|
98
|
+
buttons.push(btn('โน Stop', 'status:abort'));
|
|
99
|
+
}
|
|
100
|
+
return { lines, buttons };
|
|
101
|
+
}
|
|
102
|
+
export function registerHandlers(deps) {
|
|
103
|
+
// Maps a short token -> workspace directory, so callback_data stays under
|
|
104
|
+
// Telegram's 64-byte limit. Stable per directory (sha1-derived).
|
|
105
|
+
const wsTokens = new Map();
|
|
106
|
+
const wsToken = (dir) => {
|
|
107
|
+
const t = createHash('sha1').update(dir).digest('base64url').slice(0, 16);
|
|
108
|
+
wsTokens.set(t, dir);
|
|
109
|
+
return t;
|
|
110
|
+
};
|
|
111
|
+
// Same token-map pattern for model callbacks: a "providerID:modelID" pair
|
|
112
|
+
// (e.g. openrouter's vendor/model slugs) easily exceeds Telegram's 64-byte
|
|
113
|
+
// callback_data limit, which fails the whole keyboard with BUTTON_DATA_INVALID.
|
|
114
|
+
const modelTokens = new Map();
|
|
115
|
+
const modelToken = (providerID, modelID) => {
|
|
116
|
+
const t = createHash('sha1').update(`model:${providerID}/${modelID}`).digest('base64url').slice(0, 16);
|
|
117
|
+
modelTokens.set(t, `${providerID}/${modelID}`);
|
|
118
|
+
return t;
|
|
119
|
+
};
|
|
120
|
+
// โโ Commands โโ
|
|
121
|
+
deps.bot.command('start', async (ctx) => {
|
|
122
|
+
const healthy = await deps.backend.ping();
|
|
123
|
+
const username = ctx.from?.first_name ?? 'there';
|
|
124
|
+
const nextAgent = deps.state.getNextAgent();
|
|
125
|
+
const nextModel = deps.state.getNextModel();
|
|
126
|
+
const lines = [
|
|
127
|
+
`๐ <b>Hi ${username}</b>`,
|
|
128
|
+
'',
|
|
129
|
+
`opencode ${healthy ? '๐ข ready' : '๐ด unreachable'}`,
|
|
130
|
+
'Send any message to relay it into opencode.',
|
|
131
|
+
'',
|
|
132
|
+
'<b>Commands</b>',
|
|
133
|
+
' /status Health + current session',
|
|
134
|
+
' /sessions List all sessions',
|
|
135
|
+
' /agent Set next-message agent',
|
|
136
|
+
' /model Set next-message model',
|
|
137
|
+
' /pair Pair a new device',
|
|
138
|
+
' /files Files touched this session',
|
|
139
|
+
' /diff Pending git diff',
|
|
140
|
+
' /todo Session todo list',
|
|
141
|
+
' /context Tokens + cost + model',
|
|
142
|
+
' /abort Stop generation',
|
|
143
|
+
' /workspaces List/switch workspaces',
|
|
144
|
+
' /new New session in active workspace',
|
|
145
|
+
];
|
|
146
|
+
if (nextAgent || nextModel) {
|
|
147
|
+
lines.push('');
|
|
148
|
+
if (nextAgent)
|
|
149
|
+
lines.push(`<i>Next agent: ${nextAgent}</i>`);
|
|
150
|
+
if (nextModel)
|
|
151
|
+
lines.push(`<i>Next model: ${nextModel.modelID}</i>`);
|
|
152
|
+
}
|
|
153
|
+
await ctx.reply(lines.join('\n'), {
|
|
154
|
+
parse_mode: 'HTML',
|
|
155
|
+
...inlineKeyboard([
|
|
156
|
+
[btn('๐ Status', 'status:refresh')],
|
|
157
|
+
]),
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
deps.bot.command('status', async (ctx) => {
|
|
161
|
+
const { lines, buttons } = await buildStatusCard(deps);
|
|
162
|
+
await ctx.reply(lines.join('\n'), {
|
|
163
|
+
parse_mode: 'HTML',
|
|
164
|
+
...inlineKeyboard([buttons]),
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
deps.bot.command('sessions', async (ctx) => {
|
|
168
|
+
try {
|
|
169
|
+
const sessions = (await deps.backend.listSessionSummaries()).map(s => ({
|
|
170
|
+
...s,
|
|
171
|
+
when: s.lastActiveAt
|
|
172
|
+
? new Date(s.lastActiveAt).toLocaleString('en-US', {
|
|
173
|
+
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
|
|
174
|
+
})
|
|
175
|
+
: 'unknown',
|
|
176
|
+
}));
|
|
177
|
+
if (sessions.length === 0) {
|
|
178
|
+
await ctx.reply('No sessions.', { parse_mode: 'HTML' });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const pinned = deps.state.getLastSessionId();
|
|
182
|
+
const lines = ['<b>๐ Sessions</b>', ''];
|
|
183
|
+
for (let i = 0; i < sessions.length; i++) {
|
|
184
|
+
const s = sessions[i];
|
|
185
|
+
const pinEmoji = s.id === pinned ? '๐ ' : '';
|
|
186
|
+
lines.push(`${i + 1}. ${pinEmoji}<code>โฆ${s.id.slice(-8)}</code>`);
|
|
187
|
+
lines.push(` ${esc(s.title ?? 'Untitled')} ยท ${s.when}`);
|
|
188
|
+
if (s.directory)
|
|
189
|
+
lines.push(` ๐ ${esc(s.directory.split('/').pop() || s.directory)}`);
|
|
190
|
+
lines.push('');
|
|
191
|
+
}
|
|
192
|
+
if (pinned) {
|
|
193
|
+
lines.push(`<i>๐ Pinned: โฆ${pinned.slice(-8)}</i>`);
|
|
194
|
+
}
|
|
195
|
+
const rows = sessions.map(s => [
|
|
196
|
+
btn(`๐ Pin ${s.id.slice(-6)}`, `session:pin:${s.id}`),
|
|
197
|
+
]);
|
|
198
|
+
await ctx.reply(lines.join('\n'), {
|
|
199
|
+
parse_mode: 'HTML',
|
|
200
|
+
...inlineKeyboard(rows),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
log.error('failed to list sessions', err);
|
|
205
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
deps.bot.command('session', async (ctx) => {
|
|
209
|
+
const text = ctx.message && 'text' in ctx.message ? ctx.message.text : '';
|
|
210
|
+
const args = text ? text.split(' ').slice(1)[0]?.trim() : undefined;
|
|
211
|
+
if (args && args.length > 0) {
|
|
212
|
+
const sid = deps.state.normalizeSessionId(args);
|
|
213
|
+
deps.state.setPinnedSessionId(sid);
|
|
214
|
+
await ctx.reply(`<b>๐ Pinned</b>\n\n<code>${esc(sid)}</code>`, {
|
|
215
|
+
parse_mode: 'HTML',
|
|
216
|
+
...inlineKeyboard([
|
|
217
|
+
[btn('Unpin', 'session:unpin')],
|
|
218
|
+
]),
|
|
219
|
+
});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const pinned = deps.state.getPinnedSessionId();
|
|
223
|
+
if (!pinned) {
|
|
224
|
+
await ctx.reply('No session pinned. Use <code>/session <id></code> or pick from /sessions.', { parse_mode: 'HTML' });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
await ctx.reply(`<b>๐ Pinned session</b>\n\n<code>${pinned}</code>`, {
|
|
228
|
+
parse_mode: 'HTML',
|
|
229
|
+
...inlineKeyboard([
|
|
230
|
+
[btn('Unpin', 'session:unpin')],
|
|
231
|
+
]),
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
deps.bot.command('files', async (ctx) => {
|
|
235
|
+
const last = deps.state.getLastSessionId();
|
|
236
|
+
if (!last) {
|
|
237
|
+
await ctx.reply('<b>๐ Files</b>\n\nNo session yet. Send a message first.', { parse_mode: 'HTML' });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const cards = await deps.backend.getHistory(last, 0);
|
|
242
|
+
const fileEmoji = {
|
|
243
|
+
read: '๐', write: '๐', edit: 'โ๏ธ',
|
|
244
|
+
};
|
|
245
|
+
const fileOps = new Map();
|
|
246
|
+
for (const card of cards) {
|
|
247
|
+
if (card.kind !== 'assistant')
|
|
248
|
+
continue;
|
|
249
|
+
for (const block of card.blocks) {
|
|
250
|
+
if (block.type === 'tool' && fileEmoji[block.tool] && block.args) {
|
|
251
|
+
fileOps.set(block.args, fileEmoji[block.tool]);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const shortId = last.slice(-8);
|
|
256
|
+
if (fileOps.size === 0) {
|
|
257
|
+
await ctx.reply(`<b>๐ Files โ โฆ${shortId}</b>\n\nNo file operations recorded.`, { parse_mode: 'HTML' });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const MAX = 15;
|
|
261
|
+
const entries = [...fileOps.entries()];
|
|
262
|
+
const shown = entries.slice(0, MAX);
|
|
263
|
+
const lines = [
|
|
264
|
+
`<b>๐ Files โ โฆ${shortId}</b>`,
|
|
265
|
+
'',
|
|
266
|
+
...shown.map(([p, emoji]) => `${emoji} <code>${shortPath(p)}</code>`),
|
|
267
|
+
];
|
|
268
|
+
if (entries.length > MAX) {
|
|
269
|
+
lines.push(`\nโฆand ${entries.length - MAX} more`);
|
|
270
|
+
}
|
|
271
|
+
lines.push('', `<i>${entries.length} file operation${entries.length > 1 ? 's' : ''}</i>`);
|
|
272
|
+
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
log.error('failed to fetch files', err);
|
|
276
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
deps.bot.command('agent', async (ctx) => {
|
|
280
|
+
try {
|
|
281
|
+
const agents = await deps.backend.getAgents(deps.opencodeProject);
|
|
282
|
+
if (agents.length === 0) {
|
|
283
|
+
await ctx.reply('๐ค <b>Agent</b>\n\nNo agents configured. Add them in <code>opencode.jsonc</code>.', { parse_mode: 'HTML' });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const nextAgent = deps.state.getNextAgent();
|
|
287
|
+
const lines = ['๐ค <b>Agent</b>', ''];
|
|
288
|
+
for (const a of agents) {
|
|
289
|
+
const active = a.name === nextAgent;
|
|
290
|
+
const modelShort = a.model.split('/').pop() ?? a.model;
|
|
291
|
+
const marker = active ? 'โ' : ' ';
|
|
292
|
+
const name = active ? `<b>${a.name}</b>` : a.name;
|
|
293
|
+
const desc = a.description ? ` <i>${a.description}</i>` : '';
|
|
294
|
+
lines.push(`${marker} ${name} <code>${modelShort}</code>${desc}`);
|
|
295
|
+
}
|
|
296
|
+
if (nextAgent)
|
|
297
|
+
lines.push('', `<i>Active override: ${nextAgent}</i>`);
|
|
298
|
+
const rows = agents.map(a => [btn(a.name, `agent:set:${a.name}`)]);
|
|
299
|
+
rows.push([btn('โ Clear override', 'agent:clear')]);
|
|
300
|
+
await ctx.reply(lines.join('\n'), {
|
|
301
|
+
parse_mode: 'HTML',
|
|
302
|
+
...inlineKeyboard(rows),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
log.error('failed to list agents', err);
|
|
307
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
deps.bot.command('model', async (ctx) => {
|
|
311
|
+
try {
|
|
312
|
+
const providers = await deps.backend.getModels(deps.opencodeProject);
|
|
313
|
+
if (providers.length === 0) {
|
|
314
|
+
await ctx.reply('<b>โ๏ธ Model</b>\n\nNo models configured.', { parse_mode: 'HTML' });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const nextModel = deps.state.getNextModel();
|
|
318
|
+
const lines = ['<b>โ๏ธ Model โ Select provider</b>', ''];
|
|
319
|
+
const rows = [];
|
|
320
|
+
for (const p of providers) {
|
|
321
|
+
const count = (p.models ?? []).length;
|
|
322
|
+
const hasSelected = nextModel?.providerID === p.id;
|
|
323
|
+
const marker = hasSelected ? 'โ' : 'โธ';
|
|
324
|
+
lines.push(`${marker} <b>${p.name}</b> ยท ${count} model${count !== 1 ? 's' : ''}`);
|
|
325
|
+
rows.push([btn(p.name, `model:pick:${p.id}`)]);
|
|
326
|
+
}
|
|
327
|
+
if (nextModel) {
|
|
328
|
+
lines.push('', `<i>Current override: ${nextModel.providerID}/${nextModel.modelID}</i>`);
|
|
329
|
+
}
|
|
330
|
+
rows.push([btn('โ Clear', 'model:clear')]);
|
|
331
|
+
await ctx.reply(lines.join('\n'), {
|
|
332
|
+
parse_mode: 'HTML',
|
|
333
|
+
...inlineKeyboard(rows),
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
log.error('failed to list models', err);
|
|
338
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
deps.bot.command('current', async (ctx) => {
|
|
342
|
+
const last = deps.state.getLastSessionId();
|
|
343
|
+
if (!last) {
|
|
344
|
+
await ctx.reply('<b>๐ No session</b>\n\nSend a message to start one.', { parse_mode: 'HTML' });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
await ctx.reply(`<b>๐ Current session</b>\n\n<code>${last}</code>`, {
|
|
348
|
+
parse_mode: 'HTML',
|
|
349
|
+
...inlineKeyboard([
|
|
350
|
+
[btn('Unpin', 'session:unpin')],
|
|
351
|
+
]),
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
deps.bot.command('abort', async (ctx) => {
|
|
355
|
+
const sid = deps.abortGeneration();
|
|
356
|
+
if (!sid) {
|
|
357
|
+
await ctx.reply('No session to abort.', { parse_mode: 'HTML' });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
await deps.backend.abort(sid);
|
|
362
|
+
await ctx.reply(`<b>๐ Aborted</b>\n\n<code>โฆ${sid.slice(-8)}</code>`, { parse_mode: 'HTML' });
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
await ctx.reply(`โ Abort failed: ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
deps.bot.command('version', async (ctx) => {
|
|
369
|
+
const { version, commit, uptime, node } = getVersionInfo();
|
|
370
|
+
await ctx.reply([
|
|
371
|
+
`<b>opencode-remote-control</b> v${version}`,
|
|
372
|
+
`Commit: <code>${commit}</code>`,
|
|
373
|
+
`Uptime: ${uptime}`,
|
|
374
|
+
`Node: ${node}`,
|
|
375
|
+
].join('\n'), { parse_mode: 'HTML' });
|
|
376
|
+
});
|
|
377
|
+
deps.bot.command('pair', async (ctx) => {
|
|
378
|
+
try {
|
|
379
|
+
const { buildPairContext, buildPairUrl } = await import('../../connectivity/pairing.js');
|
|
380
|
+
const { token, url } = await buildPairContext();
|
|
381
|
+
const pairUrl = buildPairUrl(url, token);
|
|
382
|
+
// The token travels in the URL fragment. Sending it over Telegram is
|
|
383
|
+
// acceptable: the user already trusts this bot channel for control.
|
|
384
|
+
await ctx.reply(`๐ <b>Pair a device</b>\n\nOpen this once on the device:\n<code>${pairUrl}</code>`, { parse_mode: 'HTML' });
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
deps.bot.command('help', async (ctx) => {
|
|
391
|
+
await ctx.reply([
|
|
392
|
+
'<b>๐ค opencode-remote-control</b>',
|
|
393
|
+
'',
|
|
394
|
+
'<b>Commands:</b>',
|
|
395
|
+
' /start Handshake + health',
|
|
396
|
+
' /status Server health + session',
|
|
397
|
+
' /sessions List all sessions',
|
|
398
|
+
' /session Pin a session',
|
|
399
|
+
' /pair Pair a device (URL + token)',
|
|
400
|
+
' /files Files touched in last session',
|
|
401
|
+
' /diff Pending git diff',
|
|
402
|
+
' /todo Session todo list',
|
|
403
|
+
' /context Tokens + cost + model',
|
|
404
|
+
' /agent Set next agent',
|
|
405
|
+
' /model Set next model',
|
|
406
|
+
' /current Last session used',
|
|
407
|
+
' /abort Stop generation',
|
|
408
|
+
' /version Bot version + uptime',
|
|
409
|
+
' /workspaces List/switch workspaces',
|
|
410
|
+
' /new New session in active workspace',
|
|
411
|
+
' /help This message',
|
|
412
|
+
'',
|
|
413
|
+
'Send any text to relay it into opencode.',
|
|
414
|
+
].join('\n'), {
|
|
415
|
+
parse_mode: 'HTML',
|
|
416
|
+
...inlineKeyboard([
|
|
417
|
+
[btn('๐ Check status', 'status:refresh')],
|
|
418
|
+
]),
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
const workspacesHandler = async (ctx) => {
|
|
422
|
+
try {
|
|
423
|
+
const ws = scopeWorkspaces(deps, await deps.backend.listWorkspaces());
|
|
424
|
+
if (ws.length === 0) {
|
|
425
|
+
await ctx.reply('ๅฝๅๅทฅไฝๅบ่ๅด่ฎพ็ฝฎไธๆฒกๆๅฏ็จ็ๅทฅไฝๅบ๏ผM9 ๅทฅไฝๅบ่ฎฟ้ฎ่ๅด๏ผใ', { parse_mode: 'HTML' });
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const active = deps.state.getActiveWorkspace();
|
|
429
|
+
const lines = ['<b>๐ Workspaces</b>', ''];
|
|
430
|
+
for (const w of ws.slice(0, 20)) {
|
|
431
|
+
const mark = w.directory === active ? '๐ ' : '';
|
|
432
|
+
lines.push(`${mark}<b>${w.name}</b> ยท ${w.sessionCount} session${w.sessionCount === 1 ? '' : 's'}`);
|
|
433
|
+
lines.push(` <code>${w.directory}</code>`);
|
|
434
|
+
}
|
|
435
|
+
const rows = ws.slice(0, 20).map((w) => [btn(`๐ ${w.name}`, `ws:set:${wsToken(w.directory)}`)]);
|
|
436
|
+
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML', ...inlineKeyboard(rows) });
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
deps.bot.command('workspaces', workspacesHandler);
|
|
443
|
+
// grinev ๅฝไปคๅๅฏน้ฝ๏ผ/projects = /workspaces
|
|
444
|
+
deps.bot.command('projects', workspacesHandler);
|
|
445
|
+
deps.bot.callbackQuery(/^ws:set:(.+)$/, async (ctx) => {
|
|
446
|
+
const dir = wsTokens.get(ctx.match[1]);
|
|
447
|
+
if (!dir) {
|
|
448
|
+
await ctx.answerCallbackQuery('Stale โ re-run /workspaces');
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
deps.state.setActiveWorkspace(dir);
|
|
452
|
+
await ctx.answerCallbackQuery(`Workspace โ ${dir.split('/').pop()}`);
|
|
453
|
+
try {
|
|
454
|
+
await ctx.editMessageText(`๐ <b>Active workspace</b>\n\n<code>${dir}</code>\n\nUse /new to start a session here.`, { parse_mode: 'HTML' });
|
|
455
|
+
}
|
|
456
|
+
catch { /* ignore */ }
|
|
457
|
+
});
|
|
458
|
+
deps.bot.command('workspaces', workspacesHandler);
|
|
459
|
+
// grinev ๅฝไปคๅๅฏน้ฝ๏ผ/projects = /workspaces
|
|
460
|
+
deps.bot.command('projects', workspacesHandler);
|
|
461
|
+
// grinev ๅฏน้ฝ๏ผ/detach = ๅๆถ่ท้ๅฝๅไผ่ฏ๏ผ็ญไปท unpin๏ผ
|
|
462
|
+
deps.bot.command('detach', async (ctx) => {
|
|
463
|
+
const pinned = deps.state.getPinnedSessionId();
|
|
464
|
+
deps.state.setPinnedSessionId(undefined);
|
|
465
|
+
await ctx.reply(pinned ? `๐ ๅทฒๅๆถ่ท้ โฆ${pinned.slice(-8)}` : 'โน๏ธ ๅฝๅๆฒกๆ่ท้็ไผ่ฏ');
|
|
466
|
+
});
|
|
467
|
+
// grinev ๅฏน้ฝ๏ผ/commands = ๅๅบ opencode ่ชๅฎไนๅฝไปค
|
|
468
|
+
deps.bot.command('commands', async (ctx) => {
|
|
469
|
+
try {
|
|
470
|
+
const cmds = await deps.backend.listCommands();
|
|
471
|
+
if (cmds.length === 0) {
|
|
472
|
+
await ctx.reply('ๆฒกๆๅทฒ้
็ฝฎ็่ชๅฎไนๅฝไปคใ');
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const lines = ['<b>โ ่ชๅฎไนๅฝไปค</b>'];
|
|
476
|
+
for (const c of cmds.slice(0, 20))
|
|
477
|
+
lines.push(`โข <b>/${esc(c.name)}</b> โ ${esc(c.description ?? '')}`);
|
|
478
|
+
if (cmds.length > 20)
|
|
479
|
+
lines.push(`โฆ ๅ
ฑ ${cmds.length} ๆก`);
|
|
480
|
+
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
481
|
+
}
|
|
482
|
+
catch (err) {
|
|
483
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
// grinev ๅฏน้ฝ๏ผ/mcps = MCP ๆๅกๅจ็ถๆ
|
|
487
|
+
deps.bot.command('mcps', async (ctx) => {
|
|
488
|
+
try {
|
|
489
|
+
const mcps = await deps.backend.getMcp(deps.opencodeProject);
|
|
490
|
+
if (mcps.length === 0) {
|
|
491
|
+
await ctx.reply('ๆช้
็ฝฎ MCP ๆๅกๅจใ');
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const lines = ['<b>๐ MCP</b>'];
|
|
495
|
+
for (const m of mcps)
|
|
496
|
+
lines.push(`โข <b>${esc(m.name)}</b> โ ${m.status === 'configured' ? 'โ
ๅทฒ้
็ฝฎ' : 'โช ๆชๅฏ็จ'}`);
|
|
497
|
+
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
498
|
+
}
|
|
499
|
+
catch (err) {
|
|
500
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
// grinev ๅฏน้ฝ๏ผ/messages = ๅฝๅไผ่ฏๆ่ฟๆถๆฏ
|
|
504
|
+
deps.bot.command('messages', async (ctx) => {
|
|
505
|
+
try {
|
|
506
|
+
const sid = deps.state.getPinnedSessionId() ?? deps.state.getLastSessionId();
|
|
507
|
+
if (!sid) {
|
|
508
|
+
await ctx.reply('ๆฒกๆๆดปๅจไผ่ฏใ');
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const cards = await deps.backend.getHistory(sid, 6);
|
|
512
|
+
const lines = ['<b>๐ ๆ่ฟๆถๆฏ</b>'];
|
|
513
|
+
for (const c of cards) {
|
|
514
|
+
if (c.kind === 'user') {
|
|
515
|
+
lines.push(`๐ง ${esc(c.text?.slice(0, 80) ?? '')}`);
|
|
516
|
+
}
|
|
517
|
+
else if (c.kind === 'assistant') {
|
|
518
|
+
for (const b of c.blocks ?? []) {
|
|
519
|
+
if (b.type === 'text' && b.text) {
|
|
520
|
+
lines.push(`๐ค ${esc(b.text.slice(0, 80))}`);
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
527
|
+
}
|
|
528
|
+
catch (err) {
|
|
529
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
// โโ P2c parity: subagent visibility (web plan-card C1) โโ
|
|
533
|
+
deps.bot.command('subs', async (ctx) => {
|
|
534
|
+
try {
|
|
535
|
+
const sid = deps.state.getPinnedSessionId() ?? deps.state.getLastSessionId();
|
|
536
|
+
if (!sid) {
|
|
537
|
+
await ctx.reply('ๆฒกๆๆดปๅจไผ่ฏใ');
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const subs = await deps.backend.getSubagents?.(sid);
|
|
541
|
+
if (!subs) {
|
|
542
|
+
await ctx.reply('ๅฝๅๅ็ซฏไธๆฏๆๅญไปฃ็ๆฅ่ฏขใ');
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (subs.length === 0) {
|
|
546
|
+
await ctx.reply('่ฏฅไผ่ฏๆฒกๆๅญไปฃ็ใ');
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const lines = [`<b>๐งฉ ๅญไปฃ็ ยท ${subs.length}</b>`];
|
|
550
|
+
const kb = new InlineKeyboard();
|
|
551
|
+
for (const s of subs.slice(0, 8)) {
|
|
552
|
+
const prog = s.total > 0 ? ` ยท ${s.done}/${s.total}` : '';
|
|
553
|
+
lines.push(`โข <b>${esc(s.title || 'โฆ' + s.id.slice(-6))}</b>${prog}`);
|
|
554
|
+
try {
|
|
555
|
+
const { buildPairContext, buildPairUrl } = await import('../../connectivity/pairing.js');
|
|
556
|
+
const { token, url } = await buildPairContext();
|
|
557
|
+
kb.url(`๐ค ๆๅผ ${s.title.slice(0, 16) || 'โฆ' + s.id.slice(-6)}`, buildPairUrl(url, token).replace(/#.*$/, '') + `/${s.id}`).row();
|
|
558
|
+
}
|
|
559
|
+
catch { /* link build is best-effort */ }
|
|
560
|
+
}
|
|
561
|
+
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML', reply_markup: kb });
|
|
562
|
+
}
|
|
563
|
+
catch (err) {
|
|
564
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
// โโ P2c parity: session-level mode switch (web SessionControls C5) โโ
|
|
568
|
+
const modeTokens = new Map();
|
|
569
|
+
let modeSeq = 0;
|
|
570
|
+
deps.bot.command('mode', async (ctx) => {
|
|
571
|
+
try {
|
|
572
|
+
const sid = deps.state.getPinnedSessionId() ?? deps.state.getLastSessionId();
|
|
573
|
+
if (!sid) {
|
|
574
|
+
await ctx.reply('ๆฒกๆๆดปๅจไผ่ฏใ');
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const controls = await deps.backend.getControls?.(sid);
|
|
578
|
+
const options = controls?.mode?.options ?? [];
|
|
579
|
+
if (options.length === 0) {
|
|
580
|
+
await ctx.reply('ๅฝๅๅ็ซฏๆฒกๆๅฏๅๆข็ modeใ');
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const kb = new InlineKeyboard();
|
|
584
|
+
for (const o of options) {
|
|
585
|
+
const tok = ++modeSeq;
|
|
586
|
+
modeTokens.set(tok, { sid, modeId: o.id });
|
|
587
|
+
kb.text(`${o.id === controls?.mode?.current ? '๐ ' : ''}${o.name || o.id}`, `tmode:${tok}`).row();
|
|
588
|
+
}
|
|
589
|
+
await ctx.reply('<b>๐ ไผ่ฏๆจกๅผ</b>', { parse_mode: 'HTML', reply_markup: kb });
|
|
590
|
+
}
|
|
591
|
+
catch (err) {
|
|
592
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
deps.bot.callbackQuery(/^tmode:(\d+)$/, async (ctx) => {
|
|
596
|
+
const t = modeTokens.get(Number(ctx.match[1]));
|
|
597
|
+
if (!t) {
|
|
598
|
+
await ctx.answerCallbackQuery('ๅทฒ่ฟๆ โ ้ๆฐ /mode');
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
try {
|
|
602
|
+
await deps.backend.setMode?.(t.sid, t.modeId);
|
|
603
|
+
await ctx.answerCallbackQuery(`ๅทฒๅๆข โ ${t.modeId}`);
|
|
604
|
+
try {
|
|
605
|
+
await ctx.editMessageText(`๐ ๅทฒๅๆข โ <b>${esc(t.modeId)}</b>`, { parse_mode: 'HTML' });
|
|
606
|
+
}
|
|
607
|
+
catch { }
|
|
608
|
+
}
|
|
609
|
+
catch (err) {
|
|
610
|
+
await ctx.answerCallbackQuery(`ๅๆขๅคฑ่ดฅ๏ผ${err.message.slice(0, 60)}`);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
// โโ P2c parity: cleanup finished subagent sessions (web C4) โโ
|
|
614
|
+
deps.bot.command('cleanup', async (ctx) => {
|
|
615
|
+
try {
|
|
616
|
+
const all = await deps.backend.listSessions();
|
|
617
|
+
const children = all.filter((s) => s.parentID);
|
|
618
|
+
let deleted = 0;
|
|
619
|
+
for (const c of children) {
|
|
620
|
+
try {
|
|
621
|
+
await deps.backend.deleteSession(c.id);
|
|
622
|
+
deleted += 1;
|
|
623
|
+
}
|
|
624
|
+
catch { /* skip */ }
|
|
625
|
+
}
|
|
626
|
+
await ctx.reply(`๐งน ๅทฒๆธ
็ ${deleted} ไธชๅญไปฃ็ไผ่ฏใ`);
|
|
627
|
+
}
|
|
628
|
+
catch (err) {
|
|
629
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
// โโ M9: /channels โ channel status mirror (config UI lives on web) โโ
|
|
633
|
+
deps.bot.command('channels', async (ctx) => {
|
|
634
|
+
try {
|
|
635
|
+
const ch = deps.channels?.()?.enabled;
|
|
636
|
+
const tgOn = ch === undefined ? true : ch;
|
|
637
|
+
const lines = [
|
|
638
|
+
'<b>๐ค ๆบๅจไบบ / ้้</b>',
|
|
639
|
+
tgOn
|
|
640
|
+
? 'โข Telegram โ โ
ๅทฒๅฏ็จ๏ผweb ็ซฏใๆบๅจไบบใ้ขๆฟๅฏ้
็ฝฎ๏ผ'
|
|
641
|
+
: 'โข Telegram โ โ ๅทฒๅ็จ๏ผchannels.json๏ผ',
|
|
642
|
+
'โข ๅพฎไฟก โ โณ ๅพ
ๆฅๅ
ฅ๏ผๅญ่ฏๅฐไฝๅๅฏ็จ๏ผ',
|
|
643
|
+
'โข Lark โ โณ ๅพ
ๆฅๅ
ฅ',
|
|
644
|
+
];
|
|
645
|
+
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
646
|
+
}
|
|
647
|
+
catch (err) {
|
|
648
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
// โโ M8: /skills /ls /open /worktree (web parity with the Skills/Files panels) โโ
|
|
652
|
+
const skillsCache = [];
|
|
653
|
+
const lsDirs = new Map();
|
|
654
|
+
const lsFiles = new Map();
|
|
655
|
+
let lsSeq = 0;
|
|
656
|
+
const wtTokens = new Map();
|
|
657
|
+
let wtSeq = 0;
|
|
658
|
+
deps.bot.command('skills', async (ctx) => {
|
|
659
|
+
try {
|
|
660
|
+
const list = await deps.backend.getSkills?.(deps.opencodeProject);
|
|
661
|
+
if (!list || list.length === 0) {
|
|
662
|
+
await ctx.reply('ๆฒกๆๅฏ็จ็ๆ่ฝใ');
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
skillsCache.length = 0;
|
|
666
|
+
skillsCache.push(...list);
|
|
667
|
+
const PAGE = 6;
|
|
668
|
+
const pages = Math.max(1, Math.ceil(skillsCache.length / PAGE));
|
|
669
|
+
const p = 0;
|
|
670
|
+
const lines = [`<b>๐งฉ Skills</b> ยท ${p + 1}/${pages} ้กต ยท ๅ
ฑ ${skillsCache.length}`];
|
|
671
|
+
const kb = new InlineKeyboard();
|
|
672
|
+
for (const s of skillsCache.slice(p * PAGE, p * PAGE + PAGE)) {
|
|
673
|
+
lines.push(`โข <b>${esc(s.name)}</b> โ ${esc((s.description ?? '').slice(0, 90))}`);
|
|
674
|
+
}
|
|
675
|
+
if (pages > 1) {
|
|
676
|
+
if (p > 0)
|
|
677
|
+
kb.text('โ๏ธ', `sk:page:${p - 1}`);
|
|
678
|
+
kb.text(`${p + 1}/${pages}`, 'menu:noop');
|
|
679
|
+
if (p < pages - 1)
|
|
680
|
+
kb.text('โถ๏ธ', `sk:page:${p + 1}`);
|
|
681
|
+
}
|
|
682
|
+
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML', reply_markup: kb });
|
|
683
|
+
}
|
|
684
|
+
catch (err) {
|
|
685
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
deps.bot.callbackQuery(/^sk:page:(\d+)$/, async (ctx) => {
|
|
689
|
+
const p = Math.max(0, Number(ctx.match[1]));
|
|
690
|
+
const PAGE = 6;
|
|
691
|
+
const pages = Math.max(1, Math.ceil(skillsCache.length / PAGE));
|
|
692
|
+
const pp = Math.min(p, pages - 1);
|
|
693
|
+
const lines = [`<b>๐งฉ Skills</b> ยท ${pp + 1}/${pages} ้กต ยท ๅ
ฑ ${skillsCache.length}`];
|
|
694
|
+
const kb = new InlineKeyboard();
|
|
695
|
+
for (const s of skillsCache.slice(pp * PAGE, pp * PAGE + PAGE)) {
|
|
696
|
+
lines.push(`โข <b>${esc(s.name)}</b> โ ${esc((s.description ?? '').slice(0, 90))}`);
|
|
697
|
+
}
|
|
698
|
+
if (pages > 1) {
|
|
699
|
+
if (pp > 0)
|
|
700
|
+
kb.text('โ๏ธ', `sk:page:${pp - 1}`);
|
|
701
|
+
kb.text(`${pp + 1}/${pages}`, 'menu:noop');
|
|
702
|
+
if (pp < pages - 1)
|
|
703
|
+
kb.text('โถ๏ธ', `sk:page:${pp + 1}`);
|
|
704
|
+
}
|
|
705
|
+
await ctx.answerCallbackQuery();
|
|
706
|
+
try {
|
|
707
|
+
await ctx.editMessageText(lines.join('\n'), { parse_mode: 'HTML', reply_markup: kb });
|
|
708
|
+
}
|
|
709
|
+
catch { }
|
|
710
|
+
});
|
|
711
|
+
deps.bot.command('ls', async (ctx) => {
|
|
712
|
+
try {
|
|
713
|
+
const arg = (ctx.message?.text ?? '').replace(/^\/ls\s*/, '').trim();
|
|
714
|
+
const path = arg || '.';
|
|
715
|
+
const files = await deps.backend.listFiles?.(deps.opencodeProject, path);
|
|
716
|
+
if (!files) {
|
|
717
|
+
await ctx.reply('ๅฝๅๅ็ซฏไธๆฏๆๆไปถๆต่งใ');
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (files.length === 0) {
|
|
721
|
+
await ctx.reply(`๐ ${path}๏ผ็ฉบ็ฎๅฝใ`);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
const lines = [`<b>๐ ${esc(path)}</b> ยท ๅ
ฑ ${files.length}`];
|
|
725
|
+
const kb = new InlineKeyboard();
|
|
726
|
+
if (path !== '.') {
|
|
727
|
+
const upTok = ++lsSeq;
|
|
728
|
+
lsDirs.set(upTok, path);
|
|
729
|
+
kb.text('โฐ ..', `ls:up:${upTok}`);
|
|
730
|
+
}
|
|
731
|
+
for (const f of files.slice(0, 20)) {
|
|
732
|
+
if (f.type === 'directory') {
|
|
733
|
+
const tok = ++lsSeq;
|
|
734
|
+
lsDirs.set(tok, f.path);
|
|
735
|
+
kb.text(`๐ ${f.name}`, `ls:dir:${tok}`).row();
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
const tok = ++lsSeq;
|
|
739
|
+
lsFiles.set(tok, f.path);
|
|
740
|
+
kb.text(`๐ ${f.name}`, `ls:cat:${tok}`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (files.length > 20)
|
|
744
|
+
lines.push(`โฆ ๅ
ถไฝ ${files.length - 20} ้กนๆชๆพ็คบ`);
|
|
745
|
+
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML', reply_markup: kb });
|
|
746
|
+
}
|
|
747
|
+
catch (err) {
|
|
748
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
deps.bot.callbackQuery(/^ls:dir:(\d+)$/, async (ctx) => {
|
|
752
|
+
const path = lsDirs.get(Number(ctx.match[1]));
|
|
753
|
+
if (!path) {
|
|
754
|
+
await ctx.answerCallbackQuery('ๅทฒ่ฟๆ โ ้ๆฐ /ls');
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
await ctx.answerCallbackQuery();
|
|
758
|
+
try {
|
|
759
|
+
const files = (await deps.backend.listFiles?.(deps.opencodeProject, path)) ?? [];
|
|
760
|
+
const lines = [`<b>๐ ${esc(path)}</b> ยท ๅ
ฑ ${files.length}`];
|
|
761
|
+
const kb = new InlineKeyboard();
|
|
762
|
+
if (path !== '.') {
|
|
763
|
+
const upTok = ++lsSeq;
|
|
764
|
+
lsDirs.set(upTok, path);
|
|
765
|
+
kb.text('โฐ ..', `ls:up:${upTok}`);
|
|
766
|
+
}
|
|
767
|
+
for (const f of files.slice(0, 20)) {
|
|
768
|
+
if (f.type === 'directory') {
|
|
769
|
+
const tok = ++lsSeq;
|
|
770
|
+
lsDirs.set(tok, f.path);
|
|
771
|
+
kb.text(`๐ ${f.name}`, `ls:dir:${tok}`).row();
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
const tok = ++lsSeq;
|
|
775
|
+
lsFiles.set(tok, f.path);
|
|
776
|
+
kb.text(`๐ ${f.name}`, `ls:cat:${tok}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
try {
|
|
780
|
+
await ctx.editMessageText(lines.join('\n'), { parse_mode: 'HTML', reply_markup: kb });
|
|
781
|
+
}
|
|
782
|
+
catch { }
|
|
783
|
+
}
|
|
784
|
+
catch (err) {
|
|
785
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
// Directory navigation re-runs /ls logic via the registered command flow โ
|
|
789
|
+
// simplest correct path: synthesize the message through the same handler.
|
|
790
|
+
deps.bot.callbackQuery(/^ls:cat:(\d+)$/, async (ctx) => {
|
|
791
|
+
const path = lsFiles.get(Number(ctx.match[1]));
|
|
792
|
+
if (!path) {
|
|
793
|
+
await ctx.answerCallbackQuery('ๅทฒ่ฟๆ โ ้ๆฐ /ls');
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
await ctx.answerCallbackQuery();
|
|
797
|
+
try {
|
|
798
|
+
const f = await deps.backend.readFile?.(deps.opencodeProject, path);
|
|
799
|
+
if (!f) {
|
|
800
|
+
await ctx.reply('่ฏปๅๅคฑ่ดฅใ');
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (f.type !== 'text') {
|
|
804
|
+
await ctx.reply(`๐ ${esc(path)}๏ผไบ่ฟๅถๆไปถ๏ผไธ้ข่งใ`);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
const body = f.content.length > 800 ? f.content.slice(0, 800) + '\nโฆ' : f.content;
|
|
808
|
+
await ctx.reply(`<b>๐ ${esc(path)}</b>\n<pre>${esc(body)}</pre>`, { parse_mode: 'HTML' });
|
|
809
|
+
}
|
|
810
|
+
catch (err) {
|
|
811
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
deps.bot.callbackQuery(/^ls:up:(\d+)$/, async (ctx) => {
|
|
815
|
+
const cur = lsDirs.get(Number(ctx.match[1]));
|
|
816
|
+
if (!cur) {
|
|
817
|
+
await ctx.answerCallbackQuery('ๅทฒ่ฟๆ');
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const parent = cur.includes('/') ? cur.replace(/\/[^/]+\/?$/, '') || '.' : '.';
|
|
821
|
+
await ctx.answerCallbackQuery();
|
|
822
|
+
try {
|
|
823
|
+
const files = (await deps.backend.listFiles?.(deps.opencodeProject, parent)) ?? [];
|
|
824
|
+
const lines = [`<b>๐ ${esc(parent)}</b> ยท ๅ
ฑ ${files.length}`];
|
|
825
|
+
const kb = new InlineKeyboard();
|
|
826
|
+
if (parent !== '.') {
|
|
827
|
+
const upTok = ++lsSeq;
|
|
828
|
+
lsDirs.set(upTok, parent);
|
|
829
|
+
kb.text('โฐ ..', `ls:up:${upTok}`);
|
|
830
|
+
}
|
|
831
|
+
for (const f of files.slice(0, 20)) {
|
|
832
|
+
if (f.type === 'directory') {
|
|
833
|
+
const tok = ++lsSeq;
|
|
834
|
+
lsDirs.set(tok, f.path);
|
|
835
|
+
kb.text(`๐ ${f.name}`, `ls:dir:${tok}`).row();
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
const tok = ++lsSeq;
|
|
839
|
+
lsFiles.set(tok, f.path);
|
|
840
|
+
kb.text(`๐ ${f.name}`, `ls:cat:${tok}`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
await ctx.editMessageText(lines.join('\n'), { parse_mode: 'HTML', reply_markup: kb });
|
|
844
|
+
}
|
|
845
|
+
catch (err) {
|
|
846
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
deps.bot.command('open', async (ctx) => {
|
|
850
|
+
const arg = (ctx.message?.text ?? '').replace(/^\/open\s*/, '').trim();
|
|
851
|
+
if (!arg) {
|
|
852
|
+
await ctx.reply('็จๆณ๏ผ/open <ๆไปถ่ทฏๅพ>๏ผ็ธๅฏนๅฝๅๅทฅไฝๅบ๏ผๅฆ src/index.ts๏ผ');
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
try {
|
|
856
|
+
const f = await deps.backend.readFile?.(deps.opencodeProject, arg);
|
|
857
|
+
if (!f) {
|
|
858
|
+
await ctx.reply('่ฏปๅๅคฑ่ดฅใ');
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (f.type !== 'text') {
|
|
862
|
+
await ctx.reply(`๐ ${esc(arg)}๏ผไบ่ฟๅถๆไปถ๏ผไธ้ข่งใ`);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const body = f.content.length > 800 ? f.content.slice(0, 800) + '\nโฆ' : f.content;
|
|
866
|
+
await ctx.reply(`<b>๐ ${esc(arg)}</b>\n<pre>${esc(body)}</pre>`, { parse_mode: 'HTML' });
|
|
867
|
+
}
|
|
868
|
+
catch (err) {
|
|
869
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
deps.bot.command('worktree', async (ctx) => {
|
|
873
|
+
try {
|
|
874
|
+
const arg = (ctx.message?.text ?? '').replace(/^\/worktree\s*/, '').trim();
|
|
875
|
+
if (arg.startsWith('=')) {
|
|
876
|
+
const name = arg.slice(1).trim();
|
|
877
|
+
if (!name) {
|
|
878
|
+
await ctx.reply('็จๆณ๏ผ/worktree = <ๅ็งฐ>');
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const dir = deps.state.getActiveWorkspace() || deps.opencodeProject || '';
|
|
882
|
+
const created = await deps.backend.createWorktreeSandboxes?.(dir, name);
|
|
883
|
+
if (!created) {
|
|
884
|
+
await ctx.reply('โ ๅๅปบๅคฑ่ดฅ๏ผexperimental ๆฅๅฃ๏ผใ');
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
await ctx.reply(`๐ฟ ๅทฒๅๅปบ worktree๏ผ<b>${esc(created.name)}</b>`);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (arg.startsWith('rm ')) {
|
|
891
|
+
const name = arg.slice(3).trim();
|
|
892
|
+
if (!name) {
|
|
893
|
+
await ctx.reply('็จๆณ๏ผ/worktree rm <ๅ็งฐ>');
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const tok = ++wtSeq;
|
|
897
|
+
wtTokens.set(tok, name);
|
|
898
|
+
const kb = new InlineKeyboard()
|
|
899
|
+
.text('๐ ็กฎ่ฎคๅ ้ค', `wt:rm:${tok}`)
|
|
900
|
+
.text('ๅๆถ', 'menu:noop');
|
|
901
|
+
await ctx.reply(`โ ๏ธ ๅ ้ค worktree <b>${esc(name)}</b>๏ผ`, { parse_mode: 'HTML', reply_markup: kb });
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
const list = await deps.backend.listWorktreeSandboxes?.(deps.opencodeProject);
|
|
905
|
+
if (!list) {
|
|
906
|
+
await ctx.reply('ๅฝๅๅ็ซฏไธๆฏๆ worktree ๆฅ่ฏขใ');
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (list.length === 0) {
|
|
910
|
+
await ctx.reply('๐ฟ ๆๆ worktreeใๆฐๅปบ๏ผ/worktree = <ๅ็งฐ>๏ผbeta๏ผ');
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const lines = ['<b>๐ฟ Worktrees</b> ยท beta'];
|
|
914
|
+
for (const w of list)
|
|
915
|
+
lines.push(`โข ${esc(w.name)}${w.directory ? ` โ <code>${esc(w.directory)}</code>` : ''}`);
|
|
916
|
+
lines.push('ๆฐๅปบ๏ผ/worktree = <ๅ็งฐ> ยท ๅ ้ค๏ผ/worktree rm <ๅ็งฐ>');
|
|
917
|
+
await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
|
|
918
|
+
}
|
|
919
|
+
catch (err) {
|
|
920
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
deps.bot.callbackQuery(/^wt:rm:(\d+)$/, async (ctx) => {
|
|
924
|
+
const name = wtTokens.get(Number(ctx.match[1]));
|
|
925
|
+
if (!name) {
|
|
926
|
+
await ctx.answerCallbackQuery('ๅทฒ่ฟๆ โ ้ๆฐๆง่ก /worktree rm');
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const ok = (await deps.backend.removeWorktreeSandboxes?.(deps.state.getActiveWorkspace() || deps.opencodeProject || '', name)) ?? false;
|
|
930
|
+
await ctx.answerCallbackQuery(ok ? '๐ ๅทฒๅ ้ค' : 'ๅ ้คๅคฑ่ดฅ');
|
|
931
|
+
try {
|
|
932
|
+
await ctx.editMessageText(ok ? `๐ worktree <b>${esc(name)}</b> ๅทฒๅ ้คใ` : 'ๅ ้คๅคฑ่ดฅใ', { parse_mode: 'HTML' });
|
|
933
|
+
}
|
|
934
|
+
catch { }
|
|
935
|
+
});
|
|
936
|
+
deps.bot.command('new', async (ctx) => {
|
|
937
|
+
const dir = deps.state.getActiveWorkspace();
|
|
938
|
+
if (!dir) {
|
|
939
|
+
await ctx.reply('No active workspace. Use /workspaces first.', { parse_mode: 'HTML' });
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (!workspaceAllowed(deps, dir)) {
|
|
943
|
+
await ctx.reply('ๅฝๅๅทฅไฝๅบไธๅจๆบๅจไบบ็่ฎฟ้ฎ่ๅดๅ
๏ผM9 ๅทฅไฝๅบ่ฎฟ้ฎ่ๅด๏ผใ', { parse_mode: 'HTML' });
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
try {
|
|
947
|
+
const text = ctx.message && 'text' in ctx.message ? ctx.message.text.split(' ').slice(1).join(' ').trim() : '';
|
|
948
|
+
const { id } = await deps.backend.createSession({ directory: dir, ...(text ? { title: text.slice(0, 60) } : {}) });
|
|
949
|
+
deps.state.setPinnedSessionId(id);
|
|
950
|
+
await ctx.reply(`๐ <b>New session</b> in <code>${dir.split('/').pop()}</code>\n<code>โฆ${id.slice(-8)}</code> (pinned). Send a message to start.`, { parse_mode: 'HTML' });
|
|
951
|
+
}
|
|
952
|
+
catch (err) {
|
|
953
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
deps.bot.command('rename', async (ctx) => {
|
|
957
|
+
const text = ctx.message && 'text' in ctx.message ? ctx.message.text.split(' ').slice(1).join(' ').trim() : '';
|
|
958
|
+
const sid = deps.state.getPinnedSessionId() ?? deps.state.getLastSessionId();
|
|
959
|
+
if (!sid) {
|
|
960
|
+
await ctx.reply('No active session. Pin one with /sessions first.', { parse_mode: 'HTML' });
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (!text) {
|
|
964
|
+
await ctx.reply('Usage: <code>/rename New Title</code>', { parse_mode: 'HTML' });
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
await deps.backend.renameSession(sid, text.slice(0, 100));
|
|
969
|
+
await ctx.reply(`โ๏ธ Renamed <code>โฆ${sid.slice(-8)}</code> โ <b>${esc(text.slice(0, 100))}</b>`, { parse_mode: 'HTML' });
|
|
970
|
+
}
|
|
971
|
+
catch (err) {
|
|
972
|
+
await ctx.reply(`โ ${esc(err.message)}`, { parse_mode: 'HTML' });
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
// โโ P2b-M7: scheduled tasks (cross-channel; engine lives in core) โโ
|
|
976
|
+
deps.bot.command('tasks', async (ctx) => {
|
|
977
|
+
if (!deps.scheduler) {
|
|
978
|
+
await ctx.reply('โฐ ๅฎๆถไปปๅกๆชๅฏ็จ');
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
await renderTasks(ctx);
|
|
982
|
+
});
|
|
983
|
+
// grinev ๅฝไปคๅๅฏน้ฝ๏ผ/tasklist = /tasks
|
|
984
|
+
deps.bot.command('tasklist', async (ctx) => { await renderTasks(ctx); });
|
|
985
|
+
deps.bot.command('task', async (ctx) => {
|
|
986
|
+
if (!deps.scheduler) {
|
|
987
|
+
await ctx.reply('โฐ ๅฎๆถไปปๅกๆชๅฏ็จ');
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const sched = deps.scheduler;
|
|
991
|
+
const msgText = (ctx.message && 'text' in ctx.message ? ctx.message.text : '') ?? '';
|
|
992
|
+
const body = msgText.trim().replace(/^\/task\s*/, '');
|
|
993
|
+
// /task every <N> min <prompt> | /task daily HH:MM <prompt>
|
|
994
|
+
const every = body.match(/^every\s+(\d+)\s*(?:m|min|ๅ้)?\s+([\s\S]+)$/i);
|
|
995
|
+
const daily = body.match(/^daily\s+([01]\d|2[0-3]):[0-5]\d\s+([\s\S]+)$/i);
|
|
996
|
+
if (every) {
|
|
997
|
+
const s = sched.add({ prompt: every[2], spec: { kind: 'every', minutes: Number(every[1]) }, name: every[2].slice(0, 24) });
|
|
998
|
+
await ctx.reply(s ? `โฐ ๅทฒๅๅปบ๏ผๆฏ ${every[1]} ๅ้\n<code>${esc(s.prompt.slice(0, 80))}</code>\nID: ${s.id}` : 'ๅๅปบๅคฑ่ดฅ', { parse_mode: 'HTML' });
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (daily) {
|
|
1002
|
+
const time = body.match(/daily\s+([01]\d|2[0-3]):[0-5]\d/i)[1];
|
|
1003
|
+
const s = sched.add({ prompt: daily[2], spec: { kind: 'daily', time }, name: daily[2].slice(0, 24) });
|
|
1004
|
+
await ctx.reply(s ? `โฐ ๅทฒๅๅปบ๏ผๆฏๅคฉ ${time}\n<code>${esc(s.prompt.slice(0, 80))}</code>\nID: ${s.id}` : 'ๅๅปบๅคฑ่ดฅ', { parse_mode: 'HTML' });
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
await ctx.reply('็จๆณ๏ผ/task every 30m <ๆ็คบ่ฏ>\n /task daily 09:00 <ๆ็คบ่ฏ>');
|
|
1008
|
+
});
|
|
1009
|
+
deps.bot.command('taskdel', async (ctx) => {
|
|
1010
|
+
const sched = deps.scheduler;
|
|
1011
|
+
if (!sched) {
|
|
1012
|
+
await ctx.reply('โฐ ๅฎๆถไปปๅกๆชๅฏ็จ');
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
const id = (ctx.message && 'text' in ctx.message ? ctx.message.text ?? '' : '').replace(/^\/taskdel\s*/, '').trim();
|
|
1016
|
+
if (!id) {
|
|
1017
|
+
await ctx.reply('็จๆณ๏ผ/taskdel <ID>');
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
const ok = sched.remove(id);
|
|
1021
|
+
await ctx.reply(ok ? `๐ ๅทฒๅ ้ค ${id}` : `ๆชๆพๅฐ ${id}`);
|
|
1022
|
+
});
|
|
1023
|
+
const renderTasks = async (ctx) => {
|
|
1024
|
+
const list = deps.scheduler.list();
|
|
1025
|
+
if (list.length === 0) {
|
|
1026
|
+
await ctx.reply('โฐ ๆฒกๆๅฎๆถไปปๅกใ็จ /task ๅๅปบ๏ผ\n/task every 30m <ๆ็คบ่ฏ>\n/task daily 09:00 <ๆ็คบ่ฏ>');
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const lines = ['<b>โฐ ๅฎๆถไปปๅก</b>'];
|
|
1030
|
+
const rows = [];
|
|
1031
|
+
for (const s of list) {
|
|
1032
|
+
const spec = s.spec.kind === 'every' ? `ๆฏ ${s.spec.minutes} ๅ้` : `ๆฏๅคฉ ${s.spec.time}`;
|
|
1033
|
+
const on = s.enabled ? '๐ข' : 'โธ';
|
|
1034
|
+
lines.push(`${on} <b>${esc(s.name)}</b>\n ${spec} ยท ${s.id}`);
|
|
1035
|
+
rows.push([
|
|
1036
|
+
btn(s.enabled ? 'โธ' : 'โถ', `task:toggle:${s.id}`),
|
|
1037
|
+
btn('๐', `task:del:${s.id}`),
|
|
1038
|
+
]);
|
|
1039
|
+
}
|
|
1040
|
+
await ctx.reply(lines.join('\n\n'), { parse_mode: 'HTML', ...inlineKeyboard(rows) });
|
|
1041
|
+
};
|
|
1042
|
+
deps.bot.callbackQuery(/^task:toggle:(.+)$/, async (ctx) => {
|
|
1043
|
+
if (!deps.scheduler)
|
|
1044
|
+
return;
|
|
1045
|
+
const id = ctx.match[1];
|
|
1046
|
+
const s = deps.scheduler.list().find((x) => x.id === id);
|
|
1047
|
+
if (s)
|
|
1048
|
+
deps.scheduler.setEnabled(id, !s.enabled);
|
|
1049
|
+
await ctx.answerCallbackQuery(s?.enabled === false ? 'โถ ๅทฒๅฏ็จ' : 'โธ ๅทฒๅ็จ');
|
|
1050
|
+
await renderTasks(ctx);
|
|
1051
|
+
});
|
|
1052
|
+
deps.bot.callbackQuery(/^task:del:(.+)$/, async (ctx) => {
|
|
1053
|
+
if (!deps.scheduler)
|
|
1054
|
+
return;
|
|
1055
|
+
const id = ctx.match[1];
|
|
1056
|
+
deps.scheduler.remove(id);
|
|
1057
|
+
await ctx.answerCallbackQuery('ๅทฒๅ ้ค');
|
|
1058
|
+
await renderTasks(ctx);
|
|
1059
|
+
});
|
|
1060
|
+
const commands = [
|
|
1061
|
+
{ command: 'start', description: 'Handshake and health' },
|
|
1062
|
+
{ command: 'status', description: 'Server + last session' },
|
|
1063
|
+
{ command: 'sessions', description: 'List all sessions' },
|
|
1064
|
+
{ command: 'session', description: 'Pin a session' },
|
|
1065
|
+
{ command: 'files', description: 'Files touched in last session' },
|
|
1066
|
+
{ command: 'diff', description: 'Pending git diff' },
|
|
1067
|
+
{ command: 'todo', description: 'Session todo list' },
|
|
1068
|
+
{ command: 'context', description: 'Tokens + cost + model' },
|
|
1069
|
+
{ command: 'agent', description: 'Set next agent' },
|
|
1070
|
+
{ command: 'model', description: 'Set next model' },
|
|
1071
|
+
{ command: 'current', description: 'Last session used' },
|
|
1072
|
+
{ command: 'abort', description: 'Stop the current generation' },
|
|
1073
|
+
{ command: 'tasks', description: 'Scheduled tasks (list)' },
|
|
1074
|
+
{ command: 'task', description: 'Create a scheduled task' },
|
|
1075
|
+
{ command: 'taskdel', description: 'Delete a scheduled task' },
|
|
1076
|
+
{ command: 'projects', description: 'Switch project (workspace)' },
|
|
1077
|
+
{ command: 'commands', description: 'List opencode custom commands' },
|
|
1078
|
+
{ command: 'mcps', description: 'MCP server status' },
|
|
1079
|
+
{ command: 'messages', description: 'Recent messages' },
|
|
1080
|
+
{ command: 'detach', description: 'Unpin the followed session' },
|
|
1081
|
+
{ command: 'version', description: 'Bot version + uptime' },
|
|
1082
|
+
{ command: 'pair', description: 'Pair a device (URL + token)' },
|
|
1083
|
+
{ command: 'workspaces', description: 'List/switch workspaces' },
|
|
1084
|
+
{ command: 'new', description: 'New session in active workspace' },
|
|
1085
|
+
{ command: 'rename', description: 'Rename the pinned/last session' },
|
|
1086
|
+
{ command: 'channels', description: 'Bot channel status' },
|
|
1087
|
+
{ command: 'subs', description: 'List subagents of the session' },
|
|
1088
|
+
{ command: 'mode', description: 'Switch the session mode (build/planโฆ)' },
|
|
1089
|
+
{ command: 'cleanup', description: 'Delete finished subagent sessions' },
|
|
1090
|
+
{ command: 'help', description: 'Show help' },
|
|
1091
|
+
];
|
|
1092
|
+
// Register the same command list for every scope so a stale or competing bot
|
|
1093
|
+
// instance cannot shadow the default menu with a narrower (e.g. private-chat)
|
|
1094
|
+
// scope that only exposes a subset of commands.
|
|
1095
|
+
const scopes = [
|
|
1096
|
+
{ type: 'default' },
|
|
1097
|
+
{ type: 'all_private_chats' },
|
|
1098
|
+
{ type: 'all_group_chats' },
|
|
1099
|
+
];
|
|
1100
|
+
for (const scope of scopes) {
|
|
1101
|
+
const label = scope.type === 'default' ? 'default' : scope.type;
|
|
1102
|
+
const extra = scope.type === 'default' ? undefined : { scope };
|
|
1103
|
+
deps.bot.api
|
|
1104
|
+
.setMyCommands(commands, extra)
|
|
1105
|
+
.then(() => log.info(`setMyCommands OK [${label}]`))
|
|
1106
|
+
.catch((err) => log.warn(`setMyCommands [${label}] failed`, err));
|
|
1107
|
+
}
|
|
1108
|
+
// โโ Callbacks โโ
|
|
1109
|
+
deps.bot.callbackQuery(/^session:pin:(.+)$/, async (ctx) => {
|
|
1110
|
+
const id = ctx.match[1];
|
|
1111
|
+
deps.state.setPinnedSessionId(id);
|
|
1112
|
+
await ctx.answerCallbackQuery(`Pinned ${id.slice(-8)}`);
|
|
1113
|
+
try {
|
|
1114
|
+
await ctx.editMessageText(`<b>๐ Pinned</b>\n\n<code>${id}</code>`, { parse_mode: 'HTML' });
|
|
1115
|
+
}
|
|
1116
|
+
catch (err) {
|
|
1117
|
+
const msg = err.message;
|
|
1118
|
+
if (!msg.includes('message is not modified'))
|
|
1119
|
+
log.warn('session:pin edit failed', msg);
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
deps.bot.callbackQuery('session:unpin', async (ctx) => {
|
|
1123
|
+
deps.state.setPinnedSessionId(undefined);
|
|
1124
|
+
await ctx.answerCallbackQuery('Unpinned');
|
|
1125
|
+
try {
|
|
1126
|
+
await ctx.editMessageText('<b>๐ Session unpinned</b>\n\nMessages will use the most recently active session.', { parse_mode: 'HTML' });
|
|
1127
|
+
}
|
|
1128
|
+
catch (err) {
|
|
1129
|
+
const msg = err.message;
|
|
1130
|
+
if (!msg.includes('message is not modified'))
|
|
1131
|
+
log.warn('session:unpin edit failed', msg);
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
deps.bot.callbackQuery('status:refresh', async (ctx) => {
|
|
1135
|
+
const { lines, buttons } = await buildStatusCard(deps);
|
|
1136
|
+
try {
|
|
1137
|
+
await ctx.editMessageText(lines.join('\n'), {
|
|
1138
|
+
parse_mode: 'HTML',
|
|
1139
|
+
...inlineKeyboard([buttons]),
|
|
1140
|
+
});
|
|
1141
|
+
await ctx.answerCallbackQuery('Refreshed');
|
|
1142
|
+
}
|
|
1143
|
+
catch (err) {
|
|
1144
|
+
const msg = err.message;
|
|
1145
|
+
if (msg.includes('message is not modified')) {
|
|
1146
|
+
await ctx.answerCallbackQuery('Status is unchanged');
|
|
1147
|
+
}
|
|
1148
|
+
else {
|
|
1149
|
+
log.warn('status:refresh edit failed', msg);
|
|
1150
|
+
await ctx.answerCallbackQuery('Failed to refresh');
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
deps.bot.callbackQuery('status:abort', async (ctx) => {
|
|
1155
|
+
const sid = deps.abortGeneration();
|
|
1156
|
+
if (sid) {
|
|
1157
|
+
try {
|
|
1158
|
+
await deps.backend.abort(sid);
|
|
1159
|
+
}
|
|
1160
|
+
catch { }
|
|
1161
|
+
}
|
|
1162
|
+
await ctx.answerCallbackQuery('Abortingโฆ');
|
|
1163
|
+
try {
|
|
1164
|
+
await ctx.editMessageText('๐ Generation aborted.', { parse_mode: 'HTML' });
|
|
1165
|
+
}
|
|
1166
|
+
catch (err) {
|
|
1167
|
+
const msg = err.message;
|
|
1168
|
+
if (!msg.includes('message is not modified'))
|
|
1169
|
+
log.warn('status:abort edit failed', msg);
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
deps.bot.callbackQuery(/^agent:set:(.+)$/, async (ctx) => {
|
|
1173
|
+
const name = ctx.match[1];
|
|
1174
|
+
log.info(`agent:set callback: ${name}`);
|
|
1175
|
+
deps.state.setNextAgent(name);
|
|
1176
|
+
// Sync model to whatever this agent is bound to in opencode.jsonc, so the
|
|
1177
|
+
// user doesn't end up running a new agent against a stale /model override.
|
|
1178
|
+
let modelSuffix = '';
|
|
1179
|
+
try {
|
|
1180
|
+
const agents = await deps.backend.getAgents(deps.opencodeProject);
|
|
1181
|
+
const a = agents.find((x) => x.name === name);
|
|
1182
|
+
const parsed = a ? parseAgentModel(a.model) : undefined;
|
|
1183
|
+
if (parsed) {
|
|
1184
|
+
deps.state.setNextModel(parsed);
|
|
1185
|
+
modelSuffix = ` ยท <code>${parsed.modelID}</code>`;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
catch (err) {
|
|
1189
|
+
log.warn(`agent:set model sync failed: ${err.message}`);
|
|
1190
|
+
}
|
|
1191
|
+
await ctx.answerCallbackQuery(`Agent โ ${name}`);
|
|
1192
|
+
try {
|
|
1193
|
+
await ctx.editMessageText(`<b>๐ค Agent set</b>\n\nNext message will use <b>${name}</b>${modelSuffix}.`, { parse_mode: 'HTML' });
|
|
1194
|
+
}
|
|
1195
|
+
catch (err) {
|
|
1196
|
+
const msg = err.message;
|
|
1197
|
+
if (!msg.includes('message is not modified'))
|
|
1198
|
+
log.warn('agent:set edit failed', msg);
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
deps.bot.callbackQuery('agent:clear', async (ctx) => {
|
|
1202
|
+
deps.state.setNextAgent(undefined);
|
|
1203
|
+
await ctx.answerCallbackQuery('Agent cleared');
|
|
1204
|
+
try {
|
|
1205
|
+
await ctx.editMessageText('<b>๐ค Agent cleared</b>\n\nNext message will use the default agent.', { parse_mode: 'HTML' });
|
|
1206
|
+
}
|
|
1207
|
+
catch (err) {
|
|
1208
|
+
const msg = err.message;
|
|
1209
|
+
if (!msg.includes('message is not modified'))
|
|
1210
|
+
log.warn('agent:clear edit failed', msg);
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
deps.bot.callbackQuery(/^model:set:(.+)$/, async (ctx) => {
|
|
1214
|
+
const key = modelTokens.get(ctx.match[1]);
|
|
1215
|
+
if (!key) {
|
|
1216
|
+
await ctx.answerCallbackQuery('Stale โ re-run /model');
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
const idx = key.indexOf('/');
|
|
1220
|
+
const providerID = key.slice(0, idx);
|
|
1221
|
+
const modelID = key.slice(idx + 1);
|
|
1222
|
+
log.info(`model:set callback: provider=${providerID} model=${modelID}`);
|
|
1223
|
+
const parsed = { providerID, modelID };
|
|
1224
|
+
deps.state.setNextModel(parsed);
|
|
1225
|
+
await ctx.answerCallbackQuery(`Model โ ${modelID}`);
|
|
1226
|
+
try {
|
|
1227
|
+
await ctx.editMessageText(`<b>โ๏ธ Model set</b>\n\nNext message will use <code>${providerID}/${modelID}</code>.`, { parse_mode: 'HTML' });
|
|
1228
|
+
}
|
|
1229
|
+
catch (err) {
|
|
1230
|
+
const msg = err.message;
|
|
1231
|
+
if (!msg.includes('message is not modified'))
|
|
1232
|
+
log.warn('model:set edit failed', msg);
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
deps.bot.callbackQuery('model:clear', async (ctx) => {
|
|
1236
|
+
deps.state.setNextModel(undefined);
|
|
1237
|
+
await ctx.answerCallbackQuery('Model cleared');
|
|
1238
|
+
try {
|
|
1239
|
+
await ctx.editMessageText('<b>โ๏ธ Model cleared</b>\n\nNext message will use the default model.', { parse_mode: 'HTML' });
|
|
1240
|
+
}
|
|
1241
|
+
catch (err) {
|
|
1242
|
+
const msg = err.message;
|
|
1243
|
+
if (!msg.includes('message is not modified'))
|
|
1244
|
+
log.warn('model:clear edit failed', msg);
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
// Step 2: pick a specific model after selecting a provider
|
|
1248
|
+
deps.bot.callbackQuery(/^model:pick:(.+)$/, async (ctx) => {
|
|
1249
|
+
const providerID = ctx.match[1];
|
|
1250
|
+
try {
|
|
1251
|
+
const providers = await deps.backend.getModels(deps.opencodeProject);
|
|
1252
|
+
const provider = providers.find(p => p.id === providerID);
|
|
1253
|
+
if (!provider || (provider.models ?? []).length === 0) {
|
|
1254
|
+
await ctx.answerCallbackQuery('No models for this provider');
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
const nextModel = deps.state.getNextModel();
|
|
1258
|
+
const lines = [
|
|
1259
|
+
`<b>โ๏ธ Model โ ${provider.name}</b>`,
|
|
1260
|
+
'',
|
|
1261
|
+
];
|
|
1262
|
+
const rows = [];
|
|
1263
|
+
for (const m of (provider.models ?? [])) {
|
|
1264
|
+
const sel = nextModel?.providerID === providerID && nextModel?.modelID === m.id ? 'โ' : 'โ';
|
|
1265
|
+
lines.push(`${sel} ${m.name ?? m.id}`);
|
|
1266
|
+
rows.push([btn(m.name ?? m.id, `model:set:${modelToken(providerID, m.id)}`)]);
|
|
1267
|
+
}
|
|
1268
|
+
rows.push([btn('โ Back', 'model:back')]);
|
|
1269
|
+
await ctx.editMessageText(lines.join('\n'), {
|
|
1270
|
+
parse_mode: 'HTML',
|
|
1271
|
+
...inlineKeyboard(rows),
|
|
1272
|
+
});
|
|
1273
|
+
await ctx.answerCallbackQuery();
|
|
1274
|
+
}
|
|
1275
|
+
catch (err) {
|
|
1276
|
+
log.error('model:pick failed', err);
|
|
1277
|
+
await ctx.answerCallbackQuery('Failed to load models');
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
// Back to provider list
|
|
1281
|
+
deps.bot.callbackQuery('model:back', async (ctx) => {
|
|
1282
|
+
try {
|
|
1283
|
+
const providers = await deps.backend.getModels(deps.opencodeProject);
|
|
1284
|
+
const nextModel = deps.state.getNextModel();
|
|
1285
|
+
const lines = ['<b>โ๏ธ Model โ Select provider</b>', ''];
|
|
1286
|
+
const rows = [];
|
|
1287
|
+
for (const p of providers) {
|
|
1288
|
+
const count = (p.models ?? []).length;
|
|
1289
|
+
const hasSelected = nextModel?.providerID === p.id;
|
|
1290
|
+
const marker = hasSelected ? 'โ' : 'โธ';
|
|
1291
|
+
lines.push(`${marker} <b>${p.name}</b> ยท ${count} model${count !== 1 ? 's' : ''}`);
|
|
1292
|
+
rows.push([btn(p.name, `model:pick:${p.id}`)]);
|
|
1293
|
+
}
|
|
1294
|
+
if (nextModel) {
|
|
1295
|
+
lines.push('', `<i>Current override: ${nextModel.providerID}/${nextModel.modelID}</i>`);
|
|
1296
|
+
}
|
|
1297
|
+
rows.push([btn('โ Clear', 'model:clear')]);
|
|
1298
|
+
await ctx.editMessageText(lines.join('\n'), {
|
|
1299
|
+
parse_mode: 'HTML',
|
|
1300
|
+
...inlineKeyboard(rows),
|
|
1301
|
+
});
|
|
1302
|
+
await ctx.answerCallbackQuery();
|
|
1303
|
+
}
|
|
1304
|
+
catch (err) {
|
|
1305
|
+
log.error('model:back failed', err);
|
|
1306
|
+
await ctx.answerCallbackQuery('Failed');
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
deps.bot.callbackQuery('card:dismiss', async (ctx) => {
|
|
1310
|
+
await ctx.answerCallbackQuery();
|
|
1311
|
+
await ctx.deleteMessage().catch(() => { });
|
|
1312
|
+
});
|
|
1313
|
+
// โโ Info commands (split to separate file) โโ
|
|
1314
|
+
registerInfoCommands({ bot: deps.bot, backend: deps.backend, state: deps.state, opencodeProject: deps.opencodeProject });
|
|
1315
|
+
// โโ Approval callbacks โ always registered so buttons work in both modes โโ
|
|
1316
|
+
deps.bot.callbackQuery(/^approve:(once|always|reject):(.+)$/, async (ctx) => {
|
|
1317
|
+
const match = ctx.match;
|
|
1318
|
+
const response = match[1];
|
|
1319
|
+
// callback_data carries a short token (64-byte limit); resolve it back to
|
|
1320
|
+
// the permission id. Fall back to the raw value for pre-token cards.
|
|
1321
|
+
const permId = deps.approvalTokens.get(match[2]) ?? match[2];
|
|
1322
|
+
const p = deps.pendingApprovals.get(permId);
|
|
1323
|
+
if (!p) {
|
|
1324
|
+
await ctx.answerCallbackQuery('This request has already been handled.');
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
// Delete BEFORE resolving: a fast double-click must see "already handled"
|
|
1328
|
+
// on the second tap instead of racing the resolvePermission call.
|
|
1329
|
+
deps.pendingApprovals.delete(permId);
|
|
1330
|
+
try {
|
|
1331
|
+
await deps.backend.resolvePermission(p.sessionId, p.permissionId, response);
|
|
1332
|
+
}
|
|
1333
|
+
catch (err) {
|
|
1334
|
+
log.error(`failed to reply permission ${permId}`, err);
|
|
1335
|
+
await ctx.answerCallbackQuery('Failed to reply. The request may have expired.');
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
const labels = {
|
|
1339
|
+
once: 'Allowed (once)',
|
|
1340
|
+
always: 'Always Allowed',
|
|
1341
|
+
reject: 'Rejected',
|
|
1342
|
+
};
|
|
1343
|
+
const display = labels[response];
|
|
1344
|
+
await ctx.editMessageText(`${display}\n\n${esc(p.title)}`, { parse_mode: 'HTML' }).catch(() => { });
|
|
1345
|
+
await ctx.answerCallbackQuery(display);
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
//# sourceMappingURL=handlers.js.map
|