@co0ontty/wand 4.63.0 → 4.64.0-beta.g3402a64

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.
Files changed (83) hide show
  1. package/dist/build-info.d.ts +18 -0
  2. package/dist/build-info.js +13 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/config.js +6 -11
  5. package/dist/env-utils.d.ts +5 -0
  6. package/dist/env-utils.js +4 -0
  7. package/dist/git-quick-commit.js +7 -11
  8. package/dist/ios-ota.js +8 -29
  9. package/dist/layout-tree.d.ts +6 -0
  10. package/dist/layout-tree.js +9 -0
  11. package/dist/missions.js +2 -2
  12. package/dist/models.js +2 -5
  13. package/dist/path-repair.js +2 -1
  14. package/dist/process-manager.js +10 -20
  15. package/dist/provider-cli-updater.js +3 -6
  16. package/dist/pty-shell-launch.js +3 -5
  17. package/dist/server-app-connect.js +11 -46
  18. package/dist/server-github-routes.js +12 -11
  19. package/dist/server-local-preview-routes.js +6 -1
  20. package/dist/server-request.d.ts +4 -2
  21. package/dist/server-request.js +25 -4
  22. package/dist/server-session-routes.js +14 -78
  23. package/dist/server-settings-routes.d.ts +2 -6
  24. package/dist/server-settings-routes.js +3 -8
  25. package/dist/server-task-routes.d.ts +9 -6
  26. package/dist/server-task-routes.js +50 -43
  27. package/dist/server-update-routes.d.ts +2 -5
  28. package/dist/server-update-routes.js +42 -9
  29. package/dist/server-workspace-routes.js +4 -12
  30. package/dist/server.js +33 -99
  31. package/dist/session-ai-context.js +6 -38
  32. package/dist/session-provider.d.ts +16 -0
  33. package/dist/session-provider.js +49 -0
  34. package/dist/session-registry.d.ts +10 -0
  35. package/dist/session-registry.js +41 -14
  36. package/dist/session-topic.js +3 -11
  37. package/dist/shell-quote.d.ts +9 -0
  38. package/dist/shell-quote.js +13 -0
  39. package/dist/signal-utils.d.ts +10 -0
  40. package/dist/signal-utils.js +38 -0
  41. package/dist/storage.js +17 -55
  42. package/dist/structured-client-protocol.js +4 -8
  43. package/dist/structured-codex-protocol.js +1 -6
  44. package/dist/structured-content.d.ts +2 -0
  45. package/dist/structured-content.js +6 -0
  46. package/dist/structured-exec-host.d.ts +0 -2
  47. package/dist/structured-exec-host.js +2 -11
  48. package/dist/structured-exec-pump.js +2 -12
  49. package/dist/structured-grok-adapter.js +1 -5
  50. package/dist/structured-opencode-adapter.js +1 -5
  51. package/dist/structured-pi-adapter.js +9 -11
  52. package/dist/structured-provider-common.d.ts +11 -3
  53. package/dist/structured-provider-common.js +16 -23
  54. package/dist/structured-qoder-adapter.js +3 -8
  55. package/dist/structured-session-manager.js +6 -17
  56. package/dist/task-title.js +4 -12
  57. package/dist/task-types.d.ts +23 -1
  58. package/dist/task-types.js +26 -0
  59. package/dist/terminal-daemon-protocol.d.ts +0 -6
  60. package/dist/terminal-daemon-server.js +1 -9
  61. package/dist/text-utils.d.ts +7 -0
  62. package/dist/text-utils.js +15 -0
  63. package/dist/tui/attach.js +17 -93
  64. package/dist/tui/commands.d.ts +2 -0
  65. package/dist/tui/commands.js +3 -2
  66. package/dist/tui/index.js +28 -115
  67. package/dist/tui/runtime-utils.d.ts +7 -0
  68. package/dist/tui/runtime-utils.js +33 -0
  69. package/dist/tui/service-actions.d.ts +31 -0
  70. package/dist/tui/service-actions.js +86 -0
  71. package/dist/tui/snapshot.js +1 -8
  72. package/dist/update-helper.js +6 -35
  73. package/dist/web-ui/content/scripts.js +397 -497
  74. package/dist/web-ui/content/styles.css +1 -1
  75. package/dist/web-ui/content/tailwind.css +7210 -0
  76. package/dist/web-ui/embedded-assets.js +2 -2
  77. package/dist/web-ui/index.js +3 -3
  78. package/dist/web-ui/provider-identity.d.ts +4 -0
  79. package/dist/web-ui/provider-identity.js +9 -0
  80. package/dist/web-ui/styles.js +24 -7
  81. package/dist/workspace-binding.d.ts +2 -0
  82. package/dist/workspace-binding.js +4 -3
  83. package/package.json +9 -12
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 构建信息共享类型:`dist/build-info.json` 由 scripts/stamp-build-info.js 在打包时写入,
3
+ * server 组合根、设置路由、更新路由都消费同一份数据,避免各自再定义一遍形状。
4
+ */
5
+ /** 构建信息中除版本号外的字段。 */
6
+ export interface BuildInfo {
7
+ commit: string | null;
8
+ builtAt: string | null;
9
+ channel: string | null;
10
+ }
11
+ /** 服务端进程持有的完整构建信息。 */
12
+ export interface WandBuildInfo extends BuildInfo {
13
+ version: string | null;
14
+ }
15
+ /** API 展示用的 build 载荷:统一补 7 位短 commit。 */
16
+ export declare function buildInfoPayload(buildInfo: BuildInfo): BuildInfo & {
17
+ shortCommit: string | null;
18
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * 构建信息共享类型:`dist/build-info.json` 由 scripts/stamp-build-info.js 在打包时写入,
3
+ * server 组合根、设置路由、更新路由都消费同一份数据,避免各自再定义一遍形状。
4
+ */
5
+ /** API 展示用的 build 载荷:统一补 7 位短 commit。 */
6
+ export function buildInfoPayload(buildInfo) {
7
+ return {
8
+ commit: buildInfo.commit,
9
+ shortCommit: buildInfo.commit ? buildInfo.commit.slice(0, 7) : null,
10
+ builtAt: buildInfo.builtAt,
11
+ channel: buildInfo.channel,
12
+ };
13
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "680b91217f6bded493eba4b178d5b6236b8065cb",
3
- "builtAt": "2026-09-14T15:09:11.803Z",
4
- "version": "4.63.0",
5
- "channel": "stable"
2
+ "commit": "3402a645acc2941d21b168b41577424bf7416056",
3
+ "builtAt": "2026-09-15T11:50:29.917Z",
4
+ "version": "4.64.0-beta.g3402a64",
5
+ "channel": "beta"
6
6
  }
package/dist/config.js CHANGED
@@ -6,13 +6,8 @@ import path from "node:path";
6
6
  import process from "node:process";
7
7
  import { isRunningAsRoot } from "./env-utils.js";
8
8
  import { normalizeSystemAiConfig, systemAiProfiles } from "./system-ai.js";
9
- function isThinkingEffort(value) {
10
- return value === "off"
11
- || value === "standard"
12
- || value === "deep"
13
- || value === "max"
14
- || (typeof value === "string" && /^codex:[a-z0-9][a-z0-9_-]{0,31}$/.test(value));
15
- }
9
+ import { isSessionProvider } from "./session-provider.js";
10
+ import { isThinkingEffort } from "./structured-provider-common.js";
16
11
  const DEFAULT_CONFIG_DIR = ".wand";
17
12
  const DEFAULT_CONFIG_FILE = "config.json";
18
13
  /**
@@ -313,7 +308,7 @@ export function applyStoragePreferences(config, storage) {
313
308
  const defaults = defaultConfig();
314
309
  if (storage.hasPreference(preferenceStorageKey("defaultProvider"))) {
315
310
  const v = storage.getPreference(preferenceStorageKey("defaultProvider"), defaults.defaultProvider ?? "claude");
316
- if (v === "claude" || v === "codex" || v === "opencode" || v === "grok" || v === "qoder" || v === "pi")
311
+ if (isSessionProvider(v))
317
312
  config.defaultProvider = v;
318
313
  }
319
314
  if (storage.hasPreference(preferenceStorageKey("defaultSessionKind"))) {
@@ -416,8 +411,8 @@ export function writePreferenceToStorage(config, storage, key, value, options =
416
411
  const dbKey = preferenceStorageKey(key);
417
412
  switch (key) {
418
413
  case "defaultProvider": {
419
- if (value !== "claude" && value !== "codex" && value !== "opencode" && value !== "grok" && value !== "qoder" && value !== "pi")
420
- throw new Error(`无效 Provider: ${value}`);
414
+ if (!isSessionProvider(value))
415
+ throw new Error(`无效 Provider: ${String(value)}`);
421
416
  storage.setPreference(dbKey, value);
422
417
  config.defaultProvider = value;
423
418
  break;
@@ -743,7 +738,7 @@ function mergeWithDefaults(input) {
743
738
  macos: normalizeMacosDmgConfig(input.macos) ?? defaults.macos,
744
739
  ios: normalizeIosIpaConfig(input.ios) ?? defaults.ios,
745
740
  cardDefaults: normalizeCardDefaults(input.cardDefaults),
746
- defaultProvider: input.defaultProvider === "codex" || input.defaultProvider === "opencode" || input.defaultProvider === "grok" || input.defaultProvider === "qoder" || input.defaultProvider === "pi" ? input.defaultProvider : "claude",
741
+ defaultProvider: isSessionProvider(input.defaultProvider) ? input.defaultProvider : "claude",
747
742
  defaultSessionKind: input.defaultSessionKind === "pty" ? "pty" : "structured",
748
743
  defaultTaskWorktree: typeof input.defaultTaskWorktree === "boolean" ? input.defaultTaskWorktree : defaults.defaultTaskWorktree,
749
744
  defaultModel: typeof input.defaultModel === "string" ? input.defaultModel.trim() : defaults.defaultModel,
@@ -1,3 +1,8 @@
1
+ /** 子进程 env:显式传入 env 优先,否则按 inheritEnv 生成白名单环境。 */
2
+ export declare function resolveChildEnv(options: {
3
+ env?: NodeJS.ProcessEnv;
4
+ inheritEnv?: boolean;
5
+ }): NodeJS.ProcessEnv;
1
6
  /** 是否以 root 身份运行(uid 或 euid 为 0)。供 PTY runner 与 structured runner 共用。 */
2
7
  export declare function isRunningAsRoot(): boolean;
3
8
  /**
package/dist/env-utils.js CHANGED
@@ -19,6 +19,10 @@ const MINIMAL_ENV_KEYS = [
19
19
  "TEMP",
20
20
  "PWD",
21
21
  ];
22
+ /** 子进程 env:显式传入 env 优先,否则按 inheritEnv 生成白名单环境。 */
23
+ export function resolveChildEnv(options) {
24
+ return options.env ?? buildChildEnv(options.inheritEnv !== false);
25
+ }
22
26
  /** 是否以 root 身份运行(uid 或 euid 为 0)。供 PTY runner 与 structured runner 共用。 */
23
27
  export function isRunningAsRoot() {
24
28
  return process.getuid?.() === 0 || process.geteuid?.() === 0;
@@ -3,8 +3,13 @@ import { spawn } from "node:child_process";
3
3
  import { ClaudeRunError, runClaudePrint } from "./claude-sdk-runner.js";
4
4
  import { callSystemAiTextWithFallback } from "./system-ai.js";
5
5
  import { buildChildEnv } from "./env-utils.js";
6
+ import { isSessionProvider } from "./session-provider.js";
6
7
  import { runGitAsync as runGitAsyncBase, runGitRawAsync as runGitRawAsyncBase, getGitErrorMessage, } from "./git-utils.js";
7
8
  import { thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToQoderEffort, thinkingEffortToPiLevel, } from "./structured-provider-common.js";
9
+ /** provider 缺省值:无法识别时回落到 Claude。 */
10
+ function defaultProvider(provider) {
11
+ return isSessionProvider(provider) ? provider : "claude";
12
+ }
8
13
  const GIT_TIMEOUT_MS = 1500;
9
14
  const GIT_PUSH_TIMEOUT_MS = 30_000;
10
15
  const MAX_FILE_ENTRIES = 200;
@@ -301,15 +306,6 @@ async function callClaudeText(prompt, cwd, language, opts) {
301
306
  throw error;
302
307
  }
303
308
  }
304
- function normalizeProvider(provider) {
305
- return provider === "codex"
306
- || provider === "opencode"
307
- || provider === "grok"
308
- || provider === "qoder"
309
- || provider === "pi"
310
- ? provider
311
- : "claude";
312
- }
313
309
  function stripFences(raw) {
314
310
  return raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
315
311
  }
@@ -570,7 +566,7 @@ async function callPiText(prompt, cwd, opts) {
570
566
  return text;
571
567
  }
572
568
  async function callCliAiText(prompt, cwd, language, opts) {
573
- const provider = normalizeProvider(opts.provider);
569
+ const provider = defaultProvider(opts.provider);
574
570
  if (provider === "codex") {
575
571
  return callCodexText(prompt, cwd, opts);
576
572
  }
@@ -1190,7 +1186,7 @@ async function runQuickCommitFallbackCli(opts, priorError) {
1190
1186
  await assertGitWorkTreeAsync(opts.cwd);
1191
1187
  const beforeHead = await getHead(opts.cwd);
1192
1188
  const prompt = buildFallbackPrompt(opts, priorError);
1193
- const provider = normalizeProvider(opts.provider);
1189
+ const provider = defaultProvider(opts.provider);
1194
1190
  if (provider === "codex") {
1195
1191
  const args = ["exec", "--ephemeral", "--json", "--color", "never", "--skip-git-repo-check", "--dangerously-bypass-approvals-and-sandbox"];
1196
1192
  const model = opts.model?.trim();
package/dist/ios-ota.js CHANGED
@@ -1,17 +1,18 @@
1
1
  import { open } from "node:fs/promises";
2
2
  import { inflateRawSync } from "node:zlib";
3
+ import { firstHeaderListValue, forwardedParam, normalizeProtocol } from "./server-request.js";
3
4
  const DEFAULT_BUNDLE_ID = "com.wand.app";
4
5
  const DEFAULT_TITLE = "Wand";
5
6
  export function publicOriginFromRequest(req) {
6
- const forwardedProto = firstHeader(req.headers["x-forwarded-proto"])
7
- ?? forwardedParam(firstHeader(req.headers.forwarded), "proto");
7
+ const forwardedProto = firstHeaderListValue(req.headers["x-forwarded-proto"])
8
+ ?? forwardedParam(firstHeaderListValue(req.headers.forwarded), "proto");
8
9
  const proto = normalizeProtocol(forwardedProto)
9
- ?? (firstHeader(req.headers["x-forwarded-ssl"])?.toLowerCase() === "on" ? "https" : undefined)
10
- ?? (firstHeader(req.headers["x-forwarded-scheme"])?.toLowerCase() === "https" ? "https" : undefined)
10
+ ?? (firstHeaderListValue(req.headers["x-forwarded-ssl"])?.toLowerCase() === "on" ? "https" : undefined)
11
+ ?? (firstHeaderListValue(req.headers["x-forwarded-scheme"])?.toLowerCase() === "https" ? "https" : undefined)
11
12
  ?? normalizeProtocol(req.protocol);
12
- const host = firstHeader(req.headers["x-forwarded-host"])
13
- ?? forwardedParam(firstHeader(req.headers.forwarded), "host")
14
- ?? firstHeader(req.headers.host);
13
+ const host = firstHeaderListValue(req.headers["x-forwarded-host"])
14
+ ?? forwardedParam(firstHeaderListValue(req.headers.forwarded), "host")
15
+ ?? firstHeaderListValue(req.headers.host);
15
16
  if (!proto || !host)
16
17
  return null;
17
18
  return `${proto}://${host}`;
@@ -381,28 +382,6 @@ function dictEntries(entries) {
381
382
  `</dict>`,
382
383
  ].join("\n");
383
384
  }
384
- function firstHeader(value) {
385
- const raw = Array.isArray(value) ? value[0] : value;
386
- return raw?.split(",")[0]?.trim();
387
- }
388
- function forwardedParam(header, key) {
389
- if (!header)
390
- return undefined;
391
- const target = `${key}=`;
392
- for (const part of header.split(";")) {
393
- const trimmed = part.trim();
394
- if (!trimmed.toLowerCase().startsWith(target))
395
- continue;
396
- return trimmed.slice(target.length).replace(/^"|"$/g, "");
397
- }
398
- return undefined;
399
- }
400
- function normalizeProtocol(value) {
401
- const proto = value?.trim().toLowerCase();
402
- if (proto === "http" || proto === "https")
403
- return proto;
404
- return undefined;
405
- }
406
385
  function stringValue(value) {
407
386
  if (typeof value === "string" && value.trim())
408
387
  return value.trim();
@@ -0,0 +1,6 @@
1
+ import type { LayoutNode } from "./types.js";
2
+ /**
3
+ * 取布局树中最左上角可用的 tab id:pane 优先用它记住的 active,
4
+ * 否则取第一个 tab;split 递归左侧再右侧。
5
+ */
6
+ export declare function firstLayoutTabId(node: LayoutNode): string | undefined;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 取布局树中最左上角可用的 tab id:pane 优先用它记住的 active,
3
+ * 否则取第一个 tab;split 递归左侧再右侧。
4
+ */
5
+ export function firstLayoutTabId(node) {
6
+ if (node.type === "pane")
7
+ return node.tabs[node.active]?.id ?? node.tabs[0]?.id;
8
+ return firstLayoutTabId(node.children[0]) ?? firstLayoutTabId(node.children[1]);
9
+ }
package/dist/missions.js CHANGED
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { existsSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { buildMissionDiff } from "./mission-diff.js";
5
- const PROVIDERS = new Set(["claude", "codex", "opencode", "grok", "qoder", "pi"]);
5
+ import { isSessionProvider } from "./session-provider.js";
6
6
  const MAX_ATTEMPTS = 6;
7
7
  function nowIso() {
8
8
  return new Date().toISOString();
@@ -143,7 +143,7 @@ export class Missions {
143
143
  if (!existsSync(cwd) || !statSync(cwd).isDirectory())
144
144
  throw new Error("任务工作目录不存在。");
145
145
  const providers = [...new Set(input.providers ?? [])];
146
- if (providers.length === 0 || providers.length > MAX_ATTEMPTS || providers.some((provider) => !PROVIDERS.has(provider))) {
146
+ if (providers.length === 0 || providers.length > MAX_ATTEMPTS || providers.some((provider) => !isSessionProvider(provider))) {
147
147
  throw new Error(`请选择 1-${MAX_ATTEMPTS} 个有效 provider。`);
148
148
  }
149
149
  const createdAt = nowIso();
package/dist/models.js CHANGED
@@ -2,7 +2,7 @@ import { execFile } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
3
  import { promisify } from "node:util";
4
4
  import Anthropic from "@anthropic-ai/sdk";
5
- import { buildChildEnv } from "./env-utils.js";
5
+ import { resolveChildEnv } from "./env-utils.js";
6
6
  import { extractSemver } from "./version-utils.js";
7
7
  const execFileAsync = promisify(execFile);
8
8
  const CLAUDE_VERIFICATION_CACHE_KEY = "claude-model-verifications-v1";
@@ -85,9 +85,6 @@ function defaultCommandRunner(file, args, options) {
85
85
  maxBuffer: 1024 * 1024,
86
86
  }).then(({ stdout, stderr }) => ({ stdout: String(stdout), stderr: String(stderr) }));
87
87
  }
88
- function resolveProbeEnv(options) {
89
- return options.env ?? buildChildEnv(options.inheritEnv !== false);
90
- }
91
88
  function normalizeClaudeModelId(value) {
92
89
  if (typeof value !== "string")
93
90
  return null;
@@ -704,7 +701,7 @@ function savePersistedModelCatalog(storage, cache, revision) {
704
701
  }
705
702
  async function discoverModelCache(options, previous) {
706
703
  const now = options.now?.() ?? new Date();
707
- const env = resolveProbeEnv(options);
704
+ const env = resolveChildEnv(options);
708
705
  const runner = options.commandRunner ?? defaultCommandRunner;
709
706
  const [claudeVersionProbe, codexProbe, opencodeProbe, grokProbe, qoderProbe, piProbe, apiProbe] = await Promise.all([
710
707
  probeClaudeVersion(runner, env),
@@ -1,12 +1,13 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
2
  import { compareSemver } from "./version-utils.js";
3
3
  import { getErrorMessage } from "./error-utils.js";
4
+ import { providerCliCommand, SESSION_PROVIDERS } from "./session-provider.js";
4
5
  import os from "node:os";
5
6
  import path from "node:path";
6
7
  import process from "node:process";
7
8
  import { spawn, spawnSync } from "node:child_process";
8
9
  /** 关键的 CLI 工具,会被诊断输出。 */
9
- const PROBE_COMMANDS = ["claude", "codex", "opencode", "grok", "qodercli", "pi"];
10
+ const PROBE_COMMANDS = SESSION_PROVIDERS.map(providerCliCommand);
10
11
  const DEEP_PROBE_TIMEOUT_MS = 4000;
11
12
  /**
12
13
  * 构造候选 bin 目录列表(按优先级,前面的更可信)。
@@ -19,23 +19,11 @@ import { getErrorMessage } from "./error-utils.js";
19
19
  import { describePtySpawnFailure } from "./ensure-node-pty-helper.js";
20
20
  import { resolveSystemAiContext } from "./session-ai-context.js";
21
21
  import { resolveSessionCwd } from "./session-cwd.js";
22
+ import { inferProviderFromCommand } from "./session-provider.js";
22
23
  import { PtyTerminalState } from "./pty-terminal-state.js";
23
24
  import { buildPtyShellLaunchPlan, PtyCliExitMarker } from "./pty-shell-launch.js";
24
25
  import { InProcessTerminalHost, } from "./terminal-host.js";
25
26
  import { ProviderHistoryScanner, } from "./provider-history-scanner.js";
26
- function resolveProviderFromCommand(command) {
27
- if (/^(?:claude|npx\s+claude|[^\s]+\/claude)(?:\s|$)/.test(command.trim()))
28
- return "claude";
29
- if (/^codex\b/.test(command.trim()))
30
- return "codex";
31
- if (/^opencode\b/.test(command.trim()))
32
- return "opencode";
33
- if (/^grok\b/.test(command.trim()))
34
- return "grok";
35
- if (/^pi\b/.test(command.trim()))
36
- return "pi";
37
- return /^qodercli\b/.test(command.trim()) ? "qoder" : undefined;
38
- }
39
27
  /**
40
28
  * Tokenize the restricted shell-command subset accepted by the command
41
29
  * allowlist. Commands still run through a login shell, so accepting raw string
@@ -567,7 +555,7 @@ export class ProcessManager extends EventEmitter {
567
555
  continue;
568
556
  }
569
557
  this.lastPersistedMessageState.set(snapshot.id, getPersistedMessageState(snapshot.messages ?? []));
570
- const provider = snapshot.provider ?? resolveProviderFromCommand(snapshot.command);
558
+ const provider = snapshot.provider ?? inferProviderFromCommand(snapshot.command);
571
559
  const isClaudeCmd = provider === "claude";
572
560
  const isCodexCmd = provider === "codex";
573
561
  const isOpenCodeCmd = provider === "opencode";
@@ -1004,7 +992,7 @@ export class ProcessManager extends EventEmitter {
1004
992
  const resolvedCwd = worktreeSetup?.cwd ?? baseCwd;
1005
993
  const provider = opts?.interactiveShell
1006
994
  ? undefined
1007
- : opts?.provider ?? resolveProviderFromCommand(command);
995
+ : opts?.provider ?? inferProviderFromCommand(command);
1008
996
  const effectiveMode = provider === "codex" ? "full-access" : mode;
1009
997
  const isClaudeProvider = provider === "claude";
1010
998
  const selectedModel = opts?.model?.trim() || undefined;
@@ -2429,11 +2417,13 @@ export class ProcessManager extends EventEmitter {
2429
2417
  if (trimmedModel && trimmedModel !== "default" && !/--model(?:\s|=)/.test(result)) {
2430
2418
  result += ` --model '${trimmedModel.replace(/'/g, "'\\''")}'`;
2431
2419
  }
2432
- if ((mode === "managed" || mode === "full-access") && !/--(?:yolo|dangerously-skip-permissions|permission-mode)(?:\s|=|$)/.test(result)) {
2433
- result += " --permission-mode bypass_permissions";
2434
- }
2435
- else if (mode === "auto-edit" && !/--permission-mode(?:\s|=)/.test(result)) {
2436
- result += " --permission-mode accept_edits";
2420
+ // Qoder 默认以 yolo(bypass_permissions)启动:PTY 与结构化 runner 都没有运行时
2421
+ // 权限桥,保持 Qoder 自身确认会让会话卡在 TUI 弹窗或直接拒绝工具调用。只有显式
2422
+ // 选择 auto-edit 时降级为 accept_edits;命令行已带权限参数时保持原样。
2423
+ if (!/--(?:yolo|dangerously-skip-permissions|permission-mode)(?:\s|=|$)/.test(result)) {
2424
+ result += mode === "auto-edit"
2425
+ ? " --permission-mode accept_edits"
2426
+ : " --permission-mode bypass_permissions";
2437
2427
  }
2438
2428
  return result;
2439
2429
  }
@@ -2,7 +2,7 @@ import { execFile } from "node:child_process";
2
2
  import { realpathSync } from "node:fs";
3
3
  import process from "node:process";
4
4
  import { promisify } from "node:util";
5
- import { buildChildEnv } from "./env-utils.js";
5
+ import { resolveChildEnv } from "./env-utils.js";
6
6
  import { getErrorMessage } from "./error-utils.js";
7
7
  import { whichSync } from "./path-repair.js";
8
8
  import { compareSemver, extractSemver } from "./version-utils.js";
@@ -53,13 +53,10 @@ const PROVIDER_CLI_SPECS = [
53
53
  updateArgs: ["update", "self"],
54
54
  },
55
55
  ];
56
- function childEnv(options) {
57
- return options.env ?? buildChildEnv(options.inheritEnv !== false);
58
- }
59
56
  async function runCommand(command, args, timeout, options) {
60
57
  const { stdout, stderr } = await execFileAsync(command, args, {
61
58
  timeout,
62
- env: childEnv(options),
59
+ env: resolveChildEnv(options),
63
60
  maxBuffer: MAX_BUFFER,
64
61
  });
65
62
  return { stdout: String(stdout ?? ""), stderr: String(stderr ?? "") };
@@ -95,7 +92,7 @@ export function providerCliUpdateAvailable(currentVersion, latestVersion) {
95
92
  return compareSemver(latestVersion, currentVersion) > 0;
96
93
  }
97
94
  async function readInstalledVersion(spec, options) {
98
- const env = childEnv(options);
95
+ const env = resolveChildEnv(options);
99
96
  const executable = whichSync(spec.command, { env, timeoutMs: options.versionTimeoutMs ?? VERSION_TIMEOUT_MS });
100
97
  if (!executable)
101
98
  return { executable: null, version: null };
@@ -1,12 +1,10 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { shellQuote } from "./shell-quote.js";
4
5
  const CLI_EXIT_MARKER_START = "\x1eWAND_CLI_EXIT:";
5
6
  const CLI_EXIT_MARKER_END = "\x1f";
6
7
  const POSIX_SHELLS = new Set(["ash", "bash", "dash", "ksh", "ksh93", "mksh", "sh", "zsh"]);
7
- function quotePosixShell(value) {
8
- return `'${value.replace(/'/g, `'\\''`)}'`;
9
- }
10
8
  function isKnownPosixShell(shell) {
11
9
  return POSIX_SHELLS.has(path.basename(shell).toLowerCase());
12
10
  }
@@ -20,7 +18,7 @@ function isKnownPosixShell(shell) {
20
18
  * persisted `print -s`, avoiding duplicate entries.
21
19
  */
22
20
  function buildShellHistoryRegistration(command, shell) {
23
- const quotedCommand = quotePosixShell(command);
21
+ const quotedCommand = shellQuote(command);
24
22
  switch (path.basename(shell).toLowerCase()) {
25
23
  case "zsh":
26
24
  return [
@@ -114,7 +112,7 @@ function buildPosixProviderShellCommand(command, shell, marker) {
114
112
  buildTerminalRestoreCommand(),
115
113
  // -i forces an interactive prompt even if tty detection is confused after a
116
114
  // TUI teardown; -l matches Terminal.app / iTerm login-shell startup files.
117
- `exec ${quotePosixShell(shell)} -il`,
115
+ `exec ${shellQuote(shell)} -il`,
118
116
  ].join("; ");
119
117
  }
120
118
  /**
@@ -11,7 +11,7 @@ import os from "node:os";
11
11
  import path from "node:path";
12
12
  import { resolveConfigDir } from "./config.js";
13
13
  import { normalizePublicOrigin } from "./ios-ota.js";
14
- import { firstQueryStringValue, firstHeaderValue, firstHeaderListValue } from "./server-request.js";
14
+ import { firstHeaderListValue, firstHeaderValue, firstQueryStringValue, forwardedParam, normalizeProtocol, } from "./server-request.js";
15
15
  // ── App 连接码 ──
16
16
  //
17
17
  // 连接码里的 token 是密码派生的(HMAC(password, appSecret)),兼容既有连接码;
@@ -27,45 +27,16 @@ export function encodeConnectCode(url, token) {
27
27
  return Buffer.from(`${url}#${token}`).toString("base64");
28
28
  }
29
29
  // ── 转发头解析 ──
30
- function unquoteHeaderValue(value) {
31
- const trimmed = value.trim();
32
- if (trimmed.length >= 2 && trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
33
- return trimmed.slice(1, -1);
34
- }
35
- return trimmed;
36
- }
37
- function getForwardedParam(req, key) {
38
- const forwarded = firstHeaderListValue(req.headers.forwarded);
39
- if (!forwarded)
40
- return undefined;
41
- const targetKey = key.toLowerCase();
42
- for (const part of forwarded.split(";")) {
43
- const eqIdx = part.indexOf("=");
44
- if (eqIdx < 1)
45
- continue;
46
- const partKey = part.slice(0, eqIdx).trim().toLowerCase();
47
- if (partKey !== targetKey)
48
- continue;
49
- return unquoteHeaderValue(part.slice(eqIdx + 1));
50
- }
51
- return undefined;
52
- }
53
- function normalizePublicProtocol(value) {
54
- const proto = value?.trim().toLowerCase();
55
- if (proto === "http" || proto === "https")
56
- return proto;
57
- return undefined;
58
- }
59
30
  function getPublicRequestProtocol(req, fallback) {
60
- return (normalizePublicProtocol(firstHeaderListValue(req.headers["x-forwarded-proto"]))
61
- ?? normalizePublicProtocol(getForwardedParam(req, "proto"))
31
+ return (normalizeProtocol(firstHeaderListValue(req.headers["x-forwarded-proto"]))
32
+ ?? normalizeProtocol(forwardedParam(firstHeaderListValue(req.headers.forwarded), "proto"))
62
33
  ?? (firstHeaderListValue(req.headers["x-forwarded-ssl"])?.toLowerCase() === "on" ? "https" : undefined)
63
34
  ?? (firstHeaderListValue(req.headers["x-forwarded-scheme"])?.toLowerCase() === "https" ? "https" : undefined)
64
35
  ?? fallback);
65
36
  }
66
37
  function getPublicRequestHost(req, config) {
67
38
  return (firstHeaderListValue(req.headers["x-forwarded-host"])
68
- ?? getForwardedParam(req, "host")
39
+ ?? forwardedParam(firstHeaderListValue(req.headers.forwarded), "host")
69
40
  ?? req.headers.host
70
41
  ?? `${config.host}:${config.port}`);
71
42
  }
@@ -163,26 +134,20 @@ export function appTokenLoginPayload(req, config, useHttps, password) {
163
134
  function isExternalAvatarSource(value) {
164
135
  return /^(https?:|data:)/i.test(value);
165
136
  }
166
- function normalizePersonaName(value) {
167
- if (typeof value !== "string")
168
- return undefined;
169
- const trimmed = value.trim();
170
- return trimmed || undefined;
171
- }
172
- function normalizePersonaAvatar(value) {
137
+ /** persona 的 name / avatar 都是「可选非空字符串」:空白与非法类型都当未设置。 */
138
+ function normalizeOptionalText(value) {
173
139
  if (typeof value !== "string")
174
140
  return undefined;
175
- const trimmed = value.trim();
176
- return trimmed || undefined;
141
+ return value.trim() || undefined;
177
142
  }
178
143
  export function resolveStructuredChatPersona(config) {
179
144
  const persona = config.structuredChatPersona;
180
145
  if (!persona)
181
146
  return undefined;
182
- const userName = normalizePersonaName(persona.user?.name);
183
- const userAvatar = normalizePersonaAvatar(persona.user?.avatar);
184
- const assistantName = normalizePersonaName(persona.assistant?.name);
185
- const assistantAvatar = normalizePersonaAvatar(persona.assistant?.avatar);
147
+ const userName = normalizeOptionalText(persona.user?.name);
148
+ const userAvatar = normalizeOptionalText(persona.user?.avatar);
149
+ const assistantName = normalizeOptionalText(persona.assistant?.name);
150
+ const assistantAvatar = normalizeOptionalText(persona.assistant?.avatar);
186
151
  if (!userName && !userAvatar && !assistantName && !assistantAvatar) {
187
152
  return undefined;
188
153
  }
@@ -1,14 +1,15 @@
1
1
  import { asyncRoute } from "./express-async.js";
2
2
  import { connectGithub, getGithubConnectorStatus, githubRequest, } from "./github-connector.js";
3
- import { bodyObject, integerQuery, requiredString, sendRouteError, stringQuery } from "./server-request.js";
3
+ import { bodyObject, integerQuery, requiredString, sendRouteError, text } from "./server-request.js";
4
4
  function githubListQuery(reqQuery, defaults = {}) {
5
5
  const query = new URLSearchParams();
6
6
  if (defaults.state)
7
- query.set("state", stringQuery(reqQuery.state, defaults.state));
8
- if (stringQuery(reqQuery.labels))
9
- query.set("labels", stringQuery(reqQuery.labels));
10
- const sort = stringQuery(reqQuery.sort, defaults.sort ?? "");
11
- const direction = stringQuery(reqQuery.direction, defaults.direction ?? "");
7
+ query.set("state", text(reqQuery.state, defaults.state));
8
+ const labels = text(reqQuery.labels);
9
+ if (labels)
10
+ query.set("labels", labels);
11
+ const sort = text(reqQuery.sort, defaults.sort ?? "");
12
+ const direction = text(reqQuery.direction, defaults.direction ?? "");
12
13
  if (sort)
13
14
  query.set("sort", sort);
14
15
  if (direction)
@@ -111,17 +112,17 @@ export function registerGithubRoutes(app, deps) {
111
112
  app.get("/api/github/repos", requireAdmin, asyncRoute(async (req, res) => {
112
113
  try {
113
114
  const query = new URLSearchParams();
114
- const visibility = stringQuery(req.query.visibility);
115
- const affiliation = stringQuery(req.query.affiliation);
116
- const type = stringQuery(req.query.type);
115
+ const visibility = text(req.query.visibility);
116
+ const affiliation = text(req.query.affiliation);
117
+ const type = text(req.query.type);
117
118
  if (visibility)
118
119
  query.set("visibility", visibility);
119
120
  if (affiliation)
120
121
  query.set("affiliation", affiliation);
121
122
  if (type)
122
123
  query.set("type", type);
123
- query.set("sort", stringQuery(req.query.sort, "updated"));
124
- query.set("direction", stringQuery(req.query.direction, "desc"));
124
+ query.set("sort", text(req.query.sort, "updated"));
125
+ query.set("direction", text(req.query.direction, "desc"));
125
126
  query.set("per_page", String(integerQuery(req.query.per_page, 30, 1, 100)));
126
127
  query.set("page", String(integerQuery(req.query.page, 1, 1, 10_000)));
127
128
  const suffix = query.toString();
@@ -3,6 +3,11 @@ import { getErrorMessage } from "./error-utils.js";
3
3
  import { asyncRoute } from "./express-async.js";
4
4
  const MAX_PORT = 65535;
5
5
  const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
6
+ /** URL 里可能写 `localhost` / `[::1]`,建立连接前规范成裸主机名。 */
7
+ const NORMALIZED_LOOPBACK_HOSTNAMES = {
8
+ localhost: "127.0.0.1",
9
+ "[::1]": "::1",
10
+ };
6
11
  /** Headers that identify the original Wand request and must not leak downstream. */
7
12
  const FILTERED_REQUEST_HEADERS = new Set([
8
13
  "authorization",
@@ -40,7 +45,7 @@ export function parseLocalPreviewTarget(rawHost, rawPort, rawPath) {
40
45
  return null;
41
46
  const suffix = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
42
47
  return {
43
- hostname: hostname === "localhost" ? "127.0.0.1" : hostname === "[::1]" ? "::1" : hostname,
48
+ hostname: NORMALIZED_LOOPBACK_HOSTNAMES[hostname] ?? hostname,
44
49
  port,
45
50
  path: suffix,
46
51
  };
@@ -13,8 +13,10 @@ export declare function firstQueryStringValue(value: unknown): string | undefine
13
13
  export declare function firstHeaderValue(value: string | string[] | undefined): string | undefined;
14
14
  /** 逗号分隔的请求头(如 Forwarded / X-Forwarded-*)取第一段。 */
15
15
  export declare function firstHeaderListValue(value: string | string[] | undefined): string | undefined;
16
- /** 查询参数版 `text()`:显式传 fallback 时返回 fallback 而不是空串。 */
17
- export declare function stringQuery(value: unknown, fallback?: string): string;
16
+ /** 从 Forwarded 头(`proto=https;host=example.com`)里取指定参数;缺失返回 undefined。 */
17
+ export declare function forwardedParam(header: string | undefined, key: string): string | undefined;
18
+ /** 把 header / URL 里的协议名收敛成 http | https;其余情况返回 undefined。 */
19
+ export declare function normalizeProtocol(value: string | undefined): "http" | "https" | undefined;
18
20
  /** 解析范围内整数,越界或非整数回落到 fallback。 */
19
21
  export declare function integerQuery(value: unknown, fallback: number, min: number, max: number): number;
20
22
  /** 要求请求体是普通对象;数组和 null 都拒绝。 */
@@ -28,13 +28,34 @@ export function firstHeaderValue(value) {
28
28
  export function firstHeaderListValue(value) {
29
29
  return firstHeaderValue(value)?.split(",")[0]?.trim();
30
30
  }
31
- /** 查询参数版 `text()`:显式传 fallback 时返回 fallback 而不是空串。 */
32
- export function stringQuery(value, fallback = "") {
33
- return typeof value === "string" ? value.trim() : fallback;
31
+ /** 从 Forwarded 头(`proto=https;host=example.com`)里取指定参数;缺失返回 undefined。 */
32
+ export function forwardedParam(header, key) {
33
+ if (!header)
34
+ return undefined;
35
+ const targetKey = key.toLowerCase();
36
+ for (const part of header.split(";")) {
37
+ const eqIndex = part.indexOf("=");
38
+ if (eqIndex < 1)
39
+ continue;
40
+ if (part.slice(0, eqIndex).trim().toLowerCase() !== targetKey)
41
+ continue;
42
+ const value = part.slice(eqIndex + 1).trim();
43
+ return value.length >= 2 && value.startsWith("\"") && value.endsWith("\"")
44
+ ? value.slice(1, -1)
45
+ : value;
46
+ }
47
+ return undefined;
48
+ }
49
+ /** 把 header / URL 里的协议名收敛成 http | https;其余情况返回 undefined。 */
50
+ export function normalizeProtocol(value) {
51
+ const proto = value?.trim().toLowerCase();
52
+ if (proto === "http" || proto === "https")
53
+ return proto;
54
+ return undefined;
34
55
  }
35
56
  /** 解析范围内整数,越界或非整数回落到 fallback。 */
36
57
  export function integerQuery(value, fallback, min, max) {
37
- const parsed = Number(stringQuery(value));
58
+ const parsed = Number(text(value));
38
59
  return Number.isInteger(parsed) && parsed >= min && parsed <= max ? parsed : fallback;
39
60
  }
40
61
  /** 要求请求体是普通对象;数组和 null 都拒绝。 */