@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.
@@ -3,13 +3,17 @@ export function isStructuredRunnerForProvider(provider, runner) {
3
3
  return runner === "claude-sdk" || runner === "claude-cli-print";
4
4
  if (provider === "codex")
5
5
  return runner === "codex-cli-exec";
6
- return runner === "opencode-cli-run";
6
+ if (provider === "opencode")
7
+ return runner === "opencode-cli-run";
8
+ return runner === "grok-cli-headless";
7
9
  }
8
10
  export function defaultStructuredRunner(provider, configuredClaudeRunner = "cli") {
9
11
  if (provider === "codex")
10
12
  return "codex-cli-exec";
11
13
  if (provider === "opencode")
12
14
  return "opencode-cli-run";
15
+ if (provider === "grok")
16
+ return "grok-cli-headless";
13
17
  return configuredClaudeRunner === "sdk" ? "claude-sdk" : "claude-cli-print";
14
18
  }
15
19
  export function resolveStructuredRunner(provider, requestedRunner, configuredClaudeRunner = "cli") {
@@ -75,3 +79,14 @@ export function thinkingEffortToOpenCodeVariant(effort) {
75
79
  return "max";
76
80
  return effort.startsWith("codex:") ? effort.slice("codex:".length) || null : null;
77
81
  }
82
+ export function thinkingEffortToGrokEffort(effort) {
83
+ if (!effort || effort === "off")
84
+ return null;
85
+ if (effort === "standard")
86
+ return "low";
87
+ if (effort === "deep")
88
+ return "high";
89
+ if (effort === "max")
90
+ return "max";
91
+ return effort.startsWith("codex:") ? effort.slice("codex:".length) || null : null;
92
+ }
@@ -8,6 +8,7 @@ export interface StructuredSessionManagerRunners {
8
8
  claudeCli?: StructuredRunnerAdapter;
9
9
  codex?: StructuredRunnerAdapter;
10
10
  opencode?: StructuredRunnerAdapter;
11
+ grok?: StructuredRunnerAdapter;
11
12
  }
12
13
  interface CreateStructuredSessionOptions {
13
14
  cwd: string;
@@ -81,6 +82,7 @@ export declare class StructuredSessionManager {
81
82
  private readonly claudeCliRunner;
82
83
  private readonly codexRunner;
83
84
  private readonly openCodeRunner;
85
+ private readonly grokRunner;
84
86
  private disposed;
85
87
  constructor(storage: WandStorage, config: WandConfig, logger?: SessionLogger | null, sdkQueryFactory?: typeof sdkQuery, runners?: StructuredSessionManagerRunners);
86
88
  private archiveExpiredSessions;
@@ -167,6 +169,7 @@ export declare class StructuredSessionManager {
167
169
  private emit;
168
170
  private incrementApprovalStats;
169
171
  private runCodexStreaming;
172
+ private runGrokStreaming;
170
173
  private runOpenCodeStreaming;
171
174
  /**
172
175
  * Spawn `claude -p --output-format stream-json` and parse NDJSON lines as
@@ -12,7 +12,9 @@ import { normalizeStructuredToolResultContent } from "./structured-content.js";
12
12
  import { buildAppendSystemPromptParts, buildClaudeSdkThinking, ClaudeCliRunner, derivePermissionPolicy, } from "./structured-claude-adapter.js";
13
13
  import { captureTaskMeta, extractClaudeAssistantMessage, extractClaudeModelName, normalizeClaudeToolInput, stampParentTaskResults, stampSelfTask, tagSubagentBlocks, } from "./structured-claude-protocol.js";
14
14
  import { OpenCodeRunner } from "./structured-opencode-adapter.js";
15
+ import { GrokRunner } from "./structured-grok-adapter.js";
15
16
  import { defaultStructuredRunner, defaultStructuredState, isStructuredRunnerForProvider, normalizeThinkingEffort, resolveStructuredRunner, } from "./structured-provider-common.js";
17
+ import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
16
18
  export { isStructuredRunnerForProvider, normalizeThinkingEffort, resolveStructuredRunner, thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToSdkBudget, } from "./structured-provider-common.js";
17
19
  /** The runner already persisted/emitted its detailed terminal snapshot. */
18
20
  class PersistedStructuredRunnerError extends Error {
@@ -89,8 +91,9 @@ function shouldAutoApproveForMode(mode) {
89
91
  }
90
92
  function buildStructuredOutputPayload(snapshot) {
91
93
  return {
94
+ wandProtocolVersion: WAND_PROTOCOL_VERSION,
92
95
  output: snapshot.output,
93
- messages: snapshot.messages,
96
+ messages: snapshot.messages ? enrichStructuredMessages(snapshot.messages) : undefined,
94
97
  queuedMessages: snapshot.queuedMessages,
95
98
  sessionKind: "structured",
96
99
  structuredState: snapshot.structuredState,
@@ -139,12 +142,16 @@ export function isDuplicateStructuredQueueInput(snapshot, input) {
139
142
  }
140
143
  function buildIncrementalStructuredPayload(snapshot, cardDefaults) {
141
144
  const messages = snapshot.messages ?? [];
142
- const lastTurn = messages.length > 0 ? messages[messages.length - 1] : undefined;
145
+ // Derive semantics from the complete current turn before taking the
146
+ // incremental tail; TaskCreate results can live in an earlier provider frame.
147
+ const clientMessages = enrichStructuredMessages(messages);
148
+ const lastTurn = clientMessages.length > 0 ? clientMessages[clientMessages.length - 1] : undefined;
143
149
  // Streaming turn (index 0 here) is preserved verbatim; truncation only kicks
144
150
  // in if the live response is already bigger than the transport threshold,
145
151
  // matching the PTY runner's behaviour in process-manager.ts.
146
152
  const lastMessage = lastTurn ? truncateMessagesForTransport([lastTurn], cardDefaults, 0)[0] : undefined;
147
153
  return {
154
+ wandProtocolVersion: WAND_PROTOCOL_VERSION,
148
155
  incremental: true,
149
156
  queuedMessages: snapshot.queuedMessages,
150
157
  sessionKind: "structured",
@@ -195,6 +202,7 @@ export class StructuredSessionManager {
195
202
  claudeCliRunner;
196
203
  codexRunner;
197
204
  openCodeRunner;
205
+ grokRunner;
198
206
  disposed = false;
199
207
  constructor(storage, config, logger = null, sdkQueryFactory = sdkQuery, runners = {}) {
200
208
  this.storage = storage;
@@ -204,12 +212,13 @@ export class StructuredSessionManager {
204
212
  this.claudeCliRunner = runners.claudeCli ?? new ClaudeCliRunner({ language: () => this.config.language });
205
213
  this.codexRunner = runners.codex ?? new CodexRunner();
206
214
  this.openCodeRunner = runners.opencode ?? new OpenCodeRunner();
215
+ this.grokRunner = runners.grok ?? new GrokRunner();
207
216
  for (const snapshot of this.storage.loadSessions()) {
208
217
  if ((snapshot.sessionKind ?? "pty") !== "structured")
209
218
  continue;
210
219
  const restoredStatus = snapshot.status === "running" ? "idle" : snapshot.status;
211
220
  const storedProvider = snapshot.provider ?? snapshot.structuredState?.provider;
212
- const provider = storedProvider === "codex" || storedProvider === "opencode"
221
+ const provider = storedProvider === "codex" || storedProvider === "opencode" || storedProvider === "grok"
213
222
  ? storedProvider
214
223
  : "claude";
215
224
  const storedRunner = snapshot.runner ?? snapshot.structuredState?.runner;
@@ -489,7 +498,7 @@ export class StructuredSessionManager {
489
498
  const id = randomUUID();
490
499
  const startedAt = new Date().toISOString();
491
500
  const requestedProvider = options.provider ?? "claude";
492
- if (requestedProvider !== "claude" && requestedProvider !== "codex" && requestedProvider !== "opencode") {
501
+ if (requestedProvider !== "claude" && requestedProvider !== "codex" && requestedProvider !== "opencode" && requestedProvider !== "grok") {
493
502
  throw new Error(`不支持的结构化 provider: ${String(requestedProvider)}`);
494
503
  }
495
504
  const provider = requestedProvider;
@@ -511,9 +520,11 @@ export class StructuredSessionManager {
511
520
  ? "codex exec --json"
512
521
  : provider === "opencode"
513
522
  ? "opencode run --format json"
514
- : runner === "claude-sdk"
515
- ? "claude-agent-sdk (stream-json)"
516
- : "claude -p --output-format stream-json",
523
+ : provider === "grok"
524
+ ? "grok -p --output-format streaming-json"
525
+ : runner === "claude-sdk"
526
+ ? "claude-agent-sdk (stream-json)"
527
+ : "claude -p --output-format stream-json",
517
528
  cwd: worktreeSetup?.cwd ?? baseCwd,
518
529
  mode: options.mode,
519
530
  worktreeEnabled: Boolean(worktreeSetup),
@@ -714,6 +725,9 @@ export class StructuredSessionManager {
714
725
  else if (provider === "opencode") {
715
726
  await this.runOpenCodeStreaming(id, updated, prompt, requestId);
716
727
  }
728
+ else if (provider === "grok") {
729
+ await this.runGrokStreaming(id, updated, prompt, requestId);
730
+ }
717
731
  else if (runner === "claude-sdk") {
718
732
  await this.runClaudeSdkStreaming(id, updated, prompt, requestId);
719
733
  }
@@ -1306,6 +1320,150 @@ export class StructuredSessionManager {
1306
1320
  }
1307
1321
  setImmediate(() => { void this.flushNextQueuedMessage(sessionId); });
1308
1322
  }
1323
+ async runGrokStreaming(sessionId, session, prompt, requestId) {
1324
+ let emitTimer = null;
1325
+ const syncSnapshot = (turnState) => {
1326
+ const current = this.currentSessionForRequest(sessionId, requestId);
1327
+ if (!current)
1328
+ return;
1329
+ const turn = {
1330
+ role: "assistant",
1331
+ content: this.compactContentBlocks([...turnState.blocks], turnState.result),
1332
+ usage: turnState.usage,
1333
+ };
1334
+ const messages = [...(current.messages ?? [])];
1335
+ if (messages.at(-1)?.role === "assistant")
1336
+ messages[messages.length - 1] = turn;
1337
+ else
1338
+ messages.push(turn);
1339
+ const patched = {
1340
+ ...current,
1341
+ claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
1342
+ messages,
1343
+ output: turnState.result || current.output,
1344
+ };
1345
+ this.sessions.set(sessionId, patched);
1346
+ this.saveStreamingSnapshot(patched);
1347
+ };
1348
+ const flushEmit = () => {
1349
+ if (emitTimer)
1350
+ this.clearStreamEmitTimer(emitTimer);
1351
+ emitTimer = null;
1352
+ const current = this.currentSessionForRequest(sessionId, requestId);
1353
+ if (current)
1354
+ this.emit({
1355
+ type: "output",
1356
+ sessionId,
1357
+ data: buildIncrementalStructuredPayload(current, this.config.cardDefaults ?? {}),
1358
+ });
1359
+ };
1360
+ const scheduleEmit = () => {
1361
+ if (!emitTimer)
1362
+ emitTimer = this.trackStreamEmitTimer(setTimeout(flushEmit, STREAM_EMIT_DEBOUNCE_MS));
1363
+ };
1364
+ const execution = this.grokRunner.start({
1365
+ session,
1366
+ prompt,
1367
+ env: buildChildEnv(this.config.inheritEnv !== false),
1368
+ }, {
1369
+ isActive: () => this.isCurrentRequest(sessionId, requestId),
1370
+ onStdout: (text) => this.logger?.appendStructuredStdout(sessionId, text),
1371
+ onStderr: (text) => this.logger?.appendStructuredStderr(sessionId, text),
1372
+ onEvent: (event) => this.logger?.appendStreamEvent(sessionId, event),
1373
+ onUpdate: (turnState) => { syncSnapshot(turnState); scheduleEmit(); },
1374
+ });
1375
+ this.pendingRunnerExecutions.set(sessionId, execution);
1376
+ this.logger?.appendStructuredSpawn(sessionId, {
1377
+ kind: "grok-headless",
1378
+ provider: "grok",
1379
+ pid: execution.pid,
1380
+ cwd: session.cwd,
1381
+ args: execution.args,
1382
+ prompt: prompt.slice(0, 2048),
1383
+ promptLength: prompt.length,
1384
+ sessionId: session.claudeSessionId,
1385
+ spawnedAt: execution.spawnedAt,
1386
+ });
1387
+ let result;
1388
+ try {
1389
+ result = await execution.completion;
1390
+ }
1391
+ finally {
1392
+ const released = this.releasePendingRunnerExecution(sessionId, execution);
1393
+ if (released)
1394
+ this.cancelStreamingCheckpointTimer(sessionId);
1395
+ }
1396
+ if (!this.isCurrentRequest(sessionId, requestId)) {
1397
+ if (emitTimer)
1398
+ this.clearStreamEmitTimer(emitTimer);
1399
+ return;
1400
+ }
1401
+ flushEmit();
1402
+ if (result.spawnError) {
1403
+ const hint = result.spawnError.code === "ENOENT"
1404
+ ? "(PATH 中找不到 grok;请安装 Grok Build CLI,或重跑 `wand service:install` 刷新服务 PATH)"
1405
+ : "";
1406
+ throw new Error(`grok 启动失败:${result.spawnError.message}${hint}`);
1407
+ }
1408
+ this.logger?.appendStructuredSpawn(sessionId, {
1409
+ kind: "grok-headless-close",
1410
+ pid: execution.pid,
1411
+ spawnedAt: execution.spawnedAt,
1412
+ closedAt: new Date().toISOString(),
1413
+ exitCode: result.exitCode,
1414
+ stderrTail: result.stderr.slice(-2048),
1415
+ primaryError: result.primaryError,
1416
+ });
1417
+ const current = this.currentSessionForRequest(sessionId, requestId);
1418
+ if (!current)
1419
+ return;
1420
+ const interruptedByUser = this.interruptedWith.has(sessionId);
1421
+ const interruptPrompt = this.interruptedWith.get(sessionId);
1422
+ if ((result.primaryError || (result.exitCode !== 0 && result.exitCode !== null) || result.signal) && !interruptedByUser) {
1423
+ const errorText = this.formatStructuredExitError("grok", result.exitCode, result.signal, { stderr: result.stderr, primary: result.primaryError });
1424
+ const failed = this.finishStructuredFailure(current, typeof result.exitCode === "number" ? result.exitCode : 1, errorText, result.state);
1425
+ this.sessions.set(sessionId, failed);
1426
+ this.saveAuthoritativeSession(failed);
1427
+ this.emitStructuredSnapshot(failed);
1428
+ this.emitStructuredSnapshot(failed, "ended");
1429
+ throw new PersistedStructuredRunnerError(errorText);
1430
+ }
1431
+ const messages = this.buildCompletedAssistantMessages(current, result.state);
1432
+ const keepRunning = !!interruptPrompt;
1433
+ const finished = {
1434
+ ...current,
1435
+ status: keepRunning ? "running" : "idle",
1436
+ exitCode: keepRunning ? null : 0,
1437
+ endedAt: keepRunning ? null : new Date().toISOString(),
1438
+ output: result.state.result,
1439
+ claudeSessionId: result.state.sessionId ?? current.claudeSessionId,
1440
+ messages,
1441
+ queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
1442
+ pendingEscalation: null,
1443
+ permissionBlocked: false,
1444
+ structuredState: {
1445
+ ...current.structuredState,
1446
+ model: result.state.model ?? current.structuredState?.model,
1447
+ inFlight: false,
1448
+ activeRequestId: null,
1449
+ lastError: null,
1450
+ },
1451
+ };
1452
+ this.sessions.set(sessionId, finished);
1453
+ this.saveAuthoritativeSession(finished);
1454
+ this.emitStructuredSnapshot(finished);
1455
+ if (!keepRunning)
1456
+ this.emitStructuredSnapshot(finished, "ended");
1457
+ if (interruptPrompt) {
1458
+ this.interruptedWith.delete(sessionId);
1459
+ this.preserveQueueOnInterrupt.delete(sessionId);
1460
+ setImmediate(() => this.sendMessage(sessionId, interruptPrompt).catch((error) => {
1461
+ console.error("[WAND] grok interrupt-and-send failed:", error);
1462
+ }));
1463
+ return;
1464
+ }
1465
+ setImmediate(() => { void this.flushNextQueuedMessage(sessionId); });
1466
+ }
1309
1467
  async runOpenCodeStreaming(sessionId, session, prompt, requestId) {
1310
1468
  let emitTimer = null;
1311
1469
  const syncSnapshot = (turnState) => {
package/dist/system-ai.js CHANGED
@@ -99,6 +99,8 @@ export function discoverCliSystemAiConfig(preferred, home = os.homedir()) {
99
99
  const discoverers = { claude: discoverClaude, codex: discoverCodex, opencode: discoverOpenCode };
100
100
  const order = [preferred ?? "claude", "claude", "opencode", "codex"];
101
101
  for (const provider of [...new Set(order)]) {
102
+ if (provider === "grok")
103
+ continue;
102
104
  const found = discoverers[provider](home);
103
105
  if (found)
104
106
  return found;
package/dist/types.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type SessionKind = "pty" | "structured";
2
- export type SessionProvider = "claude" | "codex" | "opencode";
2
+ export type SessionProvider = "claude" | "codex" | "opencode" | "grok";
3
3
  export type CommitAiSource = "cli" | "api";
4
- export type SessionRunner = "claude-cli" | "claude-cli-print" | "claude-sdk" | "codex-cli-exec" | "opencode-cli-run" | "pty";
4
+ export type SessionRunner = "claude-cli" | "claude-cli-print" | "claude-sdk" | "codex-cli-exec" | "opencode-cli-run" | "grok-cli-headless" | "pty";
5
5
  export type SessionSource = "interactive" | "automation" | "startup";
6
6
  export type ExecutionMode = "assist" | "agent" | "agent-max" | "default" | "auto-edit" | "full-access" | "native" | "managed";
7
7
  export type AutonomyPolicy = "assist" | "agent" | "agent-max";
@@ -392,12 +392,40 @@ export interface ThinkingBlock {
392
392
  thinking: string;
393
393
  __subagent?: SubagentMeta;
394
394
  }
395
+ export interface StructuredQuestionOption {
396
+ label: string;
397
+ description?: string;
398
+ }
399
+ export interface StructuredQuestion {
400
+ question: string;
401
+ header?: string;
402
+ multiSelect: boolean;
403
+ options: StructuredQuestionOption[];
404
+ }
405
+ export interface StructuredTaskItem {
406
+ id: string;
407
+ content: string;
408
+ status: string;
409
+ activeForm?: string;
410
+ }
411
+ /**
412
+ * Wand-owned semantic projection of provider-specific tools. Clients should
413
+ * render this field and treat `name` / `input` as a legacy fallback only.
414
+ */
415
+ export type ToolUseSemantic = {
416
+ kind: "question_request";
417
+ questions: StructuredQuestion[];
418
+ } | {
419
+ kind: "task_list";
420
+ items: StructuredTaskItem[];
421
+ };
395
422
  export interface ToolUseBlock {
396
423
  type: "tool_use";
397
424
  id: string;
398
425
  name: string;
399
426
  description?: string;
400
427
  input: Record<string, unknown>;
428
+ semantic?: ToolUseSemantic;
401
429
  __subagent?: SubagentMeta;
402
430
  }
403
431
  export interface ToolResultBlock {