gentle-pi 2.1.2 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -79
- package/assets/agents/gentle-ai-worker.md +7 -3
- package/assets/agents/jd-fix-agent.md +1 -1
- package/assets/agents/jd-judge-a.md +3 -1
- package/assets/agents/jd-judge-b.md +3 -1
- package/assets/agents/review-readability.md +4 -3
- package/assets/agents/review-reliability.md +4 -3
- package/assets/agents/review-resilience.md +4 -3
- package/assets/agents/review-risk.md +4 -3
- package/assets/agents/sdd-apply.md +11 -4
- package/assets/agents/sdd-archive.md +6 -1
- package/assets/agents/sdd-design.md +6 -1
- package/assets/agents/sdd-explore.md +6 -2
- package/assets/agents/sdd-init.md +10 -2
- package/assets/agents/sdd-onboard.md +6 -1
- package/assets/agents/sdd-proposal.md +8 -1
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +10 -5
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +8 -4
- package/assets/agents/sdd-verify.md +27 -2
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +2 -2
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +2 -2
- package/assets/orchestrator-delegation.md +145 -160
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +25 -48
- package/assets/sdd-orchestrator-workflow.md +163 -25
- package/assets/support/sdd-status-contract.md +24 -6
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
- package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
- package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
- package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
- package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
- package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
- package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
- package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
- package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
- package/docs/native-authority-architecture.md +14 -16
- package/docs/review-integration.md +27 -353
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +2710 -3211
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +21 -12
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +676 -781
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +836 -118
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +578 -0
- package/lib/review-integration-v2.ts +1254 -221
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +230 -67
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -79
- package/runtime/gentle-ai-binary.mjs +174 -3
- package/runtime/native-review-cli.mjs +610 -715
- package/runtime/review-integration-v2.mjs +1223 -190
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +85 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +20 -9
- package/scripts/verify-package-files.mjs +119 -43
- package/skills/_shared/review-ledger-contract.md +11 -19
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +9 -5
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/background-subagents.test.ts +771 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +16 -0
- package/tests/delegated-key-learnings-contract.test.ts +240 -0
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
- package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
- package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
- package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
- package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
- package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
- package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
- package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +159 -0
- package/tests/gentle-ai-installer.test.ts +138 -48
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +601 -0
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +75 -2
- package/tests/native-review-cli.test.ts +588 -908
- package/tests/native-review-consent.test.ts +283 -38
- package/tests/native-review-parity-runtime.test.ts +108 -350
- package/tests/native-review-parity.test.ts +684 -732
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +158 -89
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +128 -100
- package/tests/provider-contract-bundle.test.ts +385 -0
- package/tests/provider-contract-mirror.test.ts +206 -0
- package/tests/provider-defect-handoff.test.ts +252 -0
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +898 -12
- package/tests/review-compact-contract.test.ts +29 -122
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +366 -857
- package/tests/review-controller-native-routing.test.ts +1253 -4199
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +236 -70
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +134 -0
- package/tests/review-dispatch-hydration-gap.test.ts +145 -0
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +352 -0
- package/tests/review-host-relay.test.ts +754 -0
- package/tests/review-integration-v2-forward.test.ts +654 -0
- package/tests/review-integration-v2.test.ts +317 -129
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +106 -60
- package/tests/review-recovered-lineage-routing.test.ts +199 -0
- package/tests/review-relay-transport-agent.test.ts +322 -0
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +573 -180
- package/tests/sdd-agent-tools.test.ts +53 -32
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +6 -8
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- package/tests/writer-edit-surface-scope.test.ts +230 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- package/lib/git-commit-transaction.ts +0 -801
- package/lib/native-review-remediation.ts +0 -49
- package/lib/review-compact.ts +0 -947
- package/lib/review-refuter-adapter.ts +0 -129
- package/lib/review-runtime-contract.ts +0 -68
- package/prompts/gcl.md +0 -54
- package/prompts/gis.md +0 -25
- package/prompts/gpr.md +0 -41
- package/prompts/gwr.md +0 -31
- package/runtime/git-commit-transaction.mjs +0 -802
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
- package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
- package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
- package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -302
- package/tests/review-compact.test.ts +0 -243
- package/tests/review-refuter-adapter.test.ts +0 -89
package/extensions/sdd-init.ts
CHANGED
|
@@ -771,33 +771,42 @@ function ensureOpenSpecDirs(cwd: string): void {
|
|
|
771
771
|
}
|
|
772
772
|
|
|
773
773
|
export default function (pi: ExtensionAPI) {
|
|
774
|
-
pi.registerCommand("sdd-init", {
|
|
774
|
+
pi.registerCommand("gentle-sdd-init", {
|
|
775
775
|
description:
|
|
776
776
|
"Auto-detect project stack and bootstrap openspec/config.yaml for SDD.",
|
|
777
777
|
handler: async (_args: unknown, ctx: any) => {
|
|
778
|
-
await ensureSddPreflight(ctx, {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
778
|
+
const prefs = await ensureSddPreflight(ctx, { pi, installAssets: (cwd) => installSddAssets(cwd, false), applyModelConfig: () => applySavedModelConfig(ctx) }, { promptFields: [] });
|
|
779
|
+
|
|
780
|
+
const detection = detectProject(ctx.cwd);
|
|
781
|
+
const testSummary = detection.testCommand
|
|
782
|
+
? `strict TDD enabled with \`${detection.testCommand}\``
|
|
783
|
+
: "strict TDD disabled because no test runner was detected";
|
|
784
|
+
const layerSummary = `unit: ${detection.commands.unit.length}, integration: ${detection.commands.integration.length}, e2e: ${detection.commands.e2e.length}`;
|
|
785
|
+
|
|
786
|
+
const shouldCreateOpenSpec =
|
|
787
|
+
prefs.artifactStore === "openspec" ||
|
|
788
|
+
prefs.artifactStore === "hybrid";
|
|
789
|
+
if (!shouldCreateOpenSpec) {
|
|
790
|
+
ctx.ui.notify(
|
|
791
|
+
`SDD initialized for ${prefs.artifactStore}: detected ${detection.stack.join(", ") || "project"}; ${testSummary}; tests found: ${layerSummary}.`,
|
|
792
|
+
"info",
|
|
793
|
+
);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
|
|
783
797
|
const configPath = join(ctx.cwd, CONFIG_REL_PATH);
|
|
784
798
|
if (existsSync(configPath)) {
|
|
785
799
|
ctx.ui.notify(
|
|
786
|
-
`${CONFIG_REL_PATH} already exists. Edit it manually or remove it before re-running /sdd-init.`,
|
|
800
|
+
`${CONFIG_REL_PATH} already exists. Edit it manually or remove it before re-running /gentle-sdd-init.`,
|
|
787
801
|
"warning",
|
|
788
802
|
);
|
|
789
803
|
return;
|
|
790
804
|
}
|
|
791
805
|
|
|
792
|
-
const detection = detectProject(ctx.cwd);
|
|
793
806
|
ensureOpenSpecDirs(ctx.cwd);
|
|
794
807
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
795
808
|
writeFileSync(configPath, renderConfig(detection));
|
|
796
809
|
|
|
797
|
-
const testSummary = detection.testCommand
|
|
798
|
-
? `strict TDD enabled with \`${detection.testCommand}\``
|
|
799
|
-
: "strict TDD disabled because no test runner was detected";
|
|
800
|
-
const layerSummary = `unit: ${detection.commands.unit.length}, integration: ${detection.commands.integration.length}, e2e: ${detection.commands.e2e.length}`;
|
|
801
810
|
ctx.ui.notify(
|
|
802
811
|
`Wrote ${CONFIG_REL_PATH}: detected ${detection.stack.join(", ") || "project"}; ${testSummary}; tests found: ${layerSummary}.`,
|
|
803
812
|
"info",
|
|
@@ -21,7 +21,7 @@ const EXCLUDE_NAMES = new Set(["_shared", "skill-registry"]);
|
|
|
21
21
|
const EXCLUDE_PREFIXES = ["sdd-"];
|
|
22
22
|
const ATL_IGNORE_ENTRY = ".atl/";
|
|
23
23
|
const WATCH_DEBOUNCE_MS = 500;
|
|
24
|
-
const REGISTRY_SCHEMA_VERSION =
|
|
24
|
+
const REGISTRY_SCHEMA_VERSION = 7;
|
|
25
25
|
const NO_SKILL_REGISTRY_FLAG = "no-skill-registry";
|
|
26
26
|
const NO_SKILL_REGISTRY_ENV = "GENTLE_PI_NO_SKILL_REGISTRY";
|
|
27
27
|
const LEGACY_PROJECT_REGISTRY_REL_PATH = ".pi/extensions/skill-registry.ts";
|
|
@@ -253,7 +253,14 @@ async function fingerprint(files: string[]): Promise<string> {
|
|
|
253
253
|
for (const file of files) {
|
|
254
254
|
try {
|
|
255
255
|
const info = await stat(file);
|
|
256
|
-
|
|
256
|
+
let contentHash: string;
|
|
257
|
+
try {
|
|
258
|
+
contentHash = createHash("sha1").update(await readFile(file)).digest("hex");
|
|
259
|
+
} catch {
|
|
260
|
+
lines.push(`${file}:unreadable`);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
lines.push(`${file}:${info.mtimeMs}:${info.size}:${contentHash}`);
|
|
257
264
|
} catch {
|
|
258
265
|
lines.push(`${file}:missing`);
|
|
259
266
|
}
|
|
@@ -526,6 +533,7 @@ export const __testing = {
|
|
|
526
533
|
normalizeSkillDescription,
|
|
527
534
|
parseFrontmatter,
|
|
528
535
|
renderRegistry,
|
|
536
|
+
regenerateRegistry,
|
|
529
537
|
shouldSkipSkillRegistryStartup,
|
|
530
538
|
shouldSkipDuplicateExtensionLoad,
|
|
531
539
|
startSkillRegistryWatcher,
|
|
@@ -553,7 +553,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
553
553
|
if (selected.startsWith("Rose:")) config.showRose = !config.showRose;
|
|
554
554
|
else if (selected.startsWith("Text logo:")) config.showTextLogo = !config.showTextLogo;
|
|
555
555
|
else if (selected.startsWith("Color:")) {
|
|
556
|
-
|
|
556
|
+
const color = await ctx.ui.select("Startup banner color", [...BANNER_COLORS]);
|
|
557
|
+
if (!color) return;
|
|
558
|
+
config.color = color as BannerColor;
|
|
557
559
|
}
|
|
558
560
|
await writeBannerConfig(config);
|
|
559
561
|
notifyBannerConfig(ctx, config);
|
|
@@ -577,9 +579,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
577
579
|
handler: async (args, ctx) => {
|
|
578
580
|
const config = await readBannerConfig();
|
|
579
581
|
const requested = String(args ?? "").trim() as BannerColor;
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
582
|
+
if (BANNER_COLORS.includes(requested)) {
|
|
583
|
+
config.color = requested;
|
|
584
|
+
} else {
|
|
585
|
+
const selected = await ctx.ui.select("Startup banner color", [...BANNER_COLORS]);
|
|
586
|
+
if (!selected) return;
|
|
587
|
+
config.color = selected as BannerColor;
|
|
588
|
+
}
|
|
583
589
|
await writeBannerConfig(config);
|
|
584
590
|
notifyBannerConfig(ctx, config);
|
|
585
591
|
},
|
package/lib/gentle-ai-binary.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
5
|
import {
|
|
5
6
|
GENTLE_AI_INSTALL_METHOD,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
|
|
8
9
|
GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH,
|
|
9
10
|
GENTLE_AI_WINDOWS_SOURCE_TAG,
|
|
11
|
+
INSTALLER_VERSION,
|
|
10
12
|
isGentleAiWindowsGoVersionSupported,
|
|
11
13
|
isWindowsGoSumdbSourceTarget,
|
|
12
14
|
resolveGentleAiReleaseAsset,
|
|
@@ -14,7 +16,10 @@ import {
|
|
|
14
16
|
import { fileURLToPath } from "node:url";
|
|
15
17
|
|
|
16
18
|
export const GENTLE_AI_BINARY_MISSING_CODE = "package-local-binary-missing";
|
|
17
|
-
|
|
19
|
+
// Derived from the one authoritative pinned version in
|
|
20
|
+
// scripts/gentle-ai-installer.mjs rather than repeating the literal here, so
|
|
21
|
+
// the two can never independently drift apart the way they once did.
|
|
22
|
+
export const GENTLE_AI_VERSION = INSTALLER_VERSION;
|
|
18
23
|
|
|
19
24
|
export class PackageLocalGentleAiBinaryMissingError extends Error {
|
|
20
25
|
readonly code = GENTLE_AI_BINARY_MISSING_CODE;
|
|
@@ -42,6 +47,165 @@ function sha256(value: Buffer): string {
|
|
|
42
47
|
return createHash("sha256").update(value).digest("hex");
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Dev-binary override — the maintainer field-test lane.
|
|
52
|
+
//
|
|
53
|
+
// Two explicit activation paths, in precedence order:
|
|
54
|
+
// 1. GENTLE_PI_GENTLE_AI_DEV_BINARY (session override, absolute path), then
|
|
55
|
+
// 2. the persistent registration file at
|
|
56
|
+
// <GENTLE_PI_CONFIG_HOME|~/.pi/gentle-ai>/dev-binary.json with the strict
|
|
57
|
+
// shape {"schema":"gentle-pi.dev-binary/v1","path":"<absolute path>"}.
|
|
58
|
+
//
|
|
59
|
+
// The registration deliberately pins no digest: it is the unpinned field-test
|
|
60
|
+
// mode, and the binary at that path changes on every rebuild. Every resolution
|
|
61
|
+
// re-validates the file and recomputes the sha256, so a rebuilt binary is
|
|
62
|
+
// followed automatically with a fresh digest and no re-registration.
|
|
63
|
+
//
|
|
64
|
+
// Guardrails per resolution: absolute path, regular non-symlink file, POSIX
|
|
65
|
+
// executable. Any failure — including a malformed registration document or a
|
|
66
|
+
// registered-but-missing binary — is a typed error naming its origin, never a
|
|
67
|
+
// silent fallback to the pinned binary: silently running the pin while the
|
|
68
|
+
// maintainer believes he is field-testing main is the worst possible outcome.
|
|
69
|
+
// With neither activation path present, the pinned supply-chain resolution
|
|
70
|
+
// below stays byte-identical.
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
export const GENTLE_AI_DEV_BINARY_ENV = "GENTLE_PI_GENTLE_AI_DEV_BINARY";
|
|
74
|
+
export const GENTLE_AI_DEV_BINARY_REGISTRATION_SCHEMA = "gentle-pi.dev-binary/v1";
|
|
75
|
+
export const GENTLE_AI_DEV_BINARY_OVERRIDE_INVALID_CODE = "dev-binary-override-invalid";
|
|
76
|
+
|
|
77
|
+
export interface GentleAiDevBinaryEnvironment {
|
|
78
|
+
env: Record<string, string | undefined>;
|
|
79
|
+
home: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface GentleAiDevBinaryOverride {
|
|
83
|
+
source: "env" | "registration";
|
|
84
|
+
/** The env var name or registration file path that selected this binary. */
|
|
85
|
+
origin: string;
|
|
86
|
+
path: string;
|
|
87
|
+
sha256: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class GentleAiDevBinaryOverrideError extends Error {
|
|
91
|
+
readonly code = GENTLE_AI_DEV_BINARY_OVERRIDE_INVALID_CODE;
|
|
92
|
+
readonly source: "env" | "registration";
|
|
93
|
+
readonly origin: string;
|
|
94
|
+
constructor(source: "env" | "registration", origin: string, reason: string) {
|
|
95
|
+
super(`${GENTLE_AI_DEV_BINARY_OVERRIDE_INVALID_CODE}: ${origin} ${reason}. Fix or remove the override; the pinned binary is never used silently while an override is declared.`);
|
|
96
|
+
this.name = "GentleAiDevBinaryOverrideError";
|
|
97
|
+
this.source = source;
|
|
98
|
+
this.origin = origin;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let devBinaryEnvironmentTestingOverlay: GentleAiDevBinaryEnvironment | undefined;
|
|
103
|
+
|
|
104
|
+
/** Testing-only environment overlay; production code never calls this. */
|
|
105
|
+
export function setGentleAiDevBinaryEnvironmentForTesting(environment: GentleAiDevBinaryEnvironment | undefined): void {
|
|
106
|
+
devBinaryEnvironmentTestingOverlay = environment;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function ambientDevBinaryEnvironment(): GentleAiDevBinaryEnvironment {
|
|
110
|
+
return devBinaryEnvironmentTestingOverlay ?? { env: process.env, home: homedir() };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function gentleAiDevBinaryRegistrationPath(environment: GentleAiDevBinaryEnvironment = ambientDevBinaryEnvironment()): string {
|
|
114
|
+
const configHome = environment.env.GENTLE_PI_CONFIG_HOME ?? join(environment.home, ".pi", "gentle-ai");
|
|
115
|
+
return join(configHome, "dev-binary.json");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function validateDevBinary(source: "env" | "registration", origin: string, path: string, platform: string): GentleAiDevBinaryOverride {
|
|
119
|
+
if (typeof path !== "string" || path.length === 0) throw new GentleAiDevBinaryOverrideError(source, origin, "declares an empty dev binary path");
|
|
120
|
+
if (!isAbsolute(path)) throw new GentleAiDevBinaryOverrideError(source, origin, `must name an absolute path, received "${path}"`);
|
|
121
|
+
let details: ReturnType<typeof lstatSync>;
|
|
122
|
+
try {
|
|
123
|
+
details = lstatSync(path);
|
|
124
|
+
} catch {
|
|
125
|
+
throw new GentleAiDevBinaryOverrideError(source, origin, `names "${path}", which does not exist`);
|
|
126
|
+
}
|
|
127
|
+
if (!details.isFile() || details.isSymbolicLink()) throw new GentleAiDevBinaryOverrideError(source, origin, `names "${path}", which is not a regular non-symlink file`);
|
|
128
|
+
if (platform !== "win32" && (details.mode & 0o111) === 0) throw new GentleAiDevBinaryOverrideError(source, origin, `names "${path}", which is not a POSIX executable`);
|
|
129
|
+
let digest: string;
|
|
130
|
+
try {
|
|
131
|
+
digest = sha256(readFileSync(path));
|
|
132
|
+
} catch {
|
|
133
|
+
throw new GentleAiDevBinaryOverrideError(source, origin, `names "${path}", which could not be read`);
|
|
134
|
+
}
|
|
135
|
+
return { source, origin, path, sha256: digest };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function readDevBinaryRegistration(registrationPath: string): string {
|
|
139
|
+
let contents: string;
|
|
140
|
+
try {
|
|
141
|
+
contents = readFileSync(registrationPath, "utf8");
|
|
142
|
+
} catch {
|
|
143
|
+
throw new GentleAiDevBinaryOverrideError("registration", registrationPath, "could not be read");
|
|
144
|
+
}
|
|
145
|
+
let parsed: unknown;
|
|
146
|
+
try {
|
|
147
|
+
parsed = JSON.parse(contents);
|
|
148
|
+
} catch {
|
|
149
|
+
throw new GentleAiDevBinaryOverrideError("registration", registrationPath, "is not valid JSON");
|
|
150
|
+
}
|
|
151
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new GentleAiDevBinaryOverrideError("registration", registrationPath, "must be a JSON object");
|
|
152
|
+
const record = parsed as Record<string, unknown>;
|
|
153
|
+
const keys = Object.keys(record).sort();
|
|
154
|
+
if (keys.length !== 2 || keys[0] !== "path" || keys[1] !== "schema") throw new GentleAiDevBinaryOverrideError("registration", registrationPath, `must carry exactly the keys "schema" and "path"`);
|
|
155
|
+
if (record.schema !== GENTLE_AI_DEV_BINARY_REGISTRATION_SCHEMA) throw new GentleAiDevBinaryOverrideError("registration", registrationPath, `must declare schema ${GENTLE_AI_DEV_BINARY_REGISTRATION_SCHEMA}`);
|
|
156
|
+
if (typeof record.path !== "string" || record.path.length === 0) throw new GentleAiDevBinaryOverrideError("registration", registrationPath, "must declare a non-empty string path");
|
|
157
|
+
return record.path;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Resolves the active dev-binary override, if any. Returns undefined only when
|
|
162
|
+
* neither activation path is present; a present-but-invalid override always
|
|
163
|
+
* throws a typed GentleAiDevBinaryOverrideError naming its origin.
|
|
164
|
+
*/
|
|
165
|
+
export function resolveGentleAiDevBinaryOverride(
|
|
166
|
+
environment: GentleAiDevBinaryEnvironment = ambientDevBinaryEnvironment(),
|
|
167
|
+
platform = process.platform,
|
|
168
|
+
): GentleAiDevBinaryOverride | undefined {
|
|
169
|
+
const envValue = environment.env[GENTLE_AI_DEV_BINARY_ENV];
|
|
170
|
+
if (envValue !== undefined && envValue.length > 0) return validateDevBinary("env", GENTLE_AI_DEV_BINARY_ENV, envValue, platform);
|
|
171
|
+
const registrationPath = gentleAiDevBinaryRegistrationPath(environment);
|
|
172
|
+
if (!existsSync(registrationPath)) return undefined;
|
|
173
|
+
return validateDevBinary("registration", registrationPath, readDevBinaryRegistration(registrationPath), platform);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Cheap presence probe: is a dev-binary override declared at all? Used by the
|
|
178
|
+
* native CLI to select the unpinned version gate without hashing the binary.
|
|
179
|
+
* Declared-but-invalid still counts as configured — the resolution path will
|
|
180
|
+
* fail loudly with the typed error instead of quietly using the pin.
|
|
181
|
+
*/
|
|
182
|
+
export function gentleAiDevBinaryOverrideConfigured(environment: GentleAiDevBinaryEnvironment = ambientDevBinaryEnvironment()): boolean {
|
|
183
|
+
const envValue = environment.env[GENTLE_AI_DEV_BINARY_ENV];
|
|
184
|
+
if (envValue !== undefined && envValue.length > 0) return true;
|
|
185
|
+
return existsSync(gentleAiDevBinaryRegistrationPath(environment));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Validates and persistently registers a dev binary; returns the fresh override. */
|
|
189
|
+
export function registerGentleAiDevBinary(
|
|
190
|
+
path: string,
|
|
191
|
+
environment: GentleAiDevBinaryEnvironment = ambientDevBinaryEnvironment(),
|
|
192
|
+
platform = process.platform,
|
|
193
|
+
): { registrationPath: string; override: GentleAiDevBinaryOverride } {
|
|
194
|
+
const registrationPath = gentleAiDevBinaryRegistrationPath(environment);
|
|
195
|
+
const validated = validateDevBinary("registration", registrationPath, path, platform);
|
|
196
|
+
mkdirSync(dirname(registrationPath), { recursive: true });
|
|
197
|
+
writeFileSync(registrationPath, `${JSON.stringify({ schema: GENTLE_AI_DEV_BINARY_REGISTRATION_SCHEMA, path })}\n`);
|
|
198
|
+
return { registrationPath, override: validated };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Deletes the persistent registration; returns whether one existed. */
|
|
202
|
+
export function unregisterGentleAiDevBinary(environment: GentleAiDevBinaryEnvironment = ambientDevBinaryEnvironment()): boolean {
|
|
203
|
+
const registrationPath = gentleAiDevBinaryRegistrationPath(environment);
|
|
204
|
+
if (!existsSync(registrationPath)) return false;
|
|
205
|
+
rmSync(registrationPath);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
45
209
|
function isConfined(path: string, directory: string): boolean {
|
|
46
210
|
const relativePath = relative(directory, path);
|
|
47
211
|
return relativePath !== "" && !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
@@ -106,7 +270,14 @@ export function resolveGentleAiBinary(
|
|
|
106
270
|
packageRoot = dirname(dirname(fileURLToPath(import.meta.url))),
|
|
107
271
|
platform = process.platform,
|
|
108
272
|
readBinary: (path: string) => Buffer = readFileSync,
|
|
273
|
+
environment: GentleAiDevBinaryEnvironment = ambientDevBinaryEnvironment(),
|
|
109
274
|
): string {
|
|
275
|
+
// The explicit dev-binary override wins over the pinned supply-chain path.
|
|
276
|
+
// Its typed errors propagate: a declared override never falls back to the
|
|
277
|
+
// pin. Without a declared override this call returns undefined and the
|
|
278
|
+
// pinned resolution below is byte-identical to the pre-override behavior.
|
|
279
|
+
const override = resolveGentleAiDevBinaryOverride(environment, platform);
|
|
280
|
+
if (override !== undefined) return override.path;
|
|
110
281
|
const binaryPath = gentleAiBinaryPath(packageRoot, platform);
|
|
111
282
|
const versionDirectory = dirname(binaryPath);
|
|
112
283
|
const manifestPath = join(versionDirectory, "integrity.json");
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { keyHint, type AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { sanitizeTerminalText } from "./terminal-theme.ts";
|
|
4
|
+
|
|
5
|
+
type GentleAiLifecycleColor = "warning" | "success" | "error" | "dim";
|
|
6
|
+
|
|
7
|
+
export interface GentleAiRenderTheme {
|
|
8
|
+
bold(value: string): string;
|
|
9
|
+
fg(color: GentleAiLifecycleColor, value: string): string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GentleAiRenderState {
|
|
13
|
+
lifecycleComponent?: boolean; genericLocked?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface GentleAiRenderContext {
|
|
17
|
+
argsComplete?: boolean;
|
|
18
|
+
executionStarted?: boolean;
|
|
19
|
+
isPartial?: boolean;
|
|
20
|
+
isError?: boolean;
|
|
21
|
+
lastComponent?: unknown;
|
|
22
|
+
state?: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getGentleAiRenderState(state: unknown): GentleAiRenderState | undefined {
|
|
26
|
+
if (!state || typeof state !== "object" || Array.isArray(state)) return undefined;
|
|
27
|
+
const rowState = state as Record<string, unknown>, existing = rowState.gentleAiRender;
|
|
28
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) return existing as GentleAiRenderState;
|
|
29
|
+
return (rowState.gentleAiRender = {} as GentleAiRenderState);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface GentleAiResultRenderOptions {
|
|
33
|
+
expanded?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function renderGentleAiResult(
|
|
37
|
+
result: AgentToolResult<unknown>,
|
|
38
|
+
options: GentleAiResultRenderOptions,
|
|
39
|
+
): Text {
|
|
40
|
+
const textItems = result.content.flatMap((content) => (content.type === "text" ? [sanitizeTerminalText(content.text)] : []));
|
|
41
|
+
const text = textItems.join("\n");
|
|
42
|
+
const hasExpandableText = textItems.some((item) => item.length > 0);
|
|
43
|
+
const output = !hasExpandableText ? "" : options.expanded ? text : keyHint("app.tools.expand", "to expand");
|
|
44
|
+
return new Text(output, 0, 0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function renderGentleAiLifecycleCall(
|
|
48
|
+
operationPath: string,
|
|
49
|
+
theme: GentleAiRenderTheme,
|
|
50
|
+
context?: GentleAiRenderContext,
|
|
51
|
+
detail?: string,
|
|
52
|
+
): Text {
|
|
53
|
+
const status = context?.isError
|
|
54
|
+
? "failed"
|
|
55
|
+
: context?.argsComplete === false
|
|
56
|
+
? "preparing"
|
|
57
|
+
: !context?.executionStarted || context.isPartial
|
|
58
|
+
? "running"
|
|
59
|
+
: "completed";
|
|
60
|
+
const color = status === "preparing" || status === "running" ? "warning" : status === "completed" ? "success" : "error";
|
|
61
|
+
const lines = [theme.fg(color, theme.bold(`🌹︎ Gentle AI · ${status} · ${operationPath}`))];
|
|
62
|
+
if (detail) lines.push(theme.fg("dim", sanitizeTerminalText(detail)));
|
|
63
|
+
const state = getGentleAiRenderState(context?.state);
|
|
64
|
+
const component = context?.lastComponent instanceof Text && (!state || state.lifecycleComponent === true)
|
|
65
|
+
? context.lastComponent
|
|
66
|
+
: new Text("", 0, 0);
|
|
67
|
+
if (state) state.lifecycleComponent = true;
|
|
68
|
+
component.setText(lines.join("\n"));
|
|
69
|
+
return component;
|
|
70
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { access, readFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
export const THINKING_LEVELS = [
|
|
5
|
+
"off",
|
|
6
|
+
"minimal",
|
|
7
|
+
"low",
|
|
8
|
+
"medium",
|
|
9
|
+
"high",
|
|
10
|
+
"xhigh",
|
|
11
|
+
"max",
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
15
|
+
|
|
16
|
+
export interface AgentRoutingEntry {
|
|
17
|
+
model?: string;
|
|
18
|
+
thinking?: ThinkingLevel;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type AgentModelConfig = Record<string, AgentRoutingEntry>;
|
|
22
|
+
|
|
23
|
+
export type ModelConfigFileResult =
|
|
24
|
+
| { status: "missing" }
|
|
25
|
+
| { status: "invalid"; path: string }
|
|
26
|
+
| { status: "valid"; config: AgentModelConfig };
|
|
27
|
+
|
|
28
|
+
const SAFE_MODEL_ID_PATTERN = /^[A-Za-z0-9._~:@/+%-]+$/;
|
|
29
|
+
const SAFE_AGENT_NAME_PATTERN = /^[A-Za-z0-9._:@/+%-]+$/;
|
|
30
|
+
|
|
31
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
32
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
36
|
+
try {
|
|
37
|
+
await access(path);
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isThinkingLevel(value: unknown): value is ThinkingLevel {
|
|
45
|
+
return (
|
|
46
|
+
typeof value === "string" &&
|
|
47
|
+
(THINKING_LEVELS as readonly string[]).includes(value)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function normalizeModelId(value: unknown): string | undefined {
|
|
52
|
+
if (typeof value !== "string") return undefined;
|
|
53
|
+
const model = value.trim();
|
|
54
|
+
if (model.length === 0) return undefined;
|
|
55
|
+
if (!SAFE_MODEL_ID_PATTERN.test(model)) return undefined;
|
|
56
|
+
return model;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function normalizeRoutingEntry(value: unknown): AgentRoutingEntry | undefined {
|
|
60
|
+
if (typeof value === "string") {
|
|
61
|
+
const model = normalizeModelId(value);
|
|
62
|
+
return model ? { model } : undefined;
|
|
63
|
+
}
|
|
64
|
+
if (!isRecord(value)) return undefined;
|
|
65
|
+
const model = normalizeModelId(value.model);
|
|
66
|
+
const thinking = isThinkingLevel(value.thinking) ? value.thinking : undefined;
|
|
67
|
+
if (!model && !thinking) {
|
|
68
|
+
return Object.keys(value).length === 0 ? {} : undefined;
|
|
69
|
+
}
|
|
70
|
+
return { model, thinking };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function normalizeModelConfig(value: unknown): AgentModelConfig | undefined {
|
|
74
|
+
if (!isRecord(value)) return undefined;
|
|
75
|
+
const cleaned: AgentModelConfig = {};
|
|
76
|
+
for (const [name, entryValue] of Object.entries(value)) {
|
|
77
|
+
if (!SAFE_AGENT_NAME_PATTERN.test(name)) continue;
|
|
78
|
+
const entry = normalizeRoutingEntry(entryValue);
|
|
79
|
+
if (entry) cleaned[name] = entry;
|
|
80
|
+
}
|
|
81
|
+
return cleaned;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseModelConfigFileValue(value: Record<string, unknown>): AgentModelConfig {
|
|
85
|
+
const config: AgentModelConfig = {};
|
|
86
|
+
for (const [name, entryValue] of Object.entries(value)) {
|
|
87
|
+
const entry = normalizeRoutingEntry(entryValue);
|
|
88
|
+
if (entry) config[name] = entry;
|
|
89
|
+
}
|
|
90
|
+
return config;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function readModelConfigFile(path: string): ModelConfigFileResult {
|
|
94
|
+
if (!existsSync(path)) return { status: "missing" };
|
|
95
|
+
try {
|
|
96
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
97
|
+
if (!isRecord(parsed)) return { status: "invalid", path };
|
|
98
|
+
return { status: "valid", config: parseModelConfigFileValue(parsed) };
|
|
99
|
+
} catch {
|
|
100
|
+
return { status: "invalid", path };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function readModelConfigFileAsync(
|
|
105
|
+
path: string,
|
|
106
|
+
): Promise<ModelConfigFileResult> {
|
|
107
|
+
if (!(await pathExists(path))) return { status: "missing" };
|
|
108
|
+
try {
|
|
109
|
+
const parsed: unknown = JSON.parse(await readFile(path, "utf8"));
|
|
110
|
+
if (!isRecord(parsed)) return { status: "invalid", path };
|
|
111
|
+
return { status: "valid", config: parseModelConfigFileValue(parsed) };
|
|
112
|
+
} catch {
|
|
113
|
+
return { status: "invalid", path };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function readSavedModelConfig(
|
|
118
|
+
globalPath: string,
|
|
119
|
+
projectPath: string,
|
|
120
|
+
): ModelConfigFileResult {
|
|
121
|
+
const globalResult = readModelConfigFile(globalPath);
|
|
122
|
+
if (globalResult.status !== "missing") return globalResult;
|
|
123
|
+
return readModelConfigFile(projectPath);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function readSavedModelConfigAsync(
|
|
127
|
+
globalPath: string,
|
|
128
|
+
projectPath: string,
|
|
129
|
+
): Promise<ModelConfigFileResult> {
|
|
130
|
+
const globalResult = await readModelConfigFileAsync(globalPath);
|
|
131
|
+
if (globalResult.status !== "missing") return globalResult;
|
|
132
|
+
return readModelConfigFileAsync(projectPath);
|
|
133
|
+
}
|