@co0ontty/wand 2.12.0 → 2.13.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/server.js CHANGED
@@ -37,6 +37,7 @@ import { renderApp } from "./web-ui/index.js";
37
37
  import { WsBroadcastManager } from "./ws-broadcast.js";
38
38
  import { checkRateLimit, recordFailedLogin, resetRateLimit } from "./middleware/rate-limit.js";
39
39
  import { isPathWithinBase, isBlockedFolderPath, normalizeFolderPath } from "./middleware/path-safety.js";
40
+ import { checkProviderCliUpdates, updateProviderClis, verifyProviderCliUpdateResults, } from "./provider-cli-updater.js";
40
41
  const execAsync = promisify(exec);
41
42
  const SERVER_MODULE_DIR = path.dirname(new URL(import.meta.url).pathname);
42
43
  const RUNTIME_ROOT_DIR = path.resolve(SERVER_MODULE_DIR, "..");
@@ -1108,6 +1109,15 @@ export async function startServer(config, configPath) {
1108
1109
  const processes = new ProcessManager(config, storage, configDir);
1109
1110
  const structuredLogger = new SessionLogger(configDir, config.shortcutLogMaxBytes);
1110
1111
  const structuredSessions = new StructuredSessionManager(storage, config, structuredLogger);
1112
+ let providerCliUpdateCache = null;
1113
+ let providerCliUpdateInFlight = false;
1114
+ let updateInFlight = false;
1115
+ const refreshProviderCliUpdates = async () => {
1116
+ const items = await checkProviderCliUpdates({ inheritEnv: config.inheritEnv !== false });
1117
+ const result = { items, checkedAt: new Date().toISOString() };
1118
+ providerCliUpdateCache = result;
1119
+ return result;
1120
+ };
1111
1121
  const useHttps = config.https === true;
1112
1122
  const protocol = useHttps ? "https" : "http";
1113
1123
  const requireAuth = buildRequireAuth(useHttps, storage, config);
@@ -1359,6 +1369,7 @@ export async function startServer(config, configPath) {
1359
1369
  defaultCwd: config.defaultCwd,
1360
1370
  defaultModel: defaultModels.claude,
1361
1371
  defaultCodexModel: defaultModels.codex,
1372
+ defaultOpenCodeModel: defaultModels.opencode,
1362
1373
  defaultModels,
1363
1374
  defaultThinkingEffort: config.defaultThinkingEffort ?? "off",
1364
1375
  commandPresets: config.commandPresets,
@@ -1366,6 +1377,7 @@ export async function startServer(config, configPath) {
1366
1377
  structuredRunners: [
1367
1378
  { label: "Claude Structured", runner: "claude-cli-print" },
1368
1379
  { label: "Codex Structured", runner: "codex-cli-exec" },
1380
+ { label: "OpenCode Structured", runner: "opencode-cli-run" },
1369
1381
  ],
1370
1382
  structuredChatPersona,
1371
1383
  cardDefaults: config.cardDefaults,
@@ -1527,6 +1539,7 @@ export async function startServer(config, configPath) {
1527
1539
  ...safeConfig,
1528
1540
  defaultModel: defaultModels.claude,
1529
1541
  defaultCodexModel: defaultModels.codex,
1542
+ defaultOpenCodeModel: defaultModels.opencode,
1530
1543
  defaultModels,
1531
1544
  },
1532
1545
  hasCert: existsSync(certPaths.keyPath) && existsSync(certPaths.certPath),
@@ -1543,6 +1556,7 @@ export async function startServer(config, configPath) {
1543
1556
  web: storage.getConfigValue("autoUpdateWeb") === "true",
1544
1557
  apk: storage.getConfigValue("autoUpdateApk") === "true",
1545
1558
  dmg: storage.getConfigValue("autoUpdateDmg") === "true",
1559
+ cli: storage.getConfigValue("autoUpdateProviderClis") === "true",
1546
1560
  },
1547
1561
  androidApk: {
1548
1562
  enabled: config.android?.enabled === true,
@@ -1713,6 +1727,10 @@ export async function startServer(config, configPath) {
1713
1727
  writePreferenceToStorage(config, storage, "defaultCodexModel", modelDefaults.codex);
1714
1728
  touchedPreferenceField = true;
1715
1729
  }
1730
+ if (Object.prototype.hasOwnProperty.call(modelDefaults, "opencode")) {
1731
+ writePreferenceToStorage(config, storage, "defaultOpenCodeModel", modelDefaults.opencode);
1732
+ touchedPreferenceField = true;
1733
+ }
1716
1734
  }
1717
1735
  catch (err) {
1718
1736
  res.status(400).json({ error: getErrorMessage(err, "默认模型配置校验失败") });
@@ -1748,6 +1766,7 @@ export async function startServer(config, configPath) {
1748
1766
  ...safeConfig,
1749
1767
  defaultModel: defaultModels.claude,
1750
1768
  defaultCodexModel: defaultModels.codex,
1769
+ defaultOpenCodeModel: defaultModels.opencode,
1751
1770
  defaultModels,
1752
1771
  },
1753
1772
  restartRequired: touchedDeployField,
@@ -1763,10 +1782,13 @@ export async function startServer(config, configPath) {
1763
1782
  res.json({
1764
1783
  models: cached.models,
1765
1784
  codexModels: cached.codexModels,
1785
+ opencodeModels: cached.opencodeModels,
1766
1786
  claudeVersion: cached.claudeVersion,
1787
+ opencodeVersion: cached.opencodeVersion,
1767
1788
  refreshedAt: cached.refreshedAt,
1768
1789
  defaultModel: defaultModels.claude,
1769
1790
  defaultCodexModel: defaultModels.codex,
1791
+ defaultOpenCodeModel: defaultModels.opencode,
1770
1792
  defaultModels,
1771
1793
  });
1772
1794
  });
@@ -1777,10 +1799,13 @@ export async function startServer(config, configPath) {
1777
1799
  res.json({
1778
1800
  models: refreshed.models,
1779
1801
  codexModels: refreshed.codexModels,
1802
+ opencodeModels: refreshed.opencodeModels,
1780
1803
  claudeVersion: refreshed.claudeVersion,
1804
+ opencodeVersion: refreshed.opencodeVersion,
1781
1805
  refreshedAt: refreshed.refreshedAt,
1782
1806
  defaultModel: defaultModels.claude,
1783
1807
  defaultCodexModel: defaultModels.codex,
1808
+ defaultOpenCodeModel: defaultModels.opencode,
1784
1809
  defaultModels,
1785
1810
  });
1786
1811
  }
@@ -1788,6 +1813,46 @@ export async function startServer(config, configPath) {
1788
1813
  res.status(500).json({ error: getErrorMessage(error, "刷新模型列表失败。") });
1789
1814
  }
1790
1815
  });
1816
+ app.get("/api/provider-cli-updates", async (req, res) => {
1817
+ try {
1818
+ const force = req.query.refresh === "1" || !providerCliUpdateCache;
1819
+ const data = force ? await refreshProviderCliUpdates() : providerCliUpdateCache;
1820
+ res.json({
1821
+ ...data,
1822
+ updating: providerCliUpdateInFlight,
1823
+ autoUpdate: storage.getConfigValue("autoUpdateProviderClis") === "true",
1824
+ });
1825
+ }
1826
+ catch (error) {
1827
+ res.status(500).json({ error: getErrorMessage(error, "检查 CLI 更新失败。") });
1828
+ }
1829
+ });
1830
+ app.post("/api/provider-cli-updates", express.json(), async (req, res) => {
1831
+ if (providerCliUpdateInFlight || updateInFlight) {
1832
+ res.status(409).json({ error: "CLI 更新正在进行中,请稍候。" });
1833
+ return;
1834
+ }
1835
+ const rawIds = Array.isArray(req.body?.ids) ? req.body.ids : [];
1836
+ const ids = rawIds.filter((value) => value === "claude" || value === "codex" || value === "opencode");
1837
+ providerCliUpdateInFlight = true;
1838
+ try {
1839
+ const before = await refreshProviderCliUpdates();
1840
+ const commandResults = await updateProviderClis(before.items, ids.length ? ids : undefined, {
1841
+ inheritEnv: config.inheritEnv !== false,
1842
+ onLog: (line) => process.stdout.write(`[wand] ${line}\n`),
1843
+ });
1844
+ const after = await refreshProviderCliUpdates();
1845
+ const results = verifyProviderCliUpdateResults(commandResults, after.items);
1846
+ refreshModels().catch(() => { });
1847
+ res.json({ ok: results.every((item) => item.ok), results, ...after, autoUpdate: storage.getConfigValue("autoUpdateProviderClis") === "true" });
1848
+ }
1849
+ catch (error) {
1850
+ res.status(500).json({ error: getErrorMessage(error, "更新 CLI 失败。") });
1851
+ }
1852
+ finally {
1853
+ providerCliUpdateInFlight = false;
1854
+ }
1855
+ });
1791
1856
  app.post("/api/settings/upload-cert", async (req, res) => {
1792
1857
  const { key, cert } = req.body;
1793
1858
  if (!key || !cert) {
@@ -1837,9 +1902,8 @@ export async function startServer(config, configPath) {
1837
1902
  res.status(500).json({ error: getErrorMessage(error, "检查更新失败。") });
1838
1903
  }
1839
1904
  });
1840
- let updateInFlight = false;
1841
1905
  app.post("/api/update", async (_req, res) => {
1842
- if (updateInFlight) {
1906
+ if (updateInFlight || providerCliUpdateInFlight) {
1843
1907
  res.status(409).json({ error: "更新正在进行中,请稍候。" });
1844
1908
  return;
1845
1909
  }
@@ -2354,7 +2418,11 @@ export async function startServer(config, configPath) {
2354
2418
  try {
2355
2419
  const origin = parseSessionCreationOrigin(body);
2356
2420
  const rawModel = typeof body.model === "string" ? body.model.trim() : "";
2357
- const provider = body.provider === "codex" || /^codex\b/.test(body.command.trim()) ? "codex" : "claude";
2421
+ const provider = body.provider === "codex" || /^codex\b/.test(body.command.trim())
2422
+ ? "codex"
2423
+ : body.provider === "opencode" || /^opencode\b/.test(body.command.trim())
2424
+ ? "opencode"
2425
+ : "claude";
2358
2426
  const effectiveModel = rawModel || getDefaultModelForProvider(config, provider) || undefined;
2359
2427
  const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
2360
2428
  const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
@@ -2545,10 +2613,11 @@ export async function startServer(config, configPath) {
2545
2613
  const web = storage.getConfigValue("autoUpdateWeb") === "true";
2546
2614
  const apk = storage.getConfigValue("autoUpdateApk") === "true";
2547
2615
  const dmg = storage.getConfigValue("autoUpdateDmg") === "true";
2548
- res.json({ web, apk, dmg });
2616
+ const cli = storage.getConfigValue("autoUpdateProviderClis") === "true";
2617
+ res.json({ web, apk, dmg, cli });
2549
2618
  });
2550
2619
  app.post("/api/auto-update", express.json(), (req, res) => {
2551
- const { web, apk, dmg } = req.body;
2620
+ const { web, apk, dmg, cli } = req.body;
2552
2621
  if (typeof web === "boolean") {
2553
2622
  storage.setConfigValue("autoUpdateWeb", String(web));
2554
2623
  }
@@ -2558,10 +2627,14 @@ export async function startServer(config, configPath) {
2558
2627
  if (typeof dmg === "boolean") {
2559
2628
  storage.setConfigValue("autoUpdateDmg", String(dmg));
2560
2629
  }
2630
+ if (typeof cli === "boolean") {
2631
+ storage.setConfigValue("autoUpdateProviderClis", String(cli));
2632
+ }
2561
2633
  res.json({
2562
2634
  web: storage.getConfigValue("autoUpdateWeb") === "true",
2563
2635
  apk: storage.getConfigValue("autoUpdateApk") === "true",
2564
2636
  dmg: storage.getConfigValue("autoUpdateDmg") === "true",
2637
+ cli: storage.getConfigValue("autoUpdateProviderClis") === "true",
2565
2638
  });
2566
2639
  });
2567
2640
  // ── Update channel (stable / beta) ──
@@ -2660,7 +2733,35 @@ export async function startServer(config, configPath) {
2660
2733
  });
2661
2734
  }
2662
2735
  }
2736
+ async function performProviderCliAutoUpdate() {
2737
+ if (storage.getConfigValue("autoUpdateProviderClis") !== "true" || providerCliUpdateInFlight || updateInFlight)
2738
+ return;
2739
+ providerCliUpdateInFlight = true;
2740
+ try {
2741
+ const before = await refreshProviderCliUpdates();
2742
+ const available = before.items.filter((item) => item.updateAvailable && item.updateSupported);
2743
+ if (!available.length)
2744
+ return;
2745
+ const commandResults = await updateProviderClis(before.items, available.map((item) => item.id), {
2746
+ inheritEnv: config.inheritEnv !== false,
2747
+ onLog: (line) => process.stdout.write(`[wand] ${line}\n`),
2748
+ });
2749
+ const after = await refreshProviderCliUpdates();
2750
+ const results = verifyProviderCliUpdateResults(commandResults, after.items);
2751
+ for (const result of results) {
2752
+ process.stdout.write(`[wand] CLI 自动更新 ${result.ok ? "完成" : "失败"}: ${result.message}\n`);
2753
+ }
2754
+ refreshModels().catch(() => { });
2755
+ }
2756
+ catch (error) {
2757
+ process.stdout.write(`[wand] CLI 自动更新失败: ${getErrorMessage(error)}\n`);
2758
+ }
2759
+ finally {
2760
+ providerCliUpdateInFlight = false;
2761
+ }
2762
+ }
2663
2763
  let updateCheckTimer = null;
2764
+ let providerCliUpdateTimer = null;
2664
2765
  if (updateChecksEnabled) {
2665
2766
  // Background update check on startup
2666
2767
  performAutoUpdate().catch(() => { });
@@ -2669,6 +2770,15 @@ export async function startServer(config, configPath) {
2669
2770
  performAutoUpdate().catch(() => { });
2670
2771
  }, 30 * 60 * 1000);
2671
2772
  updateCheckTimer.unref();
2773
+ // 与 Wand 自身 npm 更新错峰,避免两个全局 updater 同时改 PATH / bin 链接。
2774
+ providerCliUpdateTimer = setTimeout(() => {
2775
+ performProviderCliAutoUpdate().catch(() => { });
2776
+ providerCliUpdateTimer = setInterval(() => {
2777
+ performProviderCliAutoUpdate().catch(() => { });
2778
+ }, 30 * 60 * 1000);
2779
+ providerCliUpdateTimer.unref();
2780
+ }, 2 * 60 * 1000);
2781
+ providerCliUpdateTimer.unref();
2672
2782
  }
2673
2783
  const close = () => new Promise((resolve) => {
2674
2784
  let done = false;
@@ -2692,6 +2802,8 @@ export async function startServer(config, configPath) {
2692
2802
  catch { /* ignore */ }
2693
2803
  if (updateCheckTimer)
2694
2804
  clearInterval(updateCheckTimer);
2805
+ if (providerCliUpdateTimer)
2806
+ clearInterval(providerCliUpdateTimer);
2695
2807
  try {
2696
2808
  server.close(() => finish());
2697
2809
  }
@@ -12,6 +12,6 @@ export interface SessionAiContext {
12
12
  */
13
13
  export declare function resolveSessionProvider(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command">): SessionProvider;
14
14
  /** Build the provider-specific settings used by session-adjacent AI actions. */
15
- export declare function resolveSessionAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultThinkingEffort" | "inheritEnv">): SessionAiContext;
15
+ export declare function resolveSessionAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultThinkingEffort" | "inheritEnv">): SessionAiContext;
16
16
  /** Build the AI context for quick-commit actions from their global preferences. */
17
- export declare function resolveCommitAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultThinkingEffort" | "inheritEnv" | "commitCli" | "commitModel">): SessionAiContext;
17
+ export declare function resolveCommitAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultThinkingEffort" | "inheritEnv" | "commitCli" | "commitModel">): SessionAiContext;
@@ -5,18 +5,22 @@ import { getDefaultModelForProvider } from "./config.js";
5
5
  * still identify Codex through structuredState, runner, or command.
6
6
  */
7
7
  export function resolveSessionProvider(snapshot) {
8
- if (snapshot.provider === "claude" || snapshot.provider === "codex") {
8
+ if (snapshot.provider === "claude" || snapshot.provider === "codex" || snapshot.provider === "opencode") {
9
9
  return snapshot.provider;
10
10
  }
11
- if (snapshot.structuredState?.provider === "claude" || snapshot.structuredState?.provider === "codex") {
11
+ if (snapshot.structuredState?.provider === "claude" || snapshot.structuredState?.provider === "codex" || snapshot.structuredState?.provider === "opencode") {
12
12
  return snapshot.structuredState.provider;
13
13
  }
14
14
  const runner = snapshot.runner ?? snapshot.structuredState?.runner;
15
15
  if (runner === "codex-cli-exec")
16
16
  return "codex";
17
+ if (runner === "opencode-cli-run")
18
+ return "opencode";
17
19
  if (runner === "claude-cli" || runner === "claude-cli-print" || runner === "claude-sdk")
18
20
  return "claude";
19
- return /^codex\b/i.test(snapshot.command.trim()) ? "codex" : "claude";
21
+ if (/^codex\b/i.test(snapshot.command.trim()))
22
+ return "codex";
23
+ return /^opencode\b/i.test(snapshot.command.trim()) ? "opencode" : "claude";
20
24
  }
21
25
  function normalizeModel(value) {
22
26
  const model = value?.trim();
@@ -39,7 +43,7 @@ export function resolveCommitAiContext(snapshot, config) {
39
43
  const sessionContext = resolveSessionAiContext(snapshot, config);
40
44
  return {
41
45
  ...sessionContext,
42
- provider: config.commitCli === "codex" ? "codex" : "claude",
46
+ provider: config.commitCli === "codex" || config.commitCli === "opencode" ? config.commitCli : "claude",
43
47
  model: normalizeModel(config.commitModel),
44
48
  };
45
49
  }
package/dist/storage.js CHANGED
@@ -18,7 +18,7 @@ function parseQueuedMessages(raw) {
18
18
  return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : undefined;
19
19
  }
20
20
  function inferSessionProvider(row) {
21
- if (row.provider === "claude" || row.provider === "codex") {
21
+ if (row.provider === "claude" || row.provider === "codex" || row.provider === "opencode") {
22
22
  return row.provider;
23
23
  }
24
24
  if (row.runner === "claude-cli" || row.runner === "claude-cli-print") {
@@ -27,8 +27,13 @@ function inferSessionProvider(row) {
27
27
  if (row.runner === "codex-cli-exec") {
28
28
  return "codex";
29
29
  }
30
+ if (row.runner === "opencode-cli-run") {
31
+ return "opencode";
32
+ }
30
33
  if (/^codex\b/i.test(row.command.trim()))
31
34
  return "codex";
35
+ if (/^opencode\b/i.test(row.command.trim()))
36
+ return "opencode";
32
37
  return /^claude\b/i.test(row.command.trim()) ? "claude" : undefined;
33
38
  }
34
39
  function parseWorktreeInfo(raw) {
@@ -35,6 +35,8 @@ export declare function thinkingEffortToClaudeCliEffort(effort: SessionSnapshot[
35
35
  export declare function thinkingEffortToClaudeSlashEffort(effort: SessionSnapshot["thinkingEffort"]): string;
36
36
  /** Codex CLI 用:把 thinkingEffort 映射到 model_reasoning_effort 配置。off → 不覆盖 Codex 默认。 */
37
37
  export declare function thinkingEffortToCodexReasoningEffort(effort: SessionSnapshot["thinkingEffort"]): string | null;
38
+ /** OpenCode exposes provider-specific reasoning presets through `--variant`. */
39
+ export declare function thinkingEffortToOpenCodeVariant(effort: SessionSnapshot["thinkingEffort"]): string | null;
38
40
  /**
39
41
  * Preserve both Responses content-part arrays and arbitrary structured tool output.
40
42
  * Arrays without a `type` discriminator (for example Codex tool_search results)
@@ -179,6 +181,10 @@ export declare class StructuredSessionManager {
179
181
  private incrementApprovalStats;
180
182
  private buildCodexArgs;
181
183
  private runCodexStreaming;
184
+ private buildOpenCodeArgs;
185
+ private openCodeToolName;
186
+ private applyOpenCodeEvent;
187
+ private runOpenCodeStreaming;
182
188
  /**
183
189
  * Spawn `claude -p --output-format stream-json` and parse NDJSON lines as
184
190
  * they arrive, emitting incremental WebSocket events so the UI can render