Run Grok Build CLI as an ACP agent
Run Grok Build CLI as an ACP agent
grok agent stdio runs Grok as an ACP agent over JSON-RPC on stdin/stdout for IDE and tool integration. Authenticate with grok login or XAI_API_KEY. Prefer --no-auto-update in CI and scripts.
Flow: initialize → authenticate (xai.api_key or cached_token) → session/new → session/prompt. Assistant text arrives as session/update chunks with agent_message_chunk. session/prompt returns completion metadata.
One-shot scripts still use headless grok -p (covered in run-grok-cli-headless). ACP is the multi-turn JSON-RPC path.
grok --no-auto-update agent stdio
Condensed Node.js spawn
import { spawn } from "node:child_process";
import readline from "node:readline";
import process from "node:process";
const proc = spawn("grok", ["--no-auto-update", "agent", "stdio"], {
stdio: ["pipe", "pipe", "pipe"],
});
const rl = readline.createInterface({ input: proc.stdout });
const pending = new Map();
let nextId = 1;
let text = "";
proc.stderr.on("data", (chunk) => process.stderr.write(chunk));
rl.on("line", (line) => {
const message = JSON.parse(line);
if (message.method === "session/update") {
const update = message.params?.update;
if (update?.sessionUpdate === "agent_message_chunk" && update.content?.text) {
text += update.content.text;
}
return;
}
const pendingRequest = pending.get(message.id);
if (!pendingRequest) return;
pending.delete(message.id);
if (message.error) {
pendingRequest.reject(new Error(message.error.message ?? JSON.stringify(message.error)));
} else {
pendingRequest.resolve(message.result ?? {});
}
});
function request(method, params, timeoutMs = 30000) {
const id = nextId++;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pending.delete(id);
reject(new Error(`${method} timed out`));
}, timeoutMs);
pending.set(id, {
resolve(result) {
clearTimeout(timer);
resolve(result);
},
reject(error) {
clearTimeout(timer);
reject(error);
},
});
proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
});
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
try {
const init = await request("initialize", {
protocolVersion: 1,
clientCapabilities: {
fs: { readTextFile: true, writeTextFile: true },
terminal: true,
},
});
const authMethods = new Set((init.authMethods ?? []).map((m) => m.id));
const methodId =
process.env.XAI_API_KEY && authMethods.has("xai.api_key")
? "xai.api_key"
: authMethods.has("cached_token")
? "cached_token"
: null;
if (!methodId) {
throw new Error("Run `grok login` first, or set XAI_API_KEY.");
}
await request("authenticate", { methodId, _meta: { headless: true } });
const { sessionId } = await request("session/new", {
cwd: process.cwd(),
mcpServers: [],
});
const prompt = await request("session/prompt", {
sessionId,
prompt: [{ type: "text", text: "Say hello in one short sentence." }],
});
let lastLength = -1;
let stableChecks = 0;
while (stableChecks < 2) {
await sleep(150);
if (text.length === lastLength) stableChecks += 1;
else {
lastLength = text.length;
stableChecks = 0;
}
}
console.log(text.trim() || `No text returned (stopReason=${prompt.stopReason})`);
} finally {
rl.close();
proc.kill();
}
Pitfalls
- Collect assistant text from
session/update/agent_message_chunk. Do not expect the full answer on thesession/promptresult alone. - Without
--no-auto-update(orauto_update = falseunder[cli]in~/.grok/config.toml), background update checks can stall CI. - Run
grok loginonce on the machine, or exportXAI_API_KEY, before spawning the agent.