grok-telegram-bot 2.0.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 +135 -0
- package/CHANGELOG.md +598 -0
- package/LICENSE +21 -0
- package/README.md +644 -0
- package/bin/grok-tg.mjs +21 -0
- package/docs/INSTALL.md +153 -0
- package/docs/UPGRADE.md +253 -0
- package/docs/ops/RELEASE_CHECKLIST.md +39 -0
- package/package.json +74 -0
- package/scripts/setup.mjs +116 -0
- package/src/agents/catalog.ts +58 -0
- package/src/app/accounts.ts +162 -0
- package/src/app/auth-service.ts +136 -0
- package/src/app/grok-credentials.ts +103 -0
- package/src/app/instance-lock.ts +139 -0
- package/src/app/json-store.ts +54 -0
- package/src/app/reasoning.ts +30 -0
- package/src/app/settings-store.ts +38 -0
- package/src/app/stt.ts +53 -0
- package/src/app/types.ts +56 -0
- package/src/app/updater.ts +234 -0
- package/src/app/usage.ts +38 -0
- package/src/app/version.ts +41 -0
- package/src/bot/account-rotator.ts +52 -0
- package/src/bot/auth.ts +38 -0
- package/src/bot/bot.ts +225 -0
- package/src/bot/chat-controller.ts +317 -0
- package/src/bot/commands.ts +52 -0
- package/src/bot/deps.ts +67 -0
- package/src/bot/file-ingest.ts +190 -0
- package/src/bot/handlers/accounts.ts +220 -0
- package/src/bot/handlers/auth.ts +64 -0
- package/src/bot/handlers/control.ts +103 -0
- package/src/bot/handlers/document.ts +112 -0
- package/src/bot/handlers/history.ts +63 -0
- package/src/bot/handlers/kill.ts +54 -0
- package/src/bot/handlers/mcp.ts +206 -0
- package/src/bot/handlers/menu.ts +220 -0
- package/src/bot/handlers/message.ts +103 -0
- package/src/bot/handlers/photo.ts +123 -0
- package/src/bot/handlers/projects.ts +183 -0
- package/src/bot/handlers/running.ts +181 -0
- package/src/bot/handlers/session-card.ts +81 -0
- package/src/bot/handlers/session-kill.ts +95 -0
- package/src/bot/handlers/sessions.ts +148 -0
- package/src/bot/handlers/system.ts +51 -0
- package/src/bot/handlers/tasks.ts +224 -0
- package/src/bot/handlers/usage.ts +38 -0
- package/src/bot/handlers/voice.ts +55 -0
- package/src/bot/image-return.ts +69 -0
- package/src/bot/menu/ephemeral.ts +117 -0
- package/src/bot/menu/keyboard.ts +49 -0
- package/src/bot/menu/refresh.ts +13 -0
- package/src/bot/menu/status-panel.ts +173 -0
- package/src/bot/permission-service.ts +149 -0
- package/src/bot/prompt-content.ts +64 -0
- package/src/bot/prompt-retry.ts +70 -0
- package/src/bot/reauth-controller.ts +297 -0
- package/src/bot/registry.ts +186 -0
- package/src/bot/reply-context.ts +77 -0
- package/src/bot/session-fork.ts +35 -0
- package/src/bot/session-runtime.ts +1048 -0
- package/src/bot/telegram-io.ts +109 -0
- package/src/bot/typing.ts +35 -0
- package/src/bot/wizard/task-wizard.ts +214 -0
- package/src/cli.ts +126 -0
- package/src/config.ts +248 -0
- package/src/grok/client.ts +617 -0
- package/src/grok/models.ts +50 -0
- package/src/grok/session-log.ts +148 -0
- package/src/grok/transport.ts +51 -0
- package/src/grok/types.ts +136 -0
- package/src/index.ts +84 -0
- package/src/logger.ts +78 -0
- package/src/mcp/config.ts +120 -0
- package/src/mcp/probe.ts +218 -0
- package/src/mcp/types.ts +68 -0
- package/src/projects/manager.ts +99 -0
- package/src/render/chunk.ts +57 -0
- package/src/render/diff.ts +48 -0
- package/src/render/escape.ts +22 -0
- package/src/render/file-summary.ts +111 -0
- package/src/render/hashtags.ts +34 -0
- package/src/render/markdown.ts +130 -0
- package/src/render/progress-estimate.ts +63 -0
- package/src/render/progress.ts +80 -0
- package/src/render/subagent.ts +75 -0
- package/src/render/tool-call.ts +196 -0
- package/src/service/index.ts +24 -0
- package/src/service/linux.ts +85 -0
- package/src/service/macos.ts +101 -0
- package/src/service/platform.ts +64 -0
- package/src/service/types.ts +36 -0
- package/src/service/windows.ts +198 -0
- package/src/sessions/history.ts +225 -0
- package/src/sessions/process.ts +30 -0
- package/src/sessions/store.ts +133 -0
- package/src/sessions/tail.ts +86 -0
- package/src/sessions/types.ts +26 -0
- package/src/stream/streamer.ts +261 -0
- package/src/tasks/runner.ts +82 -0
- package/src/tasks/schedule.ts +142 -0
- package/src/tasks/scheduler.ts +53 -0
- package/src/tasks/store.ts +80 -0
- package/src/tasks/types.ts +33 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok ACP client — spawns `grok agent stdio` and speaks JSON-RPC 2.0 over
|
|
3
|
+
* stdio (the Agent Client Protocol). One persistent process manages many
|
|
4
|
+
* sessions. After `initialize` it runs the `authenticate` step (using the
|
|
5
|
+
* cached `grok login` token, or XAI_API_KEY), then callers create/load sessions
|
|
6
|
+
* and send prompts; streamed `session/update` notifications are re-emitted as
|
|
7
|
+
* "session-update" events keyed by sessionId.
|
|
8
|
+
*
|
|
9
|
+
* The bot also records the sessions it drives on disk (see SessionLog) so
|
|
10
|
+
* `/sessions`, `/history` and live-watch keep working regardless of Grok's own
|
|
11
|
+
* internal session store.
|
|
12
|
+
*/
|
|
13
|
+
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|
14
|
+
import { EventEmitter } from "node:events";
|
|
15
|
+
import { createLogger } from "../logger.js";
|
|
16
|
+
import { contextWindowFor, DEFAULT_MODEL, KNOWN_MODELS } from "./models.js";
|
|
17
|
+
import { PROGRESS_DIRECTIVE } from "../render/progress.js";
|
|
18
|
+
import { SessionLog } from "./session-log.js";
|
|
19
|
+
import { JsonRpcTransport } from "./transport.js";
|
|
20
|
+
import type {
|
|
21
|
+
ContentBlock,
|
|
22
|
+
InitializeResult,
|
|
23
|
+
JsonRpcMessage,
|
|
24
|
+
PendingStage,
|
|
25
|
+
PermissionOutcome,
|
|
26
|
+
PromptResult,
|
|
27
|
+
RequestPermissionParams,
|
|
28
|
+
SessionNotificationParams,
|
|
29
|
+
SessionUpdate,
|
|
30
|
+
SubagentInfo,
|
|
31
|
+
SubagentListUpdate,
|
|
32
|
+
} from "./types.js";
|
|
33
|
+
|
|
34
|
+
const log = createLogger("grok:client");
|
|
35
|
+
|
|
36
|
+
export interface SessionMetadata {
|
|
37
|
+
contextUsagePercentage?: number;
|
|
38
|
+
effort?: string;
|
|
39
|
+
credits?: number;
|
|
40
|
+
totalTokens?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const TRANSIENT_CODES = new Set([-32603, -32500, -32000, 500, 502, 503, 504, 429]);
|
|
44
|
+
const TRANSIENT_RE =
|
|
45
|
+
/internal error|high volume|experiencing|overloaded|temporar|unavailable|rate.?limit|too many requests|try again|capacity|dispatch failure|response stream|connection (?:reset|closed|refused|error)|reset by peer|broken pipe|socket hang ?up|econnreset|econnrefused|enotfound|eai_again|etimedout|\b50[234]\b|\b429\b/i;
|
|
46
|
+
const CONTEXT_EXHAUSTED_RE =
|
|
47
|
+
/context (?:length|window|limit|size|overflow)|maximum context|input (?:is )?too long|prompt (?:is )?too long|too many (?:input )?tokens|token limit|exceeds? (?:the )?(?:maximum|context|token)|reduce the (?:length|size)|context.{0,24}exhaust/i;
|
|
48
|
+
|
|
49
|
+
export class GrokError extends Error {
|
|
50
|
+
constructor(
|
|
51
|
+
message: string,
|
|
52
|
+
readonly code?: number,
|
|
53
|
+
readonly data?: unknown,
|
|
54
|
+
) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = "GrokError";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isTransientError(err: Error): boolean {
|
|
61
|
+
const code = (err as GrokError).code;
|
|
62
|
+
if (typeof code === "number" && TRANSIENT_CODES.has(code)) return true;
|
|
63
|
+
return TRANSIENT_RE.test(err.message);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isContextExhaustedError(err: Error): boolean {
|
|
67
|
+
return CONTEXT_EXHAUSTED_RE.test(err.message);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function shortJson(v: unknown): string {
|
|
71
|
+
try {
|
|
72
|
+
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
73
|
+
return s.length > 300 ? `${s.slice(0, 300)}\u2026` : s;
|
|
74
|
+
} catch {
|
|
75
|
+
return String(v);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface GrokClientOptions {
|
|
80
|
+
grokCliPath: string;
|
|
81
|
+
workspace: string;
|
|
82
|
+
sessionsDir: string;
|
|
83
|
+
/** Pass --always-approve to run tools without per-call permission prompts. */
|
|
84
|
+
trustAllTools: boolean;
|
|
85
|
+
/** Optional XAI_API_KEY to export for the agent (else it uses `grok login`). */
|
|
86
|
+
apiKey?: string;
|
|
87
|
+
model?: string;
|
|
88
|
+
requestTimeoutMs?: number;
|
|
89
|
+
autoRestart?: boolean;
|
|
90
|
+
promptIdleTimeoutMs?: number;
|
|
91
|
+
promptMaxMs?: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface Pending {
|
|
95
|
+
resolve: (v: unknown) => void;
|
|
96
|
+
reject: (e: Error) => void;
|
|
97
|
+
cleanup: () => void;
|
|
98
|
+
method: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export declare interface GrokClient {
|
|
102
|
+
on(e: "session-update", l: (sessionId: string, update: SessionUpdate) => void): this;
|
|
103
|
+
on(e: "notification", l: (method: string, params: unknown) => void): this;
|
|
104
|
+
on(e: "exit", l: (code: number | null) => void): this;
|
|
105
|
+
on(e: "restarted", l: () => void): this;
|
|
106
|
+
on(e: "subagents", l: (subagents: SubagentInfo[], pending: PendingStage[]) => void): this;
|
|
107
|
+
emit(e: "session-update", sessionId: string, update: SessionUpdate): boolean;
|
|
108
|
+
emit(e: "notification", method: string, params: unknown): boolean;
|
|
109
|
+
emit(e: "exit", code: number | null): boolean;
|
|
110
|
+
emit(e: "restarted"): boolean;
|
|
111
|
+
emit(e: "subagents", subagents: SubagentInfo[], pending: PendingStage[]): boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class GrokClient extends EventEmitter {
|
|
115
|
+
private proc?: ChildProcessWithoutNullStreams;
|
|
116
|
+
private transport?: JsonRpcTransport;
|
|
117
|
+
private nextId = 1;
|
|
118
|
+
private readonly pending = new Map<number | string, Pending>();
|
|
119
|
+
private readonly timeout: number;
|
|
120
|
+
private readonly promptIdleMs: number;
|
|
121
|
+
private readonly promptMaxMs: number;
|
|
122
|
+
private readonly slog: SessionLog;
|
|
123
|
+
private readonly lastActivity = new Map<string, number>();
|
|
124
|
+
private lastActivityAny = 0;
|
|
125
|
+
private stopped = false;
|
|
126
|
+
private restartAttempts = 0;
|
|
127
|
+
private restartTimer?: NodeJS.Timeout;
|
|
128
|
+
/** Per-session cwd (for logging + re-bind). */
|
|
129
|
+
private readonly cwd = new Map<string, string>();
|
|
130
|
+
/** Sessions with an in-flight prompt (drives "active"). */
|
|
131
|
+
private readonly running = new Set<string>();
|
|
132
|
+
/** Accumulated assistant text per in-flight turn (flushed to the log on end). */
|
|
133
|
+
private readonly assistantBuf = new Map<string, string>();
|
|
134
|
+
private authMethodId?: string;
|
|
135
|
+
|
|
136
|
+
agentInfo?: { name?: string; version?: string };
|
|
137
|
+
capabilities?: InitializeResult["agentCapabilities"];
|
|
138
|
+
availableModes: Array<{ id: string; name: string; description?: string }> = [];
|
|
139
|
+
currentModeId?: string;
|
|
140
|
+
availableModels: Array<{ modelId: string; name: string; description?: string }> = [];
|
|
141
|
+
currentModelId?: string;
|
|
142
|
+
private readonly metadata = new Map<string, SessionMetadata>();
|
|
143
|
+
private subagents: SubagentInfo[] = [];
|
|
144
|
+
private pendingStages: PendingStage[] = [];
|
|
145
|
+
permissionHandler?: (params: RequestPermissionParams) => Promise<PermissionOutcome>;
|
|
146
|
+
|
|
147
|
+
constructor(private readonly opts: GrokClientOptions) {
|
|
148
|
+
super();
|
|
149
|
+
this.setMaxListeners(0);
|
|
150
|
+
this.slog = new SessionLog(opts.sessionsDir);
|
|
151
|
+
this.timeout = opts.requestTimeoutMs ?? 120_000;
|
|
152
|
+
this.promptIdleMs = opts.promptIdleTimeoutMs ?? 900_000;
|
|
153
|
+
this.promptMaxMs = opts.promptMaxMs ?? 6 * 60 * 60_000;
|
|
154
|
+
this.currentModelId = opts.model || DEFAULT_MODEL;
|
|
155
|
+
this.availableModels = KNOWN_MODELS.map((m) => ({ modelId: m.modelId, name: m.name, description: m.description }));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async start(): Promise<void> {
|
|
159
|
+
this.stopped = false;
|
|
160
|
+
await this.connect();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async connect(): Promise<void> {
|
|
164
|
+
const args = ["agent", "stdio", "--no-auto-update"];
|
|
165
|
+
if (this.opts.trustAllTools) args.push("--always-approve");
|
|
166
|
+
|
|
167
|
+
log.info(`spawning: ${this.opts.grokCliPath} ${args.join(" ")}`);
|
|
168
|
+
const env = { ...process.env };
|
|
169
|
+
if (this.opts.apiKey) env.XAI_API_KEY = this.opts.apiKey;
|
|
170
|
+
const proc = spawn(this.opts.grokCliPath, args, {
|
|
171
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
172
|
+
cwd: this.opts.workspace,
|
|
173
|
+
env,
|
|
174
|
+
}) as ChildProcessWithoutNullStreams;
|
|
175
|
+
this.proc = proc;
|
|
176
|
+
|
|
177
|
+
proc.on("exit", (code) => {
|
|
178
|
+
if (this.proc !== proc) return;
|
|
179
|
+
log.warn(`grok agent exited (code ${code})`);
|
|
180
|
+
this.failAllPending(new Error(`grok agent stdio exited (code ${code})`));
|
|
181
|
+
this.emit("exit", code);
|
|
182
|
+
this.maybeRestart();
|
|
183
|
+
});
|
|
184
|
+
proc.on("error", (err) => {
|
|
185
|
+
if (this.proc !== proc) return;
|
|
186
|
+
log.error("failed to spawn grok:", err.message);
|
|
187
|
+
this.failAllPending(err);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
this.transport = new JsonRpcTransport(proc);
|
|
191
|
+
this.transport.on("message", (m: JsonRpcMessage) => this.onMessage(m));
|
|
192
|
+
|
|
193
|
+
const init = (await this.request("initialize", {
|
|
194
|
+
protocolVersion: 1,
|
|
195
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
|
|
196
|
+
clientInfo: { name: "grok-telegram-bot", version: "2.0.0" },
|
|
197
|
+
})) as InitializeResult;
|
|
198
|
+
|
|
199
|
+
this.agentInfo = init.agentInfo ?? { name: "grok" };
|
|
200
|
+
this.capabilities = init.agentCapabilities;
|
|
201
|
+
this.restartAttempts = 0;
|
|
202
|
+
this.subagents = [];
|
|
203
|
+
this.pendingStages = [];
|
|
204
|
+
|
|
205
|
+
// Authenticate: prefer the cached `grok login` token; fall back to API key.
|
|
206
|
+
const methods = new Set((init.authMethods ?? []).map((m) => m.id));
|
|
207
|
+
this.authMethodId =
|
|
208
|
+
this.opts.apiKey && methods.has("xai.api_key")
|
|
209
|
+
? "xai.api_key"
|
|
210
|
+
: methods.has("cached_token")
|
|
211
|
+
? "cached_token"
|
|
212
|
+
: (init.authMethods?.[0]?.id ?? undefined);
|
|
213
|
+
if (this.authMethodId) {
|
|
214
|
+
try {
|
|
215
|
+
await this.request("authenticate", { methodId: this.authMethodId, _meta: { headless: true } });
|
|
216
|
+
} catch (e) {
|
|
217
|
+
log.warn(`authenticate (${this.authMethodId}) failed: ${(e as Error).message}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
log.info(`connected: ${this.agentInfo?.name ?? "grok"} ${this.agentInfo?.version ?? ""}`.trim());
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private maybeRestart(): void {
|
|
224
|
+
if (this.stopped || !this.opts.autoRestart) return;
|
|
225
|
+
const delay = Math.min(30_000, 1000 * 2 ** this.restartAttempts);
|
|
226
|
+
this.restartAttempts += 1;
|
|
227
|
+
log.warn(`auto-restarting ACP in ${delay}ms (attempt ${this.restartAttempts})`);
|
|
228
|
+
this.restartTimer = setTimeout(() => {
|
|
229
|
+
this.connect()
|
|
230
|
+
.then(() => {
|
|
231
|
+
log.info("ACP reconnected");
|
|
232
|
+
this.emit("restarted");
|
|
233
|
+
})
|
|
234
|
+
.catch((e) => {
|
|
235
|
+
log.error("ACP restart failed:", (e as Error).message);
|
|
236
|
+
this.maybeRestart();
|
|
237
|
+
});
|
|
238
|
+
}, delay);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
get supportsLoadSession(): boolean {
|
|
242
|
+
return Boolean(this.capabilities?.loadSession);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
hasInflightPrompt(): boolean {
|
|
246
|
+
for (const p of this.pending.values()) if (p.method === "session/prompt") return true;
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** True while the given session has a turn in flight. */
|
|
251
|
+
isSessionActive(sessionId: string): boolean {
|
|
252
|
+
return this.running.has(sessionId);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** PID of the shared `grok agent stdio` process. */
|
|
256
|
+
get pid(): number | undefined {
|
|
257
|
+
return this.proc?.pid;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async newSession(cwd: string): Promise<string> {
|
|
261
|
+
const res = (await this.request("session/new", { cwd, mcpServers: [] })) as { sessionId: string };
|
|
262
|
+
this.parseSessionExtras(res);
|
|
263
|
+
this.cwd.set(res.sessionId, cwd);
|
|
264
|
+
this.slog.create(res.sessionId, cwd);
|
|
265
|
+
return res.sessionId;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async loadSession(sessionId: string, cwd: string): Promise<void> {
|
|
269
|
+
const res = await this.request("session/load", { sessionId, cwd, mcpServers: [] });
|
|
270
|
+
this.parseSessionExtras(res);
|
|
271
|
+
this.cwd.set(sessionId, cwd);
|
|
272
|
+
this.slog.create(sessionId, cwd, "resumed");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
hasMode(id: string): boolean {
|
|
276
|
+
return this.availableModes.some((m) => m.id === id);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
hasModel(id: string): boolean {
|
|
280
|
+
return id === "auto" || this.availableModels.some((m) => m.modelId === id);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private parseSessionExtras(result: unknown): void {
|
|
284
|
+
const r = result as {
|
|
285
|
+
modes?: { currentModeId?: string; availableModes?: Array<{ id: string; name: string; description?: string }> };
|
|
286
|
+
models?: { currentModelId?: string; availableModels?: Array<{ modelId: string; name: string; description?: string }> };
|
|
287
|
+
};
|
|
288
|
+
if (r?.modes?.availableModes?.length) this.availableModes = r.modes.availableModes;
|
|
289
|
+
if (r?.modes?.currentModeId) this.currentModeId = r.modes.currentModeId;
|
|
290
|
+
if (r?.models?.availableModels?.length) this.availableModels = r.models.availableModels;
|
|
291
|
+
if (r?.models?.currentModelId) this.currentModelId = r.models.currentModelId;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
prompt(sessionId: string, content: ContentBlock[]): Promise<PromptResult> {
|
|
295
|
+
return new Promise<PromptResult>((resolve, reject) => {
|
|
296
|
+
const id = this.nextId++;
|
|
297
|
+
const start = Date.now();
|
|
298
|
+
this.lastActivity.set(sessionId, start);
|
|
299
|
+
this.running.add(sessionId);
|
|
300
|
+
if (this.proc?.pid) this.slog.lock(sessionId, this.proc.pid);
|
|
301
|
+
const userText = this.cleanUserText(content);
|
|
302
|
+
this.slog.logUser(sessionId, userText);
|
|
303
|
+
const meta = this.slog.read(sessionId);
|
|
304
|
+
if (!meta?.title || meta.title === "(untitled)") {
|
|
305
|
+
const title = userText.replace(/\s+/g, " ").trim().slice(0, 80);
|
|
306
|
+
if (title) this.slog.update(sessionId, { title });
|
|
307
|
+
}
|
|
308
|
+
const watch = setInterval(() => {
|
|
309
|
+
const last = Math.max(this.lastActivity.get(sessionId) ?? start, this.lastActivityAny);
|
|
310
|
+
const idle = Date.now() - last;
|
|
311
|
+
const total = Date.now() - start;
|
|
312
|
+
if (total > this.promptMaxMs) {
|
|
313
|
+
this.pending.delete(id);
|
|
314
|
+
this.finishPrompt(sessionId, id);
|
|
315
|
+
clearInterval(watch);
|
|
316
|
+
void this.cancel(sessionId);
|
|
317
|
+
reject(new Error(`Prompt exceeded the ${Math.round(this.promptMaxMs / 60_000)}min cap`));
|
|
318
|
+
} else if (idle > this.promptIdleMs) {
|
|
319
|
+
this.pending.delete(id);
|
|
320
|
+
this.finishPrompt(sessionId, id);
|
|
321
|
+
clearInterval(watch);
|
|
322
|
+
void this.cancel(sessionId);
|
|
323
|
+
reject(new Error(`No agent activity for ${Math.round(idle / 1000)}s — giving up`));
|
|
324
|
+
}
|
|
325
|
+
}, 15_000);
|
|
326
|
+
this.pending.set(id, {
|
|
327
|
+
resolve: (v) => {
|
|
328
|
+
this.finishPrompt(sessionId, id);
|
|
329
|
+
resolve(v as PromptResult);
|
|
330
|
+
},
|
|
331
|
+
reject: (e) => {
|
|
332
|
+
this.finishPrompt(sessionId, id);
|
|
333
|
+
reject(e);
|
|
334
|
+
},
|
|
335
|
+
cleanup: () => clearInterval(watch),
|
|
336
|
+
method: "session/prompt",
|
|
337
|
+
});
|
|
338
|
+
try {
|
|
339
|
+
this.transport!.send({ jsonrpc: "2.0", id, method: "session/prompt", params: { sessionId, prompt: content } });
|
|
340
|
+
} catch (e) {
|
|
341
|
+
clearInterval(watch);
|
|
342
|
+
this.pending.delete(id);
|
|
343
|
+
this.finishPrompt(sessionId, id);
|
|
344
|
+
reject(e as Error);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Clear the running/lock state for a finished turn and flush its transcript. */
|
|
350
|
+
private finishPrompt(sessionId: string, _id: number): void {
|
|
351
|
+
this.running.delete(sessionId);
|
|
352
|
+
this.slog.unlock(sessionId);
|
|
353
|
+
const buf = this.assistantBuf.get(sessionId);
|
|
354
|
+
if (buf && buf.trim()) this.slog.logAssistant(sessionId, buf);
|
|
355
|
+
this.assistantBuf.delete(sessionId);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async cancel(sessionId: string): Promise<void> {
|
|
359
|
+
try {
|
|
360
|
+
this.transport?.send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
|
|
361
|
+
} catch (e) {
|
|
362
|
+
log.debug("cancel failed:", (e as Error).message);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async setModel(sessionId: string, modelId: string): Promise<void> {
|
|
367
|
+
await this.request("session/set_model", { sessionId, modelId });
|
|
368
|
+
this.currentModelId = modelId;
|
|
369
|
+
this.slog.update(sessionId, { model: modelId });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async setMode(sessionId: string, modeId: string): Promise<void> {
|
|
373
|
+
await this.request("session/set_mode", { sessionId, modeId });
|
|
374
|
+
this.currentModeId = modeId;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async executeCommand(sessionId: string, command: string): Promise<unknown> {
|
|
378
|
+
return this.request("_grok.dev/commands/execute", { sessionId, command });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
stop(): void {
|
|
382
|
+
this.stopped = true;
|
|
383
|
+
if (this.restartTimer) {
|
|
384
|
+
clearTimeout(this.restartTimer);
|
|
385
|
+
this.restartTimer = undefined;
|
|
386
|
+
}
|
|
387
|
+
void this.killCurrent();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async stopAndWait(): Promise<void> {
|
|
391
|
+
this.stopped = true;
|
|
392
|
+
if (this.restartTimer) {
|
|
393
|
+
clearTimeout(this.restartTimer);
|
|
394
|
+
this.restartTimer = undefined;
|
|
395
|
+
}
|
|
396
|
+
await this.killCurrent();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async restart(): Promise<void> {
|
|
400
|
+
if (this.restartTimer) {
|
|
401
|
+
clearTimeout(this.restartTimer);
|
|
402
|
+
this.restartTimer = undefined;
|
|
403
|
+
}
|
|
404
|
+
this.stopped = true;
|
|
405
|
+
this.restartAttempts = 0;
|
|
406
|
+
await this.killCurrent();
|
|
407
|
+
this.stopped = false;
|
|
408
|
+
await this.connect();
|
|
409
|
+
this.emit("restarted");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private killCurrent(): Promise<void> {
|
|
413
|
+
const proc = this.proc;
|
|
414
|
+
this.proc = undefined;
|
|
415
|
+
this.transport = undefined;
|
|
416
|
+
this.failAllPending(new Error("grok agent is restarting"));
|
|
417
|
+
if (!proc || proc.exitCode !== null || proc.signalCode !== null) {
|
|
418
|
+
return Promise.resolve();
|
|
419
|
+
}
|
|
420
|
+
return new Promise<void>((resolve) => {
|
|
421
|
+
let settled = false;
|
|
422
|
+
const done = (): void => {
|
|
423
|
+
if (settled) return;
|
|
424
|
+
settled = true;
|
|
425
|
+
clearTimeout(hard);
|
|
426
|
+
resolve();
|
|
427
|
+
};
|
|
428
|
+
const hard = setTimeout(() => {
|
|
429
|
+
try {
|
|
430
|
+
proc.kill("SIGKILL");
|
|
431
|
+
} catch {
|
|
432
|
+
/* ignore */
|
|
433
|
+
}
|
|
434
|
+
setTimeout(done, 500);
|
|
435
|
+
}, 4000);
|
|
436
|
+
proc.once("exit", done);
|
|
437
|
+
try {
|
|
438
|
+
proc.kill();
|
|
439
|
+
} catch {
|
|
440
|
+
done();
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
metadataFor(sessionId: string | undefined): SessionMetadata | undefined {
|
|
446
|
+
return sessionId ? this.metadata.get(sessionId) : undefined;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
currentSubagents(): SubagentInfo[] {
|
|
450
|
+
return this.subagents.slice();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
currentPendingStages(): PendingStage[] {
|
|
454
|
+
return this.pendingStages.slice();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
subagentById(sessionId: string): SubagentInfo | undefined {
|
|
458
|
+
return this.subagents.find((s) => s.sessionId === sessionId);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ── JSON-RPC plumbing ──────────────────────────────────────────────────────
|
|
462
|
+
|
|
463
|
+
private request(method: string, params: unknown): Promise<unknown> {
|
|
464
|
+
return new Promise((resolve, reject) => {
|
|
465
|
+
const id = this.nextId++;
|
|
466
|
+
const timer = setTimeout(() => {
|
|
467
|
+
this.pending.delete(id);
|
|
468
|
+
reject(new Error(`Timeout after ${this.timeout}ms: ${method}`));
|
|
469
|
+
}, this.timeout);
|
|
470
|
+
this.pending.set(id, { resolve, reject, cleanup: () => clearTimeout(timer), method });
|
|
471
|
+
try {
|
|
472
|
+
this.transport!.send({ jsonrpc: "2.0", id, method, params });
|
|
473
|
+
} catch (e) {
|
|
474
|
+
clearTimeout(timer);
|
|
475
|
+
this.pending.delete(id);
|
|
476
|
+
reject(e as Error);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private toGrokError(error: { code: number; message: string; data?: unknown }, method: string): GrokError {
|
|
482
|
+
const codeStr = typeof error.code === "number" ? ` [${error.code}]` : "";
|
|
483
|
+
const detail = error.data === undefined ? "" : ` — ${shortJson(error.data)}`;
|
|
484
|
+
const text = `${error.message || "ACP error"}${codeStr}${detail}`;
|
|
485
|
+
log.warn(`${method} failed: ${text}`);
|
|
486
|
+
return new GrokError(text, error.code, error.data);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private onMessage(msg: JsonRpcMessage): void {
|
|
490
|
+
// Response to one of our requests.
|
|
491
|
+
if (msg.id !== undefined && msg.id !== null && this.pending.has(msg.id) && msg.method === undefined) {
|
|
492
|
+
const p = this.pending.get(msg.id)!;
|
|
493
|
+
p.cleanup();
|
|
494
|
+
this.pending.delete(msg.id);
|
|
495
|
+
if (msg.error) p.reject(this.toGrokError(msg.error, p.method));
|
|
496
|
+
else p.resolve(msg.result);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
// Request from the agent (has both id and method) — needs a response.
|
|
500
|
+
if (msg.id !== undefined && msg.id !== null && msg.method) {
|
|
501
|
+
void this.respondToServerRequest(msg.id, msg.method, (msg.params as Record<string, unknown>) || {});
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
// Notification (method, no id).
|
|
505
|
+
if (msg.method) this.routeNotification(msg.method, msg.params);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private async respondToServerRequest(
|
|
509
|
+
id: number | string,
|
|
510
|
+
method: string,
|
|
511
|
+
params: Record<string, unknown>,
|
|
512
|
+
): Promise<void> {
|
|
513
|
+
try {
|
|
514
|
+
let result: unknown;
|
|
515
|
+
if (method === "session/request_permission" && this.permissionHandler) {
|
|
516
|
+
result = await this.permissionHandler(params as unknown as RequestPermissionParams);
|
|
517
|
+
} else if (method === "session/request_permission") {
|
|
518
|
+
// No handler (trust-all): auto-approve the first option, else cancel.
|
|
519
|
+
const opts = (params.options as Array<{ optionId: string }>) ?? [];
|
|
520
|
+
result = opts[0]
|
|
521
|
+
? { outcome: { outcome: "selected", optionId: opts[0].optionId } }
|
|
522
|
+
: { outcome: { outcome: "cancelled" } };
|
|
523
|
+
} else {
|
|
524
|
+
// We advertise no fs/terminal capabilities, so the agent shouldn't ask.
|
|
525
|
+
throw new GrokError(`unsupported client method: ${method}`, -32601);
|
|
526
|
+
}
|
|
527
|
+
this.transport?.send({ jsonrpc: "2.0", id, result });
|
|
528
|
+
} catch (err) {
|
|
529
|
+
this.transport?.send({ jsonrpc: "2.0", id, error: { code: -32000, message: (err as Error).message } });
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private routeNotification(method: string, params: unknown): void {
|
|
534
|
+
if (method === "session/update" || method === "_grok.dev/metadata" || method === "_grok.dev/subagent/list_update") {
|
|
535
|
+
this.lastActivityAny = Date.now();
|
|
536
|
+
}
|
|
537
|
+
if (method === "session/update") {
|
|
538
|
+
const p = params as SessionNotificationParams;
|
|
539
|
+
if (p?.sessionId && p.update) {
|
|
540
|
+
this.lastActivity.set(p.sessionId, Date.now());
|
|
541
|
+
this.recordUpdate(p.sessionId, p.update);
|
|
542
|
+
this.emit("session-update", p.sessionId, p.update);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (method === "_grok.dev/metadata") {
|
|
547
|
+
const p = (params as Record<string, unknown>) ?? {};
|
|
548
|
+
const sessionId = p.sessionId as string | undefined;
|
|
549
|
+
if (sessionId) {
|
|
550
|
+
const prev = this.metadata.get(sessionId);
|
|
551
|
+
this.metadata.set(sessionId, {
|
|
552
|
+
contextUsagePercentage: (p.contextUsagePercentage as number | undefined) ?? prev?.contextUsagePercentage,
|
|
553
|
+
effort: (p.effort as string | undefined) ?? prev?.effort,
|
|
554
|
+
credits: (p.creditsUsed as number | undefined) ?? (p.credits as number | undefined) ?? prev?.credits,
|
|
555
|
+
totalTokens: (p.totalTokens as number | undefined) ?? prev?.totalTokens,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (method === "_grok.dev/subagent/list_update") {
|
|
560
|
+
const p = (params as SubagentListUpdate) || {};
|
|
561
|
+
this.subagents = Array.isArray(p.subagents) ? p.subagents : [];
|
|
562
|
+
this.pendingStages = Array.isArray(p.pendingStages) ? p.pendingStages : [];
|
|
563
|
+
this.emit("subagents", this.subagents, this.pendingStages);
|
|
564
|
+
}
|
|
565
|
+
this.emit("notification", method, params);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Accumulate assistant text and log tool calls to the session's jsonl. */
|
|
569
|
+
private recordUpdate(sessionId: string, u: SessionUpdate): void {
|
|
570
|
+
if (u.sessionUpdate === "agent_message_chunk" && typeof u.content?.text === "string") {
|
|
571
|
+
this.assistantBuf.set(sessionId, (this.assistantBuf.get(sessionId) ?? "") + u.content.text);
|
|
572
|
+
} else if (u.sessionUpdate === "tool_call" && (u.title || u.kind)) {
|
|
573
|
+
this.slog.logTool(sessionId, u.title || u.kind || "tool", "");
|
|
574
|
+
}
|
|
575
|
+
// Derive a context-usage %/token count if the update carries usage info.
|
|
576
|
+
const usage = (u as { usage?: { totalTokens?: number } }).usage;
|
|
577
|
+
if (usage?.totalTokens) {
|
|
578
|
+
const prev = this.metadata.get(sessionId) ?? {};
|
|
579
|
+
const win = contextWindowFor(this.currentModelId);
|
|
580
|
+
this.metadata.set(sessionId, {
|
|
581
|
+
...prev,
|
|
582
|
+
totalTokens: usage.totalTokens,
|
|
583
|
+
contextUsagePercentage: Math.min(100, Math.round((usage.totalTokens / win) * 100)),
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private failAllPending(err: Error): void {
|
|
589
|
+
for (const [, p] of this.pending) {
|
|
590
|
+
p.cleanup();
|
|
591
|
+
p.reject(err);
|
|
592
|
+
}
|
|
593
|
+
this.pending.clear();
|
|
594
|
+
this.running.clear();
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private visibleText(content: ContentBlock[]): string {
|
|
598
|
+
return content
|
|
599
|
+
.filter((b) => b.type === "text" && typeof b.text === "string")
|
|
600
|
+
.map((b) => b.text as string)
|
|
601
|
+
.join("\n")
|
|
602
|
+
.trim();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** The user's message with bot-added decorations (progress directive, a
|
|
606
|
+
* leading reasoning directive, fork/priming preamble) removed, for a clean log. */
|
|
607
|
+
private cleanUserText(content: ContentBlock[]): string {
|
|
608
|
+
let t = this.visibleText(content);
|
|
609
|
+
const pi = t.indexOf(PROGRESS_DIRECTIVE);
|
|
610
|
+
if (pi !== -1) t = t.slice(0, pi).trimEnd();
|
|
611
|
+
const marker = "User's new message:\n";
|
|
612
|
+
const mi = t.lastIndexOf(marker);
|
|
613
|
+
if (mi !== -1) t = t.slice(mi + marker.length);
|
|
614
|
+
t = t.replace(/^\([^\n)]*\)\s*\n+/, "");
|
|
615
|
+
return t.trim();
|
|
616
|
+
}
|
|
617
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static catalog of known Grok Build models, used to seed the Model menu when
|
|
3
|
+
* the ACP agent doesn't advertise a model list, and to derive a context-usage %
|
|
4
|
+
* from any token counts the agent reports. `session/new` model info (when
|
|
5
|
+
* present) always wins over this list.
|
|
6
|
+
*/
|
|
7
|
+
export interface GrokModel {
|
|
8
|
+
modelId: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
/** Approximate max context window in tokens (for the context-usage bar). */
|
|
12
|
+
contextWindow: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Default model that powers Grok Build. */
|
|
16
|
+
export const DEFAULT_MODEL = "grok-4.5";
|
|
17
|
+
|
|
18
|
+
/** Known models (best-effort; the agent's own list wins when advertised). */
|
|
19
|
+
export const KNOWN_MODELS: GrokModel[] = [
|
|
20
|
+
{ modelId: "grok-4.5", name: "Grok 4.5", description: "Flagship coding model (default)", contextWindow: 256_000 },
|
|
21
|
+
{ modelId: "grok-4.20-non-reasoning", name: "Grok 4.20 (non-reasoning)", description: "Faster, no deep reasoning", contextWindow: 256_000 },
|
|
22
|
+
{ modelId: "grok-4", name: "Grok 4", description: "Grok 4", contextWindow: 256_000 },
|
|
23
|
+
{ modelId: "grok-code-fast-1", name: "Grok Code Fast", description: "Coding-optimized, fast", contextWindow: 256_000 },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const DEFAULT_CONTEXT_WINDOW = 256_000;
|
|
27
|
+
|
|
28
|
+
/** Context window for a model id (falls back to a conservative default). */
|
|
29
|
+
export function contextWindowFor(modelId: string | undefined): number {
|
|
30
|
+
if (!modelId) return DEFAULT_CONTEXT_WINDOW;
|
|
31
|
+
const hit = KNOWN_MODELS.find((m) => m.modelId === modelId);
|
|
32
|
+
if (hit) return hit.contextWindow;
|
|
33
|
+
return DEFAULT_CONTEXT_WINDOW;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Map a tool name to a coarse "kind" so the renderer can pick the right
|
|
38
|
+
* icon/label (read / edit / execute / search / …).
|
|
39
|
+
*/
|
|
40
|
+
export function toolKind(name: string | undefined): string {
|
|
41
|
+
const n = (name || "").toLowerCase();
|
|
42
|
+
if (/write|edit|create|apply|patch|str_replace|delete|move|mkdir/.test(n)) return "edit";
|
|
43
|
+
if (/read|view|open|cat|ls|list|glob|stat/.test(n)) return "read";
|
|
44
|
+
if (/search|grep|find|search_web|search_x/.test(n)) return "search";
|
|
45
|
+
if (/bash|shell|exec|run|command|terminal|process/.test(n)) return "execute";
|
|
46
|
+
if (/fetch|http|curl|web|browse/.test(n)) return "fetch";
|
|
47
|
+
if (/task|delegate|agent/.test(n)) return "think";
|
|
48
|
+
if (/image|video|media|generate_/.test(n)) return "other";
|
|
49
|
+
return "other";
|
|
50
|
+
}
|