@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.
Files changed (42) hide show
  1. package/README.md +61 -22
  2. package/bin/index.js +2120 -670
  3. package/bin/mcp.js +555 -249
  4. package/bin/migrate.js +229 -33
  5. package/bin/server.js +1542 -327
  6. package/bin/worker.js +716 -207
  7. package/dist/cli/commands/registry-reconcile.d.ts +2 -0
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +682 -276
  10. package/dist/lib/agent-sync.d.ts +26 -6
  11. package/dist/lib/auth-store.d.ts +37 -0
  12. package/dist/lib/config.d.ts +51 -0
  13. package/dist/lib/home-census.d.ts +4 -0
  14. package/dist/lib/home-migration.d.ts +8 -9
  15. package/dist/lib/native-storage.d.ts +29 -1
  16. package/dist/lib/portable-skills.d.ts +49 -6
  17. package/dist/lib/pull.d.ts +31 -0
  18. package/dist/lib/registry-reconcile.d.ts +114 -0
  19. package/dist/lib/registry-types.d.ts +9 -0
  20. package/dist/lib/registry.d.ts +7 -4
  21. package/dist/lib/remote-client.d.ts +118 -1
  22. package/dist/lib/remote-registry.d.ts +26 -0
  23. package/dist/lib/revision.d.ts +29 -0
  24. package/dist/lib/run-routing.d.ts +60 -0
  25. package/dist/sdk/index.js +14412 -13338
  26. package/dist/server/app.d.ts +4 -1
  27. package/dist/server/config.d.ts +12 -2
  28. package/dist/server/rows.d.ts +2 -1
  29. package/dist/server/skills-api.d.ts +108 -6
  30. package/dist/server/sqlite-store.d.ts +20 -3
  31. package/dist/server/store-fixtures.d.ts +6 -0
  32. package/dist/server/store.d.ts +31 -5
  33. package/dist/server/types.d.ts +98 -3
  34. package/dist/storage.d.ts +1 -1
  35. package/dist/storage.js +57 -2
  36. package/migrations/postgres/0004_hosted_pins.sql +28 -0
  37. package/migrations/postgres/0005_revision_tombstone_registry.sql +37 -0
  38. package/migrations/postgres/0005_tag_projection.sql +36 -0
  39. package/migrations/sqlite/0004_hosted_pins.sql +21 -0
  40. package/migrations/sqlite/0005_revision_tombstone_registry.sql +18 -0
  41. package/migrations/sqlite/0005_tag_projection.sql +25 -0
  42. 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/server/auth.ts
32851
- import { createHash as createHash3 } from "crypto";
32852
- function hashApiKey(token) {
32853
- return createHash3("sha256").update(token).digest("hex");
32854
- }
32855
- function publicPrincipal(partial = {}) {
32856
- return {
32857
- apiKeyId: partial.apiKeyId || "key_dev",
32858
- orgId: partial.orgId || "org_dev",
32859
- orgSlug: partial.orgSlug || "dev",
32860
- orgName: partial.orgName || "Development",
32861
- userId: partial.userId || "user_dev",
32862
- email: partial.email || "dev@example.com",
32863
- role: partial.role || "owner",
32864
- scopes: partial.scopes || ["skills:read", "runs:write"]
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/handlers.ts
33061
- import { createHash as createHash4 } from "crypto";
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/store.ts
33064
- import { randomUUID as randomUUID3 } from "crypto";
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/types.ts
33067
- class StaleLeaseGenerationError extends Error {
33068
- runId;
33069
- expectedGeneration;
33070
- currentGeneration;
33071
- status;
33072
- constructor(runId, expectedGeneration, currentGeneration, status) {
33073
- super(`late transition rejected for run ${runId}: expected lease_generation ${expectedGeneration}, ` + `current ${currentGeneration} (status ${status})`);
33074
- this.name = "StaleLeaseGenerationError";
33075
- this.runId = runId;
33076
- this.expectedGeneration = expectedGeneration;
33077
- this.currentGeneration = currentGeneration;
33078
- this.status = status;
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/sqlite-store.ts
33215
- import { Database } from "bun:sqlite";
33216
- import { randomUUID as randomUUID2 } from "crypto";
33217
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
33218
- import { dirname as dirname4, join as join5 } from "path";
33219
-
33220
- // src/server/migrations-dir.ts
33221
- import { existsSync as existsSync2 } from "fs";
33222
- import { dirname as dirname3, join as join4 } from "path";
33223
- var MIGRATION_DIALECTS = ["postgres", "sqlite"];
33224
- var MAX_WALK_UP = 6;
33225
- function findMigrationsRoot(startDirs = defaultStartDirs()) {
33226
- for (const start of startDirs) {
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
- function resolveMigrationsDir(dialect, root3 = findMigrationsRoot()) {
33241
- if (!root3) {
33242
- throw new Error(`could not locate the migrations directory for dialect "${dialect}". ` + `Expected a migrations/${dialect}/ folder alongside the package root.`);
33243
- }
33244
- const dir = join4(root3, dialect);
33245
- if (!existsSync2(dir)) {
33246
- throw new Error(`migrations directory not found: ${dir}`);
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
- function defaultStartDirs() {
33251
- const dirs = [import.meta.dir];
33252
- const cwd = process.cwd();
33253
- if (cwd && cwd !== import.meta.dir)
33254
- dirs.push(cwd);
33255
- return dirs;
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
- input.skillMd ?? null,
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
- now
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
- return this.all("SELECT * FROM skills_registry WHERE org_id = ? ORDER BY slug ASC", [principal.orgId]).map(rowToSkill);
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
- const row = this.get(`UPDATE skills_registry
33586
- SET display_name = ?, description = ?, category = ?, tags_json = ?, kind = ?, version = ?, skill_md = ?, updated_at = ?
33587
- WHERE org_id = ? AND slug = ?
33588
- RETURNING *`, [
33589
- next.displayName,
33590
- next.description,
33591
- next.category,
33592
- JSON.stringify(next.tags),
33593
- next.kind,
33594
- next.version ?? null,
33595
- next.skillMd ?? null,
33596
- nowIso(),
33597
- principal.orgId,
33598
- slug
33599
- ]);
33600
- return row ? rowToSkill(row) : null;
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 bundle_sha256 FROM skills_registry WHERE org_id = ? AND slug = ?", [principal.orgId, slug]);
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 false;
33607
- this.db.run("DELETE FROM skills_registry WHERE org_id = ? AND slug = ?", [principal.orgId, slug]);
33608
- if (typeof existing.bundle_sha256 === "string")
33609
- this.collectOrphanBundle(principal.orgId, existing.bundle_sha256);
33610
- return true;
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
- ...input.skillMd ? { skillMd: input.skillMd } : {},
33861
- ...input.bundle ? { bundleSha256: input.bundle.sha256, bundleByteSize: input.bundle.byteSize } : previous?.bundleSha256 ? { bundleSha256: previous.bundleSha256, ...previous.bundleByteSize === undefined ? {} : { bundleByteSize: previous.bundleByteSize } } : {},
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
- const next = { ...current, ...patch, updatedAt: nowIso() };
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 false;
33891
- this.skills.delete(skillKey(principal.orgId, slug));
33892
- if (current.bundleSha256)
33893
- this.collectOrphanBundle(principal.orgId, current.bundleSha256);
33894
- return true;
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`SELECT bundle_sha256 FROM skills_registry WHERE org_id = ${orgId} AND slug = ${input.slug} LIMIT 1`;
34171
- const previousSha = typeof previousRows[0]?.bundle_sha256 === "string" ? String(previousRows[0].bundle_sha256) : null;
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}, ${input.skillMd ?? 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
- const rows = await this.sql`SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} ORDER BY slug ASC`;
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
- const rows = await this.sql`
34231
- UPDATE skills_registry
34232
- SET display_name = ${next.displayName}, description = ${next.description}, category = ${next.category},
34233
- tags_json = ${JSON.stringify(next.tags)}::jsonb, kind = ${next.kind}, version = ${next.version ?? null},
34234
- skill_md = ${next.skillMd ?? null}, updated_at = now()
34235
- WHERE org_id = ${principal.orgId} AND slug = ${slug}
34236
- RETURNING *
34237
- `;
34238
- return rows[0] ? rowToSkill(rows[0]) : null;
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
- DELETE FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug}
34244
- RETURNING bundle_sha256
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
- if (!rows[0])
34247
- return false;
34248
- const sha = rows[0].bundle_sha256;
34249
- if (typeof sha === "string") {
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 skills_bundles
34252
- WHERE org_id = ${principal.orgId} AND sha256 = ${sha}
34253
- AND NOT EXISTS (SELECT 1 FROM skills_registry WHERE org_id = ${principal.orgId} AND bundle_sha256 = ${sha})
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 true;
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 open-skills deterministic worker.
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: createHash4("sha256").update(bytes).digest("hex"),
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
- await store.appendLog(run.id, run.orgId, "error", message);
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
- await store.appendLog(run.id, run.orgId, "warn", `late write rejected: run no longer owned at lease_generation ${run.leaseGeneration}`);
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 open-monitor MCP for machine health, processes, cron jobs, and cleanup workflows",
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", "open-styles"]
35527
+ tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "styles"]
35019
35528
  }
35020
35529
  ];
35021
35530