@co0ontty/wand 2.4.1 → 2.4.2

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.
@@ -60,16 +60,90 @@ export function thinkingEffortToClaudeCliEffort(effort) {
60
60
  export function thinkingEffortToClaudeSlashEffort(effort) {
61
61
  return thinkingEffortToClaudeCliEffort(effort) ?? "auto";
62
62
  }
63
- /** Codex CLI 用:把 thinkingEffort 映射到 model_reasoning_effort 配置。off → minimal。 */
63
+ /** Codex CLI 用:把 thinkingEffort 映射到 model_reasoning_effort 配置。off → 不覆盖 Codex 默认。 */
64
64
  export function thinkingEffortToCodexReasoningEffort(effort) {
65
65
  switch (effort) {
66
66
  case "standard": return "low";
67
67
  case "deep": return "medium";
68
68
  case "max": return "xhigh";
69
- case "off": return "minimal";
69
+ case "off": return null;
70
70
  default: return null;
71
71
  }
72
72
  }
73
+ function asRecord(value) {
74
+ return value && typeof value === "object" && !Array.isArray(value)
75
+ ? value
76
+ : null;
77
+ }
78
+ function getString(value) {
79
+ return typeof value === "string" ? value : "";
80
+ }
81
+ function parseJsonRecord(value) {
82
+ if (asRecord(value))
83
+ return value;
84
+ if (typeof value !== "string" || !value.trim())
85
+ return {};
86
+ try {
87
+ const parsed = JSON.parse(value);
88
+ return asRecord(parsed) ?? {};
89
+ }
90
+ catch {
91
+ return {};
92
+ }
93
+ }
94
+ function codexPatchToolName(kind) {
95
+ if (kind === "add")
96
+ return "Write";
97
+ return "Edit";
98
+ }
99
+ function codexPatchResultText(stdout, stderr, success) {
100
+ const err = getString(stderr).trim();
101
+ const out = getString(stdout).trim();
102
+ if (!success)
103
+ return err || out || "patch apply failed";
104
+ return "";
105
+ }
106
+ export function buildCodexPatchApplyBlocks(item) {
107
+ const changes = asRecord(item.changes);
108
+ if (!changes)
109
+ return [];
110
+ const callId = getString(item.call_id) || getString(item.id) || "patch";
111
+ const status = getString(item.status) || "completed";
112
+ const success = item.success !== false && status !== "failed";
113
+ const resultText = codexPatchResultText(item.stdout, item.stderr, success);
114
+ const entries = Object.entries(changes);
115
+ const blocks = [];
116
+ entries.forEach(([filePath, rawChange], index) => {
117
+ const change = asRecord(rawChange) ?? {};
118
+ const kind = getString(change.type) || "update";
119
+ const unifiedDiff = getString(change.unified_diff);
120
+ const movePath = getString(change.move_path);
121
+ const toolUseId = `${callId}#${index}`;
122
+ const input = {
123
+ file_path: filePath,
124
+ kind,
125
+ status,
126
+ };
127
+ if (unifiedDiff)
128
+ input.unified_diff = unifiedDiff;
129
+ if (movePath)
130
+ input.move_path = movePath;
131
+ blocks.push({
132
+ type: "tool_use",
133
+ id: toolUseId,
134
+ name: codexPatchToolName(kind),
135
+ description: kind,
136
+ input,
137
+ });
138
+ blocks.push({
139
+ type: "tool_result",
140
+ tool_use_id: toolUseId,
141
+ content: resultText,
142
+ is_error: !success,
143
+ });
144
+ });
145
+ return blocks;
146
+ }
73
147
  function captureTaskMeta(blocks, registry) {
74
148
  for (const b of blocks) {
75
149
  if (b.type !== "tool_use")
@@ -1128,7 +1202,7 @@ export class StructuredSessionManager {
1128
1202
  if (modelChoice && modelChoice !== "default") {
1129
1203
  args.push("--model", modelChoice);
1130
1204
  }
1131
- // 思考深度 → model_reasoning_effort(off → minimal,standard → low,deep → medium,max → xhigh)
1205
+ // 思考深度 → model_reasoning_effort(off → 不覆盖,standard → low,deep → medium,max → xhigh)
1132
1206
  // Newer Codex CLI versions removed the old dedicated exec flag, but still
1133
1207
  // accept config overrides through `-c`.
1134
1208
  const reasoningEffort = thinkingEffortToCodexReasoningEffort(session.thinkingEffort);
@@ -1241,48 +1315,62 @@ export class StructuredSessionManager {
1241
1315
  return;
1242
1316
  }
1243
1317
  this.logger?.appendStreamEvent(sessionId, parsed);
1244
- if (parsed?.type === "thread.started" && typeof parsed.thread_id === "string") {
1245
- turnState.sessionId = parsed.thread_id;
1318
+ const event = this.unwrapCodexStreamEvent(parsed);
1319
+ if (event?.type === "thread.started" && typeof event.thread_id === "string") {
1320
+ turnState.sessionId = event.thread_id;
1246
1321
  syncSnapshot();
1247
1322
  return;
1248
1323
  }
1249
- if (parsed?.type === "item.started" && parsed.item) {
1250
- this.applyCodexItem(turnState, parsed.item, "started");
1324
+ if (event?.type === "item.started" && asRecord(event.item)) {
1325
+ this.applyCodexItem(turnState, event.item, "started");
1251
1326
  syncSnapshot();
1252
1327
  scheduleEmit();
1253
1328
  return;
1254
1329
  }
1255
- if (parsed?.type === "item.updated" && parsed.item) {
1330
+ if (event?.type === "item.updated" && asRecord(event.item)) {
1256
1331
  // codex `item.updated` 重新发送完整 ThreadItem(不是 delta)。
1257
1332
  // 对 text/thinking/TodoWrite 走 codexBlockIndex 替换;对 tool_use
1258
1333
  // 仍然按现有 id 复用,避免重复卡片。
1259
- this.applyCodexItem(turnState, parsed.item, "updated");
1334
+ this.applyCodexItem(turnState, event.item, "updated");
1335
+ syncSnapshot();
1336
+ scheduleEmit();
1337
+ return;
1338
+ }
1339
+ if (event?.type === "item.completed" && asRecord(event.item)) {
1340
+ this.applyCodexItem(turnState, event.item, "completed");
1341
+ syncSnapshot();
1342
+ scheduleEmit();
1343
+ return;
1344
+ }
1345
+ if (event?.type === "turn.completed") {
1346
+ turnState.usage = this.extractCodexUsage(asRecord(event.usage) ?? undefined) ?? turnState.usage;
1260
1347
  syncSnapshot();
1261
1348
  scheduleEmit();
1262
1349
  return;
1263
1350
  }
1264
- if (parsed?.type === "item.completed" && parsed.item) {
1265
- this.applyCodexItem(turnState, parsed.item, "completed");
1351
+ if (event?.type === "token_count") {
1352
+ const info = asRecord(event.info);
1353
+ const lastUsage = asRecord(info?.last_token_usage);
1354
+ turnState.usage = this.extractCodexUsage(lastUsage ?? undefined) ?? turnState.usage;
1266
1355
  syncSnapshot();
1267
1356
  scheduleEmit();
1268
1357
  return;
1269
1358
  }
1270
- if (parsed?.type === "turn.completed") {
1271
- turnState.usage = this.extractCodexUsage(parsed.usage) ?? turnState.usage;
1359
+ if (this.applyCodexLooseEvent(turnState, event)) {
1272
1360
  syncSnapshot();
1273
1361
  scheduleEmit();
1274
1362
  return;
1275
1363
  }
1276
- if (parsed?.type === "error") {
1277
- const message = typeof parsed.message === "string" ? parsed.message : "";
1364
+ if (event?.type === "error") {
1365
+ const message = typeof event.message === "string" ? event.message : "";
1278
1366
  if (message)
1279
1367
  codexErrors.push(message);
1280
1368
  return;
1281
1369
  }
1282
- if (parsed?.type === "turn.failed") {
1283
- const errObj = (parsed.error && typeof parsed.error === "object") ? parsed.error : null;
1370
+ if (event?.type === "turn.failed") {
1371
+ const errObj = (event.error && typeof event.error === "object") ? event.error : null;
1284
1372
  const message = (errObj && typeof errObj.message === "string" && errObj.message)
1285
- || (typeof parsed.message === "string" ? parsed.message : "")
1373
+ || (typeof event.message === "string" ? event.message : "")
1286
1374
  || "codex turn failed";
1287
1375
  codexTurnFailed = message;
1288
1376
  return;
@@ -2661,6 +2749,128 @@ export class StructuredSessionManager {
2661
2749
  }
2662
2750
  return typeof content === "undefined" || content === null ? "" : String(content);
2663
2751
  }
2752
+ unwrapCodexStreamEvent(parsed) {
2753
+ const event = asRecord(parsed);
2754
+ if (!event)
2755
+ return null;
2756
+ const type = getString(event.type);
2757
+ if ((type === "response_item" || type === "event_msg") && asRecord(event.payload)) {
2758
+ return event.payload;
2759
+ }
2760
+ return event;
2761
+ }
2762
+ applyCodexLooseEvent(turnState, event) {
2763
+ if (!event)
2764
+ return false;
2765
+ const type = getString(event.type);
2766
+ const supported = new Set([
2767
+ "message",
2768
+ "agent_message",
2769
+ "reasoning",
2770
+ "function_call",
2771
+ "function_call_output",
2772
+ "custom_tool_call",
2773
+ "custom_tool_call_output",
2774
+ "patch_apply_end",
2775
+ "mcp_tool_call_end",
2776
+ "web_search_call",
2777
+ "web_search_end",
2778
+ "tool_search_call",
2779
+ "tool_search_output",
2780
+ ]);
2781
+ if (!supported.has(type))
2782
+ return false;
2783
+ this.applyCodexItem(turnState, event, "completed");
2784
+ return true;
2785
+ }
2786
+ codexFunctionToolUse(item) {
2787
+ const rawName = getString(item.name) || "function_call";
2788
+ const callId = getString(item.call_id) || getString(item.id) || rawName;
2789
+ const args = parseJsonRecord(item.arguments);
2790
+ const input = { ...args };
2791
+ if (rawName === "exec_command") {
2792
+ const command = getString(args.cmd) || getString(args.command);
2793
+ if (command)
2794
+ input.command = command;
2795
+ return {
2796
+ type: "tool_use",
2797
+ id: callId,
2798
+ name: "Bash",
2799
+ description: getString(args.workdir) || undefined,
2800
+ input,
2801
+ };
2802
+ }
2803
+ if (rawName === "write_stdin") {
2804
+ return {
2805
+ type: "tool_use",
2806
+ id: callId,
2807
+ name: "Bash",
2808
+ description: "write stdin",
2809
+ input: {
2810
+ ...input,
2811
+ command: `write_stdin ${getString(args.session_id) || getString(args.sessionId) || ""}`.trim(),
2812
+ },
2813
+ };
2814
+ }
2815
+ if (rawName === "update_plan" && Array.isArray(args.plan)) {
2816
+ const todos = args.plan.map((entry) => {
2817
+ const rec = asRecord(entry) ?? {};
2818
+ const status = getString(rec.status);
2819
+ return {
2820
+ content: getString(rec.step),
2821
+ activeForm: getString(rec.step),
2822
+ status: status === "completed" ? "completed" : status === "in_progress" ? "in_progress" : "pending",
2823
+ };
2824
+ });
2825
+ return {
2826
+ type: "tool_use",
2827
+ id: callId,
2828
+ name: "TodoWrite",
2829
+ description: getString(args.explanation) || undefined,
2830
+ input: { todos },
2831
+ };
2832
+ }
2833
+ if (rawName === "view_image") {
2834
+ const filePath = getString(args.path);
2835
+ return {
2836
+ type: "tool_use",
2837
+ id: callId,
2838
+ name: "Read",
2839
+ description: "view image",
2840
+ input: filePath ? { ...input, file_path: filePath } : input,
2841
+ };
2842
+ }
2843
+ if (rawName === "js") {
2844
+ return {
2845
+ type: "tool_use",
2846
+ id: callId,
2847
+ name: "node_repl__js",
2848
+ description: getString(args.title) || undefined,
2849
+ input,
2850
+ };
2851
+ }
2852
+ return {
2853
+ type: "tool_use",
2854
+ id: callId,
2855
+ name: rawName,
2856
+ input,
2857
+ };
2858
+ }
2859
+ codexMcpToolBlocks(item) {
2860
+ const callId = getString(item.call_id) || getString(item.id) || "mcp";
2861
+ const invocation = asRecord(item.invocation) ?? {};
2862
+ const server = getString(invocation.server) || "mcp";
2863
+ const tool = getString(invocation.tool) || "tool";
2864
+ const args = asRecord(invocation.arguments) ?? {};
2865
+ const result = asRecord(item.result);
2866
+ const isError = !!result?.Err || getString(item.status) === "failed";
2867
+ const ok = asRecord(result?.Ok);
2868
+ const content = ok ? this.extractCodexText(ok.content) || JSON.stringify(ok).slice(0, 4096) : this.extractCodexText(result);
2869
+ return [
2870
+ { type: "tool_use", id: callId, name: `${server}__${tool}`, input: args },
2871
+ { type: "tool_result", tool_use_id: callId, content, is_error: isError },
2872
+ ];
2873
+ }
2664
2874
  extractCodexText(value) {
2665
2875
  if (typeof value === "string")
2666
2876
  return value;
@@ -2752,6 +2962,13 @@ export class StructuredSessionManager {
2752
2962
  extractCodexItemBlock(item, completed) {
2753
2963
  const id = typeof item.id === "string" ? item.id : randomUUID();
2754
2964
  const type = typeof item.type === "string" ? item.type : "unknown";
2965
+ if (type === "message") {
2966
+ const role = getString(item.role);
2967
+ if (role !== "assistant")
2968
+ return [];
2969
+ const text = this.extractCodexText(item.content);
2970
+ return text ? [{ type: "text", text }] : [];
2971
+ }
2755
2972
  if (type === "agent_message") {
2756
2973
  const text = this.extractCodexText(item);
2757
2974
  return text ? [{ type: "text", text }] : [];
@@ -2800,6 +3017,43 @@ export class StructuredSessionManager {
2800
3017
  },
2801
3018
  ];
2802
3019
  }
3020
+ if (type === "function_call") {
3021
+ const block = this.codexFunctionToolUse(item);
3022
+ return block ? [block] : [];
3023
+ }
3024
+ if (type === "function_call_output") {
3025
+ const callId = getString(item.call_id) || id;
3026
+ return [{
3027
+ type: "tool_result",
3028
+ tool_use_id: callId,
3029
+ content: this.normalizeToolResultContent(item.output),
3030
+ }];
3031
+ }
3032
+ if (type === "custom_tool_call") {
3033
+ const callId = getString(item.call_id) || id;
3034
+ const name = getString(item.name) || "custom_tool_call";
3035
+ return [{
3036
+ type: "tool_use",
3037
+ id: callId,
3038
+ name,
3039
+ description: getString(item.status) || undefined,
3040
+ input: {
3041
+ input: getString(item.input),
3042
+ status: getString(item.status) || (completed ? "completed" : "in_progress"),
3043
+ },
3044
+ }];
3045
+ }
3046
+ if (type === "custom_tool_call_output") {
3047
+ const callId = getString(item.call_id) || id;
3048
+ return [{
3049
+ type: "tool_result",
3050
+ tool_use_id: callId,
3051
+ content: this.normalizeToolResultContent(item.output),
3052
+ }];
3053
+ }
3054
+ if (type === "patch_apply_end") {
3055
+ return buildCodexPatchApplyBlocks(item);
3056
+ }
2803
3057
  if (type === "file_change") {
2804
3058
  // 注意:codex exec stream 没有 old_string/new_string——只给 path + kind。
2805
3059
  // 这里每个 file 一个 sub-id(`${item.id}#${i}`),这样如果 codex 一次给多
@@ -2822,9 +3076,8 @@ export class StructuredSessionManager {
2822
3076
  input = { file_path: path, content: "", kind, status };
2823
3077
  }
2824
3078
  else if (kind === "delete") {
2825
- // 复用 Bash 终端卡,rm 语义直观
2826
- toolName = "Bash";
2827
- input = { command: `rm ${path}`, description: `delete ${path}`, kind, status };
3079
+ toolName = "Edit";
3080
+ input = { file_path: path, kind, status };
2828
3081
  }
2829
3082
  else {
2830
3083
  toolName = "Edit";
@@ -2845,6 +3098,9 @@ export class StructuredSessionManager {
2845
3098
  });
2846
3099
  return blocks;
2847
3100
  }
3101
+ if (type === "mcp_tool_call_end") {
3102
+ return this.codexMcpToolBlocks(item);
3103
+ }
2848
3104
  if (type === "mcp_tool_call") {
2849
3105
  const server = typeof item.server === "string" ? item.server : "mcp";
2850
3106
  const tool = typeof item.tool === "string" ? item.tool : "tool";
@@ -2887,6 +3143,55 @@ export class StructuredSessionManager {
2887
3143
  },
2888
3144
  ];
2889
3145
  }
3146
+ if (type === "web_search_call") {
3147
+ const callId = getString(item.call_id) || id;
3148
+ return [{
3149
+ type: "tool_use",
3150
+ id: callId,
3151
+ name: "WebSearch",
3152
+ description: getString(item.status) || "searching",
3153
+ input: {},
3154
+ }];
3155
+ }
3156
+ if (type === "web_search_end") {
3157
+ const callId = getString(item.call_id) || id;
3158
+ const action = asRecord(item.action);
3159
+ const query = getString(item.query);
3160
+ const actionType = getString(action?.type);
3161
+ return [
3162
+ {
3163
+ type: "tool_use",
3164
+ id: callId,
3165
+ name: "WebSearch",
3166
+ description: actionType || "completed",
3167
+ input: query ? { query, action: actionType } : { action: actionType },
3168
+ },
3169
+ {
3170
+ type: "tool_result",
3171
+ tool_use_id: callId,
3172
+ content: query ? `query: ${query}` : "",
3173
+ },
3174
+ ];
3175
+ }
3176
+ if (type === "tool_search_call") {
3177
+ const callId = getString(item.call_id) || id;
3178
+ const args = asRecord(item.arguments) ?? {};
3179
+ return [{
3180
+ type: "tool_use",
3181
+ id: callId,
3182
+ name: "tool_search",
3183
+ description: getString(item.status) || undefined,
3184
+ input: args,
3185
+ }];
3186
+ }
3187
+ if (type === "tool_search_output") {
3188
+ const callId = getString(item.call_id) || id;
3189
+ return [{
3190
+ type: "tool_result",
3191
+ tool_use_id: callId,
3192
+ content: this.normalizeToolResultContent(item.tools),
3193
+ }];
3194
+ }
2890
3195
  if (type === "web_search") {
2891
3196
  const query = typeof item.query === "string" ? item.query : "";
2892
3197
  const action = item.action && typeof item.action === "object" ? item.action : null;
package/dist/types.d.ts CHANGED
@@ -102,6 +102,8 @@ export interface WandConfig {
102
102
  cardDefaults?: CardExpandDefaults;
103
103
  /** 新建会话时默认使用的 Claude 模型(别名或完整 ID)。留空则不传 --model,由 claude 自行决定。 */
104
104
  defaultModel?: string;
105
+ /** 新建 Codex 会话时默认使用的模型。留空则不传 --model,由 codex 自行决定。 */
106
+ defaultCodexModel?: string;
105
107
  /** 新建会话时默认使用的思考深度。 */
106
108
  defaultThinkingEffort?: "off" | "standard" | "deep" | "max";
107
109
  /** 结构化会话使用的 runner: "cli"(默认,spawn claude -p)或 "sdk"(@anthropic-ai/claude-agent-sdk)。 */
@@ -244,7 +246,7 @@ export interface CommandRequest {
244
246
  mode?: ExecutionMode;
245
247
  initialInput?: string;
246
248
  worktreeEnabled?: boolean;
247
- /** Claude 模型(别名或完整 ID)。仅对 claude provider 生效。留空则回落到 config.defaultModel。 */
249
+ /** 模型(别名或完整 ID)。留空则按 provider 回落到服务端默认模型。 */
248
250
  model?: string;
249
251
  /** 创建会话时由前端测得的真实列数。后端用它直接 spawn PTY,避免"先 120 列再 resize"的早期错位。 */
250
252
  cols?: number;