@hasna/skills 0.1.64 → 0.1.66

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/bin/server.js CHANGED
@@ -22936,6 +22936,119 @@ __export(exports_dist_es8, {
22936
22936
  var init_dist_es9 = __esm(() => {
22937
22937
  init_fromIni();
22938
22938
  });
22939
+ // package.json
22940
+ var package_default = {
22941
+ name: "@hasna/skills",
22942
+ version: "0.1.66",
22943
+ description: "Skills library for AI coding agents",
22944
+ type: "module",
22945
+ bin: {
22946
+ skills: "bin/index.js",
22947
+ "skills-mcp": "bin/mcp.js",
22948
+ "skills-server": "bin/server.js",
22949
+ "skills-worker": "bin/worker.js",
22950
+ "skills-migrate": "bin/migrate.js"
22951
+ },
22952
+ exports: {
22953
+ ".": {
22954
+ import: "./dist/index.js",
22955
+ types: "./dist/index.d.ts"
22956
+ },
22957
+ "./storage": {
22958
+ import: "./dist/storage.js",
22959
+ types: "./dist/storage.d.ts"
22960
+ },
22961
+ "./sdk": {
22962
+ import: "./dist/sdk/index.js",
22963
+ types: "./dist/sdk/index.d.ts"
22964
+ }
22965
+ },
22966
+ files: [
22967
+ "dist/",
22968
+ "!dist/**/*.test.d.ts",
22969
+ "!dist/test-preload.d.ts",
22970
+ "!dist/platform",
22971
+ "bin/",
22972
+ "migrations/",
22973
+ "docs/skill-standard.md",
22974
+ "schemas/",
22975
+ "LICENSE",
22976
+ "README.md"
22977
+ ],
22978
+ main: "./dist/index.js",
22979
+ types: "./dist/index.d.ts",
22980
+ scripts: {
22981
+ clean: "rm -rf bin/ dist/",
22982
+ 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 --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
22983
+ test: "bun test --timeout 30000",
22984
+ dev: "bun run ./src/cli/index.tsx",
22985
+ "dev:watch": "bun --watch run ./src/cli/index.tsx",
22986
+ "dev:mcp": "bun --watch run ./src/mcp/index.ts",
22987
+ "dev:server": "bun --watch run ./src/server/index.ts",
22988
+ "dev:worker": "bun --watch run ./src/server/worker.ts",
22989
+ migrate: "bun run ./src/server/migrate.ts",
22990
+ typecheck: "tsc --noEmit",
22991
+ "verify:release": "bun run scripts/release-guard.ts",
22992
+ prepack: "bun run build && bun run verify:release",
22993
+ prepublishOnly: "bun run typecheck && bun run test",
22994
+ postinstall: "mkdir -p $HOME/.hasna/skills/custom 2>/dev/null || true"
22995
+ },
22996
+ keywords: [
22997
+ "skills",
22998
+ "ai",
22999
+ "agent",
23000
+ "cli",
23001
+ "typescript",
23002
+ "bun",
23003
+ "claude",
23004
+ "codex",
23005
+ "gemini",
23006
+ "mcp",
23007
+ "model-context-protocol",
23008
+ "open-source",
23009
+ "skills-library",
23010
+ "automation"
23011
+ ],
23012
+ author: "Hasna",
23013
+ license: "Apache-2.0",
23014
+ devDependencies: {
23015
+ "@types/bun": "latest",
23016
+ "@types/node": "25.2.3",
23017
+ "@types/react": "^18.2.0",
23018
+ "bun-types": "1.3.14",
23019
+ "react-devtools-core": "^7.0.1",
23020
+ typescript: "^5"
23021
+ },
23022
+ dependencies: {
23023
+ "@aws-sdk/client-ecs": "^3.1079.0",
23024
+ "@aws-sdk/client-s3": "^3.1079.0",
23025
+ "@hasna/events": "0.1.16",
23026
+ "@modelcontextprotocol/sdk": "^1.26.0",
23027
+ chalk: "^5.3.0",
23028
+ commander: "^12.1.0",
23029
+ ink: "^5.0.1",
23030
+ "ink-select-input": "^6.0.0",
23031
+ "ink-spinner": "^5.0.0",
23032
+ "ink-text-input": "^6.0.0",
23033
+ react: "^18.2.0",
23034
+ zod: "^4.3.6"
23035
+ },
23036
+ engines: {
23037
+ bun: ">=1.0.0"
23038
+ },
23039
+ publishConfig: {
23040
+ registry: "https://registry.npmjs.org",
23041
+ access: "public"
23042
+ },
23043
+ repository: {
23044
+ type: "git",
23045
+ url: "git+https://github.com/hasna/apps.git"
23046
+ },
23047
+ homepage: "https://github.com/hasna/skills",
23048
+ bugs: {
23049
+ url: "https://github.com/hasna/skills/issues"
23050
+ }
23051
+ };
22939
23052
 
22940
23053
  // src/lib/remote-run-contract.ts
22941
23054
  var REMOTE_SKILL_RUN_CONTRACT_VERSION = 1;
@@ -31995,7 +32108,7 @@ var WriteGetObjectResponse$ = [
31995
32108
  class CreateSessionCommand extends command(_ep4, _mw0, "CreateSession", CreateSession$) {
31996
32109
  }
31997
32110
  // ../../node_modules/.bun/@aws-sdk+client-s3@3.1106.0/node_modules/@aws-sdk/client-s3/package.json
31998
- var package_default = {
32111
+ var package_default2 = {
31999
32112
  name: "@aws-sdk/client-s3",
32000
32113
  version: "3.1106.0",
32001
32114
  description: "AWS SDK for JavaScript S3 Client for Node.js, Browser and React Native",
@@ -32545,7 +32658,7 @@ var getRuntimeConfig3 = (config) => {
32545
32658
  authSchemePreference: config?.authSchemePreference ?? import_config28.loadConfig(import_httpAuthSchemes3.NODE_AUTH_SCHEME_PREFERENCE_OPTIONS, loaderConfig),
32546
32659
  bodyLengthChecker: config?.bodyLengthChecker ?? import_serde4.calculateBodyLength,
32547
32660
  credentialDefaultProvider: config?.credentialDefaultProvider ?? defaultProvider,
32548
- defaultUserAgentProvider: config?.defaultUserAgentProvider ?? import_client19.createDefaultUserAgentProvider({ serviceId: clientSharedValues.serviceId, clientVersion: package_default.version }),
32661
+ defaultUserAgentProvider: config?.defaultUserAgentProvider ?? import_client19.createDefaultUserAgentProvider({ serviceId: clientSharedValues.serviceId, clientVersion: package_default2.version }),
32549
32662
  disableS3ExpressSessionAuth: config?.disableS3ExpressSessionAuth ?? import_config28.loadConfig($NODE_DISABLE_S3_EXPRESS_SESSION_AUTH_OPTIONS, loaderConfig),
32550
32663
  eventStreamSerdeProvider: config?.eventStreamSerdeProvider ?? import_event_streams.eventStreamSerdeProvider,
32551
32664
  maxAttempts: config?.maxAttempts ?? import_config28.loadConfig(import_retry3.NODE_MAX_ATTEMPT_CONFIG_OPTIONS, config),
@@ -32857,36 +32970,144 @@ function concat(chunks) {
32857
32970
  return merged;
32858
32971
  }
32859
32972
 
32860
- // src/server/auth.ts
32861
- import { createHash as createHash3 } from "crypto";
32862
- function hashApiKey(token) {
32863
- return createHash3("sha256").update(token).digest("hex");
32864
- }
32865
- function bearerToken(request) {
32866
- const header = request.headers.get("authorization") || "";
32867
- const match = header.match(/^Bearer\s+(.+)$/i);
32868
- const token = match?.[1]?.trim();
32869
- return token || null;
32973
+ // src/sdk/governance.ts
32974
+ var DEFAULT_OUTPUT_GOVERNANCE = {
32975
+ defaultVisibility: "private",
32976
+ redactPatterns: [
32977
+ /\bsk-[A-Za-z0-9_-]{8,}\b/g,
32978
+ /\bsk_[A-Za-z0-9_-]{8,}\b/g,
32979
+ /\bgh[opsur]_[A-Za-z0-9_]{8,}\b/g,
32980
+ /\bgithub_pat_[A-Za-z0-9_]{8,}\b/g,
32981
+ /\bnpm_[A-Za-z0-9_]{8,}\b/g,
32982
+ /\bAKIA[A-Z0-9]{12,}\b/g,
32983
+ /\bAIza[A-Za-z0-9_-]{10,}\b/g,
32984
+ /\b[A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|DATABASE_URL)[A-Z0-9_]*\s*[:=]\s*[^ \n\r\t]+/gi,
32985
+ /\bhttps?:\/\/[^ \n\r\t]+X-Amz-Signature=[^ \n\r\t]+/gi
32986
+ ],
32987
+ perOutputBytes: 10 * 1024 * 1024,
32988
+ perRunTotalBytes: 100 * 1024 * 1024,
32989
+ artifactTtlSeconds: 30 * 24 * 60 * 60
32990
+ };
32991
+ var DEFAULT_RUN_QUOTA = {
32992
+ cpu: 1,
32993
+ memoryMB: 2048,
32994
+ durationSeconds: 3600,
32995
+ networkMB: 100,
32996
+ artifactBytes: 100 * 1024 * 1024
32997
+ };
32998
+ var GOVERNANCE_ERROR_CODES = {
32999
+ ARTIFACT_LIMIT_EXCEEDED: "ARTIFACT_LIMIT_EXCEEDED",
33000
+ RUN_ARTIFACT_TOTAL_EXCEEDED: "RUN_ARTIFACT_TOTAL_EXCEEDED",
33001
+ RUN_BUDGET_EXHAUSTED: "RUN_BUDGET_EXHAUSTED",
33002
+ STALE_LEASE_GENERATION: "STALE_LEASE_GENERATION",
33003
+ FENCING_UNSUPPORTED: "FENCING_UNSUPPORTED",
33004
+ EVENT_PAYLOAD_REJECTED: "EVENT_PAYLOAD_REJECTED",
33005
+ SKILL_UNAVAILABLE_OFFLINE: "SKILL_UNAVAILABLE_OFFLINE",
33006
+ REMOTE_REQUIRED: "REMOTE_REQUIRED"
33007
+ };
33008
+
33009
+ class GovernanceError extends Error {
33010
+ code;
33011
+ gate;
33012
+ ceiling;
33013
+ constructor(code, message, options = {}) {
33014
+ super(message);
33015
+ this.name = "GovernanceError";
33016
+ this.code = code;
33017
+ this.gate = options.gate ?? code;
33018
+ this.ceiling = options.ceiling;
33019
+ }
32870
33020
  }
32871
- async function authenticateRequest(store, request) {
32872
- const token = bearerToken(request);
32873
- if (!token)
32874
- return null;
32875
- return store.authenticateApiKeyHash(hashApiKey(token));
33021
+ function runPointersOf(run) {
33022
+ return {
33023
+ runId: run.id,
33024
+ attemptId: run.id,
33025
+ leaseGeneration: run.leaseGeneration,
33026
+ correlationId: run.correlationId
33027
+ };
32876
33028
  }
32877
- function publicPrincipal(partial = {}) {
33029
+
33030
+ // src/sdk/cancel.ts
33031
+ function createCancelService(options) {
33032
+ const store = options.store;
33033
+ const governanceStore = options.governanceStore;
33034
+ const storage = options.storage ?? new ArtifactStorage;
32878
33035
  return {
32879
- apiKeyId: partial.apiKeyId || "key_dev",
32880
- orgId: partial.orgId || "org_dev",
32881
- orgSlug: partial.orgSlug || "dev",
32882
- orgName: partial.orgName || "Development",
32883
- userId: partial.userId || "user_dev",
32884
- email: partial.email || "dev@example.com",
32885
- role: partial.role || "owner",
32886
- scopes: partial.scopes || ["skills:read", "runs:write"]
33036
+ async cancel(principal, runId, requestedBy) {
33037
+ if (!store.transitionRun) {
33038
+ throw new GovernanceError(GOVERNANCE_ERROR_CODES.FENCING_UNSUPPORTED, `cannot cancel run ${runId}: the store has no transitionRun generation fencing`, { gate: "transitionRun" });
33039
+ }
33040
+ const run = await store.getRun(principal, runId);
33041
+ if (!run)
33042
+ throw new GovernanceError(GOVERNANCE_ERROR_CODES.STALE_LEASE_GENERATION, `run ${runId} not found`, { gate: "run" });
33043
+ const terminalStatuses = new Set(["succeeded", "failed", "cancelled", "expired", "refunded"]);
33044
+ if (terminalStatuses.has(run.status)) {
33045
+ const receipt2 = await governanceStore.appendReceipt({
33046
+ kind: "cancel",
33047
+ orgId: run.orgId,
33048
+ runId: run.id,
33049
+ requestedBy,
33050
+ metadata: { outcome: "already-terminal", status: run.status, ...runPointersOf(run) }
33051
+ });
33052
+ return { run, fencedGeneration: run.leaseGeneration, quarantined: [], receipt: receipt2, alreadyTerminal: true };
33053
+ }
33054
+ const fencedGeneration = run.leaseGeneration + 1;
33055
+ const cancelling = await store.transitionRun(runId, { status: "cancel_requested", leaseGeneration: fencedGeneration }, run.leaseGeneration);
33056
+ if (!cancelling) {
33057
+ throw new GovernanceError(GOVERNANCE_ERROR_CODES.STALE_LEASE_GENERATION, `run ${runId} changed while cancelling`, { gate: "transition" });
33058
+ }
33059
+ const artifacts = await store.listArtifacts(principal, runId);
33060
+ const quarantined = [];
33061
+ for (const artifact of artifacts) {
33062
+ const quarantineKey = await storage.moveToQuarantine?.(artifact);
33063
+ if (quarantineKey) {
33064
+ await governanceStore.updateArtifactStorageKey(artifact.id, artifact.orgId, quarantineKey);
33065
+ }
33066
+ await governanceStore.appendReceipt({
33067
+ kind: "quarantine",
33068
+ orgId: artifact.orgId,
33069
+ runId: run.id,
33070
+ artifactId: artifact.id,
33071
+ requestedBy,
33072
+ metadata: { relativePath: artifact.relativePath, byteSize: artifact.byteSize, quarantineKey: quarantineKey ?? null }
33073
+ });
33074
+ quarantined.push({ ...artifact, ...quarantineKey ? { storageKey: quarantineKey } : {} });
33075
+ }
33076
+ const cancelled = await store.transitionRun(runId, { status: "cancelled", completedAt: new Date().toISOString() }, fencedGeneration);
33077
+ if (!cancelled) {
33078
+ throw new GovernanceError(GOVERNANCE_ERROR_CODES.STALE_LEASE_GENERATION, `run ${runId} moved on while finalising cancellation`, { gate: "transition" });
33079
+ }
33080
+ const receipt = await governanceStore.appendReceipt({
33081
+ kind: "cancel",
33082
+ orgId: run.orgId,
33083
+ runId: run.id,
33084
+ requestedBy,
33085
+ metadata: {
33086
+ fencedGeneration,
33087
+ artifactCount: quarantined.length,
33088
+ pointers: runPointersOf(cancelled)
33089
+ }
33090
+ });
33091
+ return { run: cancelled, fencedGeneration, quarantined, receipt, alreadyTerminal: false };
33092
+ }
32887
33093
  };
32888
33094
  }
32889
33095
 
33096
+ // src/sdk/governance-store.ts
33097
+ import { Database as Database2 } from "bun:sqlite";
33098
+ import { randomUUID as randomUUID3 } from "crypto";
33099
+ import { mkdirSync as mkdirSync3 } from "fs";
33100
+ import { dirname as dirname5 } from "path";
33101
+
33102
+ // src/server/database-url.ts
33103
+ import { isAbsolute, join as join3 } from "path";
33104
+ import { fileURLToPath } from "url";
33105
+
33106
+ // src/lib/config.ts
33107
+ import { existsSync, readFileSync as readFileSync2, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
33108
+ import { join as join2, dirname as dirname2 } from "path";
33109
+ import { homedir as homedir2 } from "os";
33110
+
32890
33111
  // src/lib/retired-settings.ts
32891
33112
  var RETIRED_ENV_SUFFIXES = ["_STORAGE_MODE", "_DEPLOYMENT_MODE", "_CLOUD_MODE"];
32892
33113
  var RETIRED_CONFIG_KEYS = {
@@ -32933,54 +33154,7 @@ function assertNoRetiredConfigKeys(config, source) {
32933
33154
  }
32934
33155
  }
32935
33156
 
32936
- // src/server/config.ts
32937
- var DATABASE_URL_ENV = "HASNA_SKILLS_DATABASE_URL";
32938
- var SKILLS_ENV_NAMESPACE = "SKILLS";
32939
- function resolveServerConfig(env = process.env) {
32940
- assertNoRetiredModeEnvVars(env, {
32941
- app: SKILLS_ENV_NAMESPACE,
32942
- replacement: DATABASE_URL_ENV
32943
- });
32944
- const nodeEnv = env.NODE_ENV || "development";
32945
- const host = env.HOST || env.SKILLS_HOST || "0.0.0.0";
32946
- const port = parsePositiveInt(env.PORT || env.SKILLS_PORT, 8787);
32947
- return {
32948
- host,
32949
- port,
32950
- databaseUrl: env[DATABASE_URL_ENV] || env.DATABASE_URL || undefined,
32951
- bootstrapApiKey: env.HASNA_SKILLS_BOOTSTRAP_API_KEY || undefined,
32952
- artifactBucket: env.HASNA_SKILLS_S3_BUCKET || env.SKILLS_S3_BUCKET || undefined,
32953
- artifactPrefix: normalizePrefix(env.HASNA_SKILLS_S3_PREFIX || env.SKILLS_S3_PREFIX || "skills/artifacts"),
32954
- inlineWorker: env.HASNA_SKILLS_INLINE_WORKER === "1",
32955
- bundleSigningKey: env.HASNA_SKILLS_API_SIGNING_KEY || env.HASNA_SKILLS_SIGNING_KEY || undefined,
32956
- requestBodyLimitBytes: parsePositiveInt(env.HASNA_SKILLS_REQUEST_BODY_LIMIT_BYTES, 1e6),
32957
- skillBundleLimitBytes: parsePositiveInt(env.HASNA_SKILLS_BUNDLE_LIMIT_BYTES, 25000000),
32958
- tombstoneWindowMs: parsePositiveInt(env.HASNA_SKILLS_TOMBSTONE_WINDOW_MS, 7 * 24 * 60 * 60 * 1000),
32959
- publicBaseUrl: (env.SKILLS_PUBLIC_BASE_URL || localOrigin(host, port)).replace(/\/+$/, ""),
32960
- nodeEnv,
32961
- allowEphemeralStore: env.HASNA_SKILLS_ALLOW_EPHEMERAL_STORE === "1"
32962
- };
32963
- }
32964
- function localOrigin(host, port) {
32965
- const hostname = host === "0.0.0.0" || host === "::" ? "localhost" : host;
32966
- return `http://${hostname.includes(":") ? `[${hostname}]` : hostname}:${port}`;
32967
- }
32968
- function parsePositiveInt(value, fallback) {
32969
- const parsed = Number.parseInt(value ?? "", 10);
32970
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
32971
- }
32972
- function normalizePrefix(value) {
32973
- return value.replace(/^\/+|\/+$/g, "") || "skills/artifacts";
32974
- }
32975
-
32976
- // src/server/database-url.ts
32977
- import { isAbsolute, join as join3 } from "path";
32978
- import { fileURLToPath } from "url";
32979
-
32980
33157
  // src/lib/config.ts
32981
- import { existsSync, readFileSync as readFileSync2, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
32982
- import { join as join2, dirname as dirname2 } from "path";
32983
- import { homedir as homedir2 } from "os";
32984
33158
  var ENUM_KEYS = {
32985
33159
  defaultAgent: ["claude", "codex", "gemini", "pi", "opencode", "all"],
32986
33160
  defaultScope: ["global", "project"],
@@ -33160,39 +33334,78 @@ function looksLikeSqlitePath(value) {
33160
33334
  return SQLITE_EXTENSIONS.some((extension) => value.toLowerCase().endsWith(extension));
33161
33335
  }
33162
33336
 
33163
- // src/server/handlers.ts
33164
- import { createHash as createHash5 } from "crypto";
33337
+ // src/server/sqlite-store.ts
33338
+ import { Database } from "bun:sqlite";
33339
+ import { randomUUID as randomUUID2 } from "crypto";
33340
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
33341
+ import { dirname as dirname4, join as join5 } from "path";
33165
33342
 
33166
- // src/server/store.ts
33167
- import { randomUUID as randomUUID3 } from "crypto";
33343
+ // src/server/auth.ts
33344
+ import { createHash as createHash3 } from "crypto";
33345
+ function hashApiKey(token) {
33346
+ return createHash3("sha256").update(token).digest("hex");
33347
+ }
33348
+ function bearerToken(request) {
33349
+ const header = request.headers.get("authorization") || "";
33350
+ const match = header.match(/^Bearer\s+(.+)$/i);
33351
+ const token = match?.[1]?.trim();
33352
+ return token || null;
33353
+ }
33354
+ async function authenticateRequest(store, request) {
33355
+ const token = bearerToken(request);
33356
+ if (!token)
33357
+ return null;
33358
+ return store.authenticateApiKeyHash(hashApiKey(token));
33359
+ }
33360
+ function publicPrincipal(partial = {}) {
33361
+ return {
33362
+ apiKeyId: partial.apiKeyId || "key_dev",
33363
+ orgId: partial.orgId || "org_dev",
33364
+ orgSlug: partial.orgSlug || "dev",
33365
+ orgName: partial.orgName || "Development",
33366
+ userId: partial.userId || "user_dev",
33367
+ email: partial.email || "dev@example.com",
33368
+ role: partial.role || "owner",
33369
+ scopes: partial.scopes || ["skills:read", "runs:write"]
33370
+ };
33371
+ }
33168
33372
 
33169
- // src/server/types.ts
33170
- class StaleLeaseGenerationError extends Error {
33171
- runId;
33172
- expectedGeneration;
33173
- currentGeneration;
33174
- status;
33175
- constructor(runId, expectedGeneration, currentGeneration, status) {
33176
- super(`late transition rejected for run ${runId}: expected lease_generation ${expectedGeneration}, ` + `current ${currentGeneration} (status ${status})`);
33177
- this.name = "StaleLeaseGenerationError";
33178
- this.runId = runId;
33179
- this.expectedGeneration = expectedGeneration;
33180
- this.currentGeneration = currentGeneration;
33181
- this.status = status;
33373
+ // src/server/migrations-dir.ts
33374
+ import { existsSync as existsSync2 } from "fs";
33375
+ import { dirname as dirname3, join as join4 } from "path";
33376
+ var MIGRATION_DIALECTS = ["postgres", "sqlite"];
33377
+ var MAX_WALK_UP = 6;
33378
+ function findMigrationsRoot(startDirs = defaultStartDirs()) {
33379
+ for (const start of startDirs) {
33380
+ let dir = start;
33381
+ for (let level = 0;level < MAX_WALK_UP; level += 1) {
33382
+ const candidate = join4(dir, "migrations");
33383
+ if (MIGRATION_DIALECTS.some((dialect) => existsSync2(join4(candidate, dialect))))
33384
+ return candidate;
33385
+ const parent = dirname3(dir);
33386
+ if (parent === dir)
33387
+ break;
33388
+ dir = parent;
33389
+ }
33182
33390
  }
33391
+ return null;
33183
33392
  }
33184
-
33185
- class SkillRevisionConflictError extends Error {
33186
- slug;
33187
- expectedRevisionId;
33188
- currentRevisionId;
33189
- constructor(slug, expectedRevisionId, currentRevisionId) {
33190
- super(`revision conflict for '${slug}': expected revision ${expectedRevisionId ?? "(none)"}, ` + `current is ${currentRevisionId ?? "(none)"}. Refused rather than silently overwriting a newer revision.`);
33191
- this.name = "SkillRevisionConflictError";
33192
- this.slug = slug;
33193
- this.expectedRevisionId = expectedRevisionId;
33194
- this.currentRevisionId = currentRevisionId;
33393
+ function resolveMigrationsDir(dialect, root3 = findMigrationsRoot()) {
33394
+ if (!root3) {
33395
+ throw new Error(`could not locate the migrations directory for dialect "${dialect}". ` + `Expected a migrations/${dialect}/ folder alongside the package root.`);
33195
33396
  }
33397
+ const dir = join4(root3, dialect);
33398
+ if (!existsSync2(dir)) {
33399
+ throw new Error(`migrations directory not found: ${dir}`);
33400
+ }
33401
+ return dir;
33402
+ }
33403
+ function defaultStartDirs() {
33404
+ const dirs = [import.meta.dir];
33405
+ const cwd = process.cwd();
33406
+ if (cwd && cwd !== import.meta.dir)
33407
+ dirs.push(cwd);
33408
+ return dirs;
33196
33409
  }
33197
33410
 
33198
33411
  // src/server/rows.ts
@@ -33340,6 +33553,35 @@ function dateString(value) {
33340
33553
  return String(value);
33341
33554
  }
33342
33555
 
33556
+ // src/server/types.ts
33557
+ class StaleLeaseGenerationError extends Error {
33558
+ runId;
33559
+ expectedGeneration;
33560
+ currentGeneration;
33561
+ status;
33562
+ constructor(runId2, expectedGeneration, currentGeneration, status) {
33563
+ super(`late transition rejected for run ${runId2}: expected lease_generation ${expectedGeneration}, ` + `current ${currentGeneration} (status ${status})`);
33564
+ this.name = "StaleLeaseGenerationError";
33565
+ this.runId = runId2;
33566
+ this.expectedGeneration = expectedGeneration;
33567
+ this.currentGeneration = currentGeneration;
33568
+ this.status = status;
33569
+ }
33570
+ }
33571
+
33572
+ class SkillRevisionConflictError extends Error {
33573
+ slug;
33574
+ expectedRevisionId;
33575
+ currentRevisionId;
33576
+ constructor(slug, expectedRevisionId, currentRevisionId) {
33577
+ super(`revision conflict for '${slug}': expected revision ${expectedRevisionId ?? "(none)"}, ` + `current is ${currentRevisionId ?? "(none)"}. Refused rather than silently overwriting a newer revision.`);
33578
+ this.name = "SkillRevisionConflictError";
33579
+ this.slug = slug;
33580
+ this.expectedRevisionId = expectedRevisionId;
33581
+ this.currentRevisionId = currentRevisionId;
33582
+ }
33583
+ }
33584
+
33343
33585
  // src/lib/revision.ts
33344
33586
  import { createHash as createHash4 } from "crypto";
33345
33587
  var REVISION_ID_PATTERN = /^[0-9a-f]{64}$/;
@@ -33363,50 +33605,6 @@ function revisionIdOfRecord(record) {
33363
33605
  return revisionIdOf(record);
33364
33606
  }
33365
33607
 
33366
- // src/server/sqlite-store.ts
33367
- import { Database } from "bun:sqlite";
33368
- import { randomUUID as randomUUID2 } from "crypto";
33369
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
33370
- import { dirname as dirname4, join as join5 } from "path";
33371
-
33372
- // src/server/migrations-dir.ts
33373
- import { existsSync as existsSync2 } from "fs";
33374
- import { dirname as dirname3, join as join4 } from "path";
33375
- var MIGRATION_DIALECTS = ["postgres", "sqlite"];
33376
- var MAX_WALK_UP = 6;
33377
- function findMigrationsRoot(startDirs = defaultStartDirs()) {
33378
- for (const start of startDirs) {
33379
- let dir = start;
33380
- for (let level = 0;level < MAX_WALK_UP; level += 1) {
33381
- const candidate = join4(dir, "migrations");
33382
- if (MIGRATION_DIALECTS.some((dialect) => existsSync2(join4(candidate, dialect))))
33383
- return candidate;
33384
- const parent = dirname3(dir);
33385
- if (parent === dir)
33386
- break;
33387
- dir = parent;
33388
- }
33389
- }
33390
- return null;
33391
- }
33392
- function resolveMigrationsDir(dialect, root3 = findMigrationsRoot()) {
33393
- if (!root3) {
33394
- throw new Error(`could not locate the migrations directory for dialect "${dialect}". ` + `Expected a migrations/${dialect}/ folder alongside the package root.`);
33395
- }
33396
- const dir = join4(root3, dialect);
33397
- if (!existsSync2(dir)) {
33398
- throw new Error(`migrations directory not found: ${dir}`);
33399
- }
33400
- return dir;
33401
- }
33402
- function defaultStartDirs() {
33403
- const dirs = [import.meta.dir];
33404
- const cwd = process.cwd();
33405
- if (cwd && cwd !== import.meta.dir)
33406
- dirs.push(cwd);
33407
- return dirs;
33408
- }
33409
-
33410
33608
  // src/server/sqlite-store.ts
33411
33609
  var CLAIM_ATTEMPTS = 8;
33412
33610
  var CLAIMABLE_STATUSES = ["queued", "retrying"];
@@ -33997,7 +34195,301 @@ function parseScopes(value) {
33997
34195
  }
33998
34196
  }
33999
34197
 
34198
+ // src/sdk/governance-store.ts
34199
+ function receiptId() {
34200
+ return `rcpt_${Date.now().toString(36)}_${randomUUID3().replace(/-/g, "").slice(0, 10)}`;
34201
+ }
34202
+ function reservationId() {
34203
+ return `res_${Date.now().toString(36)}_${randomUUID3().replace(/-/g, "").slice(0, 10)}`;
34204
+ }
34205
+ var EXPIRED_ARTIFACT_SQL = `
34206
+ SELECT a.* FROM skills_artifacts a
34207
+ JOIN skills_runs r ON r.id = a.run_id
34208
+ WHERE a.expires_at IS NOT NULL AND a.expires_at <= ?
34209
+ ORDER BY a.expires_at ASC
34210
+ `;
34211
+ var ACTIVE_RUN_SQL = `
34212
+ SELECT COUNT(*) AS n FROM skills_runs
34213
+ WHERE org_id = ? AND status IN ('queued','running','cancel_requested')
34214
+ `;
34215
+ class SqliteGovernanceStore {
34216
+ backend = "sqlite";
34217
+ db;
34218
+ closed = false;
34219
+ constructor(path = SQLITE_MEMORY_PATH, options = {}) {
34220
+ if (path !== SQLITE_MEMORY_PATH)
34221
+ mkdirSync3(dirname5(path), { recursive: true });
34222
+ this.db = new Database2(path, { create: true, readwrite: true });
34223
+ this.db.exec("PRAGMA busy_timeout = 5000");
34224
+ this.db.exec("PRAGMA foreign_keys = ON");
34225
+ if (options.migrate !== false)
34226
+ applySqliteMigrations(this.db);
34227
+ }
34228
+ async close() {
34229
+ if (this.closed)
34230
+ return;
34231
+ this.closed = true;
34232
+ this.db.close(false);
34233
+ }
34234
+ get database() {
34235
+ return this.db;
34236
+ }
34237
+ async appendReceipt(receipt) {
34238
+ const next = { ...receipt, id: receiptId(), createdAt: nowIso() };
34239
+ this.db.run(`INSERT INTO skills_lifecycle_receipts (id, kind, org_id, run_id, artifact_id, requested_by, metadata_json, created_at)
34240
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [next.id, next.kind, next.orgId, next.runId, next.artifactId ?? null, next.requestedBy, JSON.stringify(next.metadata), next.createdAt]);
34241
+ return next;
34242
+ }
34243
+ async listReceipts(orgId, runId2) {
34244
+ const rows = this.db.query("SELECT * FROM skills_lifecycle_receipts WHERE org_id = ? AND run_id = ? ORDER BY created_at ASC, rowid ASC").all(orgId, runId2);
34245
+ return rows.map((row) => ({
34246
+ id: String(row.id),
34247
+ kind: String(row.kind),
34248
+ orgId: String(row.org_id),
34249
+ runId: String(row.run_id),
34250
+ ...typeof row.artifact_id === "string" ? { artifactId: row.artifact_id } : {},
34251
+ requestedBy: String(row.requested_by),
34252
+ metadata: parseJsonObject(row.metadata_json),
34253
+ createdAt: String(row.created_at)
34254
+ }));
34255
+ }
34256
+ async createReservation(input) {
34257
+ const reservation = { ...input, id: reservationId(), status: "reserved", createdAt: nowIso() };
34258
+ this.db.run(`INSERT INTO skills_credit_reservations (id, org_id, run_id, estimated_cents, status, created_at)
34259
+ VALUES (?, ?, ?, ?, ?, ?)`, [reservation.id, reservation.orgId, reservation.runId, reservation.estimatedCents, reservation.status, reservation.createdAt]);
34260
+ return reservation;
34261
+ }
34262
+ async reservationsForRun(orgId, runId2) {
34263
+ const rows = this.db.query("SELECT * FROM skills_credit_reservations WHERE org_id = ? AND run_id = ? ORDER BY created_at ASC, rowid ASC").all(orgId, runId2);
34264
+ return rows.map((row) => this.reservationFrom(row));
34265
+ }
34266
+ async reconcileReservation(reservationId2, actualCents, status) {
34267
+ const row = this.db.query("SELECT * FROM skills_credit_reservations WHERE id = ? LIMIT 1").get(reservationId2);
34268
+ if (!row || String(row.status) !== "reserved")
34269
+ return row ? this.reservationFrom(row) : null;
34270
+ const reconciledAt = nowIso();
34271
+ this.db.run("UPDATE skills_credit_reservations SET actual_cents = ?, status = ?, reconciled_at = ? WHERE id = ?", [actualCents, status, reconciledAt, reservationId2]);
34272
+ const updated = this.db.query("SELECT * FROM skills_credit_reservations WHERE id = ? LIMIT 1").get(reservationId2);
34273
+ return this.reservationFrom(updated);
34274
+ }
34275
+ async monthlySpendCents(orgId, monthPrefix) {
34276
+ const from = `${monthPrefix}-01T00:00:00.000Z`;
34277
+ const nextMonth = nextMonthPrefix(monthPrefix);
34278
+ const spent = this.db.query("SELECT COALESCE(SUM(cost_cents), 0) AS spent FROM skills_runs WHERE org_id = ? AND created_at >= ? AND created_at < ?").get(orgId, from, `${nextMonth}-01T00:00:00.000Z`);
34279
+ const pending = this.db.query("SELECT COALESCE(SUM(estimated_cents), 0) AS pending FROM skills_credit_reservations WHERE org_id = ? AND status = 'reserved'").get(orgId);
34280
+ return Number(spent.spent ?? 0) + Number(pending.pending ?? 0);
34281
+ }
34282
+ async activeRunCount(orgId) {
34283
+ const row = this.db.query(ACTIVE_RUN_SQL).get(orgId);
34284
+ return Number(row.n ?? 0);
34285
+ }
34286
+ async listExpiredArtifacts(at2) {
34287
+ const rows = this.db.query(EXPIRED_ARTIFACT_SQL).all(at2);
34288
+ return rows.map(rowToArtifact);
34289
+ }
34290
+ async deleteArtifactRow(artifactId2, orgId) {
34291
+ const result = this.db.run("DELETE FROM skills_artifacts WHERE id = ? AND org_id = ?", [artifactId2, orgId]);
34292
+ return result.changes === 1;
34293
+ }
34294
+ async updateArtifactStorageKey(artifactId2, orgId, storageKey) {
34295
+ const result = this.db.run("UPDATE skills_artifacts SET storage_key = ? WHERE id = ? AND org_id = ?", [storageKey, artifactId2, orgId]);
34296
+ return result.changes === 1;
34297
+ }
34298
+ reservationFrom(row) {
34299
+ return {
34300
+ id: String(row.id),
34301
+ orgId: String(row.org_id),
34302
+ runId: String(row.run_id),
34303
+ estimatedCents: Number(row.estimated_cents ?? 0),
34304
+ ...row.actual_cents !== null && row.actual_cents !== undefined ? { actualCents: Number(row.actual_cents) } : {},
34305
+ status: String(row.status),
34306
+ createdAt: String(row.created_at),
34307
+ ...typeof row.reconciled_at === "string" ? { reconciledAt: row.reconciled_at } : {}
34308
+ };
34309
+ }
34310
+ }
34311
+
34312
+ class PostgresGovernanceStore {
34313
+ backend = "postgres";
34314
+ sql;
34315
+ constructor(databaseUrl) {
34316
+ const bunWithSql = Bun;
34317
+ this.sql = new bunWithSql.SQL(databaseUrl, { max: 2 });
34318
+ }
34319
+ async close() {
34320
+ await this.sql.close?.();
34321
+ }
34322
+ async withContext(orgId, worker, fn) {
34323
+ return this.sql.begin(async (tx) => {
34324
+ await tx`SELECT set_config('app.skills_org_id', ${orgId ?? ""}, true)`;
34325
+ await tx`SELECT set_config('app.skills_claim_context', ${worker ? "worker" : ""}, true)`;
34326
+ return await fn(tx);
34327
+ });
34328
+ }
34329
+ async appendReceipt(receipt) {
34330
+ const next = { ...receipt, id: receiptId(), createdAt: nowIso() };
34331
+ await this.sql`
34332
+ INSERT INTO skills_lifecycle_receipts (id, kind, org_id, run_id, artifact_id, requested_by, metadata_json)
34333
+ VALUES (${next.id}, ${next.kind}, ${next.orgId}, ${next.runId}, ${next.artifactId ?? null}, ${next.requestedBy}, ${JSON.stringify(next.metadata)}::jsonb)
34334
+ `;
34335
+ return next;
34336
+ }
34337
+ async listReceipts(orgId, runId2) {
34338
+ const rows = await this.sql`
34339
+ SELECT * FROM skills_lifecycle_receipts WHERE org_id = ${orgId} AND run_id = ${runId2} ORDER BY created_at ASC
34340
+ `;
34341
+ return rows.map((row) => ({
34342
+ id: String(row.id),
34343
+ kind: String(row.kind),
34344
+ orgId: String(row.org_id),
34345
+ runId: String(row.run_id),
34346
+ ...typeof row.artifact_id === "string" ? { artifactId: row.artifact_id } : {},
34347
+ requestedBy: String(row.requested_by),
34348
+ metadata: parseJsonObject(row.metadata_json),
34349
+ createdAt: String(row.created_at)
34350
+ }));
34351
+ }
34352
+ async createReservation(input) {
34353
+ const reservation = { ...input, id: reservationId(), status: "reserved", createdAt: nowIso() };
34354
+ await this.sql`
34355
+ INSERT INTO skills_credit_reservations (id, org_id, run_id, estimated_cents, status)
34356
+ VALUES (${reservation.id}, ${reservation.orgId}, ${reservation.runId}, ${reservation.estimatedCents}, ${reservation.status})
34357
+ `;
34358
+ return reservation;
34359
+ }
34360
+ async reservationsForRun(orgId, runId2) {
34361
+ const rows = await this.sql`
34362
+ SELECT * FROM skills_credit_reservations WHERE org_id = ${orgId} AND run_id = ${runId2} ORDER BY created_at ASC
34363
+ `;
34364
+ return rows.map((row) => this.reservationFrom(row));
34365
+ }
34366
+ async reconcileReservation(reservationId2, actualCents, status) {
34367
+ const rows = await this.sql`
34368
+ UPDATE skills_credit_reservations
34369
+ SET actual_cents = ${actualCents}, status = ${status}, reconciled_at = now()
34370
+ WHERE id = ${reservationId2} AND status = ${"reserved"}
34371
+ RETURNING *
34372
+ `;
34373
+ if (!rows[0]) {
34374
+ const existing = await this.sql`SELECT * FROM skills_credit_reservations WHERE id = ${reservationId2} LIMIT 1`;
34375
+ return existing[0] ? this.reservationFrom(existing[0]) : null;
34376
+ }
34377
+ return this.reservationFrom(rows[0]);
34378
+ }
34379
+ async monthlySpendCents(orgId, monthPrefix) {
34380
+ return this.withContext(null, true, async (tx) => {
34381
+ const rows = await tx`
34382
+ SELECT
34383
+ (SELECT COALESCE(SUM(cost_cents), 0) FROM skills_runs
34384
+ WHERE org_id = ${orgId} AND created_at >= ${`${monthPrefix}-01T00:00:00.000Z`} AND created_at < ${`${nextMonthPrefix(monthPrefix)}-01T00:00:00.000Z`}) AS spent,
34385
+ (SELECT COALESCE(SUM(estimated_cents), 0) FROM skills_credit_reservations
34386
+ WHERE org_id = ${orgId} AND status = ${"reserved"}) AS pending
34387
+ `;
34388
+ return Number(rows[0]?.spent ?? 0) + Number(rows[0]?.pending ?? 0);
34389
+ });
34390
+ }
34391
+ async activeRunCount(orgId) {
34392
+ return this.withContext(orgId, false, async (tx) => {
34393
+ const rows = await tx`SELECT COUNT(*) AS n FROM skills_runs WHERE org_id = ${orgId} AND status IN (${"queued"}, ${"running"}, ${"cancel_requested"})`;
34394
+ return Number(rows[0]?.n ?? 0);
34395
+ });
34396
+ }
34397
+ async listExpiredArtifacts(at2) {
34398
+ return this.withContext(null, true, async (tx) => {
34399
+ const rows = await tx`
34400
+ SELECT a.* FROM skills_artifacts a
34401
+ JOIN skills_runs r ON r.id = a.run_id
34402
+ WHERE a.expires_at IS NOT NULL AND a.expires_at <= ${at2}
34403
+ ORDER BY a.expires_at ASC
34404
+ `;
34405
+ return rows.map(rowToArtifact);
34406
+ });
34407
+ }
34408
+ async deleteArtifactRow(artifactId2, orgId) {
34409
+ return this.withContext(orgId, true, async (tx) => {
34410
+ const rows = await tx`DELETE FROM skills_artifacts WHERE id = ${artifactId2} AND org_id = ${orgId} RETURNING id`;
34411
+ return rows.length > 0;
34412
+ });
34413
+ }
34414
+ async updateArtifactStorageKey(artifactId2, orgId, storageKey) {
34415
+ return this.withContext(orgId, true, async (tx) => {
34416
+ const rows = await tx`
34417
+ UPDATE skills_artifacts SET storage_key = ${storageKey} WHERE id = ${artifactId2} AND org_id = ${orgId} RETURNING id
34418
+ `;
34419
+ return rows.length > 0;
34420
+ });
34421
+ }
34422
+ reservationFrom(row) {
34423
+ return {
34424
+ id: String(row.id),
34425
+ orgId: String(row.org_id),
34426
+ runId: String(row.run_id),
34427
+ estimatedCents: Number(row.estimated_cents ?? 0),
34428
+ ...row.actual_cents !== null && row.actual_cents !== undefined ? { actualCents: Number(row.actual_cents) } : {},
34429
+ status: String(row.status),
34430
+ createdAt: String(row.created_at),
34431
+ ...typeof row.reconciled_at === "string" ? { reconciledAt: row.reconciled_at } : {}
34432
+ };
34433
+ }
34434
+ }
34435
+ async function createGovernanceStore(databaseUrl) {
34436
+ const target = resolveDatabaseTarget(databaseUrl);
34437
+ if (target.kind === "postgres")
34438
+ return new PostgresGovernanceStore(target.url);
34439
+ if (target.kind === "memory")
34440
+ return new SqliteGovernanceStore(SQLITE_MEMORY_PATH);
34441
+ return new SqliteGovernanceStore(target.path);
34442
+ }
34443
+ function nextMonthPrefix(monthPrefix) {
34444
+ const [year, month] = monthPrefix.split("-").map(Number);
34445
+ return month === 12 ? `${year + 1}-01` : `${year}-${String(month + 1).padStart(2, "0")}`;
34446
+ }
34447
+
34448
+ // src/server/config.ts
34449
+ var DATABASE_URL_ENV = "HASNA_SKILLS_DATABASE_URL";
34450
+ var SKILLS_ENV_NAMESPACE = "SKILLS";
34451
+ function resolveServerConfig(env = process.env) {
34452
+ assertNoRetiredModeEnvVars(env, {
34453
+ app: SKILLS_ENV_NAMESPACE,
34454
+ replacement: DATABASE_URL_ENV
34455
+ });
34456
+ const nodeEnv = env.NODE_ENV || "development";
34457
+ const host = env.HOST || env.SKILLS_HOST || "0.0.0.0";
34458
+ const port = parsePositiveInt(env.PORT || env.SKILLS_PORT, 8787);
34459
+ return {
34460
+ host,
34461
+ port,
34462
+ databaseUrl: env[DATABASE_URL_ENV] || env.DATABASE_URL || undefined,
34463
+ bootstrapApiKey: env.HASNA_SKILLS_BOOTSTRAP_API_KEY || undefined,
34464
+ artifactBucket: env.HASNA_SKILLS_S3_BUCKET || env.SKILLS_S3_BUCKET || undefined,
34465
+ artifactPrefix: normalizePrefix(env.HASNA_SKILLS_S3_PREFIX || env.SKILLS_S3_PREFIX || "skills/artifacts"),
34466
+ inlineWorker: env.HASNA_SKILLS_INLINE_WORKER === "1",
34467
+ bundleSigningKey: env.HASNA_SKILLS_API_SIGNING_KEY || env.HASNA_SKILLS_SIGNING_KEY || undefined,
34468
+ requestBodyLimitBytes: parsePositiveInt(env.HASNA_SKILLS_REQUEST_BODY_LIMIT_BYTES, 1e6),
34469
+ skillBundleLimitBytes: parsePositiveInt(env.HASNA_SKILLS_BUNDLE_LIMIT_BYTES, 25000000),
34470
+ tombstoneWindowMs: parsePositiveInt(env.HASNA_SKILLS_TOMBSTONE_WINDOW_MS, 7 * 24 * 60 * 60 * 1000),
34471
+ publicBaseUrl: (env.SKILLS_PUBLIC_BASE_URL || localOrigin(host, port)).replace(/\/+$/, ""),
34472
+ nodeEnv,
34473
+ allowEphemeralStore: env.HASNA_SKILLS_ALLOW_EPHEMERAL_STORE === "1"
34474
+ };
34475
+ }
34476
+ function localOrigin(host, port) {
34477
+ const hostname = host === "0.0.0.0" || host === "::" ? "localhost" : host;
34478
+ return `http://${hostname.includes(":") ? `[${hostname}]` : hostname}:${port}`;
34479
+ }
34480
+ function parsePositiveInt(value, fallback) {
34481
+ const parsed = Number.parseInt(value ?? "", 10);
34482
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
34483
+ }
34484
+ function normalizePrefix(value) {
34485
+ return value.replace(/^\/+|\/+$/g, "") || "skills/artifacts";
34486
+ }
34487
+
34488
+ // src/server/handlers.ts
34489
+ import { createHash as createHash5 } from "crypto";
34490
+
34000
34491
  // src/server/store.ts
34492
+ import { randomUUID as randomUUID4 } from "crypto";
34001
34493
  function recordFieldsOf(input, carriedBundle, carriedSkillMd) {
34002
34494
  return {
34003
34495
  slug: input.slug,
@@ -34085,7 +34577,7 @@ class MemorySkillsStore {
34085
34577
  input: input.input,
34086
34578
  args: input.args,
34087
34579
  ...idemKey ? { idempotencyKey: idemKey } : {},
34088
- correlationId: randomUUID3(),
34580
+ correlationId: randomUUID4(),
34089
34581
  costCents: 0,
34090
34582
  leaseGeneration: 0,
34091
34583
  createdAt: now
@@ -34400,20 +34892,20 @@ class PostgresSkillsStore {
34400
34892
  });
34401
34893
  }
34402
34894
  async createRun(input) {
34403
- if (input.idempotencyKey) {
34404
- const existing = await this.sql`
34405
- SELECT * FROM skills_runs
34406
- WHERE org_id = ${input.principal.orgId} AND idempotency_key = ${input.idempotencyKey}
34407
- LIMIT 1
34408
- `;
34409
- if (existing[0])
34410
- return rowToRun(existing[0]);
34411
- }
34412
- const id = runId();
34413
34895
  return this.withContext(input.principal.orgId, false, async (tx) => {
34896
+ if (input.idempotencyKey) {
34897
+ const existing = await tx`
34898
+ SELECT * FROM skills_runs
34899
+ WHERE org_id = ${input.principal.orgId} AND idempotency_key = ${input.idempotencyKey}
34900
+ LIMIT 1
34901
+ `;
34902
+ if (existing[0])
34903
+ return rowToRun(existing[0]);
34904
+ }
34905
+ const id = runId();
34414
34906
  const rows = await tx`
34415
34907
  INSERT INTO skills_runs (id, org_id, user_id, skill_slug, requested_slug, status, input_json, args_json, idempotency_key, correlation_id)
34416
- VALUES (${id}, ${input.principal.orgId}, ${input.principal.userId}, ${input.slug}, ${input.slug}, ${"queued"}, ${JSON.stringify(input.input)}::jsonb, ${JSON.stringify(input.args)}::jsonb, ${input.idempotencyKey ?? null}, ${randomUUID3()})
34908
+ VALUES (${id}, ${input.principal.orgId}, ${input.principal.userId}, ${input.slug}, ${input.slug}, ${"queued"}, ${JSON.stringify(input.input)}::jsonb, ${JSON.stringify(input.args)}::jsonb, ${input.idempotencyKey ?? null}, ${randomUUID4()})
34417
34909
  RETURNING *
34418
34910
  `;
34419
34911
  return rowToRun(rows[0]);
@@ -34952,7 +35444,9 @@ async function completeRun(store, run, preview) {
34952
35444
  return next ?? run;
34953
35445
  }
34954
35446
  async function failRun(store, run, code, message) {
34955
- await store.appendLog(run.id, run.orgId, "error", message);
35447
+ try {
35448
+ await store.appendLog(run.id, run.orgId, "error", message);
35449
+ } catch {}
34956
35450
  const next = await fencedTransition(store, run, {
34957
35451
  status: "failed",
34958
35452
  errorCode: code,
@@ -34966,7 +35460,9 @@ async function fencedTransition(store, run, patch) {
34966
35460
  return store.updateRun(run.id, patch);
34967
35461
  const next = await store.transitionRun(run.id, patch, run.leaseGeneration);
34968
35462
  if (!next) {
34969
- await store.appendLog(run.id, run.orgId, "warn", `late write rejected: run no longer owned at lease_generation ${run.leaseGeneration}`);
35463
+ try {
35464
+ await store.appendLog(run.id, run.orgId, "warn", `late write rejected: run no longer owned at lease_generation ${run.leaseGeneration}`);
35465
+ } catch {}
34970
35466
  }
34971
35467
  return next;
34972
35468
  }
@@ -35048,25 +35544,26 @@ function mergeSkillRegistryLists(...groups) {
35048
35544
  }
35049
35545
 
35050
35546
  // src/server/registry.ts
35051
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
35547
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
35052
35548
  import { resolve, sep } from "path";
35053
35549
 
35054
35550
  // src/lib/registry.ts
35055
- import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "fs";
35056
- import { join as join8 } from "path";
35551
+ import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync5 } from "fs";
35552
+ import { join as join9 } from "path";
35057
35553
 
35058
35554
  // src/lib/portable-skills.ts
35059
35555
  import {
35060
35556
  cpSync as cpSync2,
35061
- existsSync as existsSync4,
35062
- mkdirSync as mkdirSync4,
35063
- readdirSync as readdirSync3,
35557
+ existsSync as existsSync5,
35558
+ mkdirSync as mkdirSync5,
35559
+ mkdtempSync,
35560
+ readdirSync as readdirSync4,
35064
35561
  renameSync,
35065
35562
  rmSync,
35066
- statSync as statSync3,
35563
+ statSync as statSync4,
35067
35564
  writeFileSync as writeFileSync3
35068
35565
  } from "fs";
35069
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute2, join as join7, normalize } from "path";
35566
+ import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join8, normalize } from "path";
35070
35567
 
35071
35568
  // src/lib/registry-data/development-tools.ts
35072
35569
  var DEVELOPMENT_TOOLS_SKILLS = [
@@ -35778,8 +36275,29 @@ var SKILLS = [
35778
36275
  ];
35779
36276
 
35780
36277
  // src/lib/hosted-skill-set.ts
36278
+ import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
36279
+ import { join as join6 } from "path";
35781
36280
  var HOSTED_RUNTIMES = new Set(["hosted"]);
35782
36281
  var HOSTED_SOURCES = new Set(["remote", "private-hosted"]);
36282
+ function normalizeMarker(value) {
36283
+ return typeof value === "string" ? value.trim().toLowerCase() : "";
36284
+ }
36285
+ function isHostedMetadataPackage(pkg) {
36286
+ const skills = pkg?.skills;
36287
+ if (!skills || typeof skills !== "object")
36288
+ return false;
36289
+ return HOSTED_RUNTIMES.has(normalizeMarker(skills.runtime)) || HOSTED_SOURCES.has(normalizeMarker(skills.source));
36290
+ }
36291
+ function isHostedMetadataSkillDir(skillDir) {
36292
+ const pkgPath = join6(skillDir, "package.json");
36293
+ if (!existsSync3(pkgPath))
36294
+ return false;
36295
+ try {
36296
+ return isHostedMetadataPackage(JSON.parse(readFileSync4(pkgPath, "utf8")));
36297
+ } catch {
36298
+ return false;
36299
+ }
36300
+ }
35783
36301
  var HOSTED_METADATA_SET_EMPTY_ERROR = [
35784
36302
  "The hosted metadata skill set is empty, but the packaging guards that depend on it",
35785
36303
  "only mean anything while it is non-empty: an empty set makes every one of them pass",
@@ -35901,14 +36419,14 @@ var PORTABLE_SKILL_DEFAULT_VERSION = "0.1.0";
35901
36419
  // src/lib/portable-skills-files.ts
35902
36420
  import {
35903
36421
  cpSync,
35904
- existsSync as existsSync3,
36422
+ existsSync as existsSync4,
35905
36423
  lstatSync,
35906
- mkdirSync as mkdirSync3,
35907
- readFileSync as readFileSync4,
36424
+ mkdirSync as mkdirSync4,
36425
+ readFileSync as readFileSync5,
35908
36426
  realpathSync,
35909
36427
  writeFileSync as writeFileSync2
35910
36428
  } from "fs";
35911
- import { basename, dirname as dirname5, join as join6, relative } from "path";
36429
+ import { basename, dirname as dirname6, join as join7, relative } from "path";
35912
36430
  var ANY_SEGMENT_COPY_EXCLUDES = new Set([
35913
36431
  ".git",
35914
36432
  ".DS_Store",
@@ -35936,12 +36454,12 @@ function normalizePortableSkillName(name) {
35936
36454
  return normalized;
35937
36455
  }
35938
36456
  function readPortableSkillManifest(skillPath, fallbackName = basename(skillPath)) {
35939
- const skillJsonPath = join6(skillPath, "skill.json");
35940
- const skillMdPath = join6(skillPath, "SKILL.md");
35941
- const pkgPath = join6(skillPath, "package.json");
35942
- const jsonManifest = existsSync3(skillJsonPath) ? readJsonObject(skillJsonPath) : undefined;
35943
- const frontmatter = existsSync3(skillMdPath) ? parseSkillFrontmatter(readFileSync4(skillMdPath, "utf-8")) ?? undefined : undefined;
35944
- const pkg = existsSync3(pkgPath) ? readJsonObject(pkgPath) : undefined;
36457
+ const skillJsonPath = join7(skillPath, "skill.json");
36458
+ const skillMdPath = join7(skillPath, "SKILL.md");
36459
+ const pkgPath = join7(skillPath, "package.json");
36460
+ const jsonManifest = existsSync4(skillJsonPath) ? readJsonObject(skillJsonPath) : undefined;
36461
+ const frontmatter = existsSync4(skillMdPath) ? parseSkillFrontmatter(readFileSync5(skillMdPath, "utf-8")) ?? undefined : undefined;
36462
+ const pkg = existsSync4(pkgPath) ? readJsonObject(pkgPath) : undefined;
35945
36463
  const name = normalizePortableSkillName(stringField(jsonManifest, "name") ?? frontmatter?.name ?? stringValue(pkg?.name) ?? fallbackName);
35946
36464
  const description = stringField(jsonManifest, "description") ?? frontmatter?.description ?? stringValue(pkg?.description) ?? `${name} skill`;
35947
36465
  const version2 = stringField(jsonManifest, "version") ?? frontmatter?.version ?? stringValue(pkg?.version) ?? PORTABLE_SKILL_DEFAULT_VERSION;
@@ -36068,7 +36586,7 @@ function inferPackageCommands(pkg, fallbackName) {
36068
36586
  return;
36069
36587
  }
36070
36588
  function readJsonObject(path) {
36071
- const parsed = JSON.parse(readFileSync4(path, "utf-8"));
36589
+ const parsed = JSON.parse(readFileSync5(path, "utf-8"));
36072
36590
  if (!isRecord(parsed))
36073
36591
  throw new Error(`${basename(path)} must contain a JSON object`);
36074
36592
  return parsed;
@@ -36097,36 +36615,36 @@ var LEGACY_CUSTOM_DIRNAME = "custom";
36097
36615
  function getPortableSkillsRoot(options = {}) {
36098
36616
  if (options.rootDir)
36099
36617
  return options.rootDir;
36100
- const appDir = options.homeDir ? join7(options.homeDir, ".hasna", "skills") : getDataDir();
36101
- const cache3 = join7(appDir, SKILLS_CACHE_DIRNAME);
36618
+ const appDir = options.homeDir ? join8(options.homeDir, ".hasna", "skills") : getDataDir();
36619
+ const cache3 = join8(appDir, SKILLS_CACHE_DIRNAME);
36102
36620
  if (isOwnerLayoutMigrated(appDir) && safeIsDirectory(cache3))
36103
36621
  return cache3;
36104
- const installed = join7(appDir, INSTALLED_SKILLS_DIRNAME);
36622
+ const installed = join8(appDir, INSTALLED_SKILLS_DIRNAME);
36105
36623
  migrateLegacySkillLayout(appDir, installed);
36106
36624
  return installed;
36107
36625
  }
36108
36626
  function looksLikeSkillDirectory(path) {
36109
36627
  if (!safeIsDirectory(path))
36110
36628
  return false;
36111
- return existsSync4(join7(path, "SKILL.md")) || existsSync4(join7(path, "skill.json")) || existsSync4(join7(path, "package.json"));
36629
+ return existsSync5(join8(path, "SKILL.md")) || existsSync5(join8(path, "skill.json")) || existsSync5(join8(path, "package.json"));
36112
36630
  }
36113
36631
  function migrateLegacySkillLayout(appDir, installed) {
36114
36632
  if (!safeIsDirectory(appDir))
36115
36633
  return;
36116
36634
  const candidates = [];
36117
36635
  try {
36118
- for (const entry of readdirSync3(appDir)) {
36636
+ for (const entry of readdirSync4(appDir)) {
36119
36637
  if (entry.startsWith(".") || entry === INSTALLED_SKILLS_DIRNAME)
36120
36638
  continue;
36121
- const path = join7(appDir, entry);
36639
+ const path = join8(appDir, entry);
36122
36640
  if (entry === LEGACY_CUSTOM_DIRNAME) {
36123
36641
  if (!safeIsDirectory(path))
36124
36642
  continue;
36125
36643
  try {
36126
- for (const nested of readdirSync3(path)) {
36644
+ for (const nested of readdirSync4(path)) {
36127
36645
  if (nested.startsWith("."))
36128
36646
  continue;
36129
- const nestedPath = join7(path, nested);
36647
+ const nestedPath = join8(path, nested);
36130
36648
  if (looksLikeSkillDirectory(nestedPath))
36131
36649
  candidates.push({ from: nestedPath, name: nested });
36132
36650
  }
@@ -36140,10 +36658,10 @@ function migrateLegacySkillLayout(appDir, installed) {
36140
36658
  return;
36141
36659
  }
36142
36660
  for (const { from, name } of candidates) {
36143
- const target = join7(installed, name);
36144
- if (existsSync4(target))
36661
+ const target = join8(installed, name);
36662
+ if (existsSync5(target))
36145
36663
  continue;
36146
- const staging = join7(installed, `.migrating-${name}-${process.pid}`);
36664
+ const staging = join8(installed, `.migrating-${name}-${process.pid}`);
36147
36665
  try {
36148
36666
  rmSync(staging, { recursive: true, force: true });
36149
36667
  cpSync2(from, staging, { recursive: true, errorOnExist: false });
@@ -36156,7 +36674,7 @@ function migrateLegacySkillLayout(appDir, installed) {
36156
36674
  }
36157
36675
  }
36158
36676
  function getPortableSkillPath(name, options = {}) {
36159
- return join7(getPortableSkillsRoot(options), normalizePortableSkillName(name));
36677
+ return join8(getPortableSkillsRoot(options), normalizePortableSkillName(name));
36160
36678
  }
36161
36679
  function findPortableSkill(name, options = {}) {
36162
36680
  let normalized;
@@ -36166,7 +36684,7 @@ function findPortableSkill(name, options = {}) {
36166
36684
  return null;
36167
36685
  }
36168
36686
  const path = getPortableSkillPath(normalized, options);
36169
- if (!existsSync4(path) || !statSync3(path).isDirectory())
36687
+ if (!existsSync5(path) || !statSync4(path).isDirectory())
36170
36688
  return null;
36171
36689
  try {
36172
36690
  return summarizePortableSkill(path, normalized);
@@ -36179,10 +36697,10 @@ function listPortableSkills(options = {}) {
36179
36697
  if (!safeIsDirectory(root3))
36180
36698
  return [];
36181
36699
  const skills = [];
36182
- for (const entry of readdirSync3(root3).sort()) {
36700
+ for (const entry of readdirSync4(root3).sort()) {
36183
36701
  if (entry.startsWith("."))
36184
36702
  continue;
36185
- const path = join7(root3, entry);
36703
+ const path = join8(root3, entry);
36186
36704
  if (!safeIsDirectory(path))
36187
36705
  continue;
36188
36706
  try {
@@ -36204,7 +36722,8 @@ function listPortableSkillMetas(options = {}) {
36204
36722
  tags: manifest.tags || ["custom"],
36205
36723
  version: skill.version,
36206
36724
  ...manifest.kind ? { kind: manifest.kind } : {},
36207
- source: "custom"
36725
+ source: "custom",
36726
+ ...isHostedMetadataSkillDir(skill.path) ? { serverOwned: true } : {}
36208
36727
  };
36209
36728
  });
36210
36729
  }
@@ -36224,7 +36743,7 @@ function summarizePortableSkill(skillPath, fallbackName) {
36224
36743
  }
36225
36744
  function safeIsDirectory(path) {
36226
36745
  try {
36227
- return statSync3(path).isDirectory();
36746
+ return statSync4(path).isDirectory();
36228
36747
  } catch {
36229
36748
  return false;
36230
36749
  }
@@ -36274,20 +36793,20 @@ function parseSkillMdFrontmatter(content) {
36274
36793
  return Object.keys(result).length > 0 ? result : null;
36275
36794
  }
36276
36795
  function discoverSkillsInDir(dir, source = "custom") {
36277
- if (!existsSync5(dir))
36796
+ if (!existsSync6(dir))
36278
36797
  return [];
36279
36798
  const result = [];
36280
36799
  try {
36281
- const entries = readdirSync4(dir, { withFileTypes: true });
36800
+ const entries = readdirSync5(dir, { withFileTypes: true });
36282
36801
  for (const entry of entries) {
36283
36802
  if (!entry.isDirectory())
36284
36803
  continue;
36285
- const skillMdPath = join8(dir, entry.name, "SKILL.md");
36286
- if (!existsSync5(skillMdPath))
36804
+ const skillMdPath = join9(dir, entry.name, "SKILL.md");
36805
+ if (!existsSync6(skillMdPath))
36287
36806
  continue;
36288
36807
  let content;
36289
36808
  try {
36290
- content = readFileSync5(skillMdPath, "utf-8");
36809
+ content = readFileSync6(skillMdPath, "utf-8");
36291
36810
  } catch {
36292
36811
  continue;
36293
36812
  }
@@ -36302,6 +36821,7 @@ function discoverSkillsInDir(dir, source = "custom") {
36302
36821
  category: fm.category || "Development Tools",
36303
36822
  tags: fm.tags || [],
36304
36823
  ...fm.kind ? { kind: fm.kind } : {},
36824
+ ...isHostedMetadataSkillDir(join9(dir, entry.name)) ? { serverOwned: true } : {},
36305
36825
  source
36306
36826
  });
36307
36827
  }
@@ -36310,20 +36830,20 @@ function discoverSkillsInDir(dir, source = "custom") {
36310
36830
  }
36311
36831
  function findExtensionSkillPath(name) {
36312
36832
  const config = loadConfig4();
36313
- if (!config.extensionsDir || !existsSync5(config.extensionsDir))
36833
+ if (!config.extensionsDir || !existsSync6(config.extensionsDir))
36314
36834
  return null;
36315
36835
  try {
36316
- const entries = readdirSync4(config.extensionsDir, { withFileTypes: true });
36836
+ const entries = readdirSync5(config.extensionsDir, { withFileTypes: true });
36317
36837
  for (const entry of entries) {
36318
36838
  if (!entry.isDirectory())
36319
36839
  continue;
36320
- const skillDir = join8(config.extensionsDir, entry.name);
36321
- const skillMdPath = join8(skillDir, "SKILL.md");
36322
- if (!existsSync5(skillMdPath))
36840
+ const skillDir = join9(config.extensionsDir, entry.name);
36841
+ const skillMdPath = join9(skillDir, "SKILL.md");
36842
+ if (!existsSync6(skillMdPath))
36323
36843
  continue;
36324
36844
  let content;
36325
36845
  try {
36326
- content = readFileSync5(skillMdPath, "utf-8");
36846
+ content = readFileSync6(skillMdPath, "utf-8");
36327
36847
  } catch {
36328
36848
  continue;
36329
36849
  }
@@ -36356,7 +36876,7 @@ function loadRegistry(cwd) {
36356
36876
  const official = SKILLS.map((s2) => ({ ...s2, source: "official" }));
36357
36877
  const extensions = config.extensionsDir ? discoverSkillsInDir(config.extensionsDir, "extension") : [];
36358
36878
  const portableCustom = listPortableSkillMetas();
36359
- const legacyCustom = discoverSkillsInDir(join8(dataDir, "custom"));
36879
+ const legacyCustom = discoverSkillsInDir(join9(dataDir, "custom"));
36360
36880
  const globalCustom = mergeCustomSkills([...legacyCustom, ...portableCustom]);
36361
36881
  registryCache = mergeSkillRegistryLists(official, extensions, globalCustom);
36362
36882
  registryCacheTime = now;
@@ -36376,12 +36896,12 @@ function mergeCustomSkills(skills) {
36376
36896
  }
36377
36897
 
36378
36898
  // src/lib/skillinfo.ts
36379
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
36380
- import { join as join10 } from "path";
36899
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
36900
+ import { join as join11 } from "path";
36381
36901
 
36382
36902
  // src/lib/installer.ts
36383
- import { existsSync as existsSync6, readFileSync as readFileSync6, rmSync as rmSync2 } from "fs";
36384
- import { dirname as dirname7, join as join9 } from "path";
36903
+ import { existsSync as existsSync7, readFileSync as readFileSync7, rmSync as rmSync2 } from "fs";
36904
+ import { dirname as dirname8, join as join10 } from "path";
36385
36905
  import { fileURLToPath as fileURLToPath2 } from "url";
36386
36906
  // src/lib/utils.ts
36387
36907
  function normalizeSkillName(name) {
@@ -36389,16 +36909,16 @@ function normalizeSkillName(name) {
36389
36909
  }
36390
36910
 
36391
36911
  // src/lib/installer.ts
36392
- var __dirname2 = dirname7(fileURLToPath2(import.meta.url));
36912
+ var __dirname2 = dirname8(fileURLToPath2(import.meta.url));
36393
36913
  function findSkillsDir() {
36394
36914
  let dir = __dirname2;
36395
36915
  for (let i3 = 0;i3 < 5; i3++) {
36396
- const candidate = join9(dir, "skills");
36397
- if (existsSync6(candidate) && !dir.includes(".skills"))
36916
+ const candidate = join10(dir, "skills");
36917
+ if (existsSync7(candidate) && !dir.includes(".skills"))
36398
36918
  return candidate;
36399
- dir = dirname7(dir);
36919
+ dir = dirname8(dir);
36400
36920
  }
36401
- return join9(__dirname2, "..", "skills");
36921
+ return join10(__dirname2, "..", "skills");
36402
36922
  }
36403
36923
  var SKILLS_DIR = findSkillsDir();
36404
36924
  function getSkillPath(name) {
@@ -36406,13 +36926,13 @@ function getSkillPath(name) {
36406
36926
  const portable = findPortableSkill(skillName);
36407
36927
  if (portable)
36408
36928
  return portable.path;
36409
- const legacyCustomPath = join9(getDataDir(), "custom", skillName);
36410
- if (existsSync6(legacyCustomPath))
36929
+ const legacyCustomPath = join10(getDataDir(), "custom", skillName);
36930
+ if (existsSync7(legacyCustomPath))
36411
36931
  return legacyCustomPath;
36412
36932
  const extensionPath = findExtensionSkillPath(skillName);
36413
36933
  if (extensionPath)
36414
36934
  return extensionPath;
36415
- return join9(SKILLS_DIR, skillName);
36935
+ return join10(SKILLS_DIR, skillName);
36416
36936
  }
36417
36937
  function getCanonicalSkillName(name) {
36418
36938
  return getSkill(name)?.name ?? resolveSkillAlias(normalizeSkillSlug(name));
@@ -36421,18 +36941,18 @@ function getCanonicalSkillName(name) {
36421
36941
  // src/lib/skillinfo.ts
36422
36942
  function getSkillDocs(name) {
36423
36943
  const skillPath = getSkillPath(name);
36424
- if (!existsSync7(skillPath))
36944
+ if (!existsSync8(skillPath))
36425
36945
  return null;
36426
36946
  return {
36427
- skillMd: readIfExists(join10(skillPath, "SKILL.md")),
36428
- readme: readIfExists(join10(skillPath, "README.md")),
36429
- claudeMd: readIfExists(join10(skillPath, "CLAUDE.md"))
36947
+ skillMd: readIfExists(join11(skillPath, "SKILL.md")),
36948
+ readme: readIfExists(join11(skillPath, "README.md")),
36949
+ claudeMd: readIfExists(join11(skillPath, "CLAUDE.md"))
36430
36950
  };
36431
36951
  }
36432
36952
  function readIfExists(path) {
36433
36953
  try {
36434
- if (existsSync7(path)) {
36435
- return readFileSync7(path, "utf-8");
36954
+ if (existsSync8(path)) {
36955
+ return readFileSync8(path, "utf-8");
36436
36956
  }
36437
36957
  } catch {}
36438
36958
  return null;
@@ -36473,7 +36993,7 @@ function getServerSkillMd(slug) {
36473
36993
  const path = resolve(skillsDir, name, "SKILL.md");
36474
36994
  if (!isInsideDir(skillsDir, path))
36475
36995
  return null;
36476
- return existsSync8(path) ? readFileSync8(path, "utf8") : null;
36996
+ return existsSync9(path) ? readFileSync9(path, "utf8") : null;
36477
36997
  }
36478
36998
 
36479
36999
  // src/server/skills-api.ts
@@ -36726,7 +37246,15 @@ async function storePublishedSkill(store, artifactStorage, principal, parsed, ex
36726
37246
  const placement = await artifactStorage.putBundle(principal.orgId, input.bundle.sha256, parsed.bundleBytes, input.bundle.contentType);
36727
37247
  input = { ...input, bundle: { ...input.bundle, ...placement } };
36728
37248
  }
36729
- const record = await store.publishSkill(input);
37249
+ let record;
37250
+ try {
37251
+ record = await store.publishSkill(input);
37252
+ } catch (error) {
37253
+ if (input.bundle?.sha256) {
37254
+ await discardCollectedObject(store, artifactStorage, principal, input.bundle.sha256);
37255
+ }
37256
+ throw error;
37257
+ }
36730
37258
  if (superseded && superseded !== record.bundleSha256) {
36731
37259
  await discardCollectedObject(store, artifactStorage, principal, superseded);
36732
37260
  }
@@ -36798,7 +37326,7 @@ function buildPublishInput(manifest) {
36798
37326
  if (skillMd && byteLength(skillMd) > MAX_SKILL_MD_BYTES) {
36799
37327
  throw new SkillRequestError(413, "SKILL_MD_TOO_LARGE", `skillMd exceeds ${MAX_SKILL_MD_BYTES} bytes`);
36800
37328
  }
36801
- const kindValue = optionalString(manifest.kind) ?? "executable";
37329
+ const kindValue = optionalString(manifest.kind) ?? "instruction";
36802
37330
  if (kindValue !== "executable" && kindValue !== "instruction") {
36803
37331
  throw new SkillRequestError(400, "INVALID_KIND", "`kind` must be 'executable' or 'instruction'");
36804
37332
  }
@@ -36860,6 +37388,7 @@ async function createSkillsFetchHandler(options = {}) {
36860
37388
  bootstrapApiKey: config.bootstrapApiKey
36861
37389
  });
36862
37390
  assertDurableStore(store, config);
37391
+ const governanceStore = options.governanceStore ?? await createGovernanceStore(config.databaseUrl);
36863
37392
  const artifactStorage = new ArtifactStorage({
36864
37393
  bucket: config.artifactBucket,
36865
37394
  prefix: config.artifactPrefix
@@ -36882,7 +37411,7 @@ async function createSkillsFetchHandler(options = {}) {
36882
37411
  return json(identityPayload(principal));
36883
37412
  }
36884
37413
  if (segments[0] === "api" && segments[1] === "v1") {
36885
- return await handleApiV1(store, principal, request, segments.slice(2), config, artifactStorage);
37414
+ return await handleApiV1(store, governanceStore, principal, request, segments.slice(2), config, artifactStorage);
36886
37415
  }
36887
37416
  }
36888
37417
  return json({ error: "not found", code: "NOT_FOUND" }, { status: 404 });
@@ -36911,7 +37440,7 @@ function skillsServeLimits(config) {
36911
37440
  return { maxRequestBodySize: Math.max(config.skillBundleLimitBytes, config.requestBodyLimitBytes) + BODY_LIMIT_HEADROOM_BYTES };
36912
37441
  }
36913
37442
  var BODY_LIMIT_HEADROOM_BYTES = 1e6;
36914
- async function handleApiV1(store, principal, request, parts, config, artifactStorage) {
37443
+ async function handleApiV1(store, governanceStore, principal, request, parts, config, artifactStorage) {
36915
37444
  const [resource, id, subresource, childId] = parts;
36916
37445
  if (parts.some(segmentEscapesPath)) {
36917
37446
  return json({ error: "invalid path segment", code: "INVALID_PATH" }, { status: 400 });
@@ -37070,8 +37599,18 @@ async function handleApiV1(store, principal, request, parts, config, artifactSto
37070
37599
  const run = await store.getRun(principal, id);
37071
37600
  if (!run)
37072
37601
  return json({ error: "run not found", code: "RUN_NOT_FOUND" }, { status: 404 });
37073
- const next = await store.updateRun(id, { status: "cancel_requested" });
37074
- return json(runPayload(next ?? run));
37602
+ try {
37603
+ const outcome = await createCancelService({ store, governanceStore, storage: artifactStorage }).cancel(principal, id, principal.email);
37604
+ return json(runPayload(outcome.run));
37605
+ } catch (error) {
37606
+ if (error instanceof GovernanceError && error.code === GOVERNANCE_ERROR_CODES.STALE_LEASE_GENERATION) {
37607
+ return json({ error: error.message, code: error.code }, { status: 409 });
37608
+ }
37609
+ if (error instanceof StaleLeaseGenerationError) {
37610
+ return json({ error: error.message, code: GOVERNANCE_ERROR_CODES.STALE_LEASE_GENERATION }, { status: 409 });
37611
+ }
37612
+ throw error;
37613
+ }
37075
37614
  }
37076
37615
  }
37077
37616
  return json({ error: "not found", code: "NOT_FOUND" }, { status: 404 });
@@ -37161,6 +37700,24 @@ function clampInt(value, fallback, max) {
37161
37700
  }
37162
37701
 
37163
37702
  // src/server/index.ts
37703
+ var EARLY_ARGV = process.argv.slice(2);
37704
+ if (EARLY_ARGV.includes("--version") || EARLY_ARGV.includes("-V")) {
37705
+ console.log(package_default.version);
37706
+ process.exit(0);
37707
+ }
37708
+ if (EARLY_ARGV.includes("--help") || EARLY_ARGV.includes("-h")) {
37709
+ console.log(`Usage: skills-server [options]
37710
+
37711
+ Runs the @hasna/skills HTTP API.
37712
+
37713
+ Options:
37714
+ -V, --version output the version number
37715
+ -h, --help display help for command
37716
+
37717
+ Environment:
37718
+ SKILLS_PORT Listen port (default: 8787)`);
37719
+ process.exit(0);
37720
+ }
37164
37721
  var config = resolveServerConfig();
37165
37722
  var server = await startSkillsServer({ config });
37166
37723
  console.log(`skills API listening on http://${config.host}:${server.port}`);