grok-telegram-bot 2.6.0 → 2.7.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/.env.example +13 -0
- package/CHANGELOG.md +35 -0
- package/README.md +15 -2
- package/docs/INSTALL.md +2 -0
- package/package.json +1 -1
- package/scripts/setup.mjs +20 -3
- package/src/app/instance.ts +223 -0
- package/src/app/types.ts +7 -0
- package/src/bot/ask-user-service.ts +226 -0
- package/src/bot/bot.ts +32 -0
- package/src/bot/commands.ts +22 -2
- package/src/bot/handlers/grok-slash.ts +336 -0
- package/src/bot/handlers/system.ts +63 -1
- package/src/bot/plan-exit-service.ts +169 -0
- package/src/bot/registry.ts +11 -2
- package/src/bot/session-runtime.ts +2 -0
- package/src/cli.ts +43 -7
- package/src/config.ts +35 -25
- package/src/grok/client.ts +29 -5
- package/src/grok/plan-approval.ts +8 -0
- package/src/index.ts +4 -0
- package/src/service/linux.ts +21 -15
- package/src/service/macos.ts +20 -15
- package/src/service/platform.ts +12 -3
- package/src/service/types.ts +6 -0
- package/src/service/windows.ts +31 -22
package/src/bot/bot.ts
CHANGED
|
@@ -44,6 +44,9 @@ import { registerTasks, registerWizardInput } from "./handlers/tasks.js";
|
|
|
44
44
|
import { registerUsage } from "./handlers/usage.js";
|
|
45
45
|
import { registerVoice } from "./handlers/voice.js";
|
|
46
46
|
import { registerForum } from "./handlers/forum.js";
|
|
47
|
+
import { registerGrokSlash } from "./handlers/grok-slash.js";
|
|
48
|
+
import { AskUserService } from "./ask-user-service.js";
|
|
49
|
+
import { PlanExitService } from "./plan-exit-service.js";
|
|
47
50
|
import { StatusPanel } from "./menu/status-panel.js";
|
|
48
51
|
import { sendMarkdownDoc } from "./telegram-io.js";
|
|
49
52
|
import { Ephemeral } from "./menu/ephemeral.js";
|
|
@@ -207,6 +210,18 @@ export async function createBot(cfg: AppConfig, acp: GrokClient): Promise<BotBun
|
|
|
207
210
|
onUnpinned: (chatId) => statusPanel.ensurePinned(chatId),
|
|
208
211
|
});
|
|
209
212
|
acp.permissionHandler = (p) => permissions.handle(p);
|
|
213
|
+
|
|
214
|
+
const planExit = new PlanExitService(bot.api, registry, cfg.autoApprovePlan, (chatId) =>
|
|
215
|
+
statusPanel.ensurePinned(chatId),
|
|
216
|
+
);
|
|
217
|
+
acp.planExitHandler = (params) => planExit.handle(params);
|
|
218
|
+
|
|
219
|
+
const askUser = new AskUserService(
|
|
220
|
+
bot.api,
|
|
221
|
+
registry,
|
|
222
|
+
cfg.autoApprovePlan && cfg.autoApprovePermissions,
|
|
223
|
+
);
|
|
224
|
+
acp.askUserHandler = (params) => askUser.handle(params);
|
|
210
225
|
// /stop and /cancel must cancel pending interactive permissions for that
|
|
211
226
|
// session only (ACP requires cancelled outcomes) — never kill the agent.
|
|
212
227
|
acp.onSessionCancel = (sessionId) => {
|
|
@@ -252,6 +267,16 @@ export async function createBot(cfg: AppConfig, acp: GrokClient): Promise<BotBun
|
|
|
252
267
|
if (sid) await switchAndShow(ctx, deps, sid);
|
|
253
268
|
});
|
|
254
269
|
|
|
270
|
+
bot.callbackQuery(/^planx:(\d+):(ok|chg|no)$/, async (ctx) => {
|
|
271
|
+
const toast = planExit.resolveChoice(ctx.match![1]!, ctx.match![2]!);
|
|
272
|
+
await ctx.answerCallbackQuery({ text: toast ?? "Expired" });
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
bot.callbackQuery(/^asku:(\d+):(opt|next|prev|skip)(?::(.*))?$/, async (ctx) => {
|
|
276
|
+
const toast = askUser.tap(ctx.match![1]!, ctx.match![2]!, ctx.match![3]);
|
|
277
|
+
await ctx.answerCallbackQuery({ text: toast ?? "Expired" });
|
|
278
|
+
});
|
|
279
|
+
|
|
255
280
|
// Legacy complexity buttons (removed — agent decides; auto-plan if complex).
|
|
256
281
|
bot.callbackQuery(/^cplx:(simple|complex)$/, async (ctx) => {
|
|
257
282
|
await ctx.answerCallbackQuery({ text: "Complexity is automatic now" });
|
|
@@ -349,6 +374,12 @@ export async function createBot(cfg: AppConfig, acp: GrokClient): Promise<BotBun
|
|
|
349
374
|
|
|
350
375
|
registerMenu(bot, deps); // persistent-keyboard buttons (hears)
|
|
351
376
|
registerWizardInput(bot, deps); // wizard text input (before commands)
|
|
377
|
+
bot.on("message:text", async (ctx, next) => {
|
|
378
|
+
const text = ctx.message?.text ?? "";
|
|
379
|
+
if (!text || text.startsWith("/")) return next();
|
|
380
|
+
if (planExit.takeFeedback(ctx.chat.id, text)) return;
|
|
381
|
+
await next();
|
|
382
|
+
});
|
|
352
383
|
if (forum) registerForum(bot, deps, forum);
|
|
353
384
|
registerControl(bot, deps);
|
|
354
385
|
registerProjects(bot, deps);
|
|
@@ -367,6 +398,7 @@ export async function createBot(cfg: AppConfig, acp: GrokClient): Promise<BotBun
|
|
|
367
398
|
registerPhotos(bot, deps); // photos & image documents
|
|
368
399
|
registerDocuments(bot, deps); // non-image files (text inlined, binaries saved)
|
|
369
400
|
registerVoice(bot, deps); // voice / audio -> transcription -> prompt
|
|
401
|
+
registerGrokSlash(bot, deps); // Grok Build /goal /plan /compact … + catch-all
|
|
370
402
|
registerMessages(bot, deps); // catch-all text prompt — keep last
|
|
371
403
|
|
|
372
404
|
bot.catch((err) => {
|
package/src/bot/commands.ts
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* a shorter list with cancel/menu first — reply keyboards are unreliable there.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import { GROK_FORWARDED_COMMANDS } from "./handlers/grok-slash.js";
|
|
9
|
+
|
|
10
|
+
/** Bot-local commands (not forwarded to Grok). Order = Telegram "/" menu order. */
|
|
11
|
+
export const BOT_COMMANDS: { command: string; description: string }[] = [
|
|
10
12
|
// Core control
|
|
11
13
|
{ command: "start", description: "Welcome, menu & status panel" },
|
|
12
14
|
{ command: "menu", description: "Open the menu" },
|
|
@@ -37,10 +39,17 @@ export const COMMANDS: { command: string; description: string }[] = [
|
|
|
37
39
|
{ command: "killall", description: "Kill all active sessions on the PC" },
|
|
38
40
|
{ command: "model", description: "Switch model: /model <id>" },
|
|
39
41
|
{ command: "restart", description: "Restart the Grok agent" },
|
|
42
|
+
{ command: "sandbox", description: "Show / set Grok sandbox profile" },
|
|
40
43
|
{ command: "unwatch", description: "Stop following a live session" },
|
|
41
44
|
{ command: "help", description: "Show help" },
|
|
42
45
|
];
|
|
43
46
|
|
|
47
|
+
/** Full Telegram menu: bot-local + Grok Build shell forwards (≤100). */
|
|
48
|
+
export const COMMANDS: { command: string; description: string }[] = [
|
|
49
|
+
...BOT_COMMANDS,
|
|
50
|
+
...GROK_FORWARDED_COMMANDS.map(({ command, description }) => ({ command, description })),
|
|
51
|
+
];
|
|
52
|
+
|
|
44
53
|
/**
|
|
45
54
|
* Group / forum command menu — keep short; cancel & menu first so topics can
|
|
46
55
|
* stop a turn without the private reply-keyboard bar.
|
|
@@ -56,6 +65,9 @@ export const GROUP_COMMANDS: { command: string; description: string }[] = [
|
|
|
56
65
|
{ command: "btw", description: "Queue or run: /btw <text>" },
|
|
57
66
|
{ command: "flush", description: "Run queued follow-ups now" },
|
|
58
67
|
{ command: "model", description: "Switch model: /model <id>" },
|
|
68
|
+
{ command: "goal", description: "Grok /goal — run until done" },
|
|
69
|
+
{ command: "plan", description: "Grok /plan — enter plan mode" },
|
|
70
|
+
{ command: "compact", description: "Grok /compact — compress context" },
|
|
59
71
|
{ command: "help", description: "Show help" },
|
|
60
72
|
];
|
|
61
73
|
|
|
@@ -91,4 +103,12 @@ export const HELP_TEXT = [
|
|
|
91
103
|
"/flush \u2014 run queued follow-ups immediately",
|
|
92
104
|
"/reauth \u2014 sign in to Grok",
|
|
93
105
|
"/accounts \u2014 switch saved Grok accounts",
|
|
106
|
+
"/sandbox \u2014 show or set GROK_SANDBOX (needs /restart)",
|
|
107
|
+
"",
|
|
108
|
+
"GROK BUILD SLASH (forwarded into the active session)",
|
|
109
|
+
"/goal /plan /view_plan /compact /context /session_info",
|
|
110
|
+
"/deep_research /workflow /workflows /loop",
|
|
111
|
+
"/remember /memory /memory_flush /dream",
|
|
112
|
+
"Underscores map to hyphens (e.g. /view_plan \u2192 /view-plan).",
|
|
113
|
+
"Other non-bot Grok /commands and skills are also forwarded.",
|
|
94
114
|
].join("\n");
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forward Grok Build slash commands (e.g. /goal, /plan, /compact) into the
|
|
3
|
+
* active ACP session. Grok shell builtins are parsed from the prompt text by
|
|
4
|
+
* slash_exec — they never reached the agent before because the Telegram message
|
|
5
|
+
* handler treated unknown "/…" lines as typos.
|
|
6
|
+
*
|
|
7
|
+
* Bot-local commands (projects, sessions, reauth, …) stay reserved and are
|
|
8
|
+
* handled by their own `bot.command` registrations. Name collisions use
|
|
9
|
+
* non-colliding Telegram aliases that still send the correct Grok slash line.
|
|
10
|
+
*/
|
|
11
|
+
import type { Bot, Context } from "grammy";
|
|
12
|
+
import { textPrompt } from "../../app/types.js";
|
|
13
|
+
import { createLogger } from "../../logger.js";
|
|
14
|
+
import { isGeneralThread } from "../../forum/thread.js";
|
|
15
|
+
import type { BotDeps } from "../deps.js";
|
|
16
|
+
import { extractReplyContext } from "../reply-context.js";
|
|
17
|
+
import { resolveScope } from "../scope.js";
|
|
18
|
+
|
|
19
|
+
const log = createLogger("grok-slash");
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Telegram command names reserved by this bot (without leading slash).
|
|
23
|
+
* Must stay in sync with bot.command registrations / COMMANDS menu.
|
|
24
|
+
* Bare names in this set are NEVER stolen for Grok — use collision aliases.
|
|
25
|
+
*/
|
|
26
|
+
export const BOT_RESERVED_COMMANDS = new Set(
|
|
27
|
+
[
|
|
28
|
+
"start",
|
|
29
|
+
"menu",
|
|
30
|
+
"help",
|
|
31
|
+
"projects",
|
|
32
|
+
"project",
|
|
33
|
+
"sessions",
|
|
34
|
+
"active",
|
|
35
|
+
"running",
|
|
36
|
+
"killall",
|
|
37
|
+
"mcp",
|
|
38
|
+
"tasks",
|
|
39
|
+
"newtask",
|
|
40
|
+
"history",
|
|
41
|
+
"new",
|
|
42
|
+
"status",
|
|
43
|
+
"usage",
|
|
44
|
+
"btw",
|
|
45
|
+
"flush",
|
|
46
|
+
"queue",
|
|
47
|
+
"clearqueue",
|
|
48
|
+
"cancel",
|
|
49
|
+
"stop",
|
|
50
|
+
"unwatch",
|
|
51
|
+
"model",
|
|
52
|
+
"restart",
|
|
53
|
+
"sandbox",
|
|
54
|
+
"reauth",
|
|
55
|
+
"accounts",
|
|
56
|
+
"import",
|
|
57
|
+
"forum_setup",
|
|
58
|
+
].map((c) => c.toLowerCase()),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Telegram command name (no slash, lowercase) → Grok shell command name (no slash).
|
|
63
|
+
* Covers hyphenated multi-word forms, documented Grok aliases, and collision aliases
|
|
64
|
+
* for bot-reserved bare names that still have distinct Grok builtins.
|
|
65
|
+
*/
|
|
66
|
+
export const GROK_SLASH_ALIASES: Readonly<Record<string, string>> = {
|
|
67
|
+
// Multi-word (Telegram underscore ↔ Grok hyphen)
|
|
68
|
+
view_plan: "view-plan",
|
|
69
|
+
show_plan: "view-plan",
|
|
70
|
+
plan_view: "view-plan",
|
|
71
|
+
deep_research: "deep-research",
|
|
72
|
+
always_approve: "always-approve",
|
|
73
|
+
session_info: "session-info",
|
|
74
|
+
imagine_video: "imagine-video",
|
|
75
|
+
config_agents: "config-agents",
|
|
76
|
+
release_notes: "release-notes",
|
|
77
|
+
import_claude: "import-claude",
|
|
78
|
+
compact_mode: "compact-mode",
|
|
79
|
+
vim_mode: "vim-mode",
|
|
80
|
+
agents_dashboard: "dashboard",
|
|
81
|
+
// No-underscore convenience forms
|
|
82
|
+
viewplan: "view-plan",
|
|
83
|
+
showplan: "view-plan",
|
|
84
|
+
planview: "view-plan",
|
|
85
|
+
deepresearch: "deep-research",
|
|
86
|
+
alwaysapprove: "always-approve",
|
|
87
|
+
sessioninfo: "session-info",
|
|
88
|
+
imaginevideo: "imagine-video",
|
|
89
|
+
configagents: "config-agents",
|
|
90
|
+
releasenotes: "release-notes",
|
|
91
|
+
importclaude: "import-claude",
|
|
92
|
+
// Documented Grok aliases
|
|
93
|
+
clear: "new",
|
|
94
|
+
undo: "rewind",
|
|
95
|
+
title: "rename",
|
|
96
|
+
m: "model",
|
|
97
|
+
mem: "memory",
|
|
98
|
+
cost: "usage",
|
|
99
|
+
agents: "config-agents",
|
|
100
|
+
howto: "docs",
|
|
101
|
+
guides: "docs",
|
|
102
|
+
changelog: "release-notes",
|
|
103
|
+
ml: "multiline",
|
|
104
|
+
t: "theme",
|
|
105
|
+
full: "fullscreen",
|
|
106
|
+
tour: "tutorial",
|
|
107
|
+
onboarding: "tutorial",
|
|
108
|
+
welcome: "home",
|
|
109
|
+
exit: "quit",
|
|
110
|
+
prefs: "settings",
|
|
111
|
+
preferences: "settings",
|
|
112
|
+
config: "settings",
|
|
113
|
+
terminal_setup: "doctor",
|
|
114
|
+
terminal_check: "doctor",
|
|
115
|
+
terminal_info: "doctor",
|
|
116
|
+
terminalsetup: "doctor",
|
|
117
|
+
terminalcheck: "doctor",
|
|
118
|
+
terminalinfo: "doctor",
|
|
119
|
+
// Bot name collisions → still send the Grok builtin
|
|
120
|
+
grok_new: "new",
|
|
121
|
+
session_new: "new",
|
|
122
|
+
grok_clear: "new",
|
|
123
|
+
memory_flush: "flush",
|
|
124
|
+
grok_flush: "flush",
|
|
125
|
+
grok_usage: "usage",
|
|
126
|
+
grok_cost: "usage",
|
|
127
|
+
grok_btw: "btw",
|
|
128
|
+
// session-info aliases when bare status/info are bot-reserved
|
|
129
|
+
grok_status: "session-info",
|
|
130
|
+
grok_info: "session-info",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Official shell builtins that are meaningful over ACP (not pure TUI/pager).
|
|
135
|
+
* Used for tests / inventory; menu may advertise a subset + collision aliases.
|
|
136
|
+
*/
|
|
137
|
+
export const GROK_SHELL_ACP_COMMANDS: readonly string[] = [
|
|
138
|
+
"new",
|
|
139
|
+
"compact",
|
|
140
|
+
"context",
|
|
141
|
+
"session-info",
|
|
142
|
+
"fork",
|
|
143
|
+
"rewind",
|
|
144
|
+
"copy",
|
|
145
|
+
"export",
|
|
146
|
+
"delete",
|
|
147
|
+
"rename",
|
|
148
|
+
"model",
|
|
149
|
+
"effort",
|
|
150
|
+
"always-approve",
|
|
151
|
+
"auto",
|
|
152
|
+
"plan",
|
|
153
|
+
"view-plan",
|
|
154
|
+
"memory",
|
|
155
|
+
"flush",
|
|
156
|
+
"dream",
|
|
157
|
+
"remember",
|
|
158
|
+
"hooks",
|
|
159
|
+
"plugins",
|
|
160
|
+
"marketplace",
|
|
161
|
+
"skills",
|
|
162
|
+
"imagine",
|
|
163
|
+
"imagine-video",
|
|
164
|
+
"loop",
|
|
165
|
+
"goal",
|
|
166
|
+
"deep-research",
|
|
167
|
+
"workflow",
|
|
168
|
+
"workflows",
|
|
169
|
+
"feedback",
|
|
170
|
+
"btw",
|
|
171
|
+
"mcps",
|
|
172
|
+
"doctor",
|
|
173
|
+
"release-notes",
|
|
174
|
+
"docs",
|
|
175
|
+
"import-claude",
|
|
176
|
+
"config-agents",
|
|
177
|
+
"personas",
|
|
178
|
+
"login",
|
|
179
|
+
"logout",
|
|
180
|
+
"usage",
|
|
181
|
+
"privacy",
|
|
182
|
+
"settings",
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
/** Grok Build slash commands we advertise in the Telegram menu (Telegram-safe names). */
|
|
186
|
+
export const GROK_FORWARDED_COMMANDS: { command: string; description: string; grok: string }[] = [
|
|
187
|
+
// Session
|
|
188
|
+
{ command: "compact", description: "Grok /compact — compress context", grok: "compact" },
|
|
189
|
+
{ command: "context", description: "Grok /context — context window usage", grok: "context" },
|
|
190
|
+
{ command: "session_info", description: "Grok /session-info", grok: "session-info" },
|
|
191
|
+
{ command: "fork", description: "Grok /fork — branch session", grok: "fork" },
|
|
192
|
+
{ command: "rewind", description: "Grok /rewind — undo last turns", grok: "rewind" },
|
|
193
|
+
{ command: "copy", description: "Grok /copy — copy last response", grok: "copy" },
|
|
194
|
+
{ command: "export", description: "Grok /export — export conversation", grok: "export" },
|
|
195
|
+
{ command: "delete", description: "Grok /delete — delete session history", grok: "delete" },
|
|
196
|
+
{ command: "rename", description: "Grok /rename <title>", grok: "rename" },
|
|
197
|
+
{ command: "grok_new", description: "Grok /new — fresh session (CLI)", grok: "new" },
|
|
198
|
+
// Model / mode
|
|
199
|
+
{ command: "effort", description: "Grok /effort low|medium|high|xhigh", grok: "effort" },
|
|
200
|
+
{ command: "always_approve", description: "Grok /always-approve toggle", grok: "always-approve" },
|
|
201
|
+
{ command: "auto", description: "Grok /auto — auto permission mode", grok: "auto" },
|
|
202
|
+
{ command: "plan", description: "Grok /plan — enter plan mode", grok: "plan" },
|
|
203
|
+
{ command: "view_plan", description: "Grok /view-plan — show saved plan", grok: "view-plan" },
|
|
204
|
+
// Memory
|
|
205
|
+
{ command: "memory", description: "Grok /memory — browse memories", grok: "memory" },
|
|
206
|
+
{ command: "memory_flush", description: "Grok /flush — save session to memory", grok: "flush" },
|
|
207
|
+
{ command: "dream", description: "Grok /dream — consolidate memory", grok: "dream" },
|
|
208
|
+
{ command: "remember", description: "Grok /remember <note>", grok: "remember" },
|
|
209
|
+
// Extensions
|
|
210
|
+
{ command: "hooks", description: "Grok /hooks — hooks panel", grok: "hooks" },
|
|
211
|
+
{ command: "plugins", description: "Grok /plugins — plugins panel", grok: "plugins" },
|
|
212
|
+
{ command: "marketplace", description: "Grok /marketplace — plugin marketplace", grok: "marketplace" },
|
|
213
|
+
{ command: "skills", description: "Grok /skills — skills panel", grok: "skills" },
|
|
214
|
+
// Media
|
|
215
|
+
{ command: "imagine", description: "Grok /imagine <description>", grok: "imagine" },
|
|
216
|
+
{ command: "imagine_video", description: "Grok /imagine-video <description>", grok: "imagine-video" },
|
|
217
|
+
// Scheduling / workflows / goals
|
|
218
|
+
{ command: "loop", description: "Grok /loop [interval] <prompt>", grok: "loop" },
|
|
219
|
+
{ command: "goal", description: "Grok /goal — set/status/pause/resume/clear", grok: "goal" },
|
|
220
|
+
{ command: "deep_research", description: "Grok /deep-research <query>", grok: "deep-research" },
|
|
221
|
+
{ command: "workflow", description: "Grok /workflow — run/manage workflow", grok: "workflow" },
|
|
222
|
+
{ command: "workflows", description: "Grok /workflows — workflow dashboard", grok: "workflows" },
|
|
223
|
+
// Other
|
|
224
|
+
{ command: "feedback", description: "Grok /feedback [message]", grok: "feedback" },
|
|
225
|
+
{ command: "grok_btw", description: "Grok /btw — aside without interrupting", grok: "btw" },
|
|
226
|
+
{ command: "mcps", description: "Grok /mcps — MCP servers modal", grok: "mcps" },
|
|
227
|
+
{ command: "doctor", description: "Grok /doctor — session diagnostics", grok: "doctor" },
|
|
228
|
+
{ command: "release_notes", description: "Grok /release-notes", grok: "release-notes" },
|
|
229
|
+
{ command: "docs", description: "Grok /docs — how-to guides", grok: "docs" },
|
|
230
|
+
{ command: "import_claude", description: "Grok /import-claude", grok: "import-claude" },
|
|
231
|
+
{ command: "config_agents", description: "Grok /config-agents — agent defs", grok: "config-agents" },
|
|
232
|
+
{ command: "personas", description: "Grok /personas — manage personas", grok: "personas" },
|
|
233
|
+
{ command: "login", description: "Grok /login — re-auth in session", grok: "login" },
|
|
234
|
+
{ command: "logout", description: "Grok /logout", grok: "logout" },
|
|
235
|
+
{ command: "grok_usage", description: "Grok /usage — credit/billing", grok: "usage" },
|
|
236
|
+
{ command: "privacy", description: "Grok /privacy — data/retention", grok: "privacy" },
|
|
237
|
+
{ command: "settings", description: "Grok /settings — config modal", grok: "settings" },
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
/** Resolve a Telegram command token (no slash) to a Grok shell command name. */
|
|
241
|
+
export function resolveGrokCommandName(telegramName: string): string {
|
|
242
|
+
const name = telegramName.toLowerCase();
|
|
243
|
+
const fromAlias = GROK_SLASH_ALIASES[name];
|
|
244
|
+
if (fromAlias) return fromAlias;
|
|
245
|
+
const fromMenu = GROK_FORWARDED_COMMANDS.find((c) => c.command === name);
|
|
246
|
+
if (fromMenu) return fromMenu.grok;
|
|
247
|
+
// underscore → hyphen for multi-word Grok commands (deep_research → deep-research)
|
|
248
|
+
return name.replace(/_/g, "-");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** True when a bare slash line should be forwarded to Grok (not a bot command). */
|
|
252
|
+
export function shouldForwardSlashToGrok(text: string): boolean {
|
|
253
|
+
const t = text.trim();
|
|
254
|
+
if (!t.startsWith("/") || t.includes("\n")) return false;
|
|
255
|
+
// /cmd@botname args
|
|
256
|
+
const m = t.match(/^\/([A-Za-z0-9_]+)(?:@\w+)?(?:\s|$)/);
|
|
257
|
+
if (!m) return false;
|
|
258
|
+
const name = m[1]!.toLowerCase();
|
|
259
|
+
if (BOT_RESERVED_COMMANDS.has(name)) return false;
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Normalize Telegram `/view_plan foo` → Grok `/view-plan foo`. */
|
|
264
|
+
export function toGrokSlashLine(text: string): string {
|
|
265
|
+
const t = text.trim();
|
|
266
|
+
const m = t.match(/^\/([A-Za-z0-9_]+)(@\w+)?([\s\S]*)$/);
|
|
267
|
+
if (!m) return t;
|
|
268
|
+
const rest = m[3] ?? "";
|
|
269
|
+
const name = resolveGrokCommandName(m[1]!);
|
|
270
|
+
return `/${name}${rest}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function submitGrokSlash(ctx: Context, deps: BotDeps, line: string): Promise<void> {
|
|
274
|
+
if (!ctx.chat) return;
|
|
275
|
+
const grokLine = toGrokSlashLine(line);
|
|
276
|
+
const scope = resolveScope(ctx, deps);
|
|
277
|
+
const extra: Record<string, unknown> = { parse_mode: "Markdown", ...scope.threadExtra };
|
|
278
|
+
if (scope.isForum && isGeneralThread(scope.threadId) && /^\/goal(?:\s|$)/i.test(grokLine)) {
|
|
279
|
+
await ctx.reply(
|
|
280
|
+
"Use /goal in a **project topic** or **AI Chat**, not in General \\(manager\\).",
|
|
281
|
+
extra,
|
|
282
|
+
);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const rt = scope.rt;
|
|
286
|
+
try {
|
|
287
|
+
// Prefer dedicated ACP command RPC when the agent supports it; fall back to
|
|
288
|
+
// session/prompt (Grok slash_exec parses leading / in the prompt).
|
|
289
|
+
if (rt.sessionId) {
|
|
290
|
+
try {
|
|
291
|
+
await deps.acp.executeCommand(rt.sessionId, grokLine);
|
|
292
|
+
await ctx.reply(`\u25B6\uFE0F Sent to Grok: \`${grokLine}\``, extra);
|
|
293
|
+
return;
|
|
294
|
+
} catch (err) {
|
|
295
|
+
log.debug(
|
|
296
|
+
`executeCommand failed for ${grokLine}: ${(err as Error).message}; falling back to prompt`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const outcome = await rt.submit(
|
|
301
|
+
textPrompt(grokLine, ctx.message?.message_id, extractReplyContext(ctx), {
|
|
302
|
+
skipSelfRecheck: true,
|
|
303
|
+
rawSlashCommand: true,
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
306
|
+
if (outcome === "queued") {
|
|
307
|
+
await ctx.reply(
|
|
308
|
+
`\u{1F4E5} Queued (position ${rt.queueLength}): \`${grokLine}\` \u2014 runs after the current turn.`,
|
|
309
|
+
extra,
|
|
310
|
+
);
|
|
311
|
+
} else {
|
|
312
|
+
await ctx.reply(`\u25B6\uFE0F Running \`${grokLine}\`\u2026`, extra);
|
|
313
|
+
}
|
|
314
|
+
} catch (err) {
|
|
315
|
+
log.warn(`grok slash failed: ${(err as Error).message}`);
|
|
316
|
+
await ctx.reply(`\u274C Could not run \`${grokLine}\`: ${(err as Error).message}`, extra);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function registerGrokSlash(bot: Bot, deps: BotDeps): void {
|
|
321
|
+
// Explicit Telegram commands for advertised Grok builtins (appear in menu).
|
|
322
|
+
for (const def of GROK_FORWARDED_COMMANDS) {
|
|
323
|
+
if (BOT_RESERVED_COMMANDS.has(def.command)) continue;
|
|
324
|
+
bot.command(def.command, async (ctx) => {
|
|
325
|
+
const args = (ctx.match || "").toString();
|
|
326
|
+
const line = args ? `/${def.command} ${args}` : `/${def.command}`;
|
|
327
|
+
await submitGrokSlash(ctx, deps, line);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
// Catch-all for other Grok / skills slashes not in the menu.
|
|
331
|
+
bot.on("message:text", async (ctx, next) => {
|
|
332
|
+
const text = ctx.message?.text ?? "";
|
|
333
|
+
if (!shouldForwardSlashToGrok(text)) return next();
|
|
334
|
+
await submitGrokSlash(ctx, deps, text);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* System commands: /queue /clearqueue /model /restart.
|
|
2
|
+
* System commands: /queue /clearqueue /model /restart /sandbox.
|
|
3
3
|
*/
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
5
|
import type { Bot } from "grammy";
|
|
6
|
+
import { InlineKeyboard } from "grammy";
|
|
7
|
+
import { ENV_PATH } from "../../config.js";
|
|
5
8
|
import type { BotDeps } from "../deps.js";
|
|
6
9
|
|
|
10
|
+
const SANDBOX_PROFILES = ["workspace-safe", "workspace", "strict", "read-only", "off"] as const;
|
|
11
|
+
|
|
7
12
|
export function registerSystem(bot: Bot, deps: BotDeps): void {
|
|
8
13
|
bot.command("queue", async (ctx) => {
|
|
9
14
|
const rt = deps.registry.get(ctx.chat.id);
|
|
@@ -48,4 +53,61 @@ export function registerSystem(bot: Bot, deps: BotDeps): void {
|
|
|
48
53
|
await ctx.reply(`\u274C Restart failed: ${(err as Error).message}`);
|
|
49
54
|
}
|
|
50
55
|
});
|
|
56
|
+
|
|
57
|
+
bot.command("sandbox", async (ctx) => {
|
|
58
|
+
const arg = (ctx.match || "").toString().trim();
|
|
59
|
+
const current = deps.cfg.sandboxProfile || process.env.GROK_SANDBOX || "(from ~/.grok/config.toml)";
|
|
60
|
+
if (!arg) {
|
|
61
|
+
const kb = new InlineKeyboard();
|
|
62
|
+
for (const p of SANDBOX_PROFILES) kb.text(p, `sbx:${p}`).row();
|
|
63
|
+
await ctx.reply(
|
|
64
|
+
`\u{1F6E1} Sandbox profile now: ${current}\n` +
|
|
65
|
+
`Grok reads GROK_SANDBOX. Pick one, then /restart.\n` +
|
|
66
|
+
`Or: /sandbox workspace-safe`,
|
|
67
|
+
{ reply_markup: kb },
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!(SANDBOX_PROFILES as readonly string[]).includes(arg)) {
|
|
72
|
+
await ctx.reply(`Unknown profile. Use: ${SANDBOX_PROFILES.join(", ")}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
upsertEnv("GROK_SANDBOX", arg);
|
|
77
|
+
deps.cfg.sandboxProfile = arg;
|
|
78
|
+
process.env.GROK_SANDBOX = arg;
|
|
79
|
+
deps.acp.setAgentOptions({ sandboxProfile: arg });
|
|
80
|
+
await ctx.reply(`\u2705 GROK_SANDBOX=${arg} written. Send /restart to apply.`);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
await ctx.reply(`\u274C Could not write .env: ${(e as Error).message}`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
bot.callbackQuery(/^sbx:([\w-]+)$/, async (ctx) => {
|
|
87
|
+
const profile = ctx.match![1]!;
|
|
88
|
+
if (!(SANDBOX_PROFILES as readonly string[]).includes(profile)) {
|
|
89
|
+
await ctx.answerCallbackQuery({ text: "Unknown profile" });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
upsertEnv("GROK_SANDBOX", profile);
|
|
94
|
+
deps.cfg.sandboxProfile = profile;
|
|
95
|
+
process.env.GROK_SANDBOX = profile;
|
|
96
|
+
deps.acp.setAgentOptions({ sandboxProfile: profile });
|
|
97
|
+
await ctx.answerCallbackQuery({ text: profile });
|
|
98
|
+
await ctx.editMessageText(`\u2705 GROK_SANDBOX=${profile}. Send /restart to apply.`).catch(() => {});
|
|
99
|
+
} catch (e) {
|
|
100
|
+
await ctx.answerCallbackQuery({ text: (e as Error).message.slice(0, 40) });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Set KEY=value in the instance .env without dumping secrets. */
|
|
106
|
+
function upsertEnv(key: string, value: string): void {
|
|
107
|
+
if (!existsSync(ENV_PATH)) throw new Error(".env not found");
|
|
108
|
+
let body = readFileSync(ENV_PATH, "utf-8");
|
|
109
|
+
const re = new RegExp(`^${key}=.*$`, "m");
|
|
110
|
+
if (re.test(body)) body = body.replace(re, `${key}=${value}`);
|
|
111
|
+
else body = body.replace(/\s*$/, `\n${key}=${value}\n`);
|
|
112
|
+
writeFileSync(ENV_PATH, body, "utf-8");
|
|
51
113
|
}
|