vskill 0.5.104 → 0.5.105
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 +123 -2
- package/agents.json +1 -1
- package/dist/agents/agents-registry.d.ts +0 -1
- package/dist/agents/agents-registry.js +10 -30
- package/dist/agents/agents-registry.js.map +1 -1
- package/dist/api/client.d.ts +21 -0
- package/dist/api/client.js +39 -5
- package/dist/api/client.js.map +1 -1
- package/dist/commands/diff.d.ts +2 -5
- package/dist/commands/diff.js +82 -117
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/eval/serve.js +4 -0
- package/dist/commands/eval/serve.js.map +1 -1
- package/dist/commands/keys.d.ts +14 -0
- package/dist/commands/keys.js +166 -0
- package/dist/commands/keys.js.map +1 -0
- package/dist/commands/masked-stdin.d.ts +14 -0
- package/dist/commands/masked-stdin.js +79 -0
- package/dist/commands/masked-stdin.js.map +1 -0
- package/dist/commands/skill.d.ts +3 -29
- package/dist/commands/skill.js +6 -32
- package/dist/commands/skill.js.map +1 -1
- package/dist/eval/anthropic-catalog.d.ts +49 -0
- package/dist/eval/anthropic-catalog.js +238 -0
- package/dist/eval/anthropic-catalog.js.map +1 -0
- package/dist/eval/llm.d.ts +1 -1
- package/dist/eval/llm.js +68 -10
- package/dist/eval/llm.js.map +1 -1
- package/dist/eval/mcp-detector.js +24 -3
- package/dist/eval/mcp-detector.js.map +1 -1
- package/dist/eval/model-resolver.d.ts +39 -0
- package/dist/eval/model-resolver.js +94 -0
- package/dist/eval/model-resolver.js.map +1 -0
- package/dist/eval/pricing.js +42 -16
- package/dist/eval/pricing.js.map +1 -1
- package/dist/eval-server/api-routes.d.ts +8 -0
- package/dist/eval-server/api-routes.js +212 -51
- package/dist/eval-server/api-routes.js.map +1 -1
- package/dist/eval-server/authoring-routes.js +21 -1
- package/dist/eval-server/authoring-routes.js.map +1 -1
- package/dist/eval-server/boot-preflight.js +8 -10
- package/dist/eval-server/boot-preflight.js.map +1 -1
- package/dist/eval-server/darwin-migrator.d.ts +57 -0
- package/dist/eval-server/darwin-migrator.js +169 -0
- package/dist/eval-server/darwin-migrator.js.map +1 -0
- package/dist/eval-server/eval-server.d.ts +1 -0
- package/dist/eval-server/eval-server.js +10 -0
- package/dist/eval-server/eval-server.js.map +1 -1
- package/dist/eval-server/providers.d.ts +7 -12
- package/dist/eval-server/providers.js +13 -15
- package/dist/eval-server/providers.js.map +1 -1
- package/dist/eval-server/settings-store.d.ts +31 -26
- package/dist/eval-server/settings-store.js +246 -143
- package/dist/eval-server/settings-store.js.map +1 -1
- package/dist/eval-server/skill-create-routes.js +18 -0
- package/dist/eval-server/skill-create-routes.js.map +1 -1
- package/dist/eval-ui/assets/{CommandPalette-DiPALzlG.js → CommandPalette-DAWyTgXL.js} +1 -1
- package/dist/eval-ui/assets/CreateSkillPage-D5xmvnzV.js +12 -0
- package/dist/eval-ui/assets/UpdateDropdown-6bfwsX0-.js +1 -0
- package/dist/eval-ui/assets/index-C_-YXE6O.js +102 -0
- package/dist/eval-ui/index.html +1 -1
- package/dist/first-run-onboarding.d.ts +19 -0
- package/dist/first-run-onboarding.js +104 -0
- package/dist/first-run-onboarding.js.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/dist/installer/canonical.js +12 -13
- package/dist/installer/canonical.js.map +1 -1
- package/dist/utils/resolve-binary.js +7 -8
- package/dist/utils/resolve-binary.js.map +1 -1
- package/package.json +3 -1
- package/dist/eval-ui/assets/CreateSkillPage-BMrFELep.js +0 -12
- package/dist/eval-ui/assets/UpdateDropdown-Bj8kZzuR.js +0 -1
- package/dist/eval-ui/assets/_shimNode-D3bBqrAh.js +0 -1
- package/dist/eval-ui/assets/index-BSPDkfZG.js +0 -102
- package/dist/eval-ui/assets/resolve-binary-DIxhrZ6O.js +0 -2
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync, readdirSync, statSync } from "node:fs";
|
|
5
5
|
import { execSync } from "node:child_process";
|
|
6
6
|
import { join, resolve, dirname } from "node:path";
|
|
7
|
+
import { homedir } from "node:os";
|
|
7
8
|
import { sendJson, readBody } from "./router.js";
|
|
8
9
|
import { initSSE, sendSSE, sendSSEDone, withHeartbeat, startDynamicHeartbeat } from "./sse-helpers.js";
|
|
9
10
|
import { dataEventBus, emitDataEvent } from "./data-events.js";
|
|
@@ -17,6 +18,7 @@ import { classifyOrigin, scanSkillsTriScope } from "../eval/skill-scanner.js";
|
|
|
17
18
|
import { scanInstalledPluginSkills, scanAuthoredPluginSkills, } from "../eval/plugin-scanner.js";
|
|
18
19
|
import { resolveGlobalSkillsDir } from "../eval/path-utils.js";
|
|
19
20
|
import { loadAndValidateEvals, EvalValidationError } from "../eval/schema.js";
|
|
21
|
+
import { ANTHROPIC_CATALOG_SNAPSHOT } from "../eval/anthropic-catalog.js";
|
|
20
22
|
import { readBenchmark } from "../eval/benchmark.js";
|
|
21
23
|
import { writeHistoryEntry, listHistory, readHistoryEntry, computeRegressions, deleteHistoryEntry, getCaseHistory, computeStats } from "../eval/benchmark-history.js";
|
|
22
24
|
import { judgeAssertion } from "../eval/judge.js";
|
|
@@ -32,6 +34,8 @@ import { writeActivationRun, listActivationRuns, getActivationRun } from "../eva
|
|
|
32
34
|
import { AGENTS_REGISTRY, detectInstalledAgents } from "../agents/agents-registry.js";
|
|
33
35
|
import { resolveOllamaBaseUrl } from "../eval/env.js";
|
|
34
36
|
import * as settingsStore from "./settings-store.js";
|
|
37
|
+
import { isProviderId, getProviderById } from "./providers.js";
|
|
38
|
+
import { DarwinKeychainMigrator } from "./darwin-migrator.js";
|
|
35
39
|
import { loadStudioSelection, saveStudioSelection } from "./studio-json.js";
|
|
36
40
|
/**
|
|
37
41
|
* Build the response for GET /api/agents/installed.
|
|
@@ -474,18 +478,31 @@ function computeBenchmarkStatus(benchmark, evalIds, hasEvals) {
|
|
|
474
478
|
// Use overall_pass_rate as single source of truth
|
|
475
479
|
return (benchmark.overall_pass_rate ?? 0) >= 1 ? "pass" : "fail";
|
|
476
480
|
}
|
|
477
|
-
|
|
481
|
+
// 0711 — Anthropic models + pricing now derive from the dated catalog
|
|
482
|
+
// snapshot at `src/eval/anthropic-catalog.ts`. Manual maintenance of this
|
|
483
|
+
// list led to stale prices (Opus 4.7 shown at $15/$75 instead of $5/$25)
|
|
484
|
+
// because three different files held copies of the same fact. The catalog
|
|
485
|
+
// file is the single source of truth; CI fails if its snapshotDate is
|
|
486
|
+
// older than 6 months.
|
|
487
|
+
function buildAnthropicProviderModels() {
|
|
488
|
+
return ANTHROPIC_CATALOG_SNAPSHOT.models
|
|
489
|
+
.filter((m) => m.status === "active")
|
|
490
|
+
.map((m) => ({
|
|
491
|
+
id: m.id,
|
|
492
|
+
label: `${m.displayName} (API)`,
|
|
493
|
+
pricing: {
|
|
494
|
+
prompt: m.pricing.promptUsdPer1M,
|
|
495
|
+
completion: m.pricing.completionUsdPer1M,
|
|
496
|
+
},
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
499
|
+
export const PROVIDER_MODELS = {
|
|
478
500
|
"claude-cli": [
|
|
479
501
|
{ id: "sonnet", label: "Claude Sonnet" },
|
|
480
502
|
{ id: "opus", label: "Claude Opus" },
|
|
481
503
|
{ id: "haiku", label: "Claude Haiku" },
|
|
482
504
|
],
|
|
483
|
-
"anthropic":
|
|
484
|
-
{ id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6 (API)" },
|
|
485
|
-
{ id: "claude-opus-4-7", label: "Claude Opus 4.7 (API)" },
|
|
486
|
-
{ id: "claude-opus-4-6", label: "Claude Opus 4.6 (API)" },
|
|
487
|
-
{ id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5 (API)" },
|
|
488
|
-
],
|
|
505
|
+
"anthropic": buildAnthropicProviderModels(),
|
|
489
506
|
"ollama": [
|
|
490
507
|
{ id: "llama3.1:8b", label: "Llama 3.1 8B" },
|
|
491
508
|
{ id: "qwen2.5:32b", label: "Qwen 2.5 32B" },
|
|
@@ -500,6 +517,13 @@ const PROVIDER_MODELS = {
|
|
|
500
517
|
{ id: "o3", label: "OpenAI o3" },
|
|
501
518
|
{ id: "o4-mini", label: "OpenAI o4-mini" },
|
|
502
519
|
],
|
|
520
|
+
"openai": [
|
|
521
|
+
{ id: "gpt-4o-mini", label: "GPT-4o mini (API)", pricing: { prompt: 0.15, completion: 0.60 } },
|
|
522
|
+
{ id: "gpt-4o", label: "GPT-4o (API)", pricing: { prompt: 2.50, completion: 10 } },
|
|
523
|
+
{ id: "gpt-4.1", label: "GPT-4.1 (API)", pricing: { prompt: 2, completion: 8 } },
|
|
524
|
+
{ id: "gpt-4.1-mini", label: "GPT-4.1 mini (API)", pricing: { prompt: 0.40, completion: 1.60 } },
|
|
525
|
+
{ id: "o4-mini", label: "o4-mini (API)", pricing: { prompt: 1.10, completion: 4.40 } },
|
|
526
|
+
],
|
|
503
527
|
"openrouter": [
|
|
504
528
|
// Anthropic via OpenRouter
|
|
505
529
|
{ id: "anthropic/claude-opus-4", label: "Claude Opus 4 (via OpenRouter)" },
|
|
@@ -647,6 +671,25 @@ function isBinaryOnPath(name) {
|
|
|
647
671
|
return false;
|
|
648
672
|
}
|
|
649
673
|
}
|
|
674
|
+
// 0701 — Read the active Claude Code model from ~/.claude/settings.json so the
|
|
675
|
+
// Studio picker can surface "routing to claude-opus-4-7[1m]" under the generic
|
|
676
|
+
// Claude Code rows. Returns null on any read/parse failure — callers fall back
|
|
677
|
+
// to generic aliases. Re-read on every call (no caching) so toggling /model in
|
|
678
|
+
// Claude Code is reflected on the next picker open.
|
|
679
|
+
export function resolveClaudeCodeModel() {
|
|
680
|
+
try {
|
|
681
|
+
const path = join(homedir(), ".claude", "settings.json");
|
|
682
|
+
const raw = readFileSync(path, "utf8");
|
|
683
|
+
const parsed = JSON.parse(raw);
|
|
684
|
+
if (!parsed || typeof parsed !== "object")
|
|
685
|
+
return null;
|
|
686
|
+
const model = parsed.model;
|
|
687
|
+
return typeof model === "string" && model.length > 0 ? model : null;
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
650
693
|
export async function detectAvailableProviders() {
|
|
651
694
|
const providers = [];
|
|
652
695
|
// Claude CLI — delegates to the `claude` binary; the CLI owns session auth.
|
|
@@ -656,9 +699,10 @@ export async function detectAvailableProviders() {
|
|
|
656
699
|
label: "Use current Claude Code session",
|
|
657
700
|
available: true,
|
|
658
701
|
models: PROVIDER_MODELS["claude-cli"],
|
|
702
|
+
resolvedModel: resolveClaudeCodeModel(),
|
|
659
703
|
});
|
|
660
704
|
// Anthropic API — available if ANTHROPIC_API_KEY is set OR a key is in the
|
|
661
|
-
// settings-store
|
|
705
|
+
// settings-store. After 0702 the tier concept is gone; storage is file-only.
|
|
662
706
|
providers.push({
|
|
663
707
|
id: "anthropic",
|
|
664
708
|
label: "Anthropic API",
|
|
@@ -666,6 +710,14 @@ export async function detectAvailableProviders() {
|
|
|
666
710
|
settingsStore.hasKeySync("anthropic"),
|
|
667
711
|
models: PROVIDER_MODELS["anthropic"],
|
|
668
712
|
});
|
|
713
|
+
// OpenAI API — available if OPENAI_API_KEY is set OR a key is stored (0702 T-023).
|
|
714
|
+
providers.push({
|
|
715
|
+
id: "openai",
|
|
716
|
+
label: "OpenAI API",
|
|
717
|
+
available: !!process.env.OPENAI_API_KEY ||
|
|
718
|
+
settingsStore.hasKeySync("openai"),
|
|
719
|
+
models: PROVIDER_MODELS["openai"],
|
|
720
|
+
});
|
|
669
721
|
// OpenRouter — available if OPENROUTER_API_KEY is set OR a key is stored.
|
|
670
722
|
providers.push({
|
|
671
723
|
id: "openrouter",
|
|
@@ -788,9 +840,12 @@ export function registerRoutes(router, root, projectName) {
|
|
|
788
840
|
id: m.id,
|
|
789
841
|
name: m.name || m.id,
|
|
790
842
|
contextWindow: typeof m.context_length === "number" ? m.context_length : undefined,
|
|
843
|
+
// 0710 — OpenRouter publishes USD per token; canonicalize to USD per 1M
|
|
844
|
+
// tokens so the wire contract matches PROVIDER_MODELS["anthropic"]
|
|
845
|
+
// (3, 15, 75, …) and every consumer can assume one unit.
|
|
791
846
|
pricing: {
|
|
792
|
-
prompt: parseFloat(m.pricing?.prompt || "0"),
|
|
793
|
-
completion: parseFloat(m.pricing?.completion || "0"),
|
|
847
|
+
prompt: parseFloat(m.pricing?.prompt || "0") * 1_000_000,
|
|
848
|
+
completion: parseFloat(m.pricing?.completion || "0") * 1_000_000,
|
|
794
849
|
},
|
|
795
850
|
}));
|
|
796
851
|
OPENROUTER_CACHE.set(cacheKey, { value: models, fetchedAt: now });
|
|
@@ -813,6 +868,11 @@ export function registerRoutes(router, root, projectName) {
|
|
|
813
868
|
router.get("/api/settings/keys", async (_req, res) => {
|
|
814
869
|
sendJson(res, settingsStore.listKeys());
|
|
815
870
|
});
|
|
871
|
+
// 0702 T-024: expose the absolute keys.env path for the Settings footer +
|
|
872
|
+
// "Copy path" button. Key contents are NEVER returned — path only.
|
|
873
|
+
router.get("/api/settings/storage-path", async (_req, res) => {
|
|
874
|
+
sendJson(res, { path: settingsStore.getKeysFilePath() });
|
|
875
|
+
});
|
|
816
876
|
router.post("/api/settings/keys", async (req, res) => {
|
|
817
877
|
// Reject any request that smuggles the key in a query-string — JSON body only.
|
|
818
878
|
const url = req.url || "";
|
|
@@ -825,24 +885,22 @@ export function registerRoutes(router, root, projectName) {
|
|
|
825
885
|
sendJson(res, { error: "key must be non-empty string" }, 400);
|
|
826
886
|
return;
|
|
827
887
|
}
|
|
828
|
-
if (body.provider !== "
|
|
888
|
+
if (typeof body.provider !== "string" || !isProviderId(body.provider)) {
|
|
829
889
|
sendJson(res, { error: `unknown provider: ${String(body.provider)}` }, 400);
|
|
830
890
|
return;
|
|
831
891
|
}
|
|
892
|
+
const providerId = body.provider;
|
|
832
893
|
try {
|
|
833
|
-
const saved = await settingsStore.saveKey(
|
|
834
|
-
// Prefix hint — non-blocking, purely informational
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
warning = "key doesn't match typical OpenRouter prefix sk-or-";
|
|
841
|
-
}
|
|
894
|
+
const saved = await settingsStore.saveKey(providerId, body.key.trim());
|
|
895
|
+
// Prefix hint — non-blocking, purely informational. Sourced from PROVIDERS
|
|
896
|
+
// so adding a provider later automatically extends the warning.
|
|
897
|
+
const descriptor = getProviderById(providerId);
|
|
898
|
+
const warning = descriptor.keyPrefix && !body.key.startsWith(descriptor.keyPrefix)
|
|
899
|
+
? `key doesn't match typical ${descriptor.label} prefix ${descriptor.keyPrefix}`
|
|
900
|
+
: undefined;
|
|
842
901
|
sendJson(res, {
|
|
843
902
|
ok: true,
|
|
844
903
|
updatedAt: saved.updatedAt,
|
|
845
|
-
tier: saved.tier,
|
|
846
904
|
available: true,
|
|
847
905
|
...(warning ? { warning } : {}),
|
|
848
906
|
});
|
|
@@ -851,15 +909,51 @@ export function registerRoutes(router, root, projectName) {
|
|
|
851
909
|
sendJson(res, { error: err.message }, 500);
|
|
852
910
|
}
|
|
853
911
|
});
|
|
854
|
-
router.delete("/api/settings/keys/:provider", async (
|
|
855
|
-
const provider =
|
|
856
|
-
if (provider !== "
|
|
912
|
+
router.delete("/api/settings/keys/:provider", async (_req, res, params) => {
|
|
913
|
+
const provider = params.provider;
|
|
914
|
+
if (typeof provider !== "string" || !isProviderId(provider)) {
|
|
857
915
|
sendJson(res, { error: `unknown provider: ${String(provider)}` }, 400);
|
|
858
916
|
return;
|
|
859
917
|
}
|
|
860
918
|
await settingsStore.removeKey(provider);
|
|
861
919
|
sendJson(res, { ok: true });
|
|
862
920
|
});
|
|
921
|
+
// Migration — one-shot copy from pre-0702 macOS Keychain into the file store.
|
|
922
|
+
// Non-Darwin platforms short-circuit inside the migrator (no spawn).
|
|
923
|
+
router.get("/api/settings/migration-status", async (_req, res) => {
|
|
924
|
+
try {
|
|
925
|
+
const migrator = new DarwinKeychainMigrator();
|
|
926
|
+
const availability = await migrator.available();
|
|
927
|
+
sendJson(res, {
|
|
928
|
+
hasLegacyKeys: availability.hasLegacyKeys,
|
|
929
|
+
providers: availability.providers,
|
|
930
|
+
ackStatus: availability.ackStatus ?? null,
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
catch (err) {
|
|
934
|
+
sendJson(res, { error: err.message }, 500);
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
router.post("/api/settings/migration/perform", async (_req, res) => {
|
|
938
|
+
try {
|
|
939
|
+
const migrator = new DarwinKeychainMigrator();
|
|
940
|
+
const result = await migrator.migrate();
|
|
941
|
+
sendJson(res, { migrated: result.migrated });
|
|
942
|
+
}
|
|
943
|
+
catch (err) {
|
|
944
|
+
sendJson(res, { error: err.message }, 500);
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
router.post("/api/settings/migration/acknowledge", async (_req, res) => {
|
|
948
|
+
try {
|
|
949
|
+
const migrator = new DarwinKeychainMigrator();
|
|
950
|
+
await migrator.acknowledge();
|
|
951
|
+
sendJson(res, { ok: true });
|
|
952
|
+
}
|
|
953
|
+
catch (err) {
|
|
954
|
+
sendJson(res, { error: err.message }, 500);
|
|
955
|
+
}
|
|
956
|
+
});
|
|
863
957
|
// Config — expose current provider/model + available providers + project
|
|
864
958
|
// IMPORTANT: Return raw model IDs (e.g. "sonnet"), NOT display models
|
|
865
959
|
// (e.g. "claude-sonnet"). The frontend round-trips config.model back to
|
|
@@ -1074,35 +1168,59 @@ export function registerRoutes(router, root, projectName) {
|
|
|
1074
1168
|
}
|
|
1075
1169
|
return skill;
|
|
1076
1170
|
}
|
|
1077
|
-
// T-009: Versions
|
|
1171
|
+
// T-009 (proxy) + 0707 T-021 (harden): Versions endpoint
|
|
1172
|
+
//
|
|
1173
|
+
// Envelope: { versions: VersionEntry[], count: number, source: "platform" | "none" }
|
|
1174
|
+
// - source:"platform" → remote Verified-Skill platform responded
|
|
1175
|
+
// - source:"none" → skill has no VCS surface (local fixture, platform
|
|
1176
|
+
// unreachable, or platform returned non-OK). In this
|
|
1177
|
+
// case the `X-Skill-VCS: unavailable` response header
|
|
1178
|
+
// is also emitted so the UI can badge the skill as
|
|
1179
|
+
// "no version history" without treating it as an error.
|
|
1180
|
+
//
|
|
1181
|
+
// Never returns 5xx for the "no VCS surface" case — that is normal empty state.
|
|
1078
1182
|
router.get("/api/skills/:plugin/:skill/versions", async (req, res, params) => {
|
|
1079
1183
|
const fullName = resolveSkillApiName(params.skill);
|
|
1080
1184
|
const parts = fullName.split("/");
|
|
1081
1185
|
const apiPath = parts.length === 3
|
|
1082
1186
|
? `/api/v1/skills/${parts.map(encodeURIComponent).join("/")}/versions`
|
|
1083
1187
|
: `/api/v1/skills/${encodeURIComponent(fullName)}/versions`;
|
|
1188
|
+
const emptyEnvelope = () => {
|
|
1189
|
+
res.setHeader("X-Skill-VCS", "unavailable");
|
|
1190
|
+
sendJson(res, { versions: [], count: 0, source: "none" }, 200, req);
|
|
1191
|
+
};
|
|
1192
|
+
let fetchResp;
|
|
1084
1193
|
try {
|
|
1085
|
-
|
|
1194
|
+
fetchResp = await fetch(`${PLATFORM_BASE}${apiPath}`, {
|
|
1086
1195
|
signal: AbortSignal.timeout(10_000),
|
|
1087
1196
|
});
|
|
1088
|
-
if (!resp.ok) {
|
|
1089
|
-
sendJson(res, { error: "Platform API unavailable" }, 502, req);
|
|
1090
|
-
return;
|
|
1091
|
-
}
|
|
1092
|
-
const data = (await resp.json());
|
|
1093
|
-
const versions = Array.isArray(data.versions) ? data.versions : [];
|
|
1094
|
-
// Enrich with isInstalled from lockfile
|
|
1095
|
-
const lock = readLockfile();
|
|
1096
|
-
const installedVersion = lock?.skills[params.skill]?.version;
|
|
1097
|
-
const enriched = versions.map((v) => ({
|
|
1098
|
-
...v,
|
|
1099
|
-
isInstalled: installedVersion ? v.version === installedVersion : undefined,
|
|
1100
|
-
}));
|
|
1101
|
-
sendJson(res, enriched, 200, req);
|
|
1102
1197
|
}
|
|
1103
1198
|
catch {
|
|
1104
|
-
|
|
1199
|
+
// Network failure / timeout / no VCS surface → empty envelope, not 502.
|
|
1200
|
+
emptyEnvelope();
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
if (!fetchResp.ok) {
|
|
1204
|
+
emptyEnvelope();
|
|
1205
|
+
return;
|
|
1105
1206
|
}
|
|
1207
|
+
let data;
|
|
1208
|
+
try {
|
|
1209
|
+
data = (await fetchResp.json());
|
|
1210
|
+
}
|
|
1211
|
+
catch {
|
|
1212
|
+
emptyEnvelope();
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
const versions = Array.isArray(data.versions) ? data.versions : [];
|
|
1216
|
+
// Enrich with isInstalled from lockfile
|
|
1217
|
+
const lock = readLockfile();
|
|
1218
|
+
const installedVersion = lock?.skills[params.skill]?.version;
|
|
1219
|
+
const enriched = versions.map((v) => ({
|
|
1220
|
+
...v,
|
|
1221
|
+
isInstalled: installedVersion ? v.version === installedVersion : undefined,
|
|
1222
|
+
}));
|
|
1223
|
+
sendJson(res, { versions: enriched, count: enriched.length, source: "platform" }, 200, req);
|
|
1106
1224
|
});
|
|
1107
1225
|
// T-010: Diff proxy route
|
|
1108
1226
|
router.get("/api/skills/:plugin/:skill/versions/diff", async (req, res, params) => {
|
|
@@ -1378,20 +1496,35 @@ export function registerRoutes(router, root, projectName) {
|
|
|
1378
1496
|
sendJson(res, { description, rawContent: skillContent }, 200, req);
|
|
1379
1497
|
});
|
|
1380
1498
|
// Get evals.json
|
|
1499
|
+
//
|
|
1500
|
+
// Envelope (0707 T-023 / T-025):
|
|
1501
|
+
// 200 { exists: false, evals: [] } when evals.json is missing
|
|
1502
|
+
// 200 { exists: true, ...EvalsFile } when valid (evals[], skill_name, …)
|
|
1503
|
+
// 422 { error, errors[] } when malformed / fails schema
|
|
1504
|
+
// 500 { error } only for unexpected I/O failures
|
|
1505
|
+
//
|
|
1506
|
+
// "missing" must be distinguishable from "malformed" so the UI can render
|
|
1507
|
+
// the former as an empty-state CTA ("Create evals") and the latter as a
|
|
1508
|
+
// validation error panel. The earlier 400 status conflated malformed with
|
|
1509
|
+
// generic client errors — 422 Unprocessable Entity is the correct semantic
|
|
1510
|
+
// for well-formed requests whose payload fails validation.
|
|
1381
1511
|
router.get("/api/skills/:plugin/:skill/evals", async (req, res, params) => {
|
|
1382
1512
|
const skillDir = resolveSkillDir(root, params.plugin, params.skill);
|
|
1383
1513
|
const evalsPath = join(skillDir, "evals", "evals.json");
|
|
1384
1514
|
if (!existsSync(evalsPath)) {
|
|
1385
|
-
|
|
1515
|
+
// 0704: 200 empty-state sentinel (was 404) — "no evals.json yet" is
|
|
1516
|
+
// authoring empty state, not an error the client has to filter.
|
|
1517
|
+
sendJson(res, { exists: false, evals: [] }, 200, req);
|
|
1386
1518
|
return;
|
|
1387
1519
|
}
|
|
1388
1520
|
try {
|
|
1389
1521
|
const evals = loadAndValidateEvals(skillDir);
|
|
1390
|
-
sendJson(res, evals, 200, req);
|
|
1522
|
+
sendJson(res, { exists: true, ...evals }, 200, req);
|
|
1391
1523
|
}
|
|
1392
1524
|
catch (err) {
|
|
1393
1525
|
if (err instanceof EvalValidationError) {
|
|
1394
|
-
|
|
1526
|
+
// 0707 T-023: malformed → 422 (was 400).
|
|
1527
|
+
sendJson(res, { error: err.message, errors: err.errors }, 422, req);
|
|
1395
1528
|
}
|
|
1396
1529
|
else {
|
|
1397
1530
|
sendJson(res, { error: String(err.message) }, 500, req);
|
|
@@ -2022,14 +2155,19 @@ export function registerRoutes(router, root, projectName) {
|
|
|
2022
2155
|
sendJson(res, stats, 200, req);
|
|
2023
2156
|
});
|
|
2024
2157
|
// Get latest benchmark
|
|
2158
|
+
//
|
|
2159
|
+
// Envelope (0707 T-022 / T-025):
|
|
2160
|
+
// 200 null when no benchmark has been persisted
|
|
2161
|
+
// 200 <BenchmarkResult> when a benchmark exists
|
|
2162
|
+
//
|
|
2163
|
+
// Always 200 — a missing benchmark is normal empty state, not an error.
|
|
2164
|
+
// Works for any plugin slug (including dashes like `google-workspace`)
|
|
2165
|
+
// because routing uses `[^/]+` groups (see router.ts T-020).
|
|
2025
2166
|
router.get("/api/skills/:plugin/:skill/benchmark/latest", async (req, res, params) => {
|
|
2167
|
+
// 0704: always 200; body null = no benchmark persisted yet.
|
|
2026
2168
|
const skillDir = resolveSkillDir(root, params.plugin, params.skill);
|
|
2027
2169
|
const benchmark = await readBenchmark(skillDir);
|
|
2028
|
-
|
|
2029
|
-
sendJson(res, { error: "No benchmark found" }, 404, req);
|
|
2030
|
-
return;
|
|
2031
|
-
}
|
|
2032
|
-
sendJson(res, benchmark, 200, req);
|
|
2170
|
+
sendJson(res, benchmark ?? null, 200, req);
|
|
2033
2171
|
});
|
|
2034
2172
|
// Run activation test (SSE)
|
|
2035
2173
|
router.post("/api/skills/:plugin/:skill/activation-test", async (req, res, params) => {
|
|
@@ -2148,10 +2286,33 @@ Return ONLY the JSON lines, no other text.`;
|
|
|
2148
2286
|
}
|
|
2149
2287
|
});
|
|
2150
2288
|
// List activation test history (summaries only)
|
|
2289
|
+
//
|
|
2290
|
+
// Envelope (0707 T-024 / T-025):
|
|
2291
|
+
// 200 { runs: [], count: 0 } when the history log doesn't
|
|
2292
|
+
// exist / the skill has never been
|
|
2293
|
+
// activation-tested. listActivation-
|
|
2294
|
+
// Runs() catches ENOENT internally
|
|
2295
|
+
// and returns [] — see activation-
|
|
2296
|
+
// history.ts readHistoryFile().
|
|
2297
|
+
// 200 { runs: [...], count: <N> } when entries exist
|
|
2298
|
+
// 500 { error } only for unexpected I/O failures
|
|
2299
|
+
// (ENOENT is explicitly not one)
|
|
2151
2300
|
router.get("/api/skills/:plugin/:skill/activation-history", async (req, res, params) => {
|
|
2152
2301
|
const skillDir = resolveSkillDir(root, params.plugin, params.skill);
|
|
2153
|
-
|
|
2154
|
-
|
|
2302
|
+
try {
|
|
2303
|
+
const runs = await listActivationRuns(skillDir);
|
|
2304
|
+
sendJson(res, { runs, count: runs.length }, 200, req);
|
|
2305
|
+
}
|
|
2306
|
+
catch (err) {
|
|
2307
|
+
const code = err?.code;
|
|
2308
|
+
if (code === "ENOENT") {
|
|
2309
|
+
// Defensive — listActivationRuns already swallows ENOENT, but in case
|
|
2310
|
+
// a future refactor propagates it we still return the empty envelope.
|
|
2311
|
+
sendJson(res, { runs: [], count: 0 }, 200, req);
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
sendJson(res, { error: err.message }, 500, req);
|
|
2315
|
+
}
|
|
2155
2316
|
});
|
|
2156
2317
|
// Get full activation test run by ID
|
|
2157
2318
|
router.get("/api/skills/:plugin/:skill/activation-history/:runId", async (req, res, params) => {
|