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
|
@@ -25,11 +25,25 @@ export const TELEGRAM_SEND_PROMPT_MAX = 5;
|
|
|
25
25
|
export type TelegramAction =
|
|
26
26
|
| { action: "create_topic"; name: string; path?: string }
|
|
27
27
|
| { action: "set_path"; topic: string; path: string }
|
|
28
|
-
| {
|
|
28
|
+
| {
|
|
29
|
+
action: "send_prompt";
|
|
30
|
+
topic: string;
|
|
31
|
+
prompt: string;
|
|
32
|
+
newSession?: boolean;
|
|
33
|
+
/** Resume this exact Grok session in the topic (full id or short prefix). */
|
|
34
|
+
sessionId?: string;
|
|
35
|
+
}
|
|
36
|
+
/** User-facing message in the current chat (General: only way to talk to the user). */
|
|
37
|
+
| { action: "notify"; text: string; important?: boolean }
|
|
29
38
|
| { action: "search_memory"; query: string; limit?: number }
|
|
39
|
+
| { action: "list_topics" }
|
|
40
|
+
| { action: "list_jobs" }
|
|
30
41
|
| { action: "list_bots" }
|
|
31
42
|
| { action: "bot_command"; bot: string; command: string; args?: string };
|
|
32
43
|
|
|
44
|
+
/** Max user-facing notify messages per turn (anti-spam). */
|
|
45
|
+
export const TELEGRAM_NOTIFY_MAX = 3;
|
|
46
|
+
|
|
33
47
|
export interface TelegramActionExtract {
|
|
34
48
|
actions: TelegramAction[];
|
|
35
49
|
/** Text with telegram action fences removed. */
|
|
@@ -49,8 +63,11 @@ export const TELEGRAM_BRIDGE_DIRECTIVE_BASE = [
|
|
|
49
63
|
'{ "telegram": [',
|
|
50
64
|
' { "action": "create_topic", "name": "Topic title", "path": "optional absolute project path or exact catalog name" },',
|
|
51
65
|
' { "action": "set_path", "topic": "Topic title or #threadId", "path": "absolute path or exact catalog name" },',
|
|
52
|
-
' { "action": "send_prompt", "topic": "
|
|
66
|
+
' { "action": "send_prompt", "topic": "ExactTopicTitle or #threadId", "prompt": "work for that topic", "new_session": false, "session_id": "optional-full-or-short-session-id" },',
|
|
67
|
+
' { "action": "notify", "text": "short message for the user", "important": false },',
|
|
53
68
|
' { "action": "search_memory", "query": "keywords about past work", "limit": 8 },',
|
|
69
|
+
' { "action": "list_topics" },',
|
|
70
|
+
' { "action": "list_jobs" },',
|
|
54
71
|
' { "action": "list_bots" },',
|
|
55
72
|
' { "action": "bot_command", "bot": "username_without_at", "command": "status", "args": "optional" }',
|
|
56
73
|
"] }",
|
|
@@ -62,22 +79,50 @@ export const TELEGRAM_BRIDGE_DIRECTIVE_BASE = [
|
|
|
62
79
|
"- set_path — bind/rebind an existing topic to a project path (absolute dir or exact catalog name).",
|
|
63
80
|
" Absolute paths that do not exist yet are created on disk.",
|
|
64
81
|
"- send_prompt — start or queue a prompt in another topic's session (does not wait for that turn to finish).",
|
|
65
|
-
" From General
|
|
66
|
-
"
|
|
67
|
-
"
|
|
82
|
+
" From General (manager) you MUST create/set path then send_prompt — never code in General.",
|
|
83
|
+
" Child sessions started from General auto report back with a MANAGER WORK REPORT when done.",
|
|
84
|
+
" topic = EXACT forum title (from list_topics / memory), #threadId, \"general\", or \"ai chat\".",
|
|
85
|
+
" NEVER use placeholders like \"…\", \"...\", \"topic\", \"there\" — call list_topics if unsure.",
|
|
86
|
+
" Optional new_session=true starts a FRESH session in that topic.",
|
|
87
|
+
" Optional session_id (full UUID or short prefix like 019fc9ec) RESUMES that exact session.",
|
|
88
|
+
" When session_id is set, topic may be omitted — the bridge maps session path → topic.",
|
|
89
|
+
" session_id is REQUIRED when memory found a related session and the user wants a follow-up there.",
|
|
90
|
+
" Without session_id the bridge uses the topic's current foreground session (often wrong).",
|
|
91
|
+
"- notify — optional extra short message. In General the user ALSO sees your free-form prose,",
|
|
92
|
+
" so always write a brief chat reply; do not stay silent after search/dispatch.",
|
|
93
|
+
" Max 3 notify per turn. Never notify dispatch chatter, job tables, or \"sending to…\" spam.",
|
|
94
|
+
"- search_memory — search indexed forum topics + session titles/comments/history (memory-first).",
|
|
95
|
+
"- list_topics — list all mapped forum topics (name, #id, path, kind).",
|
|
96
|
+
"- list_jobs — list recent manager dispatches from General (status, target topic).",
|
|
68
97
|
"- list_bots — list allowlisted sibling Telegram bots and command catalogs.",
|
|
69
98
|
"- bot_command — /command@bot; waits for that bot to settle. NOT a Done; timeouts return ok=false.",
|
|
70
99
|
"",
|
|
100
|
+
"Example (General — short prose plus dispatch):",
|
|
101
|
+
"```json",
|
|
102
|
+
'{ "telegram": [',
|
|
103
|
+
' { "action": "send_prompt", "topic": "WindowsStoreListingGenerator", "session_id": "019fc9ec", "prompt": "Continue ship gate." },',
|
|
104
|
+
' { "action": "notify", "text": "On it — continuing the ship gate in WindowsStoreListingGenerator." }',
|
|
105
|
+
"] }",
|
|
106
|
+
"```",
|
|
107
|
+
"",
|
|
108
|
+
"Example (session_id only — topic inferred from session path):",
|
|
109
|
+
"```json",
|
|
110
|
+
'{ "telegram": [',
|
|
111
|
+
' { "action": "send_prompt", "session_id": "019fc9ec", "prompt": "Continue the related work." }',
|
|
112
|
+
"] }",
|
|
113
|
+
"```",
|
|
114
|
+
"",
|
|
71
115
|
"Example (from General): create project topic + path + kick off work there:",
|
|
72
|
-
|
|
116
|
+
"```json",
|
|
73
117
|
'{ "telegram": [',
|
|
74
118
|
' { "action": "create_topic", "name": "MyApp", "path": "H:\\\\Projects\\\\MyApp" },',
|
|
75
|
-
' { "action": "send_prompt", "topic": "MyApp", "prompt": "1) scaffold\\n2) tests\\n3) README" }',
|
|
119
|
+
' { "action": "send_prompt", "topic": "MyApp", "new_session": true, "prompt": "1) scaffold\\n2) tests\\n3) README" },',
|
|
120
|
+
' { "action": "notify", "text": "Created **MyApp** and started scaffolding there." }',
|
|
76
121
|
"] }",
|
|
77
122
|
"```",
|
|
78
123
|
"",
|
|
79
124
|
"After you emit these actions, the bridge runs them and may send TELEGRAM BRIDGE RESULTS. Use those facts; do not invent replies.",
|
|
80
|
-
"The bridge strips the JSON fence from
|
|
125
|
+
"The bridge strips the JSON fence from display. In General write a short prose answer the user can see.",
|
|
81
126
|
"Do not spam actions. Prefer one orchestration block per turn.",
|
|
82
127
|
].join("\n");
|
|
83
128
|
|
|
@@ -87,6 +132,8 @@ export interface TelegramBridgeCaps {
|
|
|
87
132
|
allowedBots: string[];
|
|
88
133
|
/** username → command list for first-prompt teaching */
|
|
89
134
|
botCommands?: Record<string, Array<{ command: string; description?: string }>>;
|
|
135
|
+
/** When true, General manager wording is appended. */
|
|
136
|
+
managerMode?: boolean;
|
|
90
137
|
}
|
|
91
138
|
|
|
92
139
|
/** Full first-prompt directive including live capabilities. */
|
|
@@ -95,7 +142,7 @@ export function buildTelegramBridgeDirective(caps: TelegramBridgeCaps): string {
|
|
|
95
142
|
if (caps.forumReady && caps.topicGroupId !== undefined) {
|
|
96
143
|
lines.push(
|
|
97
144
|
`- Forum topics: READY (group ${caps.topicGroupId}). You may create_topic, set_path, and send_prompt.`,
|
|
98
|
-
`- General
|
|
145
|
+
`- General is the manager topic (orchestrates only). AI Chat uses GROK_WORKSPACE for coding there; project topics use their bound path.`,
|
|
99
146
|
);
|
|
100
147
|
} else if (caps.topicGroupId !== undefined) {
|
|
101
148
|
lines.push(
|
|
@@ -123,7 +170,12 @@ export function buildTelegramBridgeDirective(caps: TelegramBridgeCaps): string {
|
|
|
123
170
|
"- Sibling bots: none configured (ALLOWED_TELEGRAM_BOTS empty). list_bots returns empty; bot_command disabled.",
|
|
124
171
|
);
|
|
125
172
|
}
|
|
126
|
-
lines.push("- search_memory:
|
|
173
|
+
lines.push("- search_memory / list_topics / list_jobs: available against bot-owned indexes.");
|
|
174
|
+
if (caps.managerMode) {
|
|
175
|
+
lines.push(
|
|
176
|
+
"- You are in General manager mode: chat-like replies only; memory-first; dispatch via send_prompt; child work auto-reports back.",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
127
179
|
return lines.join("\n");
|
|
128
180
|
}
|
|
129
181
|
|
|
@@ -165,6 +217,7 @@ export function buildTelegramBridgeResultsPrompt(results: unknown[]): string {
|
|
|
165
217
|
payload,
|
|
166
218
|
"```",
|
|
167
219
|
"Continue the user's task with this information. Do not ask the user to paste results. Do not re-emit the same telegram actions unless something failed and a retry is useful.",
|
|
220
|
+
"If actions succeeded, continue with a short prose answer — never dump job tables or dispatch narration.",
|
|
168
221
|
].join("\n");
|
|
169
222
|
}
|
|
170
223
|
|
|
@@ -190,7 +243,7 @@ export function extractTelegramActions(text: string): TelegramActionExtract {
|
|
|
190
243
|
cleaned = cleaned.replace(/```(?:json|JSON)?\s*\r?\n[\s\S]*$/i, (tail) => {
|
|
191
244
|
if (
|
|
192
245
|
/"telegram"\s*:/i.test(tail) ||
|
|
193
|
-
/"action"\s*:\s*"(?:create_topic|set_path|send_prompt|search_memory|list_bots|bot_command)"/i.test(
|
|
246
|
+
/"action"\s*:\s*"(?:create_topic|set_path|send_prompt|notify|search_memory|list_topics|list_jobs|list_bots|bot_command)"/i.test(
|
|
194
247
|
tail,
|
|
195
248
|
)
|
|
196
249
|
) {
|
|
@@ -280,19 +333,58 @@ function normalizeAction(item: unknown): TelegramAction | undefined {
|
|
|
280
333
|
case "send_prompt":
|
|
281
334
|
case "topic_prompt":
|
|
282
335
|
case "prompt_topic": {
|
|
283
|
-
|
|
336
|
+
let topic = String(rec.topic ?? rec.name ?? rec.thread ?? rec.thread_id ?? rec.threadId ?? "").trim();
|
|
284
337
|
const prompt = String(rec.prompt ?? rec.text ?? rec.message ?? "").trim();
|
|
285
|
-
if (!topic || !prompt) return undefined;
|
|
286
338
|
const newSessionRaw = rec.new_session ?? rec.newSession ?? rec.fresh;
|
|
287
339
|
const newSession =
|
|
288
340
|
newSessionRaw === true ||
|
|
289
341
|
newSessionRaw === 1 ||
|
|
290
342
|
/^(1|true|yes|y)$/i.test(String(newSessionRaw ?? "").trim());
|
|
343
|
+
const sessionIdRaw = String(
|
|
344
|
+
rec.session_id ?? rec.sessionId ?? rec.sess ?? rec.session ?? "",
|
|
345
|
+
).trim();
|
|
346
|
+
const sessionId = sessionIdRaw
|
|
347
|
+
? sessionIdRaw.replace(/^#?sess[_-]?/i, "").slice(0, 64)
|
|
348
|
+
: undefined;
|
|
349
|
+
// Drop placeholder topics ("…") so resolve can use session_id / memory inference.
|
|
350
|
+
if (
|
|
351
|
+
!topic ||
|
|
352
|
+
/^[\s.…·•⋯︙\-–—_*~`'"“”‘’\u2026\u22ef\u3002]+$/u.test(topic) ||
|
|
353
|
+
/^(topic|there|here|same|related|todo|tbd|none|null|target)$/i.test(topic)
|
|
354
|
+
) {
|
|
355
|
+
topic = "";
|
|
356
|
+
}
|
|
357
|
+
// topic optional when session_id is present OR prompt alone (memory may infer topic).
|
|
358
|
+
if (!prompt) return undefined;
|
|
359
|
+
// Allow prompt-only send_prompt: bridge may infer topic from memory + user ask.
|
|
360
|
+
// Still require at least something actionable (prompt is enough).
|
|
361
|
+
// Full prompt for the target agent — do NOT hard-crop to Telegram size.
|
|
362
|
+
// Long prompts are split only in the human-visible announce message.
|
|
363
|
+
const PROMPT_MAX = 100_000;
|
|
291
364
|
return {
|
|
292
365
|
action: "send_prompt",
|
|
293
366
|
topic: topic.slice(0, 128),
|
|
294
|
-
prompt: prompt.slice(0,
|
|
367
|
+
prompt: prompt.length > PROMPT_MAX ? prompt.slice(0, PROMPT_MAX) : prompt,
|
|
295
368
|
newSession: newSession || undefined,
|
|
369
|
+
// session_id wins over new_session when both are set (resume is explicit).
|
|
370
|
+
sessionId: sessionId || undefined,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
case "notify":
|
|
374
|
+
case "message_user":
|
|
375
|
+
case "say":
|
|
376
|
+
case "tell_user": {
|
|
377
|
+
const text = String(rec.text ?? rec.message ?? rec.body ?? rec.prompt ?? "").trim();
|
|
378
|
+
if (!text) return undefined;
|
|
379
|
+
const impRaw = rec.important ?? rec.priority ?? rec.loud;
|
|
380
|
+
const important =
|
|
381
|
+
impRaw === true ||
|
|
382
|
+
impRaw === 1 ||
|
|
383
|
+
/^(1|true|yes|y|high|urgent|important)$/i.test(String(impRaw ?? "").trim());
|
|
384
|
+
return {
|
|
385
|
+
action: "notify",
|
|
386
|
+
text: text.slice(0, 3500),
|
|
387
|
+
important: important || undefined,
|
|
296
388
|
};
|
|
297
389
|
}
|
|
298
390
|
case "search_memory":
|
|
@@ -305,6 +397,13 @@ function normalizeAction(item: unknown): TelegramAction | undefined {
|
|
|
305
397
|
limit = Math.max(1, Math.min(20, Math.round(limit)));
|
|
306
398
|
return { action: "search_memory", query: query.slice(0, 300), limit };
|
|
307
399
|
}
|
|
400
|
+
case "list_topics":
|
|
401
|
+
case "topics":
|
|
402
|
+
return { action: "list_topics" };
|
|
403
|
+
case "list_jobs":
|
|
404
|
+
case "jobs":
|
|
405
|
+
case "manager_jobs":
|
|
406
|
+
return { action: "list_jobs" };
|
|
308
407
|
case "list_bots":
|
|
309
408
|
case "bots":
|
|
310
409
|
return { action: "list_bots" };
|
|
@@ -332,6 +431,7 @@ function normalizeAction(item: unknown): TelegramAction | undefined {
|
|
|
332
431
|
function capBotCommands(actions: TelegramAction[]): TelegramAction[] {
|
|
333
432
|
let botCmds = 0;
|
|
334
433
|
let sendPrompts = 0;
|
|
434
|
+
let notifies = 0;
|
|
335
435
|
const out: TelegramAction[] = [];
|
|
336
436
|
for (const a of actions) {
|
|
337
437
|
if (a.action === "bot_command") {
|
|
@@ -342,6 +442,10 @@ function capBotCommands(actions: TelegramAction[]): TelegramAction[] {
|
|
|
342
442
|
if (sendPrompts >= TELEGRAM_SEND_PROMPT_MAX) continue;
|
|
343
443
|
sendPrompts++;
|
|
344
444
|
}
|
|
445
|
+
if (a.action === "notify") {
|
|
446
|
+
if (notifies >= TELEGRAM_NOTIFY_MAX) continue;
|
|
447
|
+
notifies++;
|
|
448
|
+
}
|
|
345
449
|
out.push(a);
|
|
346
450
|
if (out.length >= TELEGRAM_ACTION_MAX) break;
|
|
347
451
|
}
|
package/src/service/linux.ts
CHANGED
|
@@ -11,8 +11,12 @@ import type { LaunchSpec, ServiceController, ServiceResult } from "./types.js";
|
|
|
11
11
|
|
|
12
12
|
const UNIT = "grok-telegram-bot.service";
|
|
13
13
|
|
|
14
|
-
function
|
|
15
|
-
return
|
|
14
|
+
function unitName(spec?: { id?: string }): string {
|
|
15
|
+
return spec?.id ? `${spec.id}.service` : UNIT;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function unitPath(spec?: { id?: string }): string {
|
|
19
|
+
return join(homedir(), ".config", "systemd", "user", unitName(spec));
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
export const linuxController: ServiceController = {
|
|
@@ -21,35 +25,37 @@ export const linuxController: ServiceController = {
|
|
|
21
25
|
async install(spec) {
|
|
22
26
|
mkdirSync(join(homedir(), ".config", "systemd", "user"), { recursive: true });
|
|
23
27
|
mkdirSync(spec.logsDir, { recursive: true });
|
|
24
|
-
|
|
28
|
+
const unit = unitName(spec);
|
|
29
|
+
writeFileSync(unitPath(spec), unitFile(spec), "utf-8");
|
|
25
30
|
|
|
26
31
|
runSafe("systemctl", ["--user", "daemon-reload"]);
|
|
27
|
-
const en = runSafe("systemctl", ["--user", "enable", "--now",
|
|
32
|
+
const en = runSafe("systemctl", ["--user", "enable", "--now", unit]);
|
|
28
33
|
if (!en.ok) return fail(`systemctl enable failed: ${en.out}`);
|
|
29
34
|
const linger = runSafe("loginctl", ["enable-linger", userInfo().username]);
|
|
30
35
|
const note = linger.ok ? " Boot-without-login enabled (linger)." : " (run `loginctl enable-linger` for boot-without-login)";
|
|
31
|
-
return ok(`Installed and started systemd user service "${
|
|
36
|
+
return ok(`Installed and started systemd user service "${unit}".${note}`);
|
|
32
37
|
},
|
|
33
38
|
|
|
34
|
-
async uninstall() {
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
async uninstall(spec) {
|
|
40
|
+
const unit = unitName(spec);
|
|
41
|
+
runSafe("systemctl", ["--user", "disable", "--now", unit]);
|
|
42
|
+
rmSync(unitPath(spec), { force: true });
|
|
37
43
|
runSafe("systemctl", ["--user", "daemon-reload"]);
|
|
38
|
-
return ok(`Removed systemd user service "${
|
|
44
|
+
return ok(`Removed systemd user service "${unit}".`);
|
|
39
45
|
},
|
|
40
46
|
|
|
41
|
-
async start() {
|
|
42
|
-
const r = runSafe("systemctl", ["--user", "start",
|
|
47
|
+
async start(spec) {
|
|
48
|
+
const r = runSafe("systemctl", ["--user", "start", unitName(spec)]);
|
|
43
49
|
return r.ok ? ok("Started.") : fail(r.out);
|
|
44
50
|
},
|
|
45
51
|
|
|
46
|
-
async stop() {
|
|
47
|
-
const r = runSafe("systemctl", ["--user", "stop",
|
|
52
|
+
async stop(spec) {
|
|
53
|
+
const r = runSafe("systemctl", ["--user", "stop", unitName(spec)]);
|
|
48
54
|
return r.ok ? ok("Stopped.") : fail(r.out);
|
|
49
55
|
},
|
|
50
56
|
|
|
51
|
-
async status() {
|
|
52
|
-
const r = runSafe("systemctl", ["--user", "status",
|
|
57
|
+
async status(spec) {
|
|
58
|
+
const r = runSafe("systemctl", ["--user", "status", unitName(spec), "--no-pager"]);
|
|
53
59
|
return ok(r.out.trim() || "No status.");
|
|
54
60
|
},
|
|
55
61
|
};
|
package/src/service/macos.ts
CHANGED
|
@@ -10,8 +10,12 @@ import type { LaunchSpec, ServiceController, ServiceResult } from "./types.js";
|
|
|
10
10
|
|
|
11
11
|
const LABEL = "com.grok.telegrambot";
|
|
12
12
|
|
|
13
|
-
function
|
|
14
|
-
return
|
|
13
|
+
function labelOf(spec?: { macosLabel?: string }): string {
|
|
14
|
+
return spec?.macosLabel || LABEL;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function plistPath(spec?: { macosLabel?: string }): string {
|
|
18
|
+
return join(homedir(), "Library", "LaunchAgents", `${labelOf(spec)}.plist`);
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
export const macosController: ServiceController = {
|
|
@@ -20,32 +24,33 @@ export const macosController: ServiceController = {
|
|
|
20
24
|
async install(spec) {
|
|
21
25
|
mkdirSync(join(homedir(), "Library", "LaunchAgents"), { recursive: true });
|
|
22
26
|
mkdirSync(spec.logsDir, { recursive: true });
|
|
23
|
-
const path = plistPath();
|
|
27
|
+
const path = plistPath(spec);
|
|
24
28
|
runSafe("launchctl", ["unload", "-w", path]); // ignore if not loaded
|
|
25
29
|
writeFileSync(path, plist(spec), "utf-8");
|
|
26
30
|
const r = runSafe("launchctl", ["load", "-w", path]);
|
|
27
|
-
return r.ok ? ok(`Installed and loaded LaunchAgent "${
|
|
31
|
+
return r.ok ? ok(`Installed and loaded LaunchAgent "${labelOf(spec)}".`) : fail(r.out);
|
|
28
32
|
},
|
|
29
33
|
|
|
30
|
-
async uninstall() {
|
|
31
|
-
runSafe("launchctl", ["unload", "-w", plistPath()]);
|
|
32
|
-
rmSync(plistPath(), { force: true });
|
|
33
|
-
return ok(`Removed LaunchAgent "${
|
|
34
|
+
async uninstall(spec) {
|
|
35
|
+
runSafe("launchctl", ["unload", "-w", plistPath(spec)]);
|
|
36
|
+
rmSync(plistPath(spec), { force: true });
|
|
37
|
+
return ok(`Removed LaunchAgent "${labelOf(spec)}".`);
|
|
34
38
|
},
|
|
35
39
|
|
|
36
|
-
async start() {
|
|
37
|
-
const r = runSafe("launchctl", ["start",
|
|
40
|
+
async start(spec) {
|
|
41
|
+
const r = runSafe("launchctl", ["start", labelOf(spec)]);
|
|
38
42
|
return r.ok ? ok("Started.") : fail(r.out);
|
|
39
43
|
},
|
|
40
44
|
|
|
41
|
-
async stop() {
|
|
42
|
-
const r = runSafe("launchctl", ["stop",
|
|
45
|
+
async stop(spec) {
|
|
46
|
+
const r = runSafe("launchctl", ["stop", labelOf(spec)]);
|
|
43
47
|
return r.ok ? ok("Stopped.") : fail(r.out);
|
|
44
48
|
},
|
|
45
49
|
|
|
46
|
-
async status() {
|
|
50
|
+
async status(spec) {
|
|
51
|
+
const lab = labelOf(spec);
|
|
47
52
|
const r = runSafe("launchctl", ["list"]);
|
|
48
|
-
const line = r.out.split("\n").find((l) => l.includes(
|
|
53
|
+
const line = r.out.split("\n").find((l) => l.includes(lab));
|
|
49
54
|
return ok(line ? `Loaded: ${line.trim()}` : "Not loaded.");
|
|
50
55
|
},
|
|
51
56
|
};
|
|
@@ -67,7 +72,7 @@ function plist(spec: LaunchSpec): string {
|
|
|
67
72
|
'<plist version="1.0">',
|
|
68
73
|
"<dict>",
|
|
69
74
|
" <key>Label</key>",
|
|
70
|
-
` <string>${
|
|
75
|
+
` <string>${labelOf(spec)}</string>`,
|
|
71
76
|
" <key>ProgramArguments</key>",
|
|
72
77
|
" <array>",
|
|
73
78
|
args,
|
package/src/service/platform.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { execFileSync, spawn } from "node:child_process";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
+
import { parseInstanceFlags, serviceIdentity } from "../app/instance.js";
|
|
7
8
|
import { PROJECT_ROOT, INSTANCE_DIR } from "../config.js";
|
|
8
9
|
|
|
9
10
|
export type Platform = "windows" | "linux" | "macos" | "unknown";
|
|
@@ -31,9 +32,14 @@ export function buildLaunchSpec(): LaunchSpec {
|
|
|
31
32
|
// the bot where its .env/logs/data live. Only appended for a global install
|
|
32
33
|
// (instance dir differs) so in-place checkouts keep identical launch args.
|
|
33
34
|
if (INSTANCE_DIR !== PROJECT_ROOT) args.push("--instance", INSTANCE_DIR);
|
|
35
|
+
const flags = parseInstanceFlags(process.argv);
|
|
36
|
+
const ident = serviceIdentity(INSTANCE_DIR, flags.name || process.env.GROK_TG_NAME);
|
|
34
37
|
return {
|
|
35
|
-
id:
|
|
36
|
-
displayName:
|
|
38
|
+
id: ident.id,
|
|
39
|
+
displayName: ident.displayName,
|
|
40
|
+
windowsTaskName: ident.windowsTaskName,
|
|
41
|
+
macosLabel: ident.macosLabel,
|
|
42
|
+
slug: ident.slug,
|
|
37
43
|
nodePath: process.execPath,
|
|
38
44
|
args,
|
|
39
45
|
cwd: PROJECT_ROOT,
|
|
@@ -41,7 +47,10 @@ export function buildLaunchSpec(): LaunchSpec {
|
|
|
41
47
|
// relaunches on exit — so its auto-updater exits cleanly instead of
|
|
42
48
|
// re-exec'ing (which would double-run). Windows applies no env, so its
|
|
43
49
|
// Scheduled Task (no auto-restart) takes the re-exec path instead.
|
|
44
|
-
env: {
|
|
50
|
+
env: {
|
|
51
|
+
GROK_TG_SUPERVISED: "1",
|
|
52
|
+
...(ident.slug ? { GROK_TG_NAME: ident.slug } : {}),
|
|
53
|
+
},
|
|
45
54
|
logsDir,
|
|
46
55
|
logFile: join(logsDir, "grok-telegram-bot.log"),
|
|
47
56
|
};
|
package/src/service/types.ts
CHANGED
|
@@ -7,6 +7,12 @@ export interface LaunchSpec {
|
|
|
7
7
|
id: string;
|
|
8
8
|
/** Human-readable name. */
|
|
9
9
|
displayName: string;
|
|
10
|
+
/** Windows Scheduled Task name (unique per named instance). */
|
|
11
|
+
windowsTaskName?: string;
|
|
12
|
+
/** launchd label (unique per named instance). */
|
|
13
|
+
macosLabel?: string;
|
|
14
|
+
/** Named instance slug when using `--name`. */
|
|
15
|
+
slug?: string;
|
|
10
16
|
/** Absolute path to the node binary that should run the bot. */
|
|
11
17
|
nodePath: string;
|
|
12
18
|
/** Arguments after the node binary (tsx loader + entry file). */
|
package/src/service/windows.ts
CHANGED
|
@@ -15,6 +15,14 @@ const TASK = "GrokTelegramBot";
|
|
|
15
15
|
/** Launcher dropped in the per-user Startup folder when no admin is available. */
|
|
16
16
|
const STARTUP_VBS = "GrokTelegramBot.vbs";
|
|
17
17
|
|
|
18
|
+
function taskName(spec?: { windowsTaskName?: string }): string {
|
|
19
|
+
return spec?.windowsTaskName || TASK;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function startupVbsFile(spec?: { slug?: string }): string {
|
|
23
|
+
return spec?.slug ? `GrokTelegramBot-${spec.slug}.vbs` : STARTUP_VBS;
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
/** The per-user Startup folder (runs at logon for the current user, no admin).
|
|
19
27
|
* Undefined only if APPDATA is unset (e.g. running with no roaming profile). */
|
|
20
28
|
function startupDir(): string | undefined {
|
|
@@ -22,9 +30,9 @@ function startupDir(): string | undefined {
|
|
|
22
30
|
return appData ? join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup") : undefined;
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
function startupVbsPath(): string | undefined {
|
|
33
|
+
function startupVbsPath(spec?: { slug?: string }): string | undefined {
|
|
26
34
|
const dir = startupDir();
|
|
27
|
-
return dir ? join(dir,
|
|
35
|
+
return dir ? join(dir, startupVbsFile(spec)) : undefined;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
/** Remove a leftover Startup-folder launcher (e.g. from an earlier non-elevated
|
|
@@ -40,8 +48,8 @@ function vbsPath(spec: LaunchSpec): string {
|
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
/** True when our hidden Scheduled Task is registered. */
|
|
43
|
-
function taskInstalled(): boolean {
|
|
44
|
-
return runSafe("schtasks", ["/Query", "/TN",
|
|
51
|
+
function taskInstalled(spec?: { windowsTaskName?: string }): boolean {
|
|
52
|
+
return runSafe("schtasks", ["/Query", "/TN", taskName(spec)]).ok;
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
/** True when a bot process matching this spec is currently running. Launch
|
|
@@ -63,35 +71,36 @@ export const windowsController: ServiceController = {
|
|
|
63
71
|
// Preferred: a hidden ONLOGON Scheduled Task. Registering a *logon-triggered*
|
|
64
72
|
// task is a privileged operation, so /Create succeeds only from an elevated
|
|
65
73
|
// (admin) terminal. From a normal terminal it returns "Access is denied".
|
|
66
|
-
|
|
74
|
+
const tn = taskName(spec);
|
|
75
|
+
runSafe("schtasks", ["/Delete", "/F", "/TN", tn]); // replace if present
|
|
67
76
|
const res = runSafe("schtasks", [
|
|
68
77
|
"/Create",
|
|
69
78
|
"/F",
|
|
70
79
|
"/SC",
|
|
71
80
|
"ONLOGON",
|
|
72
81
|
"/TN",
|
|
73
|
-
|
|
82
|
+
tn,
|
|
74
83
|
"/TR",
|
|
75
84
|
`wscript.exe "${vbs}"`,
|
|
76
85
|
]);
|
|
77
86
|
if (res.ok) {
|
|
78
87
|
removeStartupLauncher(); // avoid a leftover launcher double-starting the bot
|
|
79
|
-
if (!isRunning(spec)) runSafe("schtasks", ["/Run", "/TN",
|
|
80
|
-
return ok(`Installed scheduled task "${
|
|
88
|
+
if (!isRunning(spec)) runSafe("schtasks", ["/Run", "/TN", tn]);
|
|
89
|
+
return ok(`Installed scheduled task "${tn}" (starts at logon) and launched it.`);
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
// A task may still exist that we just couldn't overwrite (e.g. created by an
|
|
84
93
|
// earlier elevated install). Reuse it rather than ALSO adding a Startup
|
|
85
94
|
// launcher, which would double-launch the bot at logon (409 Conflict).
|
|
86
|
-
if (taskInstalled()) {
|
|
95
|
+
if (taskInstalled(spec)) {
|
|
87
96
|
removeStartupLauncher();
|
|
88
|
-
if (!isRunning(spec)) runSafe("schtasks", ["/Run", "/TN",
|
|
89
|
-
return ok(`Scheduled task "${
|
|
97
|
+
if (!isRunning(spec)) runSafe("schtasks", ["/Run", "/TN", tn]);
|
|
98
|
+
return ok(`Scheduled task "${tn}" already exists; launched it. (Re-run elevated to recreate it.)`);
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
// Fallback (no admin — the common case): drop the launcher in the per-user
|
|
93
102
|
// Startup folder. It runs hidden at every logon with no elevation.
|
|
94
|
-
const startupVbs = startupVbsPath();
|
|
103
|
+
const startupVbs = startupVbsPath(spec);
|
|
95
104
|
const dir = startupDir();
|
|
96
105
|
if (!startupVbs || !dir) {
|
|
97
106
|
return fail(
|
|
@@ -118,21 +127,21 @@ export const windowsController: ServiceController = {
|
|
|
118
127
|
|
|
119
128
|
async uninstall(spec) {
|
|
120
129
|
await this.stop(spec);
|
|
121
|
-
runSafe("schtasks", ["/Delete", "/F", "/TN",
|
|
130
|
+
runSafe("schtasks", ["/Delete", "/F", "/TN", taskName(spec)]); // best-effort (may not exist)
|
|
122
131
|
rmSync(vbsPath(spec), { force: true });
|
|
123
|
-
const startupVbs = startupVbsPath();
|
|
132
|
+
const startupVbs = startupVbsPath(spec);
|
|
124
133
|
if (startupVbs) rmSync(startupVbs, { force: true });
|
|
125
|
-
return ok(`Removed "${
|
|
134
|
+
return ok(`Removed "${taskName(spec)}" (scheduled task and/or Startup launcher).`);
|
|
126
135
|
},
|
|
127
136
|
|
|
128
137
|
async start(spec) {
|
|
129
138
|
if (isRunning(spec)) return ok("Already running.");
|
|
130
|
-
if (taskInstalled()) {
|
|
139
|
+
if (taskInstalled(spec)) {
|
|
131
140
|
// schtasks /Run returns once the task is queued (does not wait for the bot).
|
|
132
|
-
const res = runSafe("schtasks", ["/Run", "/TN",
|
|
141
|
+
const res = runSafe("schtasks", ["/Run", "/TN", taskName(spec)]);
|
|
133
142
|
return res.ok ? ok("Started.") : fail(res.out);
|
|
134
143
|
}
|
|
135
|
-
const startupVbs = startupVbsPath();
|
|
144
|
+
const startupVbs = startupVbsPath(spec);
|
|
136
145
|
if (startupVbs && existsSync(startupVbs)) {
|
|
137
146
|
// Forever-restart VBS — must be detached or this CLI never returns.
|
|
138
147
|
const launched = launchDetached("wscript.exe", [startupVbs]);
|
|
@@ -148,20 +157,20 @@ export const windowsController: ServiceController = {
|
|
|
148
157
|
},
|
|
149
158
|
|
|
150
159
|
async stop(spec) {
|
|
151
|
-
runSafe("schtasks", ["/End", "/TN",
|
|
160
|
+
runSafe("schtasks", ["/End", "/TN", taskName(spec)]); // best-effort if task-based
|
|
152
161
|
const res = runSafe("powershell", ["-NoProfile", "-Command", killScript(entryOf(spec))]);
|
|
153
162
|
return ok(`Stopped. ${res.out.trim()}`);
|
|
154
163
|
},
|
|
155
164
|
|
|
156
165
|
async status(spec) {
|
|
157
|
-
const installedTask = taskInstalled();
|
|
158
|
-
const startupVbs = startupVbsPath();
|
|
166
|
+
const installedTask = taskInstalled(spec);
|
|
167
|
+
const startupVbs = startupVbsPath(spec);
|
|
159
168
|
const installedStartup = !!startupVbs && existsSync(startupVbs);
|
|
160
169
|
const installed = installedTask || installedStartup;
|
|
161
170
|
const running = isRunning(spec);
|
|
162
171
|
const how = installedTask ? "scheduled task" : installedStartup ? "Startup folder" : "—";
|
|
163
172
|
const detail = installedTask
|
|
164
|
-
? `\n${runSafe("schtasks", ["/Query", "/TN",
|
|
173
|
+
? `\n${runSafe("schtasks", ["/Query", "/TN", taskName(spec), "/FO", "LIST"]).out.trim()}`
|
|
165
174
|
: installedStartup
|
|
166
175
|
? `\nLauncher: ${startupVbs}`
|
|
167
176
|
: "";
|
package/src/sessions/history.ts
CHANGED
|
@@ -10,8 +10,15 @@ import {
|
|
|
10
10
|
TELEGRAM_BRIDGE_MARKER,
|
|
11
11
|
TELEGRAM_BRIDGE_RESULTS_MARKER,
|
|
12
12
|
} from "../render/telegram-bridge.js";
|
|
13
|
+
import {
|
|
14
|
+
MANAGER_DIRECTIVE_MARKER,
|
|
15
|
+
MANAGER_WORK_REPORT_MARKER,
|
|
16
|
+
} from "../render/manager-directive.js";
|
|
13
17
|
import type { HistoryEntry, HistoryRole } from "./types.js";
|
|
14
18
|
|
|
19
|
+
/** Keep in sync with bot/manager-context.ts (avoid sessions→bot import). */
|
|
20
|
+
const MANAGER_CONTEXT_MARKER = "MANAGER CONTEXT (auto — use before dispatching work):";
|
|
21
|
+
|
|
15
22
|
const TAIL_WINDOWS = [256 * 1024, 1024 * 1024, 4 * 1024 * 1024]; // grow until entries found
|
|
16
23
|
|
|
17
24
|
interface RawEvent {
|
|
@@ -261,6 +268,17 @@ function cleanStoredText(text: string): string {
|
|
|
261
268
|
if (/^SELF-RECHECK DECISION \(meta only\)/i.test(t.trim())) t = "";
|
|
262
269
|
if (/^SELF-RECHECK \(automatic quality pass/i.test(t.trim())) t = "";
|
|
263
270
|
if (t.trimStart().startsWith(TELEGRAM_BRIDGE_RESULTS_MARKER)) t = "";
|
|
271
|
+
if (t.trimStart().startsWith(MANAGER_WORK_REPORT_MARKER)) t = "";
|
|
272
|
+
// Manager directive / context: keep only the user message tail.
|
|
273
|
+
if (t.includes(MANAGER_CONTEXT_MARKER)) {
|
|
274
|
+
const split = t.split(/\n---\n\n/);
|
|
275
|
+
if (split.length > 1) t = split.slice(1).join("\n---\n\n").trim();
|
|
276
|
+
}
|
|
277
|
+
if (t.includes(MANAGER_DIRECTIVE_MARKER)) {
|
|
278
|
+
const um = "User message:";
|
|
279
|
+
const ui = t.lastIndexOf(um);
|
|
280
|
+
if (ui !== -1) t = t.slice(ui + um.length).trim();
|
|
281
|
+
}
|
|
264
282
|
return t;
|
|
265
283
|
}
|
|
266
284
|
|