@hasna/skills 0.1.63 → 0.1.64
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 +28 -6
- package/bin/index.js +1593 -228
- package/bin/mcp.js +290 -24
- package/bin/migrate.js +229 -33
- package/bin/server.js +774 -116
- package/bin/worker.js +558 -79
- package/dist/cli/commands/registry-reconcile.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +559 -251
- 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 +1 -1
- package/dist/lib/portable-skills.d.ts +35 -6
- package/dist/lib/pull.d.ts +31 -0
- package/dist/lib/registry-reconcile.d.ts +114 -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/sdk/index.js +1351 -353
- package/dist/server/app.d.ts +1 -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.d.ts +31 -5
- package/dist/server/types.d.ts +98 -3
- package/dist/storage.js +7 -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",
|
|
@@ -32919,9 +32920,10 @@ function resolveServerConfig(env = process.env) {
|
|
|
32919
32920
|
artifactBucket: env.HASNA_SKILLS_S3_BUCKET || env.SKILLS_S3_BUCKET || undefined,
|
|
32920
32921
|
artifactPrefix: normalizePrefix(env.HASNA_SKILLS_S3_PREFIX || env.SKILLS_S3_PREFIX || "skills/artifacts"),
|
|
32921
32922
|
inlineWorker: env.HASNA_SKILLS_INLINE_WORKER === "1",
|
|
32922
|
-
bundleSigningKey: env.HASNA_SKILLS_SIGNING_KEY || undefined,
|
|
32923
|
+
bundleSigningKey: env.HASNA_SKILLS_API_SIGNING_KEY || env.HASNA_SKILLS_SIGNING_KEY || undefined,
|
|
32923
32924
|
requestBodyLimitBytes: parsePositiveInt(env.HASNA_SKILLS_REQUEST_BODY_LIMIT_BYTES, 1e6),
|
|
32924
32925
|
skillBundleLimitBytes: parsePositiveInt(env.HASNA_SKILLS_BUNDLE_LIMIT_BYTES, 25000000),
|
|
32926
|
+
tombstoneWindowMs: parsePositiveInt(env.HASNA_SKILLS_TOMBSTONE_WINDOW_MS, 7 * 24 * 60 * 60 * 1000),
|
|
32925
32927
|
publicBaseUrl: (env.SKILLS_PUBLIC_BASE_URL || localOrigin(host, port)).replace(/\/+$/, ""),
|
|
32926
32928
|
nodeEnv,
|
|
32927
32929
|
allowEphemeralStore: env.HASNA_SKILLS_ALLOW_EPHEMERAL_STORE === "1"
|
|
@@ -33058,7 +33060,7 @@ function looksLikeSqlitePath(value) {
|
|
|
33058
33060
|
}
|
|
33059
33061
|
|
|
33060
33062
|
// src/server/handlers.ts
|
|
33061
|
-
import { createHash as
|
|
33063
|
+
import { createHash as createHash5 } from "crypto";
|
|
33062
33064
|
|
|
33063
33065
|
// src/server/store.ts
|
|
33064
33066
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
@@ -33079,6 +33081,19 @@ class StaleLeaseGenerationError extends Error {
|
|
|
33079
33081
|
}
|
|
33080
33082
|
}
|
|
33081
33083
|
|
|
33084
|
+
class SkillRevisionConflictError extends Error {
|
|
33085
|
+
slug;
|
|
33086
|
+
expectedRevisionId;
|
|
33087
|
+
currentRevisionId;
|
|
33088
|
+
constructor(slug, expectedRevisionId, currentRevisionId) {
|
|
33089
|
+
super(`revision conflict for '${slug}': expected revision ${expectedRevisionId ?? "(none)"}, ` + `current is ${currentRevisionId ?? "(none)"}. Refused rather than silently overwriting a newer revision.`);
|
|
33090
|
+
this.name = "SkillRevisionConflictError";
|
|
33091
|
+
this.slug = slug;
|
|
33092
|
+
this.expectedRevisionId = expectedRevisionId;
|
|
33093
|
+
this.currentRevisionId = currentRevisionId;
|
|
33094
|
+
}
|
|
33095
|
+
}
|
|
33096
|
+
|
|
33082
33097
|
// src/server/rows.ts
|
|
33083
33098
|
import { randomUUID } from "crypto";
|
|
33084
33099
|
function nowIso() {
|
|
@@ -33159,7 +33174,20 @@ function rowToSkill(row) {
|
|
|
33159
33174
|
...row.bundle_byte_size === null || row.bundle_byte_size === undefined ? {} : { bundleByteSize: Number(row.bundle_byte_size) },
|
|
33160
33175
|
...typeof row.published_by_user_id === "string" ? { publishedByUserId: row.published_by_user_id } : {},
|
|
33161
33176
|
createdAt: dateString(row.created_at),
|
|
33162
|
-
updatedAt: dateString(row.updated_at)
|
|
33177
|
+
updatedAt: dateString(row.updated_at),
|
|
33178
|
+
revisionId: String(row.revision_id ?? ""),
|
|
33179
|
+
revisionNumber: Number(row.revision_number ?? 0),
|
|
33180
|
+
...row.tombstoned_at ? { tombstonedAt: dateString(row.tombstoned_at) } : {},
|
|
33181
|
+
...row.tombstone_purge_after ? { tombstonePurgeAfter: dateString(row.tombstone_purge_after) } : {}
|
|
33182
|
+
};
|
|
33183
|
+
}
|
|
33184
|
+
function rowToPin(row) {
|
|
33185
|
+
return {
|
|
33186
|
+
orgId: String(row.org_id),
|
|
33187
|
+
principal: String(row.principal),
|
|
33188
|
+
slug: String(row.slug),
|
|
33189
|
+
pinnedAt: dateString(row.pinned_at),
|
|
33190
|
+
metadata: parseJsonObject(row.metadata_json)
|
|
33163
33191
|
};
|
|
33164
33192
|
}
|
|
33165
33193
|
function rowToSkillBundle(row) {
|
|
@@ -33211,6 +33239,28 @@ function dateString(value) {
|
|
|
33211
33239
|
return String(value);
|
|
33212
33240
|
}
|
|
33213
33241
|
|
|
33242
|
+
// src/lib/revision.ts
|
|
33243
|
+
import { createHash as createHash4 } from "crypto";
|
|
33244
|
+
function revisionIdOf(content) {
|
|
33245
|
+
const canonical = JSON.stringify({
|
|
33246
|
+
slug: content.slug,
|
|
33247
|
+
displayName: content.displayName,
|
|
33248
|
+
description: content.description,
|
|
33249
|
+
category: content.category,
|
|
33250
|
+
tags: content.tags,
|
|
33251
|
+
source: content.source,
|
|
33252
|
+
kind: content.kind,
|
|
33253
|
+
version: content.version ?? null,
|
|
33254
|
+
skillMd: content.skillMd ?? null,
|
|
33255
|
+
bundleSha256: content.bundleSha256 ?? null,
|
|
33256
|
+
bundleByteSize: content.bundleByteSize ?? null
|
|
33257
|
+
});
|
|
33258
|
+
return createHash4("sha256").update(canonical).digest("hex");
|
|
33259
|
+
}
|
|
33260
|
+
function revisionIdOfRecord(record) {
|
|
33261
|
+
return revisionIdOf(record);
|
|
33262
|
+
}
|
|
33263
|
+
|
|
33214
33264
|
// src/server/sqlite-store.ts
|
|
33215
33265
|
import { Database } from "bun:sqlite";
|
|
33216
33266
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -33258,6 +33308,7 @@ function defaultStartDirs() {
|
|
|
33258
33308
|
// src/server/sqlite-store.ts
|
|
33259
33309
|
var CLAIM_ATTEMPTS = 8;
|
|
33260
33310
|
var CLAIMABLE_STATUSES = ["queued", "retrying"];
|
|
33311
|
+
var NO_REVISION_SENTINEL = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
33261
33312
|
var LAST_USED_RESOLUTION_MS = 60000;
|
|
33262
33313
|
|
|
33263
33314
|
class SqliteSkillsStore {
|
|
@@ -33277,12 +33328,24 @@ class SqliteSkillsStore {
|
|
|
33277
33328
|
if (options.migrate !== false) {
|
|
33278
33329
|
applySqliteMigrations(this.db, options.migrationsDir);
|
|
33279
33330
|
}
|
|
33331
|
+
this.backfillLegacyRevisions();
|
|
33280
33332
|
this.backend = {
|
|
33281
33333
|
kind: "sqlite",
|
|
33282
33334
|
durable: !inMemory,
|
|
33283
33335
|
label: inMemory ? "sqlite (in-memory)" : `sqlite (${path})`
|
|
33284
33336
|
};
|
|
33285
33337
|
}
|
|
33338
|
+
backfillLegacyRevisions() {
|
|
33339
|
+
const rows = this.all("SELECT * FROM skills_registry WHERE revision_id = ''", []);
|
|
33340
|
+
for (const row of rows) {
|
|
33341
|
+
const record = rowToSkill(row);
|
|
33342
|
+
this.db.run("UPDATE skills_registry SET revision_id = ? WHERE org_id = ? AND slug = ?", [
|
|
33343
|
+
revisionIdOfRecord(record),
|
|
33344
|
+
record.orgId,
|
|
33345
|
+
record.slug
|
|
33346
|
+
]);
|
|
33347
|
+
}
|
|
33348
|
+
}
|
|
33286
33349
|
get database() {
|
|
33287
33350
|
return this.db;
|
|
33288
33351
|
}
|
|
@@ -33506,8 +33569,29 @@ class SqliteSkillsStore {
|
|
|
33506
33569
|
const orgId = input.principal.orgId;
|
|
33507
33570
|
const now = nowIso();
|
|
33508
33571
|
return this.db.transaction(() => {
|
|
33509
|
-
const previous = this.get("SELECT bundle_sha256 FROM skills_registry WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
33572
|
+
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
33573
|
const previousSha = typeof previous?.bundle_sha256 === "string" ? previous.bundle_sha256 : null;
|
|
33574
|
+
const previousRevisionId = typeof previous?.revision_id === "string" && previous.revision_id ? previous.revision_id : null;
|
|
33575
|
+
const tombstoned = previous?.tombstoned_at != null;
|
|
33576
|
+
const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : typeof previous?.skill_md === "string" ? previous.skill_md : null;
|
|
33577
|
+
if (previous && !tombstoned && input.expectedRevisionId !== previousRevisionId) {
|
|
33578
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
|
|
33579
|
+
}
|
|
33580
|
+
const carriedSha = input.bundle?.sha256 ?? previousSha;
|
|
33581
|
+
const carriedSize = input.bundle?.byteSize ?? (previous?.bundle_byte_size == null ? null : Number(previous.bundle_byte_size));
|
|
33582
|
+
const revisionId = revisionIdOfRecord({
|
|
33583
|
+
slug: input.slug,
|
|
33584
|
+
displayName: input.displayName,
|
|
33585
|
+
description: input.description,
|
|
33586
|
+
category: input.category,
|
|
33587
|
+
tags: input.tags,
|
|
33588
|
+
source: input.source,
|
|
33589
|
+
kind: input.kind,
|
|
33590
|
+
...input.version ? { version: input.version } : {},
|
|
33591
|
+
...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
|
|
33592
|
+
...carriedSha ? { bundleSha256: carriedSha } : {},
|
|
33593
|
+
...carriedSize === null || carriedSize === undefined ? {} : { bundleByteSize: carriedSize }
|
|
33594
|
+
});
|
|
33511
33595
|
if (input.bundle) {
|
|
33512
33596
|
this.db.run(`INSERT INTO skills_bundles (org_id, sha256, byte_size, content_type, storage_kind, storage_key, body_blob, created_at)
|
|
33513
33597
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -33528,8 +33612,8 @@ class SqliteSkillsStore {
|
|
|
33528
33612
|
]);
|
|
33529
33613
|
}
|
|
33530
33614
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
33615
|
+
bundle_sha256, bundle_byte_size, published_by_user_id, revision_id, revision_number, created_at, updated_at)
|
|
33616
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
33533
33617
|
ON CONFLICT (org_id, slug) DO UPDATE SET
|
|
33534
33618
|
display_name = excluded.display_name,
|
|
33535
33619
|
description = excluded.description,
|
|
@@ -33547,7 +33631,16 @@ class SqliteSkillsStore {
|
|
|
33547
33631
|
bundle_sha256 = COALESCE(excluded.bundle_sha256, skills_registry.bundle_sha256),
|
|
33548
33632
|
bundle_byte_size = COALESCE(excluded.bundle_byte_size, skills_registry.bundle_byte_size),
|
|
33549
33633
|
published_by_user_id = excluded.published_by_user_id,
|
|
33634
|
+
revision_id = excluded.revision_id,
|
|
33635
|
+
-- Current + 1, not the inserted 1: on the update path the ACTUAL row's
|
|
33636
|
+
-- counter is the truth (it may have advanced since the pre-read, which is
|
|
33637
|
+
-- exactly what the WHERE guard below detects).
|
|
33638
|
+
revision_number = skills_registry.revision_number + 1,
|
|
33639
|
+
tombstoned_at = NULL,
|
|
33640
|
+
tombstone_purge_after = NULL,
|
|
33550
33641
|
updated_at = excluded.updated_at
|
|
33642
|
+
WHERE skills_registry.tombstoned_at IS NOT NULL
|
|
33643
|
+
OR skills_registry.revision_id = ?
|
|
33551
33644
|
RETURNING *`, [
|
|
33552
33645
|
orgId,
|
|
33553
33646
|
input.slug,
|
|
@@ -33558,62 +33651,165 @@ class SqliteSkillsStore {
|
|
|
33558
33651
|
input.source,
|
|
33559
33652
|
input.kind,
|
|
33560
33653
|
input.version ?? null,
|
|
33561
|
-
|
|
33654
|
+
carriedSkillMd,
|
|
33562
33655
|
input.bundle?.sha256 ?? null,
|
|
33563
33656
|
input.bundle?.byteSize ?? null,
|
|
33564
33657
|
input.principal.userId,
|
|
33658
|
+
revisionId,
|
|
33659
|
+
now,
|
|
33565
33660
|
now,
|
|
33566
|
-
|
|
33661
|
+
input.expectedRevisionId ?? NO_REVISION_SENTINEL
|
|
33567
33662
|
]);
|
|
33663
|
+
if (!row) {
|
|
33664
|
+
const current = this.get("SELECT revision_id FROM skills_registry WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
33665
|
+
const currentId = typeof current?.revision_id === "string" ? current.revision_id : null;
|
|
33666
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, currentId);
|
|
33667
|
+
}
|
|
33568
33668
|
if (previousSha && input.bundle && previousSha !== input.bundle.sha256)
|
|
33569
33669
|
this.collectOrphanBundle(orgId, previousSha);
|
|
33670
|
+
this.db.run("DELETE FROM skills_tags WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
33671
|
+
const insertTag = this.db.prepare("INSERT OR IGNORE INTO skills_tags (org_id, slug, tag) VALUES (?, ?, ?)");
|
|
33672
|
+
for (const tag of input.tags) {
|
|
33673
|
+
if (!tag.trim())
|
|
33674
|
+
continue;
|
|
33675
|
+
insertTag.run(orgId, input.slug, tag);
|
|
33676
|
+
}
|
|
33570
33677
|
return rowToSkill(row);
|
|
33571
33678
|
})();
|
|
33572
33679
|
}
|
|
33573
33680
|
async listSkills(principal) {
|
|
33574
|
-
|
|
33681
|
+
await this.purgeExpiredTombstones(principal);
|
|
33682
|
+
return this.all("SELECT * FROM skills_registry WHERE org_id = ? AND tombstoned_at IS NULL ORDER BY slug ASC", [principal.orgId]).map(rowToSkill);
|
|
33575
33683
|
}
|
|
33576
33684
|
async getSkill(principal, slug) {
|
|
33577
33685
|
const row = this.get("SELECT * FROM skills_registry WHERE org_id = ? AND slug = ? LIMIT 1", [principal.orgId, slug]);
|
|
33578
33686
|
return row ? rowToSkill(row) : null;
|
|
33579
33687
|
}
|
|
33580
|
-
async updateSkill(principal, slug, patch) {
|
|
33688
|
+
async updateSkill(principal, slug, patch, expectedRevisionId) {
|
|
33581
33689
|
const current = await this.getSkill(principal, slug);
|
|
33582
|
-
if (!current)
|
|
33690
|
+
if (!current || current.tombstonedAt)
|
|
33583
33691
|
return null;
|
|
33692
|
+
if (expectedRevisionId !== current.revisionId) {
|
|
33693
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
|
|
33694
|
+
}
|
|
33584
33695
|
const next = { ...current, ...patch };
|
|
33585
|
-
|
|
33586
|
-
|
|
33587
|
-
|
|
33588
|
-
|
|
33589
|
-
|
|
33590
|
-
|
|
33591
|
-
|
|
33592
|
-
|
|
33593
|
-
|
|
33594
|
-
|
|
33595
|
-
|
|
33596
|
-
|
|
33597
|
-
|
|
33598
|
-
|
|
33599
|
-
|
|
33600
|
-
|
|
33696
|
+
return this.db.transaction(() => {
|
|
33697
|
+
const revisionId = revisionIdOfRecord(next);
|
|
33698
|
+
const row = this.get(`UPDATE skills_registry
|
|
33699
|
+
SET display_name = ?, description = ?, category = ?, tags_json = ?, kind = ?, version = ?, skill_md = ?,
|
|
33700
|
+
revision_id = ?, revision_number = revision_number + 1, updated_at = ?
|
|
33701
|
+
WHERE org_id = ? AND slug = ? AND tombstoned_at IS NULL AND revision_id = ?
|
|
33702
|
+
RETURNING *`, [
|
|
33703
|
+
next.displayName,
|
|
33704
|
+
next.description,
|
|
33705
|
+
next.category,
|
|
33706
|
+
JSON.stringify(next.tags),
|
|
33707
|
+
next.kind,
|
|
33708
|
+
next.version ?? null,
|
|
33709
|
+
next.skillMd ?? null,
|
|
33710
|
+
revisionId,
|
|
33711
|
+
nowIso(),
|
|
33712
|
+
principal.orgId,
|
|
33713
|
+
slug,
|
|
33714
|
+
current.revisionId
|
|
33715
|
+
]);
|
|
33716
|
+
if (!row) {
|
|
33717
|
+
const nowRow = this.get("SELECT revision_id, tombstoned_at FROM skills_registry WHERE org_id = ? AND slug = ? LIMIT 1", [
|
|
33718
|
+
principal.orgId,
|
|
33719
|
+
slug
|
|
33720
|
+
]);
|
|
33721
|
+
if (nowRow && nowRow.tombstoned_at == null) {
|
|
33722
|
+
const currentId = typeof nowRow.revision_id === "string" ? nowRow.revision_id : null;
|
|
33723
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, currentId);
|
|
33724
|
+
}
|
|
33725
|
+
return null;
|
|
33726
|
+
}
|
|
33727
|
+
this.db.run("DELETE FROM skills_tags WHERE org_id = ? AND slug = ?", [principal.orgId, slug]);
|
|
33728
|
+
const insertTag = this.db.prepare("INSERT OR IGNORE INTO skills_tags (org_id, slug, tag) VALUES (?, ?, ?)");
|
|
33729
|
+
for (const tag of next.tags) {
|
|
33730
|
+
if (!tag.trim())
|
|
33731
|
+
continue;
|
|
33732
|
+
insertTag.run(principal.orgId, slug, tag);
|
|
33733
|
+
}
|
|
33734
|
+
return rowToSkill(row);
|
|
33735
|
+
})();
|
|
33601
33736
|
}
|
|
33602
|
-
async deleteSkill(principal, slug) {
|
|
33737
|
+
async deleteSkill(principal, slug, tombstoneWindowMs) {
|
|
33603
33738
|
return this.db.transaction(() => {
|
|
33604
|
-
const existing = this.get("SELECT
|
|
33739
|
+
const existing = this.get("SELECT tombstoned_at FROM skills_registry WHERE org_id = ? AND slug = ?", [principal.orgId, slug]);
|
|
33605
33740
|
if (!existing)
|
|
33606
|
-
return
|
|
33607
|
-
|
|
33608
|
-
|
|
33609
|
-
|
|
33610
|
-
|
|
33741
|
+
return null;
|
|
33742
|
+
if (existing.tombstoned_at != null) {
|
|
33743
|
+
const row2 = this.get("SELECT * FROM skills_registry WHERE org_id = ? AND slug = ? LIMIT 1", [principal.orgId, slug]);
|
|
33744
|
+
return rowToSkill(row2);
|
|
33745
|
+
}
|
|
33746
|
+
const tombstonedAt = nowIso();
|
|
33747
|
+
const purgeAfter = new Date(Date.now() + tombstoneWindowMs).toISOString();
|
|
33748
|
+
const row = this.get(`UPDATE skills_registry
|
|
33749
|
+
SET tombstoned_at = ?, tombstone_purge_after = ?, updated_at = ?
|
|
33750
|
+
WHERE org_id = ? AND slug = ?
|
|
33751
|
+
RETURNING *`, [tombstonedAt, purgeAfter, tombstonedAt, principal.orgId, slug]);
|
|
33752
|
+
return rowToSkill(row);
|
|
33753
|
+
})();
|
|
33754
|
+
}
|
|
33755
|
+
async purgeExpiredTombstones(principal) {
|
|
33756
|
+
return this.db.transaction(() => {
|
|
33757
|
+
const now = nowIso();
|
|
33758
|
+
const expired = this.all("SELECT * FROM skills_registry WHERE org_id = ? AND tombstoned_at IS NOT NULL AND tombstone_purge_after <= ?", [principal.orgId, now]);
|
|
33759
|
+
const purged = [];
|
|
33760
|
+
for (const row of expired) {
|
|
33761
|
+
const record = rowToSkill(row);
|
|
33762
|
+
this.db.run("DELETE FROM skills_registry WHERE org_id = ? AND slug = ?", [principal.orgId, record.slug]);
|
|
33763
|
+
this.db.run("DELETE FROM skills_tags WHERE org_id = ? AND slug = ?", [principal.orgId, record.slug]);
|
|
33764
|
+
if (record.bundleSha256)
|
|
33765
|
+
this.collectOrphanBundle(principal.orgId, record.bundleSha256);
|
|
33766
|
+
purged.push(record);
|
|
33767
|
+
}
|
|
33768
|
+
return purged;
|
|
33611
33769
|
})();
|
|
33612
33770
|
}
|
|
33613
33771
|
async getSkillBundle(principal, sha256) {
|
|
33614
33772
|
const row = this.get("SELECT * FROM skills_bundles WHERE org_id = ? AND sha256 = ? LIMIT 1", [principal.orgId, sha256]);
|
|
33615
33773
|
return row ? rowToSkillBundle(row) : null;
|
|
33616
33774
|
}
|
|
33775
|
+
async pinSkill(principal, slug, metadata = {}) {
|
|
33776
|
+
const row = this.get(`INSERT INTO skills_pins (org_id, principal, slug, pinned_at, metadata_json)
|
|
33777
|
+
VALUES (?, ?, ?, ?, ?)
|
|
33778
|
+
ON CONFLICT (org_id, principal, slug) DO UPDATE SET
|
|
33779
|
+
pinned_at = excluded.pinned_at,
|
|
33780
|
+
metadata_json = excluded.metadata_json
|
|
33781
|
+
RETURNING *`, [principal.orgId, principal.apiKeyId, slug, nowIso(), JSON.stringify(metadata)]);
|
|
33782
|
+
return rowToPin(row);
|
|
33783
|
+
}
|
|
33784
|
+
async unpinSkill(principal, slug) {
|
|
33785
|
+
const result = this.db.run("DELETE FROM skills_pins WHERE org_id = ? AND principal = ? AND slug = ?", [principal.orgId, principal.apiKeyId, slug]);
|
|
33786
|
+
return result.changes > 0;
|
|
33787
|
+
}
|
|
33788
|
+
async listPins(principal) {
|
|
33789
|
+
return this.all("SELECT * FROM skills_pins WHERE org_id = ? AND principal = ? ORDER BY slug ASC", [principal.orgId, principal.apiKeyId]).map(rowToPin);
|
|
33790
|
+
}
|
|
33791
|
+
async listTags(principal) {
|
|
33792
|
+
await this.purgeExpiredTombstones(principal);
|
|
33793
|
+
return this.all("SELECT DISTINCT tag FROM skills_tags WHERE org_id = ? ORDER BY tag ASC", [principal.orgId]).map((row) => String(row.tag));
|
|
33794
|
+
}
|
|
33795
|
+
async listSkillsByTag(principal, tag) {
|
|
33796
|
+
await this.purgeExpiredTombstones(principal);
|
|
33797
|
+
return this.all(`SELECT s.* FROM skills_registry s
|
|
33798
|
+
JOIN skills_tags t ON t.org_id = s.org_id AND t.slug = s.slug
|
|
33799
|
+
WHERE t.org_id = ? AND t.tag = ? AND s.tombstoned_at IS NULL
|
|
33800
|
+
ORDER BY s.slug ASC`, [principal.orgId, tag]).map(rowToSkill);
|
|
33801
|
+
}
|
|
33802
|
+
async listPinsByTag(principal, tag) {
|
|
33803
|
+
await this.purgeExpiredTombstones(principal);
|
|
33804
|
+
return this.all(`SELECT p.* FROM skills_pins p
|
|
33805
|
+
JOIN skills_tags t ON t.org_id = p.org_id AND t.slug = p.slug
|
|
33806
|
+
JOIN skills_registry s ON s.org_id = p.org_id AND s.slug = p.slug
|
|
33807
|
+
WHERE p.org_id = ? AND p.principal = ? AND t.tag = ? AND s.tombstoned_at IS NULL
|
|
33808
|
+
ORDER BY p.slug ASC`, [principal.orgId, principal.apiKeyId, tag]).map(rowToPin);
|
|
33809
|
+
}
|
|
33810
|
+
async listPublishedSlugs(principal) {
|
|
33811
|
+
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));
|
|
33812
|
+
}
|
|
33617
33813
|
collectOrphanBundle(orgId, sha256) {
|
|
33618
33814
|
const referenced = this.get("SELECT 1 AS present FROM skills_registry WHERE org_id = ? AND bundle_sha256 = ? LIMIT 1", [orgId, sha256]);
|
|
33619
33815
|
if (referenced)
|
|
@@ -33700,6 +33896,21 @@ function parseScopes(value) {
|
|
|
33700
33896
|
}
|
|
33701
33897
|
|
|
33702
33898
|
// src/server/store.ts
|
|
33899
|
+
function recordFieldsOf(input, carriedBundle, carriedSkillMd) {
|
|
33900
|
+
return {
|
|
33901
|
+
slug: input.slug,
|
|
33902
|
+
displayName: input.displayName,
|
|
33903
|
+
description: input.description,
|
|
33904
|
+
category: input.category,
|
|
33905
|
+
tags: input.tags,
|
|
33906
|
+
source: input.source,
|
|
33907
|
+
kind: input.kind,
|
|
33908
|
+
...input.version ? { version: input.version } : {},
|
|
33909
|
+
...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
|
|
33910
|
+
bundleSha256: input.bundle?.sha256 ?? carriedBundle.bundleSha256,
|
|
33911
|
+
bundleByteSize: input.bundle?.byteSize ?? carriedBundle.bundleByteSize
|
|
33912
|
+
};
|
|
33913
|
+
}
|
|
33703
33914
|
function resolvePoolMax(env = process.env) {
|
|
33704
33915
|
const parsed = Number.parseInt(env.HASNA_SKILLS_DATABASE_POOL_MAX || env.SKILLS_DATABASE_POOL_MAX || "", 10);
|
|
33705
33916
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 4;
|
|
@@ -33737,6 +33948,7 @@ class MemorySkillsStore {
|
|
|
33737
33948
|
idempotency = new Map;
|
|
33738
33949
|
skills = new Map;
|
|
33739
33950
|
bundles = new Map;
|
|
33951
|
+
pins = new Map;
|
|
33740
33952
|
constructor(apiKeys = []) {
|
|
33741
33953
|
for (const key of apiKeys)
|
|
33742
33954
|
this.addApiKey(key.token, key.principal);
|
|
@@ -33838,6 +34050,9 @@ class MemorySkillsStore {
|
|
|
33838
34050
|
const key = skillKey(input.principal.orgId, input.slug);
|
|
33839
34051
|
const now = nowIso();
|
|
33840
34052
|
const previous = this.skills.get(key);
|
|
34053
|
+
if (previous && !previous.tombstonedAt && input.expectedRevisionId !== previous.revisionId) {
|
|
34054
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previous.revisionId);
|
|
34055
|
+
}
|
|
33841
34056
|
if (input.bundle) {
|
|
33842
34057
|
const bundleMapKey = skillKey(input.principal.orgId, input.bundle.sha256);
|
|
33843
34058
|
this.bundles.set(bundleMapKey, {
|
|
@@ -33847,6 +34062,8 @@ class MemorySkillsStore {
|
|
|
33847
34062
|
createdAt: this.bundles.get(bundleMapKey)?.createdAt ?? now
|
|
33848
34063
|
});
|
|
33849
34064
|
}
|
|
34065
|
+
const carriedBundle = !input.bundle && previous?.bundleSha256 ? { bundleSha256: previous.bundleSha256, ...previous.bundleByteSize === undefined ? {} : { bundleByteSize: previous.bundleByteSize } } : {};
|
|
34066
|
+
const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : previous?.skillMd;
|
|
33850
34067
|
const record = {
|
|
33851
34068
|
orgId: input.principal.orgId,
|
|
33852
34069
|
slug: input.slug,
|
|
@@ -33857,11 +34074,13 @@ class MemorySkillsStore {
|
|
|
33857
34074
|
source: input.source,
|
|
33858
34075
|
kind: input.kind,
|
|
33859
34076
|
...input.version ? { version: input.version } : {},
|
|
33860
|
-
...
|
|
33861
|
-
...input.bundle ? { bundleSha256: input.bundle.sha256, bundleByteSize: input.bundle.byteSize } :
|
|
34077
|
+
...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
|
|
34078
|
+
...input.bundle ? { bundleSha256: input.bundle.sha256, bundleByteSize: input.bundle.byteSize } : carriedBundle,
|
|
33862
34079
|
publishedByUserId: input.principal.userId,
|
|
33863
34080
|
createdAt: previous?.createdAt ?? now,
|
|
33864
|
-
updatedAt: now
|
|
34081
|
+
updatedAt: now,
|
|
34082
|
+
revisionId: revisionIdOfRecord(recordFieldsOf(input, carriedBundle, carriedSkillMd)),
|
|
34083
|
+
revisionNumber: (previous?.revisionNumber ?? 0) + 1
|
|
33865
34084
|
};
|
|
33866
34085
|
this.skills.set(key, record);
|
|
33867
34086
|
if (previous?.bundleSha256 && input.bundle && previous.bundleSha256 !== input.bundle.sha256) {
|
|
@@ -33870,33 +34089,107 @@ class MemorySkillsStore {
|
|
|
33870
34089
|
return record;
|
|
33871
34090
|
}
|
|
33872
34091
|
async listSkills(principal) {
|
|
33873
|
-
return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
|
|
34092
|
+
return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
|
|
33874
34093
|
}
|
|
33875
34094
|
async getSkill(principal, slug) {
|
|
33876
34095
|
const skill = this.skills.get(skillKey(principal.orgId, slug));
|
|
33877
34096
|
return skill && skill.orgId === principal.orgId ? skill : null;
|
|
33878
34097
|
}
|
|
33879
|
-
async updateSkill(principal, slug, patch) {
|
|
34098
|
+
async updateSkill(principal, slug, patch, expectedRevisionId) {
|
|
33880
34099
|
const current = await this.getSkill(principal, slug);
|
|
33881
|
-
if (!current)
|
|
34100
|
+
if (!current || current.tombstonedAt)
|
|
34101
|
+
return null;
|
|
34102
|
+
if (expectedRevisionId !== current.revisionId) {
|
|
34103
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
|
|
34104
|
+
}
|
|
34105
|
+
const latest = this.skills.get(skillKey(principal.orgId, slug));
|
|
34106
|
+
if (!latest || latest.tombstonedAt)
|
|
33882
34107
|
return null;
|
|
33883
|
-
|
|
34108
|
+
if (latest.revisionId !== current.revisionId) {
|
|
34109
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, latest.revisionId);
|
|
34110
|
+
}
|
|
34111
|
+
const next = {
|
|
34112
|
+
...latest,
|
|
34113
|
+
...patch,
|
|
34114
|
+
updatedAt: nowIso(),
|
|
34115
|
+
revisionId: revisionIdOfRecord({ ...latest, ...patch }),
|
|
34116
|
+
revisionNumber: latest.revisionNumber + 1
|
|
34117
|
+
};
|
|
33884
34118
|
this.skills.set(skillKey(principal.orgId, slug), next);
|
|
33885
34119
|
return next;
|
|
33886
34120
|
}
|
|
33887
|
-
async deleteSkill(principal, slug) {
|
|
34121
|
+
async deleteSkill(principal, slug, tombstoneWindowMs) {
|
|
33888
34122
|
const current = await this.getSkill(principal, slug);
|
|
33889
34123
|
if (!current)
|
|
33890
|
-
return
|
|
33891
|
-
|
|
33892
|
-
|
|
33893
|
-
|
|
33894
|
-
|
|
34124
|
+
return null;
|
|
34125
|
+
if (!current.tombstonedAt) {
|
|
34126
|
+
const tombstoned = nowIso();
|
|
34127
|
+
const purgeAfter = new Date(Date.now() + tombstoneWindowMs).toISOString();
|
|
34128
|
+
const next = { ...current, tombstonedAt: tombstoned, tombstonePurgeAfter: purgeAfter, updatedAt: tombstoned };
|
|
34129
|
+
this.skills.set(skillKey(principal.orgId, slug), next);
|
|
34130
|
+
return next;
|
|
34131
|
+
}
|
|
34132
|
+
return current;
|
|
34133
|
+
}
|
|
34134
|
+
async purgeExpiredTombstones(principal) {
|
|
34135
|
+
const now = nowIso();
|
|
34136
|
+
const purged = [];
|
|
34137
|
+
for (const [key, skill] of this.skills) {
|
|
34138
|
+
if (skill.orgId !== principal.orgId || !skill.tombstonedAt || !skill.tombstonePurgeAfter)
|
|
34139
|
+
continue;
|
|
34140
|
+
if (skill.tombstonePurgeAfter > now)
|
|
34141
|
+
continue;
|
|
34142
|
+
this.skills.delete(key);
|
|
34143
|
+
if (skill.bundleSha256)
|
|
34144
|
+
this.collectOrphanBundle(principal.orgId, skill.bundleSha256);
|
|
34145
|
+
purged.push(skill);
|
|
34146
|
+
}
|
|
34147
|
+
return purged;
|
|
33895
34148
|
}
|
|
33896
34149
|
async getSkillBundle(principal, sha256) {
|
|
33897
34150
|
const bundle = this.bundles.get(skillKey(principal.orgId, sha256));
|
|
33898
34151
|
return bundle && bundle.orgId === principal.orgId ? bundle : null;
|
|
33899
34152
|
}
|
|
34153
|
+
async pinSkill(principal, slug, metadata = {}) {
|
|
34154
|
+
const pin = { orgId: principal.orgId, principal: principal.apiKeyId, slug, pinnedAt: nowIso(), metadata: { ...metadata } };
|
|
34155
|
+
this.pins.set(pinKey(principal.orgId, principal.apiKeyId, slug), pin);
|
|
34156
|
+
return pin;
|
|
34157
|
+
}
|
|
34158
|
+
async unpinSkill(principal, slug) {
|
|
34159
|
+
return this.pins.delete(pinKey(principal.orgId, principal.apiKeyId, slug));
|
|
34160
|
+
}
|
|
34161
|
+
async listPins(principal) {
|
|
34162
|
+
return Array.from(this.pins.values()).filter((pin) => pin.orgId === principal.orgId && pin.principal === principal.apiKeyId).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
|
|
34163
|
+
}
|
|
34164
|
+
async listTags(principal) {
|
|
34165
|
+
await this.purgeExpiredTombstones(principal);
|
|
34166
|
+
const tags = new Set;
|
|
34167
|
+
for (const skill of this.skills.values()) {
|
|
34168
|
+
if (skill.orgId !== principal.orgId)
|
|
34169
|
+
continue;
|
|
34170
|
+
for (const tag of skill.tags) {
|
|
34171
|
+
if (tag.trim())
|
|
34172
|
+
tags.add(tag);
|
|
34173
|
+
}
|
|
34174
|
+
}
|
|
34175
|
+
return [...tags].sort();
|
|
34176
|
+
}
|
|
34177
|
+
async listSkillsByTag(principal, tag) {
|
|
34178
|
+
await this.purgeExpiredTombstones(principal);
|
|
34179
|
+
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));
|
|
34180
|
+
}
|
|
34181
|
+
async listPinsByTag(principal, tag) {
|
|
34182
|
+
await this.purgeExpiredTombstones(principal);
|
|
34183
|
+
const taggedSlugs = new Set;
|
|
34184
|
+
for (const skill of this.skills.values()) {
|
|
34185
|
+
if (skill.orgId === principal.orgId && !skill.tombstonedAt && skill.tags.includes(tag))
|
|
34186
|
+
taggedSlugs.add(skill.slug);
|
|
34187
|
+
}
|
|
34188
|
+
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));
|
|
34189
|
+
}
|
|
34190
|
+
async listPublishedSlugs(principal) {
|
|
34191
|
+
return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt).map((skill) => skill.slug).sort();
|
|
34192
|
+
}
|
|
33900
34193
|
collectOrphanBundle(orgId, sha256) {
|
|
33901
34194
|
const referenced = Array.from(this.skills.values()).some((skill) => skill.orgId === orgId && skill.bundleSha256 === sha256);
|
|
33902
34195
|
if (!referenced)
|
|
@@ -33914,6 +34207,9 @@ class MemorySkillsStore {
|
|
|
33914
34207
|
function skillKey(orgId, slug) {
|
|
33915
34208
|
return `${orgId.length}:${orgId}:${slug}`;
|
|
33916
34209
|
}
|
|
34210
|
+
function pinKey(orgId, principal, slug) {
|
|
34211
|
+
return `${orgId.length}:${orgId}:${principal.length}:${principal}:${slug}`;
|
|
34212
|
+
}
|
|
33917
34213
|
|
|
33918
34214
|
class PostgresSkillsStore {
|
|
33919
34215
|
backend = { kind: "postgres", durable: true, label: "postgres" };
|
|
@@ -33933,6 +34229,14 @@ class PostgresSkillsStore {
|
|
|
33933
34229
|
} catch (error) {
|
|
33934
34230
|
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
34231
|
}
|
|
34232
|
+
await this.backfillLegacyRevisions();
|
|
34233
|
+
}
|
|
34234
|
+
async backfillLegacyRevisions() {
|
|
34235
|
+
const rows = await this.sql`SELECT * FROM skills_registry WHERE revision_id = ${""}`;
|
|
34236
|
+
for (const row of rows) {
|
|
34237
|
+
const record = rowToSkill(row);
|
|
34238
|
+
await this.sql`UPDATE skills_registry SET revision_id = ${revisionIdOfRecord(record)} WHERE org_id = ${record.orgId} AND slug = ${record.slug}`;
|
|
34239
|
+
}
|
|
33936
34240
|
}
|
|
33937
34241
|
async close() {
|
|
33938
34242
|
await this.sql.close?.();
|
|
@@ -34167,8 +34471,33 @@ class PostgresSkillsStore {
|
|
|
34167
34471
|
async publishSkill(input) {
|
|
34168
34472
|
const orgId = input.principal.orgId;
|
|
34169
34473
|
return await this.sql.begin(async (tx) => {
|
|
34170
|
-
const previousRows = await tx`
|
|
34171
|
-
|
|
34474
|
+
const previousRows = await tx`
|
|
34475
|
+
SELECT revision_id, revision_number, bundle_sha256, bundle_byte_size, skill_md, tombstoned_at
|
|
34476
|
+
FROM skills_registry WHERE org_id = ${orgId} AND slug = ${input.slug} LIMIT 1
|
|
34477
|
+
`;
|
|
34478
|
+
const previous = previousRows[0];
|
|
34479
|
+
const previousSha = typeof previous?.bundle_sha256 === "string" ? String(previous.bundle_sha256) : null;
|
|
34480
|
+
const previousRevisionId = typeof previous?.revision_id === "string" && previous.revision_id ? String(previous.revision_id) : null;
|
|
34481
|
+
const tombstoned = previous?.tombstoned_at != null;
|
|
34482
|
+
const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : typeof previous?.skill_md === "string" ? String(previous.skill_md) : null;
|
|
34483
|
+
if (previous && !tombstoned && input.expectedRevisionId !== previousRevisionId) {
|
|
34484
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
|
|
34485
|
+
}
|
|
34486
|
+
const carriedSha = input.bundle?.sha256 ?? previousSha;
|
|
34487
|
+
const carriedSize = input.bundle?.byteSize ?? (previous?.bundle_byte_size == null ? null : Number(previous.bundle_byte_size));
|
|
34488
|
+
const revisionId = revisionIdOfRecord({
|
|
34489
|
+
slug: input.slug,
|
|
34490
|
+
displayName: input.displayName,
|
|
34491
|
+
description: input.description,
|
|
34492
|
+
category: input.category,
|
|
34493
|
+
tags: input.tags,
|
|
34494
|
+
source: input.source,
|
|
34495
|
+
kind: input.kind,
|
|
34496
|
+
...input.version ? { version: input.version } : {},
|
|
34497
|
+
...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
|
|
34498
|
+
...carriedSha ? { bundleSha256: carriedSha } : {},
|
|
34499
|
+
...carriedSize === null || carriedSize === undefined ? {} : { bundleByteSize: carriedSize }
|
|
34500
|
+
});
|
|
34172
34501
|
if (input.bundle) {
|
|
34173
34502
|
await tx`
|
|
34174
34503
|
INSERT INTO skills_bundles (org_id, sha256, byte_size, content_type, storage_kind, storage_key, body_blob)
|
|
@@ -34183,10 +34512,10 @@ class PostgresSkillsStore {
|
|
|
34183
34512
|
}
|
|
34184
34513
|
const rows = await tx`
|
|
34185
34514
|
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)
|
|
34515
|
+
bundle_sha256, bundle_byte_size, published_by_user_id, revision_id, revision_number, updated_at)
|
|
34187
34516
|
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())
|
|
34517
|
+
${input.source}, ${input.kind}, ${input.version ?? null}, ${carriedSkillMd},
|
|
34518
|
+
${input.bundle?.sha256 ?? null}, ${input.bundle?.byteSize ?? null}, ${input.principal.userId}, ${revisionId}, 1, now())
|
|
34190
34519
|
ON CONFLICT (org_id, slug) DO UPDATE SET
|
|
34191
34520
|
display_name = EXCLUDED.display_name,
|
|
34192
34521
|
description = EXCLUDED.description,
|
|
@@ -34201,9 +34530,23 @@ class PostgresSkillsStore {
|
|
|
34201
34530
|
bundle_sha256 = COALESCE(EXCLUDED.bundle_sha256, skills_registry.bundle_sha256),
|
|
34202
34531
|
bundle_byte_size = COALESCE(EXCLUDED.bundle_byte_size, skills_registry.bundle_byte_size),
|
|
34203
34532
|
published_by_user_id = EXCLUDED.published_by_user_id,
|
|
34533
|
+
revision_id = EXCLUDED.revision_id,
|
|
34534
|
+
-- Current + 1, not EXCLUDED.revision_number: the insert path minted 1, but on
|
|
34535
|
+
-- the update path the ACTUAL row's counter is the truth (it may have advanced
|
|
34536
|
+
-- since the pre-read, which is exactly what the WHERE guard below detects).
|
|
34537
|
+
revision_number = skills_registry.revision_number + 1,
|
|
34538
|
+
tombstoned_at = NULL,
|
|
34539
|
+
tombstone_purge_after = NULL,
|
|
34204
34540
|
updated_at = EXCLUDED.updated_at
|
|
34541
|
+
WHERE skills_registry.tombstoned_at IS NOT NULL
|
|
34542
|
+
OR skills_registry.revision_id = ${input.expectedRevisionId ?? NO_REVISION_SENTINEL2}
|
|
34205
34543
|
RETURNING *
|
|
34206
34544
|
`;
|
|
34545
|
+
if (!rows[0]) {
|
|
34546
|
+
const current = await tx`SELECT revision_id FROM skills_registry WHERE org_id = ${orgId} AND slug = ${input.slug} LIMIT 1`;
|
|
34547
|
+
const currentId = current[0] && typeof current[0].revision_id === "string" ? String(current[0].revision_id) : null;
|
|
34548
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, currentId);
|
|
34549
|
+
}
|
|
34207
34550
|
if (previousSha && input.bundle && previousSha !== input.bundle.sha256) {
|
|
34208
34551
|
await tx`
|
|
34209
34552
|
DELETE FROM skills_bundles
|
|
@@ -34211,55 +34554,182 @@ class PostgresSkillsStore {
|
|
|
34211
34554
|
AND NOT EXISTS (SELECT 1 FROM skills_registry WHERE org_id = ${orgId} AND bundle_sha256 = ${previousSha})
|
|
34212
34555
|
`;
|
|
34213
34556
|
}
|
|
34557
|
+
await tx`DELETE FROM skills_tags WHERE org_id = ${orgId} AND slug = ${input.slug}`;
|
|
34558
|
+
for (const tag of input.tags) {
|
|
34559
|
+
if (!tag.trim())
|
|
34560
|
+
continue;
|
|
34561
|
+
await tx`
|
|
34562
|
+
INSERT INTO skills_tags (org_id, slug, tag) VALUES (${orgId}, ${input.slug}, ${tag})
|
|
34563
|
+
ON CONFLICT DO NOTHING
|
|
34564
|
+
`;
|
|
34565
|
+
}
|
|
34214
34566
|
return rowToSkill(rows[0]);
|
|
34215
34567
|
});
|
|
34216
34568
|
}
|
|
34217
34569
|
async listSkills(principal) {
|
|
34218
|
-
|
|
34570
|
+
await this.purgeExpiredTombstones(principal);
|
|
34571
|
+
const rows = await this.sql`
|
|
34572
|
+
SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND tombstoned_at IS NULL ORDER BY slug ASC
|
|
34573
|
+
`;
|
|
34219
34574
|
return rows.map(rowToSkill);
|
|
34220
34575
|
}
|
|
34221
34576
|
async getSkill(principal, slug) {
|
|
34222
34577
|
const rows = await this.sql`SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1`;
|
|
34223
34578
|
return rows[0] ? rowToSkill(rows[0]) : null;
|
|
34224
34579
|
}
|
|
34225
|
-
async updateSkill(principal, slug, patch) {
|
|
34580
|
+
async updateSkill(principal, slug, patch, expectedRevisionId) {
|
|
34226
34581
|
const current = await this.getSkill(principal, slug);
|
|
34227
|
-
if (!current)
|
|
34582
|
+
if (!current || current.tombstonedAt)
|
|
34228
34583
|
return null;
|
|
34584
|
+
if (expectedRevisionId !== current.revisionId) {
|
|
34585
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
|
|
34586
|
+
}
|
|
34229
34587
|
const next = { ...current, ...patch };
|
|
34230
|
-
|
|
34231
|
-
|
|
34232
|
-
|
|
34233
|
-
|
|
34234
|
-
|
|
34235
|
-
|
|
34236
|
-
|
|
34237
|
-
|
|
34238
|
-
|
|
34588
|
+
return await this.sql.begin(async (tx) => {
|
|
34589
|
+
const revisionId = revisionIdOfRecord(next);
|
|
34590
|
+
const updated = await tx`
|
|
34591
|
+
UPDATE skills_registry
|
|
34592
|
+
SET display_name = ${next.displayName}, description = ${next.description}, category = ${next.category},
|
|
34593
|
+
tags_json = ${JSON.stringify(next.tags)}::jsonb, kind = ${next.kind}, version = ${next.version ?? null},
|
|
34594
|
+
skill_md = ${next.skillMd ?? null}, revision_id = ${revisionId}, revision_number = revision_number + 1, updated_at = now()
|
|
34595
|
+
WHERE org_id = ${principal.orgId} AND slug = ${slug} AND tombstoned_at IS NULL AND revision_id = ${current.revisionId}
|
|
34596
|
+
RETURNING *
|
|
34597
|
+
`;
|
|
34598
|
+
if (!updated[0]) {
|
|
34599
|
+
const nowRows = await tx`
|
|
34600
|
+
SELECT revision_id, tombstoned_at FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1
|
|
34601
|
+
`;
|
|
34602
|
+
if (nowRows[0] && nowRows[0].tombstoned_at == null) {
|
|
34603
|
+
const currentId = String(nowRows[0].revision_id);
|
|
34604
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, currentId);
|
|
34605
|
+
}
|
|
34606
|
+
return null;
|
|
34607
|
+
}
|
|
34608
|
+
await tx`DELETE FROM skills_tags WHERE org_id = ${principal.orgId} AND slug = ${slug}`;
|
|
34609
|
+
for (const tag of next.tags) {
|
|
34610
|
+
if (!tag.trim())
|
|
34611
|
+
continue;
|
|
34612
|
+
await tx`
|
|
34613
|
+
INSERT INTO skills_tags (org_id, slug, tag) VALUES (${principal.orgId}, ${slug}, ${tag})
|
|
34614
|
+
ON CONFLICT DO NOTHING
|
|
34615
|
+
`;
|
|
34616
|
+
}
|
|
34617
|
+
return rowToSkill(updated[0]);
|
|
34618
|
+
});
|
|
34239
34619
|
}
|
|
34240
|
-
async deleteSkill(principal, slug) {
|
|
34620
|
+
async deleteSkill(principal, slug, tombstoneWindowMs) {
|
|
34241
34621
|
return await this.sql.begin(async (tx) => {
|
|
34622
|
+
const existingRows = await tx`
|
|
34623
|
+
SELECT tombstoned_at FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1
|
|
34624
|
+
`;
|
|
34625
|
+
if (!existingRows[0])
|
|
34626
|
+
return null;
|
|
34627
|
+
if (existingRows[0].tombstoned_at != null) {
|
|
34628
|
+
const rows2 = await tx`SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1`;
|
|
34629
|
+
return rowToSkill(rows2[0]);
|
|
34630
|
+
}
|
|
34242
34631
|
const rows = await tx`
|
|
34243
|
-
|
|
34244
|
-
|
|
34632
|
+
UPDATE skills_registry
|
|
34633
|
+
SET tombstoned_at = now(), tombstone_purge_after = now() + (${tombstoneWindowMs}::int * interval '1 millisecond'), updated_at = now()
|
|
34634
|
+
WHERE org_id = ${principal.orgId} AND slug = ${slug}
|
|
34635
|
+
RETURNING *
|
|
34245
34636
|
`;
|
|
34246
|
-
|
|
34247
|
-
|
|
34248
|
-
|
|
34249
|
-
|
|
34637
|
+
return rows[0] ? rowToSkill(rows[0]) : null;
|
|
34638
|
+
});
|
|
34639
|
+
}
|
|
34640
|
+
async purgeExpiredTombstones(principal) {
|
|
34641
|
+
return await this.sql.begin(async (tx) => {
|
|
34642
|
+
const expiredRows = await tx`
|
|
34643
|
+
SELECT * FROM skills_registry
|
|
34644
|
+
WHERE org_id = ${principal.orgId} AND tombstoned_at IS NOT NULL AND tombstone_purge_after <= now()
|
|
34645
|
+
`;
|
|
34646
|
+
if (!expiredRows.length)
|
|
34647
|
+
return [];
|
|
34648
|
+
const purged = [];
|
|
34649
|
+
for (const row of expiredRows) {
|
|
34650
|
+
const record = rowToSkill(row);
|
|
34250
34651
|
await tx`
|
|
34251
|
-
DELETE FROM
|
|
34252
|
-
|
|
34253
|
-
|
|
34652
|
+
DELETE FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${record.slug} AND tombstone_purge_after <= now()
|
|
34653
|
+
`;
|
|
34654
|
+
await tx`DELETE FROM skills_tags WHERE org_id = ${principal.orgId} AND slug = ${record.slug}`;
|
|
34655
|
+
await tx`
|
|
34656
|
+
DELETE FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${record.slug} AND tombstone_purge_after <= now()
|
|
34254
34657
|
`;
|
|
34658
|
+
if (record.bundleSha256) {
|
|
34659
|
+
await tx`
|
|
34660
|
+
DELETE FROM skills_bundles
|
|
34661
|
+
WHERE org_id = ${principal.orgId} AND sha256 = ${record.bundleSha256}
|
|
34662
|
+
AND NOT EXISTS (SELECT 1 FROM skills_registry WHERE org_id = ${principal.orgId} AND bundle_sha256 = ${record.bundleSha256})
|
|
34663
|
+
`;
|
|
34664
|
+
}
|
|
34665
|
+
purged.push(record);
|
|
34255
34666
|
}
|
|
34256
|
-
return
|
|
34667
|
+
return purged;
|
|
34257
34668
|
});
|
|
34258
34669
|
}
|
|
34259
34670
|
async getSkillBundle(principal, sha256) {
|
|
34260
34671
|
const rows = await this.sql`SELECT * FROM skills_bundles WHERE org_id = ${principal.orgId} AND sha256 = ${sha256} LIMIT 1`;
|
|
34261
34672
|
return rows[0] ? rowToSkillBundle(rows[0]) : null;
|
|
34262
34673
|
}
|
|
34674
|
+
async pinSkill(principal, slug, metadata = {}) {
|
|
34675
|
+
const rows = await this.sql`
|
|
34676
|
+
INSERT INTO skills_pins (org_id, principal, slug, pinned_at, metadata_json)
|
|
34677
|
+
VALUES (${principal.orgId}, ${principal.apiKeyId}, ${slug}, now(), ${JSON.stringify(metadata)}::jsonb)
|
|
34678
|
+
ON CONFLICT (org_id, principal, slug) DO UPDATE SET
|
|
34679
|
+
pinned_at = now(),
|
|
34680
|
+
metadata_json = EXCLUDED.metadata_json
|
|
34681
|
+
RETURNING *
|
|
34682
|
+
`;
|
|
34683
|
+
return rowToPin(rows[0]);
|
|
34684
|
+
}
|
|
34685
|
+
async unpinSkill(principal, slug) {
|
|
34686
|
+
const rows = await this.sql`
|
|
34687
|
+
DELETE FROM skills_pins WHERE org_id = ${principal.orgId} AND principal = ${principal.apiKeyId} AND slug = ${slug}
|
|
34688
|
+
RETURNING 1 AS present
|
|
34689
|
+
`;
|
|
34690
|
+
return rows.length > 0;
|
|
34691
|
+
}
|
|
34692
|
+
async listPins(principal) {
|
|
34693
|
+
const rows = await this.sql`
|
|
34694
|
+
SELECT * FROM skills_pins WHERE org_id = ${principal.orgId} AND principal = ${principal.apiKeyId} ORDER BY slug ASC
|
|
34695
|
+
`;
|
|
34696
|
+
return rows.map(rowToPin);
|
|
34697
|
+
}
|
|
34698
|
+
async listTags(principal) {
|
|
34699
|
+
await this.purgeExpiredTombstones(principal);
|
|
34700
|
+
const rows = await this.sql`
|
|
34701
|
+
SELECT DISTINCT tag FROM skills_tags WHERE org_id = ${principal.orgId} ORDER BY tag ASC
|
|
34702
|
+
`;
|
|
34703
|
+
return rows.map((row) => String(row.tag));
|
|
34704
|
+
}
|
|
34705
|
+
async listSkillsByTag(principal, tag) {
|
|
34706
|
+
await this.purgeExpiredTombstones(principal);
|
|
34707
|
+
const rows = await this.sql`
|
|
34708
|
+
SELECT s.* FROM skills_registry s
|
|
34709
|
+
JOIN skills_tags t ON t.org_id = s.org_id AND t.slug = s.slug
|
|
34710
|
+
WHERE t.org_id = ${principal.orgId} AND t.tag = ${tag} AND s.tombstoned_at IS NULL
|
|
34711
|
+
ORDER BY s.slug ASC
|
|
34712
|
+
`;
|
|
34713
|
+
return rows.map(rowToSkill);
|
|
34714
|
+
}
|
|
34715
|
+
async listPinsByTag(principal, tag) {
|
|
34716
|
+
await this.purgeExpiredTombstones(principal);
|
|
34717
|
+
const rows = await this.sql`
|
|
34718
|
+
SELECT p.* FROM skills_pins p
|
|
34719
|
+
JOIN skills_tags t ON t.org_id = p.org_id AND t.slug = p.slug
|
|
34720
|
+
JOIN skills_registry s ON s.org_id = p.org_id AND s.slug = p.slug
|
|
34721
|
+
WHERE p.org_id = ${principal.orgId} AND p.principal = ${principal.apiKeyId}
|
|
34722
|
+
AND t.tag = ${tag} AND s.tombstoned_at IS NULL
|
|
34723
|
+
ORDER BY p.slug ASC
|
|
34724
|
+
`;
|
|
34725
|
+
return rows.map(rowToPin);
|
|
34726
|
+
}
|
|
34727
|
+
async listPublishedSlugs(principal) {
|
|
34728
|
+
const rows = await this.sql`
|
|
34729
|
+
SELECT slug FROM skills_registry WHERE org_id = ${principal.orgId} AND tombstoned_at IS NULL ORDER BY slug ASC
|
|
34730
|
+
`;
|
|
34731
|
+
return rows.map((row) => String(row.slug));
|
|
34732
|
+
}
|
|
34263
34733
|
async collectOrphanBundle(orgId, sha256) {
|
|
34264
34734
|
await this.sql`
|
|
34265
34735
|
DELETE FROM skills_bundles
|
|
@@ -34268,6 +34738,7 @@ class PostgresSkillsStore {
|
|
|
34268
34738
|
`;
|
|
34269
34739
|
}
|
|
34270
34740
|
}
|
|
34741
|
+
var NO_REVISION_SENTINEL2 = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
34271
34742
|
function isUniqueViolation(error) {
|
|
34272
34743
|
const code = error?.code;
|
|
34273
34744
|
if (code === "23505" || code === 23505)
|
|
@@ -34329,7 +34800,7 @@ ${summary}
|
|
|
34329
34800
|
textArtifact(run, "show-notes.md", `# Show Notes
|
|
34330
34801
|
|
|
34331
34802
|
- ${summary}
|
|
34332
|
-
- Generated by the
|
|
34803
|
+
- Generated by the skills deterministic worker.
|
|
34333
34804
|
`),
|
|
34334
34805
|
textArtifact(run, "clips.csv", `start,end,title,summary
|
|
34335
34806
|
00:00,00:30,"Opening","${csv(summary)}"
|
|
@@ -34363,7 +34834,7 @@ function textArtifact(run, relativePath, bodyText, contentType = relativePath.en
|
|
|
34363
34834
|
relativePath,
|
|
34364
34835
|
contentType,
|
|
34365
34836
|
byteSize: bytes.byteLength,
|
|
34366
|
-
sha256:
|
|
34837
|
+
sha256: createHash5("sha256").update(bytes).digest("hex"),
|
|
34367
34838
|
visibility: "private"
|
|
34368
34839
|
},
|
|
34369
34840
|
body: { relativePath, bodyText, contentType }
|
|
@@ -34582,7 +35053,7 @@ var DEVELOPMENT_TOOLS_SKILLS = [
|
|
|
34582
35053
|
{
|
|
34583
35054
|
name: "monitor",
|
|
34584
35055
|
displayName: "Monitor",
|
|
34585
|
-
description: "Operate the
|
|
35056
|
+
description: "Operate the monitor MCP for machine health, processes, cron jobs, and cleanup workflows",
|
|
34586
35057
|
category: "Development Tools",
|
|
34587
35058
|
tags: ["monitoring", "mcp", "processes", "operations"]
|
|
34588
35059
|
},
|
|
@@ -34628,6 +35099,14 @@ var DEVELOPMENT_TOOLS_SKILLS = [
|
|
|
34628
35099
|
description: "Validate configuration files for syntax and schema compliance",
|
|
34629
35100
|
category: "Development Tools",
|
|
34630
35101
|
tags: ["config", "validation", "schema", "linting"]
|
|
35102
|
+
},
|
|
35103
|
+
{
|
|
35104
|
+
name: "session-inject-monitor",
|
|
35105
|
+
displayName: "Session Inject Monitor",
|
|
35106
|
+
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",
|
|
35107
|
+
category: "Development Tools",
|
|
35108
|
+
tags: ["monitor", "session", "injection", "automation", "wake"],
|
|
35109
|
+
kind: "instruction"
|
|
34631
35110
|
}
|
|
34632
35111
|
];
|
|
34633
35112
|
|
|
@@ -35015,7 +35494,7 @@ var DESIGN_BRANDING_SKILLS = [
|
|
|
35015
35494
|
displayName: "Site Analyze",
|
|
35016
35495
|
description: "Analyze any website's design system \u2014 detects shadcn/ui, Tailwind, extracts colors, typography, and components via Playwright + Claude Vision.",
|
|
35017
35496
|
category: "Design & Branding",
|
|
35018
|
-
tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "
|
|
35497
|
+
tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "styles"]
|
|
35019
35498
|
}
|
|
35020
35499
|
];
|
|
35021
35500
|
|