@co0ontty/wand 4.7.1 → 4.8.0

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,6 +1,6 @@
1
1
  {
2
- "commit": "b9e0ce7ddfbbf2b457b63913046f620bac942e54",
3
- "builtAt": "2026-07-16T17:50:19.552Z",
4
- "version": "4.7.1",
2
+ "commit": "8688b5cf30adc66cff2a50d9cfa34a6b1b9a4adc",
3
+ "builtAt": "2026-07-17T00:33:01.780Z",
4
+ "version": "4.8.0",
5
5
  "channel": "stable"
6
6
  }
package/dist/config.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { CardExpandDefaults, ExecutionMode, WandConfig } from "./types.js";
1
+ import { CardExpandDefaults, ExecutionMode, SessionProvider, WandConfig } from "./types.js";
2
2
  import type { WandStorage } from "./storage.js";
3
3
  /**
4
4
  * 通过 UI 设置面板可改的"用户偏好"字段。这些字段从 SQLite app_config 表读取,
@@ -49,5 +49,5 @@ export declare function getProviderDefaultModels(config: Pick<WandConfig, "defau
49
49
  codex: string;
50
50
  opencode: string;
51
51
  };
52
- export declare function getDefaultModelForProvider(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel">, provider: "claude" | "codex" | "opencode" | undefined): string;
52
+ export declare function getDefaultModelForProvider(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel">, provider: SessionProvider | undefined): string;
53
53
  export declare function normalizeMode(input: string | undefined, fallback: ExecutionMode): ExecutionMode;
package/dist/config.js CHANGED
@@ -280,7 +280,7 @@ export function applyStoragePreferences(config, storage) {
280
280
  const defaults = defaultConfig();
281
281
  if (storage.hasPreference(preferenceStorageKey("defaultProvider"))) {
282
282
  const v = storage.getPreference(preferenceStorageKey("defaultProvider"), defaults.defaultProvider ?? "claude");
283
- if (v === "claude" || v === "codex" || v === "opencode")
283
+ if (v === "claude" || v === "codex" || v === "opencode" || v === "grok")
284
284
  config.defaultProvider = v;
285
285
  }
286
286
  if (storage.hasPreference(preferenceStorageKey("defaultSessionKind"))) {
@@ -363,7 +363,7 @@ export function writePreferenceToStorage(config, storage, key, value, options =
363
363
  const dbKey = preferenceStorageKey(key);
364
364
  switch (key) {
365
365
  case "defaultProvider": {
366
- if (value !== "claude" && value !== "codex" && value !== "opencode")
366
+ if (value !== "claude" && value !== "codex" && value !== "opencode" && value !== "grok")
367
367
  throw new Error(`无效 Provider: ${value}`);
368
368
  storage.setPreference(dbKey, value);
369
369
  config.defaultProvider = value;
@@ -628,7 +628,7 @@ function mergeWithDefaults(input) {
628
628
  android: normalizeAndroidApkConfig(input.android) ?? defaults.android,
629
629
  macos: normalizeMacosDmgConfig(input.macos) ?? defaults.macos,
630
630
  cardDefaults: normalizeCardDefaults(input.cardDefaults),
631
- defaultProvider: input.defaultProvider === "codex" || input.defaultProvider === "opencode" ? input.defaultProvider : "claude",
631
+ defaultProvider: input.defaultProvider === "codex" || input.defaultProvider === "opencode" || input.defaultProvider === "grok" ? input.defaultProvider : "claude",
632
632
  defaultSessionKind: input.defaultSessionKind === "pty" ? "pty" : "structured",
633
633
  defaultModel: typeof input.defaultModel === "string" ? input.defaultModel.trim() : defaults.defaultModel,
634
634
  defaultCodexModel: typeof input.defaultCodexModel === "string" ? input.defaultCodexModel.trim() : defaults.defaultCodexModel,
@@ -653,7 +653,7 @@ export function getProviderDefaultModels(config) {
653
653
  }
654
654
  export function getDefaultModelForProvider(config, provider) {
655
655
  const defaults = getProviderDefaultModels(config);
656
- return provider === "codex" ? defaults.codex : provider === "opencode" ? defaults.opencode : defaults.claude;
656
+ return provider === "codex" ? defaults.codex : provider === "opencode" ? defaults.opencode : provider === "grok" ? "" : defaults.claude;
657
657
  }
658
658
  export function normalizeMode(input, fallback) {
659
659
  return isExecutionMode(input) ? input : fallback;
@@ -6,7 +6,7 @@ import path from "node:path";
6
6
  import process from "node:process";
7
7
  import { spawn, spawnSync } from "node:child_process";
8
8
  /** 关键的 CLI 工具,会被诊断输出。 */
9
- const PROBE_COMMANDS = ["claude", "codex", "opencode"];
9
+ const PROBE_COMMANDS = ["claude", "codex", "opencode", "grok"];
10
10
  const DEEP_PROBE_TIMEOUT_MS = 4000;
11
11
  /**
12
12
  * 构造候选 bin 目录列表(按优先级,前面的更可信)。
@@ -27,6 +27,7 @@ function candidateBinDirs() {
27
27
  path.join(home, ".local", "bin"),
28
28
  path.join(home, "bin"),
29
29
  path.join(home, ".bun", "bin"),
30
+ path.join(home, ".grok", "bin"),
30
31
  path.join(home, ".volta", "bin"),
31
32
  path.join(home, ".cargo", "bin"),
32
33
  path.join(home, ".deno", "bin"),
@@ -259,7 +260,8 @@ function probeLoginShell(shell, timeoutMs) {
259
260
  const script = `printf 'PATH\\x1f%s\\n' "$PATH"; ` +
260
261
  `printf 'CLAUDE\\x1f%s\\n' "$(command -v claude 2>/dev/null)"; ` +
261
262
  `printf 'CODEX\\x1f%s\\n' "$(command -v codex 2>/dev/null)"; ` +
262
- `printf 'OPENCODE\\x1f%s\\n' "$(command -v opencode 2>/dev/null)"`;
263
+ `printf 'OPENCODE\\x1f%s\\n' "$(command -v opencode 2>/dev/null)"; ` +
264
+ `printf 'GROK\\x1f%s\\n' "$(command -v grok 2>/dev/null)"`;
263
265
  return new Promise((resolve, reject) => {
264
266
  const child = spawn(shell, ["-l", "-c", script], {
265
267
  env: {
@@ -299,7 +301,7 @@ function probeLoginShell(shell, timeoutMs) {
299
301
  reject(new Error(`login shell exited ${code}: ${stderr.trim().slice(0, 200)}`));
300
302
  return;
301
303
  }
302
- const out = { path: "", claude: null, codex: null, opencode: null };
304
+ const out = { path: "", claude: null, codex: null, opencode: null, grok: null };
303
305
  for (const line of stdout.split("\n")) {
304
306
  const idx = line.indexOf("\x1f");
305
307
  if (idx < 0)
@@ -316,6 +318,8 @@ function probeLoginShell(shell, timeoutMs) {
316
318
  out.codex = val;
317
319
  else if (key === "OPENCODE")
318
320
  out.opencode = val;
321
+ else if (key === "GROK")
322
+ out.grok = val;
319
323
  }
320
324
  resolve(out);
321
325
  });
@@ -22,7 +22,9 @@ import { ProviderHistoryScanner, } from "./provider-history-scanner.js";
22
22
  function resolveProviderFromCommand(command) {
23
23
  if (/^codex\b/.test(command.trim()))
24
24
  return "codex";
25
- return /^opencode\b/.test(command.trim()) ? "opencode" : "claude";
25
+ if (/^opencode\b/.test(command.trim()))
26
+ return "opencode";
27
+ return /^grok\b/.test(command.trim()) ? "grok" : "claude";
26
28
  }
27
29
  /**
28
30
  * Tokenize the restricted shell-command subset accepted by the command
@@ -2010,6 +2012,21 @@ export class ProcessManager extends EventEmitter {
2010
2012
  }
2011
2013
  return result;
2012
2014
  }
2015
+ if (provider === "grok") {
2016
+ let result = command;
2017
+ const trimmedModel = model?.trim();
2018
+ if (trimmedModel && trimmedModel !== "default" && !/--model(?:\s|=)/.test(result) && !/(?:^|\s)-m(?:\s|$)/.test(result)) {
2019
+ result += ` --model '${trimmedModel.replace(/'/g, "'\\''")}'`;
2020
+ }
2021
+ const effort = thinkingEffortToOpenCodeVariant(thinkingEffort ?? null);
2022
+ if (effort && !/--(?:reasoning-)?effort(?:\s|=)/.test(result)) {
2023
+ result += ` --effort '${effort.replace(/'/g, "'\\''")}'`;
2024
+ }
2025
+ if ((mode === "managed" || mode === "full-access" || mode === "auto-edit") && !/--(?:always-approve|yolo)(?:\s|$)/.test(result)) {
2026
+ result += " --always-approve";
2027
+ }
2028
+ return result;
2029
+ }
2013
2030
  const isClaudeCmd = /^(?:claude|npx\s+claude|[^\s]+\/claude)(?:\s|$)/.test(command);
2014
2031
  if (!isClaudeCmd)
2015
2032
  return command;
@@ -10,6 +10,7 @@ import { getErrorMessage } from "./error-utils.js";
10
10
  import { isProviderSessionId } from "./resume-policy.js";
11
11
  import { parseBoundedInteger } from "./request-limits.js";
12
12
  import { asyncRoute } from "./express-async.js";
13
+ import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
13
14
  export function parseExecutionMode(value, fallback) {
14
15
  if (value === undefined)
15
16
  return fallback;
@@ -311,7 +312,7 @@ function isMergeActionAllowed(snapshot) {
311
312
  }
312
313
  export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config, sessions, onSessionCreated) {
313
314
  const sessionResponseDTO = (snapshot) => {
314
- const windowed = windowMessagesForTransport(snapshot.messages ?? [], config.cardDefaults ?? {});
315
+ const windowed = windowMessagesForTransport(enrichStructuredMessages(snapshot.messages ?? []), config.cardDefaults ?? {});
315
316
  return toSessionDetailDTO(snapshot, {
316
317
  messages: windowed.messages,
317
318
  messageOffset: windowed.messageOffset,
@@ -324,11 +325,11 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
324
325
  app.post("/api/structured-sessions", asyncRoute(async (req, res) => {
325
326
  const body = req.body;
326
327
  try {
327
- if (body.provider && body.provider !== "claude" && body.provider !== "codex" && body.provider !== "opencode") {
328
- res.status(400).json({ error: "结构化会话当前仅支持 Claude、Codex 或 OpenCode provider。" });
328
+ if (body.provider && body.provider !== "claude" && body.provider !== "codex" && body.provider !== "opencode" && body.provider !== "grok") {
329
+ res.status(400).json({ error: "结构化会话当前仅支持 Claude、Codex、OpenCodeGrok provider。" });
329
330
  return;
330
331
  }
331
- const provider = body.provider === "codex" || body.provider === "opencode" ? body.provider : "claude";
332
+ const provider = body.provider === "codex" || body.provider === "opencode" || body.provider === "grok" ? body.provider : "claude";
332
333
  const rawModel = typeof body.model === "string" ? body.model.trim() : "";
333
334
  const origin = parseSessionCreationOrigin(body);
334
335
  const snapshot = structured.createSession({
@@ -803,7 +804,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
803
804
  const rawBudget = req.query.blockBudget;
804
805
  if (typeof rawBudget === "string" && /^\d+$/.test(rawBudget) && Number(rawBudget) > 0) {
805
806
  const blockBudget = parseBoundedInteger(rawBudget, 1, 1, 2_000);
806
- const windowed = blockWindowMessagesForTransport(snapshot.messages ?? [], config.cardDefaults ?? {}, blockBudget);
807
+ const windowed = blockWindowMessagesForTransport(enrichStructuredMessages(snapshot.messages ?? []), config.cardDefaults ?? {}, blockBudget);
807
808
  res.json(toSessionDetailDTO(snapshot, {
808
809
  output: transcriptOutput,
809
810
  messages: windowed.messages,
@@ -815,7 +816,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
815
816
  return;
816
817
  }
817
818
  // 与 WS init 对齐:只回最近一窗 turn + offset/total,更早的走 /messages 翻页。
818
- const windowed = windowMessagesForTransport(snapshot.messages ?? [], config.cardDefaults ?? {});
819
+ const windowed = windowMessagesForTransport(enrichStructuredMessages(snapshot.messages ?? []), config.cardDefaults ?? {});
819
820
  res.json(toSessionDetailDTO(snapshot, {
820
821
  output: transcriptOutput,
821
822
  messages: windowed.messages,
@@ -835,7 +836,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
835
836
  res.status(404).json({ error: "未找到该会话,可能已被删除。" });
836
837
  return;
837
838
  }
838
- const all = snapshot.messages ?? [];
839
+ const all = enrichStructuredMessages(snapshot.messages ?? []);
839
840
  const total = all.length;
840
841
  // 块级翻页(iOS):?turn=<i>&blockOffset=<当前 leading 偏移>&blockLimit=<N>
841
842
  // 取该 turn 的 [start, blockOffset) 段(start = max(0, blockOffset - blockLimit))。
@@ -853,8 +854,11 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
853
854
  const blockLimit = Math.min(Math.max(Number.isFinite(rawBlockLimit) ? rawBlockLimit : 40, 1), 200);
854
855
  const blockEnd = Math.min(Math.max(Number.isFinite(rawBlockOffset) ? rawBlockOffset : blockTotal, 0), blockTotal);
855
856
  const blockStart = Math.max(0, blockEnd - blockLimit);
856
- const blocks = sliceTurnBlocksForTransport(turn, blockStart, blockEnd, config.cardDefaults ?? {});
857
- res.json({ turnIndex, blocks, blockOffset: blockStart, blockTotal });
857
+ const blocks = enrichStructuredMessages([{
858
+ ...turn,
859
+ content: sliceTurnBlocksForTransport(turn, blockStart, blockEnd, config.cardDefaults ?? {}),
860
+ }])[0].content;
861
+ res.json({ wandProtocolVersion: WAND_PROTOCOL_VERSION, turnIndex, blocks, blockOffset: blockStart, blockTotal });
858
862
  return;
859
863
  }
860
864
  const rawLimit = parseInt(String(req.query.limit ?? ""), 10);
@@ -862,8 +866,8 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
862
866
  const limit = Math.min(Math.max(Number.isFinite(rawLimit) ? rawLimit : 40, 1), 200);
863
867
  const offset = Math.min(Math.max(Number.isFinite(rawOffset) ? rawOffset : 0, 0), total);
864
868
  const end = Math.min(offset + limit, total);
865
- const slice = truncateMessagesForTransport(all.slice(offset, end), config.cardDefaults ?? {});
866
- res.json({ messages: slice, offset, total });
869
+ const slice = enrichStructuredMessages(truncateMessagesForTransport(all.slice(offset, end), config.cardDefaults ?? {}));
870
+ res.json({ wandProtocolVersion: WAND_PROTOCOL_VERSION, messages: slice, offset, total });
867
871
  });
868
872
  app.post("/api/sessions/:id/resume", (req, res) => {
869
873
  const sessionId = req.params.id;
package/dist/server.js CHANGED
@@ -725,6 +725,7 @@ export async function startServer(config, configPath, options = {}) {
725
725
  { label: "Claude Structured", runner: "claude-cli-print" },
726
726
  { label: "Codex Structured", runner: "codex-cli-exec" },
727
727
  { label: "OpenCode Structured", runner: "opencode-cli-run" },
728
+ { label: "Grok Structured", runner: "grok-cli-headless" },
728
729
  ],
729
730
  structuredChatPersona,
730
731
  cardDefaults: config.cardDefaults,
@@ -950,7 +951,9 @@ export async function startServer(config, configPath, options = {}) {
950
951
  ? "codex"
951
952
  : body.provider === "opencode" || /^opencode\b/.test(body.command.trim())
952
953
  ? "opencode"
953
- : "claude";
954
+ : body.provider === "grok" || /^grok\b/.test(body.command.trim())
955
+ ? "grok"
956
+ : "claude";
954
957
  const effectiveModel = rawModel || getDefaultModelForProvider(config, provider) || undefined;
955
958
  const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
956
959
  const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
@@ -6,6 +6,7 @@ export interface SessionListItemDTO extends SessionBaseDTO {
6
6
  output: "";
7
7
  }
8
8
  export interface SessionDetailDTO extends SessionBaseDTO {
9
+ wandProtocolVersion: number;
9
10
  output: string;
10
11
  outputOffset: number;
11
12
  outputTotal: number;
@@ -1,3 +1,4 @@
1
+ import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
1
2
  export const SESSION_TRANSPORT_OUTPUT_LIMIT = 200_000;
2
3
  /** Explicit allow-list separating the server's session object from its wire DTO. */
3
4
  function sessionBase(snapshot) {
@@ -53,11 +54,12 @@ export function toSessionDetailDTO(snapshot, options = {}) {
53
54
  const outputOffset = Math.max(0, rawOutput.length - outputLimit);
54
55
  return {
55
56
  ...sessionBase(snapshot),
57
+ wandProtocolVersion: WAND_PROTOCOL_VERSION,
56
58
  output: outputOffset > 0 ? rawOutput.slice(outputOffset) : rawOutput,
57
59
  outputOffset,
58
60
  outputTotal: rawOutput.length,
59
61
  outputTruncated: outputOffset > 0,
60
- ...(options.messages !== undefined ? { messages: options.messages } : {}),
62
+ ...(options.messages !== undefined ? { messages: enrichStructuredMessages(options.messages) } : {}),
61
63
  ...(options.messageOffset !== undefined ? { messageOffset: options.messageOffset } : {}),
62
64
  ...(options.messageTotal !== undefined ? { messageTotal: options.messageTotal } : {}),
63
65
  ...(options.leadingBlockOffset !== undefined ? { leadingBlockOffset: options.leadingBlockOffset } : {}),
package/dist/storage.js CHANGED
@@ -169,7 +169,7 @@ function parseQueuedMessages(raw) {
169
169
  return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : undefined;
170
170
  }
171
171
  function inferSessionProvider(row) {
172
- if (row.provider === "claude" || row.provider === "codex" || row.provider === "opencode") {
172
+ if (row.provider === "claude" || row.provider === "codex" || row.provider === "opencode" || row.provider === "grok") {
173
173
  return row.provider;
174
174
  }
175
175
  if (row.runner === "claude-cli" || row.runner === "claude-cli-print") {
@@ -181,10 +181,14 @@ function inferSessionProvider(row) {
181
181
  if (row.runner === "opencode-cli-run") {
182
182
  return "opencode";
183
183
  }
184
+ if (row.runner === "grok-cli-headless")
185
+ return "grok";
184
186
  if (/^codex\b/i.test(row.command.trim()))
185
187
  return "codex";
186
188
  if (/^opencode\b/i.test(row.command.trim()))
187
189
  return "opencode";
190
+ if (/^grok\b/i.test(row.command.trim()))
191
+ return "grok";
188
192
  return /^claude\b/i.test(row.command.trim()) ? "claude" : undefined;
189
193
  }
190
194
  function parseWorktreeInfo(raw) {
@@ -0,0 +1,7 @@
1
+ import type { ConversationTurn } from "./types.js";
2
+ export declare const WAND_PROTOCOL_VERSION = 2;
3
+ /**
4
+ * Add Wand-owned semantics without mutating persisted provider blocks.
5
+ * This is the external interface consumed by every client.
6
+ */
7
+ export declare function enrichStructuredMessages(messages: ConversationTurn[]): ConversationTurn[];
@@ -0,0 +1,168 @@
1
+ export const WAND_PROTOCOL_VERSION = 2;
2
+ function record(value) {
3
+ return value && typeof value === "object" && !Array.isArray(value)
4
+ ? value
5
+ : null;
6
+ }
7
+ function text(value) {
8
+ return typeof value === "string" && value ? value : undefined;
9
+ }
10
+ function arrayValue(value) {
11
+ if (Array.isArray(value))
12
+ return value;
13
+ if (typeof value !== "string" || !value.trimStart().startsWith("["))
14
+ return null;
15
+ try {
16
+ const parsed = JSON.parse(value);
17
+ return Array.isArray(parsed) ? parsed : null;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ function questionsFromInput(input) {
24
+ const rawQuestions = arrayValue(input.questions) ?? [];
25
+ const questions = [];
26
+ for (const rawQuestion of rawQuestions) {
27
+ const question = record(rawQuestion);
28
+ if (!question)
29
+ continue;
30
+ const options = (arrayValue(question.options) ?? []).flatMap((rawOption, index) => {
31
+ const option = record(rawOption);
32
+ if (!option)
33
+ return [];
34
+ return [{
35
+ label: text(option.label) ?? `选项 ${index + 1}`,
36
+ ...(text(option.description) ? { description: text(option.description) } : {}),
37
+ }];
38
+ });
39
+ if (options.length === 0)
40
+ continue;
41
+ questions.push({
42
+ question: text(question.question) ?? "",
43
+ ...(text(question.header) ? { header: text(question.header) } : {}),
44
+ multiSelect: question.multiSelect === true,
45
+ options,
46
+ });
47
+ }
48
+ return questions;
49
+ }
50
+ function toolResultText(block) {
51
+ if (block.type !== "tool_result")
52
+ return "";
53
+ if (typeof block.content === "string")
54
+ return block.content;
55
+ return block.content.map((part) => text(part.text) ?? "").join("");
56
+ }
57
+ function tasksFromSegment(messages, start, end) {
58
+ let latestTodoWrite = null;
59
+ let targetId = null;
60
+ const resultByToolId = new Map();
61
+ for (let i = start; i < end; i++) {
62
+ for (const block of messages[i]?.content ?? []) {
63
+ if (block.type === "tool_result")
64
+ resultByToolId.set(block.tool_use_id, toolResultText(block));
65
+ if (block.type === "tool_use" && ["TodoWrite", "TaskCreate", "TaskUpdate", "TaskList"].includes(block.name)) {
66
+ targetId = block.id;
67
+ if (block.name === "TodoWrite")
68
+ latestTodoWrite = block;
69
+ }
70
+ }
71
+ }
72
+ if (latestTodoWrite) {
73
+ const todos = arrayValue(latestTodoWrite.input.todos) ?? arrayValue(latestTodoWrite.input.plan) ?? [];
74
+ const items = todos.flatMap((rawTodo, index) => {
75
+ const todo = record(rawTodo);
76
+ if (!todo)
77
+ return [];
78
+ return [{
79
+ id: text(todo.id) ?? String(index + 1),
80
+ content: text(todo.content) ?? text(todo.subject) ?? "",
81
+ status: text(todo.status) ?? "pending",
82
+ ...(text(todo.activeForm) ? { activeForm: text(todo.activeForm) } : {}),
83
+ }];
84
+ });
85
+ return { items, targetId: latestTodoWrite.id };
86
+ }
87
+ const tasks = new Map();
88
+ let fallbackId = 0;
89
+ let sawTaskTool = false;
90
+ for (let i = start; i < end; i++) {
91
+ for (const block of messages[i]?.content ?? []) {
92
+ if (block.type !== "tool_use")
93
+ continue;
94
+ if (block.name === "TaskCreate") {
95
+ sawTaskTool = true;
96
+ fallbackId++;
97
+ const match = resultByToolId.get(block.id)?.match(/#([^\s]+)/);
98
+ const id = match?.[1] ?? String(fallbackId);
99
+ tasks.set(id, {
100
+ id,
101
+ content: text(block.input.subject) ?? text(block.input.description) ?? `Task #${id}`,
102
+ status: "pending",
103
+ ...(text(block.input.activeForm) ? { activeForm: text(block.input.activeForm) } : {}),
104
+ });
105
+ }
106
+ else if (block.name === "TaskUpdate") {
107
+ sawTaskTool = true;
108
+ const id = String(block.input.taskId ?? "");
109
+ if (!id)
110
+ continue;
111
+ const previous = tasks.get(id) ?? { id, content: `Task #${id}`, status: "pending" };
112
+ tasks.set(id, {
113
+ ...previous,
114
+ ...(text(block.input.subject) ? { content: text(block.input.subject) } : {}),
115
+ ...(text(block.input.status) ? { status: text(block.input.status) } : {}),
116
+ ...(text(block.input.activeForm) ? { activeForm: text(block.input.activeForm) } : {}),
117
+ });
118
+ }
119
+ }
120
+ }
121
+ return {
122
+ items: sawTaskTool ? [...tasks.values()].filter((task) => task.status !== "deleted") : [],
123
+ targetId,
124
+ };
125
+ }
126
+ /**
127
+ * Add Wand-owned semantics without mutating persisted provider blocks.
128
+ * This is the external interface consumed by every client.
129
+ */
130
+ export function enrichStructuredMessages(messages) {
131
+ const enriched = messages.map((turn) => ({
132
+ ...turn,
133
+ content: turn.content.map((block) => {
134
+ if (block.type !== "tool_use" || block.name !== "AskUserQuestion")
135
+ return block;
136
+ const questions = questionsFromInput(block.input);
137
+ return questions.length > 0
138
+ ? { ...block, semantic: { kind: "question_request", questions } }
139
+ : block;
140
+ }),
141
+ }));
142
+ let segmentStart = 0;
143
+ for (let i = 0; i <= enriched.length; i++) {
144
+ const startsNextSegment = i === enriched.length
145
+ || (i > segmentStart && enriched[i]?.role === "user"
146
+ && enriched[i].content.some((block) => block.type === "text"));
147
+ if (!startsNextSegment)
148
+ continue;
149
+ const { items, targetId } = tasksFromSegment(enriched, segmentStart, i);
150
+ if (targetId && items.length > 0) {
151
+ for (let turnIndex = i - 1; turnIndex >= segmentStart; turnIndex--) {
152
+ const blockIndex = enriched[turnIndex].content.findIndex((block) => block.type === "tool_use" && block.id === targetId);
153
+ if (blockIndex < 0)
154
+ continue;
155
+ const block = enriched[turnIndex].content[blockIndex];
156
+ if (block.type === "tool_use") {
157
+ enriched[turnIndex].content[blockIndex] = {
158
+ ...block,
159
+ semantic: { kind: "task_list", items },
160
+ };
161
+ }
162
+ break;
163
+ }
164
+ }
165
+ segmentStart = i;
166
+ }
167
+ return enriched;
168
+ }
@@ -0,0 +1,12 @@
1
+ import { spawn } from "node:child_process";
2
+ import type { StructuredRunnerAdapter, StructuredRunnerContext, StructuredRunnerExecution, StructuredRunnerObserver, StructuredRunnerTurnState } from "./structured-runner.js";
3
+ import type { SessionSnapshot } from "./types.js";
4
+ export type GrokTurnState = StructuredRunnerTurnState;
5
+ export declare function buildGrokArgs(session: SessionSnapshot, prompt: string): string[];
6
+ /** Apply one official Grok Build `streaming-json` event. */
7
+ export declare function applyGrokEvent(state: GrokTurnState, event: Record<string, unknown>): string | null;
8
+ export declare class GrokRunner implements StructuredRunnerAdapter {
9
+ private readonly spawnProcess;
10
+ constructor(spawnProcess?: typeof spawn);
11
+ start(context: StructuredRunnerContext, observer: StructuredRunnerObserver): StructuredRunnerExecution;
12
+ }
@@ -0,0 +1,156 @@
1
+ import { spawn } from "node:child_process";
2
+ import { thinkingEffortToGrokEffort } from "./structured-provider-common.js";
3
+ function asRecord(value) {
4
+ return value && typeof value === "object" && !Array.isArray(value)
5
+ ? value
6
+ : null;
7
+ }
8
+ export function buildGrokArgs(session, prompt) {
9
+ const args = ["--no-auto-update", "-p", prompt, "--output-format", "streaming-json"];
10
+ const model = session.selectedModel?.trim();
11
+ if (model && model !== "default")
12
+ args.push("--model", model);
13
+ const effort = thinkingEffortToGrokEffort(session.thinkingEffort);
14
+ if (effort)
15
+ args.push("--effort", effort);
16
+ if (session.autoApprovePermissions === true
17
+ || session.mode === "full-access"
18
+ || session.mode === "managed"
19
+ || session.mode === "auto-edit") {
20
+ args.push("--always-approve");
21
+ }
22
+ if (session.claudeSessionId)
23
+ args.push("--resume", session.claudeSessionId);
24
+ return args;
25
+ }
26
+ /** Apply one official Grok Build `streaming-json` event. */
27
+ export function applyGrokEvent(state, event) {
28
+ const type = typeof event.type === "string" ? event.type : "";
29
+ if (type === "text" && typeof event.data === "string" && event.data) {
30
+ const previous = state.blocks.at(-1);
31
+ if (previous?.type === "text")
32
+ previous.text += event.data;
33
+ else
34
+ state.blocks.push({ type: "text", text: event.data });
35
+ state.result += event.data;
36
+ return null;
37
+ }
38
+ if (type === "thought" && typeof event.data === "string" && event.data) {
39
+ const previous = state.blocks.at(-1);
40
+ if (previous?.type === "thinking")
41
+ previous.thinking += event.data;
42
+ else
43
+ state.blocks.push({ type: "thinking", thinking: event.data });
44
+ return null;
45
+ }
46
+ if (type === "end") {
47
+ if (typeof event.sessionId === "string" && event.sessionId)
48
+ state.sessionId = event.sessionId;
49
+ const usage = asRecord(event.usage);
50
+ const modelUsage = asRecord(event.modelUsage);
51
+ const totalCostUsd = typeof event.total_cost_usd === "number"
52
+ ? event.total_cost_usd
53
+ : Object.values(modelUsage ?? {}).reduce((sum, item) => {
54
+ const cost = asRecord(item)?.costUSD;
55
+ return sum + (typeof cost === "number" ? cost : 0);
56
+ }, 0);
57
+ state.usage = {
58
+ inputTokens: typeof usage?.input_tokens === "number" ? usage.input_tokens : 0,
59
+ outputTokens: typeof usage?.output_tokens === "number" ? usage.output_tokens : 0,
60
+ reasoningOutputTokens: typeof usage?.reasoning_tokens === "number" ? usage.reasoning_tokens : 0,
61
+ cacheReadInputTokens: typeof usage?.cache_read_input_tokens === "number" ? usage.cache_read_input_tokens : 0,
62
+ ...(totalCostUsd > 0 ? { totalCostUsd } : {}),
63
+ };
64
+ return null;
65
+ }
66
+ if (type === "error") {
67
+ return typeof event.message === "string" && event.message ? event.message : "Grok failed";
68
+ }
69
+ return null;
70
+ }
71
+ export class GrokRunner {
72
+ spawnProcess;
73
+ constructor(spawnProcess = spawn) {
74
+ this.spawnProcess = spawnProcess;
75
+ }
76
+ start(context, observer) {
77
+ const args = buildGrokArgs(context.session, context.prompt);
78
+ const spawnedAt = new Date().toISOString();
79
+ const child = this.spawnProcess("grok", args, {
80
+ cwd: context.session.cwd,
81
+ env: context.env,
82
+ stdio: ["ignore", "pipe", "pipe"],
83
+ });
84
+ const state = {
85
+ blocks: [],
86
+ result: "",
87
+ sessionId: context.session.claudeSessionId,
88
+ model: context.session.selectedModel ?? context.session.structuredState?.model,
89
+ };
90
+ let lineBuffer = "";
91
+ let stderr = "";
92
+ let primaryError = null;
93
+ let settled = false;
94
+ const finish = (exitCode, signal, spawnError) => ({
95
+ state, exitCode, signal, stderr, primaryError, ...(spawnError ? { spawnError } : {}),
96
+ });
97
+ const processLine = (line) => {
98
+ if (!observer.isActive())
99
+ return;
100
+ const trimmed = line.trim();
101
+ if (!trimmed)
102
+ return;
103
+ try {
104
+ const event = JSON.parse(trimmed);
105
+ observer.onEvent?.(event);
106
+ primaryError = applyGrokEvent(state, event) ?? primaryError;
107
+ observer.onUpdate(state);
108
+ }
109
+ catch { /* Grok diagnostics belong on stderr; ignore non-JSON stdout defensively. */ }
110
+ };
111
+ const completion = new Promise((resolve) => {
112
+ child.stdout?.on("data", (chunk) => {
113
+ if (!observer.isActive())
114
+ return;
115
+ const text = chunk.toString();
116
+ observer.onStdout?.(text);
117
+ lineBuffer += text;
118
+ const lines = lineBuffer.split("\n");
119
+ lineBuffer = lines.pop() ?? "";
120
+ for (const line of lines)
121
+ processLine(line);
122
+ });
123
+ child.stderr?.on("data", (chunk) => {
124
+ if (!observer.isActive())
125
+ return;
126
+ const text = chunk.toString();
127
+ observer.onStderr?.(text);
128
+ stderr += text;
129
+ });
130
+ child.on("error", (error) => {
131
+ if (settled)
132
+ return;
133
+ settled = true;
134
+ resolve(finish(null, null, error));
135
+ });
136
+ child.on("close", (exitCode, signal) => {
137
+ if (settled)
138
+ return;
139
+ settled = true;
140
+ if (lineBuffer.trim())
141
+ processLine(lineBuffer);
142
+ resolve(finish(exitCode, signal));
143
+ });
144
+ });
145
+ return {
146
+ args,
147
+ spawnedAt,
148
+ pid: child.pid ?? null,
149
+ completion,
150
+ interrupt: () => { try {
151
+ child.kill("SIGTERM");
152
+ }
153
+ catch { /* best effort */ } },
154
+ };
155
+ }
156
+ }
@@ -9,3 +9,4 @@ export declare function thinkingEffortToClaudeCliEffort(effort: SessionSnapshot[
9
9
  export declare function thinkingEffortToClaudeSlashEffort(effort: SessionSnapshot["thinkingEffort"]): string;
10
10
  export declare function thinkingEffortToCodexReasoningEffort(effort: SessionSnapshot["thinkingEffort"]): string | null;
11
11
  export declare function thinkingEffortToOpenCodeVariant(effort: SessionSnapshot["thinkingEffort"]): string | null;
12
+ export declare function thinkingEffortToGrokEffort(effort: SessionSnapshot["thinkingEffort"]): string | null;