@co0ontty/wand 1.43.6 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "8014df702e9f99f37092a386d971bebc57702510",
3
- "builtAt": "2026-05-31T08:14:40.260Z",
4
- "version": "1.43.6",
2
+ "commit": "61d4063f143052abb4b0aa245746ab089ce32288",
3
+ "builtAt": "2026-05-31T12:11:01.894Z",
4
+ "version": "1.43.8",
5
5
  "channel": "stable"
6
6
  }
@@ -1,3 +1,5 @@
1
+ /** 是否以 root 身份运行(uid 或 euid 为 0)。供 PTY runner 与 structured runner 共用。 */
2
+ export declare function isRunningAsRoot(): boolean;
1
3
  /**
2
4
  * 根据 inheritEnv 配置组装子进程的环境变量。
3
5
  *
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;
@@ -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, SessionProvider } from "./types.js";
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
- const match = stdout.match(/\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?/);
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";
@@ -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
- try {
411
- const tool = process.platform === "win32" ? "where" : "which";
412
- const r = spawnSync(tool, ["wand"], { encoding: "utf8", timeout: 10_000, env: getChildEnv() });
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;
@@ -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
  *
@@ -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) => semverCompare(b, a))
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`;都走 process.env.PATH,因此一定要在
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;
@@ -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
  }
@@ -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
@@ -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. Exported for tests. */
141
- export function safeSliceTail(text, maxSize) {
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;
@@ -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;
@@ -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 extractSemverFromName(fileName.replace(/\.apk$/i, ""));
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 extractSemverFromName(fileName.replace(/\.dmg$/i, ""));
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);