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