grok-telegram-bot 2.4.0 → 2.6.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 +38 -2
- package/CHANGELOG.md +190 -1
- package/README.md +60 -15
- package/docs/GROUP.md +260 -0
- package/docs/INSTALL.md +3 -0
- package/package.json +4 -4
- package/src/app/lifetime-flag.ts +20 -0
- package/src/app/settings-store.ts +47 -8
- package/src/app/types.ts +38 -1
- package/src/app/updater.ts +24 -3
- package/src/bot/auth.ts +100 -15
- package/src/bot/bot.ts +193 -17
- package/src/bot/chat-controller.ts +181 -18
- package/src/bot/commands.ts +69 -29
- package/src/bot/deps.ts +3 -0
- package/src/bot/group-memory.ts +339 -0
- package/src/bot/handlers/accounts.ts +7 -0
- package/src/bot/handlers/control.ts +85 -32
- package/src/bot/handlers/document.ts +31 -4
- package/src/bot/handlers/forum.ts +217 -0
- package/src/bot/handlers/menu.ts +86 -24
- package/src/bot/handlers/message.ts +247 -27
- package/src/bot/handlers/photo.ts +126 -16
- package/src/bot/handlers/running.ts +150 -24
- package/src/bot/handlers/session-card.ts +13 -5
- package/src/bot/handlers/sessions.ts +68 -18
- package/src/bot/handlers/voice.ts +52 -7
- package/src/bot/image-return.ts +11 -5
- package/src/bot/manager-context.ts +208 -0
- package/src/bot/manager-jobs.ts +142 -0
- package/src/bot/menu/ephemeral.ts +16 -3
- package/src/bot/menu/keyboard.ts +53 -14
- package/src/bot/menu/refresh.ts +3 -1
- package/src/bot/menu/status-panel.ts +12 -6
- package/src/bot/permission-service.ts +19 -0
- package/src/bot/prompt-anchor.ts +299 -0
- package/src/bot/prompt-content.ts +8 -0
- package/src/bot/registry.ts +94 -1
- package/src/bot/scope.ts +95 -0
- package/src/bot/session-runtime.ts +1280 -183
- package/src/bot/suggestions.ts +91 -31
- package/src/bot/telegram-actions.ts +1130 -0
- package/src/bot/telegram-bots.ts +496 -0
- package/src/bot/telegram-io.ts +97 -10
- package/src/cli.ts +2 -0
- package/src/config.ts +201 -2
- package/src/forum/bind-path.ts +146 -0
- package/src/forum/manager.ts +652 -0
- package/src/forum/project-icon.ts +142 -0
- package/src/forum/thread.ts +49 -0
- package/src/forum/topic-store.ts +114 -0
- package/src/forum/types.ts +29 -0
- package/src/grok/client.ts +130 -28
- package/src/index.ts +205 -75
- package/src/projects/manager.ts +16 -3
- package/src/render/chunk.ts +17 -10
- package/src/render/hashtags.ts +5 -1
- package/src/render/manager-directive.ts +137 -0
- package/src/render/session-comment.ts +74 -7
- package/src/render/telegram-bridge.ts +464 -0
- package/src/render/tool-call.ts +56 -37
- package/src/service/platform.ts +44 -7
- package/src/service/windows.ts +16 -4
- package/src/sessions/history.ts +68 -9
- package/src/sessions/process.ts +7 -0
- package/src/sessions/types.ts +2 -2
- package/src/stream/streamer.ts +62 -15
- package/scripts/analyze-jsonl.ts +0 -33
- package/scripts/delayed-restart.ps1 +0 -29
- package/scripts/probe-exit-response-shape.py +0 -77
- package/scripts/probe-plan-exit.py +0 -60
- package/scripts/probe-plan-exit2.py +0 -48
- package/scripts/probe-plan-fields.py +0 -41
- package/scripts/probe-plan-fields2.py +0 -58
- package/scripts/probe-plan-response-path.py +0 -48
- package/scripts/sample-claude-tooluse.ts +0 -21
- package/scripts/sample-kiro-events.ts +0 -31
- package/scripts/smoke-exit-plan.ts +0 -274
- package/scripts/smoke-exit-shapes.ts +0 -252
- package/scripts/smoke-import.mjs +0 -82
- package/scripts/smoke-import.ts +0 -73
package/src/render/tool-call.ts
CHANGED
|
@@ -159,7 +159,24 @@ function formatByKind(
|
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
// ---- helpers for code fences ----
|
|
162
|
+
// ---- helpers for code fences / path labels ----
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* File paths in **bold** break Telegram MarkdownV2 (Windows `\`, dots, long
|
|
166
|
+
* session paths). Always put paths in inline code; keep only the verb bold.
|
|
167
|
+
* ✏️ **Edit** `C:\Users\…\plan.md`
|
|
168
|
+
*/
|
|
169
|
+
function pathCode(path: string | undefined | null, fallback = "file"): string {
|
|
170
|
+
const p = (path || "").trim() || fallback;
|
|
171
|
+
// Inline code cannot contain raw backticks unescaped in our MD pipeline;
|
|
172
|
+
// replace rare ` in paths so the span stays closed.
|
|
173
|
+
return "`" + p.replace(/`/g, "'") + "`";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** `**Edit** \`path\`` style header fragment (no leading emoji). */
|
|
177
|
+
function boldVerbPath(verb: string, path: string | undefined | null, fallback = "file"): string {
|
|
178
|
+
return "**" + verb + "** " + pathCode(path, fallback);
|
|
179
|
+
}
|
|
163
180
|
|
|
164
181
|
/** Fence that lengthens itself when the body contains backticks (avoids MD break). */
|
|
165
182
|
function fence(text: string, lang?: string): string {
|
|
@@ -197,8 +214,8 @@ function formatExecute(
|
|
|
197
214
|
): string {
|
|
198
215
|
const cmd = extractCommand(raw);
|
|
199
216
|
const cwd = strOf(raw.cwd) || strOf(raw.working_directory) || strOf(raw.workingDirectory);
|
|
200
|
-
|
|
201
|
-
|
|
217
|
+
let out = "\u{1F4BB} **Run command**" + tail;
|
|
218
|
+
if (cwd) out += " in " + pathCode(truncate(cwd, 80));
|
|
202
219
|
if (toolName && toolName !== "execute" && toolName !== "shell" && toolName !== "bash") {
|
|
203
220
|
out += `\n tool: \`${toolName}\``;
|
|
204
221
|
}
|
|
@@ -226,8 +243,7 @@ function formatEdit(
|
|
|
226
243
|
toolName: string,
|
|
227
244
|
): string {
|
|
228
245
|
const path = extractPath(raw);
|
|
229
|
-
|
|
230
|
-
let out = "\u270F\uFE0F **" + title + "**" + tail;
|
|
246
|
+
let out = "\u270F\uFE0F " + boldVerbPath("Edit", path) + tail;
|
|
231
247
|
if (toolName && !/edit|replace/i.test(toolName)) out += `\n tool: \`${toolName}\``;
|
|
232
248
|
if (opts.showDiffs) {
|
|
233
249
|
const diff = buildEditDiff(u, raw, opts.diffMaxLines);
|
|
@@ -242,7 +258,7 @@ function formatEdit(
|
|
|
242
258
|
function formatWrite(u: SessionUpdate, kind: string, raw: Record<string, unknown>, tail: string): string {
|
|
243
259
|
const path = extractPath(raw);
|
|
244
260
|
const verb = kind === "create" ? "Create" : "Write";
|
|
245
|
-
let out = "\u{1F4DD}
|
|
261
|
+
let out = "\u{1F4DD} " + boldVerbPath(verb, path) + tail;
|
|
246
262
|
const content = extractContent(raw) || extractToolOutput(u);
|
|
247
263
|
if (content) {
|
|
248
264
|
out += "\n" + fence(truncateMiddleLines(content, OUTPUT_PREVIEW_LINES, CONTENT_PREVIEW_MAX), detectLang(path)) + "\n";
|
|
@@ -258,14 +274,13 @@ function formatRead(
|
|
|
258
274
|
): string {
|
|
259
275
|
const path = extractPath(raw, u);
|
|
260
276
|
const range = extractReadRange(raw);
|
|
261
|
-
let title = "Read " + (path || "file");
|
|
262
277
|
const parts: string[] = [];
|
|
263
278
|
if (range.startLine) parts.push("line " + range.startLine);
|
|
264
279
|
if (range.offset) parts.push("offset " + range.offset);
|
|
265
280
|
if (range.limit) parts.push("limit " + range.limit);
|
|
266
281
|
if (range.pages) parts.push("pages " + range.pages);
|
|
267
|
-
|
|
268
|
-
|
|
282
|
+
let out = "\u{1F4D6} " + boldVerbPath("Read", path) + tail;
|
|
283
|
+
if (parts.length) out += " (" + parts.join(", ") + ")";
|
|
269
284
|
if (toolName && toolName !== "read" && toolName !== "read_file") {
|
|
270
285
|
out += `\n tool: \`${toolName}\``;
|
|
271
286
|
}
|
|
@@ -292,11 +307,11 @@ function formatList(
|
|
|
292
307
|
strOf(raw.target_directory) ||
|
|
293
308
|
strOf(raw.targetDirectory) ||
|
|
294
309
|
".";
|
|
295
|
-
let out = "\u{1F4C1}
|
|
310
|
+
let out = "\u{1F4C1} " + boldVerbPath("List", truncate(path, 120), ".") + tail;
|
|
296
311
|
if (toolName) out += `\n tool: \`${toolName}\``;
|
|
297
312
|
const filters = extractFilters(raw);
|
|
298
|
-
if (filters.include) out += "\n include: " + filters.include;
|
|
299
|
-
if (filters.exclude) out += "\n exclude: " + filters.exclude;
|
|
313
|
+
if (filters.include) out += "\n include: `" + filters.include.replace(/`/g, "'") + "`";
|
|
314
|
+
if (filters.exclude) out += "\n exclude: `" + filters.exclude.replace(/`/g, "'") + "`";
|
|
300
315
|
const body = extractToolOutput(u);
|
|
301
316
|
if (body) {
|
|
302
317
|
out += "\n" + fence(truncateMiddleLines(body, OUTPUT_PREVIEW_LINES, OUTPUT_PREVIEW_MAX)) + "\n";
|
|
@@ -314,16 +329,16 @@ function formatSearch(
|
|
|
314
329
|
const path = extractSearchPath(raw);
|
|
315
330
|
const filters = extractFilters(raw);
|
|
316
331
|
const isGlob = /glob/i.test(toolName) || (!!filters.include && !query);
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
332
|
+
const verb = isGlob ? "Glob" : "Search";
|
|
333
|
+
let out = "\u{1F50E} **" + verb + "**" + tail;
|
|
334
|
+
if (query) out += " " + pathCode(truncate(query, 120), "query");
|
|
335
|
+
else if (path) out += " " + pathCode(truncate(path, 120));
|
|
321
336
|
if (toolName && toolName !== "search" && toolName !== "grep") {
|
|
322
337
|
out += `\n tool: \`${toolName}\``;
|
|
323
338
|
}
|
|
324
|
-
if (path &&
|
|
325
|
-
if (filters.include) out += "\n \u{1F4C1} include: " + filters.include;
|
|
326
|
-
if (filters.exclude) out += "\n \u{1F6AB} exclude: " + filters.exclude;
|
|
339
|
+
if (path && query) out += "\n \u{1F4C2} in: " + pathCode(truncate(path, 100));
|
|
340
|
+
if (filters.include) out += "\n \u{1F4C1} include: " + pathCode(filters.include);
|
|
341
|
+
if (filters.exclude) out += "\n \u{1F6AB} exclude: " + pathCode(filters.exclude);
|
|
327
342
|
if (raw.case_sensitive !== undefined) {
|
|
328
343
|
out += "\n case-sensitive: " + (raw.case_sensitive ? "yes" : "no");
|
|
329
344
|
}
|
|
@@ -338,7 +353,7 @@ function formatSearch(
|
|
|
338
353
|
|
|
339
354
|
function formatDelete(raw: Record<string, unknown>, tail: string): string {
|
|
340
355
|
const path = extractPath(raw);
|
|
341
|
-
return "\u{1F5D1}\uFE0F
|
|
356
|
+
return "\u{1F5D1}\uFE0F " + boldVerbPath("Delete", path) + tail;
|
|
342
357
|
}
|
|
343
358
|
|
|
344
359
|
function formatMove(kind: string, raw: Record<string, unknown>, tail: string): string {
|
|
@@ -352,20 +367,20 @@ function formatMove(kind: string, raw: Record<string, unknown>, tail: string): s
|
|
|
352
367
|
"**" +
|
|
353
368
|
tail +
|
|
354
369
|
"\n \u{1F4C4} " +
|
|
355
|
-
truncate(src, 100) +
|
|
370
|
+
pathCode(truncate(src, 100)) +
|
|
356
371
|
"\n \u27A1\uFE0F " +
|
|
357
|
-
truncate(dst, 100)
|
|
372
|
+
pathCode(truncate(dst, 100))
|
|
358
373
|
);
|
|
359
374
|
}
|
|
360
|
-
return "\u{1F4E6}
|
|
375
|
+
return "\u{1F4E6} " + boldVerbPath(verb, src || dst) + tail;
|
|
361
376
|
}
|
|
362
377
|
|
|
363
378
|
function formatFetch(u: SessionUpdate, raw: Record<string, unknown>, tail: string): string {
|
|
364
379
|
const url = extractUrl(raw);
|
|
365
380
|
const method = strOf(raw.method) || strOf(raw.verb) || "GET";
|
|
366
|
-
let
|
|
367
|
-
|
|
368
|
-
|
|
381
|
+
let out = url
|
|
382
|
+
? "\u{1F310} **Fetch** " + pathCode(truncate(url, 200), "URL") + tail
|
|
383
|
+
: "\u{1F310} **Fetch URL**" + tail;
|
|
369
384
|
if (method && method !== "GET") out += "\n method: " + method;
|
|
370
385
|
const headers = raw.headers;
|
|
371
386
|
if (headers && typeof headers === "object") {
|
|
@@ -384,9 +399,8 @@ function formatFetch(u: SessionUpdate, raw: Record<string, unknown>, tail: strin
|
|
|
384
399
|
function formatWebSearch(u: SessionUpdate, raw: Record<string, unknown>, tail: string): string {
|
|
385
400
|
const query = extractSearchQuery(raw) || extractUrl(raw);
|
|
386
401
|
const count = strOf(raw.count) || strOf(raw.num) || strOf(raw.num_results) || numStr(raw.num_results);
|
|
387
|
-
let
|
|
388
|
-
if (query)
|
|
389
|
-
let out = "\u{1F310} **" + title + "**" + tail;
|
|
402
|
+
let out = "\u{1F310} **Web search**" + tail;
|
|
403
|
+
if (query) out += " " + pathCode(truncate(query, 150), "query");
|
|
390
404
|
if (count) out += "\n results: " + count;
|
|
391
405
|
const result = extractToolOutput(u);
|
|
392
406
|
if (result) {
|
|
@@ -428,7 +442,7 @@ function formatImage(raw: Record<string, unknown>, tail: string, toolName: strin
|
|
|
428
442
|
const path = extractPath(raw) || strOf(raw.image) || strOf(raw.output);
|
|
429
443
|
let out = "\u{1F5BC}\uFE0F **Image**" + tail;
|
|
430
444
|
if (toolName) out += `\n tool: \`${toolName}\``;
|
|
431
|
-
if (path) out += "\n " + truncate(path, 200);
|
|
445
|
+
if (path) out += "\n " + pathCode(truncate(path, 200));
|
|
432
446
|
const prompt = strOf(raw.prompt);
|
|
433
447
|
if (prompt) out += "\n prompt: " + truncate(prompt, 200);
|
|
434
448
|
return out;
|
|
@@ -482,27 +496,32 @@ function formatGeneric(
|
|
|
482
496
|
const path = extractPath(raw);
|
|
483
497
|
const cmd = extractCommand(raw);
|
|
484
498
|
const query = extractSearchQuery(raw);
|
|
485
|
-
// Never show a bare "Other" / "Tool call" — prefer real tool name /
|
|
499
|
+
// Never show a bare "Other" / "Tool call" — prefer real tool name. Paths/queries
|
|
500
|
+
// go in inline code (not bold) so Windows paths cannot break MarkdownV2.
|
|
486
501
|
let label =
|
|
487
502
|
(toolName && !/^tool[_ ]?call$/i.test(toolName) ? toolName : "") ||
|
|
488
503
|
(u.title && !/^other$/i.test(u.title.trim()) && !/^tool[_ ]?call$/i.test(u.title.trim())
|
|
489
504
|
? u.title.trim()
|
|
490
505
|
: "") ||
|
|
491
|
-
(path ? capitalize(kind !== "other" ? kind : "use")
|
|
506
|
+
(path ? capitalize(kind !== "other" ? kind : "use") : "") ||
|
|
492
507
|
(cmd ? "Run command" : "") ||
|
|
493
|
-
(query ? "Search
|
|
508
|
+
(query ? "Search" : "") ||
|
|
494
509
|
(kind && kind !== "other" ? capitalize(kind) : "") ||
|
|
495
510
|
(toolName || "Tool");
|
|
496
511
|
if (/^other$/i.test(label) || /^tool[_ ]?call$/i.test(label)) {
|
|
497
|
-
label = toolName && !/^tool[_ ]?call$/i.test(toolName) ? toolName :
|
|
512
|
+
label = toolName && !/^tool[_ ]?call$/i.test(toolName) ? toolName : "Tool";
|
|
513
|
+
}
|
|
514
|
+
// Titles that accidentally embed a path stay short: drop path from bold label.
|
|
515
|
+
if (path && label.includes(path)) {
|
|
516
|
+
label = label.replace(path, "").replace(/\s+/g, " ").trim() || capitalize(kind !== "other" ? kind : "Tool");
|
|
498
517
|
}
|
|
499
518
|
|
|
500
519
|
let out = icon + " **" + label + "**" + tail;
|
|
501
520
|
if (toolName && toolName !== label) out += `\n tool: \`${toolName}\``;
|
|
502
|
-
if (path
|
|
521
|
+
if (path) out += "\n \u{1F4C4} " + pathCode(truncate(path, 120));
|
|
503
522
|
if (cmd) out += "\n" + fence(truncateMiddle(cmd, PREVIEW_MAX), "bash") + "\n";
|
|
504
|
-
else if (query
|
|
505
|
-
else {
|
|
523
|
+
else if (query) out += "\n query: " + pathCode(truncate(query, 150), "query");
|
|
524
|
+
else if (!path && !cmd) {
|
|
506
525
|
const args = formatArgLines(raw);
|
|
507
526
|
if (args) out += "\n" + fence(truncateMiddle(args, PREVIEW_MAX)) + "\n";
|
|
508
527
|
}
|
package/src/service/platform.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Platform detection, launch-spec construction, and a small command runner
|
|
3
3
|
* shared by the per-OS service controllers.
|
|
4
4
|
*/
|
|
5
|
-
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { PROJECT_ROOT, INSTANCE_DIR } from "../config.js";
|
|
8
8
|
|
|
@@ -47,18 +47,55 @@ export function buildLaunchSpec(): LaunchSpec {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
/** Default max wait for schtasks/systemctl/powershell helpers — never hang the CLI. */
|
|
51
|
+
const RUN_TIMEOUT_MS = 45_000;
|
|
52
|
+
|
|
53
|
+
/** Run a command, returning combined output. Throws on non-zero exit or timeout. */
|
|
54
|
+
export function run(cmd: string, args: string[], timeoutMs = RUN_TIMEOUT_MS): string {
|
|
55
|
+
return execFileSync(cmd, args, {
|
|
56
|
+
encoding: "utf-8",
|
|
57
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
58
|
+
timeout: timeoutMs,
|
|
59
|
+
});
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
/** Run a command, swallowing errors and returning { ok, out }. */
|
|
56
|
-
export function runSafe(cmd: string, args: string[]): { ok: boolean; out: string } {
|
|
63
|
+
export function runSafe(cmd: string, args: string[], timeoutMs = RUN_TIMEOUT_MS): { ok: boolean; out: string } {
|
|
57
64
|
try {
|
|
58
|
-
return { ok: true, out: run(cmd, args) };
|
|
65
|
+
return { ok: true, out: run(cmd, args, timeoutMs) };
|
|
59
66
|
} catch (e) {
|
|
60
|
-
const err = e as {
|
|
67
|
+
const err = e as {
|
|
68
|
+
stdout?: Buffer | string;
|
|
69
|
+
stderr?: Buffer | string;
|
|
70
|
+
message?: string;
|
|
71
|
+
killed?: boolean;
|
|
72
|
+
code?: string;
|
|
73
|
+
};
|
|
74
|
+
// Node sets killed=true when the timeout option aborts the child.
|
|
75
|
+
if (err.killed || err.code === "ETIMEDOUT") {
|
|
76
|
+
return { ok: false, out: `timed out after ${timeoutMs}ms: ${cmd} ${args.join(" ")}` };
|
|
77
|
+
}
|
|
61
78
|
const out = String(err.stdout ?? "") + String(err.stderr ?? "") || err.message || "failed";
|
|
62
79
|
return { ok: false, out };
|
|
63
80
|
}
|
|
64
81
|
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Fire-and-forget process (detached). Use for forever-restart supervisors
|
|
85
|
+
* (Windows VBS loop) so `grok-tg install|start|restart` does not hang waiting
|
|
86
|
+
* for a process that never exits.
|
|
87
|
+
*/
|
|
88
|
+
export function launchDetached(cmd: string, args: string[]): { ok: boolean; out: string } {
|
|
89
|
+
try {
|
|
90
|
+
const child = spawn(cmd, args, {
|
|
91
|
+
detached: true,
|
|
92
|
+
stdio: "ignore",
|
|
93
|
+
windowsHide: true,
|
|
94
|
+
});
|
|
95
|
+
child.unref();
|
|
96
|
+
if (child.pid == null) return { ok: false, out: "spawn produced no pid" };
|
|
97
|
+
return { ok: true, out: `pid ${child.pid}` };
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return { ok: false, out: (e as Error).message || "spawn failed" };
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/service/windows.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
-
import { runSafe } from "./platform.js";
|
|
11
|
+
import { launchDetached, runSafe } from "./platform.js";
|
|
12
12
|
import type { LaunchSpec, ServiceController, ServiceResult } from "./types.js";
|
|
13
13
|
|
|
14
14
|
const TASK = "GrokTelegramBot";
|
|
@@ -105,7 +105,11 @@ export const windowsController: ServiceController = {
|
|
|
105
105
|
} catch (e) {
|
|
106
106
|
return fail(`Startup-folder install failed: ${(e as Error).message}`);
|
|
107
107
|
}
|
|
108
|
-
|
|
108
|
+
// Detached: the VBS is a forever-restart loop — never wait on it (hangs install/start/restart).
|
|
109
|
+
if (!isRunning(spec)) {
|
|
110
|
+
const launched = launchDetached("wscript.exe", [startupVbs]);
|
|
111
|
+
if (!launched.ok) return fail(`Installed launcher but failed to start: ${launched.out}`);
|
|
112
|
+
}
|
|
109
113
|
return ok(
|
|
110
114
|
`Installed via the Startup folder — starts hidden at logon, no admin needed — and launched it.\n` +
|
|
111
115
|
`(Tip: run "grok-tg install" from an elevated terminal to use a hidden Scheduled Task instead.)`,
|
|
@@ -124,13 +128,21 @@ export const windowsController: ServiceController = {
|
|
|
124
128
|
async start(spec) {
|
|
125
129
|
if (isRunning(spec)) return ok("Already running.");
|
|
126
130
|
if (taskInstalled()) {
|
|
131
|
+
// schtasks /Run returns once the task is queued (does not wait for the bot).
|
|
127
132
|
const res = runSafe("schtasks", ["/Run", "/TN", TASK]);
|
|
128
133
|
return res.ok ? ok("Started.") : fail(res.out);
|
|
129
134
|
}
|
|
130
135
|
const startupVbs = startupVbsPath();
|
|
131
136
|
if (startupVbs && existsSync(startupVbs)) {
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
// Forever-restart VBS — must be detached or this CLI never returns.
|
|
138
|
+
const launched = launchDetached("wscript.exe", [startupVbs]);
|
|
139
|
+
return launched.ok ? ok("Started.") : fail(launched.out);
|
|
140
|
+
}
|
|
141
|
+
// Local run-service.vbs in the package dir (task points here when elevated).
|
|
142
|
+
const local = vbsPath(spec);
|
|
143
|
+
if (existsSync(local)) {
|
|
144
|
+
const launched = launchDetached("wscript.exe", [local]);
|
|
145
|
+
return launched.ok ? ok("Started.") : fail(launched.out);
|
|
134
146
|
}
|
|
135
147
|
return fail(`Not installed. Run "grok-tg install" first.`);
|
|
136
148
|
},
|
package/src/sessions/history.ts
CHANGED
|
@@ -5,8 +5,20 @@
|
|
|
5
5
|
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
6
6
|
import { IMAGE_OUTPUT_DIRECTIVE } from "../render/image-output.js";
|
|
7
7
|
import { extractProgress, PROGRESS_DIRECTIVE } from "../render/progress.js";
|
|
8
|
+
import {
|
|
9
|
+
extractTelegramActions,
|
|
10
|
+
TELEGRAM_BRIDGE_MARKER,
|
|
11
|
+
TELEGRAM_BRIDGE_RESULTS_MARKER,
|
|
12
|
+
} from "../render/telegram-bridge.js";
|
|
13
|
+
import {
|
|
14
|
+
MANAGER_DIRECTIVE_MARKER,
|
|
15
|
+
MANAGER_WORK_REPORT_MARKER,
|
|
16
|
+
} from "../render/manager-directive.js";
|
|
8
17
|
import type { HistoryEntry, HistoryRole } from "./types.js";
|
|
9
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
|
+
|
|
10
22
|
const TAIL_WINDOWS = [256 * 1024, 1024 * 1024, 4 * 1024 * 1024]; // grow until entries found
|
|
11
23
|
|
|
12
24
|
interface RawEvent {
|
|
@@ -49,6 +61,7 @@ export function jsonlMtimeMs(jsonlPath: string): number {
|
|
|
49
61
|
/**
|
|
50
62
|
* Best-effort card blurb from the tail of a session log: last assistant prose
|
|
51
63
|
* (what was solved), else last user prompt. Skips import-confirm noise.
|
|
64
|
+
* @deprecated Prefer {@link readLastUserPrompt} for session card comments.
|
|
52
65
|
*/
|
|
53
66
|
export function readLastCardSummary(jsonlPath: string, maxEntries = 30): string {
|
|
54
67
|
const entries = readHistory(jsonlPath, maxEntries);
|
|
@@ -71,7 +84,24 @@ export function readLastCardSummary(jsonlPath: string, maxEntries = 30): string
|
|
|
71
84
|
return "";
|
|
72
85
|
}
|
|
73
86
|
|
|
74
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Last user prompt from the session log for card comments (newest → oldest).
|
|
89
|
+
* Strips complexity wrappers / import-confirm noise. Empty when none found.
|
|
90
|
+
*/
|
|
91
|
+
export function readLastUserPrompt(jsonlPath: string, maxEntries = 40, maxLen = 250): string {
|
|
92
|
+
const entries = readHistory(jsonlPath, maxEntries);
|
|
93
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
94
|
+
const e = entries[i]!;
|
|
95
|
+
if (e.role !== "user" || !e.text.trim()) continue;
|
|
96
|
+
const t = cleanCardProse(e.text, maxLen);
|
|
97
|
+
if (!t) continue;
|
|
98
|
+
if (/session import complete/i.test(t)) continue;
|
|
99
|
+
return t;
|
|
100
|
+
}
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function cleanCardProse(raw: string, max = 250): string {
|
|
75
105
|
let t = extractProgress(raw).cleaned;
|
|
76
106
|
t = t.replace(/```[\s\S]*?```/g, " ");
|
|
77
107
|
t = t.replace(/^COMPLEXITY \(decide yourself[\s\S]*?User task:\s*/i, "");
|
|
@@ -203,23 +233,52 @@ function toEntry(ev: RawEvent): HistoryEntry | undefined {
|
|
|
203
233
|
function cleanStoredText(text: string): string {
|
|
204
234
|
if (!text) return text;
|
|
205
235
|
let t = extractProgress(text).cleaned;
|
|
236
|
+
t = extractTelegramActions(t).cleaned;
|
|
206
237
|
if (t.includes(PROGRESS_DIRECTIVE)) t = t.split(PROGRESS_DIRECTIVE).join("").trim();
|
|
207
238
|
if (t.includes(IMAGE_OUTPUT_DIRECTIVE)) t = t.split(IMAGE_OUTPUT_DIRECTIVE).join("").trim();
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
239
|
+
// Prefer "User task (continued):" BEFORE plain "User task:" — the continued
|
|
240
|
+
// marker contains the substring "User task:", so lastIndexOf("User task:")
|
|
241
|
+
// would slice into "(continued):…" and leak bridge teaching into cards/logs.
|
|
242
|
+
const cont = "User task (continued):";
|
|
243
|
+
const ci = t.lastIndexOf(cont);
|
|
244
|
+
if (ci !== -1) {
|
|
245
|
+
t = t.slice(ci + cont.length).trim();
|
|
246
|
+
} else if (
|
|
247
|
+
/^COMPLEXITY \(decide yourself/i.test(t) ||
|
|
248
|
+
/^TASK COMPLEXITY:/i.test(t)
|
|
215
249
|
) {
|
|
216
|
-
|
|
250
|
+
const taskMarker = "User task:";
|
|
251
|
+
const ti = t.indexOf(taskMarker);
|
|
252
|
+
if (ti !== -1) t = t.slice(ti + taskMarker.length).trim();
|
|
253
|
+
}
|
|
254
|
+
// Strip leftover telegram bridge teaching if still present (directive-only wrap).
|
|
255
|
+
if (t.includes(TELEGRAM_BRIDGE_MARKER)) {
|
|
256
|
+
const mi = t.indexOf(TELEGRAM_BRIDGE_MARKER);
|
|
257
|
+
if (mi === 0) {
|
|
258
|
+
const after = t.slice(TELEGRAM_BRIDGE_MARKER.length);
|
|
259
|
+
const dbl = after.search(/\n\n(?![-*`])/);
|
|
260
|
+
t = dbl !== -1 ? after.slice(dbl).trim() : "";
|
|
261
|
+
} else {
|
|
262
|
+
t = t.slice(0, mi).trim();
|
|
263
|
+
}
|
|
217
264
|
}
|
|
218
265
|
// Drop removed/quiet meta-prompts if they landed in history.
|
|
219
266
|
if (/^Session status update \(meta only\)/i.test(t.trim())) t = "";
|
|
220
267
|
if (/^FOLLOW-UP SUGGESTIONS \(meta only\)/i.test(t.trim())) t = "";
|
|
221
268
|
if (/^SELF-RECHECK DECISION \(meta only\)/i.test(t.trim())) t = "";
|
|
222
269
|
if (/^SELF-RECHECK \(automatic quality pass/i.test(t.trim())) t = "";
|
|
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
|
+
}
|
|
223
282
|
return t;
|
|
224
283
|
}
|
|
225
284
|
|
package/src/sessions/process.ts
CHANGED
|
@@ -16,6 +16,13 @@ const log = createLogger("sessions:process");
|
|
|
16
16
|
*/
|
|
17
17
|
export function killPid(pid: number): boolean {
|
|
18
18
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
19
|
+
// Never kill this bot process (or a mistaken self-target). Session locks for
|
|
20
|
+
// multiplexed ACP turns store the child agent pid, not node — but a recycled
|
|
21
|
+
// or mis-attributed lock must not take the Telegram poller down.
|
|
22
|
+
if (pid === process.pid) {
|
|
23
|
+
log.warn(`refusing to kill pid ${pid} (this bot process)`);
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
19
26
|
try {
|
|
20
27
|
if (process.platform === "win32") {
|
|
21
28
|
execFileSync("taskkill", ["/F", "/T", "/PID", String(pid)], { stdio: "ignore" });
|
package/src/sessions/types.ts
CHANGED
|
@@ -14,8 +14,8 @@ export interface SessionMeta {
|
|
|
14
14
|
/** Size of the .jsonl history in bytes (proxy for conversation length). */
|
|
15
15
|
historyBytes: number;
|
|
16
16
|
/**
|
|
17
|
-
* Short status
|
|
18
|
-
*
|
|
17
|
+
* Short status for cards: last user prompt (persisted by the bot). While a
|
|
18
|
+
* turn is live, runtime may append last agent thinking as a second line.
|
|
19
19
|
*/
|
|
20
20
|
comment?: string;
|
|
21
21
|
}
|
package/src/stream/streamer.ts
CHANGED
|
@@ -15,8 +15,10 @@ import { chunkMarkdown } from "../render/chunk.js";
|
|
|
15
15
|
import { toTelegramMarkdown } from "../render/markdown.js";
|
|
16
16
|
import { extractProgress, progressBar } from "../render/progress.js";
|
|
17
17
|
import { estimateProgress } from "../render/progress-estimate.js";
|
|
18
|
+
import { stripTelegramActionFences } from "../render/telegram-bridge.js";
|
|
18
19
|
import { truncateMiddle } from "../render/truncate.js";
|
|
19
20
|
import { safeEdit, safeSend } from "../bot/telegram-io.js";
|
|
21
|
+
import { outboundThreadExtra } from "../forum/thread.js";
|
|
20
22
|
|
|
21
23
|
const SOFT_LIMIT = 3500;
|
|
22
24
|
/** Display budget for a thinking block (middle-truncated; session context keeps all). */
|
|
@@ -30,6 +32,18 @@ interface Seg {
|
|
|
30
32
|
toolId?: string;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
export interface StreamerOptions {
|
|
36
|
+
/** Chat-like mode: drop thoughts/tools/plan; only stream agent prose. */
|
|
37
|
+
proseOnly?: boolean;
|
|
38
|
+
/** When false, never render a progress bar (manager chat). Default true. */
|
|
39
|
+
showProgressBar?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Pre-posted message id to edit in place (e.g. General "Thinking…" placeholder).
|
|
42
|
+
* Avoids a separate bubble when the first real tokens arrive.
|
|
43
|
+
*/
|
|
44
|
+
seedMessageId?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
export class ResponseStreamer {
|
|
34
48
|
private readonly segs: Seg[] = [];
|
|
35
49
|
private sealedIdx = 0;
|
|
@@ -53,6 +67,8 @@ export class ResponseStreamer {
|
|
|
53
67
|
* the progress bar when set — done / in-progress / pending steps.
|
|
54
68
|
*/
|
|
55
69
|
private planMarkdown: string | undefined;
|
|
70
|
+
private readonly proseOnly: boolean;
|
|
71
|
+
private readonly showProgressBar: boolean;
|
|
56
72
|
|
|
57
73
|
constructor(
|
|
58
74
|
private readonly api: Api,
|
|
@@ -65,7 +81,14 @@ export class ResponseStreamer {
|
|
|
65
81
|
private readonly fallbackEnabled = false,
|
|
66
82
|
/** Turn start time, used by the fallback's elapsed-time signal. */
|
|
67
83
|
private readonly turnStartedAt = Date.now(),
|
|
68
|
-
|
|
84
|
+
/** Forum topic thread — required so stream edits land in the right topic. */
|
|
85
|
+
private readonly messageThreadId?: number,
|
|
86
|
+
opts?: StreamerOptions,
|
|
87
|
+
) {
|
|
88
|
+
this.proseOnly = !!opts?.proseOnly;
|
|
89
|
+
this.showProgressBar = opts?.showProgressBar !== false;
|
|
90
|
+
if (opts?.seedMessageId !== undefined) this.liveId = opts.seedMessageId;
|
|
91
|
+
}
|
|
69
92
|
|
|
70
93
|
/** Replace the hashtag footer (used after a logical fork swaps the session id
|
|
71
94
|
* mid-turn, so the streamed response carries the NEW session's tags). */
|
|
@@ -73,15 +96,26 @@ export class ResponseStreamer {
|
|
|
73
96
|
this.footer = footer;
|
|
74
97
|
}
|
|
75
98
|
|
|
99
|
+
/** Seed/replace the live bubble id (General Thinking… placeholder). */
|
|
100
|
+
seedLiveMessage(messageId: number): void {
|
|
101
|
+
this.liveId = messageId;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Current live Telegram message id (for attaching suggestions after finalize). */
|
|
105
|
+
get liveMessageId(): number | undefined {
|
|
106
|
+
return this.liveId;
|
|
107
|
+
}
|
|
108
|
+
|
|
76
109
|
/** "\n\n<footer>" appended to every finished message bubble (e.g. hashtags). */
|
|
77
110
|
private footerSuffix(): string {
|
|
78
111
|
return this.footer ? `\n\n${this.footer}` : "";
|
|
79
112
|
}
|
|
80
113
|
|
|
81
|
-
/** Strip `{progress: N%}` markers
|
|
82
|
-
*
|
|
114
|
+
/** Strip `{progress: N%}` markers and telegram action JSON fences from
|
|
115
|
+
* rendered text, remembering the latest progress value. */
|
|
83
116
|
private captureProgress(text: string): string {
|
|
84
|
-
const
|
|
117
|
+
const withoutTg = stripTelegramActionFences(text);
|
|
118
|
+
const { value, cleaned } = extractProgress(withoutTg);
|
|
85
119
|
if (value !== undefined) this.setProgressValue(value, true);
|
|
86
120
|
return cleaned;
|
|
87
121
|
}
|
|
@@ -90,6 +124,7 @@ export class ResponseStreamer {
|
|
|
90
124
|
* and notifying the owner on change. Agent markers are authoritative: once
|
|
91
125
|
* one arrives, the bot fallback stops contributing. */
|
|
92
126
|
private setProgressValue(pct: number, fromAgent: boolean): void {
|
|
127
|
+
if (!this.showProgressBar) return;
|
|
93
128
|
if (fromAgent) this.agentReported = true;
|
|
94
129
|
const next = Math.max(this.progress ?? 0, Math.round(pct));
|
|
95
130
|
if (next === this.progress) return;
|
|
@@ -104,7 +139,7 @@ export class ResponseStreamer {
|
|
|
104
139
|
/** Advance the fallback estimate from real activity signals, but only while
|
|
105
140
|
* the agent itself hasn't reported a value. No-op when fallback is off. */
|
|
106
141
|
private applyFallback(): void {
|
|
107
|
-
if (!this.fallbackEnabled || this.agentReported) return;
|
|
142
|
+
if (!this.showProgressBar || !this.fallbackEnabled || this.agentReported) return;
|
|
108
143
|
const est = estimateProgress({
|
|
109
144
|
toolCalls: this.toolCalls,
|
|
110
145
|
outputChars: this.outChars,
|
|
@@ -117,16 +152,24 @@ export class ResponseStreamer {
|
|
|
117
152
|
/** Called when the turn finishes successfully: if the agent never reported
|
|
118
153
|
* its own progress, fill the fallback bar to 100. No-op otherwise. */
|
|
119
154
|
completeFallback(): void {
|
|
120
|
-
if (!this.fallbackEnabled || this.agentReported) return;
|
|
155
|
+
if (!this.showProgressBar || !this.fallbackEnabled || this.agentReported) return;
|
|
121
156
|
this.setProgressValue(100, false);
|
|
122
157
|
}
|
|
123
158
|
|
|
159
|
+
private threadExtra(): Record<string, unknown> {
|
|
160
|
+
// Never send message_thread_id=1 (General) — Telegram rejects it.
|
|
161
|
+
return outboundThreadExtra(this.messageThreadId);
|
|
162
|
+
}
|
|
163
|
+
|
|
124
164
|
/** reply_parameters threading EVERY message of the turn to the user's prompt,
|
|
125
165
|
* so the whole response (all bubbles, tool calls and continuations) stays in
|
|
126
|
-
* one thread — not just the first message. */
|
|
166
|
+
* one thread — not just the first message. Also carries forum topic id. */
|
|
127
167
|
private replyExtra(): Record<string, unknown> {
|
|
128
|
-
|
|
129
|
-
|
|
168
|
+
const extra: Record<string, unknown> = { ...this.threadExtra() };
|
|
169
|
+
if (this.replyTo !== undefined) {
|
|
170
|
+
extra.reply_parameters = { message_id: this.replyTo, allow_sending_without_reply: true };
|
|
171
|
+
}
|
|
172
|
+
return extra;
|
|
130
173
|
}
|
|
131
174
|
|
|
132
175
|
appendOutput(text: string): void {
|
|
@@ -137,7 +180,7 @@ export class ResponseStreamer {
|
|
|
137
180
|
}
|
|
138
181
|
|
|
139
182
|
appendThought(text: string): void {
|
|
140
|
-
if (!text) return;
|
|
183
|
+
if (!text || this.proseOnly) return;
|
|
141
184
|
this.thoughtChars += text.length;
|
|
142
185
|
this.merge("think", text);
|
|
143
186
|
this.schedule();
|
|
@@ -148,7 +191,7 @@ export class ResponseStreamer {
|
|
|
148
191
|
* for ACP tool calls that stream progress/output under a stable toolCallId.
|
|
149
192
|
*/
|
|
150
193
|
addTool(rawMarkdown: string): void {
|
|
151
|
-
if (!rawMarkdown) return;
|
|
194
|
+
if (!rawMarkdown || this.proseOnly) return;
|
|
152
195
|
this.toolCalls += 1;
|
|
153
196
|
this.segs.push({ kind: "tool", text: rawMarkdown });
|
|
154
197
|
this.schedule();
|
|
@@ -161,6 +204,7 @@ export class ResponseStreamer {
|
|
|
161
204
|
*/
|
|
162
205
|
/** Replace the live plan board (or clear with empty/undefined). */
|
|
163
206
|
setPlan(markdown: string | undefined): void {
|
|
207
|
+
if (this.proseOnly) return;
|
|
164
208
|
const next = markdown?.trim() ? markdown.trim() : undefined;
|
|
165
209
|
if (next === this.planMarkdown) return;
|
|
166
210
|
this.planMarkdown = next;
|
|
@@ -168,7 +212,7 @@ export class ResponseStreamer {
|
|
|
168
212
|
}
|
|
169
213
|
|
|
170
214
|
upsertTool(toolId: string | undefined, rawMarkdown: string): void {
|
|
171
|
-
if (!rawMarkdown) return;
|
|
215
|
+
if (!rawMarkdown || this.proseOnly) return;
|
|
172
216
|
const id = (toolId || "").trim();
|
|
173
217
|
if (id) {
|
|
174
218
|
// Replace any existing segment with this id (newest first; includes rare
|
|
@@ -193,8 +237,9 @@ export class ResponseStreamer {
|
|
|
193
237
|
this.schedule();
|
|
194
238
|
}
|
|
195
239
|
|
|
240
|
+
/** True when agent prose/tools/thoughts were appended (not just a seed bubble). */
|
|
196
241
|
get hasOutput(): boolean {
|
|
197
|
-
return this.
|
|
242
|
+
return this.segs.some((s) => s.text.trim().length > 0);
|
|
198
243
|
}
|
|
199
244
|
|
|
200
245
|
async finalize(): Promise<void> {
|
|
@@ -202,6 +247,8 @@ export class ResponseStreamer {
|
|
|
202
247
|
if (this.timer) clearTimeout(this.timer);
|
|
203
248
|
this.timer = undefined;
|
|
204
249
|
await this.flush(true);
|
|
250
|
+
// Seeded Thinking… with zero agent text: leave the placeholder for the
|
|
251
|
+
// owner to replace (fallback reply) or keep while bridge results chain.
|
|
205
252
|
}
|
|
206
253
|
|
|
207
254
|
// ── internals ──────────────────────────────────────────────────────────────
|
|
@@ -240,8 +287,8 @@ export class ResponseStreamer {
|
|
|
240
287
|
// Live bubble: body → plan (always above progress) → progress bar → footer.
|
|
241
288
|
const parts: string[] = [];
|
|
242
289
|
if (base.trim()) parts.push(base);
|
|
243
|
-
if (this.planMarkdown) parts.push(this.planMarkdown);
|
|
244
|
-
if (this.progress !== undefined) parts.push(progressBar(this.progress));
|
|
290
|
+
if (!this.proseOnly && this.planMarkdown) parts.push(this.planMarkdown);
|
|
291
|
+
if (this.showProgressBar && this.progress !== undefined) parts.push(progressBar(this.progress));
|
|
245
292
|
if (parts.length === 0) return;
|
|
246
293
|
const src = `${parts.join("\n\n")}${this.footerSuffix()}`;
|
|
247
294
|
const rendered = toTelegramMarkdown(src);
|