@proagentstore/cli 0.4.159 → 0.4.161

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.
@@ -1,9 +1,10 @@
1
1
  import { stripAnsi } from "./transcript-lines.js";
2
+ import { createGrokEngineAdapter, grokOutputFormat } from "./grok-adapter.js";
2
3
  export function engineInvocationModeFromAdapter(mode) {
3
4
  return mode === "stream-json" ? "structured" : "raw";
4
5
  }
5
6
  export function structuredCapableEngine(clientType) {
6
- return clientType === "claude" || clientType === "codex";
7
+ return clientType === "claude" || clientType === "codex" || clientType === "grok";
7
8
  }
8
9
  export function engineInvocationWarning(clientType, mode) {
9
10
  if (mode !== "raw" || !structuredCapableEngine(clientType))
@@ -234,5 +235,8 @@ export function engineAdapterFor(clientType, userArgs = []) {
234
235
  return claudeEngineAdapter;
235
236
  if (clientType === "codex" && userArgs[0] === "exec" && !["resume", "fork", "review", "help"].includes(userArgs[1] ?? ""))
236
237
  return codexEngineAdapter;
238
+ // Grok's NDJSON stream (#1187) — unless the preset chose another output format on purpose, which stays raw.
239
+ if (clientType === "grok" && [null, "streaming-json"].includes(grokOutputFormat(userArgs)))
240
+ return createGrokEngineAdapter();
237
241
  return genericRawEngineAdapter;
238
242
  }
@@ -123,3 +123,43 @@ export function checkEngine(input, deps = {}) {
123
123
  ...(engine === "grok" && binaryPath ? { vendor: grokVendor(versionText, (deps.realpath ?? safeRealpath)(binaryPath)) } : {}),
124
124
  };
125
125
  }
126
+ /** `grok models` → its ids and default; null when the output has no model list in it. */
127
+ export function parseGrokModels(text) {
128
+ const lines = text.split(/\r?\n/);
129
+ const start = lines.findIndex((l) => /^\s*Available models:\s*$/i.test(l));
130
+ if (start < 0)
131
+ return null;
132
+ const models = [];
133
+ for (const line of lines.slice(start + 1)) {
134
+ const m = /^\s*[*-]\s+(\S+)/.exec(line);
135
+ if (m)
136
+ models.push(m[1]);
137
+ else if (line.trim())
138
+ break;
139
+ }
140
+ if (!models.length)
141
+ return null;
142
+ const named = /^\s*Default model:\s*(\S+)/im.exec(text)?.[1] ?? null;
143
+ const starred = lines.slice(start + 1).find((l) => /^\s*\*\s+/.test(l))?.trim().split(/\s+/)[1] ?? null;
144
+ return { models, defaultModel: named ?? starred };
145
+ }
146
+ export function listEngineModels(input, deps = {}) {
147
+ const engine = (["claude", "gemini", "codex", "grok", "generic"].includes(input.clientType ?? "") ? input.clientType : "claude");
148
+ if (engine !== "grok")
149
+ return { clientType: engine, models: null, defaultModel: null, reason: `the ${engine} CLI publishes no model list` };
150
+ const command = parseCommand(input.command);
151
+ const bin = unwrapLauncher(command.bin, command.args).bin || "grok";
152
+ const launch = resolveEngineSpawn(bin, ["models"], deps);
153
+ if (!launch.binaryPath)
154
+ return { clientType: engine, models: null, defaultModel: null, reason: "grok is not installed on this machine" };
155
+ const text = (deps.listText ?? defaultListText)(launch, deps.env ?? process.env);
156
+ const parsed = text ? parseGrokModels(text) : null;
157
+ if (!parsed)
158
+ return { clientType: engine, models: null, defaultModel: null, reason: "`grok models` gave no model list (is the CLI signed in?)" };
159
+ return { clientType: engine, ...parsed };
160
+ }
161
+ function defaultListText(launch, env) {
162
+ const command = launch.via === "direct" && launch.binaryPath ? launch.binaryPath : launch.command;
163
+ const r = spawnSync(command, launch.args, { encoding: "utf8", env, timeout: 20_000, windowsHide: true, windowsVerbatimArguments: launch.windowsVerbatimArguments });
164
+ return r.error || r.status !== 0 ? null : `${r.stdout ?? ""}${r.stderr ?? ""}`;
165
+ }
@@ -0,0 +1,110 @@
1
+ function record(v) {
2
+ return v && typeof v === "object" && !Array.isArray(v) ? v : null;
3
+ }
4
+ function hasFlag(args, flag) {
5
+ return args.some((a) => a === flag || a.startsWith(`${flag}=`));
6
+ }
7
+ /** The value a preset gave `--output-format`, or null. */
8
+ export function grokOutputFormat(args) {
9
+ for (let i = 0; i < args.length; i++) {
10
+ if (args[i] === "--output-format")
11
+ return args[i + 1] ?? null;
12
+ if (args[i].startsWith("--output-format="))
13
+ return args[i].slice("--output-format=".length);
14
+ }
15
+ return null;
16
+ }
17
+ /** The preset's argv with the structured-output flags in front — any order parses, and the trailing `-p` stays last. */
18
+ export function grokStructuredArgs(userArgs) {
19
+ const add = [];
20
+ if (!grokOutputFormat(userArgs))
21
+ add.push("--output-format", "streaming-json");
22
+ if (!hasFlag(userArgs, "--no-auto-update"))
23
+ add.push("--no-auto-update");
24
+ return [...add, ...userArgs];
25
+ }
26
+ /** The text a terminal call produced: its content blocks, else `output_for_prompt` without its `exit: N` header. */
27
+ function toolOutput(ev, raw) {
28
+ const texts = (Array.isArray(ev.content) ? ev.content : []).flatMap((c) => {
29
+ const inner = record(record(c)?.content);
30
+ return typeof inner?.text === "string" ? [inner.text] : [];
31
+ });
32
+ if (texts.some((t) => t.trim()))
33
+ return texts.join("");
34
+ const prompt = typeof raw?.output_for_prompt === "string" ? raw.output_for_prompt : "";
35
+ return prompt.replace(/^exit: -?\d+\n/, "");
36
+ }
37
+ /** A Grok call as the tool the rest of the runner knows: its terminal tool IS a Bash call. */
38
+ function asTool(ev) {
39
+ const rawInput = record(ev.rawInput) ?? {};
40
+ if (ev.kind === "execute" && typeof rawInput.command === "string")
41
+ return { name: "Bash", input: { command: rawInput.command } };
42
+ return { name: typeof ev.toolName === "string" && ev.toolName ? ev.toolName : String(ev.title ?? "tool"), input: rawInput };
43
+ }
44
+ /** One session's adapter: it holds the reply being streamed in deltas until something else happens. */
45
+ export function createGrokEngineAdapter() {
46
+ let text = "";
47
+ const flush = () => {
48
+ const t = text.trim();
49
+ text = "";
50
+ return t ? [{ kind: "assistant_text", text: t }] : [];
51
+ };
52
+ function parseLine(line) {
53
+ let ev;
54
+ try {
55
+ ev = record(JSON.parse(line));
56
+ }
57
+ catch {
58
+ return [];
59
+ }
60
+ if (!ev)
61
+ return [];
62
+ switch (ev.type) {
63
+ case "text":
64
+ if (typeof ev.data === "string")
65
+ text += ev.data;
66
+ return [{ kind: "meta" }];
67
+ case "thought":
68
+ case "usage":
69
+ case "available_commands":
70
+ return [{ kind: "meta" }];
71
+ case "tool_call": {
72
+ const id = typeof ev.toolCallId === "string" ? ev.toolCallId : "";
73
+ const { name, input } = asTool(ev);
74
+ return [...flush(), { kind: "tool_use", block: { type: "tool_use", id, name, input }, id, name, input }];
75
+ }
76
+ case "tool_call_update": {
77
+ // Pending and in-progress updates are the same call filling in; only its END settles it.
78
+ if (ev.status !== "completed" && ev.status !== "failed")
79
+ return [{ kind: "meta" }];
80
+ const id = typeof ev.toolCallId === "string" ? ev.toolCallId : "";
81
+ const raw = record(ev.rawOutput);
82
+ const exitCode = typeof raw?.exit_code === "number" ? raw.exit_code : null;
83
+ const content = toolOutput(ev, raw);
84
+ const block = { type: "tool_result", tool_use_id: id, is_error: ev.status === "failed" || (exitCode !== null && exitCode !== 0), content };
85
+ return [{ kind: "tool_result", block, toolUseId: id, content }];
86
+ }
87
+ case "end": {
88
+ const stop = typeof ev.stopReason === "string" ? ev.stopReason : "";
89
+ const isError = stop !== "end_turn";
90
+ return [...flush(), { kind: "turn_end", raw: ev, isError, result: isError ? `stopped: ${stop || "unknown"}` : "" }];
91
+ }
92
+ case "error": {
93
+ const message = typeof ev.message === "string" ? ev.message : typeof record(ev.error)?.message === "string" ? record(ev.error)?.message : "";
94
+ return message.trim() ? [{ kind: "engine_error", text: message.trim() }] : [{ kind: "meta" }];
95
+ }
96
+ default:
97
+ return [];
98
+ }
99
+ }
100
+ return {
101
+ mode: "stream-json",
102
+ persistent: false,
103
+ prepareArgs: grokStructuredArgs,
104
+ buildLaunchArgs: (userArgs) => [...userArgs],
105
+ buildTurnArgs: (userArgs, turnText) => [...userArgs, turnText],
106
+ parseLine,
107
+ // An older Grok CLI that does not know a flag refuses before it runs the prompt, so one retry raw is safe.
108
+ rejectsStructuredOutput: (line) => /(?:--output-format|--no-auto-update|streaming-json)/.test(line) && /(?:unexpected argument|unknown (?:argument|option)|unrecognized|invalid value|isn't a valid value)/i.test(line),
109
+ };
110
+ }
@@ -15,7 +15,7 @@ const HANDLERS = {
15
15
  claude: { clientType: "claude", cliCommand: "claude --dangerously-skip-permissions", envVar: "ANTHROPIC_API_KEY" },
16
16
  gemini: { clientType: "gemini", cliCommand: "gemini --approval-mode yolo --skip-trust --prompt", envVar: "GEMINI_API_KEY" },
17
17
  codex: { clientType: "codex", cliCommand: "codex exec --json --sandbox danger-full-access", envVar: "OPENAI_API_KEY" },
18
- grok: { clientType: "grok", cliCommand: "grok --permission-mode bypassPermissions -p", envVar: "XAI_API_KEY" },
18
+ grok: { clientType: "grok", cliCommand: "grok --permission-mode bypassPermissions --no-auto-update --output-format streaming-json -p", envVar: "XAI_API_KEY" },
19
19
  generic: { clientType: "generic", cliCommand: "bash", envVar: "" },
20
20
  };
21
21
  export function handlerFor(clientType) {
@@ -601,11 +601,12 @@ export class HeadlessSession {
601
601
  if (this.config.clientType === "grok" && this.grokContinuity && !this.grokConversation)
602
602
  this.grokConversation = newGrokConversation();
603
603
  const grokConversation = this.config.clientType === "grok" && this.grokContinuity ? this.grokConversation : null;
604
+ const presetArgs = this.adapter.prepareArgs?.(this.cmdArgs) ?? this.cmdArgs; // #1187: Grok's structured flags; gone with the adapter if refused
604
605
  const turnArgs = resumedCodexThreadId
605
606
  ? buildCodexResumeArgs(this.cmdArgs, resumedCodexThreadId, text)
606
607
  : grokConversation
607
- ? grokTurnArgs(this.cmdArgs, text, grokConversation)
608
- : this.adapter.buildTurnArgs(this.cmdArgs, text);
608
+ ? grokTurnArgs(presetArgs, text, grokConversation)
609
+ : this.adapter.buildTurnArgs(presetArgs, text);
609
610
  const env = this.spawnEnv;
610
611
  const target = spawnTarget(this.cmdPrefix, this.cmdBin, turnArgs);
611
612
  const launch = resolveEngineSpawn(target.command, target.args, { env });
@@ -5,7 +5,7 @@ import { URL } from "node:url";
5
5
  import { recordRequestFailure } from "./request-failure.js";
6
6
  import { LocalRunner, RunnerInputError } from "./runner.js";
7
7
  import { probeGitSshIdentity } from "./coding/repo.js";
8
- import { checkEngine } from "./coding/engine-check.js";
8
+ import { checkEngine, listEngineModels } from "./coding/engine-check.js";
9
9
  import { listGithubOrgs, listGithubRepos, searchGithubRepos, getGithubRepoDetail, getGithubCredentialScope } from "./coding/github-browse.js";
10
10
  import { routeLocalApplyHandoff } from "./local-apply/handoff-routes.js";
11
11
  import { runNativePreflight } from "./native/preflight.js";
@@ -552,10 +552,10 @@ async function route(runner, req, res) {
552
552
  // Can this machine run an engine — its binary on PATH, its subscription login stored (#879)? Asked
553
553
  // BEFORE a session is launched, so a missing engine is a named refusal rather than a dead pane.
554
554
  // An older runner 404s this, and the cloud reads that as "unverified", never as "missing".
555
- if (req.method === "POST" && path === "/coding/engine-check") {
555
+ if (req.method === "POST" && (path === "/coding/engine-check" || path === "/coding/engine-models")) {
556
556
  const b = await readJson(req);
557
557
  try {
558
- return json(res, 200, checkEngine(b));
558
+ return json(res, 200, path === "/coding/engine-models" ? listEngineModels(b) : checkEngine(b)); // #1199: the CLI's live model list
559
559
  }
560
560
  catch (e) {
561
561
  return json(res, 400, { error: e instanceof Error ? e.message : String(e) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.159",
3
+ "version": "0.4.161",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",