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/AGENTS.md +8 -14
- package/CHANGELOG.md +16 -0
- package/README.en.md +34 -16
- package/README.md +34 -16
- package/docs/assets/sponsor-alipay.png +0 -0
- package/docs/assets/sponsor-wechat.png +0 -0
- package/lib/client.js +95 -78
- package/lib/index.js +660 -538
- package/package.json +3 -1
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
|
|
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
|
|
693
|
-
* text:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 }) =>
|
|
2350
|
-
binary
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
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
|
|
2456
|
-
*
|
|
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
|
|
2488
|
-
*
|
|
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
|
-
* `
|
|
2491
|
-
*
|
|
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({
|
|
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
|
|
2737
|
-
* path — the SDK appends `/responses`
|
|
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 (`
|
|
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
|
|
2768
|
-
*
|
|
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-
|
|
3323
|
+
api: "openai-responses",
|
|
2808
3324
|
provider: PROVIDER,
|
|
2809
|
-
baseUrl:
|
|
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
|
|
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
|
|
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
|
|
4763
|
-
const
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
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
|
|
4871
|
-
|
|
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
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
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
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
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
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
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
|
-
|
|
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
|
|
5674
|
-
*
|
|
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":
|
|
6243
|
-
|
|
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
|
-
|
|
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: () =>
|
|
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
|
-
|
|
7460
|
-
|
|
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,
|
|
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 };
|