dsh-packycode-api 1.3.4 → 1.3.5

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/lib/index.js CHANGED
@@ -2,11 +2,11 @@ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypt
2
2
  import { homedir } from "node:os";
3
3
  import { basename, dirname, join, posix, resolve, win32 } from "node:path";
4
4
  import z from "@deepseek-ai/schemastery";
5
+ import { constants, existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
5
6
  import { spawn } from "node:child_process";
6
- import { constants, existsSync, readFileSync, readdirSync } from "node:fs";
7
7
  import { lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
8
- import { createServer } from "node:http";
9
8
  import { fileURLToPath } from "node:url";
9
+ import { createServer } from "node:http";
10
10
  import { HarnessError, LlmError, QUOTA_EXCEEDED_CODE, createUserMessage, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
11
11
  import { defineTool } from "@deepseek-ai/dsh-tools";
12
12
  import { credentialKey } from "@deepseek-ai/dsh-credentials";
@@ -565,6 +565,362 @@ function withProgramDirOnPath(env, program, platform = process.platform) {
565
565
  };
566
566
  }
567
567
  //#endregion
568
+ //#region src/plugin-version.js
569
+ /**
570
+ * Version self-knowledge for the settings panel.
571
+ *
572
+ * The running version comes from this package. The latest version comes from
573
+ * the npm registry. An in-place update is only offered for a registry install,
574
+ * and it installs that exact version with the host's own pnpm — never through
575
+ * `dsh plugin --profile desktop`, which the desktop app refuses. The install
576
+ * runs under the profile's own `minimumReleaseAge`: a version installed past
577
+ * it would sit in the profile's lockfile, and pnpm then refuses every later
578
+ * plugin install in that profile until the version is old enough. A version
579
+ * pnpm still holds back is reported as pending, with when it clears. Registry
580
+ * bodies and package-manager output stay host-side.
581
+ *
582
+ * @module dsh-packycode-api/plugin-version
583
+ */
584
+ const PACKAGE_NAME = "dsh-packycode-api";
585
+ const NPM_REGISTRY_LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
586
+ const DEFAULT_VERSION_TTL_MS = 5 * 6e4;
587
+ const DEFAULT_VERSION_TIMEOUT_MS = 1e4;
588
+ const DEFAULT_UPDATE_TIMEOUT_MS = 18e4;
589
+ const RELEASE_AGE_ERROR = /ERR_PNPM_(?:NO_MATURE_MATCHING_VERSION|MINIMUM_RELEASE_AGE_VIOLATION)/u;
590
+ const RELEASE_AGE_ENTRY = /(\S+@\S+) was published at (\S+?), within the minimumReleaseAge cutoff \((\S+?)\)/gu;
591
+ /**
592
+ * Recognize pnpm refusing a version that is younger than its
593
+ * `minimumReleaseAge`, whether the version was requested or already sits in
594
+ * the lockfile. Each refused entry reads `<name>@<version> was published at
595
+ * <time>, within the minimumReleaseAge cutoff (<time>)`.
596
+ * @param output - the install's stdout and stderr.
597
+ * @param finishedAt - when the install ended; pnpm drew its cutoff then.
598
+ * @returns undefined when this is not a release-age refusal; otherwise
599
+ * `{ availableAt }`, when the youngest refused entry clears, if pnpm said.
600
+ */
601
+ function releaseAgeRefusal(output, finishedAt) {
602
+ const text = String(output ?? "").replace(/\u001b\[[0-9;]*m/gu, "");
603
+ if (!RELEASE_AGE_ERROR.test(text)) return void 0;
604
+ let availableAt;
605
+ for (const [, , published, cutoff] of text.matchAll(RELEASE_AGE_ENTRY)) {
606
+ const clears = Date.parse(published) + (finishedAt - Date.parse(cutoff));
607
+ if (Number.isFinite(clears) && (availableAt === void 0 || clears > availableAt)) availableAt = clears;
608
+ }
609
+ return availableAt === void 0 ? {} : { availableAt };
610
+ }
611
+ /** Parse a strict `x.y.z[-pre][+build]` version; anything else is undefined. */
612
+ function parseSemver(value) {
613
+ if (typeof value !== "string") return void 0;
614
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value.trim());
615
+ if (match === null) return void 0;
616
+ return {
617
+ major: Number(match[1]),
618
+ minor: Number(match[2]),
619
+ patch: Number(match[3]),
620
+ pre: match[4]
621
+ };
622
+ }
623
+ /** Compare two versions: -1/0/1, or undefined when either side is not semver. */
624
+ function compareSemver(a, b) {
625
+ const left = parseSemver(a);
626
+ const right = parseSemver(b);
627
+ if (left === void 0 || right === void 0) return void 0;
628
+ for (const key of [
629
+ "major",
630
+ "minor",
631
+ "patch"
632
+ ]) if (left[key] !== right[key]) return left[key] < right[key] ? -1 : 1;
633
+ if (left.pre === right.pre) return 0;
634
+ if (left.pre === void 0) return 1;
635
+ if (right.pre === void 0) return -1;
636
+ return left.pre < right.pre ? -1 : 1;
637
+ }
638
+ /**
639
+ * Classify how the running package reached the profile. Only registry specs
640
+ * may be updated in place; local checkouts belong to the owner's iteration.
641
+ */
642
+ function classifySpec(spec) {
643
+ if (typeof spec !== "string" || spec === "") return "unknown";
644
+ if (/^(?:link|file):/u.test(spec)) return "link";
645
+ if (/^(?:\d|[~^*])/u.test(spec)) return "npm";
646
+ return "unknown";
647
+ }
648
+ const readManifest = async (path) => {
649
+ try {
650
+ const parsed = JSON.parse(await readFile(path, "utf8"));
651
+ return parsed !== null && typeof parsed === "object" ? parsed : void 0;
652
+ } catch {
653
+ return;
654
+ }
655
+ };
656
+ /**
657
+ * A zero exit only proves the command ran. pnpm can skip a too-new version
658
+ * and still exit zero, so the landed manifest is what makes the answer true.
659
+ */
660
+ async function confirmInstalled({ profileDir, expected, settleDelayMs, attempts = 3 }) {
661
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
662
+ if ((await readManifest(join(profileDir, "node_modules", "dsh-packycode-api", "package.json")))?.version === expected) return;
663
+ if (attempt < attempts) await new Promise((resolve) => {
664
+ setTimeout(resolve, settleDelayMs);
665
+ });
666
+ }
667
+ throw new Error("PackyCode plugin update did not install the requested version");
668
+ }
669
+ /**
670
+ * Find the profile that owns this running installation. pnpm symlinks resolve
671
+ * to the same realpath on both sides. When no realpath matches, fall back to
672
+ * the only profile that declares the dependency at all.
673
+ */
674
+ async function findInstall({ dshHome, ownPackageJsonUrl }) {
675
+ let ownReal;
676
+ try {
677
+ ownReal = await realpath(fileURLToPath(ownPackageJsonUrl));
678
+ } catch {
679
+ ownReal = void 0;
680
+ }
681
+ let entries = [];
682
+ try {
683
+ entries = await readdir(join(dshHome, "profiles"), { withFileTypes: true });
684
+ } catch {
685
+ entries = [];
686
+ }
687
+ const declared = [];
688
+ for (const entry of entries) {
689
+ if (!entry.isDirectory()) continue;
690
+ const profileDir = join(dshHome, "profiles", entry.name);
691
+ const spec = (await readManifest(join(profileDir, "package.json")))?.dependencies?.[PACKAGE_NAME];
692
+ if (typeof spec !== "string") continue;
693
+ declared.push({
694
+ profile: entry.name,
695
+ spec
696
+ });
697
+ if (ownReal === void 0) continue;
698
+ try {
699
+ if (await realpath(join(profileDir, "node_modules", "dsh-packycode-api", "package.json")) === ownReal) return {
700
+ kind: classifySpec(spec),
701
+ profile: entry.name
702
+ };
703
+ } catch {}
704
+ }
705
+ if (declared.length === 1) return {
706
+ kind: classifySpec(declared[0].spec),
707
+ profile: declared[0].profile
708
+ };
709
+ return { kind: "unknown" };
710
+ }
711
+ const isExistingPath = (value) => typeof value === "string" && value !== "" && existsSync(value);
712
+ /** Anchored to the file name so a directory that merely contains the word cannot match. */
713
+ const PNPM_SCRIPT = /[\\/]pnpm\.(?:mjs|cjs|js)$/u;
714
+ const dshRuntimeRoot = (value) => isExistingPath(value) && existsSync(join(value, "node_modules", "@deepseek-ai", "dsh", "package.json")) ? value : void 0;
715
+ /**
716
+ * Locate the package manager that owns this profile.
717
+ *
718
+ * `dsh plugin --profile desktop` is refused by the desktop app, so the update
719
+ * installs with pnpm directly. The desktop host puts the bundled pnpm in its
720
+ * own arguments; nothing is guessed from the entry script.
721
+ */
722
+ function resolvePnpm({ argv } = {}) {
723
+ const entries = Array.isArray(argv) ? argv : [];
724
+ for (const entry of entries) if (isExistingPath(entry) && PNPM_SCRIPT.test(entry)) return entry;
725
+ for (const entry of entries) {
726
+ const root = dshRuntimeRoot(entry);
727
+ if (root === void 0) continue;
728
+ const script = join(root, "..", "pnpm", "bin", "pnpm.mjs");
729
+ if (existsSync(script)) return script;
730
+ }
731
+ }
732
+ function resolveLauncher({ argv, execPath }) {
733
+ const pnpm = resolvePnpm({ argv });
734
+ if (pnpm === void 0) return {
735
+ command: "pnpm",
736
+ prefix: []
737
+ };
738
+ return {
739
+ command: execPath,
740
+ prefix: [pnpm]
741
+ };
742
+ }
743
+ /** Run pnpm host-side. Its output is never returned to the browser. */
744
+ function spawnRunCommand({ command, args, cwd }, { signal, timeoutMs }) {
745
+ return new Promise((resolve, reject) => {
746
+ if (signal?.aborted) {
747
+ reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("PackyCode plugin update aborted"));
748
+ return;
749
+ }
750
+ const controller = new AbortController();
751
+ const onAbort = () => controller.abort(signal.reason);
752
+ signal?.addEventListener("abort", onAbort, { once: true });
753
+ const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("PackyCode plugin update timed out")), timeoutMs);
754
+ let settled = false;
755
+ const settle = (callback, value) => {
756
+ if (settled) return;
757
+ settled = true;
758
+ clearTimeout(timer);
759
+ signal?.removeEventListener("abort", onAbort);
760
+ callback(value);
761
+ };
762
+ let child;
763
+ try {
764
+ child = spawn(command, args, {
765
+ stdio: [
766
+ "ignore",
767
+ "pipe",
768
+ "pipe"
769
+ ],
770
+ signal: controller.signal,
771
+ ...cwd === void 0 ? {} : { cwd }
772
+ });
773
+ } catch (error) {
774
+ settle(reject, error);
775
+ return;
776
+ }
777
+ let stdout = "";
778
+ let stderr = "";
779
+ child.stdout?.on("data", (chunk) => {
780
+ stdout = (stdout + chunk).slice(-8192);
781
+ });
782
+ child.stderr?.on("data", (chunk) => {
783
+ stderr = (stderr + chunk).slice(-8192);
784
+ });
785
+ child.on("error", (error) => settle(reject, error));
786
+ child.on("close", (code) => {
787
+ if (controller.signal.aborted) {
788
+ const reason = controller.signal.reason;
789
+ settle(reject, reason instanceof Error ? reason : /* @__PURE__ */ new Error("PackyCode plugin update aborted"));
790
+ return;
791
+ }
792
+ settle(resolve, {
793
+ code: code ?? 1,
794
+ stdout,
795
+ stderr
796
+ });
797
+ });
798
+ });
799
+ }
800
+ /**
801
+ * The running version, the latest registry version, which profile installed
802
+ * this copy, and registry-spec updates against that profile.
803
+ */
804
+ function createPackyCodePluginManager({ fetchImpl = ambientFetch, runCommand = spawnRunCommand, execPath = process.execPath, argv = process.argv, env = process.env, ownPackageJsonUrl = new URL("../package.json", import.meta.url), registryUrl = NPM_REGISTRY_LATEST_URL, now = Date.now, ttlMs = DEFAULT_VERSION_TTL_MS, timeoutMs = DEFAULT_VERSION_TIMEOUT_MS, updateTimeoutMs = DEFAULT_UPDATE_TIMEOUT_MS, settleDelayMs = 250 } = {}) {
805
+ if (typeof fetchImpl !== "function") throw new Error("PackyCode plugin manager requires fetch");
806
+ if (typeof runCommand !== "function") throw new Error("PackyCode plugin manager requires runCommand");
807
+ const dshHome = typeof env.DSH_HOME === "string" && env.DSH_HOME !== "" ? env.DSH_HOME : join(homedir(), ".dsh");
808
+ const launcher = resolveLauncher({
809
+ argv,
810
+ execPath
811
+ });
812
+ let updating = false;
813
+ const fetchLatest = async (signal) => {
814
+ let response;
815
+ try {
816
+ response = await boundedFetch(registryUrl, { headers: { Accept: "application/json" } }, {
817
+ fetchImpl,
818
+ timeoutMs,
819
+ signal
820
+ });
821
+ } catch (error) {
822
+ if (error.aborted) throw error.cause;
823
+ throw new Error("PackyCode plugin version check failed");
824
+ }
825
+ if (!response.ok) throw new Error("PackyCode plugin version check failed");
826
+ let version;
827
+ try {
828
+ version = (await response.json())?.version;
829
+ } catch {
830
+ version = void 0;
831
+ }
832
+ if (parseSemver(version) === void 0) throw new Error("PackyCode plugin version check failed");
833
+ return version;
834
+ };
835
+ const readOwnVersion = async () => {
836
+ const version = (await readManifest(fileURLToPath(ownPackageJsonUrl)))?.version;
837
+ if (parseSemver(version) === void 0) throw new Error("PackyCode plugin version is unavailable");
838
+ return version;
839
+ };
840
+ const load = async (signal) => {
841
+ signal?.throwIfAborted();
842
+ const [current, install, latest] = await Promise.all([
843
+ readOwnVersion(),
844
+ findInstall({
845
+ dshHome,
846
+ ownPackageJsonUrl
847
+ }),
848
+ fetchLatest(signal)
849
+ ]);
850
+ return {
851
+ current,
852
+ latest,
853
+ updateAvailable: compareSemver(latest, current) === 1,
854
+ install,
855
+ fetchedAt: now()
856
+ };
857
+ };
858
+ const cache = createCachedRead(load, {
859
+ ttlMs,
860
+ now
861
+ });
862
+ return Object.freeze({
863
+ read: (options) => cache.read(options),
864
+ async update({ signal } = {}) {
865
+ signal?.throwIfAborted();
866
+ if (updating) throw new Error("PackyCode plugin update is already running");
867
+ updating = true;
868
+ try {
869
+ const install = await findInstall({
870
+ dshHome,
871
+ ownPackageJsonUrl
872
+ });
873
+ if (install.kind === "link") throw new Error("PackyCode plugin is installed from a local checkout");
874
+ if (install.kind !== "npm" || install.profile === void 0) throw new Error("PackyCode plugin update could not find the owning profile");
875
+ const latest = await fetchLatest(signal);
876
+ const profileDir = join(dshHome, "profiles", install.profile);
877
+ let result;
878
+ try {
879
+ result = await runCommand({
880
+ command: launcher.command,
881
+ args: [
882
+ ...launcher.prefix,
883
+ "add",
884
+ "--save-exact",
885
+ `${PACKAGE_NAME}@${latest}`
886
+ ],
887
+ cwd: profileDir
888
+ }, {
889
+ signal,
890
+ timeoutMs: updateTimeoutMs
891
+ });
892
+ } catch (error) {
893
+ if (signal?.aborted) throw error;
894
+ throw new Error("PackyCode plugin update failed");
895
+ }
896
+ if (result.code !== 0) {
897
+ const refusal = releaseAgeRefusal(`${result.stdout ?? ""}\n${result.stderr ?? ""}`, now());
898
+ if (refusal !== void 0) return {
899
+ version: latest,
900
+ profile: install.profile,
901
+ pending: true,
902
+ ...refusal
903
+ };
904
+ throw new Error("PackyCode plugin update failed");
905
+ }
906
+ await confirmInstalled({
907
+ profileDir,
908
+ expected: latest,
909
+ settleDelayMs
910
+ });
911
+ cache.invalidate();
912
+ return {
913
+ version: latest,
914
+ profile: install.profile
915
+ };
916
+ } finally {
917
+ updating = false;
918
+ }
919
+ },
920
+ invalidate: () => cache.invalidate()
921
+ });
922
+ }
923
+ //#endregion
568
924
  //#region src/claude-binary.js
569
925
  /**
570
926
  * Finding and starting the official Claude Code CLI.
@@ -578,10 +934,18 @@ function withProgramDirOnPath(env, program, platform = process.platform) {
578
934
  */
579
935
  /** The npm package the official CLI ships as. */
580
936
  const CLAUDE_NPM_PACKAGE = "@anthropic-ai/claude-code";
937
+ /** The oldest CLI that takes every option the transport passes (`--permission-prompts` arrived in 2.1.259). */
938
+ const CLAUDE_MIN_VERSION = "2.1.259";
939
+ /**
940
+ * Whether a detected CLI is older than the transport needs. An unknown
941
+ * version is not judged; a CLI that then refuses an option still says so.
942
+ * @param version - the version `claude --version` printed.
943
+ */
944
+ const isClaudeCliOutdated = (version) => compareSemver(version, CLAUDE_MIN_VERSION) === -1;
581
945
  /** Isolated CLI home; the plugin owns the endpoint and key it runs with. */
582
946
  const DEFAULT_CLAUDE_CONFIG_DIR = join(homedir(), ".dsh", "packycode-claude-cli");
583
947
  /** What a request reports when no CLI could be started at all. */
584
- const CLAUDE_MISSING_MESSAGE = "本机没有可用的 Claude Code CLI,而当前 Key 的 Claude 分组只接受官方 CLI 发出的请求。请在 设置 → PackyCode → Claude Code CLI 安装,或在终端运行 npm install -g @anthropic-ai/claude-code,然后重发。 / Claude Code CLI is not installed on this device, and this key's Claude group only serves the official CLI. Install it under Settings → PackyCode, or run: npm install -g @anthropic-ai/claude-code";
948
+ const CLAUDE_MISSING_MESSAGE = "本机没有可用的 Claude Code CLI,而当前 Key 的 Claude 分组只接受官方 CLI 发出的请求。请在 设置 → PackyCode → Claude Code CLI 安装,或在终端运行 npm install -g @anthropic-ai/claude-code --allow-scripts=@anthropic-ai/claude-code,然后重发。 / Claude Code CLI is not installed on this device, and this key's Claude group only serves the official CLI. Install it under Settings → PackyCode, or run: npm install -g @anthropic-ai/claude-code --allow-scripts=@anthropic-ai/claude-code";
585
949
  /** The CLI's file names on PATH: the native installer ships an `.exe`, npm a `.cmd` launcher. */
586
950
  const claudeFileNames = (platform) => platform === "win32" ? ["claude.exe", "claude.cmd"] : ["claude"];
587
951
  /**
@@ -683,23 +1047,40 @@ function resolveSpawnCommand(binary, { platform = process.platform, exists = exi
683
1047
  //#endregion
684
1048
  //#region src/claude-install.js
685
1049
  /**
686
- * Detecting, and installing, the local Claude Code CLI.
1050
+ * Detecting, installing and updating the local Claude Code CLI.
687
1051
  *
688
1052
  * Claude requests on a key whose group serves only the official client are
689
1053
  * sent by this device's Claude Code CLI, so the settings panel shows whether
690
1054
  * one is installed (and which version), and offers to install it with
691
1055
  * `npm install -g @anthropic-ai/claude-code` — the same command on macOS and
692
- * Windows. Installation runs on the host, never in a shell built from user
693
- * text: npm's arguments are constants.
1056
+ * Windows — or to update one older than the transport needs. Both run on the
1057
+ * host, never in a shell built from user text: their arguments are constants.
1058
+ *
1059
+ * npm 12 skips install scripts unless they are allowed, and the CLI's
1060
+ * postinstall is what puts its native binary in place, so every npm install
1061
+ * allows the package's scripts (npm 10 and 11 accept the option as well).
694
1062
  *
695
1063
  * @module dsh-packycode-api/claude-install
696
1064
  */
697
- const CLAUDE_INSTALL_COMMAND = `npm install -g ${CLAUDE_NPM_PACKAGE}`;
1065
+ const ALLOW_SCRIPTS = `--allow-scripts=${CLAUDE_NPM_PACKAGE}`;
1066
+ const CLAUDE_INSTALL_COMMAND = `npm install -g ${CLAUDE_NPM_PACKAGE} ${ALLOW_SCRIPTS}`;
1067
+ /** How an npm install of the CLI is updated. */
1068
+ const CLAUDE_NPM_UPDATE_COMMAND = `npm install -g ${CLAUDE_NPM_PACKAGE}@latest ${ALLOW_SCRIPTS}`;
1069
+ /** How any other install (the native installer's, Homebrew's) is updated: by the CLI itself. */
1070
+ const CLAUDE_SELF_UPDATE_COMMAND = "claude update";
698
1071
  const INSTALL_ARGS = Object.freeze([
699
1072
  "install",
700
1073
  "-g",
701
- CLAUDE_NPM_PACKAGE
1074
+ CLAUDE_NPM_PACKAGE,
1075
+ ALLOW_SCRIPTS
702
1076
  ]);
1077
+ const NPM_UPDATE_ARGS = Object.freeze([
1078
+ "install",
1079
+ "-g",
1080
+ `${CLAUDE_NPM_PACKAGE}@latest`,
1081
+ ALLOW_SCRIPTS
1082
+ ]);
1083
+ const SELF_UPDATE_ARGS = Object.freeze(["update"]);
703
1084
  const PREFIX_ARGS = Object.freeze(["prefix", "-g"]);
704
1085
  const VERSION_TIMEOUT_MS = 2e4;
705
1086
  const PREFIX_TIMEOUT_MS = 2e4;
@@ -768,6 +1149,28 @@ function locateNpm({ platform = process.platform, home = homedir(), env = proces
768
1149
  }).find((candidate) => exists(candidate));
769
1150
  }
770
1151
  /**
1152
+ * Whether a located CLI is npm's global package. npm's own update must then
1153
+ * be used: the CLI's `claude update` runs npm without allowing the package's
1154
+ * scripts, which on npm 12 reports success and leaves no native binary.
1155
+ * @param binary - the located CLI.
1156
+ * @param options - platform and a realpath seam.
1157
+ */
1158
+ function isNpmClaudeInstall(binary, { platform = process.platform, realpath = realpathSync } = {}) {
1159
+ if (typeof binary !== "string" || binary === "") return false;
1160
+ if (platform === "win32" && /\.(?:cmd|bat)$/iu.test(binary)) return true;
1161
+ let resolved = binary;
1162
+ try {
1163
+ resolved = realpath(binary);
1164
+ } catch {}
1165
+ return /[\\/]node_modules[\\/]@anthropic-ai[\\/]claude-code[\\/]/u.test(resolved);
1166
+ }
1167
+ /**
1168
+ * The parent environment without the user's own Claude settings: a
1169
+ * `DISABLE_AUTOUPDATER`, config directory or key there must not steer the
1170
+ * plugin's update.
1171
+ */
1172
+ const withoutClaudeSettings = (env) => Object.fromEntries(Object.entries(env ?? {}).filter(([name, value]) => value !== void 0 && !/^(?:ANTHROPIC_|CLAUDE_|CLAUDECODE|DISABLE_)/iu.test(name)));
1173
+ /**
771
1174
  * Classify an npm failure so the panel can say what to do about it.
772
1175
  * @param result - the captured npm run.
773
1176
  * @returns `{ code, detail }`.
@@ -864,7 +1267,7 @@ function runCaptured(spawnImpl, command, args, options, timeoutMs) {
864
1267
  * applies without a restart), plus platform, environment, and process seams.
865
1268
  * @returns `{ binary, detect, install, status }`.
866
1269
  */
867
- function createClaudeCliManager({ explicitPath = () => "", platform = process.platform, home = homedir(), env = process.env, execPath = process.execPath, configDir = DEFAULT_CLAUDE_CONFIG_DIR, spawnImpl = spawn, exists = existsSync, readFile = (file) => readFileSync(file, "utf8"), readdir, now = () => Date.now() } = {}) {
1270
+ function createClaudeCliManager({ explicitPath = () => "", platform = process.platform, home = homedir(), env = process.env, execPath = process.execPath, configDir = DEFAULT_CLAUDE_CONFIG_DIR, spawnImpl = spawn, exists = existsSync, readFile = (file) => readFileSync(file, "utf8"), realpath = realpathSync, readdir, now = () => Date.now() } = {}) {
868
1271
  const path = pathFor(platform);
869
1272
  const configured = () => {
870
1273
  const value = explicitPath();
@@ -968,6 +1371,7 @@ function createClaudeCliManager({ explicitPath = () => "", platform = process.pl
968
1371
  });
969
1372
  return detecting;
970
1373
  };
1374
+ /** The install or update in flight or last finished; `action` says which. */
971
1375
  let installation = { state: "idle" };
972
1376
  let installing;
973
1377
  const install = () => {
@@ -976,6 +1380,7 @@ function createClaudeCliManager({ explicitPath = () => "", platform = process.pl
976
1380
  if (program === void 0) {
977
1381
  installation = {
978
1382
  state: "failed",
1383
+ action: "install",
979
1384
  code: "npm-missing",
980
1385
  finishedAt: now()
981
1386
  };
@@ -983,6 +1388,7 @@ function createClaudeCliManager({ explicitPath = () => "", platform = process.pl
983
1388
  }
984
1389
  installation = {
985
1390
  state: "running",
1391
+ action: "install",
986
1392
  startedAt: now(),
987
1393
  npm: program
988
1394
  };
@@ -991,6 +1397,7 @@ function createClaudeCliManager({ explicitPath = () => "", platform = process.pl
991
1397
  if (result.code !== 0) {
992
1398
  installation = {
993
1399
  state: "failed",
1400
+ action: "install",
994
1401
  ...explainNpmFailure(result),
995
1402
  finishedAt: now()
996
1403
  };
@@ -1000,9 +1407,11 @@ function createClaudeCliManager({ explicitPath = () => "", platform = process.pl
1000
1407
  const found = await detect({ force: true });
1001
1408
  installation = found.installed && found.version !== void 0 ? {
1002
1409
  state: "succeeded",
1410
+ action: "install",
1003
1411
  finishedAt: now()
1004
1412
  } : {
1005
1413
  state: "failed",
1414
+ action: "install",
1006
1415
  code: "not-found-after-install",
1007
1416
  detail: detailOf(result.stdout, result.stderr),
1008
1417
  finishedAt: now()
@@ -1010,6 +1419,112 @@ function createClaudeCliManager({ explicitPath = () => "", platform = process.pl
1010
1419
  }).catch((error) => {
1011
1420
  installation = {
1012
1421
  state: "failed",
1422
+ action: "install",
1423
+ code: "failed",
1424
+ detail: error instanceof Error ? error.message : String(error),
1425
+ finishedAt: now()
1426
+ };
1427
+ }).finally(() => {
1428
+ installing = void 0;
1429
+ });
1430
+ return installation;
1431
+ };
1432
+ /** npm for an npm install of the CLI: the one beside it shares its global prefix. */
1433
+ const npmBeside = (binary) => {
1434
+ const beside = path.join(path.dirname(binary), platform === "win32" ? "npm.cmd" : "npm");
1435
+ return exists(beside) ? beside : npm();
1436
+ };
1437
+ /**
1438
+ * Update the located CLI the way it was installed: npm's global package
1439
+ * through npm, anything else through `claude update` with the plugin's
1440
+ * own CLI home. Success means the CLI now detected is new enough.
1441
+ */
1442
+ const runUpdate = async () => {
1443
+ const before = await detect({ force: true });
1444
+ if (!before.installed || typeof before.path !== "string") {
1445
+ installation = {
1446
+ state: "failed",
1447
+ action: "update",
1448
+ code: "not-installed",
1449
+ finishedAt: now()
1450
+ };
1451
+ return;
1452
+ }
1453
+ let result;
1454
+ if (isNpmClaudeInstall(before.path, {
1455
+ platform,
1456
+ realpath
1457
+ })) {
1458
+ const program = npmBeside(before.path);
1459
+ if (program === void 0) {
1460
+ installation = {
1461
+ state: "failed",
1462
+ action: "update",
1463
+ code: "npm-missing",
1464
+ finishedAt: now()
1465
+ };
1466
+ return;
1467
+ }
1468
+ installation = {
1469
+ ...installation,
1470
+ npm: program
1471
+ };
1472
+ result = await runNpm(program, NPM_UPDATE_ARGS, INSTALL_TIMEOUT_MS);
1473
+ globalBin = void 0;
1474
+ } else {
1475
+ const command = resolveSpawnCommand(before.path, {
1476
+ platform,
1477
+ exists,
1478
+ readFile
1479
+ });
1480
+ result = await runCaptured(spawnImpl, command.command, [...command.args, ...SELF_UPDATE_ARGS], { env: {
1481
+ ...withProgramDirOnPath(withoutClaudeSettings(env), before.path, platform),
1482
+ CLAUDE_CONFIG_DIR: configDir,
1483
+ DISABLE_TELEMETRY: "1",
1484
+ DISABLE_ERROR_REPORTING: "1"
1485
+ } }, INSTALL_TIMEOUT_MS);
1486
+ }
1487
+ const after = await detect({ force: true });
1488
+ const from = before.version === void 0 ? {} : { from: before.version };
1489
+ if (after.version !== void 0 && !isClaudeCliOutdated(after.version)) installation = {
1490
+ state: "succeeded",
1491
+ action: "update",
1492
+ ...from,
1493
+ to: after.version,
1494
+ finishedAt: now()
1495
+ };
1496
+ else if (result.code !== 0) installation = {
1497
+ state: "failed",
1498
+ action: "update",
1499
+ ...explainNpmFailure(result),
1500
+ finishedAt: now()
1501
+ };
1502
+ else if (after.version === void 0) installation = {
1503
+ state: "failed",
1504
+ action: "update",
1505
+ code: "broken-after-update",
1506
+ detail: detailOf(after.error, result.stdout, result.stderr),
1507
+ finishedAt: now()
1508
+ };
1509
+ else installation = {
1510
+ state: "failed",
1511
+ action: "update",
1512
+ code: "still-outdated",
1513
+ detail: detailOf(result.stdout, result.stderr),
1514
+ finishedAt: now()
1515
+ };
1516
+ };
1517
+ const update = () => {
1518
+ if (installing !== void 0) return installation;
1519
+ installation = {
1520
+ state: "running",
1521
+ action: "update",
1522
+ startedAt: now()
1523
+ };
1524
+ installing = runUpdate().catch((error) => {
1525
+ installation = {
1526
+ state: "failed",
1527
+ action: "update",
1013
1528
  code: "failed",
1014
1529
  detail: error instanceof Error ? error.message : String(error),
1015
1530
  finishedAt: now()
@@ -1033,17 +1548,24 @@ function createClaudeCliManager({ explicitPath = () => "", platform = process.pl
1033
1548
  /** The last detection, without probing again; undefined before the first. */
1034
1549
  lastDetection: () => detection === void 0 ? void 0 : { ...detection },
1035
1550
  install,
1036
- /** Wait for an install in flight; for tests and scripts. */
1551
+ update,
1552
+ /** Wait for an install or update in flight; for tests and scripts. */
1037
1553
  settled: () => installing ?? Promise.resolve(),
1038
1554
  /**
1039
- * What the settings panel shows. While an install runs, the last detection
1040
- * stands instead of probing a half-written package.
1555
+ * What the settings panel shows. While an install or update runs, the
1556
+ * last detection stands instead of probing a half-written package.
1041
1557
  */
1042
1558
  async status({ force = false } = {}) {
1043
1559
  const value = installing === void 0 ? await detect({ force }) : detection;
1044
1560
  return {
1045
1561
  installed: value?.installed === true,
1046
1562
  ...value?.version === void 0 ? {} : { version: value.version },
1563
+ minVersion: CLAUDE_MIN_VERSION,
1564
+ outdated: isClaudeCliOutdated(value?.version),
1565
+ ...value?.path === void 0 ? {} : { updateCommand: isNpmClaudeInstall(value.path, {
1566
+ platform,
1567
+ realpath
1568
+ }) ? CLAUDE_NPM_UPDATE_COMMAND : CLAUDE_SELF_UPDATE_COMMAND },
1047
1569
  ...value?.path === void 0 ? {} : { path: value.path },
1048
1570
  ...value?.configuredPath === void 0 ? {} : { configuredPath: value.configuredPath },
1049
1571
  ...value?.error === void 0 ? {} : { error: value.error },
@@ -1434,7 +1956,7 @@ const classifyCliOutdated = (message) => typeof message === "string" && message.
1434
1956
  */
1435
1957
  function explainCliUnavailable(message) {
1436
1958
  const detail = relayedMessage(message).replace(/^[\s\S]*?dsh_cli_unavailable:?\s*/u, "").replace(/\b\d{3}\b/gu, "").replace(/\b(?:network|connection|socket|fetch|timed?\s*out|timeout|terminated)\b/giu, "…").trim().slice(0, 300);
1437
- return "本机没有可用的 Claude Code CLI,而当前 Key 所在的 Claude 分组(cc)只接受官方 Claude Code 客户端。请在 设置 → PackyCode → Claude Code CLI 一键安装,或在终端执行 npm install -g @anthropic-ai/claude-code,然后重发这条消息。也可以改用 cc-sale / cc-expensive 分组的 Key,它们走插件内置的 HTTP 通道。" + (detail === "" || detail === "dsh_cli_unavailable" ? "" : `(启动失败:${detail})`) + " / Claude Code CLI is not available, and this key's Claude group only serves the official client. Install it under Settings → PackyCode (npm install -g @anthropic-ai/claude-code), then resend.";
1959
+ return "本机没有可用的 Claude Code CLI,而当前 Key 所在的 Claude 分组(cc)只接受官方 Claude Code 客户端。请在 设置 → PackyCode → Claude Code CLI 一键安装,或在终端执行 npm install -g @anthropic-ai/claude-code --allow-scripts=@anthropic-ai/claude-code,然后重发这条消息。也可以改用 cc-sale / cc-expensive 分组的 Key,它们走插件内置的 HTTP 通道。" + (detail === "" || detail === "dsh_cli_unavailable" ? "" : `(启动失败:${detail})`) + " / Claude Code CLI is not available, and this key's Claude group only serves the official client. Install it under Settings → PackyCode (npm install -g @anthropic-ai/claude-code --allow-scripts=@anthropic-ai/claude-code), then resend.";
1438
1960
  }
1439
1961
  /**
1440
1962
  * Explain a CLI that is too old for the options this plugin passes.
@@ -1442,7 +1964,7 @@ function explainCliUnavailable(message) {
1442
1964
  * @returns the replacement sentence.
1443
1965
  */
1444
1966
  function explainCliOutdated(message) {
1445
- return `本机的 Claude Code CLI 版本过旧,不支持插件所需的参数。请在终端执行 claude update,或 npm install -g @anthropic-ai/claude-code@latest 更新后重发。 / The installed Claude Code CLI is too old for this plugin; update it (claude update). CLI said: ${relayedMessage(message).replace(/^[\s\S]*?dsh_cli_outdated:?\s*/u, "").replace(/\b\d{3}\b/gu, "").slice(0, 300)}`;
1967
+ return `本机的 Claude Code CLI 版本过旧,插件需要 ${CLAUDE_MIN_VERSION} 或更新版本。请在 设置 → PackyCode → Claude Code CLI 点「更新 Claude Code CLI」,或在终端执行 claude update,然后重发这条消息。 / The installed Claude Code CLI is too old for this plugin (needs ${CLAUDE_MIN_VERSION} or later). Update it under Settings → PackyCode, or run claude update, then resend. CLI said: ${relayedMessage(message).replace(/^[\s\S]*?dsh_cli_outdated:?\s*/u, "").replace(/(?<![\d.])\d{3}(?!\d|\.\d)/gu, "").slice(0, 300)}`;
1446
1968
  }
1447
1969
  /**
1448
1970
  * Explain a client-policy refusal of a request the official CLI itself sent.
@@ -2300,7 +2822,9 @@ function baseUrlOf(input) {
2300
2822
  /**
2301
2823
  * Create the transport.
2302
2824
  * @param options - how to find the CLI binary (`binary()` resolves a path or
2303
- * throws), the isolated CLI home, the version recorded in transcripts,
2825
+ * throws), the isolated CLI home, the detected CLI version (read once the
2826
+ * binary is resolved: an outdated one is refused, and it is recorded in
2827
+ * transcripts),
2304
2828
  * limits, a logger, and process seams for tests.
2305
2829
  * @returns `{ fetch(input, init, meta), close(), stats() }`.
2306
2830
  */
@@ -2346,24 +2870,29 @@ function createClaudeCliTransport({ binary, configDir = DEFAULT_CLAUDE_CONFIG_DI
2346
2870
  await mkdir(fallback, { recursive: true });
2347
2871
  return fallback;
2348
2872
  };
2349
- const open = async (request, meta, { oneShot }) => startClaudeCliSession({
2350
- binary,
2351
- configDir,
2352
- runDirectory: join(configDir, "runs", runDirectoryName()),
2353
- baseUrl: meta.baseUrl,
2354
- apiKey: meta.apiKey,
2355
- request,
2356
- conversation: splitConversation(request.messages),
2357
- cwd: await workingDirectory(meta.cwd),
2358
- sessionId: oneShot ? randomUUID() : meta.sessionId,
2359
- cliVersion: version(),
2360
- oneShot,
2361
- logger,
2362
- ...spawnImpl === void 0 ? {} : { spawnImpl },
2363
- ...platform === void 0 ? {} : { platform },
2364
- ...parentEnv === void 0 ? {} : { parentEnv },
2365
- ...startupTimeoutMs === void 0 ? {} : { startupTimeoutMs }
2366
- });
2873
+ const open = async (request, meta, { oneShot }) => {
2874
+ const program = await binary();
2875
+ const cliVersion = version();
2876
+ if (isClaudeCliOutdated(cliVersion)) throw Object.assign(/* @__PURE__ */ new Error(`Claude Code ${cliVersion} is older than ${CLAUDE_MIN_VERSION}`), { code: "CLAUDE_CLI_OUTDATED" });
2877
+ return startClaudeCliSession({
2878
+ binary: async () => program,
2879
+ configDir,
2880
+ runDirectory: join(configDir, "runs", runDirectoryName()),
2881
+ baseUrl: meta.baseUrl,
2882
+ apiKey: meta.apiKey,
2883
+ request,
2884
+ conversation: splitConversation(request.messages),
2885
+ cwd: await workingDirectory(meta.cwd),
2886
+ sessionId: oneShot ? randomUUID() : meta.sessionId,
2887
+ cliVersion,
2888
+ oneShot,
2889
+ logger,
2890
+ ...spawnImpl === void 0 ? {} : { spawnImpl },
2891
+ ...platform === void 0 ? {} : { platform },
2892
+ ...parentEnv === void 0 ? {} : { parentEnv },
2893
+ ...startupTimeoutMs === void 0 ? {} : { startupTimeoutMs }
2894
+ });
2895
+ };
2367
2896
  /**
2368
2897
  * Send one Messages request through the CLI.
2369
2898
  * @param input - the request URL the native client targeted.
@@ -2419,6 +2948,10 @@ function createClaudeCliTransport({ binary, configDir = DEFAULT_CLAUDE_CONFIG_DI
2419
2948
  return await session.respond({ kind: "start" }, init?.signal);
2420
2949
  } catch (error) {
2421
2950
  if (init?.signal?.aborted) throw error;
2951
+ if (error?.code === "CLAUDE_CLI_OUTDATED") {
2952
+ logger?.warn?.("packycode-api: %s", error.message);
2953
+ return errorResponse(400, `${CLI_OUTDATED}: ${error.message}`, CLI_OUTDATED);
2954
+ }
2422
2955
  const reason = error?.code === "CLAUDE_CLI_MISSING" ? "" : `: ${String(error?.message ?? error).slice(0, 300)}`;
2423
2956
  logger?.warn?.("packycode-api: Claude Code CLI could not start%s", reason);
2424
2957
  return errorResponse(400, `${CLI_UNAVAILABLE}${reason}`, CLI_UNAVAILABLE);
@@ -2452,8 +2985,9 @@ function createClaudeCliTransport({ binary, configDir = DEFAULT_CLAUDE_CONFIG_DI
2452
2985
  * PackyCode's `cc` group ("claude code专用") serves only the official client,
2453
2986
  * so a key in it sends its Claude requests through the real CLI.
2454
2987
  * `cc-expensive`, `cc-sale` and `claude-officially` keep the native HTTP
2455
- * route. The user can pin either transport in the settings panel.
2456
- * (`claude-sale` is delisted for now; it joins the CLI list if it returns.)
2988
+ * route. The groups alone decide: a cc key cannot be sent over HTTP, where
2989
+ * the gateway refuses it as a third-party client, and no other key needs the
2990
+ * CLI. (`claude-sale` is delisted for now; it joins the CLI list if it returns.)
2457
2991
  *
2458
2992
  * Only the transport changes: DSH's agent loop, tools, permissions, plan mode,
2459
2993
  * attachments and history are identical on both, and no other provider or
@@ -2469,33 +3003,17 @@ const HTTP_CLAUDE_GROUPS = Object.freeze([
2469
3003
  "cc-sale",
2470
3004
  "claude-officially"
2471
3005
  ]);
2472
- /** `auto` follows the key's groups; `cli` and `http` pin one transport. */
2473
- const CLAUDE_TRANSPORTS = Object.freeze([
2474
- "auto",
2475
- "cli",
2476
- "http"
2477
- ]);
2478
- const DEFAULT_CLAUDE_TRANSPORT = "auto";
2479
3006
  /** Whether a catalog model is served over the Anthropic Messages protocol. */
2480
3007
  const isClaudeModel = (modelId) => typeof modelId === "string" && modelId.startsWith("claude-");
2481
- /** Read the stored preference; anything unknown is the default. */
2482
- function readClaudeTransport(value) {
2483
- return CLAUDE_TRANSPORTS.includes(value) ? value : DEFAULT_CLAUDE_TRANSPORT;
2484
- }
2485
3008
  /**
2486
3009
  * Decide the transport for a Claude request.
2487
- * @param options - the stored preference and the request key's groups, as the
2488
- * last group reading saw them (undefined when there is no reading yet).
3010
+ * @param options - the request key's groups, as the last group reading saw
3011
+ * them (undefined when there is no reading yet).
2489
3012
  * @returns `{ transport: 'cli' | 'http', reason, groups? }`. `reason` is
2490
- * `preference`, `group` (the key's groups decided), or `unknown` (no reading
2491
- * yet; the native HTTP route stands, as it did before this choice existed).
3013
+ * `group` (the key's groups decided) or `unknown` (no reading yet; the
3014
+ * native HTTP route stands, as it did before the CLI route existed).
2492
3015
  */
2493
- function claudeTransportFor({ preference, groups } = {}) {
2494
- const pinned = readClaudeTransport(preference);
2495
- if (pinned !== "auto") return {
2496
- transport: pinned,
2497
- reason: "preference"
2498
- };
3016
+ function claudeTransportFor({ groups } = {}) {
2499
3017
  if (!Array.isArray(groups)) return {
2500
3018
  transport: "http",
2501
3019
  reason: "unknown"
@@ -3365,8 +3883,7 @@ function validateImageOptions({ model = DEFAULT_IMAGE_MODEL, quality = "auto", s
3365
3883
  * plugin's own row, read through a live reference, written through
3366
3884
  * `settings.update(rowId, patch)`, and announced by `loader/volatile-update`.
3367
3885
  * Only volatile fields are writable there, so the host overrides stay
3368
- * read-only while web search, model visibility, images and the Claude
3369
- * transport are editable.
3886
+ * read-only while web search, model visibility and images are editable.
3370
3887
  *
3371
3888
  * @module dsh-packycode-api/preferences
3372
3889
  */
@@ -3403,7 +3920,7 @@ function unwritable(message) {
3403
3920
  * current source and how to replace it, what to do when the section changes,
3404
3921
  * and the preference targets this fills in (host overrides, search, models).
3405
3922
  */
3406
- function connectSettings(settingsCtx, { ctx, Config, namespace, source, setSource, onChange, hostPreference, searchPreference, modelPreference, imagePreference, claudePreference }) {
3923
+ function connectSettings(settingsCtx, { ctx, Config, namespace, source, setSource, onChange, hostPreference, searchPreference, modelPreference, imagePreference }) {
3407
3924
  const settings = settingsCtx.settings;
3408
3925
  if (typeof settings?.installSection === "function") {
3409
3926
  try {
@@ -3421,7 +3938,6 @@ function connectSettings(settingsCtx, { ctx, Config, namespace, source, setSourc
3421
3938
  Object.assign(searchPreference, target);
3422
3939
  Object.assign(modelPreference, target);
3423
3940
  if (imagePreference !== void 0) Object.assign(imagePreference, target);
3424
- if (claudePreference !== void 0) Object.assign(claudePreference, target);
3425
3941
  }
3426
3942
  } catch (error) {
3427
3943
  ctx.logger?.warn?.("packycode-api: host overrides unavailable, defaults stand: %s", error.message);
@@ -3437,7 +3953,6 @@ function connectSettings(settingsCtx, { ctx, Config, namespace, source, setSourc
3437
3953
  Object.assign(searchPreference, target);
3438
3954
  Object.assign(modelPreference, target);
3439
3955
  if (imagePreference !== void 0) Object.assign(imagePreference, target);
3440
- if (claudePreference !== void 0) Object.assign(claudePreference, target);
3441
3956
  if (typeof settings.configure === "function") {
3442
3957
  const scope = typeof settingsCtx.effect === "function" ? settingsCtx : ctx;
3443
3958
  try {
@@ -4519,435 +5034,79 @@ var PackyCodeKeyRing = class {
4519
5034
  label: candidate.label,
4520
5035
  key: candidate.key,
4521
5036
  addedAt: candidate.addedAt
4522
- } : candidate)
4523
- };
4524
- }));
4525
- }
4526
- /**
4527
- * Mark a key unusable and move the active pointer on. Called from the stream
4528
- * guard, so the retry of the failed step resolves the next key.
4529
- */
4530
- async disable(id, reason) {
4531
- return projection(await this.#write((state) => {
4532
- const now = Date.now();
4533
- const keys = state.keys.map((candidate) => candidate.id === id ? {
4534
- ...candidate,
4535
- disabledAt: now,
4536
- disabledReason: text$1(reason) === "" ? "unusable" : text$1(reason)
4537
- } : candidate);
4538
- if (!keys.some((candidate) => candidate.id === id)) return {
4539
- activeId: state.activeId,
4540
- keys: state.keys
4541
- };
4542
- return {
4543
- activeId: state.activeId === id ? (keys.find((candidate) => candidate.id !== id && candidate.disabledAt === void 0) ?? keys.find((candidate) => candidate.id !== id))?.id : state.activeId,
4544
- keys
4545
- };
4546
- }));
4547
- }
4548
- };
4549
- //#endregion
4550
- //#region src/model-selection.js
4551
- /** Profile preferences for the models advertised in PackyCode's picker. */
4552
- /** Store exclusions so new catalog models are selected by default as well. */
4553
- function readHiddenModels(preference) {
4554
- const value = fieldValue(preference?.hiddenModels);
4555
- if (!Array.isArray(value)) return [];
4556
- const hidden = new Set(value);
4557
- return PACKYCODE_MODEL_IDS.filter((id) => hidden.has(id));
4558
- }
4559
- function createModelSelectionController({ readPreference, writePreference, isWritable, onChange }) {
4560
- const hidden = () => readHiddenModels(readPreference());
4561
- let advertised = hidden().join(",");
4562
- const reconcile = () => {
4563
- const next = hidden().join(",");
4564
- if (next === advertised) return;
4565
- onChange?.();
4566
- advertised = next;
4567
- };
4568
- const status = () => {
4569
- const excluded = new Set(hidden());
4570
- return {
4571
- writable: isWritable() === true,
4572
- models: PACKYCODE_MODELS.map(({ id, name }) => ({
4573
- id,
4574
- name,
4575
- selected: !excluded.has(id)
4576
- }))
4577
- };
4578
- };
4579
- return Object.freeze({
4580
- status,
4581
- reconcile,
4582
- isVisible: (id) => !hidden().includes(id),
4583
- async update({ selectedIds } = {}) {
4584
- if (!Array.isArray(selectedIds) || selectedIds.some((id) => !PACKYCODE_MODEL_IDS.includes(id))) throw new Error("That PackyCode model selection is not supported");
4585
- if (isWritable() !== true) throw new Error("PackyCode settings are not writable in this profile");
4586
- const selected = new Set(selectedIds);
4587
- try {
4588
- await writePreference({ hiddenModels: PACKYCODE_MODEL_IDS.filter((id) => !selected.has(id)) });
4589
- } catch {
4590
- throw new Error("PackyCode could not save the model selection");
4591
- }
4592
- reconcile();
4593
- return status();
4594
- }
4595
- });
4596
- }
4597
- //#endregion
4598
- //#region src/plugin-version.js
4599
- /**
4600
- * Version self-knowledge for the settings panel.
4601
- *
4602
- * The running version comes from this package. The latest version comes from
4603
- * the npm registry. An in-place update is only offered for a registry install,
4604
- * and it installs that exact version with the host's own pnpm — never through
4605
- * `dsh plugin --profile desktop`, which the desktop app refuses. The install
4606
- * runs under the profile's own `minimumReleaseAge`: a version installed past
4607
- * it would sit in the profile's lockfile, and pnpm then refuses every later
4608
- * plugin install in that profile until the version is old enough. A version
4609
- * pnpm still holds back is reported as pending, with when it clears. Registry
4610
- * bodies and package-manager output stay host-side.
4611
- *
4612
- * @module dsh-packycode-api/plugin-version
4613
- */
4614
- const PACKAGE_NAME = "dsh-packycode-api";
4615
- const NPM_REGISTRY_LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
4616
- const DEFAULT_VERSION_TTL_MS = 5 * 6e4;
4617
- const DEFAULT_VERSION_TIMEOUT_MS = 1e4;
4618
- const DEFAULT_UPDATE_TIMEOUT_MS = 18e4;
4619
- const RELEASE_AGE_ERROR = /ERR_PNPM_(?:NO_MATURE_MATCHING_VERSION|MINIMUM_RELEASE_AGE_VIOLATION)/u;
4620
- const RELEASE_AGE_ENTRY = /(\S+@\S+) was published at (\S+?), within the minimumReleaseAge cutoff \((\S+?)\)/gu;
4621
- /**
4622
- * Recognize pnpm refusing a version that is younger than its
4623
- * `minimumReleaseAge`, whether the version was requested or already sits in
4624
- * the lockfile. Each refused entry reads `<name>@<version> was published at
4625
- * <time>, within the minimumReleaseAge cutoff (<time>)`.
4626
- * @param output - the install's stdout and stderr.
4627
- * @param finishedAt - when the install ended; pnpm drew its cutoff then.
4628
- * @returns undefined when this is not a release-age refusal; otherwise
4629
- * `{ availableAt }`, when the youngest refused entry clears, if pnpm said.
4630
- */
4631
- function releaseAgeRefusal(output, finishedAt) {
4632
- const text = String(output ?? "").replace(/\u001b\[[0-9;]*m/gu, "");
4633
- if (!RELEASE_AGE_ERROR.test(text)) return void 0;
4634
- let availableAt;
4635
- for (const [, , published, cutoff] of text.matchAll(RELEASE_AGE_ENTRY)) {
4636
- const clears = Date.parse(published) + (finishedAt - Date.parse(cutoff));
4637
- if (Number.isFinite(clears) && (availableAt === void 0 || clears > availableAt)) availableAt = clears;
4638
- }
4639
- return availableAt === void 0 ? {} : { availableAt };
4640
- }
4641
- /** Parse a strict `x.y.z[-pre][+build]` version; anything else is undefined. */
4642
- function parseSemver(value) {
4643
- if (typeof value !== "string") return void 0;
4644
- const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value.trim());
4645
- if (match === null) return void 0;
4646
- return {
4647
- major: Number(match[1]),
4648
- minor: Number(match[2]),
4649
- patch: Number(match[3]),
4650
- pre: match[4]
4651
- };
4652
- }
4653
- /** Compare two versions: -1/0/1, or undefined when either side is not semver. */
4654
- function compareSemver(a, b) {
4655
- const left = parseSemver(a);
4656
- const right = parseSemver(b);
4657
- if (left === void 0 || right === void 0) return void 0;
4658
- for (const key of [
4659
- "major",
4660
- "minor",
4661
- "patch"
4662
- ]) if (left[key] !== right[key]) return left[key] < right[key] ? -1 : 1;
4663
- if (left.pre === right.pre) return 0;
4664
- if (left.pre === void 0) return 1;
4665
- if (right.pre === void 0) return -1;
4666
- return left.pre < right.pre ? -1 : 1;
4667
- }
4668
- /**
4669
- * Classify how the running package reached the profile. Only registry specs
4670
- * may be updated in place; local checkouts belong to the owner's iteration.
4671
- */
4672
- function classifySpec(spec) {
4673
- if (typeof spec !== "string" || spec === "") return "unknown";
4674
- if (/^(?:link|file):/u.test(spec)) return "link";
4675
- if (/^(?:\d|[~^*])/u.test(spec)) return "npm";
4676
- return "unknown";
4677
- }
4678
- const readManifest = async (path) => {
4679
- try {
4680
- const parsed = JSON.parse(await readFile(path, "utf8"));
4681
- return parsed !== null && typeof parsed === "object" ? parsed : void 0;
4682
- } catch {
4683
- return;
4684
- }
4685
- };
4686
- /**
4687
- * A zero exit only proves the command ran. pnpm can skip a too-new version
4688
- * and still exit zero, so the landed manifest is what makes the answer true.
4689
- */
4690
- async function confirmInstalled({ profileDir, expected, settleDelayMs, attempts = 3 }) {
4691
- for (let attempt = 1; attempt <= attempts; attempt += 1) {
4692
- if ((await readManifest(join(profileDir, "node_modules", "dsh-packycode-api", "package.json")))?.version === expected) return;
4693
- if (attempt < attempts) await new Promise((resolve) => {
4694
- setTimeout(resolve, settleDelayMs);
4695
- });
4696
- }
4697
- throw new Error("PackyCode plugin update did not install the requested version");
4698
- }
4699
- /**
4700
- * Find the profile that owns this running installation. pnpm symlinks resolve
4701
- * to the same realpath on both sides. When no realpath matches, fall back to
4702
- * the only profile that declares the dependency at all.
4703
- */
4704
- async function findInstall({ dshHome, ownPackageJsonUrl }) {
4705
- let ownReal;
4706
- try {
4707
- ownReal = await realpath(fileURLToPath(ownPackageJsonUrl));
4708
- } catch {
4709
- ownReal = void 0;
4710
- }
4711
- let entries = [];
4712
- try {
4713
- entries = await readdir(join(dshHome, "profiles"), { withFileTypes: true });
4714
- } catch {
4715
- entries = [];
4716
- }
4717
- const declared = [];
4718
- for (const entry of entries) {
4719
- if (!entry.isDirectory()) continue;
4720
- const profileDir = join(dshHome, "profiles", entry.name);
4721
- const spec = (await readManifest(join(profileDir, "package.json")))?.dependencies?.[PACKAGE_NAME];
4722
- if (typeof spec !== "string") continue;
4723
- declared.push({
4724
- profile: entry.name,
4725
- spec
4726
- });
4727
- if (ownReal === void 0) continue;
4728
- try {
4729
- if (await realpath(join(profileDir, "node_modules", "dsh-packycode-api", "package.json")) === ownReal) return {
4730
- kind: classifySpec(spec),
4731
- profile: entry.name
4732
- };
4733
- } catch {}
4734
- }
4735
- if (declared.length === 1) return {
4736
- kind: classifySpec(declared[0].spec),
4737
- profile: declared[0].profile
4738
- };
4739
- return { kind: "unknown" };
4740
- }
4741
- const isExistingPath = (value) => typeof value === "string" && value !== "" && existsSync(value);
4742
- /** Anchored to the file name so a directory that merely contains the word cannot match. */
4743
- const PNPM_SCRIPT = /[\\/]pnpm\.(?:mjs|cjs|js)$/u;
4744
- const dshRuntimeRoot = (value) => isExistingPath(value) && existsSync(join(value, "node_modules", "@deepseek-ai", "dsh", "package.json")) ? value : void 0;
4745
- /**
4746
- * Locate the package manager that owns this profile.
4747
- *
4748
- * `dsh plugin --profile desktop` is refused by the desktop app, so the update
4749
- * installs with pnpm directly. The desktop host puts the bundled pnpm in its
4750
- * own arguments; nothing is guessed from the entry script.
4751
- */
4752
- function resolvePnpm({ argv } = {}) {
4753
- const entries = Array.isArray(argv) ? argv : [];
4754
- for (const entry of entries) if (isExistingPath(entry) && PNPM_SCRIPT.test(entry)) return entry;
4755
- for (const entry of entries) {
4756
- const root = dshRuntimeRoot(entry);
4757
- if (root === void 0) continue;
4758
- const script = join(root, "..", "pnpm", "bin", "pnpm.mjs");
4759
- if (existsSync(script)) return script;
5037
+ } : candidate)
5038
+ };
5039
+ }));
4760
5040
  }
5041
+ /**
5042
+ * Mark a key unusable and move the active pointer on. Called from the stream
5043
+ * guard, so the retry of the failed step resolves the next key.
5044
+ */
5045
+ async disable(id, reason) {
5046
+ return projection(await this.#write((state) => {
5047
+ const now = Date.now();
5048
+ const keys = state.keys.map((candidate) => candidate.id === id ? {
5049
+ ...candidate,
5050
+ disabledAt: now,
5051
+ disabledReason: text$1(reason) === "" ? "unusable" : text$1(reason)
5052
+ } : candidate);
5053
+ if (!keys.some((candidate) => candidate.id === id)) return {
5054
+ activeId: state.activeId,
5055
+ keys: state.keys
5056
+ };
5057
+ return {
5058
+ activeId: state.activeId === id ? (keys.find((candidate) => candidate.id !== id && candidate.disabledAt === void 0) ?? keys.find((candidate) => candidate.id !== id))?.id : state.activeId,
5059
+ keys
5060
+ };
5061
+ }));
5062
+ }
5063
+ };
5064
+ //#endregion
5065
+ //#region src/model-selection.js
5066
+ /** Profile preferences for the models advertised in PackyCode's picker. */
5067
+ /** Store exclusions so new catalog models are selected by default as well. */
5068
+ function readHiddenModels(preference) {
5069
+ const value = fieldValue(preference?.hiddenModels);
5070
+ if (!Array.isArray(value)) return [];
5071
+ const hidden = new Set(value);
5072
+ return PACKYCODE_MODEL_IDS.filter((id) => hidden.has(id));
4761
5073
  }
4762
- function resolveLauncher({ argv, execPath }) {
4763
- const pnpm = resolvePnpm({ argv });
4764
- if (pnpm === void 0) return {
4765
- command: "pnpm",
4766
- prefix: []
4767
- };
4768
- return {
4769
- command: execPath,
4770
- prefix: [pnpm]
4771
- };
4772
- }
4773
- /** Run pnpm host-side. Its output is never returned to the browser. */
4774
- function spawnRunCommand({ command, args, cwd }, { signal, timeoutMs }) {
4775
- return new Promise((resolve, reject) => {
4776
- if (signal?.aborted) {
4777
- reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("PackyCode plugin update aborted"));
4778
- return;
4779
- }
4780
- const controller = new AbortController();
4781
- const onAbort = () => controller.abort(signal.reason);
4782
- signal?.addEventListener("abort", onAbort, { once: true });
4783
- const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("PackyCode plugin update timed out")), timeoutMs);
4784
- let settled = false;
4785
- const settle = (callback, value) => {
4786
- if (settled) return;
4787
- settled = true;
4788
- clearTimeout(timer);
4789
- signal?.removeEventListener("abort", onAbort);
4790
- callback(value);
4791
- };
4792
- let child;
4793
- try {
4794
- child = spawn(command, args, {
4795
- stdio: [
4796
- "ignore",
4797
- "pipe",
4798
- "pipe"
4799
- ],
4800
- signal: controller.signal,
4801
- ...cwd === void 0 ? {} : { cwd }
4802
- });
4803
- } catch (error) {
4804
- settle(reject, error);
4805
- return;
4806
- }
4807
- let stdout = "";
4808
- let stderr = "";
4809
- child.stdout?.on("data", (chunk) => {
4810
- stdout = (stdout + chunk).slice(-8192);
4811
- });
4812
- child.stderr?.on("data", (chunk) => {
4813
- stderr = (stderr + chunk).slice(-8192);
4814
- });
4815
- child.on("error", (error) => settle(reject, error));
4816
- child.on("close", (code) => {
4817
- if (controller.signal.aborted) {
4818
- const reason = controller.signal.reason;
4819
- settle(reject, reason instanceof Error ? reason : /* @__PURE__ */ new Error("PackyCode plugin update aborted"));
4820
- return;
4821
- }
4822
- settle(resolve, {
4823
- code: code ?? 1,
4824
- stdout,
4825
- stderr
4826
- });
4827
- });
4828
- });
4829
- }
4830
- /**
4831
- * The running version, the latest registry version, which profile installed
4832
- * this copy, and registry-spec updates against that profile.
4833
- */
4834
- function createPackyCodePluginManager({ fetchImpl = ambientFetch, runCommand = spawnRunCommand, execPath = process.execPath, argv = process.argv, env = process.env, ownPackageJsonUrl = new URL("../package.json", import.meta.url), registryUrl = NPM_REGISTRY_LATEST_URL, now = Date.now, ttlMs = DEFAULT_VERSION_TTL_MS, timeoutMs = DEFAULT_VERSION_TIMEOUT_MS, updateTimeoutMs = DEFAULT_UPDATE_TIMEOUT_MS, settleDelayMs = 250 } = {}) {
4835
- if (typeof fetchImpl !== "function") throw new Error("PackyCode plugin manager requires fetch");
4836
- if (typeof runCommand !== "function") throw new Error("PackyCode plugin manager requires runCommand");
4837
- const dshHome = typeof env.DSH_HOME === "string" && env.DSH_HOME !== "" ? env.DSH_HOME : join(homedir(), ".dsh");
4838
- const launcher = resolveLauncher({
4839
- argv,
4840
- execPath
4841
- });
4842
- let updating = false;
4843
- const fetchLatest = async (signal) => {
4844
- let response;
4845
- try {
4846
- response = await boundedFetch(registryUrl, { headers: { Accept: "application/json" } }, {
4847
- fetchImpl,
4848
- timeoutMs,
4849
- signal
4850
- });
4851
- } catch (error) {
4852
- if (error.aborted) throw error.cause;
4853
- throw new Error("PackyCode plugin version check failed");
4854
- }
4855
- if (!response.ok) throw new Error("PackyCode plugin version check failed");
4856
- let version;
4857
- try {
4858
- version = (await response.json())?.version;
4859
- } catch {
4860
- version = void 0;
4861
- }
4862
- if (parseSemver(version) === void 0) throw new Error("PackyCode plugin version check failed");
4863
- return version;
4864
- };
4865
- const readOwnVersion = async () => {
4866
- const version = (await readManifest(fileURLToPath(ownPackageJsonUrl)))?.version;
4867
- if (parseSemver(version) === void 0) throw new Error("PackyCode plugin version is unavailable");
4868
- return version;
5074
+ function createModelSelectionController({ readPreference, writePreference, isWritable, onChange }) {
5075
+ const hidden = () => readHiddenModels(readPreference());
5076
+ let advertised = hidden().join(",");
5077
+ const reconcile = () => {
5078
+ const next = hidden().join(",");
5079
+ if (next === advertised) return;
5080
+ onChange?.();
5081
+ advertised = next;
4869
5082
  };
4870
- const load = async (signal) => {
4871
- signal?.throwIfAborted();
4872
- const [current, install, latest] = await Promise.all([
4873
- readOwnVersion(),
4874
- findInstall({
4875
- dshHome,
4876
- ownPackageJsonUrl
4877
- }),
4878
- fetchLatest(signal)
4879
- ]);
5083
+ const status = () => {
5084
+ const excluded = new Set(hidden());
4880
5085
  return {
4881
- current,
4882
- latest,
4883
- updateAvailable: compareSemver(latest, current) === 1,
4884
- install,
4885
- fetchedAt: now()
5086
+ writable: isWritable() === true,
5087
+ models: PACKYCODE_MODELS.map(({ id, name }) => ({
5088
+ id,
5089
+ name,
5090
+ selected: !excluded.has(id)
5091
+ }))
4886
5092
  };
4887
5093
  };
4888
- const cache = createCachedRead(load, {
4889
- ttlMs,
4890
- now
4891
- });
4892
5094
  return Object.freeze({
4893
- read: (options) => cache.read(options),
4894
- async update({ signal } = {}) {
4895
- signal?.throwIfAborted();
4896
- if (updating) throw new Error("PackyCode plugin update is already running");
4897
- updating = true;
5095
+ status,
5096
+ reconcile,
5097
+ isVisible: (id) => !hidden().includes(id),
5098
+ async update({ selectedIds } = {}) {
5099
+ if (!Array.isArray(selectedIds) || selectedIds.some((id) => !PACKYCODE_MODEL_IDS.includes(id))) throw new Error("That PackyCode model selection is not supported");
5100
+ if (isWritable() !== true) throw new Error("PackyCode settings are not writable in this profile");
5101
+ const selected = new Set(selectedIds);
4898
5102
  try {
4899
- const install = await findInstall({
4900
- dshHome,
4901
- ownPackageJsonUrl
4902
- });
4903
- if (install.kind === "link") throw new Error("PackyCode plugin is installed from a local checkout");
4904
- if (install.kind !== "npm" || install.profile === void 0) throw new Error("PackyCode plugin update could not find the owning profile");
4905
- const latest = await fetchLatest(signal);
4906
- const profileDir = join(dshHome, "profiles", install.profile);
4907
- let result;
4908
- try {
4909
- result = await runCommand({
4910
- command: launcher.command,
4911
- args: [
4912
- ...launcher.prefix,
4913
- "add",
4914
- "--save-exact",
4915
- `${PACKAGE_NAME}@${latest}`
4916
- ],
4917
- cwd: profileDir
4918
- }, {
4919
- signal,
4920
- timeoutMs: updateTimeoutMs
4921
- });
4922
- } catch (error) {
4923
- if (signal?.aborted) throw error;
4924
- throw new Error("PackyCode plugin update failed");
4925
- }
4926
- if (result.code !== 0) {
4927
- const refusal = releaseAgeRefusal(`${result.stdout ?? ""}\n${result.stderr ?? ""}`, now());
4928
- if (refusal !== void 0) return {
4929
- version: latest,
4930
- profile: install.profile,
4931
- pending: true,
4932
- ...refusal
4933
- };
4934
- throw new Error("PackyCode plugin update failed");
4935
- }
4936
- await confirmInstalled({
4937
- profileDir,
4938
- expected: latest,
4939
- settleDelayMs
4940
- });
4941
- cache.invalidate();
4942
- return {
4943
- version: latest,
4944
- profile: install.profile
4945
- };
4946
- } finally {
4947
- updating = false;
5103
+ await writePreference({ hiddenModels: PACKYCODE_MODEL_IDS.filter((id) => !selected.has(id)) });
5104
+ } catch {
5105
+ throw new Error("PackyCode could not save the model selection");
4948
5106
  }
4949
- },
4950
- invalidate: () => cache.invalidate()
5107
+ reconcile();
5108
+ return status();
5109
+ }
4951
5110
  });
4952
5111
  }
4953
5112
  //#endregion
@@ -6239,8 +6398,8 @@ function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claude,
6239
6398
  cli().install();
6240
6399
  return cli().status();
6241
6400
  },
6242
- "claude-cli/update": async (payload) => {
6243
- await cli().update({ transport: payload?.transport });
6401
+ "claude-cli/update": () => {
6402
+ cli().update();
6244
6403
  return cli().status();
6245
6404
  },
6246
6405
  "plugin/version": (payload, signal) => manager().read({
@@ -7198,12 +7357,6 @@ const Config = z.object({
7198
7357
  imageEnabled: live(z.boolean().default(true)),
7199
7358
  imageModel: live(z.union([...IMAGE_MODEL_IDS]).default(DEFAULT_IMAGE_MODEL)),
7200
7359
  imageQuality: live(z.union([...IMAGE_QUALITIES]).default("auto")),
7201
- /**
7202
- * How Claude requests travel: `auto` sends a key in the `cc` group through
7203
- * the official Claude Code CLI and every other key over HTTP; `cli` and
7204
- * `http` pin one transport.
7205
- */
7206
- claudeTransport: live(z.union([...CLAUDE_TRANSPORTS]).default(DEFAULT_CLAUDE_TRANSPORT)),
7207
7360
  /** Path to the Claude Code CLI; empty means "the usual install places, then PATH". */
7208
7361
  claudeCliPath: live(z.string().default(""))
7209
7362
  });
@@ -7219,7 +7372,6 @@ function apply(ctx, config) {
7219
7372
  imageEnabled: fieldValue(config?.imageEnabled),
7220
7373
  imageModel: fieldValue(config?.imageModel),
7221
7374
  imageQuality: fieldValue(config?.imageQuality),
7222
- claudeTransport: fieldValue(config?.claudeTransport),
7223
7375
  claudeCliPath: fieldValue(config?.claudeCliPath)
7224
7376
  });
7225
7377
  const hosts = () => {
@@ -7289,7 +7441,8 @@ function apply(ctx, config) {
7289
7441
  const claudeCli = createClaudeCliManager({ explicitPath: () => fieldValue(source()?.claudeCliPath) });
7290
7442
  const cliTransport = createClaudeCliTransport({
7291
7443
  binary: async () => {
7292
- const found = await claudeCli.detect();
7444
+ let found = await claudeCli.detect();
7445
+ if (isClaudeCliOutdated(found.version)) found = await claudeCli.detect({ force: true });
7293
7446
  if (typeof found.path !== "string" || found.path === "") throw Object.assign(/* @__PURE__ */ new Error("Claude Code CLI is not installed"), { code: "CLAUDE_CLI_MISSING" });
7294
7447
  return found.path;
7295
7448
  },
@@ -7299,16 +7452,8 @@ function apply(ctx, config) {
7299
7452
  ctx.effect(() => () => {
7300
7453
  cliTransport.close();
7301
7454
  }, "packycode-api: Claude Code CLI processes");
7302
- const claudePreference = {
7303
- writable: false,
7304
- set: void 0
7305
- };
7306
- const claudeTransportPreference = () => readClaudeTransport(source()?.claudeTransport);
7307
7455
  /** The transport a Claude request on this key takes, by its last group reading. */
7308
- const claudeTransportOf = (keyId) => claudeTransportFor({
7309
- preference: claudeTransportPreference(),
7310
- groups: keyId === void 0 ? void 0 : groups.groupsOf(keyId)
7311
- });
7456
+ const claudeTransportOf = (keyId) => claudeTransportFor({ groups: keyId === void 0 ? void 0 : groups.groupsOf(keyId) });
7312
7457
  const registration = ctx.llm.registerAdapter([PROVIDER], createRouteAdapter({
7313
7458
  ring,
7314
7459
  keys,
@@ -7316,7 +7461,7 @@ function apply(ctx, config) {
7316
7461
  resolveAttachments: attachments,
7317
7462
  isModelVisible: modelSelection.isVisible,
7318
7463
  claudeCli: Object.freeze({
7319
- followsGroups: () => claudeTransportPreference() === "auto",
7464
+ followsGroups: () => true,
7320
7465
  choose: ({ apiKey, model }) => isClaudeModel(model) && claudeTransportOf(keys.idOf(apiKey)).transport === "cli",
7321
7466
  fetch: (input, init, meta) => cliTransport.fetch(input, init, meta)
7322
7467
  }),
@@ -7403,8 +7548,7 @@ function apply(ctx, config) {
7403
7548
  hostPreference,
7404
7549
  searchPreference,
7405
7550
  modelPreference,
7406
- imagePreference,
7407
- claudePreference
7551
+ imagePreference
7408
7552
  }));
7409
7553
  ctx.on?.("loader/volatile-update", () => {
7410
7554
  modelSelection.reconcile();
@@ -7450,25 +7594,12 @@ function apply(ctx, config) {
7450
7594
  async status({ force = false } = {}) {
7451
7595
  return {
7452
7596
  ...await claudeCli.status({ force }),
7453
- transport: claudeTransportPreference(),
7454
- writable: claudePreference.writable === true && typeof claudePreference.set === "function",
7455
7597
  processes: cliTransport.stats()
7456
7598
  };
7457
7599
  },
7458
7600
  install: () => claudeCli.install(),
7459
- async update({ transport } = {}) {
7460
- if (!CLAUDE_TRANSPORTS.includes(transport)) throw new Error("That PackyCode Claude transport is not supported");
7461
- if (claudePreference.writable !== true || typeof claudePreference.set !== "function") throw new Error("PackyCode settings are not writable in this profile");
7462
- try {
7463
- await claudePreference.set({ claudeTransport: transport });
7464
- } catch {
7465
- throw new Error("PackyCode could not save the Claude transport");
7466
- }
7467
- },
7468
- transportFor: (names) => claudeTransportFor({
7469
- preference: claudeTransportPreference(),
7470
- groups: names
7471
- })
7601
+ update: () => claudeCli.update(),
7602
+ transportFor: (names) => claudeTransportFor({ groups: names })
7472
7603
  }),
7473
7604
  search,
7474
7605
  modelSelection,
@@ -7512,4 +7643,4 @@ function apply(ctx, config) {
7512
7643
  });
7513
7644
  }
7514
7645
  //#endregion
7515
- export { CHANNEL, CLAUDE_INSTALL_COMMAND, CLAUDE_MISSING_MESSAGE, CLAUDE_NPM_PACKAGE, CLAUDE_TRANSPORTS, CLI_ONLY_GROUPS, CLI_SESSION_IDLE_MS, CLI_TOOL_NOTE, Config, DEFAULT_API_HOST, DEFAULT_CLAUDE_CONFIG_DIR, DEFAULT_CLAUDE_TRANSPORT, DEFAULT_IMAGE_MODEL, DEFAULT_SEARCH_BACKEND, DEFAULT_STATUS_HOST, DIAGNOSTICS_FORMAT, DIAGNOSTICS_LIMITS, DISPLAY_NAME, HTTP_CLAUDE_GROUPS, IMAGE_MODELS, IMAGE_MODEL_IDS, IMAGE_QUALITIES, IMAGE_TIMEOUT_MS, IMAGE_TOOL_NAME, LEGACY_STATUS_HOSTS, MAX_IMAGE_BYTES, MAX_LIVE_CLI_SESSIONS, MCP_SERVER_NAME, MCP_TOOL_PREFIX, PACKYCODE_AUTO_SEARCH_PROVIDER_ID, PACKYCODE_MODELS, PACKYCODE_MODEL_IDS, PACKYCODE_SEARCH_PROVIDER_ID, PLUGIN_ID, PROVIDER, PackyCodeKeyRing, SEARCH_BACKENDS, SEARCH_BACKEND_MODELS, SEARCH_MODES, SETTINGS_NAMESPACE, TEST_MESSAGE_POLICY_CODE, apply, assertApiKey, baseUrlOf, buildCliArgs, buildCliEnvironment, buildSearchRequest, buildTranscript, canonicalMessage, catalogCoverage, classifyCliOutdated, classifyCliUnavailable, classifyClientPolicy, classifyGatewayFailure, classifyTestMessagePolicy, classifyUpstreamServerError, claudeBinaryCandidates, claudeTransportFor, cliErrorMessage, cliToolName, createBalanceReader, createClaudeCliManager, createClaudeCliTransport, createClaudeToolNames, createDiagnostics, createGroupReader, createImageController, createKeyChooser, createModelSelectionController, createPackyCodeAutoSearchProvider, createPackyCodeImageClient, createPackyCodeImageTool, createPackyCodeProfile, createPackyCodeProvider, createPackyCodeRpc, createPackyCodeSearchProvider, createProfileSearchComposition, createRouteAdapter, createSearchComposition, createSearchController, decodeImagePng, describeRequest, dshSystemText, explainCliClientPolicyFailure, explainCliOutdated, explainCliUnavailable, explainClientPolicyFailure, explainNpmFailure, explainTestMessagePolicyFailure, guardKeyRotation, inject, locateClaudeBinary, locateNpm, name, normalizeFailureMessage, normalizeHost, parseClaudeVersion, parseMessagesSearchResponse, parsePricing, parseResponsesSearchResponse, parseSelectedGroups, parseTokenUsage, pickKeyForModel, planContinuation, pngDimensions, readClaudeTransport, readHiddenModels, readImagePreference, readSearchChoice, resolveSpawnCommand, splitConversation, startClaudeCliSession, startClaudeMcpBridge, statusHostOrder, summarizeSessionEvents, toolResultToMcp, validateImageOptions };
7646
+ export { CHANNEL, CLAUDE_INSTALL_COMMAND, CLAUDE_MIN_VERSION, CLAUDE_MISSING_MESSAGE, CLAUDE_NPM_PACKAGE, CLAUDE_NPM_UPDATE_COMMAND, CLAUDE_SELF_UPDATE_COMMAND, CLI_ONLY_GROUPS, CLI_SESSION_IDLE_MS, CLI_TOOL_NOTE, Config, DEFAULT_API_HOST, DEFAULT_CLAUDE_CONFIG_DIR, DEFAULT_IMAGE_MODEL, DEFAULT_SEARCH_BACKEND, DEFAULT_STATUS_HOST, DIAGNOSTICS_FORMAT, DIAGNOSTICS_LIMITS, DISPLAY_NAME, HTTP_CLAUDE_GROUPS, IMAGE_MODELS, IMAGE_MODEL_IDS, IMAGE_QUALITIES, IMAGE_TIMEOUT_MS, IMAGE_TOOL_NAME, LEGACY_STATUS_HOSTS, MAX_IMAGE_BYTES, MAX_LIVE_CLI_SESSIONS, MCP_SERVER_NAME, MCP_TOOL_PREFIX, PACKYCODE_AUTO_SEARCH_PROVIDER_ID, PACKYCODE_MODELS, PACKYCODE_MODEL_IDS, PACKYCODE_SEARCH_PROVIDER_ID, PLUGIN_ID, PROVIDER, PackyCodeKeyRing, SEARCH_BACKENDS, SEARCH_BACKEND_MODELS, SEARCH_MODES, SETTINGS_NAMESPACE, TEST_MESSAGE_POLICY_CODE, apply, assertApiKey, baseUrlOf, buildCliArgs, buildCliEnvironment, buildSearchRequest, buildTranscript, canonicalMessage, catalogCoverage, classifyCliOutdated, classifyCliUnavailable, classifyClientPolicy, classifyGatewayFailure, classifyTestMessagePolicy, classifyUpstreamServerError, claudeBinaryCandidates, claudeTransportFor, cliErrorMessage, cliToolName, createBalanceReader, createClaudeCliManager, createClaudeCliTransport, createClaudeToolNames, createDiagnostics, createGroupReader, createImageController, createKeyChooser, createModelSelectionController, createPackyCodeAutoSearchProvider, createPackyCodeImageClient, createPackyCodeImageTool, createPackyCodeProfile, createPackyCodeProvider, createPackyCodeRpc, createPackyCodeSearchProvider, createProfileSearchComposition, createRouteAdapter, createSearchComposition, createSearchController, decodeImagePng, describeRequest, dshSystemText, explainCliClientPolicyFailure, explainCliOutdated, explainCliUnavailable, explainClientPolicyFailure, explainNpmFailure, explainTestMessagePolicyFailure, guardKeyRotation, inject, isClaudeCliOutdated, isNpmClaudeInstall, locateClaudeBinary, locateNpm, name, normalizeFailureMessage, normalizeHost, parseClaudeVersion, parseMessagesSearchResponse, parsePricing, parseResponsesSearchResponse, parseSelectedGroups, parseTokenUsage, pickKeyForModel, planContinuation, pngDimensions, readHiddenModels, readImagePreference, readSearchChoice, resolveSpawnCommand, splitConversation, startClaudeCliSession, startClaudeMcpBridge, statusHostOrder, summarizeSessionEvents, toolResultToMcp, validateImageOptions };