@co0ontty/wand 4.27.1 → 4.28.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 +6 -6
- package/dist/build-info.json +3 -3
- package/dist/cli-api.d.ts +11 -0
- package/dist/cli-api.js +76 -0
- package/dist/cli.js +97 -0
- package/dist/config.d.ts +4 -3
- package/dist/config.js +20 -3
- package/dist/git-quick-commit.js +47 -1
- package/dist/git-worktree.d.ts +11 -0
- package/dist/git-worktree.js +72 -3
- package/dist/mission-diff.d.ts +8 -0
- package/dist/mission-diff.js +65 -0
- package/dist/mission-types.d.ts +90 -0
- package/dist/mission-types.js +1 -0
- package/dist/missions.d.ts +32 -0
- package/dist/missions.js +344 -0
- package/dist/models.d.ts +1 -0
- package/dist/models.js +10 -1
- package/dist/path-repair.js +1 -1
- package/dist/process-manager.js +14 -1
- package/dist/prompt-optimizer.d.ts +2 -2
- package/dist/prompt-optimizer.js +11 -32
- package/dist/provider-cli-updater.d.ts +1 -1
- package/dist/provider-cli-updater.js +8 -0
- package/dist/resume-policy.js +5 -3
- package/dist/server-mission-routes.d.ts +3 -0
- package/dist/server-mission-routes.js +81 -0
- package/dist/server-session-routes.js +5 -3
- package/dist/server-settings-routes.js +5 -0
- package/dist/server-update-routes.js +1 -1
- package/dist/server.js +31 -5
- package/dist/session-ai-context.d.ts +3 -3
- package/dist/session-ai-context.js +14 -18
- package/dist/storage.d.ts +15 -0
- package/dist/storage.js +221 -1
- package/dist/structured-pi-adapter.d.ts +11 -0
- package/dist/structured-pi-adapter.js +135 -0
- package/dist/structured-provider-common.d.ts +1 -0
- package/dist/structured-provider-common.js +16 -1
- package/dist/structured-session-manager.d.ts +4 -0
- package/dist/structured-session-manager.js +21 -7
- package/dist/system-ai.js +1 -1
- package/dist/types.d.ts +9 -4
- package/dist/web-ui/content/scripts.js +146 -69
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/web-ui/provider-identity.d.ts +2 -1
- package/dist/web-ui/provider-identity.js +9 -1
- package/package.json +1 -1
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), [OpenCode](https://opencode.ai/), [Grok Build](https://grok.com/),
|
|
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/), [Qoder CLI](https://docs.qoder.com/en/cli/quick-start), and [Pi](https://pi.dev/), 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, OpenCode, Grok,
|
|
59
|
+
- **Multiple providers** — create PTY or structured sessions for Claude Code, Codex, OpenCode, Grok, Qoder CLI, and Pi
|
|
60
60
|
- **Session management** — create, archive, and resume Claude, Codex, OpenCode, Grok, and Qoder sessions with their provider-native context; restore Claude/Codex 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,7 +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
|
|
76
|
+
- **CLI updates** — check and quickly update Claude Code, Codex, OpenCode, Qoder CLI, and Pi from server settings, with optional automatic updates
|
|
77
77
|
|
|
78
78
|
### Configuration
|
|
79
79
|
|
|
@@ -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/)、[Grok Build](https://grok.com/)
|
|
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) 和 [Pi](https://pi.dev/),提供终端和结构化对话双视图、会话持久化与恢复、权限管控、文件浏览和多平台客户端。
|
|
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、Grok 和
|
|
208
|
+
- **多 Provider 支持** — Claude Code、Codex、OpenCode、Grok、Qoder CLI 和 Pi 均可创建 PTY 或结构化会话
|
|
209
209
|
- **会话管理** — 创建、归档并携带原生上下文恢复 Claude、Codex、OpenCode、Grok、Qoder 会话;支持恢复 Claude/Codex 原生历史记录;会话列表显示摘要
|
|
210
210
|
- **权限控制** — 可视化权限提示,支持逐次确认、单次批准、本轮记忆等策略
|
|
211
211
|
|
|
@@ -222,7 +222,7 @@ npm install -g opencode-ai@latest
|
|
|
222
222
|
- **多平台客户端** — Android / iOS / macOS 原生客户端,支持加密连接码分发、自动更新检查
|
|
223
223
|
- **HTTPS** — 可选自签证书,适合远程或移动端访问
|
|
224
224
|
- **版本管理** — 内置更新检查与升级提示,支持 stable/beta 双通道
|
|
225
|
-
- **CLI 更新** — 在服务端设置中检查并快速更新 Claude Code、Codex、OpenCode,也可开启自动更新
|
|
225
|
+
- **CLI 更新** — 在服务端设置中检查并快速更新 Claude Code、Codex、OpenCode、Qoder CLI 和 Pi,也可开启自动更新
|
|
226
226
|
|
|
227
227
|
### 截图
|
|
228
228
|
|
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-08-
|
|
4
|
-
"version": "4.
|
|
2
|
+
"commit": "53c936da2a9d14e6927295c028a77c6d0abd562e",
|
|
3
|
+
"builtAt": "2026-08-05T01:35:22.744Z",
|
|
4
|
+
"version": "4.28.0",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WandConfig } from "./types.js";
|
|
2
|
+
export declare class WandCliApi {
|
|
3
|
+
private readonly config;
|
|
4
|
+
private cookie;
|
|
5
|
+
constructor(config: WandConfig);
|
|
6
|
+
get<T>(pathname: string): Promise<T>;
|
|
7
|
+
post<T>(pathname: string, body: unknown): Promise<T>;
|
|
8
|
+
private login;
|
|
9
|
+
private request;
|
|
10
|
+
private rawRequest;
|
|
11
|
+
}
|
package/dist/cli-api.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { request as httpRequest } from "node:http";
|
|
2
|
+
import { request as httpsRequest } from "node:https";
|
|
3
|
+
export class WandCliApi {
|
|
4
|
+
config;
|
|
5
|
+
cookie = "";
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
}
|
|
9
|
+
async get(pathname) {
|
|
10
|
+
return this.request("GET", pathname);
|
|
11
|
+
}
|
|
12
|
+
async post(pathname, body) {
|
|
13
|
+
return this.request("POST", pathname, body);
|
|
14
|
+
}
|
|
15
|
+
async login() {
|
|
16
|
+
const result = await this.rawRequest("POST", "/api/login", { password: this.config.password });
|
|
17
|
+
if (result.status < 200 || result.status >= 300)
|
|
18
|
+
throw new Error(`Wand 登录失败 (HTTP ${result.status})`);
|
|
19
|
+
this.cookie = result.cookies.map((value) => value.split(";", 1)[0]).join("; ");
|
|
20
|
+
if (!this.cookie)
|
|
21
|
+
throw new Error("Wand 服务未返回登录会话。");
|
|
22
|
+
}
|
|
23
|
+
async request(method, pathname, body) {
|
|
24
|
+
if (!this.cookie)
|
|
25
|
+
await this.login();
|
|
26
|
+
const result = await this.rawRequest(method, pathname, body, this.cookie);
|
|
27
|
+
let parsed = {};
|
|
28
|
+
try {
|
|
29
|
+
parsed = result.body ? JSON.parse(result.body) : {};
|
|
30
|
+
}
|
|
31
|
+
catch { /* handled below */ }
|
|
32
|
+
const error = parsed && typeof parsed === "object" && "error" in parsed && typeof parsed.error === "string"
|
|
33
|
+
? parsed.error
|
|
34
|
+
: null;
|
|
35
|
+
if (result.status < 200 || result.status >= 300 || error) {
|
|
36
|
+
throw new Error(error || `Wand 请求失败 (HTTP ${result.status})`);
|
|
37
|
+
}
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
rawRequest(method, pathname, body, cookie) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const protocol = this.config.https ? "https:" : "http:";
|
|
43
|
+
const url = new URL(`${protocol}//127.0.0.1:${this.config.port}${pathname}`);
|
|
44
|
+
const payload = body === undefined ? "" : JSON.stringify(body);
|
|
45
|
+
const request = protocol === "https:" ? httpsRequest : httpRequest;
|
|
46
|
+
const req = request(url, {
|
|
47
|
+
method,
|
|
48
|
+
rejectUnauthorized: false,
|
|
49
|
+
timeout: 30_000,
|
|
50
|
+
headers: {
|
|
51
|
+
Accept: "application/json",
|
|
52
|
+
...(payload ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) } : {}),
|
|
53
|
+
...(cookie ? { Cookie: cookie } : {}),
|
|
54
|
+
},
|
|
55
|
+
}, (res) => {
|
|
56
|
+
let responseBody = "";
|
|
57
|
+
res.setEncoding("utf8");
|
|
58
|
+
res.on("data", (chunk) => {
|
|
59
|
+
responseBody += chunk;
|
|
60
|
+
if (responseBody.length > 20 * 1024 * 1024)
|
|
61
|
+
req.destroy(new Error("Wand 响应超过 20 MB。"));
|
|
62
|
+
});
|
|
63
|
+
res.on("end", () => resolve({
|
|
64
|
+
status: res.statusCode ?? 0,
|
|
65
|
+
body: responseBody,
|
|
66
|
+
cookies: res.headers["set-cookie"] ?? [],
|
|
67
|
+
}));
|
|
68
|
+
});
|
|
69
|
+
req.on("timeout", () => req.destroy(new Error("连接 Wand 服务超时。")));
|
|
70
|
+
req.on("error", reject);
|
|
71
|
+
if (payload)
|
|
72
|
+
req.write(payload);
|
|
73
|
+
req.end();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import process from "node:process";
|
|
|
9
9
|
import { hasConfigFile, isPreferenceKey, loadConfigWithStorage, resolveConfigPath, saveConfig, writePreferenceToStorage, } from "./config.js";
|
|
10
10
|
import { isPidAlive, readPidfile, removePidfile, removeSocketFile, socketPath, writePidfile, } from "./pidfile.js";
|
|
11
11
|
import { getErrorMessage } from "./error-utils.js";
|
|
12
|
+
import { WandCliApi } from "./cli-api.js";
|
|
12
13
|
async function main() {
|
|
13
14
|
const args = process.argv.slice(2);
|
|
14
15
|
const command = args[0] || "help";
|
|
@@ -167,6 +168,19 @@ async function main() {
|
|
|
167
168
|
}
|
|
168
169
|
break;
|
|
169
170
|
}
|
|
171
|
+
case "session:list":
|
|
172
|
+
case "session:read":
|
|
173
|
+
case "session:send":
|
|
174
|
+
case "session:wait":
|
|
175
|
+
case "inbox:list":
|
|
176
|
+
case "mission:list":
|
|
177
|
+
case "mission:create":
|
|
178
|
+
case "mission:diff":
|
|
179
|
+
case "mission:review":
|
|
180
|
+
case "mission:review:send": {
|
|
181
|
+
await runAgentCliCommand(command, args, configPath);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
170
184
|
case "service:install":
|
|
171
185
|
case "service:uninstall":
|
|
172
186
|
case "service:start":
|
|
@@ -203,6 +217,18 @@ Commands:
|
|
|
203
217
|
wand config:password Print current login password
|
|
204
218
|
wand config:set Update a simple config value
|
|
205
219
|
|
|
220
|
+
Agent runtime:
|
|
221
|
+
wand session:list List sessions as JSON
|
|
222
|
+
wand session:read <id> Read a complete session snapshot
|
|
223
|
+
wand session:send <id> <text>
|
|
224
|
+
wand session:wait <id> [--timeout 900]
|
|
225
|
+
wand inbox:list List global Agent Inbox items
|
|
226
|
+
wand mission:list List parallel tasks
|
|
227
|
+
wand mission:create --prompt <text> --cwd <path> --providers claude,codex
|
|
228
|
+
wand mission:diff <mission-id> <attempt-id>
|
|
229
|
+
wand mission:review <mission-id> <attempt-id> --file <path> --body <text> [--line N]
|
|
230
|
+
wand mission:review:send <mission-id> <attempt-id>
|
|
231
|
+
|
|
206
232
|
System service (default = system-wide; pass --user for user-level):
|
|
207
233
|
wand service:install Register and start the background service (needs sudo for system)
|
|
208
234
|
wand service:uninstall Stop and remove the service
|
|
@@ -220,6 +246,77 @@ Options:
|
|
|
220
246
|
--lines <N> (service:logs) Number of log lines (default 80)
|
|
221
247
|
`);
|
|
222
248
|
}
|
|
249
|
+
async function runAgentCliCommand(command, args, configPath) {
|
|
250
|
+
const { config } = await loadConfigForCli(configPath);
|
|
251
|
+
const api = new WandCliApi(config);
|
|
252
|
+
const output = (value) => { process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); };
|
|
253
|
+
const required = (value, usage) => {
|
|
254
|
+
if (!value)
|
|
255
|
+
throw new Error(`Usage: ${usage}`);
|
|
256
|
+
return value;
|
|
257
|
+
};
|
|
258
|
+
switch (command) {
|
|
259
|
+
case "session:list": return output(await api.get("/api/sessions"));
|
|
260
|
+
case "session:read": {
|
|
261
|
+
const id = required(args[1], "wand session:read <id>");
|
|
262
|
+
return output(await api.get(`/api/sessions/${encodeURIComponent(id)}`));
|
|
263
|
+
}
|
|
264
|
+
case "session:send": {
|
|
265
|
+
const id = required(args[1], "wand session:send <id> <text>");
|
|
266
|
+
const text = required(args[2], "wand session:send <id> <text>");
|
|
267
|
+
return output(await api.post(`/api/sessions/${encodeURIComponent(id)}/input`, { input: text, respondImmediately: true }));
|
|
268
|
+
}
|
|
269
|
+
case "session:wait": {
|
|
270
|
+
const id = required(args[1], "wand session:wait <id> [--timeout 900]");
|
|
271
|
+
const timeoutSeconds = Math.max(1, Number(readFlagValue(args, "--timeout") ?? 900));
|
|
272
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
273
|
+
while (true) {
|
|
274
|
+
const session = await api.get(`/api/sessions/${encodeURIComponent(id)}`);
|
|
275
|
+
const complete = session.status !== "running" && !session.structuredState?.inFlight;
|
|
276
|
+
if (complete)
|
|
277
|
+
return output(session);
|
|
278
|
+
if (Date.now() >= deadline)
|
|
279
|
+
throw new Error(`等待会话超时:${id}`);
|
|
280
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
case "inbox:list": return output(await api.get("/api/inbox"));
|
|
284
|
+
case "mission:list": return output(await api.get("/api/missions"));
|
|
285
|
+
case "mission:create": {
|
|
286
|
+
const prompt = required(readFlagValue(args, "--prompt"), "wand mission:create --prompt <text> --cwd <path> --providers claude,codex");
|
|
287
|
+
const cwd = required(readFlagValue(args, "--cwd"), "wand mission:create --prompt <text> --cwd <path> --providers claude,codex");
|
|
288
|
+
const providers = required(readFlagValue(args, "--providers"), "wand mission:create --prompt <text> --cwd <path> --providers claude,codex")
|
|
289
|
+
.split(",").map((value) => value.trim()).filter(Boolean);
|
|
290
|
+
return output(await api.post("/api/missions", {
|
|
291
|
+
prompt, cwd, providers,
|
|
292
|
+
title: readFlagValue(args, "--title"),
|
|
293
|
+
baseRef: readFlagValue(args, "--base"),
|
|
294
|
+
sharedDirectories: readFlagValue(args, "--shared")?.split(",").filter(Boolean),
|
|
295
|
+
copyPaths: readFlagValue(args, "--copy")?.split(",").filter(Boolean),
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
case "mission:diff": {
|
|
299
|
+
const missionId = required(args[1], "wand mission:diff <mission-id> <attempt-id>");
|
|
300
|
+
const attemptId = required(args[2], "wand mission:diff <mission-id> <attempt-id>");
|
|
301
|
+
return output(await api.get(`/api/missions/${encodeURIComponent(missionId)}/attempts/${encodeURIComponent(attemptId)}/diff`));
|
|
302
|
+
}
|
|
303
|
+
case "mission:review": {
|
|
304
|
+
const missionId = required(args[1], "wand mission:review <mission-id> <attempt-id> --file <path> --body <text>");
|
|
305
|
+
const attemptId = required(args[2], "wand mission:review <mission-id> <attempt-id> --file <path> --body <text>");
|
|
306
|
+
const filePath = required(readFlagValue(args, "--file"), "wand mission:review <mission-id> <attempt-id> --file <path> --body <text>");
|
|
307
|
+
const body = required(readFlagValue(args, "--body"), "wand mission:review <mission-id> <attempt-id> --file <path> --body <text>");
|
|
308
|
+
const line = readFlagValue(args, "--line");
|
|
309
|
+
return output(await api.post(`/api/missions/${encodeURIComponent(missionId)}/attempts/${encodeURIComponent(attemptId)}/comments`, {
|
|
310
|
+
filePath, body, line: line ? Number(line) : null, side: readFlagValue(args, "--side") === "old" ? "old" : "new",
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
case "mission:review:send": {
|
|
314
|
+
const missionId = required(args[1], "wand mission:review:send <mission-id> <attempt-id>");
|
|
315
|
+
const attemptId = required(args[2], "wand mission:review:send <mission-id> <attempt-id>");
|
|
316
|
+
return output(await api.post(`/api/missions/${encodeURIComponent(missionId)}/attempts/${encodeURIComponent(attemptId)}/review/send`, {}));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
223
320
|
async function ensureRequiredFiles(configPath, opts = {}) {
|
|
224
321
|
const { config, dbPath, hadConfig, createdDb, generatedPassword } = await loadConfigForCli(configPath, {
|
|
225
322
|
ensureInitialPassword: true,
|
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", "defaultQoderModel", "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", "defaultPiModel", "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;
|
|
@@ -48,11 +48,12 @@ export declare function writePreferenceToStorage(config: WandConfig, storage: Wa
|
|
|
48
48
|
export declare function validateCommitAiConfig(_config: Pick<WandConfig, "commitAiSource" | "systemAi">): void;
|
|
49
49
|
export declare function normalizeCardDefaults(input: unknown): CardExpandDefaults;
|
|
50
50
|
export declare function isExecutionMode(value: unknown): value is ExecutionMode;
|
|
51
|
-
export declare function getProviderDefaultModels(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel">): {
|
|
51
|
+
export declare function getProviderDefaultModels(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel">): {
|
|
52
52
|
claude: string;
|
|
53
53
|
codex: string;
|
|
54
54
|
opencode: string;
|
|
55
55
|
grok: string;
|
|
56
56
|
qoder: string;
|
|
57
|
+
pi: string;
|
|
57
58
|
};
|
|
58
|
-
export declare function getDefaultModelForProvider(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel">, provider: SessionProvider | undefined): string;
|
|
59
|
+
export declare function getDefaultModelForProvider(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel">, provider: SessionProvider | undefined): string;
|
package/dist/config.js
CHANGED
|
@@ -31,6 +31,7 @@ export const PREFERENCE_KEYS = [
|
|
|
31
31
|
"defaultOpenCodeModel",
|
|
32
32
|
"defaultGrokModel",
|
|
33
33
|
"defaultQoderModel",
|
|
34
|
+
"defaultPiModel",
|
|
34
35
|
"commitCli",
|
|
35
36
|
"commitModel",
|
|
36
37
|
"commitAiSource",
|
|
@@ -75,6 +76,7 @@ export const defaultConfig = () => ({
|
|
|
75
76
|
defaultOpenCodeModel: "",
|
|
76
77
|
defaultGrokModel: "",
|
|
77
78
|
defaultQoderModel: "",
|
|
79
|
+
defaultPiModel: "",
|
|
78
80
|
commitCli: "claude",
|
|
79
81
|
commitModel: "",
|
|
80
82
|
commitAiSource: "cli",
|
|
@@ -285,7 +287,7 @@ export function applyStoragePreferences(config, storage) {
|
|
|
285
287
|
const defaults = defaultConfig();
|
|
286
288
|
if (storage.hasPreference(preferenceStorageKey("defaultProvider"))) {
|
|
287
289
|
const v = storage.getPreference(preferenceStorageKey("defaultProvider"), defaults.defaultProvider ?? "claude");
|
|
288
|
-
if (v === "claude" || v === "codex" || v === "opencode" || v === "grok" || v === "qoder")
|
|
290
|
+
if (v === "claude" || v === "codex" || v === "opencode" || v === "grok" || v === "qoder" || v === "pi")
|
|
289
291
|
config.defaultProvider = v;
|
|
290
292
|
}
|
|
291
293
|
if (storage.hasPreference(preferenceStorageKey("defaultSessionKind"))) {
|
|
@@ -328,6 +330,11 @@ export function applyStoragePreferences(config, storage) {
|
|
|
328
330
|
if (typeof v === "string")
|
|
329
331
|
config.defaultQoderModel = v.trim();
|
|
330
332
|
}
|
|
333
|
+
if (storage.hasPreference(preferenceStorageKey("defaultPiModel"))) {
|
|
334
|
+
const v = storage.getPreference(preferenceStorageKey("defaultPiModel"), defaults.defaultPiModel ?? "");
|
|
335
|
+
if (typeof v === "string")
|
|
336
|
+
config.defaultPiModel = v.trim();
|
|
337
|
+
}
|
|
331
338
|
if (storage.hasPreference(preferenceStorageKey("commitCli"))) {
|
|
332
339
|
const v = storage.getPreference(preferenceStorageKey("commitCli"), defaults.commitCli ?? "claude");
|
|
333
340
|
if (v === "claude" || v === "codex" || v === "opencode")
|
|
@@ -378,7 +385,7 @@ export function writePreferenceToStorage(config, storage, key, value, options =
|
|
|
378
385
|
const dbKey = preferenceStorageKey(key);
|
|
379
386
|
switch (key) {
|
|
380
387
|
case "defaultProvider": {
|
|
381
|
-
if (value !== "claude" && value !== "codex" && value !== "opencode" && value !== "grok" && value !== "qoder")
|
|
388
|
+
if (value !== "claude" && value !== "codex" && value !== "opencode" && value !== "grok" && value !== "qoder" && value !== "pi")
|
|
382
389
|
throw new Error(`无效 Provider: ${value}`);
|
|
383
390
|
storage.setPreference(dbKey, value);
|
|
384
391
|
config.defaultProvider = value;
|
|
@@ -434,6 +441,12 @@ export function writePreferenceToStorage(config, storage, key, value, options =
|
|
|
434
441
|
config.defaultQoderModel = v;
|
|
435
442
|
break;
|
|
436
443
|
}
|
|
444
|
+
case "defaultPiModel": {
|
|
445
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
446
|
+
storage.setPreference(dbKey, v);
|
|
447
|
+
config.defaultPiModel = v;
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
437
450
|
case "commitCli": {
|
|
438
451
|
if (value !== "claude" && value !== "codex" && value !== "opencode")
|
|
439
452
|
throw new Error(`无效 commit CLI: ${value}`);
|
|
@@ -652,13 +665,14 @@ function mergeWithDefaults(input) {
|
|
|
652
665
|
android: normalizeAndroidApkConfig(input.android) ?? defaults.android,
|
|
653
666
|
macos: normalizeMacosDmgConfig(input.macos) ?? defaults.macos,
|
|
654
667
|
cardDefaults: normalizeCardDefaults(input.cardDefaults),
|
|
655
|
-
defaultProvider: input.defaultProvider === "codex" || input.defaultProvider === "opencode" || input.defaultProvider === "grok" || input.defaultProvider === "qoder" ? input.defaultProvider : "claude",
|
|
668
|
+
defaultProvider: input.defaultProvider === "codex" || input.defaultProvider === "opencode" || input.defaultProvider === "grok" || input.defaultProvider === "qoder" || input.defaultProvider === "pi" ? input.defaultProvider : "claude",
|
|
656
669
|
defaultSessionKind: input.defaultSessionKind === "pty" ? "pty" : "structured",
|
|
657
670
|
defaultModel: typeof input.defaultModel === "string" ? input.defaultModel.trim() : defaults.defaultModel,
|
|
658
671
|
defaultCodexModel: typeof input.defaultCodexModel === "string" ? input.defaultCodexModel.trim() : defaults.defaultCodexModel,
|
|
659
672
|
defaultOpenCodeModel: typeof input.defaultOpenCodeModel === "string" ? input.defaultOpenCodeModel.trim() : defaults.defaultOpenCodeModel,
|
|
660
673
|
defaultGrokModel: typeof input.defaultGrokModel === "string" ? input.defaultGrokModel.trim() : defaults.defaultGrokModel,
|
|
661
674
|
defaultQoderModel: typeof input.defaultQoderModel === "string" ? input.defaultQoderModel.trim() : defaults.defaultQoderModel,
|
|
675
|
+
defaultPiModel: typeof input.defaultPiModel === "string" ? input.defaultPiModel.trim() : defaults.defaultPiModel,
|
|
662
676
|
commitCli: input.commitCli === "codex" || input.commitCli === "opencode" ? input.commitCli : "claude",
|
|
663
677
|
commitModel: typeof input.commitModel === "string" ? input.commitModel.trim() : defaults.commitModel,
|
|
664
678
|
commitAiSource: input.commitAiSource === "api" ? "api" : "cli",
|
|
@@ -677,6 +691,7 @@ export function getProviderDefaultModels(config) {
|
|
|
677
691
|
opencode: (config.defaultOpenCodeModel ?? "").trim(),
|
|
678
692
|
grok: (config.defaultGrokModel ?? "").trim(),
|
|
679
693
|
qoder: (config.defaultQoderModel ?? "").trim(),
|
|
694
|
+
pi: (config.defaultPiModel ?? "").trim(),
|
|
680
695
|
};
|
|
681
696
|
}
|
|
682
697
|
export function getDefaultModelForProvider(config, provider) {
|
|
@@ -689,6 +704,8 @@ export function getDefaultModelForProvider(config, provider) {
|
|
|
689
704
|
return defaults.grok;
|
|
690
705
|
if (provider === "qoder")
|
|
691
706
|
return defaults.qoder;
|
|
707
|
+
if (provider === "pi")
|
|
708
|
+
return defaults.pi;
|
|
692
709
|
return defaults.claude;
|
|
693
710
|
}
|
|
694
711
|
function normalizePresetCommand(command) {
|
package/dist/git-quick-commit.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ClaudeRunError, runClaudePrint } from "./claude-sdk-runner.js";
|
|
|
4
4
|
import { callSystemAiTextWithFallback } from "./system-ai.js";
|
|
5
5
|
import { buildChildEnv } from "./env-utils.js";
|
|
6
6
|
import { runGitAsync as runGitAsyncBase, runGitRawAsync as runGitRawAsyncBase, getGitErrorMessage, } from "./git-utils.js";
|
|
7
|
-
import { thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToQoderEffort, } from "./structured-provider-common.js";
|
|
7
|
+
import { thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToQoderEffort, thinkingEffortToPiLevel, } from "./structured-provider-common.js";
|
|
8
8
|
const GIT_TIMEOUT_MS = 1500;
|
|
9
9
|
const GIT_PUSH_TIMEOUT_MS = 30_000;
|
|
10
10
|
const MAX_FILE_ENTRIES = 200;
|
|
@@ -306,6 +306,7 @@ function normalizeProvider(provider) {
|
|
|
306
306
|
|| provider === "opencode"
|
|
307
307
|
|| provider === "grok"
|
|
308
308
|
|| provider === "qoder"
|
|
309
|
+
|| provider === "pi"
|
|
309
310
|
? provider
|
|
310
311
|
: "claude";
|
|
311
312
|
}
|
|
@@ -406,6 +407,35 @@ function extractQoderText(stdout) {
|
|
|
406
407
|
}
|
|
407
408
|
return (resultText || assistantTexts.join("\n")).trim();
|
|
408
409
|
}
|
|
410
|
+
function extractPiText(stdout) {
|
|
411
|
+
let text = "";
|
|
412
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
413
|
+
try {
|
|
414
|
+
const event = JSON.parse(line);
|
|
415
|
+
if (event.type !== "message_end" || event.message?.role !== "assistant" || !Array.isArray(event.message.content))
|
|
416
|
+
continue;
|
|
417
|
+
text = event.message.content
|
|
418
|
+
.filter((block) => !!block && typeof block === "object" && block.type === "text" && typeof block.text === "string")
|
|
419
|
+
.map((block) => block.text)
|
|
420
|
+
.join("\n");
|
|
421
|
+
}
|
|
422
|
+
catch { /* ignore non-protocol stdout */ }
|
|
423
|
+
}
|
|
424
|
+
return text.trim();
|
|
425
|
+
}
|
|
426
|
+
function buildPiTextArgs(prompt, opts, allowTools = false) {
|
|
427
|
+
const args = ["--mode", "json", "--print", "--no-session"];
|
|
428
|
+
if (!allowTools)
|
|
429
|
+
args.push("--no-tools");
|
|
430
|
+
const model = opts.model?.trim();
|
|
431
|
+
if (model && model !== "default")
|
|
432
|
+
args.push("--model", model);
|
|
433
|
+
const level = thinkingEffortToPiLevel(opts.thinkingEffort ?? "off");
|
|
434
|
+
if (level)
|
|
435
|
+
args.push("--thinking", level);
|
|
436
|
+
args.push(prompt);
|
|
437
|
+
return args;
|
|
438
|
+
}
|
|
409
439
|
function buildGrokTextArgs(prompt, opts, allowTools = false) {
|
|
410
440
|
const args = ["--no-auto-update", "-p", prompt, "--output-format", "streaming-json"];
|
|
411
441
|
const model = opts.model?.trim();
|
|
@@ -532,6 +562,13 @@ async function callQoderText(prompt, cwd, opts) {
|
|
|
532
562
|
throw new QuickCommitError("Qoder 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
|
|
533
563
|
return text;
|
|
534
564
|
}
|
|
565
|
+
async function callPiText(prompt, cwd, opts) {
|
|
566
|
+
const stdout = await runCliText("pi", buildPiTextArgs(prompt, opts), "", { cwd, timeoutMs: CODEX_MESSAGE_TIMEOUT_MS, inheritEnv: opts.inheritEnv });
|
|
567
|
+
const text = extractPiText(stdout);
|
|
568
|
+
if (!text)
|
|
569
|
+
throw new QuickCommitError("Pi 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
|
|
570
|
+
return text;
|
|
571
|
+
}
|
|
535
572
|
async function callCliAiText(prompt, cwd, language, opts) {
|
|
536
573
|
const provider = normalizeProvider(opts.provider);
|
|
537
574
|
if (provider === "codex") {
|
|
@@ -543,6 +580,8 @@ async function callCliAiText(prompt, cwd, language, opts) {
|
|
|
543
580
|
return callGrokText(prompt, cwd, opts);
|
|
544
581
|
if (provider === "qoder")
|
|
545
582
|
return callQoderText(prompt, cwd, opts);
|
|
583
|
+
if (provider === "pi")
|
|
584
|
+
return callPiText(prompt, cwd, opts);
|
|
546
585
|
return callClaudeText(prompt, cwd, language, opts);
|
|
547
586
|
}
|
|
548
587
|
async function callDirectApiText(prompt, systemAi) {
|
|
@@ -1196,6 +1235,13 @@ async function runQuickCommitFallbackCli(opts, priorError) {
|
|
|
1196
1235
|
inheritEnv: opts.inheritEnv,
|
|
1197
1236
|
});
|
|
1198
1237
|
}
|
|
1238
|
+
else if (provider === "pi") {
|
|
1239
|
+
await runCliText("pi", buildPiTextArgs(prompt, opts, true), "", {
|
|
1240
|
+
cwd: opts.cwd,
|
|
1241
|
+
timeoutMs: QUICK_COMMIT_CLI_TIMEOUT_MS,
|
|
1242
|
+
inheritEnv: opts.inheritEnv,
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1199
1245
|
else {
|
|
1200
1246
|
const args = [
|
|
1201
1247
|
"-p",
|
package/dist/git-worktree.d.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { SessionSnapshot, WorktreeMergeCheckResult, WorktreeMergeResult } from "./types.js";
|
|
2
|
+
export interface WorktreeSetupSpec {
|
|
3
|
+
/** Branch, tag, or commit used as the task's immutable starting point. */
|
|
4
|
+
baseRef?: string;
|
|
5
|
+
/** Human-readable task name used in the generated branch. */
|
|
6
|
+
taskName?: string;
|
|
7
|
+
/** Gitignored repo-relative paths copied into the new worktree. */
|
|
8
|
+
copyPaths?: string[];
|
|
9
|
+
/** Gitignored repo-relative paths symlinked from the main checkout. */
|
|
10
|
+
sharedDirectories?: string[];
|
|
11
|
+
}
|
|
2
12
|
interface WorktreeSetupOptions {
|
|
3
13
|
cwd: string;
|
|
4
14
|
sessionId: string;
|
|
15
|
+
spec?: WorktreeSetupSpec;
|
|
5
16
|
}
|
|
6
17
|
interface WorktreeSetupResult {
|
|
7
18
|
cwd: string;
|
package/dist/git-worktree.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
1
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, symlinkSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { runGit, runGitAsync, getGitErrorMessage } from "./git-utils.js";
|
|
4
4
|
const WORKTREE_MERGE_ERROR_CODES = {
|
|
@@ -39,6 +39,48 @@ function getCurrentBranch(repoRoot, timeoutMs = DEFAULT_WORKTREE_GIT_TIMEOUT_MS)
|
|
|
39
39
|
const branch = runWorktreeGit(["branch", "--show-current"], repoRoot, timeoutMs);
|
|
40
40
|
return branch || "master";
|
|
41
41
|
}
|
|
42
|
+
function normalizePortableRelativePath(value) {
|
|
43
|
+
const normalized = value.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
44
|
+
if (!normalized || path.isAbsolute(normalized) || normalized === ".git" || normalized.startsWith(".git/") || normalized.split("/").includes("..")) {
|
|
45
|
+
throw new Error(`无效的 worktree 共享路径:${value}`);
|
|
46
|
+
}
|
|
47
|
+
return normalized;
|
|
48
|
+
}
|
|
49
|
+
function ensureIgnoredPath(repoRoot, relativePath) {
|
|
50
|
+
try {
|
|
51
|
+
runGit(["check-ignore", "--quiet", "--", relativePath], repoRoot);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error(`仅允许复制或共享被 gitignore 忽略的路径:${relativePath}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function populateWorktreePaths(repoRoot, worktreePath, spec) {
|
|
58
|
+
for (const rawPath of spec.copyPaths ?? []) {
|
|
59
|
+
const relativePath = normalizePortableRelativePath(rawPath);
|
|
60
|
+
ensureIgnoredPath(repoRoot, relativePath);
|
|
61
|
+
const source = path.join(repoRoot, relativePath);
|
|
62
|
+
if (!existsSync(source))
|
|
63
|
+
throw new Error(`要复制的路径不存在:${relativePath}`);
|
|
64
|
+
const destination = path.join(worktreePath, relativePath);
|
|
65
|
+
if (existsSync(destination))
|
|
66
|
+
throw new Error(`Worktree 中目标路径已存在:${relativePath}`);
|
|
67
|
+
mkdirSync(path.dirname(destination), { recursive: true });
|
|
68
|
+
cpSync(source, destination, { recursive: true, preserveTimestamps: true });
|
|
69
|
+
}
|
|
70
|
+
for (const rawPath of spec.sharedDirectories ?? []) {
|
|
71
|
+
const relativePath = normalizePortableRelativePath(rawPath);
|
|
72
|
+
ensureIgnoredPath(repoRoot, relativePath);
|
|
73
|
+
const source = path.join(repoRoot, relativePath);
|
|
74
|
+
if (!existsSync(source) || !lstatSync(source).isDirectory()) {
|
|
75
|
+
throw new Error(`要共享的目录不存在:${relativePath}`);
|
|
76
|
+
}
|
|
77
|
+
const destination = path.join(worktreePath, relativePath);
|
|
78
|
+
if (existsSync(destination))
|
|
79
|
+
throw new Error(`Worktree 中目标路径已存在:${relativePath}`);
|
|
80
|
+
mkdirSync(path.dirname(destination), { recursive: true });
|
|
81
|
+
symlinkSync(source, destination, "dir");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
42
84
|
function ensureWorktreePath(worktree) {
|
|
43
85
|
const worktreePath = path.resolve(worktree.path);
|
|
44
86
|
if (!existsSync(worktreePath)) {
|
|
@@ -321,18 +363,45 @@ export function prepareSessionWorktree(options) {
|
|
|
321
363
|
throw new Error("当前目录不在 git 仓库中,无法启用 worktree 模式。");
|
|
322
364
|
}
|
|
323
365
|
const baseBranch = getCurrentBranch(repoRoot);
|
|
366
|
+
const requestedBaseRef = options.spec?.baseRef?.trim() || baseBranch;
|
|
367
|
+
let baseCommit;
|
|
368
|
+
try {
|
|
369
|
+
baseCommit = runGit(["rev-parse", "--verify", `${requestedBaseRef}^{commit}`], repoRoot);
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
throw new Error(`Worktree 基线不存在:${requestedBaseRef}`);
|
|
373
|
+
}
|
|
324
374
|
const branchSuffix = sanitizeBranchSegment(options.sessionId.split("-")[0] || options.sessionId);
|
|
325
|
-
const
|
|
375
|
+
const branchPrefix = sanitizeBranchSegment(options.spec?.taskName || baseBranch);
|
|
376
|
+
const branchName = `wand/${branchPrefix}-${branchSuffix}`;
|
|
326
377
|
const worktreesRoot = path.join(repoRoot, ".wand-worktrees");
|
|
327
378
|
const worktreePath = path.join(worktreesRoot, branchName.replace(/\//g, "-"));
|
|
328
379
|
mkdirSync(worktreesRoot, { recursive: true });
|
|
329
|
-
runGit(["worktree", "add", "-b", branchName, worktreePath,
|
|
380
|
+
runGit(["worktree", "add", "-b", branchName, worktreePath, baseCommit], repoRoot);
|
|
381
|
+
try {
|
|
382
|
+
populateWorktreePaths(repoRoot, worktreePath, options.spec ?? {});
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
try {
|
|
386
|
+
runGit(["worktree", "remove", "--force", worktreePath], repoRoot);
|
|
387
|
+
}
|
|
388
|
+
catch { /* preserve setup error */ }
|
|
389
|
+
try {
|
|
390
|
+
runGit(["branch", "-D", branchName], repoRoot);
|
|
391
|
+
}
|
|
392
|
+
catch { /* preserve setup error */ }
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
330
395
|
return {
|
|
331
396
|
cwd: worktreePath,
|
|
332
397
|
worktreeEnabled: true,
|
|
333
398
|
worktree: {
|
|
334
399
|
branch: branchName,
|
|
335
400
|
path: worktreePath,
|
|
401
|
+
// Pin the resolved commit so later movement of the source branch cannot
|
|
402
|
+
// change the mission's review baseline.
|
|
403
|
+
baseRef: baseCommit,
|
|
404
|
+
repoRoot,
|
|
336
405
|
},
|
|
337
406
|
};
|
|
338
407
|
}
|