@hasna/skills 0.1.44 → 0.1.46

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/dist/index.js CHANGED
@@ -1320,12 +1320,47 @@ function ensurePortableSkillFiles(skillPath, manifest) {
1320
1320
  writeFileSync2(join3(skillPath, "skill.json"), renderSkillJson(next));
1321
1321
  if (!existsSync3(join3(skillPath, "AGENTS.md")))
1322
1322
  writeFileSync2(join3(skillPath, "AGENTS.md"), renderAgentsMd(next));
1323
- if (!existsSync3(join3(skillPath, "package.json")))
1324
- writeFileSync2(join3(skillPath, "package.json"), renderPackageJson(next));
1323
+ ensurePackageJson(skillPath, next);
1325
1324
  if (!existsSync3(join3(skillPath, "tsconfig.json")))
1326
1325
  writeFileSync2(join3(skillPath, "tsconfig.json"), renderTsconfig());
1327
1326
  return readPortableSkillManifest(skillPath, next.name);
1328
1327
  }
1328
+ function ensurePackageJson(skillPath, manifest) {
1329
+ const pkgPath = join3(skillPath, "package.json");
1330
+ const first = manifest.commands[0] ?? { name: manifest.name, entry: "src/index.ts" };
1331
+ const commandName = normalizePortableSkillName(first.name || manifest.name);
1332
+ const entry = (first.entry ?? "src/index.ts").replace(/^\.\//, "");
1333
+ if (!existsSync3(pkgPath)) {
1334
+ writeFileSync2(pkgPath, renderPackageJson(manifest));
1335
+ return;
1336
+ }
1337
+ const existing = readJsonObject(pkgPath);
1338
+ const bin = {};
1339
+ if (isRecord(existing.bin)) {
1340
+ for (const [name, value] of Object.entries(existing.bin)) {
1341
+ if (typeof value === "string" && value.trim())
1342
+ bin[normalizePortableSkillName(name)] = value.replace(/^\.\//, "");
1343
+ }
1344
+ } else {
1345
+ const binEntry = stringValue(existing.bin);
1346
+ if (binEntry)
1347
+ bin[manifest.name] = binEntry.replace(/^\.\//, "");
1348
+ }
1349
+ bin[commandName] = entry;
1350
+ const scripts = isRecord(existing.scripts) ? { ...existing.scripts } : {};
1351
+ if (!stringValue(scripts.dev))
1352
+ scripts.dev = `bun run ${entry}`;
1353
+ writeFileSync2(pkgPath, `${JSON.stringify({
1354
+ ...existing,
1355
+ name: manifest.name,
1356
+ version: manifest.version,
1357
+ description: manifest.description,
1358
+ type: stringValue(existing.type) ?? "module",
1359
+ bin,
1360
+ scripts
1361
+ }, null, 2)}
1362
+ `);
1363
+ }
1329
1364
  function copySkillDirectory(source, destination) {
1330
1365
  mkdirSync2(destination, { recursive: true });
1331
1366
  cpSync(source, destination, {
@@ -18776,7 +18811,7 @@ import { dirname as dirname4, relative as relative3 } from "path";
18776
18811
  // package.json
18777
18812
  var package_default = {
18778
18813
  name: "@hasna/skills",
18779
- version: "0.1.44",
18814
+ version: "0.1.46",
18780
18815
  description: "Skills library for AI coding agents",
18781
18816
  type: "module",
18782
18817
  bin: {
@@ -18787,6 +18822,10 @@ var package_default = {
18787
18822
  ".": {
18788
18823
  import: "./dist/index.js",
18789
18824
  types: "./dist/index.d.ts"
18825
+ },
18826
+ "./storage": {
18827
+ import: "./dist/storage.js",
18828
+ types: "./dist/storage.d.ts"
18790
18829
  }
18791
18830
  },
18792
18831
  files: [
@@ -18807,7 +18846,7 @@ var package_default = {
18807
18846
  types: "./dist/index.d.ts",
18808
18847
  scripts: {
18809
18848
  clean: "rm -rf bin/ dist/",
18810
- build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun --external ink --external react --external chalk && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/index.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
18849
+ build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/index.ts ./src/storage.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
18811
18850
  test: "bun test",
18812
18851
  dev: "bun run ./src/cli/index.tsx",
18813
18852
  "dev:watch": "bun --watch run ./src/cli/index.tsx",
@@ -18837,9 +18876,11 @@ var package_default = {
18837
18876
  devDependencies: {
18838
18877
  "@types/bun": "latest",
18839
18878
  "@types/react": "^18.2.0",
18879
+ "react-devtools-core": "^7.0.1",
18840
18880
  typescript: "^5"
18841
18881
  },
18842
18882
  dependencies: {
18883
+ "@hasna/events": "^0.1.3",
18843
18884
  "@modelcontextprotocol/sdk": "^1.26.0",
18844
18885
  chalk: "^5.3.0",
18845
18886
  commander: "^12.1.0",
@@ -19314,6 +19355,46 @@ var toolContracts = [
19314
19355
  inputSchema: objectSchema(),
19315
19356
  outputSchema: objectSchema({}, [], "Setup summary.", true)
19316
19357
  },
19358
+ {
19359
+ name: "storage_status",
19360
+ title: "Storage Status",
19361
+ description: "Show local-first storage paths and optional repo-owned Postgres/S3 readiness.",
19362
+ params: ["directory?"],
19363
+ category: "storage",
19364
+ sideEffects: "none",
19365
+ stable: true,
19366
+ inputSchema: objectSchema({ directory: stringSchema("Project directory.") }),
19367
+ outputSchema: objectSchema({
19368
+ package: stringSchema("Package name."),
19369
+ mode: { type: "string", enum: ["local", "remote", "hybrid"] },
19370
+ local: objectSchema({}, [], "Local storage paths.", true),
19371
+ remote: objectSchema({}, [], "Remote storage readiness.", true)
19372
+ }, ["package", "mode", "local", "remote"])
19373
+ },
19374
+ {
19375
+ name: "storage_sync_plan",
19376
+ title: "Storage Sync Plan",
19377
+ description: "Plan .skills snapshot sync for optional Postgres/S3 storage without network access.",
19378
+ params: ["directory?", "includeSchemaSql?"],
19379
+ category: "storage",
19380
+ sideEffects: "none",
19381
+ stable: true,
19382
+ inputSchema: objectSchema({
19383
+ directory: stringSchema("Project directory."),
19384
+ includeSchemaSql: { type: "boolean", default: false }
19385
+ }),
19386
+ outputSchema: objectSchema({
19387
+ package: stringSchema("Package name."),
19388
+ noNetwork: { type: "boolean", const: true },
19389
+ mode: { type: "string", enum: ["local", "remote", "hybrid"] },
19390
+ databaseConfigured: { type: "boolean" },
19391
+ s3Configured: { type: "boolean" },
19392
+ snapshotFileCount: { type: "number" },
19393
+ s3ObjectCount: { type: "number" },
19394
+ env: objectSchema({}, [], "Storage env var names.", true),
19395
+ schemaSql: stringSchema("Optional Postgres schema SQL.")
19396
+ }, ["package", "noNetwork", "mode", "databaseConfigured", "s3Configured"])
19397
+ },
19317
19398
  {
19318
19399
  name: "schedule_skill",
19319
19400
  title: "Schedule Skill",
@@ -19666,6 +19747,553 @@ function saveFeedback(input) {
19666
19747
  }
19667
19748
  return { saved: true, category, path: getFeedbackDbPath() };
19668
19749
  }
19750
+ // src/lib/native-storage.ts
19751
+ import { createHash as createHash2, createHmac } from "crypto";
19752
+ import {
19753
+ existsSync as existsSync12,
19754
+ mkdirSync as mkdirSync9,
19755
+ readFileSync as readFileSync11,
19756
+ readdirSync as readdirSync5,
19757
+ statSync as statSync4,
19758
+ writeFileSync as writeFileSync8
19759
+ } from "fs";
19760
+ import { dirname as dirname6, join as join12, normalize as normalize3, relative as relative4, sep } from "path";
19761
+ var SKILLS_STORAGE_TABLES = [
19762
+ "skills_sync_records",
19763
+ "skills_sync_cursors"
19764
+ ];
19765
+ var STORAGE_TABLES = SKILLS_STORAGE_TABLES;
19766
+ var SKILLS_NATIVE_STORAGE_ENV = {
19767
+ mode: "HASNA_SKILLS_STORAGE_MODE",
19768
+ databaseUrl: "HASNA_SKILLS_DATABASE_URL",
19769
+ databaseSsl: "HASNA_SKILLS_DATABASE_SSL",
19770
+ databaseSchema: "HASNA_SKILLS_DATABASE_SCHEMA",
19771
+ s3Bucket: "HASNA_SKILLS_S3_BUCKET",
19772
+ s3Prefix: "HASNA_SKILLS_S3_PREFIX",
19773
+ awsRegion: "HASNA_SKILLS_AWS_REGION",
19774
+ s3Endpoint: "HASNA_SKILLS_S3_ENDPOINT",
19775
+ s3ForcePathStyle: "HASNA_SKILLS_S3_FORCE_PATH_STYLE",
19776
+ s3AccessKeyId: "HASNA_SKILLS_S3_ACCESS_KEY_ID",
19777
+ s3SecretAccessKey: "HASNA_SKILLS_S3_SECRET_ACCESS_KEY",
19778
+ s3SessionToken: "HASNA_SKILLS_S3_SESSION_TOKEN",
19779
+ syncBatchSize: "HASNA_SKILLS_SYNC_BATCH_SIZE",
19780
+ dryRun: "HASNA_SKILLS_SYNC_DRY_RUN"
19781
+ };
19782
+ var SKILLS_NATIVE_STORAGE_FALLBACK_ENV = {
19783
+ mode: "SKILLS_STORAGE_MODE",
19784
+ databaseUrl: "SKILLS_DATABASE_URL",
19785
+ databaseSsl: "SKILLS_DATABASE_SSL",
19786
+ databaseSchema: "SKILLS_DATABASE_SCHEMA",
19787
+ s3Bucket: "SKILLS_S3_BUCKET",
19788
+ s3Prefix: "SKILLS_S3_PREFIX",
19789
+ awsRegion: "SKILLS_AWS_REGION",
19790
+ s3Endpoint: "SKILLS_S3_ENDPOINT",
19791
+ s3ForcePathStyle: "SKILLS_S3_FORCE_PATH_STYLE",
19792
+ s3AccessKeyId: "SKILLS_S3_ACCESS_KEY_ID",
19793
+ s3SecretAccessKey: "SKILLS_S3_SECRET_ACCESS_KEY",
19794
+ s3SessionToken: "SKILLS_S3_SESSION_TOKEN",
19795
+ syncBatchSize: "SKILLS_SYNC_BATCH_SIZE",
19796
+ dryRun: "SKILLS_SYNC_DRY_RUN"
19797
+ };
19798
+ var SKILLS_STORAGE_ENV = SKILLS_NATIVE_STORAGE_ENV;
19799
+ var SKILLS_STORAGE_FALLBACK_ENV = SKILLS_NATIVE_STORAGE_FALLBACK_ENV;
19800
+ function resolveSkillsNativeStorageConfig(env = process.env) {
19801
+ const mode = getSkillsStorageMode(env);
19802
+ return {
19803
+ mode,
19804
+ databaseUrl: getSkillsStorageDatabaseUrl(env),
19805
+ databaseSsl: parseBoolean(readStorageEnv(env, "databaseSsl").value),
19806
+ databaseSchema: readStorageEnv(env, "databaseSchema").value,
19807
+ s3Bucket: readStorageEnv(env, "s3Bucket").value,
19808
+ s3Prefix: readStorageEnv(env, "s3Prefix").value,
19809
+ awsRegion: readStorageEnv(env, "awsRegion").value ?? "us-east-1",
19810
+ s3Endpoint: readStorageEnv(env, "s3Endpoint").value,
19811
+ s3ForcePathStyle: parseBoolean(readStorageEnv(env, "s3ForcePathStyle").value) ?? false,
19812
+ syncBatchSize: parsePositiveInteger(readStorageEnv(env, "syncBatchSize").value) ?? 500,
19813
+ dryRun: parseBoolean(readStorageEnv(env, "dryRun").value) ?? true
19814
+ };
19815
+ }
19816
+ function resolveStorageConfig(env = process.env) {
19817
+ return resolveSkillsNativeStorageConfig(env);
19818
+ }
19819
+ function getSkillsStorageMode(env = process.env) {
19820
+ return parseMode(readStorageEnv(env, "mode").value);
19821
+ }
19822
+ function getStorageMode(env = process.env) {
19823
+ return getSkillsStorageMode(env);
19824
+ }
19825
+ function getSkillsStorageDatabaseEnv(env = process.env) {
19826
+ return readStorageEnv(env, "databaseUrl").name;
19827
+ }
19828
+ function getStorageDatabaseEnv(env = process.env) {
19829
+ return getSkillsStorageDatabaseEnv(env);
19830
+ }
19831
+ function getSkillsStorageDatabaseUrl(env = process.env) {
19832
+ return readStorageEnv(env, "databaseUrl").value;
19833
+ }
19834
+ function getStorageDatabaseUrl(env = process.env) {
19835
+ return getSkillsStorageDatabaseUrl(env);
19836
+ }
19837
+ function getSkillsNativeStorageStatus(options = {}) {
19838
+ const env = options.env ?? process.env;
19839
+ const config2 = resolveSkillsNativeStorageConfig(env);
19840
+ const modeEnv = readStorageEnv(env, "mode");
19841
+ const databaseEnv = readStorageEnv(env, "databaseUrl");
19842
+ const s3BucketEnv = readStorageEnv(env, "s3Bucket");
19843
+ const targetDir = options.targetDir ?? process.cwd();
19844
+ return {
19845
+ package: "open-skills",
19846
+ mode: config2.mode,
19847
+ tables: [...SKILLS_STORAGE_TABLES],
19848
+ env: {
19849
+ mode: SKILLS_NATIVE_STORAGE_ENV.mode,
19850
+ databaseUrl: SKILLS_NATIVE_STORAGE_ENV.databaseUrl,
19851
+ s3Bucket: SKILLS_NATIVE_STORAGE_ENV.s3Bucket
19852
+ },
19853
+ local: {
19854
+ dataDir: getDataDir(),
19855
+ projectStateDir: getProjectStateDir(targetDir),
19856
+ feedbackDbPath: join12(getDataDir(), "skills.db")
19857
+ },
19858
+ remote: {
19859
+ databaseConfigured: Boolean(config2.databaseUrl),
19860
+ s3Configured: Boolean(config2.s3Bucket),
19861
+ databaseEnv: SKILLS_NATIVE_STORAGE_ENV.databaseUrl,
19862
+ s3BucketEnv: SKILLS_NATIVE_STORAGE_ENV.s3Bucket,
19863
+ activeModeEnv: modeEnv.name,
19864
+ activeDatabaseEnv: databaseEnv.name,
19865
+ activeS3BucketEnv: s3BucketEnv.name,
19866
+ region: config2.awsRegion ?? "us-east-1",
19867
+ dryRun: config2.dryRun
19868
+ }
19869
+ };
19870
+ }
19871
+ function getSkillsStorageStatus(options = {}) {
19872
+ return getSkillsNativeStorageStatus(options);
19873
+ }
19874
+ function getStorageStatus(options = {}) {
19875
+ return getSkillsNativeStorageStatus(options);
19876
+ }
19877
+ function exportSkillsLocalSnapshot(targetDir = process.cwd(), options = {}) {
19878
+ const projectStateDir = getProjectStateDir(targetDir);
19879
+ const files = [];
19880
+ if (existsSync12(projectStateDir)) {
19881
+ for (const filePath of walkFiles2(projectStateDir)) {
19882
+ const bytes = readFileSync11(filePath);
19883
+ const relativePath = toPosix(relative4(targetDir, filePath));
19884
+ files.push({
19885
+ path: relativePath,
19886
+ sizeBytes: bytes.byteLength,
19887
+ sha256: createHash2("sha256").update(bytes).digest("hex"),
19888
+ ...options.includeFileContents ? { contentBase64: Buffer.from(bytes).toString("base64") } : {}
19889
+ });
19890
+ }
19891
+ }
19892
+ return {
19893
+ schemaVersion: 1,
19894
+ exportedAt: new Date().toISOString(),
19895
+ files: files.sort((a, b) => a.path.localeCompare(b.path))
19896
+ };
19897
+ }
19898
+ function importSkillsLocalSnapshot(snapshot, targetDir = process.cwd(), options = {}) {
19899
+ let written = 0;
19900
+ let skipped = 0;
19901
+ for (const file2 of snapshot.files) {
19902
+ if (!file2.contentBase64) {
19903
+ skipped += 1;
19904
+ continue;
19905
+ }
19906
+ const absolutePath = resolveSnapshotPath(targetDir, file2.path);
19907
+ if (existsSync12(absolutePath) && !options.overwrite) {
19908
+ skipped += 1;
19909
+ continue;
19910
+ }
19911
+ const bytes = Buffer.from(file2.contentBase64, "base64");
19912
+ const hash2 = createHash2("sha256").update(bytes).digest("hex");
19913
+ if (hash2 !== file2.sha256) {
19914
+ throw new Error(`Snapshot file checksum mismatch: ${file2.path}`);
19915
+ }
19916
+ mkdirSync9(dirname6(absolutePath), { recursive: true });
19917
+ writeFileSync8(absolutePath, bytes);
19918
+ written += 1;
19919
+ }
19920
+ return { written, skipped };
19921
+ }
19922
+ var skillsPostgresSyncSchemaSql = `
19923
+ CREATE TABLE IF NOT EXISTS skills_sync_records (
19924
+ scope TEXT NOT NULL,
19925
+ kind TEXT NOT NULL,
19926
+ id TEXT NOT NULL,
19927
+ updated_at TIMESTAMPTZ NOT NULL,
19928
+ deleted_at TIMESTAMPTZ,
19929
+ source TEXT,
19930
+ payload JSONB NOT NULL,
19931
+ PRIMARY KEY (scope, kind, id)
19932
+ );
19933
+
19934
+ CREATE INDEX IF NOT EXISTS skills_sync_records_updated_at_idx
19935
+ ON skills_sync_records (updated_at);
19936
+
19937
+ CREATE TABLE IF NOT EXISTS skills_sync_cursors (
19938
+ scope TEXT NOT NULL,
19939
+ cursor_name TEXT NOT NULL,
19940
+ value TEXT NOT NULL,
19941
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
19942
+ PRIMARY KEY (scope, cursor_name)
19943
+ );
19944
+ `.trim();
19945
+
19946
+ class SkillsPostgresSyncStore {
19947
+ client;
19948
+ constructor(client) {
19949
+ this.client = client;
19950
+ }
19951
+ async ensureSchema() {
19952
+ await this.client.query(skillsPostgresSyncSchemaSql);
19953
+ }
19954
+ async upsertRecords(records) {
19955
+ let count = 0;
19956
+ for (const record2 of records) {
19957
+ await this.client.query([
19958
+ "INSERT INTO skills_sync_records",
19959
+ "(scope, kind, id, updated_at, deleted_at, source, payload)",
19960
+ "VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)",
19961
+ "ON CONFLICT (scope, kind, id) DO UPDATE SET",
19962
+ "updated_at = EXCLUDED.updated_at,",
19963
+ "deleted_at = EXCLUDED.deleted_at,",
19964
+ "source = EXCLUDED.source,",
19965
+ "payload = EXCLUDED.payload"
19966
+ ].join(" "), [
19967
+ record2.scope,
19968
+ record2.kind,
19969
+ record2.id,
19970
+ record2.updatedAt,
19971
+ record2.deletedAt ?? null,
19972
+ record2.source ?? null,
19973
+ JSON.stringify(record2.payload)
19974
+ ]);
19975
+ count += 1;
19976
+ }
19977
+ return count;
19978
+ }
19979
+ async pullUpdatedSince(params) {
19980
+ const limit = params.limit ?? 500;
19981
+ const result = await this.client.query([
19982
+ "SELECT scope, kind, id, updated_at, deleted_at, source, payload",
19983
+ "FROM skills_sync_records",
19984
+ "WHERE scope = $1 AND updated_at > $2",
19985
+ "ORDER BY updated_at ASC",
19986
+ "LIMIT $3"
19987
+ ].join(" "), [params.scope, params.since ?? "1970-01-01T00:00:00.000Z", limit]);
19988
+ return result.rows.map((row) => ({
19989
+ scope: row.scope,
19990
+ kind: row.kind,
19991
+ id: row.id,
19992
+ updatedAt: toIsoString(row.updated_at),
19993
+ deletedAt: row.deleted_at ? toIsoString(row.deleted_at) : null,
19994
+ source: row.source,
19995
+ payload: typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload
19996
+ }));
19997
+ }
19998
+ async getCursor(scope, cursorName) {
19999
+ const result = await this.client.query("SELECT value FROM skills_sync_cursors WHERE scope = $1 AND cursor_name = $2", [scope, cursorName]);
20000
+ return result.rows[0]?.value ?? null;
20001
+ }
20002
+ async setCursor(scope, cursorName, value) {
20003
+ await this.client.query([
20004
+ "INSERT INTO skills_sync_cursors (scope, cursor_name, value, updated_at)",
20005
+ "VALUES ($1, $2, $3, now())",
20006
+ "ON CONFLICT (scope, cursor_name) DO UPDATE SET",
20007
+ "value = EXCLUDED.value, updated_at = EXCLUDED.updated_at"
20008
+ ].join(" "), [scope, cursorName, value]);
20009
+ }
20010
+ }
20011
+ function createSkillsPostgresSyncStore(client) {
20012
+ return new SkillsPostgresSyncStore(client);
20013
+ }
20014
+ function createSkillsSnapshotSyncRecord(snapshot, options = {}) {
20015
+ return {
20016
+ scope: options.scope ?? "default",
20017
+ kind: "local-snapshot",
20018
+ id: options.id ?? "project-state",
20019
+ updatedAt: snapshot.exportedAt,
20020
+ source: options.source ?? "open-skills",
20021
+ payload: snapshot
20022
+ };
20023
+ }
20024
+
20025
+ class SkillsS3ObjectStore {
20026
+ options;
20027
+ fetchImpl;
20028
+ region;
20029
+ prefix;
20030
+ constructor(options) {
20031
+ this.options = options;
20032
+ this.fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
20033
+ this.region = options.region ?? "us-east-1";
20034
+ this.prefix = normalizeS3Prefix(options.prefix);
20035
+ }
20036
+ objectKey(path) {
20037
+ const cleaned = toPosix(path).replace(/^\.?\//, "").replace(/^\/+/, "");
20038
+ return [this.prefix, cleaned].filter(Boolean).join("/");
20039
+ }
20040
+ objectUrl(key) {
20041
+ return buildSkillsS3ObjectUrl({
20042
+ bucket: this.options.bucket,
20043
+ key,
20044
+ region: this.region,
20045
+ endpoint: this.options.endpoint,
20046
+ forcePathStyle: this.options.forcePathStyle
20047
+ });
20048
+ }
20049
+ async putObject(params) {
20050
+ const key = this.objectKey(params.key);
20051
+ const body = typeof params.body === "string" ? new TextEncoder().encode(params.body) : params.body;
20052
+ const url2 = this.objectUrl(key);
20053
+ const headers = {
20054
+ "content-type": params.contentType ?? "application/octet-stream",
20055
+ "x-amz-content-sha256": sha256Hex(body)
20056
+ };
20057
+ const signed = signSkillsAwsV4Request({
20058
+ method: "PUT",
20059
+ url: url2,
20060
+ region: this.region,
20061
+ service: "s3",
20062
+ headers,
20063
+ body,
20064
+ credentials: this.options.credentials
20065
+ });
20066
+ const response = await this.fetchImpl(url2, {
20067
+ method: "PUT",
20068
+ headers: signed.headers,
20069
+ body: toArrayBuffer(body)
20070
+ });
20071
+ if (!response.ok) {
20072
+ throw new Error(`S3 put failed for ${key}: ${response.status} ${response.statusText}`.trim());
20073
+ }
20074
+ return {
20075
+ key,
20076
+ url: url2,
20077
+ etag: response.headers.get("etag"),
20078
+ sizeBytes: body.byteLength
20079
+ };
20080
+ }
20081
+ async getObject(key) {
20082
+ const objectKey = this.objectKey(key);
20083
+ const url2 = this.objectUrl(objectKey);
20084
+ const signed = signSkillsAwsV4Request({
20085
+ method: "GET",
20086
+ url: url2,
20087
+ region: this.region,
20088
+ service: "s3",
20089
+ headers: { "x-amz-content-sha256": sha256Hex(new Uint8Array) },
20090
+ credentials: this.options.credentials
20091
+ });
20092
+ const response = await this.fetchImpl(url2, {
20093
+ method: "GET",
20094
+ headers: signed.headers
20095
+ });
20096
+ if (!response.ok) {
20097
+ throw new Error(`S3 get failed for ${objectKey}: ${response.status} ${response.statusText}`.trim());
20098
+ }
20099
+ return new Uint8Array(await response.arrayBuffer());
20100
+ }
20101
+ }
20102
+ function createSkillsS3ObjectStore(options) {
20103
+ return new SkillsS3ObjectStore(options);
20104
+ }
20105
+ function planSkillsS3SnapshotUpload(snapshot, options = {}) {
20106
+ const prefix = normalizeS3Prefix(options.prefix);
20107
+ return snapshot.files.map((file2) => ({
20108
+ path: file2.path,
20109
+ key: [prefix, file2.path.replace(/^\.?\//, "")].filter(Boolean).join("/"),
20110
+ sizeBytes: file2.sizeBytes,
20111
+ sha256: file2.sha256
20112
+ }));
20113
+ }
20114
+ async function uploadSkillsSnapshotFilesToS3(snapshot, store) {
20115
+ const uploaded = [];
20116
+ for (const file2 of snapshot.files) {
20117
+ if (!file2.contentBase64)
20118
+ continue;
20119
+ uploaded.push(await store.putObject({
20120
+ key: file2.path,
20121
+ body: Buffer.from(file2.contentBase64, "base64"),
20122
+ contentType: contentTypeForPath(file2.path)
20123
+ }));
20124
+ }
20125
+ return uploaded;
20126
+ }
20127
+ function signSkillsAwsV4Request(options) {
20128
+ const now = options.now ?? new Date;
20129
+ const amzDate = toAmzDate(now);
20130
+ const dateStamp = amzDate.slice(0, 8);
20131
+ const url2 = new URL(options.url);
20132
+ const bodyBytes = typeof options.body === "string" ? new TextEncoder().encode(options.body) : options.body ?? new Uint8Array;
20133
+ const payloadHash = sha256Hex(bodyBytes);
20134
+ const headers = normalizeHeaders({
20135
+ ...options.headers ?? {},
20136
+ host: url2.host,
20137
+ "x-amz-date": amzDate,
20138
+ "x-amz-content-sha256": options.headers?.["x-amz-content-sha256"] ?? payloadHash,
20139
+ ...options.credentials.sessionToken ? { "x-amz-security-token": options.credentials.sessionToken } : {}
20140
+ });
20141
+ const signedHeaderNames = Object.keys(headers).sort();
20142
+ const canonicalHeaders = signedHeaderNames.map((name) => `${name}:${headers[name]}
20143
+ `).join("");
20144
+ const canonicalQuery = canonicalizeQuery(url2.searchParams);
20145
+ const canonicalRequest = [
20146
+ options.method.toUpperCase(),
20147
+ encodeUriPath(url2.pathname),
20148
+ canonicalQuery,
20149
+ canonicalHeaders,
20150
+ signedHeaderNames.join(";"),
20151
+ headers["x-amz-content-sha256"]
20152
+ ].join(`
20153
+ `);
20154
+ const credentialScope = `${dateStamp}/${options.region}/${options.service}/aws4_request`;
20155
+ const stringToSign = [
20156
+ "AWS4-HMAC-SHA256",
20157
+ amzDate,
20158
+ credentialScope,
20159
+ sha256Hex(canonicalRequest)
20160
+ ].join(`
20161
+ `);
20162
+ const signingKey = getAwsSigningKey(options.credentials.secretAccessKey, dateStamp, options.region, options.service);
20163
+ const signature = createHmac("sha256", signingKey).update(stringToSign).digest("hex");
20164
+ headers.authorization = [
20165
+ `AWS4-HMAC-SHA256 Credential=${options.credentials.accessKeyId}/${credentialScope}`,
20166
+ `SignedHeaders=${signedHeaderNames.join(";")}`,
20167
+ `Signature=${signature}`
20168
+ ].join(", ");
20169
+ return { headers, canonicalRequest, stringToSign };
20170
+ }
20171
+ function buildSkillsS3ObjectUrl(params) {
20172
+ const region = params.region ?? "us-east-1";
20173
+ const key = params.key.split("/").map(encodeURIComponent).join("/");
20174
+ if (params.endpoint) {
20175
+ const endpoint = params.endpoint.replace(/\/+$/, "");
20176
+ return params.forcePathStyle ? `${endpoint}/${encodeURIComponent(params.bucket)}/${key}` : `${endpoint}/${key}`;
20177
+ }
20178
+ return params.forcePathStyle ? `https://s3.${region}.amazonaws.com/${encodeURIComponent(params.bucket)}/${key}` : `https://${params.bucket}.s3.${region}.amazonaws.com/${key}`;
20179
+ }
20180
+ function parseMode(value) {
20181
+ const normalized = value?.trim().toLowerCase();
20182
+ if (normalized === "remote" || normalized === "hybrid")
20183
+ return normalized;
20184
+ return "local";
20185
+ }
20186
+ function readStorageEnv(env, key) {
20187
+ const primaryName = SKILLS_NATIVE_STORAGE_ENV[key];
20188
+ const primaryValue = cleanOptional(env[primaryName]);
20189
+ if (primaryValue !== undefined)
20190
+ return { name: primaryName, value: primaryValue };
20191
+ const fallbackName = SKILLS_NATIVE_STORAGE_FALLBACK_ENV[key];
20192
+ const fallbackValue = cleanOptional(env[fallbackName]);
20193
+ if (fallbackValue !== undefined)
20194
+ return { name: fallbackName, value: fallbackValue };
20195
+ return { name: primaryName };
20196
+ }
20197
+ function cleanOptional(value) {
20198
+ const cleaned = value?.trim();
20199
+ return cleaned ? cleaned : undefined;
20200
+ }
20201
+ function parseBoolean(value) {
20202
+ if (value === undefined)
20203
+ return;
20204
+ const normalized = value.trim().toLowerCase();
20205
+ if (["1", "true", "yes", "on"].includes(normalized))
20206
+ return true;
20207
+ if (["0", "false", "no", "off"].includes(normalized))
20208
+ return false;
20209
+ return;
20210
+ }
20211
+ function parsePositiveInteger(value) {
20212
+ if (!value)
20213
+ return;
20214
+ const parsed = Number.parseInt(value, 10);
20215
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
20216
+ }
20217
+ function walkFiles2(dir) {
20218
+ const files = [];
20219
+ for (const entry of readdirSync5(dir)) {
20220
+ const full = join12(dir, entry);
20221
+ const stats = statSync4(full);
20222
+ if (stats.isDirectory())
20223
+ files.push(...walkFiles2(full));
20224
+ else
20225
+ files.push(full);
20226
+ }
20227
+ return files;
20228
+ }
20229
+ function resolveSnapshotPath(targetDir, snapshotPath) {
20230
+ const normalizedPath = normalize3(snapshotPath);
20231
+ if (normalizedPath.startsWith("..") || normalizedPath.includes(`${sep}..${sep}`) || normalizedPath.startsWith(sep)) {
20232
+ throw new Error(`Unsafe snapshot path: ${snapshotPath}`);
20233
+ }
20234
+ if (!toPosix(normalizedPath).startsWith(".skills/")) {
20235
+ throw new Error(`Snapshot path must stay inside .skills: ${snapshotPath}`);
20236
+ }
20237
+ return join12(targetDir, normalizedPath);
20238
+ }
20239
+ function normalizeS3Prefix(prefix) {
20240
+ return (prefix ?? "").trim().replace(/^\/+|\/+$/g, "");
20241
+ }
20242
+ function toPosix(path) {
20243
+ return path.split(/[\\/]+/).join("/");
20244
+ }
20245
+ function toIsoString(value) {
20246
+ const date5 = new Date(value);
20247
+ return Number.isNaN(date5.getTime()) ? value : date5.toISOString();
20248
+ }
20249
+ function sha256Hex(value) {
20250
+ return createHash2("sha256").update(value).digest("hex");
20251
+ }
20252
+ function normalizeHeaders(headers) {
20253
+ const result = {};
20254
+ for (const [key, value] of Object.entries(headers)) {
20255
+ result[key.toLowerCase()] = String(value).trim().replace(/\s+/g, " ");
20256
+ }
20257
+ return result;
20258
+ }
20259
+ function canonicalizeQuery(params) {
20260
+ return [...params.entries()].sort(([aKey, aValue], [bKey, bValue]) => aKey.localeCompare(bKey) || aValue.localeCompare(bValue)).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
20261
+ }
20262
+ function encodeUriPath(pathname) {
20263
+ return pathname.split("/").map((segment) => encodeURIComponent(decodeURIComponent(segment)).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)).join("/");
20264
+ }
20265
+ function toAmzDate(date5) {
20266
+ return date5.toISOString().replace(/[:-]|\.\d{3}/g, "");
20267
+ }
20268
+ function getAwsSigningKey(secret, dateStamp, region, service) {
20269
+ const kDate = createHmac("sha256", `AWS4${secret}`).update(dateStamp).digest();
20270
+ const kRegion = createHmac("sha256", kDate).update(region).digest();
20271
+ const kService = createHmac("sha256", kRegion).update(service).digest();
20272
+ return createHmac("sha256", kService).update("aws4_request").digest();
20273
+ }
20274
+ function contentTypeForPath(path) {
20275
+ const lower = path.toLowerCase();
20276
+ if (lower.endsWith(".json"))
20277
+ return "application/json";
20278
+ if (lower.endsWith(".log") || lower.endsWith(".txt") || lower.endsWith(".ndjson"))
20279
+ return "text/plain";
20280
+ if (lower.endsWith(".md"))
20281
+ return "text/markdown";
20282
+ if (lower.endsWith(".png"))
20283
+ return "image/png";
20284
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg"))
20285
+ return "image/jpeg";
20286
+ if (lower.endsWith(".webp"))
20287
+ return "image/webp";
20288
+ if (lower.endsWith(".pdf"))
20289
+ return "application/pdf";
20290
+ return "application/octet-stream";
20291
+ }
20292
+ function toArrayBuffer(bytes) {
20293
+ const buffer = new ArrayBuffer(bytes.byteLength);
20294
+ new Uint8Array(buffer).set(bytes);
20295
+ return buffer;
20296
+ }
19669
20297
  export {
19670
20298
  writeRunLogs,
19671
20299
  writeRegistrySyncArtifact,
@@ -19675,11 +20303,14 @@ export {
19675
20303
  validatePortableSkillDirectory,
19676
20304
  validateCron,
19677
20305
  validateBlogArticleRunOptions,
20306
+ uploadSkillsSnapshotFilesToS3,
19678
20307
  updateSkillRun,
19679
20308
  unpinSkill,
19680
20309
  unpinProjectSkill,
19681
20310
  summarizeMcpToolContract,
20311
+ skillsPostgresSyncSchemaSql,
19682
20312
  skillExists,
20313
+ signSkillsAwsV4Request,
19683
20314
  setSkillDisabled,
19684
20315
  setScheduleEnabled,
19685
20316
  searchSkills,
@@ -19690,6 +20321,8 @@ export {
19690
20321
  sanitizePublicDiscoveryText,
19691
20322
  runSkill,
19692
20323
  runPortableSkill,
20324
+ resolveStorageConfig,
20325
+ resolveSkillsNativeStorageConfig,
19693
20326
  resolveSkillAlias,
19694
20327
  removeSkillForAgent,
19695
20328
  removeSkill,
@@ -19701,6 +20334,7 @@ export {
19701
20334
  publicDiscoveryDocumentation,
19702
20335
  publicDiscoveryDependencies,
19703
20336
  portPortableSkill,
20337
+ planSkillsS3SnapshotUpload,
19704
20338
  pinSkill,
19705
20339
  pinProjectSkill,
19706
20340
  parseSkillFrontmatter,
@@ -19729,6 +20363,16 @@ export {
19729
20363
  installSkillManifest,
19730
20364
  installSkillForAgent,
19731
20365
  installSkill,
20366
+ importSkillsLocalSnapshot,
20367
+ getStorageStatus,
20368
+ getStorageMode,
20369
+ getStorageDatabaseUrl,
20370
+ getStorageDatabaseEnv,
20371
+ getSkillsStorageStatus,
20372
+ getSkillsStorageMode,
20373
+ getSkillsStorageDatabaseUrl,
20374
+ getSkillsStorageDatabaseEnv,
20375
+ getSkillsNativeStorageStatus,
19732
20376
  getSkillsByTag,
19733
20377
  getSkillsByCategory,
19734
20378
  getSkillRunCostCents,
@@ -19770,10 +20414,14 @@ export {
19770
20414
  findSkillRun,
19771
20415
  findSimilarSkills,
19772
20416
  findPortableSkill,
20417
+ exportSkillsLocalSnapshot,
19773
20418
  ensureProjectConfig,
19774
20419
  enableSkill,
19775
20420
  disableSkill,
19776
20421
  describeMcpToolContracts,
20422
+ createSkillsSnapshotSyncRecord,
20423
+ createSkillsS3ObjectStore,
20424
+ createSkillsPostgresSyncStore,
19777
20425
  createSkillRun,
19778
20426
  createSkillMcpMetadata,
19779
20427
  createRemoteSkillsClient,
@@ -19782,11 +20430,20 @@ export {
19782
20430
  createLocalSkillManifest,
19783
20431
  completeSkillRun,
19784
20432
  clearRegistryCache,
20433
+ buildSkillsS3ObjectUrl,
19785
20434
  buildSkillsApiUrl,
19786
20435
  appendRunEvent,
19787
20436
  addSchedule,
20437
+ SkillsS3ObjectStore,
20438
+ SkillsPostgresSyncStore,
20439
+ STORAGE_TABLES,
19788
20440
  SKILL_ALIASES,
20441
+ SKILLS_STORAGE_TABLES,
20442
+ SKILLS_STORAGE_FALLBACK_ENV,
20443
+ SKILLS_STORAGE_ENV,
19789
20444
  SKILLS_PROJECT_DIR,
20445
+ SKILLS_NATIVE_STORAGE_FALLBACK_ENV,
20446
+ SKILLS_NATIVE_STORAGE_ENV,
19790
20447
  SKILLS_CLI_MCP_PARITY,
19791
20448
  SKILLS,
19792
20449
  RemoteSkillsClient,