@hasna/skills 0.1.63 → 0.1.66
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 +61 -22
- package/bin/index.js +2120 -670
- package/bin/mcp.js +555 -249
- package/bin/migrate.js +229 -33
- package/bin/server.js +1542 -327
- package/bin/worker.js +716 -207
- package/dist/cli/commands/registry-reconcile.d.ts +2 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +682 -276
- package/dist/lib/agent-sync.d.ts +26 -6
- package/dist/lib/auth-store.d.ts +37 -0
- package/dist/lib/config.d.ts +51 -0
- package/dist/lib/home-census.d.ts +4 -0
- package/dist/lib/home-migration.d.ts +8 -9
- package/dist/lib/native-storage.d.ts +29 -1
- package/dist/lib/portable-skills.d.ts +49 -6
- package/dist/lib/pull.d.ts +31 -0
- package/dist/lib/registry-reconcile.d.ts +114 -0
- package/dist/lib/registry-types.d.ts +9 -0
- package/dist/lib/registry.d.ts +7 -4
- package/dist/lib/remote-client.d.ts +118 -1
- package/dist/lib/remote-registry.d.ts +26 -0
- package/dist/lib/revision.d.ts +29 -0
- package/dist/lib/run-routing.d.ts +60 -0
- package/dist/sdk/index.js +14412 -13338
- package/dist/server/app.d.ts +4 -1
- package/dist/server/config.d.ts +12 -2
- package/dist/server/rows.d.ts +2 -1
- package/dist/server/skills-api.d.ts +108 -6
- package/dist/server/sqlite-store.d.ts +20 -3
- package/dist/server/store-fixtures.d.ts +6 -0
- package/dist/server/store.d.ts +31 -5
- package/dist/server/types.d.ts +98 -3
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +57 -2
- package/migrations/postgres/0004_hosted_pins.sql +28 -0
- package/migrations/postgres/0005_revision_tombstone_registry.sql +37 -0
- package/migrations/postgres/0005_tag_projection.sql +36 -0
- package/migrations/sqlite/0004_hosted_pins.sql +21 -0
- package/migrations/sqlite/0005_revision_tombstone_registry.sql +18 -0
- package/migrations/sqlite/0005_tag_projection.sql +25 -0
- package/package.json +3 -2
package/bin/server.js
CHANGED
|
@@ -22936,6 +22936,119 @@ __export(exports_dist_es8, {
|
|
|
22936
22936
|
var init_dist_es9 = __esm(() => {
|
|
22937
22937
|
init_fromIni();
|
|
22938
22938
|
});
|
|
22939
|
+
// package.json
|
|
22940
|
+
var package_default = {
|
|
22941
|
+
name: "@hasna/skills",
|
|
22942
|
+
version: "0.1.66",
|
|
22943
|
+
description: "Skills library for AI coding agents",
|
|
22944
|
+
type: "module",
|
|
22945
|
+
bin: {
|
|
22946
|
+
skills: "bin/index.js",
|
|
22947
|
+
"skills-mcp": "bin/mcp.js",
|
|
22948
|
+
"skills-server": "bin/server.js",
|
|
22949
|
+
"skills-worker": "bin/worker.js",
|
|
22950
|
+
"skills-migrate": "bin/migrate.js"
|
|
22951
|
+
},
|
|
22952
|
+
exports: {
|
|
22953
|
+
".": {
|
|
22954
|
+
import: "./dist/index.js",
|
|
22955
|
+
types: "./dist/index.d.ts"
|
|
22956
|
+
},
|
|
22957
|
+
"./storage": {
|
|
22958
|
+
import: "./dist/storage.js",
|
|
22959
|
+
types: "./dist/storage.d.ts"
|
|
22960
|
+
},
|
|
22961
|
+
"./sdk": {
|
|
22962
|
+
import: "./dist/sdk/index.js",
|
|
22963
|
+
types: "./dist/sdk/index.d.ts"
|
|
22964
|
+
}
|
|
22965
|
+
},
|
|
22966
|
+
files: [
|
|
22967
|
+
"dist/",
|
|
22968
|
+
"!dist/**/*.test.d.ts",
|
|
22969
|
+
"!dist/test-preload.d.ts",
|
|
22970
|
+
"!dist/platform",
|
|
22971
|
+
"bin/",
|
|
22972
|
+
"migrations/",
|
|
22973
|
+
"docs/skill-standard.md",
|
|
22974
|
+
"schemas/",
|
|
22975
|
+
"LICENSE",
|
|
22976
|
+
"README.md"
|
|
22977
|
+
],
|
|
22978
|
+
main: "./dist/index.js",
|
|
22979
|
+
types: "./dist/index.d.ts",
|
|
22980
|
+
scripts: {
|
|
22981
|
+
clean: "rm -rf bin/ dist/",
|
|
22982
|
+
build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
22983
|
+
test: "bun test --timeout 30000",
|
|
22984
|
+
dev: "bun run ./src/cli/index.tsx",
|
|
22985
|
+
"dev:watch": "bun --watch run ./src/cli/index.tsx",
|
|
22986
|
+
"dev:mcp": "bun --watch run ./src/mcp/index.ts",
|
|
22987
|
+
"dev:server": "bun --watch run ./src/server/index.ts",
|
|
22988
|
+
"dev:worker": "bun --watch run ./src/server/worker.ts",
|
|
22989
|
+
migrate: "bun run ./src/server/migrate.ts",
|
|
22990
|
+
typecheck: "tsc --noEmit",
|
|
22991
|
+
"verify:release": "bun run scripts/release-guard.ts",
|
|
22992
|
+
prepack: "bun run build && bun run verify:release",
|
|
22993
|
+
prepublishOnly: "bun run typecheck && bun run test",
|
|
22994
|
+
postinstall: "mkdir -p $HOME/.hasna/skills/custom 2>/dev/null || true"
|
|
22995
|
+
},
|
|
22996
|
+
keywords: [
|
|
22997
|
+
"skills",
|
|
22998
|
+
"ai",
|
|
22999
|
+
"agent",
|
|
23000
|
+
"cli",
|
|
23001
|
+
"typescript",
|
|
23002
|
+
"bun",
|
|
23003
|
+
"claude",
|
|
23004
|
+
"codex",
|
|
23005
|
+
"gemini",
|
|
23006
|
+
"mcp",
|
|
23007
|
+
"model-context-protocol",
|
|
23008
|
+
"open-source",
|
|
23009
|
+
"skills-library",
|
|
23010
|
+
"automation"
|
|
23011
|
+
],
|
|
23012
|
+
author: "Hasna",
|
|
23013
|
+
license: "Apache-2.0",
|
|
23014
|
+
devDependencies: {
|
|
23015
|
+
"@types/bun": "latest",
|
|
23016
|
+
"@types/node": "25.2.3",
|
|
23017
|
+
"@types/react": "^18.2.0",
|
|
23018
|
+
"bun-types": "1.3.14",
|
|
23019
|
+
"react-devtools-core": "^7.0.1",
|
|
23020
|
+
typescript: "^5"
|
|
23021
|
+
},
|
|
23022
|
+
dependencies: {
|
|
23023
|
+
"@aws-sdk/client-ecs": "^3.1079.0",
|
|
23024
|
+
"@aws-sdk/client-s3": "^3.1079.0",
|
|
23025
|
+
"@hasna/events": "0.1.16",
|
|
23026
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
23027
|
+
chalk: "^5.3.0",
|
|
23028
|
+
commander: "^12.1.0",
|
|
23029
|
+
ink: "^5.0.1",
|
|
23030
|
+
"ink-select-input": "^6.0.0",
|
|
23031
|
+
"ink-spinner": "^5.0.0",
|
|
23032
|
+
"ink-text-input": "^6.0.0",
|
|
23033
|
+
react: "^18.2.0",
|
|
23034
|
+
zod: "^4.3.6"
|
|
23035
|
+
},
|
|
23036
|
+
engines: {
|
|
23037
|
+
bun: ">=1.0.0"
|
|
23038
|
+
},
|
|
23039
|
+
publishConfig: {
|
|
23040
|
+
registry: "https://registry.npmjs.org",
|
|
23041
|
+
access: "public"
|
|
23042
|
+
},
|
|
23043
|
+
repository: {
|
|
23044
|
+
type: "git",
|
|
23045
|
+
url: "git+https://github.com/hasna/apps.git"
|
|
23046
|
+
},
|
|
23047
|
+
homepage: "https://github.com/hasna/skills",
|
|
23048
|
+
bugs: {
|
|
23049
|
+
url: "https://github.com/hasna/skills/issues"
|
|
23050
|
+
}
|
|
23051
|
+
};
|
|
22939
23052
|
|
|
22940
23053
|
// src/lib/remote-run-contract.ts
|
|
22941
23054
|
var REMOTE_SKILL_RUN_CONTRACT_VERSION = 1;
|
|
@@ -22955,6 +23068,7 @@ var ANY_SEGMENT_EXCLUDES = new Set([
|
|
|
22955
23068
|
".docker"
|
|
22956
23069
|
]);
|
|
22957
23070
|
var ROOT_EXCLUDES = new Set(["dist", "build", ".turbo"]);
|
|
23071
|
+
var TOOL_SIDECAR_FILENAMES = new Set([".hasna-skills.json"]);
|
|
22958
23072
|
var CREDENTIAL_FILENAMES = new Set([
|
|
22959
23073
|
".npmrc",
|
|
22960
23074
|
".pypirc",
|
|
@@ -31994,7 +32108,7 @@ var WriteGetObjectResponse$ = [
|
|
|
31994
32108
|
class CreateSessionCommand extends command(_ep4, _mw0, "CreateSession", CreateSession$) {
|
|
31995
32109
|
}
|
|
31996
32110
|
// ../../node_modules/.bun/@aws-sdk+client-s3@3.1106.0/node_modules/@aws-sdk/client-s3/package.json
|
|
31997
|
-
var
|
|
32111
|
+
var package_default2 = {
|
|
31998
32112
|
name: "@aws-sdk/client-s3",
|
|
31999
32113
|
version: "3.1106.0",
|
|
32000
32114
|
description: "AWS SDK for JavaScript S3 Client for Node.js, Browser and React Native",
|
|
@@ -32544,7 +32658,7 @@ var getRuntimeConfig3 = (config) => {
|
|
|
32544
32658
|
authSchemePreference: config?.authSchemePreference ?? import_config28.loadConfig(import_httpAuthSchemes3.NODE_AUTH_SCHEME_PREFERENCE_OPTIONS, loaderConfig),
|
|
32545
32659
|
bodyLengthChecker: config?.bodyLengthChecker ?? import_serde4.calculateBodyLength,
|
|
32546
32660
|
credentialDefaultProvider: config?.credentialDefaultProvider ?? defaultProvider,
|
|
32547
|
-
defaultUserAgentProvider: config?.defaultUserAgentProvider ?? import_client19.createDefaultUserAgentProvider({ serviceId: clientSharedValues.serviceId, clientVersion:
|
|
32661
|
+
defaultUserAgentProvider: config?.defaultUserAgentProvider ?? import_client19.createDefaultUserAgentProvider({ serviceId: clientSharedValues.serviceId, clientVersion: package_default2.version }),
|
|
32548
32662
|
disableS3ExpressSessionAuth: config?.disableS3ExpressSessionAuth ?? import_config28.loadConfig($NODE_DISABLE_S3_EXPRESS_SESSION_AUTH_OPTIONS, loaderConfig),
|
|
32549
32663
|
eventStreamSerdeProvider: config?.eventStreamSerdeProvider ?? import_event_streams.eventStreamSerdeProvider,
|
|
32550
32664
|
maxAttempts: config?.maxAttempts ?? import_config28.loadConfig(import_retry3.NODE_MAX_ATTEMPT_CONFIG_OPTIONS, config),
|
|
@@ -32856,36 +32970,144 @@ function concat(chunks) {
|
|
|
32856
32970
|
return merged;
|
|
32857
32971
|
}
|
|
32858
32972
|
|
|
32859
|
-
// src/
|
|
32860
|
-
|
|
32861
|
-
|
|
32862
|
-
|
|
32863
|
-
}
|
|
32864
|
-
|
|
32865
|
-
|
|
32866
|
-
|
|
32867
|
-
|
|
32868
|
-
|
|
32973
|
+
// src/sdk/governance.ts
|
|
32974
|
+
var DEFAULT_OUTPUT_GOVERNANCE = {
|
|
32975
|
+
defaultVisibility: "private",
|
|
32976
|
+
redactPatterns: [
|
|
32977
|
+
/\bsk-[A-Za-z0-9_-]{8,}\b/g,
|
|
32978
|
+
/\bsk_[A-Za-z0-9_-]{8,}\b/g,
|
|
32979
|
+
/\bgh[opsur]_[A-Za-z0-9_]{8,}\b/g,
|
|
32980
|
+
/\bgithub_pat_[A-Za-z0-9_]{8,}\b/g,
|
|
32981
|
+
/\bnpm_[A-Za-z0-9_]{8,}\b/g,
|
|
32982
|
+
/\bAKIA[A-Z0-9]{12,}\b/g,
|
|
32983
|
+
/\bAIza[A-Za-z0-9_-]{10,}\b/g,
|
|
32984
|
+
/\b[A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|DATABASE_URL)[A-Z0-9_]*\s*[:=]\s*[^ \n\r\t]+/gi,
|
|
32985
|
+
/\bhttps?:\/\/[^ \n\r\t]+X-Amz-Signature=[^ \n\r\t]+/gi
|
|
32986
|
+
],
|
|
32987
|
+
perOutputBytes: 10 * 1024 * 1024,
|
|
32988
|
+
perRunTotalBytes: 100 * 1024 * 1024,
|
|
32989
|
+
artifactTtlSeconds: 30 * 24 * 60 * 60
|
|
32990
|
+
};
|
|
32991
|
+
var DEFAULT_RUN_QUOTA = {
|
|
32992
|
+
cpu: 1,
|
|
32993
|
+
memoryMB: 2048,
|
|
32994
|
+
durationSeconds: 3600,
|
|
32995
|
+
networkMB: 100,
|
|
32996
|
+
artifactBytes: 100 * 1024 * 1024
|
|
32997
|
+
};
|
|
32998
|
+
var GOVERNANCE_ERROR_CODES = {
|
|
32999
|
+
ARTIFACT_LIMIT_EXCEEDED: "ARTIFACT_LIMIT_EXCEEDED",
|
|
33000
|
+
RUN_ARTIFACT_TOTAL_EXCEEDED: "RUN_ARTIFACT_TOTAL_EXCEEDED",
|
|
33001
|
+
RUN_BUDGET_EXHAUSTED: "RUN_BUDGET_EXHAUSTED",
|
|
33002
|
+
STALE_LEASE_GENERATION: "STALE_LEASE_GENERATION",
|
|
33003
|
+
FENCING_UNSUPPORTED: "FENCING_UNSUPPORTED",
|
|
33004
|
+
EVENT_PAYLOAD_REJECTED: "EVENT_PAYLOAD_REJECTED",
|
|
33005
|
+
SKILL_UNAVAILABLE_OFFLINE: "SKILL_UNAVAILABLE_OFFLINE",
|
|
33006
|
+
REMOTE_REQUIRED: "REMOTE_REQUIRED"
|
|
33007
|
+
};
|
|
33008
|
+
|
|
33009
|
+
class GovernanceError extends Error {
|
|
33010
|
+
code;
|
|
33011
|
+
gate;
|
|
33012
|
+
ceiling;
|
|
33013
|
+
constructor(code, message, options = {}) {
|
|
33014
|
+
super(message);
|
|
33015
|
+
this.name = "GovernanceError";
|
|
33016
|
+
this.code = code;
|
|
33017
|
+
this.gate = options.gate ?? code;
|
|
33018
|
+
this.ceiling = options.ceiling;
|
|
33019
|
+
}
|
|
32869
33020
|
}
|
|
32870
|
-
|
|
32871
|
-
|
|
32872
|
-
|
|
32873
|
-
|
|
32874
|
-
|
|
33021
|
+
function runPointersOf(run) {
|
|
33022
|
+
return {
|
|
33023
|
+
runId: run.id,
|
|
33024
|
+
attemptId: run.id,
|
|
33025
|
+
leaseGeneration: run.leaseGeneration,
|
|
33026
|
+
correlationId: run.correlationId
|
|
33027
|
+
};
|
|
32875
33028
|
}
|
|
32876
|
-
|
|
33029
|
+
|
|
33030
|
+
// src/sdk/cancel.ts
|
|
33031
|
+
function createCancelService(options) {
|
|
33032
|
+
const store = options.store;
|
|
33033
|
+
const governanceStore = options.governanceStore;
|
|
33034
|
+
const storage = options.storage ?? new ArtifactStorage;
|
|
32877
33035
|
return {
|
|
32878
|
-
|
|
32879
|
-
|
|
32880
|
-
|
|
32881
|
-
|
|
32882
|
-
|
|
32883
|
-
|
|
32884
|
-
|
|
32885
|
-
|
|
33036
|
+
async cancel(principal, runId, requestedBy) {
|
|
33037
|
+
if (!store.transitionRun) {
|
|
33038
|
+
throw new GovernanceError(GOVERNANCE_ERROR_CODES.FENCING_UNSUPPORTED, `cannot cancel run ${runId}: the store has no transitionRun generation fencing`, { gate: "transitionRun" });
|
|
33039
|
+
}
|
|
33040
|
+
const run = await store.getRun(principal, runId);
|
|
33041
|
+
if (!run)
|
|
33042
|
+
throw new GovernanceError(GOVERNANCE_ERROR_CODES.STALE_LEASE_GENERATION, `run ${runId} not found`, { gate: "run" });
|
|
33043
|
+
const terminalStatuses = new Set(["succeeded", "failed", "cancelled", "expired", "refunded"]);
|
|
33044
|
+
if (terminalStatuses.has(run.status)) {
|
|
33045
|
+
const receipt2 = await governanceStore.appendReceipt({
|
|
33046
|
+
kind: "cancel",
|
|
33047
|
+
orgId: run.orgId,
|
|
33048
|
+
runId: run.id,
|
|
33049
|
+
requestedBy,
|
|
33050
|
+
metadata: { outcome: "already-terminal", status: run.status, ...runPointersOf(run) }
|
|
33051
|
+
});
|
|
33052
|
+
return { run, fencedGeneration: run.leaseGeneration, quarantined: [], receipt: receipt2, alreadyTerminal: true };
|
|
33053
|
+
}
|
|
33054
|
+
const fencedGeneration = run.leaseGeneration + 1;
|
|
33055
|
+
const cancelling = await store.transitionRun(runId, { status: "cancel_requested", leaseGeneration: fencedGeneration }, run.leaseGeneration);
|
|
33056
|
+
if (!cancelling) {
|
|
33057
|
+
throw new GovernanceError(GOVERNANCE_ERROR_CODES.STALE_LEASE_GENERATION, `run ${runId} changed while cancelling`, { gate: "transition" });
|
|
33058
|
+
}
|
|
33059
|
+
const artifacts = await store.listArtifacts(principal, runId);
|
|
33060
|
+
const quarantined = [];
|
|
33061
|
+
for (const artifact of artifacts) {
|
|
33062
|
+
const quarantineKey = await storage.moveToQuarantine?.(artifact);
|
|
33063
|
+
if (quarantineKey) {
|
|
33064
|
+
await governanceStore.updateArtifactStorageKey(artifact.id, artifact.orgId, quarantineKey);
|
|
33065
|
+
}
|
|
33066
|
+
await governanceStore.appendReceipt({
|
|
33067
|
+
kind: "quarantine",
|
|
33068
|
+
orgId: artifact.orgId,
|
|
33069
|
+
runId: run.id,
|
|
33070
|
+
artifactId: artifact.id,
|
|
33071
|
+
requestedBy,
|
|
33072
|
+
metadata: { relativePath: artifact.relativePath, byteSize: artifact.byteSize, quarantineKey: quarantineKey ?? null }
|
|
33073
|
+
});
|
|
33074
|
+
quarantined.push({ ...artifact, ...quarantineKey ? { storageKey: quarantineKey } : {} });
|
|
33075
|
+
}
|
|
33076
|
+
const cancelled = await store.transitionRun(runId, { status: "cancelled", completedAt: new Date().toISOString() }, fencedGeneration);
|
|
33077
|
+
if (!cancelled) {
|
|
33078
|
+
throw new GovernanceError(GOVERNANCE_ERROR_CODES.STALE_LEASE_GENERATION, `run ${runId} moved on while finalising cancellation`, { gate: "transition" });
|
|
33079
|
+
}
|
|
33080
|
+
const receipt = await governanceStore.appendReceipt({
|
|
33081
|
+
kind: "cancel",
|
|
33082
|
+
orgId: run.orgId,
|
|
33083
|
+
runId: run.id,
|
|
33084
|
+
requestedBy,
|
|
33085
|
+
metadata: {
|
|
33086
|
+
fencedGeneration,
|
|
33087
|
+
artifactCount: quarantined.length,
|
|
33088
|
+
pointers: runPointersOf(cancelled)
|
|
33089
|
+
}
|
|
33090
|
+
});
|
|
33091
|
+
return { run: cancelled, fencedGeneration, quarantined, receipt, alreadyTerminal: false };
|
|
33092
|
+
}
|
|
32886
33093
|
};
|
|
32887
33094
|
}
|
|
32888
33095
|
|
|
33096
|
+
// src/sdk/governance-store.ts
|
|
33097
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
33098
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
33099
|
+
import { mkdirSync as mkdirSync3 } from "fs";
|
|
33100
|
+
import { dirname as dirname5 } from "path";
|
|
33101
|
+
|
|
33102
|
+
// src/server/database-url.ts
|
|
33103
|
+
import { isAbsolute, join as join3 } from "path";
|
|
33104
|
+
import { fileURLToPath } from "url";
|
|
33105
|
+
|
|
33106
|
+
// src/lib/config.ts
|
|
33107
|
+
import { existsSync, readFileSync as readFileSync2, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
33108
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
33109
|
+
import { homedir as homedir2 } from "os";
|
|
33110
|
+
|
|
32889
33111
|
// src/lib/retired-settings.ts
|
|
32890
33112
|
var RETIRED_ENV_SUFFIXES = ["_STORAGE_MODE", "_DEPLOYMENT_MODE", "_CLOUD_MODE"];
|
|
32891
33113
|
var RETIRED_CONFIG_KEYS = {
|
|
@@ -32932,53 +33154,7 @@ function assertNoRetiredConfigKeys(config, source) {
|
|
|
32932
33154
|
}
|
|
32933
33155
|
}
|
|
32934
33156
|
|
|
32935
|
-
// src/server/config.ts
|
|
32936
|
-
var DATABASE_URL_ENV = "HASNA_SKILLS_DATABASE_URL";
|
|
32937
|
-
var SKILLS_ENV_NAMESPACE = "SKILLS";
|
|
32938
|
-
function resolveServerConfig(env = process.env) {
|
|
32939
|
-
assertNoRetiredModeEnvVars(env, {
|
|
32940
|
-
app: SKILLS_ENV_NAMESPACE,
|
|
32941
|
-
replacement: DATABASE_URL_ENV
|
|
32942
|
-
});
|
|
32943
|
-
const nodeEnv = env.NODE_ENV || "development";
|
|
32944
|
-
const host = env.HOST || env.SKILLS_HOST || "0.0.0.0";
|
|
32945
|
-
const port = parsePositiveInt(env.PORT || env.SKILLS_PORT, 8787);
|
|
32946
|
-
return {
|
|
32947
|
-
host,
|
|
32948
|
-
port,
|
|
32949
|
-
databaseUrl: env[DATABASE_URL_ENV] || env.DATABASE_URL || undefined,
|
|
32950
|
-
bootstrapApiKey: env.HASNA_SKILLS_BOOTSTRAP_API_KEY || undefined,
|
|
32951
|
-
artifactBucket: env.HASNA_SKILLS_S3_BUCKET || env.SKILLS_S3_BUCKET || undefined,
|
|
32952
|
-
artifactPrefix: normalizePrefix(env.HASNA_SKILLS_S3_PREFIX || env.SKILLS_S3_PREFIX || "skills/artifacts"),
|
|
32953
|
-
inlineWorker: env.HASNA_SKILLS_INLINE_WORKER === "1",
|
|
32954
|
-
bundleSigningKey: env.HASNA_SKILLS_SIGNING_KEY || undefined,
|
|
32955
|
-
requestBodyLimitBytes: parsePositiveInt(env.HASNA_SKILLS_REQUEST_BODY_LIMIT_BYTES, 1e6),
|
|
32956
|
-
skillBundleLimitBytes: parsePositiveInt(env.HASNA_SKILLS_BUNDLE_LIMIT_BYTES, 25000000),
|
|
32957
|
-
publicBaseUrl: (env.SKILLS_PUBLIC_BASE_URL || localOrigin(host, port)).replace(/\/+$/, ""),
|
|
32958
|
-
nodeEnv,
|
|
32959
|
-
allowEphemeralStore: env.HASNA_SKILLS_ALLOW_EPHEMERAL_STORE === "1"
|
|
32960
|
-
};
|
|
32961
|
-
}
|
|
32962
|
-
function localOrigin(host, port) {
|
|
32963
|
-
const hostname = host === "0.0.0.0" || host === "::" ? "localhost" : host;
|
|
32964
|
-
return `http://${hostname.includes(":") ? `[${hostname}]` : hostname}:${port}`;
|
|
32965
|
-
}
|
|
32966
|
-
function parsePositiveInt(value, fallback) {
|
|
32967
|
-
const parsed = Number.parseInt(value ?? "", 10);
|
|
32968
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
32969
|
-
}
|
|
32970
|
-
function normalizePrefix(value) {
|
|
32971
|
-
return value.replace(/^\/+|\/+$/g, "") || "skills/artifacts";
|
|
32972
|
-
}
|
|
32973
|
-
|
|
32974
|
-
// src/server/database-url.ts
|
|
32975
|
-
import { isAbsolute, join as join3 } from "path";
|
|
32976
|
-
import { fileURLToPath } from "url";
|
|
32977
|
-
|
|
32978
33157
|
// src/lib/config.ts
|
|
32979
|
-
import { existsSync, readFileSync as readFileSync2, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
32980
|
-
import { join as join2, dirname as dirname2 } from "path";
|
|
32981
|
-
import { homedir as homedir2 } from "os";
|
|
32982
33158
|
var ENUM_KEYS = {
|
|
32983
33159
|
defaultAgent: ["claude", "codex", "gemini", "pi", "opencode", "all"],
|
|
32984
33160
|
defaultScope: ["global", "project"],
|
|
@@ -33031,6 +33207,11 @@ function normalizeConfigValue(key, value) {
|
|
|
33031
33207
|
}
|
|
33032
33208
|
var DATA_DIR_ENV = "HASNA_SKILLS_DIR";
|
|
33033
33209
|
var INSTALLED_SKILLS_DIRNAME = "installed";
|
|
33210
|
+
var SKILLS_CACHE_DIRNAME = "skills";
|
|
33211
|
+
var LAYOUT_MIGRATION_RECORD = ".layout-migration.json";
|
|
33212
|
+
function isOwnerLayoutMigrated(appDir) {
|
|
33213
|
+
return existsSync(join2(appDir, SKILLS_CACHE_DIRNAME, LAYOUT_MIGRATION_RECORD));
|
|
33214
|
+
}
|
|
33034
33215
|
function getDataDir() {
|
|
33035
33216
|
const override = process.env[DATA_DIR_ENV];
|
|
33036
33217
|
if (override) {
|
|
@@ -33153,26 +33334,78 @@ function looksLikeSqlitePath(value) {
|
|
|
33153
33334
|
return SQLITE_EXTENSIONS.some((extension) => value.toLowerCase().endsWith(extension));
|
|
33154
33335
|
}
|
|
33155
33336
|
|
|
33156
|
-
// src/server/
|
|
33157
|
-
import {
|
|
33337
|
+
// src/server/sqlite-store.ts
|
|
33338
|
+
import { Database } from "bun:sqlite";
|
|
33339
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
33340
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
|
|
33341
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
33158
33342
|
|
|
33159
|
-
// src/server/
|
|
33160
|
-
import {
|
|
33343
|
+
// src/server/auth.ts
|
|
33344
|
+
import { createHash as createHash3 } from "crypto";
|
|
33345
|
+
function hashApiKey(token) {
|
|
33346
|
+
return createHash3("sha256").update(token).digest("hex");
|
|
33347
|
+
}
|
|
33348
|
+
function bearerToken(request) {
|
|
33349
|
+
const header = request.headers.get("authorization") || "";
|
|
33350
|
+
const match = header.match(/^Bearer\s+(.+)$/i);
|
|
33351
|
+
const token = match?.[1]?.trim();
|
|
33352
|
+
return token || null;
|
|
33353
|
+
}
|
|
33354
|
+
async function authenticateRequest(store, request) {
|
|
33355
|
+
const token = bearerToken(request);
|
|
33356
|
+
if (!token)
|
|
33357
|
+
return null;
|
|
33358
|
+
return store.authenticateApiKeyHash(hashApiKey(token));
|
|
33359
|
+
}
|
|
33360
|
+
function publicPrincipal(partial = {}) {
|
|
33361
|
+
return {
|
|
33362
|
+
apiKeyId: partial.apiKeyId || "key_dev",
|
|
33363
|
+
orgId: partial.orgId || "org_dev",
|
|
33364
|
+
orgSlug: partial.orgSlug || "dev",
|
|
33365
|
+
orgName: partial.orgName || "Development",
|
|
33366
|
+
userId: partial.userId || "user_dev",
|
|
33367
|
+
email: partial.email || "dev@example.com",
|
|
33368
|
+
role: partial.role || "owner",
|
|
33369
|
+
scopes: partial.scopes || ["skills:read", "runs:write"]
|
|
33370
|
+
};
|
|
33371
|
+
}
|
|
33161
33372
|
|
|
33162
|
-
// src/server/
|
|
33163
|
-
|
|
33164
|
-
|
|
33165
|
-
|
|
33166
|
-
|
|
33167
|
-
|
|
33168
|
-
|
|
33169
|
-
|
|
33170
|
-
|
|
33171
|
-
|
|
33172
|
-
|
|
33173
|
-
|
|
33174
|
-
|
|
33373
|
+
// src/server/migrations-dir.ts
|
|
33374
|
+
import { existsSync as existsSync2 } from "fs";
|
|
33375
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
33376
|
+
var MIGRATION_DIALECTS = ["postgres", "sqlite"];
|
|
33377
|
+
var MAX_WALK_UP = 6;
|
|
33378
|
+
function findMigrationsRoot(startDirs = defaultStartDirs()) {
|
|
33379
|
+
for (const start of startDirs) {
|
|
33380
|
+
let dir = start;
|
|
33381
|
+
for (let level = 0;level < MAX_WALK_UP; level += 1) {
|
|
33382
|
+
const candidate = join4(dir, "migrations");
|
|
33383
|
+
if (MIGRATION_DIALECTS.some((dialect) => existsSync2(join4(candidate, dialect))))
|
|
33384
|
+
return candidate;
|
|
33385
|
+
const parent = dirname3(dir);
|
|
33386
|
+
if (parent === dir)
|
|
33387
|
+
break;
|
|
33388
|
+
dir = parent;
|
|
33389
|
+
}
|
|
33390
|
+
}
|
|
33391
|
+
return null;
|
|
33392
|
+
}
|
|
33393
|
+
function resolveMigrationsDir(dialect, root3 = findMigrationsRoot()) {
|
|
33394
|
+
if (!root3) {
|
|
33395
|
+
throw new Error(`could not locate the migrations directory for dialect "${dialect}". ` + `Expected a migrations/${dialect}/ folder alongside the package root.`);
|
|
33396
|
+
}
|
|
33397
|
+
const dir = join4(root3, dialect);
|
|
33398
|
+
if (!existsSync2(dir)) {
|
|
33399
|
+
throw new Error(`migrations directory not found: ${dir}`);
|
|
33175
33400
|
}
|
|
33401
|
+
return dir;
|
|
33402
|
+
}
|
|
33403
|
+
function defaultStartDirs() {
|
|
33404
|
+
const dirs = [import.meta.dir];
|
|
33405
|
+
const cwd = process.cwd();
|
|
33406
|
+
if (cwd && cwd !== import.meta.dir)
|
|
33407
|
+
dirs.push(cwd);
|
|
33408
|
+
return dirs;
|
|
33176
33409
|
}
|
|
33177
33410
|
|
|
33178
33411
|
// src/server/rows.ts
|
|
@@ -33255,7 +33488,20 @@ function rowToSkill(row) {
|
|
|
33255
33488
|
...row.bundle_byte_size === null || row.bundle_byte_size === undefined ? {} : { bundleByteSize: Number(row.bundle_byte_size) },
|
|
33256
33489
|
...typeof row.published_by_user_id === "string" ? { publishedByUserId: row.published_by_user_id } : {},
|
|
33257
33490
|
createdAt: dateString(row.created_at),
|
|
33258
|
-
updatedAt: dateString(row.updated_at)
|
|
33491
|
+
updatedAt: dateString(row.updated_at),
|
|
33492
|
+
revisionId: String(row.revision_id ?? ""),
|
|
33493
|
+
revisionNumber: Number(row.revision_number ?? 0),
|
|
33494
|
+
...row.tombstoned_at ? { tombstonedAt: dateString(row.tombstoned_at) } : {},
|
|
33495
|
+
...row.tombstone_purge_after ? { tombstonePurgeAfter: dateString(row.tombstone_purge_after) } : {}
|
|
33496
|
+
};
|
|
33497
|
+
}
|
|
33498
|
+
function rowToPin(row) {
|
|
33499
|
+
return {
|
|
33500
|
+
orgId: String(row.org_id),
|
|
33501
|
+
principal: String(row.principal),
|
|
33502
|
+
slug: String(row.slug),
|
|
33503
|
+
pinnedAt: dateString(row.pinned_at),
|
|
33504
|
+
metadata: parseJsonObject(row.metadata_json)
|
|
33259
33505
|
};
|
|
33260
33506
|
}
|
|
33261
33507
|
function rowToSkillBundle(row) {
|
|
@@ -33307,53 +33553,62 @@ function dateString(value) {
|
|
|
33307
33553
|
return String(value);
|
|
33308
33554
|
}
|
|
33309
33555
|
|
|
33310
|
-
// src/server/
|
|
33311
|
-
|
|
33312
|
-
|
|
33313
|
-
|
|
33314
|
-
|
|
33315
|
-
|
|
33316
|
-
|
|
33317
|
-
|
|
33318
|
-
|
|
33319
|
-
|
|
33320
|
-
|
|
33321
|
-
|
|
33322
|
-
|
|
33323
|
-
let dir = start;
|
|
33324
|
-
for (let level = 0;level < MAX_WALK_UP; level += 1) {
|
|
33325
|
-
const candidate = join4(dir, "migrations");
|
|
33326
|
-
if (MIGRATION_DIALECTS.some((dialect) => existsSync2(join4(candidate, dialect))))
|
|
33327
|
-
return candidate;
|
|
33328
|
-
const parent = dirname3(dir);
|
|
33329
|
-
if (parent === dir)
|
|
33330
|
-
break;
|
|
33331
|
-
dir = parent;
|
|
33332
|
-
}
|
|
33556
|
+
// src/server/types.ts
|
|
33557
|
+
class StaleLeaseGenerationError extends Error {
|
|
33558
|
+
runId;
|
|
33559
|
+
expectedGeneration;
|
|
33560
|
+
currentGeneration;
|
|
33561
|
+
status;
|
|
33562
|
+
constructor(runId2, expectedGeneration, currentGeneration, status) {
|
|
33563
|
+
super(`late transition rejected for run ${runId2}: expected lease_generation ${expectedGeneration}, ` + `current ${currentGeneration} (status ${status})`);
|
|
33564
|
+
this.name = "StaleLeaseGenerationError";
|
|
33565
|
+
this.runId = runId2;
|
|
33566
|
+
this.expectedGeneration = expectedGeneration;
|
|
33567
|
+
this.currentGeneration = currentGeneration;
|
|
33568
|
+
this.status = status;
|
|
33333
33569
|
}
|
|
33334
|
-
return null;
|
|
33335
33570
|
}
|
|
33336
|
-
|
|
33337
|
-
|
|
33338
|
-
|
|
33571
|
+
|
|
33572
|
+
class SkillRevisionConflictError extends Error {
|
|
33573
|
+
slug;
|
|
33574
|
+
expectedRevisionId;
|
|
33575
|
+
currentRevisionId;
|
|
33576
|
+
constructor(slug, expectedRevisionId, currentRevisionId) {
|
|
33577
|
+
super(`revision conflict for '${slug}': expected revision ${expectedRevisionId ?? "(none)"}, ` + `current is ${currentRevisionId ?? "(none)"}. Refused rather than silently overwriting a newer revision.`);
|
|
33578
|
+
this.name = "SkillRevisionConflictError";
|
|
33579
|
+
this.slug = slug;
|
|
33580
|
+
this.expectedRevisionId = expectedRevisionId;
|
|
33581
|
+
this.currentRevisionId = currentRevisionId;
|
|
33339
33582
|
}
|
|
33340
|
-
const dir = join4(root3, dialect);
|
|
33341
|
-
if (!existsSync2(dir)) {
|
|
33342
|
-
throw new Error(`migrations directory not found: ${dir}`);
|
|
33343
|
-
}
|
|
33344
|
-
return dir;
|
|
33345
33583
|
}
|
|
33346
|
-
|
|
33347
|
-
|
|
33348
|
-
|
|
33349
|
-
|
|
33350
|
-
|
|
33351
|
-
|
|
33584
|
+
|
|
33585
|
+
// src/lib/revision.ts
|
|
33586
|
+
import { createHash as createHash4 } from "crypto";
|
|
33587
|
+
var REVISION_ID_PATTERN = /^[0-9a-f]{64}$/;
|
|
33588
|
+
function revisionIdOf(content) {
|
|
33589
|
+
const canonical = JSON.stringify({
|
|
33590
|
+
slug: content.slug,
|
|
33591
|
+
displayName: content.displayName,
|
|
33592
|
+
description: content.description,
|
|
33593
|
+
category: content.category,
|
|
33594
|
+
tags: content.tags,
|
|
33595
|
+
source: content.source,
|
|
33596
|
+
kind: content.kind,
|
|
33597
|
+
version: content.version ?? null,
|
|
33598
|
+
skillMd: content.skillMd ?? null,
|
|
33599
|
+
bundleSha256: content.bundleSha256 ?? null,
|
|
33600
|
+
bundleByteSize: content.bundleByteSize ?? null
|
|
33601
|
+
});
|
|
33602
|
+
return createHash4("sha256").update(canonical).digest("hex");
|
|
33603
|
+
}
|
|
33604
|
+
function revisionIdOfRecord(record) {
|
|
33605
|
+
return revisionIdOf(record);
|
|
33352
33606
|
}
|
|
33353
33607
|
|
|
33354
33608
|
// src/server/sqlite-store.ts
|
|
33355
33609
|
var CLAIM_ATTEMPTS = 8;
|
|
33356
33610
|
var CLAIMABLE_STATUSES = ["queued", "retrying"];
|
|
33611
|
+
var NO_REVISION_SENTINEL = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
33357
33612
|
var LAST_USED_RESOLUTION_MS = 60000;
|
|
33358
33613
|
|
|
33359
33614
|
class SqliteSkillsStore {
|
|
@@ -33373,12 +33628,24 @@ class SqliteSkillsStore {
|
|
|
33373
33628
|
if (options.migrate !== false) {
|
|
33374
33629
|
applySqliteMigrations(this.db, options.migrationsDir);
|
|
33375
33630
|
}
|
|
33631
|
+
this.backfillLegacyRevisions();
|
|
33376
33632
|
this.backend = {
|
|
33377
33633
|
kind: "sqlite",
|
|
33378
33634
|
durable: !inMemory,
|
|
33379
33635
|
label: inMemory ? "sqlite (in-memory)" : `sqlite (${path})`
|
|
33380
33636
|
};
|
|
33381
33637
|
}
|
|
33638
|
+
backfillLegacyRevisions() {
|
|
33639
|
+
const rows = this.all("SELECT * FROM skills_registry WHERE revision_id = ''", []);
|
|
33640
|
+
for (const row of rows) {
|
|
33641
|
+
const record = rowToSkill(row);
|
|
33642
|
+
this.db.run("UPDATE skills_registry SET revision_id = ? WHERE org_id = ? AND slug = ?", [
|
|
33643
|
+
revisionIdOfRecord(record),
|
|
33644
|
+
record.orgId,
|
|
33645
|
+
record.slug
|
|
33646
|
+
]);
|
|
33647
|
+
}
|
|
33648
|
+
}
|
|
33382
33649
|
get database() {
|
|
33383
33650
|
return this.db;
|
|
33384
33651
|
}
|
|
@@ -33602,8 +33869,29 @@ class SqliteSkillsStore {
|
|
|
33602
33869
|
const orgId = input.principal.orgId;
|
|
33603
33870
|
const now = nowIso();
|
|
33604
33871
|
return this.db.transaction(() => {
|
|
33605
|
-
const previous = this.get("SELECT bundle_sha256 FROM skills_registry WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
33872
|
+
const previous = this.get("SELECT revision_id, revision_number, bundle_sha256, bundle_byte_size, skill_md, tombstoned_at FROM skills_registry WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
33606
33873
|
const previousSha = typeof previous?.bundle_sha256 === "string" ? previous.bundle_sha256 : null;
|
|
33874
|
+
const previousRevisionId = typeof previous?.revision_id === "string" && previous.revision_id ? previous.revision_id : null;
|
|
33875
|
+
const tombstoned = previous?.tombstoned_at != null;
|
|
33876
|
+
const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : typeof previous?.skill_md === "string" ? previous.skill_md : null;
|
|
33877
|
+
if (previous && !tombstoned && input.expectedRevisionId !== previousRevisionId) {
|
|
33878
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
|
|
33879
|
+
}
|
|
33880
|
+
const carriedSha = input.bundle?.sha256 ?? previousSha;
|
|
33881
|
+
const carriedSize = input.bundle?.byteSize ?? (previous?.bundle_byte_size == null ? null : Number(previous.bundle_byte_size));
|
|
33882
|
+
const revisionId = revisionIdOfRecord({
|
|
33883
|
+
slug: input.slug,
|
|
33884
|
+
displayName: input.displayName,
|
|
33885
|
+
description: input.description,
|
|
33886
|
+
category: input.category,
|
|
33887
|
+
tags: input.tags,
|
|
33888
|
+
source: input.source,
|
|
33889
|
+
kind: input.kind,
|
|
33890
|
+
...input.version ? { version: input.version } : {},
|
|
33891
|
+
...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
|
|
33892
|
+
...carriedSha ? { bundleSha256: carriedSha } : {},
|
|
33893
|
+
...carriedSize === null || carriedSize === undefined ? {} : { bundleByteSize: carriedSize }
|
|
33894
|
+
});
|
|
33607
33895
|
if (input.bundle) {
|
|
33608
33896
|
this.db.run(`INSERT INTO skills_bundles (org_id, sha256, byte_size, content_type, storage_kind, storage_key, body_blob, created_at)
|
|
33609
33897
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -33624,8 +33912,8 @@ class SqliteSkillsStore {
|
|
|
33624
33912
|
]);
|
|
33625
33913
|
}
|
|
33626
33914
|
const row = this.get(`INSERT INTO skills_registry (org_id, slug, display_name, description, category, tags_json, source, kind, version, skill_md,
|
|
33627
|
-
bundle_sha256, bundle_byte_size, published_by_user_id, created_at, updated_at)
|
|
33628
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
33915
|
+
bundle_sha256, bundle_byte_size, published_by_user_id, revision_id, revision_number, created_at, updated_at)
|
|
33916
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
33629
33917
|
ON CONFLICT (org_id, slug) DO UPDATE SET
|
|
33630
33918
|
display_name = excluded.display_name,
|
|
33631
33919
|
description = excluded.description,
|
|
@@ -33643,7 +33931,16 @@ class SqliteSkillsStore {
|
|
|
33643
33931
|
bundle_sha256 = COALESCE(excluded.bundle_sha256, skills_registry.bundle_sha256),
|
|
33644
33932
|
bundle_byte_size = COALESCE(excluded.bundle_byte_size, skills_registry.bundle_byte_size),
|
|
33645
33933
|
published_by_user_id = excluded.published_by_user_id,
|
|
33934
|
+
revision_id = excluded.revision_id,
|
|
33935
|
+
-- Current + 1, not the inserted 1: on the update path the ACTUAL row's
|
|
33936
|
+
-- counter is the truth (it may have advanced since the pre-read, which is
|
|
33937
|
+
-- exactly what the WHERE guard below detects).
|
|
33938
|
+
revision_number = skills_registry.revision_number + 1,
|
|
33939
|
+
tombstoned_at = NULL,
|
|
33940
|
+
tombstone_purge_after = NULL,
|
|
33646
33941
|
updated_at = excluded.updated_at
|
|
33942
|
+
WHERE skills_registry.tombstoned_at IS NOT NULL
|
|
33943
|
+
OR skills_registry.revision_id = ?
|
|
33647
33944
|
RETURNING *`, [
|
|
33648
33945
|
orgId,
|
|
33649
33946
|
input.slug,
|
|
@@ -33654,62 +33951,165 @@ class SqliteSkillsStore {
|
|
|
33654
33951
|
input.source,
|
|
33655
33952
|
input.kind,
|
|
33656
33953
|
input.version ?? null,
|
|
33657
|
-
|
|
33954
|
+
carriedSkillMd,
|
|
33658
33955
|
input.bundle?.sha256 ?? null,
|
|
33659
33956
|
input.bundle?.byteSize ?? null,
|
|
33660
33957
|
input.principal.userId,
|
|
33958
|
+
revisionId,
|
|
33959
|
+
now,
|
|
33661
33960
|
now,
|
|
33662
|
-
|
|
33961
|
+
input.expectedRevisionId ?? NO_REVISION_SENTINEL
|
|
33663
33962
|
]);
|
|
33963
|
+
if (!row) {
|
|
33964
|
+
const current = this.get("SELECT revision_id FROM skills_registry WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
33965
|
+
const currentId = typeof current?.revision_id === "string" ? current.revision_id : null;
|
|
33966
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, currentId);
|
|
33967
|
+
}
|
|
33664
33968
|
if (previousSha && input.bundle && previousSha !== input.bundle.sha256)
|
|
33665
33969
|
this.collectOrphanBundle(orgId, previousSha);
|
|
33970
|
+
this.db.run("DELETE FROM skills_tags WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
33971
|
+
const insertTag = this.db.prepare("INSERT OR IGNORE INTO skills_tags (org_id, slug, tag) VALUES (?, ?, ?)");
|
|
33972
|
+
for (const tag of input.tags) {
|
|
33973
|
+
if (!tag.trim())
|
|
33974
|
+
continue;
|
|
33975
|
+
insertTag.run(orgId, input.slug, tag);
|
|
33976
|
+
}
|
|
33666
33977
|
return rowToSkill(row);
|
|
33667
33978
|
})();
|
|
33668
33979
|
}
|
|
33669
33980
|
async listSkills(principal) {
|
|
33670
|
-
|
|
33981
|
+
await this.purgeExpiredTombstones(principal);
|
|
33982
|
+
return this.all("SELECT * FROM skills_registry WHERE org_id = ? AND tombstoned_at IS NULL ORDER BY slug ASC", [principal.orgId]).map(rowToSkill);
|
|
33671
33983
|
}
|
|
33672
33984
|
async getSkill(principal, slug) {
|
|
33673
33985
|
const row = this.get("SELECT * FROM skills_registry WHERE org_id = ? AND slug = ? LIMIT 1", [principal.orgId, slug]);
|
|
33674
33986
|
return row ? rowToSkill(row) : null;
|
|
33675
33987
|
}
|
|
33676
|
-
async updateSkill(principal, slug, patch) {
|
|
33988
|
+
async updateSkill(principal, slug, patch, expectedRevisionId) {
|
|
33677
33989
|
const current = await this.getSkill(principal, slug);
|
|
33678
|
-
if (!current)
|
|
33990
|
+
if (!current || current.tombstonedAt)
|
|
33679
33991
|
return null;
|
|
33992
|
+
if (expectedRevisionId !== current.revisionId) {
|
|
33993
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
|
|
33994
|
+
}
|
|
33680
33995
|
const next = { ...current, ...patch };
|
|
33681
|
-
|
|
33682
|
-
|
|
33683
|
-
|
|
33684
|
-
|
|
33685
|
-
|
|
33686
|
-
|
|
33687
|
-
|
|
33688
|
-
|
|
33689
|
-
|
|
33690
|
-
|
|
33691
|
-
|
|
33692
|
-
|
|
33693
|
-
|
|
33694
|
-
|
|
33695
|
-
|
|
33696
|
-
|
|
33996
|
+
return this.db.transaction(() => {
|
|
33997
|
+
const revisionId = revisionIdOfRecord(next);
|
|
33998
|
+
const row = this.get(`UPDATE skills_registry
|
|
33999
|
+
SET display_name = ?, description = ?, category = ?, tags_json = ?, kind = ?, version = ?, skill_md = ?,
|
|
34000
|
+
revision_id = ?, revision_number = revision_number + 1, updated_at = ?
|
|
34001
|
+
WHERE org_id = ? AND slug = ? AND tombstoned_at IS NULL AND revision_id = ?
|
|
34002
|
+
RETURNING *`, [
|
|
34003
|
+
next.displayName,
|
|
34004
|
+
next.description,
|
|
34005
|
+
next.category,
|
|
34006
|
+
JSON.stringify(next.tags),
|
|
34007
|
+
next.kind,
|
|
34008
|
+
next.version ?? null,
|
|
34009
|
+
next.skillMd ?? null,
|
|
34010
|
+
revisionId,
|
|
34011
|
+
nowIso(),
|
|
34012
|
+
principal.orgId,
|
|
34013
|
+
slug,
|
|
34014
|
+
current.revisionId
|
|
34015
|
+
]);
|
|
34016
|
+
if (!row) {
|
|
34017
|
+
const nowRow = this.get("SELECT revision_id, tombstoned_at FROM skills_registry WHERE org_id = ? AND slug = ? LIMIT 1", [
|
|
34018
|
+
principal.orgId,
|
|
34019
|
+
slug
|
|
34020
|
+
]);
|
|
34021
|
+
if (nowRow && nowRow.tombstoned_at == null) {
|
|
34022
|
+
const currentId = typeof nowRow.revision_id === "string" ? nowRow.revision_id : null;
|
|
34023
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, currentId);
|
|
34024
|
+
}
|
|
34025
|
+
return null;
|
|
34026
|
+
}
|
|
34027
|
+
this.db.run("DELETE FROM skills_tags WHERE org_id = ? AND slug = ?", [principal.orgId, slug]);
|
|
34028
|
+
const insertTag = this.db.prepare("INSERT OR IGNORE INTO skills_tags (org_id, slug, tag) VALUES (?, ?, ?)");
|
|
34029
|
+
for (const tag of next.tags) {
|
|
34030
|
+
if (!tag.trim())
|
|
34031
|
+
continue;
|
|
34032
|
+
insertTag.run(principal.orgId, slug, tag);
|
|
34033
|
+
}
|
|
34034
|
+
return rowToSkill(row);
|
|
34035
|
+
})();
|
|
33697
34036
|
}
|
|
33698
|
-
async deleteSkill(principal, slug) {
|
|
34037
|
+
async deleteSkill(principal, slug, tombstoneWindowMs) {
|
|
33699
34038
|
return this.db.transaction(() => {
|
|
33700
|
-
const existing = this.get("SELECT
|
|
34039
|
+
const existing = this.get("SELECT tombstoned_at FROM skills_registry WHERE org_id = ? AND slug = ?", [principal.orgId, slug]);
|
|
33701
34040
|
if (!existing)
|
|
33702
|
-
return
|
|
33703
|
-
|
|
33704
|
-
|
|
33705
|
-
|
|
33706
|
-
|
|
34041
|
+
return null;
|
|
34042
|
+
if (existing.tombstoned_at != null) {
|
|
34043
|
+
const row2 = this.get("SELECT * FROM skills_registry WHERE org_id = ? AND slug = ? LIMIT 1", [principal.orgId, slug]);
|
|
34044
|
+
return rowToSkill(row2);
|
|
34045
|
+
}
|
|
34046
|
+
const tombstonedAt = nowIso();
|
|
34047
|
+
const purgeAfter = new Date(Date.now() + tombstoneWindowMs).toISOString();
|
|
34048
|
+
const row = this.get(`UPDATE skills_registry
|
|
34049
|
+
SET tombstoned_at = ?, tombstone_purge_after = ?, updated_at = ?
|
|
34050
|
+
WHERE org_id = ? AND slug = ?
|
|
34051
|
+
RETURNING *`, [tombstonedAt, purgeAfter, tombstonedAt, principal.orgId, slug]);
|
|
34052
|
+
return rowToSkill(row);
|
|
34053
|
+
})();
|
|
34054
|
+
}
|
|
34055
|
+
async purgeExpiredTombstones(principal) {
|
|
34056
|
+
return this.db.transaction(() => {
|
|
34057
|
+
const now = nowIso();
|
|
34058
|
+
const expired = this.all("SELECT * FROM skills_registry WHERE org_id = ? AND tombstoned_at IS NOT NULL AND tombstone_purge_after <= ?", [principal.orgId, now]);
|
|
34059
|
+
const purged = [];
|
|
34060
|
+
for (const row of expired) {
|
|
34061
|
+
const record = rowToSkill(row);
|
|
34062
|
+
this.db.run("DELETE FROM skills_registry WHERE org_id = ? AND slug = ?", [principal.orgId, record.slug]);
|
|
34063
|
+
this.db.run("DELETE FROM skills_tags WHERE org_id = ? AND slug = ?", [principal.orgId, record.slug]);
|
|
34064
|
+
if (record.bundleSha256)
|
|
34065
|
+
this.collectOrphanBundle(principal.orgId, record.bundleSha256);
|
|
34066
|
+
purged.push(record);
|
|
34067
|
+
}
|
|
34068
|
+
return purged;
|
|
33707
34069
|
})();
|
|
33708
34070
|
}
|
|
33709
34071
|
async getSkillBundle(principal, sha256) {
|
|
33710
34072
|
const row = this.get("SELECT * FROM skills_bundles WHERE org_id = ? AND sha256 = ? LIMIT 1", [principal.orgId, sha256]);
|
|
33711
34073
|
return row ? rowToSkillBundle(row) : null;
|
|
33712
34074
|
}
|
|
34075
|
+
async pinSkill(principal, slug, metadata = {}) {
|
|
34076
|
+
const row = this.get(`INSERT INTO skills_pins (org_id, principal, slug, pinned_at, metadata_json)
|
|
34077
|
+
VALUES (?, ?, ?, ?, ?)
|
|
34078
|
+
ON CONFLICT (org_id, principal, slug) DO UPDATE SET
|
|
34079
|
+
pinned_at = excluded.pinned_at,
|
|
34080
|
+
metadata_json = excluded.metadata_json
|
|
34081
|
+
RETURNING *`, [principal.orgId, principal.apiKeyId, slug, nowIso(), JSON.stringify(metadata)]);
|
|
34082
|
+
return rowToPin(row);
|
|
34083
|
+
}
|
|
34084
|
+
async unpinSkill(principal, slug) {
|
|
34085
|
+
const result = this.db.run("DELETE FROM skills_pins WHERE org_id = ? AND principal = ? AND slug = ?", [principal.orgId, principal.apiKeyId, slug]);
|
|
34086
|
+
return result.changes > 0;
|
|
34087
|
+
}
|
|
34088
|
+
async listPins(principal) {
|
|
34089
|
+
return this.all("SELECT * FROM skills_pins WHERE org_id = ? AND principal = ? ORDER BY slug ASC", [principal.orgId, principal.apiKeyId]).map(rowToPin);
|
|
34090
|
+
}
|
|
34091
|
+
async listTags(principal) {
|
|
34092
|
+
await this.purgeExpiredTombstones(principal);
|
|
34093
|
+
return this.all("SELECT DISTINCT tag FROM skills_tags WHERE org_id = ? ORDER BY tag ASC", [principal.orgId]).map((row) => String(row.tag));
|
|
34094
|
+
}
|
|
34095
|
+
async listSkillsByTag(principal, tag) {
|
|
34096
|
+
await this.purgeExpiredTombstones(principal);
|
|
34097
|
+
return this.all(`SELECT s.* FROM skills_registry s
|
|
34098
|
+
JOIN skills_tags t ON t.org_id = s.org_id AND t.slug = s.slug
|
|
34099
|
+
WHERE t.org_id = ? AND t.tag = ? AND s.tombstoned_at IS NULL
|
|
34100
|
+
ORDER BY s.slug ASC`, [principal.orgId, tag]).map(rowToSkill);
|
|
34101
|
+
}
|
|
34102
|
+
async listPinsByTag(principal, tag) {
|
|
34103
|
+
await this.purgeExpiredTombstones(principal);
|
|
34104
|
+
return this.all(`SELECT p.* FROM skills_pins p
|
|
34105
|
+
JOIN skills_tags t ON t.org_id = p.org_id AND t.slug = p.slug
|
|
34106
|
+
JOIN skills_registry s ON s.org_id = p.org_id AND s.slug = p.slug
|
|
34107
|
+
WHERE p.org_id = ? AND p.principal = ? AND t.tag = ? AND s.tombstoned_at IS NULL
|
|
34108
|
+
ORDER BY p.slug ASC`, [principal.orgId, principal.apiKeyId, tag]).map(rowToPin);
|
|
34109
|
+
}
|
|
34110
|
+
async listPublishedSlugs(principal) {
|
|
34111
|
+
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));
|
|
34112
|
+
}
|
|
33713
34113
|
collectOrphanBundle(orgId, sha256) {
|
|
33714
34114
|
const referenced = this.get("SELECT 1 AS present FROM skills_registry WHERE org_id = ? AND bundle_sha256 = ? LIMIT 1", [orgId, sha256]);
|
|
33715
34115
|
if (referenced)
|
|
@@ -33795,7 +34195,316 @@ function parseScopes(value) {
|
|
|
33795
34195
|
}
|
|
33796
34196
|
}
|
|
33797
34197
|
|
|
34198
|
+
// src/sdk/governance-store.ts
|
|
34199
|
+
function receiptId() {
|
|
34200
|
+
return `rcpt_${Date.now().toString(36)}_${randomUUID3().replace(/-/g, "").slice(0, 10)}`;
|
|
34201
|
+
}
|
|
34202
|
+
function reservationId() {
|
|
34203
|
+
return `res_${Date.now().toString(36)}_${randomUUID3().replace(/-/g, "").slice(0, 10)}`;
|
|
34204
|
+
}
|
|
34205
|
+
var EXPIRED_ARTIFACT_SQL = `
|
|
34206
|
+
SELECT a.* FROM skills_artifacts a
|
|
34207
|
+
JOIN skills_runs r ON r.id = a.run_id
|
|
34208
|
+
WHERE a.expires_at IS NOT NULL AND a.expires_at <= ?
|
|
34209
|
+
ORDER BY a.expires_at ASC
|
|
34210
|
+
`;
|
|
34211
|
+
var ACTIVE_RUN_SQL = `
|
|
34212
|
+
SELECT COUNT(*) AS n FROM skills_runs
|
|
34213
|
+
WHERE org_id = ? AND status IN ('queued','running','cancel_requested')
|
|
34214
|
+
`;
|
|
34215
|
+
class SqliteGovernanceStore {
|
|
34216
|
+
backend = "sqlite";
|
|
34217
|
+
db;
|
|
34218
|
+
closed = false;
|
|
34219
|
+
constructor(path = SQLITE_MEMORY_PATH, options = {}) {
|
|
34220
|
+
if (path !== SQLITE_MEMORY_PATH)
|
|
34221
|
+
mkdirSync3(dirname5(path), { recursive: true });
|
|
34222
|
+
this.db = new Database2(path, { create: true, readwrite: true });
|
|
34223
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
34224
|
+
this.db.exec("PRAGMA foreign_keys = ON");
|
|
34225
|
+
if (options.migrate !== false)
|
|
34226
|
+
applySqliteMigrations(this.db);
|
|
34227
|
+
}
|
|
34228
|
+
async close() {
|
|
34229
|
+
if (this.closed)
|
|
34230
|
+
return;
|
|
34231
|
+
this.closed = true;
|
|
34232
|
+
this.db.close(false);
|
|
34233
|
+
}
|
|
34234
|
+
get database() {
|
|
34235
|
+
return this.db;
|
|
34236
|
+
}
|
|
34237
|
+
async appendReceipt(receipt) {
|
|
34238
|
+
const next = { ...receipt, id: receiptId(), createdAt: nowIso() };
|
|
34239
|
+
this.db.run(`INSERT INTO skills_lifecycle_receipts (id, kind, org_id, run_id, artifact_id, requested_by, metadata_json, created_at)
|
|
34240
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [next.id, next.kind, next.orgId, next.runId, next.artifactId ?? null, next.requestedBy, JSON.stringify(next.metadata), next.createdAt]);
|
|
34241
|
+
return next;
|
|
34242
|
+
}
|
|
34243
|
+
async listReceipts(orgId, runId2) {
|
|
34244
|
+
const rows = this.db.query("SELECT * FROM skills_lifecycle_receipts WHERE org_id = ? AND run_id = ? ORDER BY created_at ASC, rowid ASC").all(orgId, runId2);
|
|
34245
|
+
return rows.map((row) => ({
|
|
34246
|
+
id: String(row.id),
|
|
34247
|
+
kind: String(row.kind),
|
|
34248
|
+
orgId: String(row.org_id),
|
|
34249
|
+
runId: String(row.run_id),
|
|
34250
|
+
...typeof row.artifact_id === "string" ? { artifactId: row.artifact_id } : {},
|
|
34251
|
+
requestedBy: String(row.requested_by),
|
|
34252
|
+
metadata: parseJsonObject(row.metadata_json),
|
|
34253
|
+
createdAt: String(row.created_at)
|
|
34254
|
+
}));
|
|
34255
|
+
}
|
|
34256
|
+
async createReservation(input) {
|
|
34257
|
+
const reservation = { ...input, id: reservationId(), status: "reserved", createdAt: nowIso() };
|
|
34258
|
+
this.db.run(`INSERT INTO skills_credit_reservations (id, org_id, run_id, estimated_cents, status, created_at)
|
|
34259
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [reservation.id, reservation.orgId, reservation.runId, reservation.estimatedCents, reservation.status, reservation.createdAt]);
|
|
34260
|
+
return reservation;
|
|
34261
|
+
}
|
|
34262
|
+
async reservationsForRun(orgId, runId2) {
|
|
34263
|
+
const rows = this.db.query("SELECT * FROM skills_credit_reservations WHERE org_id = ? AND run_id = ? ORDER BY created_at ASC, rowid ASC").all(orgId, runId2);
|
|
34264
|
+
return rows.map((row) => this.reservationFrom(row));
|
|
34265
|
+
}
|
|
34266
|
+
async reconcileReservation(reservationId2, actualCents, status) {
|
|
34267
|
+
const row = this.db.query("SELECT * FROM skills_credit_reservations WHERE id = ? LIMIT 1").get(reservationId2);
|
|
34268
|
+
if (!row || String(row.status) !== "reserved")
|
|
34269
|
+
return row ? this.reservationFrom(row) : null;
|
|
34270
|
+
const reconciledAt = nowIso();
|
|
34271
|
+
this.db.run("UPDATE skills_credit_reservations SET actual_cents = ?, status = ?, reconciled_at = ? WHERE id = ?", [actualCents, status, reconciledAt, reservationId2]);
|
|
34272
|
+
const updated = this.db.query("SELECT * FROM skills_credit_reservations WHERE id = ? LIMIT 1").get(reservationId2);
|
|
34273
|
+
return this.reservationFrom(updated);
|
|
34274
|
+
}
|
|
34275
|
+
async monthlySpendCents(orgId, monthPrefix) {
|
|
34276
|
+
const from = `${monthPrefix}-01T00:00:00.000Z`;
|
|
34277
|
+
const nextMonth = nextMonthPrefix(monthPrefix);
|
|
34278
|
+
const spent = this.db.query("SELECT COALESCE(SUM(cost_cents), 0) AS spent FROM skills_runs WHERE org_id = ? AND created_at >= ? AND created_at < ?").get(orgId, from, `${nextMonth}-01T00:00:00.000Z`);
|
|
34279
|
+
const pending = this.db.query("SELECT COALESCE(SUM(estimated_cents), 0) AS pending FROM skills_credit_reservations WHERE org_id = ? AND status = 'reserved'").get(orgId);
|
|
34280
|
+
return Number(spent.spent ?? 0) + Number(pending.pending ?? 0);
|
|
34281
|
+
}
|
|
34282
|
+
async activeRunCount(orgId) {
|
|
34283
|
+
const row = this.db.query(ACTIVE_RUN_SQL).get(orgId);
|
|
34284
|
+
return Number(row.n ?? 0);
|
|
34285
|
+
}
|
|
34286
|
+
async listExpiredArtifacts(at2) {
|
|
34287
|
+
const rows = this.db.query(EXPIRED_ARTIFACT_SQL).all(at2);
|
|
34288
|
+
return rows.map(rowToArtifact);
|
|
34289
|
+
}
|
|
34290
|
+
async deleteArtifactRow(artifactId2, orgId) {
|
|
34291
|
+
const result = this.db.run("DELETE FROM skills_artifacts WHERE id = ? AND org_id = ?", [artifactId2, orgId]);
|
|
34292
|
+
return result.changes === 1;
|
|
34293
|
+
}
|
|
34294
|
+
async updateArtifactStorageKey(artifactId2, orgId, storageKey) {
|
|
34295
|
+
const result = this.db.run("UPDATE skills_artifacts SET storage_key = ? WHERE id = ? AND org_id = ?", [storageKey, artifactId2, orgId]);
|
|
34296
|
+
return result.changes === 1;
|
|
34297
|
+
}
|
|
34298
|
+
reservationFrom(row) {
|
|
34299
|
+
return {
|
|
34300
|
+
id: String(row.id),
|
|
34301
|
+
orgId: String(row.org_id),
|
|
34302
|
+
runId: String(row.run_id),
|
|
34303
|
+
estimatedCents: Number(row.estimated_cents ?? 0),
|
|
34304
|
+
...row.actual_cents !== null && row.actual_cents !== undefined ? { actualCents: Number(row.actual_cents) } : {},
|
|
34305
|
+
status: String(row.status),
|
|
34306
|
+
createdAt: String(row.created_at),
|
|
34307
|
+
...typeof row.reconciled_at === "string" ? { reconciledAt: row.reconciled_at } : {}
|
|
34308
|
+
};
|
|
34309
|
+
}
|
|
34310
|
+
}
|
|
34311
|
+
|
|
34312
|
+
class PostgresGovernanceStore {
|
|
34313
|
+
backend = "postgres";
|
|
34314
|
+
sql;
|
|
34315
|
+
constructor(databaseUrl) {
|
|
34316
|
+
const bunWithSql = Bun;
|
|
34317
|
+
this.sql = new bunWithSql.SQL(databaseUrl, { max: 2 });
|
|
34318
|
+
}
|
|
34319
|
+
async close() {
|
|
34320
|
+
await this.sql.close?.();
|
|
34321
|
+
}
|
|
34322
|
+
async withContext(orgId, worker, fn) {
|
|
34323
|
+
return this.sql.begin(async (tx) => {
|
|
34324
|
+
await tx`SELECT set_config('app.skills_org_id', ${orgId ?? ""}, true)`;
|
|
34325
|
+
await tx`SELECT set_config('app.skills_claim_context', ${worker ? "worker" : ""}, true)`;
|
|
34326
|
+
return await fn(tx);
|
|
34327
|
+
});
|
|
34328
|
+
}
|
|
34329
|
+
async appendReceipt(receipt) {
|
|
34330
|
+
const next = { ...receipt, id: receiptId(), createdAt: nowIso() };
|
|
34331
|
+
await this.sql`
|
|
34332
|
+
INSERT INTO skills_lifecycle_receipts (id, kind, org_id, run_id, artifact_id, requested_by, metadata_json)
|
|
34333
|
+
VALUES (${next.id}, ${next.kind}, ${next.orgId}, ${next.runId}, ${next.artifactId ?? null}, ${next.requestedBy}, ${JSON.stringify(next.metadata)}::jsonb)
|
|
34334
|
+
`;
|
|
34335
|
+
return next;
|
|
34336
|
+
}
|
|
34337
|
+
async listReceipts(orgId, runId2) {
|
|
34338
|
+
const rows = await this.sql`
|
|
34339
|
+
SELECT * FROM skills_lifecycle_receipts WHERE org_id = ${orgId} AND run_id = ${runId2} ORDER BY created_at ASC
|
|
34340
|
+
`;
|
|
34341
|
+
return rows.map((row) => ({
|
|
34342
|
+
id: String(row.id),
|
|
34343
|
+
kind: String(row.kind),
|
|
34344
|
+
orgId: String(row.org_id),
|
|
34345
|
+
runId: String(row.run_id),
|
|
34346
|
+
...typeof row.artifact_id === "string" ? { artifactId: row.artifact_id } : {},
|
|
34347
|
+
requestedBy: String(row.requested_by),
|
|
34348
|
+
metadata: parseJsonObject(row.metadata_json),
|
|
34349
|
+
createdAt: String(row.created_at)
|
|
34350
|
+
}));
|
|
34351
|
+
}
|
|
34352
|
+
async createReservation(input) {
|
|
34353
|
+
const reservation = { ...input, id: reservationId(), status: "reserved", createdAt: nowIso() };
|
|
34354
|
+
await this.sql`
|
|
34355
|
+
INSERT INTO skills_credit_reservations (id, org_id, run_id, estimated_cents, status)
|
|
34356
|
+
VALUES (${reservation.id}, ${reservation.orgId}, ${reservation.runId}, ${reservation.estimatedCents}, ${reservation.status})
|
|
34357
|
+
`;
|
|
34358
|
+
return reservation;
|
|
34359
|
+
}
|
|
34360
|
+
async reservationsForRun(orgId, runId2) {
|
|
34361
|
+
const rows = await this.sql`
|
|
34362
|
+
SELECT * FROM skills_credit_reservations WHERE org_id = ${orgId} AND run_id = ${runId2} ORDER BY created_at ASC
|
|
34363
|
+
`;
|
|
34364
|
+
return rows.map((row) => this.reservationFrom(row));
|
|
34365
|
+
}
|
|
34366
|
+
async reconcileReservation(reservationId2, actualCents, status) {
|
|
34367
|
+
const rows = await this.sql`
|
|
34368
|
+
UPDATE skills_credit_reservations
|
|
34369
|
+
SET actual_cents = ${actualCents}, status = ${status}, reconciled_at = now()
|
|
34370
|
+
WHERE id = ${reservationId2} AND status = ${"reserved"}
|
|
34371
|
+
RETURNING *
|
|
34372
|
+
`;
|
|
34373
|
+
if (!rows[0]) {
|
|
34374
|
+
const existing = await this.sql`SELECT * FROM skills_credit_reservations WHERE id = ${reservationId2} LIMIT 1`;
|
|
34375
|
+
return existing[0] ? this.reservationFrom(existing[0]) : null;
|
|
34376
|
+
}
|
|
34377
|
+
return this.reservationFrom(rows[0]);
|
|
34378
|
+
}
|
|
34379
|
+
async monthlySpendCents(orgId, monthPrefix) {
|
|
34380
|
+
return this.withContext(null, true, async (tx) => {
|
|
34381
|
+
const rows = await tx`
|
|
34382
|
+
SELECT
|
|
34383
|
+
(SELECT COALESCE(SUM(cost_cents), 0) FROM skills_runs
|
|
34384
|
+
WHERE org_id = ${orgId} AND created_at >= ${`${monthPrefix}-01T00:00:00.000Z`} AND created_at < ${`${nextMonthPrefix(monthPrefix)}-01T00:00:00.000Z`}) AS spent,
|
|
34385
|
+
(SELECT COALESCE(SUM(estimated_cents), 0) FROM skills_credit_reservations
|
|
34386
|
+
WHERE org_id = ${orgId} AND status = ${"reserved"}) AS pending
|
|
34387
|
+
`;
|
|
34388
|
+
return Number(rows[0]?.spent ?? 0) + Number(rows[0]?.pending ?? 0);
|
|
34389
|
+
});
|
|
34390
|
+
}
|
|
34391
|
+
async activeRunCount(orgId) {
|
|
34392
|
+
return this.withContext(orgId, false, async (tx) => {
|
|
34393
|
+
const rows = await tx`SELECT COUNT(*) AS n FROM skills_runs WHERE org_id = ${orgId} AND status IN (${"queued"}, ${"running"}, ${"cancel_requested"})`;
|
|
34394
|
+
return Number(rows[0]?.n ?? 0);
|
|
34395
|
+
});
|
|
34396
|
+
}
|
|
34397
|
+
async listExpiredArtifacts(at2) {
|
|
34398
|
+
return this.withContext(null, true, async (tx) => {
|
|
34399
|
+
const rows = await tx`
|
|
34400
|
+
SELECT a.* FROM skills_artifacts a
|
|
34401
|
+
JOIN skills_runs r ON r.id = a.run_id
|
|
34402
|
+
WHERE a.expires_at IS NOT NULL AND a.expires_at <= ${at2}
|
|
34403
|
+
ORDER BY a.expires_at ASC
|
|
34404
|
+
`;
|
|
34405
|
+
return rows.map(rowToArtifact);
|
|
34406
|
+
});
|
|
34407
|
+
}
|
|
34408
|
+
async deleteArtifactRow(artifactId2, orgId) {
|
|
34409
|
+
return this.withContext(orgId, true, async (tx) => {
|
|
34410
|
+
const rows = await tx`DELETE FROM skills_artifacts WHERE id = ${artifactId2} AND org_id = ${orgId} RETURNING id`;
|
|
34411
|
+
return rows.length > 0;
|
|
34412
|
+
});
|
|
34413
|
+
}
|
|
34414
|
+
async updateArtifactStorageKey(artifactId2, orgId, storageKey) {
|
|
34415
|
+
return this.withContext(orgId, true, async (tx) => {
|
|
34416
|
+
const rows = await tx`
|
|
34417
|
+
UPDATE skills_artifacts SET storage_key = ${storageKey} WHERE id = ${artifactId2} AND org_id = ${orgId} RETURNING id
|
|
34418
|
+
`;
|
|
34419
|
+
return rows.length > 0;
|
|
34420
|
+
});
|
|
34421
|
+
}
|
|
34422
|
+
reservationFrom(row) {
|
|
34423
|
+
return {
|
|
34424
|
+
id: String(row.id),
|
|
34425
|
+
orgId: String(row.org_id),
|
|
34426
|
+
runId: String(row.run_id),
|
|
34427
|
+
estimatedCents: Number(row.estimated_cents ?? 0),
|
|
34428
|
+
...row.actual_cents !== null && row.actual_cents !== undefined ? { actualCents: Number(row.actual_cents) } : {},
|
|
34429
|
+
status: String(row.status),
|
|
34430
|
+
createdAt: String(row.created_at),
|
|
34431
|
+
...typeof row.reconciled_at === "string" ? { reconciledAt: row.reconciled_at } : {}
|
|
34432
|
+
};
|
|
34433
|
+
}
|
|
34434
|
+
}
|
|
34435
|
+
async function createGovernanceStore(databaseUrl) {
|
|
34436
|
+
const target = resolveDatabaseTarget(databaseUrl);
|
|
34437
|
+
if (target.kind === "postgres")
|
|
34438
|
+
return new PostgresGovernanceStore(target.url);
|
|
34439
|
+
if (target.kind === "memory")
|
|
34440
|
+
return new SqliteGovernanceStore(SQLITE_MEMORY_PATH);
|
|
34441
|
+
return new SqliteGovernanceStore(target.path);
|
|
34442
|
+
}
|
|
34443
|
+
function nextMonthPrefix(monthPrefix) {
|
|
34444
|
+
const [year, month] = monthPrefix.split("-").map(Number);
|
|
34445
|
+
return month === 12 ? `${year + 1}-01` : `${year}-${String(month + 1).padStart(2, "0")}`;
|
|
34446
|
+
}
|
|
34447
|
+
|
|
34448
|
+
// src/server/config.ts
|
|
34449
|
+
var DATABASE_URL_ENV = "HASNA_SKILLS_DATABASE_URL";
|
|
34450
|
+
var SKILLS_ENV_NAMESPACE = "SKILLS";
|
|
34451
|
+
function resolveServerConfig(env = process.env) {
|
|
34452
|
+
assertNoRetiredModeEnvVars(env, {
|
|
34453
|
+
app: SKILLS_ENV_NAMESPACE,
|
|
34454
|
+
replacement: DATABASE_URL_ENV
|
|
34455
|
+
});
|
|
34456
|
+
const nodeEnv = env.NODE_ENV || "development";
|
|
34457
|
+
const host = env.HOST || env.SKILLS_HOST || "0.0.0.0";
|
|
34458
|
+
const port = parsePositiveInt(env.PORT || env.SKILLS_PORT, 8787);
|
|
34459
|
+
return {
|
|
34460
|
+
host,
|
|
34461
|
+
port,
|
|
34462
|
+
databaseUrl: env[DATABASE_URL_ENV] || env.DATABASE_URL || undefined,
|
|
34463
|
+
bootstrapApiKey: env.HASNA_SKILLS_BOOTSTRAP_API_KEY || undefined,
|
|
34464
|
+
artifactBucket: env.HASNA_SKILLS_S3_BUCKET || env.SKILLS_S3_BUCKET || undefined,
|
|
34465
|
+
artifactPrefix: normalizePrefix(env.HASNA_SKILLS_S3_PREFIX || env.SKILLS_S3_PREFIX || "skills/artifacts"),
|
|
34466
|
+
inlineWorker: env.HASNA_SKILLS_INLINE_WORKER === "1",
|
|
34467
|
+
bundleSigningKey: env.HASNA_SKILLS_API_SIGNING_KEY || env.HASNA_SKILLS_SIGNING_KEY || undefined,
|
|
34468
|
+
requestBodyLimitBytes: parsePositiveInt(env.HASNA_SKILLS_REQUEST_BODY_LIMIT_BYTES, 1e6),
|
|
34469
|
+
skillBundleLimitBytes: parsePositiveInt(env.HASNA_SKILLS_BUNDLE_LIMIT_BYTES, 25000000),
|
|
34470
|
+
tombstoneWindowMs: parsePositiveInt(env.HASNA_SKILLS_TOMBSTONE_WINDOW_MS, 7 * 24 * 60 * 60 * 1000),
|
|
34471
|
+
publicBaseUrl: (env.SKILLS_PUBLIC_BASE_URL || localOrigin(host, port)).replace(/\/+$/, ""),
|
|
34472
|
+
nodeEnv,
|
|
34473
|
+
allowEphemeralStore: env.HASNA_SKILLS_ALLOW_EPHEMERAL_STORE === "1"
|
|
34474
|
+
};
|
|
34475
|
+
}
|
|
34476
|
+
function localOrigin(host, port) {
|
|
34477
|
+
const hostname = host === "0.0.0.0" || host === "::" ? "localhost" : host;
|
|
34478
|
+
return `http://${hostname.includes(":") ? `[${hostname}]` : hostname}:${port}`;
|
|
34479
|
+
}
|
|
34480
|
+
function parsePositiveInt(value, fallback) {
|
|
34481
|
+
const parsed = Number.parseInt(value ?? "", 10);
|
|
34482
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
34483
|
+
}
|
|
34484
|
+
function normalizePrefix(value) {
|
|
34485
|
+
return value.replace(/^\/+|\/+$/g, "") || "skills/artifacts";
|
|
34486
|
+
}
|
|
34487
|
+
|
|
34488
|
+
// src/server/handlers.ts
|
|
34489
|
+
import { createHash as createHash5 } from "crypto";
|
|
34490
|
+
|
|
33798
34491
|
// src/server/store.ts
|
|
34492
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
34493
|
+
function recordFieldsOf(input, carriedBundle, carriedSkillMd) {
|
|
34494
|
+
return {
|
|
34495
|
+
slug: input.slug,
|
|
34496
|
+
displayName: input.displayName,
|
|
34497
|
+
description: input.description,
|
|
34498
|
+
category: input.category,
|
|
34499
|
+
tags: input.tags,
|
|
34500
|
+
source: input.source,
|
|
34501
|
+
kind: input.kind,
|
|
34502
|
+
...input.version ? { version: input.version } : {},
|
|
34503
|
+
...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
|
|
34504
|
+
bundleSha256: input.bundle?.sha256 ?? carriedBundle.bundleSha256,
|
|
34505
|
+
bundleByteSize: input.bundle?.byteSize ?? carriedBundle.bundleByteSize
|
|
34506
|
+
};
|
|
34507
|
+
}
|
|
33799
34508
|
function resolvePoolMax(env = process.env) {
|
|
33800
34509
|
const parsed = Number.parseInt(env.HASNA_SKILLS_DATABASE_POOL_MAX || env.SKILLS_DATABASE_POOL_MAX || "", 10);
|
|
33801
34510
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 4;
|
|
@@ -33833,6 +34542,7 @@ class MemorySkillsStore {
|
|
|
33833
34542
|
idempotency = new Map;
|
|
33834
34543
|
skills = new Map;
|
|
33835
34544
|
bundles = new Map;
|
|
34545
|
+
pins = new Map;
|
|
33836
34546
|
constructor(apiKeys = []) {
|
|
33837
34547
|
for (const key of apiKeys)
|
|
33838
34548
|
this.addApiKey(key.token, key.principal);
|
|
@@ -33867,7 +34577,7 @@ class MemorySkillsStore {
|
|
|
33867
34577
|
input: input.input,
|
|
33868
34578
|
args: input.args,
|
|
33869
34579
|
...idemKey ? { idempotencyKey: idemKey } : {},
|
|
33870
|
-
correlationId:
|
|
34580
|
+
correlationId: randomUUID4(),
|
|
33871
34581
|
costCents: 0,
|
|
33872
34582
|
leaseGeneration: 0,
|
|
33873
34583
|
createdAt: now
|
|
@@ -33934,6 +34644,9 @@ class MemorySkillsStore {
|
|
|
33934
34644
|
const key = skillKey(input.principal.orgId, input.slug);
|
|
33935
34645
|
const now = nowIso();
|
|
33936
34646
|
const previous = this.skills.get(key);
|
|
34647
|
+
if (previous && !previous.tombstonedAt && input.expectedRevisionId !== previous.revisionId) {
|
|
34648
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previous.revisionId);
|
|
34649
|
+
}
|
|
33937
34650
|
if (input.bundle) {
|
|
33938
34651
|
const bundleMapKey = skillKey(input.principal.orgId, input.bundle.sha256);
|
|
33939
34652
|
this.bundles.set(bundleMapKey, {
|
|
@@ -33943,6 +34656,8 @@ class MemorySkillsStore {
|
|
|
33943
34656
|
createdAt: this.bundles.get(bundleMapKey)?.createdAt ?? now
|
|
33944
34657
|
});
|
|
33945
34658
|
}
|
|
34659
|
+
const carriedBundle = !input.bundle && previous?.bundleSha256 ? { bundleSha256: previous.bundleSha256, ...previous.bundleByteSize === undefined ? {} : { bundleByteSize: previous.bundleByteSize } } : {};
|
|
34660
|
+
const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : previous?.skillMd;
|
|
33946
34661
|
const record = {
|
|
33947
34662
|
orgId: input.principal.orgId,
|
|
33948
34663
|
slug: input.slug,
|
|
@@ -33953,11 +34668,13 @@ class MemorySkillsStore {
|
|
|
33953
34668
|
source: input.source,
|
|
33954
34669
|
kind: input.kind,
|
|
33955
34670
|
...input.version ? { version: input.version } : {},
|
|
33956
|
-
...
|
|
33957
|
-
...input.bundle ? { bundleSha256: input.bundle.sha256, bundleByteSize: input.bundle.byteSize } :
|
|
34671
|
+
...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
|
|
34672
|
+
...input.bundle ? { bundleSha256: input.bundle.sha256, bundleByteSize: input.bundle.byteSize } : carriedBundle,
|
|
33958
34673
|
publishedByUserId: input.principal.userId,
|
|
33959
34674
|
createdAt: previous?.createdAt ?? now,
|
|
33960
|
-
updatedAt: now
|
|
34675
|
+
updatedAt: now,
|
|
34676
|
+
revisionId: revisionIdOfRecord(recordFieldsOf(input, carriedBundle, carriedSkillMd)),
|
|
34677
|
+
revisionNumber: (previous?.revisionNumber ?? 0) + 1
|
|
33961
34678
|
};
|
|
33962
34679
|
this.skills.set(key, record);
|
|
33963
34680
|
if (previous?.bundleSha256 && input.bundle && previous.bundleSha256 !== input.bundle.sha256) {
|
|
@@ -33966,33 +34683,107 @@ class MemorySkillsStore {
|
|
|
33966
34683
|
return record;
|
|
33967
34684
|
}
|
|
33968
34685
|
async listSkills(principal) {
|
|
33969
|
-
return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
|
|
34686
|
+
return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
|
|
33970
34687
|
}
|
|
33971
34688
|
async getSkill(principal, slug) {
|
|
33972
34689
|
const skill = this.skills.get(skillKey(principal.orgId, slug));
|
|
33973
34690
|
return skill && skill.orgId === principal.orgId ? skill : null;
|
|
33974
34691
|
}
|
|
33975
|
-
async updateSkill(principal, slug, patch) {
|
|
34692
|
+
async updateSkill(principal, slug, patch, expectedRevisionId) {
|
|
33976
34693
|
const current = await this.getSkill(principal, slug);
|
|
33977
|
-
if (!current)
|
|
34694
|
+
if (!current || current.tombstonedAt)
|
|
33978
34695
|
return null;
|
|
33979
|
-
|
|
34696
|
+
if (expectedRevisionId !== current.revisionId) {
|
|
34697
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
|
|
34698
|
+
}
|
|
34699
|
+
const latest = this.skills.get(skillKey(principal.orgId, slug));
|
|
34700
|
+
if (!latest || latest.tombstonedAt)
|
|
34701
|
+
return null;
|
|
34702
|
+
if (latest.revisionId !== current.revisionId) {
|
|
34703
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, latest.revisionId);
|
|
34704
|
+
}
|
|
34705
|
+
const next = {
|
|
34706
|
+
...latest,
|
|
34707
|
+
...patch,
|
|
34708
|
+
updatedAt: nowIso(),
|
|
34709
|
+
revisionId: revisionIdOfRecord({ ...latest, ...patch }),
|
|
34710
|
+
revisionNumber: latest.revisionNumber + 1
|
|
34711
|
+
};
|
|
33980
34712
|
this.skills.set(skillKey(principal.orgId, slug), next);
|
|
33981
34713
|
return next;
|
|
33982
34714
|
}
|
|
33983
|
-
async deleteSkill(principal, slug) {
|
|
34715
|
+
async deleteSkill(principal, slug, tombstoneWindowMs) {
|
|
33984
34716
|
const current = await this.getSkill(principal, slug);
|
|
33985
34717
|
if (!current)
|
|
33986
|
-
return
|
|
33987
|
-
|
|
33988
|
-
|
|
33989
|
-
|
|
33990
|
-
|
|
34718
|
+
return null;
|
|
34719
|
+
if (!current.tombstonedAt) {
|
|
34720
|
+
const tombstoned = nowIso();
|
|
34721
|
+
const purgeAfter = new Date(Date.now() + tombstoneWindowMs).toISOString();
|
|
34722
|
+
const next = { ...current, tombstonedAt: tombstoned, tombstonePurgeAfter: purgeAfter, updatedAt: tombstoned };
|
|
34723
|
+
this.skills.set(skillKey(principal.orgId, slug), next);
|
|
34724
|
+
return next;
|
|
34725
|
+
}
|
|
34726
|
+
return current;
|
|
34727
|
+
}
|
|
34728
|
+
async purgeExpiredTombstones(principal) {
|
|
34729
|
+
const now = nowIso();
|
|
34730
|
+
const purged = [];
|
|
34731
|
+
for (const [key, skill] of this.skills) {
|
|
34732
|
+
if (skill.orgId !== principal.orgId || !skill.tombstonedAt || !skill.tombstonePurgeAfter)
|
|
34733
|
+
continue;
|
|
34734
|
+
if (skill.tombstonePurgeAfter > now)
|
|
34735
|
+
continue;
|
|
34736
|
+
this.skills.delete(key);
|
|
34737
|
+
if (skill.bundleSha256)
|
|
34738
|
+
this.collectOrphanBundle(principal.orgId, skill.bundleSha256);
|
|
34739
|
+
purged.push(skill);
|
|
34740
|
+
}
|
|
34741
|
+
return purged;
|
|
33991
34742
|
}
|
|
33992
34743
|
async getSkillBundle(principal, sha256) {
|
|
33993
34744
|
const bundle = this.bundles.get(skillKey(principal.orgId, sha256));
|
|
33994
34745
|
return bundle && bundle.orgId === principal.orgId ? bundle : null;
|
|
33995
34746
|
}
|
|
34747
|
+
async pinSkill(principal, slug, metadata = {}) {
|
|
34748
|
+
const pin = { orgId: principal.orgId, principal: principal.apiKeyId, slug, pinnedAt: nowIso(), metadata: { ...metadata } };
|
|
34749
|
+
this.pins.set(pinKey(principal.orgId, principal.apiKeyId, slug), pin);
|
|
34750
|
+
return pin;
|
|
34751
|
+
}
|
|
34752
|
+
async unpinSkill(principal, slug) {
|
|
34753
|
+
return this.pins.delete(pinKey(principal.orgId, principal.apiKeyId, slug));
|
|
34754
|
+
}
|
|
34755
|
+
async listPins(principal) {
|
|
34756
|
+
return Array.from(this.pins.values()).filter((pin) => pin.orgId === principal.orgId && pin.principal === principal.apiKeyId).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
|
|
34757
|
+
}
|
|
34758
|
+
async listTags(principal) {
|
|
34759
|
+
await this.purgeExpiredTombstones(principal);
|
|
34760
|
+
const tags = new Set;
|
|
34761
|
+
for (const skill of this.skills.values()) {
|
|
34762
|
+
if (skill.orgId !== principal.orgId)
|
|
34763
|
+
continue;
|
|
34764
|
+
for (const tag of skill.tags) {
|
|
34765
|
+
if (tag.trim())
|
|
34766
|
+
tags.add(tag);
|
|
34767
|
+
}
|
|
34768
|
+
}
|
|
34769
|
+
return [...tags].sort();
|
|
34770
|
+
}
|
|
34771
|
+
async listSkillsByTag(principal, tag) {
|
|
34772
|
+
await this.purgeExpiredTombstones(principal);
|
|
34773
|
+
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));
|
|
34774
|
+
}
|
|
34775
|
+
async listPinsByTag(principal, tag) {
|
|
34776
|
+
await this.purgeExpiredTombstones(principal);
|
|
34777
|
+
const taggedSlugs = new Set;
|
|
34778
|
+
for (const skill of this.skills.values()) {
|
|
34779
|
+
if (skill.orgId === principal.orgId && !skill.tombstonedAt && skill.tags.includes(tag))
|
|
34780
|
+
taggedSlugs.add(skill.slug);
|
|
34781
|
+
}
|
|
34782
|
+
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));
|
|
34783
|
+
}
|
|
34784
|
+
async listPublishedSlugs(principal) {
|
|
34785
|
+
return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt).map((skill) => skill.slug).sort();
|
|
34786
|
+
}
|
|
33996
34787
|
collectOrphanBundle(orgId, sha256) {
|
|
33997
34788
|
const referenced = Array.from(this.skills.values()).some((skill) => skill.orgId === orgId && skill.bundleSha256 === sha256);
|
|
33998
34789
|
if (!referenced)
|
|
@@ -34010,6 +34801,9 @@ class MemorySkillsStore {
|
|
|
34010
34801
|
function skillKey(orgId, slug) {
|
|
34011
34802
|
return `${orgId.length}:${orgId}:${slug}`;
|
|
34012
34803
|
}
|
|
34804
|
+
function pinKey(orgId, principal, slug) {
|
|
34805
|
+
return `${orgId.length}:${orgId}:${principal.length}:${principal}:${slug}`;
|
|
34806
|
+
}
|
|
34013
34807
|
|
|
34014
34808
|
class PostgresSkillsStore {
|
|
34015
34809
|
backend = { kind: "postgres", durable: true, label: "postgres" };
|
|
@@ -34029,6 +34823,14 @@ class PostgresSkillsStore {
|
|
|
34029
34823
|
} catch (error) {
|
|
34030
34824
|
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)}`);
|
|
34031
34825
|
}
|
|
34826
|
+
await this.backfillLegacyRevisions();
|
|
34827
|
+
}
|
|
34828
|
+
async backfillLegacyRevisions() {
|
|
34829
|
+
const rows = await this.sql`SELECT * FROM skills_registry WHERE revision_id = ${""}`;
|
|
34830
|
+
for (const row of rows) {
|
|
34831
|
+
const record = rowToSkill(row);
|
|
34832
|
+
await this.sql`UPDATE skills_registry SET revision_id = ${revisionIdOfRecord(record)} WHERE org_id = ${record.orgId} AND slug = ${record.slug}`;
|
|
34833
|
+
}
|
|
34032
34834
|
}
|
|
34033
34835
|
async close() {
|
|
34034
34836
|
await this.sql.close?.();
|
|
@@ -34090,20 +34892,20 @@ class PostgresSkillsStore {
|
|
|
34090
34892
|
});
|
|
34091
34893
|
}
|
|
34092
34894
|
async createRun(input) {
|
|
34093
|
-
if (input.idempotencyKey) {
|
|
34094
|
-
const existing = await this.sql`
|
|
34095
|
-
SELECT * FROM skills_runs
|
|
34096
|
-
WHERE org_id = ${input.principal.orgId} AND idempotency_key = ${input.idempotencyKey}
|
|
34097
|
-
LIMIT 1
|
|
34098
|
-
`;
|
|
34099
|
-
if (existing[0])
|
|
34100
|
-
return rowToRun(existing[0]);
|
|
34101
|
-
}
|
|
34102
|
-
const id = runId();
|
|
34103
34895
|
return this.withContext(input.principal.orgId, false, async (tx) => {
|
|
34896
|
+
if (input.idempotencyKey) {
|
|
34897
|
+
const existing = await tx`
|
|
34898
|
+
SELECT * FROM skills_runs
|
|
34899
|
+
WHERE org_id = ${input.principal.orgId} AND idempotency_key = ${input.idempotencyKey}
|
|
34900
|
+
LIMIT 1
|
|
34901
|
+
`;
|
|
34902
|
+
if (existing[0])
|
|
34903
|
+
return rowToRun(existing[0]);
|
|
34904
|
+
}
|
|
34905
|
+
const id = runId();
|
|
34104
34906
|
const rows = await tx`
|
|
34105
34907
|
INSERT INTO skills_runs (id, org_id, user_id, skill_slug, requested_slug, status, input_json, args_json, idempotency_key, correlation_id)
|
|
34106
|
-
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}, ${
|
|
34908
|
+
VALUES (${id}, ${input.principal.orgId}, ${input.principal.userId}, ${input.slug}, ${input.slug}, ${"queued"}, ${JSON.stringify(input.input)}::jsonb, ${JSON.stringify(input.args)}::jsonb, ${input.idempotencyKey ?? null}, ${randomUUID4()})
|
|
34107
34909
|
RETURNING *
|
|
34108
34910
|
`;
|
|
34109
34911
|
return rowToRun(rows[0]);
|
|
@@ -34263,8 +35065,33 @@ class PostgresSkillsStore {
|
|
|
34263
35065
|
async publishSkill(input) {
|
|
34264
35066
|
const orgId = input.principal.orgId;
|
|
34265
35067
|
return await this.sql.begin(async (tx) => {
|
|
34266
|
-
const previousRows = await tx`
|
|
34267
|
-
|
|
35068
|
+
const previousRows = await tx`
|
|
35069
|
+
SELECT revision_id, revision_number, bundle_sha256, bundle_byte_size, skill_md, tombstoned_at
|
|
35070
|
+
FROM skills_registry WHERE org_id = ${orgId} AND slug = ${input.slug} LIMIT 1
|
|
35071
|
+
`;
|
|
35072
|
+
const previous = previousRows[0];
|
|
35073
|
+
const previousSha = typeof previous?.bundle_sha256 === "string" ? String(previous.bundle_sha256) : null;
|
|
35074
|
+
const previousRevisionId = typeof previous?.revision_id === "string" && previous.revision_id ? String(previous.revision_id) : null;
|
|
35075
|
+
const tombstoned = previous?.tombstoned_at != null;
|
|
35076
|
+
const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : typeof previous?.skill_md === "string" ? String(previous.skill_md) : null;
|
|
35077
|
+
if (previous && !tombstoned && input.expectedRevisionId !== previousRevisionId) {
|
|
35078
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
|
|
35079
|
+
}
|
|
35080
|
+
const carriedSha = input.bundle?.sha256 ?? previousSha;
|
|
35081
|
+
const carriedSize = input.bundle?.byteSize ?? (previous?.bundle_byte_size == null ? null : Number(previous.bundle_byte_size));
|
|
35082
|
+
const revisionId = revisionIdOfRecord({
|
|
35083
|
+
slug: input.slug,
|
|
35084
|
+
displayName: input.displayName,
|
|
35085
|
+
description: input.description,
|
|
35086
|
+
category: input.category,
|
|
35087
|
+
tags: input.tags,
|
|
35088
|
+
source: input.source,
|
|
35089
|
+
kind: input.kind,
|
|
35090
|
+
...input.version ? { version: input.version } : {},
|
|
35091
|
+
...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
|
|
35092
|
+
...carriedSha ? { bundleSha256: carriedSha } : {},
|
|
35093
|
+
...carriedSize === null || carriedSize === undefined ? {} : { bundleByteSize: carriedSize }
|
|
35094
|
+
});
|
|
34268
35095
|
if (input.bundle) {
|
|
34269
35096
|
await tx`
|
|
34270
35097
|
INSERT INTO skills_bundles (org_id, sha256, byte_size, content_type, storage_kind, storage_key, body_blob)
|
|
@@ -34279,10 +35106,10 @@ class PostgresSkillsStore {
|
|
|
34279
35106
|
}
|
|
34280
35107
|
const rows = await tx`
|
|
34281
35108
|
INSERT INTO skills_registry (org_id, slug, display_name, description, category, tags_json, source, kind, version, skill_md,
|
|
34282
|
-
bundle_sha256, bundle_byte_size, published_by_user_id, updated_at)
|
|
35109
|
+
bundle_sha256, bundle_byte_size, published_by_user_id, revision_id, revision_number, updated_at)
|
|
34283
35110
|
VALUES (${orgId}, ${input.slug}, ${input.displayName}, ${input.description}, ${input.category}, ${JSON.stringify(input.tags)}::jsonb,
|
|
34284
|
-
${input.source}, ${input.kind}, ${input.version ?? null}, ${
|
|
34285
|
-
${input.bundle?.sha256 ?? null}, ${input.bundle?.byteSize ?? null}, ${input.principal.userId}, now())
|
|
35111
|
+
${input.source}, ${input.kind}, ${input.version ?? null}, ${carriedSkillMd},
|
|
35112
|
+
${input.bundle?.sha256 ?? null}, ${input.bundle?.byteSize ?? null}, ${input.principal.userId}, ${revisionId}, 1, now())
|
|
34286
35113
|
ON CONFLICT (org_id, slug) DO UPDATE SET
|
|
34287
35114
|
display_name = EXCLUDED.display_name,
|
|
34288
35115
|
description = EXCLUDED.description,
|
|
@@ -34297,9 +35124,23 @@ class PostgresSkillsStore {
|
|
|
34297
35124
|
bundle_sha256 = COALESCE(EXCLUDED.bundle_sha256, skills_registry.bundle_sha256),
|
|
34298
35125
|
bundle_byte_size = COALESCE(EXCLUDED.bundle_byte_size, skills_registry.bundle_byte_size),
|
|
34299
35126
|
published_by_user_id = EXCLUDED.published_by_user_id,
|
|
35127
|
+
revision_id = EXCLUDED.revision_id,
|
|
35128
|
+
-- Current + 1, not EXCLUDED.revision_number: the insert path minted 1, but on
|
|
35129
|
+
-- the update path the ACTUAL row's counter is the truth (it may have advanced
|
|
35130
|
+
-- since the pre-read, which is exactly what the WHERE guard below detects).
|
|
35131
|
+
revision_number = skills_registry.revision_number + 1,
|
|
35132
|
+
tombstoned_at = NULL,
|
|
35133
|
+
tombstone_purge_after = NULL,
|
|
34300
35134
|
updated_at = EXCLUDED.updated_at
|
|
35135
|
+
WHERE skills_registry.tombstoned_at IS NOT NULL
|
|
35136
|
+
OR skills_registry.revision_id = ${input.expectedRevisionId ?? NO_REVISION_SENTINEL2}
|
|
34301
35137
|
RETURNING *
|
|
34302
35138
|
`;
|
|
35139
|
+
if (!rows[0]) {
|
|
35140
|
+
const current = await tx`SELECT revision_id FROM skills_registry WHERE org_id = ${orgId} AND slug = ${input.slug} LIMIT 1`;
|
|
35141
|
+
const currentId = current[0] && typeof current[0].revision_id === "string" ? String(current[0].revision_id) : null;
|
|
35142
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, currentId);
|
|
35143
|
+
}
|
|
34303
35144
|
if (previousSha && input.bundle && previousSha !== input.bundle.sha256) {
|
|
34304
35145
|
await tx`
|
|
34305
35146
|
DELETE FROM skills_bundles
|
|
@@ -34307,55 +35148,182 @@ class PostgresSkillsStore {
|
|
|
34307
35148
|
AND NOT EXISTS (SELECT 1 FROM skills_registry WHERE org_id = ${orgId} AND bundle_sha256 = ${previousSha})
|
|
34308
35149
|
`;
|
|
34309
35150
|
}
|
|
35151
|
+
await tx`DELETE FROM skills_tags WHERE org_id = ${orgId} AND slug = ${input.slug}`;
|
|
35152
|
+
for (const tag of input.tags) {
|
|
35153
|
+
if (!tag.trim())
|
|
35154
|
+
continue;
|
|
35155
|
+
await tx`
|
|
35156
|
+
INSERT INTO skills_tags (org_id, slug, tag) VALUES (${orgId}, ${input.slug}, ${tag})
|
|
35157
|
+
ON CONFLICT DO NOTHING
|
|
35158
|
+
`;
|
|
35159
|
+
}
|
|
34310
35160
|
return rowToSkill(rows[0]);
|
|
34311
35161
|
});
|
|
34312
35162
|
}
|
|
34313
35163
|
async listSkills(principal) {
|
|
34314
|
-
|
|
35164
|
+
await this.purgeExpiredTombstones(principal);
|
|
35165
|
+
const rows = await this.sql`
|
|
35166
|
+
SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND tombstoned_at IS NULL ORDER BY slug ASC
|
|
35167
|
+
`;
|
|
34315
35168
|
return rows.map(rowToSkill);
|
|
34316
35169
|
}
|
|
34317
35170
|
async getSkill(principal, slug) {
|
|
34318
35171
|
const rows = await this.sql`SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1`;
|
|
34319
35172
|
return rows[0] ? rowToSkill(rows[0]) : null;
|
|
34320
35173
|
}
|
|
34321
|
-
async updateSkill(principal, slug, patch) {
|
|
35174
|
+
async updateSkill(principal, slug, patch, expectedRevisionId) {
|
|
34322
35175
|
const current = await this.getSkill(principal, slug);
|
|
34323
|
-
if (!current)
|
|
35176
|
+
if (!current || current.tombstonedAt)
|
|
34324
35177
|
return null;
|
|
35178
|
+
if (expectedRevisionId !== current.revisionId) {
|
|
35179
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
|
|
35180
|
+
}
|
|
34325
35181
|
const next = { ...current, ...patch };
|
|
34326
|
-
|
|
34327
|
-
|
|
34328
|
-
|
|
34329
|
-
|
|
34330
|
-
|
|
34331
|
-
|
|
34332
|
-
|
|
34333
|
-
|
|
34334
|
-
|
|
35182
|
+
return await this.sql.begin(async (tx) => {
|
|
35183
|
+
const revisionId = revisionIdOfRecord(next);
|
|
35184
|
+
const updated = await tx`
|
|
35185
|
+
UPDATE skills_registry
|
|
35186
|
+
SET display_name = ${next.displayName}, description = ${next.description}, category = ${next.category},
|
|
35187
|
+
tags_json = ${JSON.stringify(next.tags)}::jsonb, kind = ${next.kind}, version = ${next.version ?? null},
|
|
35188
|
+
skill_md = ${next.skillMd ?? null}, revision_id = ${revisionId}, revision_number = revision_number + 1, updated_at = now()
|
|
35189
|
+
WHERE org_id = ${principal.orgId} AND slug = ${slug} AND tombstoned_at IS NULL AND revision_id = ${current.revisionId}
|
|
35190
|
+
RETURNING *
|
|
35191
|
+
`;
|
|
35192
|
+
if (!updated[0]) {
|
|
35193
|
+
const nowRows = await tx`
|
|
35194
|
+
SELECT revision_id, tombstoned_at FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1
|
|
35195
|
+
`;
|
|
35196
|
+
if (nowRows[0] && nowRows[0].tombstoned_at == null) {
|
|
35197
|
+
const currentId = String(nowRows[0].revision_id);
|
|
35198
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, currentId);
|
|
35199
|
+
}
|
|
35200
|
+
return null;
|
|
35201
|
+
}
|
|
35202
|
+
await tx`DELETE FROM skills_tags WHERE org_id = ${principal.orgId} AND slug = ${slug}`;
|
|
35203
|
+
for (const tag of next.tags) {
|
|
35204
|
+
if (!tag.trim())
|
|
35205
|
+
continue;
|
|
35206
|
+
await tx`
|
|
35207
|
+
INSERT INTO skills_tags (org_id, slug, tag) VALUES (${principal.orgId}, ${slug}, ${tag})
|
|
35208
|
+
ON CONFLICT DO NOTHING
|
|
35209
|
+
`;
|
|
35210
|
+
}
|
|
35211
|
+
return rowToSkill(updated[0]);
|
|
35212
|
+
});
|
|
34335
35213
|
}
|
|
34336
|
-
async deleteSkill(principal, slug) {
|
|
35214
|
+
async deleteSkill(principal, slug, tombstoneWindowMs) {
|
|
34337
35215
|
return await this.sql.begin(async (tx) => {
|
|
35216
|
+
const existingRows = await tx`
|
|
35217
|
+
SELECT tombstoned_at FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1
|
|
35218
|
+
`;
|
|
35219
|
+
if (!existingRows[0])
|
|
35220
|
+
return null;
|
|
35221
|
+
if (existingRows[0].tombstoned_at != null) {
|
|
35222
|
+
const rows2 = await tx`SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1`;
|
|
35223
|
+
return rowToSkill(rows2[0]);
|
|
35224
|
+
}
|
|
34338
35225
|
const rows = await tx`
|
|
34339
|
-
|
|
34340
|
-
|
|
35226
|
+
UPDATE skills_registry
|
|
35227
|
+
SET tombstoned_at = now(), tombstone_purge_after = now() + (${tombstoneWindowMs}::int * interval '1 millisecond'), updated_at = now()
|
|
35228
|
+
WHERE org_id = ${principal.orgId} AND slug = ${slug}
|
|
35229
|
+
RETURNING *
|
|
34341
35230
|
`;
|
|
34342
|
-
|
|
34343
|
-
|
|
34344
|
-
|
|
34345
|
-
|
|
35231
|
+
return rows[0] ? rowToSkill(rows[0]) : null;
|
|
35232
|
+
});
|
|
35233
|
+
}
|
|
35234
|
+
async purgeExpiredTombstones(principal) {
|
|
35235
|
+
return await this.sql.begin(async (tx) => {
|
|
35236
|
+
const expiredRows = await tx`
|
|
35237
|
+
SELECT * FROM skills_registry
|
|
35238
|
+
WHERE org_id = ${principal.orgId} AND tombstoned_at IS NOT NULL AND tombstone_purge_after <= now()
|
|
35239
|
+
`;
|
|
35240
|
+
if (!expiredRows.length)
|
|
35241
|
+
return [];
|
|
35242
|
+
const purged = [];
|
|
35243
|
+
for (const row of expiredRows) {
|
|
35244
|
+
const record = rowToSkill(row);
|
|
34346
35245
|
await tx`
|
|
34347
|
-
DELETE FROM
|
|
34348
|
-
|
|
34349
|
-
|
|
35246
|
+
DELETE FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${record.slug} AND tombstone_purge_after <= now()
|
|
35247
|
+
`;
|
|
35248
|
+
await tx`DELETE FROM skills_tags WHERE org_id = ${principal.orgId} AND slug = ${record.slug}`;
|
|
35249
|
+
await tx`
|
|
35250
|
+
DELETE FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${record.slug} AND tombstone_purge_after <= now()
|
|
34350
35251
|
`;
|
|
35252
|
+
if (record.bundleSha256) {
|
|
35253
|
+
await tx`
|
|
35254
|
+
DELETE FROM skills_bundles
|
|
35255
|
+
WHERE org_id = ${principal.orgId} AND sha256 = ${record.bundleSha256}
|
|
35256
|
+
AND NOT EXISTS (SELECT 1 FROM skills_registry WHERE org_id = ${principal.orgId} AND bundle_sha256 = ${record.bundleSha256})
|
|
35257
|
+
`;
|
|
35258
|
+
}
|
|
35259
|
+
purged.push(record);
|
|
34351
35260
|
}
|
|
34352
|
-
return
|
|
35261
|
+
return purged;
|
|
34353
35262
|
});
|
|
34354
35263
|
}
|
|
34355
35264
|
async getSkillBundle(principal, sha256) {
|
|
34356
35265
|
const rows = await this.sql`SELECT * FROM skills_bundles WHERE org_id = ${principal.orgId} AND sha256 = ${sha256} LIMIT 1`;
|
|
34357
35266
|
return rows[0] ? rowToSkillBundle(rows[0]) : null;
|
|
34358
35267
|
}
|
|
35268
|
+
async pinSkill(principal, slug, metadata = {}) {
|
|
35269
|
+
const rows = await this.sql`
|
|
35270
|
+
INSERT INTO skills_pins (org_id, principal, slug, pinned_at, metadata_json)
|
|
35271
|
+
VALUES (${principal.orgId}, ${principal.apiKeyId}, ${slug}, now(), ${JSON.stringify(metadata)}::jsonb)
|
|
35272
|
+
ON CONFLICT (org_id, principal, slug) DO UPDATE SET
|
|
35273
|
+
pinned_at = now(),
|
|
35274
|
+
metadata_json = EXCLUDED.metadata_json
|
|
35275
|
+
RETURNING *
|
|
35276
|
+
`;
|
|
35277
|
+
return rowToPin(rows[0]);
|
|
35278
|
+
}
|
|
35279
|
+
async unpinSkill(principal, slug) {
|
|
35280
|
+
const rows = await this.sql`
|
|
35281
|
+
DELETE FROM skills_pins WHERE org_id = ${principal.orgId} AND principal = ${principal.apiKeyId} AND slug = ${slug}
|
|
35282
|
+
RETURNING 1 AS present
|
|
35283
|
+
`;
|
|
35284
|
+
return rows.length > 0;
|
|
35285
|
+
}
|
|
35286
|
+
async listPins(principal) {
|
|
35287
|
+
const rows = await this.sql`
|
|
35288
|
+
SELECT * FROM skills_pins WHERE org_id = ${principal.orgId} AND principal = ${principal.apiKeyId} ORDER BY slug ASC
|
|
35289
|
+
`;
|
|
35290
|
+
return rows.map(rowToPin);
|
|
35291
|
+
}
|
|
35292
|
+
async listTags(principal) {
|
|
35293
|
+
await this.purgeExpiredTombstones(principal);
|
|
35294
|
+
const rows = await this.sql`
|
|
35295
|
+
SELECT DISTINCT tag FROM skills_tags WHERE org_id = ${principal.orgId} ORDER BY tag ASC
|
|
35296
|
+
`;
|
|
35297
|
+
return rows.map((row) => String(row.tag));
|
|
35298
|
+
}
|
|
35299
|
+
async listSkillsByTag(principal, tag) {
|
|
35300
|
+
await this.purgeExpiredTombstones(principal);
|
|
35301
|
+
const rows = await this.sql`
|
|
35302
|
+
SELECT s.* FROM skills_registry s
|
|
35303
|
+
JOIN skills_tags t ON t.org_id = s.org_id AND t.slug = s.slug
|
|
35304
|
+
WHERE t.org_id = ${principal.orgId} AND t.tag = ${tag} AND s.tombstoned_at IS NULL
|
|
35305
|
+
ORDER BY s.slug ASC
|
|
35306
|
+
`;
|
|
35307
|
+
return rows.map(rowToSkill);
|
|
35308
|
+
}
|
|
35309
|
+
async listPinsByTag(principal, tag) {
|
|
35310
|
+
await this.purgeExpiredTombstones(principal);
|
|
35311
|
+
const rows = await this.sql`
|
|
35312
|
+
SELECT p.* FROM skills_pins p
|
|
35313
|
+
JOIN skills_tags t ON t.org_id = p.org_id AND t.slug = p.slug
|
|
35314
|
+
JOIN skills_registry s ON s.org_id = p.org_id AND s.slug = p.slug
|
|
35315
|
+
WHERE p.org_id = ${principal.orgId} AND p.principal = ${principal.apiKeyId}
|
|
35316
|
+
AND t.tag = ${tag} AND s.tombstoned_at IS NULL
|
|
35317
|
+
ORDER BY p.slug ASC
|
|
35318
|
+
`;
|
|
35319
|
+
return rows.map(rowToPin);
|
|
35320
|
+
}
|
|
35321
|
+
async listPublishedSlugs(principal) {
|
|
35322
|
+
const rows = await this.sql`
|
|
35323
|
+
SELECT slug FROM skills_registry WHERE org_id = ${principal.orgId} AND tombstoned_at IS NULL ORDER BY slug ASC
|
|
35324
|
+
`;
|
|
35325
|
+
return rows.map((row) => String(row.slug));
|
|
35326
|
+
}
|
|
34359
35327
|
async collectOrphanBundle(orgId, sha256) {
|
|
34360
35328
|
await this.sql`
|
|
34361
35329
|
DELETE FROM skills_bundles
|
|
@@ -34364,6 +35332,7 @@ class PostgresSkillsStore {
|
|
|
34364
35332
|
`;
|
|
34365
35333
|
}
|
|
34366
35334
|
}
|
|
35335
|
+
var NO_REVISION_SENTINEL2 = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
34367
35336
|
function isUniqueViolation(error) {
|
|
34368
35337
|
const code = error?.code;
|
|
34369
35338
|
if (code === "23505" || code === 23505)
|
|
@@ -34425,7 +35394,7 @@ ${summary}
|
|
|
34425
35394
|
textArtifact(run, "show-notes.md", `# Show Notes
|
|
34426
35395
|
|
|
34427
35396
|
- ${summary}
|
|
34428
|
-
- Generated by the
|
|
35397
|
+
- Generated by the skills deterministic worker.
|
|
34429
35398
|
`),
|
|
34430
35399
|
textArtifact(run, "clips.csv", `start,end,title,summary
|
|
34431
35400
|
00:00,00:30,"Opening","${csv(summary)}"
|
|
@@ -34459,7 +35428,7 @@ function textArtifact(run, relativePath, bodyText, contentType = relativePath.en
|
|
|
34459
35428
|
relativePath,
|
|
34460
35429
|
contentType,
|
|
34461
35430
|
byteSize: bytes.byteLength,
|
|
34462
|
-
sha256:
|
|
35431
|
+
sha256: createHash5("sha256").update(bytes).digest("hex"),
|
|
34463
35432
|
visibility: "private"
|
|
34464
35433
|
},
|
|
34465
35434
|
body: { relativePath, bodyText, contentType }
|
|
@@ -34475,7 +35444,9 @@ async function completeRun(store, run, preview) {
|
|
|
34475
35444
|
return next ?? run;
|
|
34476
35445
|
}
|
|
34477
35446
|
async function failRun(store, run, code, message) {
|
|
34478
|
-
|
|
35447
|
+
try {
|
|
35448
|
+
await store.appendLog(run.id, run.orgId, "error", message);
|
|
35449
|
+
} catch {}
|
|
34479
35450
|
const next = await fencedTransition(store, run, {
|
|
34480
35451
|
status: "failed",
|
|
34481
35452
|
errorCode: code,
|
|
@@ -34489,7 +35460,9 @@ async function fencedTransition(store, run, patch) {
|
|
|
34489
35460
|
return store.updateRun(run.id, patch);
|
|
34490
35461
|
const next = await store.transitionRun(run.id, patch, run.leaseGeneration);
|
|
34491
35462
|
if (!next) {
|
|
34492
|
-
|
|
35463
|
+
try {
|
|
35464
|
+
await store.appendLog(run.id, run.orgId, "warn", `late write rejected: run no longer owned at lease_generation ${run.leaseGeneration}`);
|
|
35465
|
+
} catch {}
|
|
34493
35466
|
}
|
|
34494
35467
|
return next;
|
|
34495
35468
|
}
|
|
@@ -34521,7 +35494,7 @@ function csv(value) {
|
|
|
34521
35494
|
}
|
|
34522
35495
|
|
|
34523
35496
|
// src/server/skills-api.ts
|
|
34524
|
-
import { createHash as
|
|
35497
|
+
import { createHash as createHash6 } from "crypto";
|
|
34525
35498
|
|
|
34526
35499
|
// src/lib/registry-merge.ts
|
|
34527
35500
|
var SKILL_SOURCE_PRECEDENCE = {
|
|
@@ -34571,25 +35544,26 @@ function mergeSkillRegistryLists(...groups) {
|
|
|
34571
35544
|
}
|
|
34572
35545
|
|
|
34573
35546
|
// src/server/registry.ts
|
|
34574
|
-
import { existsSync as
|
|
35547
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
34575
35548
|
import { resolve, sep } from "path";
|
|
34576
35549
|
|
|
34577
35550
|
// src/lib/registry.ts
|
|
34578
|
-
import { existsSync as
|
|
34579
|
-
import { join as
|
|
35551
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync5 } from "fs";
|
|
35552
|
+
import { join as join9 } from "path";
|
|
34580
35553
|
|
|
34581
35554
|
// src/lib/portable-skills.ts
|
|
34582
35555
|
import {
|
|
34583
35556
|
cpSync as cpSync2,
|
|
34584
|
-
existsSync as
|
|
34585
|
-
mkdirSync as
|
|
34586
|
-
|
|
35557
|
+
existsSync as existsSync5,
|
|
35558
|
+
mkdirSync as mkdirSync5,
|
|
35559
|
+
mkdtempSync,
|
|
35560
|
+
readdirSync as readdirSync4,
|
|
34587
35561
|
renameSync,
|
|
34588
35562
|
rmSync,
|
|
34589
|
-
statSync as
|
|
35563
|
+
statSync as statSync4,
|
|
34590
35564
|
writeFileSync as writeFileSync3
|
|
34591
35565
|
} from "fs";
|
|
34592
|
-
import { basename as basename2, dirname as
|
|
35566
|
+
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join8, normalize } from "path";
|
|
34593
35567
|
|
|
34594
35568
|
// src/lib/registry-data/development-tools.ts
|
|
34595
35569
|
var DEVELOPMENT_TOOLS_SKILLS = [
|
|
@@ -34749,7 +35723,7 @@ var DEVELOPMENT_TOOLS_SKILLS = [
|
|
|
34749
35723
|
{
|
|
34750
35724
|
name: "monitor",
|
|
34751
35725
|
displayName: "Monitor",
|
|
34752
|
-
description: "Operate the
|
|
35726
|
+
description: "Operate the monitor MCP for machine health, processes, cron jobs, and cleanup workflows",
|
|
34753
35727
|
category: "Development Tools",
|
|
34754
35728
|
tags: ["monitoring", "mcp", "processes", "operations"]
|
|
34755
35729
|
},
|
|
@@ -34795,6 +35769,14 @@ var DEVELOPMENT_TOOLS_SKILLS = [
|
|
|
34795
35769
|
description: "Validate configuration files for syntax and schema compliance",
|
|
34796
35770
|
category: "Development Tools",
|
|
34797
35771
|
tags: ["config", "validation", "schema", "linting"]
|
|
35772
|
+
},
|
|
35773
|
+
{
|
|
35774
|
+
name: "session-inject-monitor",
|
|
35775
|
+
displayName: "Session Inject Monitor",
|
|
35776
|
+
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",
|
|
35777
|
+
category: "Development Tools",
|
|
35778
|
+
tags: ["monitor", "session", "injection", "automation", "wake"],
|
|
35779
|
+
kind: "instruction"
|
|
34798
35780
|
}
|
|
34799
35781
|
];
|
|
34800
35782
|
|
|
@@ -35182,7 +36164,7 @@ var DESIGN_BRANDING_SKILLS = [
|
|
|
35182
36164
|
displayName: "Site Analyze",
|
|
35183
36165
|
description: "Analyze any website's design system \u2014 detects shadcn/ui, Tailwind, extracts colors, typography, and components via Playwright + Claude Vision.",
|
|
35184
36166
|
category: "Design & Branding",
|
|
35185
|
-
tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "
|
|
36167
|
+
tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "styles"]
|
|
35186
36168
|
}
|
|
35187
36169
|
];
|
|
35188
36170
|
|
|
@@ -35293,8 +36275,29 @@ var SKILLS = [
|
|
|
35293
36275
|
];
|
|
35294
36276
|
|
|
35295
36277
|
// src/lib/hosted-skill-set.ts
|
|
36278
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
36279
|
+
import { join as join6 } from "path";
|
|
35296
36280
|
var HOSTED_RUNTIMES = new Set(["hosted"]);
|
|
35297
36281
|
var HOSTED_SOURCES = new Set(["remote", "private-hosted"]);
|
|
36282
|
+
function normalizeMarker(value) {
|
|
36283
|
+
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
36284
|
+
}
|
|
36285
|
+
function isHostedMetadataPackage(pkg) {
|
|
36286
|
+
const skills = pkg?.skills;
|
|
36287
|
+
if (!skills || typeof skills !== "object")
|
|
36288
|
+
return false;
|
|
36289
|
+
return HOSTED_RUNTIMES.has(normalizeMarker(skills.runtime)) || HOSTED_SOURCES.has(normalizeMarker(skills.source));
|
|
36290
|
+
}
|
|
36291
|
+
function isHostedMetadataSkillDir(skillDir) {
|
|
36292
|
+
const pkgPath = join6(skillDir, "package.json");
|
|
36293
|
+
if (!existsSync3(pkgPath))
|
|
36294
|
+
return false;
|
|
36295
|
+
try {
|
|
36296
|
+
return isHostedMetadataPackage(JSON.parse(readFileSync4(pkgPath, "utf8")));
|
|
36297
|
+
} catch {
|
|
36298
|
+
return false;
|
|
36299
|
+
}
|
|
36300
|
+
}
|
|
35298
36301
|
var HOSTED_METADATA_SET_EMPTY_ERROR = [
|
|
35299
36302
|
"The hosted metadata skill set is empty, but the packaging guards that depend on it",
|
|
35300
36303
|
"only mean anything while it is non-empty: an empty set makes every one of them pass",
|
|
@@ -35416,14 +36419,14 @@ var PORTABLE_SKILL_DEFAULT_VERSION = "0.1.0";
|
|
|
35416
36419
|
// src/lib/portable-skills-files.ts
|
|
35417
36420
|
import {
|
|
35418
36421
|
cpSync,
|
|
35419
|
-
existsSync as
|
|
36422
|
+
existsSync as existsSync4,
|
|
35420
36423
|
lstatSync,
|
|
35421
|
-
mkdirSync as
|
|
35422
|
-
readFileSync as
|
|
36424
|
+
mkdirSync as mkdirSync4,
|
|
36425
|
+
readFileSync as readFileSync5,
|
|
35423
36426
|
realpathSync,
|
|
35424
36427
|
writeFileSync as writeFileSync2
|
|
35425
36428
|
} from "fs";
|
|
35426
|
-
import { basename, dirname as
|
|
36429
|
+
import { basename, dirname as dirname6, join as join7, relative } from "path";
|
|
35427
36430
|
var ANY_SEGMENT_COPY_EXCLUDES = new Set([
|
|
35428
36431
|
".git",
|
|
35429
36432
|
".DS_Store",
|
|
@@ -35451,12 +36454,12 @@ function normalizePortableSkillName(name) {
|
|
|
35451
36454
|
return normalized;
|
|
35452
36455
|
}
|
|
35453
36456
|
function readPortableSkillManifest(skillPath, fallbackName = basename(skillPath)) {
|
|
35454
|
-
const skillJsonPath =
|
|
35455
|
-
const skillMdPath =
|
|
35456
|
-
const pkgPath =
|
|
35457
|
-
const jsonManifest =
|
|
35458
|
-
const frontmatter =
|
|
35459
|
-
const pkg =
|
|
36457
|
+
const skillJsonPath = join7(skillPath, "skill.json");
|
|
36458
|
+
const skillMdPath = join7(skillPath, "SKILL.md");
|
|
36459
|
+
const pkgPath = join7(skillPath, "package.json");
|
|
36460
|
+
const jsonManifest = existsSync4(skillJsonPath) ? readJsonObject(skillJsonPath) : undefined;
|
|
36461
|
+
const frontmatter = existsSync4(skillMdPath) ? parseSkillFrontmatter(readFileSync5(skillMdPath, "utf-8")) ?? undefined : undefined;
|
|
36462
|
+
const pkg = existsSync4(pkgPath) ? readJsonObject(pkgPath) : undefined;
|
|
35460
36463
|
const name = normalizePortableSkillName(stringField(jsonManifest, "name") ?? frontmatter?.name ?? stringValue(pkg?.name) ?? fallbackName);
|
|
35461
36464
|
const description = stringField(jsonManifest, "description") ?? frontmatter?.description ?? stringValue(pkg?.description) ?? `${name} skill`;
|
|
35462
36465
|
const version2 = stringField(jsonManifest, "version") ?? frontmatter?.version ?? stringValue(pkg?.version) ?? PORTABLE_SKILL_DEFAULT_VERSION;
|
|
@@ -35583,7 +36586,7 @@ function inferPackageCommands(pkg, fallbackName) {
|
|
|
35583
36586
|
return;
|
|
35584
36587
|
}
|
|
35585
36588
|
function readJsonObject(path) {
|
|
35586
|
-
const parsed = JSON.parse(
|
|
36589
|
+
const parsed = JSON.parse(readFileSync5(path, "utf-8"));
|
|
35587
36590
|
if (!isRecord(parsed))
|
|
35588
36591
|
throw new Error(`${basename(path)} must contain a JSON object`);
|
|
35589
36592
|
return parsed;
|
|
@@ -35612,33 +36615,36 @@ var LEGACY_CUSTOM_DIRNAME = "custom";
|
|
|
35612
36615
|
function getPortableSkillsRoot(options = {}) {
|
|
35613
36616
|
if (options.rootDir)
|
|
35614
36617
|
return options.rootDir;
|
|
35615
|
-
const appDir = options.homeDir ?
|
|
35616
|
-
const
|
|
36618
|
+
const appDir = options.homeDir ? join8(options.homeDir, ".hasna", "skills") : getDataDir();
|
|
36619
|
+
const cache3 = join8(appDir, SKILLS_CACHE_DIRNAME);
|
|
36620
|
+
if (isOwnerLayoutMigrated(appDir) && safeIsDirectory(cache3))
|
|
36621
|
+
return cache3;
|
|
36622
|
+
const installed = join8(appDir, INSTALLED_SKILLS_DIRNAME);
|
|
35617
36623
|
migrateLegacySkillLayout(appDir, installed);
|
|
35618
36624
|
return installed;
|
|
35619
36625
|
}
|
|
35620
36626
|
function looksLikeSkillDirectory(path) {
|
|
35621
36627
|
if (!safeIsDirectory(path))
|
|
35622
36628
|
return false;
|
|
35623
|
-
return
|
|
36629
|
+
return existsSync5(join8(path, "SKILL.md")) || existsSync5(join8(path, "skill.json")) || existsSync5(join8(path, "package.json"));
|
|
35624
36630
|
}
|
|
35625
36631
|
function migrateLegacySkillLayout(appDir, installed) {
|
|
35626
36632
|
if (!safeIsDirectory(appDir))
|
|
35627
36633
|
return;
|
|
35628
36634
|
const candidates = [];
|
|
35629
36635
|
try {
|
|
35630
|
-
for (const entry of
|
|
36636
|
+
for (const entry of readdirSync4(appDir)) {
|
|
35631
36637
|
if (entry.startsWith(".") || entry === INSTALLED_SKILLS_DIRNAME)
|
|
35632
36638
|
continue;
|
|
35633
|
-
const path =
|
|
36639
|
+
const path = join8(appDir, entry);
|
|
35634
36640
|
if (entry === LEGACY_CUSTOM_DIRNAME) {
|
|
35635
36641
|
if (!safeIsDirectory(path))
|
|
35636
36642
|
continue;
|
|
35637
36643
|
try {
|
|
35638
|
-
for (const nested of
|
|
36644
|
+
for (const nested of readdirSync4(path)) {
|
|
35639
36645
|
if (nested.startsWith("."))
|
|
35640
36646
|
continue;
|
|
35641
|
-
const nestedPath =
|
|
36647
|
+
const nestedPath = join8(path, nested);
|
|
35642
36648
|
if (looksLikeSkillDirectory(nestedPath))
|
|
35643
36649
|
candidates.push({ from: nestedPath, name: nested });
|
|
35644
36650
|
}
|
|
@@ -35652,10 +36658,10 @@ function migrateLegacySkillLayout(appDir, installed) {
|
|
|
35652
36658
|
return;
|
|
35653
36659
|
}
|
|
35654
36660
|
for (const { from, name } of candidates) {
|
|
35655
|
-
const target =
|
|
35656
|
-
if (
|
|
36661
|
+
const target = join8(installed, name);
|
|
36662
|
+
if (existsSync5(target))
|
|
35657
36663
|
continue;
|
|
35658
|
-
const staging =
|
|
36664
|
+
const staging = join8(installed, `.migrating-${name}-${process.pid}`);
|
|
35659
36665
|
try {
|
|
35660
36666
|
rmSync(staging, { recursive: true, force: true });
|
|
35661
36667
|
cpSync2(from, staging, { recursive: true, errorOnExist: false });
|
|
@@ -35668,7 +36674,7 @@ function migrateLegacySkillLayout(appDir, installed) {
|
|
|
35668
36674
|
}
|
|
35669
36675
|
}
|
|
35670
36676
|
function getPortableSkillPath(name, options = {}) {
|
|
35671
|
-
return
|
|
36677
|
+
return join8(getPortableSkillsRoot(options), normalizePortableSkillName(name));
|
|
35672
36678
|
}
|
|
35673
36679
|
function findPortableSkill(name, options = {}) {
|
|
35674
36680
|
let normalized;
|
|
@@ -35678,7 +36684,7 @@ function findPortableSkill(name, options = {}) {
|
|
|
35678
36684
|
return null;
|
|
35679
36685
|
}
|
|
35680
36686
|
const path = getPortableSkillPath(normalized, options);
|
|
35681
|
-
if (!
|
|
36687
|
+
if (!existsSync5(path) || !statSync4(path).isDirectory())
|
|
35682
36688
|
return null;
|
|
35683
36689
|
try {
|
|
35684
36690
|
return summarizePortableSkill(path, normalized);
|
|
@@ -35691,10 +36697,10 @@ function listPortableSkills(options = {}) {
|
|
|
35691
36697
|
if (!safeIsDirectory(root3))
|
|
35692
36698
|
return [];
|
|
35693
36699
|
const skills = [];
|
|
35694
|
-
for (const entry of
|
|
36700
|
+
for (const entry of readdirSync4(root3).sort()) {
|
|
35695
36701
|
if (entry.startsWith("."))
|
|
35696
36702
|
continue;
|
|
35697
|
-
const path =
|
|
36703
|
+
const path = join8(root3, entry);
|
|
35698
36704
|
if (!safeIsDirectory(path))
|
|
35699
36705
|
continue;
|
|
35700
36706
|
try {
|
|
@@ -35716,7 +36722,8 @@ function listPortableSkillMetas(options = {}) {
|
|
|
35716
36722
|
tags: manifest.tags || ["custom"],
|
|
35717
36723
|
version: skill.version,
|
|
35718
36724
|
...manifest.kind ? { kind: manifest.kind } : {},
|
|
35719
|
-
source: "custom"
|
|
36725
|
+
source: "custom",
|
|
36726
|
+
...isHostedMetadataSkillDir(skill.path) ? { serverOwned: true } : {}
|
|
35720
36727
|
};
|
|
35721
36728
|
});
|
|
35722
36729
|
}
|
|
@@ -35736,7 +36743,7 @@ function summarizePortableSkill(skillPath, fallbackName) {
|
|
|
35736
36743
|
}
|
|
35737
36744
|
function safeIsDirectory(path) {
|
|
35738
36745
|
try {
|
|
35739
|
-
return
|
|
36746
|
+
return statSync4(path).isDirectory();
|
|
35740
36747
|
} catch {
|
|
35741
36748
|
return false;
|
|
35742
36749
|
}
|
|
@@ -35786,20 +36793,20 @@ function parseSkillMdFrontmatter(content) {
|
|
|
35786
36793
|
return Object.keys(result).length > 0 ? result : null;
|
|
35787
36794
|
}
|
|
35788
36795
|
function discoverSkillsInDir(dir, source = "custom") {
|
|
35789
|
-
if (!
|
|
36796
|
+
if (!existsSync6(dir))
|
|
35790
36797
|
return [];
|
|
35791
36798
|
const result = [];
|
|
35792
36799
|
try {
|
|
35793
|
-
const entries =
|
|
36800
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
35794
36801
|
for (const entry of entries) {
|
|
35795
36802
|
if (!entry.isDirectory())
|
|
35796
36803
|
continue;
|
|
35797
|
-
const skillMdPath =
|
|
35798
|
-
if (!
|
|
36804
|
+
const skillMdPath = join9(dir, entry.name, "SKILL.md");
|
|
36805
|
+
if (!existsSync6(skillMdPath))
|
|
35799
36806
|
continue;
|
|
35800
36807
|
let content;
|
|
35801
36808
|
try {
|
|
35802
|
-
content =
|
|
36809
|
+
content = readFileSync6(skillMdPath, "utf-8");
|
|
35803
36810
|
} catch {
|
|
35804
36811
|
continue;
|
|
35805
36812
|
}
|
|
@@ -35814,6 +36821,7 @@ function discoverSkillsInDir(dir, source = "custom") {
|
|
|
35814
36821
|
category: fm.category || "Development Tools",
|
|
35815
36822
|
tags: fm.tags || [],
|
|
35816
36823
|
...fm.kind ? { kind: fm.kind } : {},
|
|
36824
|
+
...isHostedMetadataSkillDir(join9(dir, entry.name)) ? { serverOwned: true } : {},
|
|
35817
36825
|
source
|
|
35818
36826
|
});
|
|
35819
36827
|
}
|
|
@@ -35822,20 +36830,20 @@ function discoverSkillsInDir(dir, source = "custom") {
|
|
|
35822
36830
|
}
|
|
35823
36831
|
function findExtensionSkillPath(name) {
|
|
35824
36832
|
const config = loadConfig4();
|
|
35825
|
-
if (!config.extensionsDir || !
|
|
36833
|
+
if (!config.extensionsDir || !existsSync6(config.extensionsDir))
|
|
35826
36834
|
return null;
|
|
35827
36835
|
try {
|
|
35828
|
-
const entries =
|
|
36836
|
+
const entries = readdirSync5(config.extensionsDir, { withFileTypes: true });
|
|
35829
36837
|
for (const entry of entries) {
|
|
35830
36838
|
if (!entry.isDirectory())
|
|
35831
36839
|
continue;
|
|
35832
|
-
const skillDir =
|
|
35833
|
-
const skillMdPath =
|
|
35834
|
-
if (!
|
|
36840
|
+
const skillDir = join9(config.extensionsDir, entry.name);
|
|
36841
|
+
const skillMdPath = join9(skillDir, "SKILL.md");
|
|
36842
|
+
if (!existsSync6(skillMdPath))
|
|
35835
36843
|
continue;
|
|
35836
36844
|
let content;
|
|
35837
36845
|
try {
|
|
35838
|
-
content =
|
|
36846
|
+
content = readFileSync6(skillMdPath, "utf-8");
|
|
35839
36847
|
} catch {
|
|
35840
36848
|
continue;
|
|
35841
36849
|
}
|
|
@@ -35868,7 +36876,7 @@ function loadRegistry(cwd) {
|
|
|
35868
36876
|
const official = SKILLS.map((s2) => ({ ...s2, source: "official" }));
|
|
35869
36877
|
const extensions = config.extensionsDir ? discoverSkillsInDir(config.extensionsDir, "extension") : [];
|
|
35870
36878
|
const portableCustom = listPortableSkillMetas();
|
|
35871
|
-
const legacyCustom = discoverSkillsInDir(
|
|
36879
|
+
const legacyCustom = discoverSkillsInDir(join9(dataDir, "custom"));
|
|
35872
36880
|
const globalCustom = mergeCustomSkills([...legacyCustom, ...portableCustom]);
|
|
35873
36881
|
registryCache = mergeSkillRegistryLists(official, extensions, globalCustom);
|
|
35874
36882
|
registryCacheTime = now;
|
|
@@ -35888,30 +36896,29 @@ function mergeCustomSkills(skills) {
|
|
|
35888
36896
|
}
|
|
35889
36897
|
|
|
35890
36898
|
// src/lib/skillinfo.ts
|
|
35891
|
-
import { existsSync as
|
|
35892
|
-
import { join as
|
|
36899
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
36900
|
+
import { join as join11 } from "path";
|
|
35893
36901
|
|
|
35894
36902
|
// src/lib/installer.ts
|
|
35895
|
-
import { existsSync as
|
|
35896
|
-
import { dirname as
|
|
36903
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, rmSync as rmSync2 } from "fs";
|
|
36904
|
+
import { dirname as dirname8, join as join10 } from "path";
|
|
35897
36905
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
35898
|
-
|
|
35899
36906
|
// src/lib/utils.ts
|
|
35900
36907
|
function normalizeSkillName(name) {
|
|
35901
36908
|
return name;
|
|
35902
36909
|
}
|
|
35903
36910
|
|
|
35904
36911
|
// src/lib/installer.ts
|
|
35905
|
-
var __dirname2 =
|
|
36912
|
+
var __dirname2 = dirname8(fileURLToPath2(import.meta.url));
|
|
35906
36913
|
function findSkillsDir() {
|
|
35907
36914
|
let dir = __dirname2;
|
|
35908
36915
|
for (let i3 = 0;i3 < 5; i3++) {
|
|
35909
|
-
const candidate =
|
|
35910
|
-
if (
|
|
36916
|
+
const candidate = join10(dir, "skills");
|
|
36917
|
+
if (existsSync7(candidate) && !dir.includes(".skills"))
|
|
35911
36918
|
return candidate;
|
|
35912
|
-
dir =
|
|
36919
|
+
dir = dirname8(dir);
|
|
35913
36920
|
}
|
|
35914
|
-
return
|
|
36921
|
+
return join10(__dirname2, "..", "skills");
|
|
35915
36922
|
}
|
|
35916
36923
|
var SKILLS_DIR = findSkillsDir();
|
|
35917
36924
|
function getSkillPath(name) {
|
|
@@ -35919,13 +36926,13 @@ function getSkillPath(name) {
|
|
|
35919
36926
|
const portable = findPortableSkill(skillName);
|
|
35920
36927
|
if (portable)
|
|
35921
36928
|
return portable.path;
|
|
35922
|
-
const legacyCustomPath =
|
|
35923
|
-
if (
|
|
36929
|
+
const legacyCustomPath = join10(getDataDir(), "custom", skillName);
|
|
36930
|
+
if (existsSync7(legacyCustomPath))
|
|
35924
36931
|
return legacyCustomPath;
|
|
35925
36932
|
const extensionPath = findExtensionSkillPath(skillName);
|
|
35926
36933
|
if (extensionPath)
|
|
35927
36934
|
return extensionPath;
|
|
35928
|
-
return
|
|
36935
|
+
return join10(SKILLS_DIR, skillName);
|
|
35929
36936
|
}
|
|
35930
36937
|
function getCanonicalSkillName(name) {
|
|
35931
36938
|
return getSkill(name)?.name ?? resolveSkillAlias(normalizeSkillSlug(name));
|
|
@@ -35934,18 +36941,18 @@ function getCanonicalSkillName(name) {
|
|
|
35934
36941
|
// src/lib/skillinfo.ts
|
|
35935
36942
|
function getSkillDocs(name) {
|
|
35936
36943
|
const skillPath = getSkillPath(name);
|
|
35937
|
-
if (!
|
|
36944
|
+
if (!existsSync8(skillPath))
|
|
35938
36945
|
return null;
|
|
35939
36946
|
return {
|
|
35940
|
-
skillMd: readIfExists(
|
|
35941
|
-
readme: readIfExists(
|
|
35942
|
-
claudeMd: readIfExists(
|
|
36947
|
+
skillMd: readIfExists(join11(skillPath, "SKILL.md")),
|
|
36948
|
+
readme: readIfExists(join11(skillPath, "README.md")),
|
|
36949
|
+
claudeMd: readIfExists(join11(skillPath, "CLAUDE.md"))
|
|
35943
36950
|
};
|
|
35944
36951
|
}
|
|
35945
36952
|
function readIfExists(path) {
|
|
35946
36953
|
try {
|
|
35947
|
-
if (
|
|
35948
|
-
return
|
|
36954
|
+
if (existsSync8(path)) {
|
|
36955
|
+
return readFileSync8(path, "utf-8");
|
|
35949
36956
|
}
|
|
35950
36957
|
} catch {}
|
|
35951
36958
|
return null;
|
|
@@ -35986,7 +36993,7 @@ function getServerSkillMd(slug) {
|
|
|
35986
36993
|
const path = resolve(skillsDir, name, "SKILL.md");
|
|
35987
36994
|
if (!isInsideDir(skillsDir, path))
|
|
35988
36995
|
return null;
|
|
35989
|
-
return
|
|
36996
|
+
return existsSync9(path) ? readFileSync9(path, "utf8") : null;
|
|
35990
36997
|
}
|
|
35991
36998
|
|
|
35992
36999
|
// src/server/skills-api.ts
|
|
@@ -35996,6 +37003,17 @@ var MAX_SLUG_LENGTH = 128;
|
|
|
35996
37003
|
var MAX_SKILL_MD_BYTES = 512000;
|
|
35997
37004
|
var MAX_MANIFEST_BYTES = MAX_SKILL_MD_BYTES + 64000;
|
|
35998
37005
|
var ALLOWED_PUBLISH_PARTS = new Set(["manifest", "bundle"]);
|
|
37006
|
+
function pinPayload(pin) {
|
|
37007
|
+
return { slug: pin.slug, pinnedAt: pin.pinnedAt, metadata: pin.metadata };
|
|
37008
|
+
}
|
|
37009
|
+
function pinMetadataField(body) {
|
|
37010
|
+
if (body.metadata === undefined)
|
|
37011
|
+
return {};
|
|
37012
|
+
if (!body.metadata || typeof body.metadata !== "object" || Array.isArray(body.metadata)) {
|
|
37013
|
+
throw new SkillRequestError(400, "INVALID_METADATA", "`metadata` must be a JSON object");
|
|
37014
|
+
}
|
|
37015
|
+
return body.metadata;
|
|
37016
|
+
}
|
|
35999
37017
|
|
|
36000
37018
|
class SkillRequestError extends Error {
|
|
36001
37019
|
status;
|
|
@@ -36024,33 +37042,130 @@ function publishedPayload(record) {
|
|
|
36024
37042
|
...publishedSkillMeta(record),
|
|
36025
37043
|
slug: record.slug,
|
|
36026
37044
|
publishedSource: record.source,
|
|
37045
|
+
...record.skillMd ? { skillMd: record.skillMd } : {},
|
|
36027
37046
|
...record.bundleSha256 ? { bundleSha256: record.bundleSha256, bundleByteSize: record.bundleByteSize } : {},
|
|
36028
37047
|
publishedAt: record.createdAt,
|
|
36029
|
-
updatedAt: record.updatedAt
|
|
37048
|
+
updatedAt: record.updatedAt,
|
|
37049
|
+
revisionId: record.revisionId,
|
|
37050
|
+
revisionNumber: record.revisionNumber
|
|
37051
|
+
};
|
|
37052
|
+
}
|
|
37053
|
+
function revisionEtag(revisionId) {
|
|
37054
|
+
return `"${revisionId}"`;
|
|
37055
|
+
}
|
|
37056
|
+
function parseIfMatch(value) {
|
|
37057
|
+
if (value === null || value.trim() === "")
|
|
37058
|
+
return;
|
|
37059
|
+
const trimmed = value.trim();
|
|
37060
|
+
if (trimmed === "*") {
|
|
37061
|
+
throw new SkillRequestError(400, "INVALID_IF_MATCH", "If-Match must name the exact revision id (the ETag of the current revision); '*' is not accepted");
|
|
37062
|
+
}
|
|
37063
|
+
const unquoted = trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
|
|
37064
|
+
if (!REVISION_ID_PATTERN.test(unquoted)) {
|
|
37065
|
+
throw new SkillRequestError(400, "INVALID_IF_MATCH", "If-Match must carry a revision id: a 64-character lowercase hex sha-256, quoted as the server's ETag");
|
|
37066
|
+
}
|
|
37067
|
+
return unquoted;
|
|
37068
|
+
}
|
|
37069
|
+
async function tombstoneStatus(store, artifactStorage, principal, record) {
|
|
37070
|
+
if (!record.tombstonedAt)
|
|
37071
|
+
return "live";
|
|
37072
|
+
if (record.tombstonePurgeAfter && record.tombstonePurgeAfter <= new Date().toISOString()) {
|
|
37073
|
+
const purged = await store.purgeExpiredTombstones(principal);
|
|
37074
|
+
for (const removed of purged) {
|
|
37075
|
+
if (removed.bundleSha256)
|
|
37076
|
+
await discardCollectedObject(store, artifactStorage, principal, removed.bundleSha256);
|
|
37077
|
+
}
|
|
37078
|
+
return "purged";
|
|
37079
|
+
}
|
|
37080
|
+
return {
|
|
37081
|
+
slug: record.slug,
|
|
37082
|
+
deleted: true,
|
|
37083
|
+
code: "TOMBSTONED",
|
|
37084
|
+
tombstonedAt: record.tombstonedAt,
|
|
37085
|
+
tombstonePurgeAfter: record.tombstonePurgeAfter,
|
|
37086
|
+
revisionId: record.revisionId
|
|
36030
37087
|
};
|
|
36031
37088
|
}
|
|
36032
37089
|
async function listMergedSkills(store, principal) {
|
|
36033
37090
|
const published = await store.listSkills(principal);
|
|
37091
|
+
return mergedSkillPayloads(published, listServerSkills());
|
|
37092
|
+
}
|
|
37093
|
+
function mergedSkillPayloads(published, bundled) {
|
|
36034
37094
|
const publishedBySlug = new Map(published.map((record) => [record.slug, record]));
|
|
36035
|
-
const merged = mergeSkillRegistryLists(
|
|
37095
|
+
const merged = mergeSkillRegistryLists(bundled, published.map(publishedSkillMeta));
|
|
36036
37096
|
return merged.map((skill) => {
|
|
36037
37097
|
const record = publishedBySlug.get(skill.name);
|
|
36038
37098
|
return record ? publishedPayload(record) : skill;
|
|
36039
37099
|
});
|
|
36040
37100
|
}
|
|
36041
|
-
async function
|
|
37101
|
+
async function resolvePublishedSkill(store, artifactStorage, principal, slug) {
|
|
36042
37102
|
const record = await store.getSkill(principal, slug);
|
|
36043
|
-
if (record)
|
|
36044
|
-
return
|
|
37103
|
+
if (!record)
|
|
37104
|
+
return { kind: "absent" };
|
|
37105
|
+
const status = await tombstoneStatus(store, artifactStorage, principal, record);
|
|
37106
|
+
if (status === "purged")
|
|
37107
|
+
return { kind: "absent" };
|
|
37108
|
+
if (status !== "live")
|
|
37109
|
+
return { kind: "tombstone", payload: status };
|
|
37110
|
+
return { kind: "published", record };
|
|
37111
|
+
}
|
|
37112
|
+
async function listOrgTags(store, principal) {
|
|
37113
|
+
const publishedSlugs = await store.listPublishedSlugs(principal);
|
|
37114
|
+
const tags = new Set;
|
|
37115
|
+
for (const tag of await store.listTags(principal)) {
|
|
37116
|
+
if (tag.trim())
|
|
37117
|
+
tags.add(tag);
|
|
37118
|
+
}
|
|
37119
|
+
for (const skill of listServerSkills()) {
|
|
37120
|
+
if (publishedSlugs.includes(skill.name))
|
|
37121
|
+
continue;
|
|
37122
|
+
for (const tag of skill.tags) {
|
|
37123
|
+
if (tag.trim())
|
|
37124
|
+
tags.add(tag);
|
|
37125
|
+
}
|
|
37126
|
+
}
|
|
37127
|
+
return [...tags].sort();
|
|
37128
|
+
}
|
|
37129
|
+
async function listMergedSkillsByTag(store, principal, tag) {
|
|
37130
|
+
const published = await store.listSkillsByTag(principal, tag);
|
|
37131
|
+
const publishedSlugs = await store.listPublishedSlugs(principal);
|
|
37132
|
+
const bundled = listServerSkills().filter((skill) => skill.tags.includes(tag) && !publishedSlugs.includes(skill.name));
|
|
37133
|
+
return mergedSkillPayloads(published, bundled);
|
|
37134
|
+
}
|
|
37135
|
+
function skillSummary(skill) {
|
|
37136
|
+
return {
|
|
37137
|
+
slug: String(skill.slug ?? skill.name),
|
|
37138
|
+
...typeof skill.name === "string" ? { name: skill.name } : {},
|
|
37139
|
+
...typeof skill.version === "string" ? { version: skill.version } : {},
|
|
37140
|
+
...typeof skill.updatedAt === "string" ? { updatedAt: skill.updatedAt } : {}
|
|
37141
|
+
};
|
|
37142
|
+
}
|
|
37143
|
+
async function listPinsByTag(store, principal, tag) {
|
|
37144
|
+
const publishedSlugs = await store.listPublishedSlugs(principal);
|
|
37145
|
+
const bundledTaggedSlugs = new Set;
|
|
37146
|
+
for (const skill of listServerSkills()) {
|
|
37147
|
+
if (skill.tags.includes(tag) && !publishedSlugs.includes(skill.name))
|
|
37148
|
+
bundledTaggedSlugs.add(skill.name);
|
|
37149
|
+
}
|
|
37150
|
+
const publishedPins = await store.listPinsByTag(principal, tag);
|
|
37151
|
+
const bundledPins = bundledTaggedSlugs.size ? (await store.listPins(principal)).filter((pin) => bundledTaggedSlugs.has(pin.slug)) : [];
|
|
37152
|
+
return [...publishedPins, ...bundledPins].sort((a3, b3) => a3.slug.localeCompare(b3.slug)).map(pinPayload);
|
|
37153
|
+
}
|
|
37154
|
+
async function getMergedSkill(store, artifactStorage, principal, slug) {
|
|
37155
|
+
const resolved = await resolvePublishedSkill(store, artifactStorage, principal, slug);
|
|
37156
|
+
if (resolved.kind === "tombstone")
|
|
37157
|
+
return resolved.payload;
|
|
37158
|
+
if (resolved.kind === "published")
|
|
37159
|
+
return publishedPayload(resolved.record);
|
|
36045
37160
|
const bundled = getServerSkill(slug);
|
|
36046
37161
|
return bundled ? bundled : null;
|
|
36047
37162
|
}
|
|
36048
|
-
async function getMergedSkillMd(store, principal, slug) {
|
|
36049
|
-
const
|
|
36050
|
-
if (
|
|
36051
|
-
return record.skillMd;
|
|
36052
|
-
if (record)
|
|
37163
|
+
async function getMergedSkillMd(store, artifactStorage, principal, slug) {
|
|
37164
|
+
const resolved = await resolvePublishedSkill(store, artifactStorage, principal, slug);
|
|
37165
|
+
if (resolved.kind === "tombstone")
|
|
36053
37166
|
return null;
|
|
37167
|
+
if (resolved.kind === "published")
|
|
37168
|
+
return resolved.record.skillMd ?? null;
|
|
36054
37169
|
return getServerSkillMd(slug);
|
|
36055
37170
|
}
|
|
36056
37171
|
async function parsePublishRequest(request, config) {
|
|
@@ -36092,7 +37207,7 @@ async function parsePublishRequest(request, config) {
|
|
|
36092
37207
|
if (bundleBytes.byteLength === 0) {
|
|
36093
37208
|
throw new SkillRequestError(400, "BUNDLE_EMPTY", "the uploaded bundle is empty");
|
|
36094
37209
|
}
|
|
36095
|
-
const sha256 =
|
|
37210
|
+
const sha256 = createHash6("sha256").update(bundleBytes).digest("hex");
|
|
36096
37211
|
const claimed = optionalString(manifest.bundleSha256);
|
|
36097
37212
|
if (claimed)
|
|
36098
37213
|
assertSha2562(claimed);
|
|
@@ -36120,28 +37235,33 @@ async function parsePublishRequest(request, config) {
|
|
|
36120
37235
|
}
|
|
36121
37236
|
return { input: buildPublishInput(parseManifestJson(text)) };
|
|
36122
37237
|
}
|
|
36123
|
-
async function storePublishedSkill(store, artifactStorage, principal, parsed) {
|
|
37238
|
+
async function storePublishedSkill(store, artifactStorage, principal, parsed, expectedRevisionId) {
|
|
36124
37239
|
const superseded = (await store.getSkill(principal, parsed.input.slug))?.bundleSha256;
|
|
36125
|
-
let input = {
|
|
37240
|
+
let input = {
|
|
37241
|
+
...parsed.input,
|
|
37242
|
+
principal,
|
|
37243
|
+
...expectedRevisionId ? { expectedRevisionId } : {}
|
|
37244
|
+
};
|
|
36126
37245
|
if (parsed.bundleBytes && input.bundle) {
|
|
36127
37246
|
const placement = await artifactStorage.putBundle(principal.orgId, input.bundle.sha256, parsed.bundleBytes, input.bundle.contentType);
|
|
36128
37247
|
input = { ...input, bundle: { ...input.bundle, ...placement } };
|
|
36129
37248
|
}
|
|
36130
|
-
|
|
37249
|
+
let record;
|
|
37250
|
+
try {
|
|
37251
|
+
record = await store.publishSkill(input);
|
|
37252
|
+
} catch (error) {
|
|
37253
|
+
if (input.bundle?.sha256) {
|
|
37254
|
+
await discardCollectedObject(store, artifactStorage, principal, input.bundle.sha256);
|
|
37255
|
+
}
|
|
37256
|
+
throw error;
|
|
37257
|
+
}
|
|
36131
37258
|
if (superseded && superseded !== record.bundleSha256) {
|
|
36132
37259
|
await discardCollectedObject(store, artifactStorage, principal, superseded);
|
|
36133
37260
|
}
|
|
36134
37261
|
return record;
|
|
36135
37262
|
}
|
|
36136
|
-
async function deletePublishedSkill(store, artifactStorage, principal, slug) {
|
|
36137
|
-
|
|
36138
|
-
if (!record)
|
|
36139
|
-
return false;
|
|
36140
|
-
const deleted = await store.deleteSkill(principal, slug);
|
|
36141
|
-
if (deleted && record.bundleSha256) {
|
|
36142
|
-
await discardCollectedObject(store, artifactStorage, principal, record.bundleSha256);
|
|
36143
|
-
}
|
|
36144
|
-
return deleted;
|
|
37263
|
+
async function deletePublishedSkill(store, artifactStorage, principal, slug, tombstoneWindowMs) {
|
|
37264
|
+
return store.deleteSkill(principal, slug, tombstoneWindowMs);
|
|
36145
37265
|
}
|
|
36146
37266
|
async function discardCollectedObject(store, artifactStorage, principal, sha256) {
|
|
36147
37267
|
if (await store.getSkillBundle(principal, sha256))
|
|
@@ -36161,7 +37281,7 @@ async function readPublishedBundle(store, artifactStorage, principal, slug) {
|
|
|
36161
37281
|
if (!bytes) {
|
|
36162
37282
|
throw new SkillRequestError(503, "BUNDLE_BACKEND_UNAVAILABLE", "bundle storage backend unavailable");
|
|
36163
37283
|
}
|
|
36164
|
-
const actual =
|
|
37284
|
+
const actual = createHash6("sha256").update(bytes).digest("hex");
|
|
36165
37285
|
if (actual !== record.bundleSha256) {
|
|
36166
37286
|
throw new SkillRequestError(500, "BUNDLE_DIGEST_DRIFT", `stored bundle for '${slug}' hashes to ${actual} but was published as ${record.bundleSha256}`);
|
|
36167
37287
|
}
|
|
@@ -36206,7 +37326,7 @@ function buildPublishInput(manifest) {
|
|
|
36206
37326
|
if (skillMd && byteLength(skillMd) > MAX_SKILL_MD_BYTES) {
|
|
36207
37327
|
throw new SkillRequestError(413, "SKILL_MD_TOO_LARGE", `skillMd exceeds ${MAX_SKILL_MD_BYTES} bytes`);
|
|
36208
37328
|
}
|
|
36209
|
-
const kindValue = optionalString(manifest.kind) ?? "
|
|
37329
|
+
const kindValue = optionalString(manifest.kind) ?? "instruction";
|
|
36210
37330
|
if (kindValue !== "executable" && kindValue !== "instruction") {
|
|
36211
37331
|
throw new SkillRequestError(400, "INVALID_KIND", "`kind` must be 'executable' or 'instruction'");
|
|
36212
37332
|
}
|
|
@@ -36268,6 +37388,7 @@ async function createSkillsFetchHandler(options = {}) {
|
|
|
36268
37388
|
bootstrapApiKey: config.bootstrapApiKey
|
|
36269
37389
|
});
|
|
36270
37390
|
assertDurableStore(store, config);
|
|
37391
|
+
const governanceStore = options.governanceStore ?? await createGovernanceStore(config.databaseUrl);
|
|
36271
37392
|
const artifactStorage = new ArtifactStorage({
|
|
36272
37393
|
bucket: config.artifactBucket,
|
|
36273
37394
|
prefix: config.artifactPrefix
|
|
@@ -36277,10 +37398,10 @@ async function createSkillsFetchHandler(options = {}) {
|
|
|
36277
37398
|
const segments = pathSegments(url.pathname);
|
|
36278
37399
|
try {
|
|
36279
37400
|
if (request.method === "GET" && url.pathname === "/health") {
|
|
36280
|
-
return json({ ok: true, service: "
|
|
37401
|
+
return json({ ok: true, service: "skills", time: new Date().toISOString() });
|
|
36281
37402
|
}
|
|
36282
37403
|
if (request.method === "GET" && url.pathname === "/ready") {
|
|
36283
|
-
return json({ ok: true, service: "
|
|
37404
|
+
return json({ ok: true, service: "skills" });
|
|
36284
37405
|
}
|
|
36285
37406
|
if (url.pathname.startsWith("/api/")) {
|
|
36286
37407
|
const principal = await authenticateRequest(store, request);
|
|
@@ -36290,7 +37411,7 @@ async function createSkillsFetchHandler(options = {}) {
|
|
|
36290
37411
|
return json(identityPayload(principal));
|
|
36291
37412
|
}
|
|
36292
37413
|
if (segments[0] === "api" && segments[1] === "v1") {
|
|
36293
|
-
return await handleApiV1(store, principal, request, segments.slice(2), config, artifactStorage);
|
|
37414
|
+
return await handleApiV1(store, governanceStore, principal, request, segments.slice(2), config, artifactStorage);
|
|
36294
37415
|
}
|
|
36295
37416
|
}
|
|
36296
37417
|
return json({ error: "not found", code: "NOT_FOUND" }, { status: 404 });
|
|
@@ -36298,6 +37419,14 @@ async function createSkillsFetchHandler(options = {}) {
|
|
|
36298
37419
|
if (error instanceof SkillRequestError) {
|
|
36299
37420
|
return json({ error: error.message, code: error.code }, { status: error.status });
|
|
36300
37421
|
}
|
|
37422
|
+
if (error instanceof SkillRevisionConflictError) {
|
|
37423
|
+
return json({
|
|
37424
|
+
error: error.message,
|
|
37425
|
+
code: "REVISION_CONFLICT",
|
|
37426
|
+
slug: error.slug,
|
|
37427
|
+
...error.currentRevisionId ? { currentRevisionId: error.currentRevisionId } : {}
|
|
37428
|
+
}, { status: 409 });
|
|
37429
|
+
}
|
|
36301
37430
|
return json({ error: "internal server error", detail: error.message }, { status: 500 });
|
|
36302
37431
|
}
|
|
36303
37432
|
};
|
|
@@ -36311,30 +37440,49 @@ function skillsServeLimits(config) {
|
|
|
36311
37440
|
return { maxRequestBodySize: Math.max(config.skillBundleLimitBytes, config.requestBodyLimitBytes) + BODY_LIMIT_HEADROOM_BYTES };
|
|
36312
37441
|
}
|
|
36313
37442
|
var BODY_LIMIT_HEADROOM_BYTES = 1e6;
|
|
36314
|
-
async function handleApiV1(store, principal, request, parts, config, artifactStorage) {
|
|
37443
|
+
async function handleApiV1(store, governanceStore, principal, request, parts, config, artifactStorage) {
|
|
36315
37444
|
const [resource, id, subresource, childId] = parts;
|
|
36316
37445
|
if (parts.some(segmentEscapesPath)) {
|
|
36317
37446
|
return json({ error: "invalid path segment", code: "INVALID_PATH" }, { status: 400 });
|
|
36318
37447
|
}
|
|
36319
37448
|
if (resource === "skills") {
|
|
36320
|
-
if (request.method === "GET" && !id)
|
|
37449
|
+
if (request.method === "GET" && !id) {
|
|
37450
|
+
const tag = new URL(request.url).searchParams.get("tag");
|
|
37451
|
+
if (tag !== null && tag !== "")
|
|
37452
|
+
return json(await listMergedSkillsByTag(store, principal, tag));
|
|
36321
37453
|
return json(await listMergedSkills(store, principal));
|
|
37454
|
+
}
|
|
36322
37455
|
if (request.method === "POST" && !id) {
|
|
37456
|
+
const expectedRevisionId = parseIfMatch(request.headers.get("if-match"));
|
|
36323
37457
|
const parsed = await parsePublishRequest(request, config);
|
|
36324
|
-
const record = await storePublishedSkill(store, artifactStorage, principal, parsed);
|
|
36325
|
-
return json(publishedPayload(record), { status: 201 });
|
|
37458
|
+
const record = await storePublishedSkill(store, artifactStorage, principal, parsed, expectedRevisionId);
|
|
37459
|
+
return json(publishedPayload(record), { status: 201, headers: { ETag: revisionEtag(record.revisionId) } });
|
|
36326
37460
|
}
|
|
36327
37461
|
if (request.method === "GET" && id && subresource === "skill.md") {
|
|
36328
|
-
const
|
|
37462
|
+
const resolved = await resolvePublishedSkill(store, artifactStorage, principal, id);
|
|
37463
|
+
if (resolved.kind === "tombstone") {
|
|
37464
|
+
return json({ error: "skill was deleted", ...resolved.payload }, { status: 410 });
|
|
37465
|
+
}
|
|
37466
|
+
const docs = await getMergedSkillMd(store, artifactStorage, principal, id);
|
|
36329
37467
|
return docs ? new Response(docs, { headers: { "Content-Type": "text/markdown; charset=utf-8", "Cache-Control": "no-store" } }) : json({ error: "skill not found", code: "SKILL_NOT_FOUND" }, { status: 404 });
|
|
36330
37468
|
}
|
|
36331
37469
|
if (request.method === "GET" && id && subresource === "bundle") {
|
|
37470
|
+
const resolved = await resolvePublishedSkill(store, artifactStorage, principal, id);
|
|
37471
|
+
if (resolved.kind === "tombstone") {
|
|
37472
|
+
return json({ error: "skill was deleted", ...resolved.payload }, { status: 410 });
|
|
37473
|
+
}
|
|
37474
|
+
if (resolved.kind === "absent") {
|
|
37475
|
+
return json({ error: "skill not found", code: "SKILL_NOT_FOUND" }, { status: 404 });
|
|
37476
|
+
}
|
|
36332
37477
|
const { record, bytes } = await readPublishedBundle(store, artifactStorage, principal, id);
|
|
36333
37478
|
const headers = {
|
|
36334
37479
|
"Content-Type": "application/gzip",
|
|
36335
37480
|
"Content-Length": String(bytes.byteLength),
|
|
36336
37481
|
"Content-Disposition": `attachment; filename="${record.slug}.tar.gz"`,
|
|
36337
37482
|
"X-Skill-Bundle-Sha256": record.bundleSha256 ?? "",
|
|
37483
|
+
"X-Skill-Revision-Id": record.revisionId,
|
|
37484
|
+
"X-Skill-Revision-Number": String(record.revisionNumber),
|
|
37485
|
+
ETag: revisionEtag(record.revisionId),
|
|
36338
37486
|
"Cache-Control": "no-store"
|
|
36339
37487
|
};
|
|
36340
37488
|
if (config.bundleSigningKey) {
|
|
@@ -36343,17 +37491,56 @@ async function handleApiV1(store, principal, request, parts, config, artifactSto
|
|
|
36343
37491
|
return new Response(bytes, { headers });
|
|
36344
37492
|
}
|
|
36345
37493
|
if (request.method === "GET" && id && !subresource) {
|
|
36346
|
-
const
|
|
37494
|
+
const resolved = await resolvePublishedSkill(store, artifactStorage, principal, id);
|
|
37495
|
+
if (resolved.kind === "tombstone") {
|
|
37496
|
+
return json({ error: "skill was deleted", ...resolved.payload }, { status: 410 });
|
|
37497
|
+
}
|
|
37498
|
+
if (resolved.kind === "published") {
|
|
37499
|
+
return json(publishedPayload(resolved.record), { headers: { ETag: revisionEtag(resolved.record.revisionId) } });
|
|
37500
|
+
}
|
|
37501
|
+
const skill = await getMergedSkill(store, artifactStorage, principal, id);
|
|
36347
37502
|
return skill ? json(skill) : json({ error: "skill not found", code: "SKILL_NOT_FOUND" }, { status: 404 });
|
|
36348
37503
|
}
|
|
36349
37504
|
if ((request.method === "PUT" || request.method === "PATCH") && id && !subresource) {
|
|
37505
|
+
const expectedRevisionId = parseIfMatch(request.headers.get("if-match"));
|
|
36350
37506
|
const body = await readJson(request, config.requestBodyLimitBytes);
|
|
36351
|
-
const updated = await store.updateSkill(principal, id, skillPatch(body));
|
|
36352
|
-
return updated ? json(publishedPayload(updated)) : json({ error: "published skill not found", code: "SKILL_NOT_FOUND" }, { status: 404 });
|
|
37507
|
+
const updated = await store.updateSkill(principal, id, skillPatch(body), expectedRevisionId);
|
|
37508
|
+
return updated ? json(publishedPayload(updated), { headers: { ETag: revisionEtag(updated.revisionId) } }) : json({ error: "published skill not found", code: "SKILL_NOT_FOUND" }, { status: 404 });
|
|
36353
37509
|
}
|
|
36354
37510
|
if (request.method === "DELETE" && id && !subresource) {
|
|
36355
|
-
const removed = await deletePublishedSkill(store, artifactStorage, principal, id);
|
|
36356
|
-
return removed ? json({
|
|
37511
|
+
const removed = await deletePublishedSkill(store, artifactStorage, principal, id, config.tombstoneWindowMs);
|
|
37512
|
+
return removed ? json({
|
|
37513
|
+
deleted: true,
|
|
37514
|
+
slug: id,
|
|
37515
|
+
...removed.tombstonedAt ? { tombstonedAt: removed.tombstonedAt, tombstonePurgeAfter: removed.tombstonePurgeAfter } : {}
|
|
37516
|
+
}) : json({ error: "published skill not found", code: "SKILL_NOT_FOUND" }, { status: 404 });
|
|
37517
|
+
}
|
|
37518
|
+
}
|
|
37519
|
+
if (resource === "pins") {
|
|
37520
|
+
if (request.method === "GET" && !id) {
|
|
37521
|
+
const tag = new URL(request.url).searchParams.get("tag");
|
|
37522
|
+
if (tag !== null && tag !== "")
|
|
37523
|
+
return json(await listPinsByTag(store, principal, tag));
|
|
37524
|
+
return json((await store.listPins(principal)).map(pinPayload));
|
|
37525
|
+
}
|
|
37526
|
+
if (request.method === "PUT" && id && !subresource) {
|
|
37527
|
+
assertPublishableSlug(id);
|
|
37528
|
+
const body = await readJson(request, config.requestBodyLimitBytes);
|
|
37529
|
+
const pin = await store.pinSkill(principal, id, pinMetadataField(body));
|
|
37530
|
+
return json(pinPayload(pin));
|
|
37531
|
+
}
|
|
37532
|
+
if (request.method === "DELETE" && id && !subresource) {
|
|
37533
|
+
assertPublishableSlug(id);
|
|
37534
|
+
const removed = await store.unpinSkill(principal, id);
|
|
37535
|
+
return removed ? json({ deleted: true, slug: id }) : json({ error: "pin not found", code: "PIN_NOT_FOUND" }, { status: 404 });
|
|
37536
|
+
}
|
|
37537
|
+
}
|
|
37538
|
+
if (resource === "tags") {
|
|
37539
|
+
if (request.method === "GET" && !id) {
|
|
37540
|
+
return json(await listOrgTags(store, principal));
|
|
37541
|
+
}
|
|
37542
|
+
if (request.method === "GET" && id && subresource === "skills") {
|
|
37543
|
+
return json((await listMergedSkillsByTag(store, principal, id)).map(skillSummary));
|
|
36357
37544
|
}
|
|
36358
37545
|
}
|
|
36359
37546
|
if (resource === "runs") {
|
|
@@ -36412,8 +37599,18 @@ async function handleApiV1(store, principal, request, parts, config, artifactSto
|
|
|
36412
37599
|
const run = await store.getRun(principal, id);
|
|
36413
37600
|
if (!run)
|
|
36414
37601
|
return json({ error: "run not found", code: "RUN_NOT_FOUND" }, { status: 404 });
|
|
36415
|
-
|
|
36416
|
-
|
|
37602
|
+
try {
|
|
37603
|
+
const outcome = await createCancelService({ store, governanceStore, storage: artifactStorage }).cancel(principal, id, principal.email);
|
|
37604
|
+
return json(runPayload(outcome.run));
|
|
37605
|
+
} catch (error) {
|
|
37606
|
+
if (error instanceof GovernanceError && error.code === GOVERNANCE_ERROR_CODES.STALE_LEASE_GENERATION) {
|
|
37607
|
+
return json({ error: error.message, code: error.code }, { status: 409 });
|
|
37608
|
+
}
|
|
37609
|
+
if (error instanceof StaleLeaseGenerationError) {
|
|
37610
|
+
return json({ error: error.message, code: GOVERNANCE_ERROR_CODES.STALE_LEASE_GENERATION }, { status: 409 });
|
|
37611
|
+
}
|
|
37612
|
+
throw error;
|
|
37613
|
+
}
|
|
36417
37614
|
}
|
|
36418
37615
|
}
|
|
36419
37616
|
return json({ error: "not found", code: "NOT_FOUND" }, { status: 404 });
|
|
@@ -36503,7 +37700,25 @@ function clampInt(value, fallback, max) {
|
|
|
36503
37700
|
}
|
|
36504
37701
|
|
|
36505
37702
|
// src/server/index.ts
|
|
37703
|
+
var EARLY_ARGV = process.argv.slice(2);
|
|
37704
|
+
if (EARLY_ARGV.includes("--version") || EARLY_ARGV.includes("-V")) {
|
|
37705
|
+
console.log(package_default.version);
|
|
37706
|
+
process.exit(0);
|
|
37707
|
+
}
|
|
37708
|
+
if (EARLY_ARGV.includes("--help") || EARLY_ARGV.includes("-h")) {
|
|
37709
|
+
console.log(`Usage: skills-server [options]
|
|
37710
|
+
|
|
37711
|
+
Runs the @hasna/skills HTTP API.
|
|
37712
|
+
|
|
37713
|
+
Options:
|
|
37714
|
+
-V, --version output the version number
|
|
37715
|
+
-h, --help display help for command
|
|
37716
|
+
|
|
37717
|
+
Environment:
|
|
37718
|
+
SKILLS_PORT Listen port (default: 8787)`);
|
|
37719
|
+
process.exit(0);
|
|
37720
|
+
}
|
|
36506
37721
|
var config = resolveServerConfig();
|
|
36507
37722
|
var server = await startSkillsServer({ config });
|
|
36508
|
-
console.log(`
|
|
37723
|
+
console.log(`skills API listening on http://${config.host}:${server.port}`);
|
|
36509
37724
|
console.log(`storage: ${resolveDatabaseTarget(config.databaseUrl).label}`);
|