@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/README.md +4 -2
- package/bin/index.js +24514 -24319
- package/bin/mcp.js +217 -198
- package/bin/migrate.js +114 -62
- package/bin/server.js +3384 -2892
- package/bin/worker.js +2095 -1820
- package/dist/cli/commands/install.d.ts +16 -0
- package/dist/cli/commands/publish.d.ts +35 -0
- package/dist/cli/commands/registry.d.ts +5 -0
- package/dist/index.js +475 -263
- package/dist/lib/app-home.d.ts +27 -1
- package/dist/lib/feedback.d.ts +6 -0
- package/dist/lib/installer.d.ts +2 -0
- package/dist/lib/pull.d.ts +18 -1
- package/dist/lib/remote-client.d.ts +15 -1
- package/dist/lib/skill-version.d.ts +11 -0
- package/dist/sdk/index.js +5470 -4975
- package/dist/server/app.d.ts +3 -0
- package/dist/server/artifact-storage.d.ts +27 -0
- package/dist/server/config.d.ts +2 -0
- package/dist/server/rows.d.ts +2 -1
- package/dist/server/seed-bundled.d.ts +20 -0
- package/dist/server/skills-api.d.ts +18 -1
- package/dist/server/sqlite-store.d.ts +3 -1
- package/dist/server/store.d.ts +6 -1
- package/dist/server/types.d.ts +43 -0
- package/dist/storage.js +62 -61
- package/migrations/postgres/0006_skill_versions.sql +30 -0
- package/migrations/sqlite/0006_skill_versions.sql +17 -0
- package/package.json +2 -3
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
32935
|
-
function
|
|
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 (!
|
|
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
|
|
32944
|
-
|
|
32945
|
-
}
|
|
32946
|
-
|
|
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
|
|
32954
|
-
|
|
32955
|
-
|
|
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 ??
|
|
33095
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
32958
33096
|
const platform = options.platform ?? process.platform;
|
|
32959
|
-
if (
|
|
33097
|
+
if (platform === "darwin") {
|
|
32960
33098
|
switch (kind) {
|
|
32961
33099
|
case "config":
|
|
32962
33100
|
case "data":
|
|
32963
|
-
return
|
|
33101
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
32964
33102
|
case "cache":
|
|
32965
|
-
return
|
|
33103
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
32966
33104
|
case "state":
|
|
32967
|
-
return
|
|
33105
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
32968
33106
|
}
|
|
32969
33107
|
}
|
|
32970
33108
|
switch (kind) {
|
|
32971
33109
|
case "config":
|
|
32972
|
-
return
|
|
33110
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
32973
33111
|
case "data":
|
|
32974
|
-
return
|
|
33112
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
32975
33113
|
case "state":
|
|
32976
|
-
return
|
|
33114
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
32977
33115
|
case "cache":
|
|
32978
|
-
return
|
|
33116
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
32979
33117
|
}
|
|
32980
33118
|
}
|
|
32981
|
-
function
|
|
32982
|
-
|
|
32983
|
-
const appSegment = options.internal === true ?
|
|
32984
|
-
return
|
|
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
|
|
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"] ||
|
|
33133
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir2() || "/tmp";
|
|
32998
33134
|
}
|
|
32999
33135
|
function legacyDataRoot() {
|
|
33000
|
-
return
|
|
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(
|
|
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 =
|
|
33039
|
-
const targetPath =
|
|
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 =
|
|
33060
|
-
const oldConfigFile =
|
|
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(
|
|
33200
|
+
if (existsSync2(oldConfigFile) && !existsSync2(join3(root3, "config.json"))) {
|
|
33065
33201
|
try {
|
|
33066
|
-
copyFileSync(oldConfigFile,
|
|
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
|
|
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 :
|
|
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 :
|
|
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
|
|
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
|
|
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 =
|
|
33173
|
-
if (MIGRATION_DIALECTS.some((dialect) => existsSync3(
|
|
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 =
|
|
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(
|
|
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/
|
|
33988
|
-
|
|
33989
|
-
|
|
33990
|
-
|
|
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
|
-
|
|
34076
|
-
|
|
34077
|
-
|
|
34078
|
-
|
|
34079
|
-
|
|
34080
|
-
|
|
34081
|
-
|
|
34082
|
-
|
|
34083
|
-
|
|
34084
|
-
|
|
34085
|
-
|
|
34086
|
-
|
|
34087
|
-
|
|
34088
|
-
|
|
34089
|
-
|
|
34090
|
-
|
|
34091
|
-
|
|
34092
|
-
|
|
34093
|
-
|
|
34094
|
-
|
|
34095
|
-
|
|
34096
|
-
|
|
34097
|
-
|
|
34098
|
-
|
|
34099
|
-
|
|
34100
|
-
|
|
34101
|
-
|
|
34102
|
-
|
|
34103
|
-
|
|
34104
|
-
|
|
34105
|
-
|
|
34106
|
-
|
|
34107
|
-
|
|
34108
|
-
|
|
34109
|
-
|
|
34110
|
-
|
|
34111
|
-
|
|
34112
|
-
|
|
34113
|
-
|
|
34114
|
-
|
|
34115
|
-
|
|
34116
|
-
|
|
34117
|
-
|
|
34118
|
-
|
|
34119
|
-
|
|
34120
|
-
|
|
34121
|
-
|
|
34122
|
-
|
|
34123
|
-
|
|
34124
|
-
|
|
34125
|
-
|
|
34126
|
-
|
|
34127
|
-
|
|
34128
|
-
|
|
34129
|
-
|
|
34130
|
-
}
|
|
34131
|
-
|
|
34132
|
-
|
|
34133
|
-
|
|
34134
|
-
|
|
34135
|
-
|
|
34136
|
-
|
|
34137
|
-
}
|
|
34138
|
-
|
|
34139
|
-
|
|
34140
|
-
|
|
34141
|
-
|
|
34142
|
-
|
|
34143
|
-
|
|
34144
|
-
|
|
34145
|
-
|
|
34146
|
-
|
|
34147
|
-
|
|
34148
|
-
|
|
34149
|
-
|
|
34150
|
-
|
|
34151
|
-
|
|
34152
|
-
|
|
34153
|
-
|
|
34154
|
-
|
|
34155
|
-
|
|
34156
|
-
|
|
34157
|
-
|
|
34158
|
-
|
|
34159
|
-
|
|
34160
|
-
|
|
34161
|
-
|
|
34162
|
-
|
|
34163
|
-
|
|
34164
|
-
|
|
34165
|
-
|
|
34166
|
-
}
|
|
34167
|
-
|
|
34168
|
-
|
|
34169
|
-
|
|
34170
|
-
|
|
34171
|
-
|
|
34172
|
-
|
|
34173
|
-
}
|
|
34174
|
-
|
|
34175
|
-
|
|
34176
|
-
|
|
34177
|
-
|
|
34178
|
-
|
|
34179
|
-
|
|
34180
|
-
|
|
34181
|
-
|
|
34182
|
-
|
|
34183
|
-
|
|
34184
|
-
|
|
34185
|
-
|
|
34186
|
-
|
|
34187
|
-
|
|
34188
|
-
|
|
34189
|
-
|
|
34190
|
-
|
|
34191
|
-
|
|
34192
|
-
|
|
34193
|
-
|
|
34194
|
-
|
|
34195
|
-
|
|
34196
|
-
|
|
34197
|
-
|
|
34198
|
-
|
|
34199
|
-
|
|
34200
|
-
|
|
34201
|
-
|
|
34202
|
-
|
|
34203
|
-
|
|
34204
|
-
|
|
34205
|
-
|
|
34206
|
-
|
|
34207
|
-
|
|
34208
|
-
|
|
34209
|
-
|
|
34210
|
-
|
|
34211
|
-
|
|
34212
|
-
|
|
34213
|
-
|
|
34214
|
-
|
|
34215
|
-
|
|
34216
|
-
|
|
34217
|
-
|
|
34218
|
-
|
|
34219
|
-
|
|
34220
|
-
|
|
34221
|
-
|
|
34222
|
-
|
|
34223
|
-
|
|
34224
|
-
|
|
34225
|
-
|
|
34226
|
-
|
|
34227
|
-
|
|
34228
|
-
|
|
34229
|
-
|
|
34230
|
-
|
|
34231
|
-
|
|
34232
|
-
|
|
34233
|
-
|
|
34234
|
-
|
|
34235
|
-
|
|
34236
|
-
|
|
34237
|
-
|
|
34238
|
-
|
|
34239
|
-
|
|
34240
|
-
|
|
34241
|
-
|
|
34242
|
-
|
|
34243
|
-
|
|
34244
|
-
|
|
34245
|
-
|
|
34246
|
-
|
|
34247
|
-
|
|
34248
|
-
|
|
34249
|
-
|
|
34250
|
-
|
|
34251
|
-
|
|
34252
|
-
|
|
34253
|
-
|
|
34254
|
-
|
|
34255
|
-
|
|
34256
|
-
|
|
34257
|
-
|
|
34258
|
-
|
|
34259
|
-
|
|
34260
|
-
|
|
34261
|
-
|
|
34262
|
-
|
|
34263
|
-
|
|
34264
|
-
|
|
34265
|
-
|
|
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
|
-
|
|
34268
|
-
|
|
34269
|
-
|
|
34270
|
-
|
|
34271
|
-
|
|
34272
|
-
|
|
34273
|
-
|
|
34274
|
-
|
|
34275
|
-
|
|
34276
|
-
|
|
34277
|
-
|
|
34278
|
-
|
|
34279
|
-
|
|
34280
|
-
|
|
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
|
-
|
|
34283
|
-
|
|
34284
|
-
|
|
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
|
-
|
|
34287
|
-
|
|
34288
|
-
|
|
34289
|
-
|
|
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
|
-
|
|
34292
|
-
|
|
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
|
-
|
|
34295
|
-
|
|
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
|
-
|
|
34298
|
-
|
|
34299
|
-
|
|
34300
|
-
|
|
34301
|
-
|
|
34302
|
-
|
|
34303
|
-
|
|
34304
|
-
|
|
34305
|
-
|
|
34306
|
-
|
|
34307
|
-
|
|
34308
|
-
|
|
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
|
-
|
|
34311
|
-
|
|
34312
|
-
|
|
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
|
-
|
|
34315
|
-
|
|
34316
|
-
|
|
34317
|
-
|
|
34318
|
-
|
|
34319
|
-
|
|
34320
|
-
|
|
34321
|
-
|
|
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
|
-
|
|
34324
|
-
|
|
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
|
-
|
|
34327
|
-
|
|
34328
|
-
|
|
34329
|
-
|
|
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
|
-
|
|
34332
|
-
|
|
34333
|
-
|
|
34334
|
-
|
|
34335
|
-
|
|
34336
|
-
|
|
34337
|
-
|
|
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
|
-
|
|
34341
|
-
|
|
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
|
|
34344
|
-
|
|
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
|
-
|
|
34348
|
-
|
|
34349
|
-
|
|
34350
|
-
|
|
34351
|
-
|
|
34352
|
-
|
|
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
|
-
|
|
34355
|
-
|
|
34356
|
-
|
|
34357
|
-
|
|
34358
|
-
|
|
34359
|
-
|
|
34360
|
-
|
|
34361
|
-
|
|
34362
|
-
|
|
34363
|
-
|
|
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
|
-
|
|
34368
|
-
|
|
34369
|
-
|
|
34370
|
-
|
|
34371
|
-
|
|
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
|
-
|
|
34375
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34435
|
-
|
|
34436
|
-
|
|
34437
|
-
|
|
34438
|
-
|
|
34439
|
-
|
|
34440
|
-
|
|
34441
|
-
|
|
34442
|
-
|
|
34443
|
-
|
|
34444
|
-
|
|
34445
|
-
|
|
34446
|
-
|
|
34447
|
-
|
|
34448
|
-
|
|
34449
|
-
|
|
34450
|
-
|
|
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.
|
|
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
|
-
|
|
34464
|
-
|
|
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(
|
|
34471
|
-
|
|
34472
|
-
|
|
34473
|
-
|
|
34474
|
-
|
|
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.
|
|
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
|
-
|
|
34511
|
-
|
|
34512
|
-
|
|
34513
|
-
|
|
34514
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34563
|
-
|
|
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
|
-
|
|
34574
|
-
|
|
34575
|
-
|
|
34576
|
-
|
|
34577
|
-
|
|
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
|
-
|
|
34584
|
-
|
|
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,
|
|
34594
|
-
|
|
34595
|
-
|
|
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
|
|
34606
|
-
|
|
34607
|
-
|
|
34608
|
-
|
|
34609
|
-
|
|
34610
|
-
|
|
34611
|
-
|
|
34612
|
-
|
|
34613
|
-
const
|
|
34614
|
-
|
|
34615
|
-
|
|
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
|
-
|
|
34620
|
-
|
|
34621
|
-
const
|
|
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
|
-
|
|
34624
|
-
|
|
34625
|
-
|
|
34626
|
-
|
|
34627
|
-
|
|
34628
|
-
|
|
34629
|
-
...input.
|
|
34630
|
-
|
|
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
|
-
|
|
34635
|
-
|
|
34636
|
-
|
|
34637
|
-
|
|
34638
|
-
|
|
34639
|
-
|
|
34640
|
-
|
|
34641
|
-
|
|
34642
|
-
|
|
34643
|
-
|
|
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
|
-
|
|
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
|
|
34711
|
-
return
|
|
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
|
|
34721
|
-
|
|
34722
|
-
|
|
34723
|
-
|
|
34724
|
-
|
|
34725
|
-
|
|
34726
|
-
|
|
34727
|
-
|
|
34728
|
-
|
|
34729
|
-
|
|
34730
|
-
|
|
34731
|
-
|
|
34732
|
-
|
|
34733
|
-
|
|
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
|
-
|
|
34755
|
-
|
|
34756
|
-
|
|
34757
|
-
|
|
34758
|
-
|
|
34759
|
-
|
|
34760
|
-
|
|
34761
|
-
|
|
34762
|
-
|
|
34763
|
-
|
|
34764
|
-
|
|
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
|
-
|
|
34775
|
-
|
|
34776
|
-
|
|
34777
|
-
|
|
34778
|
-
|
|
34779
|
-
if (
|
|
34780
|
-
|
|
34781
|
-
|
|
34782
|
-
|
|
34783
|
-
|
|
34784
|
-
|
|
34785
|
-
|
|
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
|
|
34805
|
-
return
|
|
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
|
|
34809
|
-
|
|
34810
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
34834
|
-
|
|
34835
|
-
|
|
34836
|
-
|
|
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
|
-
|
|
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
|
|
34851
|
-
|
|
34852
|
-
|
|
34853
|
-
|
|
34854
|
-
|
|
34855
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34867
|
-
|
|
34868
|
-
|
|
34869
|
-
|
|
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
|
-
|
|
34875
|
-
|
|
34876
|
-
|
|
34877
|
-
|
|
34878
|
-
|
|
34879
|
-
|
|
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
|
|
34960
|
-
|
|
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
|
-
|
|
34977
|
-
|
|
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
|
-
|
|
34986
|
-
|
|
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
|
-
|
|
34998
|
-
|
|
34999
|
-
|
|
35000
|
-
|
|
35001
|
-
|
|
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
|
|
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
|
-
|
|
35007
|
-
}
|
|
35008
|
-
|
|
35009
|
-
|
|
35010
|
-
}
|
|
35011
|
-
|
|
35012
|
-
|
|
35013
|
-
|
|
35014
|
-
|
|
35015
|
-
|
|
35016
|
-
|
|
35017
|
-
const
|
|
35018
|
-
|
|
35019
|
-
|
|
35020
|
-
|
|
35021
|
-
|
|
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
|
-
|
|
35024
|
-
|
|
35025
|
-
|
|
35026
|
-
|
|
35027
|
-
|
|
35028
|
-
|
|
35029
|
-
|
|
35030
|
-
|
|
35031
|
-
|
|
35032
|
-
|
|
35033
|
-
}
|
|
35034
|
-
|
|
35035
|
-
|
|
35036
|
-
|
|
35037
|
-
|
|
35038
|
-
|
|
35039
|
-
|
|
35040
|
-
|
|
35041
|
-
|
|
35042
|
-
|
|
35043
|
-
|
|
35044
|
-
|
|
35045
|
-
|
|
35046
|
-
|
|
35047
|
-
|
|
35048
|
-
|
|
35049
|
-
|
|
35050
|
-
|
|
35051
|
-
|
|
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
|
-
|
|
35259
|
-
|
|
35260
|
-
|
|
35261
|
-
|
|
35262
|
-
|
|
35263
|
-
|
|
35264
|
-
|
|
35265
|
-
|
|
35266
|
-
|
|
35267
|
-
|
|
35268
|
-
|
|
35269
|
-
|
|
35270
|
-
|
|
35271
|
-
|
|
35272
|
-
|
|
35273
|
-
|
|
35274
|
-
|
|
35275
|
-
|
|
35276
|
-
|
|
35277
|
-
|
|
35278
|
-
|
|
35279
|
-
|
|
35280
|
-
|
|
35281
|
-
|
|
35282
|
-
|
|
35283
|
-
|
|
35284
|
-
|
|
35285
|
-
|
|
35286
|
-
|
|
35287
|
-
|
|
35288
|
-
|
|
35289
|
-
|
|
35290
|
-
|
|
35291
|
-
|
|
35292
|
-
|
|
35293
|
-
|
|
35294
|
-
|
|
35295
|
-
|
|
35296
|
-
|
|
35297
|
-
|
|
35298
|
-
|
|
35299
|
-
|
|
35300
|
-
|
|
35301
|
-
|
|
35302
|
-
|
|
35303
|
-
|
|
35304
|
-
|
|
35305
|
-
|
|
35306
|
-
|
|
35307
|
-
|
|
35308
|
-
|
|
35309
|
-
|
|
35310
|
-
|
|
35311
|
-
|
|
35312
|
-
|
|
35313
|
-
|
|
35314
|
-
|
|
35315
|
-
|
|
35316
|
-
|
|
35317
|
-
|
|
35318
|
-
|
|
35319
|
-
|
|
35320
|
-
|
|
35321
|
-
|
|
35322
|
-
|
|
35323
|
-
|
|
35324
|
-
|
|
35325
|
-
|
|
35326
|
-
|
|
35327
|
-
|
|
35328
|
-
|
|
35329
|
-
|
|
35330
|
-
|
|
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
|
-
|
|
35335
|
-
|
|
35336
|
-
|
|
35337
|
-
|
|
35338
|
-
|
|
35339
|
-
|
|
35340
|
-
|
|
35341
|
-
|
|
35342
|
-
|
|
35343
|
-
|
|
35344
|
-
|
|
35345
|
-
|
|
35346
|
-
|
|
35347
|
-
|
|
35348
|
-
|
|
35349
|
-
|
|
35350
|
-
|
|
35351
|
-
|
|
35352
|
-
|
|
35353
|
-
|
|
35354
|
-
|
|
35355
|
-
|
|
35356
|
-
|
|
35357
|
-
|
|
35358
|
-
|
|
35359
|
-
|
|
35360
|
-
|
|
35361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35421
|
-
|
|
35422
|
-
|
|
35423
|
-
|
|
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
|
-
|
|
35453
|
-
|
|
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
|
-
|
|
35479
|
-
|
|
35480
|
-
|
|
35481
|
-
|
|
35482
|
-
|
|
35483
|
-
|
|
35484
|
-
|
|
35485
|
-
|
|
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
|
-
|
|
35546
|
-
|
|
35547
|
-
|
|
35548
|
-
|
|
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
|
-
|
|
35600
|
-
|
|
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
|
-
|
|
35647
|
-
|
|
35648
|
-
|
|
35649
|
-
|
|
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
|
-
|
|
35658
|
-
|
|
35659
|
-
|
|
35660
|
-
|
|
35661
|
-
|
|
35662
|
-
|
|
35663
|
-
|
|
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
|
-
|
|
35678
|
-
|
|
35679
|
-
|
|
35680
|
-
|
|
35681
|
-
|
|
35682
|
-
|
|
35683
|
-
|
|
35684
|
-
|
|
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
|
-
|
|
35717
|
-
|
|
35718
|
-
|
|
35719
|
-
|
|
35720
|
-
|
|
35721
|
-
|
|
35722
|
-
|
|
35723
|
-
|
|
35724
|
-
|
|
35725
|
-
|
|
35726
|
-
|
|
35727
|
-
|
|
35728
|
-
|
|
35729
|
-
var
|
|
35730
|
-
|
|
35731
|
-
|
|
35732
|
-
|
|
35733
|
-
|
|
35734
|
-
|
|
35735
|
-
|
|
35736
|
-
|
|
35737
|
-
|
|
35738
|
-
|
|
35739
|
-
|
|
35740
|
-
|
|
35741
|
-
|
|
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/
|
|
35753
|
-
var
|
|
35754
|
-
|
|
35755
|
-
|
|
35756
|
-
|
|
35757
|
-
|
|
35758
|
-
|
|
35759
|
-
|
|
35760
|
-
|
|
35761
|
-
|
|
35762
|
-
|
|
35763
|
-
|
|
35764
|
-
|
|
35765
|
-
|
|
35766
|
-
|
|
35767
|
-
|
|
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/
|
|
35771
|
-
var
|
|
35772
|
-
"
|
|
35773
|
-
"
|
|
35774
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
35819
|
-
|
|
36004
|
+
${text.trim()}
|
|
36005
|
+
`),
|
|
36006
|
+
textArtifact(run, "summary.md", `# Summary
|
|
35820
36007
|
|
|
35821
|
-
|
|
35822
|
-
|
|
35823
|
-
|
|
35824
|
-
|
|
35825
|
-
|
|
35826
|
-
|
|
35827
|
-
|
|
35828
|
-
|
|
35829
|
-
|
|
35830
|
-
|
|
35831
|
-
|
|
35832
|
-
|
|
35833
|
-
|
|
35834
|
-
|
|
35835
|
-
|
|
35836
|
-
|
|
35837
|
-
|
|
35838
|
-
|
|
35839
|
-
|
|
35840
|
-
|
|
35841
|
-
|
|
35842
|
-
|
|
35843
|
-
|
|
35844
|
-
|
|
35845
|
-
|
|
35846
|
-
|
|
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
|
-
|
|
35851
|
-
|
|
35852
|
-
|
|
35853
|
-
|
|
35854
|
-
|
|
35855
|
-
|
|
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);
|