@hasna/skills 0.8.12 → 0.9.2

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 CHANGED
@@ -271,6 +271,15 @@ Vendor file restoration uses an exclusive hard link to preserve concurrent files
271
271
  Do not retry an interrupted operation until its journal and both paths have been
272
272
  reconciled; a failed final journal write may leave the earlier durable intent.
273
273
 
274
+ For a reviewed migration that must retire one exact native copy, pass
275
+ `--target-manifest <file>`. The JSON manifest uses schema
276
+ `hasna.skills-native-migration-targets.v1` and binds each target to an agent,
277
+ an existing absolute `projectRoot`, a normalized project-relative `path`, and
278
+ the complete directory `treeSha256`. Exact-target mode refuses broad selector
279
+ flags, protected or vendor entries, missing or ambiguous paths, symlinks,
280
+ digest drift, and writes a manifest digest into the recovery journal. Every
281
+ inventory entry outside the manifest remains untouched.
282
+
274
283
  `skills hook agents --json` reports the supported adapters and coverage limits.
275
284
  Claude and Codex have lifecycle context hooks; Gemini uses `BeforeAgent`, and
276
285
  OpenCode uses its awaited message plugin. Cursor receives selected context at
@@ -592,13 +601,20 @@ before submitting again. The selected profile must include this exact version,
592
601
  and the consumer needs `runs:write` as well as `skills:read`.
593
602
 
594
603
  Cloud execution is enabled only when the deployment configures a reviewed image
595
- and exact bundle allowlist. The first supported lane is `pdf-generate`; arbitrary
596
- uploaded code is not admitted. Runs capture version, bundle digest, input digest,
604
+ and exact bundle allowlist; arbitrary
605
+ uploaded code is not admitted. The PDF lane accepts reviewed `pdf-generate` versions.
606
+ The optional `regex-test.v1` pure contract accepts only a tenant-specific, reviewed
607
+ self-contained Bun bundle, with no secrets, dependencies or network. Uploading or
608
+ selecting a bundle never admits it to cloud execution. Check an exact version with
609
+ `skills executions eligibility <skill> --skill-version <version> --json`; this
610
+ reads metadata without creating a run. Runs capture version, bundle digest, input digest,
597
611
  runtime image digest, limits and policy. The cloud worker runs in a separate
598
612
  Fargate task; the skill process has no API/provider credentials, no network, a
599
613
  read-only root and bounded temporary storage, execution time and output.
600
614
  `GET /skills/v1/capabilities` reports whether this deployment has cloud execution
601
- configured. Authorization and runtime availability are checked separately.
615
+ configured and lists supported `cloudExecutionContracts`. Each exact bundle needs
616
+ its own review and admission; the flag does not promise arbitrary executable
617
+ support. Authorization and runtime availability are checked separately.
602
618
 
603
619
  On a managed station, local execution also resolves the selected immutable
604
620
  bundle. Self-contained local executables run with explicit environment references
@@ -751,7 +767,7 @@ of app folders, and `XDG_CONFIG_HOME` is not consulted at all.
751
767
  | `skills sessions show <id> --json` | | Inspect one session's exact profile revision and receipt hash without loading payloads |
752
768
  | `skills sessions reconcile <id> --from-profile <id> --from-revision <rev> --receipt-sha256 <sha> --selection-profile <id> --profile-revision <rev>` | | Plan an explicit migration of one live session; `--apply --plan-digest <digest> --plan-issued-at <time> --plan-expires-at <time>` (within five minutes) preserves its old receipt and applies the reviewed replacement |
753
769
  | `skills hook agents --json` | | Report maintained adapters and explicit coverage limits |
754
- | `skills migrate native` | | Inventory native copies; `--apply` archives managed copies, with explicit `--include-unmanaged` and `--include-vendor` retirement options |
770
+ | `skills migrate native` | | Inventory native copies; `--apply` archives managed copies, with explicit `--include-unmanaged`, `--include-vendor`, or reviewed `--target-manifest` selection |
755
771
  | `skills pull --all --selection-profile <id>` | | With CLI loading active, refresh the selected profile into the verified cache |
756
772
  | `skills sync --selection-profile <id> [--check] [--station <id>]` | | Sync or check the selected profile/cache; optionally record station state |
757
773
  | `skills station-state <id>` | | Read a station's sync receipt in the authenticated workspace |
@@ -1433,7 +1449,7 @@ src/
1433
1449
  |---|---|---|
1434
1450
  | Catalog skills | 0 | `SKILLS.length` (`src/lib/registry-data/`) |
1435
1451
  | Categories | 17 | `CATEGORIES` (`src/lib/registry-types.ts`) |
1436
- | MCP tools | 81 | `tools/list` against a live `buildServer()` |
1452
+ | MCP tools | 82 | `tools/list` against a live `buildServer()` |
1437
1453
 
1438
1454
  Every number in this table is re-derived from the source tree on each test run by
1439
1455
  `src/lib/readme-derived-counts.test.ts`, so a drifted figure fails a test rather
package/bin/index.js CHANGED
@@ -36868,7 +36868,7 @@ var package_default;
36868
36868
  var init_package = __esm(() => {
36869
36869
  package_default = {
36870
36870
  name: "@hasna/skills",
36871
- version: "0.8.12",
36871
+ version: "0.9.2",
36872
36872
  description: "Skills library for AI coding agents",
36873
36873
  type: "module",
36874
36874
  bin: {
@@ -39186,6 +39186,7 @@ function readManifest(skillPath, fallbackName, normalizeName) {
39186
39186
  const version = readDeclaredSkillVersion(skillPath) ?? PORTABLE_SKILL_DEFAULT_VERSION;
39187
39187
  const kind = parseSkillKind(stringField(jsonManifest, "kind") ?? frontmatter?.kind);
39188
39188
  const commands = parseManifestCommands(jsonManifest) ?? (kind === "instruction" ? [] : inferPackageCommands(pkg, name)) ?? [];
39189
+ const runtime = parseManifestRuntime(jsonManifest) ?? inferRuntimeFromCommands(commands);
39189
39190
  return {
39190
39191
  $schema: stringField(jsonManifest, "$schema") ?? PORTABLE_SKILL_SCHEMA,
39191
39192
  standard: stringField(jsonManifest, "standard") ?? PORTABLE_SKILL_STANDARD,
@@ -39198,7 +39199,7 @@ function readManifest(skillPath, fallbackName, normalizeName) {
39198
39199
  ...kind ? { kind } : {},
39199
39200
  inputs: kind === "instruction" ? parseManifestInputs(jsonManifest) ?? [] : parseManifestInputs(jsonManifest) ?? DEFAULT_INPUTS,
39200
39201
  commands,
39201
- ...parseManifestRuntime(jsonManifest) ? { runtime: parseManifestRuntime(jsonManifest) } : {},
39202
+ ...runtime ? { runtime } : {},
39202
39203
  ...parseManifestProvenance(jsonManifest) ? { provenance: parseManifestProvenance(jsonManifest) } : {}
39203
39204
  };
39204
39205
  }
@@ -39717,6 +39718,12 @@ function inferPackageCommands(pkg, fallbackName) {
39717
39718
  }
39718
39719
  return;
39719
39720
  }
39721
+ function inferRuntimeFromCommands(commands) {
39722
+ const entrypoint = commands.find((command) => command.entry)?.entry;
39723
+ if (!entrypoint || !entrypoint.toLowerCase().endsWith(".py"))
39724
+ return;
39725
+ return { ...defaultRuntimeContract(entrypoint), runtime: "python3" };
39726
+ }
39720
39727
  function readJsonObject(path) {
39721
39728
  const parsed = JSON.parse(readFileSync6(path, "utf-8"));
39722
39729
  if (!isRecord2(parsed))
@@ -55351,6 +55358,82 @@ function treeHash(root2) {
55351
55358
  visit2(root2);
55352
55359
  return hash2.digest("hex");
55353
55360
  }
55361
+ function parseNativeMigrationTargetManifest(input) {
55362
+ const digest2 = sha(input);
55363
+ let value;
55364
+ try {
55365
+ value = JSON.parse(input.toString());
55366
+ } catch {
55367
+ throw new Error("Invalid native migration target manifest JSON");
55368
+ }
55369
+ if (!value || typeof value !== "object" || Array.isArray(value))
55370
+ throw new Error("Invalid native migration target manifest");
55371
+ const record2 = value;
55372
+ if (record2.schema !== "hasna.skills-native-migration-targets.v1" || Object.keys(record2).some((key) => !["schema", "targets"].includes(key)))
55373
+ throw new Error("Unsupported native migration target manifest schema");
55374
+ if (!Array.isArray(record2.targets) || record2.targets.length === 0 || record2.targets.length > 1000)
55375
+ throw new Error("Native migration target manifest must contain 1 to 1000 targets");
55376
+ const targets = [];
55377
+ const identities = new Set;
55378
+ for (const target of record2.targets) {
55379
+ if (!target || typeof target !== "object" || Array.isArray(target))
55380
+ throw new Error("Invalid native migration target");
55381
+ const item = target;
55382
+ if (Object.keys(item).some((key) => !["agent", "projectRoot", "path", "treeSha256"].includes(key)) || typeof item.agent !== "string" || typeof item.projectRoot !== "string" || typeof item.path !== "string" || typeof item.treeSha256 !== "string")
55383
+ throw new Error("Invalid native migration target fields");
55384
+ if (!INTEGRATION_AGENTS.includes(item.agent))
55385
+ throw new Error(`Unsupported native migration target agent: ${item.agent}`);
55386
+ const projectRoot = resolve16(item.projectRoot);
55387
+ if (!isAbsolute15(item.projectRoot) || projectRoot !== item.projectRoot || !existsSync15(projectRoot) || !lstatSync18(projectRoot).isDirectory())
55388
+ throw new Error(`Native migration target project root must be an existing absolute directory: ${item.projectRoot}`);
55389
+ assertSafePath(projectRoot);
55390
+ if (isAbsolute15(item.path) || item.path.includes("\\") || item.path.length === 0 || item.path.includes("\x00"))
55391
+ throw new Error(`Native migration target path must be normalized and project-relative: ${item.path}`);
55392
+ const resolved = resolve16(projectRoot, item.path), normalized = relative5(projectRoot, resolved);
55393
+ if (!normalized || normalized === ".." || normalized.startsWith(`..${sep6}`) || isAbsolute15(normalized) || normalized !== item.path)
55394
+ throw new Error(`Native migration target path escapes or is not normalized: ${item.path}`);
55395
+ if (!/^[0-9a-f]{64}$/.test(item.treeSha256))
55396
+ throw new Error(`Invalid native migration tree digest for ${item.path}`);
55397
+ const identity4 = `${item.agent}\x00${projectRoot}\x00${normalized}`;
55398
+ if (identities.has(identity4))
55399
+ throw new Error(`Duplicate native migration target: ${item.path}`);
55400
+ identities.add(identity4);
55401
+ targets.push({ agent: item.agent, projectRoot, path: normalized, treeSha256: item.treeSha256 });
55402
+ }
55403
+ return { schema: "hasna.skills-native-migration-targets.v1", targets, digest: digest2 };
55404
+ }
55405
+ function readNativeMigrationTargetManifest(path) {
55406
+ return parseNativeMigrationTargetManifest(readNativeBytes(path, 4 * 1024 * 1024));
55407
+ }
55408
+ function selectNativeMigrationTargets(inventory, manifest) {
55409
+ if (manifest.schema !== "hasna.skills-native-migration-targets.v1" || !/^[0-9a-f]{64}$/.test(manifest.digest) || !Array.isArray(manifest.targets) || manifest.targets.length === 0)
55410
+ throw new Error("Invalid native migration target manifest");
55411
+ const identities = new Set;
55412
+ const selected = [];
55413
+ for (const target of manifest.targets) {
55414
+ const projectRoot = resolve16(target.projectRoot), normalized = relative5(projectRoot, resolve16(projectRoot, target.path));
55415
+ if (!isAbsolute15(target.projectRoot) || projectRoot !== target.projectRoot || !existsSync15(projectRoot) || !lstatSync18(projectRoot).isDirectory())
55416
+ throw new Error(`Native migration target project root must be an existing absolute directory: ${target.projectRoot}`);
55417
+ assertSafePath(projectRoot);
55418
+ if (isAbsolute15(target.path) || target.path.includes("\\") || target.path.includes("\x00") || !normalized || normalized === ".." || normalized.startsWith(`..${sep6}`) || normalized !== target.path || !/^[0-9a-f]{64}$/.test(target.treeSha256))
55419
+ throw new Error(`Invalid native migration target: ${target.path}`);
55420
+ const identity4 = `${target.agent}\x00${projectRoot}\x00${normalized}`;
55421
+ if (identities.has(identity4))
55422
+ throw new Error(`Duplicate native migration target: ${target.path}`);
55423
+ identities.add(identity4);
55424
+ const expected = resolve16(target.projectRoot, target.path);
55425
+ const matches2 = inventory.filter((entry2) => entry2.agent === target.agent && resolve16(entry2.path) === expected);
55426
+ if (matches2.length !== 1)
55427
+ throw new Error(`Native migration target was not found exactly once: ${target.agent} ${target.projectRoot}/${target.path}`);
55428
+ const entry = matches2[0];
55429
+ if (entry.bridge || entry.system || entry.vendor)
55430
+ throw new Error(`Native migration target is protected or vendor-owned: ${entry.path}`);
55431
+ if (entry.hash !== target.treeSha256 || treeHash(entry.path) !== target.treeSha256)
55432
+ throw new Error(`Native migration target changed after review: ${entry.path}`);
55433
+ selected.push(entry);
55434
+ }
55435
+ return selected;
55436
+ }
55354
55437
  function projectAncestorDirectories(projects) {
55355
55438
  const directories = new Set;
55356
55439
  for (const project of projects) {
@@ -55839,12 +55922,13 @@ function archiveNativeSkills(inventory, options) {
55839
55922
  if (!expected || treeHash(entry.path) !== entry.hash || !isOwnedCliBridge(entry.path, [expected]))
55840
55923
  throw new Error(`Skills bridge changed after planning: ${entry.path}`);
55841
55924
  }
55842
- const selected = inventory.filter((entry) => !entry.bridge && !entry.system && (entry.vendor ? options.includeVendor : entry.managed || options.includeUnmanaged));
55925
+ const selected = options.targetManifest ? selectNativeMigrationTargets(inventory, options.targetManifest) : inventory.filter((entry) => !entry.bridge && !entry.system && (entry.vendor ? options.includeVendor : entry.managed || options.includeUnmanaged));
55843
55926
  for (const entry of selected)
55844
55927
  if (treeHash(entry.path) !== entry.hash)
55845
55928
  throw new Error(`Native skill changed after planning: ${entry.path}`);
55929
+ const targetManifest = options.targetManifest ? { schema: options.targetManifest.schema, digest: options.targetManifest.digest, targetCount: options.targetManifest.targets.length } : undefined;
55846
55930
  if (!selected.length)
55847
- return { entries: [], ...aliases.length ? { rootAliases: aliases } : {} };
55931
+ return { entries: [], ...targetManifest ? { targetManifest } : {}, ...aliases.length ? { rootAliases: aliases } : {} };
55848
55932
  const operationId = randomUUID6(), archiveRoot = join29(options.dataDir ?? getDataDir(), "migration", operationId, "native"), receiptPath = join29(archiveRoot, "receipt.json");
55849
55933
  const entries = selected.map((entry, index) => ({
55850
55934
  source: entry.vendor ? join29(entry.path, "SKILL.md") : entry.path,
@@ -55853,7 +55937,7 @@ function archiveNativeSkills(inventory, options) {
55853
55937
  ...entry.vendor ? { discoveryOnly: true } : {},
55854
55938
  status: "planned"
55855
55939
  }));
55856
- const journal = { version: 2, operationId, status: "planned", entries, ...aliases.length ? { rootAliases: aliases } : {} };
55940
+ const journal = { version: 2, operationId, status: "planned", entries, ...targetManifest ? { targetManifest } : {}, ...aliases.length ? { rootAliases: aliases } : {} };
55857
55941
  const moved = [];
55858
55942
  const verifyArchive = (entry) => {
55859
55943
  assertSafePath(entry.archive);
@@ -55947,7 +56031,7 @@ function archiveNativeSkills(inventory, options) {
55947
56031
  } catch {}
55948
56032
  throw new Error(`Native archive failed; inspect the recovery journal at ${receiptPath} before retrying.`, { cause: error });
55949
56033
  }
55950
- return { entries: entries.map(({ status, conflict, ...entry }) => entry), receiptPath, ...aliases.length ? { rootAliases: aliases } : {} };
56034
+ return { entries: entries.map(({ status, conflict, ...entry }) => entry), receiptPath, ...targetManifest ? { targetManifest } : {}, ...aliases.length ? { rootAliases: aliases } : {} };
55951
56035
  }
55952
56036
  function assertManagedAgentBridge(agent, options = {}) {
55953
56037
  const home = resolve16(options.home ?? homedir4()), dataDir3 = options.dataDir ?? getDataDirReadOnly();
@@ -76206,6 +76290,8 @@ function normalizeRemoteSkill(skill) {
76206
76290
  tags: skill.tags || ["remote"],
76207
76291
  dependencies: skill.dependencies,
76208
76292
  ...skill.version ? { version: skill.version } : {},
76293
+ ...skill.revisionId !== undefined ? { revisionId: skill.revisionId } : {},
76294
+ ...skill.lifecycle ? { lifecycle: skill.lifecycle } : {},
76209
76295
  availability: normalizeRemoteAvailability(skill.availability),
76210
76296
  source: "remote"
76211
76297
  };
@@ -76323,6 +76409,8 @@ var init_remote_registry = __esm(() => {
76323
76409
  tags: exports_external.array(exports_external.string()).optional(),
76324
76410
  dependencies: exports_external.array(exports_external.string()).optional(),
76325
76411
  version: exports_external.string().optional(),
76412
+ revisionId: exports_external.union([exports_external.string().regex(/^[\x21-\x7e]+$/), exports_external.null()]).optional(),
76413
+ lifecycle: exports_external.enum(["active", "archived"]).optional(),
76326
76414
  availability: remoteAvailabilitySchema.optional()
76327
76415
  }).passthrough().refine((skill) => skill.name || skill.slug, {
76328
76416
  message: "Remote skill requires name or slug"
@@ -77711,6 +77799,7 @@ __export(exports_remote_client, {
77711
77799
  RemoteWorkspaceSelectionError: () => RemoteWorkspaceSelectionError,
77712
77800
  RemoteWorkspaceMemberError: () => RemoteWorkspaceMemberError,
77713
77801
  RemoteSkillsClient: () => RemoteSkillsClient,
77802
+ RemoteSkillLifecycleError: () => RemoteSkillLifecycleError,
77714
77803
  RemoteRouteUnsupportedError: () => RemoteRouteUnsupportedError,
77715
77804
  RemoteRequestError: () => RemoteRequestError,
77716
77805
  RemoteQuoteUnavailableError: () => RemoteQuoteUnavailableError,
@@ -77842,6 +77931,39 @@ class RemoteSkillsClient {
77842
77931
  })();
77843
77932
  return this.capabilities;
77844
77933
  }
77934
+ async addSkillPublishScope(keyId, expectedScopes, expectedOrgId) {
77935
+ if (!/^[A-Za-z0-9_-]+$/.test(keyId))
77936
+ throw new Error("Invalid API key id");
77937
+ if (!/^[A-Za-z0-9_-]{1,256}$/.test(expectedOrgId))
77938
+ throw new Error("Invalid expected organization id");
77939
+ if (!Array.isArray(expectedScopes) || expectedScopes.length > 32 || expectedScopes.some((scope) => typeof scope !== "string" || scope.length > 128 || !/^(?:\*|[a-z][a-z0-9_-]*:(?:\*|[a-z][a-z0-9_-]*))$/.test(scope)) || new Set(expectedScopes).size !== expectedScopes.length)
77940
+ throw new Error("Invalid expected API key scopes");
77941
+ const path = `/api/v1/admin/keys/${encodeURIComponent(keyId)}/scopes`;
77942
+ const response = await this.request(path, { method: "PATCH", body: JSON.stringify({ expected_scopes: expectedScopes, add_scopes: ["skills:publish"] }) });
77943
+ if (!response.ok)
77944
+ throw new RemoteRequestError(path, response.status);
77945
+ let value;
77946
+ try {
77947
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(await readBoundedResponse(response, 64 * 1024)));
77948
+ } catch {
77949
+ throw new Error("Invalid API key scope update response");
77950
+ }
77951
+ if (!value || typeof value !== "object" || Array.isArray(value))
77952
+ throw new Error("Invalid API key scope update response");
77953
+ const body = value;
77954
+ const returnedId = body.keyId;
77955
+ const orgId = body.orgId;
77956
+ const scopes = body.scopes;
77957
+ if (returnedId !== keyId || typeof orgId !== "string" || !orgId || expectedOrgId !== undefined && orgId !== expectedOrgId || !Array.isArray(scopes) || scopes.some((scope) => typeof scope !== "string") || body.updated !== true) {
77958
+ throw new Error("Invalid API key scope update response");
77959
+ }
77960
+ const returnedScopes = scopes;
77961
+ const expectedResult = expectedScopes.includes("skills:publish") ? expectedScopes : [...expectedScopes, "skills:publish"];
77962
+ if (returnedScopes.length !== expectedResult.length || expectedResult.some((scope, index) => returnedScopes[index] !== scope)) {
77963
+ throw new Error("API key scope update response did not preserve the expected scopes");
77964
+ }
77965
+ return { keyId, orgId, scopes: [...returnedScopes], updated: true };
77966
+ }
77845
77967
  async submitQuotedRun(slug2, input2 = {}, args = [], approval = {}) {
77846
77968
  runQuoteReceipt(approval.quoteReceipt);
77847
77969
  ({ input: input2, args, approval } = JSON.parse(JSON.stringify({ input: input2, args, approval })));
@@ -78359,6 +78481,19 @@ class RemoteSkillsClient {
78359
78481
  async deleteSkill(slug2) {
78360
78482
  return this.request(`/api/v1/skills/${encodeURIComponent(slug2)}`, { method: "DELETE" });
78361
78483
  }
78484
+ async setSkillLifecycle(slug2, lifecycle, options) {
78485
+ const path = `/api/v1/skills/${encodeURIComponent(slug2)}/lifecycle`;
78486
+ const response = await this.request(path, {
78487
+ method: "PATCH",
78488
+ headers: { "If-Match": options.expectedRevisionId },
78489
+ body: JSON.stringify({ lifecycle, ...options.reason ? { reason: options.reason } : {}, ...options.replacementSlug ? { replacementSlug: options.replacementSlug } : {} })
78490
+ });
78491
+ if (!response.ok) {
78492
+ const code = await readResponseCode(response);
78493
+ throw new RemoteSkillLifecycleError(path, response.status, code);
78494
+ }
78495
+ return response.json();
78496
+ }
78362
78497
  async downloadSkillBundle(slug2) {
78363
78498
  return this.request(`/api/v1/skills/${encodeURIComponent(slug2)}/bundle`, { method: "GET" });
78364
78499
  }
@@ -78510,9 +78645,13 @@ function normalizeSkillSummaryList(payload) {
78510
78645
  return payload.map(normalizeSkillSummary);
78511
78646
  }
78512
78647
  async function responseBodyCarriesCode(response, codes) {
78648
+ const code = await readResponseCode(response);
78649
+ return code !== undefined && codes.includes(code);
78650
+ }
78651
+ async function readResponseCode(response) {
78513
78652
  const reader = response.body?.getReader();
78514
78653
  if (!reader)
78515
- return false;
78654
+ return;
78516
78655
  const maximum = 8 * 1024;
78517
78656
  let deadline;
78518
78657
  const expired = new Promise((_, reject2) => {
@@ -78527,7 +78666,7 @@ async function responseBodyCarriesCode(response, codes) {
78527
78666
  break;
78528
78667
  size2 += next.value.byteLength;
78529
78668
  if (size2 > maximum)
78530
- return false;
78669
+ return;
78531
78670
  chunks.push(next.value);
78532
78671
  }
78533
78672
  const bytes = new Uint8Array(size2);
@@ -78538,11 +78677,11 @@ async function responseBodyCarriesCode(response, codes) {
78538
78677
  }
78539
78678
  const payload = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
78540
78679
  if (!payload || typeof payload !== "object" || Array.isArray(payload) || !Object.hasOwn(payload, "code"))
78541
- return false;
78680
+ return;
78542
78681
  const code = payload.code;
78543
- return typeof code === "string" && codes.includes(code);
78682
+ return typeof code === "string" && /^[A-Z][A-Z0-9_:-]{0,127}$/.test(code) ? code : undefined;
78544
78683
  } catch {
78545
- return false;
78684
+ return;
78546
78685
  } finally {
78547
78686
  clearTimeout(deadline);
78548
78687
  reader.cancel().catch(() => {});
@@ -78571,7 +78710,7 @@ async function createRemoteSkillsClient(env3 = process.env) {
78571
78710
  function createRemoteSkillsClientReadOnly(env3 = process.env) {
78572
78711
  return createRemoteSkillsClient(env3);
78573
78712
  }
78574
- var RemoteRouteUnsupportedError, RemoteRequestError, RemoteCreditCheckoutError, RemoteQuoteUnavailableError, RemoteWorkspaceMemberError, RemoteWorkspaceSelectionError, RemoteCapabilityUnavailableError, INVALID_SKILL_VERSION_RESPONSE = "Remote skill version payload did not match the expected contract.";
78713
+ var RemoteRouteUnsupportedError, RemoteRequestError, KNOWN_SKILL_LIFECYCLE_CODES, RemoteSkillLifecycleError, RemoteCreditCheckoutError, RemoteQuoteUnavailableError, RemoteWorkspaceMemberError, RemoteWorkspaceSelectionError, RemoteCapabilityUnavailableError, INVALID_SKILL_VERSION_RESPONSE = "Remote skill version payload did not match the expected contract.";
78575
78714
  var init_remote_client = __esm(() => {
78576
78715
  init_remote_recurring();
78577
78716
  init_remote_invitations();
@@ -78608,6 +78747,17 @@ var init_remote_client = __esm(() => {
78608
78747
  this.name = "RemoteRequestError";
78609
78748
  }
78610
78749
  };
78750
+ KNOWN_SKILL_LIFECYCLE_CODES = new Set(["SKILL_ARCHIVE_PROFILE_CONFLICT", "LIFECYCLE_ROLE_REQUIRED"]);
78751
+ RemoteSkillLifecycleError = class RemoteSkillLifecycleError extends RemoteRequestError {
78752
+ code;
78753
+ constructor(path, status, code) {
78754
+ super(path, status);
78755
+ this.name = "RemoteSkillLifecycleError";
78756
+ const safeCode = code !== undefined && KNOWN_SKILL_LIFECYCLE_CODES.has(code) ? code : undefined;
78757
+ this.code = safeCode;
78758
+ this.message = `Skill lifecycle update was refused (HTTP ${status}${safeCode ? `, code ${safeCode}` : ""})`;
78759
+ }
78760
+ };
78611
78761
  RemoteCreditCheckoutError = class RemoteCreditCheckoutError extends RemoteRequestError {
78612
78762
  code;
78613
78763
  requestIdempotencyKey;
@@ -78663,7 +78813,7 @@ var init_remote_client = __esm(() => {
78663
78813
  // src/lib/revision.ts
78664
78814
  import { createHash as createHash16 } from "crypto";
78665
78815
  function revisionIdOf(content) {
78666
- const canonical4 = JSON.stringify({
78816
+ const canonicalContent = {
78667
78817
  slug: content.slug,
78668
78818
  displayName: content.displayName,
78669
78819
  description: content.description,
@@ -78675,7 +78825,13 @@ function revisionIdOf(content) {
78675
78825
  skillMd: content.skillMd ?? null,
78676
78826
  bundleSha256: content.bundleSha256 ?? null,
78677
78827
  bundleByteSize: content.bundleByteSize ?? null
78678
- });
78828
+ };
78829
+ if (content.lifecycle === "archived" || content.archiveReason !== undefined || content.replacementSlug !== undefined) {
78830
+ canonicalContent.lifecycle = content.lifecycle ?? "active";
78831
+ canonicalContent.archiveReason = content.archiveReason ?? null;
78832
+ canonicalContent.replacementSlug = content.replacementSlug ?? null;
78833
+ }
78834
+ const canonical4 = JSON.stringify(canonicalContent);
78679
78835
  return createHash16("sha256").update(canonical4).digest("hex");
78680
78836
  }
78681
78837
  var REVISION_ID_PATTERN;
@@ -82131,6 +82287,15 @@ class CloudExecutionClient {
82131
82287
  }
82132
82288
  return response;
82133
82289
  }
82290
+ async eligibility(slug2, version2, bundleDigest) {
82291
+ if (!/^[a-z0-9-]+$/.test(slug2) || !/^\d+\.\d+\.\d+$/.test(version2) || bundleDigest !== undefined && !/^(sha256:)?[a-f0-9]{64}$/.test(bundleDigest))
82292
+ throw Error("Cloud eligibility requires an exact skill version and valid digest");
82293
+ const query = new URLSearchParams({ version: version2, ...bundleDigest ? { bundleDigest } : {} });
82294
+ const v = await jsonBody(await this.request(`${slug2}/eligibility?${query}`));
82295
+ if (!v || v.contractVersion !== 1 || typeof v.eligible !== "boolean" || v.skill !== slug2 || v.version !== version2 || !/^[a-f0-9]{64}$/.test(v.bundleDigest) || bundleDigest && bundleDigest.replace(/^sha256:/, "") !== v.bundleDigest || v.eligible && (!v.executionContract || !["pdf.v1", "regex-test.v1"].includes(v.executionContract.id)))
82296
+ throw Error("Invalid cloud eligibility receipt");
82297
+ return v;
82298
+ }
82134
82299
  async submit(slug2, version2, input2, key, selection) {
82135
82300
  if (!/^[a-z0-9-]+$/.test(slug2) || !/^\d+\.\d+\.\d+$/.test(version2))
82136
82301
  throw Error("Cloud execution requires skill@exact-version");
@@ -103202,6 +103367,18 @@ var init_run_routing = __esm(() => {
103202
103367
  import { existsSync as existsSync28, readdirSync as readdirSync17, statSync as statSync16 } from "fs";
103203
103368
  import { join as join46 } from "path";
103204
103369
  function registerOperationTools(server) {
103370
+ server.registerTool("cloud_skill_eligibility", {
103371
+ title: "Cloud Skill Eligibility",
103372
+ description: "Inspect the authenticated workspace's exact reviewed cloud contract. Does not execute or admit a skill.",
103373
+ inputSchema: { name: exports_external.string(), version: exports_external.string(), bundleDigest: exports_external.string().optional() }
103374
+ }, async ({ name, version: version2, bundleDigest }) => {
103375
+ try {
103376
+ const { CloudExecutionClient: CloudExecutionClient2 } = await Promise.resolve().then(() => (init_cloud_executions(), exports_cloud_executions));
103377
+ return mcpJson(await (await CloudExecutionClient2.configured()).eligibility(name, version2, bundleDigest));
103378
+ } catch (error61) {
103379
+ return mcpError("CLOUD_ELIGIBILITY_FAILED", error61.message);
103380
+ }
103381
+ });
103205
103382
  server.registerTool("scaffold_skill", {
103206
103383
  title: "Scaffold Skill",
103207
103384
  description: "Create a portable skill folder under ~/.hasna/skills/installed/<name> with SKILL.md, skill.json, AGENTS.md, package.json, and src/index.ts.",
@@ -105518,6 +105695,15 @@ function registerRuntime(parent) {
105518
105695
  await handleRun(name, args2, options, separatorBeforeSkill);
105519
105696
  });
105520
105697
  const executions = parent.command("executions").description("Inspect isolated cloud executions");
105698
+ executions.command("eligibility").argument("<skill>").requiredOption("--skill-version <version>", "Exact published version").option("--bundle-digest <digest>", "Expected published bundle SHA256").option("--json", "Output as JSON", false).description("Inspect reviewed cloud eligibility without executing a skill").action(async (slug2, options) => {
105699
+ try {
105700
+ const client2 = await CloudExecutionClient.configured();
105701
+ await writeCliOutput(JSON.stringify(await client2.eligibility(slug2, options.skillVersion, options.bundleDigest), null, 2));
105702
+ } catch (error61) {
105703
+ console.error(JSON.stringify({ error: error61.message }));
105704
+ process.exitCode = 1;
105705
+ }
105706
+ });
105521
105707
  for (const operation of ["show", "logs", "artifacts", "cancel"]) {
105522
105708
  executions.command(operation).argument("<execution-id>").option("--json", "Output as JSON", false).action(async (id2) => {
105523
105709
  try {
@@ -108432,11 +108618,16 @@ function registerAgentIntegration(parent) {
108432
108618
  }
108433
108619
  });
108434
108620
  const migrate = parent.command("migrate").description("Preserve and retire native agent skill copies");
108435
- migrate.command("native").option("--project <directory>", "Also inventory a project and its ancestors (the current directory and its ancestors are always included)").option("--include-unmanaged", "Archive user-authored skills as well as Skills-managed copies", false).option("--include-vendor", "Retire vendor SKILL.md discovery files while preserving plugin scripts and assets", false).option("--discovery-inputs <file>", "Advanced reviewed active plugin roots and source hashes").option("--allow-root-aliases", "Allow home .claude/.codex aliases to existing directories within this home", false).option("--apply", "Move selected skills to private archives outside agent discovery roots", false).option("--json", "Output inventory and archive receipt as JSON", false).description("Inventory native skill copies; preserve complete directories before retiring them").action(async (options) => {
108621
+ migrate.command("native").option("--project <directory>", "Also inventory a project and its ancestors (the current directory and its ancestors are always included)").option("--include-unmanaged", "Archive user-authored skills as well as Skills-managed copies", false).option("--include-vendor", "Retire vendor SKILL.md discovery files while preserving plugin scripts and assets", false).option("--target-manifest <file>", "Reviewed exact native migration target manifest").option("--discovery-inputs <file>", "Advanced reviewed active plugin roots and source hashes").option("--allow-root-aliases", "Allow home .claude/.codex aliases to existing directories within this home", false).option("--apply", "Move selected skills to private archives outside agent discovery roots", false).option("--json", "Output inventory and archive receipt as JSON", false).description("Inventory native skill copies; preserve complete directories before retiring them").action(async (options) => {
108436
108622
  try {
108437
108623
  const discoveryInputs = options.discoveryInputs ? JSON.parse(readFileSync37(options.discoveryInputs, "utf8")) : undefined;
108624
+ if (options.targetManifest && (options.includeUnmanaged || options.includeVendor))
108625
+ throw new Error("--target-manifest cannot be combined with broad native migration selectors");
108626
+ const targetManifest = options.targetManifest ? readNativeMigrationTargetManifest(options.targetManifest) : undefined;
108438
108627
  const inventory = inventoryNativeSkills(undefined, { projectDirs: [process.cwd(), ...options.project ? [options.project] : []], includeVendor: options.includeVendor, configured: options.includeVendor, discoveryInputs, allowRootAliases: options.allowRootAliases });
108439
- const result2 = options.apply ? archiveNativeSkills(inventory, { includeUnmanaged: options.includeUnmanaged, includeVendor: options.includeVendor, allowRootAliases: options.allowRootAliases }) : { entries: [] };
108628
+ if (targetManifest)
108629
+ selectNativeMigrationTargets(inventory, targetManifest);
108630
+ const result2 = options.apply ? archiveNativeSkills(inventory, { includeUnmanaged: options.includeUnmanaged, includeVendor: options.includeVendor, allowRootAliases: options.allowRootAliases, targetManifest }) : { entries: [], ...targetManifest ? { targetManifest: { schema: targetManifest.schema, digest: targetManifest.digest, targetCount: targetManifest.targets.length } } : {} };
108440
108631
  if (options.json)
108441
108632
  await writeCliOutput(JSON.stringify({ applied: options.apply, inventory, ...result2 }));
108442
108633
  else
@@ -109219,6 +109410,7 @@ function validateRawCandidate(candidate2) {
109219
109410
  const issues = validatePortableManifestContract(candidate2, { strict: true });
109220
109411
  if (issues.length)
109221
109412
  throw new Error(`Invalid skill.json: ${issues.map((issue2) => issue2.code).join(", ")}`);
109413
+ validateRuntimeEntrypointCompatibility(candidate2);
109222
109414
  for (const field2 of ["inputs", "commands"]) {
109223
109415
  const items = candidate2[field2];
109224
109416
  if (items === undefined)
@@ -109244,6 +109436,18 @@ function validateRawCandidate(candidate2) {
109244
109436
  }
109245
109437
  }
109246
109438
  }
109439
+ function validateRuntimeEntrypointCompatibility(candidate2) {
109440
+ if (candidate2.kind !== "executable" || !isRecord6(candidate2.runtime))
109441
+ return;
109442
+ const runtime = candidate2.runtime.runtime;
109443
+ const entrypoint = candidate2.runtime.entrypoint;
109444
+ if (typeof runtime !== "string" || typeof entrypoint !== "string")
109445
+ return;
109446
+ const extension = entrypoint.toLowerCase().split(".").pop();
109447
+ if (extension === "py" && runtime !== "python3") {
109448
+ throw new Error("Invalid skill.json: runtime.entrypoint with .py extension requires runtime python3.");
109449
+ }
109450
+ }
109247
109451
  function compareVersions(left, right) {
109248
109452
  const parts = (value) => value.split("+")[0].split(/-(.*)/s).slice(0, 2);
109249
109453
  const [a, ap] = parts(left), [b, bp] = parts(right);
@@ -109779,7 +109983,8 @@ var exports_registry2 = {};
109779
109983
  __export(exports_registry2, {
109780
109984
  registerVersions: () => registerVersions,
109781
109985
  registerRegistry: () => registerRegistry,
109782
- registerPull: () => registerPull
109986
+ registerPull: () => registerPull,
109987
+ registerLifecycle: () => registerLifecycle
109783
109988
  });
109784
109989
  function registerRegistry(parent) {
109785
109990
  const registry2 = parent.command("registry").description("Generate registry artifacts for hosted skills services");
@@ -109935,6 +110140,25 @@ Versions of ${name}
109935
110140
  }
109936
110141
  });
109937
110142
  }
110143
+ function registerLifecycle(parent) {
110144
+ parent.command("lifecycle").argument("<name>", "Skill name on the configured instance").requiredOption("--state <state>", "Lifecycle state: active or archived").requiredOption("--revision <revision>", "Exact revision id from skills info/get").option("--reason <reason>", "Bounded archive reason").option("--replacement <slug>", "Optional successor slug").option("--json", "Output as JSON", false).description("Change a hosted skill's active/archive lifecycle with an exact revision guard").action(async (name, options) => {
110145
+ if (options.state !== "active" && options.state !== "archived")
110146
+ throw new Error("--state must be active or archived");
110147
+ const client2 = await createRemoteSkillsClient();
110148
+ if (!client2)
110149
+ throw new Error("No API key configured, so there is no instance to update.");
110150
+ try {
110151
+ const result2 = await client2.setSkillLifecycle(name.trim(), options.state, { expectedRevisionId: options.revision, ...options.reason ? { reason: options.reason } : {}, ...options.replacement ? { replacementSlug: options.replacement } : {} });
110152
+ console.log(options.json ? JSON.stringify(result2, null, 2) : `Updated ${name} to ${options.state}.`);
110153
+ } catch (error61) {
110154
+ if (options.json)
110155
+ console.log(JSON.stringify({ error: error61.message }, null, 2));
110156
+ else
110157
+ console.error(source_default.red(error61.message));
110158
+ process.exitCode = 1;
110159
+ }
110160
+ });
110161
+ }
109938
110162
  var init_registry2 = __esm(() => {
109939
110163
  init_source();
109940
110164
  init_registry_sync();
@@ -110482,6 +110706,11 @@ function authIdentityPayload(authSource, live, cached2, offline = false) {
110482
110706
  const userId = stringField2(user?.id) ?? cached2?.userId;
110483
110707
  const orgId = stringField2(organization2?.id) ?? cached2?.orgId;
110484
110708
  const role3 = stringField2(user?.role);
110709
+ const apiKey = recordField(root2.apiKey) ?? recordField(data?.apiKey);
110710
+ const apiKeyId = stringField2(apiKey?.id);
110711
+ const apiKeyScopes = apiKey?.scopes;
110712
+ const safeApiKeyId = apiKeyId && /^[A-Za-z0-9_-]{1,256}$/.test(apiKeyId) ? apiKeyId : undefined;
110713
+ const safeApiKeyScopes = Array.isArray(apiKeyScopes) && apiKeyScopes.length <= 32 && apiKeyScopes.every((scope) => typeof scope === "string" && scope.length <= 128 && /^(?:\*|[a-z][a-z0-9_-]*:(?:\*|[a-z][a-z0-9_-]*))$/.test(scope)) ? [...new Set(apiKeyScopes)] : undefined;
110485
110714
  return {
110486
110715
  status: "authenticated",
110487
110716
  authSource,
@@ -110491,7 +110720,8 @@ function authIdentityPayload(authSource, live, cached2, offline = false) {
110491
110720
  ...orgName ? { organizationName: orgName } : {},
110492
110721
  ...userId ? { userId } : {},
110493
110722
  ...orgId ? { orgId } : {},
110494
- ...role3 ? { role: role3 } : {}
110723
+ ...role3 ? { role: role3 } : {},
110724
+ ...safeApiKeyId && safeApiKeyScopes ? { apiKeyId: safeApiKeyId, scopes: safeApiKeyScopes } : {}
110495
110725
  };
110496
110726
  }
110497
110727
  function printWhoami(payload) {
@@ -110501,6 +110731,8 @@ function printWhoami(payload) {
110501
110731
  console.log(source_default.bold("Org: ") + payload.organization);
110502
110732
  if (payload.role)
110503
110733
  console.log(source_default.bold("Role: ") + payload.role);
110734
+ if (payload.apiKeyId)
110735
+ console.log(source_default.bold("Key: ") + payload.apiKeyId);
110504
110736
  if (payload.organizationName)
110505
110737
  console.log(source_default.bold("Name: ") + payload.organizationName);
110506
110738
  if (payload.authSource)
@@ -110822,6 +111054,25 @@ function registerAuth(parent) {
110822
111054
  writeCommandError(error61, "Failed to revoke API key", options.json);
110823
111055
  }
110824
111056
  });
111057
+ keys3.command("add-publish-scope").argument("<key-id>").requiredOption("--expected-scopes <csv>", "Current comma-separated scopes; stale values are refused").option("--json", "Output as JSON", false).description("Add skills:publish to one existing key without minting or rotating it").action(async (id3, options) => {
111058
+ try {
111059
+ const connection = await resolveSkillsConnection();
111060
+ if (!connection)
111061
+ throw new Error("A hosted Skills credential is required for key scope administration");
111062
+ const identity4 = await apiRequest("/api/auth/whoami", { headers: { Authorization: `Bearer ${connection.apiKey}` } }, connection.apiOrigin);
111063
+ const identityOrg = recordField(identity4.organization);
111064
+ const orgId = stringField2(identityOrg?.id);
111065
+ if (!orgId)
111066
+ throw new Error("The Skills authority did not return a tenant identity; refusing key scope administration");
111067
+ const result2 = await new RemoteSkillsClient(connection.apiKey, connection.apiOrigin).addSkillPublishScope(id3, options.expectedScopes.split(",").map((scope) => scope.trim()).filter(Boolean), orgId);
111068
+ if (options.json || !isTTY)
111069
+ console.log(JSON.stringify(result2, null, 2));
111070
+ else
111071
+ console.log(source_default.green(`Added skills:publish to ${id3}; scopes read back from the server.`));
111072
+ } catch (error61) {
111073
+ writeCommandError(error61, "Failed to add Skills publication scope", options.json);
111074
+ }
111075
+ });
110825
111076
  auth.command("login").description("Sign in with browser/device code or email code").option("--email <email>", "Email address (non-interactive)").option("--code <code>", "Verification code (non-interactive)").option("--membership-id <id>", "Enroll an exact workspace membership into an explicit HASNA_PROFILE").option("--code-stdin", "Read a fresh six-digit code for workspace enrollment from stdin").option("--api-key <key>", "Verify and store an API key").option("--device", "Use browser/device-code login", false).option("--no-open", "Do not open a browser for device-code login").option("--poll", "Poll until browser authentication completes in non-interactive mode", false).option("--poll-timeout-ms <ms>", "Maximum time to wait for device-code login").option("--json", "Output result as JSON", false).action(async (options) => {
110826
111077
  if (options.membershipId !== undefined) {
110827
111078
  if (options.apiKey || options.device || options.code || options.poll) {
@@ -119885,10 +120136,11 @@ var { registerPortableSkillCommands: registerPortableSkillCommands2 } = await Pr
119885
120136
  registerPortableSkillCommands2(program2);
119886
120137
  var { registerSchedule: registerSchedule2 } = await Promise.resolve().then(() => (init_schedule(), exports_schedule));
119887
120138
  registerSchedule2(program2);
119888
- var { registerRegistry: registerRegistry2, registerPull: registerPull2, registerVersions: registerVersions2 } = await Promise.resolve().then(() => (init_registry2(), exports_registry2));
120139
+ var { registerRegistry: registerRegistry2, registerPull: registerPull2, registerVersions: registerVersions2, registerLifecycle: registerLifecycle2 } = await Promise.resolve().then(() => (init_registry2(), exports_registry2));
119889
120140
  registerRegistry2(program2);
119890
120141
  registerPull2(program2);
119891
120142
  registerVersions2(program2);
120143
+ registerLifecycle2(program2);
119892
120144
  var { registerPublish: registerPublish2 } = await Promise.resolve().then(() => (init_publish(), exports_publish));
119893
120145
  registerPublish2(program2);
119894
120146
  var { registerPrivatePublications: registerPrivatePublications2 } = await Promise.resolve().then(() => (init_private_publications(), exports_private_publications));