@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.
- package/dist/build-info.d.ts +18 -0
- package/dist/build-info.js +13 -0
- package/dist/build-info.json +4 -4
- package/dist/config.js +6 -11
- package/dist/env-utils.d.ts +5 -0
- package/dist/env-utils.js +4 -0
- package/dist/git-quick-commit.js +7 -11
- package/dist/ios-ota.js +8 -29
- package/dist/layout-tree.d.ts +6 -0
- package/dist/layout-tree.js +9 -0
- package/dist/missions.js +2 -2
- package/dist/models.js +2 -5
- package/dist/path-repair.js +2 -1
- package/dist/process-manager.js +10 -20
- package/dist/provider-cli-updater.js +3 -6
- package/dist/pty-shell-launch.js +3 -5
- package/dist/server-app-connect.js +11 -46
- package/dist/server-github-routes.js +12 -11
- package/dist/server-local-preview-routes.js +6 -1
- package/dist/server-request.d.ts +4 -2
- package/dist/server-request.js +25 -4
- package/dist/server-session-routes.js +14 -78
- package/dist/server-settings-routes.d.ts +2 -6
- package/dist/server-settings-routes.js +3 -8
- package/dist/server-task-routes.d.ts +9 -6
- package/dist/server-task-routes.js +50 -43
- package/dist/server-update-routes.d.ts +2 -5
- package/dist/server-update-routes.js +42 -9
- package/dist/server-workspace-routes.js +4 -12
- package/dist/server.js +33 -99
- package/dist/session-ai-context.js +6 -38
- package/dist/session-provider.d.ts +16 -0
- package/dist/session-provider.js +49 -0
- package/dist/session-registry.d.ts +10 -0
- package/dist/session-registry.js +41 -14
- package/dist/session-topic.js +3 -11
- package/dist/shell-quote.d.ts +9 -0
- package/dist/shell-quote.js +13 -0
- package/dist/signal-utils.d.ts +10 -0
- package/dist/signal-utils.js +38 -0
- package/dist/storage.js +17 -55
- package/dist/structured-client-protocol.js +4 -8
- package/dist/structured-codex-protocol.js +1 -6
- package/dist/structured-content.d.ts +2 -0
- package/dist/structured-content.js +6 -0
- package/dist/structured-exec-host.d.ts +0 -2
- package/dist/structured-exec-host.js +2 -11
- package/dist/structured-exec-pump.js +2 -12
- package/dist/structured-grok-adapter.js +1 -5
- package/dist/structured-opencode-adapter.js +1 -5
- package/dist/structured-pi-adapter.js +9 -11
- package/dist/structured-provider-common.d.ts +11 -3
- package/dist/structured-provider-common.js +16 -23
- package/dist/structured-qoder-adapter.js +3 -8
- package/dist/structured-session-manager.js +6 -17
- package/dist/task-title.js +4 -12
- package/dist/task-types.d.ts +23 -1
- package/dist/task-types.js +26 -0
- package/dist/terminal-daemon-protocol.d.ts +0 -6
- package/dist/terminal-daemon-server.js +1 -9
- package/dist/text-utils.d.ts +7 -0
- package/dist/text-utils.js +15 -0
- package/dist/tui/attach.js +17 -93
- package/dist/tui/commands.d.ts +2 -0
- package/dist/tui/commands.js +3 -2
- package/dist/tui/index.js +28 -115
- package/dist/tui/runtime-utils.d.ts +7 -0
- package/dist/tui/runtime-utils.js +33 -0
- package/dist/tui/service-actions.d.ts +31 -0
- package/dist/tui/service-actions.js +86 -0
- package/dist/tui/snapshot.js +1 -8
- package/dist/update-helper.js +6 -35
- package/dist/web-ui/content/scripts.js +397 -497
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +7210 -0
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/web-ui/index.js +3 -3
- package/dist/web-ui/provider-identity.d.ts +4 -0
- package/dist/web-ui/provider-identity.js +9 -0
- package/dist/web-ui/styles.js +24 -7
- package/dist/workspace-binding.d.ts +2 -0
- package/dist/workspace-binding.js +4 -3
- 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
|
+
}
|
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-09-
|
|
4
|
-
"version": "4.
|
|
5
|
-
"channel": "
|
|
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
|
-
|
|
10
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
package/dist/env-utils.d.ts
CHANGED
|
@@ -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;
|
package/dist/git-quick-commit.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
7
|
-
?? forwardedParam(
|
|
7
|
+
const forwardedProto = firstHeaderListValue(req.headers["x-forwarded-proto"])
|
|
8
|
+
?? forwardedParam(firstHeaderListValue(req.headers.forwarded), "proto");
|
|
8
9
|
const proto = normalizeProtocol(forwardedProto)
|
|
9
|
-
?? (
|
|
10
|
-
?? (
|
|
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 =
|
|
13
|
-
?? forwardedParam(
|
|
14
|
-
??
|
|
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,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
|
-
|
|
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) => !
|
|
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 {
|
|
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 =
|
|
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),
|
package/dist/path-repair.js
CHANGED
|
@@ -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 =
|
|
10
|
+
const PROBE_COMMANDS = SESSION_PROVIDERS.map(providerCliCommand);
|
|
10
11
|
const DEEP_PROBE_TIMEOUT_MS = 4000;
|
|
11
12
|
/**
|
|
12
13
|
* 构造候选 bin 目录列表(按优先级,前面的更可信)。
|
package/dist/process-manager.js
CHANGED
|
@@ -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 ??
|
|
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 ??
|
|
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
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
result +=
|
|
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 {
|
|
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:
|
|
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 =
|
|
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 };
|
package/dist/pty-shell-launch.js
CHANGED
|
@@ -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 =
|
|
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 ${
|
|
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 {
|
|
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 (
|
|
61
|
-
??
|
|
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
|
-
??
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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 =
|
|
183
|
-
const userAvatar =
|
|
184
|
-
const assistantName =
|
|
185
|
-
const assistantAvatar =
|
|
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,
|
|
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",
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
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 =
|
|
115
|
-
const affiliation =
|
|
116
|
-
const 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",
|
|
124
|
-
query.set("direction",
|
|
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
|
|
48
|
+
hostname: NORMALIZED_LOOPBACK_HOSTNAMES[hostname] ?? hostname,
|
|
44
49
|
port,
|
|
45
50
|
path: suffix,
|
|
46
51
|
};
|
package/dist/server-request.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
17
|
-
export declare function
|
|
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 都拒绝。 */
|
package/dist/server-request.js
CHANGED
|
@@ -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
|
-
/**
|
|
32
|
-
export function
|
|
33
|
-
|
|
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(
|
|
58
|
+
const parsed = Number(text(value));
|
|
38
59
|
return Number.isInteger(parsed) && parsed >= min && parsed <= max ? parsed : fallback;
|
|
39
60
|
}
|
|
40
61
|
/** 要求请求体是普通对象;数组和 null 都拒绝。 */
|