pim-agent 0.0.1 → 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 +21 -0
- package/README.md +293 -7
- package/bin/pim.ts +151 -0
- package/package.json +82 -4
- package/packages/core/src/attachments/AttachmentStore.ts +99 -0
- package/packages/core/src/attachments/Attachments.ts +69 -0
- package/packages/core/src/extensions/CoreExtensions.ts +51 -0
- package/packages/core/src/extensions/apply-patch/coordinator.ts +53 -0
- package/packages/core/src/extensions/apply-patch/executor.ts +550 -0
- package/packages/core/src/extensions/apply-patch/index.ts +69 -0
- package/packages/core/src/extensions/apply-patch/matcher.ts +47 -0
- package/packages/core/src/extensions/apply-patch/model.ts +39 -0
- package/packages/core/src/extensions/apply-patch/parser.ts +379 -0
- package/packages/core/src/extensions/apply-patch/render.ts +216 -0
- package/packages/core/src/extensions/apply-patch/schema.ts +37 -0
- package/packages/core/src/extensions/apply-patch/types.ts +30 -0
- package/packages/core/src/extensions/bash/capture.ts +86 -0
- package/packages/core/src/extensions/bash/format.ts +110 -0
- package/packages/core/src/extensions/bash/image.ts +31 -0
- package/packages/core/src/extensions/bash/index.ts +88 -0
- package/packages/core/src/extensions/bash/render.ts +31 -0
- package/packages/core/src/extensions/bash/run.ts +221 -0
- package/packages/core/src/extensions/bash/schema.ts +64 -0
- package/packages/core/src/extensions/edit/edit.ts +366 -0
- package/packages/core/src/extensions/edit/index.ts +36 -0
- package/packages/core/src/extensions/edit/render.ts +16 -0
- package/packages/core/src/extensions/edit/schema.ts +37 -0
- package/packages/core/src/extensions/glob/glob.ts +34 -0
- package/packages/core/src/extensions/glob/index.ts +83 -0
- package/packages/core/src/extensions/glob/render.ts +72 -0
- package/packages/core/src/extensions/glob/schema.ts +66 -0
- package/packages/core/src/extensions/grep/grep.ts +256 -0
- package/packages/core/src/extensions/grep/index.ts +114 -0
- package/packages/core/src/extensions/grep/render.ts +271 -0
- package/packages/core/src/extensions/grep/schema.ts +92 -0
- package/packages/core/src/extensions/read/ImageMemory.ts +42 -0
- package/packages/core/src/extensions/read/image.ts +57 -0
- package/packages/core/src/extensions/read/index.ts +88 -0
- package/packages/core/src/extensions/read/read.ts +191 -0
- package/packages/core/src/extensions/read/render.ts +58 -0
- package/packages/core/src/extensions/read/schema.ts +49 -0
- package/packages/core/src/extensions/subagent/index.ts +33 -0
- package/packages/core/src/extensions/subagent/render.ts +127 -0
- package/packages/core/src/extensions/subagent/schema.ts +9 -0
- package/packages/core/src/extensions/subagent/subagent.ts +419 -0
- package/packages/core/src/extensions/system-prompt/index.ts +28 -0
- package/packages/core/src/extensions/system-prompt/prompt.ts +228 -0
- package/packages/core/src/extensions/todo/index.ts +123 -0
- package/packages/core/src/extensions/todo/render.ts +156 -0
- package/packages/core/src/extensions/todo/schema.ts +24 -0
- package/packages/core/src/extensions/todo/todo.ts +185 -0
- package/packages/core/src/extensions/web-fetch/JinaReaderClient.ts +187 -0
- package/packages/core/src/extensions/web-fetch/WebViewFetchClient.ts +183 -0
- package/packages/core/src/extensions/web-fetch/WebViewMarkdownSnapshot.ts +512 -0
- package/packages/core/src/extensions/web-fetch/fetch.ts +265 -0
- package/packages/core/src/extensions/web-fetch/image.ts +165 -0
- package/packages/core/src/extensions/web-fetch/index.ts +83 -0
- package/packages/core/src/extensions/web-fetch/render.ts +100 -0
- package/packages/core/src/extensions/web-fetch/schema.ts +46 -0
- package/packages/core/src/extensions/web-search/ExaMcpClient.ts +251 -0
- package/packages/core/src/extensions/web-search/SearchBreaker.ts +147 -0
- package/packages/core/src/extensions/web-search/SearchChain.ts +77 -0
- package/packages/core/src/extensions/web-search/index.ts +90 -0
- package/packages/core/src/extensions/web-search/providers/DuckDuckGoProvider.ts +195 -0
- package/packages/core/src/extensions/web-search/providers/ExaProvider.ts +54 -0
- package/packages/core/src/extensions/web-search/providers/FirecrawlProvider.ts +144 -0
- package/packages/core/src/extensions/web-search/providers/SearchProvider.ts +118 -0
- package/packages/core/src/extensions/web-search/render.ts +49 -0
- package/packages/core/src/extensions/web-search/schema.ts +29 -0
- package/packages/core/src/extensions/web-search/search.ts +23 -0
- package/packages/core/src/extensions/write/index.ts +58 -0
- package/packages/core/src/extensions/write/render.ts +16 -0
- package/packages/core/src/extensions/write/schema.ts +14 -0
- package/packages/core/src/extensions/write/write.ts +87 -0
- package/packages/core/src/picker/FilePickerSuggestionEngine.ts +14 -0
- package/packages/core/src/picker/InProcessFilePickerSuggestionEngine.ts +52 -0
- package/packages/core/src/picker/PickerItem.ts +6 -0
- package/packages/core/src/picker/PickerService.ts +110 -0
- package/packages/core/src/picker/RemoteFilePickerSuggestionEngine.ts +56 -0
- package/packages/core/src/picker/WorkerFilePickerSuggestionEngine.ts +268 -0
- package/packages/core/src/picker/catalog.ts +220 -0
- package/packages/core/src/picker/commandRanker.ts +17 -0
- package/packages/core/src/picker/filePickerWorker.ts +66 -0
- package/packages/core/src/picker/filePickerWorkerMessages.ts +39 -0
- package/packages/core/src/picker/ranker.ts +245 -0
- package/packages/core/src/picker/token.ts +70 -0
- package/packages/core/src/session/AgentRuntime.ts +85 -0
- package/packages/core/src/session/EventLog.ts +213 -0
- package/packages/core/src/session/MessageText.ts +21 -0
- package/packages/core/src/session/ReadCursors.ts +94 -0
- package/packages/core/src/session/SessionCache.ts +68 -0
- package/packages/core/src/session/SessionHost.ts +876 -0
- package/packages/core/src/session/SessionLease.ts +279 -0
- package/packages/core/src/session/SessionRegistry.ts +169 -0
- package/packages/core/src/session/WriteMark.ts +37 -0
- package/packages/core/src/shared/Cli.ts +25 -0
- package/packages/core/src/shared/DaemonUnit.ts +13 -0
- package/packages/core/src/shared/DiffLines.ts +304 -0
- package/packages/core/src/shared/DiffRenderer.ts +283 -0
- package/packages/core/src/shared/DiffView.ts +29 -0
- package/packages/core/src/shared/Directories.ts +61 -0
- package/packages/core/src/shared/EditMatcher.ts +767 -0
- package/packages/core/src/shared/Errors.ts +9 -0
- package/packages/core/src/shared/ExtensionToggles.ts +130 -0
- package/packages/core/src/shared/FileEnumerator.ts +361 -0
- package/packages/core/src/shared/FileScanner.ts +57 -0
- package/packages/core/src/shared/FileWatch.ts +107 -0
- package/packages/core/src/shared/Format.ts +74 -0
- package/packages/core/src/shared/Fs.ts +91 -0
- package/packages/core/src/shared/FsErrors.ts +67 -0
- package/packages/core/src/shared/FuzzyMatcher.ts +71 -0
- package/packages/core/src/shared/Git.ts +89 -0
- package/packages/core/src/shared/GlobExclusions.ts +13 -0
- package/packages/core/src/shared/Http.ts +14 -0
- package/packages/core/src/shared/ImageMime.ts +36 -0
- package/packages/core/src/shared/Images.ts +357 -0
- package/packages/core/src/shared/Json.ts +19 -0
- package/packages/core/src/shared/Languages.ts +95 -0
- package/packages/core/src/shared/Levenshtein.ts +30 -0
- package/packages/core/src/shared/Lines.ts +79 -0
- package/packages/core/src/shared/McpClient.ts +444 -0
- package/packages/core/src/shared/OutputBudget.ts +84 -0
- package/packages/core/src/shared/PatchSummary.ts +40 -0
- package/packages/core/src/shared/Paths.ts +80 -0
- package/packages/core/src/shared/PimSettings.ts +128 -0
- package/packages/core/src/shared/PimVersion.ts +93 -0
- package/packages/core/src/shared/Pool.ts +26 -0
- package/packages/core/src/shared/Proc.ts +30 -0
- package/packages/core/src/shared/RateLimiter.ts +50 -0
- package/packages/core/src/shared/Renderer.ts +371 -0
- package/packages/core/src/shared/SafePath.ts +17 -0
- package/packages/core/src/shared/SearchRender.ts +82 -0
- package/packages/core/src/shared/SendFile.ts +23 -0
- package/packages/core/src/shared/SpillCache.ts +78 -0
- package/packages/core/src/shared/SubagentLogs.ts +102 -0
- package/packages/core/src/shared/Supervisor.ts +460 -0
- package/packages/core/src/shared/Sweeper.ts +32 -0
- package/packages/core/src/shared/Tools.ts +801 -0
- package/packages/core/src/shared/Updater.ts +193 -0
- package/packages/core/src/view/AnsiPainter.ts +227 -0
- package/packages/core/src/view/BodyRenderer.ts +102 -0
- package/packages/core/src/view/DiffBlocks.ts +52 -0
- package/packages/core/src/view/DiffLayout.ts +69 -0
- package/packages/core/src/view/MarkdownPainter.ts +314 -0
- package/packages/core/src/view/Painting.ts +182 -0
- package/packages/core/src/view/ViewBlock.ts +124 -0
- package/packages/daemon/src/Daemon.ts +63 -0
- package/packages/daemon/src/DaemonInstall.ts +115 -0
- package/packages/daemon/src/Surfaces.ts +66 -0
- package/packages/daemon/src/TelegramSurface.ts +30 -0
- package/packages/daemon/src/WebSurface.ts +57 -0
- package/packages/daemon/src/index.ts +37 -0
- package/packages/protocol/src/Command.ts +92 -0
- package/packages/protocol/src/ImageRoute.ts +23 -0
- package/packages/protocol/src/Protocol.ts +6 -0
- package/packages/protocol/src/ServerEvent.ts +251 -0
- package/packages/server/src/AttachmentEndpoint.ts +184 -0
- package/packages/server/src/ClientConnection.ts +265 -0
- package/packages/server/src/ImageEndpoint.ts +36 -0
- package/packages/server/src/ProbeClient.ts +312 -0
- package/packages/server/src/Reloader.ts +93 -0
- package/packages/server/src/SendFileTool.ts +88 -0
- package/packages/server/src/SessionCatalogue.ts +202 -0
- package/packages/server/src/SessionProjection.ts +194 -0
- package/packages/server/src/SessionStream.ts +601 -0
- package/packages/server/src/StaticClient.ts +95 -0
- package/packages/server/src/WebOptions.ts +72 -0
- package/packages/server/src/WsGateway.ts +563 -0
- package/packages/server/src/probe.ts +146 -0
- package/packages/telegram/src/Bot.ts +229 -0
- package/packages/telegram/src/Commands.ts +768 -0
- package/packages/telegram/src/Config.ts +148 -0
- package/packages/telegram/src/Markdown.ts +235 -0
- package/packages/telegram/src/Message.ts +152 -0
- package/packages/telegram/src/Renderer.ts +456 -0
- package/packages/telegram/src/SendFileSchema.ts +18 -0
- package/packages/telegram/src/SendFileTool.ts +65 -0
- package/packages/telegram/src/Session.ts +252 -0
- package/packages/telegram/src/SessionRegistry.ts +126 -0
- package/packages/telegram/src/TaskScheduler.ts +296 -0
- package/packages/telegram/src/TaskSchema.ts +88 -0
- package/packages/telegram/src/TaskStore.ts +67 -0
- package/packages/telegram/src/TaskTool.ts +208 -0
- package/packages/telegram/src/TelegramHtml.ts +288 -0
- package/packages/telegram/src/TypingIndicator.ts +43 -0
- package/packages/telegram/src/UpdateConfirm.ts +61 -0
- package/packages/tui/src/extensions/_init/index.ts +144 -0
- package/packages/tui/src/extensions/command-picker/index.ts +43 -0
- package/packages/tui/src/extensions/file-picker/index.ts +156 -0
- package/packages/tui/src/extensions/footer/index.ts +117 -0
- package/packages/tui/src/extensions/footer/powerline.ts +87 -0
- package/packages/tui/src/extensions/footer/segments.ts +202 -0
- package/packages/tui/src/extensions/pim/index.ts +89 -0
- package/packages/tui/src/extensions/session-lease/index.ts +248 -0
- package/packages/tui/src/extensions/tps/index.ts +118 -0
- package/packages/tui/src/extensions/working-indicator/index.ts +75 -0
- package/packages/tui/src/extensions/wrapProvider.ts +18 -0
- package/packages/tui/src/themes/pim-dark.json +84 -0
- package/packages/tui/src/themes/pim-light.json +84 -0
- package/packages/tui/src/themes/themeCliArgs.ts +20 -0
- package/packages/web/dist/client/apple-touch-icon.png +0 -0
- package/packages/web/dist/client/assets/bash-LmRjUKlR.js +1 -0
- package/packages/web/dist/client/assets/c-DBVbJFpC.js +1 -0
- package/packages/web/dist/client/assets/clojure-CR7dvLJ4.js +1 -0
- package/packages/web/dist/client/assets/cmake-CgP6gB4W.js +1 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-italic-B48RJMeK.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-italic-DBq5bJCE.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-italic-BXinMwCi.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-italic-DjSHLl2N.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-normal-s0S3qwFW.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-normal-wzhe4RuD.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-600-normal-BuQDXT0M.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-600-normal-SwgSWHaV.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-700-normal-1-GU0IUE.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-700-normal-DU8mrtj2.woff2 +0 -0
- package/packages/web/dist/client/assets/core-CGdQx9la.js +3 -0
- package/packages/web/dist/client/assets/cpp-N2d8M7Nc.js +1 -0
- package/packages/web/dist/client/assets/csharp-yYt0Ikks.js +1 -0
- package/packages/web/dist/client/assets/css-D8M8GYaw.js +1 -0
- package/packages/web/dist/client/assets/diff-BhdZizse.js +1 -0
- package/packages/web/dist/client/assets/dockerfile-C0zufGsU.js +1 -0
- package/packages/web/dist/client/assets/elixir-DXIHlGsR.js +1 -0
- package/packages/web/dist/client/assets/erlang-BjYI8dhz.js +1 -0
- package/packages/web/dist/client/assets/go-CYstVVJV.js +1 -0
- package/packages/web/dist/client/assets/graphql-BHHhacaR.js +1 -0
- package/packages/web/dist/client/assets/haskell-DUkhXIda.js +1 -0
- package/packages/web/dist/client/assets/index-BMNwleWO.js +40 -0
- package/packages/web/dist/client/assets/index-CqjB2RPr.css +1 -0
- package/packages/web/dist/client/assets/ini-B1CroMsf.js +1 -0
- package/packages/web/dist/client/assets/java-Dde-Wnb4.js +1 -0
- package/packages/web/dist/client/assets/javascript-8AmHuH5O.js +1 -0
- package/packages/web/dist/client/assets/json-PmyKWHmm.js +1 -0
- package/packages/web/dist/client/assets/kotlin-DgSK8bu-.js +2 -0
- package/packages/web/dist/client/assets/less-Bp0mgmpG.js +1 -0
- package/packages/web/dist/client/assets/lua-1dXYaL-H.js +1 -0
- package/packages/web/dist/client/assets/makefile-BX-o3EIz.js +1 -0
- package/packages/web/dist/client/assets/markdown-D6FrnlqJ.js +1 -0
- package/packages/web/dist/client/assets/ocaml-B-P1Uyq8.js +1 -0
- package/packages/web/dist/client/assets/perl-CvqMZYuf.js +1 -0
- package/packages/web/dist/client/assets/php-DuZl5e-L.js +2 -0
- package/packages/web/dist/client/assets/powershell-BUe16zE4.js +1 -0
- package/packages/web/dist/client/assets/protobuf-DGOW1qT8.js +1 -0
- package/packages/web/dist/client/assets/python-gHmu6VPG.js +1 -0
- package/packages/web/dist/client/assets/r-DNWPCnjT.js +1 -0
- package/packages/web/dist/client/assets/ruby-BGpZ3p4_.js +1 -0
- package/packages/web/dist/client/assets/rust-BIKPVoi6.js +1 -0
- package/packages/web/dist/client/assets/scala-C9lP4FWY.js +1 -0
- package/packages/web/dist/client/assets/scss-gOyOKiXd.js +1 -0
- package/packages/web/dist/client/assets/sql-DWRKyvJ-.js +1 -0
- package/packages/web/dist/client/assets/swift-BTn9__hL.js +1 -0
- package/packages/web/dist/client/assets/typescript-CxkPLmSc.js +1 -0
- package/packages/web/dist/client/assets/vim-Dhhcn_CA.js +1 -0
- package/packages/web/dist/client/assets/xml-COl5yFSC.js +1 -0
- package/packages/web/dist/client/assets/yaml-DjOhhVLG.js +1 -0
- package/packages/web/dist/client/favicon.svg +1 -0
- package/packages/web/dist/client/icon-192.png +0 -0
- package/packages/web/dist/client/icon-512.png +0 -0
- package/packages/web/dist/client/icon-maskable.png +0 -0
- package/packages/web/dist/client/index.html +24 -0
- package/packages/web/dist/client/manifest.webmanifest +21 -0
- package/packages/web/dist/client/wordmark.svg +1 -0
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Context,
|
|
3
|
+
type Filter,
|
|
4
|
+
GrammyError,
|
|
5
|
+
InlineKeyboard,
|
|
6
|
+
type Api,
|
|
7
|
+
} from "grammy";
|
|
8
|
+
|
|
9
|
+
import { DaemonUnit } from "#core/shared/DaemonUnit";
|
|
10
|
+
import { Paths } from "#core/shared/Paths";
|
|
11
|
+
import { Supervisor } from "#core/shared/Supervisor";
|
|
12
|
+
import { Updater } from "#core/shared/Updater";
|
|
13
|
+
import {
|
|
14
|
+
LOGS_MODES,
|
|
15
|
+
THINKING_LEVELS,
|
|
16
|
+
type LogsMode,
|
|
17
|
+
type TelegramConfig,
|
|
18
|
+
} from "./Config";
|
|
19
|
+
import { Markdown } from "./Markdown";
|
|
20
|
+
import {
|
|
21
|
+
decodeId,
|
|
22
|
+
encodeId,
|
|
23
|
+
Session,
|
|
24
|
+
type SessionCompactResult,
|
|
25
|
+
type SessionId,
|
|
26
|
+
} from "./Session";
|
|
27
|
+
import { SessionRegistry } from "./SessionRegistry";
|
|
28
|
+
import { TypingIndicator } from "./TypingIndicator";
|
|
29
|
+
import { UpdateConfirm } from "./UpdateConfirm";
|
|
30
|
+
|
|
31
|
+
const CB_CLEAR_CONFIRM = "clear-confirm";
|
|
32
|
+
const CB_CLEAR_CANCEL = "clear-cancel";
|
|
33
|
+
const CB_EFFORT = "effort";
|
|
34
|
+
const CB_LOGS = "logs";
|
|
35
|
+
const CB_MODEL = "model";
|
|
36
|
+
const CB_TEMPORARY = "temporary";
|
|
37
|
+
|
|
38
|
+
type BotCommand = { readonly command: string; readonly description: string };
|
|
39
|
+
|
|
40
|
+
type CommandSpec = {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly description: string;
|
|
43
|
+
readonly queued?: (args: string) => boolean;
|
|
44
|
+
readonly run: (
|
|
45
|
+
self: Commands,
|
|
46
|
+
session: Session,
|
|
47
|
+
args: string
|
|
48
|
+
) => Promise<void>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const LOGS_DESCRIPTIONS: Record<LogsMode, string> = {
|
|
52
|
+
off: "final message only",
|
|
53
|
+
tool: "show tool use",
|
|
54
|
+
text: "show tool use, and intermediate texts",
|
|
55
|
+
verbose: "show tool use, intermediate texts, and thinking",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type Picker = {
|
|
59
|
+
readonly action: string;
|
|
60
|
+
readonly title: string;
|
|
61
|
+
readonly header: string;
|
|
62
|
+
readonly columns: number;
|
|
63
|
+
readonly values: (session: Session) => readonly string[];
|
|
64
|
+
readonly label: (value: string) => string;
|
|
65
|
+
readonly current: (session: Session) => string;
|
|
66
|
+
readonly apply: (
|
|
67
|
+
value: string
|
|
68
|
+
) => ((session: Session) => Promise<void>) | undefined;
|
|
69
|
+
readonly body?: readonly string[];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const EFFORT_PICKER: Picker = {
|
|
73
|
+
action: CB_EFFORT,
|
|
74
|
+
title: "Effort",
|
|
75
|
+
header: "Effort",
|
|
76
|
+
columns: 3,
|
|
77
|
+
values: (session) => session.supportedThinkingLevels,
|
|
78
|
+
label: (value) => value,
|
|
79
|
+
current: (session) => session.currentThinkingLevel,
|
|
80
|
+
apply: (value) =>
|
|
81
|
+
isMember(THINKING_LEVELS, value)
|
|
82
|
+
? (session) => session.setThinkingLevel(value)
|
|
83
|
+
: undefined,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const LOGS_PICKER: Picker = {
|
|
87
|
+
action: CB_LOGS,
|
|
88
|
+
title: "Logs",
|
|
89
|
+
header: "Level",
|
|
90
|
+
columns: 2,
|
|
91
|
+
values: () => LOGS_MODES,
|
|
92
|
+
label: (value) => value,
|
|
93
|
+
current: (session) => session.settings.logsMode ?? "text",
|
|
94
|
+
apply: (value) =>
|
|
95
|
+
isMember(LOGS_MODES, value)
|
|
96
|
+
? (session) => session.setLogsMode(value)
|
|
97
|
+
: undefined,
|
|
98
|
+
body: [
|
|
99
|
+
"",
|
|
100
|
+
`<b>Options</b>:`,
|
|
101
|
+
...LOGS_MODES.map(
|
|
102
|
+
(mode) =>
|
|
103
|
+
`• <code>${Markdown.escape(mode)}</code>: ${Markdown.escape(LOGS_DESCRIPTIONS[mode])}`
|
|
104
|
+
),
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const TEMPORARY_PICKER: Picker = {
|
|
109
|
+
action: CB_TEMPORARY,
|
|
110
|
+
title: "Temporary",
|
|
111
|
+
header: "Temporary",
|
|
112
|
+
columns: 2,
|
|
113
|
+
values: () => ["0", "1"],
|
|
114
|
+
label: (value) => (value === "1" ? "on" : "off"),
|
|
115
|
+
current: (session) => (session.temporary ? "1" : "0"),
|
|
116
|
+
apply: (value) =>
|
|
117
|
+
value === "0" || value === "1"
|
|
118
|
+
? (session) => session.setTemporary(value === "1")
|
|
119
|
+
: undefined,
|
|
120
|
+
body: [
|
|
121
|
+
"",
|
|
122
|
+
"When <b>on</b>, every message is independent and runs in a fresh session without any chat history.",
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const PICKERS: readonly Picker[] = [
|
|
127
|
+
EFFORT_PICKER,
|
|
128
|
+
LOGS_PICKER,
|
|
129
|
+
TEMPORARY_PICKER,
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
function buildPicker(
|
|
133
|
+
picker: Picker,
|
|
134
|
+
session: Session,
|
|
135
|
+
current: string
|
|
136
|
+
): { readonly kb: InlineKeyboard; readonly html: string } {
|
|
137
|
+
const key = encodeId(session.id);
|
|
138
|
+
const kb = new InlineKeyboard();
|
|
139
|
+
const values = picker.values(session);
|
|
140
|
+
for (const [i, value] of values.entries()) {
|
|
141
|
+
const label = picker.label(value);
|
|
142
|
+
kb.text(
|
|
143
|
+
value === current ? `✅ ${label}` : label,
|
|
144
|
+
`${picker.action}:${value}:${key}`
|
|
145
|
+
);
|
|
146
|
+
if ((i + 1) % picker.columns === 0 && i < values.length - 1) {
|
|
147
|
+
kb.row();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const html = [
|
|
151
|
+
`<b>${picker.header}</b>: <code>${Markdown.escape(picker.label(current))}</code>`,
|
|
152
|
+
...(picker.body ?? []),
|
|
153
|
+
].join("\n");
|
|
154
|
+
return { kb, html };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function splitValueAndKey(
|
|
158
|
+
s: string
|
|
159
|
+
): { readonly value: string; readonly key: string } | undefined {
|
|
160
|
+
const i = s.indexOf(":");
|
|
161
|
+
if (i < 0) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
return { value: s.slice(0, i), key: s.slice(i + 1) };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isMember<T extends string>(
|
|
168
|
+
tuple: readonly T[],
|
|
169
|
+
value: string
|
|
170
|
+
): value is T {
|
|
171
|
+
return (tuple as readonly string[]).includes(value);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export class Commands {
|
|
175
|
+
private readonly config: TelegramConfig;
|
|
176
|
+
private readonly api: Api;
|
|
177
|
+
private readonly registry: SessionRegistry;
|
|
178
|
+
|
|
179
|
+
private static readonly COMMANDS: readonly CommandSpec[] = [
|
|
180
|
+
{
|
|
181
|
+
name: "chatid",
|
|
182
|
+
description: "Show this chat's numeric ID",
|
|
183
|
+
run: (self, session) => self.cmdChatId(session),
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "cancel",
|
|
187
|
+
description: "Cancel the current turn",
|
|
188
|
+
run: (self, session) => self.cmdCancel(session),
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "clear",
|
|
192
|
+
description: "Reset chat history and context window",
|
|
193
|
+
queued: () => true,
|
|
194
|
+
run: (self, session) => self.cmdClear(session),
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "compact",
|
|
198
|
+
description: "Compact the current session context",
|
|
199
|
+
queued: () => true,
|
|
200
|
+
run: (self, session, args) => self.cmdCompact(session, args || undefined),
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "cd",
|
|
204
|
+
description: "Show or change the working directory",
|
|
205
|
+
queued: (args) => args.length > 0,
|
|
206
|
+
run: (self, session, args) =>
|
|
207
|
+
args ? self.cmdCdWrite(session, args) : self.cmdCdRead(session),
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: "model",
|
|
211
|
+
description: "Show or change the AI model",
|
|
212
|
+
queued: (args) => args.length > 0,
|
|
213
|
+
run: (self, session, args) =>
|
|
214
|
+
args ? self.cmdModelWrite(session, args) : self.cmdModelRead(session),
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "effort",
|
|
218
|
+
description: "Show or change thinking effort level",
|
|
219
|
+
run: (self, session) => self.cmdEffort(session),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "usage",
|
|
223
|
+
description: "Show context window and session cost",
|
|
224
|
+
run: (self, session) => self.cmdUsage(session),
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: "logs",
|
|
228
|
+
description: "Show or change log verbosity",
|
|
229
|
+
run: (self, session) => self.showPicker(LOGS_PICKER, session),
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: "temporary",
|
|
233
|
+
description: "Toggle temporary chat (no history, fresh each message)",
|
|
234
|
+
run: (self, session) => self.showPicker(TEMPORARY_PICKER, session),
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "update",
|
|
238
|
+
description: "Update the bot to the latest version",
|
|
239
|
+
queued: () => true,
|
|
240
|
+
run: (self, session) => self.cmdUpdate(session),
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: "commands",
|
|
244
|
+
description: "Register all commands with Telegram",
|
|
245
|
+
run: (self, session) => self.cmdCommands(session),
|
|
246
|
+
},
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
public static botCommands(): readonly BotCommand[] {
|
|
250
|
+
return Commands.COMMANDS.map(({ name, description }) => ({
|
|
251
|
+
command: name,
|
|
252
|
+
description,
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
public constructor(
|
|
257
|
+
config: TelegramConfig,
|
|
258
|
+
api: Api,
|
|
259
|
+
registry: SessionRegistry
|
|
260
|
+
) {
|
|
261
|
+
this.config = config;
|
|
262
|
+
this.api = api;
|
|
263
|
+
this.registry = registry;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
public async handleCommand(
|
|
267
|
+
ctx: Filter<Context, "message">,
|
|
268
|
+
session: Session,
|
|
269
|
+
raw: string
|
|
270
|
+
): Promise<void> {
|
|
271
|
+
const [first, ...rest] = raw.trim().split(/\s+/);
|
|
272
|
+
const name = (first ?? "").split("@")[0];
|
|
273
|
+
const args = rest.join(" ").trim();
|
|
274
|
+
const spec = Commands.COMMANDS.find((one) => `/${one.name}` === name);
|
|
275
|
+
try {
|
|
276
|
+
if (!spec) {
|
|
277
|
+
await this.sendPlain(session.id, `Unknown command: ${name}`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (spec.queued?.(args) === true) {
|
|
281
|
+
await this.runQueued(ctx, session, () => spec.run(this, session, args));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
await spec.run(this, session, args);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
console.error(`[bot] command ${name} failed:`, err);
|
|
287
|
+
await this.sendPlain(
|
|
288
|
+
session.id,
|
|
289
|
+
`⚠️ ${name} failed: ${(err as Error).message}`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
public async handleCallback(
|
|
295
|
+
ctx: Filter<Context, "callback_query:data">
|
|
296
|
+
): Promise<void> {
|
|
297
|
+
const data = ctx.callbackQuery.data;
|
|
298
|
+
|
|
299
|
+
if (data.startsWith(`${CB_MODEL}|`)) {
|
|
300
|
+
const idx1 = data.indexOf("|");
|
|
301
|
+
const idx2 = data.lastIndexOf("|");
|
|
302
|
+
const modelId = data.slice(idx1 + 1, idx2);
|
|
303
|
+
const keyPart = data.slice(idx2 + 1);
|
|
304
|
+
const session = this.registry.get(decodeId(keyPart));
|
|
305
|
+
await ctx.answerCallbackQuery({ text: `Model: ${modelId}` });
|
|
306
|
+
try {
|
|
307
|
+
const result = await session.setModel(modelId);
|
|
308
|
+
if (result.ok) {
|
|
309
|
+
await safeEditMessage(
|
|
310
|
+
ctx,
|
|
311
|
+
`<b>Model</b> → <code>${Markdown.escape(result.id)}</code>`
|
|
312
|
+
);
|
|
313
|
+
} else {
|
|
314
|
+
await safeEditMessage(
|
|
315
|
+
ctx,
|
|
316
|
+
strikeOriginal(ctx, `⚠️ model set failed: ${modelId}`)
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
} catch (err) {
|
|
320
|
+
console.error(`[bot] model callback failed for ${modelId}:`, err);
|
|
321
|
+
await safeEditMessage(
|
|
322
|
+
ctx,
|
|
323
|
+
strikeOriginal(ctx, `⚠️ model set failed: ${(err as Error).message}`)
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const colon = data.indexOf(":");
|
|
330
|
+
const action = colon >= 0 ? data.slice(0, colon) : data;
|
|
331
|
+
const keyPart = colon >= 0 ? data.slice(colon + 1) : "";
|
|
332
|
+
|
|
333
|
+
if (action === CB_CLEAR_CONFIRM && keyPart) {
|
|
334
|
+
const session = this.registry.get(decodeId(keyPart));
|
|
335
|
+
const wasBusy = session.isStreaming;
|
|
336
|
+
await ctx.answerCallbackQuery({
|
|
337
|
+
text: wasBusy ? "Queued — clearing after current turn" : "Cleared",
|
|
338
|
+
});
|
|
339
|
+
try {
|
|
340
|
+
await session.clear();
|
|
341
|
+
await safeEditMessage(
|
|
342
|
+
ctx,
|
|
343
|
+
strikeOriginal(ctx, "Context window cleared.")
|
|
344
|
+
);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
console.error(`[bot] queued clear failed:`, err);
|
|
347
|
+
await safeEditMessage(
|
|
348
|
+
ctx,
|
|
349
|
+
strikeOriginal(ctx, `⚠️ clear failed: ${(err as Error).message}`)
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (action === CB_CLEAR_CANCEL) {
|
|
355
|
+
await ctx.answerCallbackQuery({ text: "Cancelled" });
|
|
356
|
+
await safeEditMessage(ctx, strikeOriginal(ctx, "Cancelled."));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const picker = PICKERS.find((one) => one.action === action);
|
|
360
|
+
if (picker && keyPart) {
|
|
361
|
+
const parts = splitValueAndKey(keyPart);
|
|
362
|
+
const applyTo = parts && picker.apply(parts.value);
|
|
363
|
+
if (!parts || !applyTo) {
|
|
364
|
+
await ctx.answerCallbackQuery();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const session = this.registry.get(decodeId(parts.key));
|
|
368
|
+
await applyTo(session);
|
|
369
|
+
await ctx.answerCallbackQuery({
|
|
370
|
+
text: `${picker.title}: ${picker.label(parts.value)}`,
|
|
371
|
+
});
|
|
372
|
+
const { kb, html } = buildPicker(picker, session, parts.value);
|
|
373
|
+
await safeEditMessage(ctx, html, kb);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
await ctx.answerCallbackQuery();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private async runQueued(
|
|
380
|
+
ctx: Filter<Context, "message">,
|
|
381
|
+
session: Session,
|
|
382
|
+
work: () => Promise<void>
|
|
383
|
+
): Promise<void> {
|
|
384
|
+
const wasBusy = session.isStreaming;
|
|
385
|
+
if (wasBusy) {
|
|
386
|
+
await reactSafe(ctx, "👀");
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
await work();
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.error(`[bot] queued command failed:`, err);
|
|
392
|
+
await this.sendPlain(
|
|
393
|
+
session.id,
|
|
394
|
+
`⚠️ ${(err as Error).message ?? String(err)}`
|
|
395
|
+
);
|
|
396
|
+
} finally {
|
|
397
|
+
if (wasBusy) {
|
|
398
|
+
await reactSafe(ctx, []);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private async cmdChatId(session: Session): Promise<void> {
|
|
404
|
+
const lines = [`Chat ID: <code>${session.id.chatId}</code>`];
|
|
405
|
+
if (session.id.threadId) {
|
|
406
|
+
lines.push(`Thread ID: <code>${session.id.threadId}</code>`);
|
|
407
|
+
}
|
|
408
|
+
await this.sendWithFallback(session.id, lines.join("\n"));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private async cmdCancel(session: Session): Promise<void> {
|
|
412
|
+
const cancelled = await session.cancel();
|
|
413
|
+
await this.sendPlain(
|
|
414
|
+
session.id,
|
|
415
|
+
cancelled ? "❌ Cancelled." : "Nothing to cancel."
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private async cmdClear(session: Session): Promise<void> {
|
|
420
|
+
const key = encodeId(session.id);
|
|
421
|
+
const kb = new InlineKeyboard()
|
|
422
|
+
.text("🚫 Cancel", `${CB_CLEAR_CANCEL}:${key}`)
|
|
423
|
+
.text("👍 Yes", `${CB_CLEAR_CONFIRM}:${key}`);
|
|
424
|
+
await this.sendPlain(
|
|
425
|
+
session.id,
|
|
426
|
+
"⚠️ Are you sure you want to reset this thread's chat history and context window?",
|
|
427
|
+
kb
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private async cmdCompact(
|
|
432
|
+
session: Session,
|
|
433
|
+
customInstructions?: string
|
|
434
|
+
): Promise<void> {
|
|
435
|
+
const sent = await this.api.sendMessage(
|
|
436
|
+
session.id.chatId,
|
|
437
|
+
"⏳ Compacting context...",
|
|
438
|
+
{
|
|
439
|
+
message_thread_id: session.id.threadId,
|
|
440
|
+
link_preview_options: { is_disabled: true },
|
|
441
|
+
}
|
|
442
|
+
);
|
|
443
|
+
const typing = new TypingIndicator(this.api, session.id);
|
|
444
|
+
typing.start();
|
|
445
|
+
try {
|
|
446
|
+
const result = await session.compact(customInstructions);
|
|
447
|
+
await this.editStatusMessage(
|
|
448
|
+
session.id,
|
|
449
|
+
sent.message_id,
|
|
450
|
+
renderCompactSuccess(result)
|
|
451
|
+
);
|
|
452
|
+
} catch (err) {
|
|
453
|
+
const msg = (err as Error).message ?? String(err);
|
|
454
|
+
console.error(`[bot] compact failed:`, err);
|
|
455
|
+
await this.editStatusMessage(
|
|
456
|
+
session.id,
|
|
457
|
+
sent.message_id,
|
|
458
|
+
`⚠️ ${Markdown.escape(msg)}`
|
|
459
|
+
);
|
|
460
|
+
} finally {
|
|
461
|
+
typing.stop();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private async cmdCdRead(session: Session): Promise<void> {
|
|
466
|
+
const cwd = Paths.abbreviateHome(session.settings.cwd ?? this.config.cwd);
|
|
467
|
+
const lines = [
|
|
468
|
+
`<b>CWD</b>: <code>${Markdown.escape(cwd)}</code>`,
|
|
469
|
+
`<b>To Change</b>: <code>/cd <path></code>`,
|
|
470
|
+
];
|
|
471
|
+
await this.sendWithFallback(session.id, lines.join("\n"));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private async cmdCdWrite(session: Session, args: string): Promise<void> {
|
|
475
|
+
const resolved = Paths.resolve(
|
|
476
|
+
args,
|
|
477
|
+
session.settings.cwd ?? this.config.cwd
|
|
478
|
+
);
|
|
479
|
+
const result = await session.setCwd(resolved);
|
|
480
|
+
if (!result.ok) {
|
|
481
|
+
await this.sendPlain(session.id, `⚠️ ${result.error}`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const html = `<b>CWD</b> → <code>${Markdown.escape(Paths.abbreviateHome(resolved))}</code>`;
|
|
485
|
+
await this.sendWithFallback(session.id, html);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private async cmdModelRead(session: Session): Promise<void> {
|
|
489
|
+
const current = session.currentModelId ?? "(unset)";
|
|
490
|
+
const html = [
|
|
491
|
+
`<b>Model</b>: <code>${Markdown.escape(current)}</code>`,
|
|
492
|
+
`<b>To Change</b>: <code>/model <model_name></code>`,
|
|
493
|
+
].join("\n");
|
|
494
|
+
await this.sendWithFallback(session.id, html);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private async cmdModelWrite(session: Session, args: string): Promise<void> {
|
|
498
|
+
const result = await session.setModel(args);
|
|
499
|
+
if (!result.ok) {
|
|
500
|
+
const key = encodeId(session.id);
|
|
501
|
+
const kb = new InlineKeyboard();
|
|
502
|
+
for (const c of result.candidates) {
|
|
503
|
+
kb.text(c, `${CB_MODEL}|${c}|${key}`).row();
|
|
504
|
+
}
|
|
505
|
+
const header =
|
|
506
|
+
result.kind === "ambiguous"
|
|
507
|
+
? `⚠️ Multiple matches for "${Markdown.escape(args)}". Please choose one below or use /model with a more specific name.`
|
|
508
|
+
: `⚠️ No model matches "${Markdown.escape(args)}". Available:`;
|
|
509
|
+
await this.sendWithFallback(session.id, header, kb);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
await this.sendWithFallback(
|
|
513
|
+
session.id,
|
|
514
|
+
`<b>Model</b> → <code>${Markdown.escape(result.id)}</code>`
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private async cmdEffort(session: Session): Promise<void> {
|
|
519
|
+
const supported = session.supportedThinkingLevels;
|
|
520
|
+
if (supported.length <= 1) {
|
|
521
|
+
await this.sendPlain(
|
|
522
|
+
session.id,
|
|
523
|
+
"Effort level for the current model cannot be configured."
|
|
524
|
+
);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
await this.showPicker(EFFORT_PICKER, session);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private async showPicker(picker: Picker, session: Session): Promise<void> {
|
|
531
|
+
const { kb, html } = buildPicker(picker, session, picker.current(session));
|
|
532
|
+
await this.sendWithFallback(session.id, html, kb);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private async cmdUsage(session: Session): Promise<void> {
|
|
536
|
+
const lines: string[] = [];
|
|
537
|
+
const usage = session.usage();
|
|
538
|
+
if (usage) {
|
|
539
|
+
const pct = usage.percent !== null ? `${usage.percent.toFixed(1)}%` : "—";
|
|
540
|
+
const tok =
|
|
541
|
+
usage.tokens !== null ? usage.tokens.toLocaleString("en-US") : "—";
|
|
542
|
+
const ctx = usage.contextWindow.toLocaleString("en-US");
|
|
543
|
+
lines.push(`<b>Context</b>: <code>${tok}/${ctx} (${pct})</code>`);
|
|
544
|
+
}
|
|
545
|
+
const cost = session.sessionCost();
|
|
546
|
+
if (cost !== undefined) {
|
|
547
|
+
lines.push(`<b>Session Cost</b>: <code>$${cost.toFixed(2)}</code>`);
|
|
548
|
+
}
|
|
549
|
+
lines.push(
|
|
550
|
+
`<b>Cumulative Cost</b>: <code>$${(session.settings.cumulativeCost ?? 0).toFixed(2)}</code>`
|
|
551
|
+
);
|
|
552
|
+
await this.sendWithFallback(session.id, lines.join("\n"));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private async cmdUpdate(session: Session): Promise<void> {
|
|
556
|
+
const sent = await this.api.sendMessage(
|
|
557
|
+
session.id.chatId,
|
|
558
|
+
"🔄 Updating...",
|
|
559
|
+
{
|
|
560
|
+
message_thread_id: session.id.threadId,
|
|
561
|
+
link_preview_options: { is_disabled: true },
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
const progress = async (text: string): Promise<void> => {
|
|
565
|
+
await this.api
|
|
566
|
+
.editMessageText(session.id.chatId, sent.message_id, text, {
|
|
567
|
+
link_preview_options: { is_disabled: true },
|
|
568
|
+
})
|
|
569
|
+
.catch((err: unknown) => console.warn(`[update] edit failed:`, err));
|
|
570
|
+
};
|
|
571
|
+
const outcome = await Updater.run({
|
|
572
|
+
onStep: (label) => progress(`🔄 ${label}...`),
|
|
573
|
+
});
|
|
574
|
+
if (!outcome.ok) {
|
|
575
|
+
await this.sendPlain(session.id, `⚠️ ${outcome.error}`);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const moved =
|
|
580
|
+
outcome.to === outcome.from
|
|
581
|
+
? `Already on v${outcome.to}`
|
|
582
|
+
: `Updated v${outcome.from} → v${outcome.to}`;
|
|
583
|
+
const notes = outcome.skipped.map(
|
|
584
|
+
(s) => `\nSkipped ${s.label}: ${s.reason}.`
|
|
585
|
+
);
|
|
586
|
+
// Restart siblings first; this daemon restarts by exiting, so it must go last.
|
|
587
|
+
await Supervisor.restartSiblings(DaemonUnit);
|
|
588
|
+
if (!Supervisor.isSupervised()) {
|
|
589
|
+
await progress(
|
|
590
|
+
`✅ ${moved}.${notes.join("")}\nNo supervisor is watching this process, so restart it yourself.`
|
|
591
|
+
);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
await UpdateConfirm.append(this.config.configDir, {
|
|
596
|
+
chatId: session.id.chatId,
|
|
597
|
+
threadId: session.id.threadId,
|
|
598
|
+
messageId: sent.message_id,
|
|
599
|
+
});
|
|
600
|
+
Supervisor.restart();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private async cmdCommands(session: Session): Promise<void> {
|
|
604
|
+
const chatId = session.id.chatId;
|
|
605
|
+
try {
|
|
606
|
+
const globalScopes = [
|
|
607
|
+
{ type: "default" as const },
|
|
608
|
+
{ type: "all_private_chats" as const },
|
|
609
|
+
{ type: "all_group_chats" as const },
|
|
610
|
+
];
|
|
611
|
+
await Promise.all(
|
|
612
|
+
globalScopes.map((scope) =>
|
|
613
|
+
this.api.setMyCommands(BOT_COMMANDS, { scope })
|
|
614
|
+
)
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
await this.api.deleteMyCommands({
|
|
618
|
+
scope: { type: "chat", chat_id: chatId },
|
|
619
|
+
});
|
|
620
|
+
try {
|
|
621
|
+
await this.api.deleteMyCommands({
|
|
622
|
+
scope: { type: "chat_administrators", chat_id: chatId },
|
|
623
|
+
});
|
|
624
|
+
} catch {
|
|
625
|
+
// chat_administrators scope is only valid for group chats.
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const chat = await this.api.getChat(chatId);
|
|
629
|
+
const resolvedScope =
|
|
630
|
+
chat.type === "private"
|
|
631
|
+
? ({ type: "all_private_chats" } as const)
|
|
632
|
+
: ({ type: "all_group_chats" } as const);
|
|
633
|
+
const actual = await this.api.getMyCommands({
|
|
634
|
+
scope: resolvedScope,
|
|
635
|
+
});
|
|
636
|
+
const actualMap = new Set(actual.map((c) => c.command));
|
|
637
|
+
const lines = [
|
|
638
|
+
"✅ <b>Commands registered</b> and chat-scoped overrides cleared:",
|
|
639
|
+
"",
|
|
640
|
+
...BOT_COMMANDS.map((c) => {
|
|
641
|
+
const ok = actualMap.has(c.command) ? "✅" : "❌";
|
|
642
|
+
return `${ok} <code>/${c.command}</code> — ${Markdown.escape(c.description)}`;
|
|
643
|
+
}),
|
|
644
|
+
];
|
|
645
|
+
if (actual.length !== BOT_COMMANDS.length) {
|
|
646
|
+
lines.push(
|
|
647
|
+
"",
|
|
648
|
+
`⚠️ <b>${BOT_COMMANDS.length}</b> sent but <b>${actual.length}</b> resolved for this chat. Restart Telegram if commands don't show in autocomplete.`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
await this.sendWithFallback(session.id, lines.join("\n"));
|
|
652
|
+
} catch (err) {
|
|
653
|
+
const msg = (err as Error).message ?? String(err);
|
|
654
|
+
console.error("[bot] setMyCommands failed:", err);
|
|
655
|
+
await this.sendPlain(
|
|
656
|
+
session.id,
|
|
657
|
+
`⚠️ Failed to register commands: ${Markdown.escape(msg)}`
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
private async editStatusMessage(
|
|
663
|
+
sessionId: SessionId,
|
|
664
|
+
messageId: number,
|
|
665
|
+
html: string
|
|
666
|
+
): Promise<void> {
|
|
667
|
+
try {
|
|
668
|
+
await this.api.editMessageText(sessionId.chatId, messageId, html, {
|
|
669
|
+
parse_mode: "HTML",
|
|
670
|
+
link_preview_options: { is_disabled: true },
|
|
671
|
+
});
|
|
672
|
+
} catch (err) {
|
|
673
|
+
console.warn(`[send] status edit failed:`, err);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private async sendWithFallback(
|
|
678
|
+
sessionId: SessionId,
|
|
679
|
+
html: string,
|
|
680
|
+
replyMarkup?: InlineKeyboard
|
|
681
|
+
): Promise<void> {
|
|
682
|
+
if (!html) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
await this.api.sendMessage(sessionId.chatId, html, {
|
|
687
|
+
parse_mode: "HTML",
|
|
688
|
+
message_thread_id: sessionId.threadId,
|
|
689
|
+
link_preview_options: { is_disabled: true },
|
|
690
|
+
reply_markup: replyMarkup,
|
|
691
|
+
});
|
|
692
|
+
console.log(
|
|
693
|
+
`[send] chatId=${sessionId.chatId} threadId=${sessionId.threadId ?? "main"} html ok (${html.length}b)`
|
|
694
|
+
);
|
|
695
|
+
} catch (err) {
|
|
696
|
+
if (err instanceof GrammyError && err.error_code === 400) {
|
|
697
|
+
console.warn(`[send] HTML 400 (${err.description}) — retry plain`);
|
|
698
|
+
await this.sendPlain(sessionId, html, replyMarkup);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
throw err;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
private async sendPlain(
|
|
706
|
+
sessionId: SessionId,
|
|
707
|
+
body: string,
|
|
708
|
+
replyMarkup?: InlineKeyboard
|
|
709
|
+
): Promise<void> {
|
|
710
|
+
try {
|
|
711
|
+
await this.api.sendMessage(sessionId.chatId, body, {
|
|
712
|
+
message_thread_id: sessionId.threadId,
|
|
713
|
+
link_preview_options: { is_disabled: true },
|
|
714
|
+
reply_markup: replyMarkup,
|
|
715
|
+
});
|
|
716
|
+
console.log(
|
|
717
|
+
`[send] chatId=${sessionId.chatId} threadId=${sessionId.threadId ?? "main"} plain ok (${body.length}b)`
|
|
718
|
+
);
|
|
719
|
+
} catch (err) {
|
|
720
|
+
console.error(`[send] plain failed:`, err);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function reactSafe(
|
|
726
|
+
ctx: Filter<Context, "message">,
|
|
727
|
+
reaction: "👀" | []
|
|
728
|
+
): Promise<void> {
|
|
729
|
+
await ctx.react(reaction).catch((err: unknown) => {
|
|
730
|
+
console.warn(`[bot] react failed:`, err);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export const BOT_COMMANDS: readonly BotCommand[] = Commands.botCommands();
|
|
735
|
+
|
|
736
|
+
function strikeOriginal(
|
|
737
|
+
ctx: Filter<Context, "callback_query:data">,
|
|
738
|
+
note: string
|
|
739
|
+
): string {
|
|
740
|
+
const original = ctx.callbackQuery.message?.text ?? "";
|
|
741
|
+
return `<s>${Markdown.escape(original)}</s>\n\n<i>${note}</i>`;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function renderCompactSuccess(result: SessionCompactResult): string {
|
|
745
|
+
const before = result.compaction.tokensBefore.toLocaleString("en-US");
|
|
746
|
+
const messages = result.activeMessages.toLocaleString("en-US");
|
|
747
|
+
return [
|
|
748
|
+
"✅ <b>Context compacted.</b>",
|
|
749
|
+
"",
|
|
750
|
+
`<b>Before</b>: ${before} tokens`,
|
|
751
|
+
`<b>Now</b>: ${messages} messages (exact usage will update after next message)`,
|
|
752
|
+
].join("\n");
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async function safeEditMessage(
|
|
756
|
+
ctx: Filter<Context, "callback_query:data">,
|
|
757
|
+
html: string,
|
|
758
|
+
replyMarkup?: InlineKeyboard
|
|
759
|
+
): Promise<void> {
|
|
760
|
+
try {
|
|
761
|
+
await ctx.editMessageText(html, {
|
|
762
|
+
parse_mode: "HTML",
|
|
763
|
+
reply_markup: replyMarkup,
|
|
764
|
+
});
|
|
765
|
+
} catch {
|
|
766
|
+
// Message may have aged out past Telegram's edit window — non-fatal.
|
|
767
|
+
}
|
|
768
|
+
}
|