vskill 0.5.145 → 0.5.146

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/agents.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "generatedAt": "2026-04-26T21:53:04.704Z",
3
+ "generatedAt": "2026-04-26T22:06:59.883Z",
4
4
  "agentPrefixes": [
5
5
  ".adal",
6
6
  ".agent",
@@ -9,7 +9,7 @@ import { sendJson, readBody } from "./router.js";
9
9
  import { initSSE, sendSSE, sendSSEDone, withHeartbeat, startDynamicHeartbeat } from "./sse-helpers.js";
10
10
  import { dataEventBus, emitDataEvent } from "./data-events.js";
11
11
  import { classifyError } from "./error-classifier.js";
12
- import { readLockfile } from "../lockfile/lockfile.js";
12
+ import { readLockfile, writeLockfile } from "../lockfile/lockfile.js";
13
13
  import { resolveSkillApiName as resolveSkillApiNameImpl } from "./skill-name-resolver.js";
14
14
  import { runBenchmarkSSE, runSingleCaseSSE } from "./benchmark-runner.js";
15
15
  import { getSkillSemaphore } from "./concurrency.js";
@@ -824,22 +824,30 @@ function resolveSourceLink(skillDir, root) {
824
824
  };
825
825
  }
826
826
  // Legacy derivation from `source: github:owner/repo`.
827
- // 0743: We DO NOT default `skillPath` to "SKILL.md" here. Multi-skill repos
828
- // (vskill, marketingskills, etc.) hold the SKILL.md under a nested path,
829
- // and the legacy `source` string carries no path information. Guessing
830
- // "SKILL.md" produced confidently-wrong 404 anchors for every install from
831
- // a multi-skill repo. Returning `null` lets the UI fall back to the safe
832
- // copy-chip (local path); a fresh `vskill add` writes the explicit
833
- // `sourceSkillPath` and restores the working anchor via the branch above.
827
+ // 0743: We DO NOT blindly default `skillPath` to "SKILL.md" here. Multi-skill
828
+ // repos (vskill, marketingskills, etc.) hold the SKILL.md under a nested
829
+ // path, and the legacy `source` string carries no path information.
830
+ // Guessing "SKILL.md" produced confidently-wrong 404 anchors for every
831
+ // install from a multi-skill repo.
832
+ //
833
+ // 0773 hotfix: when the matched lockfile entry KEY equals the source repo
834
+ // basename (i.e. `vskill install anton-abyzov/greet-anton` keys the entry
835
+ // as `greet-anton` AND the repo is `greet-anton`), the repo IS the skill —
836
+ // SKILL.md sits at the repo root. Defaulting skillPath to "SKILL.md" in
837
+ // that exact shape restores the working SourceFileLink anchor for
838
+ // single-skill repos without re-introducing 404s for multi-skill repos.
834
839
  const m = /^github:([^/]+)\/([^/#]+)/.exec(entry.source ?? "");
835
840
  // 0770: do NOT fall through here — an installed skill with a non-github
836
841
  // `source` (e.g. `marketplace:...`) is still installed, not authored. Local
837
842
  // git detection would leak the workspace remote (umbrella, etc.).
838
843
  if (!m)
839
844
  return { repoUrl: null, skillPath: null };
845
+ const repoBasename = m[2];
846
+ const lockKey = lock.skills[skillName] ? skillName : parentName;
847
+ const isSingleSkillRepo = repoBasename === lockKey;
840
848
  return {
841
849
  repoUrl: `https://github.com/${m[1]}/${m[2]}`,
842
- skillPath: entry.sourceSkillPath ?? null,
850
+ skillPath: entry.sourceSkillPath ?? (isSingleSkillRepo ? "SKILL.md" : null),
843
851
  };
844
852
  }
845
853
  /**
@@ -2289,6 +2297,65 @@ export function registerRoutes(router, root, projectName) {
2289
2297
  sendJson(res, { error: `Failed to delete skill: ${err.message}` }, 500, req);
2290
2298
  }
2291
2299
  });
2300
+ // 0780 — Uninstall an installed (lockfile-tracked) skill. Symmetric to
2301
+ // `vskill install`: removes the lockfile entry AND trashes the on-disk
2302
+ // dir. Idempotent on partially-removed installs (lockfile entry without
2303
+ // disk dir, or vice versa). Returns 404 only when neither exists.
2304
+ //
2305
+ // Distinct from DELETE /api/skills/:plugin/:skill (which trashes
2306
+ // source-authored skills only and explicitly rejects installed copies):
2307
+ // this route is the canonical uninstall path for lockfile-tracked
2308
+ // installed skills.
2309
+ router.post("/api/skills/:plugin/:skill/uninstall", async (req, res, params) => {
2310
+ // Skill-name validation — same kebab-case regex used by skill-create
2311
+ // routes. Performed BEFORE any filesystem access to defang path-traversal
2312
+ // attempts (e.g. `../../etc/passwd`).
2313
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(params.skill)) {
2314
+ sendJson(res, { error: "Invalid skill name", code: "invalid-skill-name" }, 400, req);
2315
+ return;
2316
+ }
2317
+ const skillDir = join(root, ".claude", "skills", params.skill);
2318
+ const resolvedDir = resolve(skillDir);
2319
+ const resolvedRoot = resolve(root);
2320
+ if (!resolvedDir.startsWith(resolvedRoot + "/") && resolvedDir !== resolvedRoot) {
2321
+ sendJson(res, { error: "Invalid skill path", code: "invalid-path" }, 400, req);
2322
+ return;
2323
+ }
2324
+ let removedFromLockfile = false;
2325
+ try {
2326
+ const lock = readLockfile(root);
2327
+ if (lock && lock.skills && Object.prototype.hasOwnProperty.call(lock.skills, params.skill)) {
2328
+ delete lock.skills[params.skill];
2329
+ writeLockfile(lock, root);
2330
+ removedFromLockfile = true;
2331
+ }
2332
+ }
2333
+ catch (err) {
2334
+ sendJson(res, { error: `Failed to update lockfile: ${err.message}`, code: "lockfile-write-failed" }, 500, req);
2335
+ return;
2336
+ }
2337
+ let trashedDir = null;
2338
+ if (existsSync(skillDir)) {
2339
+ try {
2340
+ const trash = (await import("trash")).default;
2341
+ await trash([skillDir]);
2342
+ trashedDir = skillDir;
2343
+ }
2344
+ catch (err) {
2345
+ sendJson(res, {
2346
+ error: `Failed to trash skill dir: ${err.message}`,
2347
+ code: "trash-failed",
2348
+ removedFromLockfile,
2349
+ }, 500, req);
2350
+ return;
2351
+ }
2352
+ }
2353
+ if (!removedFromLockfile && trashedDir === null) {
2354
+ sendJson(res, { error: "Skill is not installed", code: "not-installed" }, 404, req);
2355
+ return;
2356
+ }
2357
+ sendJson(res, { ok: true, removedFromLockfile, trashedDir }, 200, req);
2358
+ });
2292
2359
  // Get skill description (for activation testing preview)
2293
2360
  router.get("/api/skills/:plugin/:skill/description", async (req, res, params) => {
2294
2361
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);