grok-telegram-bot 2.4.0 → 2.5.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 +119 -1
- package/README.md +58 -15
- package/docs/GROUP.md +225 -0
- package/docs/INSTALL.md +3 -0
- package/package.json +1 -1
- package/src/app/lifetime-flag.ts +20 -0
- package/src/app/settings-store.ts +47 -8
- package/src/app/types.ts +12 -1
- package/src/app/updater.ts +24 -3
- package/src/bot/auth.ts +96 -15
- package/src/bot/bot.ts +122 -15
- package/src/bot/chat-controller.ts +52 -18
- package/src/bot/commands.ts +69 -29
- package/src/bot/deps.ts +3 -0
- package/src/bot/group-memory.ts +159 -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 +207 -0
- package/src/bot/handlers/menu.ts +86 -24
- package/src/bot/handlers/message.ts +101 -21
- package/src/bot/handlers/photo.ts +123 -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 +8 -5
- package/src/bot/menu/ephemeral.ts +13 -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 +300 -0
- package/src/bot/prompt-content.ts +3 -0
- package/src/bot/registry.ts +94 -1
- package/src/bot/scope.ts +94 -0
- package/src/bot/session-runtime.ts +647 -158
- package/src/bot/suggestions.ts +91 -31
- package/src/bot/telegram-actions.ts +440 -0
- package/src/bot/telegram-bots.ts +495 -0
- package/src/bot/telegram-io.ts +94 -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 +651 -0
- package/src/forum/project-icon.ts +142 -0
- package/src/forum/thread.ts +16 -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/session-comment.ts +64 -7
- package/src/render/telegram-bridge.ts +360 -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 +50 -9
- package/src/sessions/process.ts +7 -0
- package/src/sessions/types.ts +2 -2
- package/src/stream/streamer.ts +17 -6
- 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/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,6 +5,11 @@
|
|
|
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";
|
|
8
13
|
import type { HistoryEntry, HistoryRole } from "./types.js";
|
|
9
14
|
|
|
10
15
|
const TAIL_WINDOWS = [256 * 1024, 1024 * 1024, 4 * 1024 * 1024]; // grow until entries found
|
|
@@ -49,6 +54,7 @@ export function jsonlMtimeMs(jsonlPath: string): number {
|
|
|
49
54
|
/**
|
|
50
55
|
* Best-effort card blurb from the tail of a session log: last assistant prose
|
|
51
56
|
* (what was solved), else last user prompt. Skips import-confirm noise.
|
|
57
|
+
* @deprecated Prefer {@link readLastUserPrompt} for session card comments.
|
|
52
58
|
*/
|
|
53
59
|
export function readLastCardSummary(jsonlPath: string, maxEntries = 30): string {
|
|
54
60
|
const entries = readHistory(jsonlPath, maxEntries);
|
|
@@ -71,7 +77,24 @@ export function readLastCardSummary(jsonlPath: string, maxEntries = 30): string
|
|
|
71
77
|
return "";
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Last user prompt from the session log for card comments (newest → oldest).
|
|
82
|
+
* Strips complexity wrappers / import-confirm noise. Empty when none found.
|
|
83
|
+
*/
|
|
84
|
+
export function readLastUserPrompt(jsonlPath: string, maxEntries = 40, maxLen = 250): string {
|
|
85
|
+
const entries = readHistory(jsonlPath, maxEntries);
|
|
86
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
87
|
+
const e = entries[i]!;
|
|
88
|
+
if (e.role !== "user" || !e.text.trim()) continue;
|
|
89
|
+
const t = cleanCardProse(e.text, maxLen);
|
|
90
|
+
if (!t) continue;
|
|
91
|
+
if (/session import complete/i.test(t)) continue;
|
|
92
|
+
return t;
|
|
93
|
+
}
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function cleanCardProse(raw: string, max = 250): string {
|
|
75
98
|
let t = extractProgress(raw).cleaned;
|
|
76
99
|
t = t.replace(/```[\s\S]*?```/g, " ");
|
|
77
100
|
t = t.replace(/^COMPLEXITY \(decide yourself[\s\S]*?User task:\s*/i, "");
|
|
@@ -203,23 +226,41 @@ function toEntry(ev: RawEvent): HistoryEntry | undefined {
|
|
|
203
226
|
function cleanStoredText(text: string): string {
|
|
204
227
|
if (!text) return text;
|
|
205
228
|
let t = extractProgress(text).cleaned;
|
|
229
|
+
t = extractTelegramActions(t).cleaned;
|
|
206
230
|
if (t.includes(PROGRESS_DIRECTIVE)) t = t.split(PROGRESS_DIRECTIVE).join("").trim();
|
|
207
231
|
if (t.includes(IMAGE_OUTPUT_DIRECTIVE)) t = t.split(IMAGE_OUTPUT_DIRECTIVE).join("").trim();
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
232
|
+
// Prefer "User task (continued):" BEFORE plain "User task:" — the continued
|
|
233
|
+
// marker contains the substring "User task:", so lastIndexOf("User task:")
|
|
234
|
+
// would slice into "(continued):…" and leak bridge teaching into cards/logs.
|
|
235
|
+
const cont = "User task (continued):";
|
|
236
|
+
const ci = t.lastIndexOf(cont);
|
|
237
|
+
if (ci !== -1) {
|
|
238
|
+
t = t.slice(ci + cont.length).trim();
|
|
239
|
+
} else if (
|
|
240
|
+
/^COMPLEXITY \(decide yourself/i.test(t) ||
|
|
241
|
+
/^TASK COMPLEXITY:/i.test(t)
|
|
215
242
|
) {
|
|
216
|
-
|
|
243
|
+
const taskMarker = "User task:";
|
|
244
|
+
const ti = t.indexOf(taskMarker);
|
|
245
|
+
if (ti !== -1) t = t.slice(ti + taskMarker.length).trim();
|
|
246
|
+
}
|
|
247
|
+
// Strip leftover telegram bridge teaching if still present (directive-only wrap).
|
|
248
|
+
if (t.includes(TELEGRAM_BRIDGE_MARKER)) {
|
|
249
|
+
const mi = t.indexOf(TELEGRAM_BRIDGE_MARKER);
|
|
250
|
+
if (mi === 0) {
|
|
251
|
+
const after = t.slice(TELEGRAM_BRIDGE_MARKER.length);
|
|
252
|
+
const dbl = after.search(/\n\n(?![-*`])/);
|
|
253
|
+
t = dbl !== -1 ? after.slice(dbl).trim() : "";
|
|
254
|
+
} else {
|
|
255
|
+
t = t.slice(0, mi).trim();
|
|
256
|
+
}
|
|
217
257
|
}
|
|
218
258
|
// Drop removed/quiet meta-prompts if they landed in history.
|
|
219
259
|
if (/^Session status update \(meta only\)/i.test(t.trim())) t = "";
|
|
220
260
|
if (/^FOLLOW-UP SUGGESTIONS \(meta only\)/i.test(t.trim())) t = "";
|
|
221
261
|
if (/^SELF-RECHECK DECISION \(meta only\)/i.test(t.trim())) t = "";
|
|
222
262
|
if (/^SELF-RECHECK \(automatic quality pass/i.test(t.trim())) t = "";
|
|
263
|
+
if (t.trimStart().startsWith(TELEGRAM_BRIDGE_RESULTS_MARKER)) t = "";
|
|
223
264
|
return t;
|
|
224
265
|
}
|
|
225
266
|
|
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,6 +15,7 @@ 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";
|
|
20
21
|
|
|
@@ -65,6 +66,8 @@ export class ResponseStreamer {
|
|
|
65
66
|
private readonly fallbackEnabled = false,
|
|
66
67
|
/** Turn start time, used by the fallback's elapsed-time signal. */
|
|
67
68
|
private readonly turnStartedAt = Date.now(),
|
|
69
|
+
/** Forum topic thread — required so stream edits land in the right topic. */
|
|
70
|
+
private readonly messageThreadId?: number,
|
|
68
71
|
) {}
|
|
69
72
|
|
|
70
73
|
/** Replace the hashtag footer (used after a logical fork swaps the session id
|
|
@@ -78,10 +81,11 @@ export class ResponseStreamer {
|
|
|
78
81
|
return this.footer ? `\n\n${this.footer}` : "";
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
/** Strip `{progress: N%}` markers
|
|
82
|
-
*
|
|
84
|
+
/** Strip `{progress: N%}` markers and telegram action JSON fences from
|
|
85
|
+
* rendered text, remembering the latest progress value. */
|
|
83
86
|
private captureProgress(text: string): string {
|
|
84
|
-
const
|
|
87
|
+
const withoutTg = stripTelegramActionFences(text);
|
|
88
|
+
const { value, cleaned } = extractProgress(withoutTg);
|
|
85
89
|
if (value !== undefined) this.setProgressValue(value, true);
|
|
86
90
|
return cleaned;
|
|
87
91
|
}
|
|
@@ -121,12 +125,19 @@ export class ResponseStreamer {
|
|
|
121
125
|
this.setProgressValue(100, false);
|
|
122
126
|
}
|
|
123
127
|
|
|
128
|
+
private threadExtra(): Record<string, unknown> {
|
|
129
|
+
return this.messageThreadId !== undefined ? { message_thread_id: this.messageThreadId } : {};
|
|
130
|
+
}
|
|
131
|
+
|
|
124
132
|
/** reply_parameters threading EVERY message of the turn to the user's prompt,
|
|
125
133
|
* so the whole response (all bubbles, tool calls and continuations) stays in
|
|
126
|
-
* one thread — not just the first message. */
|
|
134
|
+
* one thread — not just the first message. Also carries forum topic id. */
|
|
127
135
|
private replyExtra(): Record<string, unknown> {
|
|
128
|
-
|
|
129
|
-
|
|
136
|
+
const extra: Record<string, unknown> = { ...this.threadExtra() };
|
|
137
|
+
if (this.replyTo !== undefined) {
|
|
138
|
+
extra.reply_parameters = { message_id: this.replyTo, allow_sending_without_reply: true };
|
|
139
|
+
}
|
|
140
|
+
return extra;
|
|
130
141
|
}
|
|
131
142
|
|
|
132
143
|
appendOutput(text: string): void {
|
package/scripts/analyze-jsonl.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { createReadStream } from "node:fs";
|
|
2
|
-
import { createInterface } from "node:readline";
|
|
3
|
-
|
|
4
|
-
const path = process.argv[2];
|
|
5
|
-
if (!path) {
|
|
6
|
-
console.error("usage: analyze-jsonl <path>");
|
|
7
|
-
process.exit(1);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const kinds = new Map<string, number>();
|
|
11
|
-
let lines = 0;
|
|
12
|
-
let bad = 0;
|
|
13
|
-
|
|
14
|
-
const rl = createInterface({ input: createReadStream(path, { encoding: "utf8" }) });
|
|
15
|
-
for await (const line of rl) {
|
|
16
|
-
if (!line.trim()) continue;
|
|
17
|
-
lines++;
|
|
18
|
-
try {
|
|
19
|
-
const o = JSON.parse(line) as { kind?: string; type?: string };
|
|
20
|
-
const k = o.kind || o.type || "(none)";
|
|
21
|
-
kinds.set(k, (kinds.get(k) || 0) + 1);
|
|
22
|
-
} catch {
|
|
23
|
-
bad++;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
console.log(JSON.stringify({ path, lines, bad }, null, 2));
|
|
28
|
-
console.log(
|
|
29
|
-
[...kinds.entries()]
|
|
30
|
-
.sort((a, b) => b[1] - a[1])
|
|
31
|
-
.map(([k, n]) => `${n}\t${k}`)
|
|
32
|
-
.join("\n"),
|
|
33
|
-
);
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
Set-Location "H:\Lucru\Domains\grok-telegram-bot"
|
|
2
|
-
$log = "H:\Lucru\Domains\grok-telegram-bot\logs\restart-verify.log"
|
|
3
|
-
function W($m) { $line = "$(Get-Date -Format o) $m"; Add-Content -Path $log -Value $line; Write-Output $line }
|
|
4
|
-
|
|
5
|
-
W "waiting 20s for idle/flush..."
|
|
6
|
-
Start-Sleep -Seconds 20
|
|
7
|
-
|
|
8
|
-
$old = Get-CimInstance Win32_Process -Filter "name='node.exe'" | Where-Object { $_.CommandLine -match 'grok-telegram-bot\\src\\index' } | Select-Object -ExpandProperty ProcessId
|
|
9
|
-
W "old pid(s): $($old -join ',')"
|
|
10
|
-
|
|
11
|
-
W "running: npx tsx src/cli.ts restart"
|
|
12
|
-
$out = npx tsx src/cli.ts restart 2>&1 | Out-String
|
|
13
|
-
W $out
|
|
14
|
-
|
|
15
|
-
Start-Sleep -Seconds 8
|
|
16
|
-
$new = Get-CimInstance Win32_Process -Filter "name='node.exe'" | Where-Object { $_.CommandLine -match 'grok-telegram-bot\\src\\index' } | Select-Object ProcessId,CreationDate,CommandLine
|
|
17
|
-
W "new process:"
|
|
18
|
-
W ($new | Format-List | Out-String)
|
|
19
|
-
|
|
20
|
-
# Confirm loaded code path mentions outcome approved by checking process start after our file mtime
|
|
21
|
-
$fixMtime = (Get-Item "H:\Lucru\Domains\grok-telegram-bot\src\grok\plan-approval.ts").LastWriteTime
|
|
22
|
-
W "plan-approval.ts mtime: $fixMtime"
|
|
23
|
-
if ($new) {
|
|
24
|
-
W "status:"
|
|
25
|
-
W (npx tsx src/cli.ts status 2>&1 | Out-String)
|
|
26
|
-
W "RESTART_OK"
|
|
27
|
-
} else {
|
|
28
|
-
W "RESTART_FAILED no process"
|
|
29
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
"""Deep probe of grok.exe for ExitPlanMode reverse-request/response shapes."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import re
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
data = Path(r"C:\Users\artic\.grok\bin\grok.exe").read_bytes()
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def nearby(label: bytes, n: int = 3, before: int = 120, after: int = 300) -> None:
|
|
11
|
-
start = 0
|
|
12
|
-
for i in range(n):
|
|
13
|
-
j = data.find(label, start)
|
|
14
|
-
if j < 0:
|
|
15
|
-
print(f"NOT FOUND: {label!r}")
|
|
16
|
-
return
|
|
17
|
-
chunk = data[max(0, j - before) : j + after]
|
|
18
|
-
text = "".join(chr(b) if 32 <= b < 127 else "|" for b in chunk)
|
|
19
|
-
print(f"\n=== {label!r} @{j} ===\n{text}")
|
|
20
|
-
start = j + len(label)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# Key messages around reverse-request
|
|
24
|
-
for lab in [
|
|
25
|
-
b"sending ext_method to client",
|
|
26
|
-
b"client disconnected mid-approval",
|
|
27
|
-
b"no client wired",
|
|
28
|
-
b"ExitPlanModeExtRequest",
|
|
29
|
-
b"ExitPlanModeExtResponse",
|
|
30
|
-
b"Failed to parse ExitPlanMode",
|
|
31
|
-
b"x.ai/exit_plan_mode",
|
|
32
|
-
b"agent.ext_method",
|
|
33
|
-
b"ext_method",
|
|
34
|
-
b"session/request_permission",
|
|
35
|
-
]:
|
|
36
|
-
nearby(lab, n=2)
|
|
37
|
-
|
|
38
|
-
print("\n\n==== null-terminated strings containing ExitPlan or approved near ExtResponse ====")
|
|
39
|
-
idx = data.find(b"ExitPlanModeExtResponse serialization")
|
|
40
|
-
region = data[max(0, idx - 4000) : idx + 2000]
|
|
41
|
-
cur = bytearray()
|
|
42
|
-
strs: list[str] = []
|
|
43
|
-
for b in region:
|
|
44
|
-
if 32 <= b < 127:
|
|
45
|
-
cur.append(b)
|
|
46
|
-
else:
|
|
47
|
-
if 3 <= len(cur) <= 60:
|
|
48
|
-
s = cur.decode()
|
|
49
|
-
if any(
|
|
50
|
-
k in s.lower()
|
|
51
|
-
for k in (
|
|
52
|
-
"approv",
|
|
53
|
-
"abandon",
|
|
54
|
-
"feedback",
|
|
55
|
-
"decision",
|
|
56
|
-
"action",
|
|
57
|
-
"outcome",
|
|
58
|
-
"plan",
|
|
59
|
-
"comment",
|
|
60
|
-
"exit",
|
|
61
|
-
"request",
|
|
62
|
-
"session",
|
|
63
|
-
"content",
|
|
64
|
-
)
|
|
65
|
-
):
|
|
66
|
-
strs.append(s)
|
|
67
|
-
cur.clear()
|
|
68
|
-
for s in dict.fromkeys(strs):
|
|
69
|
-
print(repr(s))
|
|
70
|
-
|
|
71
|
-
print("\n\n==== method name candidates ====")
|
|
72
|
-
for m in sorted(set(re.findall(rb"x\.ai/[a-zA-Z0-9_/\-]{3,50}", data))):
|
|
73
|
-
s = m.decode("ascii", "ignore")
|
|
74
|
-
if any(k in s.lower() for k in ("plan", "exit", "ask", "approv", "question")):
|
|
75
|
-
# only print clean-looking
|
|
76
|
-
if re.fullmatch(r"x\.ai/[a-z0-9_/\-]+", s):
|
|
77
|
-
print(s)
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
"""Probe grok.exe for ExitPlanMode ACP extension request/response shape."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import re
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
path = Path(r"C:\Users\artic\.grok\bin\grok.exe")
|
|
8
|
-
data = path.read_bytes()
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def ctx(idx: int, before: int = 80, after: int = 200) -> str:
|
|
12
|
-
chunk = data[max(0, idx - before) : idx + after]
|
|
13
|
-
return "".join(chr(b) if 32 <= b < 127 else "|" for b in chunk)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
needles = [
|
|
17
|
-
b"ExitPlanModeExtRequest",
|
|
18
|
-
b"ExitPlanModeExtResponse",
|
|
19
|
-
b"Failed to parse ExitPlanModeExtRequest",
|
|
20
|
-
b"x.ai/exit_plan_mode",
|
|
21
|
-
b"[exit_plan_mode] user approved",
|
|
22
|
-
b"[exit_plan_mode] user abandoned",
|
|
23
|
-
b"request_changes",
|
|
24
|
-
b"requestChanges",
|
|
25
|
-
b"additionalFeedback",
|
|
26
|
-
b"additional_feedback",
|
|
27
|
-
b"planContent",
|
|
28
|
-
b"plan_content",
|
|
29
|
-
b"emptyPlan",
|
|
30
|
-
b"EmptyPlan",
|
|
31
|
-
b"PlanReady",
|
|
32
|
-
]
|
|
33
|
-
|
|
34
|
-
for n in needles:
|
|
35
|
-
i = 0
|
|
36
|
-
c = 0
|
|
37
|
-
while c < 3:
|
|
38
|
-
j = data.find(n, i)
|
|
39
|
-
if j < 0:
|
|
40
|
-
break
|
|
41
|
-
print(f"\n=== {n!r} @ {j} ===")
|
|
42
|
-
print(ctx(j, 40, 160))
|
|
43
|
-
i = j + len(n)
|
|
44
|
-
c += 1
|
|
45
|
-
|
|
46
|
-
print("\n=== combos approved/abandoned ===")
|
|
47
|
-
for m in re.finditer(rb"approved.{0,30}abandoned|abandoned.{0,30}approved", data, re.I):
|
|
48
|
-
print(ctx(m.start(), 10, 80))
|
|
49
|
-
|
|
50
|
-
print("\n=== identifiers near first ExtResponse ===")
|
|
51
|
-
idx = data.find(b"ExitPlanModeExtResponse")
|
|
52
|
-
if idx >= 0:
|
|
53
|
-
region = data[idx : idx + 1200]
|
|
54
|
-
print(re.findall(rb"[A-Za-z_][A-Za-z0-9_]{2,40}", region)[:60])
|
|
55
|
-
|
|
56
|
-
print("\n=== identifiers near struct ExitPlanModeExtRequest with 3 ===")
|
|
57
|
-
idx = data.find(b"struct ExitPlanModeExtRequest with 3 elements")
|
|
58
|
-
if idx >= 0:
|
|
59
|
-
region = data[idx - 400 : idx + 400]
|
|
60
|
-
print([s.decode() for s in re.findall(rb"[A-Za-z_][A-Za-z0-9_]{2,40}", region)])
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
import re
|
|
3
|
-
|
|
4
|
-
data = Path(r"C:\Users\artic\.grok\bin\grok.exe").read_bytes()
|
|
5
|
-
|
|
6
|
-
# Pull null-terminated strings of length 3-40 around key anchors
|
|
7
|
-
anchors = [
|
|
8
|
-
data.find(b"ExitPlanModeExtRequest serialization"),
|
|
9
|
-
data.find(b"ExitPlanModeExtResponse serialization"),
|
|
10
|
-
data.find(b"struct ExitPlanModeExtRequest with 3"),
|
|
11
|
-
data.find(b"struct ExitPlanModeExtResponse with 2"),
|
|
12
|
-
data.find(b"approvedabandonedAdditional feedback"),
|
|
13
|
-
data.find(b"[exit_plan_mode] intercepted"),
|
|
14
|
-
data.find(b"Plan approval requested"),
|
|
15
|
-
]
|
|
16
|
-
|
|
17
|
-
for a in anchors:
|
|
18
|
-
if a < 0:
|
|
19
|
-
continue
|
|
20
|
-
region = data[max(0, a - 800) : a + 800]
|
|
21
|
-
strs = []
|
|
22
|
-
cur = bytearray()
|
|
23
|
-
for b in region:
|
|
24
|
-
if 32 <= b < 127:
|
|
25
|
-
cur.append(b)
|
|
26
|
-
else:
|
|
27
|
-
if 3 <= len(cur) <= 48:
|
|
28
|
-
strs.append(cur.decode("ascii"))
|
|
29
|
-
cur.clear()
|
|
30
|
-
if 3 <= len(cur) <= 48:
|
|
31
|
-
strs.append(cur.decode("ascii"))
|
|
32
|
-
# unique preserve order
|
|
33
|
-
seen = set()
|
|
34
|
-
out = []
|
|
35
|
-
for s in strs:
|
|
36
|
-
if s not in seen:
|
|
37
|
-
seen.add(s)
|
|
38
|
-
out.append(s)
|
|
39
|
-
print("\n==== ANCHOR", a, "====")
|
|
40
|
-
for s in out:
|
|
41
|
-
print(s)
|
|
42
|
-
|
|
43
|
-
# Also list method names that look like reverse requests
|
|
44
|
-
print("\n==== x.ai methods clean ====")
|
|
45
|
-
for m in sorted(set(re.findall(rb"x\.ai/[a-z0-9_/\-]{3,60}", data))):
|
|
46
|
-
s = m.decode()
|
|
47
|
-
if any(k in s for k in ["plan", "ask", "question", "mode", "approv"]):
|
|
48
|
-
print(s)
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
import re
|
|
3
|
-
|
|
4
|
-
data = Path(r"C:\Users\artic\.grok\bin\grok.exe").read_bytes()
|
|
5
|
-
|
|
6
|
-
for n in [
|
|
7
|
-
b"additional_feedback",
|
|
8
|
-
b"additionalFeedback",
|
|
9
|
-
b"AdditionalFeedback",
|
|
10
|
-
b"review_comments",
|
|
11
|
-
b"reviewComments",
|
|
12
|
-
]:
|
|
13
|
-
print(n, "count", data.count(n), "at", data.find(n))
|
|
14
|
-
|
|
15
|
-
# Pull short identifiers within 2KB of ExitPlanModeExtResponse serialization
|
|
16
|
-
idx = data.find(b"ExitPlanModeExtResponse serialization")
|
|
17
|
-
region = data[idx - 2000 : idx + 500]
|
|
18
|
-
idents = re.findall(rb"[A-Za-z_][A-Za-z0-9_]{2,40}", region)
|
|
19
|
-
# filter boring
|
|
20
|
-
skip = {"struct", "with", "elements", "serialization", "should", "not", "fail", "crates", "codegen"}
|
|
21
|
-
seen = []
|
|
22
|
-
for s in idents:
|
|
23
|
-
t = s.decode()
|
|
24
|
-
if t in skip or t in seen:
|
|
25
|
-
continue
|
|
26
|
-
seen.append(t)
|
|
27
|
-
print("idents near serialization:", seen[:80])
|
|
28
|
-
|
|
29
|
-
# Look for schemars / json schema around ExitPlanModeExt
|
|
30
|
-
for pat in [rb'"decision"', rb'"action"', rb'"outcome"', rb'"approved"', rb'"abandoned"']:
|
|
31
|
-
c = 0
|
|
32
|
-
start = 0
|
|
33
|
-
while c < 5:
|
|
34
|
-
j = data.find(pat, start)
|
|
35
|
-
if j < 0:
|
|
36
|
-
break
|
|
37
|
-
window = data[j : j + 80]
|
|
38
|
-
if b"plan" in window.lower() or b"approv" in window or b"abandon" in window:
|
|
39
|
-
print(pat, j, window[:80])
|
|
40
|
-
c += 1
|
|
41
|
-
start = j + 1
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
import re
|
|
3
|
-
|
|
4
|
-
data = Path(r"C:\Users\artic\.grok\bin\grok.exe").read_bytes()
|
|
5
|
-
|
|
6
|
-
# Find null-separated sequences containing approved and abandoned as siblings
|
|
7
|
-
needle = b"approved\x00abandoned"
|
|
8
|
-
idx = data.find(needle)
|
|
9
|
-
print("approved\\0abandoned", idx)
|
|
10
|
-
if idx >= 0:
|
|
11
|
-
print(data[idx - 40 : idx + 80])
|
|
12
|
-
|
|
13
|
-
needle2 = b"abandoned\x00approved"
|
|
14
|
-
print("abandoned\\0approved", data.find(needle2))
|
|
15
|
-
|
|
16
|
-
# Also try without null (concatenated variants for Display)
|
|
17
|
-
print("concat", data.find(b"approvedabandoned"))
|
|
18
|
-
|
|
19
|
-
# Search for field name candidates near concat
|
|
20
|
-
idx = data.find(b"approvedabandoned")
|
|
21
|
-
region = data[idx - 100 : idx + 120]
|
|
22
|
-
# extract C strings
|
|
23
|
-
cur = bytearray()
|
|
24
|
-
strs = []
|
|
25
|
-
for b in region:
|
|
26
|
-
if 32 <= b < 127:
|
|
27
|
-
cur.append(b)
|
|
28
|
-
else:
|
|
29
|
-
if cur:
|
|
30
|
-
strs.append(cur.decode())
|
|
31
|
-
cur.clear()
|
|
32
|
-
print("near concat:", strs)
|
|
33
|
-
|
|
34
|
-
# Look at ExitPlanModeExtRequest 3 elements - field names for request
|
|
35
|
-
for label in [b"plan_content", b"plan_file_path", b"session_id", b"sessionId", b"tool_call_id", b"toolCallId", b"empty"]:
|
|
36
|
-
print(label, data.count(label))
|
|
37
|
-
|
|
38
|
-
# Find bytes: decision\0 or action\0 near approved
|
|
39
|
-
for field in [b"decision\x00", b"action\x00", b"outcome\x00", b"result\x00", b"choice\x00", b"feedback\x00", b"comments\x00", b"message\x00"]:
|
|
40
|
-
start = 0
|
|
41
|
-
hits = 0
|
|
42
|
-
while hits < 3:
|
|
43
|
-
j = data.find(field, start)
|
|
44
|
-
if j < 0:
|
|
45
|
-
break
|
|
46
|
-
win = data[j : j + 60]
|
|
47
|
-
if b"approv" in win or b"abandon" in win or b"plan" in win.lower():
|
|
48
|
-
print("FIELD", field, j, win)
|
|
49
|
-
hits += 1
|
|
50
|
-
start = j + 1
|
|
51
|
-
|
|
52
|
-
# Extract all strings of form [a-z_]+ that appear within 200 bytes after "ExitPlanModeExtResponse with 2"
|
|
53
|
-
idx = data.find(b"struct ExitPlanModeExtResponse with 2 elements")
|
|
54
|
-
print("struct at", idx)
|
|
55
|
-
if idx >= 0:
|
|
56
|
-
region = data[idx : idx + 300]
|
|
57
|
-
print(region)
|
|
58
|
-
print(re.findall(rb"[a-z_]{3,30}", region))
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
data = Path(r"C:\Users\artic\.grok\bin\grok.exe").read_bytes()
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def show(label: bytes, n: int = 3, before: int = 100, after: int = 250) -> None:
|
|
7
|
-
start = 0
|
|
8
|
-
for _ in range(n):
|
|
9
|
-
j = data.find(label, start)
|
|
10
|
-
if j < 0:
|
|
11
|
-
print(label, "not found")
|
|
12
|
-
return
|
|
13
|
-
chunk = data[max(0, j - before) : j + after]
|
|
14
|
-
text = "".join(chr(b) if 32 <= b < 127 else "|" for b in chunk)
|
|
15
|
-
print(f"\n=== {label!r} @ {j} ===")
|
|
16
|
-
print(text)
|
|
17
|
-
start = j + len(label)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
for lab in [
|
|
21
|
-
b"[exit_plan_mode] user approved",
|
|
22
|
-
b"[exit_plan_mode] user abandoned",
|
|
23
|
-
b"Opened plan approval view from ext_method",
|
|
24
|
-
b"Failed to parse ExitPlanModeExtResponse",
|
|
25
|
-
b"ExitPlanModeExtResponse serialization should not fail",
|
|
26
|
-
b"resume exit_plan_mode reverse-request failed",
|
|
27
|
-
b"Plan approval requested",
|
|
28
|
-
]:
|
|
29
|
-
show(lab, n=2)
|
|
30
|
-
|
|
31
|
-
# Print unique printable strings of length 4-24 in a 1KB window after
|
|
32
|
-
# "Opened plan approval view from ext_method"
|
|
33
|
-
idx = data.find(b"Opened plan approval view from ext_method")
|
|
34
|
-
region = data[idx : idx + 1500]
|
|
35
|
-
cur = bytearray()
|
|
36
|
-
seen = []
|
|
37
|
-
for b in region:
|
|
38
|
-
if 32 <= b < 127:
|
|
39
|
-
cur.append(b)
|
|
40
|
-
else:
|
|
41
|
-
if 4 <= len(cur) <= 32:
|
|
42
|
-
s = cur.decode()
|
|
43
|
-
if s not in seen:
|
|
44
|
-
seen.append(s)
|
|
45
|
-
cur.clear()
|
|
46
|
-
print("\nstrings after open plan approval:")
|
|
47
|
-
for s in seen:
|
|
48
|
-
print(" ", s)
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
|
|
3
|
-
const path =
|
|
4
|
-
"H:\\Lucru\\Domains\\claude-telegram-bot\\data\\sessions\\019f47bc-67bc-7043-a40a-f487a1d5af8b.jsonl";
|
|
5
|
-
const lines = readFileSync(path, "utf8").split(/\n/).filter(Boolean);
|
|
6
|
-
|
|
7
|
-
for (const line of lines) {
|
|
8
|
-
const o = JSON.parse(line) as { kind?: string; data?: Record<string, unknown> };
|
|
9
|
-
if (o.kind === "ToolUse") {
|
|
10
|
-
console.log("keys", Object.keys(o.data || {}));
|
|
11
|
-
console.log(JSON.stringify(o).slice(0, 1500));
|
|
12
|
-
break;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
for (const line of lines) {
|
|
16
|
-
const o = JSON.parse(line) as { kind?: string };
|
|
17
|
-
if (o.kind === "AssistantMessage") {
|
|
18
|
-
console.log("ASSIST", JSON.stringify(o).slice(0, 800));
|
|
19
|
-
break;
|
|
20
|
-
}
|
|
21
|
-
}
|