@co0ontty/wand 1.30.3 → 1.31.1
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 +39 -2
- package/dist/claude-sdk-runner.d.ts +31 -0
- package/dist/claude-sdk-runner.js +142 -0
- package/dist/cli.js +104 -0
- package/dist/git-quick-commit.js +18 -26
- package/dist/process-manager.d.ts +7 -0
- package/dist/process-manager.js +26 -2
- package/dist/prompt-optimizer.js +17 -26
- package/dist/server-session-routes.js +72 -3
- package/dist/server.js +1 -0
- package/dist/structured-session-manager.d.ts +24 -0
- package/dist/structured-session-manager.js +106 -7
- package/dist/tui/attach.js +7 -8
- package/dist/tui/commands.d.ts +24 -7
- package/dist/tui/commands.js +200 -86
- package/dist/tui/index.js +8 -8
- package/dist/tui/service-panel.js +3 -4
- package/dist/types.d.ts +2 -0
- package/dist/web-ui/content/scripts.js +927 -81
- package/dist/web-ui/content/styles.css +986 -141
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,12 +21,23 @@
|
|
|
21
21
|
bash <(curl -Ls https://raw.githubusercontent.com/co0ontty/wand/master/install.sh)
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
装完后脚本会询问:
|
|
25
|
+
|
|
26
|
+
- **1) 装为系统服务(推荐,默认)** — 写入 system-wide systemd(`/etc/systemd/system/wand.service`)或 launchd LaunchDaemon(`/Library/LaunchDaemons/com.wand.web.plist`),后台运行、开机自启、崩了自重启。**需要 sudo**(脚本会自动加)。
|
|
27
|
+
- **2) 单次启动** — 不装服务,之后手动跑 `wand web`。
|
|
28
|
+
|
|
29
|
+
> 通过管道运行(`bash <(curl ...)`)时 stdin 不是终端,默认走 **1(系统服务)**。想强制单次启动可以 `WAND_INSTALL_MODE=oneshot bash install.sh`。
|
|
30
|
+
>
|
|
31
|
+
> 不想用 sudo?可以装 user-level 版本:`wand service:install --user`(写入 `~/.config/systemd/user/wand.service`,登出会被回收,除非 `loginctl enable-linger $USER`)。
|
|
32
|
+
|
|
24
33
|
### 手动安装
|
|
25
34
|
|
|
26
35
|
```bash
|
|
27
36
|
npm install -g @co0ontty/wand
|
|
28
37
|
wand init
|
|
29
|
-
wand
|
|
38
|
+
sudo wand service:install # 装为系统服务(system-wide, 默认)
|
|
39
|
+
# 或者:wand service:install --user # 不要 sudo,但登出会被回收
|
|
40
|
+
wand web # 没服务时启动新实例;有服务时 attach TUI
|
|
30
41
|
```
|
|
31
42
|
|
|
32
43
|
安装完成后打开浏览器访问终端中提示的地址即可。
|
|
@@ -41,7 +52,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/co0ontty/wand/master/install.s
|
|
|
41
52
|
|
|
42
53
|
> 也可以直接在网页设置里点「更新」按钮,或在 TUI 模式按 `u`,wand 自己会调用同样的清理逻辑。Web 端点击更新后会自动重启服务,无需手动操作。
|
|
43
54
|
|
|
44
|
-
如果以前装过 systemd 自启服务但还是 `Restart=on-failure`(v1.25.x
|
|
55
|
+
如果以前装过 systemd 自启服务但还是 `Restart=on-failure`(v1.25.x 前的版本),重新跑 `sudo wand service:install` 重装服务即可换成 `Restart=always`,自动更新后才能正确拉起新进程。
|
|
45
56
|
|
|
46
57
|
## 功能
|
|
47
58
|
|
|
@@ -101,6 +112,32 @@ wand config:set port 9443
|
|
|
101
112
|
| `password` | (随机生成) | 登录密码 |
|
|
102
113
|
| `language` | `""` | Claude 回复语言偏好 |
|
|
103
114
|
|
|
115
|
+
## 系统服务
|
|
116
|
+
|
|
117
|
+
默认走 **system-wide**:Linux 写 `/etc/systemd/system/wand.service`,macOS 写 `/Library/LaunchDaemons/com.wand.web.plist`。开机自启、不依赖 login session、`service wand` / `systemctl status wand` 这些老命令都能用。装/卸需要 sudo。
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
sudo wand service:install # 注册并启动(首次安装走这里)
|
|
121
|
+
wand service:status # 查状态(active / inactive / failed) — 读取不要 sudo
|
|
122
|
+
sudo wand service:start # 启动
|
|
123
|
+
sudo wand service:stop # 停止
|
|
124
|
+
sudo wand service:restart # 重启
|
|
125
|
+
wand service:logs # 看最近日志(--lines N 调整行数)
|
|
126
|
+
sudo wand service:uninstall # 卸载(停服 + 删 unit)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
不想用 sudo?传 `--user` 切到 user-level(写 `~/.config/systemd/user/wand.service` 或 `~/Library/LaunchAgents/`):
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
wand service:install --user
|
|
133
|
+
wand service:status --user
|
|
134
|
+
# ...其他子命令同理
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
> User-level 版本登出后会被回收,除非跑 `loginctl enable-linger $USER`。
|
|
138
|
+
|
|
139
|
+
服务装好后,`wand web` 会自动检测正在运行的实例(同一份 `config.json` 下)并以 TUI 模式 **attach 到现有 service**,不会重复启动第二个进程。多份配置(`-c` 指向不同路径)之间彼此隔离,互不影响。
|
|
140
|
+
|
|
104
141
|
## 开发
|
|
105
142
|
|
|
106
143
|
```bash
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type ClaudeRunErrorCode = "CLAUDE_CLI_MISSING" | "CLAUDE_TIMEOUT" | "CLAUDE_CLI_FAILED" | "CLAUDE_EMPTY_RESULT";
|
|
2
|
+
export declare class ClaudeRunError extends Error {
|
|
3
|
+
readonly code: ClaudeRunErrorCode;
|
|
4
|
+
constructor(message: string, code: ClaudeRunErrorCode);
|
|
5
|
+
}
|
|
6
|
+
export interface RunClaudePrintOptions {
|
|
7
|
+
cwd?: string;
|
|
8
|
+
timeoutMs: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 用 `@anthropic-ai/claude-agent-sdk` 跑一次"prompt → 单段纯文本"调用,
|
|
12
|
+
* 等价以前的 `claude -p --output-format text`。
|
|
13
|
+
*
|
|
14
|
+
* 行为对齐 Claude Code 默认:
|
|
15
|
+
* - 不指定 model / appendSystemPrompt / agent / mcpServers / hooks 等覆盖项,
|
|
16
|
+
* 完全由 `~/.claude/settings.json`、OAuth 凭据、`CLAUDE_*` 环境变量等
|
|
17
|
+
* 用户侧配置接管,与 Claude Code 自身一致。
|
|
18
|
+
* - `tools: []` 关掉所有内置工具:这两个调用点(commit message / prompt 优化)
|
|
19
|
+
* 本质就是"纯文本生成",关掉工具能 (1) 防止 Claude 随手开个工具卡住权限询问;
|
|
20
|
+
* (2) 避免一次性短调用还顺便加载文件 / 跑 bash 这种副作用。
|
|
21
|
+
* - `persistSession: false`:这些 ephemeral 调用不应该污染 `~/.claude/projects/`
|
|
22
|
+
* 的会话历史;用户在 wand UI 里也压根看不到这些"虚拟会话"。
|
|
23
|
+
*
|
|
24
|
+
* 选择 SDK 而非以前的 `execFile("claude")`:
|
|
25
|
+
* - SDK 包内置各平台 native binary,`pathToClaudeCodeExecutable` 直接指到
|
|
26
|
+
* `node_modules` 里,**零** PATH 依赖。systemd / launchd / 双击图标启动
|
|
27
|
+
* wand server 时不会再因为 PATH 缺 nvm/npm-global 而报"未找到 claude CLI"。
|
|
28
|
+
* - 与现有 `structured-session-manager.ts` 的 SDK 调用路径同源,行为/认证/
|
|
29
|
+
* 更新策略统一。
|
|
30
|
+
*/
|
|
31
|
+
export declare function runClaudePrint(prompt: string, options: RunClaudePrintOptions): Promise<string>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { query as sdkQuery, } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
+
export class ClaudeRunError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(message, code) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = "ClaudeRunError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 判断当前 Linux 是否是 musl 系(Alpine 等)。glibc 系跑不动 musl native binary,
|
|
14
|
+
* 反之亦然,SDK 默认的优先级与本机不匹配时会抛 "Claude Code native binary not found"。
|
|
15
|
+
*/
|
|
16
|
+
function isMuslSystem() {
|
|
17
|
+
try {
|
|
18
|
+
const header = process.report?.getReport()?.header;
|
|
19
|
+
return !header?.glibcVersionRuntime;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 把 SDK 应使用的 native claude binary 路径解析出来。逻辑与
|
|
27
|
+
* `structured-session-manager.ts` 的 `resolveSdkClaudeBinary` 保持一致。
|
|
28
|
+
*/
|
|
29
|
+
function resolveSdkClaudeBinary() {
|
|
30
|
+
if (process.platform !== "linux")
|
|
31
|
+
return undefined;
|
|
32
|
+
const musl = isMuslSystem();
|
|
33
|
+
const arch = process.arch;
|
|
34
|
+
const require = createRequire(import.meta.url);
|
|
35
|
+
const candidates = musl
|
|
36
|
+
? [
|
|
37
|
+
`@anthropic-ai/claude-agent-sdk-linux-${arch}-musl/claude`,
|
|
38
|
+
`@anthropic-ai/claude-agent-sdk-linux-${arch}/claude`,
|
|
39
|
+
]
|
|
40
|
+
: [
|
|
41
|
+
`@anthropic-ai/claude-agent-sdk-linux-${arch}/claude`,
|
|
42
|
+
`@anthropic-ai/claude-agent-sdk-linux-${arch}-musl/claude`,
|
|
43
|
+
];
|
|
44
|
+
for (const pkg of candidates) {
|
|
45
|
+
try {
|
|
46
|
+
const resolved = require.resolve(pkg);
|
|
47
|
+
if (existsSync(resolved))
|
|
48
|
+
return resolved;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// 包不存在,尝试下一个
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 用 `@anthropic-ai/claude-agent-sdk` 跑一次"prompt → 单段纯文本"调用,
|
|
58
|
+
* 等价以前的 `claude -p --output-format text`。
|
|
59
|
+
*
|
|
60
|
+
* 行为对齐 Claude Code 默认:
|
|
61
|
+
* - 不指定 model / appendSystemPrompt / agent / mcpServers / hooks 等覆盖项,
|
|
62
|
+
* 完全由 `~/.claude/settings.json`、OAuth 凭据、`CLAUDE_*` 环境变量等
|
|
63
|
+
* 用户侧配置接管,与 Claude Code 自身一致。
|
|
64
|
+
* - `tools: []` 关掉所有内置工具:这两个调用点(commit message / prompt 优化)
|
|
65
|
+
* 本质就是"纯文本生成",关掉工具能 (1) 防止 Claude 随手开个工具卡住权限询问;
|
|
66
|
+
* (2) 避免一次性短调用还顺便加载文件 / 跑 bash 这种副作用。
|
|
67
|
+
* - `persistSession: false`:这些 ephemeral 调用不应该污染 `~/.claude/projects/`
|
|
68
|
+
* 的会话历史;用户在 wand UI 里也压根看不到这些"虚拟会话"。
|
|
69
|
+
*
|
|
70
|
+
* 选择 SDK 而非以前的 `execFile("claude")`:
|
|
71
|
+
* - SDK 包内置各平台 native binary,`pathToClaudeCodeExecutable` 直接指到
|
|
72
|
+
* `node_modules` 里,**零** PATH 依赖。systemd / launchd / 双击图标启动
|
|
73
|
+
* wand server 时不会再因为 PATH 缺 nvm/npm-global 而报"未找到 claude CLI"。
|
|
74
|
+
* - 与现有 `structured-session-manager.ts` 的 SDK 调用路径同源,行为/认证/
|
|
75
|
+
* 更新策略统一。
|
|
76
|
+
*/
|
|
77
|
+
export async function runClaudePrint(prompt, options) {
|
|
78
|
+
const cwd = options.cwd && options.cwd.length > 0 ? options.cwd : undefined;
|
|
79
|
+
const abortController = new AbortController();
|
|
80
|
+
const timeoutHandle = setTimeout(() => abortController.abort(), options.timeoutMs);
|
|
81
|
+
const sdkClaudeBinary = resolveSdkClaudeBinary();
|
|
82
|
+
const sdkOptions = {
|
|
83
|
+
abortController,
|
|
84
|
+
tools: [],
|
|
85
|
+
persistSession: false,
|
|
86
|
+
...(cwd ? { cwd } : {}),
|
|
87
|
+
...(sdkClaudeBinary ? { pathToClaudeCodeExecutable: sdkClaudeBinary } : {}),
|
|
88
|
+
};
|
|
89
|
+
// 单条 user message → AsyncGenerator,SDK 的 streaming input 协议要求。
|
|
90
|
+
async function* singleShot() {
|
|
91
|
+
yield {
|
|
92
|
+
type: "user",
|
|
93
|
+
message: {
|
|
94
|
+
role: "user",
|
|
95
|
+
content: [{ type: "text", text: prompt }],
|
|
96
|
+
},
|
|
97
|
+
parent_tool_use_id: null,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
let resultText = "";
|
|
101
|
+
let resultError = null;
|
|
102
|
+
try {
|
|
103
|
+
for await (const msg of sdkQuery({
|
|
104
|
+
prompt: singleShot(),
|
|
105
|
+
options: sdkOptions,
|
|
106
|
+
})) {
|
|
107
|
+
if (msg.type !== "result")
|
|
108
|
+
continue;
|
|
109
|
+
if (msg.subtype === "success" && typeof msg.result === "string") {
|
|
110
|
+
resultText = msg.result.trim();
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const errs = Array.isArray(msg.errors)
|
|
114
|
+
? msg.errors.join("; ")
|
|
115
|
+
: msg.subtype;
|
|
116
|
+
resultError = new ClaudeRunError(`Claude SDK 失败:${errs}`, "CLAUDE_CLI_FAILED");
|
|
117
|
+
}
|
|
118
|
+
break; // 一次性调用,拿到 result 即退出
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
if (abortController.signal.aborted) {
|
|
123
|
+
throw new ClaudeRunError("Claude 调用超时。", "CLAUDE_TIMEOUT");
|
|
124
|
+
}
|
|
125
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
126
|
+
// SDK 找不到 native binary 时会抛 "Claude Code native binary not found"。
|
|
127
|
+
// 极少数情况下也可能透出 ENOENT。两种都归到"CLI_MISSING",文案给用户。
|
|
128
|
+
if (/Claude Code native binary not found|ENOENT/i.test(message)) {
|
|
129
|
+
throw new ClaudeRunError("未找到 claude CLI。", "CLAUDE_CLI_MISSING");
|
|
130
|
+
}
|
|
131
|
+
throw new ClaudeRunError(`Claude SDK 失败:${message}`, "CLAUDE_CLI_FAILED");
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
clearTimeout(timeoutHandle);
|
|
135
|
+
}
|
|
136
|
+
if (resultError)
|
|
137
|
+
throw resultError;
|
|
138
|
+
if (!resultText) {
|
|
139
|
+
throw new ClaudeRunError("Claude 返回了空结果。", "CLAUDE_EMPTY_RESULT");
|
|
140
|
+
}
|
|
141
|
+
return resultText;
|
|
142
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -136,6 +136,17 @@ async function main() {
|
|
|
136
136
|
}
|
|
137
137
|
break;
|
|
138
138
|
}
|
|
139
|
+
case "service:install":
|
|
140
|
+
case "service:uninstall":
|
|
141
|
+
case "service:start":
|
|
142
|
+
case "service:stop":
|
|
143
|
+
case "service:restart":
|
|
144
|
+
case "service:status":
|
|
145
|
+
case "service:logs": {
|
|
146
|
+
const exitCode = await runServiceCommand(command, args, configPath);
|
|
147
|
+
process.exitCode = exitCode;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
139
150
|
case "help":
|
|
140
151
|
default: {
|
|
141
152
|
printHelp();
|
|
@@ -160,8 +171,21 @@ Commands:
|
|
|
160
171
|
wand config:show Print current config
|
|
161
172
|
wand config:set Update a simple config value
|
|
162
173
|
|
|
174
|
+
System service (default = system-wide; pass --user for user-level):
|
|
175
|
+
wand service:install Register and start the background service (needs sudo for system)
|
|
176
|
+
wand service:uninstall Stop and remove the service
|
|
177
|
+
wand service:start Start the service
|
|
178
|
+
wand service:stop Stop the service
|
|
179
|
+
wand service:restart Restart the service
|
|
180
|
+
wand service:status Show service status
|
|
181
|
+
wand service:logs Tail recent service logs
|
|
182
|
+
|
|
163
183
|
Options:
|
|
164
184
|
-c, --config <path> Use a custom config file (default: ~/.wand/config.json)
|
|
185
|
+
--user (service:*) Operate on the user-level service (no root needed)
|
|
186
|
+
--system (service:*) Operate on the system-wide service (default; needs root)
|
|
187
|
+
--verbose (service:*) Print full detail output
|
|
188
|
+
--lines <N> (service:logs) Number of log lines (default 80)
|
|
165
189
|
`);
|
|
166
190
|
}
|
|
167
191
|
async function ensureRequiredFiles(configPath, opts = {}) {
|
|
@@ -341,6 +365,86 @@ function setConfigValue(config, key, value) {
|
|
|
341
365
|
throw new Error(`Unsupported config key: ${key}`);
|
|
342
366
|
}
|
|
343
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* 把 `wand service:*` 子命令路由到 src/tui/commands.ts 里已有的服务管理实现。
|
|
370
|
+
*
|
|
371
|
+
* 这里只做:把 CLI args → ServiceContext,调对应函数,把 CommandResult 打印出来,
|
|
372
|
+
* 按 ok 决定 exit code。所有平台分支(Linux user-systemd / macOS launchd / 其他不支持)
|
|
373
|
+
* 都在 tui/commands.ts 内部处理。
|
|
374
|
+
*/
|
|
375
|
+
async function runServiceCommand(command, args, configPath) {
|
|
376
|
+
const { installService, uninstallService, serviceStart, serviceStop, serviceRestart, serviceStatus, serviceLogs, } = await import("./tui/commands.js");
|
|
377
|
+
const verbose = args.includes("--verbose");
|
|
378
|
+
// --user / --system 决定 scope;不传走库里 default(= system)。
|
|
379
|
+
// 同时传 --user 和 --system 时 --user 胜(更"友好"那一个不需要 root)。
|
|
380
|
+
const wantUser = args.includes("--user");
|
|
381
|
+
const wantSystem = args.includes("--system");
|
|
382
|
+
const scope = wantUser ? "user" : (wantSystem ? "system" : undefined);
|
|
383
|
+
switch (command) {
|
|
384
|
+
case "service:install": {
|
|
385
|
+
const result = installService({ configPath, scope });
|
|
386
|
+
printServiceResult(result, verbose);
|
|
387
|
+
if (result.ok && process.platform === "linux") {
|
|
388
|
+
// 仅 user scope 才需要 linger 提示
|
|
389
|
+
const installedScope = scope ?? "system";
|
|
390
|
+
if (installedScope === "user") {
|
|
391
|
+
process.stdout.write("[wand] 想保持登出后也运行:loginctl enable-linger $USER\n");
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return result.ok ? 0 : 1;
|
|
395
|
+
}
|
|
396
|
+
case "service:uninstall": {
|
|
397
|
+
const result = uninstallService(scope ? { scope } : undefined);
|
|
398
|
+
printServiceResult(result, verbose);
|
|
399
|
+
return result.ok ? 0 : 1;
|
|
400
|
+
}
|
|
401
|
+
case "service:start": {
|
|
402
|
+
const result = serviceStart(scope ? { scope } : undefined);
|
|
403
|
+
printServiceResult(result, verbose);
|
|
404
|
+
return result.ok ? 0 : 1;
|
|
405
|
+
}
|
|
406
|
+
case "service:stop": {
|
|
407
|
+
const result = serviceStop(scope ? { scope } : undefined);
|
|
408
|
+
printServiceResult(result, verbose);
|
|
409
|
+
return result.ok ? 0 : 1;
|
|
410
|
+
}
|
|
411
|
+
case "service:restart": {
|
|
412
|
+
const result = serviceRestart(scope ? { scope } : undefined);
|
|
413
|
+
printServiceResult(result, verbose);
|
|
414
|
+
return result.ok ? 0 : 1;
|
|
415
|
+
}
|
|
416
|
+
case "service:status": {
|
|
417
|
+
const status = serviceStatus(scope ? { scope } : undefined);
|
|
418
|
+
process.stdout.write(`[wand] ${status.installed ? "installed" : "not installed"} · ${status.state} · ${status.description}\n`);
|
|
419
|
+
if (verbose && status.raw) {
|
|
420
|
+
process.stdout.write(status.raw + "\n");
|
|
421
|
+
}
|
|
422
|
+
return status.installed && status.state === "active" ? 0 : 1;
|
|
423
|
+
}
|
|
424
|
+
case "service:logs": {
|
|
425
|
+
const linesArg = readFlagValue(args, "--lines");
|
|
426
|
+
const lines = linesArg ? Math.max(1, Math.min(2000, Number(linesArg) || 80)) : 80;
|
|
427
|
+
const result = serviceLogs(lines, scope ? { scope } : undefined);
|
|
428
|
+
if (result.detail) {
|
|
429
|
+
process.stdout.write(result.detail + "\n");
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
process.stdout.write(`[wand] ${result.message}\n`);
|
|
433
|
+
}
|
|
434
|
+
return result.ok ? 0 : 1;
|
|
435
|
+
}
|
|
436
|
+
default:
|
|
437
|
+
process.stderr.write(`[wand] unknown service command: ${command}\n`);
|
|
438
|
+
return 1;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function printServiceResult(result, verbose) {
|
|
442
|
+
const prefix = result.ok ? "[wand]" : "[wand] ✗";
|
|
443
|
+
process.stdout.write(`${prefix} ${result.message}\n`);
|
|
444
|
+
if (verbose && result.detail) {
|
|
445
|
+
process.stdout.write(result.detail + "\n");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
344
448
|
main().catch((error) => {
|
|
345
449
|
process.stderr.write(`[wand] ${error instanceof Error ? error.message : String(error)}\n`);
|
|
346
450
|
process.exitCode = 1;
|
package/dist/git-quick-commit.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { ClaudeRunError, runClaudePrint } from "./claude-sdk-runner.js";
|
|
3
4
|
const GIT_TIMEOUT_MS = 1500;
|
|
4
5
|
const GIT_PUSH_TIMEOUT_MS = 30_000;
|
|
5
6
|
const MAX_FILE_ENTRIES = 200;
|
|
@@ -239,32 +240,23 @@ export class QuickCommitError extends Error {
|
|
|
239
240
|
}
|
|
240
241
|
}
|
|
241
242
|
// ── AI commit message generation ──
|
|
242
|
-
function callClaudeText(prompt, cwd) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const e = error;
|
|
252
|
-
if (e.code === "ENOENT") {
|
|
253
|
-
reject(new QuickCommitError("未找到 claude CLI。", "CLAUDE_CLI_MISSING"));
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
if (e.code === "ETIMEDOUT") {
|
|
257
|
-
reject(new QuickCommitError("Claude 生成超时,请手动填写 commit message。", "CLAUDE_TIMEOUT"));
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
const msg = (stderr || "").trim() || e.message || "claude 调用失败";
|
|
261
|
-
reject(new QuickCommitError(`Claude CLI 失败:${msg}`, "CLAUDE_CLI_FAILED"));
|
|
262
|
-
return;
|
|
243
|
+
async function callClaudeText(prompt, cwd) {
|
|
244
|
+
try {
|
|
245
|
+
return await runClaudePrint(prompt, { cwd, timeoutMs: CLAUDE_MESSAGE_TIMEOUT_MS });
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (error instanceof ClaudeRunError) {
|
|
249
|
+
// 把通用 ClaudeRunError 翻译成 quick-commit 自己的错误码 + 中文话术。
|
|
250
|
+
if (error.code === "CLAUDE_TIMEOUT") {
|
|
251
|
+
throw new QuickCommitError("Claude 生成超时,请手动填写 commit message。", "CLAUDE_TIMEOUT");
|
|
263
252
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
253
|
+
if (error.code === "CLAUDE_EMPTY_RESULT") {
|
|
254
|
+
throw new QuickCommitError("Claude 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
|
|
255
|
+
}
|
|
256
|
+
throw new QuickCommitError(error.message, error.code);
|
|
257
|
+
}
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
268
260
|
}
|
|
269
261
|
function collectStagedDiff(cwd) {
|
|
270
262
|
let diff;
|
|
@@ -52,6 +52,7 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
52
52
|
reuseId?: string;
|
|
53
53
|
cols?: number;
|
|
54
54
|
rows?: number;
|
|
55
|
+
thinkingEffort?: SessionSnapshot["thinkingEffort"];
|
|
55
56
|
}): SessionSnapshot;
|
|
56
57
|
list(): SessionSnapshot[];
|
|
57
58
|
/** Return lightweight snapshots for the session list (no output/messages). */
|
|
@@ -72,6 +73,12 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
72
73
|
* the PTY so Claude Code switches on the fly.
|
|
73
74
|
*/
|
|
74
75
|
setSessionModel(id: string, model: string | null): SessionSnapshot;
|
|
76
|
+
/**
|
|
77
|
+
* Set the thinking-effort level for a PTY session. For interactive Claude PTY
|
|
78
|
+
* we don't intercept raw key input; the effort is applied only when wand UI
|
|
79
|
+
* sends a chat-view message (see sendInput → applyThinkingEffortToPrompt).
|
|
80
|
+
*/
|
|
81
|
+
setSessionThinkingEffort(id: string, effort: SessionSnapshot["thinkingEffort"]): SessionSnapshot;
|
|
75
82
|
sendInput(id: string, input: string, view?: "chat" | "terminal", shortcutKey?: string): SessionSnapshot;
|
|
76
83
|
/** Emit a task event for a session, debounced to avoid flooding */
|
|
77
84
|
private emitTask;
|
package/dist/process-manager.js
CHANGED
|
@@ -12,6 +12,7 @@ import { appendWindow, hasExplicitConfirmSyntax, hasPermissionActionContext, nor
|
|
|
12
12
|
import { buildChildEnv } from "./env-utils.js";
|
|
13
13
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
14
14
|
import { getResumeCommandSessionId } from "./resume-policy.js";
|
|
15
|
+
import { applyThinkingEffortToPrompt, normalizeThinkingEffort } from "./structured-session-manager.js";
|
|
15
16
|
function resolveProviderFromCommand(command) {
|
|
16
17
|
return /^codex\b/.test(command.trim()) ? "codex" : "claude";
|
|
17
18
|
}
|
|
@@ -634,6 +635,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
634
635
|
knownClaudeProjectMtimes: knownClaudeProjectMtimes ?? undefined,
|
|
635
636
|
approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
|
|
636
637
|
selectedModel: selectedModel ?? null,
|
|
638
|
+
thinkingEffort: normalizeThinkingEffort(opts?.thinkingEffort),
|
|
637
639
|
// cols 上限 256:与 @wterm/dom WASM grid 的 maxCols 硬编码一致,
|
|
638
640
|
// 防止服务端按 >256 cols 让 Claude 用 CSI 绝对列定位写到 wterm 实际
|
|
639
641
|
// 渲染不到的列上(表现为"内容神奇复制下行")。
|
|
@@ -926,6 +928,19 @@ export class ProcessManager extends EventEmitter {
|
|
|
926
928
|
this.emitEvent({ type: "status", sessionId: id, data: { selectedModel: normalized } });
|
|
927
929
|
return this.snapshot(record);
|
|
928
930
|
}
|
|
931
|
+
/**
|
|
932
|
+
* Set the thinking-effort level for a PTY session. For interactive Claude PTY
|
|
933
|
+
* we don't intercept raw key input; the effort is applied only when wand UI
|
|
934
|
+
* sends a chat-view message (see sendInput → applyThinkingEffortToPrompt).
|
|
935
|
+
*/
|
|
936
|
+
setSessionThinkingEffort(id, effort) {
|
|
937
|
+
const record = this.mustGet(id);
|
|
938
|
+
const normalized = normalizeThinkingEffort(effort);
|
|
939
|
+
record.thinkingEffort = normalized;
|
|
940
|
+
this.persist(record);
|
|
941
|
+
this.emitEvent({ type: "status", sessionId: id, data: { thinkingEffort: normalized } });
|
|
942
|
+
return this.snapshot(record);
|
|
943
|
+
}
|
|
929
944
|
sendInput(id, input, view, shortcutKey) {
|
|
930
945
|
const record = this.mustGet(id);
|
|
931
946
|
if (record.status !== "running") {
|
|
@@ -949,11 +964,19 @@ export class ProcessManager extends EventEmitter {
|
|
|
949
964
|
};
|
|
950
965
|
this.logger.appendShortcutLog(id, shortcutKey, tailLines, ctx);
|
|
951
966
|
}
|
|
967
|
+
// Thinking-depth magic-word injection. Only applied to chat-view submits
|
|
968
|
+
// (terminal direct keystrokes are pass-through). applyThinkingEffortToPrompt
|
|
969
|
+
// is safe on empty / lone-\r chunks (returns input unchanged) and won't
|
|
970
|
+
// double-prefix if the user already wrote the magic word themselves.
|
|
971
|
+
let effectiveInput = input;
|
|
972
|
+
if (view === "chat" && record.thinkingEffort && record.thinkingEffort !== "off") {
|
|
973
|
+
effectiveInput = applyThinkingEffortToPrompt(input, record.thinkingEffort);
|
|
974
|
+
}
|
|
952
975
|
// Track user input via bridge for Chat mode
|
|
953
976
|
if (record.ptyBridge) {
|
|
954
|
-
record.ptyBridge.onUserInput(
|
|
977
|
+
record.ptyBridge.onUserInput(effectiveInput);
|
|
955
978
|
}
|
|
956
|
-
record.ptyProcess.write(
|
|
979
|
+
record.ptyProcess.write(effectiveInput);
|
|
957
980
|
this.persist(record);
|
|
958
981
|
return this.snapshot(record);
|
|
959
982
|
}
|
|
@@ -1203,6 +1226,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1203
1226
|
summary: deriveSessionSummary(messages, record.currentTask?.title ?? null),
|
|
1204
1227
|
currentTaskTitle: record.status === "running" ? record.currentTask?.title ?? undefined : undefined,
|
|
1205
1228
|
selectedModel: record.selectedModel ?? null,
|
|
1229
|
+
thinkingEffort: record.thinkingEffort ?? null,
|
|
1206
1230
|
ptyCols: record.ptyCols,
|
|
1207
1231
|
ptyRows: record.ptyRows,
|
|
1208
1232
|
};
|
package/dist/prompt-optimizer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ClaudeRunError, runClaudePrint } from "./claude-sdk-runner.js";
|
|
2
2
|
const CLAUDE_TIMEOUT_MS = 60_000;
|
|
3
3
|
const MAX_INPUT_LENGTH = 8000;
|
|
4
4
|
export class PromptOptimizeError extends Error {
|
|
@@ -9,32 +9,23 @@ export class PromptOptimizeError extends Error {
|
|
|
9
9
|
this.name = "PromptOptimizeError";
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
-
function callClaudeText(prompt, cwd) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
if (e.code === "ETIMEDOUT") {
|
|
27
|
-
reject(new PromptOptimizeError("Claude 优化超时,请稍后重试。", "CLAUDE_TIMEOUT"));
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
const msg = (stderr || "").trim() || e.message || "claude 调用失败";
|
|
31
|
-
reject(new PromptOptimizeError(`Claude CLI 失败:${msg}`, "CLAUDE_CLI_FAILED"));
|
|
32
|
-
return;
|
|
12
|
+
async function callClaudeText(prompt, cwd) {
|
|
13
|
+
try {
|
|
14
|
+
return await runClaudePrint(prompt, { cwd, timeoutMs: CLAUDE_TIMEOUT_MS });
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error instanceof ClaudeRunError) {
|
|
18
|
+
// 翻译成 prompt-optimizer 自己的话术 + 错误码(与原文案保持一致)。
|
|
19
|
+
if (error.code === "CLAUDE_TIMEOUT") {
|
|
20
|
+
throw new PromptOptimizeError("Claude 优化超时,请稍后重试。", "CLAUDE_TIMEOUT");
|
|
21
|
+
}
|
|
22
|
+
if (error.code === "CLAUDE_EMPTY_RESULT") {
|
|
23
|
+
throw new PromptOptimizeError("Claude 返回了空结果。", "EMPTY_RESULT");
|
|
33
24
|
}
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
}
|
|
25
|
+
throw new PromptOptimizeError(error.message, error.code);
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
38
29
|
}
|
|
39
30
|
function buildOptimizePrompt(userInput, language) {
|
|
40
31
|
const lang = (language || "").trim() || "中文";
|