@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.
- package/dist/build-info.json +3 -3
- package/dist/config.d.ts +2 -2
- package/dist/config.js +4 -4
- package/dist/path-repair.js +7 -3
- package/dist/process-manager.js +18 -1
- package/dist/server-session-routes.js +15 -11
- package/dist/server.js +4 -1
- package/dist/session-transport.d.ts +1 -0
- package/dist/session-transport.js +3 -1
- package/dist/storage.js +5 -1
- package/dist/structured-client-protocol.d.ts +7 -0
- package/dist/structured-client-protocol.js +168 -0
- package/dist/structured-grok-adapter.d.ts +12 -0
- package/dist/structured-grok-adapter.js +156 -0
- package/dist/structured-provider-common.d.ts +1 -0
- package/dist/structured-provider-common.js +16 -1
- package/dist/structured-session-manager.d.ts +3 -0
- package/dist/structured-session-manager.js +165 -7
- package/dist/system-ai.js +2 -0
- package/dist/types.d.ts +30 -2
- package/dist/web-ui/content/scripts.js +42 -42
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/ws-broadcast.js +2 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-07-
|
|
4
|
-
"version": "4.
|
|
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:
|
|
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;
|
package/dist/path-repair.js
CHANGED
|
@@ -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
|
});
|
package/dist/process-manager.js
CHANGED
|
@@ -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
|
-
|
|
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 或
|
|
328
|
+
if (body.provider && body.provider !== "claude" && body.provider !== "codex" && body.provider !== "opencode" && body.provider !== "grok") {
|
|
329
|
+
res.status(400).json({ error: "结构化会话当前仅支持 Claude、Codex、OpenCode 或 Grok 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 =
|
|
857
|
-
|
|
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
|
-
: "
|
|
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;
|
|
@@ -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;
|