@hasna/skills 0.1.63 → 0.1.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -22
- package/bin/index.js +2120 -670
- package/bin/mcp.js +555 -249
- package/bin/migrate.js +229 -33
- package/bin/server.js +1542 -327
- package/bin/worker.js +716 -207
- package/dist/cli/commands/registry-reconcile.d.ts +2 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +682 -276
- package/dist/lib/agent-sync.d.ts +26 -6
- package/dist/lib/auth-store.d.ts +37 -0
- package/dist/lib/config.d.ts +51 -0
- package/dist/lib/home-census.d.ts +4 -0
- package/dist/lib/home-migration.d.ts +8 -9
- package/dist/lib/native-storage.d.ts +29 -1
- package/dist/lib/portable-skills.d.ts +49 -6
- package/dist/lib/pull.d.ts +31 -0
- package/dist/lib/registry-reconcile.d.ts +114 -0
- package/dist/lib/registry-types.d.ts +9 -0
- package/dist/lib/registry.d.ts +7 -4
- package/dist/lib/remote-client.d.ts +118 -1
- package/dist/lib/remote-registry.d.ts +26 -0
- package/dist/lib/revision.d.ts +29 -0
- package/dist/lib/run-routing.d.ts +60 -0
- package/dist/sdk/index.js +14412 -13338
- package/dist/server/app.d.ts +4 -1
- package/dist/server/config.d.ts +12 -2
- package/dist/server/rows.d.ts +2 -1
- package/dist/server/skills-api.d.ts +108 -6
- package/dist/server/sqlite-store.d.ts +20 -3
- package/dist/server/store-fixtures.d.ts +6 -0
- package/dist/server/store.d.ts +31 -5
- package/dist/server/types.d.ts +98 -3
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +57 -2
- package/migrations/postgres/0004_hosted_pins.sql +28 -0
- package/migrations/postgres/0005_revision_tombstone_registry.sql +37 -0
- package/migrations/postgres/0005_tag_projection.sql +36 -0
- package/migrations/sqlite/0004_hosted_pins.sql +21 -0
- package/migrations/sqlite/0005_revision_tombstone_registry.sql +18 -0
- package/migrations/sqlite/0005_tag_projection.sql +25 -0
- package/package.json +3 -2
package/bin/worker.js
CHANGED
|
@@ -22952,6 +22952,7 @@ var ANY_SEGMENT_EXCLUDES = new Set([
|
|
|
22952
22952
|
".docker"
|
|
22953
22953
|
]);
|
|
22954
22954
|
var ROOT_EXCLUDES = new Set(["dist", "build", ".turbo"]);
|
|
22955
|
+
var TOOL_SIDECAR_FILENAMES = new Set([".hasna-skills.json"]);
|
|
22955
22956
|
var CREDENTIAL_FILENAMES = new Set([
|
|
22956
22957
|
".npmrc",
|
|
22957
22958
|
".pypirc",
|
|
@@ -32847,23 +32848,40 @@ function concat(chunks) {
|
|
|
32847
32848
|
return merged;
|
|
32848
32849
|
}
|
|
32849
32850
|
|
|
32850
|
-
// src/
|
|
32851
|
-
|
|
32852
|
-
|
|
32853
|
-
|
|
32854
|
-
}
|
|
32855
|
-
|
|
32856
|
-
|
|
32857
|
-
|
|
32858
|
-
|
|
32859
|
-
|
|
32860
|
-
|
|
32861
|
-
|
|
32862
|
-
|
|
32863
|
-
|
|
32864
|
-
|
|
32865
|
-
|
|
32866
|
-
|
|
32851
|
+
// src/sdk/governance.ts
|
|
32852
|
+
var DEFAULT_OUTPUT_GOVERNANCE = {
|
|
32853
|
+
defaultVisibility: "private",
|
|
32854
|
+
redactPatterns: [
|
|
32855
|
+
/\bsk-[A-Za-z0-9_-]{8,}\b/g,
|
|
32856
|
+
/\bsk_[A-Za-z0-9_-]{8,}\b/g,
|
|
32857
|
+
/\bgh[opsur]_[A-Za-z0-9_]{8,}\b/g,
|
|
32858
|
+
/\bgithub_pat_[A-Za-z0-9_]{8,}\b/g,
|
|
32859
|
+
/\bnpm_[A-Za-z0-9_]{8,}\b/g,
|
|
32860
|
+
/\bAKIA[A-Z0-9]{12,}\b/g,
|
|
32861
|
+
/\bAIza[A-Za-z0-9_-]{10,}\b/g,
|
|
32862
|
+
/\b[A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|DATABASE_URL)[A-Z0-9_]*\s*[:=]\s*[^ \n\r\t]+/gi,
|
|
32863
|
+
/\bhttps?:\/\/[^ \n\r\t]+X-Amz-Signature=[^ \n\r\t]+/gi
|
|
32864
|
+
],
|
|
32865
|
+
perOutputBytes: 10 * 1024 * 1024,
|
|
32866
|
+
perRunTotalBytes: 100 * 1024 * 1024,
|
|
32867
|
+
artifactTtlSeconds: 30 * 24 * 60 * 60
|
|
32868
|
+
};
|
|
32869
|
+
var DEFAULT_RUN_QUOTA = {
|
|
32870
|
+
cpu: 1,
|
|
32871
|
+
memoryMB: 2048,
|
|
32872
|
+
durationSeconds: 3600,
|
|
32873
|
+
networkMB: 100,
|
|
32874
|
+
artifactBytes: 100 * 1024 * 1024
|
|
32875
|
+
};
|
|
32876
|
+
|
|
32877
|
+
// src/server/database-url.ts
|
|
32878
|
+
import { isAbsolute, join as join3 } from "path";
|
|
32879
|
+
import { fileURLToPath } from "url";
|
|
32880
|
+
|
|
32881
|
+
// src/lib/config.ts
|
|
32882
|
+
import { existsSync, readFileSync as readFileSync2, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
32883
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
32884
|
+
import { homedir as homedir2 } from "os";
|
|
32867
32885
|
|
|
32868
32886
|
// src/lib/retired-settings.ts
|
|
32869
32887
|
var RETIRED_ENV_SUFFIXES = ["_STORAGE_MODE", "_DEPLOYMENT_MODE", "_CLOUD_MODE"];
|
|
@@ -32900,53 +32918,7 @@ function assertNoRetiredModeEnvVars(env, options) {
|
|
|
32900
32918
|
throw new RetiredSettingError(names[0], `${names.join(", ")} ${names.length === 1 ? "is" : "are"} no longer read. ` + "Deployment modes were removed: where a server keeps its data is decided by the " + `database it is given, not by a declared label. Set ${options.replacement} to a ` + "postgres:// URL to use PostgreSQL, or leave it unset for the on-box SQLite database. " + `Then unset ${names.join(" and ")}. ` + "Refused rather than ignored, because a discarded setting looks exactly like a " + "working one until something needs the data.");
|
|
32901
32919
|
}
|
|
32902
32920
|
|
|
32903
|
-
// src/server/config.ts
|
|
32904
|
-
var DATABASE_URL_ENV = "HASNA_SKILLS_DATABASE_URL";
|
|
32905
|
-
var SKILLS_ENV_NAMESPACE = "SKILLS";
|
|
32906
|
-
function resolveServerConfig(env = process.env) {
|
|
32907
|
-
assertNoRetiredModeEnvVars(env, {
|
|
32908
|
-
app: SKILLS_ENV_NAMESPACE,
|
|
32909
|
-
replacement: DATABASE_URL_ENV
|
|
32910
|
-
});
|
|
32911
|
-
const nodeEnv = env.NODE_ENV || "development";
|
|
32912
|
-
const host = env.HOST || env.SKILLS_HOST || "0.0.0.0";
|
|
32913
|
-
const port = parsePositiveInt(env.PORT || env.SKILLS_PORT, 8787);
|
|
32914
|
-
return {
|
|
32915
|
-
host,
|
|
32916
|
-
port,
|
|
32917
|
-
databaseUrl: env[DATABASE_URL_ENV] || env.DATABASE_URL || undefined,
|
|
32918
|
-
bootstrapApiKey: env.HASNA_SKILLS_BOOTSTRAP_API_KEY || undefined,
|
|
32919
|
-
artifactBucket: env.HASNA_SKILLS_S3_BUCKET || env.SKILLS_S3_BUCKET || undefined,
|
|
32920
|
-
artifactPrefix: normalizePrefix(env.HASNA_SKILLS_S3_PREFIX || env.SKILLS_S3_PREFIX || "skills/artifacts"),
|
|
32921
|
-
inlineWorker: env.HASNA_SKILLS_INLINE_WORKER === "1",
|
|
32922
|
-
bundleSigningKey: env.HASNA_SKILLS_SIGNING_KEY || undefined,
|
|
32923
|
-
requestBodyLimitBytes: parsePositiveInt(env.HASNA_SKILLS_REQUEST_BODY_LIMIT_BYTES, 1e6),
|
|
32924
|
-
skillBundleLimitBytes: parsePositiveInt(env.HASNA_SKILLS_BUNDLE_LIMIT_BYTES, 25000000),
|
|
32925
|
-
publicBaseUrl: (env.SKILLS_PUBLIC_BASE_URL || localOrigin(host, port)).replace(/\/+$/, ""),
|
|
32926
|
-
nodeEnv,
|
|
32927
|
-
allowEphemeralStore: env.HASNA_SKILLS_ALLOW_EPHEMERAL_STORE === "1"
|
|
32928
|
-
};
|
|
32929
|
-
}
|
|
32930
|
-
function localOrigin(host, port) {
|
|
32931
|
-
const hostname = host === "0.0.0.0" || host === "::" ? "localhost" : host;
|
|
32932
|
-
return `http://${hostname.includes(":") ? `[${hostname}]` : hostname}:${port}`;
|
|
32933
|
-
}
|
|
32934
|
-
function parsePositiveInt(value, fallback) {
|
|
32935
|
-
const parsed = Number.parseInt(value ?? "", 10);
|
|
32936
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
32937
|
-
}
|
|
32938
|
-
function normalizePrefix(value) {
|
|
32939
|
-
return value.replace(/^\/+|\/+$/g, "") || "skills/artifacts";
|
|
32940
|
-
}
|
|
32941
|
-
|
|
32942
|
-
// src/server/database-url.ts
|
|
32943
|
-
import { isAbsolute, join as join3 } from "path";
|
|
32944
|
-
import { fileURLToPath } from "url";
|
|
32945
|
-
|
|
32946
32921
|
// src/lib/config.ts
|
|
32947
|
-
import { existsSync, readFileSync as readFileSync2, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
32948
|
-
import { join as join2, dirname as dirname2 } from "path";
|
|
32949
|
-
import { homedir as homedir2 } from "os";
|
|
32950
32922
|
function mergeDirectoryContents(sourceDir, targetDir) {
|
|
32951
32923
|
if (!existsSync(sourceDir))
|
|
32952
32924
|
return;
|
|
@@ -33057,26 +33029,66 @@ function looksLikeSqlitePath(value) {
|
|
|
33057
33029
|
return SQLITE_EXTENSIONS.some((extension) => value.toLowerCase().endsWith(extension));
|
|
33058
33030
|
}
|
|
33059
33031
|
|
|
33060
|
-
// src/server/
|
|
33061
|
-
import {
|
|
33032
|
+
// src/server/sqlite-store.ts
|
|
33033
|
+
import { Database } from "bun:sqlite";
|
|
33034
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
33035
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
|
|
33036
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
33062
33037
|
|
|
33063
|
-
// src/server/
|
|
33064
|
-
import {
|
|
33038
|
+
// src/server/auth.ts
|
|
33039
|
+
import { createHash as createHash3 } from "crypto";
|
|
33040
|
+
function hashApiKey(token) {
|
|
33041
|
+
return createHash3("sha256").update(token).digest("hex");
|
|
33042
|
+
}
|
|
33043
|
+
function publicPrincipal(partial = {}) {
|
|
33044
|
+
return {
|
|
33045
|
+
apiKeyId: partial.apiKeyId || "key_dev",
|
|
33046
|
+
orgId: partial.orgId || "org_dev",
|
|
33047
|
+
orgSlug: partial.orgSlug || "dev",
|
|
33048
|
+
orgName: partial.orgName || "Development",
|
|
33049
|
+
userId: partial.userId || "user_dev",
|
|
33050
|
+
email: partial.email || "dev@example.com",
|
|
33051
|
+
role: partial.role || "owner",
|
|
33052
|
+
scopes: partial.scopes || ["skills:read", "runs:write"]
|
|
33053
|
+
};
|
|
33054
|
+
}
|
|
33065
33055
|
|
|
33066
|
-
// src/server/
|
|
33067
|
-
|
|
33068
|
-
|
|
33069
|
-
|
|
33070
|
-
|
|
33071
|
-
|
|
33072
|
-
|
|
33073
|
-
|
|
33074
|
-
|
|
33075
|
-
|
|
33076
|
-
|
|
33077
|
-
|
|
33078
|
-
|
|
33056
|
+
// src/server/migrations-dir.ts
|
|
33057
|
+
import { existsSync as existsSync2 } from "fs";
|
|
33058
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
33059
|
+
var MIGRATION_DIALECTS = ["postgres", "sqlite"];
|
|
33060
|
+
var MAX_WALK_UP = 6;
|
|
33061
|
+
function findMigrationsRoot(startDirs = defaultStartDirs()) {
|
|
33062
|
+
for (const start of startDirs) {
|
|
33063
|
+
let dir = start;
|
|
33064
|
+
for (let level = 0;level < MAX_WALK_UP; level += 1) {
|
|
33065
|
+
const candidate = join4(dir, "migrations");
|
|
33066
|
+
if (MIGRATION_DIALECTS.some((dialect) => existsSync2(join4(candidate, dialect))))
|
|
33067
|
+
return candidate;
|
|
33068
|
+
const parent = dirname3(dir);
|
|
33069
|
+
if (parent === dir)
|
|
33070
|
+
break;
|
|
33071
|
+
dir = parent;
|
|
33072
|
+
}
|
|
33073
|
+
}
|
|
33074
|
+
return null;
|
|
33075
|
+
}
|
|
33076
|
+
function resolveMigrationsDir(dialect, root3 = findMigrationsRoot()) {
|
|
33077
|
+
if (!root3) {
|
|
33078
|
+
throw new Error(`could not locate the migrations directory for dialect "${dialect}". ` + `Expected a migrations/${dialect}/ folder alongside the package root.`);
|
|
33079
|
+
}
|
|
33080
|
+
const dir = join4(root3, dialect);
|
|
33081
|
+
if (!existsSync2(dir)) {
|
|
33082
|
+
throw new Error(`migrations directory not found: ${dir}`);
|
|
33079
33083
|
}
|
|
33084
|
+
return dir;
|
|
33085
|
+
}
|
|
33086
|
+
function defaultStartDirs() {
|
|
33087
|
+
const dirs = [import.meta.dir];
|
|
33088
|
+
const cwd = process.cwd();
|
|
33089
|
+
if (cwd && cwd !== import.meta.dir)
|
|
33090
|
+
dirs.push(cwd);
|
|
33091
|
+
return dirs;
|
|
33080
33092
|
}
|
|
33081
33093
|
|
|
33082
33094
|
// src/server/rows.ts
|
|
@@ -33159,7 +33171,20 @@ function rowToSkill(row) {
|
|
|
33159
33171
|
...row.bundle_byte_size === null || row.bundle_byte_size === undefined ? {} : { bundleByteSize: Number(row.bundle_byte_size) },
|
|
33160
33172
|
...typeof row.published_by_user_id === "string" ? { publishedByUserId: row.published_by_user_id } : {},
|
|
33161
33173
|
createdAt: dateString(row.created_at),
|
|
33162
|
-
updatedAt: dateString(row.updated_at)
|
|
33174
|
+
updatedAt: dateString(row.updated_at),
|
|
33175
|
+
revisionId: String(row.revision_id ?? ""),
|
|
33176
|
+
revisionNumber: Number(row.revision_number ?? 0),
|
|
33177
|
+
...row.tombstoned_at ? { tombstonedAt: dateString(row.tombstoned_at) } : {},
|
|
33178
|
+
...row.tombstone_purge_after ? { tombstonePurgeAfter: dateString(row.tombstone_purge_after) } : {}
|
|
33179
|
+
};
|
|
33180
|
+
}
|
|
33181
|
+
function rowToPin(row) {
|
|
33182
|
+
return {
|
|
33183
|
+
orgId: String(row.org_id),
|
|
33184
|
+
principal: String(row.principal),
|
|
33185
|
+
slug: String(row.slug),
|
|
33186
|
+
pinnedAt: dateString(row.pinned_at),
|
|
33187
|
+
metadata: parseJsonObject(row.metadata_json)
|
|
33163
33188
|
};
|
|
33164
33189
|
}
|
|
33165
33190
|
function rowToSkillBundle(row) {
|
|
@@ -33211,53 +33236,61 @@ function dateString(value) {
|
|
|
33211
33236
|
return String(value);
|
|
33212
33237
|
}
|
|
33213
33238
|
|
|
33214
|
-
// src/server/
|
|
33215
|
-
|
|
33216
|
-
|
|
33217
|
-
|
|
33218
|
-
|
|
33219
|
-
|
|
33220
|
-
|
|
33221
|
-
|
|
33222
|
-
|
|
33223
|
-
|
|
33224
|
-
|
|
33225
|
-
|
|
33226
|
-
|
|
33227
|
-
let dir = start;
|
|
33228
|
-
for (let level = 0;level < MAX_WALK_UP; level += 1) {
|
|
33229
|
-
const candidate = join4(dir, "migrations");
|
|
33230
|
-
if (MIGRATION_DIALECTS.some((dialect) => existsSync2(join4(candidate, dialect))))
|
|
33231
|
-
return candidate;
|
|
33232
|
-
const parent = dirname3(dir);
|
|
33233
|
-
if (parent === dir)
|
|
33234
|
-
break;
|
|
33235
|
-
dir = parent;
|
|
33236
|
-
}
|
|
33239
|
+
// src/server/types.ts
|
|
33240
|
+
class StaleLeaseGenerationError extends Error {
|
|
33241
|
+
runId;
|
|
33242
|
+
expectedGeneration;
|
|
33243
|
+
currentGeneration;
|
|
33244
|
+
status;
|
|
33245
|
+
constructor(runId2, expectedGeneration, currentGeneration, status) {
|
|
33246
|
+
super(`late transition rejected for run ${runId2}: expected lease_generation ${expectedGeneration}, ` + `current ${currentGeneration} (status ${status})`);
|
|
33247
|
+
this.name = "StaleLeaseGenerationError";
|
|
33248
|
+
this.runId = runId2;
|
|
33249
|
+
this.expectedGeneration = expectedGeneration;
|
|
33250
|
+
this.currentGeneration = currentGeneration;
|
|
33251
|
+
this.status = status;
|
|
33237
33252
|
}
|
|
33238
|
-
return null;
|
|
33239
33253
|
}
|
|
33240
|
-
|
|
33241
|
-
|
|
33242
|
-
|
|
33243
|
-
|
|
33244
|
-
|
|
33245
|
-
|
|
33246
|
-
|
|
33254
|
+
|
|
33255
|
+
class SkillRevisionConflictError extends Error {
|
|
33256
|
+
slug;
|
|
33257
|
+
expectedRevisionId;
|
|
33258
|
+
currentRevisionId;
|
|
33259
|
+
constructor(slug, expectedRevisionId, currentRevisionId) {
|
|
33260
|
+
super(`revision conflict for '${slug}': expected revision ${expectedRevisionId ?? "(none)"}, ` + `current is ${currentRevisionId ?? "(none)"}. Refused rather than silently overwriting a newer revision.`);
|
|
33261
|
+
this.name = "SkillRevisionConflictError";
|
|
33262
|
+
this.slug = slug;
|
|
33263
|
+
this.expectedRevisionId = expectedRevisionId;
|
|
33264
|
+
this.currentRevisionId = currentRevisionId;
|
|
33247
33265
|
}
|
|
33248
|
-
return dir;
|
|
33249
33266
|
}
|
|
33250
|
-
|
|
33251
|
-
|
|
33252
|
-
|
|
33253
|
-
|
|
33254
|
-
|
|
33255
|
-
|
|
33267
|
+
|
|
33268
|
+
// src/lib/revision.ts
|
|
33269
|
+
import { createHash as createHash4 } from "crypto";
|
|
33270
|
+
function revisionIdOf(content) {
|
|
33271
|
+
const canonical = JSON.stringify({
|
|
33272
|
+
slug: content.slug,
|
|
33273
|
+
displayName: content.displayName,
|
|
33274
|
+
description: content.description,
|
|
33275
|
+
category: content.category,
|
|
33276
|
+
tags: content.tags,
|
|
33277
|
+
source: content.source,
|
|
33278
|
+
kind: content.kind,
|
|
33279
|
+
version: content.version ?? null,
|
|
33280
|
+
skillMd: content.skillMd ?? null,
|
|
33281
|
+
bundleSha256: content.bundleSha256 ?? null,
|
|
33282
|
+
bundleByteSize: content.bundleByteSize ?? null
|
|
33283
|
+
});
|
|
33284
|
+
return createHash4("sha256").update(canonical).digest("hex");
|
|
33285
|
+
}
|
|
33286
|
+
function revisionIdOfRecord(record) {
|
|
33287
|
+
return revisionIdOf(record);
|
|
33256
33288
|
}
|
|
33257
33289
|
|
|
33258
33290
|
// src/server/sqlite-store.ts
|
|
33259
33291
|
var CLAIM_ATTEMPTS = 8;
|
|
33260
33292
|
var CLAIMABLE_STATUSES = ["queued", "retrying"];
|
|
33293
|
+
var NO_REVISION_SENTINEL = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
33261
33294
|
var LAST_USED_RESOLUTION_MS = 60000;
|
|
33262
33295
|
|
|
33263
33296
|
class SqliteSkillsStore {
|
|
@@ -33277,12 +33310,24 @@ class SqliteSkillsStore {
|
|
|
33277
33310
|
if (options.migrate !== false) {
|
|
33278
33311
|
applySqliteMigrations(this.db, options.migrationsDir);
|
|
33279
33312
|
}
|
|
33313
|
+
this.backfillLegacyRevisions();
|
|
33280
33314
|
this.backend = {
|
|
33281
33315
|
kind: "sqlite",
|
|
33282
33316
|
durable: !inMemory,
|
|
33283
33317
|
label: inMemory ? "sqlite (in-memory)" : `sqlite (${path})`
|
|
33284
33318
|
};
|
|
33285
33319
|
}
|
|
33320
|
+
backfillLegacyRevisions() {
|
|
33321
|
+
const rows = this.all("SELECT * FROM skills_registry WHERE revision_id = ''", []);
|
|
33322
|
+
for (const row of rows) {
|
|
33323
|
+
const record = rowToSkill(row);
|
|
33324
|
+
this.db.run("UPDATE skills_registry SET revision_id = ? WHERE org_id = ? AND slug = ?", [
|
|
33325
|
+
revisionIdOfRecord(record),
|
|
33326
|
+
record.orgId,
|
|
33327
|
+
record.slug
|
|
33328
|
+
]);
|
|
33329
|
+
}
|
|
33330
|
+
}
|
|
33286
33331
|
get database() {
|
|
33287
33332
|
return this.db;
|
|
33288
33333
|
}
|
|
@@ -33506,8 +33551,29 @@ class SqliteSkillsStore {
|
|
|
33506
33551
|
const orgId = input.principal.orgId;
|
|
33507
33552
|
const now = nowIso();
|
|
33508
33553
|
return this.db.transaction(() => {
|
|
33509
|
-
const previous = this.get("SELECT bundle_sha256 FROM skills_registry WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
33554
|
+
const previous = this.get("SELECT revision_id, revision_number, bundle_sha256, bundle_byte_size, skill_md, tombstoned_at FROM skills_registry WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
33510
33555
|
const previousSha = typeof previous?.bundle_sha256 === "string" ? previous.bundle_sha256 : null;
|
|
33556
|
+
const previousRevisionId = typeof previous?.revision_id === "string" && previous.revision_id ? previous.revision_id : null;
|
|
33557
|
+
const tombstoned = previous?.tombstoned_at != null;
|
|
33558
|
+
const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : typeof previous?.skill_md === "string" ? previous.skill_md : null;
|
|
33559
|
+
if (previous && !tombstoned && input.expectedRevisionId !== previousRevisionId) {
|
|
33560
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
|
|
33561
|
+
}
|
|
33562
|
+
const carriedSha = input.bundle?.sha256 ?? previousSha;
|
|
33563
|
+
const carriedSize = input.bundle?.byteSize ?? (previous?.bundle_byte_size == null ? null : Number(previous.bundle_byte_size));
|
|
33564
|
+
const revisionId = revisionIdOfRecord({
|
|
33565
|
+
slug: input.slug,
|
|
33566
|
+
displayName: input.displayName,
|
|
33567
|
+
description: input.description,
|
|
33568
|
+
category: input.category,
|
|
33569
|
+
tags: input.tags,
|
|
33570
|
+
source: input.source,
|
|
33571
|
+
kind: input.kind,
|
|
33572
|
+
...input.version ? { version: input.version } : {},
|
|
33573
|
+
...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
|
|
33574
|
+
...carriedSha ? { bundleSha256: carriedSha } : {},
|
|
33575
|
+
...carriedSize === null || carriedSize === undefined ? {} : { bundleByteSize: carriedSize }
|
|
33576
|
+
});
|
|
33511
33577
|
if (input.bundle) {
|
|
33512
33578
|
this.db.run(`INSERT INTO skills_bundles (org_id, sha256, byte_size, content_type, storage_kind, storage_key, body_blob, created_at)
|
|
33513
33579
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -33528,8 +33594,8 @@ class SqliteSkillsStore {
|
|
|
33528
33594
|
]);
|
|
33529
33595
|
}
|
|
33530
33596
|
const row = this.get(`INSERT INTO skills_registry (org_id, slug, display_name, description, category, tags_json, source, kind, version, skill_md,
|
|
33531
|
-
bundle_sha256, bundle_byte_size, published_by_user_id, created_at, updated_at)
|
|
33532
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
33597
|
+
bundle_sha256, bundle_byte_size, published_by_user_id, revision_id, revision_number, created_at, updated_at)
|
|
33598
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
33533
33599
|
ON CONFLICT (org_id, slug) DO UPDATE SET
|
|
33534
33600
|
display_name = excluded.display_name,
|
|
33535
33601
|
description = excluded.description,
|
|
@@ -33547,7 +33613,16 @@ class SqliteSkillsStore {
|
|
|
33547
33613
|
bundle_sha256 = COALESCE(excluded.bundle_sha256, skills_registry.bundle_sha256),
|
|
33548
33614
|
bundle_byte_size = COALESCE(excluded.bundle_byte_size, skills_registry.bundle_byte_size),
|
|
33549
33615
|
published_by_user_id = excluded.published_by_user_id,
|
|
33616
|
+
revision_id = excluded.revision_id,
|
|
33617
|
+
-- Current + 1, not the inserted 1: on the update path the ACTUAL row's
|
|
33618
|
+
-- counter is the truth (it may have advanced since the pre-read, which is
|
|
33619
|
+
-- exactly what the WHERE guard below detects).
|
|
33620
|
+
revision_number = skills_registry.revision_number + 1,
|
|
33621
|
+
tombstoned_at = NULL,
|
|
33622
|
+
tombstone_purge_after = NULL,
|
|
33550
33623
|
updated_at = excluded.updated_at
|
|
33624
|
+
WHERE skills_registry.tombstoned_at IS NOT NULL
|
|
33625
|
+
OR skills_registry.revision_id = ?
|
|
33551
33626
|
RETURNING *`, [
|
|
33552
33627
|
orgId,
|
|
33553
33628
|
input.slug,
|
|
@@ -33558,62 +33633,165 @@ class SqliteSkillsStore {
|
|
|
33558
33633
|
input.source,
|
|
33559
33634
|
input.kind,
|
|
33560
33635
|
input.version ?? null,
|
|
33561
|
-
|
|
33636
|
+
carriedSkillMd,
|
|
33562
33637
|
input.bundle?.sha256 ?? null,
|
|
33563
33638
|
input.bundle?.byteSize ?? null,
|
|
33564
33639
|
input.principal.userId,
|
|
33640
|
+
revisionId,
|
|
33641
|
+
now,
|
|
33565
33642
|
now,
|
|
33566
|
-
|
|
33643
|
+
input.expectedRevisionId ?? NO_REVISION_SENTINEL
|
|
33567
33644
|
]);
|
|
33645
|
+
if (!row) {
|
|
33646
|
+
const current = this.get("SELECT revision_id FROM skills_registry WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
33647
|
+
const currentId = typeof current?.revision_id === "string" ? current.revision_id : null;
|
|
33648
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, currentId);
|
|
33649
|
+
}
|
|
33568
33650
|
if (previousSha && input.bundle && previousSha !== input.bundle.sha256)
|
|
33569
33651
|
this.collectOrphanBundle(orgId, previousSha);
|
|
33652
|
+
this.db.run("DELETE FROM skills_tags WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
33653
|
+
const insertTag = this.db.prepare("INSERT OR IGNORE INTO skills_tags (org_id, slug, tag) VALUES (?, ?, ?)");
|
|
33654
|
+
for (const tag of input.tags) {
|
|
33655
|
+
if (!tag.trim())
|
|
33656
|
+
continue;
|
|
33657
|
+
insertTag.run(orgId, input.slug, tag);
|
|
33658
|
+
}
|
|
33570
33659
|
return rowToSkill(row);
|
|
33571
33660
|
})();
|
|
33572
33661
|
}
|
|
33573
33662
|
async listSkills(principal) {
|
|
33574
|
-
|
|
33663
|
+
await this.purgeExpiredTombstones(principal);
|
|
33664
|
+
return this.all("SELECT * FROM skills_registry WHERE org_id = ? AND tombstoned_at IS NULL ORDER BY slug ASC", [principal.orgId]).map(rowToSkill);
|
|
33575
33665
|
}
|
|
33576
33666
|
async getSkill(principal, slug) {
|
|
33577
33667
|
const row = this.get("SELECT * FROM skills_registry WHERE org_id = ? AND slug = ? LIMIT 1", [principal.orgId, slug]);
|
|
33578
33668
|
return row ? rowToSkill(row) : null;
|
|
33579
33669
|
}
|
|
33580
|
-
async updateSkill(principal, slug, patch) {
|
|
33670
|
+
async updateSkill(principal, slug, patch, expectedRevisionId) {
|
|
33581
33671
|
const current = await this.getSkill(principal, slug);
|
|
33582
|
-
if (!current)
|
|
33672
|
+
if (!current || current.tombstonedAt)
|
|
33583
33673
|
return null;
|
|
33674
|
+
if (expectedRevisionId !== current.revisionId) {
|
|
33675
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
|
|
33676
|
+
}
|
|
33584
33677
|
const next = { ...current, ...patch };
|
|
33585
|
-
|
|
33586
|
-
|
|
33587
|
-
|
|
33588
|
-
|
|
33589
|
-
|
|
33590
|
-
|
|
33591
|
-
|
|
33592
|
-
|
|
33593
|
-
|
|
33594
|
-
|
|
33595
|
-
|
|
33596
|
-
|
|
33597
|
-
|
|
33598
|
-
|
|
33599
|
-
|
|
33600
|
-
|
|
33678
|
+
return this.db.transaction(() => {
|
|
33679
|
+
const revisionId = revisionIdOfRecord(next);
|
|
33680
|
+
const row = this.get(`UPDATE skills_registry
|
|
33681
|
+
SET display_name = ?, description = ?, category = ?, tags_json = ?, kind = ?, version = ?, skill_md = ?,
|
|
33682
|
+
revision_id = ?, revision_number = revision_number + 1, updated_at = ?
|
|
33683
|
+
WHERE org_id = ? AND slug = ? AND tombstoned_at IS NULL AND revision_id = ?
|
|
33684
|
+
RETURNING *`, [
|
|
33685
|
+
next.displayName,
|
|
33686
|
+
next.description,
|
|
33687
|
+
next.category,
|
|
33688
|
+
JSON.stringify(next.tags),
|
|
33689
|
+
next.kind,
|
|
33690
|
+
next.version ?? null,
|
|
33691
|
+
next.skillMd ?? null,
|
|
33692
|
+
revisionId,
|
|
33693
|
+
nowIso(),
|
|
33694
|
+
principal.orgId,
|
|
33695
|
+
slug,
|
|
33696
|
+
current.revisionId
|
|
33697
|
+
]);
|
|
33698
|
+
if (!row) {
|
|
33699
|
+
const nowRow = this.get("SELECT revision_id, tombstoned_at FROM skills_registry WHERE org_id = ? AND slug = ? LIMIT 1", [
|
|
33700
|
+
principal.orgId,
|
|
33701
|
+
slug
|
|
33702
|
+
]);
|
|
33703
|
+
if (nowRow && nowRow.tombstoned_at == null) {
|
|
33704
|
+
const currentId = typeof nowRow.revision_id === "string" ? nowRow.revision_id : null;
|
|
33705
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, currentId);
|
|
33706
|
+
}
|
|
33707
|
+
return null;
|
|
33708
|
+
}
|
|
33709
|
+
this.db.run("DELETE FROM skills_tags WHERE org_id = ? AND slug = ?", [principal.orgId, slug]);
|
|
33710
|
+
const insertTag = this.db.prepare("INSERT OR IGNORE INTO skills_tags (org_id, slug, tag) VALUES (?, ?, ?)");
|
|
33711
|
+
for (const tag of next.tags) {
|
|
33712
|
+
if (!tag.trim())
|
|
33713
|
+
continue;
|
|
33714
|
+
insertTag.run(principal.orgId, slug, tag);
|
|
33715
|
+
}
|
|
33716
|
+
return rowToSkill(row);
|
|
33717
|
+
})();
|
|
33601
33718
|
}
|
|
33602
|
-
async deleteSkill(principal, slug) {
|
|
33719
|
+
async deleteSkill(principal, slug, tombstoneWindowMs) {
|
|
33603
33720
|
return this.db.transaction(() => {
|
|
33604
|
-
const existing = this.get("SELECT
|
|
33721
|
+
const existing = this.get("SELECT tombstoned_at FROM skills_registry WHERE org_id = ? AND slug = ?", [principal.orgId, slug]);
|
|
33605
33722
|
if (!existing)
|
|
33606
|
-
return
|
|
33607
|
-
|
|
33608
|
-
|
|
33609
|
-
|
|
33610
|
-
|
|
33723
|
+
return null;
|
|
33724
|
+
if (existing.tombstoned_at != null) {
|
|
33725
|
+
const row2 = this.get("SELECT * FROM skills_registry WHERE org_id = ? AND slug = ? LIMIT 1", [principal.orgId, slug]);
|
|
33726
|
+
return rowToSkill(row2);
|
|
33727
|
+
}
|
|
33728
|
+
const tombstonedAt = nowIso();
|
|
33729
|
+
const purgeAfter = new Date(Date.now() + tombstoneWindowMs).toISOString();
|
|
33730
|
+
const row = this.get(`UPDATE skills_registry
|
|
33731
|
+
SET tombstoned_at = ?, tombstone_purge_after = ?, updated_at = ?
|
|
33732
|
+
WHERE org_id = ? AND slug = ?
|
|
33733
|
+
RETURNING *`, [tombstonedAt, purgeAfter, tombstonedAt, principal.orgId, slug]);
|
|
33734
|
+
return rowToSkill(row);
|
|
33735
|
+
})();
|
|
33736
|
+
}
|
|
33737
|
+
async purgeExpiredTombstones(principal) {
|
|
33738
|
+
return this.db.transaction(() => {
|
|
33739
|
+
const now = nowIso();
|
|
33740
|
+
const expired = this.all("SELECT * FROM skills_registry WHERE org_id = ? AND tombstoned_at IS NOT NULL AND tombstone_purge_after <= ?", [principal.orgId, now]);
|
|
33741
|
+
const purged = [];
|
|
33742
|
+
for (const row of expired) {
|
|
33743
|
+
const record = rowToSkill(row);
|
|
33744
|
+
this.db.run("DELETE FROM skills_registry WHERE org_id = ? AND slug = ?", [principal.orgId, record.slug]);
|
|
33745
|
+
this.db.run("DELETE FROM skills_tags WHERE org_id = ? AND slug = ?", [principal.orgId, record.slug]);
|
|
33746
|
+
if (record.bundleSha256)
|
|
33747
|
+
this.collectOrphanBundle(principal.orgId, record.bundleSha256);
|
|
33748
|
+
purged.push(record);
|
|
33749
|
+
}
|
|
33750
|
+
return purged;
|
|
33611
33751
|
})();
|
|
33612
33752
|
}
|
|
33613
33753
|
async getSkillBundle(principal, sha256) {
|
|
33614
33754
|
const row = this.get("SELECT * FROM skills_bundles WHERE org_id = ? AND sha256 = ? LIMIT 1", [principal.orgId, sha256]);
|
|
33615
33755
|
return row ? rowToSkillBundle(row) : null;
|
|
33616
33756
|
}
|
|
33757
|
+
async pinSkill(principal, slug, metadata = {}) {
|
|
33758
|
+
const row = this.get(`INSERT INTO skills_pins (org_id, principal, slug, pinned_at, metadata_json)
|
|
33759
|
+
VALUES (?, ?, ?, ?, ?)
|
|
33760
|
+
ON CONFLICT (org_id, principal, slug) DO UPDATE SET
|
|
33761
|
+
pinned_at = excluded.pinned_at,
|
|
33762
|
+
metadata_json = excluded.metadata_json
|
|
33763
|
+
RETURNING *`, [principal.orgId, principal.apiKeyId, slug, nowIso(), JSON.stringify(metadata)]);
|
|
33764
|
+
return rowToPin(row);
|
|
33765
|
+
}
|
|
33766
|
+
async unpinSkill(principal, slug) {
|
|
33767
|
+
const result = this.db.run("DELETE FROM skills_pins WHERE org_id = ? AND principal = ? AND slug = ?", [principal.orgId, principal.apiKeyId, slug]);
|
|
33768
|
+
return result.changes > 0;
|
|
33769
|
+
}
|
|
33770
|
+
async listPins(principal) {
|
|
33771
|
+
return this.all("SELECT * FROM skills_pins WHERE org_id = ? AND principal = ? ORDER BY slug ASC", [principal.orgId, principal.apiKeyId]).map(rowToPin);
|
|
33772
|
+
}
|
|
33773
|
+
async listTags(principal) {
|
|
33774
|
+
await this.purgeExpiredTombstones(principal);
|
|
33775
|
+
return this.all("SELECT DISTINCT tag FROM skills_tags WHERE org_id = ? ORDER BY tag ASC", [principal.orgId]).map((row) => String(row.tag));
|
|
33776
|
+
}
|
|
33777
|
+
async listSkillsByTag(principal, tag) {
|
|
33778
|
+
await this.purgeExpiredTombstones(principal);
|
|
33779
|
+
return this.all(`SELECT s.* FROM skills_registry s
|
|
33780
|
+
JOIN skills_tags t ON t.org_id = s.org_id AND t.slug = s.slug
|
|
33781
|
+
WHERE t.org_id = ? AND t.tag = ? AND s.tombstoned_at IS NULL
|
|
33782
|
+
ORDER BY s.slug ASC`, [principal.orgId, tag]).map(rowToSkill);
|
|
33783
|
+
}
|
|
33784
|
+
async listPinsByTag(principal, tag) {
|
|
33785
|
+
await this.purgeExpiredTombstones(principal);
|
|
33786
|
+
return this.all(`SELECT p.* FROM skills_pins p
|
|
33787
|
+
JOIN skills_tags t ON t.org_id = p.org_id AND t.slug = p.slug
|
|
33788
|
+
JOIN skills_registry s ON s.org_id = p.org_id AND s.slug = p.slug
|
|
33789
|
+
WHERE p.org_id = ? AND p.principal = ? AND t.tag = ? AND s.tombstoned_at IS NULL
|
|
33790
|
+
ORDER BY p.slug ASC`, [principal.orgId, principal.apiKeyId, tag]).map(rowToPin);
|
|
33791
|
+
}
|
|
33792
|
+
async listPublishedSlugs(principal) {
|
|
33793
|
+
return this.all("SELECT slug FROM skills_registry WHERE org_id = ? AND tombstoned_at IS NULL ORDER BY slug ASC", [principal.orgId]).map((row) => String(row.slug));
|
|
33794
|
+
}
|
|
33617
33795
|
collectOrphanBundle(orgId, sha256) {
|
|
33618
33796
|
const referenced = this.get("SELECT 1 AS present FROM skills_registry WHERE org_id = ? AND bundle_sha256 = ? LIMIT 1", [orgId, sha256]);
|
|
33619
33797
|
if (referenced)
|
|
@@ -33699,7 +33877,66 @@ function parseScopes(value) {
|
|
|
33699
33877
|
}
|
|
33700
33878
|
}
|
|
33701
33879
|
|
|
33880
|
+
// src/server/config.ts
|
|
33881
|
+
var DATABASE_URL_ENV = "HASNA_SKILLS_DATABASE_URL";
|
|
33882
|
+
var SKILLS_ENV_NAMESPACE = "SKILLS";
|
|
33883
|
+
function resolveServerConfig(env = process.env) {
|
|
33884
|
+
assertNoRetiredModeEnvVars(env, {
|
|
33885
|
+
app: SKILLS_ENV_NAMESPACE,
|
|
33886
|
+
replacement: DATABASE_URL_ENV
|
|
33887
|
+
});
|
|
33888
|
+
const nodeEnv = env.NODE_ENV || "development";
|
|
33889
|
+
const host = env.HOST || env.SKILLS_HOST || "0.0.0.0";
|
|
33890
|
+
const port = parsePositiveInt(env.PORT || env.SKILLS_PORT, 8787);
|
|
33891
|
+
return {
|
|
33892
|
+
host,
|
|
33893
|
+
port,
|
|
33894
|
+
databaseUrl: env[DATABASE_URL_ENV] || env.DATABASE_URL || undefined,
|
|
33895
|
+
bootstrapApiKey: env.HASNA_SKILLS_BOOTSTRAP_API_KEY || undefined,
|
|
33896
|
+
artifactBucket: env.HASNA_SKILLS_S3_BUCKET || env.SKILLS_S3_BUCKET || undefined,
|
|
33897
|
+
artifactPrefix: normalizePrefix(env.HASNA_SKILLS_S3_PREFIX || env.SKILLS_S3_PREFIX || "skills/artifacts"),
|
|
33898
|
+
inlineWorker: env.HASNA_SKILLS_INLINE_WORKER === "1",
|
|
33899
|
+
bundleSigningKey: env.HASNA_SKILLS_API_SIGNING_KEY || env.HASNA_SKILLS_SIGNING_KEY || undefined,
|
|
33900
|
+
requestBodyLimitBytes: parsePositiveInt(env.HASNA_SKILLS_REQUEST_BODY_LIMIT_BYTES, 1e6),
|
|
33901
|
+
skillBundleLimitBytes: parsePositiveInt(env.HASNA_SKILLS_BUNDLE_LIMIT_BYTES, 25000000),
|
|
33902
|
+
tombstoneWindowMs: parsePositiveInt(env.HASNA_SKILLS_TOMBSTONE_WINDOW_MS, 7 * 24 * 60 * 60 * 1000),
|
|
33903
|
+
publicBaseUrl: (env.SKILLS_PUBLIC_BASE_URL || localOrigin(host, port)).replace(/\/+$/, ""),
|
|
33904
|
+
nodeEnv,
|
|
33905
|
+
allowEphemeralStore: env.HASNA_SKILLS_ALLOW_EPHEMERAL_STORE === "1"
|
|
33906
|
+
};
|
|
33907
|
+
}
|
|
33908
|
+
function localOrigin(host, port) {
|
|
33909
|
+
const hostname = host === "0.0.0.0" || host === "::" ? "localhost" : host;
|
|
33910
|
+
return `http://${hostname.includes(":") ? `[${hostname}]` : hostname}:${port}`;
|
|
33911
|
+
}
|
|
33912
|
+
function parsePositiveInt(value, fallback) {
|
|
33913
|
+
const parsed = Number.parseInt(value ?? "", 10);
|
|
33914
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
33915
|
+
}
|
|
33916
|
+
function normalizePrefix(value) {
|
|
33917
|
+
return value.replace(/^\/+|\/+$/g, "") || "skills/artifacts";
|
|
33918
|
+
}
|
|
33919
|
+
|
|
33920
|
+
// src/server/handlers.ts
|
|
33921
|
+
import { createHash as createHash5 } from "crypto";
|
|
33922
|
+
|
|
33702
33923
|
// src/server/store.ts
|
|
33924
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
33925
|
+
function recordFieldsOf(input, carriedBundle, carriedSkillMd) {
|
|
33926
|
+
return {
|
|
33927
|
+
slug: input.slug,
|
|
33928
|
+
displayName: input.displayName,
|
|
33929
|
+
description: input.description,
|
|
33930
|
+
category: input.category,
|
|
33931
|
+
tags: input.tags,
|
|
33932
|
+
source: input.source,
|
|
33933
|
+
kind: input.kind,
|
|
33934
|
+
...input.version ? { version: input.version } : {},
|
|
33935
|
+
...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
|
|
33936
|
+
bundleSha256: input.bundle?.sha256 ?? carriedBundle.bundleSha256,
|
|
33937
|
+
bundleByteSize: input.bundle?.byteSize ?? carriedBundle.bundleByteSize
|
|
33938
|
+
};
|
|
33939
|
+
}
|
|
33703
33940
|
function resolvePoolMax(env = process.env) {
|
|
33704
33941
|
const parsed = Number.parseInt(env.HASNA_SKILLS_DATABASE_POOL_MAX || env.SKILLS_DATABASE_POOL_MAX || "", 10);
|
|
33705
33942
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 4;
|
|
@@ -33737,6 +33974,7 @@ class MemorySkillsStore {
|
|
|
33737
33974
|
idempotency = new Map;
|
|
33738
33975
|
skills = new Map;
|
|
33739
33976
|
bundles = new Map;
|
|
33977
|
+
pins = new Map;
|
|
33740
33978
|
constructor(apiKeys = []) {
|
|
33741
33979
|
for (const key of apiKeys)
|
|
33742
33980
|
this.addApiKey(key.token, key.principal);
|
|
@@ -33838,6 +34076,9 @@ class MemorySkillsStore {
|
|
|
33838
34076
|
const key = skillKey(input.principal.orgId, input.slug);
|
|
33839
34077
|
const now = nowIso();
|
|
33840
34078
|
const previous = this.skills.get(key);
|
|
34079
|
+
if (previous && !previous.tombstonedAt && input.expectedRevisionId !== previous.revisionId) {
|
|
34080
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previous.revisionId);
|
|
34081
|
+
}
|
|
33841
34082
|
if (input.bundle) {
|
|
33842
34083
|
const bundleMapKey = skillKey(input.principal.orgId, input.bundle.sha256);
|
|
33843
34084
|
this.bundles.set(bundleMapKey, {
|
|
@@ -33847,6 +34088,8 @@ class MemorySkillsStore {
|
|
|
33847
34088
|
createdAt: this.bundles.get(bundleMapKey)?.createdAt ?? now
|
|
33848
34089
|
});
|
|
33849
34090
|
}
|
|
34091
|
+
const carriedBundle = !input.bundle && previous?.bundleSha256 ? { bundleSha256: previous.bundleSha256, ...previous.bundleByteSize === undefined ? {} : { bundleByteSize: previous.bundleByteSize } } : {};
|
|
34092
|
+
const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : previous?.skillMd;
|
|
33850
34093
|
const record = {
|
|
33851
34094
|
orgId: input.principal.orgId,
|
|
33852
34095
|
slug: input.slug,
|
|
@@ -33857,11 +34100,13 @@ class MemorySkillsStore {
|
|
|
33857
34100
|
source: input.source,
|
|
33858
34101
|
kind: input.kind,
|
|
33859
34102
|
...input.version ? { version: input.version } : {},
|
|
33860
|
-
...
|
|
33861
|
-
...input.bundle ? { bundleSha256: input.bundle.sha256, bundleByteSize: input.bundle.byteSize } :
|
|
34103
|
+
...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
|
|
34104
|
+
...input.bundle ? { bundleSha256: input.bundle.sha256, bundleByteSize: input.bundle.byteSize } : carriedBundle,
|
|
33862
34105
|
publishedByUserId: input.principal.userId,
|
|
33863
34106
|
createdAt: previous?.createdAt ?? now,
|
|
33864
|
-
updatedAt: now
|
|
34107
|
+
updatedAt: now,
|
|
34108
|
+
revisionId: revisionIdOfRecord(recordFieldsOf(input, carriedBundle, carriedSkillMd)),
|
|
34109
|
+
revisionNumber: (previous?.revisionNumber ?? 0) + 1
|
|
33865
34110
|
};
|
|
33866
34111
|
this.skills.set(key, record);
|
|
33867
34112
|
if (previous?.bundleSha256 && input.bundle && previous.bundleSha256 !== input.bundle.sha256) {
|
|
@@ -33870,33 +34115,107 @@ class MemorySkillsStore {
|
|
|
33870
34115
|
return record;
|
|
33871
34116
|
}
|
|
33872
34117
|
async listSkills(principal) {
|
|
33873
|
-
return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
|
|
34118
|
+
return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
|
|
33874
34119
|
}
|
|
33875
34120
|
async getSkill(principal, slug) {
|
|
33876
34121
|
const skill = this.skills.get(skillKey(principal.orgId, slug));
|
|
33877
34122
|
return skill && skill.orgId === principal.orgId ? skill : null;
|
|
33878
34123
|
}
|
|
33879
|
-
async updateSkill(principal, slug, patch) {
|
|
34124
|
+
async updateSkill(principal, slug, patch, expectedRevisionId) {
|
|
33880
34125
|
const current = await this.getSkill(principal, slug);
|
|
33881
|
-
if (!current)
|
|
34126
|
+
if (!current || current.tombstonedAt)
|
|
33882
34127
|
return null;
|
|
33883
|
-
|
|
34128
|
+
if (expectedRevisionId !== current.revisionId) {
|
|
34129
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
|
|
34130
|
+
}
|
|
34131
|
+
const latest = this.skills.get(skillKey(principal.orgId, slug));
|
|
34132
|
+
if (!latest || latest.tombstonedAt)
|
|
34133
|
+
return null;
|
|
34134
|
+
if (latest.revisionId !== current.revisionId) {
|
|
34135
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, latest.revisionId);
|
|
34136
|
+
}
|
|
34137
|
+
const next = {
|
|
34138
|
+
...latest,
|
|
34139
|
+
...patch,
|
|
34140
|
+
updatedAt: nowIso(),
|
|
34141
|
+
revisionId: revisionIdOfRecord({ ...latest, ...patch }),
|
|
34142
|
+
revisionNumber: latest.revisionNumber + 1
|
|
34143
|
+
};
|
|
33884
34144
|
this.skills.set(skillKey(principal.orgId, slug), next);
|
|
33885
34145
|
return next;
|
|
33886
34146
|
}
|
|
33887
|
-
async deleteSkill(principal, slug) {
|
|
34147
|
+
async deleteSkill(principal, slug, tombstoneWindowMs) {
|
|
33888
34148
|
const current = await this.getSkill(principal, slug);
|
|
33889
34149
|
if (!current)
|
|
33890
|
-
return
|
|
33891
|
-
|
|
33892
|
-
|
|
33893
|
-
|
|
33894
|
-
|
|
34150
|
+
return null;
|
|
34151
|
+
if (!current.tombstonedAt) {
|
|
34152
|
+
const tombstoned = nowIso();
|
|
34153
|
+
const purgeAfter = new Date(Date.now() + tombstoneWindowMs).toISOString();
|
|
34154
|
+
const next = { ...current, tombstonedAt: tombstoned, tombstonePurgeAfter: purgeAfter, updatedAt: tombstoned };
|
|
34155
|
+
this.skills.set(skillKey(principal.orgId, slug), next);
|
|
34156
|
+
return next;
|
|
34157
|
+
}
|
|
34158
|
+
return current;
|
|
34159
|
+
}
|
|
34160
|
+
async purgeExpiredTombstones(principal) {
|
|
34161
|
+
const now = nowIso();
|
|
34162
|
+
const purged = [];
|
|
34163
|
+
for (const [key, skill] of this.skills) {
|
|
34164
|
+
if (skill.orgId !== principal.orgId || !skill.tombstonedAt || !skill.tombstonePurgeAfter)
|
|
34165
|
+
continue;
|
|
34166
|
+
if (skill.tombstonePurgeAfter > now)
|
|
34167
|
+
continue;
|
|
34168
|
+
this.skills.delete(key);
|
|
34169
|
+
if (skill.bundleSha256)
|
|
34170
|
+
this.collectOrphanBundle(principal.orgId, skill.bundleSha256);
|
|
34171
|
+
purged.push(skill);
|
|
34172
|
+
}
|
|
34173
|
+
return purged;
|
|
33895
34174
|
}
|
|
33896
34175
|
async getSkillBundle(principal, sha256) {
|
|
33897
34176
|
const bundle = this.bundles.get(skillKey(principal.orgId, sha256));
|
|
33898
34177
|
return bundle && bundle.orgId === principal.orgId ? bundle : null;
|
|
33899
34178
|
}
|
|
34179
|
+
async pinSkill(principal, slug, metadata = {}) {
|
|
34180
|
+
const pin = { orgId: principal.orgId, principal: principal.apiKeyId, slug, pinnedAt: nowIso(), metadata: { ...metadata } };
|
|
34181
|
+
this.pins.set(pinKey(principal.orgId, principal.apiKeyId, slug), pin);
|
|
34182
|
+
return pin;
|
|
34183
|
+
}
|
|
34184
|
+
async unpinSkill(principal, slug) {
|
|
34185
|
+
return this.pins.delete(pinKey(principal.orgId, principal.apiKeyId, slug));
|
|
34186
|
+
}
|
|
34187
|
+
async listPins(principal) {
|
|
34188
|
+
return Array.from(this.pins.values()).filter((pin) => pin.orgId === principal.orgId && pin.principal === principal.apiKeyId).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
|
|
34189
|
+
}
|
|
34190
|
+
async listTags(principal) {
|
|
34191
|
+
await this.purgeExpiredTombstones(principal);
|
|
34192
|
+
const tags = new Set;
|
|
34193
|
+
for (const skill of this.skills.values()) {
|
|
34194
|
+
if (skill.orgId !== principal.orgId)
|
|
34195
|
+
continue;
|
|
34196
|
+
for (const tag of skill.tags) {
|
|
34197
|
+
if (tag.trim())
|
|
34198
|
+
tags.add(tag);
|
|
34199
|
+
}
|
|
34200
|
+
}
|
|
34201
|
+
return [...tags].sort();
|
|
34202
|
+
}
|
|
34203
|
+
async listSkillsByTag(principal, tag) {
|
|
34204
|
+
await this.purgeExpiredTombstones(principal);
|
|
34205
|
+
return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt && skill.tags.includes(tag)).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
|
|
34206
|
+
}
|
|
34207
|
+
async listPinsByTag(principal, tag) {
|
|
34208
|
+
await this.purgeExpiredTombstones(principal);
|
|
34209
|
+
const taggedSlugs = new Set;
|
|
34210
|
+
for (const skill of this.skills.values()) {
|
|
34211
|
+
if (skill.orgId === principal.orgId && !skill.tombstonedAt && skill.tags.includes(tag))
|
|
34212
|
+
taggedSlugs.add(skill.slug);
|
|
34213
|
+
}
|
|
34214
|
+
return Array.from(this.pins.values()).filter((pin) => pin.orgId === principal.orgId && pin.principal === principal.apiKeyId && taggedSlugs.has(pin.slug)).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
|
|
34215
|
+
}
|
|
34216
|
+
async listPublishedSlugs(principal) {
|
|
34217
|
+
return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt).map((skill) => skill.slug).sort();
|
|
34218
|
+
}
|
|
33900
34219
|
collectOrphanBundle(orgId, sha256) {
|
|
33901
34220
|
const referenced = Array.from(this.skills.values()).some((skill) => skill.orgId === orgId && skill.bundleSha256 === sha256);
|
|
33902
34221
|
if (!referenced)
|
|
@@ -33914,6 +34233,9 @@ class MemorySkillsStore {
|
|
|
33914
34233
|
function skillKey(orgId, slug) {
|
|
33915
34234
|
return `${orgId.length}:${orgId}:${slug}`;
|
|
33916
34235
|
}
|
|
34236
|
+
function pinKey(orgId, principal, slug) {
|
|
34237
|
+
return `${orgId.length}:${orgId}:${principal.length}:${principal}:${slug}`;
|
|
34238
|
+
}
|
|
33917
34239
|
|
|
33918
34240
|
class PostgresSkillsStore {
|
|
33919
34241
|
backend = { kind: "postgres", durable: true, label: "postgres" };
|
|
@@ -33933,6 +34255,14 @@ class PostgresSkillsStore {
|
|
|
33933
34255
|
} catch (error) {
|
|
33934
34256
|
throw new Error("the configured Postgres database is reachable but has no skills schema. Run `skills-migrate` against it " + "before starting the server - unlike SQLite, Postgres is not migrated automatically, so that several " + `replicas cannot race to migrate a shared database. Driver reported: ${connectionFailureSummary(error)}`);
|
|
33935
34257
|
}
|
|
34258
|
+
await this.backfillLegacyRevisions();
|
|
34259
|
+
}
|
|
34260
|
+
async backfillLegacyRevisions() {
|
|
34261
|
+
const rows = await this.sql`SELECT * FROM skills_registry WHERE revision_id = ${""}`;
|
|
34262
|
+
for (const row of rows) {
|
|
34263
|
+
const record = rowToSkill(row);
|
|
34264
|
+
await this.sql`UPDATE skills_registry SET revision_id = ${revisionIdOfRecord(record)} WHERE org_id = ${record.orgId} AND slug = ${record.slug}`;
|
|
34265
|
+
}
|
|
33936
34266
|
}
|
|
33937
34267
|
async close() {
|
|
33938
34268
|
await this.sql.close?.();
|
|
@@ -33994,17 +34324,17 @@ class PostgresSkillsStore {
|
|
|
33994
34324
|
});
|
|
33995
34325
|
}
|
|
33996
34326
|
async createRun(input) {
|
|
33997
|
-
if (input.idempotencyKey) {
|
|
33998
|
-
const existing = await this.sql`
|
|
33999
|
-
SELECT * FROM skills_runs
|
|
34000
|
-
WHERE org_id = ${input.principal.orgId} AND idempotency_key = ${input.idempotencyKey}
|
|
34001
|
-
LIMIT 1
|
|
34002
|
-
`;
|
|
34003
|
-
if (existing[0])
|
|
34004
|
-
return rowToRun(existing[0]);
|
|
34005
|
-
}
|
|
34006
|
-
const id = runId();
|
|
34007
34327
|
return this.withContext(input.principal.orgId, false, async (tx) => {
|
|
34328
|
+
if (input.idempotencyKey) {
|
|
34329
|
+
const existing = await tx`
|
|
34330
|
+
SELECT * FROM skills_runs
|
|
34331
|
+
WHERE org_id = ${input.principal.orgId} AND idempotency_key = ${input.idempotencyKey}
|
|
34332
|
+
LIMIT 1
|
|
34333
|
+
`;
|
|
34334
|
+
if (existing[0])
|
|
34335
|
+
return rowToRun(existing[0]);
|
|
34336
|
+
}
|
|
34337
|
+
const id = runId();
|
|
34008
34338
|
const rows = await tx`
|
|
34009
34339
|
INSERT INTO skills_runs (id, org_id, user_id, skill_slug, requested_slug, status, input_json, args_json, idempotency_key, correlation_id)
|
|
34010
34340
|
VALUES (${id}, ${input.principal.orgId}, ${input.principal.userId}, ${input.slug}, ${input.slug}, ${"queued"}, ${JSON.stringify(input.input)}::jsonb, ${JSON.stringify(input.args)}::jsonb, ${input.idempotencyKey ?? null}, ${randomUUID3()})
|
|
@@ -34167,8 +34497,33 @@ class PostgresSkillsStore {
|
|
|
34167
34497
|
async publishSkill(input) {
|
|
34168
34498
|
const orgId = input.principal.orgId;
|
|
34169
34499
|
return await this.sql.begin(async (tx) => {
|
|
34170
|
-
const previousRows = await tx`
|
|
34171
|
-
|
|
34500
|
+
const previousRows = await tx`
|
|
34501
|
+
SELECT revision_id, revision_number, bundle_sha256, bundle_byte_size, skill_md, tombstoned_at
|
|
34502
|
+
FROM skills_registry WHERE org_id = ${orgId} AND slug = ${input.slug} LIMIT 1
|
|
34503
|
+
`;
|
|
34504
|
+
const previous = previousRows[0];
|
|
34505
|
+
const previousSha = typeof previous?.bundle_sha256 === "string" ? String(previous.bundle_sha256) : null;
|
|
34506
|
+
const previousRevisionId = typeof previous?.revision_id === "string" && previous.revision_id ? String(previous.revision_id) : null;
|
|
34507
|
+
const tombstoned = previous?.tombstoned_at != null;
|
|
34508
|
+
const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : typeof previous?.skill_md === "string" ? String(previous.skill_md) : null;
|
|
34509
|
+
if (previous && !tombstoned && input.expectedRevisionId !== previousRevisionId) {
|
|
34510
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
|
|
34511
|
+
}
|
|
34512
|
+
const carriedSha = input.bundle?.sha256 ?? previousSha;
|
|
34513
|
+
const carriedSize = input.bundle?.byteSize ?? (previous?.bundle_byte_size == null ? null : Number(previous.bundle_byte_size));
|
|
34514
|
+
const revisionId = revisionIdOfRecord({
|
|
34515
|
+
slug: input.slug,
|
|
34516
|
+
displayName: input.displayName,
|
|
34517
|
+
description: input.description,
|
|
34518
|
+
category: input.category,
|
|
34519
|
+
tags: input.tags,
|
|
34520
|
+
source: input.source,
|
|
34521
|
+
kind: input.kind,
|
|
34522
|
+
...input.version ? { version: input.version } : {},
|
|
34523
|
+
...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
|
|
34524
|
+
...carriedSha ? { bundleSha256: carriedSha } : {},
|
|
34525
|
+
...carriedSize === null || carriedSize === undefined ? {} : { bundleByteSize: carriedSize }
|
|
34526
|
+
});
|
|
34172
34527
|
if (input.bundle) {
|
|
34173
34528
|
await tx`
|
|
34174
34529
|
INSERT INTO skills_bundles (org_id, sha256, byte_size, content_type, storage_kind, storage_key, body_blob)
|
|
@@ -34183,10 +34538,10 @@ class PostgresSkillsStore {
|
|
|
34183
34538
|
}
|
|
34184
34539
|
const rows = await tx`
|
|
34185
34540
|
INSERT INTO skills_registry (org_id, slug, display_name, description, category, tags_json, source, kind, version, skill_md,
|
|
34186
|
-
bundle_sha256, bundle_byte_size, published_by_user_id, updated_at)
|
|
34541
|
+
bundle_sha256, bundle_byte_size, published_by_user_id, revision_id, revision_number, updated_at)
|
|
34187
34542
|
VALUES (${orgId}, ${input.slug}, ${input.displayName}, ${input.description}, ${input.category}, ${JSON.stringify(input.tags)}::jsonb,
|
|
34188
|
-
${input.source}, ${input.kind}, ${input.version ?? null}, ${
|
|
34189
|
-
${input.bundle?.sha256 ?? null}, ${input.bundle?.byteSize ?? null}, ${input.principal.userId}, now())
|
|
34543
|
+
${input.source}, ${input.kind}, ${input.version ?? null}, ${carriedSkillMd},
|
|
34544
|
+
${input.bundle?.sha256 ?? null}, ${input.bundle?.byteSize ?? null}, ${input.principal.userId}, ${revisionId}, 1, now())
|
|
34190
34545
|
ON CONFLICT (org_id, slug) DO UPDATE SET
|
|
34191
34546
|
display_name = EXCLUDED.display_name,
|
|
34192
34547
|
description = EXCLUDED.description,
|
|
@@ -34201,9 +34556,23 @@ class PostgresSkillsStore {
|
|
|
34201
34556
|
bundle_sha256 = COALESCE(EXCLUDED.bundle_sha256, skills_registry.bundle_sha256),
|
|
34202
34557
|
bundle_byte_size = COALESCE(EXCLUDED.bundle_byte_size, skills_registry.bundle_byte_size),
|
|
34203
34558
|
published_by_user_id = EXCLUDED.published_by_user_id,
|
|
34559
|
+
revision_id = EXCLUDED.revision_id,
|
|
34560
|
+
-- Current + 1, not EXCLUDED.revision_number: the insert path minted 1, but on
|
|
34561
|
+
-- the update path the ACTUAL row's counter is the truth (it may have advanced
|
|
34562
|
+
-- since the pre-read, which is exactly what the WHERE guard below detects).
|
|
34563
|
+
revision_number = skills_registry.revision_number + 1,
|
|
34564
|
+
tombstoned_at = NULL,
|
|
34565
|
+
tombstone_purge_after = NULL,
|
|
34204
34566
|
updated_at = EXCLUDED.updated_at
|
|
34567
|
+
WHERE skills_registry.tombstoned_at IS NOT NULL
|
|
34568
|
+
OR skills_registry.revision_id = ${input.expectedRevisionId ?? NO_REVISION_SENTINEL2}
|
|
34205
34569
|
RETURNING *
|
|
34206
34570
|
`;
|
|
34571
|
+
if (!rows[0]) {
|
|
34572
|
+
const current = await tx`SELECT revision_id FROM skills_registry WHERE org_id = ${orgId} AND slug = ${input.slug} LIMIT 1`;
|
|
34573
|
+
const currentId = current[0] && typeof current[0].revision_id === "string" ? String(current[0].revision_id) : null;
|
|
34574
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, currentId);
|
|
34575
|
+
}
|
|
34207
34576
|
if (previousSha && input.bundle && previousSha !== input.bundle.sha256) {
|
|
34208
34577
|
await tx`
|
|
34209
34578
|
DELETE FROM skills_bundles
|
|
@@ -34211,55 +34580,182 @@ class PostgresSkillsStore {
|
|
|
34211
34580
|
AND NOT EXISTS (SELECT 1 FROM skills_registry WHERE org_id = ${orgId} AND bundle_sha256 = ${previousSha})
|
|
34212
34581
|
`;
|
|
34213
34582
|
}
|
|
34583
|
+
await tx`DELETE FROM skills_tags WHERE org_id = ${orgId} AND slug = ${input.slug}`;
|
|
34584
|
+
for (const tag of input.tags) {
|
|
34585
|
+
if (!tag.trim())
|
|
34586
|
+
continue;
|
|
34587
|
+
await tx`
|
|
34588
|
+
INSERT INTO skills_tags (org_id, slug, tag) VALUES (${orgId}, ${input.slug}, ${tag})
|
|
34589
|
+
ON CONFLICT DO NOTHING
|
|
34590
|
+
`;
|
|
34591
|
+
}
|
|
34214
34592
|
return rowToSkill(rows[0]);
|
|
34215
34593
|
});
|
|
34216
34594
|
}
|
|
34217
34595
|
async listSkills(principal) {
|
|
34218
|
-
|
|
34596
|
+
await this.purgeExpiredTombstones(principal);
|
|
34597
|
+
const rows = await this.sql`
|
|
34598
|
+
SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND tombstoned_at IS NULL ORDER BY slug ASC
|
|
34599
|
+
`;
|
|
34219
34600
|
return rows.map(rowToSkill);
|
|
34220
34601
|
}
|
|
34221
34602
|
async getSkill(principal, slug) {
|
|
34222
34603
|
const rows = await this.sql`SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1`;
|
|
34223
34604
|
return rows[0] ? rowToSkill(rows[0]) : null;
|
|
34224
34605
|
}
|
|
34225
|
-
async updateSkill(principal, slug, patch) {
|
|
34606
|
+
async updateSkill(principal, slug, patch, expectedRevisionId) {
|
|
34226
34607
|
const current = await this.getSkill(principal, slug);
|
|
34227
|
-
if (!current)
|
|
34608
|
+
if (!current || current.tombstonedAt)
|
|
34228
34609
|
return null;
|
|
34610
|
+
if (expectedRevisionId !== current.revisionId) {
|
|
34611
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
|
|
34612
|
+
}
|
|
34229
34613
|
const next = { ...current, ...patch };
|
|
34230
|
-
|
|
34231
|
-
|
|
34232
|
-
|
|
34233
|
-
|
|
34234
|
-
|
|
34235
|
-
|
|
34236
|
-
|
|
34237
|
-
|
|
34238
|
-
|
|
34614
|
+
return await this.sql.begin(async (tx) => {
|
|
34615
|
+
const revisionId = revisionIdOfRecord(next);
|
|
34616
|
+
const updated = await tx`
|
|
34617
|
+
UPDATE skills_registry
|
|
34618
|
+
SET display_name = ${next.displayName}, description = ${next.description}, category = ${next.category},
|
|
34619
|
+
tags_json = ${JSON.stringify(next.tags)}::jsonb, kind = ${next.kind}, version = ${next.version ?? null},
|
|
34620
|
+
skill_md = ${next.skillMd ?? null}, revision_id = ${revisionId}, revision_number = revision_number + 1, updated_at = now()
|
|
34621
|
+
WHERE org_id = ${principal.orgId} AND slug = ${slug} AND tombstoned_at IS NULL AND revision_id = ${current.revisionId}
|
|
34622
|
+
RETURNING *
|
|
34623
|
+
`;
|
|
34624
|
+
if (!updated[0]) {
|
|
34625
|
+
const nowRows = await tx`
|
|
34626
|
+
SELECT revision_id, tombstoned_at FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1
|
|
34627
|
+
`;
|
|
34628
|
+
if (nowRows[0] && nowRows[0].tombstoned_at == null) {
|
|
34629
|
+
const currentId = String(nowRows[0].revision_id);
|
|
34630
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, currentId);
|
|
34631
|
+
}
|
|
34632
|
+
return null;
|
|
34633
|
+
}
|
|
34634
|
+
await tx`DELETE FROM skills_tags WHERE org_id = ${principal.orgId} AND slug = ${slug}`;
|
|
34635
|
+
for (const tag of next.tags) {
|
|
34636
|
+
if (!tag.trim())
|
|
34637
|
+
continue;
|
|
34638
|
+
await tx`
|
|
34639
|
+
INSERT INTO skills_tags (org_id, slug, tag) VALUES (${principal.orgId}, ${slug}, ${tag})
|
|
34640
|
+
ON CONFLICT DO NOTHING
|
|
34641
|
+
`;
|
|
34642
|
+
}
|
|
34643
|
+
return rowToSkill(updated[0]);
|
|
34644
|
+
});
|
|
34239
34645
|
}
|
|
34240
|
-
async deleteSkill(principal, slug) {
|
|
34646
|
+
async deleteSkill(principal, slug, tombstoneWindowMs) {
|
|
34241
34647
|
return await this.sql.begin(async (tx) => {
|
|
34648
|
+
const existingRows = await tx`
|
|
34649
|
+
SELECT tombstoned_at FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1
|
|
34650
|
+
`;
|
|
34651
|
+
if (!existingRows[0])
|
|
34652
|
+
return null;
|
|
34653
|
+
if (existingRows[0].tombstoned_at != null) {
|
|
34654
|
+
const rows2 = await tx`SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1`;
|
|
34655
|
+
return rowToSkill(rows2[0]);
|
|
34656
|
+
}
|
|
34242
34657
|
const rows = await tx`
|
|
34243
|
-
|
|
34244
|
-
|
|
34658
|
+
UPDATE skills_registry
|
|
34659
|
+
SET tombstoned_at = now(), tombstone_purge_after = now() + (${tombstoneWindowMs}::int * interval '1 millisecond'), updated_at = now()
|
|
34660
|
+
WHERE org_id = ${principal.orgId} AND slug = ${slug}
|
|
34661
|
+
RETURNING *
|
|
34245
34662
|
`;
|
|
34246
|
-
|
|
34247
|
-
|
|
34248
|
-
|
|
34249
|
-
|
|
34663
|
+
return rows[0] ? rowToSkill(rows[0]) : null;
|
|
34664
|
+
});
|
|
34665
|
+
}
|
|
34666
|
+
async purgeExpiredTombstones(principal) {
|
|
34667
|
+
return await this.sql.begin(async (tx) => {
|
|
34668
|
+
const expiredRows = await tx`
|
|
34669
|
+
SELECT * FROM skills_registry
|
|
34670
|
+
WHERE org_id = ${principal.orgId} AND tombstoned_at IS NOT NULL AND tombstone_purge_after <= now()
|
|
34671
|
+
`;
|
|
34672
|
+
if (!expiredRows.length)
|
|
34673
|
+
return [];
|
|
34674
|
+
const purged = [];
|
|
34675
|
+
for (const row of expiredRows) {
|
|
34676
|
+
const record = rowToSkill(row);
|
|
34250
34677
|
await tx`
|
|
34251
|
-
DELETE FROM
|
|
34252
|
-
|
|
34253
|
-
|
|
34678
|
+
DELETE FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${record.slug} AND tombstone_purge_after <= now()
|
|
34679
|
+
`;
|
|
34680
|
+
await tx`DELETE FROM skills_tags WHERE org_id = ${principal.orgId} AND slug = ${record.slug}`;
|
|
34681
|
+
await tx`
|
|
34682
|
+
DELETE FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${record.slug} AND tombstone_purge_after <= now()
|
|
34254
34683
|
`;
|
|
34684
|
+
if (record.bundleSha256) {
|
|
34685
|
+
await tx`
|
|
34686
|
+
DELETE FROM skills_bundles
|
|
34687
|
+
WHERE org_id = ${principal.orgId} AND sha256 = ${record.bundleSha256}
|
|
34688
|
+
AND NOT EXISTS (SELECT 1 FROM skills_registry WHERE org_id = ${principal.orgId} AND bundle_sha256 = ${record.bundleSha256})
|
|
34689
|
+
`;
|
|
34690
|
+
}
|
|
34691
|
+
purged.push(record);
|
|
34255
34692
|
}
|
|
34256
|
-
return
|
|
34693
|
+
return purged;
|
|
34257
34694
|
});
|
|
34258
34695
|
}
|
|
34259
34696
|
async getSkillBundle(principal, sha256) {
|
|
34260
34697
|
const rows = await this.sql`SELECT * FROM skills_bundles WHERE org_id = ${principal.orgId} AND sha256 = ${sha256} LIMIT 1`;
|
|
34261
34698
|
return rows[0] ? rowToSkillBundle(rows[0]) : null;
|
|
34262
34699
|
}
|
|
34700
|
+
async pinSkill(principal, slug, metadata = {}) {
|
|
34701
|
+
const rows = await this.sql`
|
|
34702
|
+
INSERT INTO skills_pins (org_id, principal, slug, pinned_at, metadata_json)
|
|
34703
|
+
VALUES (${principal.orgId}, ${principal.apiKeyId}, ${slug}, now(), ${JSON.stringify(metadata)}::jsonb)
|
|
34704
|
+
ON CONFLICT (org_id, principal, slug) DO UPDATE SET
|
|
34705
|
+
pinned_at = now(),
|
|
34706
|
+
metadata_json = EXCLUDED.metadata_json
|
|
34707
|
+
RETURNING *
|
|
34708
|
+
`;
|
|
34709
|
+
return rowToPin(rows[0]);
|
|
34710
|
+
}
|
|
34711
|
+
async unpinSkill(principal, slug) {
|
|
34712
|
+
const rows = await this.sql`
|
|
34713
|
+
DELETE FROM skills_pins WHERE org_id = ${principal.orgId} AND principal = ${principal.apiKeyId} AND slug = ${slug}
|
|
34714
|
+
RETURNING 1 AS present
|
|
34715
|
+
`;
|
|
34716
|
+
return rows.length > 0;
|
|
34717
|
+
}
|
|
34718
|
+
async listPins(principal) {
|
|
34719
|
+
const rows = await this.sql`
|
|
34720
|
+
SELECT * FROM skills_pins WHERE org_id = ${principal.orgId} AND principal = ${principal.apiKeyId} ORDER BY slug ASC
|
|
34721
|
+
`;
|
|
34722
|
+
return rows.map(rowToPin);
|
|
34723
|
+
}
|
|
34724
|
+
async listTags(principal) {
|
|
34725
|
+
await this.purgeExpiredTombstones(principal);
|
|
34726
|
+
const rows = await this.sql`
|
|
34727
|
+
SELECT DISTINCT tag FROM skills_tags WHERE org_id = ${principal.orgId} ORDER BY tag ASC
|
|
34728
|
+
`;
|
|
34729
|
+
return rows.map((row) => String(row.tag));
|
|
34730
|
+
}
|
|
34731
|
+
async listSkillsByTag(principal, tag) {
|
|
34732
|
+
await this.purgeExpiredTombstones(principal);
|
|
34733
|
+
const rows = await this.sql`
|
|
34734
|
+
SELECT s.* FROM skills_registry s
|
|
34735
|
+
JOIN skills_tags t ON t.org_id = s.org_id AND t.slug = s.slug
|
|
34736
|
+
WHERE t.org_id = ${principal.orgId} AND t.tag = ${tag} AND s.tombstoned_at IS NULL
|
|
34737
|
+
ORDER BY s.slug ASC
|
|
34738
|
+
`;
|
|
34739
|
+
return rows.map(rowToSkill);
|
|
34740
|
+
}
|
|
34741
|
+
async listPinsByTag(principal, tag) {
|
|
34742
|
+
await this.purgeExpiredTombstones(principal);
|
|
34743
|
+
const rows = await this.sql`
|
|
34744
|
+
SELECT p.* FROM skills_pins p
|
|
34745
|
+
JOIN skills_tags t ON t.org_id = p.org_id AND t.slug = p.slug
|
|
34746
|
+
JOIN skills_registry s ON s.org_id = p.org_id AND s.slug = p.slug
|
|
34747
|
+
WHERE p.org_id = ${principal.orgId} AND p.principal = ${principal.apiKeyId}
|
|
34748
|
+
AND t.tag = ${tag} AND s.tombstoned_at IS NULL
|
|
34749
|
+
ORDER BY p.slug ASC
|
|
34750
|
+
`;
|
|
34751
|
+
return rows.map(rowToPin);
|
|
34752
|
+
}
|
|
34753
|
+
async listPublishedSlugs(principal) {
|
|
34754
|
+
const rows = await this.sql`
|
|
34755
|
+
SELECT slug FROM skills_registry WHERE org_id = ${principal.orgId} AND tombstoned_at IS NULL ORDER BY slug ASC
|
|
34756
|
+
`;
|
|
34757
|
+
return rows.map((row) => String(row.slug));
|
|
34758
|
+
}
|
|
34263
34759
|
async collectOrphanBundle(orgId, sha256) {
|
|
34264
34760
|
await this.sql`
|
|
34265
34761
|
DELETE FROM skills_bundles
|
|
@@ -34268,6 +34764,7 @@ class PostgresSkillsStore {
|
|
|
34268
34764
|
`;
|
|
34269
34765
|
}
|
|
34270
34766
|
}
|
|
34767
|
+
var NO_REVISION_SENTINEL2 = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
34271
34768
|
function isUniqueViolation(error) {
|
|
34272
34769
|
const code = error?.code;
|
|
34273
34770
|
if (code === "23505" || code === 23505)
|
|
@@ -34329,7 +34826,7 @@ ${summary}
|
|
|
34329
34826
|
textArtifact(run, "show-notes.md", `# Show Notes
|
|
34330
34827
|
|
|
34331
34828
|
- ${summary}
|
|
34332
|
-
- Generated by the
|
|
34829
|
+
- Generated by the skills deterministic worker.
|
|
34333
34830
|
`),
|
|
34334
34831
|
textArtifact(run, "clips.csv", `start,end,title,summary
|
|
34335
34832
|
00:00,00:30,"Opening","${csv(summary)}"
|
|
@@ -34363,7 +34860,7 @@ function textArtifact(run, relativePath, bodyText, contentType = relativePath.en
|
|
|
34363
34860
|
relativePath,
|
|
34364
34861
|
contentType,
|
|
34365
34862
|
byteSize: bytes.byteLength,
|
|
34366
|
-
sha256:
|
|
34863
|
+
sha256: createHash5("sha256").update(bytes).digest("hex"),
|
|
34367
34864
|
visibility: "private"
|
|
34368
34865
|
},
|
|
34369
34866
|
body: { relativePath, bodyText, contentType }
|
|
@@ -34379,7 +34876,9 @@ async function completeRun(store, run, preview) {
|
|
|
34379
34876
|
return next ?? run;
|
|
34380
34877
|
}
|
|
34381
34878
|
async function failRun(store, run, code, message) {
|
|
34382
|
-
|
|
34879
|
+
try {
|
|
34880
|
+
await store.appendLog(run.id, run.orgId, "error", message);
|
|
34881
|
+
} catch {}
|
|
34383
34882
|
const next = await fencedTransition(store, run, {
|
|
34384
34883
|
status: "failed",
|
|
34385
34884
|
errorCode: code,
|
|
@@ -34393,7 +34892,9 @@ async function fencedTransition(store, run, patch) {
|
|
|
34393
34892
|
return store.updateRun(run.id, patch);
|
|
34394
34893
|
const next = await store.transitionRun(run.id, patch, run.leaseGeneration);
|
|
34395
34894
|
if (!next) {
|
|
34396
|
-
|
|
34895
|
+
try {
|
|
34896
|
+
await store.appendLog(run.id, run.orgId, "warn", `late write rejected: run no longer owned at lease_generation ${run.leaseGeneration}`);
|
|
34897
|
+
} catch {}
|
|
34397
34898
|
}
|
|
34398
34899
|
return next;
|
|
34399
34900
|
}
|
|
@@ -34582,7 +35083,7 @@ var DEVELOPMENT_TOOLS_SKILLS = [
|
|
|
34582
35083
|
{
|
|
34583
35084
|
name: "monitor",
|
|
34584
35085
|
displayName: "Monitor",
|
|
34585
|
-
description: "Operate the
|
|
35086
|
+
description: "Operate the monitor MCP for machine health, processes, cron jobs, and cleanup workflows",
|
|
34586
35087
|
category: "Development Tools",
|
|
34587
35088
|
tags: ["monitoring", "mcp", "processes", "operations"]
|
|
34588
35089
|
},
|
|
@@ -34628,6 +35129,14 @@ var DEVELOPMENT_TOOLS_SKILLS = [
|
|
|
34628
35129
|
description: "Validate configuration files for syntax and schema compliance",
|
|
34629
35130
|
category: "Development Tools",
|
|
34630
35131
|
tags: ["config", "validation", "schema", "linting"]
|
|
35132
|
+
},
|
|
35133
|
+
{
|
|
35134
|
+
name: "session-inject-monitor",
|
|
35135
|
+
displayName: "Session Inject Monitor",
|
|
35136
|
+
description: "Set up a declarative monitor that injects a prompt into a live coding-agent session when a watched source (conversations, email, todos, knowledge, command output) has new content",
|
|
35137
|
+
category: "Development Tools",
|
|
35138
|
+
tags: ["monitor", "session", "injection", "automation", "wake"],
|
|
35139
|
+
kind: "instruction"
|
|
34631
35140
|
}
|
|
34632
35141
|
];
|
|
34633
35142
|
|
|
@@ -35015,7 +35524,7 @@ var DESIGN_BRANDING_SKILLS = [
|
|
|
35015
35524
|
displayName: "Site Analyze",
|
|
35016
35525
|
description: "Analyze any website's design system \u2014 detects shadcn/ui, Tailwind, extracts colors, typography, and components via Playwright + Claude Vision.",
|
|
35017
35526
|
category: "Design & Branding",
|
|
35018
|
-
tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "
|
|
35527
|
+
tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "styles"]
|
|
35019
35528
|
}
|
|
35020
35529
|
];
|
|
35021
35530
|
|