@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
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
|
|
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.
|
|
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 sessions for Claude Code, Codex,
|
|
59
|
+
- **Multiple providers** — create PTY or structured sessions for Claude Code, Codex, and OpenCode
|
|
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
|
|
|
@@ -73,6 +73,7 @@ wand config:password
|
|
|
73
73
|
- **Native clients** — Android, iOS, and macOS clients with encrypted connection codes and update checks
|
|
74
74
|
- **HTTPS** — optional self-signed certificates for remote or mobile access
|
|
75
75
|
- **Update channels** — built-in stable/beta update checks and upgrade prompts
|
|
76
|
+
- **CLI updates** — check and quickly update Claude Code, Codex, and OpenCode from server settings, with optional automatic updates
|
|
76
77
|
|
|
77
78
|
### Configuration
|
|
78
79
|
|
|
@@ -143,11 +144,18 @@ git submodule update --init
|
|
|
143
144
|
|
|
144
145
|
Runtime data is stored under `~/.wand/`: `config.json`, `wand.db`, and `sessions/`.
|
|
145
146
|
|
|
147
|
+
OpenCode structured sessions require the current `opencode-ai` CLI (0.1 or newer). Remove the unrelated legacy `opencode` 0.0.x package before installing it:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npm uninstall -g opencode
|
|
151
|
+
npm install -g opencode-ai@latest
|
|
152
|
+
```
|
|
153
|
+
|
|
146
154
|
## 中文
|
|
147
155
|
|
|
148
156
|
### 概览
|
|
149
157
|
|
|
150
|
-
通过浏览器远程访问和管理本地 CLI 工具的 Web
|
|
158
|
+
通过浏览器远程访问和管理本地 CLI 工具的 Web 控制台。支持 [Claude Code](https://docs.anthropic.com/en/docs/claude-code)、[Codex](https://github.com/openai/codex) 和 [OpenCode](https://opencode.ai/),提供终端和结构化对话双视图、会话持久化与恢复、权限管控、文件浏览和多平台客户端。
|
|
151
159
|
|
|
152
160
|
<p align="center">
|
|
153
161
|
<img src="docs/screenshots/clients-overview.png" width="100%" alt="Web 居中,iOS 和 Android 客户端在两侧的多端详情总览" />
|
|
@@ -185,12 +193,19 @@ wand web
|
|
|
185
193
|
wand config:password
|
|
186
194
|
```
|
|
187
195
|
|
|
196
|
+
OpenCode 结构化会话需要当前的 `opencode-ai` CLI(0.1 或更高版本)。如果装过无关的旧版 `opencode` 0.0.x 包,请先替换:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
npm uninstall -g opencode
|
|
200
|
+
npm install -g opencode-ai@latest
|
|
201
|
+
```
|
|
202
|
+
|
|
188
203
|
### 功能
|
|
189
204
|
|
|
190
205
|
#### 核心
|
|
191
206
|
|
|
192
207
|
- **双视图模式** — 终端原始输出和结构化对话视图可随时切换,同一会话两种呈现
|
|
193
|
-
- **多 Provider 支持** —
|
|
208
|
+
- **多 Provider 支持** — Claude Code、Codex 和 OpenCode 均可创建 PTY 或结构化会话
|
|
194
209
|
- **会话管理** — 创建、归档、恢复会话;支持从 Claude 原生历史记录恢复;会话列表显示摘要
|
|
195
210
|
- **权限控制** — 可视化权限提示,支持逐次确认、单次批准、本轮记忆等策略
|
|
196
211
|
|
|
@@ -207,6 +222,7 @@ wand config:password
|
|
|
207
222
|
- **多平台客户端** — Android / iOS / macOS 原生客户端,支持加密连接码分发、自动更新检查
|
|
208
223
|
- **HTTPS** — 可选自签证书,适合远程或移动端访问
|
|
209
224
|
- **版本管理** — 内置更新检查与升级提示,支持 stable/beta 双通道
|
|
225
|
+
- **CLI 更新** — 在服务端设置中检查并快速更新 Claude Code、Codex、OpenCode,也可开启自动更新
|
|
210
226
|
|
|
211
227
|
### 截图
|
|
212
228
|
|
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-07-
|
|
4
|
-
"version": "2.
|
|
2
|
+
"commit": "f1b77e3be3f2f0b4a1a158e3fc2020ccbc590c68",
|
|
3
|
+
"builtAt": "2026-07-12T10:02:57.593Z",
|
|
4
|
+
"version": "2.13.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", "commitCli", "commitModel", "defaultThinkingEffort", "structuredRunner", "language", "cardDefaults", "inheritEnv"];
|
|
10
|
+
export declare const PREFERENCE_KEYS: readonly ["defaultProvider", "defaultSessionKind", "defaultMode", "defaultCwd", "defaultModel", "defaultCodexModel", "defaultOpenCodeModel", "commitCli", "commitModel", "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;
|
|
@@ -40,9 +40,10 @@ export declare function applyStoragePreferences(config: WandConfig, storage: Wan
|
|
|
40
40
|
export declare function writePreferenceToStorage(config: WandConfig, storage: WandStorage, key: PreferenceKey, value: unknown): void;
|
|
41
41
|
export declare function normalizeCardDefaults(input: unknown): CardExpandDefaults;
|
|
42
42
|
export declare function isExecutionMode(value: unknown): value is ExecutionMode;
|
|
43
|
-
export declare function getProviderDefaultModels(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel">): {
|
|
43
|
+
export declare function getProviderDefaultModels(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel">): {
|
|
44
44
|
claude: string;
|
|
45
45
|
codex: string;
|
|
46
|
+
opencode: string;
|
|
46
47
|
};
|
|
47
|
-
export declare function getDefaultModelForProvider(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel">, provider: "claude" | "codex" | undefined): string;
|
|
48
|
+
export declare function getDefaultModelForProvider(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel">, provider: "claude" | "codex" | "opencode" | undefined): string;
|
|
48
49
|
export declare function normalizeMode(input: string | undefined, fallback: ExecutionMode): ExecutionMode;
|
package/dist/config.js
CHANGED
|
@@ -27,6 +27,7 @@ export const PREFERENCE_KEYS = [
|
|
|
27
27
|
"defaultCwd",
|
|
28
28
|
"defaultModel",
|
|
29
29
|
"defaultCodexModel",
|
|
30
|
+
"defaultOpenCodeModel",
|
|
30
31
|
"commitCli",
|
|
31
32
|
"commitModel",
|
|
32
33
|
"defaultThinkingEffort",
|
|
@@ -66,6 +67,7 @@ export const defaultConfig = () => ({
|
|
|
66
67
|
cardDefaults: defaultCardExpandDefaults(),
|
|
67
68
|
defaultModel: "",
|
|
68
69
|
defaultCodexModel: "",
|
|
70
|
+
defaultOpenCodeModel: "",
|
|
69
71
|
commitCli: "claude",
|
|
70
72
|
commitModel: "",
|
|
71
73
|
defaultThinkingEffort: "off",
|
|
@@ -254,7 +256,7 @@ export function applyStoragePreferences(config, storage) {
|
|
|
254
256
|
const defaults = defaultConfig();
|
|
255
257
|
if (storage.hasPreference(preferenceStorageKey("defaultProvider"))) {
|
|
256
258
|
const v = storage.getPreference(preferenceStorageKey("defaultProvider"), defaults.defaultProvider ?? "claude");
|
|
257
|
-
if (v === "claude" || v === "codex")
|
|
259
|
+
if (v === "claude" || v === "codex" || v === "opencode")
|
|
258
260
|
config.defaultProvider = v;
|
|
259
261
|
}
|
|
260
262
|
if (storage.hasPreference(preferenceStorageKey("defaultSessionKind"))) {
|
|
@@ -282,9 +284,14 @@ export function applyStoragePreferences(config, storage) {
|
|
|
282
284
|
if (typeof v === "string")
|
|
283
285
|
config.defaultCodexModel = v.trim();
|
|
284
286
|
}
|
|
287
|
+
if (storage.hasPreference(preferenceStorageKey("defaultOpenCodeModel"))) {
|
|
288
|
+
const v = storage.getPreference(preferenceStorageKey("defaultOpenCodeModel"), defaults.defaultOpenCodeModel ?? "");
|
|
289
|
+
if (typeof v === "string")
|
|
290
|
+
config.defaultOpenCodeModel = v.trim();
|
|
291
|
+
}
|
|
285
292
|
if (storage.hasPreference(preferenceStorageKey("commitCli"))) {
|
|
286
293
|
const v = storage.getPreference(preferenceStorageKey("commitCli"), defaults.commitCli ?? "claude");
|
|
287
|
-
if (v === "claude" || v === "codex")
|
|
294
|
+
if (v === "claude" || v === "codex" || v === "opencode")
|
|
288
295
|
config.commitCli = v;
|
|
289
296
|
}
|
|
290
297
|
if (storage.hasPreference(preferenceStorageKey("commitModel"))) {
|
|
@@ -322,7 +329,7 @@ export function writePreferenceToStorage(config, storage, key, value) {
|
|
|
322
329
|
const dbKey = preferenceStorageKey(key);
|
|
323
330
|
switch (key) {
|
|
324
331
|
case "defaultProvider": {
|
|
325
|
-
if (value !== "claude" && value !== "codex")
|
|
332
|
+
if (value !== "claude" && value !== "codex" && value !== "opencode")
|
|
326
333
|
throw new Error(`无效 Provider: ${value}`);
|
|
327
334
|
storage.setPreference(dbKey, value);
|
|
328
335
|
config.defaultProvider = value;
|
|
@@ -360,8 +367,14 @@ export function writePreferenceToStorage(config, storage, key, value) {
|
|
|
360
367
|
config.defaultCodexModel = v;
|
|
361
368
|
break;
|
|
362
369
|
}
|
|
370
|
+
case "defaultOpenCodeModel": {
|
|
371
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
372
|
+
storage.setPreference(dbKey, v);
|
|
373
|
+
config.defaultOpenCodeModel = v;
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
363
376
|
case "commitCli": {
|
|
364
|
-
if (value !== "claude" && value !== "codex")
|
|
377
|
+
if (value !== "claude" && value !== "codex" && value !== "opencode")
|
|
365
378
|
throw new Error(`无效 commit CLI: ${value}`);
|
|
366
379
|
storage.setPreference(dbKey, value);
|
|
367
380
|
config.commitCli = value;
|
|
@@ -541,11 +554,12 @@ function mergeWithDefaults(input) {
|
|
|
541
554
|
android: normalizeAndroidApkConfig(input.android) ?? defaults.android,
|
|
542
555
|
macos: normalizeMacosDmgConfig(input.macos) ?? defaults.macos,
|
|
543
556
|
cardDefaults: normalizeCardDefaults(input.cardDefaults),
|
|
544
|
-
defaultProvider: input.defaultProvider === "codex"
|
|
557
|
+
defaultProvider: input.defaultProvider === "codex" || input.defaultProvider === "opencode" ? input.defaultProvider : "claude",
|
|
545
558
|
defaultSessionKind: input.defaultSessionKind === "pty" ? "pty" : "structured",
|
|
546
559
|
defaultModel: typeof input.defaultModel === "string" ? input.defaultModel.trim() : defaults.defaultModel,
|
|
547
560
|
defaultCodexModel: typeof input.defaultCodexModel === "string" ? input.defaultCodexModel.trim() : defaults.defaultCodexModel,
|
|
548
|
-
|
|
561
|
+
defaultOpenCodeModel: typeof input.defaultOpenCodeModel === "string" ? input.defaultOpenCodeModel.trim() : defaults.defaultOpenCodeModel,
|
|
562
|
+
commitCli: input.commitCli === "codex" || input.commitCli === "opencode" ? input.commitCli : "claude",
|
|
549
563
|
commitModel: typeof input.commitModel === "string" ? input.commitModel.trim() : defaults.commitModel,
|
|
550
564
|
defaultThinkingEffort: isThinkingEffort(input.defaultThinkingEffort) ? input.defaultThinkingEffort : "off",
|
|
551
565
|
structuredRunner: (input.structuredRunner === "sdk" || input.structuredRunner === "cli") ? input.structuredRunner : defaults.structuredRunner,
|
|
@@ -559,11 +573,12 @@ export function getProviderDefaultModels(config) {
|
|
|
559
573
|
return {
|
|
560
574
|
claude: (config.defaultModel ?? "").trim(),
|
|
561
575
|
codex: (config.defaultCodexModel ?? "").trim(),
|
|
576
|
+
opencode: (config.defaultOpenCodeModel ?? "").trim(),
|
|
562
577
|
};
|
|
563
578
|
}
|
|
564
579
|
export function getDefaultModelForProvider(config, provider) {
|
|
565
580
|
const defaults = getProviderDefaultModels(config);
|
|
566
|
-
return provider === "codex" ? defaults.codex : defaults.claude;
|
|
581
|
+
return provider === "codex" ? defaults.codex : provider === "opencode" ? defaults.opencode : defaults.claude;
|
|
567
582
|
}
|
|
568
583
|
export function normalizeMode(input, fallback) {
|
|
569
584
|
return isExecutionMode(input) ? input : fallback;
|
package/dist/git-quick-commit.js
CHANGED
|
@@ -3,7 +3,7 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import { ClaudeRunError, runClaudePrint } from "./claude-sdk-runner.js";
|
|
4
4
|
import { buildChildEnv } from "./env-utils.js";
|
|
5
5
|
import { runGit as runGitBase, runGitRaw as runGitRawBase, getGitErrorMessage } from "./git-utils.js";
|
|
6
|
-
import { thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort } from "./structured-session-manager.js";
|
|
6
|
+
import { thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant } from "./structured-session-manager.js";
|
|
7
7
|
const GIT_TIMEOUT_MS = 1500;
|
|
8
8
|
const GIT_PUSH_TIMEOUT_MS = 30_000;
|
|
9
9
|
const MAX_FILE_ENTRIES = 200;
|
|
@@ -281,7 +281,7 @@ async function callClaudeText(prompt, cwd, language, model) {
|
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
283
|
function normalizeProvider(provider) {
|
|
284
|
-
return provider === "codex"
|
|
284
|
+
return provider === "codex" || provider === "opencode" ? provider : "claude";
|
|
285
285
|
}
|
|
286
286
|
function stripFences(raw) {
|
|
287
287
|
return raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
@@ -315,6 +315,24 @@ function extractCodexText(stdout) {
|
|
|
315
315
|
}
|
|
316
316
|
return "";
|
|
317
317
|
}
|
|
318
|
+
function extractOpenCodeText(stdout) {
|
|
319
|
+
const texts = [];
|
|
320
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
321
|
+
const trimmed = line.trim();
|
|
322
|
+
if (!trimmed.startsWith("{"))
|
|
323
|
+
continue;
|
|
324
|
+
try {
|
|
325
|
+
const parsed = JSON.parse(trimmed);
|
|
326
|
+
if (parsed.type === "text" && typeof parsed.part?.text === "string" && parsed.part.text.trim()) {
|
|
327
|
+
texts.push(parsed.part.text.trim());
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
// ignore diagnostics mixed into stdout
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return texts.join("\n").trim();
|
|
335
|
+
}
|
|
318
336
|
function runCliText(command, args, prompt, opts) {
|
|
319
337
|
return new Promise((resolve, reject) => {
|
|
320
338
|
const child = spawn(command, args, {
|
|
@@ -374,10 +392,31 @@ async function callCodexText(prompt, cwd, opts) {
|
|
|
374
392
|
}
|
|
375
393
|
return text;
|
|
376
394
|
}
|
|
395
|
+
async function callOpenCodeText(prompt, cwd, opts) {
|
|
396
|
+
const args = ["run", "--format", "json"];
|
|
397
|
+
const model = opts.model?.trim();
|
|
398
|
+
if (model && model !== "default")
|
|
399
|
+
args.push("--model", model);
|
|
400
|
+
const variant = thinkingEffortToOpenCodeVariant(opts.thinkingEffort ?? "off");
|
|
401
|
+
if (variant)
|
|
402
|
+
args.push("--variant", variant);
|
|
403
|
+
const stdout = await runCliText("opencode", args, prompt, {
|
|
404
|
+
cwd,
|
|
405
|
+
timeoutMs: CODEX_MESSAGE_TIMEOUT_MS,
|
|
406
|
+
inheritEnv: opts.inheritEnv,
|
|
407
|
+
});
|
|
408
|
+
const text = extractOpenCodeText(stdout);
|
|
409
|
+
if (!text)
|
|
410
|
+
throw new QuickCommitError("OpenCode 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
|
|
411
|
+
return text;
|
|
412
|
+
}
|
|
377
413
|
async function callAiText(prompt, cwd, language, opts) {
|
|
378
|
-
|
|
414
|
+
const provider = normalizeProvider(opts.provider);
|
|
415
|
+
if (provider === "codex") {
|
|
379
416
|
return callCodexText(prompt, cwd, opts);
|
|
380
417
|
}
|
|
418
|
+
if (provider === "opencode")
|
|
419
|
+
return callOpenCodeText(prompt, cwd, opts);
|
|
381
420
|
return callClaudeText(prompt, cwd, language, opts.model);
|
|
382
421
|
}
|
|
383
422
|
function collectStagedDiff(cwd) {
|
|
@@ -957,6 +996,20 @@ async function runQuickCommitFallbackCli(opts, priorError) {
|
|
|
957
996
|
inheritEnv: opts.inheritEnv,
|
|
958
997
|
});
|
|
959
998
|
}
|
|
999
|
+
else if (provider === "opencode") {
|
|
1000
|
+
const args = ["run", "--format", "json", "--dangerously-skip-permissions"];
|
|
1001
|
+
const model = opts.model?.trim();
|
|
1002
|
+
if (model && model !== "default")
|
|
1003
|
+
args.push("--model", model);
|
|
1004
|
+
const variant = thinkingEffortToOpenCodeVariant(opts.thinkingEffort ?? "off");
|
|
1005
|
+
if (variant)
|
|
1006
|
+
args.push("--variant", variant);
|
|
1007
|
+
await runCliText("opencode", args, prompt, {
|
|
1008
|
+
cwd: opts.cwd,
|
|
1009
|
+
timeoutMs: QUICK_COMMIT_CLI_TIMEOUT_MS,
|
|
1010
|
+
inheritEnv: opts.inheritEnv,
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
960
1013
|
else {
|
|
961
1014
|
const args = ["-p", "--verbose", "--output-format", "stream-json"];
|
|
962
1015
|
const model = opts.model?.trim();
|
package/dist/models.d.ts
CHANGED
|
@@ -2,9 +2,13 @@ import { ClaudeModelInfo } from "./types.js";
|
|
|
2
2
|
interface ModelCache {
|
|
3
3
|
models: ClaudeModelInfo[];
|
|
4
4
|
codexModels: ClaudeModelInfo[];
|
|
5
|
+
opencodeModels: ClaudeModelInfo[];
|
|
5
6
|
claudeVersion: string | null;
|
|
7
|
+
opencodeVersion: string | null;
|
|
6
8
|
refreshedAt: string;
|
|
7
9
|
}
|
|
10
|
+
/** Parse `opencode models`, whose stable machine-friendly output is one provider/model id per line. */
|
|
11
|
+
export declare function parseOpenCodeModels(stdout: string): ClaudeModelInfo[];
|
|
8
12
|
/** Parse the machine-readable model registry emitted by the installed Codex CLI. */
|
|
9
13
|
export declare function parseCodexModels(stdout: string): ClaudeModelInfo[];
|
|
10
14
|
export declare function getCachedModels(): ModelCache;
|
package/dist/models.js
CHANGED
|
@@ -15,6 +15,9 @@ const CLAUDE_MODELS = [
|
|
|
15
15
|
const CODEX_FALLBACK_MODELS = [
|
|
16
16
|
{ id: "default", label: "GPT-5.5 · gpt-5.5(Codex 默认)", alias: true },
|
|
17
17
|
];
|
|
18
|
+
const OPENCODE_FALLBACK_MODELS = [
|
|
19
|
+
{ id: "default", label: "跟随 OpenCode 默认", alias: true },
|
|
20
|
+
];
|
|
18
21
|
let cache = null;
|
|
19
22
|
function cloneClaudeModels() {
|
|
20
23
|
return CLAUDE_MODELS.map((m) => ({ ...m }));
|
|
@@ -37,6 +40,32 @@ async function probeCodexModels() {
|
|
|
37
40
|
return CODEX_FALLBACK_MODELS.map((m) => ({ ...m }));
|
|
38
41
|
}
|
|
39
42
|
}
|
|
43
|
+
async function probeOpenCode() {
|
|
44
|
+
const [modelsResult, versionResult] = await Promise.allSettled([
|
|
45
|
+
execAsync("opencode models", { timeout: 8000 }),
|
|
46
|
+
execAsync("opencode --version", { timeout: 5000 }),
|
|
47
|
+
]);
|
|
48
|
+
const models = modelsResult.status === "fulfilled"
|
|
49
|
+
? parseOpenCodeModels(modelsResult.value.stdout)
|
|
50
|
+
: OPENCODE_FALLBACK_MODELS.map((m) => ({ ...m }));
|
|
51
|
+
const version = versionResult.status === "fulfilled"
|
|
52
|
+
? extractSemver(versionResult.value.stdout) ?? (versionResult.value.stdout.trim().slice(0, 64) || null)
|
|
53
|
+
: null;
|
|
54
|
+
return { models, version };
|
|
55
|
+
}
|
|
56
|
+
/** Parse `opencode models`, whose stable machine-friendly output is one provider/model id per line. */
|
|
57
|
+
export function parseOpenCodeModels(stdout) {
|
|
58
|
+
const ids = Array.from(new Set(stdout
|
|
59
|
+
.split(/\r?\n/)
|
|
60
|
+
.map((line) => line.replace(/\x1b\[[0-?]*[ -\/]*[@-~]/g, "").trim())
|
|
61
|
+
.filter((line) => /^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:/-]*$/i.test(line))));
|
|
62
|
+
if (!ids.length)
|
|
63
|
+
return OPENCODE_FALLBACK_MODELS.map((m) => ({ ...m }));
|
|
64
|
+
return [
|
|
65
|
+
{ id: "default", label: "跟随 OpenCode 默认", alias: true },
|
|
66
|
+
...ids.map((id) => ({ id, label: id })),
|
|
67
|
+
];
|
|
68
|
+
}
|
|
40
69
|
/** Parse the machine-readable model registry emitted by the installed Codex CLI. */
|
|
41
70
|
export function parseCodexModels(stdout) {
|
|
42
71
|
try {
|
|
@@ -94,21 +123,26 @@ export function getCachedModels() {
|
|
|
94
123
|
cache = {
|
|
95
124
|
models: cloneClaudeModels(),
|
|
96
125
|
codexModels: CODEX_FALLBACK_MODELS.map((m) => ({ ...m })),
|
|
126
|
+
opencodeModels: OPENCODE_FALLBACK_MODELS.map((m) => ({ ...m })),
|
|
97
127
|
claudeVersion: null,
|
|
128
|
+
opencodeVersion: null,
|
|
98
129
|
refreshedAt: new Date().toISOString(),
|
|
99
130
|
};
|
|
100
131
|
}
|
|
101
132
|
return cache;
|
|
102
133
|
}
|
|
103
134
|
export async function refreshModels() {
|
|
104
|
-
const [version, codexModels] = await Promise.all([
|
|
135
|
+
const [version, codexModels, opencode] = await Promise.all([
|
|
105
136
|
probeClaudeVersion(),
|
|
106
137
|
probeCodexModels(),
|
|
138
|
+
probeOpenCode(),
|
|
107
139
|
]);
|
|
108
140
|
cache = {
|
|
109
141
|
models: cloneClaudeModels(),
|
|
110
142
|
codexModels,
|
|
143
|
+
opencodeModels: opencode.models,
|
|
111
144
|
claudeVersion: version,
|
|
145
|
+
opencodeVersion: opencode.version,
|
|
112
146
|
refreshedAt: new Date().toISOString(),
|
|
113
147
|
};
|
|
114
148
|
return cache;
|
package/dist/path-repair.d.ts
CHANGED
|
@@ -57,7 +57,7 @@ export declare function repairRuntimePath(): PathRepairResult;
|
|
|
57
57
|
* 这种动态注入的 PATH。
|
|
58
58
|
*
|
|
59
59
|
* 行为:
|
|
60
|
-
* - 跑 `${shell} -l -c '...'` 拉 $PATH 和 command -v claude/codex(4s 超时)
|
|
60
|
+
* - 跑 `${shell} -l -c '...'` 拉 $PATH 和 command -v claude/codex/opencode(4s 超时)
|
|
61
61
|
* - 按 login shell 的 PATH 顺序重排 process.env.PATH,再把仅存在于 service 的
|
|
62
62
|
* 目录追加回去。不能只前插新增目录:同一个 CLI 同时装在 nvm 和 Homebrew 时,
|
|
63
63
|
* 两个目录本来都存在,旧实现不会重排,structured 与 PTY 会命中不同版本。
|
package/dist/path-repair.js
CHANGED
|
@@ -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"];
|
|
9
|
+
const PROBE_COMMANDS = ["claude", "codex", "opencode"];
|
|
10
10
|
const DEEP_PROBE_TIMEOUT_MS = 4000;
|
|
11
11
|
/**
|
|
12
12
|
* 构造候选 bin 目录列表(按优先级,前面的更可信)。
|
|
@@ -152,7 +152,7 @@ export function repairRuntimePath() {
|
|
|
152
152
|
* 这种动态注入的 PATH。
|
|
153
153
|
*
|
|
154
154
|
* 行为:
|
|
155
|
-
* - 跑 `${shell} -l -c '...'` 拉 $PATH 和 command -v claude/codex(4s 超时)
|
|
155
|
+
* - 跑 `${shell} -l -c '...'` 拉 $PATH 和 command -v claude/codex/opencode(4s 超时)
|
|
156
156
|
* - 按 login shell 的 PATH 顺序重排 process.env.PATH,再把仅存在于 service 的
|
|
157
157
|
* 目录追加回去。不能只前插新增目录:同一个 CLI 同时装在 nvm 和 Homebrew 时,
|
|
158
158
|
* 两个目录本来都存在,旧实现不会重排,structured 与 PTY 会命中不同版本。
|
|
@@ -190,7 +190,7 @@ export async function deepRepairRuntimePath(result, opts = {}) {
|
|
|
190
190
|
.map((seg) => seg.trim())
|
|
191
191
|
.filter((seg) => seg.length > 0);
|
|
192
192
|
const existing = new Set(currentSegments);
|
|
193
|
-
// login shell 报告的所有 PATH 段 +
|
|
193
|
+
// login shell 报告的所有 PATH 段 + provider CLI 解析出的目录都纳入候选。
|
|
194
194
|
const fromShell = [];
|
|
195
195
|
for (const seg of probe.path.split(delim).map((s) => s.trim()).filter(Boolean)) {
|
|
196
196
|
fromShell.push(seg);
|
|
@@ -199,6 +199,8 @@ export async function deepRepairRuntimePath(result, opts = {}) {
|
|
|
199
199
|
fromShell.push(path.dirname(probe.claude));
|
|
200
200
|
if (probe.codex)
|
|
201
201
|
fromShell.push(path.dirname(probe.codex));
|
|
202
|
+
if (probe.opencode)
|
|
203
|
+
fromShell.push(path.dirname(probe.opencode));
|
|
202
204
|
const preferred = [];
|
|
203
205
|
const preferredSet = new Set();
|
|
204
206
|
for (const dir of fromShell) {
|
|
@@ -250,13 +252,14 @@ function pickProbeShell(configured) {
|
|
|
250
252
|
return null;
|
|
251
253
|
}
|
|
252
254
|
/**
|
|
253
|
-
* 用 login shell 跑一段最小脚本,拿到用户实际的 PATH 和
|
|
255
|
+
* 用 login shell 跑一段最小脚本,拿到用户实际的 PATH 和 provider CLI 解析路径。
|
|
254
256
|
* 用 \x1f(ASCII Unit Separator)作字段分隔符避免和路径里的字符冲突。
|
|
255
257
|
*/
|
|
256
258
|
function probeLoginShell(shell, timeoutMs) {
|
|
257
259
|
const script = `printf 'PATH\\x1f%s\\n' "$PATH"; ` +
|
|
258
260
|
`printf 'CLAUDE\\x1f%s\\n' "$(command -v claude 2>/dev/null)"; ` +
|
|
259
|
-
`printf 'CODEX\\x1f%s\\n' "$(command -v codex 2>/dev/null)"
|
|
261
|
+
`printf 'CODEX\\x1f%s\\n' "$(command -v codex 2>/dev/null)"; ` +
|
|
262
|
+
`printf 'OPENCODE\\x1f%s\\n' "$(command -v opencode 2>/dev/null)"`;
|
|
260
263
|
return new Promise((resolve, reject) => {
|
|
261
264
|
const child = spawn(shell, ["-l", "-c", script], {
|
|
262
265
|
env: {
|
|
@@ -296,7 +299,7 @@ function probeLoginShell(shell, timeoutMs) {
|
|
|
296
299
|
reject(new Error(`login shell exited ${code}: ${stderr.trim().slice(0, 200)}`));
|
|
297
300
|
return;
|
|
298
301
|
}
|
|
299
|
-
const out = { path: "", claude: null, codex: null };
|
|
302
|
+
const out = { path: "", claude: null, codex: null, opencode: null };
|
|
300
303
|
for (const line of stdout.split("\n")) {
|
|
301
304
|
const idx = line.indexOf("\x1f");
|
|
302
305
|
if (idx < 0)
|
|
@@ -311,6 +314,8 @@ function probeLoginShell(shell, timeoutMs) {
|
|
|
311
314
|
out.claude = val;
|
|
312
315
|
else if (key === "CODEX")
|
|
313
316
|
out.codex = val;
|
|
317
|
+
else if (key === "OPENCODE")
|
|
318
|
+
out.opencode = val;
|
|
314
319
|
}
|
|
315
320
|
resolve(out);
|
|
316
321
|
});
|
|
@@ -103,7 +103,7 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
103
103
|
setSessionThinkingEffort(id: string, effort: SessionSnapshot["thinkingEffort"]): SessionSnapshot;
|
|
104
104
|
/**
|
|
105
105
|
* Switch the execution mode of a PTY session mid-flight. The already-launched
|
|
106
|
-
*
|
|
106
|
+
* provider process keeps its original CLI flags, but wand's own permission
|
|
107
107
|
* auto-approval (shouldAutoApprovePermissions / escalation handling) reads
|
|
108
108
|
* record.mode, so this changes the permission posture for subsequent prompts.
|
|
109
109
|
* Mirrors setSessionModel/setSessionThinkingEffort.
|
package/dist/process-manager.js
CHANGED
|
@@ -14,12 +14,14 @@ import { ensureNodePtyHelperExecutable } from "./ensure-node-pty-helper.js";
|
|
|
14
14
|
import { buildLanguageDirective, buildManagedAutonomyDirective } from "./language-prompt.js";
|
|
15
15
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
16
16
|
import { getCodexResumeCommandSessionId, getResumeCommandSessionId } from "./resume-policy.js";
|
|
17
|
-
import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort } from "./structured-session-manager.js";
|
|
17
|
+
import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant } from "./structured-session-manager.js";
|
|
18
18
|
import { generateSessionTopic } from "./session-topic.js";
|
|
19
19
|
import { getErrorMessage } from "./error-utils.js";
|
|
20
20
|
import { resolveSessionCwd } from "./session-cwd.js";
|
|
21
21
|
function resolveProviderFromCommand(command) {
|
|
22
|
-
|
|
22
|
+
if (/^codex\b/.test(command.trim()))
|
|
23
|
+
return "codex";
|
|
24
|
+
return /^opencode\b/.test(command.trim()) ? "opencode" : "claude";
|
|
23
25
|
}
|
|
24
26
|
export class SessionInputError extends Error {
|
|
25
27
|
code;
|
|
@@ -1010,7 +1012,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1010
1012
|
}),
|
|
1011
1013
|
name: "xterm-color",
|
|
1012
1014
|
// 使用 record 上由前端协商好的真实尺寸,避免"先 120 列、几百毫秒后再 resize"
|
|
1013
|
-
// 期间
|
|
1015
|
+
// 期间 provider TUI 用错列宽渲染出 \x1b[120G 这类绝对列定位序列。
|
|
1014
1016
|
cols: record.ptyCols,
|
|
1015
1017
|
rows: record.ptyRows
|
|
1016
1018
|
});
|
|
@@ -1399,7 +1401,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1399
1401
|
}
|
|
1400
1402
|
/**
|
|
1401
1403
|
* Switch the execution mode of a PTY session mid-flight. The already-launched
|
|
1402
|
-
*
|
|
1404
|
+
* provider process keeps its original CLI flags, but wand's own permission
|
|
1403
1405
|
* auto-approval (shouldAutoApprovePermissions / escalation handling) reads
|
|
1404
1406
|
* record.mode, so this changes the permission posture for subsequent prompts.
|
|
1405
1407
|
* Mirrors setSessionModel/setSessionThinkingEffort.
|
|
@@ -1974,7 +1976,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1974
1976
|
permissionBlocked: this.isPermissionBlocked(record),
|
|
1975
1977
|
};
|
|
1976
1978
|
// 透传 bridge 给出的 isResponding(true=流式中, false=本轮已完成)。
|
|
1977
|
-
// 前端用它检测 thinking→idle 边界并主动做一次终端 resync,把
|
|
1979
|
+
// 前端用它检测 thinking→idle 边界并主动做一次终端 resync,把 provider TUI
|
|
1978
1980
|
// 在流式渲染过程中残留的错位光标定位序列洗掉(等价于按一次右上角缩放)。
|
|
1979
1981
|
if (bridgeData && typeof bridgeData.isResponding === "boolean") {
|
|
1980
1982
|
data.isResponding = bridgeData.isResponding;
|
|
@@ -2142,6 +2144,22 @@ export class ProcessManager extends EventEmitter {
|
|
|
2142
2144
|
}
|
|
2143
2145
|
return result;
|
|
2144
2146
|
}
|
|
2147
|
+
if (provider === "opencode") {
|
|
2148
|
+
let result = command;
|
|
2149
|
+
const trimmedModel = model?.trim();
|
|
2150
|
+
if (trimmedModel && trimmedModel !== "default" && !/--model(?:\s|=)/.test(result) && !/(?:^|\s)-m(?:\s|$)/.test(result)) {
|
|
2151
|
+
const escapedModel = trimmedModel.replace(/'/g, "'\\''");
|
|
2152
|
+
result += ` --model '${escapedModel}'`;
|
|
2153
|
+
}
|
|
2154
|
+
const variant = thinkingEffortToOpenCodeVariant(thinkingEffort ?? null);
|
|
2155
|
+
if (variant && !/--variant(?:\s|=)/.test(result)) {
|
|
2156
|
+
result += ` --variant '${variant.replace(/'/g, "'\\''")}'`;
|
|
2157
|
+
}
|
|
2158
|
+
if ((mode === "managed" || mode === "full-access" || mode === "auto-edit") && !/--auto(?:\s|$)/.test(result)) {
|
|
2159
|
+
result += " --auto";
|
|
2160
|
+
}
|
|
2161
|
+
return result;
|
|
2162
|
+
}
|
|
2145
2163
|
const isClaudeCmd = /^(?:claude|npx\s+claude|[^\s]+\/claude)(?:\s|$)/.test(command);
|
|
2146
2164
|
if (!isClaudeCmd)
|
|
2147
2165
|
return command;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type ProviderCliId = "claude" | "codex" | "opencode";
|
|
2
|
+
export interface ProviderCliUpdateStatus {
|
|
3
|
+
id: ProviderCliId;
|
|
4
|
+
label: string;
|
|
5
|
+
command: string;
|
|
6
|
+
executable: string | null;
|
|
7
|
+
installed: boolean;
|
|
8
|
+
currentVersion: string | null;
|
|
9
|
+
latestVersion: string | null;
|
|
10
|
+
updateAvailable: boolean;
|
|
11
|
+
updateSupported: boolean;
|
|
12
|
+
installKind: "native" | "npm" | "brew" | "legacy" | "unknown";
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ProviderCliUpdateResult {
|
|
16
|
+
id: ProviderCliId;
|
|
17
|
+
label: string;
|
|
18
|
+
ok: boolean;
|
|
19
|
+
skipped: boolean;
|
|
20
|
+
fromVersion: string | null;
|
|
21
|
+
toVersion: string | null;
|
|
22
|
+
message: string;
|
|
23
|
+
output?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ProviderCliUpdaterOptions {
|
|
26
|
+
inheritEnv?: boolean;
|
|
27
|
+
versionTimeoutMs?: number;
|
|
28
|
+
registryTimeoutMs?: number;
|
|
29
|
+
updateTimeoutMs?: number;
|
|
30
|
+
env?: NodeJS.ProcessEnv;
|
|
31
|
+
onLog?: (line: string) => void;
|
|
32
|
+
}
|
|
33
|
+
export declare function parseProviderCliVersion(output: string): string | null;
|
|
34
|
+
export declare function providerCliUpdateAvailable(currentVersion: string | null, latestVersion: string | null): boolean;
|
|
35
|
+
export declare function checkProviderCliUpdates(options?: ProviderCliUpdaterOptions): Promise<ProviderCliUpdateStatus[]>;
|
|
36
|
+
export declare function updateProviderClis(statuses: ProviderCliUpdateStatus[], ids?: ProviderCliId[], options?: ProviderCliUpdaterOptions): Promise<ProviderCliUpdateResult[]>;
|
|
37
|
+
/** Re-check the active PATH after updating so duplicate installs cannot masquerade as success. */
|
|
38
|
+
export declare function verifyProviderCliUpdateResults(results: ProviderCliUpdateResult[], statuses: ProviderCliUpdateStatus[]): ProviderCliUpdateResult[];
|