@hasna/skills 0.1.64 → 0.1.67
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 +33 -16
- package/bin/index.js +710 -511
- package/bin/mcp.js +285 -239
- package/bin/server.js +791 -232
- package/bin/worker.js +178 -148
- package/dist/index.d.ts +1 -1
- package/dist/index.js +421 -321
- package/dist/lib/agent-sync.d.ts +2 -2
- package/dist/lib/native-storage.d.ts +28 -0
- package/dist/lib/portable-skills.d.ts +14 -0
- package/dist/lib/registry-types.d.ts +9 -0
- package/dist/lib/run-routing.d.ts +60 -0
- package/dist/mcp/index.d.ts +10 -0
- package/dist/sdk/index.js +14095 -14019
- package/dist/server/app.d.ts +3 -0
- package/dist/server/store-fixtures.d.ts +6 -0
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +50 -0
- package/package.json +3 -1
package/bin/server.js
CHANGED
|
@@ -22936,6 +22936,121 @@ __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.67",
|
|
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
|
+
"build:js": "rm -rf dist && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
22984
|
+
test: "bun test --timeout 30000",
|
|
22985
|
+
dev: "bun run ./src/cli/index.tsx",
|
|
22986
|
+
"dev:watch": "bun --watch run ./src/cli/index.tsx",
|
|
22987
|
+
"dev:mcp": "bun --watch run ./src/mcp/index.ts",
|
|
22988
|
+
"dev:server": "bun --watch run ./src/server/index.ts",
|
|
22989
|
+
"dev:worker": "bun --watch run ./src/server/worker.ts",
|
|
22990
|
+
migrate: "bun run ./src/server/migrate.ts",
|
|
22991
|
+
typecheck: "tsc --noEmit",
|
|
22992
|
+
"verify:release": "bun run scripts/release-guard.ts",
|
|
22993
|
+
prepare: "bun run build:js",
|
|
22994
|
+
prepack: "bun run build && bun run verify:release",
|
|
22995
|
+
prepublishOnly: "bun run typecheck && bun run test",
|
|
22996
|
+
postinstall: "mkdir -p $HOME/.hasna/skills/custom 2>/dev/null || true"
|
|
22997
|
+
},
|
|
22998
|
+
keywords: [
|
|
22999
|
+
"skills",
|
|
23000
|
+
"ai",
|
|
23001
|
+
"agent",
|
|
23002
|
+
"cli",
|
|
23003
|
+
"typescript",
|
|
23004
|
+
"bun",
|
|
23005
|
+
"claude",
|
|
23006
|
+
"codex",
|
|
23007
|
+
"gemini",
|
|
23008
|
+
"mcp",
|
|
23009
|
+
"model-context-protocol",
|
|
23010
|
+
"open-source",
|
|
23011
|
+
"skills-library",
|
|
23012
|
+
"automation"
|
|
23013
|
+
],
|
|
23014
|
+
author: "Hasna",
|
|
23015
|
+
license: "Apache-2.0",
|
|
23016
|
+
devDependencies: {
|
|
23017
|
+
"@types/bun": "latest",
|
|
23018
|
+
"@types/node": "25.2.3",
|
|
23019
|
+
"@types/react": "^18.2.0",
|
|
23020
|
+
"bun-types": "1.3.14",
|
|
23021
|
+
"react-devtools-core": "^7.0.1",
|
|
23022
|
+
typescript: "^5"
|
|
23023
|
+
},
|
|
23024
|
+
dependencies: {
|
|
23025
|
+
"@aws-sdk/client-ecs": "^3.1079.0",
|
|
23026
|
+
"@aws-sdk/client-s3": "^3.1079.0",
|
|
23027
|
+
"@hasna/events": "0.1.16",
|
|
23028
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
23029
|
+
chalk: "^5.3.0",
|
|
23030
|
+
commander: "^12.1.0",
|
|
23031
|
+
ink: "^5.0.1",
|
|
23032
|
+
"ink-select-input": "^6.0.0",
|
|
23033
|
+
"ink-spinner": "^5.0.0",
|
|
23034
|
+
"ink-text-input": "^6.0.0",
|
|
23035
|
+
react: "^18.2.0",
|
|
23036
|
+
zod: "^4.3.6"
|
|
23037
|
+
},
|
|
23038
|
+
engines: {
|
|
23039
|
+
bun: ">=1.0.0"
|
|
23040
|
+
},
|
|
23041
|
+
publishConfig: {
|
|
23042
|
+
registry: "https://registry.npmjs.org",
|
|
23043
|
+
access: "public"
|
|
23044
|
+
},
|
|
23045
|
+
repository: {
|
|
23046
|
+
type: "git",
|
|
23047
|
+
url: "git+https://github.com/hasna/apps.git"
|
|
23048
|
+
},
|
|
23049
|
+
homepage: "https://github.com/hasna/skills",
|
|
23050
|
+
bugs: {
|
|
23051
|
+
url: "https://github.com/hasna/skills/issues"
|
|
23052
|
+
}
|
|
23053
|
+
};
|
|
22939
23054
|
|
|
22940
23055
|
// src/lib/remote-run-contract.ts
|
|
22941
23056
|
var REMOTE_SKILL_RUN_CONTRACT_VERSION = 1;
|
|
@@ -31995,7 +32110,7 @@ var WriteGetObjectResponse$ = [
|
|
|
31995
32110
|
class CreateSessionCommand extends command(_ep4, _mw0, "CreateSession", CreateSession$) {
|
|
31996
32111
|
}
|
|
31997
32112
|
// ../../node_modules/.bun/@aws-sdk+client-s3@3.1106.0/node_modules/@aws-sdk/client-s3/package.json
|
|
31998
|
-
var
|
|
32113
|
+
var package_default2 = {
|
|
31999
32114
|
name: "@aws-sdk/client-s3",
|
|
32000
32115
|
version: "3.1106.0",
|
|
32001
32116
|
description: "AWS SDK for JavaScript S3 Client for Node.js, Browser and React Native",
|
|
@@ -32545,7 +32660,7 @@ var getRuntimeConfig3 = (config) => {
|
|
|
32545
32660
|
authSchemePreference: config?.authSchemePreference ?? import_config28.loadConfig(import_httpAuthSchemes3.NODE_AUTH_SCHEME_PREFERENCE_OPTIONS, loaderConfig),
|
|
32546
32661
|
bodyLengthChecker: config?.bodyLengthChecker ?? import_serde4.calculateBodyLength,
|
|
32547
32662
|
credentialDefaultProvider: config?.credentialDefaultProvider ?? defaultProvider,
|
|
32548
|
-
defaultUserAgentProvider: config?.defaultUserAgentProvider ?? import_client19.createDefaultUserAgentProvider({ serviceId: clientSharedValues.serviceId, clientVersion:
|
|
32663
|
+
defaultUserAgentProvider: config?.defaultUserAgentProvider ?? import_client19.createDefaultUserAgentProvider({ serviceId: clientSharedValues.serviceId, clientVersion: package_default2.version }),
|
|
32549
32664
|
disableS3ExpressSessionAuth: config?.disableS3ExpressSessionAuth ?? import_config28.loadConfig($NODE_DISABLE_S3_EXPRESS_SESSION_AUTH_OPTIONS, loaderConfig),
|
|
32550
32665
|
eventStreamSerdeProvider: config?.eventStreamSerdeProvider ?? import_event_streams.eventStreamSerdeProvider,
|
|
32551
32666
|
maxAttempts: config?.maxAttempts ?? import_config28.loadConfig(import_retry3.NODE_MAX_ATTEMPT_CONFIG_OPTIONS, config),
|
|
@@ -32857,36 +32972,144 @@ function concat(chunks) {
|
|
|
32857
32972
|
return merged;
|
|
32858
32973
|
}
|
|
32859
32974
|
|
|
32860
|
-
// src/
|
|
32861
|
-
|
|
32862
|
-
|
|
32863
|
-
|
|
32864
|
-
}
|
|
32865
|
-
|
|
32866
|
-
|
|
32867
|
-
|
|
32868
|
-
|
|
32869
|
-
|
|
32975
|
+
// src/sdk/governance.ts
|
|
32976
|
+
var DEFAULT_OUTPUT_GOVERNANCE = {
|
|
32977
|
+
defaultVisibility: "private",
|
|
32978
|
+
redactPatterns: [
|
|
32979
|
+
/\bsk-[A-Za-z0-9_-]{8,}\b/g,
|
|
32980
|
+
/\bsk_[A-Za-z0-9_-]{8,}\b/g,
|
|
32981
|
+
/\bgh[opsur]_[A-Za-z0-9_]{8,}\b/g,
|
|
32982
|
+
/\bgithub_pat_[A-Za-z0-9_]{8,}\b/g,
|
|
32983
|
+
/\bnpm_[A-Za-z0-9_]{8,}\b/g,
|
|
32984
|
+
/\bAKIA[A-Z0-9]{12,}\b/g,
|
|
32985
|
+
/\bAIza[A-Za-z0-9_-]{10,}\b/g,
|
|
32986
|
+
/\b[A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|DATABASE_URL)[A-Z0-9_]*\s*[:=]\s*[^ \n\r\t]+/gi,
|
|
32987
|
+
/\bhttps?:\/\/[^ \n\r\t]+X-Amz-Signature=[^ \n\r\t]+/gi
|
|
32988
|
+
],
|
|
32989
|
+
perOutputBytes: 10 * 1024 * 1024,
|
|
32990
|
+
perRunTotalBytes: 100 * 1024 * 1024,
|
|
32991
|
+
artifactTtlSeconds: 30 * 24 * 60 * 60
|
|
32992
|
+
};
|
|
32993
|
+
var DEFAULT_RUN_QUOTA = {
|
|
32994
|
+
cpu: 1,
|
|
32995
|
+
memoryMB: 2048,
|
|
32996
|
+
durationSeconds: 3600,
|
|
32997
|
+
networkMB: 100,
|
|
32998
|
+
artifactBytes: 100 * 1024 * 1024
|
|
32999
|
+
};
|
|
33000
|
+
var GOVERNANCE_ERROR_CODES = {
|
|
33001
|
+
ARTIFACT_LIMIT_EXCEEDED: "ARTIFACT_LIMIT_EXCEEDED",
|
|
33002
|
+
RUN_ARTIFACT_TOTAL_EXCEEDED: "RUN_ARTIFACT_TOTAL_EXCEEDED",
|
|
33003
|
+
RUN_BUDGET_EXHAUSTED: "RUN_BUDGET_EXHAUSTED",
|
|
33004
|
+
STALE_LEASE_GENERATION: "STALE_LEASE_GENERATION",
|
|
33005
|
+
FENCING_UNSUPPORTED: "FENCING_UNSUPPORTED",
|
|
33006
|
+
EVENT_PAYLOAD_REJECTED: "EVENT_PAYLOAD_REJECTED",
|
|
33007
|
+
SKILL_UNAVAILABLE_OFFLINE: "SKILL_UNAVAILABLE_OFFLINE",
|
|
33008
|
+
REMOTE_REQUIRED: "REMOTE_REQUIRED"
|
|
33009
|
+
};
|
|
33010
|
+
|
|
33011
|
+
class GovernanceError extends Error {
|
|
33012
|
+
code;
|
|
33013
|
+
gate;
|
|
33014
|
+
ceiling;
|
|
33015
|
+
constructor(code, message, options = {}) {
|
|
33016
|
+
super(message);
|
|
33017
|
+
this.name = "GovernanceError";
|
|
33018
|
+
this.code = code;
|
|
33019
|
+
this.gate = options.gate ?? code;
|
|
33020
|
+
this.ceiling = options.ceiling;
|
|
33021
|
+
}
|
|
32870
33022
|
}
|
|
32871
|
-
|
|
32872
|
-
|
|
32873
|
-
|
|
32874
|
-
|
|
32875
|
-
|
|
33023
|
+
function runPointersOf(run) {
|
|
33024
|
+
return {
|
|
33025
|
+
runId: run.id,
|
|
33026
|
+
attemptId: run.id,
|
|
33027
|
+
leaseGeneration: run.leaseGeneration,
|
|
33028
|
+
correlationId: run.correlationId
|
|
33029
|
+
};
|
|
32876
33030
|
}
|
|
32877
|
-
|
|
33031
|
+
|
|
33032
|
+
// src/sdk/cancel.ts
|
|
33033
|
+
function createCancelService(options) {
|
|
33034
|
+
const store = options.store;
|
|
33035
|
+
const governanceStore = options.governanceStore;
|
|
33036
|
+
const storage = options.storage ?? new ArtifactStorage;
|
|
32878
33037
|
return {
|
|
32879
|
-
|
|
32880
|
-
|
|
32881
|
-
|
|
32882
|
-
|
|
32883
|
-
|
|
32884
|
-
|
|
32885
|
-
|
|
32886
|
-
|
|
33038
|
+
async cancel(principal, runId, requestedBy) {
|
|
33039
|
+
if (!store.transitionRun) {
|
|
33040
|
+
throw new GovernanceError(GOVERNANCE_ERROR_CODES.FENCING_UNSUPPORTED, `cannot cancel run ${runId}: the store has no transitionRun generation fencing`, { gate: "transitionRun" });
|
|
33041
|
+
}
|
|
33042
|
+
const run = await store.getRun(principal, runId);
|
|
33043
|
+
if (!run)
|
|
33044
|
+
throw new GovernanceError(GOVERNANCE_ERROR_CODES.STALE_LEASE_GENERATION, `run ${runId} not found`, { gate: "run" });
|
|
33045
|
+
const terminalStatuses = new Set(["succeeded", "failed", "cancelled", "expired", "refunded"]);
|
|
33046
|
+
if (terminalStatuses.has(run.status)) {
|
|
33047
|
+
const receipt2 = await governanceStore.appendReceipt({
|
|
33048
|
+
kind: "cancel",
|
|
33049
|
+
orgId: run.orgId,
|
|
33050
|
+
runId: run.id,
|
|
33051
|
+
requestedBy,
|
|
33052
|
+
metadata: { outcome: "already-terminal", status: run.status, ...runPointersOf(run) }
|
|
33053
|
+
});
|
|
33054
|
+
return { run, fencedGeneration: run.leaseGeneration, quarantined: [], receipt: receipt2, alreadyTerminal: true };
|
|
33055
|
+
}
|
|
33056
|
+
const fencedGeneration = run.leaseGeneration + 1;
|
|
33057
|
+
const cancelling = await store.transitionRun(runId, { status: "cancel_requested", leaseGeneration: fencedGeneration }, run.leaseGeneration);
|
|
33058
|
+
if (!cancelling) {
|
|
33059
|
+
throw new GovernanceError(GOVERNANCE_ERROR_CODES.STALE_LEASE_GENERATION, `run ${runId} changed while cancelling`, { gate: "transition" });
|
|
33060
|
+
}
|
|
33061
|
+
const artifacts = await store.listArtifacts(principal, runId);
|
|
33062
|
+
const quarantined = [];
|
|
33063
|
+
for (const artifact of artifacts) {
|
|
33064
|
+
const quarantineKey = await storage.moveToQuarantine?.(artifact);
|
|
33065
|
+
if (quarantineKey) {
|
|
33066
|
+
await governanceStore.updateArtifactStorageKey(artifact.id, artifact.orgId, quarantineKey);
|
|
33067
|
+
}
|
|
33068
|
+
await governanceStore.appendReceipt({
|
|
33069
|
+
kind: "quarantine",
|
|
33070
|
+
orgId: artifact.orgId,
|
|
33071
|
+
runId: run.id,
|
|
33072
|
+
artifactId: artifact.id,
|
|
33073
|
+
requestedBy,
|
|
33074
|
+
metadata: { relativePath: artifact.relativePath, byteSize: artifact.byteSize, quarantineKey: quarantineKey ?? null }
|
|
33075
|
+
});
|
|
33076
|
+
quarantined.push({ ...artifact, ...quarantineKey ? { storageKey: quarantineKey } : {} });
|
|
33077
|
+
}
|
|
33078
|
+
const cancelled = await store.transitionRun(runId, { status: "cancelled", completedAt: new Date().toISOString() }, fencedGeneration);
|
|
33079
|
+
if (!cancelled) {
|
|
33080
|
+
throw new GovernanceError(GOVERNANCE_ERROR_CODES.STALE_LEASE_GENERATION, `run ${runId} moved on while finalising cancellation`, { gate: "transition" });
|
|
33081
|
+
}
|
|
33082
|
+
const receipt = await governanceStore.appendReceipt({
|
|
33083
|
+
kind: "cancel",
|
|
33084
|
+
orgId: run.orgId,
|
|
33085
|
+
runId: run.id,
|
|
33086
|
+
requestedBy,
|
|
33087
|
+
metadata: {
|
|
33088
|
+
fencedGeneration,
|
|
33089
|
+
artifactCount: quarantined.length,
|
|
33090
|
+
pointers: runPointersOf(cancelled)
|
|
33091
|
+
}
|
|
33092
|
+
});
|
|
33093
|
+
return { run: cancelled, fencedGeneration, quarantined, receipt, alreadyTerminal: false };
|
|
33094
|
+
}
|
|
32887
33095
|
};
|
|
32888
33096
|
}
|
|
32889
33097
|
|
|
33098
|
+
// src/sdk/governance-store.ts
|
|
33099
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
33100
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
33101
|
+
import { mkdirSync as mkdirSync3 } from "fs";
|
|
33102
|
+
import { dirname as dirname5 } from "path";
|
|
33103
|
+
|
|
33104
|
+
// src/server/database-url.ts
|
|
33105
|
+
import { isAbsolute, join as join3 } from "path";
|
|
33106
|
+
import { fileURLToPath } from "url";
|
|
33107
|
+
|
|
33108
|
+
// src/lib/config.ts
|
|
33109
|
+
import { existsSync, readFileSync as readFileSync2, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
33110
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
33111
|
+
import { homedir as homedir2 } from "os";
|
|
33112
|
+
|
|
32890
33113
|
// src/lib/retired-settings.ts
|
|
32891
33114
|
var RETIRED_ENV_SUFFIXES = ["_STORAGE_MODE", "_DEPLOYMENT_MODE", "_CLOUD_MODE"];
|
|
32892
33115
|
var RETIRED_CONFIG_KEYS = {
|
|
@@ -32933,54 +33156,7 @@ function assertNoRetiredConfigKeys(config, source) {
|
|
|
32933
33156
|
}
|
|
32934
33157
|
}
|
|
32935
33158
|
|
|
32936
|
-
// src/server/config.ts
|
|
32937
|
-
var DATABASE_URL_ENV = "HASNA_SKILLS_DATABASE_URL";
|
|
32938
|
-
var SKILLS_ENV_NAMESPACE = "SKILLS";
|
|
32939
|
-
function resolveServerConfig(env = process.env) {
|
|
32940
|
-
assertNoRetiredModeEnvVars(env, {
|
|
32941
|
-
app: SKILLS_ENV_NAMESPACE,
|
|
32942
|
-
replacement: DATABASE_URL_ENV
|
|
32943
|
-
});
|
|
32944
|
-
const nodeEnv = env.NODE_ENV || "development";
|
|
32945
|
-
const host = env.HOST || env.SKILLS_HOST || "0.0.0.0";
|
|
32946
|
-
const port = parsePositiveInt(env.PORT || env.SKILLS_PORT, 8787);
|
|
32947
|
-
return {
|
|
32948
|
-
host,
|
|
32949
|
-
port,
|
|
32950
|
-
databaseUrl: env[DATABASE_URL_ENV] || env.DATABASE_URL || undefined,
|
|
32951
|
-
bootstrapApiKey: env.HASNA_SKILLS_BOOTSTRAP_API_KEY || undefined,
|
|
32952
|
-
artifactBucket: env.HASNA_SKILLS_S3_BUCKET || env.SKILLS_S3_BUCKET || undefined,
|
|
32953
|
-
artifactPrefix: normalizePrefix(env.HASNA_SKILLS_S3_PREFIX || env.SKILLS_S3_PREFIX || "skills/artifacts"),
|
|
32954
|
-
inlineWorker: env.HASNA_SKILLS_INLINE_WORKER === "1",
|
|
32955
|
-
bundleSigningKey: env.HASNA_SKILLS_API_SIGNING_KEY || env.HASNA_SKILLS_SIGNING_KEY || undefined,
|
|
32956
|
-
requestBodyLimitBytes: parsePositiveInt(env.HASNA_SKILLS_REQUEST_BODY_LIMIT_BYTES, 1e6),
|
|
32957
|
-
skillBundleLimitBytes: parsePositiveInt(env.HASNA_SKILLS_BUNDLE_LIMIT_BYTES, 25000000),
|
|
32958
|
-
tombstoneWindowMs: parsePositiveInt(env.HASNA_SKILLS_TOMBSTONE_WINDOW_MS, 7 * 24 * 60 * 60 * 1000),
|
|
32959
|
-
publicBaseUrl: (env.SKILLS_PUBLIC_BASE_URL || localOrigin(host, port)).replace(/\/+$/, ""),
|
|
32960
|
-
nodeEnv,
|
|
32961
|
-
allowEphemeralStore: env.HASNA_SKILLS_ALLOW_EPHEMERAL_STORE === "1"
|
|
32962
|
-
};
|
|
32963
|
-
}
|
|
32964
|
-
function localOrigin(host, port) {
|
|
32965
|
-
const hostname = host === "0.0.0.0" || host === "::" ? "localhost" : host;
|
|
32966
|
-
return `http://${hostname.includes(":") ? `[${hostname}]` : hostname}:${port}`;
|
|
32967
|
-
}
|
|
32968
|
-
function parsePositiveInt(value, fallback) {
|
|
32969
|
-
const parsed = Number.parseInt(value ?? "", 10);
|
|
32970
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
32971
|
-
}
|
|
32972
|
-
function normalizePrefix(value) {
|
|
32973
|
-
return value.replace(/^\/+|\/+$/g, "") || "skills/artifacts";
|
|
32974
|
-
}
|
|
32975
|
-
|
|
32976
|
-
// src/server/database-url.ts
|
|
32977
|
-
import { isAbsolute, join as join3 } from "path";
|
|
32978
|
-
import { fileURLToPath } from "url";
|
|
32979
|
-
|
|
32980
33159
|
// src/lib/config.ts
|
|
32981
|
-
import { existsSync, readFileSync as readFileSync2, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
32982
|
-
import { join as join2, dirname as dirname2 } from "path";
|
|
32983
|
-
import { homedir as homedir2 } from "os";
|
|
32984
33160
|
var ENUM_KEYS = {
|
|
32985
33161
|
defaultAgent: ["claude", "codex", "gemini", "pi", "opencode", "all"],
|
|
32986
33162
|
defaultScope: ["global", "project"],
|
|
@@ -33160,39 +33336,78 @@ function looksLikeSqlitePath(value) {
|
|
|
33160
33336
|
return SQLITE_EXTENSIONS.some((extension) => value.toLowerCase().endsWith(extension));
|
|
33161
33337
|
}
|
|
33162
33338
|
|
|
33163
|
-
// src/server/
|
|
33164
|
-
import {
|
|
33339
|
+
// src/server/sqlite-store.ts
|
|
33340
|
+
import { Database } from "bun:sqlite";
|
|
33341
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
33342
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
|
|
33343
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
33165
33344
|
|
|
33166
|
-
// src/server/
|
|
33167
|
-
import {
|
|
33345
|
+
// src/server/auth.ts
|
|
33346
|
+
import { createHash as createHash3 } from "crypto";
|
|
33347
|
+
function hashApiKey(token) {
|
|
33348
|
+
return createHash3("sha256").update(token).digest("hex");
|
|
33349
|
+
}
|
|
33350
|
+
function bearerToken(request) {
|
|
33351
|
+
const header = request.headers.get("authorization") || "";
|
|
33352
|
+
const match = header.match(/^Bearer\s+(.+)$/i);
|
|
33353
|
+
const token = match?.[1]?.trim();
|
|
33354
|
+
return token || null;
|
|
33355
|
+
}
|
|
33356
|
+
async function authenticateRequest(store, request) {
|
|
33357
|
+
const token = bearerToken(request);
|
|
33358
|
+
if (!token)
|
|
33359
|
+
return null;
|
|
33360
|
+
return store.authenticateApiKeyHash(hashApiKey(token));
|
|
33361
|
+
}
|
|
33362
|
+
function publicPrincipal(partial = {}) {
|
|
33363
|
+
return {
|
|
33364
|
+
apiKeyId: partial.apiKeyId || "key_dev",
|
|
33365
|
+
orgId: partial.orgId || "org_dev",
|
|
33366
|
+
orgSlug: partial.orgSlug || "dev",
|
|
33367
|
+
orgName: partial.orgName || "Development",
|
|
33368
|
+
userId: partial.userId || "user_dev",
|
|
33369
|
+
email: partial.email || "dev@example.com",
|
|
33370
|
+
role: partial.role || "owner",
|
|
33371
|
+
scopes: partial.scopes || ["skills:read", "runs:write"]
|
|
33372
|
+
};
|
|
33373
|
+
}
|
|
33168
33374
|
|
|
33169
|
-
// src/server/
|
|
33170
|
-
|
|
33171
|
-
|
|
33172
|
-
|
|
33173
|
-
|
|
33174
|
-
|
|
33175
|
-
|
|
33176
|
-
|
|
33177
|
-
|
|
33178
|
-
|
|
33179
|
-
|
|
33180
|
-
|
|
33181
|
-
|
|
33375
|
+
// src/server/migrations-dir.ts
|
|
33376
|
+
import { existsSync as existsSync2 } from "fs";
|
|
33377
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
33378
|
+
var MIGRATION_DIALECTS = ["postgres", "sqlite"];
|
|
33379
|
+
var MAX_WALK_UP = 6;
|
|
33380
|
+
function findMigrationsRoot(startDirs = defaultStartDirs()) {
|
|
33381
|
+
for (const start of startDirs) {
|
|
33382
|
+
let dir = start;
|
|
33383
|
+
for (let level = 0;level < MAX_WALK_UP; level += 1) {
|
|
33384
|
+
const candidate = join4(dir, "migrations");
|
|
33385
|
+
if (MIGRATION_DIALECTS.some((dialect) => existsSync2(join4(candidate, dialect))))
|
|
33386
|
+
return candidate;
|
|
33387
|
+
const parent = dirname3(dir);
|
|
33388
|
+
if (parent === dir)
|
|
33389
|
+
break;
|
|
33390
|
+
dir = parent;
|
|
33391
|
+
}
|
|
33182
33392
|
}
|
|
33393
|
+
return null;
|
|
33183
33394
|
}
|
|
33184
|
-
|
|
33185
|
-
|
|
33186
|
-
|
|
33187
|
-
expectedRevisionId;
|
|
33188
|
-
currentRevisionId;
|
|
33189
|
-
constructor(slug, expectedRevisionId, currentRevisionId) {
|
|
33190
|
-
super(`revision conflict for '${slug}': expected revision ${expectedRevisionId ?? "(none)"}, ` + `current is ${currentRevisionId ?? "(none)"}. Refused rather than silently overwriting a newer revision.`);
|
|
33191
|
-
this.name = "SkillRevisionConflictError";
|
|
33192
|
-
this.slug = slug;
|
|
33193
|
-
this.expectedRevisionId = expectedRevisionId;
|
|
33194
|
-
this.currentRevisionId = currentRevisionId;
|
|
33395
|
+
function resolveMigrationsDir(dialect, root3 = findMigrationsRoot()) {
|
|
33396
|
+
if (!root3) {
|
|
33397
|
+
throw new Error(`could not locate the migrations directory for dialect "${dialect}". ` + `Expected a migrations/${dialect}/ folder alongside the package root.`);
|
|
33195
33398
|
}
|
|
33399
|
+
const dir = join4(root3, dialect);
|
|
33400
|
+
if (!existsSync2(dir)) {
|
|
33401
|
+
throw new Error(`migrations directory not found: ${dir}`);
|
|
33402
|
+
}
|
|
33403
|
+
return dir;
|
|
33404
|
+
}
|
|
33405
|
+
function defaultStartDirs() {
|
|
33406
|
+
const dirs = [import.meta.dir];
|
|
33407
|
+
const cwd = process.cwd();
|
|
33408
|
+
if (cwd && cwd !== import.meta.dir)
|
|
33409
|
+
dirs.push(cwd);
|
|
33410
|
+
return dirs;
|
|
33196
33411
|
}
|
|
33197
33412
|
|
|
33198
33413
|
// src/server/rows.ts
|
|
@@ -33340,6 +33555,35 @@ function dateString(value) {
|
|
|
33340
33555
|
return String(value);
|
|
33341
33556
|
}
|
|
33342
33557
|
|
|
33558
|
+
// src/server/types.ts
|
|
33559
|
+
class StaleLeaseGenerationError extends Error {
|
|
33560
|
+
runId;
|
|
33561
|
+
expectedGeneration;
|
|
33562
|
+
currentGeneration;
|
|
33563
|
+
status;
|
|
33564
|
+
constructor(runId2, expectedGeneration, currentGeneration, status) {
|
|
33565
|
+
super(`late transition rejected for run ${runId2}: expected lease_generation ${expectedGeneration}, ` + `current ${currentGeneration} (status ${status})`);
|
|
33566
|
+
this.name = "StaleLeaseGenerationError";
|
|
33567
|
+
this.runId = runId2;
|
|
33568
|
+
this.expectedGeneration = expectedGeneration;
|
|
33569
|
+
this.currentGeneration = currentGeneration;
|
|
33570
|
+
this.status = status;
|
|
33571
|
+
}
|
|
33572
|
+
}
|
|
33573
|
+
|
|
33574
|
+
class SkillRevisionConflictError extends Error {
|
|
33575
|
+
slug;
|
|
33576
|
+
expectedRevisionId;
|
|
33577
|
+
currentRevisionId;
|
|
33578
|
+
constructor(slug, expectedRevisionId, currentRevisionId) {
|
|
33579
|
+
super(`revision conflict for '${slug}': expected revision ${expectedRevisionId ?? "(none)"}, ` + `current is ${currentRevisionId ?? "(none)"}. Refused rather than silently overwriting a newer revision.`);
|
|
33580
|
+
this.name = "SkillRevisionConflictError";
|
|
33581
|
+
this.slug = slug;
|
|
33582
|
+
this.expectedRevisionId = expectedRevisionId;
|
|
33583
|
+
this.currentRevisionId = currentRevisionId;
|
|
33584
|
+
}
|
|
33585
|
+
}
|
|
33586
|
+
|
|
33343
33587
|
// src/lib/revision.ts
|
|
33344
33588
|
import { createHash as createHash4 } from "crypto";
|
|
33345
33589
|
var REVISION_ID_PATTERN = /^[0-9a-f]{64}$/;
|
|
@@ -33363,50 +33607,6 @@ function revisionIdOfRecord(record) {
|
|
|
33363
33607
|
return revisionIdOf(record);
|
|
33364
33608
|
}
|
|
33365
33609
|
|
|
33366
|
-
// src/server/sqlite-store.ts
|
|
33367
|
-
import { Database } from "bun:sqlite";
|
|
33368
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
33369
|
-
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
|
|
33370
|
-
import { dirname as dirname4, join as join5 } from "path";
|
|
33371
|
-
|
|
33372
|
-
// src/server/migrations-dir.ts
|
|
33373
|
-
import { existsSync as existsSync2 } from "fs";
|
|
33374
|
-
import { dirname as dirname3, join as join4 } from "path";
|
|
33375
|
-
var MIGRATION_DIALECTS = ["postgres", "sqlite"];
|
|
33376
|
-
var MAX_WALK_UP = 6;
|
|
33377
|
-
function findMigrationsRoot(startDirs = defaultStartDirs()) {
|
|
33378
|
-
for (const start of startDirs) {
|
|
33379
|
-
let dir = start;
|
|
33380
|
-
for (let level = 0;level < MAX_WALK_UP; level += 1) {
|
|
33381
|
-
const candidate = join4(dir, "migrations");
|
|
33382
|
-
if (MIGRATION_DIALECTS.some((dialect) => existsSync2(join4(candidate, dialect))))
|
|
33383
|
-
return candidate;
|
|
33384
|
-
const parent = dirname3(dir);
|
|
33385
|
-
if (parent === dir)
|
|
33386
|
-
break;
|
|
33387
|
-
dir = parent;
|
|
33388
|
-
}
|
|
33389
|
-
}
|
|
33390
|
-
return null;
|
|
33391
|
-
}
|
|
33392
|
-
function resolveMigrationsDir(dialect, root3 = findMigrationsRoot()) {
|
|
33393
|
-
if (!root3) {
|
|
33394
|
-
throw new Error(`could not locate the migrations directory for dialect "${dialect}". ` + `Expected a migrations/${dialect}/ folder alongside the package root.`);
|
|
33395
|
-
}
|
|
33396
|
-
const dir = join4(root3, dialect);
|
|
33397
|
-
if (!existsSync2(dir)) {
|
|
33398
|
-
throw new Error(`migrations directory not found: ${dir}`);
|
|
33399
|
-
}
|
|
33400
|
-
return dir;
|
|
33401
|
-
}
|
|
33402
|
-
function defaultStartDirs() {
|
|
33403
|
-
const dirs = [import.meta.dir];
|
|
33404
|
-
const cwd = process.cwd();
|
|
33405
|
-
if (cwd && cwd !== import.meta.dir)
|
|
33406
|
-
dirs.push(cwd);
|
|
33407
|
-
return dirs;
|
|
33408
|
-
}
|
|
33409
|
-
|
|
33410
33610
|
// src/server/sqlite-store.ts
|
|
33411
33611
|
var CLAIM_ATTEMPTS = 8;
|
|
33412
33612
|
var CLAIMABLE_STATUSES = ["queued", "retrying"];
|
|
@@ -33997,7 +34197,301 @@ function parseScopes(value) {
|
|
|
33997
34197
|
}
|
|
33998
34198
|
}
|
|
33999
34199
|
|
|
34200
|
+
// src/sdk/governance-store.ts
|
|
34201
|
+
function receiptId() {
|
|
34202
|
+
return `rcpt_${Date.now().toString(36)}_${randomUUID3().replace(/-/g, "").slice(0, 10)}`;
|
|
34203
|
+
}
|
|
34204
|
+
function reservationId() {
|
|
34205
|
+
return `res_${Date.now().toString(36)}_${randomUUID3().replace(/-/g, "").slice(0, 10)}`;
|
|
34206
|
+
}
|
|
34207
|
+
var EXPIRED_ARTIFACT_SQL = `
|
|
34208
|
+
SELECT a.* FROM skills_artifacts a
|
|
34209
|
+
JOIN skills_runs r ON r.id = a.run_id
|
|
34210
|
+
WHERE a.expires_at IS NOT NULL AND a.expires_at <= ?
|
|
34211
|
+
ORDER BY a.expires_at ASC
|
|
34212
|
+
`;
|
|
34213
|
+
var ACTIVE_RUN_SQL = `
|
|
34214
|
+
SELECT COUNT(*) AS n FROM skills_runs
|
|
34215
|
+
WHERE org_id = ? AND status IN ('queued','running','cancel_requested')
|
|
34216
|
+
`;
|
|
34217
|
+
class SqliteGovernanceStore {
|
|
34218
|
+
backend = "sqlite";
|
|
34219
|
+
db;
|
|
34220
|
+
closed = false;
|
|
34221
|
+
constructor(path = SQLITE_MEMORY_PATH, options = {}) {
|
|
34222
|
+
if (path !== SQLITE_MEMORY_PATH)
|
|
34223
|
+
mkdirSync3(dirname5(path), { recursive: true });
|
|
34224
|
+
this.db = new Database2(path, { create: true, readwrite: true });
|
|
34225
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
34226
|
+
this.db.exec("PRAGMA foreign_keys = ON");
|
|
34227
|
+
if (options.migrate !== false)
|
|
34228
|
+
applySqliteMigrations(this.db);
|
|
34229
|
+
}
|
|
34230
|
+
async close() {
|
|
34231
|
+
if (this.closed)
|
|
34232
|
+
return;
|
|
34233
|
+
this.closed = true;
|
|
34234
|
+
this.db.close(false);
|
|
34235
|
+
}
|
|
34236
|
+
get database() {
|
|
34237
|
+
return this.db;
|
|
34238
|
+
}
|
|
34239
|
+
async appendReceipt(receipt) {
|
|
34240
|
+
const next = { ...receipt, id: receiptId(), createdAt: nowIso() };
|
|
34241
|
+
this.db.run(`INSERT INTO skills_lifecycle_receipts (id, kind, org_id, run_id, artifact_id, requested_by, metadata_json, created_at)
|
|
34242
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [next.id, next.kind, next.orgId, next.runId, next.artifactId ?? null, next.requestedBy, JSON.stringify(next.metadata), next.createdAt]);
|
|
34243
|
+
return next;
|
|
34244
|
+
}
|
|
34245
|
+
async listReceipts(orgId, runId2) {
|
|
34246
|
+
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);
|
|
34247
|
+
return rows.map((row) => ({
|
|
34248
|
+
id: String(row.id),
|
|
34249
|
+
kind: String(row.kind),
|
|
34250
|
+
orgId: String(row.org_id),
|
|
34251
|
+
runId: String(row.run_id),
|
|
34252
|
+
...typeof row.artifact_id === "string" ? { artifactId: row.artifact_id } : {},
|
|
34253
|
+
requestedBy: String(row.requested_by),
|
|
34254
|
+
metadata: parseJsonObject(row.metadata_json),
|
|
34255
|
+
createdAt: String(row.created_at)
|
|
34256
|
+
}));
|
|
34257
|
+
}
|
|
34258
|
+
async createReservation(input) {
|
|
34259
|
+
const reservation = { ...input, id: reservationId(), status: "reserved", createdAt: nowIso() };
|
|
34260
|
+
this.db.run(`INSERT INTO skills_credit_reservations (id, org_id, run_id, estimated_cents, status, created_at)
|
|
34261
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [reservation.id, reservation.orgId, reservation.runId, reservation.estimatedCents, reservation.status, reservation.createdAt]);
|
|
34262
|
+
return reservation;
|
|
34263
|
+
}
|
|
34264
|
+
async reservationsForRun(orgId, runId2) {
|
|
34265
|
+
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);
|
|
34266
|
+
return rows.map((row) => this.reservationFrom(row));
|
|
34267
|
+
}
|
|
34268
|
+
async reconcileReservation(reservationId2, actualCents, status) {
|
|
34269
|
+
const row = this.db.query("SELECT * FROM skills_credit_reservations WHERE id = ? LIMIT 1").get(reservationId2);
|
|
34270
|
+
if (!row || String(row.status) !== "reserved")
|
|
34271
|
+
return row ? this.reservationFrom(row) : null;
|
|
34272
|
+
const reconciledAt = nowIso();
|
|
34273
|
+
this.db.run("UPDATE skills_credit_reservations SET actual_cents = ?, status = ?, reconciled_at = ? WHERE id = ?", [actualCents, status, reconciledAt, reservationId2]);
|
|
34274
|
+
const updated = this.db.query("SELECT * FROM skills_credit_reservations WHERE id = ? LIMIT 1").get(reservationId2);
|
|
34275
|
+
return this.reservationFrom(updated);
|
|
34276
|
+
}
|
|
34277
|
+
async monthlySpendCents(orgId, monthPrefix) {
|
|
34278
|
+
const from = `${monthPrefix}-01T00:00:00.000Z`;
|
|
34279
|
+
const nextMonth = nextMonthPrefix(monthPrefix);
|
|
34280
|
+
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`);
|
|
34281
|
+
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);
|
|
34282
|
+
return Number(spent.spent ?? 0) + Number(pending.pending ?? 0);
|
|
34283
|
+
}
|
|
34284
|
+
async activeRunCount(orgId) {
|
|
34285
|
+
const row = this.db.query(ACTIVE_RUN_SQL).get(orgId);
|
|
34286
|
+
return Number(row.n ?? 0);
|
|
34287
|
+
}
|
|
34288
|
+
async listExpiredArtifacts(at2) {
|
|
34289
|
+
const rows = this.db.query(EXPIRED_ARTIFACT_SQL).all(at2);
|
|
34290
|
+
return rows.map(rowToArtifact);
|
|
34291
|
+
}
|
|
34292
|
+
async deleteArtifactRow(artifactId2, orgId) {
|
|
34293
|
+
const result = this.db.run("DELETE FROM skills_artifacts WHERE id = ? AND org_id = ?", [artifactId2, orgId]);
|
|
34294
|
+
return result.changes === 1;
|
|
34295
|
+
}
|
|
34296
|
+
async updateArtifactStorageKey(artifactId2, orgId, storageKey) {
|
|
34297
|
+
const result = this.db.run("UPDATE skills_artifacts SET storage_key = ? WHERE id = ? AND org_id = ?", [storageKey, artifactId2, orgId]);
|
|
34298
|
+
return result.changes === 1;
|
|
34299
|
+
}
|
|
34300
|
+
reservationFrom(row) {
|
|
34301
|
+
return {
|
|
34302
|
+
id: String(row.id),
|
|
34303
|
+
orgId: String(row.org_id),
|
|
34304
|
+
runId: String(row.run_id),
|
|
34305
|
+
estimatedCents: Number(row.estimated_cents ?? 0),
|
|
34306
|
+
...row.actual_cents !== null && row.actual_cents !== undefined ? { actualCents: Number(row.actual_cents) } : {},
|
|
34307
|
+
status: String(row.status),
|
|
34308
|
+
createdAt: String(row.created_at),
|
|
34309
|
+
...typeof row.reconciled_at === "string" ? { reconciledAt: row.reconciled_at } : {}
|
|
34310
|
+
};
|
|
34311
|
+
}
|
|
34312
|
+
}
|
|
34313
|
+
|
|
34314
|
+
class PostgresGovernanceStore {
|
|
34315
|
+
backend = "postgres";
|
|
34316
|
+
sql;
|
|
34317
|
+
constructor(databaseUrl) {
|
|
34318
|
+
const bunWithSql = Bun;
|
|
34319
|
+
this.sql = new bunWithSql.SQL(databaseUrl, { max: 2 });
|
|
34320
|
+
}
|
|
34321
|
+
async close() {
|
|
34322
|
+
await this.sql.close?.();
|
|
34323
|
+
}
|
|
34324
|
+
async withContext(orgId, worker, fn) {
|
|
34325
|
+
return this.sql.begin(async (tx) => {
|
|
34326
|
+
await tx`SELECT set_config('app.skills_org_id', ${orgId ?? ""}, true)`;
|
|
34327
|
+
await tx`SELECT set_config('app.skills_claim_context', ${worker ? "worker" : ""}, true)`;
|
|
34328
|
+
return await fn(tx);
|
|
34329
|
+
});
|
|
34330
|
+
}
|
|
34331
|
+
async appendReceipt(receipt) {
|
|
34332
|
+
const next = { ...receipt, id: receiptId(), createdAt: nowIso() };
|
|
34333
|
+
await this.sql`
|
|
34334
|
+
INSERT INTO skills_lifecycle_receipts (id, kind, org_id, run_id, artifact_id, requested_by, metadata_json)
|
|
34335
|
+
VALUES (${next.id}, ${next.kind}, ${next.orgId}, ${next.runId}, ${next.artifactId ?? null}, ${next.requestedBy}, ${JSON.stringify(next.metadata)}::jsonb)
|
|
34336
|
+
`;
|
|
34337
|
+
return next;
|
|
34338
|
+
}
|
|
34339
|
+
async listReceipts(orgId, runId2) {
|
|
34340
|
+
const rows = await this.sql`
|
|
34341
|
+
SELECT * FROM skills_lifecycle_receipts WHERE org_id = ${orgId} AND run_id = ${runId2} ORDER BY created_at ASC
|
|
34342
|
+
`;
|
|
34343
|
+
return rows.map((row) => ({
|
|
34344
|
+
id: String(row.id),
|
|
34345
|
+
kind: String(row.kind),
|
|
34346
|
+
orgId: String(row.org_id),
|
|
34347
|
+
runId: String(row.run_id),
|
|
34348
|
+
...typeof row.artifact_id === "string" ? { artifactId: row.artifact_id } : {},
|
|
34349
|
+
requestedBy: String(row.requested_by),
|
|
34350
|
+
metadata: parseJsonObject(row.metadata_json),
|
|
34351
|
+
createdAt: String(row.created_at)
|
|
34352
|
+
}));
|
|
34353
|
+
}
|
|
34354
|
+
async createReservation(input) {
|
|
34355
|
+
const reservation = { ...input, id: reservationId(), status: "reserved", createdAt: nowIso() };
|
|
34356
|
+
await this.sql`
|
|
34357
|
+
INSERT INTO skills_credit_reservations (id, org_id, run_id, estimated_cents, status)
|
|
34358
|
+
VALUES (${reservation.id}, ${reservation.orgId}, ${reservation.runId}, ${reservation.estimatedCents}, ${reservation.status})
|
|
34359
|
+
`;
|
|
34360
|
+
return reservation;
|
|
34361
|
+
}
|
|
34362
|
+
async reservationsForRun(orgId, runId2) {
|
|
34363
|
+
const rows = await this.sql`
|
|
34364
|
+
SELECT * FROM skills_credit_reservations WHERE org_id = ${orgId} AND run_id = ${runId2} ORDER BY created_at ASC
|
|
34365
|
+
`;
|
|
34366
|
+
return rows.map((row) => this.reservationFrom(row));
|
|
34367
|
+
}
|
|
34368
|
+
async reconcileReservation(reservationId2, actualCents, status) {
|
|
34369
|
+
const rows = await this.sql`
|
|
34370
|
+
UPDATE skills_credit_reservations
|
|
34371
|
+
SET actual_cents = ${actualCents}, status = ${status}, reconciled_at = now()
|
|
34372
|
+
WHERE id = ${reservationId2} AND status = ${"reserved"}
|
|
34373
|
+
RETURNING *
|
|
34374
|
+
`;
|
|
34375
|
+
if (!rows[0]) {
|
|
34376
|
+
const existing = await this.sql`SELECT * FROM skills_credit_reservations WHERE id = ${reservationId2} LIMIT 1`;
|
|
34377
|
+
return existing[0] ? this.reservationFrom(existing[0]) : null;
|
|
34378
|
+
}
|
|
34379
|
+
return this.reservationFrom(rows[0]);
|
|
34380
|
+
}
|
|
34381
|
+
async monthlySpendCents(orgId, monthPrefix) {
|
|
34382
|
+
return this.withContext(null, true, async (tx) => {
|
|
34383
|
+
const rows = await tx`
|
|
34384
|
+
SELECT
|
|
34385
|
+
(SELECT COALESCE(SUM(cost_cents), 0) FROM skills_runs
|
|
34386
|
+
WHERE org_id = ${orgId} AND created_at >= ${`${monthPrefix}-01T00:00:00.000Z`} AND created_at < ${`${nextMonthPrefix(monthPrefix)}-01T00:00:00.000Z`}) AS spent,
|
|
34387
|
+
(SELECT COALESCE(SUM(estimated_cents), 0) FROM skills_credit_reservations
|
|
34388
|
+
WHERE org_id = ${orgId} AND status = ${"reserved"}) AS pending
|
|
34389
|
+
`;
|
|
34390
|
+
return Number(rows[0]?.spent ?? 0) + Number(rows[0]?.pending ?? 0);
|
|
34391
|
+
});
|
|
34392
|
+
}
|
|
34393
|
+
async activeRunCount(orgId) {
|
|
34394
|
+
return this.withContext(orgId, false, async (tx) => {
|
|
34395
|
+
const rows = await tx`SELECT COUNT(*) AS n FROM skills_runs WHERE org_id = ${orgId} AND status IN (${"queued"}, ${"running"}, ${"cancel_requested"})`;
|
|
34396
|
+
return Number(rows[0]?.n ?? 0);
|
|
34397
|
+
});
|
|
34398
|
+
}
|
|
34399
|
+
async listExpiredArtifacts(at2) {
|
|
34400
|
+
return this.withContext(null, true, async (tx) => {
|
|
34401
|
+
const rows = await tx`
|
|
34402
|
+
SELECT a.* FROM skills_artifacts a
|
|
34403
|
+
JOIN skills_runs r ON r.id = a.run_id
|
|
34404
|
+
WHERE a.expires_at IS NOT NULL AND a.expires_at <= ${at2}
|
|
34405
|
+
ORDER BY a.expires_at ASC
|
|
34406
|
+
`;
|
|
34407
|
+
return rows.map(rowToArtifact);
|
|
34408
|
+
});
|
|
34409
|
+
}
|
|
34410
|
+
async deleteArtifactRow(artifactId2, orgId) {
|
|
34411
|
+
return this.withContext(orgId, true, async (tx) => {
|
|
34412
|
+
const rows = await tx`DELETE FROM skills_artifacts WHERE id = ${artifactId2} AND org_id = ${orgId} RETURNING id`;
|
|
34413
|
+
return rows.length > 0;
|
|
34414
|
+
});
|
|
34415
|
+
}
|
|
34416
|
+
async updateArtifactStorageKey(artifactId2, orgId, storageKey) {
|
|
34417
|
+
return this.withContext(orgId, true, async (tx) => {
|
|
34418
|
+
const rows = await tx`
|
|
34419
|
+
UPDATE skills_artifacts SET storage_key = ${storageKey} WHERE id = ${artifactId2} AND org_id = ${orgId} RETURNING id
|
|
34420
|
+
`;
|
|
34421
|
+
return rows.length > 0;
|
|
34422
|
+
});
|
|
34423
|
+
}
|
|
34424
|
+
reservationFrom(row) {
|
|
34425
|
+
return {
|
|
34426
|
+
id: String(row.id),
|
|
34427
|
+
orgId: String(row.org_id),
|
|
34428
|
+
runId: String(row.run_id),
|
|
34429
|
+
estimatedCents: Number(row.estimated_cents ?? 0),
|
|
34430
|
+
...row.actual_cents !== null && row.actual_cents !== undefined ? { actualCents: Number(row.actual_cents) } : {},
|
|
34431
|
+
status: String(row.status),
|
|
34432
|
+
createdAt: String(row.created_at),
|
|
34433
|
+
...typeof row.reconciled_at === "string" ? { reconciledAt: row.reconciled_at } : {}
|
|
34434
|
+
};
|
|
34435
|
+
}
|
|
34436
|
+
}
|
|
34437
|
+
async function createGovernanceStore(databaseUrl) {
|
|
34438
|
+
const target = resolveDatabaseTarget(databaseUrl);
|
|
34439
|
+
if (target.kind === "postgres")
|
|
34440
|
+
return new PostgresGovernanceStore(target.url);
|
|
34441
|
+
if (target.kind === "memory")
|
|
34442
|
+
return new SqliteGovernanceStore(SQLITE_MEMORY_PATH);
|
|
34443
|
+
return new SqliteGovernanceStore(target.path);
|
|
34444
|
+
}
|
|
34445
|
+
function nextMonthPrefix(monthPrefix) {
|
|
34446
|
+
const [year, month] = monthPrefix.split("-").map(Number);
|
|
34447
|
+
return month === 12 ? `${year + 1}-01` : `${year}-${String(month + 1).padStart(2, "0")}`;
|
|
34448
|
+
}
|
|
34449
|
+
|
|
34450
|
+
// src/server/config.ts
|
|
34451
|
+
var DATABASE_URL_ENV = "HASNA_SKILLS_DATABASE_URL";
|
|
34452
|
+
var SKILLS_ENV_NAMESPACE = "SKILLS";
|
|
34453
|
+
function resolveServerConfig(env = process.env) {
|
|
34454
|
+
assertNoRetiredModeEnvVars(env, {
|
|
34455
|
+
app: SKILLS_ENV_NAMESPACE,
|
|
34456
|
+
replacement: DATABASE_URL_ENV
|
|
34457
|
+
});
|
|
34458
|
+
const nodeEnv = env.NODE_ENV || "development";
|
|
34459
|
+
const host = env.HOST || env.SKILLS_HOST || "0.0.0.0";
|
|
34460
|
+
const port = parsePositiveInt(env.PORT || env.SKILLS_PORT, 8787);
|
|
34461
|
+
return {
|
|
34462
|
+
host,
|
|
34463
|
+
port,
|
|
34464
|
+
databaseUrl: env[DATABASE_URL_ENV] || env.DATABASE_URL || undefined,
|
|
34465
|
+
bootstrapApiKey: env.HASNA_SKILLS_BOOTSTRAP_API_KEY || undefined,
|
|
34466
|
+
artifactBucket: env.HASNA_SKILLS_S3_BUCKET || env.SKILLS_S3_BUCKET || undefined,
|
|
34467
|
+
artifactPrefix: normalizePrefix(env.HASNA_SKILLS_S3_PREFIX || env.SKILLS_S3_PREFIX || "skills/artifacts"),
|
|
34468
|
+
inlineWorker: env.HASNA_SKILLS_INLINE_WORKER === "1",
|
|
34469
|
+
bundleSigningKey: env.HASNA_SKILLS_API_SIGNING_KEY || env.HASNA_SKILLS_SIGNING_KEY || undefined,
|
|
34470
|
+
requestBodyLimitBytes: parsePositiveInt(env.HASNA_SKILLS_REQUEST_BODY_LIMIT_BYTES, 1e6),
|
|
34471
|
+
skillBundleLimitBytes: parsePositiveInt(env.HASNA_SKILLS_BUNDLE_LIMIT_BYTES, 25000000),
|
|
34472
|
+
tombstoneWindowMs: parsePositiveInt(env.HASNA_SKILLS_TOMBSTONE_WINDOW_MS, 7 * 24 * 60 * 60 * 1000),
|
|
34473
|
+
publicBaseUrl: (env.SKILLS_PUBLIC_BASE_URL || localOrigin(host, port)).replace(/\/+$/, ""),
|
|
34474
|
+
nodeEnv,
|
|
34475
|
+
allowEphemeralStore: env.HASNA_SKILLS_ALLOW_EPHEMERAL_STORE === "1"
|
|
34476
|
+
};
|
|
34477
|
+
}
|
|
34478
|
+
function localOrigin(host, port) {
|
|
34479
|
+
const hostname = host === "0.0.0.0" || host === "::" ? "localhost" : host;
|
|
34480
|
+
return `http://${hostname.includes(":") ? `[${hostname}]` : hostname}:${port}`;
|
|
34481
|
+
}
|
|
34482
|
+
function parsePositiveInt(value, fallback) {
|
|
34483
|
+
const parsed = Number.parseInt(value ?? "", 10);
|
|
34484
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
34485
|
+
}
|
|
34486
|
+
function normalizePrefix(value) {
|
|
34487
|
+
return value.replace(/^\/+|\/+$/g, "") || "skills/artifacts";
|
|
34488
|
+
}
|
|
34489
|
+
|
|
34490
|
+
// src/server/handlers.ts
|
|
34491
|
+
import { createHash as createHash5 } from "crypto";
|
|
34492
|
+
|
|
34000
34493
|
// src/server/store.ts
|
|
34494
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
34001
34495
|
function recordFieldsOf(input, carriedBundle, carriedSkillMd) {
|
|
34002
34496
|
return {
|
|
34003
34497
|
slug: input.slug,
|
|
@@ -34085,7 +34579,7 @@ class MemorySkillsStore {
|
|
|
34085
34579
|
input: input.input,
|
|
34086
34580
|
args: input.args,
|
|
34087
34581
|
...idemKey ? { idempotencyKey: idemKey } : {},
|
|
34088
|
-
correlationId:
|
|
34582
|
+
correlationId: randomUUID4(),
|
|
34089
34583
|
costCents: 0,
|
|
34090
34584
|
leaseGeneration: 0,
|
|
34091
34585
|
createdAt: now
|
|
@@ -34400,20 +34894,20 @@ class PostgresSkillsStore {
|
|
|
34400
34894
|
});
|
|
34401
34895
|
}
|
|
34402
34896
|
async createRun(input) {
|
|
34403
|
-
if (input.idempotencyKey) {
|
|
34404
|
-
const existing = await this.sql`
|
|
34405
|
-
SELECT * FROM skills_runs
|
|
34406
|
-
WHERE org_id = ${input.principal.orgId} AND idempotency_key = ${input.idempotencyKey}
|
|
34407
|
-
LIMIT 1
|
|
34408
|
-
`;
|
|
34409
|
-
if (existing[0])
|
|
34410
|
-
return rowToRun(existing[0]);
|
|
34411
|
-
}
|
|
34412
|
-
const id = runId();
|
|
34413
34897
|
return this.withContext(input.principal.orgId, false, async (tx) => {
|
|
34898
|
+
if (input.idempotencyKey) {
|
|
34899
|
+
const existing = await tx`
|
|
34900
|
+
SELECT * FROM skills_runs
|
|
34901
|
+
WHERE org_id = ${input.principal.orgId} AND idempotency_key = ${input.idempotencyKey}
|
|
34902
|
+
LIMIT 1
|
|
34903
|
+
`;
|
|
34904
|
+
if (existing[0])
|
|
34905
|
+
return rowToRun(existing[0]);
|
|
34906
|
+
}
|
|
34907
|
+
const id = runId();
|
|
34414
34908
|
const rows = await tx`
|
|
34415
34909
|
INSERT INTO skills_runs (id, org_id, user_id, skill_slug, requested_slug, status, input_json, args_json, idempotency_key, correlation_id)
|
|
34416
|
-
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}, ${
|
|
34910
|
+
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()})
|
|
34417
34911
|
RETURNING *
|
|
34418
34912
|
`;
|
|
34419
34913
|
return rowToRun(rows[0]);
|
|
@@ -34952,7 +35446,9 @@ async function completeRun(store, run, preview) {
|
|
|
34952
35446
|
return next ?? run;
|
|
34953
35447
|
}
|
|
34954
35448
|
async function failRun(store, run, code, message) {
|
|
34955
|
-
|
|
35449
|
+
try {
|
|
35450
|
+
await store.appendLog(run.id, run.orgId, "error", message);
|
|
35451
|
+
} catch {}
|
|
34956
35452
|
const next = await fencedTransition(store, run, {
|
|
34957
35453
|
status: "failed",
|
|
34958
35454
|
errorCode: code,
|
|
@@ -34966,7 +35462,9 @@ async function fencedTransition(store, run, patch) {
|
|
|
34966
35462
|
return store.updateRun(run.id, patch);
|
|
34967
35463
|
const next = await store.transitionRun(run.id, patch, run.leaseGeneration);
|
|
34968
35464
|
if (!next) {
|
|
34969
|
-
|
|
35465
|
+
try {
|
|
35466
|
+
await store.appendLog(run.id, run.orgId, "warn", `late write rejected: run no longer owned at lease_generation ${run.leaseGeneration}`);
|
|
35467
|
+
} catch {}
|
|
34970
35468
|
}
|
|
34971
35469
|
return next;
|
|
34972
35470
|
}
|
|
@@ -35048,25 +35546,26 @@ function mergeSkillRegistryLists(...groups) {
|
|
|
35048
35546
|
}
|
|
35049
35547
|
|
|
35050
35548
|
// src/server/registry.ts
|
|
35051
|
-
import { existsSync as
|
|
35549
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
35052
35550
|
import { resolve, sep } from "path";
|
|
35053
35551
|
|
|
35054
35552
|
// src/lib/registry.ts
|
|
35055
|
-
import { existsSync as
|
|
35056
|
-
import { join as
|
|
35553
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync5 } from "fs";
|
|
35554
|
+
import { join as join9 } from "path";
|
|
35057
35555
|
|
|
35058
35556
|
// src/lib/portable-skills.ts
|
|
35059
35557
|
import {
|
|
35060
35558
|
cpSync as cpSync2,
|
|
35061
|
-
existsSync as
|
|
35062
|
-
mkdirSync as
|
|
35063
|
-
|
|
35559
|
+
existsSync as existsSync5,
|
|
35560
|
+
mkdirSync as mkdirSync5,
|
|
35561
|
+
mkdtempSync,
|
|
35562
|
+
readdirSync as readdirSync4,
|
|
35064
35563
|
renameSync,
|
|
35065
35564
|
rmSync,
|
|
35066
|
-
statSync as
|
|
35565
|
+
statSync as statSync4,
|
|
35067
35566
|
writeFileSync as writeFileSync3
|
|
35068
35567
|
} from "fs";
|
|
35069
|
-
import { basename as basename2, dirname as
|
|
35568
|
+
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join8, normalize } from "path";
|
|
35070
35569
|
|
|
35071
35570
|
// src/lib/registry-data/development-tools.ts
|
|
35072
35571
|
var DEVELOPMENT_TOOLS_SKILLS = [
|
|
@@ -35778,8 +36277,29 @@ var SKILLS = [
|
|
|
35778
36277
|
];
|
|
35779
36278
|
|
|
35780
36279
|
// src/lib/hosted-skill-set.ts
|
|
36280
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
36281
|
+
import { join as join6 } from "path";
|
|
35781
36282
|
var HOSTED_RUNTIMES = new Set(["hosted"]);
|
|
35782
36283
|
var HOSTED_SOURCES = new Set(["remote", "private-hosted"]);
|
|
36284
|
+
function normalizeMarker(value) {
|
|
36285
|
+
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
36286
|
+
}
|
|
36287
|
+
function isHostedMetadataPackage(pkg) {
|
|
36288
|
+
const skills = pkg?.skills;
|
|
36289
|
+
if (!skills || typeof skills !== "object")
|
|
36290
|
+
return false;
|
|
36291
|
+
return HOSTED_RUNTIMES.has(normalizeMarker(skills.runtime)) || HOSTED_SOURCES.has(normalizeMarker(skills.source));
|
|
36292
|
+
}
|
|
36293
|
+
function isHostedMetadataSkillDir(skillDir) {
|
|
36294
|
+
const pkgPath = join6(skillDir, "package.json");
|
|
36295
|
+
if (!existsSync3(pkgPath))
|
|
36296
|
+
return false;
|
|
36297
|
+
try {
|
|
36298
|
+
return isHostedMetadataPackage(JSON.parse(readFileSync4(pkgPath, "utf8")));
|
|
36299
|
+
} catch {
|
|
36300
|
+
return false;
|
|
36301
|
+
}
|
|
36302
|
+
}
|
|
35783
36303
|
var HOSTED_METADATA_SET_EMPTY_ERROR = [
|
|
35784
36304
|
"The hosted metadata skill set is empty, but the packaging guards that depend on it",
|
|
35785
36305
|
"only mean anything while it is non-empty: an empty set makes every one of them pass",
|
|
@@ -35901,14 +36421,14 @@ var PORTABLE_SKILL_DEFAULT_VERSION = "0.1.0";
|
|
|
35901
36421
|
// src/lib/portable-skills-files.ts
|
|
35902
36422
|
import {
|
|
35903
36423
|
cpSync,
|
|
35904
|
-
existsSync as
|
|
36424
|
+
existsSync as existsSync4,
|
|
35905
36425
|
lstatSync,
|
|
35906
|
-
mkdirSync as
|
|
35907
|
-
readFileSync as
|
|
36426
|
+
mkdirSync as mkdirSync4,
|
|
36427
|
+
readFileSync as readFileSync5,
|
|
35908
36428
|
realpathSync,
|
|
35909
36429
|
writeFileSync as writeFileSync2
|
|
35910
36430
|
} from "fs";
|
|
35911
|
-
import { basename, dirname as
|
|
36431
|
+
import { basename, dirname as dirname6, join as join7, relative } from "path";
|
|
35912
36432
|
var ANY_SEGMENT_COPY_EXCLUDES = new Set([
|
|
35913
36433
|
".git",
|
|
35914
36434
|
".DS_Store",
|
|
@@ -35936,12 +36456,12 @@ function normalizePortableSkillName(name) {
|
|
|
35936
36456
|
return normalized;
|
|
35937
36457
|
}
|
|
35938
36458
|
function readPortableSkillManifest(skillPath, fallbackName = basename(skillPath)) {
|
|
35939
|
-
const skillJsonPath =
|
|
35940
|
-
const skillMdPath =
|
|
35941
|
-
const pkgPath =
|
|
35942
|
-
const jsonManifest =
|
|
35943
|
-
const frontmatter =
|
|
35944
|
-
const pkg =
|
|
36459
|
+
const skillJsonPath = join7(skillPath, "skill.json");
|
|
36460
|
+
const skillMdPath = join7(skillPath, "SKILL.md");
|
|
36461
|
+
const pkgPath = join7(skillPath, "package.json");
|
|
36462
|
+
const jsonManifest = existsSync4(skillJsonPath) ? readJsonObject(skillJsonPath) : undefined;
|
|
36463
|
+
const frontmatter = existsSync4(skillMdPath) ? parseSkillFrontmatter(readFileSync5(skillMdPath, "utf-8")) ?? undefined : undefined;
|
|
36464
|
+
const pkg = existsSync4(pkgPath) ? readJsonObject(pkgPath) : undefined;
|
|
35945
36465
|
const name = normalizePortableSkillName(stringField(jsonManifest, "name") ?? frontmatter?.name ?? stringValue(pkg?.name) ?? fallbackName);
|
|
35946
36466
|
const description = stringField(jsonManifest, "description") ?? frontmatter?.description ?? stringValue(pkg?.description) ?? `${name} skill`;
|
|
35947
36467
|
const version2 = stringField(jsonManifest, "version") ?? frontmatter?.version ?? stringValue(pkg?.version) ?? PORTABLE_SKILL_DEFAULT_VERSION;
|
|
@@ -36068,7 +36588,7 @@ function inferPackageCommands(pkg, fallbackName) {
|
|
|
36068
36588
|
return;
|
|
36069
36589
|
}
|
|
36070
36590
|
function readJsonObject(path) {
|
|
36071
|
-
const parsed = JSON.parse(
|
|
36591
|
+
const parsed = JSON.parse(readFileSync5(path, "utf-8"));
|
|
36072
36592
|
if (!isRecord(parsed))
|
|
36073
36593
|
throw new Error(`${basename(path)} must contain a JSON object`);
|
|
36074
36594
|
return parsed;
|
|
@@ -36097,36 +36617,36 @@ var LEGACY_CUSTOM_DIRNAME = "custom";
|
|
|
36097
36617
|
function getPortableSkillsRoot(options = {}) {
|
|
36098
36618
|
if (options.rootDir)
|
|
36099
36619
|
return options.rootDir;
|
|
36100
|
-
const appDir = options.homeDir ?
|
|
36101
|
-
const cache3 =
|
|
36620
|
+
const appDir = options.homeDir ? join8(options.homeDir, ".hasna", "skills") : getDataDir();
|
|
36621
|
+
const cache3 = join8(appDir, SKILLS_CACHE_DIRNAME);
|
|
36102
36622
|
if (isOwnerLayoutMigrated(appDir) && safeIsDirectory(cache3))
|
|
36103
36623
|
return cache3;
|
|
36104
|
-
const installed =
|
|
36624
|
+
const installed = join8(appDir, INSTALLED_SKILLS_DIRNAME);
|
|
36105
36625
|
migrateLegacySkillLayout(appDir, installed);
|
|
36106
36626
|
return installed;
|
|
36107
36627
|
}
|
|
36108
36628
|
function looksLikeSkillDirectory(path) {
|
|
36109
36629
|
if (!safeIsDirectory(path))
|
|
36110
36630
|
return false;
|
|
36111
|
-
return
|
|
36631
|
+
return existsSync5(join8(path, "SKILL.md")) || existsSync5(join8(path, "skill.json")) || existsSync5(join8(path, "package.json"));
|
|
36112
36632
|
}
|
|
36113
36633
|
function migrateLegacySkillLayout(appDir, installed) {
|
|
36114
36634
|
if (!safeIsDirectory(appDir))
|
|
36115
36635
|
return;
|
|
36116
36636
|
const candidates = [];
|
|
36117
36637
|
try {
|
|
36118
|
-
for (const entry of
|
|
36638
|
+
for (const entry of readdirSync4(appDir)) {
|
|
36119
36639
|
if (entry.startsWith(".") || entry === INSTALLED_SKILLS_DIRNAME)
|
|
36120
36640
|
continue;
|
|
36121
|
-
const path =
|
|
36641
|
+
const path = join8(appDir, entry);
|
|
36122
36642
|
if (entry === LEGACY_CUSTOM_DIRNAME) {
|
|
36123
36643
|
if (!safeIsDirectory(path))
|
|
36124
36644
|
continue;
|
|
36125
36645
|
try {
|
|
36126
|
-
for (const nested of
|
|
36646
|
+
for (const nested of readdirSync4(path)) {
|
|
36127
36647
|
if (nested.startsWith("."))
|
|
36128
36648
|
continue;
|
|
36129
|
-
const nestedPath =
|
|
36649
|
+
const nestedPath = join8(path, nested);
|
|
36130
36650
|
if (looksLikeSkillDirectory(nestedPath))
|
|
36131
36651
|
candidates.push({ from: nestedPath, name: nested });
|
|
36132
36652
|
}
|
|
@@ -36140,10 +36660,10 @@ function migrateLegacySkillLayout(appDir, installed) {
|
|
|
36140
36660
|
return;
|
|
36141
36661
|
}
|
|
36142
36662
|
for (const { from, name } of candidates) {
|
|
36143
|
-
const target =
|
|
36144
|
-
if (
|
|
36663
|
+
const target = join8(installed, name);
|
|
36664
|
+
if (existsSync5(target))
|
|
36145
36665
|
continue;
|
|
36146
|
-
const staging =
|
|
36666
|
+
const staging = join8(installed, `.migrating-${name}-${process.pid}`);
|
|
36147
36667
|
try {
|
|
36148
36668
|
rmSync(staging, { recursive: true, force: true });
|
|
36149
36669
|
cpSync2(from, staging, { recursive: true, errorOnExist: false });
|
|
@@ -36156,7 +36676,7 @@ function migrateLegacySkillLayout(appDir, installed) {
|
|
|
36156
36676
|
}
|
|
36157
36677
|
}
|
|
36158
36678
|
function getPortableSkillPath(name, options = {}) {
|
|
36159
|
-
return
|
|
36679
|
+
return join8(getPortableSkillsRoot(options), normalizePortableSkillName(name));
|
|
36160
36680
|
}
|
|
36161
36681
|
function findPortableSkill(name, options = {}) {
|
|
36162
36682
|
let normalized;
|
|
@@ -36166,7 +36686,7 @@ function findPortableSkill(name, options = {}) {
|
|
|
36166
36686
|
return null;
|
|
36167
36687
|
}
|
|
36168
36688
|
const path = getPortableSkillPath(normalized, options);
|
|
36169
|
-
if (!
|
|
36689
|
+
if (!existsSync5(path) || !statSync4(path).isDirectory())
|
|
36170
36690
|
return null;
|
|
36171
36691
|
try {
|
|
36172
36692
|
return summarizePortableSkill(path, normalized);
|
|
@@ -36179,10 +36699,10 @@ function listPortableSkills(options = {}) {
|
|
|
36179
36699
|
if (!safeIsDirectory(root3))
|
|
36180
36700
|
return [];
|
|
36181
36701
|
const skills = [];
|
|
36182
|
-
for (const entry of
|
|
36702
|
+
for (const entry of readdirSync4(root3).sort()) {
|
|
36183
36703
|
if (entry.startsWith("."))
|
|
36184
36704
|
continue;
|
|
36185
|
-
const path =
|
|
36705
|
+
const path = join8(root3, entry);
|
|
36186
36706
|
if (!safeIsDirectory(path))
|
|
36187
36707
|
continue;
|
|
36188
36708
|
try {
|
|
@@ -36204,7 +36724,8 @@ function listPortableSkillMetas(options = {}) {
|
|
|
36204
36724
|
tags: manifest.tags || ["custom"],
|
|
36205
36725
|
version: skill.version,
|
|
36206
36726
|
...manifest.kind ? { kind: manifest.kind } : {},
|
|
36207
|
-
source: "custom"
|
|
36727
|
+
source: "custom",
|
|
36728
|
+
...isHostedMetadataSkillDir(skill.path) ? { serverOwned: true } : {}
|
|
36208
36729
|
};
|
|
36209
36730
|
});
|
|
36210
36731
|
}
|
|
@@ -36224,7 +36745,7 @@ function summarizePortableSkill(skillPath, fallbackName) {
|
|
|
36224
36745
|
}
|
|
36225
36746
|
function safeIsDirectory(path) {
|
|
36226
36747
|
try {
|
|
36227
|
-
return
|
|
36748
|
+
return statSync4(path).isDirectory();
|
|
36228
36749
|
} catch {
|
|
36229
36750
|
return false;
|
|
36230
36751
|
}
|
|
@@ -36274,20 +36795,20 @@ function parseSkillMdFrontmatter(content) {
|
|
|
36274
36795
|
return Object.keys(result).length > 0 ? result : null;
|
|
36275
36796
|
}
|
|
36276
36797
|
function discoverSkillsInDir(dir, source = "custom") {
|
|
36277
|
-
if (!
|
|
36798
|
+
if (!existsSync6(dir))
|
|
36278
36799
|
return [];
|
|
36279
36800
|
const result = [];
|
|
36280
36801
|
try {
|
|
36281
|
-
const entries =
|
|
36802
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
36282
36803
|
for (const entry of entries) {
|
|
36283
36804
|
if (!entry.isDirectory())
|
|
36284
36805
|
continue;
|
|
36285
|
-
const skillMdPath =
|
|
36286
|
-
if (!
|
|
36806
|
+
const skillMdPath = join9(dir, entry.name, "SKILL.md");
|
|
36807
|
+
if (!existsSync6(skillMdPath))
|
|
36287
36808
|
continue;
|
|
36288
36809
|
let content;
|
|
36289
36810
|
try {
|
|
36290
|
-
content =
|
|
36811
|
+
content = readFileSync6(skillMdPath, "utf-8");
|
|
36291
36812
|
} catch {
|
|
36292
36813
|
continue;
|
|
36293
36814
|
}
|
|
@@ -36302,6 +36823,7 @@ function discoverSkillsInDir(dir, source = "custom") {
|
|
|
36302
36823
|
category: fm.category || "Development Tools",
|
|
36303
36824
|
tags: fm.tags || [],
|
|
36304
36825
|
...fm.kind ? { kind: fm.kind } : {},
|
|
36826
|
+
...isHostedMetadataSkillDir(join9(dir, entry.name)) ? { serverOwned: true } : {},
|
|
36305
36827
|
source
|
|
36306
36828
|
});
|
|
36307
36829
|
}
|
|
@@ -36310,20 +36832,20 @@ function discoverSkillsInDir(dir, source = "custom") {
|
|
|
36310
36832
|
}
|
|
36311
36833
|
function findExtensionSkillPath(name) {
|
|
36312
36834
|
const config = loadConfig4();
|
|
36313
|
-
if (!config.extensionsDir || !
|
|
36835
|
+
if (!config.extensionsDir || !existsSync6(config.extensionsDir))
|
|
36314
36836
|
return null;
|
|
36315
36837
|
try {
|
|
36316
|
-
const entries =
|
|
36838
|
+
const entries = readdirSync5(config.extensionsDir, { withFileTypes: true });
|
|
36317
36839
|
for (const entry of entries) {
|
|
36318
36840
|
if (!entry.isDirectory())
|
|
36319
36841
|
continue;
|
|
36320
|
-
const skillDir =
|
|
36321
|
-
const skillMdPath =
|
|
36322
|
-
if (!
|
|
36842
|
+
const skillDir = join9(config.extensionsDir, entry.name);
|
|
36843
|
+
const skillMdPath = join9(skillDir, "SKILL.md");
|
|
36844
|
+
if (!existsSync6(skillMdPath))
|
|
36323
36845
|
continue;
|
|
36324
36846
|
let content;
|
|
36325
36847
|
try {
|
|
36326
|
-
content =
|
|
36848
|
+
content = readFileSync6(skillMdPath, "utf-8");
|
|
36327
36849
|
} catch {
|
|
36328
36850
|
continue;
|
|
36329
36851
|
}
|
|
@@ -36356,7 +36878,7 @@ function loadRegistry(cwd) {
|
|
|
36356
36878
|
const official = SKILLS.map((s2) => ({ ...s2, source: "official" }));
|
|
36357
36879
|
const extensions = config.extensionsDir ? discoverSkillsInDir(config.extensionsDir, "extension") : [];
|
|
36358
36880
|
const portableCustom = listPortableSkillMetas();
|
|
36359
|
-
const legacyCustom = discoverSkillsInDir(
|
|
36881
|
+
const legacyCustom = discoverSkillsInDir(join9(dataDir, "custom"));
|
|
36360
36882
|
const globalCustom = mergeCustomSkills([...legacyCustom, ...portableCustom]);
|
|
36361
36883
|
registryCache = mergeSkillRegistryLists(official, extensions, globalCustom);
|
|
36362
36884
|
registryCacheTime = now;
|
|
@@ -36376,12 +36898,12 @@ function mergeCustomSkills(skills) {
|
|
|
36376
36898
|
}
|
|
36377
36899
|
|
|
36378
36900
|
// src/lib/skillinfo.ts
|
|
36379
|
-
import { existsSync as
|
|
36380
|
-
import { join as
|
|
36901
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
36902
|
+
import { join as join11 } from "path";
|
|
36381
36903
|
|
|
36382
36904
|
// src/lib/installer.ts
|
|
36383
|
-
import { existsSync as
|
|
36384
|
-
import { dirname as
|
|
36905
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, rmSync as rmSync2 } from "fs";
|
|
36906
|
+
import { dirname as dirname8, join as join10 } from "path";
|
|
36385
36907
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
36386
36908
|
// src/lib/utils.ts
|
|
36387
36909
|
function normalizeSkillName(name) {
|
|
@@ -36389,16 +36911,16 @@ function normalizeSkillName(name) {
|
|
|
36389
36911
|
}
|
|
36390
36912
|
|
|
36391
36913
|
// src/lib/installer.ts
|
|
36392
|
-
var __dirname2 =
|
|
36914
|
+
var __dirname2 = dirname8(fileURLToPath2(import.meta.url));
|
|
36393
36915
|
function findSkillsDir() {
|
|
36394
36916
|
let dir = __dirname2;
|
|
36395
36917
|
for (let i3 = 0;i3 < 5; i3++) {
|
|
36396
|
-
const candidate =
|
|
36397
|
-
if (
|
|
36918
|
+
const candidate = join10(dir, "skills");
|
|
36919
|
+
if (existsSync7(candidate) && !dir.includes(".skills"))
|
|
36398
36920
|
return candidate;
|
|
36399
|
-
dir =
|
|
36921
|
+
dir = dirname8(dir);
|
|
36400
36922
|
}
|
|
36401
|
-
return
|
|
36923
|
+
return join10(__dirname2, "..", "skills");
|
|
36402
36924
|
}
|
|
36403
36925
|
var SKILLS_DIR = findSkillsDir();
|
|
36404
36926
|
function getSkillPath(name) {
|
|
@@ -36406,13 +36928,13 @@ function getSkillPath(name) {
|
|
|
36406
36928
|
const portable = findPortableSkill(skillName);
|
|
36407
36929
|
if (portable)
|
|
36408
36930
|
return portable.path;
|
|
36409
|
-
const legacyCustomPath =
|
|
36410
|
-
if (
|
|
36931
|
+
const legacyCustomPath = join10(getDataDir(), "custom", skillName);
|
|
36932
|
+
if (existsSync7(legacyCustomPath))
|
|
36411
36933
|
return legacyCustomPath;
|
|
36412
36934
|
const extensionPath = findExtensionSkillPath(skillName);
|
|
36413
36935
|
if (extensionPath)
|
|
36414
36936
|
return extensionPath;
|
|
36415
|
-
return
|
|
36937
|
+
return join10(SKILLS_DIR, skillName);
|
|
36416
36938
|
}
|
|
36417
36939
|
function getCanonicalSkillName(name) {
|
|
36418
36940
|
return getSkill(name)?.name ?? resolveSkillAlias(normalizeSkillSlug(name));
|
|
@@ -36421,18 +36943,18 @@ function getCanonicalSkillName(name) {
|
|
|
36421
36943
|
// src/lib/skillinfo.ts
|
|
36422
36944
|
function getSkillDocs(name) {
|
|
36423
36945
|
const skillPath = getSkillPath(name);
|
|
36424
|
-
if (!
|
|
36946
|
+
if (!existsSync8(skillPath))
|
|
36425
36947
|
return null;
|
|
36426
36948
|
return {
|
|
36427
|
-
skillMd: readIfExists(
|
|
36428
|
-
readme: readIfExists(
|
|
36429
|
-
claudeMd: readIfExists(
|
|
36949
|
+
skillMd: readIfExists(join11(skillPath, "SKILL.md")),
|
|
36950
|
+
readme: readIfExists(join11(skillPath, "README.md")),
|
|
36951
|
+
claudeMd: readIfExists(join11(skillPath, "CLAUDE.md"))
|
|
36430
36952
|
};
|
|
36431
36953
|
}
|
|
36432
36954
|
function readIfExists(path) {
|
|
36433
36955
|
try {
|
|
36434
|
-
if (
|
|
36435
|
-
return
|
|
36956
|
+
if (existsSync8(path)) {
|
|
36957
|
+
return readFileSync8(path, "utf-8");
|
|
36436
36958
|
}
|
|
36437
36959
|
} catch {}
|
|
36438
36960
|
return null;
|
|
@@ -36473,7 +36995,7 @@ function getServerSkillMd(slug) {
|
|
|
36473
36995
|
const path = resolve(skillsDir, name, "SKILL.md");
|
|
36474
36996
|
if (!isInsideDir(skillsDir, path))
|
|
36475
36997
|
return null;
|
|
36476
|
-
return
|
|
36998
|
+
return existsSync9(path) ? readFileSync9(path, "utf8") : null;
|
|
36477
36999
|
}
|
|
36478
37000
|
|
|
36479
37001
|
// src/server/skills-api.ts
|
|
@@ -36726,7 +37248,15 @@ async function storePublishedSkill(store, artifactStorage, principal, parsed, ex
|
|
|
36726
37248
|
const placement = await artifactStorage.putBundle(principal.orgId, input.bundle.sha256, parsed.bundleBytes, input.bundle.contentType);
|
|
36727
37249
|
input = { ...input, bundle: { ...input.bundle, ...placement } };
|
|
36728
37250
|
}
|
|
36729
|
-
|
|
37251
|
+
let record;
|
|
37252
|
+
try {
|
|
37253
|
+
record = await store.publishSkill(input);
|
|
37254
|
+
} catch (error) {
|
|
37255
|
+
if (input.bundle?.sha256) {
|
|
37256
|
+
await discardCollectedObject(store, artifactStorage, principal, input.bundle.sha256);
|
|
37257
|
+
}
|
|
37258
|
+
throw error;
|
|
37259
|
+
}
|
|
36730
37260
|
if (superseded && superseded !== record.bundleSha256) {
|
|
36731
37261
|
await discardCollectedObject(store, artifactStorage, principal, superseded);
|
|
36732
37262
|
}
|
|
@@ -36798,7 +37328,7 @@ function buildPublishInput(manifest) {
|
|
|
36798
37328
|
if (skillMd && byteLength(skillMd) > MAX_SKILL_MD_BYTES) {
|
|
36799
37329
|
throw new SkillRequestError(413, "SKILL_MD_TOO_LARGE", `skillMd exceeds ${MAX_SKILL_MD_BYTES} bytes`);
|
|
36800
37330
|
}
|
|
36801
|
-
const kindValue = optionalString(manifest.kind) ?? "
|
|
37331
|
+
const kindValue = optionalString(manifest.kind) ?? "instruction";
|
|
36802
37332
|
if (kindValue !== "executable" && kindValue !== "instruction") {
|
|
36803
37333
|
throw new SkillRequestError(400, "INVALID_KIND", "`kind` must be 'executable' or 'instruction'");
|
|
36804
37334
|
}
|
|
@@ -36860,6 +37390,7 @@ async function createSkillsFetchHandler(options = {}) {
|
|
|
36860
37390
|
bootstrapApiKey: config.bootstrapApiKey
|
|
36861
37391
|
});
|
|
36862
37392
|
assertDurableStore(store, config);
|
|
37393
|
+
const governanceStore = options.governanceStore ?? await createGovernanceStore(config.databaseUrl);
|
|
36863
37394
|
const artifactStorage = new ArtifactStorage({
|
|
36864
37395
|
bucket: config.artifactBucket,
|
|
36865
37396
|
prefix: config.artifactPrefix
|
|
@@ -36882,7 +37413,7 @@ async function createSkillsFetchHandler(options = {}) {
|
|
|
36882
37413
|
return json(identityPayload(principal));
|
|
36883
37414
|
}
|
|
36884
37415
|
if (segments[0] === "api" && segments[1] === "v1") {
|
|
36885
|
-
return await handleApiV1(store, principal, request, segments.slice(2), config, artifactStorage);
|
|
37416
|
+
return await handleApiV1(store, governanceStore, principal, request, segments.slice(2), config, artifactStorage);
|
|
36886
37417
|
}
|
|
36887
37418
|
}
|
|
36888
37419
|
return json({ error: "not found", code: "NOT_FOUND" }, { status: 404 });
|
|
@@ -36911,7 +37442,7 @@ function skillsServeLimits(config) {
|
|
|
36911
37442
|
return { maxRequestBodySize: Math.max(config.skillBundleLimitBytes, config.requestBodyLimitBytes) + BODY_LIMIT_HEADROOM_BYTES };
|
|
36912
37443
|
}
|
|
36913
37444
|
var BODY_LIMIT_HEADROOM_BYTES = 1e6;
|
|
36914
|
-
async function handleApiV1(store, principal, request, parts, config, artifactStorage) {
|
|
37445
|
+
async function handleApiV1(store, governanceStore, principal, request, parts, config, artifactStorage) {
|
|
36915
37446
|
const [resource, id, subresource, childId] = parts;
|
|
36916
37447
|
if (parts.some(segmentEscapesPath)) {
|
|
36917
37448
|
return json({ error: "invalid path segment", code: "INVALID_PATH" }, { status: 400 });
|
|
@@ -37070,8 +37601,18 @@ async function handleApiV1(store, principal, request, parts, config, artifactSto
|
|
|
37070
37601
|
const run = await store.getRun(principal, id);
|
|
37071
37602
|
if (!run)
|
|
37072
37603
|
return json({ error: "run not found", code: "RUN_NOT_FOUND" }, { status: 404 });
|
|
37073
|
-
|
|
37074
|
-
|
|
37604
|
+
try {
|
|
37605
|
+
const outcome = await createCancelService({ store, governanceStore, storage: artifactStorage }).cancel(principal, id, principal.email);
|
|
37606
|
+
return json(runPayload(outcome.run));
|
|
37607
|
+
} catch (error) {
|
|
37608
|
+
if (error instanceof GovernanceError && error.code === GOVERNANCE_ERROR_CODES.STALE_LEASE_GENERATION) {
|
|
37609
|
+
return json({ error: error.message, code: error.code }, { status: 409 });
|
|
37610
|
+
}
|
|
37611
|
+
if (error instanceof StaleLeaseGenerationError) {
|
|
37612
|
+
return json({ error: error.message, code: GOVERNANCE_ERROR_CODES.STALE_LEASE_GENERATION }, { status: 409 });
|
|
37613
|
+
}
|
|
37614
|
+
throw error;
|
|
37615
|
+
}
|
|
37075
37616
|
}
|
|
37076
37617
|
}
|
|
37077
37618
|
return json({ error: "not found", code: "NOT_FOUND" }, { status: 404 });
|
|
@@ -37161,6 +37702,24 @@ function clampInt(value, fallback, max) {
|
|
|
37161
37702
|
}
|
|
37162
37703
|
|
|
37163
37704
|
// src/server/index.ts
|
|
37705
|
+
var EARLY_ARGV = process.argv.slice(2);
|
|
37706
|
+
if (EARLY_ARGV.includes("--version") || EARLY_ARGV.includes("-V")) {
|
|
37707
|
+
console.log(package_default.version);
|
|
37708
|
+
process.exit(0);
|
|
37709
|
+
}
|
|
37710
|
+
if (EARLY_ARGV.includes("--help") || EARLY_ARGV.includes("-h")) {
|
|
37711
|
+
console.log(`Usage: skills-server [options]
|
|
37712
|
+
|
|
37713
|
+
Runs the @hasna/skills HTTP API.
|
|
37714
|
+
|
|
37715
|
+
Options:
|
|
37716
|
+
-V, --version output the version number
|
|
37717
|
+
-h, --help display help for command
|
|
37718
|
+
|
|
37719
|
+
Environment:
|
|
37720
|
+
SKILLS_PORT Listen port (default: 8787)`);
|
|
37721
|
+
process.exit(0);
|
|
37722
|
+
}
|
|
37164
37723
|
var config = resolveServerConfig();
|
|
37165
37724
|
var server = await startSkillsServer({ config });
|
|
37166
37725
|
console.log(`skills API listening on http://${config.host}:${server.port}`);
|