@proagentstore/cli 0.4.153 → 0.4.154

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,11 +1,38 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { unwrapLauncher } from "./launcher-wrapper.js";
3
- import { statSync } from "node:fs";
3
+ import { realpathSync, statSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { handlerFor } from "./handlers.js";
7
7
  import { parseCommand } from "./headless.js";
8
8
  import { resolveEngineSpawn } from "./engine-spawn.js";
9
+ function safeRealpath(p) {
10
+ try {
11
+ return realpathSync(p);
12
+ }
13
+ catch {
14
+ return p;
15
+ }
16
+ }
17
+ function defaultVersionText(launch, env) {
18
+ // The file the check FOUND, not the bare name: off Windows `command` is the name as given, which spawnSync would
19
+ // resolve against this process's PATH rather than the one being checked — and report another binary's version.
20
+ const command = launch.via === "direct" && launch.binaryPath ? launch.binaryPath : launch.command;
21
+ const r = spawnSync(command, launch.args, { encoding: "utf8", env, timeout: 5_000, windowsHide: true, windowsVerbatimArguments: launch.windowsVerbatimArguments });
22
+ return r.error || r.status !== 0 ? null : `${r.stdout ?? ""}${r.stderr ?? ""}`;
23
+ }
24
+ /** `codex-cli 0.146.0`, `2.1.3 (Claude Code)`, `grok 1.0.62 (1474f9cbdd91)` → the version. Null when there is none. */
25
+ export function parseEngineVersion(text) {
26
+ return text?.match(/\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/)?.[0] ?? null;
27
+ }
28
+ /** Whose `grok` answered: xAI's names itself; the community npm one prints a bare version from a node_modules script. */
29
+ export function grokVendor(text, binaryPath) {
30
+ if (text && /^\s*grok \d+\.\d+/m.test(text))
31
+ return "xai";
32
+ if (/[\\/]node_modules[\\/]|@vibe-kit/.test(binaryPath ?? ""))
33
+ return "community";
34
+ return "unknown";
35
+ }
9
36
  function defaultExists(p) {
10
37
  try {
11
38
  statSync(p);
@@ -52,6 +79,14 @@ export function detectEngineLogin(engine, deps = {}) {
52
79
  }
53
80
  return { login: "missing", where: file };
54
81
  }
82
+ if (engine === "grok") {
83
+ // xAI's Grok CLI keeps its subscription login (grok.com / accounts.x.ai) in auth.json under GROK_HOME, default
84
+ // ~/.grok. An external auth provider or OIDC setup signs in another way, so absence is not proof there.
85
+ if (env.GROK_AUTH_PROVIDER_COMMAND?.trim() || env.GROK_OIDC_ISSUER?.trim() || env.GROK_DEPLOYMENT_KEY?.trim())
86
+ return { login: "unknown" };
87
+ const file = join(env.GROK_HOME?.trim() || join(home, ".grok"), "auth.json");
88
+ return { login: exists(file) ? "found" : "missing", where: file };
89
+ }
55
90
  if (engine === "gemini") {
56
91
  // Google sign-in lands here; Gemini also accepts other auth (gcloud, Vertex), so absence is
57
92
  // not proof of "signed out".
@@ -73,6 +108,9 @@ export function checkEngine(input, deps = {}) {
73
108
  const bin = parsed.bin || parseCommand(handlerFor(engine).cliCommand).bin || engine;
74
109
  const { binaryPath, via } = resolveEngineSpawn(bin, [], deps);
75
110
  const login = input.credentialInjected ? { login: "found", where: "platform" } : detectEngineLogin(engine, deps);
111
+ // Grok's version is what tells xAI's CLI from a same-named npm package, so it is always read for Grok (a native
112
+ // binary, milliseconds); the others only when asked (coding_engine_get), because each is a Node startup.
113
+ const versionText = binaryPath && (engine === "grok" || input.withVersion) ? (deps.versionText ?? defaultVersionText)(resolveEngineSpawn(bin, ["--version"], deps), deps.env ?? process.env) : null;
76
114
  return {
77
115
  checked: true,
78
116
  engine,
@@ -81,5 +119,7 @@ export function checkEngine(input, deps = {}) {
81
119
  ...(binaryPath ? { binaryPath, binaryVia: via } : {}),
82
120
  login: login.login,
83
121
  ...(login.where ? { loginWhere: login.where } : {}),
122
+ ...(binaryPath && (engine === "grok" || input.withVersion) ? { version: parseEngineVersion(versionText) } : {}),
123
+ ...(engine === "grok" && binaryPath ? { vendor: grokVendor(versionText, (deps.realpath ?? safeRealpath)(binaryPath)) } : {}),
84
124
  };
85
125
  }
@@ -1,5 +1,23 @@
1
1
  import { accessSync, constants, readFileSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
2
3
  import { posix, win32 } from "node:path";
4
+ /**
5
+ * xAI's Grok CLI for a bare `grok`, wherever PATH points (#1156). Its installer (x.ai/cli/install.sh, install.ps1) always
6
+ * puts it in `~/.grok/bin` (`GROK_BIN_DIR` overrides), and adds that to PATH only in the SHELL's profile — so a
7
+ * `pags up` started by launchd, systemd, a Scheduled Task or an older terminal does not see it. And an npm package of
8
+ * the same name, the community `@vibe-kit/grok-cli` (API-key billed, different flags), can sit earlier on PATH: on the
9
+ * Mac mini it did, in nvm's bin. The preset means xAI's CLI, so its own location wins when it is there. An explicit path
10
+ * in a preset is the owner's choice and is never rewritten.
11
+ */
12
+ export function officialEngineBinary(bin, deps = {}) {
13
+ if (bin !== "grok")
14
+ return null;
15
+ const windows = (deps.platform ?? process.platform) === "win32";
16
+ const env = deps.env ?? process.env;
17
+ const dir = envVar(env, "GROK_BIN_DIR", windows)?.trim() || (windows ? win32 : posix).join(deps.home ?? homedir(), ".grok", "bin");
18
+ const file = (windows ? win32 : posix).join(dir, windows ? "grok.exe" : "grok");
19
+ return (deps.isExecutable ?? defaultIsExecutable)(file) ? file : null;
20
+ }
3
21
  function defaultIsExecutable(p) {
4
22
  try {
5
23
  if (!statSync(p).isFile())
@@ -67,7 +85,8 @@ export function quoteCmdArg(arg) {
67
85
  a = `"${a}"`;
68
86
  return a.replace(CMD_META, "^$1").replace(CMD_META, "^$1");
69
87
  }
70
- export function resolveEngineSpawn(bin, args, deps = {}) {
88
+ export function resolveEngineSpawn(requested, args, deps = {}) {
89
+ const bin = officialEngineBinary(requested, deps) ?? requested;
71
90
  const windows = (deps.platform ?? process.platform) === "win32";
72
91
  if (!windows)
73
92
  return { command: bin, args, binaryPath: resolveBinary(bin, deps), via: "direct" };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Grok keeps ITS OWN conversation across one-shot turns (#1156) — by an id the runner chooses.
3
+ *
4
+ * Every Grok turn is a new `grok … -p "<turn>"` process, so without this each turn started with no memory of the last
5
+ * one but the platform's replay. `-c/--continue` was the CLI's only offer when the preset was written, and it picks the
6
+ * MOST RECENT session in the directory — another `grok` run in the same checkout (the owner's own) would answer instead
7
+ * (`coding-engines.ts`, "NO PRESET RESUMES"). xAI's Grok CLI now takes an explicit id, so continuity is exact:
8
+ *
9
+ * first turn `-s <uuid>` a NEW conversation under an id the runner made up (1.x: must be a UUID, must not exist);
10
+ * later turns `-r <uuid>` resume exactly that one (errors if it does not exist — never "the most recent").
11
+ *
12
+ * Checked against `grok --help` of xAI's Grok CLI 1.0.62 (2026-10-18). 0.2.x also accepts both: there `-s` creates-or-
13
+ * resumes and `-r` errors on an unknown id. NOT yet exercised end to end — the CLI refuses every turn until it is signed
14
+ * in, and it was not on the machine this was written on — so the runner treats it as unproven: a turn whose flags are
15
+ * rejected is retried once without them and the session runs without continuity, and the platform's replay is still
16
+ * sent with every turn (`holdsConversationThisTurn` stays false for Grok) until a signed-in run proves it.
17
+ */
18
+ import { randomUUID } from "node:crypto";
19
+ const UUID_RE = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i;
20
+ /** Only a UUID ever reaches argv — anything else read back from the state file is discarded. */
21
+ export function isGrokConversationId(value) {
22
+ return typeof value === "string" && UUID_RE.test(value);
23
+ }
24
+ export function newGrokConversation(makeId = randomUUID) {
25
+ return { id: makeId(), started: false };
26
+ }
27
+ /** The turn's argv: the continuity flag first (any order parses), then the preset, then the turn as `-p`'s value. */
28
+ export function grokTurnArgs(userArgs, turnText, conversation) {
29
+ if (!conversation)
30
+ return [...userArgs, turnText];
31
+ return [conversation.started ? "-r" : "-s", conversation.id, ...userArgs, turnText];
32
+ }
33
+ /**
34
+ * The CLI refused the continuity flags themselves — an argument error, printed before it signs in or runs anything (the
35
+ * argument parser runs first). Narrow on purpose: retrying any other failure could run a real instruction twice.
36
+ */
37
+ // Only a line that BEGINS with the CLI's own `error:` (clap's argument errors), and only about these flags or the id —
38
+ // a tool's output in a failed turn that merely mentions a session must never cause a same-turn retry.
39
+ const REJECTED_RE = /^\s*error:\s(?:.*unexpected argument '?-(?:s|r)\b|.*(?:--session-id|--resume)\b|.*must be a valid uuid|.*session\b.*\b(?:not found|already exists|does not exist))/i;
40
+ export function grokRejectedContinuity(lines) {
41
+ return lines.some((l) => REJECTED_RE.test(l));
42
+ }
43
+ /** What a finished turn means for the conversation. Pure. */
44
+ export function afterGrokTurn(conversation, outcome) {
45
+ // The flags themselves were refused before anything ran: run without continuity from now on.
46
+ if (outcome.code && outcome.rejected)
47
+ return { next: null, persist: null, disable: true };
48
+ // Done: the CLI holds the conversation now. Remember it, so the next turn — and the next runner start — resume it.
49
+ if (outcome.code === 0)
50
+ return { next: { ...conversation, started: true }, persist: conversation.id, disable: false };
51
+ // Any other failure: the conversation may be half-made or unknown. Forget it, so the next turn starts a clean one.
52
+ return { next: null, persist: null, disable: false };
53
+ }
@@ -5,7 +5,7 @@ export function isCodexThreadId(value) {
5
5
  return typeof value === "string" && /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(value);
6
6
  }
7
7
  function stateKey(id, engine) {
8
- return engine === "codex" ? `codex:${id}` : id;
8
+ return engine === "claude" ? id : `${engine}:${id}`;
9
9
  }
10
10
  function loadFile(path) {
11
11
  if (!path || !existsSync(path))
@@ -14,6 +14,7 @@ import { handlerFor } from "./handlers.js";
14
14
  import { resolveEngineAuth } from "./engine-auth.js";
15
15
  import { buildCodexResumeArgs, engineAdapterFor, engineInvocationModeFromAdapter, engineInvocationWarning, genericRawEngineAdapter } from "./engine-adapter.js";
16
16
  import { isCodexThreadId, readCodexState, readState, writeState } from "./headless-state.js";
17
+ import { afterGrokTurn, grokRejectedContinuity, grokTurnArgs, isGrokConversationId, newGrokConversation } from "./grok-continuity.js";
17
18
  export { buildClaudeArgs } from "./engine-adapter.js";
18
19
  export { defaultStatePath } from "./headless-state.js";
19
20
  /**
@@ -70,6 +71,10 @@ export class HeadlessSession {
70
71
  claudeSessionId = null;
71
72
  /** Codex's thread id from `thread.started` — used only by the explicit-ID resume path (#848). */
72
73
  codexThreadId = null;
74
+ /** Grok's own conversation across one-shot turns (#1156, grok-continuity.ts); null = the next turn starts one. */
75
+ grokConversation = null;
76
+ /** Off once this machine's CLI refused the continuity flags: the session then runs as before, turn by turn. */
77
+ grokContinuity = true;
73
78
  /**
74
79
  * The cloud's context brief, until the first turn spends it (ADR 0005, #693). Null once
75
80
  * delivered, and null from the start when the engine resumed its own conversation.
@@ -270,6 +275,8 @@ export class HeadlessSession {
270
275
  config.clientType === "codex" && this.mode === "stream-json" && !config.fresh
271
276
  ? readCodexState(config.statePath, config.id) ?? (config.resumeFrom ? readCodexState(config.statePath, config.resumeFrom) : null)
272
277
  : null;
278
+ const storedGrok = config.clientType === "grok" && !config.fresh ? (readState(config.statePath, config.id, "grok") ?? (config.resumeFrom ? readState(config.statePath, config.resumeFrom, "grok") : null)) : null;
279
+ this.grokConversation = isGrokConversationId(storedGrok) ? { id: storedGrok, started: true } : null;
273
280
  // AFTER both lines above, because `resumedConversation` reads them: the brief is the fallback,
274
281
  // so an engine that found its own conversation drops it unread rather than being handed a
275
282
  // summary of the conversation it is already in.
@@ -590,9 +597,15 @@ export class HeadlessSession {
590
597
  this.structuredOutputRejected = false;
591
598
  const resumedCodexThreadId = this.codexResumeThreadId;
592
599
  const inputId = this.turnInputId;
600
+ // #1156: Grok carries its conversation by an id this runner chose — `-s` to make it, `-r` to resume it.
601
+ if (this.config.clientType === "grok" && this.grokContinuity && !this.grokConversation)
602
+ this.grokConversation = newGrokConversation();
603
+ const grokConversation = this.config.clientType === "grok" && this.grokContinuity ? this.grokConversation : null;
593
604
  const turnArgs = resumedCodexThreadId
594
605
  ? buildCodexResumeArgs(this.cmdArgs, resumedCodexThreadId, text)
595
- : this.adapter.buildTurnArgs(this.cmdArgs, text);
606
+ : grokConversation
607
+ ? grokTurnArgs(this.cmdArgs, text, grokConversation)
608
+ : this.adapter.buildTurnArgs(this.cmdArgs, text);
596
609
  const env = this.spawnEnv;
597
610
  const target = spawnTarget(this.cmdPrefix, this.cmdBin, turnArgs);
598
611
  const launch = resolveEngineSpawn(target.command, target.args, { env });
@@ -687,6 +700,19 @@ export class HeadlessSession {
687
700
  this.runOneShot(text);
688
701
  return;
689
702
  }
703
+ if (grokConversation) {
704
+ const after = afterGrokTurn(grokConversation, { code, rejected: grokRejectedContinuity(this.turnTail) });
705
+ this.grokConversation = after.next;
706
+ writeState(this.config.statePath, this.config.id, after.persist, "grok");
707
+ // The flags were refused before anything ran (an argument error): the same turn again, without them.
708
+ if (after.disable) {
709
+ this.grokContinuity = false;
710
+ this.proc = null;
711
+ this.push("[grok CLI rejected its session flags; retrying this turn without conversation continuity]");
712
+ this.runOneShot(text);
713
+ return;
714
+ }
715
+ }
690
716
  // A rejected/expired explicit ID is not retried as the same turn: the CLI may have started
691
717
  // work before failing. Drop only that known-bad local key so the NEXT turn is safely fresh.
692
718
  if (code && resumedCodexThreadId && this.codexThreadId === resumedCodexThreadId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.153",
3
+ "version": "0.4.154",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",