vskill 0.5.104 → 0.5.106
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 +9 -0
- package/dist/eval-server/api-routes.js +241 -73
- 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 +21 -0
- package/dist/eval-server/eval-server.js.map +1 -1
- package/dist/eval-server/integration-routes.js +7 -0
- package/dist/eval-server/integration-routes.js.map +1 -1
- package/dist/eval-server/platform-proxy.d.ts +18 -0
- package/dist/eval-server/platform-proxy.js +153 -0
- package/dist/eval-server/platform-proxy.js.map +1 -0
- 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-server/skill-name-resolver.d.ts +35 -0
- package/dist/eval-server/skill-name-resolver.js +146 -0
- package/dist/eval-server/skill-name-resolver.js.map +1 -0
- package/dist/eval-ui/assets/{CommandPalette-DiPALzlG.js → CommandPalette-COqdrmRl.js} +1 -1
- package/dist/eval-ui/assets/CreateSkillPage-C3IjO8es.js +12 -0
- package/dist/eval-ui/assets/UpdateDropdown-DnKKMBBN.js +1 -0
- package/dist/eval-ui/assets/index-Dmja1p3A.css +1 -0
- package/dist/eval-ui/assets/index-KIcQ5e5a.js +102 -0
- package/dist/eval-ui/index.html +2 -2
- 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 +5 -2
- 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/index-C0Gc_4KC.css +0 -1
- package/dist/eval-ui/assets/resolve-binary-DIxhrZ6O.js +0 -2
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// api-routes.ts -- REST API route handlers for the eval UI
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync,
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, 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";
|
|
10
11
|
import { classifyError } from "./error-classifier.js";
|
|
11
12
|
import { readLockfile } from "../lockfile/lockfile.js";
|
|
12
|
-
import {
|
|
13
|
+
import { resolveSkillApiName as resolveSkillApiNameImpl } from "./skill-name-resolver.js";
|
|
13
14
|
import { runBenchmarkSSE, runSingleCaseSSE } from "./benchmark-runner.js";
|
|
14
15
|
import { getSkillSemaphore } from "./concurrency.js";
|
|
15
16
|
import { resolveSkillDir } from "./skill-resolver.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, findAnthropicModel } 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,41 @@ 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
|
+
function aliasInfo(alias, fallbackLabel) {
|
|
500
|
+
const entry = findAnthropicModel(alias);
|
|
501
|
+
if (!entry)
|
|
502
|
+
return { label: fallbackLabel };
|
|
503
|
+
return { label: entry.displayName, resolvedId: entry.id };
|
|
504
|
+
}
|
|
505
|
+
export const PROVIDER_MODELS = {
|
|
506
|
+
// Opus first so it is the default when no override is set
|
|
507
|
+
// (getEffectiveRawModel returns models[0]). Labels come from the catalog so
|
|
508
|
+
// the picker shows the exact dated version (e.g. "Claude Opus 4.7"), not the
|
|
509
|
+
// bare family name — keeps the Studio truthful when a model is bumped.
|
|
478
510
|
"claude-cli": [
|
|
479
|
-
{ id: "
|
|
480
|
-
{ id: "
|
|
481
|
-
{ id: "haiku",
|
|
482
|
-
],
|
|
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)" },
|
|
511
|
+
{ id: "opus", ...aliasInfo("opus", "Claude Opus") },
|
|
512
|
+
{ id: "sonnet", ...aliasInfo("sonnet", "Claude Sonnet") },
|
|
513
|
+
{ id: "haiku", ...aliasInfo("haiku", "Claude Haiku") },
|
|
488
514
|
],
|
|
515
|
+
"anthropic": buildAnthropicProviderModels(),
|
|
489
516
|
"ollama": [
|
|
490
517
|
{ id: "llama3.1:8b", label: "Llama 3.1 8B" },
|
|
491
518
|
{ id: "qwen2.5:32b", label: "Qwen 2.5 32B" },
|
|
@@ -500,6 +527,13 @@ const PROVIDER_MODELS = {
|
|
|
500
527
|
{ id: "o3", label: "OpenAI o3" },
|
|
501
528
|
{ id: "o4-mini", label: "OpenAI o4-mini" },
|
|
502
529
|
],
|
|
530
|
+
"openai": [
|
|
531
|
+
{ id: "gpt-4o-mini", label: "GPT-4o mini (API)", pricing: { prompt: 0.15, completion: 0.60 } },
|
|
532
|
+
{ id: "gpt-4o", label: "GPT-4o (API)", pricing: { prompt: 2.50, completion: 10 } },
|
|
533
|
+
{ id: "gpt-4.1", label: "GPT-4.1 (API)", pricing: { prompt: 2, completion: 8 } },
|
|
534
|
+
{ id: "gpt-4.1-mini", label: "GPT-4.1 mini (API)", pricing: { prompt: 0.40, completion: 1.60 } },
|
|
535
|
+
{ id: "o4-mini", label: "o4-mini (API)", pricing: { prompt: 1.10, completion: 4.40 } },
|
|
536
|
+
],
|
|
503
537
|
"openrouter": [
|
|
504
538
|
// Anthropic via OpenRouter
|
|
505
539
|
{ id: "anthropic/claude-opus-4", label: "Claude Opus 4 (via OpenRouter)" },
|
|
@@ -647,6 +681,25 @@ function isBinaryOnPath(name) {
|
|
|
647
681
|
return false;
|
|
648
682
|
}
|
|
649
683
|
}
|
|
684
|
+
// 0701 — Read the active Claude Code model from ~/.claude/settings.json so the
|
|
685
|
+
// Studio picker can surface "routing to claude-opus-4-7[1m]" under the generic
|
|
686
|
+
// Claude Code rows. Returns null on any read/parse failure — callers fall back
|
|
687
|
+
// to generic aliases. Re-read on every call (no caching) so toggling /model in
|
|
688
|
+
// Claude Code is reflected on the next picker open.
|
|
689
|
+
export function resolveClaudeCodeModel() {
|
|
690
|
+
try {
|
|
691
|
+
const path = join(homedir(), ".claude", "settings.json");
|
|
692
|
+
const raw = readFileSync(path, "utf8");
|
|
693
|
+
const parsed = JSON.parse(raw);
|
|
694
|
+
if (!parsed || typeof parsed !== "object")
|
|
695
|
+
return null;
|
|
696
|
+
const model = parsed.model;
|
|
697
|
+
return typeof model === "string" && model.length > 0 ? model : null;
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
650
703
|
export async function detectAvailableProviders() {
|
|
651
704
|
const providers = [];
|
|
652
705
|
// Claude CLI — delegates to the `claude` binary; the CLI owns session auth.
|
|
@@ -656,9 +709,10 @@ export async function detectAvailableProviders() {
|
|
|
656
709
|
label: "Use current Claude Code session",
|
|
657
710
|
available: true,
|
|
658
711
|
models: PROVIDER_MODELS["claude-cli"],
|
|
712
|
+
resolvedModel: resolveClaudeCodeModel(),
|
|
659
713
|
});
|
|
660
714
|
// Anthropic API — available if ANTHROPIC_API_KEY is set OR a key is in the
|
|
661
|
-
// settings-store
|
|
715
|
+
// settings-store. After 0702 the tier concept is gone; storage is file-only.
|
|
662
716
|
providers.push({
|
|
663
717
|
id: "anthropic",
|
|
664
718
|
label: "Anthropic API",
|
|
@@ -666,6 +720,14 @@ export async function detectAvailableProviders() {
|
|
|
666
720
|
settingsStore.hasKeySync("anthropic"),
|
|
667
721
|
models: PROVIDER_MODELS["anthropic"],
|
|
668
722
|
});
|
|
723
|
+
// OpenAI API — available if OPENAI_API_KEY is set OR a key is stored (0702 T-023).
|
|
724
|
+
providers.push({
|
|
725
|
+
id: "openai",
|
|
726
|
+
label: "OpenAI API",
|
|
727
|
+
available: !!process.env.OPENAI_API_KEY ||
|
|
728
|
+
settingsStore.hasKeySync("openai"),
|
|
729
|
+
models: PROVIDER_MODELS["openai"],
|
|
730
|
+
});
|
|
669
731
|
// OpenRouter — available if OPENROUTER_API_KEY is set OR a key is stored.
|
|
670
732
|
providers.push({
|
|
671
733
|
id: "openrouter",
|
|
@@ -788,9 +850,12 @@ export function registerRoutes(router, root, projectName) {
|
|
|
788
850
|
id: m.id,
|
|
789
851
|
name: m.name || m.id,
|
|
790
852
|
contextWindow: typeof m.context_length === "number" ? m.context_length : undefined,
|
|
853
|
+
// 0710 — OpenRouter publishes USD per token; canonicalize to USD per 1M
|
|
854
|
+
// tokens so the wire contract matches PROVIDER_MODELS["anthropic"]
|
|
855
|
+
// (3, 15, 75, …) and every consumer can assume one unit.
|
|
791
856
|
pricing: {
|
|
792
|
-
prompt: parseFloat(m.pricing?.prompt || "0"),
|
|
793
|
-
completion: parseFloat(m.pricing?.completion || "0"),
|
|
857
|
+
prompt: parseFloat(m.pricing?.prompt || "0") * 1_000_000,
|
|
858
|
+
completion: parseFloat(m.pricing?.completion || "0") * 1_000_000,
|
|
794
859
|
},
|
|
795
860
|
}));
|
|
796
861
|
OPENROUTER_CACHE.set(cacheKey, { value: models, fetchedAt: now });
|
|
@@ -813,6 +878,11 @@ export function registerRoutes(router, root, projectName) {
|
|
|
813
878
|
router.get("/api/settings/keys", async (_req, res) => {
|
|
814
879
|
sendJson(res, settingsStore.listKeys());
|
|
815
880
|
});
|
|
881
|
+
// 0702 T-024: expose the absolute keys.env path for the Settings footer +
|
|
882
|
+
// "Copy path" button. Key contents are NEVER returned — path only.
|
|
883
|
+
router.get("/api/settings/storage-path", async (_req, res) => {
|
|
884
|
+
sendJson(res, { path: settingsStore.getKeysFilePath() });
|
|
885
|
+
});
|
|
816
886
|
router.post("/api/settings/keys", async (req, res) => {
|
|
817
887
|
// Reject any request that smuggles the key in a query-string — JSON body only.
|
|
818
888
|
const url = req.url || "";
|
|
@@ -825,24 +895,22 @@ export function registerRoutes(router, root, projectName) {
|
|
|
825
895
|
sendJson(res, { error: "key must be non-empty string" }, 400);
|
|
826
896
|
return;
|
|
827
897
|
}
|
|
828
|
-
if (body.provider !== "
|
|
898
|
+
if (typeof body.provider !== "string" || !isProviderId(body.provider)) {
|
|
829
899
|
sendJson(res, { error: `unknown provider: ${String(body.provider)}` }, 400);
|
|
830
900
|
return;
|
|
831
901
|
}
|
|
902
|
+
const providerId = body.provider;
|
|
832
903
|
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
|
-
}
|
|
904
|
+
const saved = await settingsStore.saveKey(providerId, body.key.trim());
|
|
905
|
+
// Prefix hint — non-blocking, purely informational. Sourced from PROVIDERS
|
|
906
|
+
// so adding a provider later automatically extends the warning.
|
|
907
|
+
const descriptor = getProviderById(providerId);
|
|
908
|
+
const warning = descriptor.keyPrefix && !body.key.startsWith(descriptor.keyPrefix)
|
|
909
|
+
? `key doesn't match typical ${descriptor.label} prefix ${descriptor.keyPrefix}`
|
|
910
|
+
: undefined;
|
|
842
911
|
sendJson(res, {
|
|
843
912
|
ok: true,
|
|
844
913
|
updatedAt: saved.updatedAt,
|
|
845
|
-
tier: saved.tier,
|
|
846
914
|
available: true,
|
|
847
915
|
...(warning ? { warning } : {}),
|
|
848
916
|
});
|
|
@@ -851,15 +919,51 @@ export function registerRoutes(router, root, projectName) {
|
|
|
851
919
|
sendJson(res, { error: err.message }, 500);
|
|
852
920
|
}
|
|
853
921
|
});
|
|
854
|
-
router.delete("/api/settings/keys/:provider", async (
|
|
855
|
-
const provider =
|
|
856
|
-
if (provider !== "
|
|
922
|
+
router.delete("/api/settings/keys/:provider", async (_req, res, params) => {
|
|
923
|
+
const provider = params.provider;
|
|
924
|
+
if (typeof provider !== "string" || !isProviderId(provider)) {
|
|
857
925
|
sendJson(res, { error: `unknown provider: ${String(provider)}` }, 400);
|
|
858
926
|
return;
|
|
859
927
|
}
|
|
860
928
|
await settingsStore.removeKey(provider);
|
|
861
929
|
sendJson(res, { ok: true });
|
|
862
930
|
});
|
|
931
|
+
// Migration — one-shot copy from pre-0702 macOS Keychain into the file store.
|
|
932
|
+
// Non-Darwin platforms short-circuit inside the migrator (no spawn).
|
|
933
|
+
router.get("/api/settings/migration-status", async (_req, res) => {
|
|
934
|
+
try {
|
|
935
|
+
const migrator = new DarwinKeychainMigrator();
|
|
936
|
+
const availability = await migrator.available();
|
|
937
|
+
sendJson(res, {
|
|
938
|
+
hasLegacyKeys: availability.hasLegacyKeys,
|
|
939
|
+
providers: availability.providers,
|
|
940
|
+
ackStatus: availability.ackStatus ?? null,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
catch (err) {
|
|
944
|
+
sendJson(res, { error: err.message }, 500);
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
router.post("/api/settings/migration/perform", async (_req, res) => {
|
|
948
|
+
try {
|
|
949
|
+
const migrator = new DarwinKeychainMigrator();
|
|
950
|
+
const result = await migrator.migrate();
|
|
951
|
+
sendJson(res, { migrated: result.migrated });
|
|
952
|
+
}
|
|
953
|
+
catch (err) {
|
|
954
|
+
sendJson(res, { error: err.message }, 500);
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
router.post("/api/settings/migration/acknowledge", async (_req, res) => {
|
|
958
|
+
try {
|
|
959
|
+
const migrator = new DarwinKeychainMigrator();
|
|
960
|
+
await migrator.acknowledge();
|
|
961
|
+
sendJson(res, { ok: true });
|
|
962
|
+
}
|
|
963
|
+
catch (err) {
|
|
964
|
+
sendJson(res, { error: err.message }, 500);
|
|
965
|
+
}
|
|
966
|
+
});
|
|
863
967
|
// Config — expose current provider/model + available providers + project
|
|
864
968
|
// IMPORTANT: Return raw model IDs (e.g. "sonnet"), NOT display models
|
|
865
969
|
// (e.g. "claude-sonnet"). The frontend round-trips config.model back to
|
|
@@ -1060,49 +1164,67 @@ export function registerRoutes(router, root, projectName) {
|
|
|
1060
1164
|
// MUST be registered BEFORE the /:plugin/:skill catch-all below.
|
|
1061
1165
|
// -------------------------------------------------------------------------
|
|
1062
1166
|
const PLATFORM_BASE = "https://verified-skill.com";
|
|
1063
|
-
/**
|
|
1167
|
+
/**
|
|
1168
|
+
* Resolve plugin/skill to full hierarchical API name. Lockfile path first
|
|
1169
|
+
* (installed skills); falls back to authored-skill discovery on disk +
|
|
1170
|
+
* git remote parse for skills authored in this repo. See skill-name-resolver.ts.
|
|
1171
|
+
*/
|
|
1064
1172
|
function resolveSkillApiName(skill) {
|
|
1065
|
-
|
|
1066
|
-
if (!lock)
|
|
1067
|
-
return skill;
|
|
1068
|
-
const entry = lock.skills[skill];
|
|
1069
|
-
if (!entry?.source)
|
|
1070
|
-
return skill;
|
|
1071
|
-
const parsed = parseSource(entry.source);
|
|
1072
|
-
if (parsed.type === "github" || parsed.type === "github-plugin" || parsed.type === "marketplace") {
|
|
1073
|
-
return `${parsed.owner}/${parsed.repo}/${skill}`;
|
|
1074
|
-
}
|
|
1075
|
-
return skill;
|
|
1173
|
+
return resolveSkillApiNameImpl(skill, root);
|
|
1076
1174
|
}
|
|
1077
|
-
// T-009: Versions
|
|
1175
|
+
// T-009 (proxy) + 0707 T-021 (harden): Versions endpoint
|
|
1176
|
+
//
|
|
1177
|
+
// Envelope: { versions: VersionEntry[], count: number, source: "platform" | "none" }
|
|
1178
|
+
// - source:"platform" → remote Verified-Skill platform responded
|
|
1179
|
+
// - source:"none" → skill has no VCS surface (local fixture, platform
|
|
1180
|
+
// unreachable, or platform returned non-OK). In this
|
|
1181
|
+
// case the `X-Skill-VCS: unavailable` response header
|
|
1182
|
+
// is also emitted so the UI can badge the skill as
|
|
1183
|
+
// "no version history" without treating it as an error.
|
|
1184
|
+
//
|
|
1185
|
+
// Never returns 5xx for the "no VCS surface" case — that is normal empty state.
|
|
1078
1186
|
router.get("/api/skills/:plugin/:skill/versions", async (req, res, params) => {
|
|
1079
|
-
const fullName = resolveSkillApiName(params.skill);
|
|
1187
|
+
const fullName = await resolveSkillApiName(params.skill);
|
|
1080
1188
|
const parts = fullName.split("/");
|
|
1081
1189
|
const apiPath = parts.length === 3
|
|
1082
1190
|
? `/api/v1/skills/${parts.map(encodeURIComponent).join("/")}/versions`
|
|
1083
1191
|
: `/api/v1/skills/${encodeURIComponent(fullName)}/versions`;
|
|
1192
|
+
const emptyEnvelope = () => {
|
|
1193
|
+
res.setHeader("X-Skill-VCS", "unavailable");
|
|
1194
|
+
sendJson(res, { versions: [], count: 0, source: "none" }, 200, req);
|
|
1195
|
+
};
|
|
1196
|
+
let fetchResp;
|
|
1084
1197
|
try {
|
|
1085
|
-
|
|
1198
|
+
fetchResp = await fetch(`${PLATFORM_BASE}${apiPath}`, {
|
|
1086
1199
|
signal: AbortSignal.timeout(10_000),
|
|
1087
1200
|
});
|
|
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
1201
|
}
|
|
1103
1202
|
catch {
|
|
1104
|
-
|
|
1203
|
+
// Network failure / timeout / no VCS surface → empty envelope, not 502.
|
|
1204
|
+
emptyEnvelope();
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
if (!fetchResp.ok) {
|
|
1208
|
+
emptyEnvelope();
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
let data;
|
|
1212
|
+
try {
|
|
1213
|
+
data = (await fetchResp.json());
|
|
1214
|
+
}
|
|
1215
|
+
catch {
|
|
1216
|
+
emptyEnvelope();
|
|
1217
|
+
return;
|
|
1105
1218
|
}
|
|
1219
|
+
const versions = Array.isArray(data.versions) ? data.versions : [];
|
|
1220
|
+
// Enrich with isInstalled from lockfile
|
|
1221
|
+
const lock = readLockfile();
|
|
1222
|
+
const installedVersion = lock?.skills[params.skill]?.version;
|
|
1223
|
+
const enriched = versions.map((v) => ({
|
|
1224
|
+
...v,
|
|
1225
|
+
isInstalled: installedVersion ? v.version === installedVersion : undefined,
|
|
1226
|
+
}));
|
|
1227
|
+
sendJson(res, { versions: enriched, count: enriched.length, source: "platform" }, 200, req);
|
|
1106
1228
|
});
|
|
1107
1229
|
// T-010: Diff proxy route
|
|
1108
1230
|
router.get("/api/skills/:plugin/:skill/versions/diff", async (req, res, params) => {
|
|
@@ -1113,11 +1235,11 @@ export function registerRoutes(router, root, projectName) {
|
|
|
1113
1235
|
sendJson(res, { error: "Missing required query params: from and to" }, 400, req);
|
|
1114
1236
|
return;
|
|
1115
1237
|
}
|
|
1116
|
-
const fullName = resolveSkillApiName(params.skill);
|
|
1238
|
+
const fullName = await resolveSkillApiName(params.skill);
|
|
1117
1239
|
const parts = fullName.split("/");
|
|
1118
1240
|
const basePath = parts.length === 3
|
|
1119
|
-
? `/api/v1/skills/${parts.map(encodeURIComponent).join("/")}/versions`
|
|
1120
|
-
: `/api/v1/skills/${encodeURIComponent(fullName)}/versions`;
|
|
1241
|
+
? `/api/v1/skills/${parts.map(encodeURIComponent).join("/")}/versions/diff`
|
|
1242
|
+
: `/api/v1/skills/${encodeURIComponent(fullName)}/versions/diff`;
|
|
1121
1243
|
try {
|
|
1122
1244
|
const resp = await fetch(`${PLATFORM_BASE}${basePath}?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`, { signal: AbortSignal.timeout(10_000) });
|
|
1123
1245
|
if (!resp.ok) {
|
|
@@ -1357,7 +1479,10 @@ export function registerRoutes(router, root, projectName) {
|
|
|
1357
1479
|
return;
|
|
1358
1480
|
}
|
|
1359
1481
|
try {
|
|
1360
|
-
|
|
1482
|
+
// 0722: route to OS trash (Trash / Recycle Bin / XDG) instead of hard delete
|
|
1483
|
+
// so accidental deletes are recoverable from the user's native Trash app.
|
|
1484
|
+
const trash = (await import("trash")).default;
|
|
1485
|
+
await trash([skillDir]);
|
|
1361
1486
|
sendJson(res, { ok: true, deleted: `${params.plugin}/${params.skill}` }, 200, req);
|
|
1362
1487
|
}
|
|
1363
1488
|
catch (err) {
|
|
@@ -1378,20 +1503,35 @@ export function registerRoutes(router, root, projectName) {
|
|
|
1378
1503
|
sendJson(res, { description, rawContent: skillContent }, 200, req);
|
|
1379
1504
|
});
|
|
1380
1505
|
// Get evals.json
|
|
1506
|
+
//
|
|
1507
|
+
// Envelope (0707 T-023 / T-025):
|
|
1508
|
+
// 200 { exists: false, evals: [] } when evals.json is missing
|
|
1509
|
+
// 200 { exists: true, ...EvalsFile } when valid (evals[], skill_name, …)
|
|
1510
|
+
// 422 { error, errors[] } when malformed / fails schema
|
|
1511
|
+
// 500 { error } only for unexpected I/O failures
|
|
1512
|
+
//
|
|
1513
|
+
// "missing" must be distinguishable from "malformed" so the UI can render
|
|
1514
|
+
// the former as an empty-state CTA ("Create evals") and the latter as a
|
|
1515
|
+
// validation error panel. The earlier 400 status conflated malformed with
|
|
1516
|
+
// generic client errors — 422 Unprocessable Entity is the correct semantic
|
|
1517
|
+
// for well-formed requests whose payload fails validation.
|
|
1381
1518
|
router.get("/api/skills/:plugin/:skill/evals", async (req, res, params) => {
|
|
1382
1519
|
const skillDir = resolveSkillDir(root, params.plugin, params.skill);
|
|
1383
1520
|
const evalsPath = join(skillDir, "evals", "evals.json");
|
|
1384
1521
|
if (!existsSync(evalsPath)) {
|
|
1385
|
-
|
|
1522
|
+
// 0704: 200 empty-state sentinel (was 404) — "no evals.json yet" is
|
|
1523
|
+
// authoring empty state, not an error the client has to filter.
|
|
1524
|
+
sendJson(res, { exists: false, evals: [] }, 200, req);
|
|
1386
1525
|
return;
|
|
1387
1526
|
}
|
|
1388
1527
|
try {
|
|
1389
1528
|
const evals = loadAndValidateEvals(skillDir);
|
|
1390
|
-
sendJson(res, evals, 200, req);
|
|
1529
|
+
sendJson(res, { exists: true, ...evals }, 200, req);
|
|
1391
1530
|
}
|
|
1392
1531
|
catch (err) {
|
|
1393
1532
|
if (err instanceof EvalValidationError) {
|
|
1394
|
-
|
|
1533
|
+
// 0707 T-023: malformed → 422 (was 400).
|
|
1534
|
+
sendJson(res, { error: err.message, errors: err.errors }, 422, req);
|
|
1395
1535
|
}
|
|
1396
1536
|
else {
|
|
1397
1537
|
sendJson(res, { error: String(err.message) }, 500, req);
|
|
@@ -2022,14 +2162,19 @@ export function registerRoutes(router, root, projectName) {
|
|
|
2022
2162
|
sendJson(res, stats, 200, req);
|
|
2023
2163
|
});
|
|
2024
2164
|
// Get latest benchmark
|
|
2165
|
+
//
|
|
2166
|
+
// Envelope (0707 T-022 / T-025):
|
|
2167
|
+
// 200 null when no benchmark has been persisted
|
|
2168
|
+
// 200 <BenchmarkResult> when a benchmark exists
|
|
2169
|
+
//
|
|
2170
|
+
// Always 200 — a missing benchmark is normal empty state, not an error.
|
|
2171
|
+
// Works for any plugin slug (including dashes like `google-workspace`)
|
|
2172
|
+
// because routing uses `[^/]+` groups (see router.ts T-020).
|
|
2025
2173
|
router.get("/api/skills/:plugin/:skill/benchmark/latest", async (req, res, params) => {
|
|
2174
|
+
// 0704: always 200; body null = no benchmark persisted yet.
|
|
2026
2175
|
const skillDir = resolveSkillDir(root, params.plugin, params.skill);
|
|
2027
2176
|
const benchmark = await readBenchmark(skillDir);
|
|
2028
|
-
|
|
2029
|
-
sendJson(res, { error: "No benchmark found" }, 404, req);
|
|
2030
|
-
return;
|
|
2031
|
-
}
|
|
2032
|
-
sendJson(res, benchmark, 200, req);
|
|
2177
|
+
sendJson(res, benchmark ?? null, 200, req);
|
|
2033
2178
|
});
|
|
2034
2179
|
// Run activation test (SSE)
|
|
2035
2180
|
router.post("/api/skills/:plugin/:skill/activation-test", async (req, res, params) => {
|
|
@@ -2148,10 +2293,33 @@ Return ONLY the JSON lines, no other text.`;
|
|
|
2148
2293
|
}
|
|
2149
2294
|
});
|
|
2150
2295
|
// List activation test history (summaries only)
|
|
2296
|
+
//
|
|
2297
|
+
// Envelope (0707 T-024 / T-025):
|
|
2298
|
+
// 200 { runs: [], count: 0 } when the history log doesn't
|
|
2299
|
+
// exist / the skill has never been
|
|
2300
|
+
// activation-tested. listActivation-
|
|
2301
|
+
// Runs() catches ENOENT internally
|
|
2302
|
+
// and returns [] — see activation-
|
|
2303
|
+
// history.ts readHistoryFile().
|
|
2304
|
+
// 200 { runs: [...], count: <N> } when entries exist
|
|
2305
|
+
// 500 { error } only for unexpected I/O failures
|
|
2306
|
+
// (ENOENT is explicitly not one)
|
|
2151
2307
|
router.get("/api/skills/:plugin/:skill/activation-history", async (req, res, params) => {
|
|
2152
2308
|
const skillDir = resolveSkillDir(root, params.plugin, params.skill);
|
|
2153
|
-
|
|
2154
|
-
|
|
2309
|
+
try {
|
|
2310
|
+
const runs = await listActivationRuns(skillDir);
|
|
2311
|
+
sendJson(res, { runs, count: runs.length }, 200, req);
|
|
2312
|
+
}
|
|
2313
|
+
catch (err) {
|
|
2314
|
+
const code = err?.code;
|
|
2315
|
+
if (code === "ENOENT") {
|
|
2316
|
+
// Defensive — listActivationRuns already swallows ENOENT, but in case
|
|
2317
|
+
// a future refactor propagates it we still return the empty envelope.
|
|
2318
|
+
sendJson(res, { runs: [], count: 0 }, 200, req);
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
sendJson(res, { error: err.message }, 500, req);
|
|
2322
|
+
}
|
|
2155
2323
|
});
|
|
2156
2324
|
// Get full activation test run by ID
|
|
2157
2325
|
router.get("/api/skills/:plugin/:skill/activation-history/:runId", async (req, res, params) => {
|