Switches the Grok runtime plugin to execute prompts via the Grok CLI, parsing its NDJSON stream output instead of the previous invocation path. - Add cli-stream.ts to spawn and stream the Grok CLI process - Add stream-parser.ts to parse NDJSON CLI output into runtime events - Rework runtime-adapter.ts to route execution through CLI streaming - Extend types.ts with CLI stream/NDJSON event types - Add docs/grok-cli-contract.md documenting the CLI streaming contract - Add/update tests for stream-parser and runtime-adapter - Update plugin README with CLI streaming details - Add changeset for the Grok CLI streaming change Files changed: .changeset/fn-7722-grok-cli-streaming.md | 7 + docs/grok-cli-contract.md | 200 +++++++++++++++++++++ plugins/fusion-plugin-grok-runtime/README.md | 28 +++ .../src/__tests__/runtime-adapter.test.ts | 135 ++++++++++++-- .../src/__tests__/stream-parser.test.ts | 79 ++++++++ .../fusion-plugin-grok-runtime/src/cli-stream.ts | 52 ++++++ .../src/runtime-adapter.ts | 182 ++++++++++++++++--- .../src/stream-parser.ts | 54 ++++++ plugins/fusion-plugin-grok-runtime/src/types.ts | 120 +++++++++++++ 9 files changed, 816 insertions(+), 41 deletions(-) Fusion-Task-Id: FN-7722 Fusion-Task-Lineage: c5f33e9d-0032-432b-88b5-4ad8d786d67e Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
9.7 KiB
Grok CLI Contract (FN-7722)
Date: 2026-07-09
Research method
fn_web_fetchagainst the canonical upstream repository (https://github.com/superagent-ai/grok-cli), specifically:README.md(headless-mode overview, feature summary).src/index.ts(commander CLI argument parsing — the exact flag spellings and headless dispatch).src/headless/output.ts(the actual NDJSON event emitter — the authoritative schema source, not just docs prose).src/headless/output.test.ts(fixture-level confirmation of the emitted JSONL shapes, used as ground truth for this plugin's own fixture tests).
- No live
grokbinary was invoked; no field name or flag spelling in this document is guessed — every claim below traces to one of the four files above. Raw captured research (queries + verbatim schema) is preserved as this task'sresearchtask document (fn_task_document_readkeyresearchon FN-7722).
Confirmed non-interactive invocation
grok --prompt "<text>" --format json
# short flags:
grok -p "<text>" --format json
-p, --prompt <prompt>— run a single prompt headlessly, then exit.--format <format>— headless output format,text(default) orjson; invalid values are rejected by commander'sInvalidArgumentError(parseHeadlessOutputFormat/isHeadlessOutputFormatinsrc/index.ts).- Useful companion flags confirmed in the same
program.option(...)chain:-d, --directory <dir>(cwd),-m, --model <model>,-s, --session <id>(resume a saved session, orlatest),-k, --api-key <key>(inline key). --format jsonoutput is newline-delimited JSON (NDJSON/JSONL) — one JSON object per line — not a single JSON document. This is directly confirmed bycreateHeadlessJsonlEmitter()'sjsonLine()helper insrc/headless/output.ts, which appends\nafter eachJSON.stringify.
Verified NDJSON event schema (verbatim)
Source: HeadlessJsonEvent union type in src/headless/output.ts.
type HeadlessJsonEvent =
| { type: "step_start"; sessionID?: string; stepNumber: number; timestamp: number }
| { type: "text"; sessionID?: string; stepNumber: number; text: string; timestamp: number }
| {
type: "tool_use";
sessionID?: string;
stepNumber: number;
timestamp: number;
toolCall: ToolCall;
toolResult: ToolResult;
timing?: { startedAt?: number; finishedAt?: number; durationMs?: number };
}
| {
type: "step_finish";
sessionID?: string;
stepNumber: number;
timestamp: number;
finishReason: string;
usage: { inputTokens?: number; outputTokens?: number; totalTokens?: number; costUsdTicks?: number };
}
| { type: "error"; sessionID?: string; message: string; timestamp: number };
Notes:
sessionIDappears on every event type when a session id is available (agent.getSessionId()); it is simply absent from the JSON object otherwise (notnull).textevents are per-step, buffered assistant content — onetextevent per step carrying the accumulated text for that step, flushed either right before a tool-triggeringstep_finishor inline with a tool-lessstep_finish.- No
thinking/reasoningNDJSON event exists. The underlyingStreamChunkunion used internally does carry a"reasoning"chunk type, butcreateHeadlessJsonlEmitter().consumeChunk()explicitly no-ops on it (case "reasoning": break;insrc/headless/output.ts) — reasoning content is never surfaced through--format json. This is a confirmed absence, notupstream-pending-verification: the Grok streaming adapter therefore drivesonTextonly; there is noonThinkingsignal to bridge for this CLI path today. - There is no explicit terminal
done/resultevent type. A prompt run can contain multiplestep_start/step_finishpairs (multi-round tool use); the authoritative "the run is over" signal is the headless process's stdout stream ending (readlineclose) / subprocess exit, mirroring how the Droid CLI adapter treats subprocesscloseas terminal.errorevents ({ type: "error", message, timestamp }) can also appear inline without necessarily ending the process. - A fatal, pre-JSON failure (e.g. missing API key) is not a JSON line at
all:
src/index.ts'srequireApiKey()writes a plainconsole.error(...)line to stderr and callsprocess.exit(1)before any NDJSON is emitted. Consumers must therefore also treat a non-zero exit with no JSON output as a distinct failure mode from a well-formederrorevent.
Auth / readiness
- The
grokCLI owns authentication end-to-end for CLI-routed execution.runHeadless()insrc/index.tsis only reached viarequireApiKey(config.apiKey), which resolves the key from (in order viaresolveConfig/getApiKey()):-k/--api-keyflag,GROK_API_KEYenv var, project.env, or~/.grok/user-settings.json'sapiKeyfield. If none resolve, the CLI itself exits 1 with an actionable error — Fusion does not need to pass, see, or validate a key for this path to work, as long as the operator'sgrokinstall already has one configured by any of those methods. - Auth implication for this task: because CLI-routed model selections let
the
grokbinary own both auth and inference, the direct-endpointGROK_API_KEYFusion-visibility requirement established by FN-7711 (built-inxai/openai-completionsprovider) and FN-7714 (hydratingGROK_API_KEYfrom~/.grok/user-settings.jsonwhen the env var is unset) becomes unnecessary for CLI-routed selections specifically. It remains necessary and unchanged for the direct xAI OpenAI-compatible path, which stays the default (see "What stays unchanged" below). - This mirrors FN-7716's separate finding that Grok CLI readiness (probe/
auth-status surfacing) does not require Fusion to see a key either — that
surface (
probe.ts,register-auth-routes.ts,GrokCliProviderCard.tsx) is out of scope for this task and is not modified here.
Wiring gap (recorded, not closed by this task)
packages/engine/src/runtime-resolution.ts's resolveRuntime() only reaches
a plugin runtime adapter (like GrokRuntimeAdapter) when
runtimeConfig.runtimeHint === "grok". A repo-wide grep at Step 0 of this
task confirmed nothing in the product sets runtimeHint to "grok"
today (task/agent config, settings, or otherwise) — the same wiring gap
FN-7715's stale comment already noted. This task lands the adapter
implementation and its tests, but does not wire an end-to-end path that
exercises it (no product code sets runtimeHint: "grok", and no settings
toggle exists to prefer CLI execution over the direct endpoint). That wiring
is filed as a follow-up task (see fn_task_create entries linked from this
task).
Decision
Route Grok execution through the CLI: YES, as a scoped, additive
GrokRuntimeAdapter implementation.
Rationale:
- The non-interactive contract is fully pinned to primary source code
(
src/index.tsCLI parsing +src/headless/output.tsemitter +src/headless/output.test.tsfixtures), not just README prose — this clears the External Integration Evidence bar and the "testable from fixture lines without a live binary" bar from the task mission — the parser can be fixture-tested exactly like the Droid plugin'sstream-parser.ts, with no live-binary dependency in tests. - The event schema is simple (
step_start/text/tool_use/step_finish/error) and text-only for this scoped adapter (nothinkingevent exists to bridge), so the implementation stays narrow: a resilient NDJSON line parser plus anonTextbridge, deliberately leaving tool-call/break-early bridging as a documented follow-up (the Droid adapter's much largerprovider.tsis the effort ceiling, not the target shape). - It is fully reversible: the adapter is only reachable via
runtimeHint === "grok", which nothing sets today, so landing it carries no behavioral change to any exercised path.
What stays unchanged
- The direct xAI OpenAI-compatible streaming path (base URL
https://api.x.ai/v1, api typeopenai-completions,GROK_API_KEYsourced per FN-7711/FN-7714) remains the default, exercised Grok execution path. This task does not touchpackages/core/src/grok-provider.tsorpackages/engine/src/pi.ts. - FN-7716's probe/auth-readiness surface (
probe.ts,register-auth-routes.ts,GrokCliProviderCard.tsx) is untouched by this task. - End-to-end routing (making the product actually set
runtimeHint === "grok", or adding a settings toggle to prefer the CLI over the direct endpoint) is explicitly out of scope here and is filed as a follow-up task.
Follow-ups filed from this task
See the task's fn_task_create calls (linked from FN-7722) for:
- End-to-end routing wiring — actually setting
runtimeHint === "grok"(or a settings toggle preferring the CLI) soGrokRuntimeAdapteris exercised in a real execution path. - Full tool-call/break-early bridging for
tool_useNDJSON events, if a future need for Grok-CLI-driven tool execution arises (out of scope for the scoped text/no-thinking adapter landed here).