@hasna/skills 0.1.71 → 0.2.0

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/worker.js CHANGED
@@ -22940,6 +22940,127 @@ var init_dist_es9 = __esm(() => {
22940
22940
 
22941
22941
  // src/server/worker.ts
22942
22942
  import { randomUUID as randomUUID4 } from "crypto";
22943
+ // package.json
22944
+ var package_default = {
22945
+ name: "@hasna/skills",
22946
+ version: "0.2.0",
22947
+ description: "Skills library for AI coding agents",
22948
+ type: "module",
22949
+ bin: {
22950
+ skills: "bin/index.js",
22951
+ "skills-mcp": "bin/mcp.js",
22952
+ "skills-server": "bin/server.js",
22953
+ "skills-worker": "bin/worker.js",
22954
+ "skills-migrate": "bin/migrate.js"
22955
+ },
22956
+ exports: {
22957
+ ".": {
22958
+ import: "./dist/index.js",
22959
+ types: "./dist/index.d.ts"
22960
+ },
22961
+ "./storage": {
22962
+ import: "./dist/storage.js",
22963
+ types: "./dist/storage.d.ts"
22964
+ },
22965
+ "./sdk": {
22966
+ import: "./dist/sdk/index.js",
22967
+ types: "./dist/sdk/index.d.ts"
22968
+ },
22969
+ "./admin-contract": {
22970
+ import: "./dist/admin-contract.js",
22971
+ types: "./dist/admin-contract.d.ts"
22972
+ }
22973
+ },
22974
+ files: [
22975
+ "dist/",
22976
+ "!dist/**/*.test.d.ts",
22977
+ "!dist/test-preload.d.ts",
22978
+ "!dist/platform",
22979
+ "bin/",
22980
+ "migrations/",
22981
+ "docs/skill-standard.md",
22982
+ "schemas/",
22983
+ "LICENSE",
22984
+ "README.md"
22985
+ ],
22986
+ main: "./dist/index.js",
22987
+ types: "./dist/index.d.ts",
22988
+ scripts: {
22989
+ clean: "rm -rf bin/ dist/",
22990
+ build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
22991
+ "build:js": "rm -rf dist && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
22992
+ test: "bun test --timeout 30000",
22993
+ dev: "bun run ./src/cli/index.tsx",
22994
+ "dev:watch": "bun --watch run ./src/cli/index.tsx",
22995
+ "dev:mcp": "bun --watch run ./src/mcp/index.ts",
22996
+ "dev:server": "bun --watch run ./src/server/index.ts",
22997
+ "dev:worker": "bun --watch run ./src/server/worker.ts",
22998
+ "docker:check": "bash scripts/docker-build-check.sh",
22999
+ migrate: "bun run ./src/server/migrate.ts",
23000
+ typecheck: "tsc --noEmit",
23001
+ "verify:release": "bun run scripts/release-guard.ts",
23002
+ prepare: "bun run build:js",
23003
+ prepack: "bun run build && bun run verify:release",
23004
+ prepublishOnly: "bun run typecheck && bun run test",
23005
+ postinstall: "mkdir -p $HOME/.hasna/skills/custom 2>/dev/null || true"
23006
+ },
23007
+ keywords: [
23008
+ "skills",
23009
+ "ai",
23010
+ "agent",
23011
+ "cli",
23012
+ "typescript",
23013
+ "bun",
23014
+ "claude",
23015
+ "codex",
23016
+ "gemini",
23017
+ "mcp",
23018
+ "model-context-protocol",
23019
+ "open-source",
23020
+ "skills-library",
23021
+ "automation"
23022
+ ],
23023
+ author: "Hasna",
23024
+ license: "Apache-2.0",
23025
+ devDependencies: {
23026
+ "@types/bun": "1.3.14",
23027
+ "@types/node": "25.2.3",
23028
+ "@types/react": "^18.2.0",
23029
+ "bun-types": "1.3.14",
23030
+ "react-devtools-core": "^7.0.1",
23031
+ typescript: "^5",
23032
+ yaml: "^2.9.0"
23033
+ },
23034
+ dependencies: {
23035
+ "@aws-sdk/client-ecs": "^3.1079.0",
23036
+ "@aws-sdk/client-s3": "^3.1079.0",
23037
+ "@hasna/events": "0.1.16",
23038
+ "@modelcontextprotocol/sdk": "^1.26.0",
23039
+ chalk: "^5.3.0",
23040
+ commander: "^12.1.0",
23041
+ ink: "^5.0.1",
23042
+ "ink-select-input": "^6.0.0",
23043
+ "ink-spinner": "^5.0.0",
23044
+ "ink-text-input": "^6.0.0",
23045
+ react: "^18.2.0",
23046
+ zod: "^4.3.6"
23047
+ },
23048
+ engines: {
23049
+ bun: ">=1.0.0"
23050
+ },
23051
+ publishConfig: {
23052
+ registry: "https://registry.npmjs.org",
23053
+ access: "public"
23054
+ },
23055
+ repository: {
23056
+ type: "git",
23057
+ url: "git+https://github.com/hasna/apps.git"
23058
+ },
23059
+ homepage: "https://github.com/hasna/skills",
23060
+ bugs: {
23061
+ url: "https://github.com/hasna/skills/issues"
23062
+ }
23063
+ };
22943
23064
 
22944
23065
  // src/lib/skill-bundle.ts
22945
23066
  var ANY_SEGMENT_EXCLUDES = new Set([
@@ -31987,7 +32108,7 @@ var WriteGetObjectResponse$ = [
31987
32108
  class CreateSessionCommand extends command(_ep4, _mw0, "CreateSession", CreateSession$) {
31988
32109
  }
31989
32110
  // ../../node_modules/.bun/@aws-sdk+client-s3@3.1112.0/node_modules/@aws-sdk/client-s3/package.json
31990
- var package_default = {
32111
+ var package_default2 = {
31991
32112
  name: "@aws-sdk/client-s3",
31992
32113
  version: "3.1112.0",
31993
32114
  description: "AWS SDK for JavaScript S3 Client for Node.js, Browser and React Native",
@@ -32537,7 +32658,7 @@ var getRuntimeConfig3 = (config) => {
32537
32658
  authSchemePreference: config?.authSchemePreference ?? import_config28.loadConfig(import_httpAuthSchemes3.NODE_AUTH_SCHEME_PREFERENCE_OPTIONS, loaderConfig),
32538
32659
  bodyLengthChecker: config?.bodyLengthChecker ?? import_serde4.calculateBodyLength,
32539
32660
  credentialDefaultProvider: config?.credentialDefaultProvider ?? defaultProvider,
32540
- 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 }),
32541
32662
  disableS3ExpressSessionAuth: config?.disableS3ExpressSessionAuth ?? import_config28.loadConfig($NODE_DISABLE_S3_EXPRESS_SESSION_AUTH_OPTIONS, loaderConfig),
32542
32663
  eventStreamSerdeProvider: config?.eventStreamSerdeProvider ?? import_event_streams.eventStreamSerdeProvider,
32543
32664
  maxAttempts: config?.maxAttempts ?? import_config28.loadConfig(import_retry3.NODE_MAX_ATTEMPT_CONFIG_OPTIONS, config),
@@ -32677,7 +32798,7 @@ class ArtifactStorage {
32677
32798
  constructor(options = {}) {
32678
32799
  this.bucket = options.bucket;
32679
32800
  this.prefix = (options.prefix || "skills/artifacts").replace(/^\/+|\/+$/g, "");
32680
- this.s3 = this.bucket ? new S3Client({ region: options.region || process.env.AWS_REGION || "us-east-1" }) : undefined;
32801
+ this.s3 = this.bucket ? options.client ?? new S3Client({ region: options.region || process.env.AWS_REGION || "us-east-1" }) : undefined;
32681
32802
  }
32682
32803
  get usesS3() {
32683
32804
  return Boolean(this.bucket);
@@ -32775,6 +32896,27 @@ class ArtifactStorage {
32775
32896
  keyFor(run, relativePath) {
32776
32897
  return this.objectKeyFor(run.orgId, run.id, relativePath);
32777
32898
  }
32899
+ async putVersionObjects(orgId, slug, version2, bytes, manifest, contentType = "application/gzip") {
32900
+ if (!this.bucket || !this.s3)
32901
+ return { storageKind: "db" };
32902
+ const key = this.versionKeyFor(orgId, slug, version2, "bundle.tar.gz");
32903
+ await this.s3.send(new PutObjectCommand({ Bucket: this.bucket, Key: key, Body: bytes, ContentType: contentType }));
32904
+ await this.s3.send(new PutObjectCommand({
32905
+ Bucket: this.bucket,
32906
+ Key: this.versionKeyFor(orgId, slug, version2, "manifest.json"),
32907
+ Body: new TextEncoder().encode(JSON.stringify(manifest, null, 2)),
32908
+ ContentType: "application/json"
32909
+ }));
32910
+ return { storageKind: "s3", storageKey: key };
32911
+ }
32912
+ versionPlacement(orgId, slug, version2) {
32913
+ if (!this.bucket || !this.s3)
32914
+ return { storageKind: "db" };
32915
+ return { storageKind: "s3", storageKey: this.versionKeyFor(orgId, slug, version2, "bundle.tar.gz") };
32916
+ }
32917
+ versionKeyFor(orgId, slug, version2, file) {
32918
+ return `${this.prefix}/skills/${encodeURIComponent(orgId)}/${encodeURIComponent(slug)}/${encodeURIComponent(version2)}/${file}`;
32919
+ }
32778
32920
  bundleKeyFor(orgId, sha256) {
32779
32921
  return `${this.prefix}/bundles/${encodeURIComponent(orgId)}/${sha256}.tar.gz`;
32780
32922
  }
@@ -32876,13 +33018,12 @@ var DEFAULT_RUN_QUOTA = {
32876
33018
  };
32877
33019
 
32878
33020
  // src/server/database-url.ts
32879
- import { isAbsolute, join as join3 } from "path";
33021
+ import { isAbsolute, join as join4 } from "path";
32880
33022
  import { fileURLToPath } from "url";
32881
33023
 
32882
33024
  // src/lib/config.ts
32883
- import { existsSync, readFileSync as readFileSync2, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
32884
- import { join as join2, dirname as dirname2 } from "path";
32885
- import { homedir as homedir2 } from "os";
33025
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
33026
+ import { join as join3, dirname as dirname2 } from "path";
32886
33027
 
32887
33028
  // src/lib/retired-settings.ts
32888
33029
  var RETIRED_ENV_SUFFIXES = ["_STORAGE_MODE", "_DEPLOYMENT_MODE", "_CLOUD_MODE"];
@@ -32919,59 +33060,160 @@ function assertNoRetiredModeEnvVars(env, options) {
32919
33060
  throw new RetiredSettingError(names[0], `${names.join(", ")} ${names.length === 1 ? "is" : "are"} no longer read. ` + "Deployment modes were removed: where a server keeps its data is decided by the " + `database it is given, not by a declared label. Set ${options.replacement} to a ` + "postgres:// URL to use PostgreSQL, or leave it unset for the on-box SQLite database. " + `Then unset ${names.join(" and ")}. ` + "Refused rather than ignored, because a discarded setting looks exactly like a " + "working one until something needs the data.");
32920
33061
  }
32921
33062
 
33063
+ // src/lib/app-home.ts
33064
+ import { existsSync } from "fs";
33065
+ import { homedir as homedir2 } from "os";
33066
+ import { join as join2, resolve } from "path";
33067
+ import { homedir as pathsResolverHomedir } from "os";
33068
+ import { join as pathsResolverJoin } from "path";
33069
+ var PATHS_RESOLVER_KIND_ENV = {
33070
+ config: "HASNA_CONFIG_HOME",
33071
+ data: "HASNA_DATA_HOME",
33072
+ state: "HASNA_STATE_HOME",
33073
+ cache: "HASNA_CACHE_HOME"
33074
+ };
33075
+ var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
33076
+ function pathsResolverAssertApp(app) {
33077
+ if (typeof app !== "string" || app.length === 0) {
33078
+ throw new TypeError("paths: app must be a non-empty string");
33079
+ }
33080
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
33081
+ throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
33082
+ }
33083
+ }
33084
+ function pathsResolverAssertKind(kind) {
33085
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
33086
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
33087
+ }
33088
+ }
33089
+ function pathsResolverBaseDir(kind, options) {
33090
+ pathsResolverAssertKind(kind);
33091
+ const env = options.env ?? process.env;
33092
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
33093
+ if (typeof override === "string" && override.length > 0)
33094
+ return override;
33095
+ const home = options.home ?? pathsResolverHomedir();
33096
+ const platform = options.platform ?? process.platform;
33097
+ if (platform === "darwin") {
33098
+ switch (kind) {
33099
+ case "config":
33100
+ case "data":
33101
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
33102
+ case "cache":
33103
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
33104
+ case "state":
33105
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
33106
+ }
33107
+ }
33108
+ switch (kind) {
33109
+ case "config":
33110
+ return pathsResolverJoin(home, ".config", "hasna");
33111
+ case "data":
33112
+ return pathsResolverJoin(home, ".local", "share", "hasna");
33113
+ case "state":
33114
+ return pathsResolverJoin(home, ".local", "state", "hasna");
33115
+ case "cache":
33116
+ return pathsResolverJoin(home, ".cache", "hasna");
33117
+ }
33118
+ }
33119
+ function pathsResolverResolve(kind, options) {
33120
+ pathsResolverAssertApp(options.app);
33121
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
33122
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
33123
+ }
33124
+ function dataDir(options) {
33125
+ return pathsResolverResolve("data", options);
33126
+ }
33127
+ var DATA_DIR_ENV = "HASNA_SKILLS_DIR";
33128
+ var HASNA_SKILLS_HOME_ENV = "HASNA_SKILLS_HOME";
33129
+ var SKILLS_HOME_ENV = "SKILLS_HOME";
33130
+ var DEFAULT_SQLITE_FILENAME = "server.db";
33131
+ var GLOBAL_CONFIG_FILENAME = "config.json";
33132
+ function effectiveHome() {
33133
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir2() || "/tmp";
33134
+ }
33135
+ function legacyDataRoot() {
33136
+ return join2(effectiveHome(), ".hasna", "skills");
33137
+ }
33138
+ function resolverDataRoot(home = effectiveHome(), env) {
33139
+ return dataDir({ app: "skills", home, env });
33140
+ }
33141
+ function adoptResolverDataRoot(resolved, env = process.env) {
33142
+ const dataOverride = env.HASNA_DATA_HOME;
33143
+ if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
33144
+ return true;
33145
+ return existsSync(join2(resolved, DEFAULT_SQLITE_FILENAME)) || existsSync(join2(resolved, GLOBAL_CONFIG_FILENAME));
33146
+ }
33147
+ function exactDataRoot() {
33148
+ for (const key of [DATA_DIR_ENV, HASNA_SKILLS_HOME_ENV, SKILLS_HOME_ENV]) {
33149
+ const dir = process.env[key]?.trim();
33150
+ if (dir)
33151
+ return resolve(dir);
33152
+ }
33153
+ return;
33154
+ }
33155
+ function hasExactOverride(env = process.env) {
33156
+ return Boolean(env[DATA_DIR_ENV]?.trim()) || Boolean(env[HASNA_SKILLS_HOME_ENV]?.trim()) || Boolean(env[SKILLS_HOME_ENV]?.trim());
33157
+ }
33158
+ function hasOperatorOverride(env = process.env) {
33159
+ return hasExactOverride(env) || Boolean(env.HASNA_DATA_HOME?.trim());
33160
+ }
33161
+ function getDataRoot() {
33162
+ const exact = exactDataRoot();
33163
+ if (exact)
33164
+ return exact;
33165
+ const resolved = resolverDataRoot();
33166
+ return adoptResolverDataRoot(resolved) ? resolve(resolved) : resolve(legacyDataRoot());
33167
+ }
32922
33168
  // src/lib/config.ts
32923
33169
  function mergeDirectoryContents(sourceDir, targetDir) {
32924
- if (!existsSync(sourceDir))
33170
+ if (!existsSync2(sourceDir))
32925
33171
  return;
32926
33172
  mkdirSync(targetDir, { recursive: true });
32927
33173
  for (const entry of readdirSync(sourceDir)) {
32928
- const sourcePath = join2(sourceDir, entry);
32929
- const targetPath = join2(targetDir, entry);
33174
+ const sourcePath = join3(sourceDir, entry);
33175
+ const targetPath = join3(targetDir, entry);
32930
33176
  try {
32931
33177
  const sourceStat = statSync(sourcePath);
32932
33178
  if (sourceStat.isDirectory()) {
32933
33179
  mergeDirectoryContents(sourcePath, targetPath);
32934
33180
  continue;
32935
33181
  }
32936
- if (!existsSync(targetPath))
33182
+ if (!existsSync2(targetPath))
32937
33183
  copyFileSync(sourcePath, targetPath);
32938
33184
  } catch {}
32939
33185
  }
32940
33186
  }
32941
- var DATA_DIR_ENV = "HASNA_SKILLS_DIR";
32942
33187
  function getDataDir() {
32943
- const override = process.env[DATA_DIR_ENV];
32944
- if (override) {
32945
- try {
32946
- mkdirSync(override, { recursive: true });
32947
- } catch {}
32948
- return override;
32949
- }
32950
- const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
32951
- const newDir = join2(home, ".hasna", "skills");
32952
- const oldDir = join2(home, ".skills");
32953
- const oldConfigFile = join2(home, ".skillsrc");
32954
- mkdirSync(newDir, { recursive: true });
33188
+ const root3 = getDataRoot();
33189
+ try {
33190
+ mkdirSync(root3, { recursive: true });
33191
+ } catch {}
33192
+ if (hasOperatorOverride())
33193
+ return root3;
33194
+ const home = effectiveHome();
33195
+ const oldDir = join3(home, ".skills");
33196
+ const oldConfigFile = join3(home, ".skillsrc");
32955
33197
  try {
32956
- mergeDirectoryContents(oldDir, newDir);
33198
+ mergeDirectoryContents(oldDir, root3);
32957
33199
  } catch {}
32958
- if (existsSync(oldConfigFile) && !existsSync(join2(newDir, "config.json"))) {
33200
+ if (existsSync2(oldConfigFile) && !existsSync2(join3(root3, "config.json"))) {
32959
33201
  try {
32960
- copyFileSync(oldConfigFile, join2(newDir, "config.json"));
33202
+ copyFileSync(oldConfigFile, join3(root3, "config.json"));
32961
33203
  } catch {}
32962
33204
  }
32963
- return newDir;
33205
+ return root3;
32964
33206
  }
32965
33207
 
32966
33208
  // src/server/database-url.ts
32967
- var DEFAULT_SQLITE_FILENAME = "server.db";
33209
+ var DEFAULT_SQLITE_FILENAME2 = "server.db";
32968
33210
  var SQLITE_MEMORY_PATH = ":memory:";
32969
33211
  var POSTGRES_SCHEMES = new Set(["postgres", "postgresql"]);
32970
33212
  var SQLITE_SCHEMES = new Set(["sqlite", "sqlite3", "file"]);
32971
33213
  var MEMORY_SCHEMES = new Set(["memory"]);
32972
33214
  var SQLITE_EXTENSIONS = [".db", ".sqlite", ".sqlite3", ".db3"];
32973
33215
  function defaultSqlitePath() {
32974
- return join3(getDataDir(), DEFAULT_SQLITE_FILENAME);
33216
+ return join4(getDataDir(), DEFAULT_SQLITE_FILENAME2);
32975
33217
  }
32976
33218
  function resolveDatabaseTarget(raw) {
32977
33219
  const value = raw?.trim();
@@ -32998,7 +33240,7 @@ function resolveDatabaseTarget(raw) {
32998
33240
  throw new Error(`unsupported database scheme "${scheme}:". Supported: postgres://, postgresql://, sqlite:, file:, ` + `an absolute or relative path to a .db/.sqlite file, ":memory:", or "memory:" (non-durable, tests only). ` + `Leave the setting empty to use the default SQLite database at ${defaultSqlitePath()}.`);
32999
33241
  }
33000
33242
  if (looksLikeSqlitePath(value)) {
33001
- const path = isAbsolute(value) ? value : join3(process.cwd(), value);
33243
+ const path = isAbsolute(value) ? value : join4(process.cwd(), value);
33002
33244
  return { kind: "sqlite", path, durable: true, label: `sqlite (${path})` };
33003
33245
  }
33004
33246
  throw new Error(`could not resolve a database backend from "${value}". Use postgres://\u2026, sqlite:\u2026, an absolute or ` + `relative path ending in ${SQLITE_EXTENSIONS.join("/")}, ":memory:", or leave it empty for the ` + `default SQLite database at ${defaultSqlitePath()}.`);
@@ -33022,7 +33264,7 @@ function sqlitePathFromUrl(value, scheme) {
33022
33264
  }
33023
33265
  return rest.slice(2).replace(/^\/\/+/, "/");
33024
33266
  }
33025
- return isAbsolute(rest) ? rest : join3(process.cwd(), rest);
33267
+ return isAbsolute(rest) ? rest : join4(process.cwd(), rest);
33026
33268
  }
33027
33269
  function looksLikeSqlitePath(value) {
33028
33270
  if (value.includes("/"))
@@ -33034,7 +33276,7 @@ function looksLikeSqlitePath(value) {
33034
33276
  import { Database } from "bun:sqlite";
33035
33277
  import { randomUUID as randomUUID2 } from "crypto";
33036
33278
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
33037
- import { dirname as dirname4, join as join5 } from "path";
33279
+ import { dirname as dirname4, join as join6 } from "path";
33038
33280
 
33039
33281
  // src/server/auth.ts
33040
33282
  import { createHash as createHash3 } from "crypto";
@@ -33055,16 +33297,16 @@ function publicPrincipal(partial = {}) {
33055
33297
  }
33056
33298
 
33057
33299
  // src/server/migrations-dir.ts
33058
- import { existsSync as existsSync2 } from "fs";
33059
- import { dirname as dirname3, join as join4 } from "path";
33300
+ import { existsSync as existsSync3 } from "fs";
33301
+ import { dirname as dirname3, join as join5 } from "path";
33060
33302
  var MIGRATION_DIALECTS = ["postgres", "sqlite"];
33061
33303
  var MAX_WALK_UP = 6;
33062
33304
  function findMigrationsRoot(startDirs = defaultStartDirs()) {
33063
33305
  for (const start of startDirs) {
33064
33306
  let dir = start;
33065
33307
  for (let level = 0;level < MAX_WALK_UP; level += 1) {
33066
- const candidate = join4(dir, "migrations");
33067
- if (MIGRATION_DIALECTS.some((dialect) => existsSync2(join4(candidate, dialect))))
33308
+ const candidate = join5(dir, "migrations");
33309
+ if (MIGRATION_DIALECTS.some((dialect) => existsSync3(join5(candidate, dialect))))
33068
33310
  return candidate;
33069
33311
  const parent = dirname3(dir);
33070
33312
  if (parent === dir)
@@ -33078,8 +33320,8 @@ function resolveMigrationsDir(dialect, root3 = findMigrationsRoot()) {
33078
33320
  if (!root3) {
33079
33321
  throw new Error(`could not locate the migrations directory for dialect "${dialect}". ` + `Expected a migrations/${dialect}/ folder alongside the package root.`);
33080
33322
  }
33081
- const dir = join4(root3, dialect);
33082
- if (!existsSync2(dir)) {
33323
+ const dir = join5(root3, dialect);
33324
+ if (!existsSync3(dir)) {
33083
33325
  throw new Error(`migrations directory not found: ${dir}`);
33084
33326
  }
33085
33327
  return dir;
@@ -33236,6 +33478,20 @@ function dateString(value) {
33236
33478
  return value.toISOString();
33237
33479
  return String(value);
33238
33480
  }
33481
+ function rowToSkillVersion(row) {
33482
+ return {
33483
+ orgId: String(row.org_id),
33484
+ slug: String(row.slug),
33485
+ version: String(row.version),
33486
+ bundleSha256: String(row.bundle_sha256),
33487
+ bundleByteSize: Number(row.bundle_byte_size),
33488
+ storageKind: String(row.storage_kind ?? "db"),
33489
+ ...typeof row.storage_key === "string" ? { storageKey: row.storage_key } : {},
33490
+ manifest: parseJsonObject(row.manifest_json),
33491
+ ...typeof row.published_by_user_id === "string" ? { publishedByUserId: row.published_by_user_id } : {},
33492
+ createdAt: dateString(row.created_at)
33493
+ };
33494
+ }
33239
33495
 
33240
33496
  // src/server/types.ts
33241
33497
  class StaleLeaseGenerationError extends Error {
@@ -33266,6 +33522,21 @@ class SkillRevisionConflictError extends Error {
33266
33522
  }
33267
33523
  }
33268
33524
 
33525
+ class SkillVersionExistsError extends Error {
33526
+ slug;
33527
+ version;
33528
+ existingSha256;
33529
+ attemptedSha256;
33530
+ constructor(slug, version2, existingSha256, attemptedSha256) {
33531
+ super(`version conflict for '${slug}@${version2}': already published with bundle ${existingSha256}, ` + `refusing to overwrite it with ${attemptedSha256}. Publish a new version instead.`);
33532
+ this.slug = slug;
33533
+ this.version = version2;
33534
+ this.existingSha256 = existingSha256;
33535
+ this.attemptedSha256 = attemptedSha256;
33536
+ this.name = "SkillVersionExistsError";
33537
+ }
33538
+ }
33539
+
33269
33540
  // src/lib/revision.ts
33270
33541
  import { createHash as createHash4 } from "crypto";
33271
33542
  function revisionIdOf(content) {
@@ -33561,6 +33832,13 @@ class SqliteSkillsStore {
33561
33832
  throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
33562
33833
  }
33563
33834
  const carriedSha = input.bundle?.sha256 ?? previousSha;
33835
+ if (input.version && carriedSha) {
33836
+ const existingVersion = this.get("SELECT bundle_sha256 FROM skills_versions WHERE org_id = ? AND slug = ? AND version = ? LIMIT 1", [orgId, input.slug, input.version]);
33837
+ const existingSha = typeof existingVersion?.bundle_sha256 === "string" ? existingVersion.bundle_sha256 : null;
33838
+ if (existingSha && existingSha !== carriedSha) {
33839
+ throw new SkillVersionExistsError(input.slug, input.version, existingSha, carriedSha);
33840
+ }
33841
+ }
33564
33842
  const carriedSize = input.bundle?.byteSize ?? (previous?.bundle_byte_size == null ? null : Number(previous.bundle_byte_size));
33565
33843
  const revisionId = revisionIdOfRecord({
33566
33844
  slug: input.slug,
@@ -33648,6 +33926,21 @@ class SqliteSkillsStore {
33648
33926
  const currentId = typeof current?.revision_id === "string" ? current.revision_id : null;
33649
33927
  throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, currentId);
33650
33928
  }
33929
+ if (input.version && carriedSha) {
33930
+ this.db.run(`INSERT OR IGNORE INTO skills_versions (org_id, slug, version, bundle_sha256, bundle_byte_size, storage_kind, storage_key, manifest_json, published_by_user_id, created_at)
33931
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
33932
+ orgId,
33933
+ input.slug,
33934
+ input.version,
33935
+ carriedSha,
33936
+ carriedSize ?? 0,
33937
+ input.versionStorage?.storageKind ?? "db",
33938
+ input.versionStorage?.storageKey ?? null,
33939
+ JSON.stringify(input.versionManifest ?? {}),
33940
+ input.principal.userId ?? null,
33941
+ now
33942
+ ]);
33943
+ }
33651
33944
  if (previousSha && input.bundle && previousSha !== input.bundle.sha256)
33652
33945
  this.collectOrphanBundle(orgId, previousSha);
33653
33946
  this.db.run("DELETE FROM skills_tags WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
@@ -33755,6 +34048,13 @@ class SqliteSkillsStore {
33755
34048
  const row = this.get("SELECT * FROM skills_bundles WHERE org_id = ? AND sha256 = ? LIMIT 1", [principal.orgId, sha256]);
33756
34049
  return row ? rowToSkillBundle(row) : null;
33757
34050
  }
34051
+ async listSkillVersions(principal, slug) {
34052
+ return this.all("SELECT * FROM skills_versions WHERE org_id = ? AND slug = ? ORDER BY created_at DESC, version DESC", [principal.orgId, slug]).map(rowToSkillVersion);
34053
+ }
34054
+ async getSkillVersion(principal, slug, version2) {
34055
+ const row = this.get("SELECT * FROM skills_versions WHERE org_id = ? AND slug = ? AND version = ? LIMIT 1", [principal.orgId, slug, version2]);
34056
+ return row ? rowToSkillVersion(row) : null;
34057
+ }
33758
34058
  async pinSkill(principal, slug, metadata = {}) {
33759
34059
  const row = this.get(`INSERT INTO skills_pins (org_id, principal, slug, pinned_at, metadata_json)
33760
34060
  VALUES (?, ?, ?, ?, ?)
@@ -33794,7 +34094,7 @@ class SqliteSkillsStore {
33794
34094
  return this.all("SELECT slug FROM skills_registry WHERE org_id = ? AND tombstoned_at IS NULL ORDER BY slug ASC", [principal.orgId]).map((row) => String(row.slug));
33795
34095
  }
33796
34096
  collectOrphanBundle(orgId, sha256) {
33797
- const referenced = this.get("SELECT 1 AS present FROM skills_registry WHERE org_id = ? AND bundle_sha256 = ? LIMIT 1", [orgId, sha256]);
34097
+ const referenced = this.get("SELECT 1 AS present FROM skills_registry WHERE org_id = ? AND bundle_sha256 = ? LIMIT 1", [orgId, sha256]) ?? this.get("SELECT 1 AS present FROM skills_versions WHERE org_id = ? AND bundle_sha256 = ? LIMIT 1", [orgId, sha256]);
33798
34098
  if (referenced)
33799
34099
  return;
33800
34100
  this.db.run("DELETE FROM skills_bundles WHERE org_id = ? AND sha256 = ?", [orgId, sha256]);
@@ -33823,7 +34123,7 @@ function applySqliteMigrations(db, migrationsDir = resolveMigrationsDir("sqlite"
33823
34123
  const appliedNow = [];
33824
34124
  for (const file of files) {
33825
34125
  const version2 = file.replace(/\.sql$/, "");
33826
- const text = readFileSync3(join5(migrationsDir, file), "utf8");
34126
+ const text = readFileSync3(join6(migrationsDir, file), "utf8");
33827
34127
  const apply = db.transaction(() => {
33828
34128
  const already = db.query("SELECT 1 AS present FROM schema_migrations WHERE version = ? LIMIT 1").get(version2);
33829
34129
  if (already)
@@ -33878,731 +34178,1096 @@ function parseScopes(value) {
33878
34178
  }
33879
34179
  }
33880
34180
 
33881
- // src/server/config.ts
33882
- var DATABASE_URL_ENV = "HASNA_SKILLS_DATABASE_URL";
33883
- var SKILLS_ENV_NAMESPACE = "SKILLS";
33884
- function resolveServerConfig(env = process.env) {
33885
- assertNoRetiredModeEnvVars(env, {
33886
- app: SKILLS_ENV_NAMESPACE,
33887
- replacement: DATABASE_URL_ENV
33888
- });
33889
- const nodeEnv = env.NODE_ENV || "development";
33890
- const host = env.HOST || env.SKILLS_HOST || "0.0.0.0";
33891
- const port = parsePositiveInt(env.PORT || env.SKILLS_PORT, 8787);
33892
- return {
33893
- host,
33894
- port,
33895
- databaseUrl: env[DATABASE_URL_ENV] || env.DATABASE_URL || undefined,
33896
- bootstrapApiKey: env.HASNA_SKILLS_BOOTSTRAP_API_KEY || undefined,
33897
- artifactBucket: env.HASNA_SKILLS_S3_BUCKET || env.SKILLS_S3_BUCKET || undefined,
33898
- artifactPrefix: normalizePrefix(env.HASNA_SKILLS_S3_PREFIX || env.SKILLS_S3_PREFIX || "skills/artifacts"),
33899
- inlineWorker: env.HASNA_SKILLS_INLINE_WORKER === "1",
33900
- bundleSigningKey: env.HASNA_SKILLS_API_SIGNING_KEY || env.HASNA_SKILLS_SIGNING_KEY || undefined,
33901
- requestBodyLimitBytes: parsePositiveInt(env.HASNA_SKILLS_REQUEST_BODY_LIMIT_BYTES, 1e6),
33902
- skillBundleLimitBytes: parsePositiveInt(env.HASNA_SKILLS_BUNDLE_LIMIT_BYTES, 25000000),
33903
- tombstoneWindowMs: parsePositiveInt(env.HASNA_SKILLS_TOMBSTONE_WINDOW_MS, 7 * 24 * 60 * 60 * 1000),
33904
- publicBaseUrl: (env.SKILLS_PUBLIC_BASE_URL || localOrigin(host, port)).replace(/\/+$/, ""),
33905
- nodeEnv,
33906
- allowEphemeralStore: env.HASNA_SKILLS_ALLOW_EPHEMERAL_STORE === "1"
33907
- };
33908
- }
33909
- function localOrigin(host, port) {
33910
- const hostname = host === "0.0.0.0" || host === "::" ? "localhost" : host;
33911
- return `http://${hostname.includes(":") ? `[${hostname}]` : hostname}:${port}`;
33912
- }
33913
- function parsePositiveInt(value, fallback) {
33914
- const parsed = Number.parseInt(value ?? "", 10);
33915
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
33916
- }
33917
- function normalizePrefix(value) {
33918
- return value.replace(/^\/+|\/+$/g, "") || "skills/artifacts";
33919
- }
33920
-
33921
- // src/server/handlers.ts
33922
- import { createHash as createHash5 } from "crypto";
33923
-
33924
- // src/server/store.ts
33925
- import { randomUUID as randomUUID3 } from "crypto";
33926
- function recordFieldsOf(input, carriedBundle, carriedSkillMd) {
33927
- return {
33928
- slug: input.slug,
33929
- displayName: input.displayName,
33930
- description: input.description,
33931
- category: input.category,
33932
- tags: input.tags,
33933
- source: input.source,
33934
- kind: input.kind,
33935
- ...input.version ? { version: input.version } : {},
33936
- ...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
33937
- bundleSha256: input.bundle?.sha256 ?? carriedBundle.bundleSha256,
33938
- bundleByteSize: input.bundle?.byteSize ?? carriedBundle.bundleByteSize
33939
- };
33940
- }
33941
- function resolvePoolMax(env = process.env) {
33942
- const parsed = Number.parseInt(env.HASNA_SKILLS_DATABASE_POOL_MAX || env.SKILLS_DATABASE_POOL_MAX || "", 10);
33943
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 4;
33944
- }
33945
- var LOG_SEQUENCE_ATTEMPTS = 12;
33946
- function createArtifactId() {
33947
- return artifactId();
33948
- }
33949
- async function createStore(options = {}) {
33950
- const target = resolveDatabaseTarget(options.databaseUrl);
33951
- const store = instantiateStore(target, options.sqlite);
33952
- await store.verifyConnectivity?.();
33953
- if (options.bootstrapApiKey && store.ensureBootstrapApiKey) {
33954
- await store.ensureBootstrapApiKey(options.bootstrapApiKey);
33955
- }
33956
- return store;
33957
- }
33958
- function instantiateStore(target, sqliteOptions) {
33959
- switch (target.kind) {
33960
- case "postgres":
33961
- return new PostgresSkillsStore(target.url);
33962
- case "sqlite":
33963
- return new SqliteSkillsStore(target.path, sqliteOptions);
33964
- case "memory":
33965
- return new MemorySkillsStore;
33966
- }
33967
- }
34181
+ // src/lib/installer.ts
34182
+ import { existsSync as existsSync4, readFileSync as readFileSync4, rmSync } from "fs";
34183
+ import { dirname as dirname5, join as join7 } from "path";
34184
+ import { fileURLToPath as fileURLToPath2 } from "url";
33968
34185
 
33969
- class MemorySkillsStore {
33970
- backend = { kind: "memory", durable: false, label: "memory (non-durable)" };
33971
- apiKeys = new Map;
33972
- runs = new Map;
33973
- logs = new Map;
33974
- artifacts = new Map;
33975
- idempotency = new Map;
33976
- skills = new Map;
33977
- bundles = new Map;
33978
- pins = new Map;
33979
- constructor(apiKeys = []) {
33980
- for (const key of apiKeys)
33981
- this.addApiKey(key.token, key.principal);
33982
- }
33983
- addApiKey(token, principal) {
33984
- const resolved = publicPrincipal(principal);
33985
- this.apiKeys.set(hashApiKey(token), resolved);
33986
- return resolved;
33987
- }
33988
- async ensureBootstrapApiKey(token, principal) {
33989
- this.addApiKey(token, principal);
33990
- }
33991
- async authenticateApiKeyHash(hash) {
33992
- return this.apiKeys.get(hash) ?? null;
33993
- }
33994
- async createRun(input) {
33995
- const idemKey = input.idempotencyKey?.trim();
33996
- const idemMapKey = idemKey ? `${input.principal.orgId}:${idemKey}` : undefined;
33997
- if (idemMapKey) {
33998
- const existing = this.idempotency.get(idemMapKey);
33999
- if (existing)
34000
- return this.runs.get(existing);
34001
- }
34002
- const now = nowIso();
34003
- const run = {
34004
- id: runId(),
34005
- orgId: input.principal.orgId,
34006
- userId: input.principal.userId,
34007
- skill: input.slug,
34008
- requestedSlug: input.slug,
34009
- status: "queued",
34010
- input: input.input,
34011
- args: input.args,
34012
- ...idemKey ? { idempotencyKey: idemKey } : {},
34013
- correlationId: randomUUID3(),
34014
- costCents: 0,
34015
- leaseGeneration: 0,
34016
- createdAt: now
34017
- };
34018
- this.runs.set(run.id, run);
34019
- this.logs.set(run.id, []);
34020
- this.artifacts.set(run.id, []);
34021
- if (idemMapKey)
34022
- this.idempotency.set(idemMapKey, run.id);
34023
- return run;
34024
- }
34025
- async listRuns(principal, limit) {
34026
- return Array.from(this.runs.values()).filter((run) => run.orgId === principal.orgId).sort((a3, b3) => b3.createdAt.localeCompare(a3.createdAt)).slice(0, normalizeLimit(limit));
34027
- }
34028
- async getRun(principal, runId2) {
34029
- const run = this.runs.get(runId2);
34030
- return run && run.orgId === principal.orgId ? run : null;
34031
- }
34032
- async claimNextRun(_input) {
34033
- const run = Array.from(this.runs.values()).filter((candidate) => candidate.status === "queued" || candidate.status === "retrying").sort((a3, b3) => a3.createdAt.localeCompare(b3.createdAt))[0];
34034
- if (!run)
34035
- return null;
34036
- return this.patchRun(run.id, { status: "running", startedAt: run.startedAt ?? nowIso(), leaseGeneration: run.leaseGeneration + 1 });
34037
- }
34038
- async updateRun(runId2, patch) {
34039
- return this.patchRun(runId2, patch);
34040
- }
34041
- async transitionRun(runId2, patch, expectedGeneration) {
34042
- const run = this.runs.get(runId2);
34043
- if (!run)
34044
- return null;
34045
- if (run.leaseGeneration !== expectedGeneration) {
34046
- throw new StaleLeaseGenerationError(runId2, expectedGeneration, run.leaseGeneration, run.status);
34047
- }
34048
- return this.patchRun(runId2, patch);
34049
- }
34050
- async appendLog(runId2, orgId, level, message) {
34051
- const entries = this.logs.get(runId2) ?? [];
34052
- const log = { runId: runId2, sequence: entries.length + 1, level, message, createdAt: nowIso() };
34053
- entries.push(log);
34054
- this.logs.set(runId2, entries);
34055
- return log;
34056
- }
34057
- async listLogs(principal, runId2) {
34058
- const run = await this.getRun(principal, runId2);
34059
- return run ? [...this.logs.get(runId2) ?? []] : [];
34060
- }
34061
- async addArtifact(artifact) {
34062
- const next = { ...artifact, createdAt: nowIso() };
34063
- const artifacts = this.artifacts.get(artifact.runId) ?? [];
34064
- artifacts.push(next);
34065
- this.artifacts.set(artifact.runId, artifacts);
34066
- return next;
34067
- }
34068
- async listArtifacts(principal, runId2) {
34069
- const run = await this.getRun(principal, runId2);
34070
- return run ? [...this.artifacts.get(runId2) ?? []] : [];
34071
- }
34072
- async getArtifact(principal, runId2, id) {
34073
- const artifacts = await this.listArtifacts(principal, runId2);
34074
- return artifacts.find((artifact) => artifact.id === id) ?? null;
34075
- }
34076
- async publishSkill(input) {
34077
- const key = skillKey(input.principal.orgId, input.slug);
34078
- const now = nowIso();
34079
- const previous = this.skills.get(key);
34080
- if (previous && !previous.tombstonedAt && input.expectedRevisionId !== previous.revisionId) {
34081
- throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previous.revisionId);
34082
- }
34083
- if (input.bundle) {
34084
- const bundleMapKey = skillKey(input.principal.orgId, input.bundle.sha256);
34085
- this.bundles.set(bundleMapKey, {
34086
- ...this.bundles.get(bundleMapKey),
34087
- ...input.bundle,
34088
- orgId: input.principal.orgId,
34089
- createdAt: this.bundles.get(bundleMapKey)?.createdAt ?? now
34090
- });
34091
- }
34092
- const carriedBundle = !input.bundle && previous?.bundleSha256 ? { bundleSha256: previous.bundleSha256, ...previous.bundleByteSize === undefined ? {} : { bundleByteSize: previous.bundleByteSize } } : {};
34093
- const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : previous?.skillMd;
34094
- const record = {
34095
- orgId: input.principal.orgId,
34096
- slug: input.slug,
34097
- displayName: input.displayName,
34098
- description: input.description,
34099
- category: input.category,
34100
- tags: [...input.tags],
34101
- source: input.source,
34102
- kind: input.kind,
34103
- ...input.version ? { version: input.version } : {},
34104
- ...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
34105
- ...input.bundle ? { bundleSha256: input.bundle.sha256, bundleByteSize: input.bundle.byteSize } : carriedBundle,
34106
- publishedByUserId: input.principal.userId,
34107
- createdAt: previous?.createdAt ?? now,
34108
- updatedAt: now,
34109
- revisionId: revisionIdOfRecord(recordFieldsOf(input, carriedBundle, carriedSkillMd)),
34110
- revisionNumber: (previous?.revisionNumber ?? 0) + 1
34111
- };
34112
- this.skills.set(key, record);
34113
- if (previous?.bundleSha256 && input.bundle && previous.bundleSha256 !== input.bundle.sha256) {
34114
- this.collectOrphanBundle(input.principal.orgId, previous.bundleSha256);
34115
- }
34116
- return record;
34117
- }
34118
- async listSkills(principal) {
34119
- return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
34120
- }
34121
- async getSkill(principal, slug) {
34122
- const skill = this.skills.get(skillKey(principal.orgId, slug));
34123
- return skill && skill.orgId === principal.orgId ? skill : null;
34124
- }
34125
- async updateSkill(principal, slug, patch, expectedRevisionId) {
34126
- const current = await this.getSkill(principal, slug);
34127
- if (!current || current.tombstonedAt)
34128
- return null;
34129
- if (expectedRevisionId !== current.revisionId) {
34130
- throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
34131
- }
34132
- const latest = this.skills.get(skillKey(principal.orgId, slug));
34133
- if (!latest || latest.tombstonedAt)
34134
- return null;
34135
- if (latest.revisionId !== current.revisionId) {
34136
- throw new SkillRevisionConflictError(slug, expectedRevisionId, latest.revisionId);
34137
- }
34138
- const next = {
34139
- ...latest,
34140
- ...patch,
34141
- updatedAt: nowIso(),
34142
- revisionId: revisionIdOfRecord({ ...latest, ...patch }),
34143
- revisionNumber: latest.revisionNumber + 1
34144
- };
34145
- this.skills.set(skillKey(principal.orgId, slug), next);
34146
- return next;
34186
+ // src/lib/registry-data/development-tools.ts
34187
+ var DEVELOPMENT_TOOLS_SKILLS = [
34188
+ {
34189
+ name: "repo-onboarding-report",
34190
+ displayName: "Repo Onboarding Report",
34191
+ description: "Generate repository onboarding packages with architecture maps, setup guides, risk registers, and first-week plans",
34192
+ category: "Development Tools",
34193
+ kind: "instruction",
34194
+ tags: ["repository", "onboarding", "architecture", "developer-tools"]
34195
+ },
34196
+ {
34197
+ name: "security-audit-report",
34198
+ displayName: "Security Audit Report",
34199
+ description: "Generate application security hardening reports covering auth, secrets, headers, webhooks, RLS, permissions, dependencies, and prioritized fixes",
34200
+ category: "Development Tools",
34201
+ kind: "instruction",
34202
+ tags: ["security", "audit", "hardening", "rls", "webhooks"]
34203
+ },
34204
+ {
34205
+ name: "performance-audit-report",
34206
+ displayName: "Performance Audit Report",
34207
+ description: "Generate performance audit reports with metrics, findings, budgets, remediation plans, and manifest artifacts",
34208
+ category: "Development Tools",
34209
+ kind: "instruction",
34210
+ tags: ["performance", "audit", "latency", "budget", "web"]
34211
+ },
34212
+ {
34213
+ name: "migration-plan-pack",
34214
+ displayName: "Migration Plan Pack",
34215
+ description: "Generate migration plans for frameworks, libraries, databases, infrastructure, and architecture upgrades with risk matrix, checklist, rollout, and test strategy artifacts",
34216
+ category: "Development Tools",
34217
+ kind: "instruction",
34218
+ tags: ["migration", "upgrade", "planning", "frameworks", "databases"]
34219
+ },
34220
+ {
34221
+ name: "test-suite-generator",
34222
+ displayName: "Test Suite Generator",
34223
+ description: "Generate runnable API, unit, and browser test suite packages with coverage notes",
34224
+ category: "Development Tools",
34225
+ kind: "instruction",
34226
+ tags: ["testing", "qa", "api-tests", "browser-tests", "coverage"]
34227
+ },
34228
+ {
34229
+ name: "api-test-suite",
34230
+ displayName: "API Test Suite",
34231
+ description: "Generate and run API test suites with comprehensive endpoint coverage",
34232
+ category: "Development Tools",
34233
+ tags: ["api", "testing", "automation", "qa"]
34234
+ },
34235
+ {
34236
+ name: "api-docs-portal",
34237
+ displayName: "API Docs Portal",
34238
+ description: "Generate static API documentation portals from OpenAPI specs, route lists, and endpoint examples",
34239
+ category: "Development Tools",
34240
+ tags: ["api", "documentation", "openapi", "portal"]
34241
+ },
34242
+ {
34243
+ name: "codefix",
34244
+ displayName: "Code Fix",
34245
+ description: "Code quality CLI for auto-linting, formatting, fixing, and style enforcement",
34246
+ category: "Development Tools",
34247
+ tags: ["code", "linting", "formatting", "quality"]
34248
+ },
34249
+ {
34250
+ name: "commitpush",
34251
+ displayName: "Commit Push",
34252
+ description: "Create logical commits from repo changes and push directly to the main branch",
34253
+ category: "Development Tools",
34254
+ tags: ["git", "commit", "push", "automation"]
34255
+ },
34256
+ {
34257
+ name: "commitpushpr",
34258
+ displayName: "Commit Push PR",
34259
+ description: "Create logical commits, push a feature branch, and open a GitHub pull request",
34260
+ category: "Development Tools",
34261
+ dependencies: ["commitpush"],
34262
+ tags: ["git", "commit", "pull-request", "github", "automation"]
34263
+ },
34264
+ {
34265
+ name: "database-explorer",
34266
+ displayName: "Database Explorer",
34267
+ description: "Explore and query databases with an interactive interface",
34268
+ category: "Development Tools",
34269
+ tags: ["database", "explorer", "sql", "query"]
34270
+ },
34271
+ {
34272
+ name: "diff-viewer",
34273
+ displayName: "Diff Viewer",
34274
+ description: "View and analyze file differences with visual diff representation",
34275
+ category: "Development Tools",
34276
+ tags: ["diff", "comparison", "files", "code-review"]
34277
+ },
34278
+ {
34279
+ name: "generate-api-client",
34280
+ displayName: "Generate API Client",
34281
+ description: "Generate API client libraries from OpenAPI specs and documentation",
34282
+ category: "Development Tools",
34283
+ tags: ["api", "client", "code-generation", "openapi"]
34284
+ },
34285
+ {
34286
+ name: "generate-dockerfile",
34287
+ displayName: "Generate Dockerfile",
34288
+ description: "Generate optimized Dockerfiles for containerized applications",
34289
+ category: "Development Tools",
34290
+ tags: ["docker", "dockerfile", "containers", "devops"]
34291
+ },
34292
+ {
34293
+ name: "generate-env",
34294
+ displayName: "Generate Env",
34295
+ description: "Generate environment variable files from templates and configurations",
34296
+ category: "Development Tools",
34297
+ tags: ["env", "environment", "configuration", "dotenv"]
34298
+ },
34299
+ {
34300
+ name: "generate-sitemap",
34301
+ displayName: "Generate Sitemap",
34302
+ description: "Generate XML sitemaps for websites and web applications",
34303
+ category: "Development Tools",
34304
+ tags: ["sitemap", "seo", "xml", "web"]
34305
+ },
34306
+ {
34307
+ name: "hook",
34308
+ displayName: "Hook",
34309
+ description: "Claude Code hook creation skill - generates standardized hook scaffolds",
34310
+ category: "Development Tools",
34311
+ tags: ["hooks", "scaffold", "claude-code", "automation"]
34312
+ },
34313
+ {
34314
+ name: "http-server",
34315
+ displayName: "HTTP Server",
34316
+ description: "Spin up local HTTP servers for development and testing",
34317
+ category: "Development Tools",
34318
+ tags: ["http", "server", "development", "local"]
34319
+ },
34320
+ {
34321
+ name: "lorem-generator",
34322
+ displayName: "Lorem Generator",
34323
+ description: "Generate placeholder text in various styles and lengths",
34324
+ category: "Development Tools",
34325
+ tags: ["lorem", "placeholder", "text", "mockup"]
34326
+ },
34327
+ {
34328
+ name: "managemcp",
34329
+ displayName: "Manage MCP",
34330
+ description: "Manage MCP servers with install, configure, and lifecycle operations",
34331
+ category: "Development Tools",
34332
+ tags: ["mcp", "management", "servers", "configuration"]
34333
+ },
34334
+ {
34335
+ name: "markdown-validator",
34336
+ displayName: "Markdown Validator",
34337
+ description: "Validate markdown files for syntax, links, and formatting issues",
34338
+ category: "Development Tools",
34339
+ tags: ["markdown", "validation", "linting", "formatting"]
34340
+ },
34341
+ {
34342
+ name: "monitor",
34343
+ displayName: "Monitor",
34344
+ description: "Operate the monitor MCP for machine health, processes, cron jobs, and cleanup workflows",
34345
+ category: "Development Tools",
34346
+ tags: ["monitoring", "mcp", "processes", "operations"]
34347
+ },
34348
+ {
34349
+ name: "regex-tester",
34350
+ displayName: "Regex Tester",
34351
+ description: "Test and validate regular expressions with sample inputs",
34352
+ category: "Development Tools",
34353
+ tags: ["regex", "testing", "validation", "patterns"]
34354
+ },
34355
+ {
34356
+ name: "scancommitpr",
34357
+ displayName: "Scan Commit PR",
34358
+ description: "Scan repo changes, group into logical commits, push, and optionally create a PR",
34359
+ category: "Development Tools",
34360
+ dependencies: ["scancommitpush"],
34361
+ tags: ["git", "commit", "push", "pull-request", "automation"]
34362
+ },
34363
+ {
34364
+ name: "scancommitpush",
34365
+ displayName: "Scan Commit Push",
34366
+ description: "Scan repo changes, group into logical commits with conventional messages, and push to GitHub",
34367
+ category: "Development Tools",
34368
+ tags: ["git", "commit", "push", "automation"]
34369
+ },
34370
+ {
34371
+ name: "security-audit",
34372
+ displayName: "Security Audit",
34373
+ description: "Perform security audits on codebases and infrastructure configurations",
34374
+ category: "Development Tools",
34375
+ tags: ["security", "audit", "vulnerabilities", "scanning"]
34376
+ },
34377
+ {
34378
+ name: "tmux-session",
34379
+ displayName: "Tmux Session",
34380
+ description: "Create and manage grouped tmux sessions with workspace-aware naming and window layout guidance",
34381
+ category: "Development Tools",
34382
+ tags: ["tmux", "terminal", "sessions", "workspace"]
34383
+ },
34384
+ {
34385
+ name: "validate-config",
34386
+ displayName: "Validate Config",
34387
+ description: "Validate configuration files for syntax and schema compliance",
34388
+ category: "Development Tools",
34389
+ tags: ["config", "validation", "schema", "linting"]
34390
+ },
34391
+ {
34392
+ name: "oss-app-two-backend-storage",
34393
+ displayName: "OSS App Two-Backend Storage",
34394
+ description: "Recipe for the Hasna two-backend storage contract: client transport + HTTP store, server PG/SQLite backend, pg-migrations + apply script, fail-closed URL-without-key, bun bins, contract manifest, Dockerfile",
34395
+ category: "Development Tools",
34396
+ tags: ["storage", "backend", "postgresql", "sqlite", "two-backend", "oss-app"],
34397
+ kind: "instruction"
34147
34398
  }
34148
- async deleteSkill(principal, slug, tombstoneWindowMs) {
34149
- const current = await this.getSkill(principal, slug);
34150
- if (!current)
34151
- return null;
34152
- if (!current.tombstonedAt) {
34153
- const tombstoned = nowIso();
34154
- const purgeAfter = new Date(Date.now() + tombstoneWindowMs).toISOString();
34155
- const next = { ...current, tombstonedAt: tombstoned, tombstonePurgeAfter: purgeAfter, updatedAt: tombstoned };
34156
- this.skills.set(skillKey(principal.orgId, slug), next);
34157
- return next;
34158
- }
34159
- return current;
34399
+ ];
34400
+
34401
+ // src/lib/registry-data/business-marketing.ts
34402
+ var BUSINESS_MARKETING_SKILLS = [
34403
+ {
34404
+ name: "customer-feedback-report",
34405
+ displayName: "Customer Feedback Report",
34406
+ description: "Generate customer feedback reports with clusters, sentiment, root causes, roadmap recommendations, evidence, and PDF/Markdown artifacts",
34407
+ category: "Business & Marketing",
34408
+ kind: "instruction",
34409
+ tags: ["feedback", "customer", "sentiment", "roadmap", "report"]
34410
+ },
34411
+ {
34412
+ name: "pitch-deck",
34413
+ displayName: "Pitch Deck",
34414
+ description: "Generate investor and sales deck packages with slides, speaker notes, design direction, PDF, and PPTX artifacts",
34415
+ category: "Business & Marketing",
34416
+ kind: "instruction",
34417
+ dependencies: ["market-research-report"],
34418
+ tags: ["deck", "presentation", "investor", "sales", "pptx"]
34419
+ },
34420
+ {
34421
+ name: "proposal-pack",
34422
+ displayName: "Proposal Pack",
34423
+ description: "Generate client proposal packages with proposal, statement of work, pricing, timeline, assumptions, cover email, and PDF/Markdown artifacts",
34424
+ category: "Business & Marketing",
34425
+ kind: "instruction",
34426
+ tags: ["proposal", "sow", "sales", "pricing", "timeline"]
34427
+ },
34428
+ {
34429
+ name: "seo-content-pack",
34430
+ displayName: "SEO Content Pack",
34431
+ description: "Generate SEO content packages with topic clusters, articles, metadata, internal links, FAQs, and publishing cadence",
34432
+ category: "Business & Marketing",
34433
+ kind: "instruction",
34434
+ tags: ["seo", "content", "articles", "metadata"]
34435
+ },
34436
+ {
34437
+ name: "landing-page-pack",
34438
+ displayName: "Landing Page Pack",
34439
+ description: "Generate landing page packages with conversion copy, wireframes, CTA maps, experiments, preview HTML, and implementation notes",
34440
+ category: "Business & Marketing",
34441
+ kind: "instruction",
34442
+ tags: ["landing-page", "sales", "copywriting", "conversion"]
34443
+ },
34444
+ {
34445
+ name: "ad-creative-pack",
34446
+ displayName: "Ad Creative Pack",
34447
+ description: "Generate paid ad packages with platform copy, creative concepts, image prompts, audience angles, and test matrices",
34448
+ category: "Business & Marketing",
34449
+ kind: "instruction",
34450
+ tags: ["ads", "creative", "marketing", "copywriting"]
34451
+ },
34452
+ {
34453
+ name: "email-sequence",
34454
+ displayName: "Email Sequence",
34455
+ description: "Generate email campaign packages with subject lines, preview text, body copy, segmentation notes, CTA variants, HTML emails, and send plans",
34456
+ category: "Business & Marketing",
34457
+ kind: "instruction",
34458
+ tags: ["email", "marketing", "campaign", "copywriting"]
34459
+ },
34460
+ {
34461
+ name: "social-content-calendar",
34462
+ displayName: "Social Content Calendar",
34463
+ description: "Generate social content calendars with daily posts, channel strategy, asset briefs, hooks, publishing schedules, and repurposing maps",
34464
+ category: "Business & Marketing",
34465
+ kind: "instruction",
34466
+ tags: ["social", "content", "calendar", "marketing"]
34467
+ },
34468
+ {
34469
+ name: "one-page-website",
34470
+ displayName: "One Page Website",
34471
+ description: "Generate static one-page website bundles with HTML, CSS, JavaScript, copy, section maps, deploy notes, and manifest",
34472
+ category: "Business & Marketing",
34473
+ tags: ["website", "landing-page", "static-site", "html"]
34160
34474
  }
34161
- async purgeExpiredTombstones(principal) {
34162
- const now = nowIso();
34163
- const purged = [];
34164
- for (const [key, skill] of this.skills) {
34165
- if (skill.orgId !== principal.orgId || !skill.tombstonedAt || !skill.tombstonePurgeAfter)
34166
- continue;
34167
- if (skill.tombstonePurgeAfter > now)
34168
- continue;
34169
- this.skills.delete(key);
34170
- if (skill.bundleSha256)
34171
- this.collectOrphanBundle(principal.orgId, skill.bundleSha256);
34172
- purged.push(skill);
34173
- }
34174
- return purged;
34475
+ ];
34476
+
34477
+ // src/lib/registry-data/productivity-organization.ts
34478
+ var PRODUCTIVITY_ORGANIZATION_SKILLS = [
34479
+ {
34480
+ name: "meeting-pack",
34481
+ displayName: "Meeting Pack",
34482
+ description: "Generate meeting artifact packages with summaries, decisions, action items, follow-up email, timeline, project export, and manifest",
34483
+ category: "Productivity & Organization",
34484
+ kind: "instruction",
34485
+ tags: ["meeting", "summary", "action-items", "decisions"]
34486
+ },
34487
+ {
34488
+ name: "file-organizer",
34489
+ displayName: "File Organizer",
34490
+ description: "Organize files into structured directories based on type, date, or content",
34491
+ category: "Productivity & Organization",
34492
+ tags: ["files", "organization", "sorting", "cleanup"]
34493
+ },
34494
+ {
34495
+ name: "folder-tree",
34496
+ displayName: "Folder Tree",
34497
+ description: "Generate and display folder tree structures for documentation",
34498
+ category: "Productivity & Organization",
34499
+ tags: ["folder", "tree", "structure", "visualization"]
34500
+ },
34501
+ {
34502
+ name: "form-filler",
34503
+ displayName: "Form Filler",
34504
+ description: "Automatically fill out web forms and document templates",
34505
+ category: "Productivity & Organization",
34506
+ tags: ["forms", "automation", "filling", "data-entry"]
34507
+ },
34508
+ {
34509
+ name: "merge-pdfs",
34510
+ displayName: "Merge PDFs",
34511
+ description: "Merge multiple PDF files into a single document",
34512
+ category: "Productivity & Organization",
34513
+ tags: ["pdf", "merge", "documents", "combining"]
34514
+ },
34515
+ {
34516
+ name: "split-pdf",
34517
+ displayName: "Split PDF",
34518
+ description: "Split PDF documents into separate pages or sections",
34519
+ category: "Productivity & Organization",
34520
+ tags: ["pdf", "split", "documents", "pages"]
34175
34521
  }
34176
- async getSkillBundle(principal, sha256) {
34177
- const bundle = this.bundles.get(skillKey(principal.orgId, sha256));
34178
- return bundle && bundle.orgId === principal.orgId ? bundle : null;
34522
+ ];
34523
+
34524
+ // src/lib/registry-data/project-management.ts
34525
+ var PROJECT_MANAGEMENT_SKILLS = [
34526
+ {
34527
+ name: "businessactivity",
34528
+ displayName: "Business Activity",
34529
+ description: "Business activity, workflow, and ownership management service",
34530
+ category: "Project Management",
34531
+ tags: ["business", "workflow", "activities", "management"]
34532
+ },
34533
+ {
34534
+ name: "implementation",
34535
+ displayName: "Implementation",
34536
+ description: "Create .implementation scaffold for project development tracking",
34537
+ category: "Project Management",
34538
+ tags: ["implementation", "tracking", "scaffold", "project"]
34539
+ },
34540
+ {
34541
+ name: "implementation-plan",
34542
+ displayName: "Implementation Plan",
34543
+ description: "Generate detailed implementation plans with phases and milestones",
34544
+ category: "Project Management",
34545
+ tags: ["implementation", "planning", "milestones", "phases"]
34546
+ },
34547
+ {
34548
+ name: "implementation-todo",
34549
+ displayName: "Implementation Todo",
34550
+ description: "Manage implementation task lists and todo items",
34551
+ category: "Project Management",
34552
+ tags: ["implementation", "todo", "tasks", "tracking"]
34553
+ },
34554
+ {
34555
+ name: "todos-plan",
34556
+ displayName: "Todos Plan",
34557
+ description: "Author, sync, route, and verify Todos plans using Todos CLI plan IDs as source of truth",
34558
+ category: "Project Management",
34559
+ tags: ["todos", "plans", "tasks", "verification", "workflow"]
34179
34560
  }
34180
- async pinSkill(principal, slug, metadata = {}) {
34181
- const pin = { orgId: principal.orgId, principal: principal.apiKeyId, slug, pinnedAt: nowIso(), metadata: { ...metadata } };
34182
- this.pins.set(pinKey(principal.orgId, principal.apiKeyId, slug), pin);
34183
- return pin;
34561
+ ];
34562
+
34563
+ // src/lib/registry-data/content-generation.ts
34564
+ var CONTENT_GENERATION_SKILLS = [
34565
+ {
34566
+ name: "pdf-generate",
34567
+ displayName: "PDF Generate",
34568
+ description: "Generate PDF documents with rich formatting and layouts",
34569
+ category: "Content Generation",
34570
+ tags: ["pdf", "document", "generation", "formatting"]
34571
+ },
34572
+ {
34573
+ name: "slide-deck-generator",
34574
+ displayName: "Slide Deck Generator",
34575
+ description: "Generate slide decks from briefs, docs, or outlines with PDF, PPTX, speaker notes, and structured slide metadata",
34576
+ category: "Content Generation",
34577
+ tags: ["presentation", "slides", "deck", "documents"]
34578
+ },
34579
+ {
34580
+ name: "generate-qrcode",
34581
+ displayName: "Generate QR Code",
34582
+ description: "Generate QR codes with custom styling and embedded data",
34583
+ category: "Content Generation",
34584
+ tags: ["qrcode", "generation", "encoding", "visual"]
34585
+ },
34586
+ {
34587
+ name: "generate-resume",
34588
+ displayName: "Generate Resume",
34589
+ description: "Generate professional resumes with formatting and content optimization",
34590
+ category: "Content Generation",
34591
+ tags: ["resume", "cv", "career", "generation"]
34184
34592
  }
34185
- async unpinSkill(principal, slug) {
34186
- return this.pins.delete(pinKey(principal.orgId, principal.apiKeyId, slug));
34593
+ ];
34594
+
34595
+ // src/lib/registry-data/finance-compliance.ts
34596
+ var FINANCE_COMPLIANCE_SKILLS = [
34597
+ {
34598
+ name: "contract-review-report",
34599
+ displayName: "Contract Review Report",
34600
+ description: "Generate contract review reports with risk register, clause summary, redline suggestions, negotiation email, and manifest artifacts",
34601
+ category: "Finance & Compliance",
34602
+ kind: "instruction",
34603
+ tags: ["contract", "legal", "review", "risk"]
34604
+ },
34605
+ {
34606
+ name: "invoice",
34607
+ displayName: "Invoice",
34608
+ description: "Generate professional invoices with company management and PDF export",
34609
+ category: "Finance & Compliance",
34610
+ tags: ["invoice", "billing", "pdf", "finance"]
34611
+ },
34612
+ {
34613
+ name: "invoice-reconciliation",
34614
+ displayName: "Invoice Reconciliation",
34615
+ description: "Generate invoice reconciliation reports with matched payments, discrepancies, anomaly notes, summaries, and manifest artifacts",
34616
+ category: "Finance & Compliance",
34617
+ tags: ["invoice", "payments", "reconciliation", "finance"]
34187
34618
  }
34188
- async listPins(principal) {
34189
- return Array.from(this.pins.values()).filter((pin) => pin.orgId === principal.orgId && pin.principal === principal.apiKeyId).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
34619
+ ];
34620
+
34621
+ // src/lib/registry-data/data-analysis.ts
34622
+ var DATA_ANALYSIS_SKILLS = [
34623
+ {
34624
+ name: "analyze-data",
34625
+ displayName: "Analyze Data",
34626
+ description: "Data science insights for CSV and JSON datasets with statistical analysis",
34627
+ category: "Data & Analysis",
34628
+ tags: ["data", "analysis", "csv", "json", "statistics"]
34629
+ },
34630
+ {
34631
+ name: "dashboard-builder",
34632
+ displayName: "Dashboard Builder",
34633
+ description: "Build data dashboards with charts, metrics, and visualizations",
34634
+ category: "Data & Analysis",
34635
+ tags: ["dashboard", "visualization", "charts", "metrics"]
34636
+ },
34637
+ {
34638
+ name: "data-anonymizer",
34639
+ displayName: "Data Anonymizer",
34640
+ description: "Anonymize sensitive data in datasets for privacy compliance",
34641
+ category: "Data & Analysis",
34642
+ tags: ["anonymization", "privacy", "data", "compliance"]
34643
+ },
34644
+ {
34645
+ name: "generate-chart",
34646
+ displayName: "Generate Chart",
34647
+ description: "Generate data charts and visualizations from datasets",
34648
+ category: "Data & Analysis",
34649
+ tags: ["charts", "visualization", "data", "graphs"]
34650
+ },
34651
+ {
34652
+ name: "read-csv",
34653
+ displayName: "Read CSV",
34654
+ description: "Parse CSV files into structured JSON with delimiter and encoding detection",
34655
+ category: "Data & Analysis",
34656
+ tags: ["csv", "parsing", "tabular", "data"]
34657
+ },
34658
+ {
34659
+ name: "read-excel",
34660
+ displayName: "Read Excel",
34661
+ description: "Parse XLS and XLSX workbooks into structured JSON with sheet and formatted cell metadata",
34662
+ category: "Data & Analysis",
34663
+ tags: ["excel", "spreadsheet", "xlsx", "data"]
34664
+ },
34665
+ {
34666
+ name: "pdf-to-markdown",
34667
+ displayName: "PDF to Markdown",
34668
+ description: "Convert PDFs into clean markdown with remote extraction and structure cleanup",
34669
+ category: "Data & Analysis",
34670
+ tags: ["pdf", "markdown", "conversion"]
34671
+ },
34672
+ {
34673
+ name: "pdf-to-dataset",
34674
+ displayName: "PDF to Dataset",
34675
+ description: "Extract PDF tables, forms, invoices, and semi-structured content into CSV and JSON datasets",
34676
+ category: "Data & Analysis",
34677
+ tags: ["pdf", "dataset", "csv", "json", "extraction"]
34678
+ },
34679
+ {
34680
+ name: "doc-read",
34681
+ displayName: "Doc Read",
34682
+ description: "Read and extract text from DOCX files with section parsing and metadata extraction",
34683
+ category: "Data & Analysis",
34684
+ tags: ["docx", "reader", "extraction", "word"]
34190
34685
  }
34191
- async listTags(principal) {
34192
- await this.purgeExpiredTombstones(principal);
34193
- const tags = new Set;
34194
- for (const skill of this.skills.values()) {
34195
- if (skill.orgId !== principal.orgId)
34196
- continue;
34197
- for (const tag of skill.tags) {
34198
- if (tag.trim())
34199
- tags.add(tag);
34200
- }
34201
- }
34202
- return [...tags].sort();
34686
+ ];
34687
+
34688
+ // src/lib/registry-data/media-processing.ts
34689
+ var MEDIA_PROCESSING_SKILLS = [
34690
+ {
34691
+ name: "video-highlight-pack",
34692
+ displayName: "Video Highlight Pack",
34693
+ description: "Generate video highlight packages with clip plans, captions, thumbnail briefs, chapter markers, social posts, and edit decisions",
34694
+ category: "Media Processing",
34695
+ kind: "instruction",
34696
+ tags: ["video", "highlights", "clips", "captions"]
34697
+ },
34698
+ {
34699
+ name: "compress-video",
34700
+ displayName: "Compress Video",
34701
+ description: "Compress video files while preserving visual quality using ffmpeg",
34702
+ category: "Media Processing",
34703
+ tags: ["video", "compression", "ffmpeg", "optimization"]
34704
+ },
34705
+ {
34706
+ name: "audio-extract",
34707
+ displayName: "Audio Extract",
34708
+ description: "Extract audio tracks from video files with multiple format support",
34709
+ category: "Media Processing",
34710
+ tags: ["audio", "extraction", "video", "conversion"]
34711
+ },
34712
+ {
34713
+ name: "extract-frames",
34714
+ displayName: "Extract Frames",
34715
+ description: "Extract frames from video files at specified intervals or timestamps",
34716
+ category: "Media Processing",
34717
+ tags: ["frames", "video", "extraction", "images"]
34718
+ },
34719
+ {
34720
+ name: "gif-maker",
34721
+ displayName: "GIF Maker",
34722
+ description: "Create animated GIFs from images, videos, or screen recordings",
34723
+ category: "Media Processing",
34724
+ tags: ["gif", "animation", "images", "video"]
34725
+ },
34726
+ {
34727
+ name: "video-downloader",
34728
+ displayName: "Video Downloader",
34729
+ description: "Download videos from various online platforms and services",
34730
+ category: "Media Processing",
34731
+ tags: ["video", "download", "platforms", "media"]
34732
+ },
34733
+ {
34734
+ name: "watermark",
34735
+ displayName: "Watermark",
34736
+ description: "Add watermarks to images and documents for copyright protection",
34737
+ category: "Media Processing",
34738
+ tags: ["watermark", "protection", "copyright", "images"]
34203
34739
  }
34204
- async listSkillsByTag(principal, tag) {
34205
- await this.purgeExpiredTombstones(principal);
34206
- return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt && skill.tags.includes(tag)).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
34740
+ ];
34741
+
34742
+ // src/lib/registry-data/design-branding.ts
34743
+ var DESIGN_BRANDING_SKILLS = [
34744
+ {
34745
+ name: "brand-kit",
34746
+ displayName: "Brand Kit",
34747
+ description: "Generate brand kits with logo usage, palette, typography, brand voice, sample applications, Markdown guide, PDF guide, and SVG assets",
34748
+ category: "Design & Branding",
34749
+ kind: "instruction",
34750
+ tags: ["brand", "design", "palette", "typography"]
34751
+ },
34752
+ {
34753
+ name: "brand-assets",
34754
+ displayName: "Brand Assets",
34755
+ description: "Fetch official brand assets from a website or brand name with logos, PNG sizes, palette, typography, source metadata, and manifests",
34756
+ category: "Design & Branding",
34757
+ tags: ["brand", "logo", "assets", "palette", "typography"]
34758
+ },
34759
+ {
34760
+ name: "logo-design",
34761
+ displayName: "Logo Design",
34762
+ description: "Generate multi-variant logo packages with transparent PNGs, vector-style SVGs, usage notes, and manifests",
34763
+ category: "Design & Branding",
34764
+ tags: ["logo", "design", "branding", "identity"]
34765
+ },
34766
+ {
34767
+ name: "generate-favicon",
34768
+ displayName: "Generate Favicon",
34769
+ description: "Generate favicons in multiple sizes and formats for websites",
34770
+ category: "Design & Branding",
34771
+ tags: ["favicon", "icon", "design", "web"]
34772
+ },
34773
+ {
34774
+ name: "product-mockup",
34775
+ displayName: "Product Mockup",
34776
+ description: "Generate product mockup packages with visual variants, prompts, usage notes, and asset metadata",
34777
+ category: "Design & Branding",
34778
+ tags: ["product", "mockup", "visualization", "marketing"]
34779
+ },
34780
+ {
34781
+ name: "siteanalyze",
34782
+ displayName: "Site Analyze",
34783
+ description: "Analyze any website's design system \u2014 detects shadcn/ui, Tailwind, extracts colors, typography, and components via Playwright + Claude Vision.",
34784
+ category: "Design & Branding",
34785
+ tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "styles"]
34207
34786
  }
34208
- async listPinsByTag(principal, tag) {
34209
- await this.purgeExpiredTombstones(principal);
34210
- const taggedSlugs = new Set;
34211
- for (const skill of this.skills.values()) {
34212
- if (skill.orgId === principal.orgId && !skill.tombstonedAt && skill.tags.includes(tag))
34213
- taggedSlugs.add(skill.slug);
34214
- }
34215
- return Array.from(this.pins.values()).filter((pin) => pin.orgId === principal.orgId && pin.principal === principal.apiKeyId && taggedSlugs.has(pin.slug)).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
34787
+ ];
34788
+
34789
+ // src/lib/registry-data/web-browser.ts
34790
+ var WEB_BROWSER_SKILLS = [
34791
+ {
34792
+ name: "domainpurchase",
34793
+ displayName: "Domain Purchase",
34794
+ description: "Purchase and manage domains via registrar connectors",
34795
+ category: "Web & Browser",
34796
+ tags: ["domain", "purchase", "registrar", "management"]
34216
34797
  }
34217
- async listPublishedSlugs(principal) {
34218
- return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt).map((skill) => skill.slug).sort();
34798
+ ];
34799
+
34800
+ // src/lib/registry-data/research-writing.ts
34801
+ var RESEARCH_WRITING_SKILLS = [
34802
+ {
34803
+ name: "blog-article",
34804
+ displayName: "Blog Article",
34805
+ description: "Create SEO-optimized blog article artifact packages",
34806
+ category: "Research & Writing",
34807
+ kind: "instruction",
34808
+ tags: ["blog", "article", "writing", "seo"]
34809
+ },
34810
+ {
34811
+ name: "market-research-report",
34812
+ displayName: "Market Research Report",
34813
+ description: "Generate market research packages with competitor tables, positioning, pricing notes, source notes, and PDF/Markdown artifacts",
34814
+ category: "Research & Writing",
34815
+ kind: "instruction",
34816
+ tags: ["market-research", "competitors", "positioning", "pricing", "report"]
34219
34817
  }
34220
- collectOrphanBundle(orgId, sha256) {
34221
- const referenced = Array.from(this.skills.values()).some((skill) => skill.orgId === orgId && skill.bundleSha256 === sha256);
34222
- if (!referenced)
34223
- this.bundles.delete(skillKey(orgId, sha256));
34818
+ ];
34819
+
34820
+ // src/lib/registry-data/science-academic.ts
34821
+ var SCIENCE_ACADEMIC_SKILLS = [
34822
+ {
34823
+ name: "bio-sequence-tool",
34824
+ displayName: "Bio Sequence Tool",
34825
+ description: "Analyze and manipulate biological sequences including DNA, RNA, and protein data",
34826
+ category: "Science & Academic",
34827
+ tags: ["biology", "dna", "sequence", "bioinformatics"]
34828
+ },
34829
+ {
34830
+ name: "experiment-power-calculator",
34831
+ displayName: "Experiment Power Calculator",
34832
+ description: "Calculate statistical power and sample size for experiments",
34833
+ category: "Science & Academic",
34834
+ tags: ["experiment", "statistics", "power-analysis", "sample-size"]
34835
+ },
34836
+ {
34837
+ name: "latex-table-generator",
34838
+ displayName: "LaTeX Table Generator",
34839
+ description: "Generate formatted LaTeX tables from data for academic papers",
34840
+ category: "Science & Academic",
34841
+ tags: ["latex", "tables", "academic", "formatting"]
34842
+ },
34843
+ {
34844
+ name: "scientific-figure-check",
34845
+ displayName: "Scientific Figure Check",
34846
+ description: "Validate scientific figures for accuracy, formatting, and publication standards",
34847
+ category: "Science & Academic",
34848
+ tags: ["scientific", "figures", "validation", "publishing"]
34849
+ },
34850
+ {
34851
+ name: "statistical-test-selector",
34852
+ displayName: "Statistical Test Selector",
34853
+ description: "Recommend appropriate statistical tests based on data and research questions",
34854
+ category: "Science & Academic",
34855
+ tags: ["statistics", "test-selection", "research", "analysis"]
34224
34856
  }
34225
- patchRun(runId2, patch) {
34226
- const run = this.runs.get(runId2);
34227
- if (!run)
34228
- return null;
34229
- const next = { ...run, ...patch };
34230
- this.runs.set(runId2, next);
34231
- return next;
34857
+ ];
34858
+
34859
+ // src/lib/registry-data/education-learning.ts
34860
+ var EDUCATION_LEARNING_SKILLS = [];
34861
+
34862
+ // src/lib/registry-data/communication.ts
34863
+ var COMMUNICATION_SKILLS = [];
34864
+
34865
+ // src/lib/registry-data/health-wellness.ts
34866
+ var HEALTH_WELLNESS_SKILLS = [];
34867
+
34868
+ // src/lib/registry-data/travel-lifestyle.ts
34869
+ var TRAVEL_LIFESTYLE_SKILLS = [];
34870
+
34871
+ // src/lib/registry-data/event-management.ts
34872
+ var EVENT_MANAGEMENT_SKILLS = [];
34873
+
34874
+ // src/lib/registry-data/index.ts
34875
+ var SKILLS = [
34876
+ ...DEVELOPMENT_TOOLS_SKILLS,
34877
+ ...BUSINESS_MARKETING_SKILLS,
34878
+ ...PRODUCTIVITY_ORGANIZATION_SKILLS,
34879
+ ...PROJECT_MANAGEMENT_SKILLS,
34880
+ ...CONTENT_GENERATION_SKILLS,
34881
+ ...FINANCE_COMPLIANCE_SKILLS,
34882
+ ...DATA_ANALYSIS_SKILLS,
34883
+ ...MEDIA_PROCESSING_SKILLS,
34884
+ ...DESIGN_BRANDING_SKILLS,
34885
+ ...WEB_BROWSER_SKILLS,
34886
+ ...RESEARCH_WRITING_SKILLS,
34887
+ ...SCIENCE_ACADEMIC_SKILLS,
34888
+ ...EDUCATION_LEARNING_SKILLS,
34889
+ ...COMMUNICATION_SKILLS,
34890
+ ...HEALTH_WELLNESS_SKILLS,
34891
+ ...TRAVEL_LIFESTYLE_SKILLS,
34892
+ ...EVENT_MANAGEMENT_SKILLS
34893
+ ];
34894
+
34895
+ // src/lib/hosted-skill-set.ts
34896
+ var HOSTED_RUNTIMES = new Set(["hosted"]);
34897
+ var HOSTED_SOURCES = new Set(["remote", "private-hosted"]);
34898
+ var HOSTED_METADATA_SET_EMPTY_ERROR = [
34899
+ "The hosted metadata skill set is empty, but the packaging guards that depend on it",
34900
+ "only mean anything while it is non-empty: an empty set makes every one of them pass",
34901
+ "vacuously and silently removes the protection that keeps hosted implementation source",
34902
+ "out of the published package.",
34903
+ "",
34904
+ "If emptying the set is intentional (every hosted skill was deleted or converted to a",
34905
+ "runnable in-repo skill), retire the derived guards in the SAME change \u2014 do not let them",
34906
+ "stay green and empty."
34907
+ ].join(`
34908
+ `);
34909
+ var SLUG = "[^,{}/]+";
34910
+ var BRACE_SOURCE_EXCLUSION = new RegExp(`^!skills/\\{(${SLUG}(?:,${SLUG})+)\\}/src$`);
34911
+ var SINGLE_SOURCE_EXCLUSION = new RegExp(`^!skills/(${SLUG})/src$`);
34912
+
34913
+ // src/lib/skill-validation.ts
34914
+ var RESERVED_SKILL_ENTRIES = new Set([
34915
+ ".env",
34916
+ ".npmrc",
34917
+ ".pypirc",
34918
+ ".netrc",
34919
+ "id_rsa",
34920
+ "id_ed25519"
34921
+ ]);
34922
+ var KNOWN_TOP_LEVEL_ENTRIES = new Set([
34923
+ ".claude",
34924
+ ".env.example",
34925
+ ".gitignore",
34926
+ ".skills",
34927
+ "CLAUDE.md",
34928
+ "AGENTS.md",
34929
+ "LICENSE",
34930
+ "PROJECT_OVERVIEW.md",
34931
+ "QUICKSTART.md",
34932
+ "README.md",
34933
+ "SKILL.md",
34934
+ "api-docs-list.json",
34935
+ "auth.ts",
34936
+ "bun.lock",
34937
+ "bunfig.toml",
34938
+ "data",
34939
+ "dist",
34940
+ "examples",
34941
+ "exports",
34942
+ "http-client.ts",
34943
+ "index.ts",
34944
+ "install.sh",
34945
+ "installer.ts",
34946
+ "logs",
34947
+ "node_modules",
34948
+ "package.json",
34949
+ "scripts",
34950
+ "skill-install.ts",
34951
+ "skill.json",
34952
+ "src",
34953
+ "tests",
34954
+ "references",
34955
+ "assets",
34956
+ "tsconfig.json",
34957
+ "vision.ts"
34958
+ ]);
34959
+ var VALID_PROVENANCE_SOURCES = new Set(["official", "custom", "remote", "private", "private-hosted", "upstream", "extension"]);
34960
+
34961
+ // src/lib/skill-hash.ts
34962
+ var HASH_EXCLUDE_DIRS = new Set([".git", "node_modules", "dist", "build", ".turbo"]);
34963
+
34964
+ // src/lib/portable-skills-files.ts
34965
+ var ANY_SEGMENT_COPY_EXCLUDES = new Set([
34966
+ ".git",
34967
+ ".DS_Store",
34968
+ ".system",
34969
+ "node_modules"
34970
+ ]);
34971
+ var FIRST_SEGMENT_COPY_EXCLUDES = new Set([
34972
+ "dist",
34973
+ "build",
34974
+ ".turbo"
34975
+ ]);
34976
+ // src/lib/portable-skills.ts
34977
+ var OFFICIAL_SKILL_NAMES = new Set(SKILLS.map((skill) => skill.name));
34978
+ // src/lib/installer.ts
34979
+ var __dirname2 = dirname5(fileURLToPath2(import.meta.url));
34980
+ function findSkillsDir() {
34981
+ let dir = __dirname2;
34982
+ for (let i3 = 0;i3 < 5; i3++) {
34983
+ const candidate = join7(dir, "skills");
34984
+ if (existsSync4(candidate) && !dir.includes(".skills"))
34985
+ return candidate;
34986
+ dir = dirname5(dir);
34232
34987
  }
34988
+ return join7(__dirname2, "..", "skills");
34233
34989
  }
34234
- function skillKey(orgId, slug) {
34235
- return `${orgId.length}:${orgId}:${slug}`;
34990
+ var SKILLS_DIR = findSkillsDir();
34991
+
34992
+ // src/server/skills-api.ts
34993
+ var MAX_SKILL_MD_BYTES = 512000;
34994
+ var MAX_MANIFEST_BYTES = MAX_SKILL_MD_BYTES + 64000;
34995
+ var ALLOWED_PUBLISH_PARTS = new Set(["manifest", "bundle"]);
34996
+ var MAX_VERSION_MANIFEST_BYTES = 512 * 1024;
34997
+
34998
+ // src/server/config.ts
34999
+ var DATABASE_URL_ENV = "HASNA_SKILLS_DATABASE_URL";
35000
+ var SKILLS_ENV_NAMESPACE = "SKILLS";
35001
+ function resolveServerConfig(env = process.env) {
35002
+ assertNoRetiredModeEnvVars(env, {
35003
+ app: SKILLS_ENV_NAMESPACE,
35004
+ replacement: DATABASE_URL_ENV
35005
+ });
35006
+ const nodeEnv = env.NODE_ENV || "development";
35007
+ const host = env.HOST || env.SKILLS_HOST || "0.0.0.0";
35008
+ const port = parsePositiveInt(env.PORT || env.SKILLS_PORT, 8787);
35009
+ return {
35010
+ host,
35011
+ port,
35012
+ databaseUrl: env[DATABASE_URL_ENV] || env.DATABASE_URL || undefined,
35013
+ bootstrapApiKey: env.HASNA_SKILLS_BOOTSTRAP_API_KEY || undefined,
35014
+ seedBundledCorpus: (env.HASNA_SKILLS_SEED_BUNDLED_CORPUS ?? "1") !== "0",
35015
+ artifactBucket: env.HASNA_SKILLS_S3_BUCKET || env.SKILLS_S3_BUCKET || undefined,
35016
+ artifactPrefix: normalizePrefix(env.HASNA_SKILLS_S3_PREFIX || env.SKILLS_S3_PREFIX || "skills/artifacts"),
35017
+ inlineWorker: env.HASNA_SKILLS_INLINE_WORKER === "1",
35018
+ bundleSigningKey: env.HASNA_SKILLS_API_SIGNING_KEY || env.HASNA_SKILLS_SIGNING_KEY || undefined,
35019
+ requestBodyLimitBytes: parsePositiveInt(env.HASNA_SKILLS_REQUEST_BODY_LIMIT_BYTES, 1e6),
35020
+ skillBundleLimitBytes: parsePositiveInt(env.HASNA_SKILLS_BUNDLE_LIMIT_BYTES, 25000000),
35021
+ tombstoneWindowMs: parsePositiveInt(env.HASNA_SKILLS_TOMBSTONE_WINDOW_MS, 7 * 24 * 60 * 60 * 1000),
35022
+ publicBaseUrl: (env.SKILLS_PUBLIC_BASE_URL || localOrigin(host, port)).replace(/\/+$/, ""),
35023
+ nodeEnv,
35024
+ allowEphemeralStore: env.HASNA_SKILLS_ALLOW_EPHEMERAL_STORE === "1"
35025
+ };
34236
35026
  }
34237
- function pinKey(orgId, principal, slug) {
34238
- return `${orgId.length}:${orgId}:${principal.length}:${principal}:${slug}`;
35027
+ function localOrigin(host, port) {
35028
+ const hostname = host === "0.0.0.0" || host === "::" ? "localhost" : host;
35029
+ return `http://${hostname.includes(":") ? `[${hostname}]` : hostname}:${port}`;
35030
+ }
35031
+ function parsePositiveInt(value, fallback) {
35032
+ const parsed = Number.parseInt(value ?? "", 10);
35033
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
35034
+ }
35035
+ function normalizePrefix(value) {
35036
+ return value.replace(/^\/+|\/+$/g, "") || "skills/artifacts";
34239
35037
  }
34240
35038
 
34241
- class PostgresSkillsStore {
34242
- backend = { kind: "postgres", durable: true, label: "postgres" };
34243
- sql;
34244
- constructor(databaseUrl) {
34245
- const bunWithSql = Bun;
34246
- this.sql = new bunWithSql.SQL(databaseUrl, { max: resolvePoolMax() });
34247
- }
34248
- async verifyConnectivity() {
34249
- try {
34250
- await this.sql`SELECT 1`;
34251
- } catch (error) {
34252
- throw new Error("cannot reach the configured Postgres database. The server will not start with an unreachable " + "database rather than fall back to another backend and silently split your data across two stores. " + `Check HASNA_SKILLS_DATABASE_URL and that the instance is accepting connections. Driver reported: ${connectionFailureSummary(error)}`);
34253
- }
34254
- try {
34255
- await this.sql`SELECT 1 FROM api_keys LIMIT 0`;
34256
- } catch (error) {
34257
- throw new Error("the configured Postgres database is reachable but has no skills schema. Run `skills-migrate` against it " + "before starting the server - unlike SQLite, Postgres is not migrated automatically, so that several " + `replicas cannot race to migrate a shared database. Driver reported: ${connectionFailureSummary(error)}`);
34258
- }
34259
- await this.backfillLegacyRevisions();
35039
+ // src/server/handlers.ts
35040
+ import { createHash as createHash5 } from "crypto";
35041
+
35042
+ // src/server/store.ts
35043
+ import { randomUUID as randomUUID3 } from "crypto";
35044
+ function recordFieldsOf(input, carriedBundle, carriedSkillMd) {
35045
+ return {
35046
+ slug: input.slug,
35047
+ displayName: input.displayName,
35048
+ description: input.description,
35049
+ category: input.category,
35050
+ tags: input.tags,
35051
+ source: input.source,
35052
+ kind: input.kind,
35053
+ ...input.version ? { version: input.version } : {},
35054
+ ...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
35055
+ bundleSha256: input.bundle?.sha256 ?? carriedBundle.bundleSha256,
35056
+ bundleByteSize: input.bundle?.byteSize ?? carriedBundle.bundleByteSize
35057
+ };
35058
+ }
35059
+ function resolvePoolMax(env = process.env) {
35060
+ const parsed = Number.parseInt(env.HASNA_SKILLS_DATABASE_POOL_MAX || env.SKILLS_DATABASE_POOL_MAX || "", 10);
35061
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 4;
35062
+ }
35063
+ var LOG_SEQUENCE_ATTEMPTS = 12;
35064
+ function createArtifactId() {
35065
+ return artifactId();
35066
+ }
35067
+ async function createStore(options = {}) {
35068
+ const target = resolveDatabaseTarget(options.databaseUrl);
35069
+ const store = instantiateStore(target, options.sqlite);
35070
+ await store.verifyConnectivity?.();
35071
+ if (options.bootstrapApiKey && store.ensureBootstrapApiKey) {
35072
+ await store.ensureBootstrapApiKey(options.bootstrapApiKey);
34260
35073
  }
34261
- async backfillLegacyRevisions() {
34262
- const rows = await this.sql`SELECT * FROM skills_registry WHERE revision_id = ${""}`;
34263
- for (const row of rows) {
34264
- const record = rowToSkill(row);
34265
- await this.sql`UPDATE skills_registry SET revision_id = ${revisionIdOfRecord(record)} WHERE org_id = ${record.orgId} AND slug = ${record.slug}`;
34266
- }
35074
+ return store;
35075
+ }
35076
+ function instantiateStore(target, sqliteOptions) {
35077
+ switch (target.kind) {
35078
+ case "postgres":
35079
+ return new PostgresSkillsStore(target.url);
35080
+ case "sqlite":
35081
+ return new SqliteSkillsStore(target.path, sqliteOptions);
35082
+ case "memory":
35083
+ return new MemorySkillsStore;
34267
35084
  }
34268
- async close() {
34269
- await this.sql.close?.();
35085
+ }
35086
+
35087
+ class MemorySkillsStore {
35088
+ backend = { kind: "memory", durable: false, label: "memory (non-durable)" };
35089
+ apiKeys = new Map;
35090
+ runs = new Map;
35091
+ logs = new Map;
35092
+ artifacts = new Map;
35093
+ idempotency = new Map;
35094
+ skills = new Map;
35095
+ bundles = new Map;
35096
+ versions = new Map;
35097
+ pins = new Map;
35098
+ constructor(apiKeys = []) {
35099
+ for (const key of apiKeys)
35100
+ this.addApiKey(key.token, key.principal);
34270
35101
  }
34271
- async ensureBootstrapApiKey(token, principal) {
35102
+ addApiKey(token, principal) {
34272
35103
  const resolved = publicPrincipal(principal);
34273
- await this.sql`
34274
- INSERT INTO organizations (id, slug, name)
34275
- VALUES (${resolved.orgId}, ${resolved.orgSlug}, ${resolved.orgName})
34276
- ON CONFLICT (id) DO UPDATE SET slug = EXCLUDED.slug, name = EXCLUDED.name
34277
- `;
34278
- await this.sql`
34279
- INSERT INTO users (id, email, name)
34280
- VALUES (${resolved.userId}, ${resolved.email}, ${resolved.email})
34281
- ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email
34282
- `;
34283
- await this.sql`
34284
- INSERT INTO organization_members (org_id, user_id, role)
34285
- VALUES (${resolved.orgId}, ${resolved.userId}, ${resolved.role})
34286
- ON CONFLICT (org_id, user_id) DO UPDATE SET role = EXCLUDED.role
34287
- `;
34288
- await this.sql`
34289
- INSERT INTO api_keys (id, org_id, user_id, name, key_hash, scopes_json)
34290
- VALUES (${resolved.apiKeyId}, ${resolved.orgId}, ${resolved.userId}, ${"bootstrap"}, ${hashApiKey(token)}, ${JSON.stringify(resolved.scopes)}::jsonb)
34291
- ON CONFLICT (key_hash) DO NOTHING
34292
- `;
35104
+ this.apiKeys.set(hashApiKey(token), resolved);
35105
+ return resolved;
34293
35106
  }
34294
- async authenticateApiKeyHash(hash) {
34295
- const rows = await this.sql`
34296
- SELECT k.id AS api_key_id, k.scopes_json, o.id AS org_id, o.slug AS org_slug, o.name AS org_name,
34297
- u.id AS user_id, u.email, m.role
34298
- FROM api_keys k
34299
- JOIN organizations o ON o.id = k.org_id
34300
- JOIN users u ON u.id = k.user_id
34301
- LEFT JOIN organization_members m ON m.org_id = k.org_id AND m.user_id = k.user_id
34302
- WHERE k.key_hash = ${hash} AND k.revoked_at IS NULL
34303
- LIMIT 1
34304
- `;
34305
- const row = rows[0];
34306
- if (!row)
34307
- return null;
34308
- await this.sql`UPDATE api_keys SET last_used_at = now() WHERE id = ${String(row.api_key_id)}`;
34309
- return {
34310
- apiKeyId: String(row.api_key_id),
34311
- orgId: String(row.org_id),
34312
- orgSlug: String(row.org_slug),
34313
- orgName: String(row.org_name),
34314
- userId: String(row.user_id),
34315
- email: String(row.email),
34316
- role: typeof row.role === "string" ? row.role : "member",
34317
- scopes: parseJsonArray(row.scopes_json)
34318
- };
35107
+ async ensureBootstrapApiKey(token, principal) {
35108
+ this.addApiKey(token, principal);
34319
35109
  }
34320
- async withContext(orgId, worker, fn) {
34321
- return this.sql.begin(async (tx) => {
34322
- await tx`SELECT set_config('app.skills_org_id', ${orgId ?? ""}, true)`;
34323
- await tx`SELECT set_config('app.skills_claim_context', ${worker ? "worker" : ""}, true)`;
34324
- return await fn(tx);
34325
- });
35110
+ async authenticateApiKeyHash(hash) {
35111
+ return this.apiKeys.get(hash) ?? null;
34326
35112
  }
34327
35113
  async createRun(input) {
34328
- return this.withContext(input.principal.orgId, false, async (tx) => {
34329
- if (input.idempotencyKey) {
34330
- const existing = await tx`
34331
- SELECT * FROM skills_runs
34332
- WHERE org_id = ${input.principal.orgId} AND idempotency_key = ${input.idempotencyKey}
34333
- LIMIT 1
34334
- `;
34335
- if (existing[0])
34336
- return rowToRun(existing[0]);
34337
- }
34338
- const id = runId();
34339
- const rows = await tx`
34340
- INSERT INTO skills_runs (id, org_id, user_id, skill_slug, requested_slug, status, input_json, args_json, idempotency_key, correlation_id)
34341
- 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()})
34342
- RETURNING *
34343
- `;
34344
- return rowToRun(rows[0]);
34345
- });
35114
+ const idemKey = input.idempotencyKey?.trim();
35115
+ const idemMapKey = idemKey ? `${input.principal.orgId}:${idemKey}` : undefined;
35116
+ if (idemMapKey) {
35117
+ const existing = this.idempotency.get(idemMapKey);
35118
+ if (existing)
35119
+ return this.runs.get(existing);
35120
+ }
35121
+ const now = nowIso();
35122
+ const run = {
35123
+ id: runId(),
35124
+ orgId: input.principal.orgId,
35125
+ userId: input.principal.userId,
35126
+ skill: input.slug,
35127
+ requestedSlug: input.slug,
35128
+ status: "queued",
35129
+ input: input.input,
35130
+ args: input.args,
35131
+ ...idemKey ? { idempotencyKey: idemKey } : {},
35132
+ correlationId: randomUUID3(),
35133
+ costCents: 0,
35134
+ leaseGeneration: 0,
35135
+ createdAt: now
35136
+ };
35137
+ this.runs.set(run.id, run);
35138
+ this.logs.set(run.id, []);
35139
+ this.artifacts.set(run.id, []);
35140
+ if (idemMapKey)
35141
+ this.idempotency.set(idemMapKey, run.id);
35142
+ return run;
34346
35143
  }
34347
35144
  async listRuns(principal, limit) {
34348
- return this.withContext(principal.orgId, false, async (tx) => {
34349
- const rows = await tx`
34350
- SELECT * FROM skills_runs WHERE org_id = ${principal.orgId}
34351
- ORDER BY created_at DESC LIMIT ${normalizeLimit(limit)}
34352
- `;
34353
- return rows.map(rowToRun);
34354
- });
35145
+ return Array.from(this.runs.values()).filter((run) => run.orgId === principal.orgId).sort((a3, b3) => b3.createdAt.localeCompare(a3.createdAt)).slice(0, normalizeLimit(limit));
34355
35146
  }
34356
35147
  async getRun(principal, runId2) {
34357
- return this.withContext(principal.orgId, false, async (tx) => {
34358
- const rows = await tx`
34359
- SELECT * FROM skills_runs WHERE id = ${runId2} AND org_id = ${principal.orgId} LIMIT 1
34360
- `;
34361
- return rows[0] ? rowToRun(rows[0]) : null;
34362
- });
35148
+ const run = this.runs.get(runId2);
35149
+ return run && run.orgId === principal.orgId ? run : null;
34363
35150
  }
34364
- async claimNextRun(input) {
34365
- return this.withContext(null, true, async (tx) => {
34366
- const rows = await tx`
34367
- UPDATE skills_runs
34368
- SET status = ${"running"}, started_at = COALESCE(started_at, now()), locked_by = ${input.workerId}, locked_at = now(),
34369
- lease_generation = lease_generation + 1
34370
- WHERE id = (
34371
- SELECT id FROM skills_runs
34372
- WHERE status IN (${"queued"}, ${"retrying"})
34373
- ORDER BY created_at ASC
34374
- FOR UPDATE SKIP LOCKED
34375
- LIMIT 1
34376
- )
34377
- RETURNING *
34378
- `;
34379
- return rows[0] ? rowToRun(rows[0]) : null;
34380
- });
35151
+ async claimNextRun(_input) {
35152
+ const run = Array.from(this.runs.values()).filter((candidate) => candidate.status === "queued" || candidate.status === "retrying").sort((a3, b3) => a3.createdAt.localeCompare(b3.createdAt))[0];
35153
+ if (!run)
35154
+ return null;
35155
+ return this.patchRun(run.id, { status: "running", startedAt: run.startedAt ?? nowIso(), leaseGeneration: run.leaseGeneration + 1 });
34381
35156
  }
34382
35157
  async updateRun(runId2, patch) {
34383
- return this.withContext(null, true, async (tx) => {
34384
- const current = await tx`SELECT * FROM skills_runs WHERE id = ${runId2} LIMIT 1`;
34385
- if (!current[0])
34386
- return null;
34387
- const run = { ...rowToRun(current[0]), ...patch };
34388
- const rows = await tx`
34389
- UPDATE skills_runs
34390
- SET status = ${run.status},
34391
- output_type = ${run.outputType ?? null},
34392
- output_preview = ${run.outputPreview ?? null},
34393
- error_code = ${run.errorCode ?? null},
34394
- error_message = ${run.errorMessage ?? null},
34395
- started_at = ${run.startedAt ?? null},
34396
- completed_at = ${run.completedAt ?? null}
34397
- WHERE id = ${runId2}
34398
- RETURNING *
34399
- `;
34400
- return rows[0] ? rowToRun(rows[0]) : null;
34401
- });
35158
+ return this.patchRun(runId2, patch);
34402
35159
  }
34403
35160
  async transitionRun(runId2, patch, expectedGeneration) {
34404
- return this.withContext(null, true, async (tx) => {
34405
- const current = await tx`SELECT * FROM skills_runs WHERE id = ${runId2} LIMIT 1`;
34406
- if (!current[0])
34407
- return null;
34408
- const stored = rowToRun(current[0]);
34409
- if (stored.leaseGeneration !== expectedGeneration) {
34410
- throw new StaleLeaseGenerationError(runId2, expectedGeneration, stored.leaseGeneration, stored.status);
34411
- }
34412
- const run = { ...stored, ...patch };
34413
- const rows = await tx`
34414
- UPDATE skills_runs
34415
- SET status = ${run.status},
34416
- output_type = ${run.outputType ?? null},
34417
- output_preview = ${run.outputPreview ?? null},
34418
- error_code = ${run.errorCode ?? null},
34419
- error_message = ${run.errorMessage ?? null},
34420
- started_at = ${run.startedAt ?? null},
34421
- completed_at = ${run.completedAt ?? null},
34422
- lease_generation = ${run.leaseGeneration}
34423
- WHERE id = ${runId2} AND lease_generation = ${expectedGeneration}
34424
- RETURNING *
34425
- `;
34426
- return rows[0] ? rowToRun(rows[0]) : null;
34427
- });
35161
+ const run = this.runs.get(runId2);
35162
+ if (!run)
35163
+ return null;
35164
+ if (run.leaseGeneration !== expectedGeneration) {
35165
+ throw new StaleLeaseGenerationError(runId2, expectedGeneration, run.leaseGeneration, run.status);
35166
+ }
35167
+ return this.patchRun(runId2, patch);
34428
35168
  }
34429
35169
  async appendLog(runId2, orgId, level, message) {
34430
- let lastError;
34431
- for (let attempt = 0;attempt < LOG_SEQUENCE_ATTEMPTS; attempt += 1) {
34432
- try {
34433
- const rows = await this.withContext(orgId, true, async (tx) => {
34434
- return await tx`
34435
- INSERT INTO skills_run_logs (run_id, org_id, sequence, level, message)
34436
- VALUES (
34437
- ${runId2},
34438
- ${orgId},
34439
- (SELECT COALESCE(MAX(sequence), 0) + 1 FROM skills_run_logs WHERE run_id = ${runId2}),
34440
- ${level},
34441
- ${message}
34442
- )
34443
- RETURNING *
34444
- `;
34445
- });
34446
- return rowToLog(rows[0]);
34447
- } catch (error) {
34448
- if (!isUniqueViolation(error))
34449
- throw error;
34450
- lastError = error;
34451
- }
34452
- }
34453
- throw lastError;
35170
+ const entries = this.logs.get(runId2) ?? [];
35171
+ const log = { runId: runId2, sequence: entries.length + 1, level, message, createdAt: nowIso() };
35172
+ entries.push(log);
35173
+ this.logs.set(runId2, entries);
35174
+ return log;
34454
35175
  }
34455
35176
  async listLogs(principal, runId2) {
34456
- return this.withContext(principal.orgId, false, async (tx) => {
34457
- const rows = await tx`
34458
- SELECT l.* FROM skills_run_logs l
34459
- JOIN skills_runs r ON r.id = l.run_id AND r.org_id = ${principal.orgId}
34460
- WHERE l.run_id = ${runId2}
34461
- ORDER BY l.sequence ASC
34462
- `;
34463
- return rows.map(rowToLog);
34464
- });
35177
+ const run = await this.getRun(principal, runId2);
35178
+ return run ? [...this.logs.get(runId2) ?? []] : [];
34465
35179
  }
34466
35180
  async addArtifact(artifact) {
34467
- return this.withContext(artifact.orgId, true, async (tx) => {
34468
- const rows = await tx`
34469
- INSERT INTO skills_artifacts (id, run_id, org_id, file_name, relative_path, content_type, byte_size, sha256, storage_kind, storage_key, body_text, visibility, expires_at)
34470
- VALUES (${artifact.id}, ${artifact.runId}, ${artifact.orgId}, ${artifact.fileName}, ${artifact.relativePath}, ${artifact.contentType}, ${artifact.byteSize}, ${artifact.sha256}, ${artifact.storageKind}, ${artifact.storageKey ?? null}, ${artifact.bodyText ?? null}, ${artifact.visibility}, ${artifact.expiresAt ?? null})
34471
- RETURNING *
34472
- `;
34473
- return rowToArtifact(rows[0]);
34474
- });
35181
+ const next = { ...artifact, createdAt: nowIso() };
35182
+ const artifacts = this.artifacts.get(artifact.runId) ?? [];
35183
+ artifacts.push(next);
35184
+ this.artifacts.set(artifact.runId, artifacts);
35185
+ return next;
34475
35186
  }
34476
35187
  async listArtifacts(principal, runId2) {
34477
- return this.withContext(principal.orgId, false, async (tx) => {
34478
- const rows = await tx`
34479
- SELECT a.* FROM skills_artifacts a
34480
- JOIN skills_runs r ON r.id = a.run_id AND r.org_id = ${principal.orgId}
34481
- WHERE a.run_id = ${runId2}
34482
- ORDER BY a.created_at ASC
34483
- `;
34484
- return rows.map(rowToArtifact);
34485
- });
35188
+ const run = await this.getRun(principal, runId2);
35189
+ return run ? [...this.artifacts.get(runId2) ?? []] : [];
34486
35190
  }
34487
- async getArtifact(principal, runId2, artifactId2) {
34488
- return this.withContext(principal.orgId, false, async (tx) => {
34489
- const rows = await tx`
34490
- SELECT a.* FROM skills_artifacts a
34491
- JOIN skills_runs r ON r.id = a.run_id AND r.org_id = ${principal.orgId}
34492
- WHERE a.run_id = ${runId2} AND a.id = ${artifactId2}
34493
- LIMIT 1
34494
- `;
34495
- return rows[0] ? rowToArtifact(rows[0]) : null;
34496
- });
35191
+ async getArtifact(principal, runId2, id) {
35192
+ const artifacts = await this.listArtifacts(principal, runId2);
35193
+ return artifacts.find((artifact) => artifact.id === id) ?? null;
34497
35194
  }
34498
35195
  async publishSkill(input) {
34499
- const orgId = input.principal.orgId;
34500
- return await this.sql.begin(async (tx) => {
34501
- const previousRows = await tx`
34502
- SELECT revision_id, revision_number, bundle_sha256, bundle_byte_size, skill_md, tombstoned_at
34503
- FROM skills_registry WHERE org_id = ${orgId} AND slug = ${input.slug} LIMIT 1
34504
- `;
34505
- const previous = previousRows[0];
34506
- const previousSha = typeof previous?.bundle_sha256 === "string" ? String(previous.bundle_sha256) : null;
34507
- const previousRevisionId = typeof previous?.revision_id === "string" && previous.revision_id ? String(previous.revision_id) : null;
34508
- const tombstoned = previous?.tombstoned_at != null;
34509
- const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : typeof previous?.skill_md === "string" ? String(previous.skill_md) : null;
34510
- if (previous && !tombstoned && input.expectedRevisionId !== previousRevisionId) {
34511
- throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
35196
+ const key = skillKey(input.principal.orgId, input.slug);
35197
+ const now = nowIso();
35198
+ const previous = this.skills.get(key);
35199
+ if (previous && !previous.tombstonedAt && input.expectedRevisionId !== previous.revisionId) {
35200
+ throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previous.revisionId);
35201
+ }
35202
+ const versionSha = input.bundle?.sha256 ?? previous?.bundleSha256;
35203
+ if (input.version && versionSha) {
35204
+ const existing = this.versions.get(versionKey(input.principal.orgId, input.slug, input.version));
35205
+ if (existing && existing.bundleSha256 !== versionSha) {
35206
+ throw new SkillVersionExistsError(input.slug, input.version, existing.bundleSha256, versionSha);
34512
35207
  }
34513
- const carriedSha = input.bundle?.sha256 ?? previousSha;
34514
- const carriedSize = input.bundle?.byteSize ?? (previous?.bundle_byte_size == null ? null : Number(previous.bundle_byte_size));
34515
- const revisionId = revisionIdOfRecord({
35208
+ }
35209
+ if (input.bundle) {
35210
+ const bundleMapKey = skillKey(input.principal.orgId, input.bundle.sha256);
35211
+ this.bundles.set(bundleMapKey, {
35212
+ ...this.bundles.get(bundleMapKey),
35213
+ ...input.bundle,
35214
+ orgId: input.principal.orgId,
35215
+ createdAt: this.bundles.get(bundleMapKey)?.createdAt ?? now
35216
+ });
35217
+ }
35218
+ const carriedBundle = !input.bundle && previous?.bundleSha256 ? { bundleSha256: previous.bundleSha256, ...previous.bundleByteSize === undefined ? {} : { bundleByteSize: previous.bundleByteSize } } : {};
35219
+ const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : previous?.skillMd;
35220
+ const record = {
35221
+ orgId: input.principal.orgId,
35222
+ slug: input.slug,
35223
+ displayName: input.displayName,
35224
+ description: input.description,
35225
+ category: input.category,
35226
+ tags: [...input.tags],
35227
+ source: input.source,
35228
+ kind: input.kind,
35229
+ ...input.version ? { version: input.version } : {},
35230
+ ...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
35231
+ ...input.bundle ? { bundleSha256: input.bundle.sha256, bundleByteSize: input.bundle.byteSize } : carriedBundle,
35232
+ publishedByUserId: input.principal.userId,
35233
+ createdAt: previous?.createdAt ?? now,
35234
+ updatedAt: now,
35235
+ revisionId: revisionIdOfRecord(recordFieldsOf(input, carriedBundle, carriedSkillMd)),
35236
+ revisionNumber: (previous?.revisionNumber ?? 0) + 1
35237
+ };
35238
+ this.skills.set(key, record);
35239
+ if (input.version && versionSha && !this.versions.has(versionKey(input.principal.orgId, input.slug, input.version))) {
35240
+ this.versions.set(versionKey(input.principal.orgId, input.slug, input.version), {
35241
+ orgId: input.principal.orgId,
34516
35242
  slug: input.slug,
34517
- displayName: input.displayName,
34518
- description: input.description,
34519
- category: input.category,
34520
- tags: input.tags,
34521
- source: input.source,
34522
- kind: input.kind,
34523
- ...input.version ? { version: input.version } : {},
34524
- ...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
34525
- ...carriedSha ? { bundleSha256: carriedSha } : {},
34526
- ...carriedSize === null || carriedSize === undefined ? {} : { bundleByteSize: carriedSize }
35243
+ version: input.version,
35244
+ bundleSha256: versionSha,
35245
+ bundleByteSize: input.bundle?.byteSize ?? previous?.bundleByteSize ?? 0,
35246
+ storageKind: input.versionStorage?.storageKind ?? "db",
35247
+ ...input.versionStorage?.storageKey ? { storageKey: input.versionStorage.storageKey } : {},
35248
+ manifest: { ...input.versionManifest ?? {} },
35249
+ ...input.principal.userId ? { publishedByUserId: input.principal.userId } : {},
35250
+ createdAt: now
34527
35251
  });
34528
- if (input.bundle) {
34529
- await tx`
34530
- INSERT INTO skills_bundles (org_id, sha256, byte_size, content_type, storage_kind, storage_key, body_blob)
34531
- VALUES (${orgId}, ${input.bundle.sha256}, ${input.bundle.byteSize}, ${input.bundle.contentType}, ${input.bundle.storageKind}, ${input.bundle.storageKey ?? null}, ${input.bundle.bytes ?? null})
34532
- ON CONFLICT (org_id, sha256) DO UPDATE SET
34533
- byte_size = EXCLUDED.byte_size,
34534
- content_type = EXCLUDED.content_type,
34535
- storage_kind = EXCLUDED.storage_kind,
34536
- storage_key = EXCLUDED.storage_key,
34537
- body_blob = EXCLUDED.body_blob
34538
- `;
34539
- }
34540
- const rows = await tx`
34541
- INSERT INTO skills_registry (org_id, slug, display_name, description, category, tags_json, source, kind, version, skill_md,
34542
- bundle_sha256, bundle_byte_size, published_by_user_id, revision_id, revision_number, updated_at)
34543
- VALUES (${orgId}, ${input.slug}, ${input.displayName}, ${input.description}, ${input.category}, ${JSON.stringify(input.tags)}::jsonb,
34544
- ${input.source}, ${input.kind}, ${input.version ?? null}, ${carriedSkillMd},
34545
- ${input.bundle?.sha256 ?? null}, ${input.bundle?.byteSize ?? null}, ${input.principal.userId}, ${revisionId}, 1, now())
34546
- ON CONFLICT (org_id, slug) DO UPDATE SET
34547
- display_name = EXCLUDED.display_name,
34548
- description = EXCLUDED.description,
34549
- category = EXCLUDED.category,
34550
- tags_json = EXCLUDED.tags_json,
34551
- source = EXCLUDED.source,
34552
- kind = EXCLUDED.kind,
34553
- version = EXCLUDED.version,
34554
- skill_md = EXCLUDED.skill_md,
34555
- -- COALESCE: see the SQLite twin. A bundle-less publish is a metadata update,
34556
- -- not an instruction to discard the stored tarball.
34557
- bundle_sha256 = COALESCE(EXCLUDED.bundle_sha256, skills_registry.bundle_sha256),
34558
- bundle_byte_size = COALESCE(EXCLUDED.bundle_byte_size, skills_registry.bundle_byte_size),
34559
- published_by_user_id = EXCLUDED.published_by_user_id,
34560
- revision_id = EXCLUDED.revision_id,
34561
- -- Current + 1, not EXCLUDED.revision_number: the insert path minted 1, but on
34562
- -- the update path the ACTUAL row's counter is the truth (it may have advanced
34563
- -- since the pre-read, which is exactly what the WHERE guard below detects).
34564
- revision_number = skills_registry.revision_number + 1,
34565
- tombstoned_at = NULL,
34566
- tombstone_purge_after = NULL,
34567
- updated_at = EXCLUDED.updated_at
34568
- WHERE skills_registry.tombstoned_at IS NOT NULL
34569
- OR skills_registry.revision_id = ${input.expectedRevisionId ?? NO_REVISION_SENTINEL2}
34570
- RETURNING *
34571
- `;
34572
- if (!rows[0]) {
34573
- const current = await tx`SELECT revision_id FROM skills_registry WHERE org_id = ${orgId} AND slug = ${input.slug} LIMIT 1`;
34574
- const currentId = current[0] && typeof current[0].revision_id === "string" ? String(current[0].revision_id) : null;
34575
- throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, currentId);
34576
- }
34577
- if (previousSha && input.bundle && previousSha !== input.bundle.sha256) {
34578
- await tx`
34579
- DELETE FROM skills_bundles
34580
- WHERE org_id = ${orgId} AND sha256 = ${previousSha}
34581
- AND NOT EXISTS (SELECT 1 FROM skills_registry WHERE org_id = ${orgId} AND bundle_sha256 = ${previousSha})
34582
- `;
34583
- }
34584
- await tx`DELETE FROM skills_tags WHERE org_id = ${orgId} AND slug = ${input.slug}`;
34585
- for (const tag of input.tags) {
34586
- if (!tag.trim())
34587
- continue;
34588
- await tx`
34589
- INSERT INTO skills_tags (org_id, slug, tag) VALUES (${orgId}, ${input.slug}, ${tag})
34590
- ON CONFLICT DO NOTHING
34591
- `;
34592
- }
34593
- return rowToSkill(rows[0]);
34594
- });
35252
+ }
35253
+ if (previous?.bundleSha256 && input.bundle && previous.bundleSha256 !== input.bundle.sha256) {
35254
+ this.collectOrphanBundle(input.principal.orgId, previous.bundleSha256);
35255
+ }
35256
+ return record;
35257
+ }
35258
+ async listSkillVersions(principal, slug) {
35259
+ return Array.from(this.versions.values()).filter((v2) => v2.orgId === principal.orgId && v2.slug === slug).sort((a3, b3) => b3.createdAt.localeCompare(a3.createdAt) || b3.version.localeCompare(a3.version));
35260
+ }
35261
+ async getSkillVersion(principal, slug, version2) {
35262
+ const found = this.versions.get(versionKey(principal.orgId, slug, version2));
35263
+ return found && found.orgId === principal.orgId ? found : null;
34595
35264
  }
34596
35265
  async listSkills(principal) {
34597
- await this.purgeExpiredTombstones(principal);
34598
- const rows = await this.sql`
34599
- SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND tombstoned_at IS NULL ORDER BY slug ASC
34600
- `;
34601
- return rows.map(rowToSkill);
35266
+ return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
34602
35267
  }
34603
35268
  async getSkill(principal, slug) {
34604
- const rows = await this.sql`SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1`;
34605
- return rows[0] ? rowToSkill(rows[0]) : null;
35269
+ const skill = this.skills.get(skillKey(principal.orgId, slug));
35270
+ return skill && skill.orgId === principal.orgId ? skill : null;
34606
35271
  }
34607
35272
  async updateSkill(principal, slug, patch, expectedRevisionId) {
34608
35273
  const current = await this.getSkill(principal, slug);
@@ -34611,1142 +35276,838 @@ class PostgresSkillsStore {
34611
35276
  if (expectedRevisionId !== current.revisionId) {
34612
35277
  throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
34613
35278
  }
34614
- const next = { ...current, ...patch };
34615
- return await this.sql.begin(async (tx) => {
34616
- const revisionId = revisionIdOfRecord(next);
34617
- const updated = await tx`
34618
- UPDATE skills_registry
34619
- SET display_name = ${next.displayName}, description = ${next.description}, category = ${next.category},
34620
- tags_json = ${JSON.stringify(next.tags)}::jsonb, kind = ${next.kind}, version = ${next.version ?? null},
34621
- skill_md = ${next.skillMd ?? null}, revision_id = ${revisionId}, revision_number = revision_number + 1, updated_at = now()
34622
- WHERE org_id = ${principal.orgId} AND slug = ${slug} AND tombstoned_at IS NULL AND revision_id = ${current.revisionId}
34623
- RETURNING *
34624
- `;
34625
- if (!updated[0]) {
34626
- const nowRows = await tx`
34627
- SELECT revision_id, tombstoned_at FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1
34628
- `;
34629
- if (nowRows[0] && nowRows[0].tombstoned_at == null) {
34630
- const currentId = String(nowRows[0].revision_id);
34631
- throw new SkillRevisionConflictError(slug, expectedRevisionId, currentId);
34632
- }
34633
- return null;
34634
- }
34635
- await tx`DELETE FROM skills_tags WHERE org_id = ${principal.orgId} AND slug = ${slug}`;
34636
- for (const tag of next.tags) {
34637
- if (!tag.trim())
34638
- continue;
34639
- await tx`
34640
- INSERT INTO skills_tags (org_id, slug, tag) VALUES (${principal.orgId}, ${slug}, ${tag})
34641
- ON CONFLICT DO NOTHING
34642
- `;
34643
- }
34644
- return rowToSkill(updated[0]);
34645
- });
34646
- }
34647
- async deleteSkill(principal, slug, tombstoneWindowMs) {
34648
- return await this.sql.begin(async (tx) => {
34649
- const existingRows = await tx`
34650
- SELECT tombstoned_at FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1
34651
- `;
34652
- if (!existingRows[0])
34653
- return null;
34654
- if (existingRows[0].tombstoned_at != null) {
34655
- const rows2 = await tx`SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1`;
34656
- return rowToSkill(rows2[0]);
34657
- }
34658
- const rows = await tx`
34659
- UPDATE skills_registry
34660
- SET tombstoned_at = now(), tombstone_purge_after = now() + (${tombstoneWindowMs}::int * interval '1 millisecond'), updated_at = now()
34661
- WHERE org_id = ${principal.orgId} AND slug = ${slug}
34662
- RETURNING *
34663
- `;
34664
- return rows[0] ? rowToSkill(rows[0]) : null;
34665
- });
35279
+ const latest = this.skills.get(skillKey(principal.orgId, slug));
35280
+ if (!latest || latest.tombstonedAt)
35281
+ return null;
35282
+ if (latest.revisionId !== current.revisionId) {
35283
+ throw new SkillRevisionConflictError(slug, expectedRevisionId, latest.revisionId);
35284
+ }
35285
+ const next = {
35286
+ ...latest,
35287
+ ...patch,
35288
+ updatedAt: nowIso(),
35289
+ revisionId: revisionIdOfRecord({ ...latest, ...patch }),
35290
+ revisionNumber: latest.revisionNumber + 1
35291
+ };
35292
+ this.skills.set(skillKey(principal.orgId, slug), next);
35293
+ return next;
35294
+ }
35295
+ async deleteSkill(principal, slug, tombstoneWindowMs) {
35296
+ const current = await this.getSkill(principal, slug);
35297
+ if (!current)
35298
+ return null;
35299
+ if (!current.tombstonedAt) {
35300
+ const tombstoned = nowIso();
35301
+ const purgeAfter = new Date(Date.now() + tombstoneWindowMs).toISOString();
35302
+ const next = { ...current, tombstonedAt: tombstoned, tombstonePurgeAfter: purgeAfter, updatedAt: tombstoned };
35303
+ this.skills.set(skillKey(principal.orgId, slug), next);
35304
+ return next;
35305
+ }
35306
+ return current;
34666
35307
  }
34667
35308
  async purgeExpiredTombstones(principal) {
34668
- return await this.sql.begin(async (tx) => {
34669
- const expiredRows = await tx`
34670
- SELECT * FROM skills_registry
34671
- WHERE org_id = ${principal.orgId} AND tombstoned_at IS NOT NULL AND tombstone_purge_after <= now()
34672
- `;
34673
- if (!expiredRows.length)
34674
- return [];
34675
- const purged = [];
34676
- for (const row of expiredRows) {
34677
- const record = rowToSkill(row);
34678
- await tx`
34679
- DELETE FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${record.slug} AND tombstone_purge_after <= now()
34680
- `;
34681
- await tx`DELETE FROM skills_tags WHERE org_id = ${principal.orgId} AND slug = ${record.slug}`;
34682
- await tx`
34683
- DELETE FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${record.slug} AND tombstone_purge_after <= now()
34684
- `;
34685
- if (record.bundleSha256) {
34686
- await tx`
34687
- DELETE FROM skills_bundles
34688
- WHERE org_id = ${principal.orgId} AND sha256 = ${record.bundleSha256}
34689
- AND NOT EXISTS (SELECT 1 FROM skills_registry WHERE org_id = ${principal.orgId} AND bundle_sha256 = ${record.bundleSha256})
34690
- `;
34691
- }
34692
- purged.push(record);
34693
- }
34694
- return purged;
34695
- });
35309
+ const now = nowIso();
35310
+ const purged = [];
35311
+ for (const [key, skill] of this.skills) {
35312
+ if (skill.orgId !== principal.orgId || !skill.tombstonedAt || !skill.tombstonePurgeAfter)
35313
+ continue;
35314
+ if (skill.tombstonePurgeAfter > now)
35315
+ continue;
35316
+ this.skills.delete(key);
35317
+ if (skill.bundleSha256)
35318
+ this.collectOrphanBundle(principal.orgId, skill.bundleSha256);
35319
+ purged.push(skill);
35320
+ }
35321
+ return purged;
34696
35322
  }
34697
35323
  async getSkillBundle(principal, sha256) {
34698
- const rows = await this.sql`SELECT * FROM skills_bundles WHERE org_id = ${principal.orgId} AND sha256 = ${sha256} LIMIT 1`;
34699
- return rows[0] ? rowToSkillBundle(rows[0]) : null;
35324
+ const bundle = this.bundles.get(skillKey(principal.orgId, sha256));
35325
+ return bundle && bundle.orgId === principal.orgId ? bundle : null;
34700
35326
  }
34701
35327
  async pinSkill(principal, slug, metadata = {}) {
34702
- const rows = await this.sql`
34703
- INSERT INTO skills_pins (org_id, principal, slug, pinned_at, metadata_json)
34704
- VALUES (${principal.orgId}, ${principal.apiKeyId}, ${slug}, now(), ${JSON.stringify(metadata)}::jsonb)
34705
- ON CONFLICT (org_id, principal, slug) DO UPDATE SET
34706
- pinned_at = now(),
34707
- metadata_json = EXCLUDED.metadata_json
34708
- RETURNING *
34709
- `;
34710
- return rowToPin(rows[0]);
35328
+ const pin = { orgId: principal.orgId, principal: principal.apiKeyId, slug, pinnedAt: nowIso(), metadata: { ...metadata } };
35329
+ this.pins.set(pinKey(principal.orgId, principal.apiKeyId, slug), pin);
35330
+ return pin;
34711
35331
  }
34712
35332
  async unpinSkill(principal, slug) {
34713
- const rows = await this.sql`
34714
- DELETE FROM skills_pins WHERE org_id = ${principal.orgId} AND principal = ${principal.apiKeyId} AND slug = ${slug}
34715
- RETURNING 1 AS present
34716
- `;
34717
- return rows.length > 0;
35333
+ return this.pins.delete(pinKey(principal.orgId, principal.apiKeyId, slug));
34718
35334
  }
34719
35335
  async listPins(principal) {
34720
- const rows = await this.sql`
34721
- SELECT * FROM skills_pins WHERE org_id = ${principal.orgId} AND principal = ${principal.apiKeyId} ORDER BY slug ASC
34722
- `;
34723
- return rows.map(rowToPin);
35336
+ return Array.from(this.pins.values()).filter((pin) => pin.orgId === principal.orgId && pin.principal === principal.apiKeyId).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
34724
35337
  }
34725
35338
  async listTags(principal) {
34726
35339
  await this.purgeExpiredTombstones(principal);
34727
- const rows = await this.sql`
34728
- SELECT DISTINCT tag FROM skills_tags WHERE org_id = ${principal.orgId} ORDER BY tag ASC
34729
- `;
34730
- return rows.map((row) => String(row.tag));
35340
+ const tags = new Set;
35341
+ for (const skill of this.skills.values()) {
35342
+ if (skill.orgId !== principal.orgId)
35343
+ continue;
35344
+ for (const tag of skill.tags) {
35345
+ if (tag.trim())
35346
+ tags.add(tag);
35347
+ }
35348
+ }
35349
+ return [...tags].sort();
34731
35350
  }
34732
35351
  async listSkillsByTag(principal, tag) {
34733
35352
  await this.purgeExpiredTombstones(principal);
35353
+ return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt && skill.tags.includes(tag)).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
35354
+ }
35355
+ async listPinsByTag(principal, tag) {
35356
+ await this.purgeExpiredTombstones(principal);
35357
+ const taggedSlugs = new Set;
35358
+ for (const skill of this.skills.values()) {
35359
+ if (skill.orgId === principal.orgId && !skill.tombstonedAt && skill.tags.includes(tag))
35360
+ taggedSlugs.add(skill.slug);
35361
+ }
35362
+ return Array.from(this.pins.values()).filter((pin) => pin.orgId === principal.orgId && pin.principal === principal.apiKeyId && taggedSlugs.has(pin.slug)).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
35363
+ }
35364
+ async listPublishedSlugs(principal) {
35365
+ return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt).map((skill) => skill.slug).sort();
35366
+ }
35367
+ collectOrphanBundle(orgId, sha256) {
35368
+ const referenced = Array.from(this.skills.values()).some((skill) => skill.orgId === orgId && skill.bundleSha256 === sha256) || Array.from(this.versions.values()).some((v2) => v2.orgId === orgId && v2.bundleSha256 === sha256);
35369
+ if (!referenced)
35370
+ this.bundles.delete(skillKey(orgId, sha256));
35371
+ }
35372
+ patchRun(runId2, patch) {
35373
+ const run = this.runs.get(runId2);
35374
+ if (!run)
35375
+ return null;
35376
+ const next = { ...run, ...patch };
35377
+ this.runs.set(runId2, next);
35378
+ return next;
35379
+ }
35380
+ }
35381
+ function skillKey(orgId, slug) {
35382
+ return `${orgId.length}:${orgId}:${slug}`;
35383
+ }
35384
+ function pinKey(orgId, principal, slug) {
35385
+ return `${orgId.length}:${orgId}:${principal.length}:${principal}:${slug}`;
35386
+ }
35387
+ function versionKey(orgId, slug, version2) {
35388
+ return `${orgId.length}:${orgId}:${slug.length}:${slug}:${version2}`;
35389
+ }
35390
+
35391
+ class PostgresSkillsStore {
35392
+ backend = { kind: "postgres", durable: true, label: "postgres" };
35393
+ sql;
35394
+ constructor(databaseUrl) {
35395
+ const bunWithSql = Bun;
35396
+ this.sql = new bunWithSql.SQL(databaseUrl, { max: resolvePoolMax() });
35397
+ }
35398
+ async verifyConnectivity() {
35399
+ try {
35400
+ await this.sql`SELECT 1`;
35401
+ } catch (error) {
35402
+ throw new Error("cannot reach the configured Postgres database. The server will not start with an unreachable " + "database rather than fall back to another backend and silently split your data across two stores. " + `Check HASNA_SKILLS_DATABASE_URL and that the instance is accepting connections. Driver reported: ${connectionFailureSummary(error)}`);
35403
+ }
35404
+ try {
35405
+ await this.sql`SELECT 1 FROM api_keys LIMIT 0`;
35406
+ } catch (error) {
35407
+ throw new Error("the configured Postgres database is reachable but has no skills schema. Run `skills-migrate` against it " + "before starting the server - unlike SQLite, Postgres is not migrated automatically, so that several " + `replicas cannot race to migrate a shared database. Driver reported: ${connectionFailureSummary(error)}`);
35408
+ }
35409
+ await this.backfillLegacyRevisions();
35410
+ }
35411
+ async backfillLegacyRevisions() {
35412
+ const rows = await this.sql`SELECT * FROM skills_registry WHERE revision_id = ${""}`;
35413
+ for (const row of rows) {
35414
+ const record = rowToSkill(row);
35415
+ await this.sql`UPDATE skills_registry SET revision_id = ${revisionIdOfRecord(record)} WHERE org_id = ${record.orgId} AND slug = ${record.slug}`;
35416
+ }
35417
+ }
35418
+ async close() {
35419
+ await this.sql.close?.();
35420
+ }
35421
+ async ensureBootstrapApiKey(token, principal) {
35422
+ const resolved = publicPrincipal(principal);
35423
+ await this.sql`
35424
+ INSERT INTO organizations (id, slug, name)
35425
+ VALUES (${resolved.orgId}, ${resolved.orgSlug}, ${resolved.orgName})
35426
+ ON CONFLICT (id) DO UPDATE SET slug = EXCLUDED.slug, name = EXCLUDED.name
35427
+ `;
35428
+ await this.sql`
35429
+ INSERT INTO users (id, email, name)
35430
+ VALUES (${resolved.userId}, ${resolved.email}, ${resolved.email})
35431
+ ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email
35432
+ `;
35433
+ await this.sql`
35434
+ INSERT INTO organization_members (org_id, user_id, role)
35435
+ VALUES (${resolved.orgId}, ${resolved.userId}, ${resolved.role})
35436
+ ON CONFLICT (org_id, user_id) DO UPDATE SET role = EXCLUDED.role
35437
+ `;
35438
+ await this.sql`
35439
+ INSERT INTO api_keys (id, org_id, user_id, name, key_hash, scopes_json)
35440
+ VALUES (${resolved.apiKeyId}, ${resolved.orgId}, ${resolved.userId}, ${"bootstrap"}, ${hashApiKey(token)}, ${JSON.stringify(resolved.scopes)}::jsonb)
35441
+ ON CONFLICT (key_hash) DO NOTHING
35442
+ `;
35443
+ }
35444
+ async authenticateApiKeyHash(hash) {
34734
35445
  const rows = await this.sql`
34735
- SELECT s.* FROM skills_registry s
34736
- JOIN skills_tags t ON t.org_id = s.org_id AND t.slug = s.slug
34737
- WHERE t.org_id = ${principal.orgId} AND t.tag = ${tag} AND s.tombstoned_at IS NULL
34738
- ORDER BY s.slug ASC
35446
+ SELECT k.id AS api_key_id, k.scopes_json, o.id AS org_id, o.slug AS org_slug, o.name AS org_name,
35447
+ u.id AS user_id, u.email, m.role
35448
+ FROM api_keys k
35449
+ JOIN organizations o ON o.id = k.org_id
35450
+ JOIN users u ON u.id = k.user_id
35451
+ LEFT JOIN organization_members m ON m.org_id = k.org_id AND m.user_id = k.user_id
35452
+ WHERE k.key_hash = ${hash} AND k.revoked_at IS NULL
35453
+ LIMIT 1
34739
35454
  `;
34740
- return rows.map(rowToSkill);
35455
+ const row = rows[0];
35456
+ if (!row)
35457
+ return null;
35458
+ await this.sql`UPDATE api_keys SET last_used_at = now() WHERE id = ${String(row.api_key_id)}`;
35459
+ return {
35460
+ apiKeyId: String(row.api_key_id),
35461
+ orgId: String(row.org_id),
35462
+ orgSlug: String(row.org_slug),
35463
+ orgName: String(row.org_name),
35464
+ userId: String(row.user_id),
35465
+ email: String(row.email),
35466
+ role: typeof row.role === "string" ? row.role : "member",
35467
+ scopes: parseJsonArray(row.scopes_json)
35468
+ };
35469
+ }
35470
+ async withContext(orgId, worker, fn) {
35471
+ return this.sql.begin(async (tx) => {
35472
+ await tx`SELECT set_config('app.skills_org_id', ${orgId ?? ""}, true)`;
35473
+ await tx`SELECT set_config('app.skills_claim_context', ${worker ? "worker" : ""}, true)`;
35474
+ return await fn(tx);
35475
+ });
35476
+ }
35477
+ async createRun(input) {
35478
+ return this.withContext(input.principal.orgId, false, async (tx) => {
35479
+ if (input.idempotencyKey) {
35480
+ const existing = await tx`
35481
+ SELECT * FROM skills_runs
35482
+ WHERE org_id = ${input.principal.orgId} AND idempotency_key = ${input.idempotencyKey}
35483
+ LIMIT 1
35484
+ `;
35485
+ if (existing[0])
35486
+ return rowToRun(existing[0]);
35487
+ }
35488
+ const id = runId();
35489
+ const rows = await tx`
35490
+ INSERT INTO skills_runs (id, org_id, user_id, skill_slug, requested_slug, status, input_json, args_json, idempotency_key, correlation_id)
35491
+ 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()})
35492
+ RETURNING *
35493
+ `;
35494
+ return rowToRun(rows[0]);
35495
+ });
35496
+ }
35497
+ async listRuns(principal, limit) {
35498
+ return this.withContext(principal.orgId, false, async (tx) => {
35499
+ const rows = await tx`
35500
+ SELECT * FROM skills_runs WHERE org_id = ${principal.orgId}
35501
+ ORDER BY created_at DESC LIMIT ${normalizeLimit(limit)}
35502
+ `;
35503
+ return rows.map(rowToRun);
35504
+ });
34741
35505
  }
34742
- async listPinsByTag(principal, tag) {
34743
- await this.purgeExpiredTombstones(principal);
34744
- const rows = await this.sql`
34745
- SELECT p.* FROM skills_pins p
34746
- JOIN skills_tags t ON t.org_id = p.org_id AND t.slug = p.slug
34747
- JOIN skills_registry s ON s.org_id = p.org_id AND s.slug = p.slug
34748
- WHERE p.org_id = ${principal.orgId} AND p.principal = ${principal.apiKeyId}
34749
- AND t.tag = ${tag} AND s.tombstoned_at IS NULL
34750
- ORDER BY p.slug ASC
34751
- `;
34752
- return rows.map(rowToPin);
35506
+ async getRun(principal, runId2) {
35507
+ return this.withContext(principal.orgId, false, async (tx) => {
35508
+ const rows = await tx`
35509
+ SELECT * FROM skills_runs WHERE id = ${runId2} AND org_id = ${principal.orgId} LIMIT 1
35510
+ `;
35511
+ return rows[0] ? rowToRun(rows[0]) : null;
35512
+ });
34753
35513
  }
34754
- async listPublishedSlugs(principal) {
34755
- const rows = await this.sql`
34756
- SELECT slug FROM skills_registry WHERE org_id = ${principal.orgId} AND tombstoned_at IS NULL ORDER BY slug ASC
34757
- `;
34758
- return rows.map((row) => String(row.slug));
35514
+ async claimNextRun(input) {
35515
+ return this.withContext(null, true, async (tx) => {
35516
+ const rows = await tx`
35517
+ UPDATE skills_runs
35518
+ SET status = ${"running"}, started_at = COALESCE(started_at, now()), locked_by = ${input.workerId}, locked_at = now(),
35519
+ lease_generation = lease_generation + 1
35520
+ WHERE id = (
35521
+ SELECT id FROM skills_runs
35522
+ WHERE status IN (${"queued"}, ${"retrying"})
35523
+ ORDER BY created_at ASC
35524
+ FOR UPDATE SKIP LOCKED
35525
+ LIMIT 1
35526
+ )
35527
+ RETURNING *
35528
+ `;
35529
+ return rows[0] ? rowToRun(rows[0]) : null;
35530
+ });
34759
35531
  }
34760
- async collectOrphanBundle(orgId, sha256) {
34761
- await this.sql`
34762
- DELETE FROM skills_bundles
34763
- WHERE org_id = ${orgId} AND sha256 = ${sha256}
34764
- AND NOT EXISTS (SELECT 1 FROM skills_registry WHERE org_id = ${orgId} AND bundle_sha256 = ${sha256})
34765
- `;
35532
+ async updateRun(runId2, patch) {
35533
+ return this.withContext(null, true, async (tx) => {
35534
+ const current = await tx`SELECT * FROM skills_runs WHERE id = ${runId2} LIMIT 1`;
35535
+ if (!current[0])
35536
+ return null;
35537
+ const run = { ...rowToRun(current[0]), ...patch };
35538
+ const rows = await tx`
35539
+ UPDATE skills_runs
35540
+ SET status = ${run.status},
35541
+ output_type = ${run.outputType ?? null},
35542
+ output_preview = ${run.outputPreview ?? null},
35543
+ error_code = ${run.errorCode ?? null},
35544
+ error_message = ${run.errorMessage ?? null},
35545
+ started_at = ${run.startedAt ?? null},
35546
+ completed_at = ${run.completedAt ?? null}
35547
+ WHERE id = ${runId2}
35548
+ RETURNING *
35549
+ `;
35550
+ return rows[0] ? rowToRun(rows[0]) : null;
35551
+ });
34766
35552
  }
34767
- }
34768
- var NO_REVISION_SENTINEL2 = "0000000000000000000000000000000000000000000000000000000000000000";
34769
- function isUniqueViolation(error) {
34770
- const code = error?.code;
34771
- if (code === "23505" || code === 23505)
34772
- return true;
34773
- const message = error instanceof Error ? error.message : String(error);
34774
- return /duplicate key value violates unique constraint/i.test(message);
34775
- }
34776
- function connectionFailureSummary(error) {
34777
- const message = error instanceof Error ? error.message : String(error);
34778
- return message.replace(/[a-z][a-z0-9+.-]*:\/\/\S*/gi, "<redacted-url>").split(`
34779
- `)[0].slice(0, 200);
34780
- }
34781
-
34782
- // src/server/redaction.ts
34783
- var SECRET_PATTERNS = [
34784
- /\bsk-[A-Za-z0-9_-]{8,}\b/g,
34785
- /\bsk_[A-Za-z0-9_-]{8,}\b/g,
34786
- /\bgh[opsur]_[A-Za-z0-9_]{8,}\b/g,
34787
- /\bgithub_pat_[A-Za-z0-9_]{8,}\b/g,
34788
- /\bnpm_[A-Za-z0-9_]{8,}\b/g,
34789
- /\bAKIA[A-Z0-9]{12,}\b/g,
34790
- /\bAIza[A-Za-z0-9_-]{10,}\b/g,
34791
- /\b[A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|DATABASE_URL)[A-Z0-9_]*\s*[:=]\s*[^ \n\r\t]+/gi,
34792
- /\bhttps?:\/\/[^ \n\r\t]+X-Amz-Signature=[^ \n\r\t]+/gi
34793
- ];
34794
- function redactForClient(value) {
34795
- const text = typeof value === "string" ? value : JSON.stringify(value);
34796
- return SECRET_PATTERNS.reduce((current, pattern) => current.replace(pattern, "credential"), text).replace(/\s+$/g, "").slice(0, 20000);
34797
- }
34798
-
34799
- // src/server/handlers.ts
34800
- var DETERMINISTIC_TEXT_HANDLERS = new Set([
34801
- "audio-transcript-pack",
34802
- "transcript",
34803
- "video-highlight-pack"
34804
- ]);
34805
- async function executeRun(store, run, storage = new ArtifactStorage) {
34806
- try {
34807
- await store.appendLog(run.id, run.orgId, "info", redactForClient(`starting run ${run.skill}`));
34808
- if (!DETERMINISTIC_TEXT_HANDLERS.has(run.skill)) {
34809
- await store.appendLog(run.id, run.orgId, "warn", `${run.skill} has no provider-free handler yet`);
34810
- return await failRun(store, run, "HANDLER_UNAVAILABLE", "This deployment has no safe provider-free handler for that skill yet.");
34811
- }
34812
- const text = extractText(run);
34813
- if (!text.trim()) {
34814
- return await failRun(store, run, "INVALID_INPUT", "Provide transcript text with input.transcript, input.text, --text, --source, or positional text.");
34815
- }
34816
- const title = extractOption(run.args, "--title") || stringInput(run.input, "title") || "Skills run";
34817
- const summary = summarize(text);
34818
- const artifacts = [
34819
- textArtifact(run, "transcript.md", `# ${title}
34820
-
34821
- ${text.trim()}
34822
- `),
34823
- textArtifact(run, "summary.md", `# Summary
34824
-
34825
- ${summary}
34826
- `),
34827
- textArtifact(run, "show-notes.md", `# Show Notes
34828
-
34829
- - ${summary}
34830
- - Generated by the skills deterministic worker.
34831
- `),
34832
- textArtifact(run, "clips.csv", `start,end,title,summary
34833
- 00:00,00:30,"Opening","${csv(summary)}"
34834
- `),
34835
- textArtifact(run, "manifest.json", JSON.stringify({
34836
- runId: run.id,
34837
- skill: run.skill,
34838
- requestedSlug: run.requestedSlug,
34839
- generatedAt: new Date().toISOString(),
34840
- artifacts: ["transcript.md", "summary.md", "show-notes.md", "clips.csv"]
34841
- }, null, 2) + `
34842
- `, "application/json")
34843
- ];
34844
- for (const artifact of artifacts) {
34845
- await store.addArtifact(await storage.materialize(run, artifact.meta, artifact.body));
35553
+ async transitionRun(runId2, patch, expectedGeneration) {
35554
+ return this.withContext(null, true, async (tx) => {
35555
+ const current = await tx`SELECT * FROM skills_runs WHERE id = ${runId2} LIMIT 1`;
35556
+ if (!current[0])
35557
+ return null;
35558
+ const stored = rowToRun(current[0]);
35559
+ if (stored.leaseGeneration !== expectedGeneration) {
35560
+ throw new StaleLeaseGenerationError(runId2, expectedGeneration, stored.leaseGeneration, stored.status);
35561
+ }
35562
+ const run = { ...stored, ...patch };
35563
+ const rows = await tx`
35564
+ UPDATE skills_runs
35565
+ SET status = ${run.status},
35566
+ output_type = ${run.outputType ?? null},
35567
+ output_preview = ${run.outputPreview ?? null},
35568
+ error_code = ${run.errorCode ?? null},
35569
+ error_message = ${run.errorMessage ?? null},
35570
+ started_at = ${run.startedAt ?? null},
35571
+ completed_at = ${run.completedAt ?? null},
35572
+ lease_generation = ${run.leaseGeneration}
35573
+ WHERE id = ${runId2} AND lease_generation = ${expectedGeneration}
35574
+ RETURNING *
35575
+ `;
35576
+ return rows[0] ? rowToRun(rows[0]) : null;
35577
+ });
35578
+ }
35579
+ async appendLog(runId2, orgId, level, message) {
35580
+ let lastError;
35581
+ for (let attempt = 0;attempt < LOG_SEQUENCE_ATTEMPTS; attempt += 1) {
35582
+ try {
35583
+ const rows = await this.withContext(orgId, true, async (tx) => {
35584
+ return await tx`
35585
+ INSERT INTO skills_run_logs (run_id, org_id, sequence, level, message)
35586
+ VALUES (
35587
+ ${runId2},
35588
+ ${orgId},
35589
+ (SELECT COALESCE(MAX(sequence), 0) + 1 FROM skills_run_logs WHERE run_id = ${runId2}),
35590
+ ${level},
35591
+ ${message}
35592
+ )
35593
+ RETURNING *
35594
+ `;
35595
+ });
35596
+ return rowToLog(rows[0]);
35597
+ } catch (error) {
35598
+ if (!isUniqueViolation(error))
35599
+ throw error;
35600
+ lastError = error;
35601
+ }
34846
35602
  }
34847
- await store.appendLog(run.id, run.orgId, "info", `generated ${artifacts.length} artifacts`);
34848
- return await completeRun(store, run, summary);
34849
- } catch (error) {
34850
- return await failRun(store, run, "WORKER_ERROR", redactForClient(error.message));
35603
+ throw lastError;
34851
35604
  }
34852
- }
34853
- function textArtifact(run, relativePath, bodyText, contentType = relativePath.endsWith(".json") ? "application/json" : "text/markdown; charset=utf-8") {
34854
- const bytes = new TextEncoder().encode(bodyText);
34855
- return {
34856
- meta: {
34857
- id: createArtifactId(),
34858
- runId: run.id,
34859
- orgId: run.orgId,
34860
- fileName: relativePath.split("/").pop() || relativePath,
34861
- relativePath,
34862
- contentType,
34863
- byteSize: bytes.byteLength,
34864
- sha256: createHash5("sha256").update(bytes).digest("hex"),
34865
- visibility: "private"
34866
- },
34867
- body: { relativePath, bodyText, contentType }
34868
- };
34869
- }
34870
- async function completeRun(store, run, preview) {
34871
- const next = await fencedTransition(store, run, {
34872
- status: "succeeded",
34873
- outputType: "artifact_bundle",
34874
- outputPreview: preview,
34875
- completedAt: new Date().toISOString()
34876
- });
34877
- return next ?? run;
34878
- }
34879
- async function failRun(store, run, code, message) {
34880
- try {
34881
- await store.appendLog(run.id, run.orgId, "error", message);
34882
- } catch {}
34883
- const next = await fencedTransition(store, run, {
34884
- status: "failed",
34885
- errorCode: code,
34886
- errorMessage: message,
34887
- completedAt: new Date().toISOString()
34888
- });
34889
- return next ?? run;
34890
- }
34891
- async function fencedTransition(store, run, patch) {
34892
- if (!store.transitionRun)
34893
- return store.updateRun(run.id, patch);
34894
- const next = await store.transitionRun(run.id, patch, run.leaseGeneration);
34895
- if (!next) {
34896
- try {
34897
- await store.appendLog(run.id, run.orgId, "warn", `late write rejected: run no longer owned at lease_generation ${run.leaseGeneration}`);
34898
- } catch {}
35605
+ async listLogs(principal, runId2) {
35606
+ return this.withContext(principal.orgId, false, async (tx) => {
35607
+ const rows = await tx`
35608
+ SELECT l.* FROM skills_run_logs l
35609
+ JOIN skills_runs r ON r.id = l.run_id AND r.org_id = ${principal.orgId}
35610
+ WHERE l.run_id = ${runId2}
35611
+ ORDER BY l.sequence ASC
35612
+ `;
35613
+ return rows.map(rowToLog);
35614
+ });
34899
35615
  }
34900
- return next;
34901
- }
34902
- function extractText(run) {
34903
- return stringInput(run.input, "transcript") || stringInput(run.input, "text") || extractOption(run.args, "--text") || extractOption(run.args, "--source") || run.args.filter((arg) => !arg.startsWith("--")).join(" ");
34904
- }
34905
- function stringInput(input, key) {
34906
- const value = input[key];
34907
- return typeof value === "string" && value.trim() ? value : undefined;
34908
- }
34909
- function extractOption(args, flag) {
34910
- for (let i3 = 0;i3 < args.length; i3++) {
34911
- const arg = args[i3];
34912
- if (arg === flag && args[i3 + 1])
34913
- return args[i3 + 1];
34914
- if (arg.startsWith(`${flag}=`))
34915
- return arg.slice(flag.length + 1);
35616
+ async addArtifact(artifact) {
35617
+ return this.withContext(artifact.orgId, true, async (tx) => {
35618
+ const rows = await tx`
35619
+ INSERT INTO skills_artifacts (id, run_id, org_id, file_name, relative_path, content_type, byte_size, sha256, storage_kind, storage_key, body_text, visibility, expires_at)
35620
+ VALUES (${artifact.id}, ${artifact.runId}, ${artifact.orgId}, ${artifact.fileName}, ${artifact.relativePath}, ${artifact.contentType}, ${artifact.byteSize}, ${artifact.sha256}, ${artifact.storageKind}, ${artifact.storageKey ?? null}, ${artifact.bodyText ?? null}, ${artifact.visibility}, ${artifact.expiresAt ?? null})
35621
+ RETURNING *
35622
+ `;
35623
+ return rowToArtifact(rows[0]);
35624
+ });
34916
35625
  }
34917
- return;
34918
- }
34919
- function summarize(text) {
34920
- const normalized = redactForClient(text).replace(/\s+/g, " ").trim();
34921
- if (normalized.length <= 220)
34922
- return normalized;
34923
- return `${normalized.slice(0, 217)}...`;
34924
- }
34925
- function csv(value) {
34926
- return value.replace(/"/g, '""');
34927
- }
34928
-
34929
- // src/lib/registry-data/development-tools.ts
34930
- var DEVELOPMENT_TOOLS_SKILLS = [
34931
- {
34932
- name: "repo-onboarding-report",
34933
- displayName: "Repo Onboarding Report",
34934
- description: "Generate repository onboarding packages with architecture maps, setup guides, risk registers, and first-week plans",
34935
- category: "Development Tools",
34936
- kind: "instruction",
34937
- tags: ["repository", "onboarding", "architecture", "developer-tools"]
34938
- },
34939
- {
34940
- name: "security-audit-report",
34941
- displayName: "Security Audit Report",
34942
- description: "Generate application security hardening reports covering auth, secrets, headers, webhooks, RLS, permissions, dependencies, and prioritized fixes",
34943
- category: "Development Tools",
34944
- kind: "instruction",
34945
- tags: ["security", "audit", "hardening", "rls", "webhooks"]
34946
- },
34947
- {
34948
- name: "performance-audit-report",
34949
- displayName: "Performance Audit Report",
34950
- description: "Generate performance audit reports with metrics, findings, budgets, remediation plans, and manifest artifacts",
34951
- category: "Development Tools",
34952
- kind: "instruction",
34953
- tags: ["performance", "audit", "latency", "budget", "web"]
34954
- },
34955
- {
34956
- name: "migration-plan-pack",
34957
- displayName: "Migration Plan Pack",
34958
- description: "Generate migration plans for frameworks, libraries, databases, infrastructure, and architecture upgrades with risk matrix, checklist, rollout, and test strategy artifacts",
34959
- category: "Development Tools",
34960
- kind: "instruction",
34961
- tags: ["migration", "upgrade", "planning", "frameworks", "databases"]
34962
- },
34963
- {
34964
- name: "test-suite-generator",
34965
- displayName: "Test Suite Generator",
34966
- description: "Generate runnable API, unit, and browser test suite packages with coverage notes",
34967
- category: "Development Tools",
34968
- kind: "instruction",
34969
- tags: ["testing", "qa", "api-tests", "browser-tests", "coverage"]
34970
- },
34971
- {
34972
- name: "api-test-suite",
34973
- displayName: "API Test Suite",
34974
- description: "Generate and run API test suites with comprehensive endpoint coverage",
34975
- category: "Development Tools",
34976
- tags: ["api", "testing", "automation", "qa"]
34977
- },
34978
- {
34979
- name: "api-docs-portal",
34980
- displayName: "API Docs Portal",
34981
- description: "Generate static API documentation portals from OpenAPI specs, route lists, and endpoint examples",
34982
- category: "Development Tools",
34983
- tags: ["api", "documentation", "openapi", "portal"]
34984
- },
34985
- {
34986
- name: "codefix",
34987
- displayName: "Code Fix",
34988
- description: "Code quality CLI for auto-linting, formatting, fixing, and style enforcement",
34989
- category: "Development Tools",
34990
- tags: ["code", "linting", "formatting", "quality"]
34991
- },
34992
- {
34993
- name: "commitpush",
34994
- displayName: "Commit Push",
34995
- description: "Create logical commits from repo changes and push directly to the main branch",
34996
- category: "Development Tools",
34997
- tags: ["git", "commit", "push", "automation"]
34998
- },
34999
- {
35000
- name: "commitpushpr",
35001
- displayName: "Commit Push PR",
35002
- description: "Create logical commits, push a feature branch, and open a GitHub pull request",
35003
- category: "Development Tools",
35004
- dependencies: ["commitpush"],
35005
- tags: ["git", "commit", "pull-request", "github", "automation"]
35006
- },
35007
- {
35008
- name: "database-explorer",
35009
- displayName: "Database Explorer",
35010
- description: "Explore and query databases with an interactive interface",
35011
- category: "Development Tools",
35012
- tags: ["database", "explorer", "sql", "query"]
35013
- },
35014
- {
35015
- name: "diff-viewer",
35016
- displayName: "Diff Viewer",
35017
- description: "View and analyze file differences with visual diff representation",
35018
- category: "Development Tools",
35019
- tags: ["diff", "comparison", "files", "code-review"]
35020
- },
35021
- {
35022
- name: "generate-api-client",
35023
- displayName: "Generate API Client",
35024
- description: "Generate API client libraries from OpenAPI specs and documentation",
35025
- category: "Development Tools",
35026
- tags: ["api", "client", "code-generation", "openapi"]
35027
- },
35028
- {
35029
- name: "generate-dockerfile",
35030
- displayName: "Generate Dockerfile",
35031
- description: "Generate optimized Dockerfiles for containerized applications",
35032
- category: "Development Tools",
35033
- tags: ["docker", "dockerfile", "containers", "devops"]
35034
- },
35035
- {
35036
- name: "generate-env",
35037
- displayName: "Generate Env",
35038
- description: "Generate environment variable files from templates and configurations",
35039
- category: "Development Tools",
35040
- tags: ["env", "environment", "configuration", "dotenv"]
35041
- },
35042
- {
35043
- name: "generate-sitemap",
35044
- displayName: "Generate Sitemap",
35045
- description: "Generate XML sitemaps for websites and web applications",
35046
- category: "Development Tools",
35047
- tags: ["sitemap", "seo", "xml", "web"]
35048
- },
35049
- {
35050
- name: "hook",
35051
- displayName: "Hook",
35052
- description: "Claude Code hook creation skill - generates standardized hook scaffolds",
35053
- category: "Development Tools",
35054
- tags: ["hooks", "scaffold", "claude-code", "automation"]
35055
- },
35056
- {
35057
- name: "http-server",
35058
- displayName: "HTTP Server",
35059
- description: "Spin up local HTTP servers for development and testing",
35060
- category: "Development Tools",
35061
- tags: ["http", "server", "development", "local"]
35062
- },
35063
- {
35064
- name: "lorem-generator",
35065
- displayName: "Lorem Generator",
35066
- description: "Generate placeholder text in various styles and lengths",
35067
- category: "Development Tools",
35068
- tags: ["lorem", "placeholder", "text", "mockup"]
35069
- },
35070
- {
35071
- name: "managemcp",
35072
- displayName: "Manage MCP",
35073
- description: "Manage MCP servers with install, configure, and lifecycle operations",
35074
- category: "Development Tools",
35075
- tags: ["mcp", "management", "servers", "configuration"]
35076
- },
35077
- {
35078
- name: "markdown-validator",
35079
- displayName: "Markdown Validator",
35080
- description: "Validate markdown files for syntax, links, and formatting issues",
35081
- category: "Development Tools",
35082
- tags: ["markdown", "validation", "linting", "formatting"]
35083
- },
35084
- {
35085
- name: "monitor",
35086
- displayName: "Monitor",
35087
- description: "Operate the monitor MCP for machine health, processes, cron jobs, and cleanup workflows",
35088
- category: "Development Tools",
35089
- tags: ["monitoring", "mcp", "processes", "operations"]
35090
- },
35091
- {
35092
- name: "regex-tester",
35093
- displayName: "Regex Tester",
35094
- description: "Test and validate regular expressions with sample inputs",
35095
- category: "Development Tools",
35096
- tags: ["regex", "testing", "validation", "patterns"]
35097
- },
35098
- {
35099
- name: "scancommitpr",
35100
- displayName: "Scan Commit PR",
35101
- description: "Scan repo changes, group into logical commits, push, and optionally create a PR",
35102
- category: "Development Tools",
35103
- dependencies: ["scancommitpush"],
35104
- tags: ["git", "commit", "push", "pull-request", "automation"]
35105
- },
35106
- {
35107
- name: "scancommitpush",
35108
- displayName: "Scan Commit Push",
35109
- description: "Scan repo changes, group into logical commits with conventional messages, and push to GitHub",
35110
- category: "Development Tools",
35111
- tags: ["git", "commit", "push", "automation"]
35112
- },
35113
- {
35114
- name: "security-audit",
35115
- displayName: "Security Audit",
35116
- description: "Perform security audits on codebases and infrastructure configurations",
35117
- category: "Development Tools",
35118
- tags: ["security", "audit", "vulnerabilities", "scanning"]
35119
- },
35120
- {
35121
- name: "tmux-session",
35122
- displayName: "Tmux Session",
35123
- description: "Create and manage grouped tmux sessions with workspace-aware naming and window layout guidance",
35124
- category: "Development Tools",
35125
- tags: ["tmux", "terminal", "sessions", "workspace"]
35126
- },
35127
- {
35128
- name: "validate-config",
35129
- displayName: "Validate Config",
35130
- description: "Validate configuration files for syntax and schema compliance",
35131
- category: "Development Tools",
35132
- tags: ["config", "validation", "schema", "linting"]
35133
- },
35134
- {
35135
- name: "oss-app-two-backend-storage",
35136
- displayName: "OSS App Two-Backend Storage",
35137
- description: "Recipe for the Hasna two-backend storage contract: client transport + HTTP store, server PG/SQLite backend, pg-migrations + apply script, fail-closed URL-without-key, bun bins, contract manifest, Dockerfile",
35138
- category: "Development Tools",
35139
- tags: ["storage", "backend", "postgresql", "sqlite", "two-backend", "oss-app"],
35140
- kind: "instruction"
35141
- },
35142
- {
35143
- name: "session-inject-monitor",
35144
- displayName: "Session Inject Monitor",
35145
- description: "Set up a declarative monitor that injects a prompt into a live coding-agent session when a watched source (conversations, email, todos, knowledge, command output) has new content",
35146
- category: "Development Tools",
35147
- tags: ["monitor", "session", "injection", "automation", "wake"],
35148
- kind: "instruction"
35626
+ async listArtifacts(principal, runId2) {
35627
+ return this.withContext(principal.orgId, false, async (tx) => {
35628
+ const rows = await tx`
35629
+ SELECT a.* FROM skills_artifacts a
35630
+ JOIN skills_runs r ON r.id = a.run_id AND r.org_id = ${principal.orgId}
35631
+ WHERE a.run_id = ${runId2}
35632
+ ORDER BY a.created_at ASC
35633
+ `;
35634
+ return rows.map(rowToArtifact);
35635
+ });
35149
35636
  }
35150
- ];
35151
-
35152
- // src/lib/registry-data/business-marketing.ts
35153
- var BUSINESS_MARKETING_SKILLS = [
35154
- {
35155
- name: "customer-feedback-report",
35156
- displayName: "Customer Feedback Report",
35157
- description: "Generate customer feedback reports with clusters, sentiment, root causes, roadmap recommendations, evidence, and PDF/Markdown artifacts",
35158
- category: "Business & Marketing",
35159
- kind: "instruction",
35160
- tags: ["feedback", "customer", "sentiment", "roadmap", "report"]
35161
- },
35162
- {
35163
- name: "pitch-deck",
35164
- displayName: "Pitch Deck",
35165
- description: "Generate investor and sales deck packages with slides, speaker notes, design direction, PDF, and PPTX artifacts",
35166
- category: "Business & Marketing",
35167
- kind: "instruction",
35168
- dependencies: ["market-research-report"],
35169
- tags: ["deck", "presentation", "investor", "sales", "pptx"]
35170
- },
35171
- {
35172
- name: "proposal-pack",
35173
- displayName: "Proposal Pack",
35174
- description: "Generate client proposal packages with proposal, statement of work, pricing, timeline, assumptions, cover email, and PDF/Markdown artifacts",
35175
- category: "Business & Marketing",
35176
- kind: "instruction",
35177
- tags: ["proposal", "sow", "sales", "pricing", "timeline"]
35178
- },
35179
- {
35180
- name: "seo-content-pack",
35181
- displayName: "SEO Content Pack",
35182
- description: "Generate SEO content packages with topic clusters, articles, metadata, internal links, FAQs, and publishing cadence",
35183
- category: "Business & Marketing",
35184
- kind: "instruction",
35185
- tags: ["seo", "content", "articles", "metadata"]
35186
- },
35187
- {
35188
- name: "landing-page-pack",
35189
- displayName: "Landing Page Pack",
35190
- description: "Generate landing page packages with conversion copy, wireframes, CTA maps, experiments, preview HTML, and implementation notes",
35191
- category: "Business & Marketing",
35192
- kind: "instruction",
35193
- tags: ["landing-page", "sales", "copywriting", "conversion"]
35194
- },
35195
- {
35196
- name: "ad-creative-pack",
35197
- displayName: "Ad Creative Pack",
35198
- description: "Generate paid ad packages with platform copy, creative concepts, image prompts, audience angles, and test matrices",
35199
- category: "Business & Marketing",
35200
- kind: "instruction",
35201
- tags: ["ads", "creative", "marketing", "copywriting"]
35202
- },
35203
- {
35204
- name: "email-sequence",
35205
- displayName: "Email Sequence",
35206
- description: "Generate email campaign packages with subject lines, preview text, body copy, segmentation notes, CTA variants, HTML emails, and send plans",
35207
- category: "Business & Marketing",
35208
- kind: "instruction",
35209
- tags: ["email", "marketing", "campaign", "copywriting"]
35210
- },
35211
- {
35212
- name: "social-content-calendar",
35213
- displayName: "Social Content Calendar",
35214
- description: "Generate social content calendars with daily posts, channel strategy, asset briefs, hooks, publishing schedules, and repurposing maps",
35215
- category: "Business & Marketing",
35216
- kind: "instruction",
35217
- tags: ["social", "content", "calendar", "marketing"]
35218
- },
35219
- {
35220
- name: "one-page-website",
35221
- displayName: "One Page Website",
35222
- description: "Generate static one-page website bundles with HTML, CSS, JavaScript, copy, section maps, deploy notes, and manifest",
35223
- category: "Business & Marketing",
35224
- tags: ["website", "landing-page", "static-site", "html"]
35637
+ async getArtifact(principal, runId2, artifactId2) {
35638
+ return this.withContext(principal.orgId, false, async (tx) => {
35639
+ const rows = await tx`
35640
+ SELECT a.* FROM skills_artifacts a
35641
+ JOIN skills_runs r ON r.id = a.run_id AND r.org_id = ${principal.orgId}
35642
+ WHERE a.run_id = ${runId2} AND a.id = ${artifactId2}
35643
+ LIMIT 1
35644
+ `;
35645
+ return rows[0] ? rowToArtifact(rows[0]) : null;
35646
+ });
35647
+ }
35648
+ async publishSkill(input) {
35649
+ const orgId = input.principal.orgId;
35650
+ return await this.sql.begin(async (tx) => {
35651
+ const previousRows = await tx`
35652
+ SELECT revision_id, revision_number, bundle_sha256, bundle_byte_size, skill_md, tombstoned_at
35653
+ FROM skills_registry WHERE org_id = ${orgId} AND slug = ${input.slug} LIMIT 1
35654
+ `;
35655
+ const previous = previousRows[0];
35656
+ const previousSha = typeof previous?.bundle_sha256 === "string" ? String(previous.bundle_sha256) : null;
35657
+ const previousRevisionId = typeof previous?.revision_id === "string" && previous.revision_id ? String(previous.revision_id) : null;
35658
+ const tombstoned = previous?.tombstoned_at != null;
35659
+ const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : typeof previous?.skill_md === "string" ? String(previous.skill_md) : null;
35660
+ if (previous && !tombstoned && input.expectedRevisionId !== previousRevisionId) {
35661
+ throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
35662
+ }
35663
+ const carriedSha = input.bundle?.sha256 ?? previousSha;
35664
+ if (input.version && carriedSha) {
35665
+ const existingVersion = await tx`
35666
+ SELECT bundle_sha256 FROM skills_versions WHERE org_id = ${orgId} AND slug = ${input.slug} AND version = ${input.version} LIMIT 1
35667
+ `;
35668
+ const existingSha = existingVersion[0] && typeof existingVersion[0].bundle_sha256 === "string" ? String(existingVersion[0].bundle_sha256) : null;
35669
+ if (existingSha && existingSha !== carriedSha) {
35670
+ throw new SkillVersionExistsError(input.slug, input.version, existingSha, carriedSha);
35671
+ }
35672
+ }
35673
+ const carriedSize = input.bundle?.byteSize ?? (previous?.bundle_byte_size == null ? null : Number(previous.bundle_byte_size));
35674
+ const revisionId = revisionIdOfRecord({
35675
+ slug: input.slug,
35676
+ displayName: input.displayName,
35677
+ description: input.description,
35678
+ category: input.category,
35679
+ tags: input.tags,
35680
+ source: input.source,
35681
+ kind: input.kind,
35682
+ ...input.version ? { version: input.version } : {},
35683
+ ...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
35684
+ ...carriedSha ? { bundleSha256: carriedSha } : {},
35685
+ ...carriedSize === null || carriedSize === undefined ? {} : { bundleByteSize: carriedSize }
35686
+ });
35687
+ if (input.bundle) {
35688
+ await tx`
35689
+ INSERT INTO skills_bundles (org_id, sha256, byte_size, content_type, storage_kind, storage_key, body_blob)
35690
+ VALUES (${orgId}, ${input.bundle.sha256}, ${input.bundle.byteSize}, ${input.bundle.contentType}, ${input.bundle.storageKind}, ${input.bundle.storageKey ?? null}, ${input.bundle.bytes ?? null})
35691
+ ON CONFLICT (org_id, sha256) DO UPDATE SET
35692
+ byte_size = EXCLUDED.byte_size,
35693
+ content_type = EXCLUDED.content_type,
35694
+ storage_kind = EXCLUDED.storage_kind,
35695
+ storage_key = EXCLUDED.storage_key,
35696
+ body_blob = EXCLUDED.body_blob
35697
+ `;
35698
+ }
35699
+ const rows = await tx`
35700
+ INSERT INTO skills_registry (org_id, slug, display_name, description, category, tags_json, source, kind, version, skill_md,
35701
+ bundle_sha256, bundle_byte_size, published_by_user_id, revision_id, revision_number, updated_at)
35702
+ VALUES (${orgId}, ${input.slug}, ${input.displayName}, ${input.description}, ${input.category}, ${JSON.stringify(input.tags)}::jsonb,
35703
+ ${input.source}, ${input.kind}, ${input.version ?? null}, ${carriedSkillMd},
35704
+ ${input.bundle?.sha256 ?? null}, ${input.bundle?.byteSize ?? null}, ${input.principal.userId}, ${revisionId}, 1, now())
35705
+ ON CONFLICT (org_id, slug) DO UPDATE SET
35706
+ display_name = EXCLUDED.display_name,
35707
+ description = EXCLUDED.description,
35708
+ category = EXCLUDED.category,
35709
+ tags_json = EXCLUDED.tags_json,
35710
+ source = EXCLUDED.source,
35711
+ kind = EXCLUDED.kind,
35712
+ version = EXCLUDED.version,
35713
+ skill_md = EXCLUDED.skill_md,
35714
+ -- COALESCE: see the SQLite twin. A bundle-less publish is a metadata update,
35715
+ -- not an instruction to discard the stored tarball.
35716
+ bundle_sha256 = COALESCE(EXCLUDED.bundle_sha256, skills_registry.bundle_sha256),
35717
+ bundle_byte_size = COALESCE(EXCLUDED.bundle_byte_size, skills_registry.bundle_byte_size),
35718
+ published_by_user_id = EXCLUDED.published_by_user_id,
35719
+ revision_id = EXCLUDED.revision_id,
35720
+ -- Current + 1, not EXCLUDED.revision_number: the insert path minted 1, but on
35721
+ -- the update path the ACTUAL row's counter is the truth (it may have advanced
35722
+ -- since the pre-read, which is exactly what the WHERE guard below detects).
35723
+ revision_number = skills_registry.revision_number + 1,
35724
+ tombstoned_at = NULL,
35725
+ tombstone_purge_after = NULL,
35726
+ updated_at = EXCLUDED.updated_at
35727
+ WHERE skills_registry.tombstoned_at IS NOT NULL
35728
+ OR skills_registry.revision_id = ${input.expectedRevisionId ?? NO_REVISION_SENTINEL2}
35729
+ RETURNING *
35730
+ `;
35731
+ if (!rows[0]) {
35732
+ const current = await tx`SELECT revision_id FROM skills_registry WHERE org_id = ${orgId} AND slug = ${input.slug} LIMIT 1`;
35733
+ const currentId = current[0] && typeof current[0].revision_id === "string" ? String(current[0].revision_id) : null;
35734
+ throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, currentId);
35735
+ }
35736
+ if (input.version && carriedSha) {
35737
+ await tx`
35738
+ INSERT INTO skills_versions (org_id, slug, version, bundle_sha256, bundle_byte_size, storage_kind, storage_key, manifest_json, published_by_user_id)
35739
+ VALUES (${orgId}, ${input.slug}, ${input.version}, ${carriedSha}, ${carriedSize ?? 0}, ${input.versionStorage?.storageKind ?? "db"},
35740
+ ${input.versionStorage?.storageKey ?? null}, ${JSON.stringify(input.versionManifest ?? {})}::jsonb, ${input.principal.userId})
35741
+ ON CONFLICT (org_id, slug, version) DO NOTHING
35742
+ `;
35743
+ }
35744
+ if (previousSha && input.bundle && previousSha !== input.bundle.sha256) {
35745
+ await tx`
35746
+ DELETE FROM skills_bundles
35747
+ WHERE org_id = ${orgId} AND sha256 = ${previousSha}
35748
+ AND NOT EXISTS (SELECT 1 FROM skills_registry WHERE org_id = ${orgId} AND bundle_sha256 = ${previousSha})
35749
+ AND NOT EXISTS (SELECT 1 FROM skills_versions WHERE org_id = ${orgId} AND bundle_sha256 = ${previousSha})
35750
+ `;
35751
+ }
35752
+ await tx`DELETE FROM skills_tags WHERE org_id = ${orgId} AND slug = ${input.slug}`;
35753
+ for (const tag of input.tags) {
35754
+ if (!tag.trim())
35755
+ continue;
35756
+ await tx`
35757
+ INSERT INTO skills_tags (org_id, slug, tag) VALUES (${orgId}, ${input.slug}, ${tag})
35758
+ ON CONFLICT DO NOTHING
35759
+ `;
35760
+ }
35761
+ return rowToSkill(rows[0]);
35762
+ });
35763
+ }
35764
+ async listSkills(principal) {
35765
+ await this.purgeExpiredTombstones(principal);
35766
+ const rows = await this.sql`
35767
+ SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND tombstoned_at IS NULL ORDER BY slug ASC
35768
+ `;
35769
+ return rows.map(rowToSkill);
35770
+ }
35771
+ async getSkill(principal, slug) {
35772
+ const rows = await this.sql`SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1`;
35773
+ return rows[0] ? rowToSkill(rows[0]) : null;
35774
+ }
35775
+ async updateSkill(principal, slug, patch, expectedRevisionId) {
35776
+ const current = await this.getSkill(principal, slug);
35777
+ if (!current || current.tombstonedAt)
35778
+ return null;
35779
+ if (expectedRevisionId !== current.revisionId) {
35780
+ throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
35781
+ }
35782
+ const next = { ...current, ...patch };
35783
+ return await this.sql.begin(async (tx) => {
35784
+ const revisionId = revisionIdOfRecord(next);
35785
+ const updated = await tx`
35786
+ UPDATE skills_registry
35787
+ SET display_name = ${next.displayName}, description = ${next.description}, category = ${next.category},
35788
+ tags_json = ${JSON.stringify(next.tags)}::jsonb, kind = ${next.kind}, version = ${next.version ?? null},
35789
+ skill_md = ${next.skillMd ?? null}, revision_id = ${revisionId}, revision_number = revision_number + 1, updated_at = now()
35790
+ WHERE org_id = ${principal.orgId} AND slug = ${slug} AND tombstoned_at IS NULL AND revision_id = ${current.revisionId}
35791
+ RETURNING *
35792
+ `;
35793
+ if (!updated[0]) {
35794
+ const nowRows = await tx`
35795
+ SELECT revision_id, tombstoned_at FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1
35796
+ `;
35797
+ if (nowRows[0] && nowRows[0].tombstoned_at == null) {
35798
+ const currentId = String(nowRows[0].revision_id);
35799
+ throw new SkillRevisionConflictError(slug, expectedRevisionId, currentId);
35800
+ }
35801
+ return null;
35802
+ }
35803
+ await tx`DELETE FROM skills_tags WHERE org_id = ${principal.orgId} AND slug = ${slug}`;
35804
+ for (const tag of next.tags) {
35805
+ if (!tag.trim())
35806
+ continue;
35807
+ await tx`
35808
+ INSERT INTO skills_tags (org_id, slug, tag) VALUES (${principal.orgId}, ${slug}, ${tag})
35809
+ ON CONFLICT DO NOTHING
35810
+ `;
35811
+ }
35812
+ return rowToSkill(updated[0]);
35813
+ });
35814
+ }
35815
+ async deleteSkill(principal, slug, tombstoneWindowMs) {
35816
+ return await this.sql.begin(async (tx) => {
35817
+ const existingRows = await tx`
35818
+ SELECT tombstoned_at FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1
35819
+ `;
35820
+ if (!existingRows[0])
35821
+ return null;
35822
+ if (existingRows[0].tombstoned_at != null) {
35823
+ const rows2 = await tx`SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1`;
35824
+ return rowToSkill(rows2[0]);
35825
+ }
35826
+ const rows = await tx`
35827
+ UPDATE skills_registry
35828
+ SET tombstoned_at = now(), tombstone_purge_after = now() + (${tombstoneWindowMs}::int * interval '1 millisecond'), updated_at = now()
35829
+ WHERE org_id = ${principal.orgId} AND slug = ${slug}
35830
+ RETURNING *
35831
+ `;
35832
+ return rows[0] ? rowToSkill(rows[0]) : null;
35833
+ });
35225
35834
  }
35226
- ];
35227
-
35228
- // src/lib/registry-data/productivity-organization.ts
35229
- var PRODUCTIVITY_ORGANIZATION_SKILLS = [
35230
- {
35231
- name: "meeting-pack",
35232
- displayName: "Meeting Pack",
35233
- description: "Generate meeting artifact packages with summaries, decisions, action items, follow-up email, timeline, project export, and manifest",
35234
- category: "Productivity & Organization",
35235
- kind: "instruction",
35236
- tags: ["meeting", "summary", "action-items", "decisions"]
35237
- },
35238
- {
35239
- name: "file-organizer",
35240
- displayName: "File Organizer",
35241
- description: "Organize files into structured directories based on type, date, or content",
35242
- category: "Productivity & Organization",
35243
- tags: ["files", "organization", "sorting", "cleanup"]
35244
- },
35245
- {
35246
- name: "folder-tree",
35247
- displayName: "Folder Tree",
35248
- description: "Generate and display folder tree structures for documentation",
35249
- category: "Productivity & Organization",
35250
- tags: ["folder", "tree", "structure", "visualization"]
35251
- },
35252
- {
35253
- name: "form-filler",
35254
- displayName: "Form Filler",
35255
- description: "Automatically fill out web forms and document templates",
35256
- category: "Productivity & Organization",
35257
- tags: ["forms", "automation", "filling", "data-entry"]
35258
- },
35259
- {
35260
- name: "merge-pdfs",
35261
- displayName: "Merge PDFs",
35262
- description: "Merge multiple PDF files into a single document",
35263
- category: "Productivity & Organization",
35264
- tags: ["pdf", "merge", "documents", "combining"]
35265
- },
35266
- {
35267
- name: "split-pdf",
35268
- displayName: "Split PDF",
35269
- description: "Split PDF documents into separate pages or sections",
35270
- category: "Productivity & Organization",
35271
- tags: ["pdf", "split", "documents", "pages"]
35835
+ async purgeExpiredTombstones(principal) {
35836
+ return await this.sql.begin(async (tx) => {
35837
+ const expiredRows = await tx`
35838
+ SELECT * FROM skills_registry
35839
+ WHERE org_id = ${principal.orgId} AND tombstoned_at IS NOT NULL AND tombstone_purge_after <= now()
35840
+ `;
35841
+ if (!expiredRows.length)
35842
+ return [];
35843
+ const purged = [];
35844
+ for (const row of expiredRows) {
35845
+ const record = rowToSkill(row);
35846
+ await tx`
35847
+ DELETE FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${record.slug} AND tombstone_purge_after <= now()
35848
+ `;
35849
+ await tx`DELETE FROM skills_tags WHERE org_id = ${principal.orgId} AND slug = ${record.slug}`;
35850
+ await tx`
35851
+ DELETE FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${record.slug} AND tombstone_purge_after <= now()
35852
+ `;
35853
+ if (record.bundleSha256) {
35854
+ await tx`
35855
+ DELETE FROM skills_bundles
35856
+ WHERE org_id = ${principal.orgId} AND sha256 = ${record.bundleSha256}
35857
+ AND NOT EXISTS (SELECT 1 FROM skills_registry WHERE org_id = ${principal.orgId} AND bundle_sha256 = ${record.bundleSha256})
35858
+ AND NOT EXISTS (SELECT 1 FROM skills_versions WHERE org_id = ${principal.orgId} AND bundle_sha256 = ${record.bundleSha256})
35859
+ `;
35860
+ }
35861
+ purged.push(record);
35862
+ }
35863
+ return purged;
35864
+ });
35272
35865
  }
35273
- ];
35274
-
35275
- // src/lib/registry-data/project-management.ts
35276
- var PROJECT_MANAGEMENT_SKILLS = [
35277
- {
35278
- name: "businessactivity",
35279
- displayName: "Business Activity",
35280
- description: "Business activity, workflow, and ownership management service",
35281
- category: "Project Management",
35282
- tags: ["business", "workflow", "activities", "management"]
35283
- },
35284
- {
35285
- name: "implementation",
35286
- displayName: "Implementation",
35287
- description: "Create .implementation scaffold for project development tracking",
35288
- category: "Project Management",
35289
- tags: ["implementation", "tracking", "scaffold", "project"]
35290
- },
35291
- {
35292
- name: "implementation-plan",
35293
- displayName: "Implementation Plan",
35294
- description: "Generate detailed implementation plans with phases and milestones",
35295
- category: "Project Management",
35296
- tags: ["implementation", "planning", "milestones", "phases"]
35297
- },
35298
- {
35299
- name: "implementation-todo",
35300
- displayName: "Implementation Todo",
35301
- description: "Manage implementation task lists and todo items",
35302
- category: "Project Management",
35303
- tags: ["implementation", "todo", "tasks", "tracking"]
35304
- },
35305
- {
35306
- name: "todos-plan",
35307
- displayName: "Todos Plan",
35308
- description: "Author, sync, route, and verify Todos plans using Todos CLI plan IDs as source of truth",
35309
- category: "Project Management",
35310
- tags: ["todos", "plans", "tasks", "verification", "workflow"]
35866
+ async getSkillBundle(principal, sha256) {
35867
+ const rows = await this.sql`SELECT * FROM skills_bundles WHERE org_id = ${principal.orgId} AND sha256 = ${sha256} LIMIT 1`;
35868
+ return rows[0] ? rowToSkillBundle(rows[0]) : null;
35311
35869
  }
35312
- ];
35313
-
35314
- // src/lib/registry-data/content-generation.ts
35315
- var CONTENT_GENERATION_SKILLS = [
35316
- {
35317
- name: "pdf-generate",
35318
- displayName: "PDF Generate",
35319
- description: "Generate PDF documents with rich formatting and layouts",
35320
- category: "Content Generation",
35321
- tags: ["pdf", "document", "generation", "formatting"]
35322
- },
35323
- {
35324
- name: "slide-deck-generator",
35325
- displayName: "Slide Deck Generator",
35326
- description: "Generate slide decks from briefs, docs, or outlines with PDF, PPTX, speaker notes, and structured slide metadata",
35327
- category: "Content Generation",
35328
- tags: ["presentation", "slides", "deck", "documents"]
35329
- },
35330
- {
35331
- name: "generate-qrcode",
35332
- displayName: "Generate QR Code",
35333
- description: "Generate QR codes with custom styling and embedded data",
35334
- category: "Content Generation",
35335
- tags: ["qrcode", "generation", "encoding", "visual"]
35336
- },
35337
- {
35338
- name: "generate-resume",
35339
- displayName: "Generate Resume",
35340
- description: "Generate professional resumes with formatting and content optimization",
35341
- category: "Content Generation",
35342
- tags: ["resume", "cv", "career", "generation"]
35870
+ async listSkillVersions(principal, slug) {
35871
+ const rows = await this.sql`
35872
+ SELECT * FROM skills_versions WHERE org_id = ${principal.orgId} AND slug = ${slug}
35873
+ ORDER BY created_at DESC, version DESC
35874
+ `;
35875
+ return rows.map((row) => rowToSkillVersion(row));
35343
35876
  }
35344
- ];
35345
-
35346
- // src/lib/registry-data/finance-compliance.ts
35347
- var FINANCE_COMPLIANCE_SKILLS = [
35348
- {
35349
- name: "contract-review-report",
35350
- displayName: "Contract Review Report",
35351
- description: "Generate contract review reports with risk register, clause summary, redline suggestions, negotiation email, and manifest artifacts",
35352
- category: "Finance & Compliance",
35353
- kind: "instruction",
35354
- tags: ["contract", "legal", "review", "risk"]
35355
- },
35356
- {
35357
- name: "invoice",
35358
- displayName: "Invoice",
35359
- description: "Generate professional invoices with company management and PDF export",
35360
- category: "Finance & Compliance",
35361
- tags: ["invoice", "billing", "pdf", "finance"]
35362
- },
35363
- {
35364
- name: "invoice-reconciliation",
35365
- displayName: "Invoice Reconciliation",
35366
- description: "Generate invoice reconciliation reports with matched payments, discrepancies, anomaly notes, summaries, and manifest artifacts",
35367
- category: "Finance & Compliance",
35368
- tags: ["invoice", "payments", "reconciliation", "finance"]
35877
+ async getSkillVersion(principal, slug, version2) {
35878
+ const rows = await this.sql`
35879
+ SELECT * FROM skills_versions WHERE org_id = ${principal.orgId} AND slug = ${slug} AND version = ${version2} LIMIT 1
35880
+ `;
35881
+ return rows[0] ? rowToSkillVersion(rows[0]) : null;
35369
35882
  }
35370
- ];
35371
-
35372
- // src/lib/registry-data/data-analysis.ts
35373
- var DATA_ANALYSIS_SKILLS = [
35374
- {
35375
- name: "analyze-data",
35376
- displayName: "Analyze Data",
35377
- description: "Data science insights for CSV and JSON datasets with statistical analysis",
35378
- category: "Data & Analysis",
35379
- tags: ["data", "analysis", "csv", "json", "statistics"]
35380
- },
35381
- {
35382
- name: "dashboard-builder",
35383
- displayName: "Dashboard Builder",
35384
- description: "Build data dashboards with charts, metrics, and visualizations",
35385
- category: "Data & Analysis",
35386
- tags: ["dashboard", "visualization", "charts", "metrics"]
35387
- },
35388
- {
35389
- name: "data-anonymizer",
35390
- displayName: "Data Anonymizer",
35391
- description: "Anonymize sensitive data in datasets for privacy compliance",
35392
- category: "Data & Analysis",
35393
- tags: ["anonymization", "privacy", "data", "compliance"]
35394
- },
35395
- {
35396
- name: "generate-chart",
35397
- displayName: "Generate Chart",
35398
- description: "Generate data charts and visualizations from datasets",
35399
- category: "Data & Analysis",
35400
- tags: ["charts", "visualization", "data", "graphs"]
35401
- },
35402
- {
35403
- name: "read-csv",
35404
- displayName: "Read CSV",
35405
- description: "Parse CSV files into structured JSON with delimiter and encoding detection",
35406
- category: "Data & Analysis",
35407
- tags: ["csv", "parsing", "tabular", "data"]
35408
- },
35409
- {
35410
- name: "read-excel",
35411
- displayName: "Read Excel",
35412
- description: "Parse XLS and XLSX workbooks into structured JSON with sheet and formatted cell metadata",
35413
- category: "Data & Analysis",
35414
- tags: ["excel", "spreadsheet", "xlsx", "data"]
35415
- },
35416
- {
35417
- name: "pdf-to-markdown",
35418
- displayName: "PDF to Markdown",
35419
- description: "Convert PDFs into clean markdown with remote extraction and structure cleanup",
35420
- category: "Data & Analysis",
35421
- tags: ["pdf", "markdown", "conversion"]
35422
- },
35423
- {
35424
- name: "pdf-to-dataset",
35425
- displayName: "PDF to Dataset",
35426
- description: "Extract PDF tables, forms, invoices, and semi-structured content into CSV and JSON datasets",
35427
- category: "Data & Analysis",
35428
- tags: ["pdf", "dataset", "csv", "json", "extraction"]
35429
- },
35430
- {
35431
- name: "doc-read",
35432
- displayName: "Doc Read",
35433
- description: "Read and extract text from DOCX files with section parsing and metadata extraction",
35434
- category: "Data & Analysis",
35435
- tags: ["docx", "reader", "extraction", "word"]
35883
+ async pinSkill(principal, slug, metadata = {}) {
35884
+ const rows = await this.sql`
35885
+ INSERT INTO skills_pins (org_id, principal, slug, pinned_at, metadata_json)
35886
+ VALUES (${principal.orgId}, ${principal.apiKeyId}, ${slug}, now(), ${JSON.stringify(metadata)}::jsonb)
35887
+ ON CONFLICT (org_id, principal, slug) DO UPDATE SET
35888
+ pinned_at = now(),
35889
+ metadata_json = EXCLUDED.metadata_json
35890
+ RETURNING *
35891
+ `;
35892
+ return rowToPin(rows[0]);
35436
35893
  }
35437
- ];
35438
-
35439
- // src/lib/registry-data/media-processing.ts
35440
- var MEDIA_PROCESSING_SKILLS = [
35441
- {
35442
- name: "video-highlight-pack",
35443
- displayName: "Video Highlight Pack",
35444
- description: "Generate video highlight packages with clip plans, captions, thumbnail briefs, chapter markers, social posts, and edit decisions",
35445
- category: "Media Processing",
35446
- kind: "instruction",
35447
- tags: ["video", "highlights", "clips", "captions"]
35448
- },
35449
- {
35450
- name: "compress-video",
35451
- displayName: "Compress Video",
35452
- description: "Compress video files while preserving visual quality using ffmpeg",
35453
- category: "Media Processing",
35454
- tags: ["video", "compression", "ffmpeg", "optimization"]
35455
- },
35456
- {
35457
- name: "audio-extract",
35458
- displayName: "Audio Extract",
35459
- description: "Extract audio tracks from video files with multiple format support",
35460
- category: "Media Processing",
35461
- tags: ["audio", "extraction", "video", "conversion"]
35462
- },
35463
- {
35464
- name: "extract-frames",
35465
- displayName: "Extract Frames",
35466
- description: "Extract frames from video files at specified intervals or timestamps",
35467
- category: "Media Processing",
35468
- tags: ["frames", "video", "extraction", "images"]
35469
- },
35470
- {
35471
- name: "gif-maker",
35472
- displayName: "GIF Maker",
35473
- description: "Create animated GIFs from images, videos, or screen recordings",
35474
- category: "Media Processing",
35475
- tags: ["gif", "animation", "images", "video"]
35476
- },
35477
- {
35478
- name: "video-downloader",
35479
- displayName: "Video Downloader",
35480
- description: "Download videos from various online platforms and services",
35481
- category: "Media Processing",
35482
- tags: ["video", "download", "platforms", "media"]
35483
- },
35484
- {
35485
- name: "watermark",
35486
- displayName: "Watermark",
35487
- description: "Add watermarks to images and documents for copyright protection",
35488
- category: "Media Processing",
35489
- tags: ["watermark", "protection", "copyright", "images"]
35894
+ async unpinSkill(principal, slug) {
35895
+ const rows = await this.sql`
35896
+ DELETE FROM skills_pins WHERE org_id = ${principal.orgId} AND principal = ${principal.apiKeyId} AND slug = ${slug}
35897
+ RETURNING 1 AS present
35898
+ `;
35899
+ return rows.length > 0;
35490
35900
  }
35491
- ];
35492
-
35493
- // src/lib/registry-data/design-branding.ts
35494
- var DESIGN_BRANDING_SKILLS = [
35495
- {
35496
- name: "brand-kit",
35497
- displayName: "Brand Kit",
35498
- description: "Generate brand kits with logo usage, palette, typography, brand voice, sample applications, Markdown guide, PDF guide, and SVG assets",
35499
- category: "Design & Branding",
35500
- kind: "instruction",
35501
- tags: ["brand", "design", "palette", "typography"]
35502
- },
35503
- {
35504
- name: "brand-assets",
35505
- displayName: "Brand Assets",
35506
- description: "Fetch official brand assets from a website or brand name with logos, PNG sizes, palette, typography, source metadata, and manifests",
35507
- category: "Design & Branding",
35508
- tags: ["brand", "logo", "assets", "palette", "typography"]
35509
- },
35510
- {
35511
- name: "logo-design",
35512
- displayName: "Logo Design",
35513
- description: "Generate multi-variant logo packages with transparent PNGs, vector-style SVGs, usage notes, and manifests",
35514
- category: "Design & Branding",
35515
- tags: ["logo", "design", "branding", "identity"]
35516
- },
35517
- {
35518
- name: "generate-favicon",
35519
- displayName: "Generate Favicon",
35520
- description: "Generate favicons in multiple sizes and formats for websites",
35521
- category: "Design & Branding",
35522
- tags: ["favicon", "icon", "design", "web"]
35523
- },
35524
- {
35525
- name: "product-mockup",
35526
- displayName: "Product Mockup",
35527
- description: "Generate product mockup packages with visual variants, prompts, usage notes, and asset metadata",
35528
- category: "Design & Branding",
35529
- tags: ["product", "mockup", "visualization", "marketing"]
35530
- },
35531
- {
35532
- name: "siteanalyze",
35533
- displayName: "Site Analyze",
35534
- description: "Analyze any website's design system \u2014 detects shadcn/ui, Tailwind, extracts colors, typography, and components via Playwright + Claude Vision.",
35535
- category: "Design & Branding",
35536
- tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "styles"]
35901
+ async listPins(principal) {
35902
+ const rows = await this.sql`
35903
+ SELECT * FROM skills_pins WHERE org_id = ${principal.orgId} AND principal = ${principal.apiKeyId} ORDER BY slug ASC
35904
+ `;
35905
+ return rows.map(rowToPin);
35537
35906
  }
35538
- ];
35539
-
35540
- // src/lib/registry-data/web-browser.ts
35541
- var WEB_BROWSER_SKILLS = [
35542
- {
35543
- name: "domainpurchase",
35544
- displayName: "Domain Purchase",
35545
- description: "Purchase and manage domains via registrar connectors",
35546
- category: "Web & Browser",
35547
- tags: ["domain", "purchase", "registrar", "management"]
35907
+ async listTags(principal) {
35908
+ await this.purgeExpiredTombstones(principal);
35909
+ const rows = await this.sql`
35910
+ SELECT DISTINCT tag FROM skills_tags WHERE org_id = ${principal.orgId} ORDER BY tag ASC
35911
+ `;
35912
+ return rows.map((row) => String(row.tag));
35548
35913
  }
35549
- ];
35550
-
35551
- // src/lib/registry-data/research-writing.ts
35552
- var RESEARCH_WRITING_SKILLS = [
35553
- {
35554
- name: "blog-article",
35555
- displayName: "Blog Article",
35556
- description: "Create SEO-optimized blog article artifact packages",
35557
- category: "Research & Writing",
35558
- kind: "instruction",
35559
- tags: ["blog", "article", "writing", "seo"]
35560
- },
35561
- {
35562
- name: "market-research-report",
35563
- displayName: "Market Research Report",
35564
- description: "Generate market research packages with competitor tables, positioning, pricing notes, source notes, and PDF/Markdown artifacts",
35565
- category: "Research & Writing",
35566
- kind: "instruction",
35567
- tags: ["market-research", "competitors", "positioning", "pricing", "report"]
35914
+ async listSkillsByTag(principal, tag) {
35915
+ await this.purgeExpiredTombstones(principal);
35916
+ const rows = await this.sql`
35917
+ SELECT s.* FROM skills_registry s
35918
+ JOIN skills_tags t ON t.org_id = s.org_id AND t.slug = s.slug
35919
+ WHERE t.org_id = ${principal.orgId} AND t.tag = ${tag} AND s.tombstoned_at IS NULL
35920
+ ORDER BY s.slug ASC
35921
+ `;
35922
+ return rows.map(rowToSkill);
35568
35923
  }
35569
- ];
35570
-
35571
- // src/lib/registry-data/science-academic.ts
35572
- var SCIENCE_ACADEMIC_SKILLS = [
35573
- {
35574
- name: "bio-sequence-tool",
35575
- displayName: "Bio Sequence Tool",
35576
- description: "Analyze and manipulate biological sequences including DNA, RNA, and protein data",
35577
- category: "Science & Academic",
35578
- tags: ["biology", "dna", "sequence", "bioinformatics"]
35579
- },
35580
- {
35581
- name: "experiment-power-calculator",
35582
- displayName: "Experiment Power Calculator",
35583
- description: "Calculate statistical power and sample size for experiments",
35584
- category: "Science & Academic",
35585
- tags: ["experiment", "statistics", "power-analysis", "sample-size"]
35586
- },
35587
- {
35588
- name: "latex-table-generator",
35589
- displayName: "LaTeX Table Generator",
35590
- description: "Generate formatted LaTeX tables from data for academic papers",
35591
- category: "Science & Academic",
35592
- tags: ["latex", "tables", "academic", "formatting"]
35593
- },
35594
- {
35595
- name: "scientific-figure-check",
35596
- displayName: "Scientific Figure Check",
35597
- description: "Validate scientific figures for accuracy, formatting, and publication standards",
35598
- category: "Science & Academic",
35599
- tags: ["scientific", "figures", "validation", "publishing"]
35600
- },
35601
- {
35602
- name: "statistical-test-selector",
35603
- displayName: "Statistical Test Selector",
35604
- description: "Recommend appropriate statistical tests based on data and research questions",
35605
- category: "Science & Academic",
35606
- tags: ["statistics", "test-selection", "research", "analysis"]
35924
+ async listPinsByTag(principal, tag) {
35925
+ await this.purgeExpiredTombstones(principal);
35926
+ const rows = await this.sql`
35927
+ SELECT p.* FROM skills_pins p
35928
+ JOIN skills_tags t ON t.org_id = p.org_id AND t.slug = p.slug
35929
+ JOIN skills_registry s ON s.org_id = p.org_id AND s.slug = p.slug
35930
+ WHERE p.org_id = ${principal.orgId} AND p.principal = ${principal.apiKeyId}
35931
+ AND t.tag = ${tag} AND s.tombstoned_at IS NULL
35932
+ ORDER BY p.slug ASC
35933
+ `;
35934
+ return rows.map(rowToPin);
35607
35935
  }
35608
- ];
35609
-
35610
- // src/lib/registry-data/education-learning.ts
35611
- var EDUCATION_LEARNING_SKILLS = [];
35612
-
35613
- // src/lib/registry-data/communication.ts
35614
- var COMMUNICATION_SKILLS = [];
35615
-
35616
- // src/lib/registry-data/health-wellness.ts
35617
- var HEALTH_WELLNESS_SKILLS = [];
35618
-
35619
- // src/lib/registry-data/travel-lifestyle.ts
35620
- var TRAVEL_LIFESTYLE_SKILLS = [];
35621
-
35622
- // src/lib/registry-data/event-management.ts
35623
- var EVENT_MANAGEMENT_SKILLS = [];
35624
-
35625
- // src/lib/registry-data/index.ts
35626
- var SKILLS = [
35627
- ...DEVELOPMENT_TOOLS_SKILLS,
35628
- ...BUSINESS_MARKETING_SKILLS,
35629
- ...PRODUCTIVITY_ORGANIZATION_SKILLS,
35630
- ...PROJECT_MANAGEMENT_SKILLS,
35631
- ...CONTENT_GENERATION_SKILLS,
35632
- ...FINANCE_COMPLIANCE_SKILLS,
35633
- ...DATA_ANALYSIS_SKILLS,
35634
- ...MEDIA_PROCESSING_SKILLS,
35635
- ...DESIGN_BRANDING_SKILLS,
35636
- ...WEB_BROWSER_SKILLS,
35637
- ...RESEARCH_WRITING_SKILLS,
35638
- ...SCIENCE_ACADEMIC_SKILLS,
35639
- ...EDUCATION_LEARNING_SKILLS,
35640
- ...COMMUNICATION_SKILLS,
35641
- ...HEALTH_WELLNESS_SKILLS,
35642
- ...TRAVEL_LIFESTYLE_SKILLS,
35643
- ...EVENT_MANAGEMENT_SKILLS
35644
- ];
35936
+ async listPublishedSlugs(principal) {
35937
+ const rows = await this.sql`
35938
+ SELECT slug FROM skills_registry WHERE org_id = ${principal.orgId} AND tombstoned_at IS NULL ORDER BY slug ASC
35939
+ `;
35940
+ return rows.map((row) => String(row.slug));
35941
+ }
35942
+ async collectOrphanBundle(orgId, sha256) {
35943
+ await this.sql`
35944
+ DELETE FROM skills_bundles
35945
+ WHERE org_id = ${orgId} AND sha256 = ${sha256}
35946
+ AND NOT EXISTS (SELECT 1 FROM skills_registry WHERE org_id = ${orgId} AND bundle_sha256 = ${sha256})
35947
+ AND NOT EXISTS (SELECT 1 FROM skills_versions WHERE org_id = ${orgId} AND bundle_sha256 = ${sha256})
35948
+ `;
35949
+ }
35950
+ }
35951
+ var NO_REVISION_SENTINEL2 = "0000000000000000000000000000000000000000000000000000000000000000";
35952
+ function isUniqueViolation(error) {
35953
+ const code = error?.code;
35954
+ if (code === "23505" || code === 23505)
35955
+ return true;
35956
+ const message = error instanceof Error ? error.message : String(error);
35957
+ return /duplicate key value violates unique constraint/i.test(message);
35958
+ }
35959
+ function connectionFailureSummary(error) {
35960
+ const message = error instanceof Error ? error.message : String(error);
35961
+ return message.replace(/[a-z][a-z0-9+.-]*:\/\/\S*/gi, "<redacted-url>").split(`
35962
+ `)[0].slice(0, 200);
35963
+ }
35645
35964
 
35646
- // src/lib/hosted-skill-set.ts
35647
- var HOSTED_RUNTIMES = new Set(["hosted"]);
35648
- var HOSTED_SOURCES = new Set(["remote", "private-hosted"]);
35649
- var HOSTED_METADATA_SET_EMPTY_ERROR = [
35650
- "The hosted metadata skill set is empty, but the packaging guards that depend on it",
35651
- "only mean anything while it is non-empty: an empty set makes every one of them pass",
35652
- "vacuously and silently removes the protection that keeps hosted implementation source",
35653
- "out of the published package.",
35654
- "",
35655
- "If emptying the set is intentional (every hosted skill was deleted or converted to a",
35656
- "runnable in-repo skill), retire the derived guards in the SAME change \u2014 do not let them",
35657
- "stay green and empty."
35658
- ].join(`
35659
- `);
35660
- var SLUG = "[^,{}/]+";
35661
- var BRACE_SOURCE_EXCLUSION = new RegExp(`^!skills/\\{(${SLUG}(?:,${SLUG})+)\\}/src$`);
35662
- var SINGLE_SOURCE_EXCLUSION = new RegExp(`^!skills/(${SLUG})/src$`);
35965
+ // src/server/redaction.ts
35966
+ var SECRET_PATTERNS = [
35967
+ /\bsk-[A-Za-z0-9_-]{8,}\b/g,
35968
+ /\bsk_[A-Za-z0-9_-]{8,}\b/g,
35969
+ /\bgh[opsur]_[A-Za-z0-9_]{8,}\b/g,
35970
+ /\bgithub_pat_[A-Za-z0-9_]{8,}\b/g,
35971
+ /\bnpm_[A-Za-z0-9_]{8,}\b/g,
35972
+ /\bAKIA[A-Z0-9]{12,}\b/g,
35973
+ /\bAIza[A-Za-z0-9_-]{10,}\b/g,
35974
+ /\b[A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|DATABASE_URL)[A-Z0-9_]*\s*[:=]\s*[^ \n\r\t]+/gi,
35975
+ /\bhttps?:\/\/[^ \n\r\t]+X-Amz-Signature=[^ \n\r\t]+/gi
35976
+ ];
35977
+ function redactForClient(value) {
35978
+ const text = typeof value === "string" ? value : JSON.stringify(value);
35979
+ return SECRET_PATTERNS.reduce((current, pattern) => current.replace(pattern, "credential"), text).replace(/\s+$/g, "").slice(0, 20000);
35980
+ }
35663
35981
 
35664
- // src/lib/skill-validation.ts
35665
- var RESERVED_SKILL_ENTRIES = new Set([
35666
- ".env",
35667
- ".npmrc",
35668
- ".pypirc",
35669
- ".netrc",
35670
- "id_rsa",
35671
- "id_ed25519"
35672
- ]);
35673
- var KNOWN_TOP_LEVEL_ENTRIES = new Set([
35674
- ".claude",
35675
- ".env.example",
35676
- ".gitignore",
35677
- ".skills",
35678
- "CLAUDE.md",
35679
- "AGENTS.md",
35680
- "LICENSE",
35681
- "PROJECT_OVERVIEW.md",
35682
- "QUICKSTART.md",
35683
- "README.md",
35684
- "SKILL.md",
35685
- "api-docs-list.json",
35686
- "auth.ts",
35687
- "bun.lock",
35688
- "bunfig.toml",
35689
- "data",
35690
- "dist",
35691
- "examples",
35692
- "exports",
35693
- "http-client.ts",
35694
- "index.ts",
35695
- "install.sh",
35696
- "installer.ts",
35697
- "logs",
35698
- "node_modules",
35699
- "package.json",
35700
- "scripts",
35701
- "skill-install.ts",
35702
- "skill.json",
35703
- "src",
35704
- "tests",
35705
- "references",
35706
- "assets",
35707
- "tsconfig.json",
35708
- "vision.ts"
35982
+ // src/server/handlers.ts
35983
+ var DETERMINISTIC_TEXT_HANDLERS = new Set([
35984
+ "audio-transcript-pack",
35985
+ "transcript",
35986
+ "video-highlight-pack"
35709
35987
  ]);
35710
- var VALID_PROVENANCE_SOURCES = new Set(["official", "custom", "remote", "private", "private-hosted", "upstream", "extension"]);
35988
+ async function executeRun(store, run, storage = new ArtifactStorage) {
35989
+ try {
35990
+ await store.appendLog(run.id, run.orgId, "info", redactForClient(`starting run ${run.skill}`));
35991
+ if (!DETERMINISTIC_TEXT_HANDLERS.has(run.skill)) {
35992
+ await store.appendLog(run.id, run.orgId, "warn", `${run.skill} has no provider-free handler yet`);
35993
+ return await failRun(store, run, "HANDLER_UNAVAILABLE", "This deployment has no safe provider-free handler for that skill yet.");
35994
+ }
35995
+ const text = extractText(run);
35996
+ if (!text.trim()) {
35997
+ return await failRun(store, run, "INVALID_INPUT", "Provide transcript text with input.transcript, input.text, --text, --source, or positional text.");
35998
+ }
35999
+ const title = extractOption(run.args, "--title") || stringInput(run.input, "title") || "Skills run";
36000
+ const summary = summarize(text);
36001
+ const artifacts = [
36002
+ textArtifact(run, "transcript.md", `# ${title}
35711
36003
 
35712
- // src/lib/skill-hash.ts
35713
- var HASH_EXCLUDE_DIRS = new Set([".git", "node_modules", "dist", "build", ".turbo"]);
36004
+ ${text.trim()}
36005
+ `),
36006
+ textArtifact(run, "summary.md", `# Summary
35714
36007
 
35715
- // src/lib/portable-skills-files.ts
35716
- var ANY_SEGMENT_COPY_EXCLUDES = new Set([
35717
- ".git",
35718
- ".DS_Store",
35719
- ".system",
35720
- "node_modules"
35721
- ]);
35722
- var FIRST_SEGMENT_COPY_EXCLUDES = new Set([
35723
- "dist",
35724
- "build",
35725
- ".turbo"
35726
- ]);
35727
- // src/lib/portable-skills.ts
35728
- var OFFICIAL_SKILL_NAMES = new Set(SKILLS.map((skill) => skill.name));
35729
- // src/lib/installer.ts
35730
- import { existsSync as existsSync3, readFileSync as readFileSync4, rmSync } from "fs";
35731
- import { dirname as dirname5, join as join6 } from "path";
35732
- import { fileURLToPath as fileURLToPath2 } from "url";
35733
- var __dirname2 = dirname5(fileURLToPath2(import.meta.url));
35734
- function findSkillsDir() {
35735
- let dir = __dirname2;
35736
- for (let i3 = 0;i3 < 5; i3++) {
35737
- const candidate = join6(dir, "skills");
35738
- if (existsSync3(candidate) && !dir.includes(".skills"))
35739
- return candidate;
35740
- dir = dirname5(dir);
36008
+ ${summary}
36009
+ `),
36010
+ textArtifact(run, "show-notes.md", `# Show Notes
36011
+
36012
+ - ${summary}
36013
+ - Generated by the skills deterministic worker.
36014
+ `),
36015
+ textArtifact(run, "clips.csv", `start,end,title,summary
36016
+ 00:00,00:30,"Opening","${csv(summary)}"
36017
+ `),
36018
+ textArtifact(run, "manifest.json", JSON.stringify({
36019
+ runId: run.id,
36020
+ skill: run.skill,
36021
+ requestedSlug: run.requestedSlug,
36022
+ generatedAt: new Date().toISOString(),
36023
+ artifacts: ["transcript.md", "summary.md", "show-notes.md", "clips.csv"]
36024
+ }, null, 2) + `
36025
+ `, "application/json")
36026
+ ];
36027
+ for (const artifact of artifacts) {
36028
+ await store.addArtifact(await storage.materialize(run, artifact.meta, artifact.body));
36029
+ }
36030
+ await store.appendLog(run.id, run.orgId, "info", `generated ${artifacts.length} artifacts`);
36031
+ return await completeRun(store, run, summary);
36032
+ } catch (error) {
36033
+ return await failRun(store, run, "WORKER_ERROR", redactForClient(error.message));
35741
36034
  }
35742
- return join6(__dirname2, "..", "skills");
35743
36035
  }
35744
- var SKILLS_DIR = findSkillsDir();
35745
-
35746
- // src/server/skills-api.ts
35747
- var MAX_SKILL_MD_BYTES = 512000;
35748
- var MAX_MANIFEST_BYTES = MAX_SKILL_MD_BYTES + 64000;
35749
- var ALLOWED_PUBLISH_PARTS = new Set(["manifest", "bundle"]);
36036
+ function textArtifact(run, relativePath, bodyText, contentType = relativePath.endsWith(".json") ? "application/json" : "text/markdown; charset=utf-8") {
36037
+ const bytes = new TextEncoder().encode(bodyText);
36038
+ return {
36039
+ meta: {
36040
+ id: createArtifactId(),
36041
+ runId: run.id,
36042
+ orgId: run.orgId,
36043
+ fileName: relativePath.split("/").pop() || relativePath,
36044
+ relativePath,
36045
+ contentType,
36046
+ byteSize: bytes.byteLength,
36047
+ sha256: createHash5("sha256").update(bytes).digest("hex"),
36048
+ visibility: "private"
36049
+ },
36050
+ body: { relativePath, bodyText, contentType }
36051
+ };
36052
+ }
36053
+ async function completeRun(store, run, preview) {
36054
+ const next = await fencedTransition(store, run, {
36055
+ status: "succeeded",
36056
+ outputType: "artifact_bundle",
36057
+ outputPreview: preview,
36058
+ completedAt: new Date().toISOString()
36059
+ });
36060
+ return next ?? run;
36061
+ }
36062
+ async function failRun(store, run, code, message) {
36063
+ try {
36064
+ await store.appendLog(run.id, run.orgId, "error", message);
36065
+ } catch {}
36066
+ const next = await fencedTransition(store, run, {
36067
+ status: "failed",
36068
+ errorCode: code,
36069
+ errorMessage: message,
36070
+ completedAt: new Date().toISOString()
36071
+ });
36072
+ return next ?? run;
36073
+ }
36074
+ async function fencedTransition(store, run, patch) {
36075
+ if (!store.transitionRun)
36076
+ return store.updateRun(run.id, patch);
36077
+ const next = await store.transitionRun(run.id, patch, run.leaseGeneration);
36078
+ if (!next) {
36079
+ try {
36080
+ await store.appendLog(run.id, run.orgId, "warn", `late write rejected: run no longer owned at lease_generation ${run.leaseGeneration}`);
36081
+ } catch {}
36082
+ }
36083
+ return next;
36084
+ }
36085
+ function extractText(run) {
36086
+ return stringInput(run.input, "transcript") || stringInput(run.input, "text") || extractOption(run.args, "--text") || extractOption(run.args, "--source") || run.args.filter((arg) => !arg.startsWith("--")).join(" ");
36087
+ }
36088
+ function stringInput(input, key) {
36089
+ const value = input[key];
36090
+ return typeof value === "string" && value.trim() ? value : undefined;
36091
+ }
36092
+ function extractOption(args, flag) {
36093
+ for (let i3 = 0;i3 < args.length; i3++) {
36094
+ const arg = args[i3];
36095
+ if (arg === flag && args[i3 + 1])
36096
+ return args[i3 + 1];
36097
+ if (arg.startsWith(`${flag}=`))
36098
+ return arg.slice(flag.length + 1);
36099
+ }
36100
+ return;
36101
+ }
36102
+ function summarize(text) {
36103
+ const normalized = redactForClient(text).replace(/\s+/g, " ").trim();
36104
+ if (normalized.length <= 220)
36105
+ return normalized;
36106
+ return `${normalized.slice(0, 217)}...`;
36107
+ }
36108
+ function csv(value) {
36109
+ return value.replace(/"/g, '""');
36110
+ }
35750
36111
 
35751
36112
  // src/server/app.ts
35752
36113
  function assertDurableStore(store, config) {
@@ -35774,6 +36135,26 @@ async function runWorkerOnce(store, workerId = `worker_${randomUUID4().slice(0,
35774
36135
  return true;
35775
36136
  }
35776
36137
  if (import.meta.main) {
36138
+ const EARLY_ARGV = process.argv.slice(2);
36139
+ if (EARLY_ARGV.includes("--version") || EARLY_ARGV.includes("-V")) {
36140
+ console.log(package_default.version);
36141
+ process.exit(0);
36142
+ }
36143
+ if (EARLY_ARGV.includes("--help") || EARLY_ARGV.includes("-h")) {
36144
+ console.log(`Usage: skills-worker [options]
36145
+
36146
+ Drains @hasna/skills runs from the store.
36147
+
36148
+ Options:
36149
+ -V, --version output the version number
36150
+ -h, --help display help for command
36151
+ --once process exactly one run, then exit
36152
+
36153
+ Environment:
36154
+ HASNA_SKILLS_WORKER_ID Worker identity (default: worker_<random>)
36155
+ HASNA_SKILLS_WORKER_IDLE_MS Idle pause between polls (default: 1000)`);
36156
+ process.exit(0);
36157
+ }
35777
36158
  const config = resolveServerConfig();
35778
36159
  const store = await createStore({ databaseUrl: config.databaseUrl, bootstrapApiKey: config.bootstrapApiKey });
35779
36160
  assertDurableStore(store, config);
@@ -35794,13 +36175,13 @@ if (import.meta.main) {
35794
36175
  console.error(`worker ${workerId}: claim/execute failed (${consecutiveErrors} in a row):`, error.message);
35795
36176
  if (once)
35796
36177
  process.exit(1);
35797
- await new Promise((resolve) => setTimeout(resolve, Math.min(pause * consecutiveErrors, 30000)));
36178
+ await new Promise((resolve2) => setTimeout(resolve2, Math.min(pause * consecutiveErrors, 30000)));
35798
36179
  continue;
35799
36180
  }
35800
36181
  if (once)
35801
36182
  process.exit(processed ? 0 : 2);
35802
36183
  if (!processed)
35803
- await new Promise((resolve) => setTimeout(resolve, pause));
36184
+ await new Promise((resolve2) => setTimeout(resolve2, pause));
35804
36185
  } while (true);
35805
36186
  }
35806
36187
  export {