@hasna/skills 0.1.63 → 0.1.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -6
- package/bin/index.js +1593 -228
- package/bin/mcp.js +290 -24
- package/bin/migrate.js +229 -33
- package/bin/server.js +774 -116
- package/bin/worker.js +558 -79
- package/dist/cli/commands/registry-reconcile.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +559 -251
- package/dist/lib/agent-sync.d.ts +26 -6
- package/dist/lib/auth-store.d.ts +37 -0
- package/dist/lib/config.d.ts +51 -0
- package/dist/lib/home-census.d.ts +4 -0
- package/dist/lib/home-migration.d.ts +8 -9
- package/dist/lib/native-storage.d.ts +1 -1
- package/dist/lib/portable-skills.d.ts +35 -6
- package/dist/lib/pull.d.ts +31 -0
- package/dist/lib/registry-reconcile.d.ts +114 -0
- package/dist/lib/registry.d.ts +7 -4
- package/dist/lib/remote-client.d.ts +118 -1
- package/dist/lib/remote-registry.d.ts +26 -0
- package/dist/lib/revision.d.ts +29 -0
- package/dist/sdk/index.js +1351 -353
- package/dist/server/app.d.ts +1 -1
- package/dist/server/config.d.ts +12 -2
- package/dist/server/rows.d.ts +2 -1
- package/dist/server/skills-api.d.ts +108 -6
- package/dist/server/sqlite-store.d.ts +20 -3
- package/dist/server/store.d.ts +31 -5
- package/dist/server/types.d.ts +98 -3
- package/dist/storage.js +7 -2
- package/migrations/postgres/0004_hosted_pins.sql +28 -0
- package/migrations/postgres/0005_revision_tombstone_registry.sql +37 -0
- package/migrations/postgres/0005_tag_projection.sql +36 -0
- package/migrations/sqlite/0004_hosted_pins.sql +21 -0
- package/migrations/sqlite/0005_revision_tombstone_registry.sql +18 -0
- package/migrations/sqlite/0005_tag_projection.sql +25 -0
- package/package.json +3 -2
package/dist/sdk/index.js
CHANGED
|
@@ -22936,11 +22936,12 @@ var init_dist_es9 = __esm(() => {
|
|
|
22936
22936
|
init_fromIni();
|
|
22937
22937
|
});
|
|
22938
22938
|
|
|
22939
|
-
//
|
|
22939
|
+
// ../events/dist/index.js
|
|
22940
22940
|
import { chmod, mkdir, readFile, rename, writeFile as writeFile2 } from "fs/promises";
|
|
22941
|
-
import {
|
|
22941
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
22942
|
+
import { existsSync as existsSync13 } from "fs";
|
|
22942
22943
|
import { homedir as homedir5 } from "os";
|
|
22943
|
-
import { join as
|
|
22944
|
+
import { join as join15 } from "path";
|
|
22944
22945
|
import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
22945
22946
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
22946
22947
|
import { spawn } from "child_process";
|
|
@@ -22953,29 +22954,83 @@ function getPathValue(input, path) {
|
|
|
22953
22954
|
return;
|
|
22954
22955
|
}, input);
|
|
22955
22956
|
}
|
|
22956
|
-
function
|
|
22957
|
-
const
|
|
22958
|
-
|
|
22957
|
+
function getFieldValues(input, path) {
|
|
22958
|
+
const values = [];
|
|
22959
|
+
const push = (value) => {
|
|
22960
|
+
if (!values.some((item) => Object.is(item, value)))
|
|
22961
|
+
values.push(value);
|
|
22962
|
+
};
|
|
22963
|
+
if (path.includes(".") && path in input)
|
|
22964
|
+
push(input[path]);
|
|
22965
|
+
const nestedValue = getPathValue(input, path);
|
|
22966
|
+
if (nestedValue !== undefined || !path.includes("."))
|
|
22967
|
+
push(nestedValue);
|
|
22968
|
+
return values;
|
|
22969
|
+
}
|
|
22970
|
+
function wildcardToRegExp(pattern, options = {}) {
|
|
22971
|
+
let body = "";
|
|
22972
|
+
for (let index = 0;index < pattern.length; index += 1) {
|
|
22973
|
+
const char = pattern[index];
|
|
22974
|
+
if (char === "*") {
|
|
22975
|
+
if (pattern[index + 1] === "*") {
|
|
22976
|
+
body += ".*";
|
|
22977
|
+
index += 1;
|
|
22978
|
+
} else {
|
|
22979
|
+
body += options.segmentSafe ? "[^/]*" : ".*";
|
|
22980
|
+
}
|
|
22981
|
+
} else {
|
|
22982
|
+
body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
22983
|
+
}
|
|
22984
|
+
}
|
|
22985
|
+
return new RegExp(`^${body}$`);
|
|
22959
22986
|
}
|
|
22960
|
-
function matchString(value, matcher) {
|
|
22987
|
+
function matchString(value, matcher, options = {}) {
|
|
22961
22988
|
if (matcher === undefined)
|
|
22962
22989
|
return true;
|
|
22963
22990
|
if (value === undefined)
|
|
22964
22991
|
return false;
|
|
22965
22992
|
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
22966
|
-
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
22993
|
+
return matchers.some((item) => wildcardToRegExp(item, options).test(value));
|
|
22967
22994
|
}
|
|
22968
22995
|
function matchRecord(input, matcher) {
|
|
22969
22996
|
if (!matcher)
|
|
22970
22997
|
return true;
|
|
22971
22998
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
22972
|
-
const
|
|
22973
|
-
|
|
22974
|
-
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
22975
|
-
}
|
|
22976
|
-
return actual === expected;
|
|
22999
|
+
const actualValues = getFieldValues(input, path);
|
|
23000
|
+
return matchField(actualValues, expected, path);
|
|
22977
23001
|
});
|
|
22978
23002
|
}
|
|
23003
|
+
function matchField(actualValues, expected, path) {
|
|
23004
|
+
if (isNegativeMatcher(expected)) {
|
|
23005
|
+
return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
|
|
23006
|
+
}
|
|
23007
|
+
return actualValues.some((actual) => matchPositiveField(actual, expected, path));
|
|
23008
|
+
}
|
|
23009
|
+
function matchPositiveField(actual, expected, path) {
|
|
23010
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
23011
|
+
return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
|
|
23012
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
23013
|
+
}));
|
|
23014
|
+
}
|
|
23015
|
+
if (Array.isArray(actual)) {
|
|
23016
|
+
return actual.some((item) => item === expected);
|
|
23017
|
+
}
|
|
23018
|
+
return actual === expected;
|
|
23019
|
+
}
|
|
23020
|
+
function stringCandidates(actual) {
|
|
23021
|
+
if (actual === undefined)
|
|
23022
|
+
return [];
|
|
23023
|
+
if (Array.isArray(actual)) {
|
|
23024
|
+
return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
|
|
23025
|
+
}
|
|
23026
|
+
return [String(actual)];
|
|
23027
|
+
}
|
|
23028
|
+
function isPrimitiveFieldValue(value) {
|
|
23029
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
23030
|
+
}
|
|
23031
|
+
function isNegativeMatcher(value) {
|
|
23032
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
|
|
23033
|
+
}
|
|
22979
23034
|
function eventMatchesFilter(event, filter) {
|
|
22980
23035
|
return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
|
|
22981
23036
|
}
|
|
@@ -22987,19 +23042,21 @@ function channelMatchesEvent(channel, event) {
|
|
|
22987
23042
|
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
22988
23043
|
}
|
|
22989
23044
|
function getEventsDataDir(override) {
|
|
22990
|
-
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] ||
|
|
23045
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join15(homedir5(), ".hasna", "events");
|
|
22991
23046
|
}
|
|
22992
23047
|
|
|
22993
23048
|
class JsonEventsStore {
|
|
22994
23049
|
dataDir;
|
|
23050
|
+
runtime;
|
|
22995
23051
|
channelsPath;
|
|
22996
23052
|
eventsPath;
|
|
22997
23053
|
deliveriesPath;
|
|
22998
23054
|
constructor(dataDir = getEventsDataDir()) {
|
|
22999
23055
|
this.dataDir = dataDir;
|
|
23000
|
-
this.
|
|
23001
|
-
this.
|
|
23002
|
-
this.
|
|
23056
|
+
this.runtime = localJsonRuntime(dataDir);
|
|
23057
|
+
this.channelsPath = join15(dataDir, "channels.json");
|
|
23058
|
+
this.eventsPath = join15(dataDir, "events.json");
|
|
23059
|
+
this.deliveriesPath = join15(dataDir, "deliveries.json");
|
|
23003
23060
|
}
|
|
23004
23061
|
async init() {
|
|
23005
23062
|
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
@@ -23044,13 +23101,58 @@ class JsonEventsStore {
|
|
|
23044
23101
|
await this.writeJson(this.eventsPath, events);
|
|
23045
23102
|
return event;
|
|
23046
23103
|
}
|
|
23047
|
-
async
|
|
23104
|
+
async appendEventOnce(event, options = {}) {
|
|
23048
23105
|
await this.init();
|
|
23049
|
-
|
|
23106
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
23107
|
+
const dedupe = options.dedupe !== false;
|
|
23108
|
+
if (dedupe) {
|
|
23109
|
+
const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
|
|
23110
|
+
if (existing) {
|
|
23111
|
+
return {
|
|
23112
|
+
event: existing,
|
|
23113
|
+
stored: false,
|
|
23114
|
+
deduped: true,
|
|
23115
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
23116
|
+
};
|
|
23117
|
+
}
|
|
23118
|
+
}
|
|
23119
|
+
events.push(event);
|
|
23120
|
+
await this.writeJson(this.eventsPath, events);
|
|
23121
|
+
return {
|
|
23122
|
+
event,
|
|
23123
|
+
stored: true,
|
|
23124
|
+
deduped: false,
|
|
23125
|
+
identity: { id: event.id, dedupeKey: event.dedupeKey }
|
|
23126
|
+
};
|
|
23127
|
+
}
|
|
23128
|
+
async listEvents(options = {}) {
|
|
23129
|
+
await this.init();
|
|
23130
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
23131
|
+
return queryEvents(events, options);
|
|
23132
|
+
}
|
|
23133
|
+
async listEventsPage(options = {}) {
|
|
23134
|
+
await this.init();
|
|
23135
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
23136
|
+
const queried = queryEvents(events, {
|
|
23137
|
+
eventId: options.eventId,
|
|
23138
|
+
source: options.source,
|
|
23139
|
+
type: options.type
|
|
23140
|
+
});
|
|
23141
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
23142
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
23143
|
+
const pageEvents = queried.slice(offset, offset + limit);
|
|
23144
|
+
const nextOffset = offset + pageEvents.length;
|
|
23145
|
+
const hasMore = nextOffset < queried.length;
|
|
23146
|
+
return {
|
|
23147
|
+
events: pageEvents,
|
|
23148
|
+
cursor: options.cursor,
|
|
23149
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
23150
|
+
hasMore
|
|
23151
|
+
};
|
|
23050
23152
|
}
|
|
23051
23153
|
async findEventByIdentity(identity) {
|
|
23052
23154
|
const events = await this.listEvents();
|
|
23053
|
-
return events
|
|
23155
|
+
return findEventByIdentity(events, identity);
|
|
23054
23156
|
}
|
|
23055
23157
|
async appendDelivery(result) {
|
|
23056
23158
|
await this.init();
|
|
@@ -23071,7 +23173,7 @@ class JsonEventsStore {
|
|
|
23071
23173
|
};
|
|
23072
23174
|
}
|
|
23073
23175
|
async ensureArrayFile(path) {
|
|
23074
|
-
if (!
|
|
23176
|
+
if (!existsSync13(path)) {
|
|
23075
23177
|
await writeFile2(path, `[]
|
|
23076
23178
|
`, { encoding: "utf-8", mode: 384 });
|
|
23077
23179
|
}
|
|
@@ -23101,6 +23203,83 @@ class JsonEventsStore {
|
|
|
23101
23203
|
});
|
|
23102
23204
|
}
|
|
23103
23205
|
}
|
|
23206
|
+
function localJsonRuntime(dataDir = getEventsDataDir()) {
|
|
23207
|
+
return {
|
|
23208
|
+
mode: "local-files",
|
|
23209
|
+
name: "json-events-store",
|
|
23210
|
+
remote: false,
|
|
23211
|
+
localFiles: true,
|
|
23212
|
+
localSqlite: false,
|
|
23213
|
+
postgres: false,
|
|
23214
|
+
s3: false,
|
|
23215
|
+
aws: false,
|
|
23216
|
+
durable: true,
|
|
23217
|
+
idempotency: "best-effort-local",
|
|
23218
|
+
replayCursors: true,
|
|
23219
|
+
description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
|
|
23220
|
+
};
|
|
23221
|
+
}
|
|
23222
|
+
function encodeLocalJsonEventCursor(offset, options = {}) {
|
|
23223
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
23224
|
+
throw new Error(`Invalid event cursor offset: ${offset}`);
|
|
23225
|
+
const payload = {
|
|
23226
|
+
offset,
|
|
23227
|
+
eventId: options.eventId,
|
|
23228
|
+
source: options.source,
|
|
23229
|
+
type: options.type
|
|
23230
|
+
};
|
|
23231
|
+
return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
|
|
23232
|
+
}
|
|
23233
|
+
function decodeLocalJsonEventCursor(cursor, options = {}) {
|
|
23234
|
+
if (!cursor)
|
|
23235
|
+
return 0;
|
|
23236
|
+
if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
|
|
23237
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
23238
|
+
const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
|
|
23239
|
+
let payload;
|
|
23240
|
+
try {
|
|
23241
|
+
payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
|
|
23242
|
+
} catch {
|
|
23243
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
23244
|
+
}
|
|
23245
|
+
const offset = payload.offset;
|
|
23246
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
23247
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
23248
|
+
assertCursorFilter("eventId", payload.eventId, options.eventId);
|
|
23249
|
+
assertCursorFilter("source", payload.source, options.source);
|
|
23250
|
+
assertCursorFilter("type", payload.type, options.type);
|
|
23251
|
+
return offset;
|
|
23252
|
+
}
|
|
23253
|
+
function normalizeEventPageLimit(limit) {
|
|
23254
|
+
if (limit === undefined)
|
|
23255
|
+
return DEFAULT_EVENT_PAGE_LIMIT;
|
|
23256
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
23257
|
+
throw new Error(`Event page limit must be a positive integer, got ${limit}`);
|
|
23258
|
+
return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
|
|
23259
|
+
}
|
|
23260
|
+
function queryEvents(events, options) {
|
|
23261
|
+
let rows = events;
|
|
23262
|
+
if (options.eventId)
|
|
23263
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
23264
|
+
if (options.source)
|
|
23265
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
23266
|
+
if (options.type)
|
|
23267
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
23268
|
+
if (options.cursor) {
|
|
23269
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
23270
|
+
rows = rows.slice(offset);
|
|
23271
|
+
}
|
|
23272
|
+
if (options.limit !== undefined)
|
|
23273
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
23274
|
+
return rows;
|
|
23275
|
+
}
|
|
23276
|
+
function assertCursorFilter(name, cursorValue, optionValue) {
|
|
23277
|
+
if (cursorValue !== optionValue)
|
|
23278
|
+
throw new Error(`Local JSON event cursor ${name} filter mismatch`);
|
|
23279
|
+
}
|
|
23280
|
+
function findEventByIdentity(events, identity) {
|
|
23281
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
23282
|
+
}
|
|
23104
23283
|
function buildSignatureBase(timestamp, body) {
|
|
23105
23284
|
return `${timestamp}.${body}`;
|
|
23106
23285
|
}
|
|
@@ -23114,21 +23293,27 @@ function now() {
|
|
|
23114
23293
|
function truncate(value, max = 4096) {
|
|
23115
23294
|
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
23116
23295
|
}
|
|
23117
|
-
function buildWebhookRequest(event, channel) {
|
|
23296
|
+
function buildWebhookRequest(event, channel, options = {}) {
|
|
23118
23297
|
if (!channel.webhook)
|
|
23119
23298
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
23299
|
+
for (const name of Object.keys(channel.webhook.headers ?? {})) {
|
|
23300
|
+
if (/^x-hasna-/i.test(name)) {
|
|
23301
|
+
throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
|
|
23302
|
+
}
|
|
23303
|
+
}
|
|
23120
23304
|
const body = JSON.stringify(event);
|
|
23121
|
-
const timestamp =
|
|
23305
|
+
const timestamp = options.timestamp ?? new Date().toISOString();
|
|
23122
23306
|
const headers = {
|
|
23123
23307
|
"Content-Type": "application/json",
|
|
23124
23308
|
"User-Agent": "@hasna/events",
|
|
23125
23309
|
"X-Hasna-Event-Id": event.id,
|
|
23126
23310
|
"X-Hasna-Event-Type": event.type,
|
|
23127
|
-
|
|
23128
|
-
|
|
23311
|
+
...channel.webhook.headers,
|
|
23312
|
+
"X-Hasna-Timestamp": timestamp
|
|
23129
23313
|
};
|
|
23130
|
-
|
|
23131
|
-
|
|
23314
|
+
const secret = options.secret ?? channel.webhook.secret;
|
|
23315
|
+
if (secret) {
|
|
23316
|
+
headers["X-Hasna-Signature"] = signPayload(secret, timestamp, body);
|
|
23132
23317
|
}
|
|
23133
23318
|
return { body, headers };
|
|
23134
23319
|
}
|
|
@@ -23136,7 +23321,21 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
23136
23321
|
if (!channel.webhook)
|
|
23137
23322
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
23138
23323
|
const startedAt = now();
|
|
23139
|
-
|
|
23324
|
+
let secret = channel.webhook.secret;
|
|
23325
|
+
if (channel.webhook.secretRef) {
|
|
23326
|
+
if (!options.secretResolver) {
|
|
23327
|
+
return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
|
|
23328
|
+
}
|
|
23329
|
+
try {
|
|
23330
|
+
secret = await options.secretResolver(channel.webhook.secretRef);
|
|
23331
|
+
} catch {
|
|
23332
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
23333
|
+
}
|
|
23334
|
+
if (!secret)
|
|
23335
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
23336
|
+
}
|
|
23337
|
+
const timestamp = (options.now?.() ?? new Date).toISOString();
|
|
23338
|
+
const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp });
|
|
23140
23339
|
const controller = new AbortController;
|
|
23141
23340
|
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
23142
23341
|
try {
|
|
@@ -23168,6 +23367,15 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
23168
23367
|
clearTimeout(timeout);
|
|
23169
23368
|
}
|
|
23170
23369
|
}
|
|
23370
|
+
function failedAttempt(startedAt, error) {
|
|
23371
|
+
return {
|
|
23372
|
+
attempt: 1,
|
|
23373
|
+
status: "failed",
|
|
23374
|
+
startedAt,
|
|
23375
|
+
completedAt: now(),
|
|
23376
|
+
error
|
|
23377
|
+
};
|
|
23378
|
+
}
|
|
23171
23379
|
async function dispatchCommand(event, channel) {
|
|
23172
23380
|
if (!channel.command)
|
|
23173
23381
|
throw new Error(`Channel ${channel.id} has no command config`);
|
|
@@ -23256,6 +23464,76 @@ function createDeliveryResult(event, channel, attempts) {
|
|
|
23256
23464
|
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
23257
23465
|
};
|
|
23258
23466
|
}
|
|
23467
|
+
|
|
23468
|
+
class EventTypeCatalog {
|
|
23469
|
+
definitions = new Map;
|
|
23470
|
+
register(definition) {
|
|
23471
|
+
this.definitions.set(definition.type, definition);
|
|
23472
|
+
return this;
|
|
23473
|
+
}
|
|
23474
|
+
unregister(type) {
|
|
23475
|
+
return this.definitions.delete(type);
|
|
23476
|
+
}
|
|
23477
|
+
has(type) {
|
|
23478
|
+
return this.definitions.has(type);
|
|
23479
|
+
}
|
|
23480
|
+
get(type) {
|
|
23481
|
+
return this.definitions.get(type);
|
|
23482
|
+
}
|
|
23483
|
+
list() {
|
|
23484
|
+
return [...this.definitions.values()];
|
|
23485
|
+
}
|
|
23486
|
+
validateEvent(event) {
|
|
23487
|
+
const definition = this.definitions.get(event.type);
|
|
23488
|
+
if (!definition)
|
|
23489
|
+
return { ok: true };
|
|
23490
|
+
return definition.validate(event.data, event);
|
|
23491
|
+
}
|
|
23492
|
+
assertEventValid(event) {
|
|
23493
|
+
const result = this.validateEvent(event);
|
|
23494
|
+
if (!result.ok) {
|
|
23495
|
+
throw new EventValidationError(event.type, result.issues);
|
|
23496
|
+
}
|
|
23497
|
+
}
|
|
23498
|
+
}
|
|
23499
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
23500
|
+
if (paths.length === 0)
|
|
23501
|
+
return event;
|
|
23502
|
+
const copy = structuredClone(event);
|
|
23503
|
+
for (const path of paths) {
|
|
23504
|
+
setPath(copy, path, replacement);
|
|
23505
|
+
}
|
|
23506
|
+
return copy;
|
|
23507
|
+
}
|
|
23508
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
23509
|
+
return redactValue(event, replacement);
|
|
23510
|
+
}
|
|
23511
|
+
function shouldRedactKey(key) {
|
|
23512
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
23513
|
+
}
|
|
23514
|
+
function redactValue(value, replacement) {
|
|
23515
|
+
if (Array.isArray(value))
|
|
23516
|
+
return value.map((item) => redactValue(item, replacement));
|
|
23517
|
+
if (!value || typeof value !== "object")
|
|
23518
|
+
return value;
|
|
23519
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
23520
|
+
key,
|
|
23521
|
+
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
23522
|
+
]));
|
|
23523
|
+
}
|
|
23524
|
+
function setPath(input, path, replacement) {
|
|
23525
|
+
const parts = path.split(".");
|
|
23526
|
+
let cursor = input;
|
|
23527
|
+
for (const part of parts.slice(0, -1)) {
|
|
23528
|
+
const next = cursor[part];
|
|
23529
|
+
if (!next || typeof next !== "object")
|
|
23530
|
+
return;
|
|
23531
|
+
cursor = next;
|
|
23532
|
+
}
|
|
23533
|
+
const last = parts.at(-1);
|
|
23534
|
+
if (last && last in cursor)
|
|
23535
|
+
cursor[last] = replacement;
|
|
23536
|
+
}
|
|
23259
23537
|
function createEvent(input) {
|
|
23260
23538
|
return {
|
|
23261
23539
|
id: input.id ?? randomUUID22(),
|
|
@@ -23276,10 +23554,18 @@ class EventsClient {
|
|
|
23276
23554
|
store;
|
|
23277
23555
|
redactors;
|
|
23278
23556
|
transportOptions;
|
|
23557
|
+
catalog;
|
|
23558
|
+
validateCatalogTypes;
|
|
23279
23559
|
constructor(options = {}) {
|
|
23280
23560
|
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
23281
23561
|
this.redactors = options.redactors ?? [];
|
|
23282
|
-
this.transportOptions = {
|
|
23562
|
+
this.transportOptions = {
|
|
23563
|
+
fetchImpl: options.fetchImpl,
|
|
23564
|
+
secretResolver: options.secretResolver,
|
|
23565
|
+
now: options.now
|
|
23566
|
+
};
|
|
23567
|
+
this.catalog = options.catalog ?? defaultEventTypeCatalog;
|
|
23568
|
+
this.validateCatalogTypes = options.validateCatalogTypes ?? false;
|
|
23283
23569
|
}
|
|
23284
23570
|
async addChannel(input) {
|
|
23285
23571
|
const timestamp = new Date().toISOString();
|
|
@@ -23297,18 +23583,40 @@ class EventsClient {
|
|
|
23297
23583
|
}
|
|
23298
23584
|
async emit(input, options = {}) {
|
|
23299
23585
|
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
23300
|
-
if (options.
|
|
23301
|
-
|
|
23302
|
-
|
|
23303
|
-
|
|
23304
|
-
|
|
23305
|
-
|
|
23306
|
-
|
|
23307
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
23308
|
-
return { event, deliveries, deduped: false };
|
|
23309
|
-
}
|
|
23310
|
-
async listEvents() {
|
|
23311
|
-
|
|
23586
|
+
if (options.validate ?? this.validateCatalogTypes) {
|
|
23587
|
+
this.catalog.assertEventValid(event);
|
|
23588
|
+
}
|
|
23589
|
+
const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
|
|
23590
|
+
if (append.deduped) {
|
|
23591
|
+
return { event: append.event, deliveries: [], deduped: true };
|
|
23592
|
+
}
|
|
23593
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
|
|
23594
|
+
return { event: append.event, deliveries, deduped: false };
|
|
23595
|
+
}
|
|
23596
|
+
async listEvents(options = {}) {
|
|
23597
|
+
if (Object.keys(options).length === 0)
|
|
23598
|
+
return this.store.listEvents();
|
|
23599
|
+
return queryClientEvents(await this.store.listEvents(), options);
|
|
23600
|
+
}
|
|
23601
|
+
async listEventsPage(options = {}) {
|
|
23602
|
+
if (this.store.listEventsPage)
|
|
23603
|
+
return this.store.listEventsPage(options);
|
|
23604
|
+
const events = queryClientEvents(await this.store.listEvents(), {
|
|
23605
|
+
eventId: options.eventId,
|
|
23606
|
+
source: options.source,
|
|
23607
|
+
type: options.type
|
|
23608
|
+
});
|
|
23609
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
23610
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
23611
|
+
const pageEvents = events.slice(offset, offset + limit);
|
|
23612
|
+
const nextOffset = offset + pageEvents.length;
|
|
23613
|
+
const hasMore = nextOffset < events.length;
|
|
23614
|
+
return {
|
|
23615
|
+
events: pageEvents,
|
|
23616
|
+
cursor: options.cursor,
|
|
23617
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
23618
|
+
hasMore
|
|
23619
|
+
};
|
|
23312
23620
|
}
|
|
23313
23621
|
async listDeliveries() {
|
|
23314
23622
|
return this.store.listDeliveries();
|
|
@@ -23325,7 +23633,7 @@ class EventsClient {
|
|
|
23325
23633
|
}
|
|
23326
23634
|
return deliveries;
|
|
23327
23635
|
}
|
|
23328
|
-
async
|
|
23636
|
+
async matchChannel(id, input = {}) {
|
|
23329
23637
|
const channel = await this.store.getChannel(id);
|
|
23330
23638
|
if (!channel)
|
|
23331
23639
|
throw new Error(`Channel not found: ${id}`);
|
|
@@ -23342,28 +23650,71 @@ class EventsClient {
|
|
|
23342
23650
|
time: input.time,
|
|
23343
23651
|
id: input.id
|
|
23344
23652
|
});
|
|
23653
|
+
const matched = channelMatchesEvent(channel, event);
|
|
23654
|
+
return {
|
|
23655
|
+
channelId: channel.id,
|
|
23656
|
+
matched,
|
|
23657
|
+
event,
|
|
23658
|
+
filters: channel.filters,
|
|
23659
|
+
reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
|
|
23660
|
+
};
|
|
23661
|
+
}
|
|
23662
|
+
async testChannel(id, input = {}, options = {}) {
|
|
23663
|
+
const channel = await this.store.getChannel(id);
|
|
23664
|
+
if (!channel)
|
|
23665
|
+
throw new Error(`Channel not found: ${id}`);
|
|
23666
|
+
const match = await this.matchChannel(id, input);
|
|
23667
|
+
const event = match.event;
|
|
23668
|
+
if (options.honorFilters && !match.matched) {
|
|
23669
|
+
const timestamp = new Date().toISOString();
|
|
23670
|
+
const result2 = createDeliveryResult(event, channel, [{
|
|
23671
|
+
attempt: 1,
|
|
23672
|
+
status: "skipped",
|
|
23673
|
+
startedAt: timestamp,
|
|
23674
|
+
completedAt: timestamp,
|
|
23675
|
+
error: match.reason
|
|
23676
|
+
}]);
|
|
23677
|
+
result2.metadata = { reason: "filter_mismatch" };
|
|
23678
|
+
await this.store.appendDelivery(result2);
|
|
23679
|
+
return result2;
|
|
23680
|
+
}
|
|
23345
23681
|
const eventForChannel = await this.applyRedaction(event, channel);
|
|
23346
23682
|
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
23347
23683
|
await this.store.appendDelivery(result);
|
|
23348
23684
|
return result;
|
|
23349
23685
|
}
|
|
23350
23686
|
async replay(options = {}) {
|
|
23351
|
-
const
|
|
23352
|
-
if (options.eventId && event.id !== options.eventId)
|
|
23353
|
-
return false;
|
|
23354
|
-
if (options.source && event.source !== options.source)
|
|
23355
|
-
return false;
|
|
23356
|
-
if (options.type && event.type !== options.type)
|
|
23357
|
-
return false;
|
|
23358
|
-
return true;
|
|
23359
|
-
});
|
|
23687
|
+
const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
|
|
23360
23688
|
if (options.dryRun)
|
|
23361
|
-
return { events, deliveries: [] };
|
|
23689
|
+
return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
23362
23690
|
const deliveries = [];
|
|
23363
|
-
for (const event of events) {
|
|
23691
|
+
for (const event of page.events) {
|
|
23364
23692
|
deliveries.push(...await this.deliver(event));
|
|
23365
23693
|
}
|
|
23366
|
-
return { events, deliveries };
|
|
23694
|
+
return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
23695
|
+
}
|
|
23696
|
+
async appendEvent(event, options) {
|
|
23697
|
+
if (this.store.appendEventOnce) {
|
|
23698
|
+
return this.store.appendEventOnce(event, { dedupe: options.dedupe });
|
|
23699
|
+
}
|
|
23700
|
+
if (options.dedupe) {
|
|
23701
|
+
const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
|
|
23702
|
+
if (existing) {
|
|
23703
|
+
return {
|
|
23704
|
+
event: existing,
|
|
23705
|
+
stored: false,
|
|
23706
|
+
deduped: true,
|
|
23707
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
23708
|
+
};
|
|
23709
|
+
}
|
|
23710
|
+
}
|
|
23711
|
+
const stored = await this.store.appendEvent(event);
|
|
23712
|
+
return {
|
|
23713
|
+
event: stored,
|
|
23714
|
+
stored: true,
|
|
23715
|
+
deduped: false,
|
|
23716
|
+
identity: { id: stored.id, dedupeKey: stored.dedupeKey }
|
|
23717
|
+
};
|
|
23367
23718
|
}
|
|
23368
23719
|
async applyRedaction(event, channel) {
|
|
23369
23720
|
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
@@ -23390,43 +23741,19 @@ class EventsClient {
|
|
|
23390
23741
|
return createDeliveryResult(event, channel, attempts);
|
|
23391
23742
|
}
|
|
23392
23743
|
}
|
|
23393
|
-
function
|
|
23394
|
-
|
|
23395
|
-
|
|
23396
|
-
|
|
23397
|
-
|
|
23398
|
-
|
|
23399
|
-
|
|
23400
|
-
|
|
23401
|
-
|
|
23402
|
-
|
|
23403
|
-
|
|
23404
|
-
|
|
23405
|
-
|
|
23406
|
-
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
23407
|
-
}
|
|
23408
|
-
function redactValue(value, replacement) {
|
|
23409
|
-
if (Array.isArray(value))
|
|
23410
|
-
return value.map((item) => redactValue(item, replacement));
|
|
23411
|
-
if (!value || typeof value !== "object")
|
|
23412
|
-
return value;
|
|
23413
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
23414
|
-
key,
|
|
23415
|
-
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
23416
|
-
]));
|
|
23417
|
-
}
|
|
23418
|
-
function setPath(input, path, replacement) {
|
|
23419
|
-
const parts = path.split(".");
|
|
23420
|
-
let cursor = input;
|
|
23421
|
-
for (const part of parts.slice(0, -1)) {
|
|
23422
|
-
const next = cursor[part];
|
|
23423
|
-
if (!next || typeof next !== "object")
|
|
23424
|
-
return;
|
|
23425
|
-
cursor = next;
|
|
23426
|
-
}
|
|
23427
|
-
const last = parts.at(-1);
|
|
23428
|
-
if (last && last in cursor)
|
|
23429
|
-
cursor[last] = replacement;
|
|
23744
|
+
function queryClientEvents(events, options) {
|
|
23745
|
+
let rows = events;
|
|
23746
|
+
if (options.eventId)
|
|
23747
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
23748
|
+
if (options.source)
|
|
23749
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
23750
|
+
if (options.type)
|
|
23751
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
23752
|
+
if (options.cursor)
|
|
23753
|
+
rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
|
|
23754
|
+
if (options.limit !== undefined)
|
|
23755
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
23756
|
+
return rows;
|
|
23430
23757
|
}
|
|
23431
23758
|
function normalizeTime(value) {
|
|
23432
23759
|
if (!value)
|
|
@@ -23440,9 +23767,22 @@ function normalizeRetryPolicy(policy) {
|
|
|
23440
23767
|
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
23441
23768
|
};
|
|
23442
23769
|
}
|
|
23443
|
-
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", DEFAULT_SIGNATURE_TOLERANCE_MS;
|
|
23770
|
+
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:", DEFAULT_EVENT_PAGE_LIMIT = 100, MAX_EVENT_PAGE_LIMIT = 1000, DEFAULT_SIGNATURE_TOLERANCE_MS, EventValidationError, defaultEventTypeCatalog, APP_EVENT_V1_MAX_DATA_BYTES;
|
|
23444
23771
|
var init_dist = __esm(() => {
|
|
23445
23772
|
DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
23773
|
+
EventValidationError = class EventValidationError extends Error {
|
|
23774
|
+
eventType;
|
|
23775
|
+
issues;
|
|
23776
|
+
constructor(eventType, issues) {
|
|
23777
|
+
const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
|
|
23778
|
+
super(`Event validation failed for type "${eventType}": ${detail}`);
|
|
23779
|
+
this.name = "EventValidationError";
|
|
23780
|
+
this.eventType = eventType;
|
|
23781
|
+
this.issues = issues;
|
|
23782
|
+
}
|
|
23783
|
+
};
|
|
23784
|
+
defaultEventTypeCatalog = new EventTypeCatalog;
|
|
23785
|
+
APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
|
|
23446
23786
|
});
|
|
23447
23787
|
|
|
23448
23788
|
// src/sdk/event-sink.ts
|
|
@@ -23526,6 +23866,7 @@ var ANY_SEGMENT_EXCLUDES = new Set([
|
|
|
23526
23866
|
".docker"
|
|
23527
23867
|
]);
|
|
23528
23868
|
var ROOT_EXCLUDES = new Set(["dist", "build", ".turbo"]);
|
|
23869
|
+
var TOOL_SIDECAR_FILENAMES = new Set([".hasna-skills.json"]);
|
|
23529
23870
|
var CREDENTIAL_FILENAMES = new Set([
|
|
23530
23871
|
".npmrc",
|
|
23531
23872
|
".pypirc",
|
|
@@ -33591,9 +33932,10 @@ function resolveServerConfig(env = process.env) {
|
|
|
33591
33932
|
artifactBucket: env.HASNA_SKILLS_S3_BUCKET || env.SKILLS_S3_BUCKET || undefined,
|
|
33592
33933
|
artifactPrefix: normalizePrefix(env.HASNA_SKILLS_S3_PREFIX || env.SKILLS_S3_PREFIX || "skills/artifacts"),
|
|
33593
33934
|
inlineWorker: env.HASNA_SKILLS_INLINE_WORKER === "1",
|
|
33594
|
-
bundleSigningKey: env.HASNA_SKILLS_SIGNING_KEY || undefined,
|
|
33935
|
+
bundleSigningKey: env.HASNA_SKILLS_API_SIGNING_KEY || env.HASNA_SKILLS_SIGNING_KEY || undefined,
|
|
33595
33936
|
requestBodyLimitBytes: parsePositiveInt(env.HASNA_SKILLS_REQUEST_BODY_LIMIT_BYTES, 1e6),
|
|
33596
33937
|
skillBundleLimitBytes: parsePositiveInt(env.HASNA_SKILLS_BUNDLE_LIMIT_BYTES, 25000000),
|
|
33938
|
+
tombstoneWindowMs: parsePositiveInt(env.HASNA_SKILLS_TOMBSTONE_WINDOW_MS, 7 * 24 * 60 * 60 * 1000),
|
|
33597
33939
|
publicBaseUrl: (env.SKILLS_PUBLIC_BASE_URL || localOrigin(host, port)).replace(/\/+$/, ""),
|
|
33598
33940
|
nodeEnv,
|
|
33599
33941
|
allowEphemeralStore: env.HASNA_SKILLS_ALLOW_EPHEMERAL_STORE === "1"
|
|
@@ -33671,6 +34013,11 @@ function normalizeConfigValue(key, value) {
|
|
|
33671
34013
|
}
|
|
33672
34014
|
var DATA_DIR_ENV = "HASNA_SKILLS_DIR";
|
|
33673
34015
|
var INSTALLED_SKILLS_DIRNAME = "installed";
|
|
34016
|
+
var SKILLS_CACHE_DIRNAME = "skills";
|
|
34017
|
+
var LAYOUT_MIGRATION_RECORD = ".layout-migration.json";
|
|
34018
|
+
function isOwnerLayoutMigrated(appDir) {
|
|
34019
|
+
return existsSync(join2(appDir, SKILLS_CACHE_DIRNAME, LAYOUT_MIGRATION_RECORD));
|
|
34020
|
+
}
|
|
33674
34021
|
function getDataDir() {
|
|
33675
34022
|
const override = process.env[DATA_DIR_ENV];
|
|
33676
34023
|
if (override) {
|
|
@@ -33849,7 +34196,7 @@ function looksLikeSqlitePath(value) {
|
|
|
33849
34196
|
}
|
|
33850
34197
|
|
|
33851
34198
|
// src/server/handlers.ts
|
|
33852
|
-
import { createHash as
|
|
34199
|
+
import { createHash as createHash6 } from "crypto";
|
|
33853
34200
|
|
|
33854
34201
|
// src/server/store.ts
|
|
33855
34202
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
@@ -33870,6 +34217,19 @@ class StaleLeaseGenerationError extends Error {
|
|
|
33870
34217
|
}
|
|
33871
34218
|
}
|
|
33872
34219
|
|
|
34220
|
+
class SkillRevisionConflictError extends Error {
|
|
34221
|
+
slug;
|
|
34222
|
+
expectedRevisionId;
|
|
34223
|
+
currentRevisionId;
|
|
34224
|
+
constructor(slug, expectedRevisionId, currentRevisionId) {
|
|
34225
|
+
super(`revision conflict for '${slug}': expected revision ${expectedRevisionId ?? "(none)"}, ` + `current is ${currentRevisionId ?? "(none)"}. Refused rather than silently overwriting a newer revision.`);
|
|
34226
|
+
this.name = "SkillRevisionConflictError";
|
|
34227
|
+
this.slug = slug;
|
|
34228
|
+
this.expectedRevisionId = expectedRevisionId;
|
|
34229
|
+
this.currentRevisionId = currentRevisionId;
|
|
34230
|
+
}
|
|
34231
|
+
}
|
|
34232
|
+
|
|
33873
34233
|
// src/server/rows.ts
|
|
33874
34234
|
import { randomUUID } from "crypto";
|
|
33875
34235
|
function nowIso() {
|
|
@@ -33950,7 +34310,20 @@ function rowToSkill(row) {
|
|
|
33950
34310
|
...row.bundle_byte_size === null || row.bundle_byte_size === undefined ? {} : { bundleByteSize: Number(row.bundle_byte_size) },
|
|
33951
34311
|
...typeof row.published_by_user_id === "string" ? { publishedByUserId: row.published_by_user_id } : {},
|
|
33952
34312
|
createdAt: dateString(row.created_at),
|
|
33953
|
-
updatedAt: dateString(row.updated_at)
|
|
34313
|
+
updatedAt: dateString(row.updated_at),
|
|
34314
|
+
revisionId: String(row.revision_id ?? ""),
|
|
34315
|
+
revisionNumber: Number(row.revision_number ?? 0),
|
|
34316
|
+
...row.tombstoned_at ? { tombstonedAt: dateString(row.tombstoned_at) } : {},
|
|
34317
|
+
...row.tombstone_purge_after ? { tombstonePurgeAfter: dateString(row.tombstone_purge_after) } : {}
|
|
34318
|
+
};
|
|
34319
|
+
}
|
|
34320
|
+
function rowToPin(row) {
|
|
34321
|
+
return {
|
|
34322
|
+
orgId: String(row.org_id),
|
|
34323
|
+
principal: String(row.principal),
|
|
34324
|
+
slug: String(row.slug),
|
|
34325
|
+
pinnedAt: dateString(row.pinned_at),
|
|
34326
|
+
metadata: parseJsonObject(row.metadata_json)
|
|
33954
34327
|
};
|
|
33955
34328
|
}
|
|
33956
34329
|
function rowToSkillBundle(row) {
|
|
@@ -34002,6 +34375,29 @@ function dateString(value) {
|
|
|
34002
34375
|
return String(value);
|
|
34003
34376
|
}
|
|
34004
34377
|
|
|
34378
|
+
// src/lib/revision.ts
|
|
34379
|
+
import { createHash as createHash5 } from "crypto";
|
|
34380
|
+
var REVISION_ID_PATTERN = /^[0-9a-f]{64}$/;
|
|
34381
|
+
function revisionIdOf(content) {
|
|
34382
|
+
const canonical = JSON.stringify({
|
|
34383
|
+
slug: content.slug,
|
|
34384
|
+
displayName: content.displayName,
|
|
34385
|
+
description: content.description,
|
|
34386
|
+
category: content.category,
|
|
34387
|
+
tags: content.tags,
|
|
34388
|
+
source: content.source,
|
|
34389
|
+
kind: content.kind,
|
|
34390
|
+
version: content.version ?? null,
|
|
34391
|
+
skillMd: content.skillMd ?? null,
|
|
34392
|
+
bundleSha256: content.bundleSha256 ?? null,
|
|
34393
|
+
bundleByteSize: content.bundleByteSize ?? null
|
|
34394
|
+
});
|
|
34395
|
+
return createHash5("sha256").update(canonical).digest("hex");
|
|
34396
|
+
}
|
|
34397
|
+
function revisionIdOfRecord(record) {
|
|
34398
|
+
return revisionIdOf(record);
|
|
34399
|
+
}
|
|
34400
|
+
|
|
34005
34401
|
// src/server/sqlite-store.ts
|
|
34006
34402
|
import { Database } from "bun:sqlite";
|
|
34007
34403
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -34049,6 +34445,7 @@ function defaultStartDirs() {
|
|
|
34049
34445
|
// src/server/sqlite-store.ts
|
|
34050
34446
|
var CLAIM_ATTEMPTS = 8;
|
|
34051
34447
|
var CLAIMABLE_STATUSES = ["queued", "retrying"];
|
|
34448
|
+
var NO_REVISION_SENTINEL = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
34052
34449
|
var LAST_USED_RESOLUTION_MS = 60000;
|
|
34053
34450
|
|
|
34054
34451
|
class SqliteSkillsStore {
|
|
@@ -34068,12 +34465,24 @@ class SqliteSkillsStore {
|
|
|
34068
34465
|
if (options.migrate !== false) {
|
|
34069
34466
|
applySqliteMigrations(this.db, options.migrationsDir);
|
|
34070
34467
|
}
|
|
34468
|
+
this.backfillLegacyRevisions();
|
|
34071
34469
|
this.backend = {
|
|
34072
34470
|
kind: "sqlite",
|
|
34073
34471
|
durable: !inMemory,
|
|
34074
34472
|
label: inMemory ? "sqlite (in-memory)" : `sqlite (${path})`
|
|
34075
34473
|
};
|
|
34076
34474
|
}
|
|
34475
|
+
backfillLegacyRevisions() {
|
|
34476
|
+
const rows = this.all("SELECT * FROM skills_registry WHERE revision_id = ''", []);
|
|
34477
|
+
for (const row of rows) {
|
|
34478
|
+
const record = rowToSkill(row);
|
|
34479
|
+
this.db.run("UPDATE skills_registry SET revision_id = ? WHERE org_id = ? AND slug = ?", [
|
|
34480
|
+
revisionIdOfRecord(record),
|
|
34481
|
+
record.orgId,
|
|
34482
|
+
record.slug
|
|
34483
|
+
]);
|
|
34484
|
+
}
|
|
34485
|
+
}
|
|
34077
34486
|
get database() {
|
|
34078
34487
|
return this.db;
|
|
34079
34488
|
}
|
|
@@ -34297,8 +34706,29 @@ class SqliteSkillsStore {
|
|
|
34297
34706
|
const orgId = input.principal.orgId;
|
|
34298
34707
|
const now = nowIso();
|
|
34299
34708
|
return this.db.transaction(() => {
|
|
34300
|
-
const previous = this.get("SELECT bundle_sha256 FROM skills_registry WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
34709
|
+
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]);
|
|
34301
34710
|
const previousSha = typeof previous?.bundle_sha256 === "string" ? previous.bundle_sha256 : null;
|
|
34711
|
+
const previousRevisionId = typeof previous?.revision_id === "string" && previous.revision_id ? previous.revision_id : null;
|
|
34712
|
+
const tombstoned = previous?.tombstoned_at != null;
|
|
34713
|
+
const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : typeof previous?.skill_md === "string" ? previous.skill_md : null;
|
|
34714
|
+
if (previous && !tombstoned && input.expectedRevisionId !== previousRevisionId) {
|
|
34715
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
|
|
34716
|
+
}
|
|
34717
|
+
const carriedSha = input.bundle?.sha256 ?? previousSha;
|
|
34718
|
+
const carriedSize = input.bundle?.byteSize ?? (previous?.bundle_byte_size == null ? null : Number(previous.bundle_byte_size));
|
|
34719
|
+
const revisionId = revisionIdOfRecord({
|
|
34720
|
+
slug: input.slug,
|
|
34721
|
+
displayName: input.displayName,
|
|
34722
|
+
description: input.description,
|
|
34723
|
+
category: input.category,
|
|
34724
|
+
tags: input.tags,
|
|
34725
|
+
source: input.source,
|
|
34726
|
+
kind: input.kind,
|
|
34727
|
+
...input.version ? { version: input.version } : {},
|
|
34728
|
+
...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
|
|
34729
|
+
...carriedSha ? { bundleSha256: carriedSha } : {},
|
|
34730
|
+
...carriedSize === null || carriedSize === undefined ? {} : { bundleByteSize: carriedSize }
|
|
34731
|
+
});
|
|
34302
34732
|
if (input.bundle) {
|
|
34303
34733
|
this.db.run(`INSERT INTO skills_bundles (org_id, sha256, byte_size, content_type, storage_kind, storage_key, body_blob, created_at)
|
|
34304
34734
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -34319,8 +34749,8 @@ class SqliteSkillsStore {
|
|
|
34319
34749
|
]);
|
|
34320
34750
|
}
|
|
34321
34751
|
const row = this.get(`INSERT INTO skills_registry (org_id, slug, display_name, description, category, tags_json, source, kind, version, skill_md,
|
|
34322
|
-
bundle_sha256, bundle_byte_size, published_by_user_id, created_at, updated_at)
|
|
34323
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
34752
|
+
bundle_sha256, bundle_byte_size, published_by_user_id, revision_id, revision_number, created_at, updated_at)
|
|
34753
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
34324
34754
|
ON CONFLICT (org_id, slug) DO UPDATE SET
|
|
34325
34755
|
display_name = excluded.display_name,
|
|
34326
34756
|
description = excluded.description,
|
|
@@ -34338,7 +34768,16 @@ class SqliteSkillsStore {
|
|
|
34338
34768
|
bundle_sha256 = COALESCE(excluded.bundle_sha256, skills_registry.bundle_sha256),
|
|
34339
34769
|
bundle_byte_size = COALESCE(excluded.bundle_byte_size, skills_registry.bundle_byte_size),
|
|
34340
34770
|
published_by_user_id = excluded.published_by_user_id,
|
|
34771
|
+
revision_id = excluded.revision_id,
|
|
34772
|
+
-- Current + 1, not the inserted 1: on the update path the ACTUAL row's
|
|
34773
|
+
-- counter is the truth (it may have advanced since the pre-read, which is
|
|
34774
|
+
-- exactly what the WHERE guard below detects).
|
|
34775
|
+
revision_number = skills_registry.revision_number + 1,
|
|
34776
|
+
tombstoned_at = NULL,
|
|
34777
|
+
tombstone_purge_after = NULL,
|
|
34341
34778
|
updated_at = excluded.updated_at
|
|
34779
|
+
WHERE skills_registry.tombstoned_at IS NOT NULL
|
|
34780
|
+
OR skills_registry.revision_id = ?
|
|
34342
34781
|
RETURNING *`, [
|
|
34343
34782
|
orgId,
|
|
34344
34783
|
input.slug,
|
|
@@ -34349,62 +34788,165 @@ class SqliteSkillsStore {
|
|
|
34349
34788
|
input.source,
|
|
34350
34789
|
input.kind,
|
|
34351
34790
|
input.version ?? null,
|
|
34352
|
-
|
|
34791
|
+
carriedSkillMd,
|
|
34353
34792
|
input.bundle?.sha256 ?? null,
|
|
34354
34793
|
input.bundle?.byteSize ?? null,
|
|
34355
34794
|
input.principal.userId,
|
|
34795
|
+
revisionId,
|
|
34356
34796
|
now,
|
|
34357
|
-
now
|
|
34797
|
+
now,
|
|
34798
|
+
input.expectedRevisionId ?? NO_REVISION_SENTINEL
|
|
34358
34799
|
]);
|
|
34800
|
+
if (!row) {
|
|
34801
|
+
const current = this.get("SELECT revision_id FROM skills_registry WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
34802
|
+
const currentId = typeof current?.revision_id === "string" ? current.revision_id : null;
|
|
34803
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, currentId);
|
|
34804
|
+
}
|
|
34359
34805
|
if (previousSha && input.bundle && previousSha !== input.bundle.sha256)
|
|
34360
34806
|
this.collectOrphanBundle(orgId, previousSha);
|
|
34807
|
+
this.db.run("DELETE FROM skills_tags WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
34808
|
+
const insertTag = this.db.prepare("INSERT OR IGNORE INTO skills_tags (org_id, slug, tag) VALUES (?, ?, ?)");
|
|
34809
|
+
for (const tag of input.tags) {
|
|
34810
|
+
if (!tag.trim())
|
|
34811
|
+
continue;
|
|
34812
|
+
insertTag.run(orgId, input.slug, tag);
|
|
34813
|
+
}
|
|
34361
34814
|
return rowToSkill(row);
|
|
34362
34815
|
})();
|
|
34363
34816
|
}
|
|
34364
34817
|
async listSkills(principal) {
|
|
34365
|
-
|
|
34818
|
+
await this.purgeExpiredTombstones(principal);
|
|
34819
|
+
return this.all("SELECT * FROM skills_registry WHERE org_id = ? AND tombstoned_at IS NULL ORDER BY slug ASC", [principal.orgId]).map(rowToSkill);
|
|
34366
34820
|
}
|
|
34367
34821
|
async getSkill(principal, slug) {
|
|
34368
34822
|
const row = this.get("SELECT * FROM skills_registry WHERE org_id = ? AND slug = ? LIMIT 1", [principal.orgId, slug]);
|
|
34369
34823
|
return row ? rowToSkill(row) : null;
|
|
34370
34824
|
}
|
|
34371
|
-
async updateSkill(principal, slug, patch) {
|
|
34825
|
+
async updateSkill(principal, slug, patch, expectedRevisionId) {
|
|
34372
34826
|
const current = await this.getSkill(principal, slug);
|
|
34373
|
-
if (!current)
|
|
34827
|
+
if (!current || current.tombstonedAt)
|
|
34374
34828
|
return null;
|
|
34829
|
+
if (expectedRevisionId !== current.revisionId) {
|
|
34830
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
|
|
34831
|
+
}
|
|
34375
34832
|
const next = { ...current, ...patch };
|
|
34376
|
-
|
|
34377
|
-
|
|
34378
|
-
|
|
34379
|
-
|
|
34380
|
-
|
|
34381
|
-
|
|
34382
|
-
|
|
34383
|
-
|
|
34384
|
-
|
|
34385
|
-
|
|
34386
|
-
|
|
34387
|
-
|
|
34388
|
-
|
|
34389
|
-
|
|
34390
|
-
|
|
34391
|
-
|
|
34833
|
+
return this.db.transaction(() => {
|
|
34834
|
+
const revisionId = revisionIdOfRecord(next);
|
|
34835
|
+
const row = this.get(`UPDATE skills_registry
|
|
34836
|
+
SET display_name = ?, description = ?, category = ?, tags_json = ?, kind = ?, version = ?, skill_md = ?,
|
|
34837
|
+
revision_id = ?, revision_number = revision_number + 1, updated_at = ?
|
|
34838
|
+
WHERE org_id = ? AND slug = ? AND tombstoned_at IS NULL AND revision_id = ?
|
|
34839
|
+
RETURNING *`, [
|
|
34840
|
+
next.displayName,
|
|
34841
|
+
next.description,
|
|
34842
|
+
next.category,
|
|
34843
|
+
JSON.stringify(next.tags),
|
|
34844
|
+
next.kind,
|
|
34845
|
+
next.version ?? null,
|
|
34846
|
+
next.skillMd ?? null,
|
|
34847
|
+
revisionId,
|
|
34848
|
+
nowIso(),
|
|
34849
|
+
principal.orgId,
|
|
34850
|
+
slug,
|
|
34851
|
+
current.revisionId
|
|
34852
|
+
]);
|
|
34853
|
+
if (!row) {
|
|
34854
|
+
const nowRow = this.get("SELECT revision_id, tombstoned_at FROM skills_registry WHERE org_id = ? AND slug = ? LIMIT 1", [
|
|
34855
|
+
principal.orgId,
|
|
34856
|
+
slug
|
|
34857
|
+
]);
|
|
34858
|
+
if (nowRow && nowRow.tombstoned_at == null) {
|
|
34859
|
+
const currentId = typeof nowRow.revision_id === "string" ? nowRow.revision_id : null;
|
|
34860
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, currentId);
|
|
34861
|
+
}
|
|
34862
|
+
return null;
|
|
34863
|
+
}
|
|
34864
|
+
this.db.run("DELETE FROM skills_tags WHERE org_id = ? AND slug = ?", [principal.orgId, slug]);
|
|
34865
|
+
const insertTag = this.db.prepare("INSERT OR IGNORE INTO skills_tags (org_id, slug, tag) VALUES (?, ?, ?)");
|
|
34866
|
+
for (const tag of next.tags) {
|
|
34867
|
+
if (!tag.trim())
|
|
34868
|
+
continue;
|
|
34869
|
+
insertTag.run(principal.orgId, slug, tag);
|
|
34870
|
+
}
|
|
34871
|
+
return rowToSkill(row);
|
|
34872
|
+
})();
|
|
34392
34873
|
}
|
|
34393
|
-
async deleteSkill(principal, slug) {
|
|
34874
|
+
async deleteSkill(principal, slug, tombstoneWindowMs) {
|
|
34394
34875
|
return this.db.transaction(() => {
|
|
34395
|
-
const existing = this.get("SELECT
|
|
34876
|
+
const existing = this.get("SELECT tombstoned_at FROM skills_registry WHERE org_id = ? AND slug = ?", [principal.orgId, slug]);
|
|
34396
34877
|
if (!existing)
|
|
34397
|
-
return
|
|
34398
|
-
|
|
34399
|
-
|
|
34400
|
-
|
|
34401
|
-
|
|
34878
|
+
return null;
|
|
34879
|
+
if (existing.tombstoned_at != null) {
|
|
34880
|
+
const row2 = this.get("SELECT * FROM skills_registry WHERE org_id = ? AND slug = ? LIMIT 1", [principal.orgId, slug]);
|
|
34881
|
+
return rowToSkill(row2);
|
|
34882
|
+
}
|
|
34883
|
+
const tombstonedAt = nowIso();
|
|
34884
|
+
const purgeAfter = new Date(Date.now() + tombstoneWindowMs).toISOString();
|
|
34885
|
+
const row = this.get(`UPDATE skills_registry
|
|
34886
|
+
SET tombstoned_at = ?, tombstone_purge_after = ?, updated_at = ?
|
|
34887
|
+
WHERE org_id = ? AND slug = ?
|
|
34888
|
+
RETURNING *`, [tombstonedAt, purgeAfter, tombstonedAt, principal.orgId, slug]);
|
|
34889
|
+
return rowToSkill(row);
|
|
34890
|
+
})();
|
|
34891
|
+
}
|
|
34892
|
+
async purgeExpiredTombstones(principal) {
|
|
34893
|
+
return this.db.transaction(() => {
|
|
34894
|
+
const now = nowIso();
|
|
34895
|
+
const expired = this.all("SELECT * FROM skills_registry WHERE org_id = ? AND tombstoned_at IS NOT NULL AND tombstone_purge_after <= ?", [principal.orgId, now]);
|
|
34896
|
+
const purged = [];
|
|
34897
|
+
for (const row of expired) {
|
|
34898
|
+
const record = rowToSkill(row);
|
|
34899
|
+
this.db.run("DELETE FROM skills_registry WHERE org_id = ? AND slug = ?", [principal.orgId, record.slug]);
|
|
34900
|
+
this.db.run("DELETE FROM skills_tags WHERE org_id = ? AND slug = ?", [principal.orgId, record.slug]);
|
|
34901
|
+
if (record.bundleSha256)
|
|
34902
|
+
this.collectOrphanBundle(principal.orgId, record.bundleSha256);
|
|
34903
|
+
purged.push(record);
|
|
34904
|
+
}
|
|
34905
|
+
return purged;
|
|
34402
34906
|
})();
|
|
34403
34907
|
}
|
|
34404
34908
|
async getSkillBundle(principal, sha256) {
|
|
34405
34909
|
const row = this.get("SELECT * FROM skills_bundles WHERE org_id = ? AND sha256 = ? LIMIT 1", [principal.orgId, sha256]);
|
|
34406
34910
|
return row ? rowToSkillBundle(row) : null;
|
|
34407
34911
|
}
|
|
34912
|
+
async pinSkill(principal, slug, metadata = {}) {
|
|
34913
|
+
const row = this.get(`INSERT INTO skills_pins (org_id, principal, slug, pinned_at, metadata_json)
|
|
34914
|
+
VALUES (?, ?, ?, ?, ?)
|
|
34915
|
+
ON CONFLICT (org_id, principal, slug) DO UPDATE SET
|
|
34916
|
+
pinned_at = excluded.pinned_at,
|
|
34917
|
+
metadata_json = excluded.metadata_json
|
|
34918
|
+
RETURNING *`, [principal.orgId, principal.apiKeyId, slug, nowIso(), JSON.stringify(metadata)]);
|
|
34919
|
+
return rowToPin(row);
|
|
34920
|
+
}
|
|
34921
|
+
async unpinSkill(principal, slug) {
|
|
34922
|
+
const result = this.db.run("DELETE FROM skills_pins WHERE org_id = ? AND principal = ? AND slug = ?", [principal.orgId, principal.apiKeyId, slug]);
|
|
34923
|
+
return result.changes > 0;
|
|
34924
|
+
}
|
|
34925
|
+
async listPins(principal) {
|
|
34926
|
+
return this.all("SELECT * FROM skills_pins WHERE org_id = ? AND principal = ? ORDER BY slug ASC", [principal.orgId, principal.apiKeyId]).map(rowToPin);
|
|
34927
|
+
}
|
|
34928
|
+
async listTags(principal) {
|
|
34929
|
+
await this.purgeExpiredTombstones(principal);
|
|
34930
|
+
return this.all("SELECT DISTINCT tag FROM skills_tags WHERE org_id = ? ORDER BY tag ASC", [principal.orgId]).map((row) => String(row.tag));
|
|
34931
|
+
}
|
|
34932
|
+
async listSkillsByTag(principal, tag) {
|
|
34933
|
+
await this.purgeExpiredTombstones(principal);
|
|
34934
|
+
return this.all(`SELECT s.* FROM skills_registry s
|
|
34935
|
+
JOIN skills_tags t ON t.org_id = s.org_id AND t.slug = s.slug
|
|
34936
|
+
WHERE t.org_id = ? AND t.tag = ? AND s.tombstoned_at IS NULL
|
|
34937
|
+
ORDER BY s.slug ASC`, [principal.orgId, tag]).map(rowToSkill);
|
|
34938
|
+
}
|
|
34939
|
+
async listPinsByTag(principal, tag) {
|
|
34940
|
+
await this.purgeExpiredTombstones(principal);
|
|
34941
|
+
return this.all(`SELECT p.* FROM skills_pins p
|
|
34942
|
+
JOIN skills_tags t ON t.org_id = p.org_id AND t.slug = p.slug
|
|
34943
|
+
JOIN skills_registry s ON s.org_id = p.org_id AND s.slug = p.slug
|
|
34944
|
+
WHERE p.org_id = ? AND p.principal = ? AND t.tag = ? AND s.tombstoned_at IS NULL
|
|
34945
|
+
ORDER BY p.slug ASC`, [principal.orgId, principal.apiKeyId, tag]).map(rowToPin);
|
|
34946
|
+
}
|
|
34947
|
+
async listPublishedSlugs(principal) {
|
|
34948
|
+
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));
|
|
34949
|
+
}
|
|
34408
34950
|
collectOrphanBundle(orgId, sha256) {
|
|
34409
34951
|
const referenced = this.get("SELECT 1 AS present FROM skills_registry WHERE org_id = ? AND bundle_sha256 = ? LIMIT 1", [orgId, sha256]);
|
|
34410
34952
|
if (referenced)
|
|
@@ -34491,6 +35033,21 @@ function parseScopes(value) {
|
|
|
34491
35033
|
}
|
|
34492
35034
|
|
|
34493
35035
|
// src/server/store.ts
|
|
35036
|
+
function recordFieldsOf(input, carriedBundle, carriedSkillMd) {
|
|
35037
|
+
return {
|
|
35038
|
+
slug: input.slug,
|
|
35039
|
+
displayName: input.displayName,
|
|
35040
|
+
description: input.description,
|
|
35041
|
+
category: input.category,
|
|
35042
|
+
tags: input.tags,
|
|
35043
|
+
source: input.source,
|
|
35044
|
+
kind: input.kind,
|
|
35045
|
+
...input.version ? { version: input.version } : {},
|
|
35046
|
+
...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
|
|
35047
|
+
bundleSha256: input.bundle?.sha256 ?? carriedBundle.bundleSha256,
|
|
35048
|
+
bundleByteSize: input.bundle?.byteSize ?? carriedBundle.bundleByteSize
|
|
35049
|
+
};
|
|
35050
|
+
}
|
|
34494
35051
|
function resolvePoolMax(env = process.env) {
|
|
34495
35052
|
const parsed = Number.parseInt(env.HASNA_SKILLS_DATABASE_POOL_MAX || env.SKILLS_DATABASE_POOL_MAX || "", 10);
|
|
34496
35053
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 4;
|
|
@@ -34528,6 +35085,7 @@ class MemorySkillsStore {
|
|
|
34528
35085
|
idempotency = new Map;
|
|
34529
35086
|
skills = new Map;
|
|
34530
35087
|
bundles = new Map;
|
|
35088
|
+
pins = new Map;
|
|
34531
35089
|
constructor(apiKeys = []) {
|
|
34532
35090
|
for (const key of apiKeys)
|
|
34533
35091
|
this.addApiKey(key.token, key.principal);
|
|
@@ -34629,6 +35187,9 @@ class MemorySkillsStore {
|
|
|
34629
35187
|
const key = skillKey(input.principal.orgId, input.slug);
|
|
34630
35188
|
const now = nowIso();
|
|
34631
35189
|
const previous = this.skills.get(key);
|
|
35190
|
+
if (previous && !previous.tombstonedAt && input.expectedRevisionId !== previous.revisionId) {
|
|
35191
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previous.revisionId);
|
|
35192
|
+
}
|
|
34632
35193
|
if (input.bundle) {
|
|
34633
35194
|
const bundleMapKey = skillKey(input.principal.orgId, input.bundle.sha256);
|
|
34634
35195
|
this.bundles.set(bundleMapKey, {
|
|
@@ -34638,6 +35199,8 @@ class MemorySkillsStore {
|
|
|
34638
35199
|
createdAt: this.bundles.get(bundleMapKey)?.createdAt ?? now
|
|
34639
35200
|
});
|
|
34640
35201
|
}
|
|
35202
|
+
const carriedBundle = !input.bundle && previous?.bundleSha256 ? { bundleSha256: previous.bundleSha256, ...previous.bundleByteSize === undefined ? {} : { bundleByteSize: previous.bundleByteSize } } : {};
|
|
35203
|
+
const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : previous?.skillMd;
|
|
34641
35204
|
const record = {
|
|
34642
35205
|
orgId: input.principal.orgId,
|
|
34643
35206
|
slug: input.slug,
|
|
@@ -34648,11 +35211,13 @@ class MemorySkillsStore {
|
|
|
34648
35211
|
source: input.source,
|
|
34649
35212
|
kind: input.kind,
|
|
34650
35213
|
...input.version ? { version: input.version } : {},
|
|
34651
|
-
...
|
|
34652
|
-
...input.bundle ? { bundleSha256: input.bundle.sha256, bundleByteSize: input.bundle.byteSize } :
|
|
35214
|
+
...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
|
|
35215
|
+
...input.bundle ? { bundleSha256: input.bundle.sha256, bundleByteSize: input.bundle.byteSize } : carriedBundle,
|
|
34653
35216
|
publishedByUserId: input.principal.userId,
|
|
34654
35217
|
createdAt: previous?.createdAt ?? now,
|
|
34655
|
-
updatedAt: now
|
|
35218
|
+
updatedAt: now,
|
|
35219
|
+
revisionId: revisionIdOfRecord(recordFieldsOf(input, carriedBundle, carriedSkillMd)),
|
|
35220
|
+
revisionNumber: (previous?.revisionNumber ?? 0) + 1
|
|
34656
35221
|
};
|
|
34657
35222
|
this.skills.set(key, record);
|
|
34658
35223
|
if (previous?.bundleSha256 && input.bundle && previous.bundleSha256 !== input.bundle.sha256) {
|
|
@@ -34661,33 +35226,107 @@ class MemorySkillsStore {
|
|
|
34661
35226
|
return record;
|
|
34662
35227
|
}
|
|
34663
35228
|
async listSkills(principal) {
|
|
34664
|
-
return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
|
|
35229
|
+
return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
|
|
34665
35230
|
}
|
|
34666
35231
|
async getSkill(principal, slug) {
|
|
34667
35232
|
const skill = this.skills.get(skillKey(principal.orgId, slug));
|
|
34668
35233
|
return skill && skill.orgId === principal.orgId ? skill : null;
|
|
34669
35234
|
}
|
|
34670
|
-
async updateSkill(principal, slug, patch) {
|
|
35235
|
+
async updateSkill(principal, slug, patch, expectedRevisionId) {
|
|
34671
35236
|
const current = await this.getSkill(principal, slug);
|
|
34672
|
-
if (!current)
|
|
35237
|
+
if (!current || current.tombstonedAt)
|
|
35238
|
+
return null;
|
|
35239
|
+
if (expectedRevisionId !== current.revisionId) {
|
|
35240
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
|
|
35241
|
+
}
|
|
35242
|
+
const latest = this.skills.get(skillKey(principal.orgId, slug));
|
|
35243
|
+
if (!latest || latest.tombstonedAt)
|
|
34673
35244
|
return null;
|
|
34674
|
-
|
|
35245
|
+
if (latest.revisionId !== current.revisionId) {
|
|
35246
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, latest.revisionId);
|
|
35247
|
+
}
|
|
35248
|
+
const next = {
|
|
35249
|
+
...latest,
|
|
35250
|
+
...patch,
|
|
35251
|
+
updatedAt: nowIso(),
|
|
35252
|
+
revisionId: revisionIdOfRecord({ ...latest, ...patch }),
|
|
35253
|
+
revisionNumber: latest.revisionNumber + 1
|
|
35254
|
+
};
|
|
34675
35255
|
this.skills.set(skillKey(principal.orgId, slug), next);
|
|
34676
35256
|
return next;
|
|
34677
35257
|
}
|
|
34678
|
-
async deleteSkill(principal, slug) {
|
|
35258
|
+
async deleteSkill(principal, slug, tombstoneWindowMs) {
|
|
34679
35259
|
const current = await this.getSkill(principal, slug);
|
|
34680
35260
|
if (!current)
|
|
34681
|
-
return
|
|
34682
|
-
|
|
34683
|
-
|
|
34684
|
-
|
|
34685
|
-
|
|
35261
|
+
return null;
|
|
35262
|
+
if (!current.tombstonedAt) {
|
|
35263
|
+
const tombstoned = nowIso();
|
|
35264
|
+
const purgeAfter = new Date(Date.now() + tombstoneWindowMs).toISOString();
|
|
35265
|
+
const next = { ...current, tombstonedAt: tombstoned, tombstonePurgeAfter: purgeAfter, updatedAt: tombstoned };
|
|
35266
|
+
this.skills.set(skillKey(principal.orgId, slug), next);
|
|
35267
|
+
return next;
|
|
35268
|
+
}
|
|
35269
|
+
return current;
|
|
35270
|
+
}
|
|
35271
|
+
async purgeExpiredTombstones(principal) {
|
|
35272
|
+
const now = nowIso();
|
|
35273
|
+
const purged = [];
|
|
35274
|
+
for (const [key, skill] of this.skills) {
|
|
35275
|
+
if (skill.orgId !== principal.orgId || !skill.tombstonedAt || !skill.tombstonePurgeAfter)
|
|
35276
|
+
continue;
|
|
35277
|
+
if (skill.tombstonePurgeAfter > now)
|
|
35278
|
+
continue;
|
|
35279
|
+
this.skills.delete(key);
|
|
35280
|
+
if (skill.bundleSha256)
|
|
35281
|
+
this.collectOrphanBundle(principal.orgId, skill.bundleSha256);
|
|
35282
|
+
purged.push(skill);
|
|
35283
|
+
}
|
|
35284
|
+
return purged;
|
|
34686
35285
|
}
|
|
34687
35286
|
async getSkillBundle(principal, sha256) {
|
|
34688
35287
|
const bundle = this.bundles.get(skillKey(principal.orgId, sha256));
|
|
34689
35288
|
return bundle && bundle.orgId === principal.orgId ? bundle : null;
|
|
34690
35289
|
}
|
|
35290
|
+
async pinSkill(principal, slug, metadata = {}) {
|
|
35291
|
+
const pin = { orgId: principal.orgId, principal: principal.apiKeyId, slug, pinnedAt: nowIso(), metadata: { ...metadata } };
|
|
35292
|
+
this.pins.set(pinKey(principal.orgId, principal.apiKeyId, slug), pin);
|
|
35293
|
+
return pin;
|
|
35294
|
+
}
|
|
35295
|
+
async unpinSkill(principal, slug) {
|
|
35296
|
+
return this.pins.delete(pinKey(principal.orgId, principal.apiKeyId, slug));
|
|
35297
|
+
}
|
|
35298
|
+
async listPins(principal) {
|
|
35299
|
+
return Array.from(this.pins.values()).filter((pin) => pin.orgId === principal.orgId && pin.principal === principal.apiKeyId).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
|
|
35300
|
+
}
|
|
35301
|
+
async listTags(principal) {
|
|
35302
|
+
await this.purgeExpiredTombstones(principal);
|
|
35303
|
+
const tags = new Set;
|
|
35304
|
+
for (const skill of this.skills.values()) {
|
|
35305
|
+
if (skill.orgId !== principal.orgId)
|
|
35306
|
+
continue;
|
|
35307
|
+
for (const tag of skill.tags) {
|
|
35308
|
+
if (tag.trim())
|
|
35309
|
+
tags.add(tag);
|
|
35310
|
+
}
|
|
35311
|
+
}
|
|
35312
|
+
return [...tags].sort();
|
|
35313
|
+
}
|
|
35314
|
+
async listSkillsByTag(principal, tag) {
|
|
35315
|
+
await this.purgeExpiredTombstones(principal);
|
|
35316
|
+
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));
|
|
35317
|
+
}
|
|
35318
|
+
async listPinsByTag(principal, tag) {
|
|
35319
|
+
await this.purgeExpiredTombstones(principal);
|
|
35320
|
+
const taggedSlugs = new Set;
|
|
35321
|
+
for (const skill of this.skills.values()) {
|
|
35322
|
+
if (skill.orgId === principal.orgId && !skill.tombstonedAt && skill.tags.includes(tag))
|
|
35323
|
+
taggedSlugs.add(skill.slug);
|
|
35324
|
+
}
|
|
35325
|
+
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));
|
|
35326
|
+
}
|
|
35327
|
+
async listPublishedSlugs(principal) {
|
|
35328
|
+
return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt).map((skill) => skill.slug).sort();
|
|
35329
|
+
}
|
|
34691
35330
|
collectOrphanBundle(orgId, sha256) {
|
|
34692
35331
|
const referenced = Array.from(this.skills.values()).some((skill) => skill.orgId === orgId && skill.bundleSha256 === sha256);
|
|
34693
35332
|
if (!referenced)
|
|
@@ -34705,6 +35344,9 @@ class MemorySkillsStore {
|
|
|
34705
35344
|
function skillKey(orgId, slug) {
|
|
34706
35345
|
return `${orgId.length}:${orgId}:${slug}`;
|
|
34707
35346
|
}
|
|
35347
|
+
function pinKey(orgId, principal, slug) {
|
|
35348
|
+
return `${orgId.length}:${orgId}:${principal.length}:${principal}:${slug}`;
|
|
35349
|
+
}
|
|
34708
35350
|
|
|
34709
35351
|
class PostgresSkillsStore {
|
|
34710
35352
|
backend = { kind: "postgres", durable: true, label: "postgres" };
|
|
@@ -34724,6 +35366,14 @@ class PostgresSkillsStore {
|
|
|
34724
35366
|
} catch (error) {
|
|
34725
35367
|
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)}`);
|
|
34726
35368
|
}
|
|
35369
|
+
await this.backfillLegacyRevisions();
|
|
35370
|
+
}
|
|
35371
|
+
async backfillLegacyRevisions() {
|
|
35372
|
+
const rows = await this.sql`SELECT * FROM skills_registry WHERE revision_id = ${""}`;
|
|
35373
|
+
for (const row of rows) {
|
|
35374
|
+
const record = rowToSkill(row);
|
|
35375
|
+
await this.sql`UPDATE skills_registry SET revision_id = ${revisionIdOfRecord(record)} WHERE org_id = ${record.orgId} AND slug = ${record.slug}`;
|
|
35376
|
+
}
|
|
34727
35377
|
}
|
|
34728
35378
|
async close() {
|
|
34729
35379
|
await this.sql.close?.();
|
|
@@ -34958,8 +35608,33 @@ class PostgresSkillsStore {
|
|
|
34958
35608
|
async publishSkill(input) {
|
|
34959
35609
|
const orgId = input.principal.orgId;
|
|
34960
35610
|
return await this.sql.begin(async (tx) => {
|
|
34961
|
-
const previousRows = await tx`
|
|
34962
|
-
|
|
35611
|
+
const previousRows = await tx`
|
|
35612
|
+
SELECT revision_id, revision_number, bundle_sha256, bundle_byte_size, skill_md, tombstoned_at
|
|
35613
|
+
FROM skills_registry WHERE org_id = ${orgId} AND slug = ${input.slug} LIMIT 1
|
|
35614
|
+
`;
|
|
35615
|
+
const previous = previousRows[0];
|
|
35616
|
+
const previousSha = typeof previous?.bundle_sha256 === "string" ? String(previous.bundle_sha256) : null;
|
|
35617
|
+
const previousRevisionId = typeof previous?.revision_id === "string" && previous.revision_id ? String(previous.revision_id) : null;
|
|
35618
|
+
const tombstoned = previous?.tombstoned_at != null;
|
|
35619
|
+
const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : typeof previous?.skill_md === "string" ? String(previous.skill_md) : null;
|
|
35620
|
+
if (previous && !tombstoned && input.expectedRevisionId !== previousRevisionId) {
|
|
35621
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
|
|
35622
|
+
}
|
|
35623
|
+
const carriedSha = input.bundle?.sha256 ?? previousSha;
|
|
35624
|
+
const carriedSize = input.bundle?.byteSize ?? (previous?.bundle_byte_size == null ? null : Number(previous.bundle_byte_size));
|
|
35625
|
+
const revisionId = revisionIdOfRecord({
|
|
35626
|
+
slug: input.slug,
|
|
35627
|
+
displayName: input.displayName,
|
|
35628
|
+
description: input.description,
|
|
35629
|
+
category: input.category,
|
|
35630
|
+
tags: input.tags,
|
|
35631
|
+
source: input.source,
|
|
35632
|
+
kind: input.kind,
|
|
35633
|
+
...input.version ? { version: input.version } : {},
|
|
35634
|
+
...carriedSkillMd ? { skillMd: carriedSkillMd } : {},
|
|
35635
|
+
...carriedSha ? { bundleSha256: carriedSha } : {},
|
|
35636
|
+
...carriedSize === null || carriedSize === undefined ? {} : { bundleByteSize: carriedSize }
|
|
35637
|
+
});
|
|
34963
35638
|
if (input.bundle) {
|
|
34964
35639
|
await tx`
|
|
34965
35640
|
INSERT INTO skills_bundles (org_id, sha256, byte_size, content_type, storage_kind, storage_key, body_blob)
|
|
@@ -34974,10 +35649,10 @@ class PostgresSkillsStore {
|
|
|
34974
35649
|
}
|
|
34975
35650
|
const rows = await tx`
|
|
34976
35651
|
INSERT INTO skills_registry (org_id, slug, display_name, description, category, tags_json, source, kind, version, skill_md,
|
|
34977
|
-
bundle_sha256, bundle_byte_size, published_by_user_id, updated_at)
|
|
35652
|
+
bundle_sha256, bundle_byte_size, published_by_user_id, revision_id, revision_number, updated_at)
|
|
34978
35653
|
VALUES (${orgId}, ${input.slug}, ${input.displayName}, ${input.description}, ${input.category}, ${JSON.stringify(input.tags)}::jsonb,
|
|
34979
|
-
${input.source}, ${input.kind}, ${input.version ?? null}, ${
|
|
34980
|
-
${input.bundle?.sha256 ?? null}, ${input.bundle?.byteSize ?? null}, ${input.principal.userId}, now())
|
|
35654
|
+
${input.source}, ${input.kind}, ${input.version ?? null}, ${carriedSkillMd},
|
|
35655
|
+
${input.bundle?.sha256 ?? null}, ${input.bundle?.byteSize ?? null}, ${input.principal.userId}, ${revisionId}, 1, now())
|
|
34981
35656
|
ON CONFLICT (org_id, slug) DO UPDATE SET
|
|
34982
35657
|
display_name = EXCLUDED.display_name,
|
|
34983
35658
|
description = EXCLUDED.description,
|
|
@@ -34992,9 +35667,23 @@ class PostgresSkillsStore {
|
|
|
34992
35667
|
bundle_sha256 = COALESCE(EXCLUDED.bundle_sha256, skills_registry.bundle_sha256),
|
|
34993
35668
|
bundle_byte_size = COALESCE(EXCLUDED.bundle_byte_size, skills_registry.bundle_byte_size),
|
|
34994
35669
|
published_by_user_id = EXCLUDED.published_by_user_id,
|
|
35670
|
+
revision_id = EXCLUDED.revision_id,
|
|
35671
|
+
-- Current + 1, not EXCLUDED.revision_number: the insert path minted 1, but on
|
|
35672
|
+
-- the update path the ACTUAL row's counter is the truth (it may have advanced
|
|
35673
|
+
-- since the pre-read, which is exactly what the WHERE guard below detects).
|
|
35674
|
+
revision_number = skills_registry.revision_number + 1,
|
|
35675
|
+
tombstoned_at = NULL,
|
|
35676
|
+
tombstone_purge_after = NULL,
|
|
34995
35677
|
updated_at = EXCLUDED.updated_at
|
|
35678
|
+
WHERE skills_registry.tombstoned_at IS NOT NULL
|
|
35679
|
+
OR skills_registry.revision_id = ${input.expectedRevisionId ?? NO_REVISION_SENTINEL2}
|
|
34996
35680
|
RETURNING *
|
|
34997
35681
|
`;
|
|
35682
|
+
if (!rows[0]) {
|
|
35683
|
+
const current = await tx`SELECT revision_id FROM skills_registry WHERE org_id = ${orgId} AND slug = ${input.slug} LIMIT 1`;
|
|
35684
|
+
const currentId = current[0] && typeof current[0].revision_id === "string" ? String(current[0].revision_id) : null;
|
|
35685
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, currentId);
|
|
35686
|
+
}
|
|
34998
35687
|
if (previousSha && input.bundle && previousSha !== input.bundle.sha256) {
|
|
34999
35688
|
await tx`
|
|
35000
35689
|
DELETE FROM skills_bundles
|
|
@@ -35002,55 +35691,182 @@ class PostgresSkillsStore {
|
|
|
35002
35691
|
AND NOT EXISTS (SELECT 1 FROM skills_registry WHERE org_id = ${orgId} AND bundle_sha256 = ${previousSha})
|
|
35003
35692
|
`;
|
|
35004
35693
|
}
|
|
35694
|
+
await tx`DELETE FROM skills_tags WHERE org_id = ${orgId} AND slug = ${input.slug}`;
|
|
35695
|
+
for (const tag of input.tags) {
|
|
35696
|
+
if (!tag.trim())
|
|
35697
|
+
continue;
|
|
35698
|
+
await tx`
|
|
35699
|
+
INSERT INTO skills_tags (org_id, slug, tag) VALUES (${orgId}, ${input.slug}, ${tag})
|
|
35700
|
+
ON CONFLICT DO NOTHING
|
|
35701
|
+
`;
|
|
35702
|
+
}
|
|
35005
35703
|
return rowToSkill(rows[0]);
|
|
35006
35704
|
});
|
|
35007
35705
|
}
|
|
35008
35706
|
async listSkills(principal) {
|
|
35009
|
-
|
|
35707
|
+
await this.purgeExpiredTombstones(principal);
|
|
35708
|
+
const rows = await this.sql`
|
|
35709
|
+
SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND tombstoned_at IS NULL ORDER BY slug ASC
|
|
35710
|
+
`;
|
|
35010
35711
|
return rows.map(rowToSkill);
|
|
35011
35712
|
}
|
|
35012
35713
|
async getSkill(principal, slug) {
|
|
35013
35714
|
const rows = await this.sql`SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1`;
|
|
35014
35715
|
return rows[0] ? rowToSkill(rows[0]) : null;
|
|
35015
35716
|
}
|
|
35016
|
-
async updateSkill(principal, slug, patch) {
|
|
35717
|
+
async updateSkill(principal, slug, patch, expectedRevisionId) {
|
|
35017
35718
|
const current = await this.getSkill(principal, slug);
|
|
35018
|
-
if (!current)
|
|
35719
|
+
if (!current || current.tombstonedAt)
|
|
35019
35720
|
return null;
|
|
35721
|
+
if (expectedRevisionId !== current.revisionId) {
|
|
35722
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
|
|
35723
|
+
}
|
|
35020
35724
|
const next = { ...current, ...patch };
|
|
35021
|
-
|
|
35022
|
-
|
|
35023
|
-
|
|
35024
|
-
|
|
35025
|
-
|
|
35026
|
-
|
|
35027
|
-
|
|
35028
|
-
|
|
35029
|
-
|
|
35725
|
+
return await this.sql.begin(async (tx) => {
|
|
35726
|
+
const revisionId = revisionIdOfRecord(next);
|
|
35727
|
+
const updated = await tx`
|
|
35728
|
+
UPDATE skills_registry
|
|
35729
|
+
SET display_name = ${next.displayName}, description = ${next.description}, category = ${next.category},
|
|
35730
|
+
tags_json = ${JSON.stringify(next.tags)}::jsonb, kind = ${next.kind}, version = ${next.version ?? null},
|
|
35731
|
+
skill_md = ${next.skillMd ?? null}, revision_id = ${revisionId}, revision_number = revision_number + 1, updated_at = now()
|
|
35732
|
+
WHERE org_id = ${principal.orgId} AND slug = ${slug} AND tombstoned_at IS NULL AND revision_id = ${current.revisionId}
|
|
35733
|
+
RETURNING *
|
|
35734
|
+
`;
|
|
35735
|
+
if (!updated[0]) {
|
|
35736
|
+
const nowRows = await tx`
|
|
35737
|
+
SELECT revision_id, tombstoned_at FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1
|
|
35738
|
+
`;
|
|
35739
|
+
if (nowRows[0] && nowRows[0].tombstoned_at == null) {
|
|
35740
|
+
const currentId = String(nowRows[0].revision_id);
|
|
35741
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, currentId);
|
|
35742
|
+
}
|
|
35743
|
+
return null;
|
|
35744
|
+
}
|
|
35745
|
+
await tx`DELETE FROM skills_tags WHERE org_id = ${principal.orgId} AND slug = ${slug}`;
|
|
35746
|
+
for (const tag of next.tags) {
|
|
35747
|
+
if (!tag.trim())
|
|
35748
|
+
continue;
|
|
35749
|
+
await tx`
|
|
35750
|
+
INSERT INTO skills_tags (org_id, slug, tag) VALUES (${principal.orgId}, ${slug}, ${tag})
|
|
35751
|
+
ON CONFLICT DO NOTHING
|
|
35752
|
+
`;
|
|
35753
|
+
}
|
|
35754
|
+
return rowToSkill(updated[0]);
|
|
35755
|
+
});
|
|
35030
35756
|
}
|
|
35031
|
-
async deleteSkill(principal, slug) {
|
|
35757
|
+
async deleteSkill(principal, slug, tombstoneWindowMs) {
|
|
35032
35758
|
return await this.sql.begin(async (tx) => {
|
|
35759
|
+
const existingRows = await tx`
|
|
35760
|
+
SELECT tombstoned_at FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1
|
|
35761
|
+
`;
|
|
35762
|
+
if (!existingRows[0])
|
|
35763
|
+
return null;
|
|
35764
|
+
if (existingRows[0].tombstoned_at != null) {
|
|
35765
|
+
const rows2 = await tx`SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1`;
|
|
35766
|
+
return rowToSkill(rows2[0]);
|
|
35767
|
+
}
|
|
35033
35768
|
const rows = await tx`
|
|
35034
|
-
|
|
35035
|
-
|
|
35769
|
+
UPDATE skills_registry
|
|
35770
|
+
SET tombstoned_at = now(), tombstone_purge_after = now() + (${tombstoneWindowMs}::int * interval '1 millisecond'), updated_at = now()
|
|
35771
|
+
WHERE org_id = ${principal.orgId} AND slug = ${slug}
|
|
35772
|
+
RETURNING *
|
|
35036
35773
|
`;
|
|
35037
|
-
|
|
35038
|
-
|
|
35039
|
-
|
|
35040
|
-
|
|
35774
|
+
return rows[0] ? rowToSkill(rows[0]) : null;
|
|
35775
|
+
});
|
|
35776
|
+
}
|
|
35777
|
+
async purgeExpiredTombstones(principal) {
|
|
35778
|
+
return await this.sql.begin(async (tx) => {
|
|
35779
|
+
const expiredRows = await tx`
|
|
35780
|
+
SELECT * FROM skills_registry
|
|
35781
|
+
WHERE org_id = ${principal.orgId} AND tombstoned_at IS NOT NULL AND tombstone_purge_after <= now()
|
|
35782
|
+
`;
|
|
35783
|
+
if (!expiredRows.length)
|
|
35784
|
+
return [];
|
|
35785
|
+
const purged = [];
|
|
35786
|
+
for (const row of expiredRows) {
|
|
35787
|
+
const record = rowToSkill(row);
|
|
35041
35788
|
await tx`
|
|
35042
|
-
DELETE FROM
|
|
35043
|
-
|
|
35044
|
-
|
|
35789
|
+
DELETE FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${record.slug} AND tombstone_purge_after <= now()
|
|
35790
|
+
`;
|
|
35791
|
+
await tx`DELETE FROM skills_tags WHERE org_id = ${principal.orgId} AND slug = ${record.slug}`;
|
|
35792
|
+
await tx`
|
|
35793
|
+
DELETE FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${record.slug} AND tombstone_purge_after <= now()
|
|
35045
35794
|
`;
|
|
35795
|
+
if (record.bundleSha256) {
|
|
35796
|
+
await tx`
|
|
35797
|
+
DELETE FROM skills_bundles
|
|
35798
|
+
WHERE org_id = ${principal.orgId} AND sha256 = ${record.bundleSha256}
|
|
35799
|
+
AND NOT EXISTS (SELECT 1 FROM skills_registry WHERE org_id = ${principal.orgId} AND bundle_sha256 = ${record.bundleSha256})
|
|
35800
|
+
`;
|
|
35801
|
+
}
|
|
35802
|
+
purged.push(record);
|
|
35046
35803
|
}
|
|
35047
|
-
return
|
|
35804
|
+
return purged;
|
|
35048
35805
|
});
|
|
35049
35806
|
}
|
|
35050
35807
|
async getSkillBundle(principal, sha256) {
|
|
35051
35808
|
const rows = await this.sql`SELECT * FROM skills_bundles WHERE org_id = ${principal.orgId} AND sha256 = ${sha256} LIMIT 1`;
|
|
35052
35809
|
return rows[0] ? rowToSkillBundle(rows[0]) : null;
|
|
35053
35810
|
}
|
|
35811
|
+
async pinSkill(principal, slug, metadata = {}) {
|
|
35812
|
+
const rows = await this.sql`
|
|
35813
|
+
INSERT INTO skills_pins (org_id, principal, slug, pinned_at, metadata_json)
|
|
35814
|
+
VALUES (${principal.orgId}, ${principal.apiKeyId}, ${slug}, now(), ${JSON.stringify(metadata)}::jsonb)
|
|
35815
|
+
ON CONFLICT (org_id, principal, slug) DO UPDATE SET
|
|
35816
|
+
pinned_at = now(),
|
|
35817
|
+
metadata_json = EXCLUDED.metadata_json
|
|
35818
|
+
RETURNING *
|
|
35819
|
+
`;
|
|
35820
|
+
return rowToPin(rows[0]);
|
|
35821
|
+
}
|
|
35822
|
+
async unpinSkill(principal, slug) {
|
|
35823
|
+
const rows = await this.sql`
|
|
35824
|
+
DELETE FROM skills_pins WHERE org_id = ${principal.orgId} AND principal = ${principal.apiKeyId} AND slug = ${slug}
|
|
35825
|
+
RETURNING 1 AS present
|
|
35826
|
+
`;
|
|
35827
|
+
return rows.length > 0;
|
|
35828
|
+
}
|
|
35829
|
+
async listPins(principal) {
|
|
35830
|
+
const rows = await this.sql`
|
|
35831
|
+
SELECT * FROM skills_pins WHERE org_id = ${principal.orgId} AND principal = ${principal.apiKeyId} ORDER BY slug ASC
|
|
35832
|
+
`;
|
|
35833
|
+
return rows.map(rowToPin);
|
|
35834
|
+
}
|
|
35835
|
+
async listTags(principal) {
|
|
35836
|
+
await this.purgeExpiredTombstones(principal);
|
|
35837
|
+
const rows = await this.sql`
|
|
35838
|
+
SELECT DISTINCT tag FROM skills_tags WHERE org_id = ${principal.orgId} ORDER BY tag ASC
|
|
35839
|
+
`;
|
|
35840
|
+
return rows.map((row) => String(row.tag));
|
|
35841
|
+
}
|
|
35842
|
+
async listSkillsByTag(principal, tag) {
|
|
35843
|
+
await this.purgeExpiredTombstones(principal);
|
|
35844
|
+
const rows = await this.sql`
|
|
35845
|
+
SELECT s.* FROM skills_registry s
|
|
35846
|
+
JOIN skills_tags t ON t.org_id = s.org_id AND t.slug = s.slug
|
|
35847
|
+
WHERE t.org_id = ${principal.orgId} AND t.tag = ${tag} AND s.tombstoned_at IS NULL
|
|
35848
|
+
ORDER BY s.slug ASC
|
|
35849
|
+
`;
|
|
35850
|
+
return rows.map(rowToSkill);
|
|
35851
|
+
}
|
|
35852
|
+
async listPinsByTag(principal, tag) {
|
|
35853
|
+
await this.purgeExpiredTombstones(principal);
|
|
35854
|
+
const rows = await this.sql`
|
|
35855
|
+
SELECT p.* FROM skills_pins p
|
|
35856
|
+
JOIN skills_tags t ON t.org_id = p.org_id AND t.slug = p.slug
|
|
35857
|
+
JOIN skills_registry s ON s.org_id = p.org_id AND s.slug = p.slug
|
|
35858
|
+
WHERE p.org_id = ${principal.orgId} AND p.principal = ${principal.apiKeyId}
|
|
35859
|
+
AND t.tag = ${tag} AND s.tombstoned_at IS NULL
|
|
35860
|
+
ORDER BY p.slug ASC
|
|
35861
|
+
`;
|
|
35862
|
+
return rows.map(rowToPin);
|
|
35863
|
+
}
|
|
35864
|
+
async listPublishedSlugs(principal) {
|
|
35865
|
+
const rows = await this.sql`
|
|
35866
|
+
SELECT slug FROM skills_registry WHERE org_id = ${principal.orgId} AND tombstoned_at IS NULL ORDER BY slug ASC
|
|
35867
|
+
`;
|
|
35868
|
+
return rows.map((row) => String(row.slug));
|
|
35869
|
+
}
|
|
35054
35870
|
async collectOrphanBundle(orgId, sha256) {
|
|
35055
35871
|
await this.sql`
|
|
35056
35872
|
DELETE FROM skills_bundles
|
|
@@ -35059,6 +35875,7 @@ class PostgresSkillsStore {
|
|
|
35059
35875
|
`;
|
|
35060
35876
|
}
|
|
35061
35877
|
}
|
|
35878
|
+
var NO_REVISION_SENTINEL2 = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
35062
35879
|
function isUniqueViolation(error) {
|
|
35063
35880
|
const code = error?.code;
|
|
35064
35881
|
if (code === "23505" || code === 23505)
|
|
@@ -35120,7 +35937,7 @@ ${summary}
|
|
|
35120
35937
|
textArtifact(run, "show-notes.md", `# Show Notes
|
|
35121
35938
|
|
|
35122
35939
|
- ${summary}
|
|
35123
|
-
- Generated by the
|
|
35940
|
+
- Generated by the skills deterministic worker.
|
|
35124
35941
|
`),
|
|
35125
35942
|
textArtifact(run, "clips.csv", `start,end,title,summary
|
|
35126
35943
|
00:00,00:30,"Opening","${csv(summary)}"
|
|
@@ -35154,7 +35971,7 @@ function textArtifact(run, relativePath, bodyText, contentType = relativePath.en
|
|
|
35154
35971
|
relativePath,
|
|
35155
35972
|
contentType,
|
|
35156
35973
|
byteSize: bytes.byteLength,
|
|
35157
|
-
sha256:
|
|
35974
|
+
sha256: createHash6("sha256").update(bytes).digest("hex"),
|
|
35158
35975
|
visibility: "private"
|
|
35159
35976
|
},
|
|
35160
35977
|
body: { relativePath, bodyText, contentType }
|
|
@@ -35216,7 +36033,7 @@ function csv(value) {
|
|
|
35216
36033
|
}
|
|
35217
36034
|
|
|
35218
36035
|
// src/server/skills-api.ts
|
|
35219
|
-
import { createHash as
|
|
36036
|
+
import { createHash as createHash8 } from "crypto";
|
|
35220
36037
|
|
|
35221
36038
|
// src/lib/registry-merge.ts
|
|
35222
36039
|
var SKILL_SOURCE_PRECEDENCE = {
|
|
@@ -35266,7 +36083,7 @@ function mergeSkillRegistryLists(...groups) {
|
|
|
35266
36083
|
}
|
|
35267
36084
|
|
|
35268
36085
|
// src/server/registry.ts
|
|
35269
|
-
import { existsSync as
|
|
36086
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
|
|
35270
36087
|
import { resolve, sep as sep2 } from "path";
|
|
35271
36088
|
|
|
35272
36089
|
// src/lib/registry.ts
|
|
@@ -35444,7 +36261,7 @@ var DEVELOPMENT_TOOLS_SKILLS = [
|
|
|
35444
36261
|
{
|
|
35445
36262
|
name: "monitor",
|
|
35446
36263
|
displayName: "Monitor",
|
|
35447
|
-
description: "Operate the
|
|
36264
|
+
description: "Operate the monitor MCP for machine health, processes, cron jobs, and cleanup workflows",
|
|
35448
36265
|
category: "Development Tools",
|
|
35449
36266
|
tags: ["monitoring", "mcp", "processes", "operations"]
|
|
35450
36267
|
},
|
|
@@ -35490,6 +36307,14 @@ var DEVELOPMENT_TOOLS_SKILLS = [
|
|
|
35490
36307
|
description: "Validate configuration files for syntax and schema compliance",
|
|
35491
36308
|
category: "Development Tools",
|
|
35492
36309
|
tags: ["config", "validation", "schema", "linting"]
|
|
36310
|
+
},
|
|
36311
|
+
{
|
|
36312
|
+
name: "session-inject-monitor",
|
|
36313
|
+
displayName: "Session Inject Monitor",
|
|
36314
|
+
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",
|
|
36315
|
+
category: "Development Tools",
|
|
36316
|
+
tags: ["monitor", "session", "injection", "automation", "wake"],
|
|
36317
|
+
kind: "instruction"
|
|
35493
36318
|
}
|
|
35494
36319
|
];
|
|
35495
36320
|
|
|
@@ -35877,7 +36702,7 @@ var DESIGN_BRANDING_SKILLS = [
|
|
|
35877
36702
|
displayName: "Site Analyze",
|
|
35878
36703
|
description: "Analyze any website's design system \u2014 detects shadcn/ui, Tailwind, extracts colors, typography, and components via Playwright + Claude Vision.",
|
|
35879
36704
|
category: "Design & Branding",
|
|
35880
|
-
tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "
|
|
36705
|
+
tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "styles"]
|
|
35881
36706
|
}
|
|
35882
36707
|
];
|
|
35883
36708
|
|
|
@@ -36361,7 +37186,7 @@ function validateRegistryConsistency(registry, skillsDir) {
|
|
|
36361
37186
|
}
|
|
36362
37187
|
|
|
36363
37188
|
// src/lib/skill-hash.ts
|
|
36364
|
-
import { createHash as
|
|
37189
|
+
import { createHash as createHash7 } from "crypto";
|
|
36365
37190
|
import { existsSync as existsSync4, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
|
|
36366
37191
|
import { join as join7, sep } from "path";
|
|
36367
37192
|
var CONTENT_HASH_ALGORITHM = "sha256";
|
|
@@ -36469,7 +37294,7 @@ function collectFile(files, absolute, rel) {
|
|
|
36469
37294
|
files.push({ rel: rel.split(sep).join("/"), content: buffer });
|
|
36470
37295
|
}
|
|
36471
37296
|
function computeContentHash(skillPath) {
|
|
36472
|
-
const hash =
|
|
37297
|
+
const hash = createHash7(CONTENT_HASH_ALGORITHM);
|
|
36473
37298
|
for (const file of collectBundleFiles(skillPath)) {
|
|
36474
37299
|
hash.update(new TextEncoder().encode(file.rel));
|
|
36475
37300
|
hash.update(new TextEncoder().encode(`\x00${file.content.length}\x00`));
|
|
@@ -37243,6 +38068,9 @@ function getPortableSkillsRoot(options = {}) {
|
|
|
37243
38068
|
if (options.rootDir)
|
|
37244
38069
|
return options.rootDir;
|
|
37245
38070
|
const appDir = options.homeDir ? join9(options.homeDir, ".hasna", "skills") : getDataDir();
|
|
38071
|
+
const cache3 = join9(appDir, SKILLS_CACHE_DIRNAME);
|
|
38072
|
+
if (isOwnerLayoutMigrated(appDir) && safeIsDirectory(cache3))
|
|
38073
|
+
return cache3;
|
|
37246
38074
|
const installed = join9(appDir, INSTALLED_SKILLS_DIRNAME);
|
|
37247
38075
|
migrateLegacySkillLayout(appDir, installed);
|
|
37248
38076
|
return installed;
|
|
@@ -37463,10 +38291,7 @@ function writeCorpusSkill(input, options = {}) {
|
|
|
37463
38291
|
const skillPath = join9(root3, name);
|
|
37464
38292
|
const created = !existsSync6(skillPath);
|
|
37465
38293
|
mkdirSync4(skillPath, { recursive: true });
|
|
37466
|
-
|
|
37467
|
-
`) ? input.skillMd : `${input.skillMd}
|
|
37468
|
-
`;
|
|
37469
|
-
writeFileSync3(join9(skillPath, "SKILL.md"), skillMd);
|
|
38294
|
+
writeFileSync3(join9(skillPath, "SKILL.md"), input.skillMd);
|
|
37470
38295
|
const frontmatter = parseSkillFrontmatter(input.skillMd) ?? undefined;
|
|
37471
38296
|
const kind = input.meta?.kind ?? parseSkillKind(frontmatter?.kind) ?? "instruction";
|
|
37472
38297
|
const manifest = {
|
|
@@ -37954,49 +38779,32 @@ function getAllTags() {
|
|
|
37954
38779
|
}
|
|
37955
38780
|
|
|
37956
38781
|
// src/lib/skillinfo.ts
|
|
37957
|
-
import { existsSync as
|
|
37958
|
-
import { join as
|
|
38782
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
|
|
38783
|
+
import { join as join14 } from "path";
|
|
37959
38784
|
|
|
37960
38785
|
// src/lib/installer.ts
|
|
37961
|
-
import { existsSync as
|
|
37962
|
-
import { dirname as dirname8, join as
|
|
38786
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10, rmSync as rmSync3 } from "fs";
|
|
38787
|
+
import { dirname as dirname8, join as join13 } from "path";
|
|
37963
38788
|
import { homedir as homedir4 } from "os";
|
|
37964
38789
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
37965
38790
|
|
|
37966
38791
|
// src/lib/agent-sync.ts
|
|
37967
38792
|
import {
|
|
37968
38793
|
cpSync as cpSync3,
|
|
37969
|
-
existsSync as
|
|
37970
|
-
mkdirSync as
|
|
38794
|
+
existsSync as existsSync8,
|
|
38795
|
+
mkdirSync as mkdirSync5,
|
|
37971
38796
|
mkdtempSync,
|
|
37972
38797
|
readFileSync as readFileSync8,
|
|
37973
|
-
readdirSync as
|
|
37974
|
-
renameSync as
|
|
37975
|
-
rmSync as
|
|
37976
|
-
statSync as
|
|
37977
|
-
writeFileSync as
|
|
38798
|
+
readdirSync as readdirSync7,
|
|
38799
|
+
renameSync as renameSync2,
|
|
38800
|
+
rmSync as rmSync2,
|
|
38801
|
+
statSync as statSync6,
|
|
38802
|
+
writeFileSync as writeFileSync4
|
|
37978
38803
|
} from "fs";
|
|
37979
38804
|
import { homedir as homedir3 } from "os";
|
|
37980
|
-
import { basename as basename3, dirname as dirname7, join as
|
|
37981
|
-
|
|
38805
|
+
import { basename as basename3, dirname as dirname7, join as join11 } from "path";
|
|
37982
38806
|
// src/lib/home-migration.ts
|
|
37983
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync7, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
37984
|
-
import { join as join11 } from "path";
|
|
37985
|
-
var SKILLS_CACHE_DIRNAME = "skills";
|
|
37986
|
-
var LAYOUT_MIGRATION_RECORD = ".layout-migration.json";
|
|
37987
|
-
function layoutMigrationRecordPath(appDir) {
|
|
37988
|
-
return join11(appDir, SKILLS_CACHE_DIRNAME, LAYOUT_MIGRATION_RECORD);
|
|
37989
|
-
}
|
|
37990
|
-
function isOwnerLayoutMigrated(appDir) {
|
|
37991
|
-
return existsSync8(layoutMigrationRecordPath(appDir));
|
|
37992
|
-
}
|
|
37993
38807
|
function resolveCorpusRoot(options = {}) {
|
|
37994
|
-
if (options.rootDir)
|
|
37995
|
-
return options.rootDir;
|
|
37996
|
-
const appDir = options.homeDir ? join11(options.homeDir, ".hasna", "skills") : getDataDir();
|
|
37997
|
-
const cache3 = join11(appDir, SKILLS_CACHE_DIRNAME);
|
|
37998
|
-
if (isOwnerLayoutMigrated(appDir) && existsSync8(cache3))
|
|
37999
|
-
return cache3;
|
|
38000
38808
|
return getPortableSkillsRoot(options);
|
|
38001
38809
|
}
|
|
38002
38810
|
|
|
@@ -38019,9 +38827,9 @@ function resolveSyncAgents(arg) {
|
|
|
38019
38827
|
function agentGlobalSkillsDir(agent, homeDir = homedir3()) {
|
|
38020
38828
|
switch (agent) {
|
|
38021
38829
|
case "opencode":
|
|
38022
|
-
return
|
|
38830
|
+
return join11(homeDir, ".config", "opencode", "skills");
|
|
38023
38831
|
default:
|
|
38024
|
-
return
|
|
38832
|
+
return join11(homeDir, `.${agent}`, "skills");
|
|
38025
38833
|
}
|
|
38026
38834
|
}
|
|
38027
38835
|
function adaptSkillMdForAgent(skillMd, agent) {
|
|
@@ -38042,6 +38850,10 @@ ${lines.join(`
|
|
|
38042
38850
|
---
|
|
38043
38851
|
${body}`;
|
|
38044
38852
|
}
|
|
38853
|
+
var POINTER_MARKER_PHRASE = "This is an executable skill from the @hasna/skills catalog";
|
|
38854
|
+
function isPointerSkillMd(markdown) {
|
|
38855
|
+
return markdown.includes(POINTER_MARKER_PHRASE) && /^kind:\s*executable\b/m.test(markdown);
|
|
38856
|
+
}
|
|
38045
38857
|
function pointerSkillMd(name, description) {
|
|
38046
38858
|
const display = name.replace(/[-_]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
38047
38859
|
return [
|
|
@@ -38067,7 +38879,7 @@ function resolveSyncCorpus(options = {}) {
|
|
|
38067
38879
|
if (explicit) {
|
|
38068
38880
|
const roots = packageSourceRoots(explicit);
|
|
38069
38881
|
if (roots.length === 0) {
|
|
38070
|
-
throw new Error(`SKILLS_SOURCE '${explicit}' contains no skills: expected a corpus directory or a package root with skills
|
|
38882
|
+
throw new Error(`SKILLS_SOURCE '${explicit}' contains no skills: expected a corpus directory or a package root with skills/`);
|
|
38071
38883
|
}
|
|
38072
38884
|
return { roots, source: "source" };
|
|
38073
38885
|
}
|
|
@@ -38075,9 +38887,9 @@ function resolveSyncCorpus(options = {}) {
|
|
|
38075
38887
|
}
|
|
38076
38888
|
function packageSourceRoots(source) {
|
|
38077
38889
|
const roots = [];
|
|
38078
|
-
for (const sub of ["skills"
|
|
38079
|
-
const candidate =
|
|
38080
|
-
if (
|
|
38890
|
+
for (const sub of ["skills"]) {
|
|
38891
|
+
const candidate = join11(source, sub);
|
|
38892
|
+
if (existsSync8(candidate) && isDirectory(candidate))
|
|
38081
38893
|
roots.push(candidate);
|
|
38082
38894
|
}
|
|
38083
38895
|
if (roots.length > 0)
|
|
@@ -38087,20 +38899,20 @@ function packageSourceRoots(source) {
|
|
|
38087
38899
|
function containsSkillDirectories(path) {
|
|
38088
38900
|
let entries;
|
|
38089
38901
|
try {
|
|
38090
|
-
entries =
|
|
38902
|
+
entries = readdirSync7(path);
|
|
38091
38903
|
} catch {
|
|
38092
38904
|
return false;
|
|
38093
38905
|
}
|
|
38094
38906
|
return entries.some((entry) => {
|
|
38095
|
-
const candidate =
|
|
38907
|
+
const candidate = join11(path, entry);
|
|
38096
38908
|
if (!isDirectory(candidate))
|
|
38097
38909
|
return false;
|
|
38098
|
-
return
|
|
38910
|
+
return existsSync8(join11(candidate, "SKILL.md")) || existsSync8(join11(candidate, "skill.json")) || existsSync8(join11(candidate, "package.json"));
|
|
38099
38911
|
});
|
|
38100
38912
|
}
|
|
38101
38913
|
function isDirectory(path) {
|
|
38102
38914
|
try {
|
|
38103
|
-
return
|
|
38915
|
+
return statSync6(path).isDirectory();
|
|
38104
38916
|
} catch {
|
|
38105
38917
|
return false;
|
|
38106
38918
|
}
|
|
@@ -38134,7 +38946,7 @@ function syncSkillsToAgents(options = {}) {
|
|
|
38134
38946
|
actions.push({
|
|
38135
38947
|
skill: name,
|
|
38136
38948
|
agent,
|
|
38137
|
-
path:
|
|
38949
|
+
path: join11(agentGlobalSkillsDir(agent, homeDir), name, "SKILL.md"),
|
|
38138
38950
|
action: "skip",
|
|
38139
38951
|
reason: "not found in this machine's corpus"
|
|
38140
38952
|
});
|
|
@@ -38164,7 +38976,7 @@ function syncSkillsToAgents(options = {}) {
|
|
|
38164
38976
|
}
|
|
38165
38977
|
function writeManagedAgentSkill(params) {
|
|
38166
38978
|
const homeDir = params.homeDir ?? homedir3();
|
|
38167
|
-
const dir =
|
|
38979
|
+
const dir = join11(agentGlobalSkillsDir(params.agent, homeDir), params.skill);
|
|
38168
38980
|
const result = writeManagedSkillDir(dir, params.skillMd, {
|
|
38169
38981
|
skill: params.skill,
|
|
38170
38982
|
source: params.source,
|
|
@@ -38181,11 +38993,11 @@ function writeManagedAgentSkill(params) {
|
|
|
38181
38993
|
};
|
|
38182
38994
|
}
|
|
38183
38995
|
function writeManagedSkillDir(dir, skillMd, options) {
|
|
38184
|
-
const skillMdPath =
|
|
38185
|
-
const markerPath =
|
|
38186
|
-
const dirExists =
|
|
38187
|
-
const managed =
|
|
38188
|
-
const hasSkillMd =
|
|
38996
|
+
const skillMdPath = join11(dir, "SKILL.md");
|
|
38997
|
+
const markerPath = join11(dir, SYNC_MARKER_FILE);
|
|
38998
|
+
const dirExists = existsSync8(dir);
|
|
38999
|
+
const managed = existsSync8(markerPath);
|
|
39000
|
+
const hasSkillMd = existsSync8(skillMdPath);
|
|
38189
39001
|
if (dirExists && !managed && !hasSkillMd) {
|
|
38190
39002
|
return {
|
|
38191
39003
|
action: "skip",
|
|
@@ -38200,35 +39012,50 @@ function writeManagedSkillDir(dir, skillMd, options) {
|
|
|
38200
39012
|
reason: "an unmanaged SKILL.md already exists here (hand-authored); pass --force to overwrite"
|
|
38201
39013
|
};
|
|
38202
39014
|
}
|
|
39015
|
+
if (dirExists && managed && hasSkillMd && !options.force && isPointerSkillMd(skillMd)) {
|
|
39016
|
+
let existingIsStub = false;
|
|
39017
|
+
try {
|
|
39018
|
+
existingIsStub = isPointerSkillMd(readFileSync8(skillMdPath, "utf-8"));
|
|
39019
|
+
} catch {
|
|
39020
|
+
existingIsStub = false;
|
|
39021
|
+
}
|
|
39022
|
+
if (!existingIsStub) {
|
|
39023
|
+
return {
|
|
39024
|
+
action: "skip",
|
|
39025
|
+
path: skillMdPath,
|
|
39026
|
+
reason: "refusing to replace a content-bearing managed home with an executable pointer stub (the corpus entry lacks kind: instruction); pass --force to overwrite"
|
|
39027
|
+
};
|
|
39028
|
+
}
|
|
39029
|
+
}
|
|
38203
39030
|
const action = dirExists ? "update" : "create";
|
|
38204
39031
|
if (options.dryRun)
|
|
38205
39032
|
return { action, path: skillMdPath };
|
|
38206
39033
|
const parentDir = dirname7(dir);
|
|
38207
|
-
|
|
38208
|
-
const transactionDir = mkdtempSync(
|
|
38209
|
-
const candidateDir =
|
|
38210
|
-
const backupDir =
|
|
38211
|
-
const candidateSkillMdPath =
|
|
38212
|
-
const candidateMarkerPath =
|
|
39034
|
+
mkdirSync5(parentDir, { recursive: true });
|
|
39035
|
+
const transactionDir = mkdtempSync(join11(parentDir, `.hasna-skills-write-${basename3(dir)}-`));
|
|
39036
|
+
const candidateDir = join11(transactionDir, "candidate");
|
|
39037
|
+
const backupDir = join11(transactionDir, "backup");
|
|
39038
|
+
const candidateSkillMdPath = join11(candidateDir, "SKILL.md");
|
|
39039
|
+
const candidateMarkerPath = join11(candidateDir, SYNC_MARKER_FILE);
|
|
38213
39040
|
const marker = {
|
|
38214
39041
|
managedBy: SYNC_MARKER_MANAGED_BY,
|
|
38215
39042
|
skill: options.skill,
|
|
38216
39043
|
source: options.source ?? "corpus",
|
|
38217
39044
|
syncedAt: new Date().toISOString()
|
|
38218
39045
|
};
|
|
38219
|
-
const renameDirectory = options.renameDirectory ??
|
|
39046
|
+
const renameDirectory = options.renameDirectory ?? renameSync2;
|
|
38220
39047
|
let originalMoved = false;
|
|
38221
39048
|
let preserveTransaction = false;
|
|
38222
39049
|
try {
|
|
38223
39050
|
if (options.resourceDir) {
|
|
38224
39051
|
cpSync3(options.resourceDir, candidateDir, { recursive: true, force: true });
|
|
38225
39052
|
} else {
|
|
38226
|
-
|
|
39053
|
+
mkdirSync5(candidateDir, { recursive: true });
|
|
38227
39054
|
}
|
|
38228
|
-
|
|
39055
|
+
writeFileSync4(candidateSkillMdPath, skillMd.endsWith(`
|
|
38229
39056
|
`) ? skillMd : `${skillMd}
|
|
38230
39057
|
`);
|
|
38231
|
-
|
|
39058
|
+
writeFileSync4(candidateMarkerPath, `${JSON.stringify(marker, null, 2)}
|
|
38232
39059
|
`);
|
|
38233
39060
|
if (dirExists) {
|
|
38234
39061
|
originalMoved = true;
|
|
@@ -38236,10 +39063,10 @@ function writeManagedSkillDir(dir, skillMd, options) {
|
|
|
38236
39063
|
}
|
|
38237
39064
|
renameDirectory(candidateDir, dir);
|
|
38238
39065
|
} catch (error) {
|
|
38239
|
-
if (originalMoved &&
|
|
39066
|
+
if (originalMoved && existsSync8(backupDir)) {
|
|
38240
39067
|
try {
|
|
38241
|
-
if (
|
|
38242
|
-
|
|
39068
|
+
if (existsSync8(dir))
|
|
39069
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
38243
39070
|
renameDirectory(backupDir, dir);
|
|
38244
39071
|
originalMoved = false;
|
|
38245
39072
|
} catch (rollbackError) {
|
|
@@ -38251,23 +39078,23 @@ function writeManagedSkillDir(dir, skillMd, options) {
|
|
|
38251
39078
|
} finally {
|
|
38252
39079
|
if (!preserveTransaction) {
|
|
38253
39080
|
try {
|
|
38254
|
-
|
|
39081
|
+
rmSync2(transactionDir, { recursive: true, force: true });
|
|
38255
39082
|
} catch {}
|
|
38256
39083
|
}
|
|
38257
39084
|
}
|
|
38258
39085
|
return { action, path: skillMdPath };
|
|
38259
39086
|
}
|
|
38260
39087
|
function removeManagedAgentSkill(skill, agent, homeDir = homedir3()) {
|
|
38261
|
-
const dir =
|
|
38262
|
-
if (!
|
|
39088
|
+
const dir = join11(agentGlobalSkillsDir(agent, homeDir), skill);
|
|
39089
|
+
if (!existsSync8(join11(dir, SYNC_MARKER_FILE)))
|
|
38263
39090
|
return false;
|
|
38264
|
-
|
|
39091
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
38265
39092
|
return true;
|
|
38266
39093
|
}
|
|
38267
39094
|
function sourceSkillMd(skillPath, name, description, kind, preferBundledDocs = false) {
|
|
38268
39095
|
if (kind === "instruction" || preferBundledDocs) {
|
|
38269
|
-
const skillMdPath =
|
|
38270
|
-
if (
|
|
39096
|
+
const skillMdPath = join11(skillPath, "SKILL.md");
|
|
39097
|
+
if (existsSync8(skillMdPath))
|
|
38271
39098
|
return readFileSync8(skillMdPath, "utf-8");
|
|
38272
39099
|
}
|
|
38273
39100
|
return pointerSkillMd(name, description);
|
|
@@ -38298,8 +39125,8 @@ function normalizeSkillName(name) {
|
|
|
38298
39125
|
}
|
|
38299
39126
|
|
|
38300
39127
|
// src/lib/project-state.ts
|
|
38301
|
-
import { existsSync as
|
|
38302
|
-
import { join as
|
|
39128
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
|
|
39129
|
+
import { join as join12 } from "path";
|
|
38303
39130
|
var VALID_PIN_SOURCES = [
|
|
38304
39131
|
"official",
|
|
38305
39132
|
"custom",
|
|
@@ -38314,14 +39141,14 @@ var SKILLS_PROJECT_DIR = ".skills";
|
|
|
38314
39141
|
var PROJECT_CONFIG_FILE = "project.json";
|
|
38315
39142
|
var DEFAULT_EXPORT_DIR = ".skills/exports";
|
|
38316
39143
|
function getProjectStateDir(targetDir = process.cwd()) {
|
|
38317
|
-
return
|
|
39144
|
+
return join12(targetDir, SKILLS_PROJECT_DIR);
|
|
38318
39145
|
}
|
|
38319
39146
|
function getProjectConfigPath(targetDir = process.cwd()) {
|
|
38320
|
-
return
|
|
39147
|
+
return join12(getProjectStateDir(targetDir), PROJECT_CONFIG_FILE);
|
|
38321
39148
|
}
|
|
38322
39149
|
function loadProjectConfig(targetDir = process.cwd()) {
|
|
38323
39150
|
const path = getProjectConfigPath(targetDir);
|
|
38324
|
-
if (!
|
|
39151
|
+
if (!existsSync9(path))
|
|
38325
39152
|
return null;
|
|
38326
39153
|
try {
|
|
38327
39154
|
return normalizeProjectConfig(JSON.parse(readFileSync9(path, "utf-8")));
|
|
@@ -38345,9 +39172,9 @@ function ensureProjectConfig(targetDir = process.cwd()) {
|
|
|
38345
39172
|
}
|
|
38346
39173
|
function saveProjectConfig(config, targetDir = process.cwd()) {
|
|
38347
39174
|
const dir = getProjectStateDir(targetDir);
|
|
38348
|
-
|
|
39175
|
+
mkdirSync6(dir, { recursive: true });
|
|
38349
39176
|
const normalized = normalizeProjectConfig({ ...config, updatedAt: new Date().toISOString() });
|
|
38350
|
-
|
|
39177
|
+
writeFileSync5(getProjectConfigPath(targetDir), JSON.stringify(normalized, null, 2) + `
|
|
38351
39178
|
`);
|
|
38352
39179
|
}
|
|
38353
39180
|
function pinProjectSkill(name, details = {}, targetDir = process.cwd()) {
|
|
@@ -38442,12 +39269,12 @@ var __dirname2 = dirname8(fileURLToPath2(import.meta.url));
|
|
|
38442
39269
|
function findSkillsDir() {
|
|
38443
39270
|
let dir = __dirname2;
|
|
38444
39271
|
for (let i3 = 0;i3 < 5; i3++) {
|
|
38445
|
-
const candidate =
|
|
38446
|
-
if (
|
|
39272
|
+
const candidate = join13(dir, "skills");
|
|
39273
|
+
if (existsSync10(candidate) && !dir.includes(".skills"))
|
|
38447
39274
|
return candidate;
|
|
38448
39275
|
dir = dirname8(dir);
|
|
38449
39276
|
}
|
|
38450
|
-
return
|
|
39277
|
+
return join13(__dirname2, "..", "skills");
|
|
38451
39278
|
}
|
|
38452
39279
|
var SKILLS_DIR = findSkillsDir();
|
|
38453
39280
|
function getSkillPath(name) {
|
|
@@ -38455,25 +39282,25 @@ function getSkillPath(name) {
|
|
|
38455
39282
|
const portable = findPortableSkill(skillName);
|
|
38456
39283
|
if (portable)
|
|
38457
39284
|
return portable.path;
|
|
38458
|
-
const legacyCustomPath =
|
|
38459
|
-
if (
|
|
39285
|
+
const legacyCustomPath = join13(getDataDir(), "custom", skillName);
|
|
39286
|
+
if (existsSync10(legacyCustomPath))
|
|
38460
39287
|
return legacyCustomPath;
|
|
38461
39288
|
const extensionPath = findExtensionSkillPath(skillName);
|
|
38462
39289
|
if (extensionPath)
|
|
38463
39290
|
return extensionPath;
|
|
38464
|
-
return
|
|
39291
|
+
return join13(SKILLS_DIR, skillName);
|
|
38465
39292
|
}
|
|
38466
39293
|
function getCanonicalSkillName(name) {
|
|
38467
39294
|
return getSkill(name)?.name ?? resolveSkillAlias(normalizeSkillSlug(name));
|
|
38468
39295
|
}
|
|
38469
39296
|
function skillExists(name) {
|
|
38470
|
-
return
|
|
39297
|
+
return existsSync10(getSkillPath(name));
|
|
38471
39298
|
}
|
|
38472
39299
|
function installSkill(name, options = {}) {
|
|
38473
39300
|
const { targetDir = process.cwd(), overwrite = false } = options;
|
|
38474
39301
|
const canonicalName = getCanonicalSkillName(name);
|
|
38475
39302
|
const skillName = normalizeSkillName(canonicalName);
|
|
38476
|
-
if (!
|
|
39303
|
+
if (!existsSync10(getSkillPath(name))) {
|
|
38477
39304
|
const knownOfficial = Boolean(getSkill(name));
|
|
38478
39305
|
return {
|
|
38479
39306
|
skill: canonicalName,
|
|
@@ -38500,7 +39327,7 @@ function installSkill(name, options = {}) {
|
|
|
38500
39327
|
}
|
|
38501
39328
|
function installSkillSource(name, _options = {}) {
|
|
38502
39329
|
const canonicalName = getCanonicalSkillName(name);
|
|
38503
|
-
if (!
|
|
39330
|
+
if (!existsSync10(getSkillPath(name))) {
|
|
38504
39331
|
return { skill: canonicalName, success: false, error: `Skill '${name}' not found`, mode: "source" };
|
|
38505
39332
|
}
|
|
38506
39333
|
return {
|
|
@@ -38521,11 +39348,11 @@ function installSkillManifest(manifest, _options = {}) {
|
|
|
38521
39348
|
}
|
|
38522
39349
|
function createLocalSkillManifest(name, generateSkillMd) {
|
|
38523
39350
|
const sourcePath = getSkillPath(name);
|
|
38524
|
-
if (!
|
|
39351
|
+
if (!existsSync10(sourcePath))
|
|
38525
39352
|
return null;
|
|
38526
39353
|
let skillMd = "";
|
|
38527
|
-
const skillMdPath =
|
|
38528
|
-
if (
|
|
39354
|
+
const skillMdPath = join13(sourcePath, "SKILL.md");
|
|
39355
|
+
if (existsSync10(skillMdPath)) {
|
|
38529
39356
|
skillMd = readFileSync10(skillMdPath, "utf-8");
|
|
38530
39357
|
} else if (generateSkillMd) {
|
|
38531
39358
|
skillMd = generateSkillMd(name) ?? "";
|
|
@@ -38598,20 +39425,20 @@ function getAgentSkillsDir(agent, scope = "global", projectDir) {
|
|
|
38598
39425
|
const base = projectDir || process.cwd();
|
|
38599
39426
|
switch (agent) {
|
|
38600
39427
|
case "pi":
|
|
38601
|
-
return scope === "project" ?
|
|
39428
|
+
return scope === "project" ? join13(base, ".pi", "skills") : join13(homedir4(), ".pi", "agent", "skills");
|
|
38602
39429
|
case "opencode":
|
|
38603
|
-
return scope === "project" ?
|
|
39430
|
+
return scope === "project" ? join13(base, ".opencode", "skills") : join13(homedir4(), ".config", "opencode", "skills");
|
|
38604
39431
|
default:
|
|
38605
|
-
return scope === "project" ?
|
|
39432
|
+
return scope === "project" ? join13(base, `.${agent}`, "skills") : join13(homedir4(), `.${agent}`, "skills");
|
|
38606
39433
|
}
|
|
38607
39434
|
}
|
|
38608
39435
|
function getAgentSkillPath(name, agent, scope = "global", projectDir) {
|
|
38609
39436
|
const skillName = normalizeSkillName(getCanonicalSkillName(name));
|
|
38610
|
-
return
|
|
39437
|
+
return join13(getAgentSkillsDir(agent, scope, projectDir), skillName);
|
|
38611
39438
|
}
|
|
38612
39439
|
function installSkillForAgent(name, options, generateSkillMd) {
|
|
38613
39440
|
const canonicalName = getCanonicalSkillName(name);
|
|
38614
|
-
if (!
|
|
39441
|
+
if (!existsSync10(getSkillPath(name))) {
|
|
38615
39442
|
return { skill: canonicalName, success: false, error: `Skill '${name}' not found` };
|
|
38616
39443
|
}
|
|
38617
39444
|
const scope = options.scope ?? "global";
|
|
@@ -38635,15 +39462,15 @@ function removeSkillForAgent(name, options) {
|
|
|
38635
39462
|
const canonicalName = getCanonicalSkillName(name);
|
|
38636
39463
|
const scope = options.scope ?? "global";
|
|
38637
39464
|
const dir = getAgentSkillPath(canonicalName, options.agent, scope, options.projectDir);
|
|
38638
|
-
if (!
|
|
39465
|
+
if (!existsSync10(join13(dir, SYNC_MARKER_FILE)))
|
|
38639
39466
|
return false;
|
|
38640
|
-
|
|
39467
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
38641
39468
|
return true;
|
|
38642
39469
|
}
|
|
38643
39470
|
function resolveAgentSkillMd(name, generateSkillMd) {
|
|
38644
39471
|
const sourcePath = getSkillPath(name);
|
|
38645
|
-
const skillMdPath =
|
|
38646
|
-
if (
|
|
39472
|
+
const skillMdPath = join13(sourcePath, "SKILL.md");
|
|
39473
|
+
if (existsSync10(skillMdPath))
|
|
38647
39474
|
return readFileSync10(skillMdPath, "utf-8");
|
|
38648
39475
|
if (generateSkillMd)
|
|
38649
39476
|
return generateSkillMd(name);
|
|
@@ -38662,7 +39489,7 @@ function warnMissingDependencies(name, targetDir) {
|
|
|
38662
39489
|
}
|
|
38663
39490
|
function generateMinimalSkillMd(name) {
|
|
38664
39491
|
const sourcePath = getSkillPath(name);
|
|
38665
|
-
if (!
|
|
39492
|
+
if (!existsSync10(sourcePath))
|
|
38666
39493
|
return null;
|
|
38667
39494
|
const canonicalName = getCanonicalSkillName(name);
|
|
38668
39495
|
const meta = getSkill(canonicalName);
|
|
@@ -38677,7 +39504,7 @@ function generateMinimalSkillMd(name) {
|
|
|
38677
39504
|
"---",
|
|
38678
39505
|
""
|
|
38679
39506
|
].filter(Boolean);
|
|
38680
|
-
const fallbackDoc = readFileIfExists(
|
|
39507
|
+
const fallbackDoc = readFileIfExists(join13(sourcePath, "README.md")) || readFileIfExists(join13(sourcePath, "CLAUDE.md"));
|
|
38681
39508
|
if (fallbackDoc)
|
|
38682
39509
|
return `${frontmatter.join(`
|
|
38683
39510
|
`)}${fallbackDoc.trim()}
|
|
@@ -38696,8 +39523,8 @@ skills run ${canonicalName}
|
|
|
38696
39523
|
`;
|
|
38697
39524
|
}
|
|
38698
39525
|
function readBundledSkillVersion(name) {
|
|
38699
|
-
const pkgPath =
|
|
38700
|
-
if (!
|
|
39526
|
+
const pkgPath = join13(getSkillPath(name), "package.json");
|
|
39527
|
+
if (!existsSync10(pkgPath))
|
|
38701
39528
|
return "unknown";
|
|
38702
39529
|
try {
|
|
38703
39530
|
const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
|
|
@@ -38707,7 +39534,7 @@ function readBundledSkillVersion(name) {
|
|
|
38707
39534
|
}
|
|
38708
39535
|
}
|
|
38709
39536
|
function readFileIfExists(path) {
|
|
38710
|
-
return
|
|
39537
|
+
return existsSync10(path) ? readFileSync10(path, "utf-8") : null;
|
|
38711
39538
|
}
|
|
38712
39539
|
function loadProjectConfigCompat(targetDir) {
|
|
38713
39540
|
return loadProjectConfig(targetDir);
|
|
@@ -38717,8 +39544,8 @@ function loadProjectConfigCompat(targetDir) {
|
|
|
38717
39544
|
function isInstructionSkillDir(skillPath, meta) {
|
|
38718
39545
|
if (meta?.kind === "instruction")
|
|
38719
39546
|
return true;
|
|
38720
|
-
const skillMdPath =
|
|
38721
|
-
if (!
|
|
39547
|
+
const skillMdPath = join14(skillPath, "SKILL.md");
|
|
39548
|
+
if (!existsSync11(skillMdPath))
|
|
38722
39549
|
return false;
|
|
38723
39550
|
try {
|
|
38724
39551
|
return parseSkillFrontmatter(readFileSync11(skillMdPath, "utf-8"))?.kind === "instruction";
|
|
@@ -38744,12 +39571,12 @@ var HOSTED_PROVIDER_ENV_PREFIXES = [
|
|
|
38744
39571
|
];
|
|
38745
39572
|
function getSkillDocs(name) {
|
|
38746
39573
|
const skillPath = getSkillPath(name);
|
|
38747
|
-
if (!
|
|
39574
|
+
if (!existsSync11(skillPath))
|
|
38748
39575
|
return null;
|
|
38749
39576
|
return {
|
|
38750
|
-
skillMd: readIfExists(
|
|
38751
|
-
readme: readIfExists(
|
|
38752
|
-
claudeMd: readIfExists(
|
|
39577
|
+
skillMd: readIfExists(join14(skillPath, "SKILL.md")),
|
|
39578
|
+
readme: readIfExists(join14(skillPath, "README.md")),
|
|
39579
|
+
claudeMd: readIfExists(join14(skillPath, "CLAUDE.md"))
|
|
38753
39580
|
};
|
|
38754
39581
|
}
|
|
38755
39582
|
function getSkillBestDoc(name) {
|
|
@@ -38760,11 +39587,11 @@ function getSkillBestDoc(name) {
|
|
|
38760
39587
|
}
|
|
38761
39588
|
function getSkillRequirements(name) {
|
|
38762
39589
|
const skillPath = getSkillPath(name);
|
|
38763
|
-
if (!
|
|
39590
|
+
if (!existsSync11(skillPath))
|
|
38764
39591
|
return null;
|
|
38765
39592
|
const texts = [];
|
|
38766
39593
|
for (const file of ["SKILL.md", "README.md", "CLAUDE.md", ".env.example", ".env.local.example"]) {
|
|
38767
|
-
const content = readIfExists(
|
|
39594
|
+
const content = readIfExists(join14(skillPath, file));
|
|
38768
39595
|
if (content)
|
|
38769
39596
|
texts.push(content);
|
|
38770
39597
|
}
|
|
@@ -38803,8 +39630,8 @@ function getSkillRequirements(name) {
|
|
|
38803
39630
|
const skillName = normalizeSkillName(name);
|
|
38804
39631
|
let cliCommand = `skills run ${skillName}`;
|
|
38805
39632
|
let dependencies = {};
|
|
38806
|
-
const pkgPath =
|
|
38807
|
-
if (
|
|
39633
|
+
const pkgPath = join14(skillPath, "package.json");
|
|
39634
|
+
if (existsSync11(pkgPath)) {
|
|
38808
39635
|
try {
|
|
38809
39636
|
const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
|
|
38810
39637
|
dependencies = pkg.dependencies || {};
|
|
@@ -38824,7 +39651,7 @@ async function runSkill(name, args, options = {}) {
|
|
|
38824
39651
|
const meta = getSkill(name);
|
|
38825
39652
|
const canonicalName = meta?.name ?? name;
|
|
38826
39653
|
const skillPath = getSkillPath(canonicalName);
|
|
38827
|
-
if (!
|
|
39654
|
+
if (!existsSync11(skillPath)) {
|
|
38828
39655
|
return { exitCode: 1, error: `Skill '${name}' not found` };
|
|
38829
39656
|
}
|
|
38830
39657
|
if (isInstructionSkillDir(skillPath, meta)) {
|
|
@@ -38833,8 +39660,8 @@ async function runSkill(name, args, options = {}) {
|
|
|
38833
39660
|
error: `Skill '${name}' is an instruction skill (kind: instruction) and is not runnable. Instruction skills are consumed by coding agents via SKILL.md, not executed with 'skills run'.`
|
|
38834
39661
|
};
|
|
38835
39662
|
}
|
|
38836
|
-
const pkgPath =
|
|
38837
|
-
if (!
|
|
39663
|
+
const pkgPath = join14(skillPath, "package.json");
|
|
39664
|
+
if (!existsSync11(pkgPath)) {
|
|
38838
39665
|
return { exitCode: 1, error: `No package.json in skill '${name}'` };
|
|
38839
39666
|
}
|
|
38840
39667
|
let entryPoint;
|
|
@@ -38853,12 +39680,12 @@ async function runSkill(name, args, options = {}) {
|
|
|
38853
39680
|
} catch {
|
|
38854
39681
|
return { exitCode: 1, error: `Failed to parse package.json for skill '${name}'` };
|
|
38855
39682
|
}
|
|
38856
|
-
const entryPath =
|
|
38857
|
-
if (!
|
|
39683
|
+
const entryPath = join14(skillPath, entryPoint);
|
|
39684
|
+
if (!existsSync11(entryPath)) {
|
|
38858
39685
|
return { exitCode: 1, error: `Entry point '${entryPoint}' not found in skill '${name}'` };
|
|
38859
39686
|
}
|
|
38860
|
-
const nodeModules =
|
|
38861
|
-
if (!
|
|
39687
|
+
const nodeModules = join14(skillPath, "node_modules");
|
|
39688
|
+
if (!existsSync11(nodeModules)) {
|
|
38862
39689
|
const install = Bun.spawn(["bun", "install", "--no-save"], {
|
|
38863
39690
|
cwd: skillPath,
|
|
38864
39691
|
stdout: "pipe",
|
|
@@ -38930,7 +39757,7 @@ function generateSkillMd(name) {
|
|
|
38930
39757
|
if (!meta)
|
|
38931
39758
|
return null;
|
|
38932
39759
|
const skillPath = getSkillPath(name);
|
|
38933
|
-
if (!
|
|
39760
|
+
if (!existsSync11(skillPath))
|
|
38934
39761
|
return null;
|
|
38935
39762
|
const frontmatter = [
|
|
38936
39763
|
"---",
|
|
@@ -38939,11 +39766,11 @@ function generateSkillMd(name) {
|
|
|
38939
39766
|
"---"
|
|
38940
39767
|
].join(`
|
|
38941
39768
|
`);
|
|
38942
|
-
const readme = readIfExists(
|
|
38943
|
-
const claudeMd = readIfExists(
|
|
39769
|
+
const readme = readIfExists(join14(skillPath, "README.md"));
|
|
39770
|
+
const claudeMd = readIfExists(join14(skillPath, "CLAUDE.md"));
|
|
38944
39771
|
let cliCommand = null;
|
|
38945
|
-
const pkgPath =
|
|
38946
|
-
if (
|
|
39772
|
+
const pkgPath = join14(skillPath, "package.json");
|
|
39773
|
+
if (existsSync11(pkgPath)) {
|
|
38947
39774
|
try {
|
|
38948
39775
|
const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
|
|
38949
39776
|
if (pkg.bin) {
|
|
@@ -39020,7 +39847,7 @@ function extractEnvVars(text) {
|
|
|
39020
39847
|
}
|
|
39021
39848
|
function readIfExists(path) {
|
|
39022
39849
|
try {
|
|
39023
|
-
if (
|
|
39850
|
+
if (existsSync11(path)) {
|
|
39024
39851
|
return readFileSync11(path, "utf-8");
|
|
39025
39852
|
}
|
|
39026
39853
|
} catch {}
|
|
@@ -39062,7 +39889,7 @@ function getServerSkillMd(slug) {
|
|
|
39062
39889
|
const path = resolve(skillsDir, name, "SKILL.md");
|
|
39063
39890
|
if (!isInsideDir(skillsDir, path))
|
|
39064
39891
|
return null;
|
|
39065
|
-
return
|
|
39892
|
+
return existsSync12(path) ? readFileSync12(path, "utf8") : null;
|
|
39066
39893
|
}
|
|
39067
39894
|
|
|
39068
39895
|
// src/server/skills-api.ts
|
|
@@ -39072,6 +39899,17 @@ var MAX_SLUG_LENGTH = 128;
|
|
|
39072
39899
|
var MAX_SKILL_MD_BYTES = 512000;
|
|
39073
39900
|
var MAX_MANIFEST_BYTES = MAX_SKILL_MD_BYTES + 64000;
|
|
39074
39901
|
var ALLOWED_PUBLISH_PARTS = new Set(["manifest", "bundle"]);
|
|
39902
|
+
function pinPayload(pin) {
|
|
39903
|
+
return { slug: pin.slug, pinnedAt: pin.pinnedAt, metadata: pin.metadata };
|
|
39904
|
+
}
|
|
39905
|
+
function pinMetadataField(body) {
|
|
39906
|
+
if (body.metadata === undefined)
|
|
39907
|
+
return {};
|
|
39908
|
+
if (!body.metadata || typeof body.metadata !== "object" || Array.isArray(body.metadata)) {
|
|
39909
|
+
throw new SkillRequestError(400, "INVALID_METADATA", "`metadata` must be a JSON object");
|
|
39910
|
+
}
|
|
39911
|
+
return body.metadata;
|
|
39912
|
+
}
|
|
39075
39913
|
|
|
39076
39914
|
class SkillRequestError extends Error {
|
|
39077
39915
|
status;
|
|
@@ -39100,33 +39938,130 @@ function publishedPayload(record) {
|
|
|
39100
39938
|
...publishedSkillMeta(record),
|
|
39101
39939
|
slug: record.slug,
|
|
39102
39940
|
publishedSource: record.source,
|
|
39941
|
+
...record.skillMd ? { skillMd: record.skillMd } : {},
|
|
39103
39942
|
...record.bundleSha256 ? { bundleSha256: record.bundleSha256, bundleByteSize: record.bundleByteSize } : {},
|
|
39104
39943
|
publishedAt: record.createdAt,
|
|
39105
|
-
updatedAt: record.updatedAt
|
|
39944
|
+
updatedAt: record.updatedAt,
|
|
39945
|
+
revisionId: record.revisionId,
|
|
39946
|
+
revisionNumber: record.revisionNumber
|
|
39947
|
+
};
|
|
39948
|
+
}
|
|
39949
|
+
function revisionEtag(revisionId) {
|
|
39950
|
+
return `"${revisionId}"`;
|
|
39951
|
+
}
|
|
39952
|
+
function parseIfMatch(value) {
|
|
39953
|
+
if (value === null || value.trim() === "")
|
|
39954
|
+
return;
|
|
39955
|
+
const trimmed = value.trim();
|
|
39956
|
+
if (trimmed === "*") {
|
|
39957
|
+
throw new SkillRequestError(400, "INVALID_IF_MATCH", "If-Match must name the exact revision id (the ETag of the current revision); '*' is not accepted");
|
|
39958
|
+
}
|
|
39959
|
+
const unquoted = trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
|
|
39960
|
+
if (!REVISION_ID_PATTERN.test(unquoted)) {
|
|
39961
|
+
throw new SkillRequestError(400, "INVALID_IF_MATCH", "If-Match must carry a revision id: a 64-character lowercase hex sha-256, quoted as the server's ETag");
|
|
39962
|
+
}
|
|
39963
|
+
return unquoted;
|
|
39964
|
+
}
|
|
39965
|
+
async function tombstoneStatus(store, artifactStorage, principal, record) {
|
|
39966
|
+
if (!record.tombstonedAt)
|
|
39967
|
+
return "live";
|
|
39968
|
+
if (record.tombstonePurgeAfter && record.tombstonePurgeAfter <= new Date().toISOString()) {
|
|
39969
|
+
const purged = await store.purgeExpiredTombstones(principal);
|
|
39970
|
+
for (const removed of purged) {
|
|
39971
|
+
if (removed.bundleSha256)
|
|
39972
|
+
await discardCollectedObject(store, artifactStorage, principal, removed.bundleSha256);
|
|
39973
|
+
}
|
|
39974
|
+
return "purged";
|
|
39975
|
+
}
|
|
39976
|
+
return {
|
|
39977
|
+
slug: record.slug,
|
|
39978
|
+
deleted: true,
|
|
39979
|
+
code: "TOMBSTONED",
|
|
39980
|
+
tombstonedAt: record.tombstonedAt,
|
|
39981
|
+
tombstonePurgeAfter: record.tombstonePurgeAfter,
|
|
39982
|
+
revisionId: record.revisionId
|
|
39106
39983
|
};
|
|
39107
39984
|
}
|
|
39108
39985
|
async function listMergedSkills(store, principal) {
|
|
39109
39986
|
const published = await store.listSkills(principal);
|
|
39987
|
+
return mergedSkillPayloads(published, listServerSkills());
|
|
39988
|
+
}
|
|
39989
|
+
function mergedSkillPayloads(published, bundled) {
|
|
39110
39990
|
const publishedBySlug = new Map(published.map((record) => [record.slug, record]));
|
|
39111
|
-
const merged = mergeSkillRegistryLists(
|
|
39991
|
+
const merged = mergeSkillRegistryLists(bundled, published.map(publishedSkillMeta));
|
|
39112
39992
|
return merged.map((skill) => {
|
|
39113
39993
|
const record = publishedBySlug.get(skill.name);
|
|
39114
39994
|
return record ? publishedPayload(record) : skill;
|
|
39115
39995
|
});
|
|
39116
39996
|
}
|
|
39117
|
-
async function
|
|
39997
|
+
async function resolvePublishedSkill(store, artifactStorage, principal, slug) {
|
|
39118
39998
|
const record = await store.getSkill(principal, slug);
|
|
39119
|
-
if (record)
|
|
39120
|
-
return
|
|
39999
|
+
if (!record)
|
|
40000
|
+
return { kind: "absent" };
|
|
40001
|
+
const status = await tombstoneStatus(store, artifactStorage, principal, record);
|
|
40002
|
+
if (status === "purged")
|
|
40003
|
+
return { kind: "absent" };
|
|
40004
|
+
if (status !== "live")
|
|
40005
|
+
return { kind: "tombstone", payload: status };
|
|
40006
|
+
return { kind: "published", record };
|
|
40007
|
+
}
|
|
40008
|
+
async function listOrgTags(store, principal) {
|
|
40009
|
+
const publishedSlugs = await store.listPublishedSlugs(principal);
|
|
40010
|
+
const tags = new Set;
|
|
40011
|
+
for (const tag of await store.listTags(principal)) {
|
|
40012
|
+
if (tag.trim())
|
|
40013
|
+
tags.add(tag);
|
|
40014
|
+
}
|
|
40015
|
+
for (const skill of listServerSkills()) {
|
|
40016
|
+
if (publishedSlugs.includes(skill.name))
|
|
40017
|
+
continue;
|
|
40018
|
+
for (const tag of skill.tags) {
|
|
40019
|
+
if (tag.trim())
|
|
40020
|
+
tags.add(tag);
|
|
40021
|
+
}
|
|
40022
|
+
}
|
|
40023
|
+
return [...tags].sort();
|
|
40024
|
+
}
|
|
40025
|
+
async function listMergedSkillsByTag(store, principal, tag) {
|
|
40026
|
+
const published = await store.listSkillsByTag(principal, tag);
|
|
40027
|
+
const publishedSlugs = await store.listPublishedSlugs(principal);
|
|
40028
|
+
const bundled = listServerSkills().filter((skill) => skill.tags.includes(tag) && !publishedSlugs.includes(skill.name));
|
|
40029
|
+
return mergedSkillPayloads(published, bundled);
|
|
40030
|
+
}
|
|
40031
|
+
function skillSummary(skill) {
|
|
40032
|
+
return {
|
|
40033
|
+
slug: String(skill.slug ?? skill.name),
|
|
40034
|
+
...typeof skill.name === "string" ? { name: skill.name } : {},
|
|
40035
|
+
...typeof skill.version === "string" ? { version: skill.version } : {},
|
|
40036
|
+
...typeof skill.updatedAt === "string" ? { updatedAt: skill.updatedAt } : {}
|
|
40037
|
+
};
|
|
40038
|
+
}
|
|
40039
|
+
async function listPinsByTag(store, principal, tag) {
|
|
40040
|
+
const publishedSlugs = await store.listPublishedSlugs(principal);
|
|
40041
|
+
const bundledTaggedSlugs = new Set;
|
|
40042
|
+
for (const skill of listServerSkills()) {
|
|
40043
|
+
if (skill.tags.includes(tag) && !publishedSlugs.includes(skill.name))
|
|
40044
|
+
bundledTaggedSlugs.add(skill.name);
|
|
40045
|
+
}
|
|
40046
|
+
const publishedPins = await store.listPinsByTag(principal, tag);
|
|
40047
|
+
const bundledPins = bundledTaggedSlugs.size ? (await store.listPins(principal)).filter((pin) => bundledTaggedSlugs.has(pin.slug)) : [];
|
|
40048
|
+
return [...publishedPins, ...bundledPins].sort((a3, b3) => a3.slug.localeCompare(b3.slug)).map(pinPayload);
|
|
40049
|
+
}
|
|
40050
|
+
async function getMergedSkill(store, artifactStorage, principal, slug) {
|
|
40051
|
+
const resolved = await resolvePublishedSkill(store, artifactStorage, principal, slug);
|
|
40052
|
+
if (resolved.kind === "tombstone")
|
|
40053
|
+
return resolved.payload;
|
|
40054
|
+
if (resolved.kind === "published")
|
|
40055
|
+
return publishedPayload(resolved.record);
|
|
39121
40056
|
const bundled = getServerSkill(slug);
|
|
39122
40057
|
return bundled ? bundled : null;
|
|
39123
40058
|
}
|
|
39124
|
-
async function getMergedSkillMd(store, principal, slug) {
|
|
39125
|
-
const
|
|
39126
|
-
if (
|
|
39127
|
-
return record.skillMd;
|
|
39128
|
-
if (record)
|
|
40059
|
+
async function getMergedSkillMd(store, artifactStorage, principal, slug) {
|
|
40060
|
+
const resolved = await resolvePublishedSkill(store, artifactStorage, principal, slug);
|
|
40061
|
+
if (resolved.kind === "tombstone")
|
|
39129
40062
|
return null;
|
|
40063
|
+
if (resolved.kind === "published")
|
|
40064
|
+
return resolved.record.skillMd ?? null;
|
|
39130
40065
|
return getServerSkillMd(slug);
|
|
39131
40066
|
}
|
|
39132
40067
|
async function parsePublishRequest(request, config) {
|
|
@@ -39168,7 +40103,7 @@ async function parsePublishRequest(request, config) {
|
|
|
39168
40103
|
if (bundleBytes.byteLength === 0) {
|
|
39169
40104
|
throw new SkillRequestError(400, "BUNDLE_EMPTY", "the uploaded bundle is empty");
|
|
39170
40105
|
}
|
|
39171
|
-
const sha256 =
|
|
40106
|
+
const sha256 = createHash8("sha256").update(bundleBytes).digest("hex");
|
|
39172
40107
|
const claimed = optionalString(manifest.bundleSha256);
|
|
39173
40108
|
if (claimed)
|
|
39174
40109
|
assertSha2562(claimed);
|
|
@@ -39196,9 +40131,13 @@ async function parsePublishRequest(request, config) {
|
|
|
39196
40131
|
}
|
|
39197
40132
|
return { input: buildPublishInput(parseManifestJson(text)) };
|
|
39198
40133
|
}
|
|
39199
|
-
async function storePublishedSkill(store, artifactStorage, principal, parsed) {
|
|
40134
|
+
async function storePublishedSkill(store, artifactStorage, principal, parsed, expectedRevisionId) {
|
|
39200
40135
|
const superseded = (await store.getSkill(principal, parsed.input.slug))?.bundleSha256;
|
|
39201
|
-
let input = {
|
|
40136
|
+
let input = {
|
|
40137
|
+
...parsed.input,
|
|
40138
|
+
principal,
|
|
40139
|
+
...expectedRevisionId ? { expectedRevisionId } : {}
|
|
40140
|
+
};
|
|
39202
40141
|
if (parsed.bundleBytes && input.bundle) {
|
|
39203
40142
|
const placement = await artifactStorage.putBundle(principal.orgId, input.bundle.sha256, parsed.bundleBytes, input.bundle.contentType);
|
|
39204
40143
|
input = { ...input, bundle: { ...input.bundle, ...placement } };
|
|
@@ -39209,15 +40148,8 @@ async function storePublishedSkill(store, artifactStorage, principal, parsed) {
|
|
|
39209
40148
|
}
|
|
39210
40149
|
return record;
|
|
39211
40150
|
}
|
|
39212
|
-
async function deletePublishedSkill(store, artifactStorage, principal, slug) {
|
|
39213
|
-
|
|
39214
|
-
if (!record)
|
|
39215
|
-
return false;
|
|
39216
|
-
const deleted = await store.deleteSkill(principal, slug);
|
|
39217
|
-
if (deleted && record.bundleSha256) {
|
|
39218
|
-
await discardCollectedObject(store, artifactStorage, principal, record.bundleSha256);
|
|
39219
|
-
}
|
|
39220
|
-
return deleted;
|
|
40151
|
+
async function deletePublishedSkill(store, artifactStorage, principal, slug, tombstoneWindowMs) {
|
|
40152
|
+
return store.deleteSkill(principal, slug, tombstoneWindowMs);
|
|
39221
40153
|
}
|
|
39222
40154
|
async function discardCollectedObject(store, artifactStorage, principal, sha256) {
|
|
39223
40155
|
if (await store.getSkillBundle(principal, sha256))
|
|
@@ -39237,7 +40169,7 @@ async function readPublishedBundle(store, artifactStorage, principal, slug) {
|
|
|
39237
40169
|
if (!bytes) {
|
|
39238
40170
|
throw new SkillRequestError(503, "BUNDLE_BACKEND_UNAVAILABLE", "bundle storage backend unavailable");
|
|
39239
40171
|
}
|
|
39240
|
-
const actual =
|
|
40172
|
+
const actual = createHash8("sha256").update(bytes).digest("hex");
|
|
39241
40173
|
if (actual !== record.bundleSha256) {
|
|
39242
40174
|
throw new SkillRequestError(500, "BUNDLE_DIGEST_DRIFT", `stored bundle for '${slug}' hashes to ${actual} but was published as ${record.bundleSha256}`);
|
|
39243
40175
|
}
|
|
@@ -39353,10 +40285,10 @@ async function createSkillsFetchHandler(options = {}) {
|
|
|
39353
40285
|
const segments = pathSegments(url.pathname);
|
|
39354
40286
|
try {
|
|
39355
40287
|
if (request.method === "GET" && url.pathname === "/health") {
|
|
39356
|
-
return json({ ok: true, service: "
|
|
40288
|
+
return json({ ok: true, service: "skills", time: new Date().toISOString() });
|
|
39357
40289
|
}
|
|
39358
40290
|
if (request.method === "GET" && url.pathname === "/ready") {
|
|
39359
|
-
return json({ ok: true, service: "
|
|
40291
|
+
return json({ ok: true, service: "skills" });
|
|
39360
40292
|
}
|
|
39361
40293
|
if (url.pathname.startsWith("/api/")) {
|
|
39362
40294
|
const principal = await authenticateRequest(store, request);
|
|
@@ -39374,6 +40306,14 @@ async function createSkillsFetchHandler(options = {}) {
|
|
|
39374
40306
|
if (error instanceof SkillRequestError) {
|
|
39375
40307
|
return json({ error: error.message, code: error.code }, { status: error.status });
|
|
39376
40308
|
}
|
|
40309
|
+
if (error instanceof SkillRevisionConflictError) {
|
|
40310
|
+
return json({
|
|
40311
|
+
error: error.message,
|
|
40312
|
+
code: "REVISION_CONFLICT",
|
|
40313
|
+
slug: error.slug,
|
|
40314
|
+
...error.currentRevisionId ? { currentRevisionId: error.currentRevisionId } : {}
|
|
40315
|
+
}, { status: 409 });
|
|
40316
|
+
}
|
|
39377
40317
|
return json({ error: "internal server error", detail: error.message }, { status: 500 });
|
|
39378
40318
|
}
|
|
39379
40319
|
};
|
|
@@ -39393,24 +40333,43 @@ async function handleApiV1(store, principal, request, parts, config, artifactSto
|
|
|
39393
40333
|
return json({ error: "invalid path segment", code: "INVALID_PATH" }, { status: 400 });
|
|
39394
40334
|
}
|
|
39395
40335
|
if (resource === "skills") {
|
|
39396
|
-
if (request.method === "GET" && !id)
|
|
40336
|
+
if (request.method === "GET" && !id) {
|
|
40337
|
+
const tag = new URL(request.url).searchParams.get("tag");
|
|
40338
|
+
if (tag !== null && tag !== "")
|
|
40339
|
+
return json(await listMergedSkillsByTag(store, principal, tag));
|
|
39397
40340
|
return json(await listMergedSkills(store, principal));
|
|
40341
|
+
}
|
|
39398
40342
|
if (request.method === "POST" && !id) {
|
|
40343
|
+
const expectedRevisionId = parseIfMatch(request.headers.get("if-match"));
|
|
39399
40344
|
const parsed = await parsePublishRequest(request, config);
|
|
39400
|
-
const record = await storePublishedSkill(store, artifactStorage, principal, parsed);
|
|
39401
|
-
return json(publishedPayload(record), { status: 201 });
|
|
40345
|
+
const record = await storePublishedSkill(store, artifactStorage, principal, parsed, expectedRevisionId);
|
|
40346
|
+
return json(publishedPayload(record), { status: 201, headers: { ETag: revisionEtag(record.revisionId) } });
|
|
39402
40347
|
}
|
|
39403
40348
|
if (request.method === "GET" && id && subresource === "skill.md") {
|
|
39404
|
-
const
|
|
40349
|
+
const resolved = await resolvePublishedSkill(store, artifactStorage, principal, id);
|
|
40350
|
+
if (resolved.kind === "tombstone") {
|
|
40351
|
+
return json({ error: "skill was deleted", ...resolved.payload }, { status: 410 });
|
|
40352
|
+
}
|
|
40353
|
+
const docs = await getMergedSkillMd(store, artifactStorage, principal, id);
|
|
39405
40354
|
return docs ? new Response(docs, { headers: { "Content-Type": "text/markdown; charset=utf-8", "Cache-Control": "no-store" } }) : json({ error: "skill not found", code: "SKILL_NOT_FOUND" }, { status: 404 });
|
|
39406
40355
|
}
|
|
39407
40356
|
if (request.method === "GET" && id && subresource === "bundle") {
|
|
40357
|
+
const resolved = await resolvePublishedSkill(store, artifactStorage, principal, id);
|
|
40358
|
+
if (resolved.kind === "tombstone") {
|
|
40359
|
+
return json({ error: "skill was deleted", ...resolved.payload }, { status: 410 });
|
|
40360
|
+
}
|
|
40361
|
+
if (resolved.kind === "absent") {
|
|
40362
|
+
return json({ error: "skill not found", code: "SKILL_NOT_FOUND" }, { status: 404 });
|
|
40363
|
+
}
|
|
39408
40364
|
const { record, bytes } = await readPublishedBundle(store, artifactStorage, principal, id);
|
|
39409
40365
|
const headers = {
|
|
39410
40366
|
"Content-Type": "application/gzip",
|
|
39411
40367
|
"Content-Length": String(bytes.byteLength),
|
|
39412
40368
|
"Content-Disposition": `attachment; filename="${record.slug}.tar.gz"`,
|
|
39413
40369
|
"X-Skill-Bundle-Sha256": record.bundleSha256 ?? "",
|
|
40370
|
+
"X-Skill-Revision-Id": record.revisionId,
|
|
40371
|
+
"X-Skill-Revision-Number": String(record.revisionNumber),
|
|
40372
|
+
ETag: revisionEtag(record.revisionId),
|
|
39414
40373
|
"Cache-Control": "no-store"
|
|
39415
40374
|
};
|
|
39416
40375
|
if (config.bundleSigningKey) {
|
|
@@ -39419,17 +40378,56 @@ async function handleApiV1(store, principal, request, parts, config, artifactSto
|
|
|
39419
40378
|
return new Response(bytes, { headers });
|
|
39420
40379
|
}
|
|
39421
40380
|
if (request.method === "GET" && id && !subresource) {
|
|
39422
|
-
const
|
|
40381
|
+
const resolved = await resolvePublishedSkill(store, artifactStorage, principal, id);
|
|
40382
|
+
if (resolved.kind === "tombstone") {
|
|
40383
|
+
return json({ error: "skill was deleted", ...resolved.payload }, { status: 410 });
|
|
40384
|
+
}
|
|
40385
|
+
if (resolved.kind === "published") {
|
|
40386
|
+
return json(publishedPayload(resolved.record), { headers: { ETag: revisionEtag(resolved.record.revisionId) } });
|
|
40387
|
+
}
|
|
40388
|
+
const skill = await getMergedSkill(store, artifactStorage, principal, id);
|
|
39423
40389
|
return skill ? json(skill) : json({ error: "skill not found", code: "SKILL_NOT_FOUND" }, { status: 404 });
|
|
39424
40390
|
}
|
|
39425
40391
|
if ((request.method === "PUT" || request.method === "PATCH") && id && !subresource) {
|
|
40392
|
+
const expectedRevisionId = parseIfMatch(request.headers.get("if-match"));
|
|
39426
40393
|
const body = await readJson(request, config.requestBodyLimitBytes);
|
|
39427
|
-
const updated = await store.updateSkill(principal, id, skillPatch(body));
|
|
39428
|
-
return updated ? json(publishedPayload(updated)) : json({ error: "published skill not found", code: "SKILL_NOT_FOUND" }, { status: 404 });
|
|
40394
|
+
const updated = await store.updateSkill(principal, id, skillPatch(body), expectedRevisionId);
|
|
40395
|
+
return updated ? json(publishedPayload(updated), { headers: { ETag: revisionEtag(updated.revisionId) } }) : json({ error: "published skill not found", code: "SKILL_NOT_FOUND" }, { status: 404 });
|
|
39429
40396
|
}
|
|
39430
40397
|
if (request.method === "DELETE" && id && !subresource) {
|
|
39431
|
-
const removed = await deletePublishedSkill(store, artifactStorage, principal, id);
|
|
39432
|
-
return removed ? json({
|
|
40398
|
+
const removed = await deletePublishedSkill(store, artifactStorage, principal, id, config.tombstoneWindowMs);
|
|
40399
|
+
return removed ? json({
|
|
40400
|
+
deleted: true,
|
|
40401
|
+
slug: id,
|
|
40402
|
+
...removed.tombstonedAt ? { tombstonedAt: removed.tombstonedAt, tombstonePurgeAfter: removed.tombstonePurgeAfter } : {}
|
|
40403
|
+
}) : json({ error: "published skill not found", code: "SKILL_NOT_FOUND" }, { status: 404 });
|
|
40404
|
+
}
|
|
40405
|
+
}
|
|
40406
|
+
if (resource === "pins") {
|
|
40407
|
+
if (request.method === "GET" && !id) {
|
|
40408
|
+
const tag = new URL(request.url).searchParams.get("tag");
|
|
40409
|
+
if (tag !== null && tag !== "")
|
|
40410
|
+
return json(await listPinsByTag(store, principal, tag));
|
|
40411
|
+
return json((await store.listPins(principal)).map(pinPayload));
|
|
40412
|
+
}
|
|
40413
|
+
if (request.method === "PUT" && id && !subresource) {
|
|
40414
|
+
assertPublishableSlug(id);
|
|
40415
|
+
const body = await readJson(request, config.requestBodyLimitBytes);
|
|
40416
|
+
const pin = await store.pinSkill(principal, id, pinMetadataField(body));
|
|
40417
|
+
return json(pinPayload(pin));
|
|
40418
|
+
}
|
|
40419
|
+
if (request.method === "DELETE" && id && !subresource) {
|
|
40420
|
+
assertPublishableSlug(id);
|
|
40421
|
+
const removed = await store.unpinSkill(principal, id);
|
|
40422
|
+
return removed ? json({ deleted: true, slug: id }) : json({ error: "pin not found", code: "PIN_NOT_FOUND" }, { status: 404 });
|
|
40423
|
+
}
|
|
40424
|
+
}
|
|
40425
|
+
if (resource === "tags") {
|
|
40426
|
+
if (request.method === "GET" && !id) {
|
|
40427
|
+
return json(await listOrgTags(store, principal));
|
|
40428
|
+
}
|
|
40429
|
+
if (request.method === "GET" && id && subresource === "skills") {
|
|
40430
|
+
return json((await listMergedSkillsByTag(store, principal, id)).map(skillSummary));
|
|
39433
40431
|
}
|
|
39434
40432
|
}
|
|
39435
40433
|
if (resource === "runs") {
|
|
@@ -44144,7 +45142,7 @@ CREATE TABLE IF NOT EXISTS execution_receipts (
|
|
|
44144
45142
|
);
|
|
44145
45143
|
`;
|
|
44146
45144
|
// src/sdk/execution/admission.ts
|
|
44147
|
-
import { createHash as
|
|
45145
|
+
import { createHash as createHash10 } from "crypto";
|
|
44148
45146
|
|
|
44149
45147
|
// src/sdk/execution/image-profile.ts
|
|
44150
45148
|
class ImageProfileResolutionError extends Error {
|
|
@@ -44269,7 +45267,7 @@ function createSubmitRunService(options) {
|
|
|
44269
45267
|
};
|
|
44270
45268
|
}
|
|
44271
45269
|
function digestInput(input) {
|
|
44272
|
-
return
|
|
45270
|
+
return createHash10("sha256").update(canonicalJson(input)).digest("hex");
|
|
44273
45271
|
}
|
|
44274
45272
|
// src/sdk/execution/state-machine.ts
|
|
44275
45273
|
var LEGAL_TRANSITIONS = {
|
|
@@ -44431,13 +45429,13 @@ function createReceiptService(store) {
|
|
|
44431
45429
|
};
|
|
44432
45430
|
}
|
|
44433
45431
|
// src/sdk/execution/dispatchers/ecs.ts
|
|
44434
|
-
import { createHash as
|
|
45432
|
+
import { createHash as createHash11 } from "crypto";
|
|
44435
45433
|
|
|
44436
45434
|
// ../../node_modules/.bun/@aws-sdk+client-ecs@3.1106.0/node_modules/@aws-sdk/client-ecs/dist-es/ECSClient.js
|
|
44437
45435
|
var import_client33 = __toESM(require_client2(), 1);
|
|
44438
45436
|
var import_core2 = __toESM(require_dist_cjs2(), 1);
|
|
44439
45437
|
var import_client34 = __toESM(require_client(), 1);
|
|
44440
|
-
var
|
|
45438
|
+
var import_config39 = __toESM(require_config(), 1);
|
|
44441
45439
|
var import_endpoints8 = __toESM(require_endpoints(), 1);
|
|
44442
45440
|
var import_protocols10 = __toESM(require_protocols(), 1);
|
|
44443
45441
|
var import_retry6 = __toESM(require_retry(), 1);
|
|
@@ -44576,7 +45574,7 @@ var package_default2 = {
|
|
|
44576
45574
|
var import_client29 = __toESM(require_client2(), 1);
|
|
44577
45575
|
var import_httpAuthSchemes6 = __toESM(require_httpAuthSchemes(), 1);
|
|
44578
45576
|
var import_client30 = __toESM(require_client(), 1);
|
|
44579
|
-
var
|
|
45577
|
+
var import_config38 = __toESM(require_config(), 1);
|
|
44580
45578
|
var import_retry5 = __toESM(require_retry(), 1);
|
|
44581
45579
|
var import_serde6 = __toESM(require_serde(), 1);
|
|
44582
45580
|
var import_node_http_handler3 = __toESM(require_dist_cjs6(), 1);
|
|
@@ -47517,7 +48515,7 @@ var getRuntimeConfig4 = (config) => {
|
|
|
47517
48515
|
// ../../node_modules/.bun/@aws-sdk+client-ecs@3.1106.0/node_modules/@aws-sdk/client-ecs/dist-es/runtimeConfig.js
|
|
47518
48516
|
var getRuntimeConfig5 = (config) => {
|
|
47519
48517
|
import_client30.emitWarningIfUnsupportedVersion(process.version);
|
|
47520
|
-
const defaultsMode =
|
|
48518
|
+
const defaultsMode = import_config38.resolveDefaultsModeConfig(config);
|
|
47521
48519
|
const defaultConfigProvider = () => defaultsMode().then(import_client30.loadConfigsForDefaultMode);
|
|
47522
48520
|
const clientSharedValues = getRuntimeConfig4(config);
|
|
47523
48521
|
import_client29.emitWarningIfUnsupportedVersion(process.version);
|
|
@@ -47530,21 +48528,21 @@ var getRuntimeConfig5 = (config) => {
|
|
|
47530
48528
|
...config,
|
|
47531
48529
|
runtime: "node",
|
|
47532
48530
|
defaultsMode,
|
|
47533
|
-
authSchemePreference: config?.authSchemePreference ??
|
|
48531
|
+
authSchemePreference: config?.authSchemePreference ?? import_config38.loadConfig(import_httpAuthSchemes6.NODE_AUTH_SCHEME_PREFERENCE_OPTIONS, loaderConfig),
|
|
47534
48532
|
bodyLengthChecker: config?.bodyLengthChecker ?? import_serde6.calculateBodyLength,
|
|
47535
48533
|
credentialDefaultProvider: config?.credentialDefaultProvider ?? defaultProvider,
|
|
47536
48534
|
defaultUserAgentProvider: config?.defaultUserAgentProvider ?? import_client29.createDefaultUserAgentProvider({ serviceId: clientSharedValues.serviceId, clientVersion: package_default2.version }),
|
|
47537
|
-
maxAttempts: config?.maxAttempts ??
|
|
47538
|
-
region: config?.region ??
|
|
48535
|
+
maxAttempts: config?.maxAttempts ?? import_config38.loadConfig(import_retry5.NODE_MAX_ATTEMPT_CONFIG_OPTIONS, config),
|
|
48536
|
+
region: config?.region ?? import_config38.loadConfig(import_config38.NODE_REGION_CONFIG_OPTIONS, { ...import_config38.NODE_REGION_CONFIG_FILE_OPTIONS, ...loaderConfig }),
|
|
47539
48537
|
requestHandler: import_node_http_handler3.NodeHttpHandler.create(config?.requestHandler ?? defaultConfigProvider),
|
|
47540
|
-
retryMode: config?.retryMode ??
|
|
48538
|
+
retryMode: config?.retryMode ?? import_config38.loadConfig({
|
|
47541
48539
|
...import_retry5.NODE_RETRY_MODE_CONFIG_OPTIONS,
|
|
47542
48540
|
default: async () => (await defaultConfigProvider()).retryMode || import_retry5.DEFAULT_RETRY_MODE
|
|
47543
48541
|
}, config),
|
|
47544
48542
|
streamCollector: config?.streamCollector ?? import_node_http_handler3.streamCollector,
|
|
47545
|
-
useDualstackEndpoint: config?.useDualstackEndpoint ??
|
|
47546
|
-
useFipsEndpoint: config?.useFipsEndpoint ??
|
|
47547
|
-
userAgentAppId: config?.userAgentAppId ??
|
|
48543
|
+
useDualstackEndpoint: config?.useDualstackEndpoint ?? import_config38.loadConfig(import_config38.NODE_USE_DUALSTACK_ENDPOINT_CONFIG_OPTIONS, loaderConfig),
|
|
48544
|
+
useFipsEndpoint: config?.useFipsEndpoint ?? import_config38.loadConfig(import_config38.NODE_USE_FIPS_ENDPOINT_CONFIG_OPTIONS, loaderConfig),
|
|
48545
|
+
userAgentAppId: config?.userAgentAppId ?? import_config38.loadConfig(import_client29.NODE_APP_ID_CONFIG_OPTIONS, loaderConfig)
|
|
47548
48546
|
};
|
|
47549
48547
|
};
|
|
47550
48548
|
|
|
@@ -47609,7 +48607,7 @@ class ECSClient extends import_client34.Client {
|
|
|
47609
48607
|
const _config_1 = resolveClientEndpointParameters3(_config_0);
|
|
47610
48608
|
const _config_2 = import_client33.resolveUserAgentConfig(_config_1);
|
|
47611
48609
|
const _config_3 = import_retry6.resolveRetryConfig(_config_2);
|
|
47612
|
-
const _config_4 =
|
|
48610
|
+
const _config_4 = import_config39.resolveRegionConfig(_config_3);
|
|
47613
48611
|
const _config_5 = import_client33.resolveHostHeaderConfig(_config_4);
|
|
47614
48612
|
const _config_6 = import_endpoints8.resolveEndpointConfig(_config_5);
|
|
47615
48613
|
const _config_7 = resolveHttpAuthSchemeConfig3(_config_6);
|
|
@@ -47662,13 +48660,13 @@ class StopTaskCommand extends command3(_ep03, _mw03, "StopTask", StopTask$) {
|
|
|
47662
48660
|
var CLIENT_TOKEN_BYTES = 16;
|
|
47663
48661
|
var TERMINAL_TASK_STATUSES = new Set(["STOPPED"]);
|
|
47664
48662
|
function clientTokenFor(runId2, attemptId) {
|
|
47665
|
-
return
|
|
48663
|
+
return createHash11("sha256").update(`${runId2}\x00${attemptId}`).digest("hex").slice(0, CLIENT_TOKEN_BYTES * 2);
|
|
47666
48664
|
}
|
|
47667
48665
|
function startedByFor(runId2, attemptNumber) {
|
|
47668
48666
|
return `skills-exec/${runId2}/a${attemptNumber}`;
|
|
47669
48667
|
}
|
|
47670
48668
|
function requestDigestFor(admission, attemptId) {
|
|
47671
|
-
return
|
|
48669
|
+
return createHash11("sha256").update(canonicalJson({ admission, attemptId })).digest("hex");
|
|
47672
48670
|
}
|
|
47673
48671
|
|
|
47674
48672
|
class EcsDispatcher {
|
|
@@ -48043,7 +49041,7 @@ function runPointersOf(run) {
|
|
|
48043
49041
|
// src/sdk/governance-store.ts
|
|
48044
49042
|
import { Database as Database3 } from "bun:sqlite";
|
|
48045
49043
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
48046
|
-
import { mkdirSync as
|
|
49044
|
+
import { mkdirSync as mkdirSync7 } from "fs";
|
|
48047
49045
|
import { dirname as dirname9 } from "path";
|
|
48048
49046
|
function receiptId() {
|
|
48049
49047
|
return `rcpt_${Date.now().toString(36)}_${randomUUID4().replace(/-/g, "").slice(0, 10)}`;
|
|
@@ -48133,7 +49131,7 @@ class SqliteGovernanceStore {
|
|
|
48133
49131
|
closed = false;
|
|
48134
49132
|
constructor(path = SQLITE_MEMORY_PATH, options = {}) {
|
|
48135
49133
|
if (path !== SQLITE_MEMORY_PATH)
|
|
48136
|
-
|
|
49134
|
+
mkdirSync7(dirname9(path), { recursive: true });
|
|
48137
49135
|
this.db = new Database3(path, { create: true, readwrite: true });
|
|
48138
49136
|
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
48139
49137
|
this.db.exec("PRAGMA foreign_keys = ON");
|