@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/README.md +20 -4
- package/dist/build-info.json +3 -3
- package/dist/config.d.ts +4 -3
- package/dist/config.js +22 -7
- package/dist/git-quick-commit.js +56 -3
- package/dist/models.d.ts +4 -0
- package/dist/models.js +35 -1
- package/dist/path-repair.d.ts +1 -1
- package/dist/path-repair.js +11 -6
- package/dist/process-manager.d.ts +1 -1
- package/dist/process-manager.js +23 -5
- package/dist/provider-cli-updater.d.ts +38 -0
- package/dist/provider-cli-updater.js +225 -0
- package/dist/pty-text-utils.js +76 -131
- package/dist/server-session-routes.js +4 -4
- package/dist/server.js +117 -5
- package/dist/session-ai-context.d.ts +2 -2
- package/dist/session-ai-context.js +8 -4
- package/dist/storage.js +6 -1
- package/dist/structured-session-manager.d.ts +6 -0
- package/dist/structured-session-manager.js +296 -5
- package/dist/types.d.ts +4 -2
- package/dist/web-ui/content/scripts.js +37 -37
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/package.json +2 -2
|
@@ -13,7 +13,7 @@ import { buildLanguageDirective, buildManagedAutonomyDirective } from "./languag
|
|
|
13
13
|
import { generateSessionTopic } from "./session-topic.js";
|
|
14
14
|
import { resolveSessionCwd } from "./session-cwd.js";
|
|
15
15
|
function defaultStructuredRunner(provider) {
|
|
16
|
-
return provider === "codex" ? "codex-cli-exec" : "claude-cli-print";
|
|
16
|
+
return provider === "codex" ? "codex-cli-exec" : provider === "opencode" ? "opencode-cli-run" : "claude-cli-print";
|
|
17
17
|
}
|
|
18
18
|
function defaultStructuredState(provider, runner = defaultStructuredRunner(provider)) {
|
|
19
19
|
return {
|
|
@@ -75,6 +75,18 @@ export function thinkingEffortToCodexReasoningEffort(effort) {
|
|
|
75
75
|
default: return null;
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
+
/** OpenCode exposes provider-specific reasoning presets through `--variant`. */
|
|
79
|
+
export function thinkingEffortToOpenCodeVariant(effort) {
|
|
80
|
+
if (!effort || effort === "off")
|
|
81
|
+
return null;
|
|
82
|
+
if (effort === "standard")
|
|
83
|
+
return "low";
|
|
84
|
+
if (effort === "deep")
|
|
85
|
+
return "high";
|
|
86
|
+
if (effort === "max")
|
|
87
|
+
return "max";
|
|
88
|
+
return effort.startsWith("codex:") ? effort.slice("codex:".length) || null : null;
|
|
89
|
+
}
|
|
78
90
|
function asRecord(value) {
|
|
79
91
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
80
92
|
? value
|
|
@@ -914,7 +926,7 @@ export class StructuredSessionManager {
|
|
|
914
926
|
const id = randomUUID();
|
|
915
927
|
const startedAt = new Date().toISOString();
|
|
916
928
|
const prompt = options.prompt?.trim();
|
|
917
|
-
const provider = options.provider === "codex"
|
|
929
|
+
const provider = options.provider === "codex" || options.provider === "opencode" ? options.provider : "claude";
|
|
918
930
|
const runner = options.runner ?? defaultStructuredRunner(provider);
|
|
919
931
|
const baseCwd = resolveSessionCwd(options.cwd, this.config.defaultCwd);
|
|
920
932
|
const worktreeSetup = options.worktreeEnabled
|
|
@@ -931,9 +943,11 @@ export class StructuredSessionManager {
|
|
|
931
943
|
runner,
|
|
932
944
|
command: provider === "codex"
|
|
933
945
|
? "codex exec --json"
|
|
934
|
-
:
|
|
935
|
-
? "
|
|
936
|
-
: "claude
|
|
946
|
+
: provider === "opencode"
|
|
947
|
+
? "opencode run --format json"
|
|
948
|
+
: runner === "claude-sdk"
|
|
949
|
+
? "claude-agent-sdk (stream-json)"
|
|
950
|
+
: "claude -p --output-format stream-json",
|
|
937
951
|
cwd: worktreeSetup?.cwd ?? baseCwd,
|
|
938
952
|
mode: options.mode,
|
|
939
953
|
worktreeEnabled: Boolean(worktreeSetup),
|
|
@@ -1122,6 +1136,9 @@ export class StructuredSessionManager {
|
|
|
1122
1136
|
if ((updated.provider ?? "claude") === "codex") {
|
|
1123
1137
|
await this.runCodexStreaming(id, updated, prompt);
|
|
1124
1138
|
}
|
|
1139
|
+
else if ((updated.provider ?? "claude") === "opencode") {
|
|
1140
|
+
await this.runOpenCodeStreaming(id, updated, prompt);
|
|
1141
|
+
}
|
|
1125
1142
|
else if (this.config.structuredRunner === "sdk") {
|
|
1126
1143
|
await this.runClaudeSdkStreaming(id, updated, prompt);
|
|
1127
1144
|
}
|
|
@@ -1850,6 +1867,280 @@ export class StructuredSessionManager {
|
|
|
1850
1867
|
});
|
|
1851
1868
|
});
|
|
1852
1869
|
}
|
|
1870
|
+
buildOpenCodeArgs(session) {
|
|
1871
|
+
const args = ["run", "--format", "json", "--thinking"];
|
|
1872
|
+
const modelChoice = session.selectedModel?.trim();
|
|
1873
|
+
if (modelChoice && modelChoice !== "default") {
|
|
1874
|
+
args.push("--model", modelChoice);
|
|
1875
|
+
}
|
|
1876
|
+
const variant = thinkingEffortToOpenCodeVariant(session.thinkingEffort);
|
|
1877
|
+
if (variant)
|
|
1878
|
+
args.push("--variant", variant);
|
|
1879
|
+
if (session.autoApprovePermissions === true || session.mode === "full-access" || session.mode === "managed" || session.mode === "auto-edit") {
|
|
1880
|
+
args.push("--dangerously-skip-permissions");
|
|
1881
|
+
}
|
|
1882
|
+
if (session.claudeSessionId) {
|
|
1883
|
+
args.push("--session", session.claudeSessionId);
|
|
1884
|
+
}
|
|
1885
|
+
return args;
|
|
1886
|
+
}
|
|
1887
|
+
openCodeToolName(name) {
|
|
1888
|
+
const mapped = {
|
|
1889
|
+
bash: "Bash",
|
|
1890
|
+
shell: "Bash",
|
|
1891
|
+
read: "Read",
|
|
1892
|
+
edit: "Edit",
|
|
1893
|
+
write: "Write",
|
|
1894
|
+
glob: "Glob",
|
|
1895
|
+
grep: "Grep",
|
|
1896
|
+
webfetch: "WebFetch",
|
|
1897
|
+
websearch: "WebSearch",
|
|
1898
|
+
todowrite: "TodoWrite",
|
|
1899
|
+
task: "Task",
|
|
1900
|
+
skill: "Skill",
|
|
1901
|
+
};
|
|
1902
|
+
return mapped[name.toLowerCase()] ?? `OpenCode/${name}`;
|
|
1903
|
+
}
|
|
1904
|
+
applyOpenCodeEvent(turnState, event) {
|
|
1905
|
+
if (typeof event.sessionID === "string" && event.sessionID)
|
|
1906
|
+
turnState.sessionId = event.sessionID;
|
|
1907
|
+
const type = typeof event.type === "string" ? event.type : "";
|
|
1908
|
+
const part = asRecord(event.part);
|
|
1909
|
+
if (!part) {
|
|
1910
|
+
if (type === "error")
|
|
1911
|
+
return this.extractCodexText(event.error) || "OpenCode run failed";
|
|
1912
|
+
return null;
|
|
1913
|
+
}
|
|
1914
|
+
if (type === "text" && typeof part.text === "string" && part.text.trim()) {
|
|
1915
|
+
turnState.blocks.push({ type: "text", text: part.text });
|
|
1916
|
+
turnState.result += (turnState.result ? "\n" : "") + part.text;
|
|
1917
|
+
return null;
|
|
1918
|
+
}
|
|
1919
|
+
if (type === "reasoning" && typeof part.text === "string" && part.text.trim()) {
|
|
1920
|
+
turnState.blocks.push({ type: "thinking", thinking: part.text });
|
|
1921
|
+
return null;
|
|
1922
|
+
}
|
|
1923
|
+
if (type === "tool_use") {
|
|
1924
|
+
const state = asRecord(part.state) ?? {};
|
|
1925
|
+
const tool = typeof part.tool === "string" && part.tool ? part.tool : "tool";
|
|
1926
|
+
const toolId = typeof part.callID === "string" && part.callID
|
|
1927
|
+
? part.callID
|
|
1928
|
+
: typeof part.id === "string" && part.id
|
|
1929
|
+
? part.id
|
|
1930
|
+
: randomUUID();
|
|
1931
|
+
const input = asRecord(state.input) ?? {};
|
|
1932
|
+
turnState.blocks.push({
|
|
1933
|
+
type: "tool_use",
|
|
1934
|
+
id: toolId,
|
|
1935
|
+
name: this.openCodeToolName(tool),
|
|
1936
|
+
description: typeof state.title === "string" ? state.title : undefined,
|
|
1937
|
+
input,
|
|
1938
|
+
});
|
|
1939
|
+
const failed = state.status === "error";
|
|
1940
|
+
const content = failed
|
|
1941
|
+
? (typeof state.error === "string" ? state.error : "OpenCode tool failed")
|
|
1942
|
+
: (typeof state.output === "string" ? state.output : "");
|
|
1943
|
+
turnState.blocks.push({ type: "tool_result", tool_use_id: toolId, content, is_error: failed });
|
|
1944
|
+
return null;
|
|
1945
|
+
}
|
|
1946
|
+
if (type === "step_finish") {
|
|
1947
|
+
const tokens = asRecord(part.tokens);
|
|
1948
|
+
const cache = asRecord(tokens?.cache);
|
|
1949
|
+
const previous = turnState.usage ?? {};
|
|
1950
|
+
turnState.usage = {
|
|
1951
|
+
inputTokens: (previous.inputTokens ?? 0) + (typeof tokens?.input === "number" ? tokens.input : 0),
|
|
1952
|
+
outputTokens: (previous.outputTokens ?? 0) + (typeof tokens?.output === "number" ? tokens.output : 0),
|
|
1953
|
+
reasoningOutputTokens: (previous.reasoningOutputTokens ?? 0) + (typeof tokens?.reasoning === "number" ? tokens.reasoning : 0),
|
|
1954
|
+
cacheReadInputTokens: (previous.cacheReadInputTokens ?? 0) + (typeof cache?.read === "number" ? cache.read : 0),
|
|
1955
|
+
cacheCreationInputTokens: (previous.cacheCreationInputTokens ?? 0) + (typeof cache?.write === "number" ? cache.write : 0),
|
|
1956
|
+
totalCostUsd: (previous.totalCostUsd ?? 0) + (typeof part.cost === "number" ? part.cost : 0),
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
return null;
|
|
1960
|
+
}
|
|
1961
|
+
runOpenCodeStreaming(sessionId, session, prompt) {
|
|
1962
|
+
this.userStopped.delete(sessionId);
|
|
1963
|
+
return new Promise((resolve, reject) => {
|
|
1964
|
+
const args = this.buildOpenCodeArgs(session);
|
|
1965
|
+
const spawnedAt = new Date().toISOString();
|
|
1966
|
+
const child = spawn("opencode", args, {
|
|
1967
|
+
cwd: session.cwd,
|
|
1968
|
+
env: buildChildEnv(this.config.inheritEnv !== false),
|
|
1969
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1970
|
+
});
|
|
1971
|
+
this.logger?.appendStructuredSpawn(sessionId, {
|
|
1972
|
+
kind: "opencode-run",
|
|
1973
|
+
provider: "opencode",
|
|
1974
|
+
pid: child.pid ?? null,
|
|
1975
|
+
cwd: session.cwd,
|
|
1976
|
+
args,
|
|
1977
|
+
prompt: prompt.slice(0, 2048),
|
|
1978
|
+
promptLength: prompt.length,
|
|
1979
|
+
sessionId: session.claudeSessionId,
|
|
1980
|
+
spawnedAt,
|
|
1981
|
+
});
|
|
1982
|
+
this.pendingChildren.set(sessionId, child);
|
|
1983
|
+
child.stdin?.end(prompt);
|
|
1984
|
+
const turnState = {
|
|
1985
|
+
blocks: [],
|
|
1986
|
+
result: "",
|
|
1987
|
+
sessionId: session.claudeSessionId,
|
|
1988
|
+
model: session.selectedModel ?? session.structuredState?.model,
|
|
1989
|
+
usage: undefined,
|
|
1990
|
+
codexBlockIndex: new Map(),
|
|
1991
|
+
cwd: session.cwd,
|
|
1992
|
+
};
|
|
1993
|
+
let lineBuf = "";
|
|
1994
|
+
let stderr = "";
|
|
1995
|
+
let primaryError = null;
|
|
1996
|
+
let emitTimer = null;
|
|
1997
|
+
const syncSnapshot = () => {
|
|
1998
|
+
const current = this.sessions.get(sessionId);
|
|
1999
|
+
if (!current)
|
|
2000
|
+
return;
|
|
2001
|
+
const turn = {
|
|
2002
|
+
role: "assistant",
|
|
2003
|
+
content: this.compactContentBlocks([...turnState.blocks], turnState.result),
|
|
2004
|
+
usage: turnState.usage,
|
|
2005
|
+
};
|
|
2006
|
+
const messages = [...(current.messages ?? [])];
|
|
2007
|
+
if (messages[messages.length - 1]?.role === "assistant")
|
|
2008
|
+
messages[messages.length - 1] = turn;
|
|
2009
|
+
else
|
|
2010
|
+
messages.push(turn);
|
|
2011
|
+
const patched = {
|
|
2012
|
+
...current,
|
|
2013
|
+
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
2014
|
+
messages,
|
|
2015
|
+
output: turnState.result || current.output,
|
|
2016
|
+
};
|
|
2017
|
+
this.sessions.set(sessionId, patched);
|
|
2018
|
+
this.saveStreamingSnapshot(patched);
|
|
2019
|
+
};
|
|
2020
|
+
const flushEmit = () => {
|
|
2021
|
+
if (emitTimer)
|
|
2022
|
+
clearTimeout(emitTimer);
|
|
2023
|
+
emitTimer = null;
|
|
2024
|
+
const current = this.sessions.get(sessionId);
|
|
2025
|
+
if (current)
|
|
2026
|
+
this.emit({ type: "output", sessionId, data: buildIncrementalStructuredPayload(current, this.config.cardDefaults ?? {}) });
|
|
2027
|
+
};
|
|
2028
|
+
const scheduleEmit = () => {
|
|
2029
|
+
if (!emitTimer)
|
|
2030
|
+
emitTimer = setTimeout(flushEmit, STREAM_EMIT_DEBOUNCE_MS);
|
|
2031
|
+
};
|
|
2032
|
+
const processLine = (line) => {
|
|
2033
|
+
const trimmed = line.trim();
|
|
2034
|
+
if (!trimmed)
|
|
2035
|
+
return;
|
|
2036
|
+
let event;
|
|
2037
|
+
try {
|
|
2038
|
+
event = JSON.parse(trimmed);
|
|
2039
|
+
}
|
|
2040
|
+
catch {
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
this.logger?.appendStreamEvent(sessionId, event);
|
|
2044
|
+
const error = this.applyOpenCodeEvent(turnState, event);
|
|
2045
|
+
if (error)
|
|
2046
|
+
primaryError = error;
|
|
2047
|
+
syncSnapshot();
|
|
2048
|
+
scheduleEmit();
|
|
2049
|
+
};
|
|
2050
|
+
child.stdout?.on("data", (chunk) => {
|
|
2051
|
+
const text = chunk.toString();
|
|
2052
|
+
this.logger?.appendStructuredStdout(sessionId, text);
|
|
2053
|
+
lineBuf += text;
|
|
2054
|
+
const lines = lineBuf.split("\n");
|
|
2055
|
+
lineBuf = lines.pop() ?? "";
|
|
2056
|
+
for (const line of lines)
|
|
2057
|
+
processLine(line);
|
|
2058
|
+
});
|
|
2059
|
+
child.stderr?.on("data", (chunk) => {
|
|
2060
|
+
const text = chunk.toString();
|
|
2061
|
+
this.logger?.appendStructuredStderr(sessionId, text);
|
|
2062
|
+
stderr += text;
|
|
2063
|
+
});
|
|
2064
|
+
child.on("error", (error) => {
|
|
2065
|
+
this.pendingChildren.delete(sessionId);
|
|
2066
|
+
this.lastStreamSaveAt.delete(sessionId);
|
|
2067
|
+
if (emitTimer)
|
|
2068
|
+
clearTimeout(emitTimer);
|
|
2069
|
+
const nodeError = error;
|
|
2070
|
+
const hint = nodeError.code === "ENOENT"
|
|
2071
|
+
? "(PATH 中找不到 opencode;请安装 opencode-ai,或重跑 `wand service:install` 刷新服务 PATH)"
|
|
2072
|
+
: "";
|
|
2073
|
+
reject(new Error(`opencode run 启动失败:${error.message}${hint}`));
|
|
2074
|
+
});
|
|
2075
|
+
child.on("close", (code, signal) => {
|
|
2076
|
+
this.pendingChildren.delete(sessionId);
|
|
2077
|
+
this.lastStreamSaveAt.delete(sessionId);
|
|
2078
|
+
if (lineBuf.trim())
|
|
2079
|
+
processLine(lineBuf);
|
|
2080
|
+
flushEmit();
|
|
2081
|
+
const current = this.sessions.get(sessionId);
|
|
2082
|
+
if (!current) {
|
|
2083
|
+
reject(new Error("Session removed during execution."));
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
const interruptedByUser = this.interruptedWith.has(sessionId);
|
|
2087
|
+
const interruptPrompt = this.interruptedWith.get(sessionId);
|
|
2088
|
+
const userStopped = this.userStopped.delete(sessionId);
|
|
2089
|
+
if ((primaryError || (code !== 0 && code !== null) || signal) && !interruptedByUser && !userStopped) {
|
|
2090
|
+
const legacyHint = /unknown command|unknown flag|No help topic for 'run'/i.test(stderr)
|
|
2091
|
+
? "\n检测到旧版 OpenCode CLI;请卸载 0.0.x 旧包并安装 `opencode-ai@latest`。"
|
|
2092
|
+
: "";
|
|
2093
|
+
const errorText = this.formatStructuredExitError("opencode run", code, signal, { stderr, primary: primaryError }) + legacyHint;
|
|
2094
|
+
const failed = this.finishStructuredFailure(current, typeof code === "number" ? code : 1, errorText, turnState);
|
|
2095
|
+
this.sessions.set(sessionId, failed);
|
|
2096
|
+
this.storage.saveSession(failed);
|
|
2097
|
+
this.emitStructuredSnapshot(failed);
|
|
2098
|
+
this.emitStructuredSnapshot(failed, "ended");
|
|
2099
|
+
reject(new Error(errorText));
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
const messages = this.buildCompletedAssistantMessages(current, turnState);
|
|
2103
|
+
const keepRunning = !!interruptPrompt;
|
|
2104
|
+
const finished = {
|
|
2105
|
+
...current,
|
|
2106
|
+
status: keepRunning ? "running" : "idle",
|
|
2107
|
+
exitCode: keepRunning ? null : 0,
|
|
2108
|
+
endedAt: keepRunning ? null : new Date().toISOString(),
|
|
2109
|
+
output: turnState.result,
|
|
2110
|
+
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
2111
|
+
messages,
|
|
2112
|
+
queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
|
|
2113
|
+
pendingEscalation: null,
|
|
2114
|
+
permissionBlocked: false,
|
|
2115
|
+
structuredState: {
|
|
2116
|
+
...current.structuredState,
|
|
2117
|
+
model: turnState.model ?? current.structuredState?.model,
|
|
2118
|
+
inFlight: false,
|
|
2119
|
+
activeRequestId: null,
|
|
2120
|
+
lastError: null,
|
|
2121
|
+
},
|
|
2122
|
+
};
|
|
2123
|
+
this.sessions.set(sessionId, finished);
|
|
2124
|
+
this.storage.saveSession(finished);
|
|
2125
|
+
this.emitStructuredSnapshot(finished);
|
|
2126
|
+
if (!keepRunning)
|
|
2127
|
+
this.emitStructuredSnapshot(finished, "ended");
|
|
2128
|
+
if (interruptPrompt) {
|
|
2129
|
+
this.interruptedWith.delete(sessionId);
|
|
2130
|
+
this.preserveQueueOnInterrupt.delete(sessionId);
|
|
2131
|
+
resolve();
|
|
2132
|
+
setImmediate(() => {
|
|
2133
|
+
this.sendMessage(sessionId, interruptPrompt).catch((error) => {
|
|
2134
|
+
console.error("[WAND] opencode interrupt-and-send failed:", error);
|
|
2135
|
+
});
|
|
2136
|
+
});
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
resolve();
|
|
2140
|
+
setImmediate(() => { void this.flushNextQueuedMessage(sessionId); });
|
|
2141
|
+
});
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
1853
2144
|
// ---------------------------------------------------------------------------
|
|
1854
2145
|
// Streaming claude -p execution
|
|
1855
2146
|
// ---------------------------------------------------------------------------
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type SessionKind = "pty" | "structured";
|
|
2
|
-
export type SessionProvider = "claude" | "codex";
|
|
3
|
-
export type SessionRunner = "claude-cli" | "claude-cli-print" | "claude-sdk" | "codex-cli-exec" | "pty";
|
|
2
|
+
export type SessionProvider = "claude" | "codex" | "opencode";
|
|
3
|
+
export type SessionRunner = "claude-cli" | "claude-cli-print" | "claude-sdk" | "codex-cli-exec" | "opencode-cli-run" | "pty";
|
|
4
4
|
export type SessionSource = "interactive" | "automation" | "startup";
|
|
5
5
|
export type ExecutionMode = "assist" | "agent" | "agent-max" | "default" | "auto-edit" | "full-access" | "native" | "managed";
|
|
6
6
|
export type AutonomyPolicy = "assist" | "agent" | "agent-max";
|
|
@@ -109,6 +109,8 @@ export interface WandConfig {
|
|
|
109
109
|
defaultModel?: string;
|
|
110
110
|
/** 新建 Codex 会话时默认使用的模型。留空则不传 --model,由 codex 自行决定。 */
|
|
111
111
|
defaultCodexModel?: string;
|
|
112
|
+
/** 新建 OpenCode 会话时默认使用的 provider/model。留空则由 opencode 自行决定。 */
|
|
113
|
+
defaultOpenCodeModel?: string;
|
|
112
114
|
/** 快捷提交生成 commit message / tag 时使用的 CLI。 */
|
|
113
115
|
commitCli?: SessionProvider;
|
|
114
116
|
/** 快捷提交专用模型。留空则跟随所选 CLI 的默认模型。 */
|