@hasna/skills 0.1.64 → 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 +33 -16
- package/bin/index.js +594 -509
- package/bin/mcp.js +277 -237
- package/bin/server.js +789 -232
- package/bin/worker.js +178 -148
- package/dist/index.d.ts +1 -1
- package/dist/index.js +419 -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/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 +1 -1
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;
|
|
@@ -31995,7 +32108,7 @@ var WriteGetObjectResponse$ = [
|
|
|
31995
32108
|
class CreateSessionCommand extends command(_ep4, _mw0, "CreateSession", CreateSession$) {
|
|
31996
32109
|
}
|
|
31997
32110
|
// ../../node_modules/.bun/@aws-sdk+client-s3@3.1106.0/node_modules/@aws-sdk/client-s3/package.json
|
|
31998
|
-
var
|
|
32111
|
+
var package_default2 = {
|
|
31999
32112
|
name: "@aws-sdk/client-s3",
|
|
32000
32113
|
version: "3.1106.0",
|
|
32001
32114
|
description: "AWS SDK for JavaScript S3 Client for Node.js, Browser and React Native",
|
|
@@ -32545,7 +32658,7 @@ var getRuntimeConfig3 = (config) => {
|
|
|
32545
32658
|
authSchemePreference: config?.authSchemePreference ?? import_config28.loadConfig(import_httpAuthSchemes3.NODE_AUTH_SCHEME_PREFERENCE_OPTIONS, loaderConfig),
|
|
32546
32659
|
bodyLengthChecker: config?.bodyLengthChecker ?? import_serde4.calculateBodyLength,
|
|
32547
32660
|
credentialDefaultProvider: config?.credentialDefaultProvider ?? defaultProvider,
|
|
32548
|
-
defaultUserAgentProvider: config?.defaultUserAgentProvider ?? import_client19.createDefaultUserAgentProvider({ serviceId: clientSharedValues.serviceId, clientVersion:
|
|
32661
|
+
defaultUserAgentProvider: config?.defaultUserAgentProvider ?? import_client19.createDefaultUserAgentProvider({ serviceId: clientSharedValues.serviceId, clientVersion: package_default2.version }),
|
|
32549
32662
|
disableS3ExpressSessionAuth: config?.disableS3ExpressSessionAuth ?? import_config28.loadConfig($NODE_DISABLE_S3_EXPRESS_SESSION_AUTH_OPTIONS, loaderConfig),
|
|
32550
32663
|
eventStreamSerdeProvider: config?.eventStreamSerdeProvider ?? import_event_streams.eventStreamSerdeProvider,
|
|
32551
32664
|
maxAttempts: config?.maxAttempts ?? import_config28.loadConfig(import_retry3.NODE_MAX_ATTEMPT_CONFIG_OPTIONS, config),
|
|
@@ -32857,36 +32970,144 @@ function concat(chunks) {
|
|
|
32857
32970
|
return merged;
|
|
32858
32971
|
}
|
|
32859
32972
|
|
|
32860
|
-
// src/
|
|
32861
|
-
|
|
32862
|
-
|
|
32863
|
-
|
|
32864
|
-
}
|
|
32865
|
-
|
|
32866
|
-
|
|
32867
|
-
|
|
32868
|
-
|
|
32869
|
-
|
|
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
|
+
}
|
|
32870
33020
|
}
|
|
32871
|
-
|
|
32872
|
-
|
|
32873
|
-
|
|
32874
|
-
|
|
32875
|
-
|
|
33021
|
+
function runPointersOf(run) {
|
|
33022
|
+
return {
|
|
33023
|
+
runId: run.id,
|
|
33024
|
+
attemptId: run.id,
|
|
33025
|
+
leaseGeneration: run.leaseGeneration,
|
|
33026
|
+
correlationId: run.correlationId
|
|
33027
|
+
};
|
|
32876
33028
|
}
|
|
32877
|
-
|
|
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;
|
|
32878
33035
|
return {
|
|
32879
|
-
|
|
32880
|
-
|
|
32881
|
-
|
|
32882
|
-
|
|
32883
|
-
|
|
32884
|
-
|
|
32885
|
-
|
|
32886
|
-
|
|
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
|
+
}
|
|
32887
33093
|
};
|
|
32888
33094
|
}
|
|
32889
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
|
+
|
|
32890
33111
|
// src/lib/retired-settings.ts
|
|
32891
33112
|
var RETIRED_ENV_SUFFIXES = ["_STORAGE_MODE", "_DEPLOYMENT_MODE", "_CLOUD_MODE"];
|
|
32892
33113
|
var RETIRED_CONFIG_KEYS = {
|
|
@@ -32933,54 +33154,7 @@ function assertNoRetiredConfigKeys(config, source) {
|
|
|
32933
33154
|
}
|
|
32934
33155
|
}
|
|
32935
33156
|
|
|
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
33157
|
// 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
33158
|
var ENUM_KEYS = {
|
|
32985
33159
|
defaultAgent: ["claude", "codex", "gemini", "pi", "opencode", "all"],
|
|
32986
33160
|
defaultScope: ["global", "project"],
|
|
@@ -33160,39 +33334,78 @@ function looksLikeSqlitePath(value) {
|
|
|
33160
33334
|
return SQLITE_EXTENSIONS.some((extension) => value.toLowerCase().endsWith(extension));
|
|
33161
33335
|
}
|
|
33162
33336
|
|
|
33163
|
-
// src/server/
|
|
33164
|
-
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";
|
|
33165
33342
|
|
|
33166
|
-
// src/server/
|
|
33167
|
-
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
|
+
}
|
|
33168
33372
|
|
|
33169
|
-
// src/server/
|
|
33170
|
-
|
|
33171
|
-
|
|
33172
|
-
|
|
33173
|
-
|
|
33174
|
-
|
|
33175
|
-
|
|
33176
|
-
|
|
33177
|
-
|
|
33178
|
-
|
|
33179
|
-
|
|
33180
|
-
|
|
33181
|
-
|
|
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
|
+
}
|
|
33182
33390
|
}
|
|
33391
|
+
return null;
|
|
33183
33392
|
}
|
|
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;
|
|
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.`);
|
|
33195
33396
|
}
|
|
33397
|
+
const dir = join4(root3, dialect);
|
|
33398
|
+
if (!existsSync2(dir)) {
|
|
33399
|
+
throw new Error(`migrations directory not found: ${dir}`);
|
|
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;
|
|
33196
33409
|
}
|
|
33197
33410
|
|
|
33198
33411
|
// src/server/rows.ts
|
|
@@ -33340,6 +33553,35 @@ function dateString(value) {
|
|
|
33340
33553
|
return String(value);
|
|
33341
33554
|
}
|
|
33342
33555
|
|
|
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;
|
|
33569
|
+
}
|
|
33570
|
+
}
|
|
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;
|
|
33582
|
+
}
|
|
33583
|
+
}
|
|
33584
|
+
|
|
33343
33585
|
// src/lib/revision.ts
|
|
33344
33586
|
import { createHash as createHash4 } from "crypto";
|
|
33345
33587
|
var REVISION_ID_PATTERN = /^[0-9a-f]{64}$/;
|
|
@@ -33363,50 +33605,6 @@ function revisionIdOfRecord(record) {
|
|
|
33363
33605
|
return revisionIdOf(record);
|
|
33364
33606
|
}
|
|
33365
33607
|
|
|
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
33608
|
// src/server/sqlite-store.ts
|
|
33411
33609
|
var CLAIM_ATTEMPTS = 8;
|
|
33412
33610
|
var CLAIMABLE_STATUSES = ["queued", "retrying"];
|
|
@@ -33997,7 +34195,301 @@ function parseScopes(value) {
|
|
|
33997
34195
|
}
|
|
33998
34196
|
}
|
|
33999
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
|
+
|
|
34000
34491
|
// src/server/store.ts
|
|
34492
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
34001
34493
|
function recordFieldsOf(input, carriedBundle, carriedSkillMd) {
|
|
34002
34494
|
return {
|
|
34003
34495
|
slug: input.slug,
|
|
@@ -34085,7 +34577,7 @@ class MemorySkillsStore {
|
|
|
34085
34577
|
input: input.input,
|
|
34086
34578
|
args: input.args,
|
|
34087
34579
|
...idemKey ? { idempotencyKey: idemKey } : {},
|
|
34088
|
-
correlationId:
|
|
34580
|
+
correlationId: randomUUID4(),
|
|
34089
34581
|
costCents: 0,
|
|
34090
34582
|
leaseGeneration: 0,
|
|
34091
34583
|
createdAt: now
|
|
@@ -34400,20 +34892,20 @@ class PostgresSkillsStore {
|
|
|
34400
34892
|
});
|
|
34401
34893
|
}
|
|
34402
34894
|
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
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();
|
|
34414
34906
|
const rows = await tx`
|
|
34415
34907
|
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}, ${
|
|
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()})
|
|
34417
34909
|
RETURNING *
|
|
34418
34910
|
`;
|
|
34419
34911
|
return rowToRun(rows[0]);
|
|
@@ -34952,7 +35444,9 @@ async function completeRun(store, run, preview) {
|
|
|
34952
35444
|
return next ?? run;
|
|
34953
35445
|
}
|
|
34954
35446
|
async function failRun(store, run, code, message) {
|
|
34955
|
-
|
|
35447
|
+
try {
|
|
35448
|
+
await store.appendLog(run.id, run.orgId, "error", message);
|
|
35449
|
+
} catch {}
|
|
34956
35450
|
const next = await fencedTransition(store, run, {
|
|
34957
35451
|
status: "failed",
|
|
34958
35452
|
errorCode: code,
|
|
@@ -34966,7 +35460,9 @@ async function fencedTransition(store, run, patch) {
|
|
|
34966
35460
|
return store.updateRun(run.id, patch);
|
|
34967
35461
|
const next = await store.transitionRun(run.id, patch, run.leaseGeneration);
|
|
34968
35462
|
if (!next) {
|
|
34969
|
-
|
|
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 {}
|
|
34970
35466
|
}
|
|
34971
35467
|
return next;
|
|
34972
35468
|
}
|
|
@@ -35048,25 +35544,26 @@ function mergeSkillRegistryLists(...groups) {
|
|
|
35048
35544
|
}
|
|
35049
35545
|
|
|
35050
35546
|
// src/server/registry.ts
|
|
35051
|
-
import { existsSync as
|
|
35547
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
35052
35548
|
import { resolve, sep } from "path";
|
|
35053
35549
|
|
|
35054
35550
|
// src/lib/registry.ts
|
|
35055
|
-
import { existsSync as
|
|
35056
|
-
import { join as
|
|
35551
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync5 } from "fs";
|
|
35552
|
+
import { join as join9 } from "path";
|
|
35057
35553
|
|
|
35058
35554
|
// src/lib/portable-skills.ts
|
|
35059
35555
|
import {
|
|
35060
35556
|
cpSync as cpSync2,
|
|
35061
|
-
existsSync as
|
|
35062
|
-
mkdirSync as
|
|
35063
|
-
|
|
35557
|
+
existsSync as existsSync5,
|
|
35558
|
+
mkdirSync as mkdirSync5,
|
|
35559
|
+
mkdtempSync,
|
|
35560
|
+
readdirSync as readdirSync4,
|
|
35064
35561
|
renameSync,
|
|
35065
35562
|
rmSync,
|
|
35066
|
-
statSync as
|
|
35563
|
+
statSync as statSync4,
|
|
35067
35564
|
writeFileSync as writeFileSync3
|
|
35068
35565
|
} from "fs";
|
|
35069
|
-
import { basename as basename2, dirname as
|
|
35566
|
+
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join8, normalize } from "path";
|
|
35070
35567
|
|
|
35071
35568
|
// src/lib/registry-data/development-tools.ts
|
|
35072
35569
|
var DEVELOPMENT_TOOLS_SKILLS = [
|
|
@@ -35778,8 +36275,29 @@ var SKILLS = [
|
|
|
35778
36275
|
];
|
|
35779
36276
|
|
|
35780
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";
|
|
35781
36280
|
var HOSTED_RUNTIMES = new Set(["hosted"]);
|
|
35782
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
|
+
}
|
|
35783
36301
|
var HOSTED_METADATA_SET_EMPTY_ERROR = [
|
|
35784
36302
|
"The hosted metadata skill set is empty, but the packaging guards that depend on it",
|
|
35785
36303
|
"only mean anything while it is non-empty: an empty set makes every one of them pass",
|
|
@@ -35901,14 +36419,14 @@ var PORTABLE_SKILL_DEFAULT_VERSION = "0.1.0";
|
|
|
35901
36419
|
// src/lib/portable-skills-files.ts
|
|
35902
36420
|
import {
|
|
35903
36421
|
cpSync,
|
|
35904
|
-
existsSync as
|
|
36422
|
+
existsSync as existsSync4,
|
|
35905
36423
|
lstatSync,
|
|
35906
|
-
mkdirSync as
|
|
35907
|
-
readFileSync as
|
|
36424
|
+
mkdirSync as mkdirSync4,
|
|
36425
|
+
readFileSync as readFileSync5,
|
|
35908
36426
|
realpathSync,
|
|
35909
36427
|
writeFileSync as writeFileSync2
|
|
35910
36428
|
} from "fs";
|
|
35911
|
-
import { basename, dirname as
|
|
36429
|
+
import { basename, dirname as dirname6, join as join7, relative } from "path";
|
|
35912
36430
|
var ANY_SEGMENT_COPY_EXCLUDES = new Set([
|
|
35913
36431
|
".git",
|
|
35914
36432
|
".DS_Store",
|
|
@@ -35936,12 +36454,12 @@ function normalizePortableSkillName(name) {
|
|
|
35936
36454
|
return normalized;
|
|
35937
36455
|
}
|
|
35938
36456
|
function readPortableSkillManifest(skillPath, fallbackName = basename(skillPath)) {
|
|
35939
|
-
const skillJsonPath =
|
|
35940
|
-
const skillMdPath =
|
|
35941
|
-
const pkgPath =
|
|
35942
|
-
const jsonManifest =
|
|
35943
|
-
const frontmatter =
|
|
35944
|
-
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;
|
|
35945
36463
|
const name = normalizePortableSkillName(stringField(jsonManifest, "name") ?? frontmatter?.name ?? stringValue(pkg?.name) ?? fallbackName);
|
|
35946
36464
|
const description = stringField(jsonManifest, "description") ?? frontmatter?.description ?? stringValue(pkg?.description) ?? `${name} skill`;
|
|
35947
36465
|
const version2 = stringField(jsonManifest, "version") ?? frontmatter?.version ?? stringValue(pkg?.version) ?? PORTABLE_SKILL_DEFAULT_VERSION;
|
|
@@ -36068,7 +36586,7 @@ function inferPackageCommands(pkg, fallbackName) {
|
|
|
36068
36586
|
return;
|
|
36069
36587
|
}
|
|
36070
36588
|
function readJsonObject(path) {
|
|
36071
|
-
const parsed = JSON.parse(
|
|
36589
|
+
const parsed = JSON.parse(readFileSync5(path, "utf-8"));
|
|
36072
36590
|
if (!isRecord(parsed))
|
|
36073
36591
|
throw new Error(`${basename(path)} must contain a JSON object`);
|
|
36074
36592
|
return parsed;
|
|
@@ -36097,36 +36615,36 @@ var LEGACY_CUSTOM_DIRNAME = "custom";
|
|
|
36097
36615
|
function getPortableSkillsRoot(options = {}) {
|
|
36098
36616
|
if (options.rootDir)
|
|
36099
36617
|
return options.rootDir;
|
|
36100
|
-
const appDir = options.homeDir ?
|
|
36101
|
-
const cache3 =
|
|
36618
|
+
const appDir = options.homeDir ? join8(options.homeDir, ".hasna", "skills") : getDataDir();
|
|
36619
|
+
const cache3 = join8(appDir, SKILLS_CACHE_DIRNAME);
|
|
36102
36620
|
if (isOwnerLayoutMigrated(appDir) && safeIsDirectory(cache3))
|
|
36103
36621
|
return cache3;
|
|
36104
|
-
const installed =
|
|
36622
|
+
const installed = join8(appDir, INSTALLED_SKILLS_DIRNAME);
|
|
36105
36623
|
migrateLegacySkillLayout(appDir, installed);
|
|
36106
36624
|
return installed;
|
|
36107
36625
|
}
|
|
36108
36626
|
function looksLikeSkillDirectory(path) {
|
|
36109
36627
|
if (!safeIsDirectory(path))
|
|
36110
36628
|
return false;
|
|
36111
|
-
return
|
|
36629
|
+
return existsSync5(join8(path, "SKILL.md")) || existsSync5(join8(path, "skill.json")) || existsSync5(join8(path, "package.json"));
|
|
36112
36630
|
}
|
|
36113
36631
|
function migrateLegacySkillLayout(appDir, installed) {
|
|
36114
36632
|
if (!safeIsDirectory(appDir))
|
|
36115
36633
|
return;
|
|
36116
36634
|
const candidates = [];
|
|
36117
36635
|
try {
|
|
36118
|
-
for (const entry of
|
|
36636
|
+
for (const entry of readdirSync4(appDir)) {
|
|
36119
36637
|
if (entry.startsWith(".") || entry === INSTALLED_SKILLS_DIRNAME)
|
|
36120
36638
|
continue;
|
|
36121
|
-
const path =
|
|
36639
|
+
const path = join8(appDir, entry);
|
|
36122
36640
|
if (entry === LEGACY_CUSTOM_DIRNAME) {
|
|
36123
36641
|
if (!safeIsDirectory(path))
|
|
36124
36642
|
continue;
|
|
36125
36643
|
try {
|
|
36126
|
-
for (const nested of
|
|
36644
|
+
for (const nested of readdirSync4(path)) {
|
|
36127
36645
|
if (nested.startsWith("."))
|
|
36128
36646
|
continue;
|
|
36129
|
-
const nestedPath =
|
|
36647
|
+
const nestedPath = join8(path, nested);
|
|
36130
36648
|
if (looksLikeSkillDirectory(nestedPath))
|
|
36131
36649
|
candidates.push({ from: nestedPath, name: nested });
|
|
36132
36650
|
}
|
|
@@ -36140,10 +36658,10 @@ function migrateLegacySkillLayout(appDir, installed) {
|
|
|
36140
36658
|
return;
|
|
36141
36659
|
}
|
|
36142
36660
|
for (const { from, name } of candidates) {
|
|
36143
|
-
const target =
|
|
36144
|
-
if (
|
|
36661
|
+
const target = join8(installed, name);
|
|
36662
|
+
if (existsSync5(target))
|
|
36145
36663
|
continue;
|
|
36146
|
-
const staging =
|
|
36664
|
+
const staging = join8(installed, `.migrating-${name}-${process.pid}`);
|
|
36147
36665
|
try {
|
|
36148
36666
|
rmSync(staging, { recursive: true, force: true });
|
|
36149
36667
|
cpSync2(from, staging, { recursive: true, errorOnExist: false });
|
|
@@ -36156,7 +36674,7 @@ function migrateLegacySkillLayout(appDir, installed) {
|
|
|
36156
36674
|
}
|
|
36157
36675
|
}
|
|
36158
36676
|
function getPortableSkillPath(name, options = {}) {
|
|
36159
|
-
return
|
|
36677
|
+
return join8(getPortableSkillsRoot(options), normalizePortableSkillName(name));
|
|
36160
36678
|
}
|
|
36161
36679
|
function findPortableSkill(name, options = {}) {
|
|
36162
36680
|
let normalized;
|
|
@@ -36166,7 +36684,7 @@ function findPortableSkill(name, options = {}) {
|
|
|
36166
36684
|
return null;
|
|
36167
36685
|
}
|
|
36168
36686
|
const path = getPortableSkillPath(normalized, options);
|
|
36169
|
-
if (!
|
|
36687
|
+
if (!existsSync5(path) || !statSync4(path).isDirectory())
|
|
36170
36688
|
return null;
|
|
36171
36689
|
try {
|
|
36172
36690
|
return summarizePortableSkill(path, normalized);
|
|
@@ -36179,10 +36697,10 @@ function listPortableSkills(options = {}) {
|
|
|
36179
36697
|
if (!safeIsDirectory(root3))
|
|
36180
36698
|
return [];
|
|
36181
36699
|
const skills = [];
|
|
36182
|
-
for (const entry of
|
|
36700
|
+
for (const entry of readdirSync4(root3).sort()) {
|
|
36183
36701
|
if (entry.startsWith("."))
|
|
36184
36702
|
continue;
|
|
36185
|
-
const path =
|
|
36703
|
+
const path = join8(root3, entry);
|
|
36186
36704
|
if (!safeIsDirectory(path))
|
|
36187
36705
|
continue;
|
|
36188
36706
|
try {
|
|
@@ -36204,7 +36722,8 @@ function listPortableSkillMetas(options = {}) {
|
|
|
36204
36722
|
tags: manifest.tags || ["custom"],
|
|
36205
36723
|
version: skill.version,
|
|
36206
36724
|
...manifest.kind ? { kind: manifest.kind } : {},
|
|
36207
|
-
source: "custom"
|
|
36725
|
+
source: "custom",
|
|
36726
|
+
...isHostedMetadataSkillDir(skill.path) ? { serverOwned: true } : {}
|
|
36208
36727
|
};
|
|
36209
36728
|
});
|
|
36210
36729
|
}
|
|
@@ -36224,7 +36743,7 @@ function summarizePortableSkill(skillPath, fallbackName) {
|
|
|
36224
36743
|
}
|
|
36225
36744
|
function safeIsDirectory(path) {
|
|
36226
36745
|
try {
|
|
36227
|
-
return
|
|
36746
|
+
return statSync4(path).isDirectory();
|
|
36228
36747
|
} catch {
|
|
36229
36748
|
return false;
|
|
36230
36749
|
}
|
|
@@ -36274,20 +36793,20 @@ function parseSkillMdFrontmatter(content) {
|
|
|
36274
36793
|
return Object.keys(result).length > 0 ? result : null;
|
|
36275
36794
|
}
|
|
36276
36795
|
function discoverSkillsInDir(dir, source = "custom") {
|
|
36277
|
-
if (!
|
|
36796
|
+
if (!existsSync6(dir))
|
|
36278
36797
|
return [];
|
|
36279
36798
|
const result = [];
|
|
36280
36799
|
try {
|
|
36281
|
-
const entries =
|
|
36800
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
36282
36801
|
for (const entry of entries) {
|
|
36283
36802
|
if (!entry.isDirectory())
|
|
36284
36803
|
continue;
|
|
36285
|
-
const skillMdPath =
|
|
36286
|
-
if (!
|
|
36804
|
+
const skillMdPath = join9(dir, entry.name, "SKILL.md");
|
|
36805
|
+
if (!existsSync6(skillMdPath))
|
|
36287
36806
|
continue;
|
|
36288
36807
|
let content;
|
|
36289
36808
|
try {
|
|
36290
|
-
content =
|
|
36809
|
+
content = readFileSync6(skillMdPath, "utf-8");
|
|
36291
36810
|
} catch {
|
|
36292
36811
|
continue;
|
|
36293
36812
|
}
|
|
@@ -36302,6 +36821,7 @@ function discoverSkillsInDir(dir, source = "custom") {
|
|
|
36302
36821
|
category: fm.category || "Development Tools",
|
|
36303
36822
|
tags: fm.tags || [],
|
|
36304
36823
|
...fm.kind ? { kind: fm.kind } : {},
|
|
36824
|
+
...isHostedMetadataSkillDir(join9(dir, entry.name)) ? { serverOwned: true } : {},
|
|
36305
36825
|
source
|
|
36306
36826
|
});
|
|
36307
36827
|
}
|
|
@@ -36310,20 +36830,20 @@ function discoverSkillsInDir(dir, source = "custom") {
|
|
|
36310
36830
|
}
|
|
36311
36831
|
function findExtensionSkillPath(name) {
|
|
36312
36832
|
const config = loadConfig4();
|
|
36313
|
-
if (!config.extensionsDir || !
|
|
36833
|
+
if (!config.extensionsDir || !existsSync6(config.extensionsDir))
|
|
36314
36834
|
return null;
|
|
36315
36835
|
try {
|
|
36316
|
-
const entries =
|
|
36836
|
+
const entries = readdirSync5(config.extensionsDir, { withFileTypes: true });
|
|
36317
36837
|
for (const entry of entries) {
|
|
36318
36838
|
if (!entry.isDirectory())
|
|
36319
36839
|
continue;
|
|
36320
|
-
const skillDir =
|
|
36321
|
-
const skillMdPath =
|
|
36322
|
-
if (!
|
|
36840
|
+
const skillDir = join9(config.extensionsDir, entry.name);
|
|
36841
|
+
const skillMdPath = join9(skillDir, "SKILL.md");
|
|
36842
|
+
if (!existsSync6(skillMdPath))
|
|
36323
36843
|
continue;
|
|
36324
36844
|
let content;
|
|
36325
36845
|
try {
|
|
36326
|
-
content =
|
|
36846
|
+
content = readFileSync6(skillMdPath, "utf-8");
|
|
36327
36847
|
} catch {
|
|
36328
36848
|
continue;
|
|
36329
36849
|
}
|
|
@@ -36356,7 +36876,7 @@ function loadRegistry(cwd) {
|
|
|
36356
36876
|
const official = SKILLS.map((s2) => ({ ...s2, source: "official" }));
|
|
36357
36877
|
const extensions = config.extensionsDir ? discoverSkillsInDir(config.extensionsDir, "extension") : [];
|
|
36358
36878
|
const portableCustom = listPortableSkillMetas();
|
|
36359
|
-
const legacyCustom = discoverSkillsInDir(
|
|
36879
|
+
const legacyCustom = discoverSkillsInDir(join9(dataDir, "custom"));
|
|
36360
36880
|
const globalCustom = mergeCustomSkills([...legacyCustom, ...portableCustom]);
|
|
36361
36881
|
registryCache = mergeSkillRegistryLists(official, extensions, globalCustom);
|
|
36362
36882
|
registryCacheTime = now;
|
|
@@ -36376,12 +36896,12 @@ function mergeCustomSkills(skills) {
|
|
|
36376
36896
|
}
|
|
36377
36897
|
|
|
36378
36898
|
// src/lib/skillinfo.ts
|
|
36379
|
-
import { existsSync as
|
|
36380
|
-
import { join as
|
|
36899
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
36900
|
+
import { join as join11 } from "path";
|
|
36381
36901
|
|
|
36382
36902
|
// src/lib/installer.ts
|
|
36383
|
-
import { existsSync as
|
|
36384
|
-
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";
|
|
36385
36905
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
36386
36906
|
// src/lib/utils.ts
|
|
36387
36907
|
function normalizeSkillName(name) {
|
|
@@ -36389,16 +36909,16 @@ function normalizeSkillName(name) {
|
|
|
36389
36909
|
}
|
|
36390
36910
|
|
|
36391
36911
|
// src/lib/installer.ts
|
|
36392
|
-
var __dirname2 =
|
|
36912
|
+
var __dirname2 = dirname8(fileURLToPath2(import.meta.url));
|
|
36393
36913
|
function findSkillsDir() {
|
|
36394
36914
|
let dir = __dirname2;
|
|
36395
36915
|
for (let i3 = 0;i3 < 5; i3++) {
|
|
36396
|
-
const candidate =
|
|
36397
|
-
if (
|
|
36916
|
+
const candidate = join10(dir, "skills");
|
|
36917
|
+
if (existsSync7(candidate) && !dir.includes(".skills"))
|
|
36398
36918
|
return candidate;
|
|
36399
|
-
dir =
|
|
36919
|
+
dir = dirname8(dir);
|
|
36400
36920
|
}
|
|
36401
|
-
return
|
|
36921
|
+
return join10(__dirname2, "..", "skills");
|
|
36402
36922
|
}
|
|
36403
36923
|
var SKILLS_DIR = findSkillsDir();
|
|
36404
36924
|
function getSkillPath(name) {
|
|
@@ -36406,13 +36926,13 @@ function getSkillPath(name) {
|
|
|
36406
36926
|
const portable = findPortableSkill(skillName);
|
|
36407
36927
|
if (portable)
|
|
36408
36928
|
return portable.path;
|
|
36409
|
-
const legacyCustomPath =
|
|
36410
|
-
if (
|
|
36929
|
+
const legacyCustomPath = join10(getDataDir(), "custom", skillName);
|
|
36930
|
+
if (existsSync7(legacyCustomPath))
|
|
36411
36931
|
return legacyCustomPath;
|
|
36412
36932
|
const extensionPath = findExtensionSkillPath(skillName);
|
|
36413
36933
|
if (extensionPath)
|
|
36414
36934
|
return extensionPath;
|
|
36415
|
-
return
|
|
36935
|
+
return join10(SKILLS_DIR, skillName);
|
|
36416
36936
|
}
|
|
36417
36937
|
function getCanonicalSkillName(name) {
|
|
36418
36938
|
return getSkill(name)?.name ?? resolveSkillAlias(normalizeSkillSlug(name));
|
|
@@ -36421,18 +36941,18 @@ function getCanonicalSkillName(name) {
|
|
|
36421
36941
|
// src/lib/skillinfo.ts
|
|
36422
36942
|
function getSkillDocs(name) {
|
|
36423
36943
|
const skillPath = getSkillPath(name);
|
|
36424
|
-
if (!
|
|
36944
|
+
if (!existsSync8(skillPath))
|
|
36425
36945
|
return null;
|
|
36426
36946
|
return {
|
|
36427
|
-
skillMd: readIfExists(
|
|
36428
|
-
readme: readIfExists(
|
|
36429
|
-
claudeMd: readIfExists(
|
|
36947
|
+
skillMd: readIfExists(join11(skillPath, "SKILL.md")),
|
|
36948
|
+
readme: readIfExists(join11(skillPath, "README.md")),
|
|
36949
|
+
claudeMd: readIfExists(join11(skillPath, "CLAUDE.md"))
|
|
36430
36950
|
};
|
|
36431
36951
|
}
|
|
36432
36952
|
function readIfExists(path) {
|
|
36433
36953
|
try {
|
|
36434
|
-
if (
|
|
36435
|
-
return
|
|
36954
|
+
if (existsSync8(path)) {
|
|
36955
|
+
return readFileSync8(path, "utf-8");
|
|
36436
36956
|
}
|
|
36437
36957
|
} catch {}
|
|
36438
36958
|
return null;
|
|
@@ -36473,7 +36993,7 @@ function getServerSkillMd(slug) {
|
|
|
36473
36993
|
const path = resolve(skillsDir, name, "SKILL.md");
|
|
36474
36994
|
if (!isInsideDir(skillsDir, path))
|
|
36475
36995
|
return null;
|
|
36476
|
-
return
|
|
36996
|
+
return existsSync9(path) ? readFileSync9(path, "utf8") : null;
|
|
36477
36997
|
}
|
|
36478
36998
|
|
|
36479
36999
|
// src/server/skills-api.ts
|
|
@@ -36726,7 +37246,15 @@ async function storePublishedSkill(store, artifactStorage, principal, parsed, ex
|
|
|
36726
37246
|
const placement = await artifactStorage.putBundle(principal.orgId, input.bundle.sha256, parsed.bundleBytes, input.bundle.contentType);
|
|
36727
37247
|
input = { ...input, bundle: { ...input.bundle, ...placement } };
|
|
36728
37248
|
}
|
|
36729
|
-
|
|
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
|
+
}
|
|
36730
37258
|
if (superseded && superseded !== record.bundleSha256) {
|
|
36731
37259
|
await discardCollectedObject(store, artifactStorage, principal, superseded);
|
|
36732
37260
|
}
|
|
@@ -36798,7 +37326,7 @@ function buildPublishInput(manifest) {
|
|
|
36798
37326
|
if (skillMd && byteLength(skillMd) > MAX_SKILL_MD_BYTES) {
|
|
36799
37327
|
throw new SkillRequestError(413, "SKILL_MD_TOO_LARGE", `skillMd exceeds ${MAX_SKILL_MD_BYTES} bytes`);
|
|
36800
37328
|
}
|
|
36801
|
-
const kindValue = optionalString(manifest.kind) ?? "
|
|
37329
|
+
const kindValue = optionalString(manifest.kind) ?? "instruction";
|
|
36802
37330
|
if (kindValue !== "executable" && kindValue !== "instruction") {
|
|
36803
37331
|
throw new SkillRequestError(400, "INVALID_KIND", "`kind` must be 'executable' or 'instruction'");
|
|
36804
37332
|
}
|
|
@@ -36860,6 +37388,7 @@ async function createSkillsFetchHandler(options = {}) {
|
|
|
36860
37388
|
bootstrapApiKey: config.bootstrapApiKey
|
|
36861
37389
|
});
|
|
36862
37390
|
assertDurableStore(store, config);
|
|
37391
|
+
const governanceStore = options.governanceStore ?? await createGovernanceStore(config.databaseUrl);
|
|
36863
37392
|
const artifactStorage = new ArtifactStorage({
|
|
36864
37393
|
bucket: config.artifactBucket,
|
|
36865
37394
|
prefix: config.artifactPrefix
|
|
@@ -36882,7 +37411,7 @@ async function createSkillsFetchHandler(options = {}) {
|
|
|
36882
37411
|
return json(identityPayload(principal));
|
|
36883
37412
|
}
|
|
36884
37413
|
if (segments[0] === "api" && segments[1] === "v1") {
|
|
36885
|
-
return await handleApiV1(store, principal, request, segments.slice(2), config, artifactStorage);
|
|
37414
|
+
return await handleApiV1(store, governanceStore, principal, request, segments.slice(2), config, artifactStorage);
|
|
36886
37415
|
}
|
|
36887
37416
|
}
|
|
36888
37417
|
return json({ error: "not found", code: "NOT_FOUND" }, { status: 404 });
|
|
@@ -36911,7 +37440,7 @@ function skillsServeLimits(config) {
|
|
|
36911
37440
|
return { maxRequestBodySize: Math.max(config.skillBundleLimitBytes, config.requestBodyLimitBytes) + BODY_LIMIT_HEADROOM_BYTES };
|
|
36912
37441
|
}
|
|
36913
37442
|
var BODY_LIMIT_HEADROOM_BYTES = 1e6;
|
|
36914
|
-
async function handleApiV1(store, principal, request, parts, config, artifactStorage) {
|
|
37443
|
+
async function handleApiV1(store, governanceStore, principal, request, parts, config, artifactStorage) {
|
|
36915
37444
|
const [resource, id, subresource, childId] = parts;
|
|
36916
37445
|
if (parts.some(segmentEscapesPath)) {
|
|
36917
37446
|
return json({ error: "invalid path segment", code: "INVALID_PATH" }, { status: 400 });
|
|
@@ -37070,8 +37599,18 @@ async function handleApiV1(store, principal, request, parts, config, artifactSto
|
|
|
37070
37599
|
const run = await store.getRun(principal, id);
|
|
37071
37600
|
if (!run)
|
|
37072
37601
|
return json({ error: "run not found", code: "RUN_NOT_FOUND" }, { status: 404 });
|
|
37073
|
-
|
|
37074
|
-
|
|
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
|
+
}
|
|
37075
37614
|
}
|
|
37076
37615
|
}
|
|
37077
37616
|
return json({ error: "not found", code: "NOT_FOUND" }, { status: 404 });
|
|
@@ -37161,6 +37700,24 @@ function clampInt(value, fallback, max) {
|
|
|
37161
37700
|
}
|
|
37162
37701
|
|
|
37163
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
|
+
}
|
|
37164
37721
|
var config = resolveServerConfig();
|
|
37165
37722
|
var server = await startSkillsServer({ config });
|
|
37166
37723
|
console.log(`skills API listening on http://${config.host}:${server.port}`);
|