@hasna/skills 0.1.64 → 0.1.67

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