@co0ontty/wand 1.43.7 → 1.43.8
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/dist/build-info.json +3 -3
- package/dist/env-utils.d.ts +2 -0
- package/dist/env-utils.js +4 -0
- package/dist/language-prompt.d.ts +5 -0
- package/dist/language-prompt.js +9 -0
- package/dist/models.d.ts +1 -6
- package/dist/models.js +2 -14
- package/dist/npm-update-utils.d.ts +0 -1
- package/dist/npm-update-utils.js +4 -13
- package/dist/path-repair.d.ts +4 -0
- package/dist/path-repair.js +6 -16
- package/dist/process-manager.d.ts +0 -2
- package/dist/process-manager.js +3 -13
- package/dist/pty-text-utils.d.ts +0 -2
- package/dist/pty-text-utils.js +2 -2
- package/dist/resume-policy.d.ts +0 -2
- package/dist/resume-policy.js +0 -11
- package/dist/server-session-routes.js +0 -8
- package/dist/server.js +3 -65
- package/dist/storage.d.ts +0 -1
- package/dist/storage.js +0 -3
- package/dist/structured-session-manager.d.ts +0 -5
- package/dist/structured-session-manager.js +6 -46
- package/dist/tui/commands.js +6 -16
- package/dist/version-utils.d.ts +16 -0
- package/dist/version-utils.js +66 -0
- package/dist/web-ui/content/scripts.js +54 -21849
- package/dist/web-ui/content/styles.css +1 -16358
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/package.json +5 -6
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-05-
|
|
4
|
-
"version": "1.43.
|
|
2
|
+
"commit": "61d4063f143052abb4b0aa245746ab089ce32288",
|
|
3
|
+
"builtAt": "2026-05-31T12:11:01.894Z",
|
|
4
|
+
"version": "1.43.8",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
package/dist/env-utils.d.ts
CHANGED
package/dist/env-utils.js
CHANGED
|
@@ -19,6 +19,10 @@ const MINIMAL_ENV_KEYS = [
|
|
|
19
19
|
"TEMP",
|
|
20
20
|
"PWD",
|
|
21
21
|
];
|
|
22
|
+
/** 是否以 root 身份运行(uid 或 euid 为 0)。供 PTY runner 与 structured runner 共用。 */
|
|
23
|
+
export function isRunningAsRoot() {
|
|
24
|
+
return process.getuid?.() === 0 || process.geteuid?.() === 0;
|
|
25
|
+
}
|
|
22
26
|
/**
|
|
23
27
|
* 根据 inheritEnv 配置组装子进程的环境变量。
|
|
24
28
|
*
|
|
@@ -15,3 +15,8 @@
|
|
|
15
15
|
* 英文模式("English")下 Claude 默认就用英文,只补一句 subagent 透传。
|
|
16
16
|
*/
|
|
17
17
|
export declare function buildLanguageDirective(language: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* 完全托管自主模式的系统提示:供 PTY runner 与 structured runner(CLI + SDK)共用,
|
|
20
|
+
* 避免两处各抄一份中英双语文案导致改一处漏一处。
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildManagedAutonomyDirective(isChinese: boolean): string;
|
package/dist/language-prompt.js
CHANGED
|
@@ -66,3 +66,12 @@ export function buildLanguageDirective(language) {
|
|
|
66
66
|
`Subagent: when you dispatch a subagent via the Task tool, you MUST explicitly instruct the subagent in its prompt to also respond in ${trimmed}.`,
|
|
67
67
|
].join("\n");
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* 完全托管自主模式的系统提示:供 PTY runner 与 structured runner(CLI + SDK)共用,
|
|
71
|
+
* 避免两处各抄一份中英双语文案导致改一处漏一处。
|
|
72
|
+
*/
|
|
73
|
+
export function buildManagedAutonomyDirective(isChinese) {
|
|
74
|
+
return isChinese
|
|
75
|
+
? "你正在完全托管的自主模式下运行。用户可能无法及时回复问题或确认。你必须独立做出所有决策——自行选择最佳方案,而不是向用户询问偏好、确认或澄清。如果有多种可行方案,选择你认为最合适的并继续执行。除非任务本身存在根本性的歧义且无法合理推断,否则不要等待用户输入。果断行动,自主决策。"
|
|
76
|
+
: "You are running in a fully managed, autonomous mode. The user may not be available to respond to questions or confirmations in a timely manner. You MUST make all decisions independently — choose the best approach yourself instead of asking the user for preferences, confirmations, or clarifications. If multiple approaches are viable, pick the one you judge most appropriate and proceed. Never block on user input unless the task is fundamentally ambiguous and cannot be reasonably inferred. Be decisive and self-directed.";
|
|
77
|
+
}
|
package/dist/models.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ClaudeModelInfo
|
|
1
|
+
import { ClaudeModelInfo } from "./types.js";
|
|
2
2
|
interface ModelCache {
|
|
3
3
|
models: ClaudeModelInfo[];
|
|
4
4
|
codexModels: ClaudeModelInfo[];
|
|
@@ -7,9 +7,4 @@ interface ModelCache {
|
|
|
7
7
|
}
|
|
8
8
|
export declare function getCachedModels(): ModelCache;
|
|
9
9
|
export declare function refreshModels(): Promise<ModelCache>;
|
|
10
|
-
export declare function getModelsForProvider(provider: SessionProvider): ClaudeModelInfo[];
|
|
11
|
-
/** 返回可用于 claude CLI 的全部已知 model id(含别名) */
|
|
12
|
-
export declare function knownModelIds(): string[];
|
|
13
|
-
/** 判断传入值是否是已知模型;允许自由文本,因此总是返回 true。保留接口以便将来严格校验。 */
|
|
14
|
-
export declare function isKnownModel(_value: string): boolean;
|
|
15
10
|
export {};
|
package/dist/models.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { exec } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import { extractSemver } from "./version-utils.js";
|
|
3
4
|
const execAsync = promisify(exec);
|
|
4
5
|
const CLAUDE_MODELS = [
|
|
5
6
|
{ id: "default", label: "default(跟随 Claude Code 默认)", alias: true },
|
|
@@ -21,8 +22,7 @@ function cloneClaudeModels() {
|
|
|
21
22
|
async function probeClaudeVersion() {
|
|
22
23
|
try {
|
|
23
24
|
const { stdout } = await execAsync("claude --version", { timeout: 5000 });
|
|
24
|
-
|
|
25
|
-
return match ? match[0] : stdout.trim().slice(0, 64) || null;
|
|
25
|
+
return extractSemver(stdout) ?? (stdout.trim().slice(0, 64) || null);
|
|
26
26
|
}
|
|
27
27
|
catch {
|
|
28
28
|
return null;
|
|
@@ -78,15 +78,3 @@ export async function refreshModels() {
|
|
|
78
78
|
};
|
|
79
79
|
return cache;
|
|
80
80
|
}
|
|
81
|
-
export function getModelsForProvider(provider) {
|
|
82
|
-
const cached = getCachedModels();
|
|
83
|
-
return provider === "codex" ? cached.codexModels : cached.models;
|
|
84
|
-
}
|
|
85
|
-
/** 返回可用于 claude CLI 的全部已知 model id(含别名) */
|
|
86
|
-
export function knownModelIds() {
|
|
87
|
-
return CLAUDE_MODELS.map((m) => m.id);
|
|
88
|
-
}
|
|
89
|
-
/** 判断传入值是否是已知模型;允许自由文本,因此总是返回 true。保留接口以便将来严格校验。 */
|
|
90
|
-
export function isKnownModel(_value) {
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
@@ -58,4 +58,3 @@ export declare function installPackageGloballySync(pkg: string, timeoutMs: numbe
|
|
|
58
58
|
* (npm 全局 bin 里的符号链接,node 跟随软链一样能跑)。都找不到返回 null。
|
|
59
59
|
*/
|
|
60
60
|
export declare function resolveGlobalWandCli(): string | null;
|
|
61
|
-
export declare const NPM_UPDATE_PACKAGE_NAME = "@co0ontty/wand";
|
package/dist/npm-update-utils.js
CHANGED
|
@@ -17,6 +17,7 @@ import os from "node:os";
|
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import process from "node:process";
|
|
19
19
|
import { promisify } from "node:util";
|
|
20
|
+
import { whichSync } from "./path-repair.js";
|
|
20
21
|
const execFileAsync = promisify(execFile);
|
|
21
22
|
const PACKAGE_NAME = "@co0ontty/wand";
|
|
22
23
|
const PACKAGE_SCOPE = "@co0ontty";
|
|
@@ -407,18 +408,8 @@ export function resolveGlobalWandCli() {
|
|
|
407
408
|
/* ignore */
|
|
408
409
|
}
|
|
409
410
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (r.status === 0) {
|
|
414
|
-
const first = (r.stdout || "").split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
415
|
-
if (first)
|
|
416
|
-
return first.trim();
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
catch {
|
|
420
|
-
/* ignore */
|
|
421
|
-
}
|
|
411
|
+
const found = whichSync("wand", { env: getChildEnv(), timeoutMs: 10_000 });
|
|
412
|
+
if (found)
|
|
413
|
+
return found;
|
|
422
414
|
return null;
|
|
423
415
|
}
|
|
424
|
-
export const NPM_UPDATE_PACKAGE_NAME = PACKAGE_NAME;
|
package/dist/path-repair.d.ts
CHANGED
|
@@ -68,6 +68,10 @@ export declare function deepRepairRuntimePath(result: PathRepairResult, opts?: {
|
|
|
68
68
|
shell?: string;
|
|
69
69
|
timeoutMs?: number;
|
|
70
70
|
}): Promise<PathRepairResult>;
|
|
71
|
+
export declare function whichSync(cmd: string, options?: {
|
|
72
|
+
env?: NodeJS.ProcessEnv;
|
|
73
|
+
timeoutMs?: number;
|
|
74
|
+
}): string | null;
|
|
71
75
|
/**
|
|
72
76
|
* 把修复结果格式化为一行可读摘要(startServer 启动日志用)。
|
|
73
77
|
*
|
package/dist/path-repair.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { compareSemver } from "./version-utils.js";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import process from "node:process";
|
|
@@ -84,24 +85,13 @@ function listLatestVersions(base, limit) {
|
|
|
84
85
|
const entries = readdirSync(base);
|
|
85
86
|
return entries
|
|
86
87
|
.filter((e) => /^v?\d+\.\d+\.\d+/.test(e))
|
|
87
|
-
.sort((a, b) =>
|
|
88
|
+
.sort((a, b) => compareSemver(b, a))
|
|
88
89
|
.slice(0, limit);
|
|
89
90
|
}
|
|
90
91
|
catch {
|
|
91
92
|
return [];
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
|
-
function semverCompare(a, b) {
|
|
95
|
-
const pa = a.replace(/^v/, "").split(/[.\-+]/).map((x) => Number(x) || 0);
|
|
96
|
-
const pb = b.replace(/^v/, "").split(/[.\-+]/).map((x) => Number(x) || 0);
|
|
97
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
98
|
-
const da = pa[i] ?? 0;
|
|
99
|
-
const db = pb[i] ?? 0;
|
|
100
|
-
if (da !== db)
|
|
101
|
-
return da - db;
|
|
102
|
-
}
|
|
103
|
-
return 0;
|
|
104
|
-
}
|
|
105
95
|
/**
|
|
106
96
|
* 把候选目录中"存在 + 未在 PATH 中"的追加到 process.env.PATH。
|
|
107
97
|
*
|
|
@@ -328,12 +318,12 @@ function probeCommands() {
|
|
|
328
318
|
}
|
|
329
319
|
return out;
|
|
330
320
|
}
|
|
331
|
-
function whichSync(cmd) {
|
|
332
|
-
// Windows 用 `where`,POSIX 用 `which
|
|
333
|
-
// repairRuntimePath() 追加完 PATH
|
|
321
|
+
export function whichSync(cmd, options) {
|
|
322
|
+
// Windows 用 `where`,POSIX 用 `which`。默认走 process.env.PATH(因此一定要在
|
|
323
|
+
// repairRuntimePath() 追加完 PATH 之后再调);options.env 可覆盖(如用 buildChildEnv 的 PATH)。
|
|
334
324
|
const tool = process.platform === "win32" ? "where" : "which";
|
|
335
325
|
try {
|
|
336
|
-
const res = spawnSync(tool, [cmd], { encoding: "utf8" });
|
|
326
|
+
const res = spawnSync(tool, [cmd], { encoding: "utf8", env: options?.env, timeout: options?.timeoutMs });
|
|
337
327
|
if (res.status !== 0)
|
|
338
328
|
return null;
|
|
339
329
|
const first = (res.stdout || "").split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
@@ -144,8 +144,6 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
144
144
|
* Handle events from ClaudePtyBridge
|
|
145
145
|
*/
|
|
146
146
|
private handleBridgeEvent;
|
|
147
|
-
/** Check if a command is a Claude CLI command */
|
|
148
|
-
private isClaudeCommand;
|
|
149
147
|
private mustGet;
|
|
150
148
|
private buildShellArgs;
|
|
151
149
|
private shouldAutoApprovePermissions;
|
package/dist/process-manager.js
CHANGED
|
@@ -9,17 +9,14 @@ import { SessionLogger } from "./session-logger.js";
|
|
|
9
9
|
import { ClaudePtyBridge } from "./claude-pty-bridge.js";
|
|
10
10
|
import { truncateMessagesForTransport } from "./message-truncator.js";
|
|
11
11
|
import { appendWindow, hasExplicitConfirmSyntax, hasPermissionActionContext, normalizePromptText, PTY_OUTPUT_MAX_SIZE } from "./pty-text-utils.js";
|
|
12
|
-
import { buildChildEnv } from "./env-utils.js";
|
|
13
|
-
import { buildLanguageDirective } from "./language-prompt.js";
|
|
12
|
+
import { buildChildEnv, isRunningAsRoot } from "./env-utils.js";
|
|
13
|
+
import { buildLanguageDirective, buildManagedAutonomyDirective } from "./language-prompt.js";
|
|
14
14
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
15
15
|
import { getCodexResumeCommandSessionId, getResumeCommandSessionId } from "./resume-policy.js";
|
|
16
16
|
import { applyThinkingEffortToPrompt, normalizeThinkingEffort } from "./structured-session-manager.js";
|
|
17
17
|
function resolveProviderFromCommand(command) {
|
|
18
18
|
return /^codex\b/.test(command.trim()) ? "codex" : "claude";
|
|
19
19
|
}
|
|
20
|
-
function isRunningAsRoot() {
|
|
21
|
-
return typeof process.getuid === "function" && process.getuid() === 0;
|
|
22
|
-
}
|
|
23
20
|
export class SessionInputError extends Error {
|
|
24
21
|
code;
|
|
25
22
|
sessionId;
|
|
@@ -1866,11 +1863,6 @@ export class ProcessManager extends EventEmitter {
|
|
|
1866
1863
|
break;
|
|
1867
1864
|
}
|
|
1868
1865
|
}
|
|
1869
|
-
/** Check if a command is a Claude CLI command */
|
|
1870
|
-
isClaudeCommand(command) {
|
|
1871
|
-
const trimmed = command.trim();
|
|
1872
|
-
return /^claude\b/.test(trimmed);
|
|
1873
|
-
}
|
|
1874
1866
|
mustGet(id) {
|
|
1875
1867
|
const record = this.sessions.get(id);
|
|
1876
1868
|
if (!record) {
|
|
@@ -1953,9 +1945,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1953
1945
|
const language = this.config.language?.trim();
|
|
1954
1946
|
const isChinese = language === "中文";
|
|
1955
1947
|
if (mode === "managed") {
|
|
1956
|
-
const autonomousPrompt = isChinese
|
|
1957
|
-
? "你正在完全托管的自主模式下运行。用户可能无法及时回复问题或确认。你必须独立做出所有决策——自行选择最佳方案,而不是向用户询问偏好、确认或澄清。如果有多种可行方案,选择你认为最合适的并继续执行。除非任务本身存在根本性的歧义且无法合理推断,否则不要等待用户输入。果断行动,自主决策。"
|
|
1958
|
-
: "You are running in a fully managed, autonomous mode. The user may not be available to respond to questions or confirmations in a timely manner. You MUST make all decisions independently — choose the best approach yourself instead of asking the user for preferences, confirmations, or clarifications. If multiple approaches are viable, pick the one you judge most appropriate and proceed. Never block on user input unless the task is fundamentally ambiguous and cannot be reasonably inferred. Be decisive and self-directed.";
|
|
1948
|
+
const autonomousPrompt = buildManagedAutonomyDirective(isChinese);
|
|
1959
1949
|
const escaped = autonomousPrompt.replace(/'/g, "'\\''");
|
|
1960
1950
|
result += ` --append-system-prompt '${escaped}'`;
|
|
1961
1951
|
}
|
package/dist/pty-text-utils.d.ts
CHANGED
|
@@ -22,8 +22,6 @@ export declare function isNoiseLine(line: string): boolean;
|
|
|
22
22
|
* The returned buffer may be slightly shorter than maxSize.
|
|
23
23
|
*/
|
|
24
24
|
export declare function appendWindow(buffer: string, chunk: string, maxSize: number): string;
|
|
25
|
-
/** Slice keeping the last ~maxSize chars on a safe boundary. Exported for tests. */
|
|
26
|
-
export declare function safeSliceTail(text: string, maxSize: number): string;
|
|
27
25
|
/**
|
|
28
26
|
* Strip a string down to the printable codepoints used for echo matching.
|
|
29
27
|
* Removes control characters, whitespace and ANSI escapes; keeps all other
|
package/dist/pty-text-utils.js
CHANGED
|
@@ -137,8 +137,8 @@ export function appendWindow(buffer, chunk, maxSize) {
|
|
|
137
137
|
return next;
|
|
138
138
|
return safeSliceTail(next, maxSize);
|
|
139
139
|
}
|
|
140
|
-
/** Slice keeping the last ~maxSize chars on a safe boundary.
|
|
141
|
-
|
|
140
|
+
/** Slice keeping the last ~maxSize chars on a safe boundary. */
|
|
141
|
+
function safeSliceTail(text, maxSize) {
|
|
142
142
|
if (text.length <= maxSize)
|
|
143
143
|
return text;
|
|
144
144
|
let start = text.length - maxSize;
|
package/dist/resume-policy.d.ts
CHANGED
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
import { ConversationTurn } from "./types.js";
|
|
2
|
-
export declare function hasRealConversationMessages(messages: ConversationTurn[] | undefined): boolean;
|
|
3
1
|
export declare function getResumeCommandSessionId(command: string): string | null;
|
|
4
2
|
export declare function getCodexResumeCommandSessionId(command: string): string | null;
|
package/dist/resume-policy.js
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
|
-
const REAL_CONVERSATION_MIN_MESSAGES = 2;
|
|
2
1
|
const UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
3
2
|
const RESUME_COMMAND_ID_PATTERN = new RegExp(`(?:^|\\s)--resume\\s+(${UUID_PATTERN})(?:\\s|$)`, "i");
|
|
4
3
|
const CODEX_RESUME_COMMAND_ID_PATTERN = new RegExp(`(?:^|\\s)resume\\s+(${UUID_PATTERN})(?:\\s|$)`, "i");
|
|
5
|
-
export function hasRealConversationMessages(messages) {
|
|
6
|
-
if (!messages || messages.length < REAL_CONVERSATION_MIN_MESSAGES) {
|
|
7
|
-
return false;
|
|
8
|
-
}
|
|
9
|
-
const hasUser = messages.some((turn) => turn.role === "user"
|
|
10
|
-
&& turn.content.some((block) => block.type === "text" && block.text.trim().length > 0));
|
|
11
|
-
const hasAssistant = messages.some((turn) => turn.role === "assistant"
|
|
12
|
-
&& turn.content.some((block) => block.type === "text" && block.text.trim().length > 0));
|
|
13
|
-
return hasUser && hasAssistant;
|
|
14
|
-
}
|
|
15
4
|
export function getResumeCommandSessionId(command) {
|
|
16
5
|
const match = RESUME_COMMAND_ID_PATTERN.exec(command);
|
|
17
6
|
return match?.[1] ?? null;
|
|
@@ -146,12 +146,10 @@ function isMergeActionAllowed(snapshot) {
|
|
|
146
146
|
export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config, onSessionCreated) {
|
|
147
147
|
app.get("/api/sessions", (_req, res) => {
|
|
148
148
|
const all = listAllSessionsSlim(processes, structured);
|
|
149
|
-
console.log("[WAND] GET /api/sessions count:", all.length, "sessions:", all.map(s => ({ id: s.id.substring(0, 8), kind: s.sessionKind, runner: s.runner, status: s.status })));
|
|
150
149
|
res.json(all);
|
|
151
150
|
});
|
|
152
151
|
app.post("/api/structured-sessions", express.json(), async (req, res) => {
|
|
153
152
|
const body = req.body;
|
|
154
|
-
console.log("[WAND] POST /api/structured-sessions body:", JSON.stringify({ cwd: body.cwd, mode: body.mode, runner: body.runner, provider: body.provider, worktreeEnabled: body.worktreeEnabled === true, hasPrompt: !!body.prompt, model: body.model, thinkingEffort: body.thinkingEffort }));
|
|
155
153
|
try {
|
|
156
154
|
if (body.provider && body.provider !== "claude" && body.provider !== "codex") {
|
|
157
155
|
res.status(400).json({ error: "结构化会话当前仅支持 Claude 或 Codex provider。" });
|
|
@@ -169,7 +167,6 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
169
167
|
? body.thinkingEffort
|
|
170
168
|
: undefined,
|
|
171
169
|
});
|
|
172
|
-
console.log("[WAND] structured session created:", JSON.stringify({ id: snapshot.id, sessionKind: snapshot.sessionKind, runner: snapshot.runner, status: snapshot.status }));
|
|
173
170
|
onSessionCreated?.(body.cwd ?? snapshot.cwd);
|
|
174
171
|
const prompt = body.prompt?.trim();
|
|
175
172
|
if (prompt) {
|
|
@@ -244,7 +241,6 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
244
241
|
// 让退出 handler 不要把剩余 queuedMessages 清空(默认行为是清空)。
|
|
245
242
|
const preserveQueue = !!req.body?.preserveQueue;
|
|
246
243
|
const idempotencyKey = typeof req.body?.idempotencyKey === "string" ? req.body.idempotencyKey : undefined;
|
|
247
|
-
console.log("[WAND] POST /api/structured-sessions/:id/messages id:", req.params.id, "input:", input.substring(0, 50), "interrupt:", interrupt, "preserveQueue:", preserveQueue, "idempotencyKey:", idempotencyKey);
|
|
248
244
|
try {
|
|
249
245
|
const snapshot = await structured.sendMessage(req.params.id, input, { interrupt, preserveQueue, idempotencyKey });
|
|
250
246
|
res.json(snapshot);
|
|
@@ -591,10 +587,8 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
591
587
|
app.post("/api/sessions/:id/resume", (req, res) => {
|
|
592
588
|
const sessionId = req.params.id;
|
|
593
589
|
const body = req.body;
|
|
594
|
-
console.log("[WAND] POST /api/sessions/:id/resume sessionId:", sessionId);
|
|
595
590
|
try {
|
|
596
591
|
const existingSession = processes.get(sessionId) || storage.getSession(sessionId);
|
|
597
|
-
console.log("[WAND] resume lookup: found:", !!existingSession, "sessionKind:", existingSession?.sessionKind, "claudeSessionId:", existingSession?.claudeSessionId);
|
|
598
592
|
if (!existingSession) {
|
|
599
593
|
res.status(404).json({ error: "会话不存在。" });
|
|
600
594
|
return;
|
|
@@ -644,7 +638,6 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
644
638
|
app.post("/api/claude-sessions/:claudeSessionId/resume", (req, res) => {
|
|
645
639
|
const claudeSessionId = String(req.params.claudeSessionId || "").trim();
|
|
646
640
|
const body = req.body;
|
|
647
|
-
console.log("[WAND] POST /api/claude-sessions/:claudeSessionId/resume claudeSessionId:", claudeSessionId, "cwd:", body.cwd);
|
|
648
641
|
try {
|
|
649
642
|
if (!claudeSessionId) {
|
|
650
643
|
res.status(400).json({ error: "Claude 会话 ID 不能为空。" });
|
|
@@ -722,7 +715,6 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
722
715
|
app.post("/api/codex-sessions/:threadId/resume", express.json(), async (req, res) => {
|
|
723
716
|
const threadId = String(req.params.threadId || "").trim();
|
|
724
717
|
const body = req.body;
|
|
725
|
-
console.log("[WAND] POST /api/codex-sessions/:threadId/resume threadId:", threadId, "cwd:", body.cwd);
|
|
726
718
|
try {
|
|
727
719
|
if (!threadId) {
|
|
728
720
|
res.status(400).json({ error: "Codex 会话 ID 不能为空。" });
|
package/dist/server.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { compareSemver, extractSemver } from "./version-utils.js";
|
|
2
3
|
import compression from "compression";
|
|
3
4
|
import express from "express";
|
|
4
5
|
import { createReadStream, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -70,56 +71,6 @@ async function checkNpmLatestVersion(forceRefresh = false) {
|
|
|
70
71
|
updateAvailable: latest !== PKG_VERSION && compareSemver(latest, PKG_VERSION) > 0,
|
|
71
72
|
};
|
|
72
73
|
}
|
|
73
|
-
function compareSemver(a, b) {
|
|
74
|
-
const parse = (v) => {
|
|
75
|
-
const [main, ...rest] = v.split("-");
|
|
76
|
-
const pre = rest.join("-");
|
|
77
|
-
const mainParts = main.split(".").map((n) => Number(n) || 0);
|
|
78
|
-
return { mainParts, pre };
|
|
79
|
-
};
|
|
80
|
-
const pa = parse(a);
|
|
81
|
-
const pb = parse(b);
|
|
82
|
-
for (let i = 0; i < 3; i++) {
|
|
83
|
-
const diff = (pa.mainParts[i] || 0) - (pb.mainParts[i] || 0);
|
|
84
|
-
if (diff !== 0)
|
|
85
|
-
return diff;
|
|
86
|
-
}
|
|
87
|
-
// Main version equal — apply semver prerelease rule: no prerelease > with prerelease.
|
|
88
|
-
if (!pa.pre && pb.pre)
|
|
89
|
-
return 1;
|
|
90
|
-
if (pa.pre && !pb.pre)
|
|
91
|
-
return -1;
|
|
92
|
-
if (!pa.pre && !pb.pre)
|
|
93
|
-
return 0;
|
|
94
|
-
// Both have prerelease: 按 . 分段比较 (数字段数值比, 非数字段字典序), 贴近标准 semver,
|
|
95
|
-
// 避免跨月/跨年的 debug.MMDDHHMM 后缀因纯字典序而排反。
|
|
96
|
-
const segA = pa.pre.split(".");
|
|
97
|
-
const segB = pb.pre.split(".");
|
|
98
|
-
const segLen = Math.max(segA.length, segB.length);
|
|
99
|
-
for (let i = 0; i < segLen; i++) {
|
|
100
|
-
const sa = segA[i];
|
|
101
|
-
const sb = segB[i];
|
|
102
|
-
if (sa === undefined)
|
|
103
|
-
return -1; // 段少者更小
|
|
104
|
-
if (sb === undefined)
|
|
105
|
-
return 1;
|
|
106
|
-
const na = Number(sa);
|
|
107
|
-
const nb = Number(sb);
|
|
108
|
-
const aIsNum = sa !== "" && !Number.isNaN(na);
|
|
109
|
-
const bIsNum = sb !== "" && !Number.isNaN(nb);
|
|
110
|
-
if (aIsNum && bIsNum) {
|
|
111
|
-
if (na !== nb)
|
|
112
|
-
return na < nb ? -1 : 1;
|
|
113
|
-
}
|
|
114
|
-
else if (aIsNum !== bIsNum) {
|
|
115
|
-
return aIsNum ? -1 : 1; // 数字段 < 非数字段
|
|
116
|
-
}
|
|
117
|
-
else if (sa !== sb) {
|
|
118
|
-
return sa < sb ? -1 : 1;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return 0;
|
|
122
|
-
}
|
|
123
74
|
/** 读取 dist/build-info.json(由 scripts/stamp-build-info.js 在 build 时生成)。 */
|
|
124
75
|
function readBuildInfo() {
|
|
125
76
|
try {
|
|
@@ -382,15 +333,6 @@ async function buildStructuredChatPersonaPayload(configPath, config) {
|
|
|
382
333
|
return { user, assistant };
|
|
383
334
|
}
|
|
384
335
|
// ── Git helpers ──
|
|
385
|
-
async function isGitRepo(dirPath) {
|
|
386
|
-
try {
|
|
387
|
-
await execAsync("git rev-parse --is-inside-work-tree", { cwd: dirPath });
|
|
388
|
-
return true;
|
|
389
|
-
}
|
|
390
|
-
catch {
|
|
391
|
-
return false;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
336
|
async function getGitRepoRoot(dirPath) {
|
|
395
337
|
try {
|
|
396
338
|
const { stdout } = await execAsync("git rev-parse --show-toplevel", { cwd: dirPath });
|
|
@@ -576,10 +518,6 @@ function normalizeMode(input, fallback) {
|
|
|
576
518
|
return isExecutionMode(input) ? input : fallback;
|
|
577
519
|
}
|
|
578
520
|
/** Match a semver-looking token in a file name (with optional pre-release / build metadata). */
|
|
579
|
-
function extractSemverFromName(name) {
|
|
580
|
-
const match = name.match(/(\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?)/);
|
|
581
|
-
return match ? match[1] : null;
|
|
582
|
-
}
|
|
583
521
|
function resolveAndroidApkDir(configDir, config) {
|
|
584
522
|
const configuredDir = config.android?.apkDir?.trim();
|
|
585
523
|
if (!configuredDir) {
|
|
@@ -588,7 +526,7 @@ function resolveAndroidApkDir(configDir, config) {
|
|
|
588
526
|
return path.isAbsolute(configuredDir) ? configuredDir : path.resolve(configDir, configuredDir);
|
|
589
527
|
}
|
|
590
528
|
function extractAndroidApkVersion(fileName) {
|
|
591
|
-
return
|
|
529
|
+
return extractSemver(fileName.replace(/\.apk$/i, ""));
|
|
592
530
|
}
|
|
593
531
|
async function resolveAndroidApkAsset(configDir, config) {
|
|
594
532
|
if (config.android?.enabled !== true)
|
|
@@ -666,7 +604,7 @@ function resolveMacosDmgDir(configDir, config) {
|
|
|
666
604
|
return path.isAbsolute(configuredDir) ? configuredDir : path.resolve(configDir, configuredDir);
|
|
667
605
|
}
|
|
668
606
|
function extractMacosDmgVersion(fileName) {
|
|
669
|
-
return
|
|
607
|
+
return extractSemver(fileName.replace(/\.dmg$/i, ""));
|
|
670
608
|
}
|
|
671
609
|
async function resolveMacosDmgAsset(configDir, config) {
|
|
672
610
|
if (config.macos?.enabled !== true)
|
package/dist/storage.d.ts
CHANGED
|
@@ -32,7 +32,6 @@ export declare class WandStorage {
|
|
|
32
32
|
getAppSecret(): string | null;
|
|
33
33
|
/** Persist appSecret in database (DB is the authoritative source after first migration) */
|
|
34
34
|
setAppSecret(value: string): void;
|
|
35
|
-
hasAppSecret(): boolean;
|
|
36
35
|
saveAuthSession(token: string, expiresAt: number): void;
|
|
37
36
|
getAuthSession(token: string): PersistedAuthSession | null;
|
|
38
37
|
deleteAuthSession(token: string): void;
|
package/dist/storage.js
CHANGED
|
@@ -310,9 +310,6 @@ export class WandStorage {
|
|
|
310
310
|
setAppSecret(value) {
|
|
311
311
|
this.setConfigValue("appSecret", value);
|
|
312
312
|
}
|
|
313
|
-
hasAppSecret() {
|
|
314
|
-
return this.getAppSecret() !== null;
|
|
315
|
-
}
|
|
316
313
|
// ============ Auth Session Methods ============
|
|
317
314
|
saveAuthSession(token, expiresAt) {
|
|
318
315
|
this.db
|
|
@@ -88,10 +88,6 @@ export declare class StructuredSessionManager {
|
|
|
88
88
|
idempotencyKey?: string;
|
|
89
89
|
preserveQueue?: boolean;
|
|
90
90
|
}): Promise<SessionSnapshot>;
|
|
91
|
-
/** Approve a pending permission request. */
|
|
92
|
-
approvePermission(sessionId: string): SessionSnapshot;
|
|
93
|
-
/** Deny a pending permission request. */
|
|
94
|
-
denyPermission(sessionId: string): SessionSnapshot;
|
|
95
91
|
/**
|
|
96
92
|
* Reorder the pending queued messages. `order` is a permutation of the current
|
|
97
93
|
* indices, e.g. `[2, 0, 1]` means "move the third queued message to the front,
|
|
@@ -125,7 +121,6 @@ export declare class StructuredSessionManager {
|
|
|
125
121
|
private emitStructuredSnapshot;
|
|
126
122
|
private flushNextQueuedMessage;
|
|
127
123
|
private emit;
|
|
128
|
-
private resolvePermission;
|
|
129
124
|
private incrementApprovalStats;
|
|
130
125
|
private buildCodexArgs;
|
|
131
126
|
private runCodexStreaming;
|
|
@@ -7,8 +7,8 @@ import path from "node:path";
|
|
|
7
7
|
import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk";
|
|
8
8
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
9
9
|
import { truncateMessagesForTransport } from "./message-truncator.js";
|
|
10
|
-
import { buildChildEnv } from "./env-utils.js";
|
|
11
|
-
import { buildLanguageDirective } from "./language-prompt.js";
|
|
10
|
+
import { buildChildEnv, isRunningAsRoot } from "./env-utils.js";
|
|
11
|
+
import { buildLanguageDirective, buildManagedAutonomyDirective } from "./language-prompt.js";
|
|
12
12
|
function defaultStructuredRunner(provider) {
|
|
13
13
|
return provider === "codex" ? "codex-cli-exec" : "claude-cli-print";
|
|
14
14
|
}
|
|
@@ -169,9 +169,6 @@ const STREAM_EMIT_DEBOUNCE_MS = 16;
|
|
|
169
169
|
* authoritative final snapshot. */
|
|
170
170
|
const STREAM_SAVE_THROTTLE_MS = 200;
|
|
171
171
|
const ARCHIVE_AFTER_MS = 1000 * 60 * 60 * 24;
|
|
172
|
-
function isRunningAsRoot() {
|
|
173
|
-
return process.getuid?.() === 0 || process.geteuid?.() === 0;
|
|
174
|
-
}
|
|
175
172
|
/**
|
|
176
173
|
* 检测当前系统是否使用 musl libc(Alpine Linux 等)。
|
|
177
174
|
* Node.js 进程报告中 glibcVersionRuntime 仅在 glibc 系统存在;musl 系统为 undefined。
|
|
@@ -385,9 +382,7 @@ function buildAppendSystemPromptParts(language, mode) {
|
|
|
385
382
|
const isChinese = trimmedLanguage === "中文";
|
|
386
383
|
const parts = [];
|
|
387
384
|
if (mode === "managed") {
|
|
388
|
-
parts.push(isChinese
|
|
389
|
-
? "你正在完全托管的自主模式下运行。用户可能无法及时回复问题或确认。你必须独立做出所有决策——自行选择最佳方案,而不是向用户询问偏好、确认或澄清。如果有多种可行方案,选择你认为最合适的并继续执行。除非任务本身存在根本性的歧义且无法合理推断,否则不要等待用户输入。果断行动,自主决策。"
|
|
390
|
-
: "You are running in a fully managed, autonomous mode. The user may not be available to respond to questions or confirmations in a timely manner. You MUST make all decisions independently — choose the best approach yourself instead of asking the user for preferences, confirmations, or clarifications. If multiple approaches are viable, pick the one you judge most appropriate and proceed. Never block on user input unless the task is fundamentally ambiguous and cannot be reasonably inferred. Be decisive and self-directed.");
|
|
385
|
+
parts.push(buildManagedAutonomyDirective(isChinese));
|
|
391
386
|
}
|
|
392
387
|
if (trimmedLanguage) {
|
|
393
388
|
const directive = buildLanguageDirective(trimmedLanguage);
|
|
@@ -610,7 +605,6 @@ export class StructuredSessionManager {
|
|
|
610
605
|
if (opts?.idempotencyKey) {
|
|
611
606
|
const mapKey = `${id}:${opts.idempotencyKey}`;
|
|
612
607
|
if (this.seenIdempotencyKeys.has(mapKey)) {
|
|
613
|
-
console.log("[WAND] sendMessage: duplicate idempotency key rejected", { id, key: opts.idempotencyKey });
|
|
614
608
|
const err = new Error("检测到重复发送,已拦截。");
|
|
615
609
|
err.code = "duplicate_idempotency_key";
|
|
616
610
|
throw err;
|
|
@@ -773,17 +767,6 @@ export class StructuredSessionManager {
|
|
|
773
767
|
throw error;
|
|
774
768
|
}
|
|
775
769
|
}
|
|
776
|
-
// ---------------------------------------------------------------------------
|
|
777
|
-
// Permission resolution (called from server routes)
|
|
778
|
-
// ---------------------------------------------------------------------------
|
|
779
|
-
/** Approve a pending permission request. */
|
|
780
|
-
approvePermission(sessionId) {
|
|
781
|
-
return this.resolvePermission(sessionId, true);
|
|
782
|
-
}
|
|
783
|
-
/** Deny a pending permission request. */
|
|
784
|
-
denyPermission(sessionId) {
|
|
785
|
-
return this.resolvePermission(sessionId, false);
|
|
786
|
-
}
|
|
787
770
|
/**
|
|
788
771
|
* Reorder the pending queued messages. `order` is a permutation of the current
|
|
789
772
|
* indices, e.g. `[2, 0, 1]` means "move the third queued message to the front,
|
|
@@ -1055,31 +1038,6 @@ export class StructuredSessionManager {
|
|
|
1055
1038
|
this.emitEvent(event);
|
|
1056
1039
|
}
|
|
1057
1040
|
}
|
|
1058
|
-
resolvePermission(sessionId, approved) {
|
|
1059
|
-
const session = this.requireSession(sessionId);
|
|
1060
|
-
const scope = session.pendingEscalation?.scope;
|
|
1061
|
-
if (approved && scope) {
|
|
1062
|
-
this.incrementApprovalStats(session, scope);
|
|
1063
|
-
}
|
|
1064
|
-
const updated = {
|
|
1065
|
-
...session,
|
|
1066
|
-
pendingEscalation: null,
|
|
1067
|
-
permissionBlocked: false,
|
|
1068
|
-
lastEscalationResult: session.pendingEscalation ? {
|
|
1069
|
-
requestId: session.pendingEscalation.requestId,
|
|
1070
|
-
resolution: approved ? "approve_once" : "deny",
|
|
1071
|
-
reason: approved ? "user_approved" : "user_denied",
|
|
1072
|
-
} : session.lastEscalationResult ?? null,
|
|
1073
|
-
};
|
|
1074
|
-
this.sessions.set(sessionId, updated);
|
|
1075
|
-
this.storage.saveSession(updated);
|
|
1076
|
-
this.emit({
|
|
1077
|
-
type: "status",
|
|
1078
|
-
sessionId,
|
|
1079
|
-
data: { permissionBlocked: false, approvalStats: updated.approvalStats, sessionKind: "structured" },
|
|
1080
|
-
});
|
|
1081
|
-
return updated;
|
|
1082
|
-
}
|
|
1083
1041
|
incrementApprovalStats(session, scope) {
|
|
1084
1042
|
const prev = session.approvalStats ?? { tool: 0, command: 0, file: 0, total: 0 };
|
|
1085
1043
|
const stats = { ...prev };
|
|
@@ -2417,7 +2375,7 @@ export class StructuredSessionManager {
|
|
|
2417
2375
|
output: turnState.result,
|
|
2418
2376
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
2419
2377
|
messages: msgs,
|
|
2420
|
-
queuedMessages: interruptPrompt ? [] : current.queuedMessages,
|
|
2378
|
+
queuedMessages: interruptPrompt && !this.preserveQueueOnInterrupt.has(sessionId) ? [] : current.queuedMessages,
|
|
2421
2379
|
pendingEscalation: null,
|
|
2422
2380
|
permissionBlocked: false,
|
|
2423
2381
|
structuredState: {
|
|
@@ -2437,6 +2395,8 @@ export class StructuredSessionManager {
|
|
|
2437
2395
|
return;
|
|
2438
2396
|
if (interruptPrompt) {
|
|
2439
2397
|
this.interruptedWith.delete(sessionId);
|
|
2398
|
+
// 与 codex/cli runner 对齐:清掉"保留队列"标记,避免 stale flag 影响下一次普通 interrupt。
|
|
2399
|
+
this.preserveQueueOnInterrupt.delete(sessionId);
|
|
2440
2400
|
setImmediate(() => {
|
|
2441
2401
|
this.sendMessage(sessionId, interruptPrompt).catch((err) => {
|
|
2442
2402
|
console.error("[WAND] sdk interrupt-and-send failed:", err);
|