grok-telegram-bot 2.3.0 → 2.4.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 +26 -0
- package/CHANGELOG.md +55 -0
- package/package.json +1 -1
- package/scripts/analyze-jsonl.ts +33 -0
- package/scripts/delayed-restart.ps1 +29 -0
- package/scripts/probe-exit-response-shape.py +77 -0
- package/scripts/probe-plan-exit.py +60 -0
- package/scripts/probe-plan-exit2.py +48 -0
- package/scripts/probe-plan-fields.py +41 -0
- package/scripts/probe-plan-fields2.py +58 -0
- package/scripts/probe-plan-response-path.py +48 -0
- package/scripts/sample-claude-tooluse.ts +21 -0
- package/scripts/sample-kiro-events.ts +31 -0
- package/scripts/smoke-exit-plan.ts +274 -0
- package/scripts/smoke-exit-shapes.ts +252 -0
- package/scripts/smoke-import.mjs +82 -0
- package/scripts/smoke-import.ts +73 -0
- package/src/app/accounts.ts +84 -0
- package/src/app/instance-lock.ts +6 -0
- package/src/app/types.ts +19 -2
- package/src/app/updater.ts +17 -6
- package/src/app/usage.ts +204 -7
- package/src/bot/account-rotator.ts +71 -2
- package/src/bot/bot.ts +36 -0
- package/src/bot/chat-controller.ts +35 -0
- package/src/bot/commands.ts +2 -0
- package/src/bot/complexity-gate.ts +69 -0
- package/src/bot/deps.ts +19 -0
- package/src/bot/handlers/accounts.ts +55 -5
- package/src/bot/handlers/import-session.ts +290 -0
- package/src/bot/handlers/menu.ts +17 -38
- package/src/bot/handlers/message.ts +1 -0
- package/src/bot/handlers/running.ts +35 -5
- package/src/bot/handlers/session-card.ts +12 -0
- package/src/bot/handlers/sessions.ts +14 -3
- package/src/bot/handlers/usage.ts +118 -16
- package/src/bot/menu/keyboard.ts +5 -4
- package/src/bot/menu/status-panel.ts +19 -6
- package/src/bot/prompt-content.ts +4 -0
- package/src/bot/reauth-controller.ts +2 -2
- package/src/bot/session-fork.ts +11 -0
- package/src/bot/session-runtime.ts +831 -64
- package/src/bot/suggestions.ts +429 -0
- package/src/config.ts +41 -0
- package/src/grok/client.ts +106 -20
- package/src/grok/plan-approval.ts +72 -0
- package/src/grok/session-log.ts +16 -0
- package/src/grok/types.ts +21 -2
- package/src/import/build-import.ts +132 -0
- package/src/import/history-readers.ts +681 -0
- package/src/import/list-running.ts +100 -0
- package/src/import/sources.ts +78 -0
- package/src/index.ts +179 -24
- package/src/render/diff.ts +11 -2
- package/src/render/file-summary.ts +31 -1
- package/src/render/markdown.ts +293 -35
- package/src/render/plan.ts +127 -0
- package/src/render/session-comment.ts +261 -0
- package/src/render/tool-call-detail.ts +400 -19
- package/src/render/tool-call-merge.ts +115 -0
- package/src/render/tool-call.ts +405 -142
- package/src/render/truncate.ts +85 -0
- package/src/service/windows.ts +14 -2
- package/src/sessions/history.ts +57 -0
- package/src/sessions/store.ts +3 -0
- package/src/sessions/types.ts +5 -0
- package/src/stream/streamer.ts +73 -9
- package/src/tasks/runner.ts +4 -3
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke-test plan exit over ACP without touching the running Telegram bot.
|
|
3
|
+
*
|
|
4
|
+
* Spawns its OWN `grok agent --no-leader --always-approve stdio` process,
|
|
5
|
+
* creates a fresh session, enters plan mode, writes a tiny plan, exits plan
|
|
6
|
+
* mode, and logs every reverse-request + tool result.
|
|
7
|
+
*
|
|
8
|
+
* Usage: npx tsx scripts/smoke-exit-plan.ts
|
|
9
|
+
*/
|
|
10
|
+
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { autoApproveExitPlanMode, autoSkipAskUserQuestion, isAskUserQuestionMethod, isPlanExitMethod } from "../src/grok/plan-approval.js";
|
|
15
|
+
|
|
16
|
+
const GROK = process.env.GROK_CLI_PATH?.trim() || join(homedir(), ".grok", "bin", "grok.exe");
|
|
17
|
+
const CWD = process.env.SMOKE_CWD?.trim() || join(homedir(), "AppData", "Local", "Temp", "grok-plan-exit-smoke");
|
|
18
|
+
|
|
19
|
+
type JsonRpc = {
|
|
20
|
+
jsonrpc?: string;
|
|
21
|
+
id?: number | string;
|
|
22
|
+
method?: string;
|
|
23
|
+
params?: unknown;
|
|
24
|
+
result?: unknown;
|
|
25
|
+
error?: { code: number; message: string; data?: unknown };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function main(): void {
|
|
29
|
+
mkdirSync(CWD, { recursive: true });
|
|
30
|
+
console.log("grok:", GROK);
|
|
31
|
+
console.log("cwd:", CWD);
|
|
32
|
+
|
|
33
|
+
const args = ["agent", "--no-leader", "--always-approve", "stdio"];
|
|
34
|
+
const proc = spawn(GROK, args, {
|
|
35
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
36
|
+
cwd: CWD,
|
|
37
|
+
env: { ...process.env },
|
|
38
|
+
}) as ChildProcessWithoutNullStreams;
|
|
39
|
+
|
|
40
|
+
let buf = "";
|
|
41
|
+
let nextId = 1;
|
|
42
|
+
const pending = new Map<number | string, { resolve: (v: unknown) => void; reject: (e: Error) => void; method: string }>();
|
|
43
|
+
const reverseLog: Array<{ method: string; params: unknown; result: unknown }> = [];
|
|
44
|
+
const toolEvents: Array<{ title?: string; status?: string; text?: string }> = [];
|
|
45
|
+
let sessionId = "";
|
|
46
|
+
|
|
47
|
+
proc.stderr.setEncoding("utf8");
|
|
48
|
+
proc.stderr.on("data", (c: string) => {
|
|
49
|
+
for (const line of c.split(/\r?\n/)) {
|
|
50
|
+
if (line.trim()) console.log("[stderr]", line.slice(0, 300));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
proc.stdout.setEncoding("utf8");
|
|
55
|
+
proc.stdout.on("data", (chunk: string) => {
|
|
56
|
+
buf += chunk;
|
|
57
|
+
let idx: number;
|
|
58
|
+
while ((idx = buf.indexOf("\n")) !== -1) {
|
|
59
|
+
const line = buf.slice(0, idx).trim();
|
|
60
|
+
buf = buf.slice(idx + 1);
|
|
61
|
+
if (!line) continue;
|
|
62
|
+
let msg: JsonRpc;
|
|
63
|
+
try {
|
|
64
|
+
msg = JSON.parse(line) as JsonRpc;
|
|
65
|
+
} catch {
|
|
66
|
+
console.log("[non-json]", line.slice(0, 200));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
handleMessage(msg);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
function send(msg: object): void {
|
|
74
|
+
proc.stdin.write(JSON.stringify(msg) + "\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function request(method: string, params: unknown): Promise<unknown> {
|
|
78
|
+
const id = nextId++;
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const t = setTimeout(() => {
|
|
81
|
+
pending.delete(id);
|
|
82
|
+
reject(new Error(`timeout: ${method}`));
|
|
83
|
+
}, 120_000);
|
|
84
|
+
pending.set(id, {
|
|
85
|
+
resolve: (v) => {
|
|
86
|
+
clearTimeout(t);
|
|
87
|
+
resolve(v);
|
|
88
|
+
},
|
|
89
|
+
reject: (e) => {
|
|
90
|
+
clearTimeout(t);
|
|
91
|
+
reject(e);
|
|
92
|
+
},
|
|
93
|
+
method,
|
|
94
|
+
});
|
|
95
|
+
send({ jsonrpc: "2.0", id, method, params });
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function handleMessage(msg: JsonRpc): void {
|
|
100
|
+
// Response to our request
|
|
101
|
+
if (msg.id !== undefined && msg.id !== null && pending.has(msg.id) && !msg.method) {
|
|
102
|
+
const p = pending.get(msg.id)!;
|
|
103
|
+
pending.delete(msg.id);
|
|
104
|
+
if (msg.error) p.reject(new Error(`${p.method}: ${msg.error.message}`));
|
|
105
|
+
else p.resolve(msg.result);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Reverse request
|
|
109
|
+
if (msg.id !== undefined && msg.id !== null && msg.method) {
|
|
110
|
+
const method = msg.method;
|
|
111
|
+
const params = (msg.params as Record<string, unknown>) || {};
|
|
112
|
+
console.log("\n<<< REVERSE REQUEST", method);
|
|
113
|
+
console.log(" params keys:", Object.keys(params));
|
|
114
|
+
console.log(" params sample:", JSON.stringify(params).slice(0, 500));
|
|
115
|
+
|
|
116
|
+
let result: unknown = null;
|
|
117
|
+
if (isPlanExitMethod(method)) {
|
|
118
|
+
result = autoApproveExitPlanMode(params);
|
|
119
|
+
console.log(">>> auto-approve exit:", JSON.stringify(result));
|
|
120
|
+
} else if (isAskUserQuestionMethod(method)) {
|
|
121
|
+
result = autoSkipAskUserQuestion(params);
|
|
122
|
+
console.log(">>> skip question:", JSON.stringify(result));
|
|
123
|
+
} else if (method === "session/request_permission") {
|
|
124
|
+
const opts = (params.options as Array<{ optionId: string }>) || [];
|
|
125
|
+
const first = opts[0]?.optionId;
|
|
126
|
+
result = first
|
|
127
|
+
? { outcome: { outcome: "selected", optionId: first } }
|
|
128
|
+
: { outcome: { outcome: "cancelled" } };
|
|
129
|
+
console.log(">>> permission:", JSON.stringify(result));
|
|
130
|
+
} else {
|
|
131
|
+
console.log(">>> UNKNOWN reverse method — error method-not-found (same as old bot bug)");
|
|
132
|
+
send({
|
|
133
|
+
jsonrpc: "2.0",
|
|
134
|
+
id: msg.id,
|
|
135
|
+
error: { code: -32601, message: `unsupported client method: ${method}` },
|
|
136
|
+
});
|
|
137
|
+
reverseLog.push({ method, params, result: { error: -32601 } });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
reverseLog.push({ method, params, result });
|
|
141
|
+
send({ jsonrpc: "2.0", id: msg.id, result });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// Notification
|
|
145
|
+
if (msg.method === "session/update") {
|
|
146
|
+
const p = msg.params as { sessionId?: string; update?: Record<string, unknown> };
|
|
147
|
+
const u = p?.update || {};
|
|
148
|
+
const kind = String(u.sessionUpdate || "");
|
|
149
|
+
if (kind === "tool_call" || kind === "tool_call_update") {
|
|
150
|
+
const title = typeof u.title === "string" ? u.title : undefined;
|
|
151
|
+
const status = typeof u.status === "string" ? u.status : undefined;
|
|
152
|
+
let text = "";
|
|
153
|
+
const content = u.content as unknown;
|
|
154
|
+
if (Array.isArray(content)) {
|
|
155
|
+
for (const c of content) {
|
|
156
|
+
const t = (c as { content?: { text?: string } })?.content?.text;
|
|
157
|
+
if (t) text += t;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
toolEvents.push({ title, status, text: text.slice(0, 300) });
|
|
161
|
+
if (title?.toLowerCase().includes("plan") || status === "failed" || status === "completed") {
|
|
162
|
+
console.log(`[tool] ${kind} title=${title} status=${status} text=${text.slice(0, 200)}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function run(): Promise<void> {
|
|
169
|
+
try {
|
|
170
|
+
const init = (await request("initialize", {
|
|
171
|
+
protocolVersion: 1,
|
|
172
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
|
|
173
|
+
clientInfo: { name: "smoke-exit-plan", version: "0.0.1" },
|
|
174
|
+
})) as { authMethods?: Array<{ id: string }> };
|
|
175
|
+
console.log("initialized");
|
|
176
|
+
|
|
177
|
+
// Authenticate headless if needed
|
|
178
|
+
const methods = init.authMethods || [];
|
|
179
|
+
const authId =
|
|
180
|
+
methods.find((m) => /cached_token|api_key|xai/i.test(m.id))?.id ||
|
|
181
|
+
methods[0]?.id;
|
|
182
|
+
if (authId) {
|
|
183
|
+
try {
|
|
184
|
+
await request("authenticate", { methodId: authId, _meta: { headless: true } });
|
|
185
|
+
console.log("authenticated:", authId);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
console.warn("auth soft-fail:", (e as Error).message);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const ns = (await request("session/new", { cwd: CWD, mcpServers: [] })) as { sessionId: string };
|
|
192
|
+
sessionId = ns.sessionId;
|
|
193
|
+
console.log("session:", sessionId);
|
|
194
|
+
|
|
195
|
+
// Force a quick enter+exit plan cycle.
|
|
196
|
+
const prompt =
|
|
197
|
+
"You MUST use tools. " +
|
|
198
|
+
"1) Call enter_plan_mode. " +
|
|
199
|
+
"2) Write a one-line plan into the plan file with search_replace or write. " +
|
|
200
|
+
"3) Immediately call exit_plan_mode. " +
|
|
201
|
+
"4) After exit succeeds, reply with exactly: PLAN_EXIT_OK. " +
|
|
202
|
+
"Do not do anything else.";
|
|
203
|
+
|
|
204
|
+
console.log("prompting…");
|
|
205
|
+
const result = await request("session/prompt", {
|
|
206
|
+
sessionId,
|
|
207
|
+
prompt: [{ type: "text", text: prompt }],
|
|
208
|
+
});
|
|
209
|
+
console.log("prompt result:", JSON.stringify(result).slice(0, 300));
|
|
210
|
+
|
|
211
|
+
// Find plan_mode.json
|
|
212
|
+
const enc = encodeURIComponent(CWD);
|
|
213
|
+
const sessDir = join(homedir(), ".grok", "sessions", enc, sessionId);
|
|
214
|
+
const planModePath = join(sessDir, "plan_mode.json");
|
|
215
|
+
console.log("\n=== plan_mode.json ===", planModePath);
|
|
216
|
+
if (existsSync(planModePath)) {
|
|
217
|
+
console.log(readFileSync(planModePath, "utf8"));
|
|
218
|
+
} else {
|
|
219
|
+
console.log("(missing)");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log("\n=== reverse requests ===");
|
|
223
|
+
console.log(JSON.stringify(reverseLog, null, 2));
|
|
224
|
+
|
|
225
|
+
console.log("\n=== plan-related tool events ===");
|
|
226
|
+
for (const t of toolEvents.filter((e) => (e.title || "").toLowerCase().includes("plan") || e.status === "failed")) {
|
|
227
|
+
console.log(JSON.stringify(t));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const exitFailed = toolEvents.some(
|
|
231
|
+
(e) =>
|
|
232
|
+
(e.title || "").toLowerCase().includes("exit") &&
|
|
233
|
+
e.status === "failed",
|
|
234
|
+
);
|
|
235
|
+
const exitOk = toolEvents.some(
|
|
236
|
+
(e) =>
|
|
237
|
+
(e.title || "").toLowerCase().includes("exit") &&
|
|
238
|
+
e.status === "completed",
|
|
239
|
+
);
|
|
240
|
+
const disconnected = toolEvents.some((e) => (e.text || "").includes("client disconnected"));
|
|
241
|
+
|
|
242
|
+
console.log("\n=== VERDICT ===");
|
|
243
|
+
console.log({ exitOk, exitFailed, disconnected, reverseCount: reverseLog.length });
|
|
244
|
+
|
|
245
|
+
writeFileSync(
|
|
246
|
+
join(CWD, "smoke-exit-plan-report.json"),
|
|
247
|
+
JSON.stringify({ reverseLog, toolEvents, exitOk, exitFailed, disconnected, sessionId }, null, 2),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
if (!exitOk || exitFailed || disconnected) {
|
|
251
|
+
process.exitCode = 2;
|
|
252
|
+
}
|
|
253
|
+
} catch (e) {
|
|
254
|
+
console.error("SMOKE FAILED:", e);
|
|
255
|
+
process.exitCode = 1;
|
|
256
|
+
} finally {
|
|
257
|
+
try {
|
|
258
|
+
proc.kill();
|
|
259
|
+
} catch {
|
|
260
|
+
/* ignore */
|
|
261
|
+
}
|
|
262
|
+
setTimeout(() => process.exit(process.exitCode ?? 0), 500);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
proc.on("error", (e) => {
|
|
267
|
+
console.error("spawn error", e);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
void run();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
main();
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Try multiple ExitPlanMode reverse-response shapes until plan mode goes Inactive
|
|
3
|
+
* or tool output says approved / implement.
|
|
4
|
+
*
|
|
5
|
+
* Does NOT touch the running Telegram bot.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
const GROK = process.env.GROK_CLI_PATH?.trim() || join(homedir(), ".grok", "bin", "grok.exe");
|
|
13
|
+
const BASE = join(homedir(), "AppData", "Local", "Temp", "grok-plan-exit-shapes");
|
|
14
|
+
|
|
15
|
+
type JsonRpc = {
|
|
16
|
+
id?: number | string;
|
|
17
|
+
method?: string;
|
|
18
|
+
params?: unknown;
|
|
19
|
+
result?: unknown;
|
|
20
|
+
error?: { code: number; message: string };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const SHAPES: Array<{ name: string; body: unknown }> = [
|
|
24
|
+
{ name: "decision_approved", body: { decision: "approved", additional_feedback: "" } },
|
|
25
|
+
{ name: "action_approved", body: { action: "approved", feedback: "" } },
|
|
26
|
+
{ name: "outcome_approved", body: { outcome: "approved", feedback: "" } },
|
|
27
|
+
{ name: "result_approved", body: { result: "approved" } },
|
|
28
|
+
{ name: "approved_true", body: { approved: true, feedback: "" } },
|
|
29
|
+
{ name: "ext_approved_null", body: { approved: null } },
|
|
30
|
+
{ name: "ext_Approved_null", body: { Approved: null } },
|
|
31
|
+
{ name: "ext_approved_obj", body: { approved: { feedback: "" } } },
|
|
32
|
+
{ name: "ext_Approved_obj", body: { Approved: { additional_feedback: "" } } },
|
|
33
|
+
{ name: "choice_approve", body: { choice: "approve" } },
|
|
34
|
+
{ name: "choice_a", body: { choice: "a" } },
|
|
35
|
+
{ name: "option_approve", body: { option: "approve", optionId: "approve" } },
|
|
36
|
+
{ name: "status_approved", body: { status: "approved" } },
|
|
37
|
+
{ name: "type_approved", body: { type: "approved", feedback: "" } },
|
|
38
|
+
{ name: "kind_approved", body: { kind: "approved" } },
|
|
39
|
+
{ name: "verdict_approved", body: { verdict: "approved", comments: "" } },
|
|
40
|
+
{ name: "perm_style", body: { outcome: { outcome: "selected", optionId: "approve" } } },
|
|
41
|
+
{ name: "string_approved", body: "approved" },
|
|
42
|
+
{ name: "array_approved", body: ["approved", ""] },
|
|
43
|
+
{ name: "empty", body: {} },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
async function tryShape(shape: { name: string; body: unknown }): Promise<{
|
|
47
|
+
name: string;
|
|
48
|
+
planState?: string;
|
|
49
|
+
awaiting?: boolean;
|
|
50
|
+
toolText: string[];
|
|
51
|
+
reverseOk: boolean;
|
|
52
|
+
}> {
|
|
53
|
+
const CWD = join(BASE, shape.name);
|
|
54
|
+
mkdirSync(CWD, { recursive: true });
|
|
55
|
+
|
|
56
|
+
const proc = spawn(GROK, ["agent", "--no-leader", "--always-approve", "stdio"], {
|
|
57
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
58
|
+
cwd: CWD,
|
|
59
|
+
env: { ...process.env },
|
|
60
|
+
}) as ChildProcessWithoutNullStreams;
|
|
61
|
+
|
|
62
|
+
let buf = "";
|
|
63
|
+
let nextId = 1;
|
|
64
|
+
const pending = new Map<number | string, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
|
|
65
|
+
const toolText: string[] = [];
|
|
66
|
+
let reverseOk = false;
|
|
67
|
+
let sessionId = "";
|
|
68
|
+
|
|
69
|
+
const send = (msg: object) => proc.stdin.write(JSON.stringify(msg) + "\n");
|
|
70
|
+
|
|
71
|
+
const request = (method: string, params: unknown) =>
|
|
72
|
+
new Promise<unknown>((resolve, reject) => {
|
|
73
|
+
const id = nextId++;
|
|
74
|
+
const t = setTimeout(() => {
|
|
75
|
+
pending.delete(id);
|
|
76
|
+
reject(new Error(`timeout ${method}`));
|
|
77
|
+
}, 90_000);
|
|
78
|
+
pending.set(id, {
|
|
79
|
+
resolve: (v) => {
|
|
80
|
+
clearTimeout(t);
|
|
81
|
+
resolve(v);
|
|
82
|
+
},
|
|
83
|
+
reject: (e) => {
|
|
84
|
+
clearTimeout(t);
|
|
85
|
+
reject(e);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
send({ jsonrpc: "2.0", id, method, params });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
proc.stdout.setEncoding("utf8");
|
|
92
|
+
proc.stdout.on("data", (chunk: string) => {
|
|
93
|
+
buf += chunk;
|
|
94
|
+
let idx: number;
|
|
95
|
+
while ((idx = buf.indexOf("\n")) !== -1) {
|
|
96
|
+
const line = buf.slice(0, idx).trim();
|
|
97
|
+
buf = buf.slice(idx + 1);
|
|
98
|
+
if (!line) continue;
|
|
99
|
+
let msg: JsonRpc;
|
|
100
|
+
try {
|
|
101
|
+
msg = JSON.parse(line) as JsonRpc;
|
|
102
|
+
} catch {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (msg.id !== undefined && msg.id !== null && pending.has(msg.id) && !msg.method) {
|
|
106
|
+
const p = pending.get(msg.id)!;
|
|
107
|
+
pending.delete(msg.id);
|
|
108
|
+
if (msg.error) p.reject(new Error(msg.error.message));
|
|
109
|
+
else p.resolve(msg.result);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (msg.id !== undefined && msg.id !== null && msg.method) {
|
|
113
|
+
const method = msg.method;
|
|
114
|
+
if (method === "_x.ai/exit_plan_mode" || method === "x.ai/exit_plan_mode") {
|
|
115
|
+
reverseOk = true;
|
|
116
|
+
send({ jsonrpc: "2.0", id: msg.id, result: shape.body });
|
|
117
|
+
} else if (method === "session/request_permission") {
|
|
118
|
+
const opts = ((msg.params as { options?: Array<{ optionId: string }> })?.options) || [];
|
|
119
|
+
const first = opts[0]?.optionId;
|
|
120
|
+
send({
|
|
121
|
+
jsonrpc: "2.0",
|
|
122
|
+
id: msg.id,
|
|
123
|
+
result: first
|
|
124
|
+
? { outcome: { outcome: "selected", optionId: first } }
|
|
125
|
+
: { outcome: { outcome: "cancelled" } },
|
|
126
|
+
});
|
|
127
|
+
} else if (method.includes("ask_user_question")) {
|
|
128
|
+
send({ jsonrpc: "2.0", id: msg.id, result: { SkipInterview: null } });
|
|
129
|
+
} else {
|
|
130
|
+
send({
|
|
131
|
+
jsonrpc: "2.0",
|
|
132
|
+
id: msg.id,
|
|
133
|
+
error: { code: -32601, message: `unsupported: ${method}` },
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (msg.method === "session/update") {
|
|
139
|
+
const u = (msg.params as { update?: Record<string, unknown> })?.update || {};
|
|
140
|
+
const kind = String(u.sessionUpdate || "");
|
|
141
|
+
if (kind === "tool_call_update" || kind === "tool_call") {
|
|
142
|
+
const content = u.content as unknown;
|
|
143
|
+
if (Array.isArray(content)) {
|
|
144
|
+
for (const c of content) {
|
|
145
|
+
const t = (c as { content?: { text?: string } })?.content?.text;
|
|
146
|
+
if (t) toolText.push(t.slice(0, 200));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const init = (await request("initialize", {
|
|
156
|
+
protocolVersion: 1,
|
|
157
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
|
|
158
|
+
clientInfo: { name: "smoke-shapes", version: "0" },
|
|
159
|
+
})) as { authMethods?: Array<{ id: string }> };
|
|
160
|
+
const authId =
|
|
161
|
+
init.authMethods?.find((m) => /cached_token|api_key|xai/i.test(m.id))?.id ||
|
|
162
|
+
init.authMethods?.[0]?.id;
|
|
163
|
+
if (authId) {
|
|
164
|
+
try {
|
|
165
|
+
await request("authenticate", { methodId: authId, _meta: { headless: true } });
|
|
166
|
+
} catch {
|
|
167
|
+
/* soft */
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const ns = (await request("session/new", { cwd: CWD, mcpServers: [] })) as { sessionId: string };
|
|
171
|
+
sessionId = ns.sessionId;
|
|
172
|
+
|
|
173
|
+
await request("session/prompt", {
|
|
174
|
+
sessionId,
|
|
175
|
+
prompt: [
|
|
176
|
+
{
|
|
177
|
+
type: "text",
|
|
178
|
+
text:
|
|
179
|
+
"Use tools only: 1) enter_plan_mode 2) write one line to the plan file 3) exit_plan_mode. " +
|
|
180
|
+
"After exit, reply PLAN_EXIT_OK. Nothing else.",
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const sessDir = join(homedir(), ".grok", "sessions", encodeURIComponent(CWD), sessionId);
|
|
186
|
+
const planModePath = join(sessDir, "plan_mode.json");
|
|
187
|
+
let planState: string | undefined;
|
|
188
|
+
let awaiting: boolean | undefined;
|
|
189
|
+
if (existsSync(planModePath)) {
|
|
190
|
+
const pm = JSON.parse(readFileSync(planModePath, "utf8")) as {
|
|
191
|
+
state?: string;
|
|
192
|
+
awaiting_plan_approval?: boolean;
|
|
193
|
+
};
|
|
194
|
+
planState = pm.state;
|
|
195
|
+
awaiting = pm.awaiting_plan_approval;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { name: shape.name, planState, awaiting, toolText, reverseOk };
|
|
199
|
+
} finally {
|
|
200
|
+
try {
|
|
201
|
+
proc.kill();
|
|
202
|
+
} catch {
|
|
203
|
+
/* ignore */
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function main(): Promise<void> {
|
|
209
|
+
mkdirSync(BASE, { recursive: true });
|
|
210
|
+
const results = [];
|
|
211
|
+
for (const shape of SHAPES) {
|
|
212
|
+
process.stdout.write(`\n==== trying ${shape.name} ====\n`);
|
|
213
|
+
try {
|
|
214
|
+
const r = await tryShape(shape);
|
|
215
|
+
const joined = r.toolText.join(" | ");
|
|
216
|
+
const ok =
|
|
217
|
+
r.planState === "Inactive" ||
|
|
218
|
+
/approved the plan|Implement the plan|PLAN_EXIT_OK|plan mode has been disabled/i.test(joined);
|
|
219
|
+
const revise = /revise the plan|request(ed)? changes/i.test(joined);
|
|
220
|
+
const abandon = /abandon/i.test(joined);
|
|
221
|
+
const disconnected = /client disconnected/i.test(joined);
|
|
222
|
+
console.log({
|
|
223
|
+
name: r.name,
|
|
224
|
+
planState: r.planState,
|
|
225
|
+
awaiting: r.awaiting,
|
|
226
|
+
reverseOk: r.reverseOk,
|
|
227
|
+
ok,
|
|
228
|
+
revise,
|
|
229
|
+
abandon,
|
|
230
|
+
disconnected,
|
|
231
|
+
texts: r.toolText.slice(-3),
|
|
232
|
+
});
|
|
233
|
+
results.push({ ...r, ok, revise, abandon, disconnected });
|
|
234
|
+
if (ok) {
|
|
235
|
+
console.log("\n*** SUCCESS SHAPE ***", shape.name, JSON.stringify(shape.body));
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
} catch (e) {
|
|
239
|
+
console.log("ERROR", shape.name, (e as Error).message);
|
|
240
|
+
results.push({ name: shape.name, error: (e as Error).message });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
writeFileSync(join(BASE, "results.json"), JSON.stringify(results, null, 2));
|
|
244
|
+
console.log("\nWrote", join(BASE, "results.json"));
|
|
245
|
+
const win = results.find((r) => (r as { ok?: boolean }).ok);
|
|
246
|
+
process.exit(win ? 0 : 2);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
main().catch((e) => {
|
|
250
|
+
console.error(e);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, readFileSync, readdirSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir, homedir } from "node:os";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
// Load compiled-less TS via tsx dynamic import of source
|
|
7
|
+
const root = new URL("..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1");
|
|
8
|
+
// On Windows pathToFileURL for dynamic import
|
|
9
|
+
async function load(rel) {
|
|
10
|
+
const p = join(process.cwd(), rel);
|
|
11
|
+
return import(pathToFileURL(p).href);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { IMPORT_SOURCES, getImportSource } = await load("src/import/sources.ts");
|
|
15
|
+
const { listRunningFromSource } = await load("src/import/list-running.ts");
|
|
16
|
+
const { readForeignHistory } = await load("src/import/history-readers.ts");
|
|
17
|
+
const { buildImportPackage } = await load("src/import/build-import.ts");
|
|
18
|
+
|
|
19
|
+
for (const src of IMPORT_SOURCES) {
|
|
20
|
+
const running = listRunningFromSource(src);
|
|
21
|
+
console.log("===", src.id, "running:", running.length);
|
|
22
|
+
for (const s of running.slice(0, 3)) {
|
|
23
|
+
const hist = readForeignHistory(src.format, src.sessionsRoot, s.sessionId);
|
|
24
|
+
console.log(
|
|
25
|
+
" -",
|
|
26
|
+
s.sessionId.slice(0, 18),
|
|
27
|
+
"entries=",
|
|
28
|
+
hist.length,
|
|
29
|
+
"bytes=",
|
|
30
|
+
s.historyBytes,
|
|
31
|
+
"cwd=",
|
|
32
|
+
(s.cwd || "").slice(0, 48),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const kiro = getImportSource("kiro");
|
|
38
|
+
const kRunning = listRunningFromSource(kiro);
|
|
39
|
+
const withHist = kRunning.find((s) => s.historyBytes > 1000) || kRunning[0];
|
|
40
|
+
if (withHist) {
|
|
41
|
+
const dir = mkdtempSync(join(tmpdir(), "grok-imp-"));
|
|
42
|
+
const pkg = buildImportPackage(kiro, withHist, dir);
|
|
43
|
+
console.log("KIRO IMPORT", {
|
|
44
|
+
entries: pkg.entryCount,
|
|
45
|
+
chars: pkg.transcriptChars,
|
|
46
|
+
truncated: pkg.truncatedInline,
|
|
47
|
+
});
|
|
48
|
+
console.log("file size", readFileSync(pkg.transcriptPath, "utf8").length);
|
|
49
|
+
rmSync(dir, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const codex = getImportSource("codex");
|
|
53
|
+
const cRunning = listRunningFromSource(codex);
|
|
54
|
+
if (cRunning[0]) {
|
|
55
|
+
const h = readForeignHistory(codex.format, codex.sessionsRoot, cRunning[0].sessionId);
|
|
56
|
+
console.log("codex first", cRunning[0].sessionId, "entries", h.length);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const claude = getImportSource("claude");
|
|
60
|
+
const files = existsSync(claude.sessionsRoot)
|
|
61
|
+
? readdirSync(claude.sessionsRoot).filter((f) => f.endsWith(".jsonl")).slice(0, 1)
|
|
62
|
+
: [];
|
|
63
|
+
if (files[0]) {
|
|
64
|
+
const id = files[0].replace(/\.jsonl$/, "");
|
|
65
|
+
const h = readForeignHistory(claude.format, claude.sessionsRoot, id);
|
|
66
|
+
console.log("claude sample", id, "entries", h.length);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const msgRoot = join(homedir(), ".local", "share", "opencode", "storage", "message");
|
|
70
|
+
if (existsSync(msgRoot)) {
|
|
71
|
+
const dirs = readdirSync(msgRoot).filter((d) => d.startsWith("ses_")).slice(0, 3);
|
|
72
|
+
for (const d of dirs) {
|
|
73
|
+
const h = readForeignHistory(
|
|
74
|
+
"opencode-storage",
|
|
75
|
+
join(homedir(), ".local", "share", "opencode"),
|
|
76
|
+
d,
|
|
77
|
+
);
|
|
78
|
+
console.log("opencode", d.slice(0, 24), "entries", h.length, h[0]?.role);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log("OK");
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { homedir, tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { buildImportPackage } from "../src/import/build-import.js";
|
|
5
|
+
import { readForeignHistory } from "../src/import/history-readers.js";
|
|
6
|
+
import { listRunningFromSource } from "../src/import/list-running.js";
|
|
7
|
+
import { getImportSource, IMPORT_SOURCES } from "../src/import/sources.js";
|
|
8
|
+
|
|
9
|
+
for (const src of IMPORT_SOURCES) {
|
|
10
|
+
const running = listRunningFromSource(src);
|
|
11
|
+
console.log("===", src.id, "running:", running.length);
|
|
12
|
+
for (const s of running.slice(0, 3)) {
|
|
13
|
+
const hist = readForeignHistory(src.format, src.sessionsRoot, s.sessionId);
|
|
14
|
+
console.log(
|
|
15
|
+
" -",
|
|
16
|
+
s.sessionId.slice(0, 18),
|
|
17
|
+
"entries=",
|
|
18
|
+
hist.length,
|
|
19
|
+
"bytes=",
|
|
20
|
+
s.historyBytes,
|
|
21
|
+
"cwd=",
|
|
22
|
+
(s.cwd || "").slice(0, 48),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const kiro = getImportSource("kiro")!;
|
|
28
|
+
const kRunning = listRunningFromSource(kiro);
|
|
29
|
+
const withHist = kRunning.find((s) => s.historyBytes > 1000) || kRunning[0];
|
|
30
|
+
if (withHist) {
|
|
31
|
+
const dir = mkdtempSync(join(tmpdir(), "grok-imp-"));
|
|
32
|
+
const pkg = buildImportPackage(kiro, withHist, dir);
|
|
33
|
+
console.log("KIRO IMPORT", {
|
|
34
|
+
entries: pkg.entryCount,
|
|
35
|
+
chars: pkg.transcriptChars,
|
|
36
|
+
truncated: pkg.truncatedInline,
|
|
37
|
+
});
|
|
38
|
+
console.log("file size", readFileSync(pkg.transcriptPath, "utf8").length);
|
|
39
|
+
console.log("priming includes path", pkg.priming.includes(pkg.transcriptPath));
|
|
40
|
+
rmSync(dir, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const codex = getImportSource("codex")!;
|
|
44
|
+
const cRunning = listRunningFromSource(codex);
|
|
45
|
+
if (cRunning[0]) {
|
|
46
|
+
const h = readForeignHistory(codex.format, codex.sessionsRoot, cRunning[0].sessionId);
|
|
47
|
+
console.log("codex first", cRunning[0].sessionId, "entries", h.length);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const claude = getImportSource("claude")!;
|
|
51
|
+
const files = existsSync(claude.sessionsRoot)
|
|
52
|
+
? readdirSync(claude.sessionsRoot).filter((f) => f.endsWith(".jsonl")).slice(0, 1)
|
|
53
|
+
: [];
|
|
54
|
+
if (files[0]) {
|
|
55
|
+
const id = files[0]!.replace(/\.jsonl$/, "");
|
|
56
|
+
const h = readForeignHistory(claude.format, claude.sessionsRoot, id);
|
|
57
|
+
console.log("claude sample", id, "entries", h.length);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const msgRoot = join(homedir(), ".local", "share", "opencode", "storage", "message");
|
|
61
|
+
if (existsSync(msgRoot)) {
|
|
62
|
+
const dirs = readdirSync(msgRoot).filter((d) => d.startsWith("ses_")).slice(0, 3);
|
|
63
|
+
for (const d of dirs) {
|
|
64
|
+
const h = readForeignHistory(
|
|
65
|
+
"opencode-storage",
|
|
66
|
+
join(homedir(), ".local", "share", "opencode"),
|
|
67
|
+
d,
|
|
68
|
+
);
|
|
69
|
+
console.log("opencode", d.slice(0, 24), "entries", h.length, h[0]?.role);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log("OK");
|