@co0ontty/wand 1.43.7 → 1.44.0

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": "e30a59c36bbf6143ba83af70d01db35a7fb449b3",
3
- "builtAt": "2026-05-31T08:35:33.496Z",
4
- "version": "1.43.7",
2
+ "commit": "82bf2e6b3f7f01bbee5ee3213967f3316020601b",
3
+ "builtAt": "2026-05-31T13:20:33.303Z",
4
+ "version": "1.44.0",
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
  *
@@ -10,6 +10,11 @@ interface QuickCommitOptions {
10
10
  /** When `tag` is empty, ask Claude to generate one based on the diff + commit message. */
11
11
  autoTag?: boolean;
12
12
  push?: boolean;
13
+ /**
14
+ * 是否把 commit / tag / push 递归进入各 submodule 内部。默认 false:
15
+ * 只处理父仓库自身(含已变化的 submodule 指针),不碰 submodule 内部 dirty。
16
+ */
17
+ submodule?: boolean;
13
18
  }
14
19
  export declare class QuickCommitError extends Error {
15
20
  readonly code: QuickCommitErrorCode;
@@ -35,6 +40,10 @@ interface PushOptions {
35
40
  cwd: string;
36
41
  pushCommits?: boolean;
37
42
  pushTags?: boolean;
43
+ /** 是否同时把各 submodule 的 HEAD(+ 同名 tag)分别推送到各自远端分支。 */
44
+ submodule?: boolean;
45
+ /** `submodule` + `pushTags` 时,要连带推送到 submodule 的同名 tag。 */
46
+ tagName?: string;
38
47
  }
39
48
  export declare function runPush(opts: PushOptions): Promise<PushResult>;
40
49
  export declare function runQuickCommit(opts: QuickCommitOptions): Promise<QuickCommitResult>;
@@ -241,6 +241,8 @@ export function getGitStatus(cwd) {
241
241
  behind,
242
242
  lastCommit,
243
243
  latestTag,
244
+ // 基于全量 allEntries 判定(不受 files 的 200 条 slice 影响),供前端决定是否渲染 Submodule 球。
245
+ hasSubmodule: allEntries.some((e) => e.isSubmodule),
244
246
  };
245
247
  }
246
248
  export class QuickCommitError extends Error {
@@ -445,6 +447,7 @@ ${diff}`;
445
447
  */
446
448
  function doPush(opts) {
447
449
  const { cwd, pushCommits, pushTags } = opts;
450
+ const recurseFlag = `--recurse-submodules=${opts.recurseSubmodules ?? "check"}`;
448
451
  let pushedCommits = false;
449
452
  let pushedTags = false;
450
453
  let hasUpstream = false;
@@ -459,10 +462,10 @@ function doPush(opts) {
459
462
  try {
460
463
  if (pushCommits) {
461
464
  if (hasUpstream) {
462
- runGit(["push", "--recurse-submodules=on-demand"], cwd, GIT_PUSH_TIMEOUT_MS);
465
+ runGit(["push", recurseFlag], cwd, GIT_PUSH_TIMEOUT_MS);
463
466
  }
464
467
  else {
465
- runGit(["push", "-u", "--recurse-submodules=on-demand", pushRemote, "HEAD"], cwd, GIT_PUSH_TIMEOUT_MS);
468
+ runGit(["push", "-u", recurseFlag, pushRemote, "HEAD"], cwd, GIT_PUSH_TIMEOUT_MS);
466
469
  }
467
470
  pushedCommits = true;
468
471
  }
@@ -538,19 +541,63 @@ export async function runTagHead(opts) {
538
541
  };
539
542
  }
540
543
  export async function runPush(opts) {
541
- const { cwd, pushCommits = true, pushTags = false } = opts;
544
+ const { cwd, pushCommits = true, pushTags = false, submodule = false, tagName } = opts;
542
545
  assertGitWorkTree(cwd);
543
546
  if (!pushCommits && !pushTags) {
544
547
  throw new QuickCommitError("没有要推送的内容。", "NOTHING_TO_PUSH");
545
548
  }
546
- const outcome = doPush({ cwd, pushCommits, pushTags });
549
+ // 纳入 submodule:逐个把声明的 submodule HEAD 推到各自远端分支(已是最新则 git 自身 no-op)。
550
+ // 这覆盖「先 commit(含 submodule)后补 Push & Close」的场景——此时 submodule 已 clean、
551
+ // status 看不到,但本地可能仍领先远端。父仓库随后用 recurse=no 推送。
552
+ let subPushErrors = [];
553
+ if (submodule) {
554
+ const { base, infos } = collectSubmodulesForPush(cwd);
555
+ if (infos.length > 0) {
556
+ const subPush = pushSubmodules(base, infos, { pushTags: !!(pushTags && tagName), tagName });
557
+ subPushErrors = subPush.errors;
558
+ }
559
+ }
560
+ const outcome = doPush({ cwd, pushCommits, pushTags, recurseSubmodules: submodule ? "no" : "check" });
547
561
  return {
548
- ok: !outcome.error,
562
+ ok: !outcome.error && subPushErrors.length === 0,
549
563
  pushedCommits: outcome.pushedCommits,
550
564
  pushedTags: outcome.pushedTags,
551
- error: outcome.error,
565
+ error: outcome.error || (subPushErrors.length ? subPushErrors.join(";") : undefined),
552
566
  };
553
567
  }
568
+ /**
569
+ * 解析一个 submodule 当前 HEAD 应推到哪个远端分支。submodule 经 `git submodule update`
570
+ * 后通常处于 detached HEAD,不能直接 `git push`,必须显式 `HEAD:<branch>`,否则父仓库
571
+ * `--recurse-submodules=on-demand` 会以「HEAD does not match the named branch」整体失败。
572
+ * 优先取远端默认分支(`<remote>/HEAD` 指向)→ 当前命名分支 → 回退 main/master。
573
+ */
574
+ function resolveSubmodulePushBranch(subCwd, remote) {
575
+ const prefix = `${remote}/`;
576
+ try {
577
+ const ref = runGit(["symbolic-ref", "--short", `refs/remotes/${remote}/HEAD`], subCwd);
578
+ const name = (ref.startsWith(prefix) ? ref.slice(prefix.length) : ref).trim();
579
+ if (name)
580
+ return name;
581
+ }
582
+ catch {
583
+ // ignore — fall through
584
+ }
585
+ try {
586
+ const cur = runGit(["branch", "--show-current"], subCwd);
587
+ if (cur)
588
+ return cur;
589
+ }
590
+ catch {
591
+ // ignore
592
+ }
593
+ try {
594
+ runGit(["rev-parse", "--verify", `refs/remotes/${remote}/main`], subCwd);
595
+ return "main";
596
+ }
597
+ catch {
598
+ return "master";
599
+ }
600
+ }
554
601
  /**
555
602
  * 在 commit 父仓库之前,先在每个内部 dirty / untracked 的 submodule 里
556
603
  * 执行一次 `git add -A` + `git commit -m <msg>`,让父仓库的 add -A
@@ -615,12 +662,109 @@ function commitDirtySubmodules(parentCwd, message) {
615
662
  hash = runGit(["rev-parse", "--short", "HEAD"], subCwd);
616
663
  }
617
664
  catch { /* ignore */ }
618
- commits.push({ path: entry.path, hash });
665
+ const remote = resolvePushRemote(subCwd);
666
+ const branch = resolveSubmodulePushBranch(subCwd, remote);
667
+ commits.push({ path: entry.path, hash, remote, branch });
619
668
  }
620
669
  return { commits, errors };
621
670
  }
671
+ /** 对刚提交的 submodule 打上与父仓库同名的 tag。已存在同名 tag 则记为非致命跳过。 */
672
+ function tagSubmodules(parentCwd, subInfos, tagName) {
673
+ const tagged = [];
674
+ const errors = [];
675
+ for (const info of subInfos) {
676
+ const subCwd = `${parentCwd}/${info.path}`;
677
+ try {
678
+ runGit(["rev-parse", "--verify", `refs/tags/${tagName}`], subCwd);
679
+ errors.push(`submodule ${info.path} 已存在 tag ${tagName},跳过`);
680
+ continue;
681
+ }
682
+ catch {
683
+ // 不存在 → 继续打 tag
684
+ }
685
+ try {
686
+ runGit(["tag", tagName], subCwd);
687
+ tagged.push(info.path);
688
+ }
689
+ catch (error) {
690
+ errors.push(`submodule ${info.path} 打 tag 失败:${getGitErrorMessage(error)}`);
691
+ }
692
+ }
693
+ return { tagged, errors };
694
+ }
695
+ /**
696
+ * 对每个 submodule 单独把当前 HEAD 推送到其远端分支(`HEAD:refs/heads/<branch>`),
697
+ * 解决 detached HEAD 无法直接 push 的问题;`pushTags` 时连带把同名 tag 推上去。
698
+ * 单个 submodule 失败收集为非致命错误。
699
+ */
700
+ function pushSubmodules(parentCwd, subInfos, opts) {
701
+ const pushed = [];
702
+ const errors = [];
703
+ for (const info of subInfos) {
704
+ const subCwd = `${parentCwd}/${info.path}`;
705
+ if (!existsSync(subCwd)) {
706
+ errors.push(`submodule ${info.path} 路径不存在`);
707
+ continue;
708
+ }
709
+ try {
710
+ runGit(["push", info.remote, `HEAD:refs/heads/${info.branch}`], subCwd, GIT_PUSH_TIMEOUT_MS);
711
+ }
712
+ catch (error) {
713
+ errors.push(`submodule ${info.path} 推送失败:${getGitErrorMessage(error)}`);
714
+ continue;
715
+ }
716
+ if (opts.pushTags && opts.tagName) {
717
+ try {
718
+ runGit(["push", info.remote, `refs/tags/${opts.tagName}`], subCwd, GIT_PUSH_TIMEOUT_MS);
719
+ }
720
+ catch (error) {
721
+ errors.push(`submodule ${info.path} 推送 tag 失败:${getGitErrorMessage(error)}`);
722
+ }
723
+ }
724
+ pushed.push(info.path);
725
+ }
726
+ return { pushed, errors };
727
+ }
728
+ /**
729
+ * 枚举仓库声明的全部 submodule(读 repo 根的 .gitmodules),为「单独 push」准备
730
+ * remote + 目标分支。用于结果面板的 Push & Close:此时 submodule 多已 clean,
731
+ * status 里看不到,但本地 HEAD 可能仍领先远端,需要逐个尝试推送(up-to-date 则 no-op)。
732
+ */
733
+ function collectSubmodulesForPush(cwd) {
734
+ let base;
735
+ try {
736
+ base = runGit(["rev-parse", "--show-toplevel"], cwd);
737
+ }
738
+ catch {
739
+ base = cwd;
740
+ }
741
+ let raw;
742
+ try {
743
+ raw = runGitAllowEmpty(["config", "-f", `${base}/.gitmodules`, "--get-regexp", "\\.path$"], base);
744
+ }
745
+ catch {
746
+ return { base, infos: [] };
747
+ }
748
+ const infos = [];
749
+ for (const line of raw.split(/\r?\n/)) {
750
+ const trimmed = line.trim();
751
+ if (!trimmed)
752
+ continue;
753
+ // 行格式:submodule.<name>.path <relpath>
754
+ const rel = trimmed.split(/\s+/).slice(1).join(" ").trim();
755
+ if (!rel)
756
+ continue;
757
+ const subCwd = `${base}/${rel}`;
758
+ if (!existsSync(subCwd))
759
+ continue;
760
+ const remote = resolvePushRemote(subCwd);
761
+ const branch = resolveSubmodulePushBranch(subCwd, remote);
762
+ infos.push({ path: rel, hash: "", remote, branch });
763
+ }
764
+ return { base, infos };
765
+ }
622
766
  export async function runQuickCommit(opts) {
623
- const { cwd, language, autoMessage, customMessage, tag, autoTag, push } = opts;
767
+ const { cwd, language, autoMessage, customMessage, tag, autoTag, push, submodule } = opts;
624
768
  assertGitWorkTree(cwd);
625
769
  // 先 add 一次让我们能在 collectStagedDiff 看到完整改动(包含 submodule 指针),
626
770
  // AI 生成 message 时也基于这个 staged diff。
@@ -646,7 +790,11 @@ export async function runQuickCommit(opts) {
646
790
  submoduleHasDirty = parsePorcelainV2(porcelain).some((e) => e.isSubmodule && (e.submoduleState?.hasTrackedChanges || e.submoduleState?.hasUntracked));
647
791
  }
648
792
  catch { /* keep submoduleHasDirty=false */ }
649
- if (!parentHasStaged && !submoduleHasDirty) {
793
+ // 默认不纳入 submodule:只有显式 opts.submodule 时,submodule 内部 dirty 才计入「有改动」。
794
+ if (!parentHasStaged && !(submodule && submoduleHasDirty)) {
795
+ if (submoduleHasDirty && !submodule) {
796
+ throw new QuickCommitError("父仓库没有改动;检测到 submodule 内部有改动,拖入 Submodule 球可一起提交。", "NOTHING_TO_COMMIT");
797
+ }
650
798
  throw new QuickCommitError("没有任何改动可以提交。", "NOTHING_TO_COMMIT");
651
799
  }
652
800
  let message;
@@ -659,22 +807,25 @@ export async function runQuickCommit(opts) {
659
807
  throw new QuickCommitError("commit message 不能为空。", "EMPTY_MESSAGE");
660
808
  }
661
809
  }
662
- // 先提交 submodule 内部改动;父仓库随后再 add 一次,picks up 新的 submodule 指针。
663
- const submoduleOutcome = commitDirtySubmodules(cwd, message);
664
- if (submoduleOutcome.commits.length > 0) {
665
- try {
666
- runGit(["add", "-A"], cwd, 5000);
667
- }
668
- catch (error) {
669
- throw new QuickCommitError(`父仓库 add submodule 指针失败:${getGitErrorMessage(error)}`, "GIT_ADD_FAILED");
670
- }
671
- // 重新评估父仓库是否有 staged 内容:如果 submodule 是新引入的或指针变了,
672
- // 这里应当为真;如果完全没变就走 commit --allow-empty 路径不合适,直接报错。
673
- try {
674
- stagedFiles = runGitAllowEmpty(["diff", "--cached", "--name-only"], cwd).trim();
675
- parentHasStaged = stagedFiles.length > 0;
810
+ // 仅当用户显式纳入 submodule 时,才进入各 submodule 内部提交其 dirty 改动;
811
+ // 父仓库随后再 add 一次,picks up 新的 submodule 指针。
812
+ let submoduleOutcome = { commits: [], errors: [] };
813
+ if (submodule) {
814
+ submoduleOutcome = commitDirtySubmodules(cwd, message);
815
+ if (submoduleOutcome.commits.length > 0) {
816
+ try {
817
+ runGit(["add", "-A"], cwd, 5000);
818
+ }
819
+ catch (error) {
820
+ throw new QuickCommitError(`父仓库 add submodule 指针失败:${getGitErrorMessage(error)}`, "GIT_ADD_FAILED");
821
+ }
822
+ // 重新评估父仓库是否有 staged 内容:submodule 指针变了这里应为真。
823
+ try {
824
+ stagedFiles = runGitAllowEmpty(["diff", "--cached", "--name-only"], cwd).trim();
825
+ parentHasStaged = stagedFiles.length > 0;
826
+ }
827
+ catch { /* keep stale value */ }
676
828
  }
677
- catch { /* keep stale value */ }
678
829
  }
679
830
  if (!parentHasStaged) {
680
831
  // submodule 都提交了但父仓库还是没有 staged —— 通常意味着 .gitmodules 没动
@@ -710,18 +861,32 @@ export async function runQuickCommit(opts) {
710
861
  catch (error) {
711
862
  throw new QuickCommitError(`git tag 失败:${getGitErrorMessage(error)}`, "GIT_TAG_FAILED");
712
863
  }
864
+ // 纳入 submodule 时给刚提交的 submodule 打同名 tag(非致命,失败不阻断父仓库流程)。
865
+ if (submodule && submoduleOutcome.commits.length > 0) {
866
+ tagSubmodules(cwd, submoduleOutcome.commits, tagName);
867
+ }
713
868
  }
714
869
  let pushed = false;
715
870
  let pushError;
716
871
  if (push) {
872
+ // 纳入 submodule:先把各 submodule 的 HEAD(+ 同名 tag)分别推到各自远端分支,
873
+ // 解决 detached HEAD 无法被父仓库 on-demand 递归推送的问题;父仓库随后用
874
+ // recurse=no 单独推(submodule 已就绪)。否则父仓库用 recurse=check 做安全校验。
875
+ const includeSub = !!submodule && submoduleOutcome.commits.length > 0;
876
+ let subPushErrors = [];
877
+ if (includeSub) {
878
+ const subPush = pushSubmodules(cwd, submoduleOutcome.commits, { pushTags: !!tagName, tagName });
879
+ subPushErrors = subPush.errors;
880
+ }
717
881
  const outcome = doPush({
718
882
  cwd,
719
883
  pushCommits: true,
720
884
  // Push only the freshly-created tag — avoids surprising users by pushing stale local tags.
721
885
  pushTags: tagName ? [tagName] : false,
886
+ recurseSubmodules: includeSub ? "no" : "check",
722
887
  });
723
- pushed = outcome.pushedCommits && (tagName ? outcome.pushedTags : true);
724
- pushError = outcome.error;
888
+ pushed = outcome.pushedCommits && (tagName ? outcome.pushedTags : true) && subPushErrors.length === 0;
889
+ pushError = outcome.error || (subPushErrors.length ? subPushErrors.join(";") : undefined);
725
890
  }
726
891
  return {
727
892
  ok: true,
@@ -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;