grok-telegram-bot 2.5.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 +106 -0
- package/README.md +20 -5
- package/docs/GROUP.md +39 -4
- package/docs/INSTALL.md +2 -0
- package/package.json +4 -4
- package/scripts/setup.mjs +20 -3
- package/src/app/instance.ts +223 -0
- package/src/app/types.ts +34 -1
- package/src/bot/ask-user-service.ts +226 -0
- package/src/bot/auth.ts +5 -1
- package/src/bot/bot.ts +105 -4
- package/src/bot/chat-controller.ts +129 -0
- package/src/bot/commands.ts +22 -2
- package/src/bot/group-memory.ts +192 -12
- package/src/bot/handlers/forum.ts +16 -6
- package/src/bot/handlers/grok-slash.ts +336 -0
- package/src/bot/handlers/message.ts +165 -25
- package/src/bot/handlers/photo.ts +4 -1
- package/src/bot/handlers/system.ts +63 -1
- package/src/bot/image-return.ts +4 -1
- package/src/bot/manager-context.ts +208 -0
- package/src/bot/manager-jobs.ts +142 -0
- package/src/bot/menu/ephemeral.ts +4 -1
- package/src/bot/plan-exit-service.ts +169 -0
- package/src/bot/prompt-anchor.ts +2 -3
- package/src/bot/prompt-content.ts +5 -0
- package/src/bot/registry.ts +11 -2
- package/src/bot/scope.ts +9 -8
- package/src/bot/session-runtime.ts +665 -55
- package/src/bot/telegram-actions.ts +728 -38
- package/src/bot/telegram-bots.ts +2 -1
- package/src/bot/telegram-io.ts +4 -1
- package/src/cli.ts +43 -7
- package/src/config.ts +35 -25
- package/src/forum/manager.ts +2 -1
- package/src/forum/thread.ts +33 -0
- package/src/grok/client.ts +29 -5
- package/src/grok/plan-approval.ts +8 -0
- package/src/index.ts +4 -0
- package/src/render/manager-directive.ts +137 -0
- package/src/render/session-comment.ts +10 -0
- package/src/render/telegram-bridge.ts +118 -14
- 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/sessions/history.ts +18 -0
- package/src/stream/streamer.ts +46 -10
package/src/bot/telegram-bots.ts
CHANGED
|
@@ -176,7 +176,8 @@ export class TelegramBotService {
|
|
|
176
176
|
: `/${cmd}@${username}`;
|
|
177
177
|
|
|
178
178
|
const extra: Record<string, unknown> = {};
|
|
179
|
-
|
|
179
|
+
// Omit General (1) — Bot API rejects message_thread_id=1.
|
|
180
|
+
if (opts.messageThreadId !== undefined && opts.messageThreadId !== 1) {
|
|
180
181
|
extra.message_thread_id = opts.messageThreadId;
|
|
181
182
|
}
|
|
182
183
|
|
package/src/bot/telegram-io.ts
CHANGED
|
@@ -183,7 +183,10 @@ export async function sendMarkdownDoc(
|
|
|
183
183
|
opts?: { loud?: boolean; messageThreadId?: number },
|
|
184
184
|
): Promise<void> {
|
|
185
185
|
const extra: Record<string, unknown> = opts?.loud ? { disable_notification: false } : {};
|
|
186
|
-
|
|
186
|
+
// Omit General (1) — Bot API rejects message_thread_id=1.
|
|
187
|
+
if (opts?.messageThreadId !== undefined && opts.messageThreadId !== 1) {
|
|
188
|
+
extra.message_thread_id = opts.messageThreadId;
|
|
189
|
+
}
|
|
187
190
|
const rendered = toTelegramMarkdown(rawMarkdown);
|
|
188
191
|
const mdChunks = chunkMarkdown(rendered);
|
|
189
192
|
const plainChunks = chunkMarkdown(rawMarkdown);
|
package/src/cli.ts
CHANGED
|
@@ -10,16 +10,17 @@
|
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
11
|
import { existsSync, readFileSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
+
import { listKnownInstances, stripInstanceFlags } from "./app/instance.js";
|
|
13
14
|
import { ENV_PATH, INSTANCE_DIR, PROJECT_ROOT } from "./config.js";
|
|
14
15
|
import { buildLaunchSpec, getController } from "./service/index.js";
|
|
15
16
|
|
|
16
17
|
const HELP = `Grok Telegram Bot — CLI
|
|
17
18
|
|
|
18
|
-
Usage: grok-tg <command>
|
|
19
|
+
Usage: grok-tg [--name <slug>] [--instance <dir>] <command>
|
|
19
20
|
|
|
20
21
|
run Run in the foreground
|
|
21
|
-
setup [--path] Create/update .env (default ~/.grok/tg/.env
|
|
22
|
-
|
|
22
|
+
setup [--path] Create/update .env (default ~/.grok/tg/.env);
|
|
23
|
+
--name <slug> writes ~/.grok/tg/instances/<slug>/.env
|
|
23
24
|
install Install + start a background service (autostart on boot)
|
|
24
25
|
uninstall Stop + remove the background service
|
|
25
26
|
start Start the service
|
|
@@ -27,12 +28,20 @@ Usage: grok-tg <command>
|
|
|
27
28
|
restart Restart the service
|
|
28
29
|
status Show install + running status
|
|
29
30
|
logs [n] Show the last n log lines (default 100)
|
|
31
|
+
instances List named bot instances on this host
|
|
30
32
|
help Show this help
|
|
33
|
+
|
|
34
|
+
Several Telegram bots on one host (one chat per project):
|
|
35
|
+
|
|
36
|
+
grok-tg --name work setup <BOT_TOKEN> <YOUR_USER_ID>
|
|
37
|
+
grok-tg --name work install
|
|
38
|
+
grok-tg --name work status
|
|
31
39
|
`;
|
|
32
40
|
|
|
33
41
|
async function main(): Promise<void> {
|
|
34
42
|
const args = process.argv.slice(2);
|
|
35
|
-
const
|
|
43
|
+
const rest = stripInstanceFlags(args);
|
|
44
|
+
const [cmd, arg] = rest;
|
|
36
45
|
|
|
37
46
|
switch (cmd) {
|
|
38
47
|
case "run":
|
|
@@ -45,9 +54,9 @@ async function main(): Promise<void> {
|
|
|
45
54
|
// Run the plain-node setup script, targeting this folder (.env lives in
|
|
46
55
|
// the instance dir). Pass through optional <token> [userId] args.
|
|
47
56
|
const script = join(PROJECT_ROOT, "scripts", "setup.mjs");
|
|
48
|
-
const r = spawnSync(process.execPath, [script, ...
|
|
57
|
+
const r = spawnSync(process.execPath, [script, "--instance", INSTANCE_DIR, ...rest.slice(1)], {
|
|
49
58
|
stdio: "inherit",
|
|
50
|
-
env: { ...process.env, GROK_TG_CWD: INSTANCE_DIR },
|
|
59
|
+
env: { ...process.env, GROK_TG_DIR: INSTANCE_DIR, GROK_TG_CWD: INSTANCE_DIR },
|
|
51
60
|
});
|
|
52
61
|
process.exit(r.status ?? 0);
|
|
53
62
|
break;
|
|
@@ -57,7 +66,11 @@ async function main(): Promise<void> {
|
|
|
57
66
|
preflight();
|
|
58
67
|
const r = await getController().install(buildLaunchSpec());
|
|
59
68
|
console.log(r.ok ? `✓ ${r.message}` : `✗ ${r.message}`);
|
|
60
|
-
if (r.ok)
|
|
69
|
+
if (r.ok) {
|
|
70
|
+
const spec = buildLaunchSpec();
|
|
71
|
+
const flag = spec.slug ? `--name ${spec.slug} ` : "";
|
|
72
|
+
console.log(`\nManage it with: grok-tg ${flag}status | stop | restart | logs`);
|
|
73
|
+
}
|
|
61
74
|
process.exit(r.ok ? 0 : 1);
|
|
62
75
|
break;
|
|
63
76
|
}
|
|
@@ -86,6 +99,11 @@ async function main(): Promise<void> {
|
|
|
86
99
|
process.exit(0);
|
|
87
100
|
break;
|
|
88
101
|
|
|
102
|
+
case "instances":
|
|
103
|
+
printInstances();
|
|
104
|
+
process.exit(0);
|
|
105
|
+
break;
|
|
106
|
+
|
|
89
107
|
case "help":
|
|
90
108
|
case "--help":
|
|
91
109
|
case "-h":
|
|
@@ -112,6 +130,24 @@ function preflight(): void {
|
|
|
112
130
|
}
|
|
113
131
|
}
|
|
114
132
|
|
|
133
|
+
function printInstances(): void {
|
|
134
|
+
const items = listKnownInstances();
|
|
135
|
+
if (items.length === 0) {
|
|
136
|
+
console.log("No instances found.");
|
|
137
|
+
console.log(" grok-tg setup # default bot (~/.grok/tg)");
|
|
138
|
+
console.log(" grok-tg --name work setup <token> <userId> # second bot");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
console.log("Instances:\n");
|
|
142
|
+
for (const it of items) {
|
|
143
|
+
const manage = it.slug ? `grok-tg --name ${it.slug}` : "grok-tg";
|
|
144
|
+
console.log(` ${it.name}`);
|
|
145
|
+
console.log(` dir: ${it.dir}`);
|
|
146
|
+
console.log(` service: ${it.identity.id}`);
|
|
147
|
+
console.log(` manage: ${manage} status | restart | logs\n`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
115
151
|
function printLogs(n: number): void {
|
|
116
152
|
const file = buildLaunchSpec().logFile;
|
|
117
153
|
if (!existsSync(file)) {
|
package/src/config.ts
CHANGED
|
@@ -10,51 +10,42 @@ import { existsSync } from "node:fs";
|
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
+
import {
|
|
14
|
+
CANONICAL_DIR,
|
|
15
|
+
expandHome,
|
|
16
|
+
resolveInstanceDir as resolveNamedInstanceDir,
|
|
17
|
+
} from "./app/instance.js";
|
|
18
|
+
|
|
19
|
+
export { CANONICAL_DIR, expandHome };
|
|
13
20
|
|
|
14
21
|
/** Absolute path to the installed bot code (one level above src/). For a global
|
|
15
22
|
* npm install this lives inside node_modules — code lives here, never user data. */
|
|
16
23
|
export const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
24
|
|
|
18
|
-
/** Canonical, path-independent home for this bot's `.env`, `logs/`, `data/` and
|
|
19
|
-
* the single-instance locks: `~/.grok/tg`. Used whenever the bot is started
|
|
20
|
-
* without an explicit instance dir and there's no `.env` in the current folder,
|
|
21
|
-
* so the SAME configuration is found no matter which directory you launch from. */
|
|
22
|
-
export const CANONICAL_DIR = join(homedir(), ".grok", "tg");
|
|
23
|
-
|
|
24
25
|
/**
|
|
25
26
|
* Directory holding THIS instance's `.env`, `logs/` and `data/`. Resolution
|
|
26
27
|
* (first match wins):
|
|
27
28
|
* 1. `--instance <dir>` argv — set by the installed background service,
|
|
28
|
-
* 2. `
|
|
29
|
-
* 3. `
|
|
30
|
-
* 4.
|
|
29
|
+
* 2. `--name` / `GROK_TG_NAME` — named instance under `~/.grok/tg/instances/`,
|
|
30
|
+
* 3. `GROK_TG_DIR` env — an explicit override,
|
|
31
|
+
* 4. `GROK_TG_CWD` or cwd IF that folder already contains a `.env`,
|
|
31
32
|
* 5. the canonical `~/.grok/tg` home — the path-independent default.
|
|
32
33
|
*/
|
|
33
|
-
export const INSTANCE_DIR =
|
|
34
|
+
export const INSTANCE_DIR = resolveNamedInstanceDir({
|
|
35
|
+
argv: process.argv,
|
|
36
|
+
envDir: process.env.GROK_TG_DIR,
|
|
37
|
+
nameEnv: process.env.GROK_TG_NAME,
|
|
38
|
+
cwdHint: process.env.GROK_TG_CWD,
|
|
39
|
+
});
|
|
34
40
|
|
|
35
41
|
/** Absolute path to the `.env` this instance loads (and that `setup` writes). */
|
|
36
42
|
export const ENV_PATH = join(INSTANCE_DIR, ".env");
|
|
37
43
|
|
|
38
|
-
function resolveInstanceDir(): string {
|
|
39
|
-
const flag = process.argv.indexOf("--instance");
|
|
40
|
-
if (flag !== -1 && process.argv[flag + 1]) return resolve(process.argv[flag + 1]!);
|
|
41
|
-
const envDir = process.env.GROK_TG_DIR?.trim() || process.env.GROK_TG_CWD?.trim();
|
|
42
|
-
if (envDir) return resolve(expandHome(envDir));
|
|
43
|
-
if (existsSync(join(process.cwd(), ".env"))) return process.cwd();
|
|
44
|
-
return CANONICAL_DIR;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
44
|
// Load .env from the resolved instance directory. Keep the parsed values as
|
|
48
45
|
// well: a machine-wide TELEGRAM_BOT_TOKEN may belong to a sibling bot (Codex,
|
|
49
46
|
// Kiro, etc.) and must never override this Grok instance's identity.
|
|
50
47
|
const instanceEnv = loadDotenv({ path: ENV_PATH }).parsed ?? {};
|
|
51
48
|
|
|
52
|
-
function expandHome(p: string): string {
|
|
53
|
-
if (p === "~") return homedir();
|
|
54
|
-
if (p.startsWith("~/") || p.startsWith("~\\")) return join(homedir(), p.slice(2));
|
|
55
|
-
return p;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
49
|
function bool(v: string | undefined, def: boolean): boolean {
|
|
59
50
|
if (v === undefined || v === "") return def;
|
|
60
51
|
return ["1", "true", "yes", "on"].includes(v.toLowerCase());
|
|
@@ -142,6 +133,20 @@ export interface AppConfig {
|
|
|
142
133
|
* Approve/Deny buttons.
|
|
143
134
|
*/
|
|
144
135
|
autoApprovePermissions: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Auto-approve Grok plan-mode exit (no Approve/Changes/Abandon buttons).
|
|
138
|
+
* Default true so unattended/24/7 bots never wait on a TUI. Set false for
|
|
139
|
+
* interactive review in Telegram.
|
|
140
|
+
*/
|
|
141
|
+
autoApprovePlan: boolean;
|
|
142
|
+
/** GROK_SANDBOX profile (workspace-safe, strict, off, …). */
|
|
143
|
+
sandboxProfile?: string;
|
|
144
|
+
/** GROK_MEMORY setting forwarded to the agent process. */
|
|
145
|
+
grokMemory?: string;
|
|
146
|
+
/** `--agent-profile` for `grok agent`. */
|
|
147
|
+
agentProfile?: string;
|
|
148
|
+
/** `--plugin-dir` for `grok agent`. */
|
|
149
|
+
pluginDir?: string;
|
|
145
150
|
projectRoots: string[];
|
|
146
151
|
streamThrottleMs: number;
|
|
147
152
|
messageBatchMs: number;
|
|
@@ -301,6 +306,11 @@ export function loadConfig(): AppConfig {
|
|
|
301
306
|
trustAllTools: bool(process.env.GROK_TRUST_ALL_TOOLS, true),
|
|
302
307
|
// Default true: auto-approve with session-scope when the agent still asks.
|
|
303
308
|
autoApprovePermissions: bool(process.env.AUTO_APPROVE_PERMISSIONS, true),
|
|
309
|
+
autoApprovePlan: bool(process.env.AUTO_APPROVE_PLAN, true),
|
|
310
|
+
sandboxProfile: process.env.GROK_SANDBOX?.trim() || undefined,
|
|
311
|
+
grokMemory: process.env.GROK_MEMORY?.trim() || undefined,
|
|
312
|
+
agentProfile: process.env.GROK_AGENT_PROFILE?.trim() || undefined,
|
|
313
|
+
pluginDir: process.env.GROK_PLUGIN_DIR?.trim() || undefined,
|
|
304
314
|
projectRoots: [...new Set(roots)],
|
|
305
315
|
streamThrottleMs: num(process.env.STREAM_THROTTLE_MS, 1500),
|
|
306
316
|
messageBatchMs: nonNegNum(process.env.MESSAGE_BATCH_MS, 800),
|
package/src/forum/manager.ts
CHANGED
|
@@ -265,7 +265,8 @@ export class ForumManager {
|
|
|
265
265
|
() =>
|
|
266
266
|
this.api.sendMessage(
|
|
267
267
|
this.groupId,
|
|
268
|
-
`\u{1F916} **AI Chat** —
|
|
268
|
+
`\u{1F916} **AI Chat** — workspace coding chat.\nPath: \`${this.cfg.workspace}\`\n\n` +
|
|
269
|
+
`Use **General** to orchestrate projects (manager). Code here or in a project topic.`,
|
|
269
270
|
{ message_thread_id: topic.message_thread_id, parse_mode: "Markdown" },
|
|
270
271
|
),
|
|
271
272
|
"ai-chat-announce",
|
package/src/forum/thread.ts
CHANGED
|
@@ -14,3 +14,36 @@ export function batchKey(chatId: number, threadId: number | undefined, isForumGr
|
|
|
14
14
|
export function forumThreadId(threadId: number | undefined): number {
|
|
15
15
|
return threadId ?? FORUM_GENERAL_THREAD_ID;
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* True when this forum thread is the General manager topic.
|
|
20
|
+
* Only exact id `1` — do NOT treat private chats (undefined) as General.
|
|
21
|
+
* Callers should pass {@link forumThreadId} first when reading raw Telegram ids.
|
|
22
|
+
*/
|
|
23
|
+
export function isGeneralThread(threadId: number | undefined): boolean {
|
|
24
|
+
return threadId === FORUM_GENERAL_THREAD_ID;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Thread id for **outbound** Telegram API calls (`sendMessage`, `editMessage`, …).
|
|
29
|
+
*
|
|
30
|
+
* Critical: Bot API often rejects `message_thread_id: 1` for the General forum
|
|
31
|
+
* topic with "Bad Request: message thread not found". Omitting the field posts
|
|
32
|
+
* to General correctly. Real project topics (id > 1) must still pass the id.
|
|
33
|
+
*
|
|
34
|
+
* Inbound routing still uses {@link forumThreadId} (undefined → 1).
|
|
35
|
+
*/
|
|
36
|
+
export function outboundMessageThreadId(
|
|
37
|
+
threadId: number | undefined,
|
|
38
|
+
): number | undefined {
|
|
39
|
+
if (threadId === undefined || threadId === FORUM_GENERAL_THREAD_ID) return undefined;
|
|
40
|
+
return threadId;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Extra object for send/edit: only includes message_thread_id when safe. */
|
|
44
|
+
export function outboundThreadExtra(
|
|
45
|
+
threadId: number | undefined,
|
|
46
|
+
): { message_thread_id?: number } {
|
|
47
|
+
const id = outboundMessageThreadId(threadId);
|
|
48
|
+
return id !== undefined ? { message_thread_id: id } : {};
|
|
49
|
+
}
|
package/src/grok/client.ts
CHANGED
|
@@ -218,6 +218,10 @@ export interface GrokClientOptions {
|
|
|
218
218
|
autoRestart?: boolean;
|
|
219
219
|
promptIdleTimeoutMs?: number;
|
|
220
220
|
promptMaxMs?: number;
|
|
221
|
+
sandboxProfile?: string;
|
|
222
|
+
grokMemory?: string;
|
|
223
|
+
agentProfile?: string;
|
|
224
|
+
pluginDir?: string;
|
|
221
225
|
}
|
|
222
226
|
|
|
223
227
|
interface Pending {
|
|
@@ -288,6 +292,10 @@ export class GrokClient extends EventEmitter {
|
|
|
288
292
|
* outcomes). Must never kill the agent process.
|
|
289
293
|
*/
|
|
290
294
|
onSessionCancel?: (sessionId: string) => void;
|
|
295
|
+
/** Interactive (or auto) plan-mode exit. Default: auto-approve. */
|
|
296
|
+
planExitHandler?: (params: Record<string, unknown>) => Promise<unknown>;
|
|
297
|
+
/** Interactive (or skip) ask_user_question. Default: SkipInterview. */
|
|
298
|
+
askUserHandler?: (params: Record<string, unknown>) => Promise<unknown>;
|
|
291
299
|
|
|
292
300
|
constructor(private readonly opts: GrokClientOptions) {
|
|
293
301
|
super();
|
|
@@ -313,11 +321,15 @@ export class GrokClient extends EventEmitter {
|
|
|
313
321
|
// the new token. `--no-auto-update` was removed in grok 0.2.x (exit 2).
|
|
314
322
|
const args = ["agent", "--no-leader"];
|
|
315
323
|
if (this.opts.trustAllTools) args.push("--always-approve");
|
|
324
|
+
if (this.opts.agentProfile) args.push("--agent-profile", this.opts.agentProfile);
|
|
325
|
+
if (this.opts.pluginDir) args.push("--plugin-dir", this.opts.pluginDir);
|
|
316
326
|
args.push("stdio");
|
|
317
327
|
|
|
318
328
|
log.info(`spawning: ${this.opts.grokCliPath} ${args.join(" ")}`);
|
|
319
329
|
const env = { ...process.env };
|
|
320
330
|
if (this.opts.apiKey) env.XAI_API_KEY = this.opts.apiKey;
|
|
331
|
+
if (this.opts.sandboxProfile) env.GROK_SANDBOX = this.opts.sandboxProfile;
|
|
332
|
+
if (this.opts.grokMemory) env.GROK_MEMORY = this.opts.grokMemory;
|
|
321
333
|
const proc = spawn(this.opts.grokCliPath, args, {
|
|
322
334
|
stdio: ["pipe", "pipe", "pipe"],
|
|
323
335
|
cwd: this.opts.workspace,
|
|
@@ -419,7 +431,11 @@ export class GrokClient extends EventEmitter {
|
|
|
419
431
|
}
|
|
420
432
|
|
|
421
433
|
async newSession(cwd: string): Promise<string> {
|
|
422
|
-
const res = (await this.request("session/new", {
|
|
434
|
+
const res = (await this.request("session/new", {
|
|
435
|
+
cwd,
|
|
436
|
+
mcpServers: [],
|
|
437
|
+
...(this.opts.trustAllTools ? { _meta: { yoloMode: true } } : {}),
|
|
438
|
+
})) as { sessionId: string };
|
|
423
439
|
this.parseSessionExtras(res);
|
|
424
440
|
this.cwd.set(res.sessionId, cwd);
|
|
425
441
|
this.slog.create(res.sessionId, cwd);
|
|
@@ -622,6 +638,12 @@ export class GrokClient extends EventEmitter {
|
|
|
622
638
|
return this.request("_grok.dev/commands/execute", { sessionId, command });
|
|
623
639
|
}
|
|
624
640
|
|
|
641
|
+
/** Update spawn-time agent env (applied on the next `grok agent` restart). */
|
|
642
|
+
setAgentOptions(opts: { sandboxProfile?: string; grokMemory?: string }): void {
|
|
643
|
+
if (opts.sandboxProfile !== undefined) this.opts.sandboxProfile = opts.sandboxProfile;
|
|
644
|
+
if (opts.grokMemory !== undefined) this.opts.grokMemory = opts.grokMemory;
|
|
645
|
+
}
|
|
646
|
+
|
|
625
647
|
stop(): void {
|
|
626
648
|
this.stopped = true;
|
|
627
649
|
if (this.restartTimer) {
|
|
@@ -780,11 +802,13 @@ export class GrokClient extends EventEmitter {
|
|
|
780
802
|
(planSnippet ? ` plan=${planSnippet.replace(/\s+/g, " ").slice(0, 80)}` : "") +
|
|
781
803
|
(keys ? ` keys=[${keys}]` : ""),
|
|
782
804
|
);
|
|
783
|
-
result =
|
|
805
|
+
result = this.planExitHandler
|
|
806
|
+
? await this.planExitHandler(params)
|
|
807
|
+
: autoApproveExitPlanMode(params);
|
|
784
808
|
} else if (isAskUserQuestionMethod(method)) {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
809
|
+
result = this.askUserHandler
|
|
810
|
+
? await this.askUserHandler(params)
|
|
811
|
+
: autoSkipAskUserQuestion(params);
|
|
788
812
|
} else {
|
|
789
813
|
// We advertise no fs/terminal capabilities, so the agent shouldn't ask.
|
|
790
814
|
// Log at warn — unknown reverse methods used to silently break plan exit
|
|
@@ -24,6 +24,14 @@ export const EXT_EXIT_PLAN_MODE_ALT = "x.ai/exit_plan_mode";
|
|
|
24
24
|
export const EXT_ASK_USER_QUESTION = "_x.ai/ask_user_question";
|
|
25
25
|
export const EXT_ASK_USER_QUESTION_ALT = "x.ai/ask_user_question";
|
|
26
26
|
|
|
27
|
+
/** Plan approval outcomes accepted by Grok Build's ExitPlanModeExtResponse. */
|
|
28
|
+
export type PlanExitOutcome = "approved" | "abandoned" | "request_changes";
|
|
29
|
+
|
|
30
|
+
export interface PlanExitDecision {
|
|
31
|
+
outcome: PlanExitOutcome;
|
|
32
|
+
feedback?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
/**
|
|
28
36
|
* Auto-approve leaving plan mode so the agent can implement.
|
|
29
37
|
*
|
package/src/index.ts
CHANGED
|
@@ -146,6 +146,10 @@ async function main(): Promise<void> {
|
|
|
146
146
|
model: cfg.grokModel,
|
|
147
147
|
autoRestart: cfg.grokAutoRestart,
|
|
148
148
|
promptIdleTimeoutMs: cfg.promptIdleMs,
|
|
149
|
+
sandboxProfile: cfg.sandboxProfile,
|
|
150
|
+
grokMemory: cfg.grokMemory,
|
|
151
|
+
agentProfile: cfg.agentProfile,
|
|
152
|
+
pluginDir: cfg.pluginDir,
|
|
149
153
|
});
|
|
150
154
|
|
|
151
155
|
// Retry ACP connect — agent crash at boot should not kill the Telegram bot.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manager (OpenClaw-style) directive for the Telegram General topic.
|
|
3
|
+
*
|
|
4
|
+
* General is a chat-like orchestrator: it routes work to project topics,
|
|
5
|
+
* never implements project code itself, and reports statuses back to the user.
|
|
6
|
+
* Keep tidy-idempotent (no digit `{progress:…}` markers) so history cleaners
|
|
7
|
+
* can strip by exact match.
|
|
8
|
+
*/
|
|
9
|
+
import type { PromptInput } from "../app/types.js";
|
|
10
|
+
|
|
11
|
+
export const MANAGER_DIRECTIVE_MARKER = "MANAGER MODE (General topic — OpenClaw-style orchestrator):";
|
|
12
|
+
|
|
13
|
+
/** Marker for child-session completion wakes injected into General. */
|
|
14
|
+
export const MANAGER_WORK_REPORT_MARKER =
|
|
15
|
+
"MANAGER WORK REPORT (system — analyze and report to the user; do not invent facts):";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* First-prompt / steering block for General. Free of real progress digit tokens.
|
|
19
|
+
*/
|
|
20
|
+
export const MANAGER_DIRECTIVE = [
|
|
21
|
+
MANAGER_DIRECTIVE_MARKER,
|
|
22
|
+
"You are the global manager of this Telegram forum group — like OpenClaw.",
|
|
23
|
+
"This topic is a chat control room, not a coding workspace.",
|
|
24
|
+
"",
|
|
25
|
+
"How you behave:",
|
|
26
|
+
"- The user SEES your free-form prose in this chat. Always answer them in short chat text.",
|
|
27
|
+
"- Keep replies brief (a few sentences). No progress bars, tool dumps, job tables, or",
|
|
28
|
+
" \"Dispatching…\" / \"Sending to…\" narration.",
|
|
29
|
+
"- Use telegram JSON for side effects: search_memory, list_topics, send_prompt, notify.",
|
|
30
|
+
"- notify is an OPTIONAL extra ping — do NOT rely on it as the only user-facing channel.",
|
|
31
|
+
"- Example good reply: \"On it — continuing the ship gate in WindowsStoreListingGenerator.\"",
|
|
32
|
+
"- NEVER emit task-progress markers (no progress percent footers).",
|
|
33
|
+
"- NEVER enter plan mode, run self-recheck, or dump long tool/trace spam here.",
|
|
34
|
+
"- NEVER implement app code, edit project files, run builds/tests, or do multi-file work in General.",
|
|
35
|
+
"- Always DELEGATE real work to the correct project topic via telegram bridge actions.",
|
|
36
|
+
"",
|
|
37
|
+
"MEMORY-FIRST (mandatory — do this BEFORE any git/shell/file tools):",
|
|
38
|
+
"1. Read the auto-injected MANAGER CONTEXT (General history, memory hits, topics, jobs).",
|
|
39
|
+
"2. Call search_memory with the user's keywords (and list_topics if needed).",
|
|
40
|
+
"3. Prefer Telegram/bot memory + project topic session history over `git log` / filesystem.",
|
|
41
|
+
"4. Only use git if the user explicitly asks for git, or after memory has no useful hits.",
|
|
42
|
+
"5. Order of truth for \"what changed / last work\":",
|
|
43
|
+
" (a) General chat + manager memory hits with [age] stamps (newest first),",
|
|
44
|
+
" (b) that project's MOST RECENT topic sessions (highest recency / last user prompts / Done notes),",
|
|
45
|
+
" (c) ignore older sessions for the same app when a newer session exists,",
|
|
46
|
+
" (d) then optional git — never jump to git first.",
|
|
47
|
+
"6. When summarizing last work: weight last user prompts and assistant Done text by time,",
|
|
48
|
+
" not by how many times a keyword appears in an old session.",
|
|
49
|
+
"",
|
|
50
|
+
"Dispatch workflow:",
|
|
51
|
+
"1. Identify the target topic (exact title, #threadId, or create a new project topic).",
|
|
52
|
+
"2. Build a RICH child prompt from memory (what was done, what remains, acceptance criteria).",
|
|
53
|
+
"3. Emit one telegram JSON block: create_topic/set_path as needed, then send_prompt,",
|
|
54
|
+
" plus optional single notify if the user should hear about it.",
|
|
55
|
+
"4. After bridge results: silent is fine if dispatch ok; notify only on failure or if user asked.",
|
|
56
|
+
"5. MANAGER WORK REPORT wakes: notify only for outcomes the user needs (done/fail/important);",
|
|
57
|
+
" otherwise process silently (no notify).",
|
|
58
|
+
"",
|
|
59
|
+
"RESUME RELATED SESSIONS (critical):",
|
|
60
|
+
"- When memory/context shows a related session (session=019fc9ec or full UUID) and the user",
|
|
61
|
+
" wants a follow-up / continue / fix there, you MUST pass session_id on send_prompt.",
|
|
62
|
+
"- Without session_id the bridge uses the topic's CURRENT open session — often the wrong one.",
|
|
63
|
+
"- topic must be the EXACT forum title or #threadId from list_topics / memory — NEVER \"…\" / \"...\" / placeholders.",
|
|
64
|
+
"- If you only know the session id, omit topic: { \"action\": \"send_prompt\", \"session_id\": \"019fc9ec\", \"prompt\": \"...\" }",
|
|
65
|
+
"- Example: { \"action\": \"send_prompt\", \"topic\": \"MyApp\", \"session_id\": \"019fc9ec\", \"prompt\": \"...\" }",
|
|
66
|
+
"- Only omit session_id for brand-new work on the topic's foreground, or use new_session=true for a fresh session.",
|
|
67
|
+
"- On Topic not found: call list_topics and retry with the exact name or #id (or session_id only).",
|
|
68
|
+
"",
|
|
69
|
+
"New projects: create_topic with name + absolute path (folder is created if missing),",
|
|
70
|
+
"then send_prompt into that topic with the full kickoff instructions.",
|
|
71
|
+
"",
|
|
72
|
+
"User message:",
|
|
73
|
+
].join("\n");
|
|
74
|
+
|
|
75
|
+
/** True when text is a system work-report wake (meta; skip recheck / manager re-wrap noise). */
|
|
76
|
+
export function isManagerWorkReportPrompt(text: string): boolean {
|
|
77
|
+
return text.trimStart().startsWith(MANAGER_WORK_REPORT_MARKER);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Prepend manager directive (idempotent). */
|
|
81
|
+
export function wrapManagerDirective(input: PromptInput): PromptInput {
|
|
82
|
+
const body = input.text.trim() || "(see attached media / files)";
|
|
83
|
+
if (body.startsWith(MANAGER_DIRECTIVE_MARKER) || body.includes(MANAGER_DIRECTIVE_MARKER)) {
|
|
84
|
+
return input;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
...input,
|
|
88
|
+
text: `${MANAGER_DIRECTIVE}\n${body}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Build the meta prompt that wakes General after a child topic finishes. */
|
|
93
|
+
export function buildManagerWorkReportPrompt(payload: {
|
|
94
|
+
jobId: string;
|
|
95
|
+
targetName: string;
|
|
96
|
+
targetThreadId: number;
|
|
97
|
+
targetPath: string;
|
|
98
|
+
userAskPreview: string;
|
|
99
|
+
dispatchPromptPreview: string;
|
|
100
|
+
status: "done" | "failed" | "cancelled";
|
|
101
|
+
stopReason?: string;
|
|
102
|
+
error?: string;
|
|
103
|
+
assistantSummary: string;
|
|
104
|
+
filesSummary?: string;
|
|
105
|
+
childSessionId?: string;
|
|
106
|
+
}): string {
|
|
107
|
+
const lines = [
|
|
108
|
+
MANAGER_WORK_REPORT_MARKER,
|
|
109
|
+
"```json",
|
|
110
|
+
JSON.stringify(
|
|
111
|
+
{
|
|
112
|
+
jobId: payload.jobId,
|
|
113
|
+
status: payload.status,
|
|
114
|
+
target: {
|
|
115
|
+
name: payload.targetName,
|
|
116
|
+
threadId: payload.targetThreadId,
|
|
117
|
+
path: payload.targetPath,
|
|
118
|
+
},
|
|
119
|
+
userAskPreview: payload.userAskPreview.slice(0, 500),
|
|
120
|
+
dispatchPromptPreview: payload.dispatchPromptPreview.slice(0, 800),
|
|
121
|
+
stopReason: payload.stopReason,
|
|
122
|
+
error: payload.error,
|
|
123
|
+
childSessionId: payload.childSessionId,
|
|
124
|
+
filesSummary: payload.filesSummary,
|
|
125
|
+
assistantSummary: payload.assistantSummary.slice(0, 3500),
|
|
126
|
+
},
|
|
127
|
+
null,
|
|
128
|
+
2,
|
|
129
|
+
),
|
|
130
|
+
"```",
|
|
131
|
+
"If the user needs to know (success, failure, blocked, needs a decision), reply in short prose.",
|
|
132
|
+
"If this is routine and they did not ask for a status, a one-line note is enough.",
|
|
133
|
+
"Do not re-emit the same send_prompt unless a retry is clearly useful.",
|
|
134
|
+
"No progress markers. No job tables or multi-message status spam.",
|
|
135
|
+
];
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
|
@@ -52,6 +52,14 @@ export function cleanCommentLine(raw: string, max = COMMENT_MAX): string {
|
|
|
52
52
|
*/
|
|
53
53
|
export function stripDirectiveWrappers(raw: string): string {
|
|
54
54
|
let t = raw.trim().replace(/^\([^)]*\)\s*/, "");
|
|
55
|
+
// Manager context + work reports (meta).
|
|
56
|
+
if (/^MANAGER WORK REPORT \(system/i.test(t)) t = "";
|
|
57
|
+
if (/MANAGER CONTEXT \(auto/i.test(t)) {
|
|
58
|
+
t = t.replace(/MANAGER CONTEXT \(auto[\s\S]*?\n---\n\n/i, "");
|
|
59
|
+
}
|
|
60
|
+
if (/^MANAGER MODE \(General topic/i.test(t)) {
|
|
61
|
+
t = t.replace(/^MANAGER MODE \(General topic[\s\S]*?User message:\s*/i, "");
|
|
62
|
+
}
|
|
55
63
|
const marker = "User's new message:";
|
|
56
64
|
const i = t.lastIndexOf(marker);
|
|
57
65
|
if (i !== -1) t = t.slice(i + marker.length);
|
|
@@ -68,6 +76,7 @@ export function stripDirectiveWrappers(raw: string): string {
|
|
|
68
76
|
}
|
|
69
77
|
}
|
|
70
78
|
if (/^TELEGRAM BRIDGE RESULTS \(system/i.test(t.trim())) t = "";
|
|
79
|
+
if (/^MANAGER WORK REPORT \(system/i.test(t.trim())) t = "";
|
|
71
80
|
return t.trim();
|
|
72
81
|
}
|
|
73
82
|
|
|
@@ -81,6 +90,7 @@ export function cleanUserPreview(raw: string, max = COMMENT_MAX): string {
|
|
|
81
90
|
if (/^SELF-RECHECK DECISION \(meta only\)/i.test(t)) return "";
|
|
82
91
|
if (/^FOLLOW-UP SUGGESTIONS \(meta only\)/i.test(t)) return "";
|
|
83
92
|
if (/^TELEGRAM BRIDGE RESULTS \(system/i.test(t)) return "Telegram bridge";
|
|
93
|
+
if (/^MANAGER WORK REPORT \(system/i.test(t)) return "Manager work report";
|
|
84
94
|
return cleanCommentLine(t, max);
|
|
85
95
|
}
|
|
86
96
|
|