dsh-packycode-api 1.3.4 → 1.3.6

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";
@@ -14,7 +14,6 @@ import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
14
14
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
15
15
  import { lazyStream } from "@earendil-works/pi-ai/api/lazy";
16
16
  import { clampThinkingLevel, createProvider } from "@earendil-works/pi-ai";
17
- import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
18
17
  import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
19
18
  import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
20
19
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -565,6 +564,362 @@ function withProgramDirOnPath(env, program, platform = process.platform) {
565
564
  };
566
565
  }
567
566
  //#endregion
567
+ //#region src/plugin-version.js
568
+ /**
569
+ * Version self-knowledge for the settings panel.
570
+ *
571
+ * The running version comes from this package. The latest version comes from
572
+ * the npm registry. An in-place update is only offered for a registry install,
573
+ * and it installs that exact version with the host's own pnpm — never through
574
+ * `dsh plugin --profile desktop`, which the desktop app refuses. The install
575
+ * runs under the profile's own `minimumReleaseAge`: a version installed past
576
+ * it would sit in the profile's lockfile, and pnpm then refuses every later
577
+ * plugin install in that profile until the version is old enough. A version
578
+ * pnpm still holds back is reported as pending, with when it clears. Registry
579
+ * bodies and package-manager output stay host-side.
580
+ *
581
+ * @module dsh-packycode-api/plugin-version
582
+ */
583
+ const PACKAGE_NAME = "dsh-packycode-api";
584
+ const NPM_REGISTRY_LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
585
+ const DEFAULT_VERSION_TTL_MS = 5 * 6e4;
586
+ const DEFAULT_VERSION_TIMEOUT_MS = 1e4;
587
+ const DEFAULT_UPDATE_TIMEOUT_MS = 18e4;
588
+ const RELEASE_AGE_ERROR = /ERR_PNPM_(?:NO_MATURE_MATCHING_VERSION|MINIMUM_RELEASE_AGE_VIOLATION)/u;
589
+ const RELEASE_AGE_ENTRY = /(\S+@\S+) was published at (\S+?), within the minimumReleaseAge cutoff \((\S+?)\)/gu;
590
+ /**
591
+ * Recognize pnpm refusing a version that is younger than its
592
+ * `minimumReleaseAge`, whether the version was requested or already sits in
593
+ * the lockfile. Each refused entry reads `<name>@<version> was published at
594
+ * <time>, within the minimumReleaseAge cutoff (<time>)`.
595
+ * @param output - the install's stdout and stderr.
596
+ * @param finishedAt - when the install ended; pnpm drew its cutoff then.
597
+ * @returns undefined when this is not a release-age refusal; otherwise
598
+ * `{ availableAt }`, when the youngest refused entry clears, if pnpm said.
599
+ */
600
+ function releaseAgeRefusal(output, finishedAt) {
601
+ const text = String(output ?? "").replace(/\u001b\[[0-9;]*m/gu, "");
602
+ if (!RELEASE_AGE_ERROR.test(text)) return void 0;
603
+ let availableAt;
604
+ for (const [, , published, cutoff] of text.matchAll(RELEASE_AGE_ENTRY)) {
605
+ const clears = Date.parse(published) + (finishedAt - Date.parse(cutoff));
606
+ if (Number.isFinite(clears) && (availableAt === void 0 || clears > availableAt)) availableAt = clears;
607
+ }
608
+ return availableAt === void 0 ? {} : { availableAt };
609
+ }
610
+ /** Parse a strict `x.y.z[-pre][+build]` version; anything else is undefined. */
611
+ function parseSemver(value) {
612
+ if (typeof value !== "string") return void 0;
613
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value.trim());
614
+ if (match === null) return void 0;
615
+ return {
616
+ major: Number(match[1]),
617
+ minor: Number(match[2]),
618
+ patch: Number(match[3]),
619
+ pre: match[4]
620
+ };
621
+ }
622
+ /** Compare two versions: -1/0/1, or undefined when either side is not semver. */
623
+ function compareSemver(a, b) {
624
+ const left = parseSemver(a);
625
+ const right = parseSemver(b);
626
+ if (left === void 0 || right === void 0) return void 0;
627
+ for (const key of [
628
+ "major",
629
+ "minor",
630
+ "patch"
631
+ ]) if (left[key] !== right[key]) return left[key] < right[key] ? -1 : 1;
632
+ if (left.pre === right.pre) return 0;
633
+ if (left.pre === void 0) return 1;
634
+ if (right.pre === void 0) return -1;
635
+ return left.pre < right.pre ? -1 : 1;
636
+ }
637
+ /**
638
+ * Classify how the running package reached the profile. Only registry specs
639
+ * may be updated in place; local checkouts belong to the owner's iteration.
640
+ */
641
+ function classifySpec(spec) {
642
+ if (typeof spec !== "string" || spec === "") return "unknown";
643
+ if (/^(?:link|file):/u.test(spec)) return "link";
644
+ if (/^(?:\d|[~^*])/u.test(spec)) return "npm";
645
+ return "unknown";
646
+ }
647
+ const readManifest = async (path) => {
648
+ try {
649
+ const parsed = JSON.parse(await readFile(path, "utf8"));
650
+ return parsed !== null && typeof parsed === "object" ? parsed : void 0;
651
+ } catch {
652
+ return;
653
+ }
654
+ };
655
+ /**
656
+ * A zero exit only proves the command ran. pnpm can skip a too-new version
657
+ * and still exit zero, so the landed manifest is what makes the answer true.
658
+ */
659
+ async function confirmInstalled({ profileDir, expected, settleDelayMs, attempts = 3 }) {
660
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
661
+ if ((await readManifest(join(profileDir, "node_modules", "dsh-packycode-api", "package.json")))?.version === expected) return;
662
+ if (attempt < attempts) await new Promise((resolve) => {
663
+ setTimeout(resolve, settleDelayMs);
664
+ });
665
+ }
666
+ throw new Error("PackyCode plugin update did not install the requested version");
667
+ }
668
+ /**
669
+ * Find the profile that owns this running installation. pnpm symlinks resolve
670
+ * to the same realpath on both sides. When no realpath matches, fall back to
671
+ * the only profile that declares the dependency at all.
672
+ */
673
+ async function findInstall({ dshHome, ownPackageJsonUrl }) {
674
+ let ownReal;
675
+ try {
676
+ ownReal = await realpath(fileURLToPath(ownPackageJsonUrl));
677
+ } catch {
678
+ ownReal = void 0;
679
+ }
680
+ let entries = [];
681
+ try {
682
+ entries = await readdir(join(dshHome, "profiles"), { withFileTypes: true });
683
+ } catch {
684
+ entries = [];
685
+ }
686
+ const declared = [];
687
+ for (const entry of entries) {
688
+ if (!entry.isDirectory()) continue;
689
+ const profileDir = join(dshHome, "profiles", entry.name);
690
+ const spec = (await readManifest(join(profileDir, "package.json")))?.dependencies?.[PACKAGE_NAME];
691
+ if (typeof spec !== "string") continue;
692
+ declared.push({
693
+ profile: entry.name,
694
+ spec
695
+ });
696
+ if (ownReal === void 0) continue;
697
+ try {
698
+ if (await realpath(join(profileDir, "node_modules", "dsh-packycode-api", "package.json")) === ownReal) return {
699
+ kind: classifySpec(spec),
700
+ profile: entry.name
701
+ };
702
+ } catch {}
703
+ }
704
+ if (declared.length === 1) return {
705
+ kind: classifySpec(declared[0].spec),
706
+ profile: declared[0].profile
707
+ };
708
+ return { kind: "unknown" };
709
+ }
710
+ const isExistingPath = (value) => typeof value === "string" && value !== "" && existsSync(value);
711
+ /** Anchored to the file name so a directory that merely contains the word cannot match. */
712
+ const PNPM_SCRIPT = /[\\/]pnpm\.(?:mjs|cjs|js)$/u;
713
+ const dshRuntimeRoot = (value) => isExistingPath(value) && existsSync(join(value, "node_modules", "@deepseek-ai", "dsh", "package.json")) ? value : void 0;
714
+ /**
715
+ * Locate the package manager that owns this profile.
716
+ *
717
+ * `dsh plugin --profile desktop` is refused by the desktop app, so the update
718
+ * installs with pnpm directly. The desktop host puts the bundled pnpm in its
719
+ * own arguments; nothing is guessed from the entry script.
720
+ */
721
+ function resolvePnpm({ argv } = {}) {
722
+ const entries = Array.isArray(argv) ? argv : [];
723
+ for (const entry of entries) if (isExistingPath(entry) && PNPM_SCRIPT.test(entry)) return entry;
724
+ for (const entry of entries) {
725
+ const root = dshRuntimeRoot(entry);
726
+ if (root === void 0) continue;
727
+ const script = join(root, "..", "pnpm", "bin", "pnpm.mjs");
728
+ if (existsSync(script)) return script;
729
+ }
730
+ }
731
+ function resolveLauncher({ argv, execPath }) {
732
+ const pnpm = resolvePnpm({ argv });
733
+ if (pnpm === void 0) return {
734
+ command: "pnpm",
735
+ prefix: []
736
+ };
737
+ return {
738
+ command: execPath,
739
+ prefix: [pnpm]
740
+ };
741
+ }
742
+ /** Run pnpm host-side. Its output is never returned to the browser. */
743
+ function spawnRunCommand({ command, args, cwd }, { signal, timeoutMs }) {
744
+ return new Promise((resolve, reject) => {
745
+ if (signal?.aborted) {
746
+ reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("PackyCode plugin update aborted"));
747
+ return;
748
+ }
749
+ const controller = new AbortController();
750
+ const onAbort = () => controller.abort(signal.reason);
751
+ signal?.addEventListener("abort", onAbort, { once: true });
752
+ const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("PackyCode plugin update timed out")), timeoutMs);
753
+ let settled = false;
754
+ const settle = (callback, value) => {
755
+ if (settled) return;
756
+ settled = true;
757
+ clearTimeout(timer);
758
+ signal?.removeEventListener("abort", onAbort);
759
+ callback(value);
760
+ };
761
+ let child;
762
+ try {
763
+ child = spawn(command, args, {
764
+ stdio: [
765
+ "ignore",
766
+ "pipe",
767
+ "pipe"
768
+ ],
769
+ signal: controller.signal,
770
+ ...cwd === void 0 ? {} : { cwd }
771
+ });
772
+ } catch (error) {
773
+ settle(reject, error);
774
+ return;
775
+ }
776
+ let stdout = "";
777
+ let stderr = "";
778
+ child.stdout?.on("data", (chunk) => {
779
+ stdout = (stdout + chunk).slice(-8192);
780
+ });
781
+ child.stderr?.on("data", (chunk) => {
782
+ stderr = (stderr + chunk).slice(-8192);
783
+ });
784
+ child.on("error", (error) => settle(reject, error));
785
+ child.on("close", (code) => {
786
+ if (controller.signal.aborted) {
787
+ const reason = controller.signal.reason;
788
+ settle(reject, reason instanceof Error ? reason : /* @__PURE__ */ new Error("PackyCode plugin update aborted"));
789
+ return;
790
+ }
791
+ settle(resolve, {
792
+ code: code ?? 1,
793
+ stdout,
794
+ stderr
795
+ });
796
+ });
797
+ });
798
+ }
799
+ /**
800
+ * The running version, the latest registry version, which profile installed
801
+ * this copy, and registry-spec updates against that profile.
802
+ */
803
+ 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 } = {}) {
804
+ if (typeof fetchImpl !== "function") throw new Error("PackyCode plugin manager requires fetch");
805
+ if (typeof runCommand !== "function") throw new Error("PackyCode plugin manager requires runCommand");
806
+ const dshHome = typeof env.DSH_HOME === "string" && env.DSH_HOME !== "" ? env.DSH_HOME : join(homedir(), ".dsh");
807
+ const launcher = resolveLauncher({
808
+ argv,
809
+ execPath
810
+ });
811
+ let updating = false;
812
+ const fetchLatest = async (signal) => {
813
+ let response;
814
+ try {
815
+ response = await boundedFetch(registryUrl, { headers: { Accept: "application/json" } }, {
816
+ fetchImpl,
817
+ timeoutMs,
818
+ signal
819
+ });
820
+ } catch (error) {
821
+ if (error.aborted) throw error.cause;
822
+ throw new Error("PackyCode plugin version check failed");
823
+ }
824
+ if (!response.ok) throw new Error("PackyCode plugin version check failed");
825
+ let version;
826
+ try {
827
+ version = (await response.json())?.version;
828
+ } catch {
829
+ version = void 0;
830
+ }
831
+ if (parseSemver(version) === void 0) throw new Error("PackyCode plugin version check failed");
832
+ return version;
833
+ };
834
+ const readOwnVersion = async () => {
835
+ const version = (await readManifest(fileURLToPath(ownPackageJsonUrl)))?.version;
836
+ if (parseSemver(version) === void 0) throw new Error("PackyCode plugin version is unavailable");
837
+ return version;
838
+ };
839
+ const load = async (signal) => {
840
+ signal?.throwIfAborted();
841
+ const [current, install, latest] = await Promise.all([
842
+ readOwnVersion(),
843
+ findInstall({
844
+ dshHome,
845
+ ownPackageJsonUrl
846
+ }),
847
+ fetchLatest(signal)
848
+ ]);
849
+ return {
850
+ current,
851
+ latest,
852
+ updateAvailable: compareSemver(latest, current) === 1,
853
+ install,
854
+ fetchedAt: now()
855
+ };
856
+ };
857
+ const cache = createCachedRead(load, {
858
+ ttlMs,
859
+ now
860
+ });
861
+ return Object.freeze({
862
+ read: (options) => cache.read(options),
863
+ async update({ signal } = {}) {
864
+ signal?.throwIfAborted();
865
+ if (updating) throw new Error("PackyCode plugin update is already running");
866
+ updating = true;
867
+ try {
868
+ const install = await findInstall({
869
+ dshHome,
870
+ ownPackageJsonUrl
871
+ });
872
+ if (install.kind === "link") throw new Error("PackyCode plugin is installed from a local checkout");
873
+ if (install.kind !== "npm" || install.profile === void 0) throw new Error("PackyCode plugin update could not find the owning profile");
874
+ const latest = await fetchLatest(signal);
875
+ const profileDir = join(dshHome, "profiles", install.profile);
876
+ let result;
877
+ try {
878
+ result = await runCommand({
879
+ command: launcher.command,
880
+ args: [
881
+ ...launcher.prefix,
882
+ "add",
883
+ "--save-exact",
884
+ `${PACKAGE_NAME}@${latest}`
885
+ ],
886
+ cwd: profileDir
887
+ }, {
888
+ signal,
889
+ timeoutMs: updateTimeoutMs
890
+ });
891
+ } catch (error) {
892
+ if (signal?.aborted) throw error;
893
+ throw new Error("PackyCode plugin update failed");
894
+ }
895
+ if (result.code !== 0) {
896
+ const refusal = releaseAgeRefusal(`${result.stdout ?? ""}\n${result.stderr ?? ""}`, now());
897
+ if (refusal !== void 0) return {
898
+ version: latest,
899
+ profile: install.profile,
900
+ pending: true,
901
+ ...refusal
902
+ };
903
+ throw new Error("PackyCode plugin update failed");
904
+ }
905
+ await confirmInstalled({
906
+ profileDir,
907
+ expected: latest,
908
+ settleDelayMs
909
+ });
910
+ cache.invalidate();
911
+ return {
912
+ version: latest,
913
+ profile: install.profile
914
+ };
915
+ } finally {
916
+ updating = false;
917
+ }
918
+ },
919
+ invalidate: () => cache.invalidate()
920
+ });
921
+ }
922
+ //#endregion
568
923
  //#region src/claude-binary.js
569
924
  /**
570
925
  * Finding and starting the official Claude Code CLI.
@@ -578,10 +933,18 @@ function withProgramDirOnPath(env, program, platform = process.platform) {
578
933
  */
579
934
  /** The npm package the official CLI ships as. */
580
935
  const CLAUDE_NPM_PACKAGE = "@anthropic-ai/claude-code";
936
+ /** The oldest CLI that takes every option the transport passes (`--permission-prompts` arrived in 2.1.259). */
937
+ const CLAUDE_MIN_VERSION = "2.1.259";
938
+ /**
939
+ * Whether a detected CLI is older than the transport needs. An unknown
940
+ * version is not judged; a CLI that then refuses an option still says so.
941
+ * @param version - the version `claude --version` printed.
942
+ */
943
+ const isClaudeCliOutdated = (version) => compareSemver(version, CLAUDE_MIN_VERSION) === -1;
581
944
  /** Isolated CLI home; the plugin owns the endpoint and key it runs with. */
582
945
  const DEFAULT_CLAUDE_CONFIG_DIR = join(homedir(), ".dsh", "packycode-claude-cli");
583
946
  /** 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";
947
+ 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
948
  /** The CLI's file names on PATH: the native installer ships an `.exe`, npm a `.cmd` launcher. */
586
949
  const claudeFileNames = (platform) => platform === "win32" ? ["claude.exe", "claude.cmd"] : ["claude"];
587
950
  /**
@@ -683,23 +1046,40 @@ function resolveSpawnCommand(binary, { platform = process.platform, exists = exi
683
1046
  //#endregion
684
1047
  //#region src/claude-install.js
685
1048
  /**
686
- * Detecting, and installing, the local Claude Code CLI.
1049
+ * Detecting, installing and updating the local Claude Code CLI.
687
1050
  *
688
1051
  * Claude requests on a key whose group serves only the official client are
689
1052
  * sent by this device's Claude Code CLI, so the settings panel shows whether
690
1053
  * one is installed (and which version), and offers to install it with
691
1054
  * `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.
1055
+ * Windows — or to update one older than the transport needs. Both run on the
1056
+ * host, never in a shell built from user text: their arguments are constants.
1057
+ *
1058
+ * npm 12 skips install scripts unless they are allowed, and the CLI's
1059
+ * postinstall is what puts its native binary in place, so every npm install
1060
+ * allows the package's scripts (npm 10 and 11 accept the option as well).
694
1061
  *
695
1062
  * @module dsh-packycode-api/claude-install
696
1063
  */
697
- const CLAUDE_INSTALL_COMMAND = `npm install -g ${CLAUDE_NPM_PACKAGE}`;
1064
+ const ALLOW_SCRIPTS = `--allow-scripts=${CLAUDE_NPM_PACKAGE}`;
1065
+ const CLAUDE_INSTALL_COMMAND = `npm install -g ${CLAUDE_NPM_PACKAGE} ${ALLOW_SCRIPTS}`;
1066
+ /** How an npm install of the CLI is updated. */
1067
+ const CLAUDE_NPM_UPDATE_COMMAND = `npm install -g ${CLAUDE_NPM_PACKAGE}@latest ${ALLOW_SCRIPTS}`;
1068
+ /** How any other install (the native installer's, Homebrew's) is updated: by the CLI itself. */
1069
+ const CLAUDE_SELF_UPDATE_COMMAND = "claude update";
698
1070
  const INSTALL_ARGS = Object.freeze([
699
1071
  "install",
700
1072
  "-g",
701
- CLAUDE_NPM_PACKAGE
1073
+ CLAUDE_NPM_PACKAGE,
1074
+ ALLOW_SCRIPTS
702
1075
  ]);
1076
+ const NPM_UPDATE_ARGS = Object.freeze([
1077
+ "install",
1078
+ "-g",
1079
+ `${CLAUDE_NPM_PACKAGE}@latest`,
1080
+ ALLOW_SCRIPTS
1081
+ ]);
1082
+ const SELF_UPDATE_ARGS = Object.freeze(["update"]);
703
1083
  const PREFIX_ARGS = Object.freeze(["prefix", "-g"]);
704
1084
  const VERSION_TIMEOUT_MS = 2e4;
705
1085
  const PREFIX_TIMEOUT_MS = 2e4;
@@ -768,6 +1148,28 @@ function locateNpm({ platform = process.platform, home = homedir(), env = proces
768
1148
  }).find((candidate) => exists(candidate));
769
1149
  }
770
1150
  /**
1151
+ * Whether a located CLI is npm's global package. npm's own update must then
1152
+ * be used: the CLI's `claude update` runs npm without allowing the package's
1153
+ * scripts, which on npm 12 reports success and leaves no native binary.
1154
+ * @param binary - the located CLI.
1155
+ * @param options - platform and a realpath seam.
1156
+ */
1157
+ function isNpmClaudeInstall(binary, { platform = process.platform, realpath = realpathSync } = {}) {
1158
+ if (typeof binary !== "string" || binary === "") return false;
1159
+ if (platform === "win32" && /\.(?:cmd|bat)$/iu.test(binary)) return true;
1160
+ let resolved = binary;
1161
+ try {
1162
+ resolved = realpath(binary);
1163
+ } catch {}
1164
+ return /[\\/]node_modules[\\/]@anthropic-ai[\\/]claude-code[\\/]/u.test(resolved);
1165
+ }
1166
+ /**
1167
+ * The parent environment without the user's own Claude settings: a
1168
+ * `DISABLE_AUTOUPDATER`, config directory or key there must not steer the
1169
+ * plugin's update.
1170
+ */
1171
+ const withoutClaudeSettings = (env) => Object.fromEntries(Object.entries(env ?? {}).filter(([name, value]) => value !== void 0 && !/^(?:ANTHROPIC_|CLAUDE_|CLAUDECODE|DISABLE_)/iu.test(name)));
1172
+ /**
771
1173
  * Classify an npm failure so the panel can say what to do about it.
772
1174
  * @param result - the captured npm run.
773
1175
  * @returns `{ code, detail }`.
@@ -864,7 +1266,7 @@ function runCaptured(spawnImpl, command, args, options, timeoutMs) {
864
1266
  * applies without a restart), plus platform, environment, and process seams.
865
1267
  * @returns `{ binary, detect, install, status }`.
866
1268
  */
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() } = {}) {
1269
+ 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
1270
  const path = pathFor(platform);
869
1271
  const configured = () => {
870
1272
  const value = explicitPath();
@@ -968,6 +1370,7 @@ function createClaudeCliManager({ explicitPath = () => "", platform = process.pl
968
1370
  });
969
1371
  return detecting;
970
1372
  };
1373
+ /** The install or update in flight or last finished; `action` says which. */
971
1374
  let installation = { state: "idle" };
972
1375
  let installing;
973
1376
  const install = () => {
@@ -976,6 +1379,7 @@ function createClaudeCliManager({ explicitPath = () => "", platform = process.pl
976
1379
  if (program === void 0) {
977
1380
  installation = {
978
1381
  state: "failed",
1382
+ action: "install",
979
1383
  code: "npm-missing",
980
1384
  finishedAt: now()
981
1385
  };
@@ -983,6 +1387,7 @@ function createClaudeCliManager({ explicitPath = () => "", platform = process.pl
983
1387
  }
984
1388
  installation = {
985
1389
  state: "running",
1390
+ action: "install",
986
1391
  startedAt: now(),
987
1392
  npm: program
988
1393
  };
@@ -991,6 +1396,7 @@ function createClaudeCliManager({ explicitPath = () => "", platform = process.pl
991
1396
  if (result.code !== 0) {
992
1397
  installation = {
993
1398
  state: "failed",
1399
+ action: "install",
994
1400
  ...explainNpmFailure(result),
995
1401
  finishedAt: now()
996
1402
  };
@@ -1000,9 +1406,11 @@ function createClaudeCliManager({ explicitPath = () => "", platform = process.pl
1000
1406
  const found = await detect({ force: true });
1001
1407
  installation = found.installed && found.version !== void 0 ? {
1002
1408
  state: "succeeded",
1409
+ action: "install",
1003
1410
  finishedAt: now()
1004
1411
  } : {
1005
1412
  state: "failed",
1413
+ action: "install",
1006
1414
  code: "not-found-after-install",
1007
1415
  detail: detailOf(result.stdout, result.stderr),
1008
1416
  finishedAt: now()
@@ -1010,6 +1418,112 @@ function createClaudeCliManager({ explicitPath = () => "", platform = process.pl
1010
1418
  }).catch((error) => {
1011
1419
  installation = {
1012
1420
  state: "failed",
1421
+ action: "install",
1422
+ code: "failed",
1423
+ detail: error instanceof Error ? error.message : String(error),
1424
+ finishedAt: now()
1425
+ };
1426
+ }).finally(() => {
1427
+ installing = void 0;
1428
+ });
1429
+ return installation;
1430
+ };
1431
+ /** npm for an npm install of the CLI: the one beside it shares its global prefix. */
1432
+ const npmBeside = (binary) => {
1433
+ const beside = path.join(path.dirname(binary), platform === "win32" ? "npm.cmd" : "npm");
1434
+ return exists(beside) ? beside : npm();
1435
+ };
1436
+ /**
1437
+ * Update the located CLI the way it was installed: npm's global package
1438
+ * through npm, anything else through `claude update` with the plugin's
1439
+ * own CLI home. Success means the CLI now detected is new enough.
1440
+ */
1441
+ const runUpdate = async () => {
1442
+ const before = await detect({ force: true });
1443
+ if (!before.installed || typeof before.path !== "string") {
1444
+ installation = {
1445
+ state: "failed",
1446
+ action: "update",
1447
+ code: "not-installed",
1448
+ finishedAt: now()
1449
+ };
1450
+ return;
1451
+ }
1452
+ let result;
1453
+ if (isNpmClaudeInstall(before.path, {
1454
+ platform,
1455
+ realpath
1456
+ })) {
1457
+ const program = npmBeside(before.path);
1458
+ if (program === void 0) {
1459
+ installation = {
1460
+ state: "failed",
1461
+ action: "update",
1462
+ code: "npm-missing",
1463
+ finishedAt: now()
1464
+ };
1465
+ return;
1466
+ }
1467
+ installation = {
1468
+ ...installation,
1469
+ npm: program
1470
+ };
1471
+ result = await runNpm(program, NPM_UPDATE_ARGS, INSTALL_TIMEOUT_MS);
1472
+ globalBin = void 0;
1473
+ } else {
1474
+ const command = resolveSpawnCommand(before.path, {
1475
+ platform,
1476
+ exists,
1477
+ readFile
1478
+ });
1479
+ result = await runCaptured(spawnImpl, command.command, [...command.args, ...SELF_UPDATE_ARGS], { env: {
1480
+ ...withProgramDirOnPath(withoutClaudeSettings(env), before.path, platform),
1481
+ CLAUDE_CONFIG_DIR: configDir,
1482
+ DISABLE_TELEMETRY: "1",
1483
+ DISABLE_ERROR_REPORTING: "1"
1484
+ } }, INSTALL_TIMEOUT_MS);
1485
+ }
1486
+ const after = await detect({ force: true });
1487
+ const from = before.version === void 0 ? {} : { from: before.version };
1488
+ if (after.version !== void 0 && !isClaudeCliOutdated(after.version)) installation = {
1489
+ state: "succeeded",
1490
+ action: "update",
1491
+ ...from,
1492
+ to: after.version,
1493
+ finishedAt: now()
1494
+ };
1495
+ else if (result.code !== 0) installation = {
1496
+ state: "failed",
1497
+ action: "update",
1498
+ ...explainNpmFailure(result),
1499
+ finishedAt: now()
1500
+ };
1501
+ else if (after.version === void 0) installation = {
1502
+ state: "failed",
1503
+ action: "update",
1504
+ code: "broken-after-update",
1505
+ detail: detailOf(after.error, result.stdout, result.stderr),
1506
+ finishedAt: now()
1507
+ };
1508
+ else installation = {
1509
+ state: "failed",
1510
+ action: "update",
1511
+ code: "still-outdated",
1512
+ detail: detailOf(result.stdout, result.stderr),
1513
+ finishedAt: now()
1514
+ };
1515
+ };
1516
+ const update = () => {
1517
+ if (installing !== void 0) return installation;
1518
+ installation = {
1519
+ state: "running",
1520
+ action: "update",
1521
+ startedAt: now()
1522
+ };
1523
+ installing = runUpdate().catch((error) => {
1524
+ installation = {
1525
+ state: "failed",
1526
+ action: "update",
1013
1527
  code: "failed",
1014
1528
  detail: error instanceof Error ? error.message : String(error),
1015
1529
  finishedAt: now()
@@ -1033,17 +1547,24 @@ function createClaudeCliManager({ explicitPath = () => "", platform = process.pl
1033
1547
  /** The last detection, without probing again; undefined before the first. */
1034
1548
  lastDetection: () => detection === void 0 ? void 0 : { ...detection },
1035
1549
  install,
1036
- /** Wait for an install in flight; for tests and scripts. */
1550
+ update,
1551
+ /** Wait for an install or update in flight; for tests and scripts. */
1037
1552
  settled: () => installing ?? Promise.resolve(),
1038
1553
  /**
1039
- * What the settings panel shows. While an install runs, the last detection
1040
- * stands instead of probing a half-written package.
1554
+ * What the settings panel shows. While an install or update runs, the
1555
+ * last detection stands instead of probing a half-written package.
1041
1556
  */
1042
1557
  async status({ force = false } = {}) {
1043
1558
  const value = installing === void 0 ? await detect({ force }) : detection;
1044
1559
  return {
1045
1560
  installed: value?.installed === true,
1046
1561
  ...value?.version === void 0 ? {} : { version: value.version },
1562
+ minVersion: CLAUDE_MIN_VERSION,
1563
+ outdated: isClaudeCliOutdated(value?.version),
1564
+ ...value?.path === void 0 ? {} : { updateCommand: isNpmClaudeInstall(value.path, {
1565
+ platform,
1566
+ realpath
1567
+ }) ? CLAUDE_NPM_UPDATE_COMMAND : CLAUDE_SELF_UPDATE_COMMAND },
1047
1568
  ...value?.path === void 0 ? {} : { path: value.path },
1048
1569
  ...value?.configuredPath === void 0 ? {} : { configuredPath: value.configuredPath },
1049
1570
  ...value?.error === void 0 ? {} : { error: value.error },
@@ -1434,7 +1955,7 @@ const classifyCliOutdated = (message) => typeof message === "string" && message.
1434
1955
  */
1435
1956
  function explainCliUnavailable(message) {
1436
1957
  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.";
1958
+ 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
1959
  }
1439
1960
  /**
1440
1961
  * Explain a CLI that is too old for the options this plugin passes.
@@ -1442,7 +1963,7 @@ function explainCliUnavailable(message) {
1442
1963
  * @returns the replacement sentence.
1443
1964
  */
1444
1965
  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)}`;
1966
+ 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
1967
  }
1447
1968
  /**
1448
1969
  * Explain a client-policy refusal of a request the official CLI itself sent.
@@ -2300,7 +2821,9 @@ function baseUrlOf(input) {
2300
2821
  /**
2301
2822
  * Create the transport.
2302
2823
  * @param options - how to find the CLI binary (`binary()` resolves a path or
2303
- * throws), the isolated CLI home, the version recorded in transcripts,
2824
+ * throws), the isolated CLI home, the detected CLI version (read once the
2825
+ * binary is resolved: an outdated one is refused, and it is recorded in
2826
+ * transcripts),
2304
2827
  * limits, a logger, and process seams for tests.
2305
2828
  * @returns `{ fetch(input, init, meta), close(), stats() }`.
2306
2829
  */
@@ -2346,24 +2869,29 @@ function createClaudeCliTransport({ binary, configDir = DEFAULT_CLAUDE_CONFIG_DI
2346
2869
  await mkdir(fallback, { recursive: true });
2347
2870
  return fallback;
2348
2871
  };
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
- });
2872
+ const open = async (request, meta, { oneShot }) => {
2873
+ const program = await binary();
2874
+ const cliVersion = version();
2875
+ if (isClaudeCliOutdated(cliVersion)) throw Object.assign(/* @__PURE__ */ new Error(`Claude Code ${cliVersion} is older than ${CLAUDE_MIN_VERSION}`), { code: "CLAUDE_CLI_OUTDATED" });
2876
+ return startClaudeCliSession({
2877
+ binary: async () => program,
2878
+ configDir,
2879
+ runDirectory: join(configDir, "runs", runDirectoryName()),
2880
+ baseUrl: meta.baseUrl,
2881
+ apiKey: meta.apiKey,
2882
+ request,
2883
+ conversation: splitConversation(request.messages),
2884
+ cwd: await workingDirectory(meta.cwd),
2885
+ sessionId: oneShot ? randomUUID() : meta.sessionId,
2886
+ cliVersion,
2887
+ oneShot,
2888
+ logger,
2889
+ ...spawnImpl === void 0 ? {} : { spawnImpl },
2890
+ ...platform === void 0 ? {} : { platform },
2891
+ ...parentEnv === void 0 ? {} : { parentEnv },
2892
+ ...startupTimeoutMs === void 0 ? {} : { startupTimeoutMs }
2893
+ });
2894
+ };
2367
2895
  /**
2368
2896
  * Send one Messages request through the CLI.
2369
2897
  * @param input - the request URL the native client targeted.
@@ -2419,6 +2947,10 @@ function createClaudeCliTransport({ binary, configDir = DEFAULT_CLAUDE_CONFIG_DI
2419
2947
  return await session.respond({ kind: "start" }, init?.signal);
2420
2948
  } catch (error) {
2421
2949
  if (init?.signal?.aborted) throw error;
2950
+ if (error?.code === "CLAUDE_CLI_OUTDATED") {
2951
+ logger?.warn?.("packycode-api: %s", error.message);
2952
+ return errorResponse(400, `${CLI_OUTDATED}: ${error.message}`, CLI_OUTDATED);
2953
+ }
2422
2954
  const reason = error?.code === "CLAUDE_CLI_MISSING" ? "" : `: ${String(error?.message ?? error).slice(0, 300)}`;
2423
2955
  logger?.warn?.("packycode-api: Claude Code CLI could not start%s", reason);
2424
2956
  return errorResponse(400, `${CLI_UNAVAILABLE}${reason}`, CLI_UNAVAILABLE);
@@ -2452,8 +2984,9 @@ function createClaudeCliTransport({ binary, configDir = DEFAULT_CLAUDE_CONFIG_DI
2452
2984
  * PackyCode's `cc` group ("claude code专用") serves only the official client,
2453
2985
  * so a key in it sends its Claude requests through the real CLI.
2454
2986
  * `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.)
2987
+ * route. The groups alone decide: a cc key cannot be sent over HTTP, where
2988
+ * the gateway refuses it as a third-party client, and no other key needs the
2989
+ * CLI. (`claude-sale` is delisted for now; it joins the CLI list if it returns.)
2457
2990
  *
2458
2991
  * Only the transport changes: DSH's agent loop, tools, permissions, plan mode,
2459
2992
  * attachments and history are identical on both, and no other provider or
@@ -2469,33 +3002,17 @@ const HTTP_CLAUDE_GROUPS = Object.freeze([
2469
3002
  "cc-sale",
2470
3003
  "claude-officially"
2471
3004
  ]);
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
3005
  /** Whether a catalog model is served over the Anthropic Messages protocol. */
2480
3006
  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
3007
  /**
2486
3008
  * 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).
3009
+ * @param options - the request key's groups, as the last group reading saw
3010
+ * them (undefined when there is no reading yet).
2489
3011
  * @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).
3012
+ * `group` (the key's groups decided) or `unknown` (no reading yet; the
3013
+ * native HTTP route stands, as it did before the CLI route existed).
2492
3014
  */
2493
- function claudeTransportFor({ preference, groups } = {}) {
2494
- const pinned = readClaudeTransport(preference);
2495
- if (pinned !== "auto") return {
2496
- transport: pinned,
2497
- reason: "preference"
2498
- };
3015
+ function claudeTransportFor({ groups } = {}) {
2499
3016
  if (!Array.isArray(groups)) return {
2500
3017
  transport: "http",
2501
3018
  reason: "unknown"
@@ -2733,8 +3250,8 @@ function createDiagnostics({ sessionQuery, readEvents, ring, groups, secrets = a
2733
3250
  *
2734
3251
  * The gateway's own guidance for these routes, which the base URLs and pinned
2735
3252
  * protocols below exist to satisfy:
2736
- * - OpenAI/Codex traffic takes `/v1` in the base URL and never an endpoint
2737
- * path — the SDK appends `/responses` or `/chat/completions` itself.
3253
+ * - OpenAI-protocol traffic takes `/v1` in the base URL and never an endpoint
3254
+ * path — the SDK appends `/responses` itself.
2738
3255
  * - Anthropic traffic takes `https://cf.api.fan` with **no** `/v1`: the
2739
3256
  * Anthropic client appends `/v1/messages` itself.
2740
3257
  * - A model asked over the wrong protocol does not fail cleanly; the stream
@@ -2744,7 +3261,7 @@ function createDiagnostics({ sessionQuery, readEvents, ring, groups, secrets = a
2744
3261
  * model does not support is refused before the request is sent: Claude Opus
2745
3262
  * 5.5 (platform.claude.com effort docs), GPT-6.1 Sol, GPT-6 Astra and GPT-5.6
2746
3263
  * Sol `reasoning.effort` (developers.openai.com model pages), DeepSeek
2747
- * thinking mode (`thinking` toggle plus `low|high|max` effort), Grok 4.7
3264
+ * thinking mode (`low|high|max` effort, or off), Grok 4.7
2748
3265
  * (docs.x.ai/developers/grok-4-7), and Gemini 4 Argon
2749
3266
  * (ai.google.dev/gemini-api/docs/models/gemini-4-argon and docs/openai).
2750
3267
  *
@@ -2757,15 +3274,14 @@ const NO_COST = Object.freeze({
2757
3274
  cacheRead: 0,
2758
3275
  cacheWrite: 0
2759
3276
  });
2760
- const COMPLETIONS_BASE = `${DEFAULT_API_HOST}/v1`;
2761
3277
  const RESPONSES_BASE = `${DEFAULT_API_HOST}/v1`;
2762
3278
  const MESSAGES_BASE = DEFAULT_API_HOST;
2763
3279
  const GEMINI_BASE = `${DEFAULT_API_HOST}/v1beta`;
2764
3280
  /**
2765
3281
  * pi-ai thinking-level maps. A level pinned to `null` is unsupported; an
2766
3282
  * absent key keeps pi-ai's own default for that level, which is how `off`
2767
- * stays available for DeepSeek (`off` sends nothing and the deepseek dialect
2768
- * then emits `thinking: disabled`).
3283
+ * stays available for DeepSeek: pi-ai's Responses API then sends
3284
+ * `reasoning.effort: "none"`, which the gateway answers without reasoning.
2769
3285
  */
2770
3286
  const DEEPSEEK_LEVELS = Object.freeze({
2771
3287
  minimal: null,
@@ -2804,21 +3320,15 @@ const PACKYCODE_MODELS = Object.freeze([
2804
3320
  Object.freeze({
2805
3321
  id: "deepseek-flash",
2806
3322
  name: "DeepSeek Flash",
2807
- api: "openai-completions",
3323
+ api: "openai-responses",
2808
3324
  provider: PROVIDER,
2809
- baseUrl: COMPLETIONS_BASE,
3325
+ baseUrl: RESPONSES_BASE,
2810
3326
  reasoning: true,
2811
3327
  input: Object.freeze(["text", "image"]),
2812
3328
  cost: NO_COST,
2813
3329
  contextWindow: 1e6,
2814
3330
  maxTokens: 384e3,
2815
- thinkingLevelMap: DEEPSEEK_LEVELS,
2816
- compat: Object.freeze({
2817
- thinkingFormat: "deepseek",
2818
- maxTokensField: "max_tokens",
2819
- requiresReasoningContentOnAssistantMessages: true,
2820
- supportsDeveloperRole: false
2821
- })
3331
+ thinkingLevelMap: DEEPSEEK_LEVELS
2822
3332
  }),
2823
3333
  Object.freeze({
2824
3334
  id: "gpt-6.1-sol",
@@ -3365,8 +3875,7 @@ function validateImageOptions({ model = DEFAULT_IMAGE_MODEL, quality = "auto", s
3365
3875
  * plugin's own row, read through a live reference, written through
3366
3876
  * `settings.update(rowId, patch)`, and announced by `loader/volatile-update`.
3367
3877
  * 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.
3878
+ * read-only while web search, model visibility and images are editable.
3370
3879
  *
3371
3880
  * @module dsh-packycode-api/preferences
3372
3881
  */
@@ -3403,7 +3912,7 @@ function unwritable(message) {
3403
3912
  * current source and how to replace it, what to do when the section changes,
3404
3913
  * and the preference targets this fills in (host overrides, search, models).
3405
3914
  */
3406
- function connectSettings(settingsCtx, { ctx, Config, namespace, source, setSource, onChange, hostPreference, searchPreference, modelPreference, imagePreference, claudePreference }) {
3915
+ function connectSettings(settingsCtx, { ctx, Config, namespace, source, setSource, onChange, hostPreference, searchPreference, modelPreference, imagePreference }) {
3407
3916
  const settings = settingsCtx.settings;
3408
3917
  if (typeof settings?.installSection === "function") {
3409
3918
  try {
@@ -3421,7 +3930,6 @@ function connectSettings(settingsCtx, { ctx, Config, namespace, source, setSourc
3421
3930
  Object.assign(searchPreference, target);
3422
3931
  Object.assign(modelPreference, target);
3423
3932
  if (imagePreference !== void 0) Object.assign(imagePreference, target);
3424
- if (claudePreference !== void 0) Object.assign(claudePreference, target);
3425
3933
  }
3426
3934
  } catch (error) {
3427
3935
  ctx.logger?.warn?.("packycode-api: host overrides unavailable, defaults stand: %s", error.message);
@@ -3437,7 +3945,6 @@ function connectSettings(settingsCtx, { ctx, Config, namespace, source, setSourc
3437
3945
  Object.assign(searchPreference, target);
3438
3946
  Object.assign(modelPreference, target);
3439
3947
  if (imagePreference !== void 0) Object.assign(imagePreference, target);
3440
- if (claudePreference !== void 0) Object.assign(claudePreference, target);
3441
3948
  if (typeof settings.configure === "function") {
3442
3949
  const scope = typeof settingsCtx.effect === "function" ? settingsCtx : ctx;
3443
3950
  try {
@@ -4519,435 +5026,79 @@ var PackyCodeKeyRing = class {
4519
5026
  label: candidate.label,
4520
5027
  key: candidate.key,
4521
5028
  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;
5029
+ } : candidate)
5030
+ };
5031
+ }));
4760
5032
  }
5033
+ /**
5034
+ * Mark a key unusable and move the active pointer on. Called from the stream
5035
+ * guard, so the retry of the failed step resolves the next key.
5036
+ */
5037
+ async disable(id, reason) {
5038
+ return projection(await this.#write((state) => {
5039
+ const now = Date.now();
5040
+ const keys = state.keys.map((candidate) => candidate.id === id ? {
5041
+ ...candidate,
5042
+ disabledAt: now,
5043
+ disabledReason: text$1(reason) === "" ? "unusable" : text$1(reason)
5044
+ } : candidate);
5045
+ if (!keys.some((candidate) => candidate.id === id)) return {
5046
+ activeId: state.activeId,
5047
+ keys: state.keys
5048
+ };
5049
+ return {
5050
+ activeId: state.activeId === id ? (keys.find((candidate) => candidate.id !== id && candidate.disabledAt === void 0) ?? keys.find((candidate) => candidate.id !== id))?.id : state.activeId,
5051
+ keys
5052
+ };
5053
+ }));
5054
+ }
5055
+ };
5056
+ //#endregion
5057
+ //#region src/model-selection.js
5058
+ /** Profile preferences for the models advertised in PackyCode's picker. */
5059
+ /** Store exclusions so new catalog models are selected by default as well. */
5060
+ function readHiddenModels(preference) {
5061
+ const value = fieldValue(preference?.hiddenModels);
5062
+ if (!Array.isArray(value)) return [];
5063
+ const hidden = new Set(value);
5064
+ return PACKYCODE_MODEL_IDS.filter((id) => hidden.has(id));
4761
5065
  }
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;
5066
+ function createModelSelectionController({ readPreference, writePreference, isWritable, onChange }) {
5067
+ const hidden = () => readHiddenModels(readPreference());
5068
+ let advertised = hidden().join(",");
5069
+ const reconcile = () => {
5070
+ const next = hidden().join(",");
5071
+ if (next === advertised) return;
5072
+ onChange?.();
5073
+ advertised = next;
4869
5074
  };
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
- ]);
5075
+ const status = () => {
5076
+ const excluded = new Set(hidden());
4880
5077
  return {
4881
- current,
4882
- latest,
4883
- updateAvailable: compareSemver(latest, current) === 1,
4884
- install,
4885
- fetchedAt: now()
5078
+ writable: isWritable() === true,
5079
+ models: PACKYCODE_MODELS.map(({ id, name }) => ({
5080
+ id,
5081
+ name,
5082
+ selected: !excluded.has(id)
5083
+ }))
4886
5084
  };
4887
5085
  };
4888
- const cache = createCachedRead(load, {
4889
- ttlMs,
4890
- now
4891
- });
4892
5086
  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;
5087
+ status,
5088
+ reconcile,
5089
+ isVisible: (id) => !hidden().includes(id),
5090
+ async update({ selectedIds } = {}) {
5091
+ if (!Array.isArray(selectedIds) || selectedIds.some((id) => !PACKYCODE_MODEL_IDS.includes(id))) throw new Error("That PackyCode model selection is not supported");
5092
+ if (isWritable() !== true) throw new Error("PackyCode settings are not writable in this profile");
5093
+ const selected = new Set(selectedIds);
4898
5094
  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;
5095
+ await writePreference({ hiddenModels: PACKYCODE_MODEL_IDS.filter((id) => !selected.has(id)) });
5096
+ } catch {
5097
+ throw new Error("PackyCode could not save the model selection");
4948
5098
  }
4949
- },
4950
- invalidate: () => cache.invalidate()
5099
+ reconcile();
5100
+ return status();
5101
+ }
4951
5102
  });
4952
5103
  }
4953
5104
  //#endregion
@@ -5670,8 +5821,8 @@ function createGeminiApi() {
5670
5821
  * The PackyCode pi-ai provider, built in code so the user configures no route.
5671
5822
  *
5672
5823
  * `createProvider` dispatches on `model.api`, which is what lets one route
5673
- * carry Chat Completions, Responses, Anthropic Messages and native Gemini
5674
- * models — the gateway refuses some of them over other protocols.
5824
+ * carry Responses, Anthropic Messages and native Gemini models — the gateway
5825
+ * refuses some of them over other protocols.
5675
5826
  *
5676
5827
  * @module dsh-packycode-api/provider
5677
5828
  */
@@ -5715,7 +5866,6 @@ function createPackyCodeProvider({ claudeCli } = {}) {
5715
5866
  auth: keyRingAuth(),
5716
5867
  models: PACKYCODE_MODELS,
5717
5868
  api: {
5718
- "openai-completions": openAICompletionsApi(),
5719
5869
  "openai-responses": openAIResponsesApi(),
5720
5870
  "anthropic-messages": createClaudeApi({ cli: claudeCli }),
5721
5871
  "google-generative-ai": createGeminiApi()
@@ -6239,8 +6389,8 @@ function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claude,
6239
6389
  cli().install();
6240
6390
  return cli().status();
6241
6391
  },
6242
- "claude-cli/update": async (payload) => {
6243
- await cli().update({ transport: payload?.transport });
6392
+ "claude-cli/update": () => {
6393
+ cli().update();
6244
6394
  return cli().status();
6245
6395
  },
6246
6396
  "plugin/version": (payload, signal) => manager().read({
@@ -7198,12 +7348,6 @@ const Config = z.object({
7198
7348
  imageEnabled: live(z.boolean().default(true)),
7199
7349
  imageModel: live(z.union([...IMAGE_MODEL_IDS]).default(DEFAULT_IMAGE_MODEL)),
7200
7350
  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
7351
  /** Path to the Claude Code CLI; empty means "the usual install places, then PATH". */
7208
7352
  claudeCliPath: live(z.string().default(""))
7209
7353
  });
@@ -7219,7 +7363,6 @@ function apply(ctx, config) {
7219
7363
  imageEnabled: fieldValue(config?.imageEnabled),
7220
7364
  imageModel: fieldValue(config?.imageModel),
7221
7365
  imageQuality: fieldValue(config?.imageQuality),
7222
- claudeTransport: fieldValue(config?.claudeTransport),
7223
7366
  claudeCliPath: fieldValue(config?.claudeCliPath)
7224
7367
  });
7225
7368
  const hosts = () => {
@@ -7289,7 +7432,8 @@ function apply(ctx, config) {
7289
7432
  const claudeCli = createClaudeCliManager({ explicitPath: () => fieldValue(source()?.claudeCliPath) });
7290
7433
  const cliTransport = createClaudeCliTransport({
7291
7434
  binary: async () => {
7292
- const found = await claudeCli.detect();
7435
+ let found = await claudeCli.detect();
7436
+ if (isClaudeCliOutdated(found.version)) found = await claudeCli.detect({ force: true });
7293
7437
  if (typeof found.path !== "string" || found.path === "") throw Object.assign(/* @__PURE__ */ new Error("Claude Code CLI is not installed"), { code: "CLAUDE_CLI_MISSING" });
7294
7438
  return found.path;
7295
7439
  },
@@ -7299,16 +7443,8 @@ function apply(ctx, config) {
7299
7443
  ctx.effect(() => () => {
7300
7444
  cliTransport.close();
7301
7445
  }, "packycode-api: Claude Code CLI processes");
7302
- const claudePreference = {
7303
- writable: false,
7304
- set: void 0
7305
- };
7306
- const claudeTransportPreference = () => readClaudeTransport(source()?.claudeTransport);
7307
7446
  /** 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
- });
7447
+ const claudeTransportOf = (keyId) => claudeTransportFor({ groups: keyId === void 0 ? void 0 : groups.groupsOf(keyId) });
7312
7448
  const registration = ctx.llm.registerAdapter([PROVIDER], createRouteAdapter({
7313
7449
  ring,
7314
7450
  keys,
@@ -7316,7 +7452,7 @@ function apply(ctx, config) {
7316
7452
  resolveAttachments: attachments,
7317
7453
  isModelVisible: modelSelection.isVisible,
7318
7454
  claudeCli: Object.freeze({
7319
- followsGroups: () => claudeTransportPreference() === "auto",
7455
+ followsGroups: () => true,
7320
7456
  choose: ({ apiKey, model }) => isClaudeModel(model) && claudeTransportOf(keys.idOf(apiKey)).transport === "cli",
7321
7457
  fetch: (input, init, meta) => cliTransport.fetch(input, init, meta)
7322
7458
  }),
@@ -7403,8 +7539,7 @@ function apply(ctx, config) {
7403
7539
  hostPreference,
7404
7540
  searchPreference,
7405
7541
  modelPreference,
7406
- imagePreference,
7407
- claudePreference
7542
+ imagePreference
7408
7543
  }));
7409
7544
  ctx.on?.("loader/volatile-update", () => {
7410
7545
  modelSelection.reconcile();
@@ -7450,25 +7585,12 @@ function apply(ctx, config) {
7450
7585
  async status({ force = false } = {}) {
7451
7586
  return {
7452
7587
  ...await claudeCli.status({ force }),
7453
- transport: claudeTransportPreference(),
7454
- writable: claudePreference.writable === true && typeof claudePreference.set === "function",
7455
7588
  processes: cliTransport.stats()
7456
7589
  };
7457
7590
  },
7458
7591
  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
- })
7592
+ update: () => claudeCli.update(),
7593
+ transportFor: (names) => claudeTransportFor({ groups: names })
7472
7594
  }),
7473
7595
  search,
7474
7596
  modelSelection,
@@ -7512,4 +7634,4 @@ function apply(ctx, config) {
7512
7634
  });
7513
7635
  }
7514
7636
  //#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 };
7637
+ 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 };