@hasna/skills 0.9.1 → 0.9.3

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/dist/index.js CHANGED
@@ -11448,7 +11448,7 @@ import { isAbsolute as isAbsolute10, join as join20, resolve as resolve10 } from
11448
11448
  // package.json
11449
11449
  var package_default = {
11450
11450
  name: "@hasna/skills",
11451
- version: "0.9.1",
11451
+ version: "0.9.3",
11452
11452
  description: "Skills library for AI coding agents",
11453
11453
  type: "module",
11454
11454
  bin: {
@@ -11457,6 +11457,7 @@ var package_default = {
11457
11457
  "skills-serve": "bin/server.js",
11458
11458
  "skills-server": "bin/server.js",
11459
11459
  "skills-worker": "bin/worker.js",
11460
+ "skills-maintenance": "bin/maintenance.js",
11460
11461
  "skills-migrate": "bin/migrate.js"
11461
11462
  },
11462
11463
  exports: {
@@ -11497,7 +11498,7 @@ var package_default = {
11497
11498
  types: "./dist/index.d.ts",
11498
11499
  scripts: {
11499
11500
  clean: "rm -rf bin/ dist/",
11500
- build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
11501
+ build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/server/maintenance.ts --outfile ./bin/maintenance.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
11501
11502
  "build:js": "rm -rf dist && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
11502
11503
  test: "bun test --timeout 30000",
11503
11504
  dev: "bun run ./src/cli/index.tsx",
@@ -15738,6 +15739,35 @@ function treeHash(root2) {
15738
15739
  visit2(root2);
15739
15740
  return hash2.digest("hex");
15740
15741
  }
15742
+ function selectNativeMigrationTargets(inventory, manifest) {
15743
+ if (manifest.schema !== "hasna.skills-native-migration-targets.v1" || !/^[0-9a-f]{64}$/.test(manifest.digest) || !Array.isArray(manifest.targets) || manifest.targets.length === 0)
15744
+ throw new Error("Invalid native migration target manifest");
15745
+ const identities = new Set;
15746
+ const selected = [];
15747
+ for (const target of manifest.targets) {
15748
+ const projectRoot = resolve15(target.projectRoot), normalized = relative5(projectRoot, resolve15(projectRoot, target.path));
15749
+ if (!isAbsolute15(target.projectRoot) || projectRoot !== target.projectRoot || !existsSync11(projectRoot) || !lstatSync18(projectRoot).isDirectory())
15750
+ throw new Error(`Native migration target project root must be an existing absolute directory: ${target.projectRoot}`);
15751
+ assertSafePath(projectRoot);
15752
+ 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))
15753
+ throw new Error(`Invalid native migration target: ${target.path}`);
15754
+ const identity3 = `${target.agent}\x00${projectRoot}\x00${normalized}`;
15755
+ if (identities.has(identity3))
15756
+ throw new Error(`Duplicate native migration target: ${target.path}`);
15757
+ identities.add(identity3);
15758
+ const expected = resolve15(target.projectRoot, target.path);
15759
+ const matches = inventory.filter((entry2) => entry2.agent === target.agent && resolve15(entry2.path) === expected);
15760
+ if (matches.length !== 1)
15761
+ throw new Error(`Native migration target was not found exactly once: ${target.agent} ${target.projectRoot}/${target.path}`);
15762
+ const entry = matches[0];
15763
+ if (entry.bridge || entry.system || entry.vendor)
15764
+ throw new Error(`Native migration target is protected or vendor-owned: ${entry.path}`);
15765
+ if (entry.hash !== target.treeSha256 || treeHash(entry.path) !== target.treeSha256)
15766
+ throw new Error(`Native migration target changed after review: ${entry.path}`);
15767
+ selected.push(entry);
15768
+ }
15769
+ return selected;
15770
+ }
15741
15771
  function projectAncestorDirectories(projects) {
15742
15772
  const directories = new Set;
15743
15773
  for (const project of projects) {
@@ -15755,10 +15785,12 @@ function projectAncestorDirectories(projects) {
15755
15785
  }
15756
15786
  function inventoryNativeSkills(home = homedir3(), options = {}) {
15757
15787
  const aliases = rootAliases(home, options.allowRootAliases);
15758
- const roots = ROOTS.map(([agent, path]) => [agent, canonicalAgentPath(join25(home, path), aliases)]);
15788
+ const selectedAgents = options.agents ? new Set(options.agents) : undefined;
15789
+ const rootDefinitions = selectedAgents ? ROOTS.filter(([agent, path]) => selectedAgents.has(agent) || selectedAgents.has("hermes") && agent === "codex" && path === ".agents/skills") : ROOTS;
15790
+ const roots = rootDefinitions.map(([agent, path]) => [agent, canonicalAgentPath(join25(home, path), aliases)]);
15759
15791
  const bridgePaths = Object.values(AGENT_ADAPTERS).map((adapter) => canonicalAgentPath(join25(home, adapter.root, CLI_BRIDGE_NAME), aliases));
15760
15792
  for (const project of projectAncestorDirectories([...options.projectDirs ?? [], ...options.projectDir ? [options.projectDir] : []])) {
15761
- for (const [agent, path] of ROOTS)
15793
+ for (const [agent, path] of rootDefinitions)
15762
15794
  roots.push([agent, canonicalAgentPath(join25(project, path), aliases)]);
15763
15795
  }
15764
15796
  const entries = [], seen = new Set;
@@ -16014,7 +16046,7 @@ function planAgentIntegration(options) {
16014
16046
  return [agent, { command, profileId: profileId2 }];
16015
16047
  }));
16016
16048
  const discoveries = [...new Set(options.agents)].map((agent) => resolveAgentDiscovery({ home, agent, reviewed: options.discoveryInputs, canonical: (path) => canonicalAgentPath(path, aliases) }));
16017
- const nativeSkills = inventoryNativeSkills(home, { includeVendor: true, guardHermes: options.agents.includes("hermes"), projectDir: options.projectDir, agentRoots: discoveries.flatMap((binding) => binding.roots.map((path) => ({ agent: binding.agent, path }))), allowRootAliases: options.allowRootAliases });
16049
+ const nativeSkills = inventoryNativeSkills(home, { includeVendor: true, guardHermes: options.agents.includes("hermes"), agents: options.agents, projectDir: options.projectDir, agentRoots: discoveries.flatMap((binding) => binding.roots.map((path) => ({ agent: binding.agent, path }))), allowRootAliases: options.allowRootAliases });
16018
16050
  const changes = [];
16019
16051
  for (const agent of [...new Set(options.agents)]) {
16020
16052
  if (!INTEGRATION_AGENTS.includes(agent))
@@ -16226,12 +16258,13 @@ function archiveNativeSkills(inventory, options) {
16226
16258
  if (!expected || treeHash(entry.path) !== entry.hash || !isOwnedCliBridge(entry.path, [expected]))
16227
16259
  throw new Error(`Skills bridge changed after planning: ${entry.path}`);
16228
16260
  }
16229
- const selected = inventory.filter((entry) => !entry.bridge && !entry.system && (entry.vendor ? options.includeVendor : entry.managed || options.includeUnmanaged));
16261
+ const selected = options.targetManifest ? selectNativeMigrationTargets(inventory, options.targetManifest) : inventory.filter((entry) => !entry.bridge && !entry.system && (entry.vendor ? options.includeVendor : entry.managed || options.includeUnmanaged));
16230
16262
  for (const entry of selected)
16231
16263
  if (treeHash(entry.path) !== entry.hash)
16232
16264
  throw new Error(`Native skill changed after planning: ${entry.path}`);
16265
+ const targetManifest = options.targetManifest ? { schema: options.targetManifest.schema, digest: options.targetManifest.digest, targetCount: options.targetManifest.targets.length } : undefined;
16233
16266
  if (!selected.length)
16234
- return { entries: [], ...aliases.length ? { rootAliases: aliases } : {} };
16267
+ return { entries: [], ...targetManifest ? { targetManifest } : {}, ...aliases.length ? { rootAliases: aliases } : {} };
16235
16268
  const operationId = randomUUID3(), archiveRoot = join25(options.dataDir ?? getDataDir(), "migration", operationId, "native"), receiptPath = join25(archiveRoot, "receipt.json");
16236
16269
  const entries = selected.map((entry, index) => ({
16237
16270
  source: entry.vendor ? join25(entry.path, "SKILL.md") : entry.path,
@@ -16240,7 +16273,7 @@ function archiveNativeSkills(inventory, options) {
16240
16273
  ...entry.vendor ? { discoveryOnly: true } : {},
16241
16274
  status: "planned"
16242
16275
  }));
16243
- const journal = { version: 2, operationId, status: "planned", entries, ...aliases.length ? { rootAliases: aliases } : {} };
16276
+ const journal = { version: 2, operationId, status: "planned", entries, ...targetManifest ? { targetManifest } : {}, ...aliases.length ? { rootAliases: aliases } : {} };
16244
16277
  const moved = [];
16245
16278
  const verifyArchive = (entry) => {
16246
16279
  assertSafePath(entry.archive);
@@ -16334,7 +16367,7 @@ function archiveNativeSkills(inventory, options) {
16334
16367
  } catch {}
16335
16368
  throw new Error(`Native archive failed; inspect the recovery journal at ${receiptPath} before retrying.`, { cause: error });
16336
16369
  }
16337
- return { entries: entries.map(({ status, conflict, ...entry }) => entry), receiptPath, ...aliases.length ? { rootAliases: aliases } : {} };
16370
+ return { entries: entries.map(({ status, conflict, ...entry }) => entry), receiptPath, ...targetManifest ? { targetManifest } : {}, ...aliases.length ? { rootAliases: aliases } : {} };
16338
16371
  }
16339
16372
  function assertNativeExportAllowed(dataDir2 = getDataDirReadOnly()) {
16340
16373
  if (requiresCliSkillLoading(dataDir2))
@@ -22,6 +22,17 @@ export interface NativeSkillEntry {
22
22
  bridgeHome?: string;
23
23
  rootAlias?: AgentRootAlias;
24
24
  }
25
+ export interface NativeMigrationTarget {
26
+ agent: string;
27
+ projectRoot: string;
28
+ path: string;
29
+ treeSha256: string;
30
+ }
31
+ export interface NativeMigrationTargetManifest {
32
+ schema: "hasna.skills-native-migration-targets.v1";
33
+ targets: NativeMigrationTarget[];
34
+ digest: string;
35
+ }
25
36
  export interface AgentConfigChange {
26
37
  path: string;
27
38
  before: string | null;
@@ -40,11 +51,21 @@ export interface AgentIntegrationPlan {
40
51
  discoveryAfter?: AgentDiscoveryBinding[];
41
52
  rootAliases?: AgentRootAlias[];
42
53
  }
54
+ /** Parse the reviewed exact-target binding used by native migration. Paths are
55
+ * deliberately project-relative so a manifest cannot silently select another
56
+ * checkout when a project root changes. */
57
+ export declare function parseNativeMigrationTargetManifest(input: string | Buffer): NativeMigrationTargetManifest;
58
+ /** Read a manifest without following links or opening special files, with a
59
+ * finite bound so a CLI cannot block on an untrusted path. */
60
+ export declare function readNativeMigrationTargetManifest(path: string): NativeMigrationTargetManifest;
61
+ /** Validate every reviewed target before any archive directory or journal is created. */
62
+ export declare function selectNativeMigrationTargets(inventory: NativeSkillEntry[], manifest: NativeMigrationTargetManifest): NativeSkillEntry[];
43
63
  export declare function inventoryNativeSkills(home?: string, options?: {
44
64
  includeVendor?: boolean;
45
65
  guardHermes?: boolean;
46
66
  projectDir?: string;
47
67
  projectDirs?: string[];
68
+ agents?: readonly IntegrationAgent[];
48
69
  agentRoots?: Array<{
49
70
  agent: string;
50
71
  path: string;
@@ -75,6 +96,7 @@ export declare function archiveNativeSkills(inventory: NativeSkillEntry[], optio
75
96
  includeUnmanaged?: boolean;
76
97
  includeVendor?: boolean;
77
98
  allowRootAliases?: boolean;
99
+ targetManifest?: NativeMigrationTargetManifest;
78
100
  }): {
79
101
  entries: Array<{
80
102
  source: string;
@@ -84,6 +106,11 @@ export declare function archiveNativeSkills(inventory: NativeSkillEntry[], optio
84
106
  }>;
85
107
  receiptPath?: string;
86
108
  rootAliases?: AgentRootAlias[];
109
+ targetManifest?: {
110
+ schema: string;
111
+ digest: string;
112
+ targetCount: number;
113
+ };
87
114
  };
88
115
  /** Run before any prompt context load. Missing ownership or reappearing native
89
116
  * discovery files require repair; verified cache availability is not an override. */
@@ -1,4 +1,4 @@
1
- export declare const SUPPORTED_CODEX_HOOK_VERSIONS: readonly ["codex-cli 0.153.0", "codex-cli 0.154.0", "codex-cli 0.155.0"];
1
+ export declare const SUPPORTED_CODEX_HOOK_VERSIONS: readonly ["codex-cli 0.153.0", "codex-cli 0.154.0", "codex-cli 0.155.0", "codex-cli 0.155.1"];
2
2
  export interface CodexHookRpc {
3
3
  version: string;
4
4
  processId?: number;
package/dist/sdk/index.js CHANGED
@@ -32373,7 +32373,7 @@ var init_event_sink = __esm(() => {
32373
32373
  // package.json
32374
32374
  var package_default = {
32375
32375
  name: "@hasna/skills",
32376
- version: "0.9.1",
32376
+ version: "0.9.3",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
@@ -32382,6 +32382,7 @@ var package_default = {
32382
32382
  "skills-serve": "bin/server.js",
32383
32383
  "skills-server": "bin/server.js",
32384
32384
  "skills-worker": "bin/worker.js",
32385
+ "skills-maintenance": "bin/maintenance.js",
32385
32386
  "skills-migrate": "bin/migrate.js"
32386
32387
  },
32387
32388
  exports: {
@@ -32422,7 +32423,7 @@ var package_default = {
32422
32423
  types: "./dist/index.d.ts",
32423
32424
  scripts: {
32424
32425
  clean: "rm -rf bin/ dist/",
32425
- build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
32426
+ build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/server/maintenance.ts --outfile ./bin/maintenance.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
32426
32427
  "build:js": "rm -rf dist && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
32427
32428
  test: "bun test --timeout 30000",
32428
32429
  dev: "bun run ./src/cli/index.tsx",
@@ -40661,6 +40662,35 @@ function treeHash(root2) {
40661
40662
  visit2(root2);
40662
40663
  return hash2.digest("hex");
40663
40664
  }
40665
+ function selectNativeMigrationTargets(inventory, manifest) {
40666
+ if (manifest.schema !== "hasna.skills-native-migration-targets.v1" || !/^[0-9a-f]{64}$/.test(manifest.digest) || !Array.isArray(manifest.targets) || manifest.targets.length === 0)
40667
+ throw new Error("Invalid native migration target manifest");
40668
+ const identities = new Set;
40669
+ const selected = [];
40670
+ for (const target of manifest.targets) {
40671
+ const projectRoot = resolve14(target.projectRoot), normalized = relative3(projectRoot, resolve14(projectRoot, target.path));
40672
+ if (!isAbsolute15(target.projectRoot) || projectRoot !== target.projectRoot || !existsSync6(projectRoot) || !lstatSync17(projectRoot).isDirectory())
40673
+ throw new Error(`Native migration target project root must be an existing absolute directory: ${target.projectRoot}`);
40674
+ assertSafePath(projectRoot);
40675
+ if (isAbsolute15(target.path) || target.path.includes("\\") || target.path.includes("\x00") || !normalized || normalized === ".." || normalized.startsWith(`..${sep4}`) || normalized !== target.path || !/^[0-9a-f]{64}$/.test(target.treeSha256))
40676
+ throw new Error(`Invalid native migration target: ${target.path}`);
40677
+ const identity3 = `${target.agent}\x00${projectRoot}\x00${normalized}`;
40678
+ if (identities.has(identity3))
40679
+ throw new Error(`Duplicate native migration target: ${target.path}`);
40680
+ identities.add(identity3);
40681
+ const expected = resolve14(target.projectRoot, target.path);
40682
+ const matches = inventory.filter((entry2) => entry2.agent === target.agent && resolve14(entry2.path) === expected);
40683
+ if (matches.length !== 1)
40684
+ throw new Error(`Native migration target was not found exactly once: ${target.agent} ${target.projectRoot}/${target.path}`);
40685
+ const entry = matches[0];
40686
+ if (entry.bridge || entry.system || entry.vendor)
40687
+ throw new Error(`Native migration target is protected or vendor-owned: ${entry.path}`);
40688
+ if (entry.hash !== target.treeSha256 || treeHash(entry.path) !== target.treeSha256)
40689
+ throw new Error(`Native migration target changed after review: ${entry.path}`);
40690
+ selected.push(entry);
40691
+ }
40692
+ return selected;
40693
+ }
40664
40694
  function projectAncestorDirectories(projects) {
40665
40695
  const directories = new Set;
40666
40696
  for (const project of projects) {
@@ -40678,10 +40708,12 @@ function projectAncestorDirectories(projects) {
40678
40708
  }
40679
40709
  function inventoryNativeSkills(home = homedir3(), options = {}) {
40680
40710
  const aliases = rootAliases(home, options.allowRootAliases);
40681
- const roots = ROOTS.map(([agent, path]) => [agent, canonicalAgentPath(join21(home, path), aliases)]);
40711
+ const selectedAgents = options.agents ? new Set(options.agents) : undefined;
40712
+ const rootDefinitions = selectedAgents ? ROOTS.filter(([agent, path]) => selectedAgents.has(agent) || selectedAgents.has("hermes") && agent === "codex" && path === ".agents/skills") : ROOTS;
40713
+ const roots = rootDefinitions.map(([agent, path]) => [agent, canonicalAgentPath(join21(home, path), aliases)]);
40682
40714
  const bridgePaths = Object.values(AGENT_ADAPTERS).map((adapter) => canonicalAgentPath(join21(home, adapter.root, CLI_BRIDGE_NAME), aliases));
40683
40715
  for (const project of projectAncestorDirectories([...options.projectDirs ?? [], ...options.projectDir ? [options.projectDir] : []])) {
40684
- for (const [agent, path] of ROOTS)
40716
+ for (const [agent, path] of rootDefinitions)
40685
40717
  roots.push([agent, canonicalAgentPath(join21(project, path), aliases)]);
40686
40718
  }
40687
40719
  const entries = [], seen = new Set;
@@ -40937,7 +40969,7 @@ function planAgentIntegration(options) {
40937
40969
  return [agent, { command, profileId: profileId2 }];
40938
40970
  }));
40939
40971
  const discoveries = [...new Set(options.agents)].map((agent) => resolveAgentDiscovery({ home, agent, reviewed: options.discoveryInputs, canonical: (path) => canonicalAgentPath(path, aliases) }));
40940
- const nativeSkills = inventoryNativeSkills(home, { includeVendor: true, guardHermes: options.agents.includes("hermes"), projectDir: options.projectDir, agentRoots: discoveries.flatMap((binding) => binding.roots.map((path) => ({ agent: binding.agent, path }))), allowRootAliases: options.allowRootAliases });
40972
+ const nativeSkills = inventoryNativeSkills(home, { includeVendor: true, guardHermes: options.agents.includes("hermes"), agents: options.agents, projectDir: options.projectDir, agentRoots: discoveries.flatMap((binding) => binding.roots.map((path) => ({ agent: binding.agent, path }))), allowRootAliases: options.allowRootAliases });
40941
40973
  const changes = [];
40942
40974
  for (const agent of [...new Set(options.agents)]) {
40943
40975
  if (!INTEGRATION_AGENTS.includes(agent))
@@ -41149,12 +41181,13 @@ function archiveNativeSkills(inventory, options) {
41149
41181
  if (!expected || treeHash(entry.path) !== entry.hash || !isOwnedCliBridge(entry.path, [expected]))
41150
41182
  throw new Error(`Skills bridge changed after planning: ${entry.path}`);
41151
41183
  }
41152
- const selected = inventory.filter((entry) => !entry.bridge && !entry.system && (entry.vendor ? options.includeVendor : entry.managed || options.includeUnmanaged));
41184
+ const selected = options.targetManifest ? selectNativeMigrationTargets(inventory, options.targetManifest) : inventory.filter((entry) => !entry.bridge && !entry.system && (entry.vendor ? options.includeVendor : entry.managed || options.includeUnmanaged));
41153
41185
  for (const entry of selected)
41154
41186
  if (treeHash(entry.path) !== entry.hash)
41155
41187
  throw new Error(`Native skill changed after planning: ${entry.path}`);
41188
+ const targetManifest = options.targetManifest ? { schema: options.targetManifest.schema, digest: options.targetManifest.digest, targetCount: options.targetManifest.targets.length } : undefined;
41156
41189
  if (!selected.length)
41157
- return { entries: [], ...aliases.length ? { rootAliases: aliases } : {} };
41190
+ return { entries: [], ...targetManifest ? { targetManifest } : {}, ...aliases.length ? { rootAliases: aliases } : {} };
41158
41191
  const operationId = randomUUID3(), archiveRoot = join21(options.dataDir ?? getDataDir(), "migration", operationId, "native"), receiptPath = join21(archiveRoot, "receipt.json");
41159
41192
  const entries = selected.map((entry, index) => ({
41160
41193
  source: entry.vendor ? join21(entry.path, "SKILL.md") : entry.path,
@@ -41163,7 +41196,7 @@ function archiveNativeSkills(inventory, options) {
41163
41196
  ...entry.vendor ? { discoveryOnly: true } : {},
41164
41197
  status: "planned"
41165
41198
  }));
41166
- const journal = { version: 2, operationId, status: "planned", entries, ...aliases.length ? { rootAliases: aliases } : {} };
41199
+ const journal = { version: 2, operationId, status: "planned", entries, ...targetManifest ? { targetManifest } : {}, ...aliases.length ? { rootAliases: aliases } : {} };
41167
41200
  const moved = [];
41168
41201
  const verifyArchive = (entry) => {
41169
41202
  assertSafePath(entry.archive);
@@ -41257,7 +41290,7 @@ function archiveNativeSkills(inventory, options) {
41257
41290
  } catch {}
41258
41291
  throw new Error(`Native archive failed; inspect the recovery journal at ${receiptPath} before retrying.`, { cause: error });
41259
41292
  }
41260
- return { entries: entries.map(({ status, conflict, ...entry }) => entry), receiptPath, ...aliases.length ? { rootAliases: aliases } : {} };
41293
+ return { entries: entries.map(({ status, conflict, ...entry }) => entry), receiptPath, ...targetManifest ? { targetManifest } : {}, ...aliases.length ? { rootAliases: aliases } : {} };
41261
41294
  }
41262
41295
  function assertNativeExportAllowed(dataDir2 = getDataDirReadOnly()) {
41263
41296
  if (requiresCliSkillLoading(dataDir2))
@@ -55195,6 +55228,10 @@ class SkillLifecycleConflictError extends Error {
55195
55228
  this.name = "SkillLifecycleConflictError";
55196
55229
  }
55197
55230
  }
55231
+ function validOperatorScopeEnrollmentInput(input) {
55232
+ const bounded = (value, max = 256) => typeof value === "string" && value.length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value);
55233
+ return bounded(input.keyId) && bounded(input.stationId) && bounded(input.orgId) && bounded(input.operationId) && bounded(input.manifestDigest, 64) && /^[a-f0-9]{64}$/.test(input.manifestDigest) && bounded(input.operatorJobId) && bounded(input.operatorTaskArn, 512) && input.expectedScopes.length > 0 && input.expectedScopes.length <= 32 && input.expectedScopes.every((scope) => bounded(scope, 128) && /^[a-z][a-z0-9_-]*:[a-z][a-z0-9_-]*$/.test(scope));
55234
+ }
55198
55235
 
55199
55236
  // src/lib/revision.ts
55200
55237
  import { createHash as createHash20 } from "crypto";
@@ -55343,6 +55380,54 @@ class SqliteSkillsStore {
55343
55380
  }
55344
55381
  return { kind: "updated", scopes };
55345
55382
  }
55383
+ async enrollPublishScopeByOperator(input) {
55384
+ if (!validOperatorScopeEnrollmentInput(input))
55385
+ return { kind: "invalid" };
55386
+ if (input.expectedScopes.includes("skills:publish"))
55387
+ return { kind: "stale", scopes: input.expectedScopes };
55388
+ const operation = this.get("SELECT org_id, target_id FROM skills_audit_events WHERE action = ? AND operator_operation_id = ? LIMIT 1", ["api_key_scopes_added", input.operationId]);
55389
+ if (operation && (String(operation.org_id) !== input.orgId || String(operation.target_id) !== input.keyId))
55390
+ return { kind: "target_mismatch" };
55391
+ const row = this.get("SELECT id, org_id, name, scopes_json FROM api_keys WHERE id = ? AND revoked_at IS NULL LIMIT 1", [input.keyId]);
55392
+ if (!row)
55393
+ return { kind: "not_found" };
55394
+ if (String(row.org_id) !== input.orgId)
55395
+ return { kind: "target_mismatch" };
55396
+ const current = parseScopes(row.scopes_json);
55397
+ const prior = this.get("SELECT metadata_json FROM skills_audit_events WHERE action = ? AND target_type = ? AND target_id = ? AND json_extract(metadata_json, '$.operator_operation_id') = ? LIMIT 1", ["api_key_scopes_added", "api_key", input.keyId, input.operationId]);
55398
+ if (prior) {
55399
+ try {
55400
+ if (JSON.parse(String(prior.metadata_json)).target_manifest_digest !== input.manifestDigest)
55401
+ return { kind: "target_mismatch" };
55402
+ } catch {
55403
+ return { kind: "target_mismatch" };
55404
+ }
55405
+ return current.includes("skills:publish") ? { kind: "already_applied", scopes: current } : { kind: "stale", scopes: current };
55406
+ }
55407
+ if (current.length !== input.expectedScopes.length || current.some((scope, index) => scope !== input.expectedScopes[index]))
55408
+ return { kind: "stale", scopes: current };
55409
+ const scopes = [...current, "skills:publish"];
55410
+ const tx = this.db.transaction(() => {
55411
+ const changed = this.db.run("UPDATE api_keys SET scopes_json = ? WHERE id = ? AND org_id = ? AND revoked_at IS NULL AND scopes_json = ?", [JSON.stringify(scopes), input.keyId, input.orgId, JSON.stringify(input.expectedScopes)]);
55412
+ if (changed.changes !== 1)
55413
+ return false;
55414
+ this.db.run("INSERT INTO skills_audit_events (org_id, user_id, api_key_id, action, target_type, target_id, operator_operation_id, metadata_json) VALUES (?, NULL, NULL, ?, ?, ?, ?, ?)", [input.orgId, "api_key_scopes_added", "api_key", input.keyId, input.operationId, JSON.stringify({ added: ["skills:publish"], scopes, operator_operation_id: input.operationId, operator_job_id: input.operatorJobId, operator_task_arn: input.operatorTaskArn, target_manifest_digest: input.manifestDigest, station_id: input.stationId })]);
55415
+ return true;
55416
+ });
55417
+ if (!tx()) {
55418
+ const latest = this.get("SELECT scopes_json FROM api_keys WHERE id = ? AND org_id = ? AND revoked_at IS NULL LIMIT 1", [input.keyId, input.orgId]);
55419
+ return { kind: "stale", scopes: latest ? parseScopes(latest.scopes_json) : current };
55420
+ }
55421
+ return { kind: "updated", scopes };
55422
+ }
55423
+ async inspectOperatorScopeTarget(keyId, orgId) {
55424
+ const row = this.get("SELECT org_id, scopes_json FROM api_keys WHERE id = ? AND revoked_at IS NULL LIMIT 1", [keyId]);
55425
+ if (!row)
55426
+ return { kind: "not_found" };
55427
+ if (String(row.org_id) !== orgId)
55428
+ return { kind: "target_mismatch" };
55429
+ return { kind: "found", scopes: parseScopes(row.scopes_json) };
55430
+ }
55346
55431
  async createRun(input) {
55347
55432
  if (input.idempotencyKey) {
55348
55433
  const existing = this.get("SELECT * FROM skills_runs WHERE org_id = ? AND idempotency_key = ? LIMIT 1", [input.principal.orgId, input.idempotencyKey]);
@@ -62590,6 +62675,7 @@ class MemorySkillsStore {
62590
62675
  bundles = new Map;
62591
62676
  versions = new Map;
62592
62677
  pins = new Map;
62678
+ operatorEnrollments = new Map;
62593
62679
  constructor(apiKeys = []) {
62594
62680
  for (const key of apiKeys)
62595
62681
  this.addApiKey(key.token, key.principal);
@@ -62619,6 +62705,41 @@ class MemorySkillsStore {
62619
62705
  }
62620
62706
  return { kind: "not_found" };
62621
62707
  }
62708
+ async enrollPublishScopeByOperator(input) {
62709
+ if (!validOperatorScopeEnrollmentInput(input))
62710
+ return { kind: "invalid" };
62711
+ const previous = this.operatorEnrollments.get(input.operationId);
62712
+ if (previous) {
62713
+ if (previous.keyId !== input.keyId || previous.manifestDigest !== input.manifestDigest)
62714
+ return { kind: "target_mismatch" };
62715
+ for (const target of this.apiKeys.values()) {
62716
+ if (target.apiKeyId === input.keyId && target.orgId === input.orgId && target.scopes.includes("skills:publish")) {
62717
+ return { kind: "already_applied", scopes: [...target.scopes] };
62718
+ }
62719
+ }
62720
+ return { kind: "stale", scopes: [] };
62721
+ }
62722
+ for (const [hash3, target] of this.apiKeys) {
62723
+ if (target.apiKeyId !== input.keyId || target.orgId !== input.orgId)
62724
+ continue;
62725
+ if (target.scopes.length !== input.expectedScopes.length || target.scopes.some((scope2, index) => scope2 !== input.expectedScopes[index])) {
62726
+ return { kind: "stale", scopes: [...target.scopes] };
62727
+ }
62728
+ const scopes = target.scopes.includes("skills:publish") ? [...target.scopes] : [...target.scopes, "skills:publish"];
62729
+ this.apiKeys.set(hash3, { ...target, scopes });
62730
+ this.operatorEnrollments.set(input.operationId, { keyId: input.keyId, manifestDigest: input.manifestDigest });
62731
+ return { kind: "updated", scopes };
62732
+ }
62733
+ return { kind: "not_found" };
62734
+ }
62735
+ async inspectOperatorScopeTarget(keyId, orgId) {
62736
+ for (const target of this.apiKeys.values()) {
62737
+ if (target.apiKeyId !== keyId)
62738
+ continue;
62739
+ return target.orgId === orgId ? { kind: "found", scopes: [...target.scopes] } : { kind: "target_mismatch" };
62740
+ }
62741
+ return { kind: "not_found" };
62742
+ }
62622
62743
  async createRun(input) {
62623
62744
  const idemKey = input.idempotencyKey?.trim();
62624
62745
  const idemMapKey = idemKey ? `${input.principal.orgId}:${idemKey}` : undefined;
@@ -63033,24 +63154,98 @@ class PostgresSkillsStore {
63033
63154
  if (current.length !== expectedScopes.length || current.some((scope2, index) => scope2 !== expectedScopes[index])) {
63034
63155
  return { kind: "stale", scopes: current };
63035
63156
  }
63036
- const scopes = [...current, ...addScopes.filter((scope2) => !current.includes(scope2))];
63037
- const updated = await tx`
63038
- UPDATE api_keys SET scopes_json = ${JSON.stringify(scopes)}::jsonb
63039
- WHERE id = ${keyId} AND org_id = ${actor.orgId} AND revoked_at IS NULL
63040
- AND scopes_json = ${JSON.stringify(expectedScopes)}::jsonb
63041
- RETURNING scopes_json
63042
- `;
63157
+ const updated = await this.casUpdateApiKeyScopes(tx, keyId, actor.orgId, expectedScopes, addScopes);
63043
63158
  if (!updated[0]) {
63044
63159
  const latest = await tx`SELECT scopes_json FROM api_keys WHERE id = ${keyId} AND org_id = ${actor.orgId} AND revoked_at IS NULL LIMIT 1`;
63045
63160
  return { kind: "stale", scopes: latest[0] ? parseJsonArray(latest[0].scopes_json) : current };
63046
63161
  }
63047
63162
  await tx`
63048
63163
  INSERT INTO skills_audit_events (org_id, user_id, api_key_id, action, target_type, target_id, metadata_json)
63049
- VALUES (${actor.orgId}, ${actor.userId}, ${actor.apiKeyId}, ${"api_key_scopes_added"}, ${"api_key"}, ${keyId}, ${JSON.stringify({ added: addScopes, scopes })}::jsonb)
63164
+ VALUES (${actor.orgId}, ${actor.userId}, ${actor.apiKeyId}, ${"api_key_scopes_added"}, ${"api_key"}, ${keyId}, ${JSON.stringify({ added: addScopes, scopes: parseJsonArray(updated[0].scopes_json) })}::jsonb)
63165
+ `;
63166
+ return { kind: "updated", scopes: parseJsonArray(updated[0].scopes_json) };
63167
+ });
63168
+ }
63169
+ async casUpdateApiKeyScopes(tx, keyId, orgId, expectedScopes, addScopes) {
63170
+ const current = expectedScopes;
63171
+ const scopes = [...current, ...addScopes.filter((scope2) => !current.includes(scope2))];
63172
+ return tx`
63173
+ UPDATE api_keys SET scopes_json = ${JSON.stringify(scopes)}::jsonb
63174
+ WHERE id = ${keyId} AND org_id = ${orgId} AND revoked_at IS NULL
63175
+ AND scopes_json = ${JSON.stringify(expectedScopes)}::jsonb
63176
+ RETURNING scopes_json
63177
+ `;
63178
+ }
63179
+ async enrollPublishScopeByOperator(input) {
63180
+ if (!validOperatorScopeEnrollmentInput(input))
63181
+ return { kind: "invalid" };
63182
+ if (input.expectedScopes.includes("skills:publish"))
63183
+ return { kind: "stale", scopes: input.expectedScopes };
63184
+ return this.sql.begin(async (tx) => {
63185
+ const operation = await tx`
63186
+ SELECT org_id, target_id FROM skills_audit_events
63187
+ WHERE action = ${"api_key_scopes_added"} AND operator_operation_id = ${input.operationId}
63188
+ LIMIT 1
63189
+ `;
63190
+ if (operation[0] && (String(operation[0].org_id) !== input.orgId || String(operation[0].target_id) !== input.keyId))
63191
+ return { kind: "target_mismatch" };
63192
+ const rows = await tx`
63193
+ SELECT id, org_id, name, scopes_json FROM api_keys
63194
+ WHERE id = ${input.keyId} AND revoked_at IS NULL
63195
+ FOR UPDATE
63196
+ `;
63197
+ const row = rows[0];
63198
+ if (!row)
63199
+ return { kind: "not_found" };
63200
+ if (String(row.org_id) !== input.orgId)
63201
+ return { kind: "target_mismatch" };
63202
+ const current = parseJsonArray(row.scopes_json);
63203
+ const prior = await tx`
63204
+ SELECT metadata_json, metadata_json->>'target_manifest_digest' AS manifest_digest FROM skills_audit_events
63205
+ WHERE action = ${"api_key_scopes_added"} AND target_type = ${"api_key"} AND target_id = ${input.keyId}
63206
+ AND operator_operation_id = ${input.operationId}
63207
+ LIMIT 1
63208
+ `;
63209
+ if (prior[0]) {
63210
+ const priorMetadata = typeof prior[0].metadata_json === "string" ? JSON.parse(prior[0].metadata_json) : prior[0].metadata_json;
63211
+ const priorDigest = prior[0].manifest_digest ?? priorMetadata?.target_manifest_digest;
63212
+ if (String(priorDigest) !== input.manifestDigest)
63213
+ return { kind: "target_mismatch" };
63214
+ if (!current.includes("skills:publish"))
63215
+ return { kind: "stale", scopes: current };
63216
+ return { kind: "already_applied", scopes: current };
63217
+ }
63218
+ if (current.length !== input.expectedScopes.length || current.some((scope2, index) => scope2 !== input.expectedScopes[index])) {
63219
+ return { kind: "stale", scopes: current };
63220
+ }
63221
+ const updated = await this.casUpdateApiKeyScopes(tx, input.keyId, input.orgId, input.expectedScopes, ["skills:publish"]);
63222
+ if (!updated[0])
63223
+ return { kind: "stale", scopes: current };
63224
+ const scopes = parseJsonArray(updated[0].scopes_json);
63225
+ await tx`
63226
+ INSERT INTO skills_audit_events (org_id, user_id, api_key_id, action, target_type, target_id, operator_operation_id, metadata_json)
63227
+ VALUES (${input.orgId}, NULL, NULL, ${"api_key_scopes_added"}, ${"api_key"}, ${input.keyId}, ${input.operationId}, ${JSON.stringify({
63228
+ added: ["skills:publish"],
63229
+ scopes,
63230
+ operator_operation_id: input.operationId,
63231
+ operator_job_id: input.operatorJobId,
63232
+ operator_task_arn: input.operatorTaskArn,
63233
+ target_manifest_digest: input.manifestDigest,
63234
+ station_id: input.stationId
63235
+ })}::jsonb)
63050
63236
  `;
63051
63237
  return { kind: "updated", scopes };
63052
63238
  });
63053
63239
  }
63240
+ async inspectOperatorScopeTarget(keyId, orgId) {
63241
+ const rows = await this.sql`SELECT org_id, scopes_json FROM api_keys WHERE id = ${keyId} AND revoked_at IS NULL LIMIT 1`;
63242
+ const row = rows[0];
63243
+ if (!row)
63244
+ return { kind: "not_found" };
63245
+ if (String(row.org_id) !== orgId)
63246
+ return { kind: "target_mismatch" };
63247
+ return { kind: "found", scopes: parseJsonArray(row.scopes_json) };
63248
+ }
63054
63249
  async withContext(orgId, worker, fn) {
63055
63250
  return this.sql.begin(async (tx) => {
63056
63251
  await tx`SELECT set_config('app.skills_org_id', ${orgId ?? ""}, true)`;
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerMaintenance(parent: Command): void;
@@ -0,0 +1 @@
1
+ export {};
@@ -15,7 +15,7 @@ import { SqliteExecutionGrantStore } from "./execution-grant-store.js";
15
15
  */
16
16
  import { Database } from "bun:sqlite";
17
17
  import { SqliteSkillSelectionStore } from "./selection-store.js";
18
- import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillVersion, ApiKeyScopeUpdateResult, ServerSkillRecord, SkillLifecyclePatch, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
18
+ import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillVersion, ApiKeyScopeUpdateResult, OperatorScopeEnrollmentInput, OperatorScopeEnrollmentResult, OperatorScopeTargetSnapshot, ServerSkillRecord, SkillLifecyclePatch, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
19
19
  export interface SqliteStoreOptions {
20
20
  /** Apply pending migrations on open. Default true - it is what makes zero-config work. */
21
21
  migrate?: boolean;
@@ -53,6 +53,8 @@ export declare class SqliteSkillsStore implements SkillsProductStore {
53
53
  ensureBootstrapApiKey(token: string, principal?: Partial<ApiPrincipal>): Promise<void>;
54
54
  authenticateApiKeyHash(hash: string): Promise<ApiPrincipal | null>;
55
55
  updateApiKeyScopes(actor: ApiPrincipal, keyId: string, expectedScopes: string[], addScopes: string[]): Promise<ApiKeyScopeUpdateResult>;
56
+ enrollPublishScopeByOperator(input: OperatorScopeEnrollmentInput): Promise<OperatorScopeEnrollmentResult>;
57
+ inspectOperatorScopeTarget(keyId: string, orgId: string): Promise<OperatorScopeTargetSnapshot>;
56
58
  createRun(input: CreateRunInput): Promise<ServerRunRecord>;
57
59
  listRuns(principal: ApiPrincipal, limit: number): Promise<ServerRunRecord[]>;
58
60
  getRun(principal: ApiPrincipal, id: string): Promise<ServerRunRecord | null>;
@@ -1,6 +1,6 @@
1
1
  import { MemoryExecutionGrantStore, PostgresExecutionGrantStore } from "./execution-grant-store.js";
2
2
  import { MemorySkillSelectionStore, PostgresSkillSelectionStore } from "./selection-store.js";
3
- import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord, ServerSkillVersion, ApiKeyScopeUpdateResult, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
3
+ import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord, ServerSkillVersion, ApiKeyScopeUpdateResult, OperatorScopeEnrollmentInput, OperatorScopeEnrollmentResult, OperatorScopeTargetSnapshot, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
4
4
  import { type SkillLifecyclePatch } from "./types.js";
5
5
  import { type SqliteStoreOptions } from "./sqlite-store.js";
6
6
  export declare function createArtifactId(): string;
@@ -38,6 +38,7 @@ export declare class MemorySkillsStore implements SkillsProductStore {
38
38
  private bundles;
39
39
  private versions;
40
40
  private pins;
41
+ private operatorEnrollments;
41
42
  constructor(apiKeys?: Array<{
42
43
  token: string;
43
44
  principal?: Partial<ApiPrincipal>;
@@ -46,6 +47,8 @@ export declare class MemorySkillsStore implements SkillsProductStore {
46
47
  ensureBootstrapApiKey(token: string, principal?: Partial<ApiPrincipal>): Promise<void>;
47
48
  authenticateApiKeyHash(hash: string): Promise<ApiPrincipal | null>;
48
49
  updateApiKeyScopes(actor: ApiPrincipal, keyId: string, expectedScopes: string[], addScopes: string[]): Promise<ApiKeyScopeUpdateResult>;
50
+ enrollPublishScopeByOperator(input: OperatorScopeEnrollmentInput): Promise<OperatorScopeEnrollmentResult>;
51
+ inspectOperatorScopeTarget(keyId: string, orgId: string): Promise<OperatorScopeTargetSnapshot>;
49
52
  createRun(input: CreateRunInput): Promise<ServerRunRecord>;
50
53
  listRuns(principal: ApiPrincipal, limit: number): Promise<ServerRunRecord[]>;
51
54
  getRun(principal: ApiPrincipal, runId: string): Promise<ServerRunRecord | null>;
@@ -122,6 +125,9 @@ export declare class PostgresSkillsStore implements SkillsProductStore {
122
125
  ensureBootstrapApiKey(token: string, principal?: Partial<ApiPrincipal>): Promise<void>;
123
126
  authenticateApiKeyHash(hash: string): Promise<ApiPrincipal | null>;
124
127
  updateApiKeyScopes(actor: ApiPrincipal, keyId: string, expectedScopes: string[], addScopes: string[]): Promise<ApiKeyScopeUpdateResult>;
128
+ private casUpdateApiKeyScopes;
129
+ enrollPublishScopeByOperator(input: OperatorScopeEnrollmentInput): Promise<OperatorScopeEnrollmentResult>;
130
+ inspectOperatorScopeTarget(keyId: string, orgId: string): Promise<OperatorScopeTargetSnapshot>;
125
131
  /**
126
132
  * Run a callback under an RLS tenant or worker context, on one pooled
127
133
  * connection, for the duration of one transaction only.