BUILD / run-grok-cli-acp

Build

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: initializeauthenticate (xai.api_key or cached_token) → session/newsession/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 the session/prompt result alone.
  • Without --no-auto-update (or auto_update = false under [cli] in ~/.grok/config.toml), background update checks can stall CI.
  • Run grok login once on the machine, or export XAI_API_KEY, before spawning the agent.