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