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