@co0ontty/wand 1.21.8 → 1.21.10

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/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 ["defaultMode", "defaultCwd", "defaultModel", "structuredRunner", "language", "cardDefaults"];
10
+ export declare const PREFERENCE_KEYS: readonly ["defaultMode", "defaultCwd", "defaultModel", "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;
package/dist/config.js CHANGED
@@ -19,6 +19,7 @@ export const PREFERENCE_KEYS = [
19
19
  "structuredRunner",
20
20
  "language",
21
21
  "cardDefaults",
22
+ "inheritEnv",
22
23
  ];
23
24
  const PREFERENCE_KEY_SET = new Set(PREFERENCE_KEYS);
24
25
  function preferenceStorageKey(key) {
@@ -43,6 +44,7 @@ export const defaultConfig = () => ({
43
44
  cardDefaults: defaultCardExpandDefaults(),
44
45
  defaultModel: "",
45
46
  structuredRunner: "cli",
47
+ inheritEnv: true,
46
48
  commandPresets: [
47
49
  {
48
50
  label: "Claude",
@@ -201,6 +203,10 @@ export function applyStoragePreferences(config, storage) {
201
203
  const v = storage.getPreference(preferenceStorageKey("cardDefaults"), defaults.cardDefaults);
202
204
  config.cardDefaults = normalizeCardDefaults(v);
203
205
  }
206
+ if (storage.hasPreference(preferenceStorageKey("inheritEnv"))) {
207
+ const v = storage.getPreference(preferenceStorageKey("inheritEnv"), defaults.inheritEnv ?? true);
208
+ config.inheritEnv = v === false ? false : true;
209
+ }
204
210
  return config;
205
211
  }
206
212
  /** Write a single preference value to DB and (in-place) update the live config object. */
@@ -244,6 +250,12 @@ export function writePreferenceToStorage(config, storage, key, value) {
244
250
  config.cardDefaults = normalized;
245
251
  break;
246
252
  }
253
+ case "inheritEnv": {
254
+ const v = value === false ? false : true;
255
+ storage.setPreference(dbKey, v);
256
+ config.inheritEnv = v;
257
+ break;
258
+ }
247
259
  }
248
260
  }
249
261
  function defaultCardExpandDefaults() {
@@ -349,6 +361,7 @@ function mergeWithDefaults(input) {
349
361
  cardDefaults: normalizeCardDefaults(input.cardDefaults),
350
362
  defaultModel: typeof input.defaultModel === "string" ? input.defaultModel.trim() : defaults.defaultModel,
351
363
  structuredRunner: (input.structuredRunner === "sdk" || input.structuredRunner === "cli") ? input.structuredRunner : defaults.structuredRunner,
364
+ inheritEnv: typeof input.inheritEnv === "boolean" ? input.inheritEnv : (defaults.inheritEnv ?? true),
352
365
  };
353
366
  }
354
367
  export function isExecutionMode(value) {
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 根据 inheritEnv 配置组装子进程的环境变量。
3
+ *
4
+ * - inheritEnv=true(默认):继承父进程全部 env,再合并 extras 覆盖。
5
+ * - inheritEnv=false:仅保留 MINIMAL_ENV_KEYS 中存在的字段,再合并 extras 覆盖。
6
+ *
7
+ * extras 中的 undefined 字段会被剔除(spawn 不允许 env 值为 undefined)。
8
+ */
9
+ export declare function buildChildEnv(inheritEnv: boolean, extras?: Record<string, string | undefined>): NodeJS.ProcessEnv;
@@ -0,0 +1,47 @@
1
+ import process from "node:process";
2
+ /**
3
+ * 用于子进程 spawn 时的环境变量白名单(当用户关闭"继承环境变量"时使用)。
4
+ * 仅保留运行 CLI 工具所需的最小集合,避免把 API key、token 等敏感凭据继承到子命令。
5
+ */
6
+ const MINIMAL_ENV_KEYS = [
7
+ "PATH",
8
+ "HOME",
9
+ "USER",
10
+ "LOGNAME",
11
+ "SHELL",
12
+ "LANG",
13
+ "LC_ALL",
14
+ "LC_CTYPE",
15
+ "TERM",
16
+ "TZ",
17
+ "TMPDIR",
18
+ "TMP",
19
+ "TEMP",
20
+ "PWD",
21
+ ];
22
+ /**
23
+ * 根据 inheritEnv 配置组装子进程的环境变量。
24
+ *
25
+ * - inheritEnv=true(默认):继承父进程全部 env,再合并 extras 覆盖。
26
+ * - inheritEnv=false:仅保留 MINIMAL_ENV_KEYS 中存在的字段,再合并 extras 覆盖。
27
+ *
28
+ * extras 中的 undefined 字段会被剔除(spawn 不允许 env 值为 undefined)。
29
+ */
30
+ export function buildChildEnv(inheritEnv, extras = {}) {
31
+ const base = {};
32
+ if (inheritEnv) {
33
+ Object.assign(base, process.env);
34
+ }
35
+ else {
36
+ for (const key of MINIMAL_ENV_KEYS) {
37
+ const v = process.env[key];
38
+ if (typeof v === "string")
39
+ base[key] = v;
40
+ }
41
+ }
42
+ for (const [k, v] of Object.entries(extras)) {
43
+ if (typeof v === "string")
44
+ base[k] = v;
45
+ }
46
+ return base;
47
+ }
@@ -9,6 +9,7 @@ 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";
12
13
  import { prepareSessionWorktree } from "./git-worktree.js";
13
14
  import { getResumeCommandSessionId } from "./resume-policy.js";
14
15
  function resolveProviderFromCommand(command) {
@@ -659,12 +660,11 @@ export class ProcessManager extends EventEmitter {
659
660
  try {
660
661
  child = pty.spawn(this.config.shell, shellArgs, {
661
662
  cwd: resolvedCwd,
662
- env: {
663
- ...process.env,
663
+ env: buildChildEnv(this.config.inheritEnv !== false, {
664
664
  WAND_MODE: effectiveMode,
665
665
  WAND_AUTO_CONFIRM: effectiveMode === "full-access" ? "1" : "0",
666
666
  WAND_AUTO_EDIT: effectiveMode === "auto-edit" ? "1" : "0"
667
- },
667
+ }),
668
668
  name: "xterm-color",
669
669
  // 使用 record 上由前端协商好的真实尺寸,避免"先 120 列、几百毫秒后再 resize"
670
670
  // 期间 Claude/Codex 用错列宽渲染出 \x1b[120G 这类绝对列定位序列。
@@ -1,7 +1,10 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
+ import { createRequire } from "node:module";
4
+ import { existsSync } from "node:fs";
3
5
  import { prepareSessionWorktree } from "./git-worktree.js";
4
6
  import { truncateMessagesForTransport } from "./message-truncator.js";
7
+ import { buildChildEnv } from "./env-utils.js";
5
8
  function defaultStructuredRunner(provider) {
6
9
  return provider === "codex" ? "codex-cli-exec" : "claude-cli-print";
7
10
  }
@@ -24,6 +27,47 @@ const ARCHIVE_AFTER_MS = 1000 * 60 * 60 * 24;
24
27
  function isRunningAsRoot() {
25
28
  return process.getuid?.() === 0 || process.geteuid?.() === 0;
26
29
  }
30
+ /**
31
+ * 检测当前系统是否使用 musl libc(Alpine Linux 等)。
32
+ * Node.js 进程报告中 glibcVersionRuntime 仅在 glibc 系统存在;musl 系统为 undefined。
33
+ */
34
+ function isMuslSystem() {
35
+ try {
36
+ const header = process.report?.getReport()?.header;
37
+ return !header?.glibcVersionRuntime;
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ /**
44
+ * 解析 claude-agent-sdk 应使用的 native binary 路径。
45
+ * SDK 默认在 Linux 上优先选 musl 包,但 glibc 系统(Debian/Ubuntu 等)跑不动 musl binary,
46
+ * 会抛 "Claude Code native binary not found" 错误。这里手动按 libc 类型选正确的包,
47
+ * 找不到时回退到系统 PATH 上的 `claude`。
48
+ */
49
+ function resolveSdkClaudeBinary() {
50
+ if (process.platform !== "linux")
51
+ return undefined;
52
+ const musl = isMuslSystem();
53
+ const arch = process.arch;
54
+ const require = createRequire(import.meta.url);
55
+ // 按当前 libc 类型决定优先顺序
56
+ const candidates = musl
57
+ ? [`@anthropic-ai/claude-agent-sdk-linux-${arch}-musl/claude`, `@anthropic-ai/claude-agent-sdk-linux-${arch}/claude`]
58
+ : [`@anthropic-ai/claude-agent-sdk-linux-${arch}/claude`, `@anthropic-ai/claude-agent-sdk-linux-${arch}-musl/claude`];
59
+ for (const pkg of candidates) {
60
+ try {
61
+ const resolved = require.resolve(pkg);
62
+ if (existsSync(resolved))
63
+ return resolved;
64
+ }
65
+ catch {
66
+ // 包不存在,继续
67
+ }
68
+ }
69
+ return undefined;
70
+ }
27
71
  /**
28
72
  * 找出最后一条 assistant turn 中尚未配对 tool_result 的 AskUserQuestion tool_use。
29
73
  * 用来识别"刚被 SIGTERM 中断、正在等用户提交答案"的状态。
@@ -713,7 +757,7 @@ export class StructuredSessionManager {
713
757
  const spawnedAt = new Date().toISOString();
714
758
  const child = spawn("codex", args, {
715
759
  cwd: session.cwd,
716
- env: process.env,
760
+ env: buildChildEnv(this.config.inheritEnv !== false),
717
761
  stdio: ["pipe", "pipe", "pipe"],
718
762
  });
719
763
  this.logger?.appendStructuredSpawn(sessionId, {
@@ -1042,7 +1086,7 @@ export class StructuredSessionManager {
1042
1086
  const spawnedAt = new Date().toISOString();
1043
1087
  const child = spawn("claude", args, {
1044
1088
  cwd: session.cwd,
1045
- env: process.env,
1089
+ env: buildChildEnv(this.config.inheritEnv !== false),
1046
1090
  stdio: ["pipe", "pipe", "pipe"],
1047
1091
  });
1048
1092
  this.logger?.appendStructuredSpawn(sessionId, {
@@ -1491,6 +1535,7 @@ export class StructuredSessionManager {
1491
1535
  ? "请使用中文回复。所有解释、注释和对话文本都使用中文。"
1492
1536
  : `Please respond in ${language}. Use ${language} for all your explanations, comments, and conversational text.`);
1493
1537
  }
1538
+ const sdkClaudeBinary = resolveSdkClaudeBinary();
1494
1539
  const sdkOptions = {
1495
1540
  cwd: session.cwd,
1496
1541
  abortController,
@@ -1500,6 +1545,7 @@ export class StructuredSessionManager {
1500
1545
  ...(isManaged ? { disallowedTools: ["AskUserQuestion"] } : {}),
1501
1546
  includePartialMessages: true,
1502
1547
  ...(systemPromptParts.length > 0 ? { appendSystemPrompt: systemPromptParts.join("\n\n") } : {}),
1548
+ ...(sdkClaudeBinary ? { pathToClaudeCodeExecutable: sdkClaudeBinary } : {}),
1503
1549
  };
1504
1550
  if (session.claudeSessionId)
1505
1551
  sdkOptions.resume = session.claudeSessionId;
package/dist/types.d.ts CHANGED
@@ -93,6 +93,12 @@ export interface WandConfig {
93
93
  defaultModel?: string;
94
94
  /** 结构化会话使用的 runner: "cli"(默认,spawn claude -p)或 "sdk"(@anthropic-ai/claude-agent-sdk)。 */
95
95
  structuredRunner?: "cli" | "sdk";
96
+ /**
97
+ * 启动 PTY / 结构化子进程时是否继承父进程的环境变量(process.env)。默认 true。
98
+ * 关闭后子进程仅获得最小可用环境(PATH/HOME/SHELL/LANG/LC_ALL/TERM 等)外加 WAND_* 控制变量,
99
+ * 用于隔离敏感凭据或避免 API key 泄漏到子命令。
100
+ */
101
+ inheritEnv?: boolean;
96
102
  }
97
103
  export interface ClaudeModelInfo {
98
104
  /** 传给 --model 的值(别名或完整模型 ID) */
@@ -2142,6 +2142,16 @@
2142
2142
  '</select>' +
2143
2143
  '<p class="field-hint" style="margin-top:4px;">SDK 模式使用官方 Agent SDK 替代 CLI subprocess,接口更整洁,功能等价。保存后对新建会话立即生效。</p>' +
2144
2144
  '</div>' +
2145
+ '<div class="settings-toggle-row">' +
2146
+ '<div class="settings-toggle-text">' +
2147
+ '<label class="settings-toggle-title" for="cfg-inherit-env">继承环境变量</label>' +
2148
+ '<span class="settings-toggle-desc">启动 PTY / 结构化子进程时,把当前服务进程的环境变量传给 claude / codex。关闭后子进程仅获得最小可用环境(PATH/HOME/SHELL/LANG/TERM 等),可用于隔离 API key 等敏感凭据。</span>' +
2149
+ '</div>' +
2150
+ '<label class="settings-switch">' +
2151
+ '<input id="cfg-inherit-env" type="checkbox" class="switch-toggle" />' +
2152
+ '<span class="switch-slider"></span>' +
2153
+ '</label>' +
2154
+ '</div>' +
2145
2155
  '<div class="field">' +
2146
2156
  '<label class="field-label" for="cfg-default-model">默认模型</label>' +
2147
2157
  '<div class="settings-row-with-action">' +
@@ -7539,6 +7549,9 @@
7539
7549
  var srEl = document.getElementById("cfg-structured-runner");
7540
7550
  if (srEl) srEl.value = cfg.structuredRunner || "cli";
7541
7551
 
7552
+ var inheritEnvEl = document.getElementById("cfg-inherit-env");
7553
+ if (inheritEnvEl) inheritEnvEl.checked = cfg.inheritEnv !== false;
7554
+
7542
7555
  // Default model
7543
7556
  state.configDefaultModel = cfg.defaultModel || "";
7544
7557
  updateSettingsDefaultModelSelect();
@@ -7598,6 +7611,7 @@
7598
7611
  language: (document.getElementById("cfg-language") || {}).value || "",
7599
7612
  defaultModel: (document.getElementById("cfg-default-model") || {}).value || "",
7600
7613
  structuredRunner: (document.getElementById("cfg-structured-runner") || {}).value || "cli",
7614
+ inheritEnv: (document.getElementById("cfg-inherit-env") || {}).checked !== false,
7601
7615
  };
7602
7616
 
7603
7617
  var previousDefaultModel = (state.config && state.config.defaultModel) || "";
@@ -15597,7 +15611,7 @@
15597
15611
  // ── Native Live Progress Sync ──
15598
15612
 
15599
15613
  var _progressSyncTimers = {};
15600
- var _PROGRESS_SYNC_DEBOUNCE_MS = 300;
15614
+ var _PROGRESS_SYNC_DEBOUNCE_MS = 100;
15601
15615
 
15602
15616
  // Strip markdown formatting and clamp to a single short line so the
15603
15617
  // native Live Activity / lock-screen card stays readable. 100 chars
@@ -15647,6 +15661,7 @@
15647
15661
  var todos = null;
15648
15662
  var latestUserText = "";
15649
15663
  var latestAssistantText = "";
15664
+ var recentUserTexts = [];
15650
15665
  var messages = session.messages || [];
15651
15666
  for (var i = messages.length - 1; i >= 0; i--) {
15652
15667
  var msg = messages[i];
@@ -15662,7 +15677,7 @@
15662
15677
  }
15663
15678
  }
15664
15679
 
15665
- if (!latestUserText && msg.role === "user") {
15680
+ if (msg.role === "user") {
15666
15681
  // Skip queued / synthetic placeholder turns — they don't represent
15667
15682
  // user-visible "I just asked this" prompts.
15668
15683
  var isPlaceholder = msg.content.some(function(b) { return b && b.__queued; });
@@ -15670,7 +15685,9 @@
15670
15685
  for (var ui = 0; ui < msg.content.length; ui++) {
15671
15686
  var ublock = msg.content[ui];
15672
15687
  if (ublock && ublock.type === "text" && ublock.text && ublock.text.trim()) {
15673
- latestUserText = _compactNotificationText(ublock.text);
15688
+ var utext = _compactNotificationText(ublock.text);
15689
+ if (!latestUserText) latestUserText = utext;
15690
+ if (recentUserTexts.length < 4) recentUserTexts.push(utext);
15674
15691
  break;
15675
15692
  }
15676
15693
  }
@@ -15688,8 +15705,9 @@
15688
15705
  }
15689
15706
  }
15690
15707
 
15691
- if (todos && latestUserText && latestAssistantText) break;
15708
+ if (todos && recentUserTexts.length >= 4 && latestAssistantText) break;
15692
15709
  }
15710
+ recentUserTexts.reverse();
15693
15711
 
15694
15712
  // Get current task
15695
15713
  var currentTask = "";
@@ -15703,7 +15721,8 @@
15703
15721
  currentTask: currentTask,
15704
15722
  latestUserText: latestUserText,
15705
15723
  latestAssistantText: latestAssistantText,
15706
- todos: todos || []
15724
+ todos: todos || [],
15725
+ recentUserTexts: recentUserTexts
15707
15726
  };
15708
15727
 
15709
15728
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.21.8",
3
+ "version": "1.21.10",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {