@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.
@@ -0,0 +1,124 @@
1
+ import { spawn } from "node:child_process";
2
+ import { ClaudeCliProtocolReducer } from "./structured-claude-protocol.js";
3
+ export function buildQoderArgs(session, prompt) {
4
+ const args = ["-p", prompt, "--output-format", "stream-json"];
5
+ const model = session.selectedModel?.trim();
6
+ if (model && model !== "default")
7
+ args.push("--model", model);
8
+ if (session.autoApprovePermissions === true
9
+ || session.mode === "full-access"
10
+ || session.mode === "managed") {
11
+ args.push("--permission-mode", "bypass_permissions");
12
+ }
13
+ else if (session.mode === "auto-edit") {
14
+ args.push("--permission-mode", "accept_edits");
15
+ }
16
+ if (session.claudeSessionId)
17
+ args.push("-r", session.claudeSessionId);
18
+ return args;
19
+ }
20
+ /** Owns the official Qoder CLI print process and its stream-json protocol. */
21
+ export class QoderRunner {
22
+ spawnProcess;
23
+ constructor(spawnProcess = spawn) {
24
+ this.spawnProcess = spawnProcess;
25
+ }
26
+ start(context, observer) {
27
+ const args = buildQoderArgs(context.session, context.prompt);
28
+ const spawnedAt = new Date().toISOString();
29
+ const child = this.spawnProcess("qodercli", args, {
30
+ cwd: context.session.cwd,
31
+ env: context.env,
32
+ stdio: ["ignore", "pipe", "pipe"],
33
+ });
34
+ const reducer = new ClaudeCliProtocolReducer(context.session);
35
+ let lineBuffer = "";
36
+ let stderr = "";
37
+ let stdoutTail = "";
38
+ let primaryError = null;
39
+ let settled = false;
40
+ const result = (exitCode, signal, spawnError) => ({
41
+ state: reducer.state,
42
+ exitCode,
43
+ signal,
44
+ stderr,
45
+ stdoutTail,
46
+ primaryError,
47
+ ...(spawnError ? { spawnError } : {}),
48
+ });
49
+ const processLine = (line) => {
50
+ if (!observer.isActive())
51
+ return;
52
+ const trimmed = line.trim();
53
+ if (!trimmed)
54
+ return;
55
+ let event;
56
+ try {
57
+ event = JSON.parse(trimmed);
58
+ }
59
+ catch {
60
+ return;
61
+ }
62
+ if (event && typeof event === "object" && !Array.isArray(event)) {
63
+ const record = event;
64
+ observer.onEvent?.(record);
65
+ if (record.type === "result" && record.subtype !== "success") {
66
+ const errors = Array.isArray(record.errors)
67
+ ? record.errors.filter((item) => typeof item === "string")
68
+ : [];
69
+ primaryError = errors.join("\n") || "Qoder CLI execution failed";
70
+ }
71
+ }
72
+ if (reducer.apply(event, context.session.mode === "managed"))
73
+ observer.onUpdate(reducer.state);
74
+ };
75
+ const completion = new Promise((resolve) => {
76
+ child.stdout?.on("data", (chunk) => {
77
+ if (!observer.isActive())
78
+ return;
79
+ const text = chunk.toString();
80
+ observer.onStdout?.(text);
81
+ const trimmed = text.trim();
82
+ if (trimmed)
83
+ stdoutTail = trimmed.slice(-1024);
84
+ lineBuffer += text;
85
+ const lines = lineBuffer.split("\n");
86
+ lineBuffer = lines.pop() ?? "";
87
+ for (const line of lines)
88
+ processLine(line);
89
+ });
90
+ child.stderr?.on("data", (chunk) => {
91
+ if (!observer.isActive())
92
+ return;
93
+ const text = chunk.toString();
94
+ observer.onStderr?.(text);
95
+ stderr += text;
96
+ });
97
+ child.on("error", (error) => {
98
+ if (settled)
99
+ return;
100
+ settled = true;
101
+ resolve(result(null, null, error));
102
+ });
103
+ child.on("close", (exitCode, signal) => {
104
+ if (settled)
105
+ return;
106
+ settled = true;
107
+ if (lineBuffer.trim())
108
+ processLine(lineBuffer);
109
+ lineBuffer = "";
110
+ resolve(result(exitCode, signal));
111
+ });
112
+ });
113
+ return {
114
+ args,
115
+ spawnedAt,
116
+ pid: child.pid ?? null,
117
+ completion,
118
+ interrupt: () => { try {
119
+ child.kill("SIGTERM");
120
+ }
121
+ catch { /* best effort */ } },
122
+ };
123
+ }
124
+ }
@@ -9,6 +9,7 @@ export interface StructuredSessionManagerRunners {
9
9
  codex?: StructuredRunnerAdapter;
10
10
  opencode?: StructuredRunnerAdapter;
11
11
  grok?: StructuredRunnerAdapter;
12
+ qoder?: StructuredRunnerAdapter;
12
13
  }
13
14
  interface CreateStructuredSessionOptions {
14
15
  cwd: string;
@@ -83,6 +84,7 @@ export declare class StructuredSessionManager {
83
84
  private readonly codexRunner;
84
85
  private readonly openCodeRunner;
85
86
  private readonly grokRunner;
87
+ private readonly qoderRunner;
86
88
  private disposed;
87
89
  constructor(storage: WandStorage, config: WandConfig, logger?: SessionLogger | null, sdkQueryFactory?: typeof sdkQuery, runners?: StructuredSessionManagerRunners);
88
90
  private archiveExpiredSessions;
@@ -13,6 +13,7 @@ import { buildAppendSystemPromptParts, buildClaudeSdkThinking, ClaudeCliRunner,
13
13
  import { captureTaskMeta, extractClaudeAssistantMessage, extractClaudeModelName, normalizeClaudeToolInput, stampParentTaskResults, stampSelfTask, tagSubagentBlocks, } from "./structured-claude-protocol.js";
14
14
  import { OpenCodeRunner } from "./structured-opencode-adapter.js";
15
15
  import { GrokRunner } from "./structured-grok-adapter.js";
16
+ import { QoderRunner } from "./structured-qoder-adapter.js";
16
17
  import { defaultStructuredRunner, defaultStructuredState, isStructuredRunnerForProvider, normalizeThinkingEffort, resolveStructuredRunner, } from "./structured-provider-common.js";
17
18
  import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
18
19
  export { isStructuredRunnerForProvider, normalizeThinkingEffort, resolveStructuredRunner, thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToSdkBudget, } from "./structured-provider-common.js";
@@ -203,6 +204,7 @@ export class StructuredSessionManager {
203
204
  codexRunner;
204
205
  openCodeRunner;
205
206
  grokRunner;
207
+ qoderRunner;
206
208
  disposed = false;
207
209
  constructor(storage, config, logger = null, sdkQueryFactory = sdkQuery, runners = {}) {
208
210
  this.storage = storage;
@@ -213,12 +215,13 @@ export class StructuredSessionManager {
213
215
  this.codexRunner = runners.codex ?? new CodexRunner();
214
216
  this.openCodeRunner = runners.opencode ?? new OpenCodeRunner();
215
217
  this.grokRunner = runners.grok ?? new GrokRunner();
218
+ this.qoderRunner = runners.qoder ?? new QoderRunner();
216
219
  for (const snapshot of this.storage.loadSessions()) {
217
220
  if ((snapshot.sessionKind ?? "pty") !== "structured")
218
221
  continue;
219
222
  const restoredStatus = snapshot.status === "running" ? "idle" : snapshot.status;
220
223
  const storedProvider = snapshot.provider ?? snapshot.structuredState?.provider;
221
- const provider = storedProvider === "codex" || storedProvider === "opencode" || storedProvider === "grok"
224
+ const provider = storedProvider === "codex" || storedProvider === "opencode" || storedProvider === "grok" || storedProvider === "qoder"
222
225
  ? storedProvider
223
226
  : "claude";
224
227
  const storedRunner = snapshot.runner ?? snapshot.structuredState?.runner;
@@ -498,7 +501,7 @@ export class StructuredSessionManager {
498
501
  const id = randomUUID();
499
502
  const startedAt = new Date().toISOString();
500
503
  const requestedProvider = options.provider ?? "claude";
501
- if (requestedProvider !== "claude" && requestedProvider !== "codex" && requestedProvider !== "opencode" && requestedProvider !== "grok") {
504
+ if (requestedProvider !== "claude" && requestedProvider !== "codex" && requestedProvider !== "opencode" && requestedProvider !== "grok" && requestedProvider !== "qoder") {
502
505
  throw new Error(`不支持的结构化 provider: ${String(requestedProvider)}`);
503
506
  }
504
507
  const provider = requestedProvider;
@@ -522,9 +525,11 @@ export class StructuredSessionManager {
522
525
  ? "opencode run --format json"
523
526
  : provider === "grok"
524
527
  ? "grok -p --output-format streaming-json"
525
- : runner === "claude-sdk"
526
- ? "claude-agent-sdk (stream-json)"
527
- : "claude -p --output-format stream-json",
528
+ : provider === "qoder"
529
+ ? "qodercli -p --output-format stream-json"
530
+ : runner === "claude-sdk"
531
+ ? "claude-agent-sdk (stream-json)"
532
+ : "claude -p --output-format stream-json",
528
533
  cwd: worktreeSetup?.cwd ?? baseCwd,
529
534
  mode: options.mode,
530
535
  worktreeEnabled: Boolean(worktreeSetup),
@@ -728,6 +733,15 @@ export class StructuredSessionManager {
728
733
  else if (provider === "grok") {
729
734
  await this.runGrokStreaming(id, updated, prompt, requestId);
730
735
  }
736
+ else if (provider === "qoder") {
737
+ await this.runClaudeStreaming(id, updated, prompt, requestId, {
738
+ runner: this.qoderRunner,
739
+ provider: "qoder",
740
+ commandLabel: "qodercli -p",
741
+ logKind: "qoder-print",
742
+ installHint: "请安装 @qoder-ai/qodercli,或重跑 `wand service:install` 刷新服务的 PATH",
743
+ });
744
+ }
731
745
  else if (runner === "claude-sdk") {
732
746
  await this.runClaudeSdkStreaming(id, updated, prompt, requestId);
733
747
  }
@@ -1632,7 +1646,7 @@ export class StructuredSessionManager {
1632
1646
  * - Root: --permission-mode acceptEdits + --allowedTools (extends approval
1633
1647
  * outside CWD). stdin is always "ignore" — no ACP bidirectional control.
1634
1648
  */
1635
- async runClaudeStreaming(sessionId, session, prompt, requestId) {
1649
+ async runClaudeStreaming(sessionId, session, prompt, requestId, options = {}) {
1636
1650
  let emitTimer = null;
1637
1651
  const syncSnapshot = (turnState) => {
1638
1652
  const current = this.currentSessionForRequest(sessionId, requestId);
@@ -1681,7 +1695,11 @@ export class StructuredSessionManager {
1681
1695
  if (!emitTimer)
1682
1696
  emitTimer = this.trackStreamEmitTimer(setTimeout(flushEmit, STREAM_EMIT_DEBOUNCE_MS));
1683
1697
  };
1684
- const execution = this.claudeCliRunner.start({
1698
+ const runner = options.runner ?? this.claudeCliRunner;
1699
+ const provider = options.provider ?? "claude";
1700
+ const commandLabel = options.commandLabel ?? "claude -p";
1701
+ const logKind = options.logKind ?? "claude-print";
1702
+ const execution = runner.start({
1685
1703
  session,
1686
1704
  prompt,
1687
1705
  env: buildChildEnv(this.config.inheritEnv !== false),
@@ -1697,8 +1715,8 @@ export class StructuredSessionManager {
1697
1715
  });
1698
1716
  this.pendingRunnerExecutions.set(sessionId, execution);
1699
1717
  this.logger?.appendStructuredSpawn(sessionId, {
1700
- kind: "claude-print",
1701
- provider: "claude",
1718
+ kind: logKind,
1719
+ provider,
1702
1720
  pid: execution.pid,
1703
1721
  cwd: session.cwd,
1704
1722
  args: execution.args,
@@ -1724,12 +1742,12 @@ export class StructuredSessionManager {
1724
1742
  flushEmit();
1725
1743
  if (result.spawnError) {
1726
1744
  const hint = result.spawnError.code === "ENOENT"
1727
- ? "(PATH 中找不到 claude 可执行文件;请确认 claude 已安装,或重跑 `wand service:install` 刷新服务的 PATH"
1745
+ ? `(PATH 中找不到 ${provider === "qoder" ? "qodercli" : "claude"} 可执行文件;${options.installHint ?? "请确认 claude 已安装,或重跑 `wand service:install` 刷新服务的 PATH"})`
1728
1746
  : "";
1729
- throw new Error(`claude -p 启动失败:${result.spawnError.message}${hint}`);
1747
+ throw new Error(`${commandLabel} 启动失败:${result.spawnError.message}${hint}`);
1730
1748
  }
1731
1749
  this.logger?.appendStructuredSpawn(sessionId, {
1732
- kind: "claude-print-close",
1750
+ kind: `${logKind}-close`,
1733
1751
  pid: execution.pid,
1734
1752
  spawnedAt: execution.spawnedAt,
1735
1753
  closedAt: new Date().toISOString(),
@@ -1743,9 +1761,10 @@ export class StructuredSessionManager {
1743
1761
  const interruptedForQuestion = result.stopReason === "ask-user-question";
1744
1762
  const failedExit = (result.exitCode !== null && result.exitCode !== 0) || result.signal !== null;
1745
1763
  if (failedExit && !interruptedByUser && !interruptedForQuestion) {
1746
- const errorText = this.formatStructuredExitError("claude -p", result.exitCode, result.signal, {
1764
+ const errorText = this.formatStructuredExitError(commandLabel, result.exitCode, result.signal, {
1747
1765
  stderr: result.stderr,
1748
1766
  stdoutTail: result.stdoutTail,
1767
+ primary: result.primaryError,
1749
1768
  });
1750
1769
  const failed = this.finishStructuredFailure(current, typeof result.exitCode === "number" ? result.exitCode : 1, errorText, result.state);
1751
1770
  this.sessions.set(sessionId, failed);
package/dist/system-ai.js CHANGED
@@ -99,7 +99,7 @@ export function discoverCliSystemAiConfig(preferred, home = os.homedir()) {
99
99
  const discoverers = { claude: discoverClaude, codex: discoverCodex, opencode: discoverOpenCode };
100
100
  const order = [preferred ?? "claude", "claude", "opencode", "codex"];
101
101
  for (const provider of [...new Set(order)]) {
102
- if (provider === "grok")
102
+ if (provider === "grok" || provider === "qoder")
103
103
  continue;
104
104
  const found = discoverers[provider](home);
105
105
  if (found)
package/dist/types.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type SessionKind = "pty" | "structured";
2
- export type SessionProvider = "claude" | "codex" | "opencode" | "grok";
2
+ export type SessionProvider = "claude" | "codex" | "opencode" | "grok" | "qoder";
3
3
  export type CommitAiSource = "cli" | "api";
4
- export type SessionRunner = "claude-cli" | "claude-cli-print" | "claude-sdk" | "codex-cli-exec" | "opencode-cli-run" | "grok-cli-headless" | "pty";
4
+ export type SessionRunner = "claude-cli" | "claude-cli-print" | "claude-sdk" | "codex-cli-exec" | "opencode-cli-run" | "grok-cli-headless" | "qoder-cli-print" | "pty";
5
5
  export type SessionSource = "interactive" | "automation" | "startup";
6
6
  export type ExecutionMode = "assist" | "agent" | "agent-max" | "default" | "auto-edit" | "full-access" | "native" | "managed";
7
7
  export type AutonomyPolicy = "assist" | "agent" | "agent-max";
@@ -114,6 +114,8 @@ export interface WandConfig {
114
114
  defaultOpenCodeModel?: string;
115
115
  /** 新建 Grok 会话时默认使用的模型。留空则不传 --model,由 grok 自行决定。 */
116
116
  defaultGrokModel?: string;
117
+ /** 新建 Qoder 会话时默认使用的模型层级。留空则由 qodercli 自行决定。 */
118
+ defaultQoderModel?: string;
117
119
  /** 快捷提交生成 commit message / tag 时使用的 CLI。 */
118
120
  commitCli?: SessionProvider;
119
121
  /** 快捷提交专用模型。留空则跟随所选 CLI 的默认模型。 */