@hasna/skills 0.8.12 → 0.9.2

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/bin/worker.js CHANGED
@@ -24125,7 +24125,7 @@ import { randomUUID as randomUUID6 } from "crypto";
24125
24125
  // package.json
24126
24126
  var package_default = {
24127
24127
  name: "@hasna/skills",
24128
- version: "0.8.12",
24128
+ version: "0.9.2",
24129
24129
  description: "Skills library for AI coding agents",
24130
24130
  type: "module",
24131
24131
  bin: {
@@ -34682,6 +34682,9 @@ class MemorySkillSelectionStore {
34682
34682
  async getProfile(p2, id) {
34683
34683
  return structuredClone(this.profiles.get(this.key(p2.orgId, id)) ?? null);
34684
34684
  }
34685
+ async profilesReferencingSkill(p2, slug) {
34686
+ return [...this.profiles.values()].filter((profile2) => profile2.workspaceId === p2.orgId && profile2.selections.some((selection) => selection.slug === slug)).map((profile2) => profile2.id).sort();
34687
+ }
34685
34688
  async saveProfile(p2, id, selected, expected) {
34686
34689
  const key = this.key(p2.orgId, id), previous = this.profiles.get(key);
34687
34690
  if ((previous?.revision ?? null) !== expected)
@@ -34719,14 +34722,25 @@ class SqliteSkillSelectionStore {
34719
34722
  constructor(db) {
34720
34723
  this.db = db;
34721
34724
  }
34725
+ async profilesReferencingSkill(p2, slug) {
34726
+ const rows = this.db.query("SELECT profile_id,selections_json FROM skills_profiles WHERE org_id=?").all(p2.orgId);
34727
+ return rows.filter((row) => selections(row.selections_json).some((selection) => selection.slug === slug)).map((row) => String(row.profile_id)).sort();
34728
+ }
34722
34729
  async getProfile(p2, id) {
34723
34730
  const row = this.db.query("SELECT * FROM skills_profiles WHERE org_id=? AND profile_id=?").get(p2.orgId, id);
34724
34731
  return row ? profile(row) : null;
34725
34732
  }
34726
34733
  async saveProfile(p2, id, selected, expected) {
34727
- const revision = randomUUID2(), timestamp = new Date().toISOString();
34728
- const row = expected === null ? this.db.query("INSERT INTO skills_profiles(org_id,profile_id,revision,selections_json,updated_at) VALUES(?,?,?,?,?) ON CONFLICT(org_id,profile_id) DO NOTHING RETURNING *").get(p2.orgId, id, revision, JSON.stringify(selected), timestamp) : this.db.query("UPDATE skills_profiles SET revision=?,selections_json=?,updated_at=? WHERE org_id=? AND profile_id=? AND revision=? RETURNING *").get(revision, JSON.stringify(selected), timestamp, p2.orgId, id, expected);
34729
- return row ? profile(row) : null;
34734
+ return this.db.transaction(() => {
34735
+ for (const selection of [...selected].sort((a3, b3) => a3.slug.localeCompare(b3.slug))) {
34736
+ const skill = this.db.query("SELECT lifecycle FROM skills_registry WHERE org_id=? AND slug=?").get(p2.orgId, selection.slug);
34737
+ if (skill?.lifecycle === "archived")
34738
+ return null;
34739
+ }
34740
+ const revision = randomUUID2(), timestamp = new Date().toISOString();
34741
+ const row = expected === null ? this.db.query("INSERT INTO skills_profiles(org_id,profile_id,revision,selections_json,updated_at) VALUES(?,?,?,?,?) ON CONFLICT(org_id,profile_id) DO NOTHING RETURNING *").get(p2.orgId, id, revision, JSON.stringify(selected), timestamp) : this.db.query("UPDATE skills_profiles SET revision=?,selections_json=?,updated_at=? WHERE org_id=? AND profile_id=? AND revision=? RETURNING *").get(revision, JSON.stringify(selected), timestamp, p2.orgId, id, expected);
34742
+ return row ? profile(row) : null;
34743
+ }).immediate();
34730
34744
  }
34731
34745
  async getStationState(p2, id) {
34732
34746
  const row = this.db.query("SELECT * FROM skills_station_state WHERE org_id=? AND actor_id=? AND station_id=?").get(p2.orgId, p2.userId, id);
@@ -34745,14 +34759,25 @@ class PostgresSkillSelectionStore {
34745
34759
  constructor(sql) {
34746
34760
  this.sql = sql;
34747
34761
  }
34762
+ async profilesReferencingSkill(p2, slug) {
34763
+ const rows = await this.sql`SELECT profile_id,selections_json FROM skills_profiles WHERE org_id=${p2.orgId}`;
34764
+ return rows.filter((row) => selections(row.selections_json).some((selection) => selection.slug === slug)).map((row) => String(row.profile_id)).sort();
34765
+ }
34748
34766
  async getProfile(p2, id) {
34749
34767
  const rows = await this.sql`SELECT * FROM skills_profiles WHERE org_id=${p2.orgId} AND profile_id=${id}`;
34750
34768
  return rows[0] ? profile(rows[0]) : null;
34751
34769
  }
34752
34770
  async saveProfile(p2, id, selected, expected) {
34753
- const revision = randomUUID2(), timestamp = new Date().toISOString();
34754
- const rows = expected === null ? await this.sql`INSERT INTO skills_profiles(org_id,profile_id,revision,selections_json,updated_at) VALUES(${p2.orgId},${id},${revision},${JSON.stringify(selected)}::jsonb,${timestamp}) ON CONFLICT(org_id,profile_id) DO NOTHING RETURNING *` : await this.sql`UPDATE skills_profiles SET revision=${revision},selections_json=${JSON.stringify(selected)}::jsonb,updated_at=${timestamp} WHERE org_id=${p2.orgId} AND profile_id=${id} AND revision=${expected} RETURNING *`;
34755
- return rows[0] ? profile(rows[0]) : null;
34771
+ return this.sql.begin(async (tx) => {
34772
+ for (const selection of [...selected].sort((a3, b3) => a3.slug.localeCompare(b3.slug))) {
34773
+ const rows2 = await tx`SELECT lifecycle FROM skills_registry WHERE org_id=${p2.orgId} AND slug=${selection.slug} FOR UPDATE`;
34774
+ if (rows2[0]?.lifecycle === "archived")
34775
+ return null;
34776
+ }
34777
+ const revision = randomUUID2(), timestamp = new Date().toISOString();
34778
+ const rows = expected === null ? await tx`INSERT INTO skills_profiles(org_id,profile_id,revision,selections_json,updated_at) VALUES(${p2.orgId},${id},${revision},${JSON.stringify(selected)}::jsonb,${timestamp}) ON CONFLICT(org_id,profile_id) DO NOTHING RETURNING *` : await tx`UPDATE skills_profiles SET revision=${revision},selections_json=${JSON.stringify(selected)}::jsonb,updated_at=${timestamp} WHERE org_id=${p2.orgId} AND profile_id=${id} AND revision=${expected} RETURNING *`;
34779
+ return rows[0] ? profile(rows[0]) : null;
34780
+ });
34756
34781
  }
34757
34782
  async getStationState(p2, id) {
34758
34783
  const rows = await this.sql`SELECT * FROM skills_station_state WHERE org_id=${p2.orgId} AND actor_id=${p2.userId} AND station_id=${id}`;
@@ -34909,7 +34934,11 @@ function rowToSkill(row) {
34909
34934
  revisionId: String(row.revision_id ?? ""),
34910
34935
  revisionNumber: Number(row.revision_number ?? 0),
34911
34936
  ...row.tombstoned_at ? { tombstonedAt: dateString(row.tombstoned_at) } : {},
34912
- ...row.tombstone_purge_after ? { tombstonePurgeAfter: dateString(row.tombstone_purge_after) } : {}
34937
+ ...row.tombstone_purge_after ? { tombstonePurgeAfter: dateString(row.tombstone_purge_after) } : {},
34938
+ lifecycle: row.lifecycle === "archived" ? "archived" : "active",
34939
+ ...row.archived_at ? { archivedAt: dateString(row.archived_at) } : {},
34940
+ ...row.archive_reason ? { archiveReason: String(row.archive_reason) } : {},
34941
+ ...row.replacement_slug ? { replacementSlug: String(row.replacement_slug) } : {}
34913
34942
  };
34914
34943
  }
34915
34944
  function rowToPin(row) {
@@ -35028,10 +35057,21 @@ class SkillVersionExistsError extends Error {
35028
35057
  }
35029
35058
  }
35030
35059
 
35060
+ class SkillLifecycleConflictError extends Error {
35061
+ slug;
35062
+ profiles;
35063
+ constructor(slug, profiles) {
35064
+ super(`cannot archive '${slug}' while active profiles reference it: ${profiles.join(", ")}`);
35065
+ this.slug = slug;
35066
+ this.profiles = profiles;
35067
+ this.name = "SkillLifecycleConflictError";
35068
+ }
35069
+ }
35070
+
35031
35071
  // src/lib/revision.ts
35032
35072
  import { createHash as createHash4 } from "crypto";
35033
35073
  function revisionIdOf(content) {
35034
- const canonical = JSON.stringify({
35074
+ const canonicalContent = {
35035
35075
  slug: content.slug,
35036
35076
  displayName: content.displayName,
35037
35077
  description: content.description,
@@ -35043,7 +35083,13 @@ function revisionIdOf(content) {
35043
35083
  skillMd: content.skillMd ?? null,
35044
35084
  bundleSha256: content.bundleSha256 ?? null,
35045
35085
  bundleByteSize: content.bundleByteSize ?? null
35046
- });
35086
+ };
35087
+ if (content.lifecycle === "archived" || content.archiveReason !== undefined || content.replacementSlug !== undefined) {
35088
+ canonicalContent.lifecycle = content.lifecycle ?? "active";
35089
+ canonicalContent.archiveReason = content.archiveReason ?? null;
35090
+ canonicalContent.replacementSlug = content.replacementSlug ?? null;
35091
+ }
35092
+ const canonical = JSON.stringify(canonicalContent);
35047
35093
  return createHash4("sha256").update(canonical).digest("hex");
35048
35094
  }
35049
35095
  function revisionIdOfRecord(record) {
@@ -35146,6 +35192,28 @@ class SqliteSkillsStore {
35146
35192
  scopes: parseScopes(row.scopes_json)
35147
35193
  };
35148
35194
  }
35195
+ async updateApiKeyScopes(actor, keyId, expectedScopes, addScopes) {
35196
+ const row = this.get("SELECT scopes_json FROM api_keys WHERE id = ? AND org_id = ? AND revoked_at IS NULL LIMIT 1", [keyId, actor.orgId]);
35197
+ if (!row)
35198
+ return { kind: "not_found" };
35199
+ const current = parseScopes(row.scopes_json);
35200
+ if (current.length !== expectedScopes.length || current.some((scope, index) => scope !== expectedScopes[index])) {
35201
+ return { kind: "stale", scopes: current };
35202
+ }
35203
+ const scopes = [...current, ...addScopes.filter((scope) => !current.includes(scope))];
35204
+ const tx = this.db.transaction(() => {
35205
+ const changed = this.db.run("UPDATE api_keys SET scopes_json = ? WHERE id = ? AND org_id = ? AND revoked_at IS NULL AND scopes_json = ?", [JSON.stringify(scopes), keyId, actor.orgId, JSON.stringify(expectedScopes)]);
35206
+ if (changed.changes !== 1)
35207
+ return false;
35208
+ this.db.run("INSERT INTO skills_audit_events (org_id, user_id, api_key_id, action, target_type, target_id, metadata_json) VALUES (?, ?, ?, ?, ?, ?, ?)", [actor.orgId, actor.userId, actor.apiKeyId, "api_key_scopes_added", "api_key", keyId, JSON.stringify({ added: addScopes, scopes })]);
35209
+ return true;
35210
+ });
35211
+ if (!tx()) {
35212
+ const latest = this.get("SELECT scopes_json FROM api_keys WHERE id = ? AND org_id = ? AND revoked_at IS NULL LIMIT 1", [keyId, actor.orgId]);
35213
+ return { kind: "stale", scopes: latest ? parseScopes(latest.scopes_json) : current };
35214
+ }
35215
+ return { kind: "updated", scopes };
35216
+ }
35149
35217
  async createRun(input) {
35150
35218
  if (input.idempotencyKey) {
35151
35219
  const existing = this.get("SELECT * FROM skills_runs WHERE org_id = ? AND idempotency_key = ? LIMIT 1", [input.principal.orgId, input.idempotencyKey]);
@@ -35467,6 +35535,39 @@ class SqliteSkillsStore {
35467
35535
  const row = this.get("SELECT * FROM skills_registry WHERE org_id = ? AND slug = ? LIMIT 1", [principal.orgId, slug]);
35468
35536
  return row ? rowToSkill(row) : null;
35469
35537
  }
35538
+ async setSkillLifecycle(principal, slug, patch, expectedRevisionId) {
35539
+ return this.db.transaction(() => {
35540
+ const rowCurrent = this.get("SELECT * FROM skills_registry WHERE org_id=? AND slug=? LIMIT 1", [principal.orgId, slug]);
35541
+ const current = rowCurrent ? rowToSkill(rowCurrent) : null;
35542
+ if (!current || current.tombstonedAt)
35543
+ return null;
35544
+ if (expectedRevisionId !== current.revisionId)
35545
+ throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
35546
+ if (patch.lifecycle === "archived" && current.lifecycle !== "archived") {
35547
+ const profiles = this.db.query("SELECT profile_id,selections_json FROM skills_profiles WHERE org_id=?").all(principal.orgId);
35548
+ const referencing = profiles.filter((profile2) => {
35549
+ const selections2 = typeof profile2.selections_json === "string" ? JSON.parse(profile2.selections_json) : profile2.selections_json;
35550
+ return Array.isArray(selections2) && selections2.some((selection) => selection?.slug === slug);
35551
+ }).map((profile2) => String(profile2.profile_id)).sort();
35552
+ if (referencing.length)
35553
+ throw new SkillLifecycleConflictError(slug, referencing);
35554
+ }
35555
+ const next2 = { ...current, lifecycle: patch.lifecycle, updatedAt: nowIso(), ...patch.lifecycle === "archived" ? { archivedAt: current.archivedAt ?? nowIso(), ...patch.reason ? { archiveReason: patch.reason } : {}, ...patch.replacementSlug ? { replacementSlug: patch.replacementSlug } : {} } : {} };
35556
+ if (patch.lifecycle === "active") {
35557
+ delete next2.archivedAt;
35558
+ delete next2.archiveReason;
35559
+ delete next2.replacementSlug;
35560
+ }
35561
+ const row = this.get("UPDATE skills_registry SET lifecycle=?, archived_at=?, archive_reason=?, replacement_slug=?, revision_id=?, revision_number=revision_number+1, updated_at=? WHERE org_id=? AND slug=? AND tombstoned_at IS NULL AND revision_id=? RETURNING *", [next2.lifecycle, next2.archivedAt ?? null, next2.archiveReason ?? null, next2.replacementSlug ?? null, revisionIdOfRecord(next2), next2.updatedAt, principal.orgId, slug, current.revisionId]);
35562
+ if (!row)
35563
+ throw new SkillRevisionConflictError(slug, expectedRevisionId, this.getSkillSync(principal, slug)?.revisionId ?? null);
35564
+ return rowToSkill(row);
35565
+ }).immediate();
35566
+ }
35567
+ getSkillSync(principal, slug) {
35568
+ const row = this.get("SELECT * FROM skills_registry WHERE org_id=? AND slug=? LIMIT 1", [principal.orgId, slug]);
35569
+ return row ? rowToSkill(row) : null;
35570
+ }
35470
35571
  async updateSkill(principal, slug, patch, expectedRevisionId) {
35471
35572
  const current = await this.getSkill(principal, slug);
35472
35573
  if (!current || current.tombstonedAt)
@@ -35579,13 +35680,13 @@ class SqliteSkillsStore {
35579
35680
  }
35580
35681
  async listTags(principal) {
35581
35682
  await this.purgeExpiredTombstones(principal);
35582
- return this.all("SELECT DISTINCT tag FROM skills_tags WHERE org_id = ? ORDER BY tag ASC", [principal.orgId]).map((row) => String(row.tag));
35683
+ return this.all("SELECT DISTINCT t.tag FROM skills_tags t JOIN skills_registry s ON s.org_id=t.org_id AND s.slug=t.slug WHERE t.org_id = ? AND s.lifecycle='active' ORDER BY t.tag ASC", [principal.orgId]).map((row) => String(row.tag));
35583
35684
  }
35584
35685
  async listSkillsByTag(principal, tag) {
35585
35686
  await this.purgeExpiredTombstones(principal);
35586
35687
  return this.all(`SELECT s.* FROM skills_registry s
35587
35688
  JOIN skills_tags t ON t.org_id = s.org_id AND t.slug = s.slug
35588
- WHERE t.org_id = ? AND t.tag = ? AND s.tombstoned_at IS NULL
35689
+ WHERE t.org_id = ? AND t.tag = ? AND s.tombstoned_at IS NULL AND s.lifecycle = 'active'
35589
35690
  ORDER BY s.slug ASC`, [principal.orgId, tag]).map(rowToSkill);
35590
35691
  }
35591
35692
  async listPinsByTag(principal, tag) {
@@ -35593,7 +35694,7 @@ class SqliteSkillsStore {
35593
35694
  return this.all(`SELECT p.* FROM skills_pins p
35594
35695
  JOIN skills_tags t ON t.org_id = p.org_id AND t.slug = p.slug
35595
35696
  JOIN skills_registry s ON s.org_id = p.org_id AND s.slug = p.slug
35596
- WHERE p.org_id = ? AND p.principal = ? AND t.tag = ? AND s.tombstoned_at IS NULL
35697
+ WHERE p.org_id = ? AND p.principal = ? AND t.tag = ? AND s.tombstoned_at IS NULL AND s.lifecycle = 'active'
35597
35698
  ORDER BY p.slug ASC`, [principal.orgId, principal.apiKeyId, tag]).map(rowToPin);
35598
35699
  }
35599
35700
  async listPublishedSlugs(principal) {
@@ -35962,6 +36063,27 @@ var MAX_MANIFEST_BYTES = MAX_SKILL_MD_BYTES + 64000;
35962
36063
  var ALLOWED_PUBLISH_PARTS = new Set(["manifest", "bundle"]);
35963
36064
  var MAX_VERSION_MANIFEST_BYTES = 512 * 1024;
35964
36065
 
36066
+ // src/sdk/execution/types.ts
36067
+ function canonicalJson(value) {
36068
+ return JSON.stringify(sortKeys(value));
36069
+ }
36070
+ function sortKeys(value) {
36071
+ if (Array.isArray(value))
36072
+ return value.map(sortKeys);
36073
+ if (value !== null && typeof value === "object") {
36074
+ const record = value;
36075
+ const sorted = Object.create(null);
36076
+ for (const key of Object.keys(record).sort()) {
36077
+ sorted[key] = sortKeys(record[key]);
36078
+ }
36079
+ return sorted;
36080
+ }
36081
+ return value;
36082
+ }
36083
+
36084
+ // src/sdk/execution/admission.ts
36085
+ import { createHash as createHash5 } from "crypto";
36086
+
35965
36087
  // src/sdk/execution/storage.ts
35966
36088
  class MemoryRunExecutionStore {
35967
36089
  durable = false;
@@ -36142,10 +36264,38 @@ var DEFAULT_RUN_LIMITS = {
36142
36264
  maxArtifactsBytes: 10 * 1024 * 1024,
36143
36265
  maxConcurrency: 1
36144
36266
  };
36267
+ function digestInput(input) {
36268
+ return createHash5("sha256").update(canonicalJson(input)).digest("hex");
36269
+ }
36145
36270
 
36146
36271
  // src/sdk/execution/dispatchers/ecs.ts
36147
36272
  var LIVE_TASK_STATUSES = new Set(["PROVISIONING", "PENDING", "ACTIVATING", "RUNNING", "DEACTIVATING", "STOPPING", "DEPROVISIONING"]);
36148
36273
 
36274
+ // src/server/runtime-pure-contract.ts
36275
+ var PURE_DESCRIPTOR = Object.freeze({
36276
+ id: "regex-test.v1",
36277
+ runtime: "bun",
36278
+ adapter: "skills-input-json.v1",
36279
+ input: Object.freeze({ patternBytes: 512, textBytes: 4096, totalBytes: 8192, flags: "dgimsuvy" }),
36280
+ output: "regex-matches.v1",
36281
+ secrets: "none",
36282
+ egress: "deny",
36283
+ maxDurationMs: 5000,
36284
+ maxMemoryMb: 512,
36285
+ maxCpuUnits: 256,
36286
+ stdoutBytes: 16384,
36287
+ stderrBytes: 8192,
36288
+ artifactsBytes: 0
36289
+ });
36290
+ var PURE_DESCRIPTOR_DIGEST = digestInput(PURE_DESCRIPTOR);
36291
+ var PURE_LIMITS = Object.freeze({
36292
+ maxDurationMs: 5000,
36293
+ maxMemoryMb: 512,
36294
+ maxCpuUnits: 256,
36295
+ maxArtifactsBytes: 0,
36296
+ maxConcurrency: 1
36297
+ });
36298
+
36149
36299
  // src/server/config.ts
36150
36300
  var DATABASE_URL_ENV = "HASNA_SKILLS_DATABASE_URL";
36151
36301
  var SKILLS_ENV_NAMESPACE = "SKILLS";
@@ -36292,6 +36442,20 @@ class MemorySkillsStore {
36292
36442
  async authenticateApiKeyHash(hash) {
36293
36443
  return this.apiKeys.get(hash) ?? null;
36294
36444
  }
36445
+ async updateApiKeyScopes(actor, keyId, expectedScopes, addScopes) {
36446
+ for (const [hash, target] of this.apiKeys) {
36447
+ if (target.apiKeyId !== keyId || target.orgId !== actor.orgId)
36448
+ continue;
36449
+ if (target.scopes.length !== expectedScopes.length || target.scopes.some((scope, index) => scope !== expectedScopes[index])) {
36450
+ return { kind: "stale", scopes: [...target.scopes] };
36451
+ }
36452
+ const scopes = [...target.scopes, ...addScopes.filter((scope) => !target.scopes.includes(scope))];
36453
+ const updated = { ...target, scopes };
36454
+ this.apiKeys.set(hash, updated);
36455
+ return { kind: "updated", scopes };
36456
+ }
36457
+ return { kind: "not_found" };
36458
+ }
36295
36459
  async createRun(input) {
36296
36460
  const idemKey = input.idempotencyKey?.trim();
36297
36461
  const idemMapKey = idemKey ? `${input.principal.orgId}:${idemKey}` : undefined;
@@ -36421,7 +36585,11 @@ class MemorySkillsStore {
36421
36585
  createdAt: previous?.createdAt ?? now,
36422
36586
  updatedAt: now,
36423
36587
  revisionId: revisionIdOfRecord(recordFieldsOf(input, carriedBundle, carriedSkillMd)),
36424
- revisionNumber: (previous?.revisionNumber ?? 0) + 1
36588
+ revisionNumber: (previous?.revisionNumber ?? 0) + 1,
36589
+ lifecycle: previous?.lifecycle ?? "active",
36590
+ ...previous?.archivedAt ? { archivedAt: previous.archivedAt } : {},
36591
+ ...previous?.archiveReason ? { archiveReason: previous.archiveReason } : {},
36592
+ ...previous?.replacementSlug ? { replacementSlug: previous.replacementSlug } : {}
36425
36593
  };
36426
36594
  this.skills.set(key, record);
36427
36595
  if (input.version && versionSha && !this.versions.has(versionKey(input.principal.orgId, input.slug, input.version))) {
@@ -36457,6 +36625,33 @@ class MemorySkillsStore {
36457
36625
  const skill = this.skills.get(skillKey(principal.orgId, slug));
36458
36626
  return skill && skill.orgId === principal.orgId ? skill : null;
36459
36627
  }
36628
+ async setSkillLifecycle(principal, slug, patch, expectedRevisionId) {
36629
+ const current = await this.getSkill(principal, slug);
36630
+ if (!current || current.tombstonedAt)
36631
+ return null;
36632
+ if (expectedRevisionId !== current.revisionId)
36633
+ throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
36634
+ if (patch.lifecycle === "archived" && current.lifecycle !== "archived") {
36635
+ const profiles = await this.selectionStore.profilesReferencingSkill(principal, slug);
36636
+ if (profiles.length)
36637
+ throw new SkillLifecycleConflictError(slug, profiles);
36638
+ }
36639
+ const next2 = {
36640
+ ...current,
36641
+ lifecycle: patch.lifecycle,
36642
+ updatedAt: nowIso(),
36643
+ revisionId: revisionIdOfRecord({ ...current, lifecycle: patch.lifecycle, archiveReason: patch.reason, replacementSlug: patch.replacementSlug }),
36644
+ revisionNumber: current.revisionNumber + 1,
36645
+ ...patch.lifecycle === "archived" ? { archivedAt: current.archivedAt ?? nowIso(), ...patch.reason ? { archiveReason: patch.reason } : {}, ...patch.replacementSlug ? { replacementSlug: patch.replacementSlug } : {} } : {}
36646
+ };
36647
+ if (patch.lifecycle === "active") {
36648
+ delete next2.archivedAt;
36649
+ delete next2.archiveReason;
36650
+ delete next2.replacementSlug;
36651
+ }
36652
+ this.skills.set(skillKey(principal.orgId, slug), next2);
36653
+ return next2;
36654
+ }
36460
36655
  async updateSkill(principal, slug, patch, expectedRevisionId) {
36461
36656
  const current = await this.getSkill(principal, slug);
36462
36657
  if (!current || current.tombstonedAt)
@@ -36527,7 +36722,7 @@ class MemorySkillsStore {
36527
36722
  await this.purgeExpiredTombstones(principal);
36528
36723
  const tags = new Set;
36529
36724
  for (const skill of this.skills.values()) {
36530
- if (skill.orgId !== principal.orgId)
36725
+ if (skill.orgId !== principal.orgId || skill.lifecycle !== "active")
36531
36726
  continue;
36532
36727
  for (const tag of skill.tags) {
36533
36728
  if (tag.trim())
@@ -36538,13 +36733,13 @@ class MemorySkillsStore {
36538
36733
  }
36539
36734
  async listSkillsByTag(principal, tag) {
36540
36735
  await this.purgeExpiredTombstones(principal);
36541
- 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));
36736
+ return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt && skill.lifecycle === "active" && skill.tags.includes(tag)).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
36542
36737
  }
36543
36738
  async listPinsByTag(principal, tag) {
36544
36739
  await this.purgeExpiredTombstones(principal);
36545
36740
  const taggedSlugs = new Set;
36546
36741
  for (const skill of this.skills.values()) {
36547
- if (skill.orgId === principal.orgId && !skill.tombstonedAt && skill.tags.includes(tag))
36742
+ if (skill.orgId === principal.orgId && !skill.tombstonedAt && skill.lifecycle === "active" && skill.tags.includes(tag))
36548
36743
  taggedSlugs.add(skill.slug);
36549
36744
  }
36550
36745
  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));
@@ -36661,6 +36856,38 @@ class PostgresSkillsStore {
36661
36856
  scopes: parseJsonArray(row.scopes_json)
36662
36857
  };
36663
36858
  }
36859
+ async updateApiKeyScopes(actor, keyId, expectedScopes, addScopes) {
36860
+ return this.sql.begin(async (tx) => {
36861
+ const rows = await tx`
36862
+ SELECT scopes_json FROM api_keys
36863
+ WHERE id = ${keyId} AND org_id = ${actor.orgId} AND revoked_at IS NULL
36864
+ LIMIT 1
36865
+ `;
36866
+ const row = rows[0];
36867
+ if (!row)
36868
+ return { kind: "not_found" };
36869
+ const current = parseJsonArray(row.scopes_json);
36870
+ if (current.length !== expectedScopes.length || current.some((scope, index) => scope !== expectedScopes[index])) {
36871
+ return { kind: "stale", scopes: current };
36872
+ }
36873
+ const scopes = [...current, ...addScopes.filter((scope) => !current.includes(scope))];
36874
+ const updated = await tx`
36875
+ UPDATE api_keys SET scopes_json = ${JSON.stringify(scopes)}::jsonb
36876
+ WHERE id = ${keyId} AND org_id = ${actor.orgId} AND revoked_at IS NULL
36877
+ AND scopes_json = ${JSON.stringify(expectedScopes)}::jsonb
36878
+ RETURNING scopes_json
36879
+ `;
36880
+ if (!updated[0]) {
36881
+ const latest = await tx`SELECT scopes_json FROM api_keys WHERE id = ${keyId} AND org_id = ${actor.orgId} AND revoked_at IS NULL LIMIT 1`;
36882
+ return { kind: "stale", scopes: latest[0] ? parseJsonArray(latest[0].scopes_json) : current };
36883
+ }
36884
+ await tx`
36885
+ INSERT INTO skills_audit_events (org_id, user_id, api_key_id, action, target_type, target_id, metadata_json)
36886
+ VALUES (${actor.orgId}, ${actor.userId}, ${actor.apiKeyId}, ${"api_key_scopes_added"}, ${"api_key"}, ${keyId}, ${JSON.stringify({ added: addScopes, scopes })}::jsonb)
36887
+ `;
36888
+ return { kind: "updated", scopes };
36889
+ });
36890
+ }
36664
36891
  async withContext(orgId, worker, fn) {
36665
36892
  return this.sql.begin(async (tx) => {
36666
36893
  await tx`SELECT set_config('app.skills_org_id', ${orgId ?? ""}, true)`;
@@ -36981,6 +37208,42 @@ class PostgresSkillsStore {
36981
37208
  const rows = await this.sql`SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1`;
36982
37209
  return rows[0] ? rowToSkill(rows[0]) : null;
36983
37210
  }
37211
+ async setSkillLifecycle(principal, slug, patch, expectedRevisionId) {
37212
+ return this.sql.begin(async (tx) => {
37213
+ const currentRows = await tx`SELECT * FROM skills_registry WHERE org_id=${principal.orgId} AND slug=${slug} LIMIT 1 FOR UPDATE`;
37214
+ const current = currentRows[0] ? rowToSkill(currentRows[0]) : null;
37215
+ if (!current || current.tombstonedAt)
37216
+ return null;
37217
+ if (expectedRevisionId !== current.revisionId)
37218
+ throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
37219
+ if (patch.lifecycle === "archived" && current.lifecycle !== "archived") {
37220
+ const profiles = await tx`SELECT profile_id,selections_json FROM skills_profiles WHERE org_id=${principal.orgId}`;
37221
+ const referencing = profiles.filter((profile2) => {
37222
+ const selections2 = typeof profile2.selections_json === "string" ? JSON.parse(profile2.selections_json) : profile2.selections_json;
37223
+ return Array.isArray(selections2) && selections2.some((selection) => selection?.slug === slug);
37224
+ }).map((profile2) => String(profile2.profile_id)).sort();
37225
+ if (referencing.length)
37226
+ throw new SkillLifecycleConflictError(slug, referencing);
37227
+ }
37228
+ const next2 = {
37229
+ ...current,
37230
+ lifecycle: patch.lifecycle,
37231
+ updatedAt: nowIso(),
37232
+ revisionId: revisionIdOfRecord({ ...current, lifecycle: patch.lifecycle, archiveReason: patch.reason, replacementSlug: patch.replacementSlug }),
37233
+ revisionNumber: current.revisionNumber + 1,
37234
+ ...patch.lifecycle === "archived" ? { archivedAt: current.archivedAt ?? nowIso(), ...patch.reason ? { archiveReason: patch.reason } : {}, ...patch.replacementSlug ? { replacementSlug: patch.replacementSlug } : {} } : {}
37235
+ };
37236
+ if (patch.lifecycle === "active") {
37237
+ delete next2.archivedAt;
37238
+ delete next2.archiveReason;
37239
+ delete next2.replacementSlug;
37240
+ }
37241
+ const rows = await tx`UPDATE skills_registry SET lifecycle=${next2.lifecycle}, archived_at=${next2.archivedAt ?? null}, archive_reason=${next2.archiveReason ?? null}, replacement_slug=${next2.replacementSlug ?? null}, revision_id=${next2.revisionId}, revision_number=revision_number+1, updated_at=${next2.updatedAt} WHERE org_id=${principal.orgId} AND slug=${slug} AND tombstoned_at IS NULL AND revision_id=${current.revisionId} RETURNING *`;
37242
+ if (!rows[0])
37243
+ throw new SkillRevisionConflictError(slug, expectedRevisionId, (await this.getSkill(principal, slug))?.revisionId ?? null);
37244
+ return rowToSkill(rows[0]);
37245
+ });
37246
+ }
36984
37247
  async updateSkill(principal, slug, patch, expectedRevisionId) {
36985
37248
  const current = await this.getSkill(principal, slug);
36986
37249
  if (!current || current.tombstonedAt)
@@ -37116,7 +37379,7 @@ class PostgresSkillsStore {
37116
37379
  async listTags(principal) {
37117
37380
  await this.purgeExpiredTombstones(principal);
37118
37381
  const rows = await this.sql`
37119
- SELECT DISTINCT tag FROM skills_tags WHERE org_id = ${principal.orgId} ORDER BY tag ASC
37382
+ SELECT DISTINCT t.tag FROM skills_tags t JOIN skills_registry s ON s.org_id = t.org_id AND s.slug = t.slug WHERE t.org_id = ${principal.orgId} AND s.lifecycle = 'active' ORDER BY t.tag ASC
37120
37383
  `;
37121
37384
  return rows.map((row) => String(row.tag));
37122
37385
  }
@@ -37125,7 +37388,7 @@ class PostgresSkillsStore {
37125
37388
  const rows = await this.sql`
37126
37389
  SELECT s.* FROM skills_registry s
37127
37390
  JOIN skills_tags t ON t.org_id = s.org_id AND t.slug = s.slug
37128
- WHERE t.org_id = ${principal.orgId} AND t.tag = ${tag} AND s.tombstoned_at IS NULL
37391
+ WHERE t.org_id = ${principal.orgId} AND t.tag = ${tag} AND s.tombstoned_at IS NULL AND s.lifecycle = 'active'
37129
37392
  ORDER BY s.slug ASC
37130
37393
  `;
37131
37394
  return rows.map(rowToSkill);
@@ -37137,7 +37400,7 @@ class PostgresSkillsStore {
37137
37400
  JOIN skills_tags t ON t.org_id = p.org_id AND t.slug = p.slug
37138
37401
  JOIN skills_registry s ON s.org_id = p.org_id AND s.slug = p.slug
37139
37402
  WHERE p.org_id = ${principal.orgId} AND p.principal = ${principal.apiKeyId}
37140
- AND t.tag = ${tag} AND s.tombstoned_at IS NULL
37403
+ AND t.tag = ${tag} AND s.tombstoned_at IS NULL AND s.lifecycle = 'active'
37141
37404
  ORDER BY p.slug ASC
37142
37405
  `;
37143
37406
  return rows.map(rowToPin);
@@ -13,3 +13,4 @@ export declare function registerPull(parent: Command): void;
13
13
  * instance, newest first, with the one the instance currently serves marked (hasna/apps#1630).
14
14
  */
15
15
  export declare function registerVersions(parent: Command): void;
16
+ export declare function registerLifecycle(parent: Command): void;