@co0ontty/wand 4.10.3 → 4.11.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/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  ### Overview
13
13
 
14
- Wand is a web console for remotely accessing and managing local CLI tools from a browser. It supports [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex](https://github.com/openai/codex), and [OpenCode](https://opencode.ai/), with terminal and structured conversation views, persistent resumable sessions, permission controls, file browsing, and native clients for multiple platforms.
14
+ Wand is a web console for remotely accessing and managing local CLI tools from a browser. It supports [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex](https://github.com/openai/codex), [OpenCode](https://opencode.ai/), [Grok Build](https://grok.com/), and [Qoder CLI](https://docs.qoder.com/en/cli/quick-start), with terminal and structured conversation views, persistent resumable sessions, permission controls, file browsing, and native clients for multiple platforms.
15
15
 
16
16
  The browser password manager extension source lives in `browser-extension/`; setup and supported baseline features are documented in [docs/browser-extension.md](docs/browser-extension.md).
17
17
 
@@ -56,7 +56,7 @@ wand config:password
56
56
  #### Core
57
57
 
58
58
  - **Dual view modes** — switch between raw terminal output and a structured conversation view for the same session
59
- - **Multiple providers** — create PTY or structured sessions for Claude Code, Codex, and OpenCode
59
+ - **Multiple providers** — create PTY or structured sessions for Claude Code, Codex, OpenCode, Grok, and Qoder CLI
60
60
  - **Session management** — create, archive, and resume sessions; restore Claude native history; show summaries in the session list
61
61
  - **Permission control** — visual permission prompts with one-time approval, per-turn memory, and related policies
62
62
 
@@ -155,7 +155,7 @@ npm install -g opencode-ai@latest
155
155
 
156
156
  ### 概览
157
157
 
158
- 通过浏览器远程访问和管理本地 CLI 工具的 Web 控制台。支持 [Claude Code](https://docs.anthropic.com/en/docs/claude-code)、[Codex](https://github.com/openai/codex)[OpenCode](https://opencode.ai/),提供终端和结构化对话双视图、会话持久化与恢复、权限管控、文件浏览和多平台客户端。
158
+ 通过浏览器远程访问和管理本地 CLI 工具的 Web 控制台。支持 [Claude Code](https://docs.anthropic.com/en/docs/claude-code)、[Codex](https://github.com/openai/codex)[OpenCode](https://opencode.ai/)、[Grok Build](https://grok.com/) 和 [Qoder CLI](https://docs.qoder.com/zh/cli/quick-start),提供终端和结构化对话双视图、会话持久化与恢复、权限管控、文件浏览和多平台客户端。
159
159
 
160
160
  <p align="center">
161
161
  <img src="docs/screenshots/clients-overview.png" width="100%" alt="Web 居中,iOS 和 Android 客户端在两侧的多端详情总览" />
@@ -205,7 +205,7 @@ npm install -g opencode-ai@latest
205
205
  #### 核心
206
206
 
207
207
  - **双视图模式** — 终端原始输出和结构化对话视图可随时切换,同一会话两种呈现
208
- - **多 Provider 支持** — Claude Code、Codex 和 OpenCode 均可创建 PTY 或结构化会话
208
+ - **多 Provider 支持** — Claude Code、Codex、OpenCode、GrokQoder CLI 均可创建 PTY 或结构化会话
209
209
  - **会话管理** — 创建、归档、恢复会话;支持从 Claude 原生历史记录恢复;会话列表显示摘要
210
210
  - **权限控制** — 可视化权限提示,支持逐次确认、单次批准、本轮记忆等策略
211
211
 
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "f95b8e7018d6c81a50bae385c3d7ed3cb5b39109",
3
- "builtAt": "2026-07-17T05:30:24.232Z",
4
- "version": "4.10.3",
2
+ "commit": "439abee1c0b8d5601059d7eee4a142adf6cc0c7b",
3
+ "builtAt": "2026-07-17T09:40:00.200Z",
4
+ "version": "4.11.0",
5
5
  "channel": "stable"
6
6
  }
package/dist/config.d.ts CHANGED
@@ -7,7 +7,7 @@ import type { WandStorage } from "./storage.js";
7
7
  * 升级路径:老 JSON 里仍存有这些字段时,首次启动会被搬到 DB(见 migrateLegacyPreferencesToDb),
8
8
  * 然后下一次 saveConfig 写回 JSON 时它们会被剥离(见 stripPreferenceFields)。
9
9
  */
10
- export declare const PREFERENCE_KEYS: readonly ["defaultProvider", "defaultSessionKind", "defaultMode", "defaultCwd", "defaultModel", "defaultCodexModel", "defaultOpenCodeModel", "defaultGrokModel", "commitCli", "commitModel", "commitAiSource", "systemAi", "defaultThinkingEffort", "structuredRunner", "language", "cardDefaults", "inheritEnv"];
10
+ export declare const PREFERENCE_KEYS: readonly ["defaultProvider", "defaultSessionKind", "defaultMode", "defaultCwd", "defaultModel", "defaultCodexModel", "defaultOpenCodeModel", "defaultGrokModel", "defaultQoderModel", "commitCli", "commitModel", "commitAiSource", "systemAi", "defaultThinkingEffort", "structuredRunner", "language", "cardDefaults", "inheritEnv"];
11
11
  export type PreferenceKey = (typeof PREFERENCE_KEYS)[number];
12
12
  export declare function isPreferenceKey(key: string): key is PreferenceKey;
13
13
  export declare const defaultConfig: () => WandConfig;
@@ -44,11 +44,12 @@ export declare function writePreferenceToStorage(config: WandConfig, storage: Wa
44
44
  export declare function validateCommitAiConfig(config: Pick<WandConfig, "commitAiSource" | "systemAi">): void;
45
45
  export declare function normalizeCardDefaults(input: unknown): CardExpandDefaults;
46
46
  export declare function isExecutionMode(value: unknown): value is ExecutionMode;
47
- export declare function getProviderDefaultModels(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel">): {
47
+ export declare function getProviderDefaultModels(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel">): {
48
48
  claude: string;
49
49
  codex: string;
50
50
  opencode: string;
51
51
  grok: string;
52
+ qoder: string;
52
53
  };
53
- export declare function getDefaultModelForProvider(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel">, provider: SessionProvider | undefined): string;
54
+ export declare function getDefaultModelForProvider(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel">, provider: SessionProvider | undefined): string;
54
55
  export declare function normalizeMode(input: string | undefined, fallback: ExecutionMode): ExecutionMode;
package/dist/config.js CHANGED
@@ -30,6 +30,7 @@ export const PREFERENCE_KEYS = [
30
30
  "defaultCodexModel",
31
31
  "defaultOpenCodeModel",
32
32
  "defaultGrokModel",
33
+ "defaultQoderModel",
33
34
  "commitCli",
34
35
  "commitModel",
35
36
  "commitAiSource",
@@ -73,6 +74,7 @@ export const defaultConfig = () => ({
73
74
  defaultCodexModel: "",
74
75
  defaultOpenCodeModel: "",
75
76
  defaultGrokModel: "",
77
+ defaultQoderModel: "",
76
78
  commitCli: "claude",
77
79
  commitModel: "",
78
80
  commitAiSource: "cli",
@@ -282,7 +284,7 @@ export function applyStoragePreferences(config, storage) {
282
284
  const defaults = defaultConfig();
283
285
  if (storage.hasPreference(preferenceStorageKey("defaultProvider"))) {
284
286
  const v = storage.getPreference(preferenceStorageKey("defaultProvider"), defaults.defaultProvider ?? "claude");
285
- if (v === "claude" || v === "codex" || v === "opencode" || v === "grok")
287
+ if (v === "claude" || v === "codex" || v === "opencode" || v === "grok" || v === "qoder")
286
288
  config.defaultProvider = v;
287
289
  }
288
290
  if (storage.hasPreference(preferenceStorageKey("defaultSessionKind"))) {
@@ -320,6 +322,11 @@ export function applyStoragePreferences(config, storage) {
320
322
  if (typeof v === "string")
321
323
  config.defaultGrokModel = v.trim();
322
324
  }
325
+ if (storage.hasPreference(preferenceStorageKey("defaultQoderModel"))) {
326
+ const v = storage.getPreference(preferenceStorageKey("defaultQoderModel"), defaults.defaultQoderModel ?? "");
327
+ if (typeof v === "string")
328
+ config.defaultQoderModel = v.trim();
329
+ }
323
330
  if (storage.hasPreference(preferenceStorageKey("commitCli"))) {
324
331
  const v = storage.getPreference(preferenceStorageKey("commitCli"), defaults.commitCli ?? "claude");
325
332
  if (v === "claude" || v === "codex" || v === "opencode")
@@ -370,7 +377,7 @@ export function writePreferenceToStorage(config, storage, key, value, options =
370
377
  const dbKey = preferenceStorageKey(key);
371
378
  switch (key) {
372
379
  case "defaultProvider": {
373
- if (value !== "claude" && value !== "codex" && value !== "opencode" && value !== "grok")
380
+ if (value !== "claude" && value !== "codex" && value !== "opencode" && value !== "grok" && value !== "qoder")
374
381
  throw new Error(`无效 Provider: ${value}`);
375
382
  storage.setPreference(dbKey, value);
376
383
  config.defaultProvider = value;
@@ -420,6 +427,12 @@ export function writePreferenceToStorage(config, storage, key, value, options =
420
427
  config.defaultGrokModel = v;
421
428
  break;
422
429
  }
430
+ case "defaultQoderModel": {
431
+ const v = typeof value === "string" ? value.trim() : "";
432
+ storage.setPreference(dbKey, v);
433
+ config.defaultQoderModel = v;
434
+ break;
435
+ }
423
436
  case "commitCli": {
424
437
  if (value !== "claude" && value !== "codex" && value !== "opencode")
425
438
  throw new Error(`无效 commit CLI: ${value}`);
@@ -641,12 +654,13 @@ function mergeWithDefaults(input) {
641
654
  android: normalizeAndroidApkConfig(input.android) ?? defaults.android,
642
655
  macos: normalizeMacosDmgConfig(input.macos) ?? defaults.macos,
643
656
  cardDefaults: normalizeCardDefaults(input.cardDefaults),
644
- defaultProvider: input.defaultProvider === "codex" || input.defaultProvider === "opencode" || input.defaultProvider === "grok" ? input.defaultProvider : "claude",
657
+ defaultProvider: input.defaultProvider === "codex" || input.defaultProvider === "opencode" || input.defaultProvider === "grok" || input.defaultProvider === "qoder" ? input.defaultProvider : "claude",
645
658
  defaultSessionKind: input.defaultSessionKind === "pty" ? "pty" : "structured",
646
659
  defaultModel: typeof input.defaultModel === "string" ? input.defaultModel.trim() : defaults.defaultModel,
647
660
  defaultCodexModel: typeof input.defaultCodexModel === "string" ? input.defaultCodexModel.trim() : defaults.defaultCodexModel,
648
661
  defaultOpenCodeModel: typeof input.defaultOpenCodeModel === "string" ? input.defaultOpenCodeModel.trim() : defaults.defaultOpenCodeModel,
649
662
  defaultGrokModel: typeof input.defaultGrokModel === "string" ? input.defaultGrokModel.trim() : defaults.defaultGrokModel,
663
+ defaultQoderModel: typeof input.defaultQoderModel === "string" ? input.defaultQoderModel.trim() : defaults.defaultQoderModel,
650
664
  commitCli: input.commitCli === "codex" || input.commitCli === "opencode" ? input.commitCli : "claude",
651
665
  commitModel: typeof input.commitModel === "string" ? input.commitModel.trim() : defaults.commitModel,
652
666
  commitAiSource: input.commitAiSource === "api" ? "api" : "cli",
@@ -664,6 +678,7 @@ export function getProviderDefaultModels(config) {
664
678
  codex: (config.defaultCodexModel ?? "").trim(),
665
679
  opencode: (config.defaultOpenCodeModel ?? "").trim(),
666
680
  grok: (config.defaultGrokModel ?? "").trim(),
681
+ qoder: (config.defaultQoderModel ?? "").trim(),
667
682
  };
668
683
  }
669
684
  export function getDefaultModelForProvider(config, provider) {
@@ -674,6 +689,8 @@ export function getDefaultModelForProvider(config, provider) {
674
689
  return defaults.opencode;
675
690
  if (provider === "grok")
676
691
  return defaults.grok;
692
+ if (provider === "qoder")
693
+ return defaults.qoder;
677
694
  return defaults.claude;
678
695
  }
679
696
  export function normalizeMode(input, fallback) {
package/dist/models.d.ts CHANGED
@@ -35,6 +35,7 @@ export interface ModelCache {
35
35
  codexModels: ClaudeModelInfo[];
36
36
  opencodeModels: ClaudeModelInfo[];
37
37
  grokModels: ClaudeModelInfo[];
38
+ qoderModels: ClaudeModelInfo[];
38
39
  claudeVersion: string | null;
39
40
  opencodeVersion: string | null;
40
41
  refreshedAt: string;
@@ -47,6 +48,8 @@ export interface ModelCache {
47
48
  * * grok-4.5 (default)
48
49
  */
49
50
  export declare function parseGrokModels(stdout: string): ClaudeModelInfo[];
51
+ /** Parse `qodercli --list-models`, whose custom entries end in their selectable model ID. */
52
+ export declare function parseQoderModels(stdout: string): ClaudeModelInfo[];
50
53
  /** Parse `opencode models`, whose stable machine-friendly output is one provider/model id per line. */
51
54
  export declare function parseOpenCodeModels(stdout: string): ClaudeModelInfo[];
52
55
  /** Parse the machine-readable model registry emitted by the installed Codex CLI. */
package/dist/models.js CHANGED
@@ -33,6 +33,14 @@ const GROK_FALLBACK_MODELS = [
33
33
  { id: "default", label: "跟随 Grok 默认", alias: true },
34
34
  { id: "grok-4.5", label: "grok-4.5" },
35
35
  ];
36
+ const QODER_FALLBACK_MODELS = [
37
+ { id: "default", label: "跟随 Qoder 默认", alias: true },
38
+ { id: "lite", label: "Lite" },
39
+ { id: "efficient", label: "Efficient" },
40
+ { id: "auto", label: "Auto" },
41
+ { id: "performance", label: "Performance" },
42
+ { id: "ultimate", label: "Ultimate" },
43
+ ];
36
44
  let cache = null;
37
45
  function cloneModels(models) {
38
46
  return models.map((model) => ({ ...model }));
@@ -198,6 +206,7 @@ function createInitialCache(options) {
198
206
  codexModels: cloneModels(CODEX_FALLBACK_MODELS),
199
207
  opencodeModels: cloneModels(OPENCODE_FALLBACK_MODELS),
200
208
  grokModels: cloneModels(GROK_FALLBACK_MODELS),
209
+ qoderModels: cloneModels(QODER_FALLBACK_MODELS),
201
210
  claudeVersion: null,
202
211
  opencodeVersion: null,
203
212
  refreshedAt: now.toISOString(),
@@ -267,6 +276,15 @@ async function probeGrokModels(runner, env) {
267
276
  return cloneModels(GROK_FALLBACK_MODELS);
268
277
  }
269
278
  }
279
+ async function probeQoderModels(runner, env) {
280
+ try {
281
+ const { stdout } = await runner("qodercli", ["--list-models"], { env, timeout: 8000 });
282
+ return parseQoderModels(stdout);
283
+ }
284
+ catch {
285
+ return cloneModels(QODER_FALLBACK_MODELS);
286
+ }
287
+ }
270
288
  function createOfficialModelsApi(apiKey) {
271
289
  const client = new Anthropic({ apiKey });
272
290
  return {
@@ -374,6 +392,23 @@ export function parseGrokModels(stdout) {
374
392
  ...unique.map((id) => ({ id, label: id })),
375
393
  ];
376
394
  }
395
+ /** Parse `qodercli --list-models`, whose custom entries end in their selectable model ID. */
396
+ export function parseQoderModels(stdout) {
397
+ const discovered = [];
398
+ const seen = new Set(QODER_FALLBACK_MODELS.map((model) => model.id));
399
+ for (const rawLine of stdout.split(/\r?\n/)) {
400
+ const line = rawLine.replace(/\x1b\[[0-?]*[ -\/]*[@-~]/g, "").trim();
401
+ const match = line.match(/^(.*?)\s+\(([A-Za-z0-9][A-Za-z0-9._:-]*\/[A-Za-z0-9][A-Za-z0-9._:/-]*)\)\s*$/);
402
+ if (!match)
403
+ continue;
404
+ const [, displayName, id] = match;
405
+ if (!displayName || !id || seen.has(id))
406
+ continue;
407
+ seen.add(id);
408
+ discovered.push({ id, label: displayName.trim() || id });
409
+ }
410
+ return [...cloneModels(QODER_FALLBACK_MODELS), ...discovered];
411
+ }
377
412
  /** Parse `opencode models`, whose stable machine-friendly output is one provider/model id per line. */
378
413
  export function parseOpenCodeModels(stdout) {
379
414
  const ids = Array.from(new Set(stdout
@@ -452,11 +487,12 @@ export async function refreshModels(options = {}) {
452
487
  const now = options.now?.() ?? new Date();
453
488
  const env = resolveProbeEnv(options);
454
489
  const runner = options.commandRunner ?? defaultCommandRunner;
455
- const [claudeVersion, codexModels, opencode, grokModels, apiModels] = await Promise.all([
490
+ const [claudeVersion, codexModels, opencode, grokModels, qoderModels, apiModels] = await Promise.all([
456
491
  probeClaudeVersion(runner, env),
457
492
  probeCodexModels(runner, env),
458
493
  probeOpenCode(runner, env),
459
494
  probeGrokModels(runner, env),
495
+ probeQoderModels(runner, env),
460
496
  listClaudeModelsFromApi(options, env),
461
497
  ]);
462
498
  const priorVerifications = loadClaudeVerifications(options.storage);
@@ -484,6 +520,7 @@ export async function refreshModels(options = {}) {
484
520
  codexModels,
485
521
  opencodeModels: opencode.models,
486
522
  grokModels,
523
+ qoderModels,
487
524
  claudeVersion,
488
525
  opencodeVersion: opencode.version,
489
526
  refreshedAt: now.toISOString(),
@@ -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", "grok"];
9
+ const PROBE_COMMANDS = ["claude", "codex", "opencode", "grok", "qodercli"];
10
10
  const DEEP_PROBE_TIMEOUT_MS = 4000;
11
11
  /**
12
12
  * 构造候选 bin 目录列表(按优先级,前面的更可信)。
@@ -261,7 +261,8 @@ function probeLoginShell(shell, timeoutMs) {
261
261
  `printf 'CLAUDE\\x1f%s\\n' "$(command -v claude 2>/dev/null)"; ` +
262
262
  `printf 'CODEX\\x1f%s\\n' "$(command -v codex 2>/dev/null)"; ` +
263
263
  `printf 'OPENCODE\\x1f%s\\n' "$(command -v opencode 2>/dev/null)"; ` +
264
- `printf 'GROK\\x1f%s\\n' "$(command -v grok 2>/dev/null)"`;
264
+ `printf 'GROK\\x1f%s\\n' "$(command -v grok 2>/dev/null)"; ` +
265
+ `printf 'QODERCLI\\x1f%s\\n' "$(command -v qodercli 2>/dev/null)"`;
265
266
  return new Promise((resolve, reject) => {
266
267
  const child = spawn(shell, ["-l", "-c", script], {
267
268
  env: {
@@ -301,7 +302,7 @@ function probeLoginShell(shell, timeoutMs) {
301
302
  reject(new Error(`login shell exited ${code}: ${stderr.trim().slice(0, 200)}`));
302
303
  return;
303
304
  }
304
- const out = { path: "", claude: null, codex: null, opencode: null, grok: null };
305
+ const out = { path: "", claude: null, codex: null, opencode: null, grok: null, qodercli: null };
305
306
  for (const line of stdout.split("\n")) {
306
307
  const idx = line.indexOf("\x1f");
307
308
  if (idx < 0)
@@ -320,6 +321,8 @@ function probeLoginShell(shell, timeoutMs) {
320
321
  out.opencode = val;
321
322
  else if (key === "GROK")
322
323
  out.grok = val;
324
+ else if (key === "QODERCLI")
325
+ out.qodercli = val;
323
326
  }
324
327
  resolve(out);
325
328
  });
@@ -24,7 +24,9 @@ function resolveProviderFromCommand(command) {
24
24
  return "codex";
25
25
  if (/^opencode\b/.test(command.trim()))
26
26
  return "opencode";
27
- return /^grok\b/.test(command.trim()) ? "grok" : "claude";
27
+ if (/^grok\b/.test(command.trim()))
28
+ return "grok";
29
+ return /^qodercli\b/.test(command.trim()) ? "qoder" : "claude";
28
30
  }
29
31
  /**
30
32
  * Tokenize the restricted shell-command subset accepted by the command
@@ -2027,6 +2029,20 @@ export class ProcessManager extends EventEmitter {
2027
2029
  }
2028
2030
  return result;
2029
2031
  }
2032
+ if (provider === "qoder") {
2033
+ let result = command;
2034
+ const trimmedModel = model?.trim();
2035
+ if (trimmedModel && trimmedModel !== "default" && !/--model(?:\s|=)/.test(result)) {
2036
+ result += ` --model '${trimmedModel.replace(/'/g, "'\\''")}'`;
2037
+ }
2038
+ if ((mode === "managed" || mode === "full-access") && !/--(?:yolo|dangerously-skip-permissions|permission-mode)(?:\s|=|$)/.test(result)) {
2039
+ result += " --permission-mode bypass_permissions";
2040
+ }
2041
+ else if (mode === "auto-edit" && !/--permission-mode(?:\s|=)/.test(result)) {
2042
+ result += " --permission-mode accept_edits";
2043
+ }
2044
+ return result;
2045
+ }
2030
2046
  const isClaudeCmd = /^(?:claude|npx\s+claude|[^\s]+\/claude)(?:\s|$)/.test(command);
2031
2047
  if (!isClaudeCmd)
2032
2048
  return command;
@@ -1,4 +1,4 @@
1
- export type ProviderCliId = "claude" | "codex" | "opencode";
1
+ export type ProviderCliId = "claude" | "codex" | "opencode" | "qoder";
2
2
  export interface ProviderCliUpdateStatus {
3
3
  id: ProviderCliId;
4
4
  label: string;
@@ -36,6 +36,14 @@ const PROVIDER_CLI_SPECS = [
36
36
  versionArgs: ["--version"],
37
37
  updateArgs: ["upgrade"],
38
38
  },
39
+ {
40
+ id: "qoder",
41
+ label: "Qoder CLI",
42
+ command: "qodercli",
43
+ npmPackage: "@qoder-ai/qodercli",
44
+ versionArgs: ["--version"],
45
+ updateArgs: ["update"],
46
+ },
39
47
  ];
40
48
  function childEnv(options) {
41
49
  return options.env ?? buildChildEnv(options.inheritEnv !== false);
@@ -66,7 +74,7 @@ function resolveInstallKind(executable, id, version) {
66
74
  return "npm";
67
75
  if (normalized.includes("/cellar/") || normalized.includes("/caskroom/") || normalized.includes("/homebrew/"))
68
76
  return "brew";
69
- if (normalized.includes("/.claude/") || normalized.includes("/.codex/") || normalized.includes("/.opencode/"))
77
+ if (normalized.includes("/.claude/") || normalized.includes("/.codex/") || normalized.includes("/.opencode/") || normalized.includes("/.qoder/"))
70
78
  return "native";
71
79
  return "unknown";
72
80
  }
@@ -325,11 +325,11 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
325
325
  app.post("/api/structured-sessions", asyncRoute(async (req, res) => {
326
326
  const body = req.body;
327
327
  try {
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。" });
328
+ if (body.provider && body.provider !== "claude" && body.provider !== "codex" && body.provider !== "opencode" && body.provider !== "grok" && body.provider !== "qoder") {
329
+ res.status(400).json({ error: "结构化会话当前仅支持 Claude、Codex、OpenCode、GrokQoder provider。" });
330
330
  return;
331
331
  }
332
- const provider = body.provider === "codex" || body.provider === "opencode" || body.provider === "grok" ? body.provider : "claude";
332
+ const provider = body.provider === "codex" || body.provider === "opencode" || body.provider === "grok" || body.provider === "qoder" ? body.provider : "claude";
333
333
  const rawModel = typeof body.model === "string" ? body.model.trim() : "";
334
334
  const origin = parseSessionCreationOrigin(body);
335
335
  const snapshot = structured.createSession({
@@ -21,6 +21,7 @@ function publicConfig(config) {
21
21
  defaultCodexModel: defaultModels.codex,
22
22
  defaultOpenCodeModel: defaultModels.opencode,
23
23
  defaultGrokModel: defaultModels.grok,
24
+ defaultQoderModel: defaultModels.qoder,
24
25
  defaultModels,
25
26
  };
26
27
  }
@@ -187,6 +188,8 @@ export function registerSettingsRoutes(app, deps) {
187
188
  stagePreference("defaultOpenCodeModel", body.defaultModels.opencode);
188
189
  if (Object.hasOwn(body.defaultModels, "grok"))
189
190
  stagePreference("defaultGrokModel", body.defaultModels.grok);
191
+ if (Object.hasOwn(body.defaultModels, "qoder"))
192
+ stagePreference("defaultQoderModel", body.defaultModels.qoder);
190
193
  }
191
194
  if (body.systemAi !== undefined) {
192
195
  if (!body.systemAi || typeof body.systemAi !== "object" || Array.isArray(body.systemAi)) {
@@ -256,6 +259,7 @@ export function registerSettingsRoutes(app, deps) {
256
259
  defaultCodexModel: defaults.codex,
257
260
  defaultOpenCodeModel: defaults.opencode,
258
261
  defaultGrokModel: defaults.grok,
262
+ defaultQoderModel: defaults.qoder,
259
263
  defaultModels: defaults,
260
264
  });
261
265
  });
@@ -269,6 +273,7 @@ export function registerSettingsRoutes(app, deps) {
269
273
  defaultCodexModel: defaults.codex,
270
274
  defaultOpenCodeModel: defaults.opencode,
271
275
  defaultGrokModel: defaults.grok,
276
+ defaultQoderModel: defaults.qoder,
272
277
  defaultModels: defaults,
273
278
  });
274
279
  }
@@ -127,7 +127,7 @@ export function registerAdminUpdateRoutes(app, deps) {
127
127
  return;
128
128
  }
129
129
  const rawIds = Array.isArray(req.body?.ids) ? req.body.ids : [];
130
- const ids = rawIds.filter((value) => value === "claude" || value === "codex" || value === "opencode");
130
+ const ids = rawIds.filter((value) => value === "claude" || value === "codex" || value === "opencode" || value === "qoder");
131
131
  state.providerCliUpdateInFlight = true;
132
132
  try {
133
133
  const before = await refreshProviderCliUpdateState(state, config);
package/dist/server.js CHANGED
@@ -198,6 +198,7 @@ const CONNECTED_APP_PREFERENCE_KEYS = new Set([
198
198
  "defaultCodexModel",
199
199
  "defaultOpenCodeModel",
200
200
  "defaultGrokModel",
201
+ "defaultQoderModel",
201
202
  "defaultModels",
202
203
  "defaultThinkingEffort",
203
204
  "defaultProvider",
@@ -719,6 +720,7 @@ export async function startServer(config, configPath, options = {}) {
719
720
  defaultCodexModel: defaultModels.codex,
720
721
  defaultOpenCodeModel: defaultModels.opencode,
721
722
  defaultGrokModel: defaultModels.grok,
723
+ defaultQoderModel: defaultModels.qoder,
722
724
  defaultModels,
723
725
  defaultThinkingEffort: config.defaultThinkingEffort ?? "off",
724
726
  commandPresets: config.commandPresets,
@@ -728,6 +730,7 @@ export async function startServer(config, configPath, options = {}) {
728
730
  { label: "Codex Structured", runner: "codex-cli-exec" },
729
731
  { label: "OpenCode Structured", runner: "opencode-cli-run" },
730
732
  { label: "Grok Structured", runner: "grok-cli-headless" },
733
+ { label: "Qoder Structured", runner: "qoder-cli-print" },
731
734
  ],
732
735
  structuredChatPersona,
733
736
  cardDefaults: config.cardDefaults,
@@ -955,7 +958,9 @@ export async function startServer(config, configPath, options = {}) {
955
958
  ? "opencode"
956
959
  : body.provider === "grok" || /^grok\b/.test(body.command.trim())
957
960
  ? "grok"
958
- : "claude";
961
+ : body.provider === "qoder" || /^qodercli\b/.test(body.command.trim())
962
+ ? "qoder"
963
+ : "claude";
959
964
  const effectiveModel = rawModel || getDefaultModelForProvider(config, provider) || undefined;
960
965
  const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
961
966
  const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
@@ -13,6 +13,6 @@ export interface SessionAiContext {
13
13
  */
14
14
  export declare function resolveSessionProvider(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command">): SessionProvider;
15
15
  /** Build the provider-specific settings used by session-adjacent AI actions. */
16
- export declare function resolveSessionAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultThinkingEffort" | "inheritEnv">): SessionAiContext;
16
+ export declare function resolveSessionAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultThinkingEffort" | "inheritEnv">): SessionAiContext;
17
17
  /** Build the AI context for quick-commit actions from their global preferences. */
18
- export declare function resolveCommitAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultThinkingEffort" | "inheritEnv" | "commitCli" | "commitModel" | "commitAiSource" | "systemAi">): SessionAiContext;
18
+ export declare function resolveCommitAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultThinkingEffort" | "inheritEnv" | "commitCli" | "commitModel" | "commitAiSource" | "systemAi">): SessionAiContext;
@@ -8,13 +8,15 @@ export function resolveSessionProvider(snapshot) {
8
8
  if (snapshot.provider === "claude"
9
9
  || snapshot.provider === "codex"
10
10
  || snapshot.provider === "opencode"
11
- || snapshot.provider === "grok") {
11
+ || snapshot.provider === "grok"
12
+ || snapshot.provider === "qoder") {
12
13
  return snapshot.provider;
13
14
  }
14
15
  if (snapshot.structuredState?.provider === "claude"
15
16
  || snapshot.structuredState?.provider === "codex"
16
17
  || snapshot.structuredState?.provider === "opencode"
17
- || snapshot.structuredState?.provider === "grok") {
18
+ || snapshot.structuredState?.provider === "grok"
19
+ || snapshot.structuredState?.provider === "qoder") {
18
20
  return snapshot.structuredState.provider;
19
21
  }
20
22
  const runner = snapshot.runner ?? snapshot.structuredState?.runner;
@@ -24,6 +26,8 @@ export function resolveSessionProvider(snapshot) {
24
26
  return "opencode";
25
27
  if (runner === "grok-cli-headless")
26
28
  return "grok";
29
+ if (runner === "qoder-cli-print")
30
+ return "qoder";
27
31
  if (runner === "claude-cli" || runner === "claude-cli-print" || runner === "claude-sdk")
28
32
  return "claude";
29
33
  if (/^codex\b/i.test(snapshot.command.trim()))
@@ -32,6 +36,8 @@ export function resolveSessionProvider(snapshot) {
32
36
  return "opencode";
33
37
  if (/^grok\b/i.test(snapshot.command.trim()))
34
38
  return "grok";
39
+ if (/^qodercli\b/i.test(snapshot.command.trim()))
40
+ return "qoder";
35
41
  return "claude";
36
42
  }
37
43
  function normalizeModel(value) {
@@ -129,7 +129,9 @@ export class SessionRegistry {
129
129
  ?? snapshot.structuredState?.provider
130
130
  ?? (/^codex\b/i.test(snapshot.command.trim())
131
131
  ? "codex"
132
- : /^opencode\b/i.test(snapshot.command.trim()) ? "opencode" : "claude");
132
+ : /^opencode\b/i.test(snapshot.command.trim()) ? "opencode"
133
+ : /^grok\b/i.test(snapshot.command.trim()) ? "grok"
134
+ : /^qodercli\b/i.test(snapshot.command.trim()) ? "qoder" : "claude");
133
135
  if (provider === "claude") {
134
136
  this.processes.deleteClaudeHistoryFiles([{ claudeSessionId: providerSessionId, cwd: snapshot.cwd }]);
135
137
  }
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" || row.provider === "grok") {
172
+ if (row.provider === "claude" || row.provider === "codex" || row.provider === "opencode" || row.provider === "grok" || row.provider === "qoder") {
173
173
  return row.provider;
174
174
  }
175
175
  if (row.runner === "claude-cli" || row.runner === "claude-cli-print") {
@@ -183,12 +183,16 @@ function inferSessionProvider(row) {
183
183
  }
184
184
  if (row.runner === "grok-cli-headless")
185
185
  return "grok";
186
+ if (row.runner === "qoder-cli-print")
187
+ return "qoder";
186
188
  if (/^codex\b/i.test(row.command.trim()))
187
189
  return "codex";
188
190
  if (/^opencode\b/i.test(row.command.trim()))
189
191
  return "opencode";
190
192
  if (/^grok\b/i.test(row.command.trim()))
191
193
  return "grok";
194
+ if (/^qodercli\b/i.test(row.command.trim()))
195
+ return "qoder";
192
196
  return /^claude\b/i.test(row.command.trim()) ? "claude" : undefined;
193
197
  }
194
198
  function parseWorktreeInfo(raw) {
@@ -5,7 +5,9 @@ export function isStructuredRunnerForProvider(provider, runner) {
5
5
  return runner === "codex-cli-exec";
6
6
  if (provider === "opencode")
7
7
  return runner === "opencode-cli-run";
8
- return runner === "grok-cli-headless";
8
+ if (provider === "grok")
9
+ return runner === "grok-cli-headless";
10
+ return runner === "qoder-cli-print";
9
11
  }
10
12
  export function defaultStructuredRunner(provider, configuredClaudeRunner = "cli") {
11
13
  if (provider === "codex")
@@ -14,6 +16,8 @@ export function defaultStructuredRunner(provider, configuredClaudeRunner = "cli"
14
16
  return "opencode-cli-run";
15
17
  if (provider === "grok")
16
18
  return "grok-cli-headless";
19
+ if (provider === "qoder")
20
+ return "qoder-cli-print";
17
21
  return configuredClaudeRunner === "sdk" ? "claude-sdk" : "claude-cli-print";
18
22
  }
19
23
  export function resolveStructuredRunner(provider, requestedRunner, configuredClaudeRunner = "cli") {
@@ -0,0 +1,10 @@
1
+ import { spawn } from "node:child_process";
2
+ import type { StructuredRunnerAdapter, StructuredRunnerContext, StructuredRunnerExecution, StructuredRunnerObserver } from "./structured-runner.js";
3
+ import type { SessionSnapshot } from "./types.js";
4
+ export declare function buildQoderArgs(session: SessionSnapshot, prompt: string): string[];
5
+ /** Owns the official Qoder CLI print process and its stream-json protocol. */
6
+ export declare class QoderRunner implements StructuredRunnerAdapter {
7
+ private readonly spawnProcess;
8
+ constructor(spawnProcess?: typeof spawn);
9
+ start(context: StructuredRunnerContext, observer: StructuredRunnerObserver): StructuredRunnerExecution;
10
+ }