@hasna/skills 0.1.44 → 0.1.45
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 +36 -0
- package/bin/index.js +52503 -10651
- package/bin/mcp.js +309 -2
- package/dist/cli/commands/storage.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +624 -2
- package/dist/lib/mcp-contracts.d.ts +1 -1
- package/dist/lib/native-storage.d.ts +251 -0
- package/dist/mcp/storage-tools.d.ts +2 -0
- package/dist/storage.d.ts +1 -0
- package/dist/storage.js +854 -0
- package/package.json +8 -2
package/dist/index.js
CHANGED
|
@@ -18776,7 +18776,7 @@ import { dirname as dirname4, relative as relative3 } from "path";
|
|
|
18776
18776
|
// package.json
|
|
18777
18777
|
var package_default = {
|
|
18778
18778
|
name: "@hasna/skills",
|
|
18779
|
-
version: "0.1.
|
|
18779
|
+
version: "0.1.45",
|
|
18780
18780
|
description: "Skills library for AI coding agents",
|
|
18781
18781
|
type: "module",
|
|
18782
18782
|
bin: {
|
|
@@ -18787,6 +18787,10 @@ var package_default = {
|
|
|
18787
18787
|
".": {
|
|
18788
18788
|
import: "./dist/index.js",
|
|
18789
18789
|
types: "./dist/index.d.ts"
|
|
18790
|
+
},
|
|
18791
|
+
"./storage": {
|
|
18792
|
+
import: "./dist/storage.js",
|
|
18793
|
+
types: "./dist/storage.d.ts"
|
|
18790
18794
|
}
|
|
18791
18795
|
},
|
|
18792
18796
|
files: [
|
|
@@ -18807,7 +18811,7 @@ var package_default = {
|
|
|
18807
18811
|
types: "./dist/index.d.ts",
|
|
18808
18812
|
scripts: {
|
|
18809
18813
|
clean: "rm -rf bin/ dist/",
|
|
18810
|
-
build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun
|
|
18814
|
+
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
18815
|
test: "bun test",
|
|
18812
18816
|
dev: "bun run ./src/cli/index.tsx",
|
|
18813
18817
|
"dev:watch": "bun --watch run ./src/cli/index.tsx",
|
|
@@ -18837,9 +18841,11 @@ var package_default = {
|
|
|
18837
18841
|
devDependencies: {
|
|
18838
18842
|
"@types/bun": "latest",
|
|
18839
18843
|
"@types/react": "^18.2.0",
|
|
18844
|
+
"react-devtools-core": "^7.0.1",
|
|
18840
18845
|
typescript: "^5"
|
|
18841
18846
|
},
|
|
18842
18847
|
dependencies: {
|
|
18848
|
+
"@hasna/events": "^0.1.3",
|
|
18843
18849
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
18844
18850
|
chalk: "^5.3.0",
|
|
18845
18851
|
commander: "^12.1.0",
|
|
@@ -19314,6 +19320,46 @@ var toolContracts = [
|
|
|
19314
19320
|
inputSchema: objectSchema(),
|
|
19315
19321
|
outputSchema: objectSchema({}, [], "Setup summary.", true)
|
|
19316
19322
|
},
|
|
19323
|
+
{
|
|
19324
|
+
name: "storage_status",
|
|
19325
|
+
title: "Storage Status",
|
|
19326
|
+
description: "Show local-first storage paths and optional repo-owned Postgres/S3 readiness.",
|
|
19327
|
+
params: ["directory?"],
|
|
19328
|
+
category: "storage",
|
|
19329
|
+
sideEffects: "none",
|
|
19330
|
+
stable: true,
|
|
19331
|
+
inputSchema: objectSchema({ directory: stringSchema("Project directory.") }),
|
|
19332
|
+
outputSchema: objectSchema({
|
|
19333
|
+
package: stringSchema("Package name."),
|
|
19334
|
+
mode: { type: "string", enum: ["local", "remote", "hybrid"] },
|
|
19335
|
+
local: objectSchema({}, [], "Local storage paths.", true),
|
|
19336
|
+
remote: objectSchema({}, [], "Remote storage readiness.", true)
|
|
19337
|
+
}, ["package", "mode", "local", "remote"])
|
|
19338
|
+
},
|
|
19339
|
+
{
|
|
19340
|
+
name: "storage_sync_plan",
|
|
19341
|
+
title: "Storage Sync Plan",
|
|
19342
|
+
description: "Plan .skills snapshot sync for optional Postgres/S3 storage without network access.",
|
|
19343
|
+
params: ["directory?", "includeSchemaSql?"],
|
|
19344
|
+
category: "storage",
|
|
19345
|
+
sideEffects: "none",
|
|
19346
|
+
stable: true,
|
|
19347
|
+
inputSchema: objectSchema({
|
|
19348
|
+
directory: stringSchema("Project directory."),
|
|
19349
|
+
includeSchemaSql: { type: "boolean", default: false }
|
|
19350
|
+
}),
|
|
19351
|
+
outputSchema: objectSchema({
|
|
19352
|
+
package: stringSchema("Package name."),
|
|
19353
|
+
noNetwork: { type: "boolean", const: true },
|
|
19354
|
+
mode: { type: "string", enum: ["local", "remote", "hybrid"] },
|
|
19355
|
+
databaseConfigured: { type: "boolean" },
|
|
19356
|
+
s3Configured: { type: "boolean" },
|
|
19357
|
+
snapshotFileCount: { type: "number" },
|
|
19358
|
+
s3ObjectCount: { type: "number" },
|
|
19359
|
+
env: objectSchema({}, [], "Storage env var names.", true),
|
|
19360
|
+
schemaSql: stringSchema("Optional Postgres schema SQL.")
|
|
19361
|
+
}, ["package", "noNetwork", "mode", "databaseConfigured", "s3Configured"])
|
|
19362
|
+
},
|
|
19317
19363
|
{
|
|
19318
19364
|
name: "schedule_skill",
|
|
19319
19365
|
title: "Schedule Skill",
|
|
@@ -19666,6 +19712,553 @@ function saveFeedback(input) {
|
|
|
19666
19712
|
}
|
|
19667
19713
|
return { saved: true, category, path: getFeedbackDbPath() };
|
|
19668
19714
|
}
|
|
19715
|
+
// src/lib/native-storage.ts
|
|
19716
|
+
import { createHash as createHash2, createHmac } from "crypto";
|
|
19717
|
+
import {
|
|
19718
|
+
existsSync as existsSync12,
|
|
19719
|
+
mkdirSync as mkdirSync9,
|
|
19720
|
+
readFileSync as readFileSync11,
|
|
19721
|
+
readdirSync as readdirSync5,
|
|
19722
|
+
statSync as statSync4,
|
|
19723
|
+
writeFileSync as writeFileSync8
|
|
19724
|
+
} from "fs";
|
|
19725
|
+
import { dirname as dirname6, join as join12, normalize as normalize3, relative as relative4, sep } from "path";
|
|
19726
|
+
var SKILLS_STORAGE_TABLES = [
|
|
19727
|
+
"skills_sync_records",
|
|
19728
|
+
"skills_sync_cursors"
|
|
19729
|
+
];
|
|
19730
|
+
var STORAGE_TABLES = SKILLS_STORAGE_TABLES;
|
|
19731
|
+
var SKILLS_NATIVE_STORAGE_ENV = {
|
|
19732
|
+
mode: "HASNA_SKILLS_STORAGE_MODE",
|
|
19733
|
+
databaseUrl: "HASNA_SKILLS_DATABASE_URL",
|
|
19734
|
+
databaseSsl: "HASNA_SKILLS_DATABASE_SSL",
|
|
19735
|
+
databaseSchema: "HASNA_SKILLS_DATABASE_SCHEMA",
|
|
19736
|
+
s3Bucket: "HASNA_SKILLS_S3_BUCKET",
|
|
19737
|
+
s3Prefix: "HASNA_SKILLS_S3_PREFIX",
|
|
19738
|
+
awsRegion: "HASNA_SKILLS_AWS_REGION",
|
|
19739
|
+
s3Endpoint: "HASNA_SKILLS_S3_ENDPOINT",
|
|
19740
|
+
s3ForcePathStyle: "HASNA_SKILLS_S3_FORCE_PATH_STYLE",
|
|
19741
|
+
s3AccessKeyId: "HASNA_SKILLS_S3_ACCESS_KEY_ID",
|
|
19742
|
+
s3SecretAccessKey: "HASNA_SKILLS_S3_SECRET_ACCESS_KEY",
|
|
19743
|
+
s3SessionToken: "HASNA_SKILLS_S3_SESSION_TOKEN",
|
|
19744
|
+
syncBatchSize: "HASNA_SKILLS_SYNC_BATCH_SIZE",
|
|
19745
|
+
dryRun: "HASNA_SKILLS_SYNC_DRY_RUN"
|
|
19746
|
+
};
|
|
19747
|
+
var SKILLS_NATIVE_STORAGE_FALLBACK_ENV = {
|
|
19748
|
+
mode: "SKILLS_STORAGE_MODE",
|
|
19749
|
+
databaseUrl: "SKILLS_DATABASE_URL",
|
|
19750
|
+
databaseSsl: "SKILLS_DATABASE_SSL",
|
|
19751
|
+
databaseSchema: "SKILLS_DATABASE_SCHEMA",
|
|
19752
|
+
s3Bucket: "SKILLS_S3_BUCKET",
|
|
19753
|
+
s3Prefix: "SKILLS_S3_PREFIX",
|
|
19754
|
+
awsRegion: "SKILLS_AWS_REGION",
|
|
19755
|
+
s3Endpoint: "SKILLS_S3_ENDPOINT",
|
|
19756
|
+
s3ForcePathStyle: "SKILLS_S3_FORCE_PATH_STYLE",
|
|
19757
|
+
s3AccessKeyId: "SKILLS_S3_ACCESS_KEY_ID",
|
|
19758
|
+
s3SecretAccessKey: "SKILLS_S3_SECRET_ACCESS_KEY",
|
|
19759
|
+
s3SessionToken: "SKILLS_S3_SESSION_TOKEN",
|
|
19760
|
+
syncBatchSize: "SKILLS_SYNC_BATCH_SIZE",
|
|
19761
|
+
dryRun: "SKILLS_SYNC_DRY_RUN"
|
|
19762
|
+
};
|
|
19763
|
+
var SKILLS_STORAGE_ENV = SKILLS_NATIVE_STORAGE_ENV;
|
|
19764
|
+
var SKILLS_STORAGE_FALLBACK_ENV = SKILLS_NATIVE_STORAGE_FALLBACK_ENV;
|
|
19765
|
+
function resolveSkillsNativeStorageConfig(env = process.env) {
|
|
19766
|
+
const mode = getSkillsStorageMode(env);
|
|
19767
|
+
return {
|
|
19768
|
+
mode,
|
|
19769
|
+
databaseUrl: getSkillsStorageDatabaseUrl(env),
|
|
19770
|
+
databaseSsl: parseBoolean(readStorageEnv(env, "databaseSsl").value),
|
|
19771
|
+
databaseSchema: readStorageEnv(env, "databaseSchema").value,
|
|
19772
|
+
s3Bucket: readStorageEnv(env, "s3Bucket").value,
|
|
19773
|
+
s3Prefix: readStorageEnv(env, "s3Prefix").value,
|
|
19774
|
+
awsRegion: readStorageEnv(env, "awsRegion").value ?? "us-east-1",
|
|
19775
|
+
s3Endpoint: readStorageEnv(env, "s3Endpoint").value,
|
|
19776
|
+
s3ForcePathStyle: parseBoolean(readStorageEnv(env, "s3ForcePathStyle").value) ?? false,
|
|
19777
|
+
syncBatchSize: parsePositiveInteger(readStorageEnv(env, "syncBatchSize").value) ?? 500,
|
|
19778
|
+
dryRun: parseBoolean(readStorageEnv(env, "dryRun").value) ?? true
|
|
19779
|
+
};
|
|
19780
|
+
}
|
|
19781
|
+
function resolveStorageConfig(env = process.env) {
|
|
19782
|
+
return resolveSkillsNativeStorageConfig(env);
|
|
19783
|
+
}
|
|
19784
|
+
function getSkillsStorageMode(env = process.env) {
|
|
19785
|
+
return parseMode(readStorageEnv(env, "mode").value);
|
|
19786
|
+
}
|
|
19787
|
+
function getStorageMode(env = process.env) {
|
|
19788
|
+
return getSkillsStorageMode(env);
|
|
19789
|
+
}
|
|
19790
|
+
function getSkillsStorageDatabaseEnv(env = process.env) {
|
|
19791
|
+
return readStorageEnv(env, "databaseUrl").name;
|
|
19792
|
+
}
|
|
19793
|
+
function getStorageDatabaseEnv(env = process.env) {
|
|
19794
|
+
return getSkillsStorageDatabaseEnv(env);
|
|
19795
|
+
}
|
|
19796
|
+
function getSkillsStorageDatabaseUrl(env = process.env) {
|
|
19797
|
+
return readStorageEnv(env, "databaseUrl").value;
|
|
19798
|
+
}
|
|
19799
|
+
function getStorageDatabaseUrl(env = process.env) {
|
|
19800
|
+
return getSkillsStorageDatabaseUrl(env);
|
|
19801
|
+
}
|
|
19802
|
+
function getSkillsNativeStorageStatus(options = {}) {
|
|
19803
|
+
const env = options.env ?? process.env;
|
|
19804
|
+
const config2 = resolveSkillsNativeStorageConfig(env);
|
|
19805
|
+
const modeEnv = readStorageEnv(env, "mode");
|
|
19806
|
+
const databaseEnv = readStorageEnv(env, "databaseUrl");
|
|
19807
|
+
const s3BucketEnv = readStorageEnv(env, "s3Bucket");
|
|
19808
|
+
const targetDir = options.targetDir ?? process.cwd();
|
|
19809
|
+
return {
|
|
19810
|
+
package: "open-skills",
|
|
19811
|
+
mode: config2.mode,
|
|
19812
|
+
tables: [...SKILLS_STORAGE_TABLES],
|
|
19813
|
+
env: {
|
|
19814
|
+
mode: SKILLS_NATIVE_STORAGE_ENV.mode,
|
|
19815
|
+
databaseUrl: SKILLS_NATIVE_STORAGE_ENV.databaseUrl,
|
|
19816
|
+
s3Bucket: SKILLS_NATIVE_STORAGE_ENV.s3Bucket
|
|
19817
|
+
},
|
|
19818
|
+
local: {
|
|
19819
|
+
dataDir: getDataDir(),
|
|
19820
|
+
projectStateDir: getProjectStateDir(targetDir),
|
|
19821
|
+
feedbackDbPath: join12(getDataDir(), "skills.db")
|
|
19822
|
+
},
|
|
19823
|
+
remote: {
|
|
19824
|
+
databaseConfigured: Boolean(config2.databaseUrl),
|
|
19825
|
+
s3Configured: Boolean(config2.s3Bucket),
|
|
19826
|
+
databaseEnv: SKILLS_NATIVE_STORAGE_ENV.databaseUrl,
|
|
19827
|
+
s3BucketEnv: SKILLS_NATIVE_STORAGE_ENV.s3Bucket,
|
|
19828
|
+
activeModeEnv: modeEnv.name,
|
|
19829
|
+
activeDatabaseEnv: databaseEnv.name,
|
|
19830
|
+
activeS3BucketEnv: s3BucketEnv.name,
|
|
19831
|
+
region: config2.awsRegion ?? "us-east-1",
|
|
19832
|
+
dryRun: config2.dryRun
|
|
19833
|
+
}
|
|
19834
|
+
};
|
|
19835
|
+
}
|
|
19836
|
+
function getSkillsStorageStatus(options = {}) {
|
|
19837
|
+
return getSkillsNativeStorageStatus(options);
|
|
19838
|
+
}
|
|
19839
|
+
function getStorageStatus(options = {}) {
|
|
19840
|
+
return getSkillsNativeStorageStatus(options);
|
|
19841
|
+
}
|
|
19842
|
+
function exportSkillsLocalSnapshot(targetDir = process.cwd(), options = {}) {
|
|
19843
|
+
const projectStateDir = getProjectStateDir(targetDir);
|
|
19844
|
+
const files = [];
|
|
19845
|
+
if (existsSync12(projectStateDir)) {
|
|
19846
|
+
for (const filePath of walkFiles2(projectStateDir)) {
|
|
19847
|
+
const bytes = readFileSync11(filePath);
|
|
19848
|
+
const relativePath = toPosix(relative4(targetDir, filePath));
|
|
19849
|
+
files.push({
|
|
19850
|
+
path: relativePath,
|
|
19851
|
+
sizeBytes: bytes.byteLength,
|
|
19852
|
+
sha256: createHash2("sha256").update(bytes).digest("hex"),
|
|
19853
|
+
...options.includeFileContents ? { contentBase64: Buffer.from(bytes).toString("base64") } : {}
|
|
19854
|
+
});
|
|
19855
|
+
}
|
|
19856
|
+
}
|
|
19857
|
+
return {
|
|
19858
|
+
schemaVersion: 1,
|
|
19859
|
+
exportedAt: new Date().toISOString(),
|
|
19860
|
+
files: files.sort((a, b) => a.path.localeCompare(b.path))
|
|
19861
|
+
};
|
|
19862
|
+
}
|
|
19863
|
+
function importSkillsLocalSnapshot(snapshot, targetDir = process.cwd(), options = {}) {
|
|
19864
|
+
let written = 0;
|
|
19865
|
+
let skipped = 0;
|
|
19866
|
+
for (const file2 of snapshot.files) {
|
|
19867
|
+
if (!file2.contentBase64) {
|
|
19868
|
+
skipped += 1;
|
|
19869
|
+
continue;
|
|
19870
|
+
}
|
|
19871
|
+
const absolutePath = resolveSnapshotPath(targetDir, file2.path);
|
|
19872
|
+
if (existsSync12(absolutePath) && !options.overwrite) {
|
|
19873
|
+
skipped += 1;
|
|
19874
|
+
continue;
|
|
19875
|
+
}
|
|
19876
|
+
const bytes = Buffer.from(file2.contentBase64, "base64");
|
|
19877
|
+
const hash2 = createHash2("sha256").update(bytes).digest("hex");
|
|
19878
|
+
if (hash2 !== file2.sha256) {
|
|
19879
|
+
throw new Error(`Snapshot file checksum mismatch: ${file2.path}`);
|
|
19880
|
+
}
|
|
19881
|
+
mkdirSync9(dirname6(absolutePath), { recursive: true });
|
|
19882
|
+
writeFileSync8(absolutePath, bytes);
|
|
19883
|
+
written += 1;
|
|
19884
|
+
}
|
|
19885
|
+
return { written, skipped };
|
|
19886
|
+
}
|
|
19887
|
+
var skillsPostgresSyncSchemaSql = `
|
|
19888
|
+
CREATE TABLE IF NOT EXISTS skills_sync_records (
|
|
19889
|
+
scope TEXT NOT NULL,
|
|
19890
|
+
kind TEXT NOT NULL,
|
|
19891
|
+
id TEXT NOT NULL,
|
|
19892
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
19893
|
+
deleted_at TIMESTAMPTZ,
|
|
19894
|
+
source TEXT,
|
|
19895
|
+
payload JSONB NOT NULL,
|
|
19896
|
+
PRIMARY KEY (scope, kind, id)
|
|
19897
|
+
);
|
|
19898
|
+
|
|
19899
|
+
CREATE INDEX IF NOT EXISTS skills_sync_records_updated_at_idx
|
|
19900
|
+
ON skills_sync_records (updated_at);
|
|
19901
|
+
|
|
19902
|
+
CREATE TABLE IF NOT EXISTS skills_sync_cursors (
|
|
19903
|
+
scope TEXT NOT NULL,
|
|
19904
|
+
cursor_name TEXT NOT NULL,
|
|
19905
|
+
value TEXT NOT NULL,
|
|
19906
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
19907
|
+
PRIMARY KEY (scope, cursor_name)
|
|
19908
|
+
);
|
|
19909
|
+
`.trim();
|
|
19910
|
+
|
|
19911
|
+
class SkillsPostgresSyncStore {
|
|
19912
|
+
client;
|
|
19913
|
+
constructor(client) {
|
|
19914
|
+
this.client = client;
|
|
19915
|
+
}
|
|
19916
|
+
async ensureSchema() {
|
|
19917
|
+
await this.client.query(skillsPostgresSyncSchemaSql);
|
|
19918
|
+
}
|
|
19919
|
+
async upsertRecords(records) {
|
|
19920
|
+
let count = 0;
|
|
19921
|
+
for (const record2 of records) {
|
|
19922
|
+
await this.client.query([
|
|
19923
|
+
"INSERT INTO skills_sync_records",
|
|
19924
|
+
"(scope, kind, id, updated_at, deleted_at, source, payload)",
|
|
19925
|
+
"VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)",
|
|
19926
|
+
"ON CONFLICT (scope, kind, id) DO UPDATE SET",
|
|
19927
|
+
"updated_at = EXCLUDED.updated_at,",
|
|
19928
|
+
"deleted_at = EXCLUDED.deleted_at,",
|
|
19929
|
+
"source = EXCLUDED.source,",
|
|
19930
|
+
"payload = EXCLUDED.payload"
|
|
19931
|
+
].join(" "), [
|
|
19932
|
+
record2.scope,
|
|
19933
|
+
record2.kind,
|
|
19934
|
+
record2.id,
|
|
19935
|
+
record2.updatedAt,
|
|
19936
|
+
record2.deletedAt ?? null,
|
|
19937
|
+
record2.source ?? null,
|
|
19938
|
+
JSON.stringify(record2.payload)
|
|
19939
|
+
]);
|
|
19940
|
+
count += 1;
|
|
19941
|
+
}
|
|
19942
|
+
return count;
|
|
19943
|
+
}
|
|
19944
|
+
async pullUpdatedSince(params) {
|
|
19945
|
+
const limit = params.limit ?? 500;
|
|
19946
|
+
const result = await this.client.query([
|
|
19947
|
+
"SELECT scope, kind, id, updated_at, deleted_at, source, payload",
|
|
19948
|
+
"FROM skills_sync_records",
|
|
19949
|
+
"WHERE scope = $1 AND updated_at > $2",
|
|
19950
|
+
"ORDER BY updated_at ASC",
|
|
19951
|
+
"LIMIT $3"
|
|
19952
|
+
].join(" "), [params.scope, params.since ?? "1970-01-01T00:00:00.000Z", limit]);
|
|
19953
|
+
return result.rows.map((row) => ({
|
|
19954
|
+
scope: row.scope,
|
|
19955
|
+
kind: row.kind,
|
|
19956
|
+
id: row.id,
|
|
19957
|
+
updatedAt: toIsoString(row.updated_at),
|
|
19958
|
+
deletedAt: row.deleted_at ? toIsoString(row.deleted_at) : null,
|
|
19959
|
+
source: row.source,
|
|
19960
|
+
payload: typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload
|
|
19961
|
+
}));
|
|
19962
|
+
}
|
|
19963
|
+
async getCursor(scope, cursorName) {
|
|
19964
|
+
const result = await this.client.query("SELECT value FROM skills_sync_cursors WHERE scope = $1 AND cursor_name = $2", [scope, cursorName]);
|
|
19965
|
+
return result.rows[0]?.value ?? null;
|
|
19966
|
+
}
|
|
19967
|
+
async setCursor(scope, cursorName, value) {
|
|
19968
|
+
await this.client.query([
|
|
19969
|
+
"INSERT INTO skills_sync_cursors (scope, cursor_name, value, updated_at)",
|
|
19970
|
+
"VALUES ($1, $2, $3, now())",
|
|
19971
|
+
"ON CONFLICT (scope, cursor_name) DO UPDATE SET",
|
|
19972
|
+
"value = EXCLUDED.value, updated_at = EXCLUDED.updated_at"
|
|
19973
|
+
].join(" "), [scope, cursorName, value]);
|
|
19974
|
+
}
|
|
19975
|
+
}
|
|
19976
|
+
function createSkillsPostgresSyncStore(client) {
|
|
19977
|
+
return new SkillsPostgresSyncStore(client);
|
|
19978
|
+
}
|
|
19979
|
+
function createSkillsSnapshotSyncRecord(snapshot, options = {}) {
|
|
19980
|
+
return {
|
|
19981
|
+
scope: options.scope ?? "default",
|
|
19982
|
+
kind: "local-snapshot",
|
|
19983
|
+
id: options.id ?? "project-state",
|
|
19984
|
+
updatedAt: snapshot.exportedAt,
|
|
19985
|
+
source: options.source ?? "open-skills",
|
|
19986
|
+
payload: snapshot
|
|
19987
|
+
};
|
|
19988
|
+
}
|
|
19989
|
+
|
|
19990
|
+
class SkillsS3ObjectStore {
|
|
19991
|
+
options;
|
|
19992
|
+
fetchImpl;
|
|
19993
|
+
region;
|
|
19994
|
+
prefix;
|
|
19995
|
+
constructor(options) {
|
|
19996
|
+
this.options = options;
|
|
19997
|
+
this.fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
|
|
19998
|
+
this.region = options.region ?? "us-east-1";
|
|
19999
|
+
this.prefix = normalizeS3Prefix(options.prefix);
|
|
20000
|
+
}
|
|
20001
|
+
objectKey(path) {
|
|
20002
|
+
const cleaned = toPosix(path).replace(/^\.?\//, "").replace(/^\/+/, "");
|
|
20003
|
+
return [this.prefix, cleaned].filter(Boolean).join("/");
|
|
20004
|
+
}
|
|
20005
|
+
objectUrl(key) {
|
|
20006
|
+
return buildSkillsS3ObjectUrl({
|
|
20007
|
+
bucket: this.options.bucket,
|
|
20008
|
+
key,
|
|
20009
|
+
region: this.region,
|
|
20010
|
+
endpoint: this.options.endpoint,
|
|
20011
|
+
forcePathStyle: this.options.forcePathStyle
|
|
20012
|
+
});
|
|
20013
|
+
}
|
|
20014
|
+
async putObject(params) {
|
|
20015
|
+
const key = this.objectKey(params.key);
|
|
20016
|
+
const body = typeof params.body === "string" ? new TextEncoder().encode(params.body) : params.body;
|
|
20017
|
+
const url2 = this.objectUrl(key);
|
|
20018
|
+
const headers = {
|
|
20019
|
+
"content-type": params.contentType ?? "application/octet-stream",
|
|
20020
|
+
"x-amz-content-sha256": sha256Hex(body)
|
|
20021
|
+
};
|
|
20022
|
+
const signed = signSkillsAwsV4Request({
|
|
20023
|
+
method: "PUT",
|
|
20024
|
+
url: url2,
|
|
20025
|
+
region: this.region,
|
|
20026
|
+
service: "s3",
|
|
20027
|
+
headers,
|
|
20028
|
+
body,
|
|
20029
|
+
credentials: this.options.credentials
|
|
20030
|
+
});
|
|
20031
|
+
const response = await this.fetchImpl(url2, {
|
|
20032
|
+
method: "PUT",
|
|
20033
|
+
headers: signed.headers,
|
|
20034
|
+
body: toArrayBuffer(body)
|
|
20035
|
+
});
|
|
20036
|
+
if (!response.ok) {
|
|
20037
|
+
throw new Error(`S3 put failed for ${key}: ${response.status} ${response.statusText}`.trim());
|
|
20038
|
+
}
|
|
20039
|
+
return {
|
|
20040
|
+
key,
|
|
20041
|
+
url: url2,
|
|
20042
|
+
etag: response.headers.get("etag"),
|
|
20043
|
+
sizeBytes: body.byteLength
|
|
20044
|
+
};
|
|
20045
|
+
}
|
|
20046
|
+
async getObject(key) {
|
|
20047
|
+
const objectKey = this.objectKey(key);
|
|
20048
|
+
const url2 = this.objectUrl(objectKey);
|
|
20049
|
+
const signed = signSkillsAwsV4Request({
|
|
20050
|
+
method: "GET",
|
|
20051
|
+
url: url2,
|
|
20052
|
+
region: this.region,
|
|
20053
|
+
service: "s3",
|
|
20054
|
+
headers: { "x-amz-content-sha256": sha256Hex(new Uint8Array) },
|
|
20055
|
+
credentials: this.options.credentials
|
|
20056
|
+
});
|
|
20057
|
+
const response = await this.fetchImpl(url2, {
|
|
20058
|
+
method: "GET",
|
|
20059
|
+
headers: signed.headers
|
|
20060
|
+
});
|
|
20061
|
+
if (!response.ok) {
|
|
20062
|
+
throw new Error(`S3 get failed for ${objectKey}: ${response.status} ${response.statusText}`.trim());
|
|
20063
|
+
}
|
|
20064
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
20065
|
+
}
|
|
20066
|
+
}
|
|
20067
|
+
function createSkillsS3ObjectStore(options) {
|
|
20068
|
+
return new SkillsS3ObjectStore(options);
|
|
20069
|
+
}
|
|
20070
|
+
function planSkillsS3SnapshotUpload(snapshot, options = {}) {
|
|
20071
|
+
const prefix = normalizeS3Prefix(options.prefix);
|
|
20072
|
+
return snapshot.files.map((file2) => ({
|
|
20073
|
+
path: file2.path,
|
|
20074
|
+
key: [prefix, file2.path.replace(/^\.?\//, "")].filter(Boolean).join("/"),
|
|
20075
|
+
sizeBytes: file2.sizeBytes,
|
|
20076
|
+
sha256: file2.sha256
|
|
20077
|
+
}));
|
|
20078
|
+
}
|
|
20079
|
+
async function uploadSkillsSnapshotFilesToS3(snapshot, store) {
|
|
20080
|
+
const uploaded = [];
|
|
20081
|
+
for (const file2 of snapshot.files) {
|
|
20082
|
+
if (!file2.contentBase64)
|
|
20083
|
+
continue;
|
|
20084
|
+
uploaded.push(await store.putObject({
|
|
20085
|
+
key: file2.path,
|
|
20086
|
+
body: Buffer.from(file2.contentBase64, "base64"),
|
|
20087
|
+
contentType: contentTypeForPath(file2.path)
|
|
20088
|
+
}));
|
|
20089
|
+
}
|
|
20090
|
+
return uploaded;
|
|
20091
|
+
}
|
|
20092
|
+
function signSkillsAwsV4Request(options) {
|
|
20093
|
+
const now = options.now ?? new Date;
|
|
20094
|
+
const amzDate = toAmzDate(now);
|
|
20095
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
20096
|
+
const url2 = new URL(options.url);
|
|
20097
|
+
const bodyBytes = typeof options.body === "string" ? new TextEncoder().encode(options.body) : options.body ?? new Uint8Array;
|
|
20098
|
+
const payloadHash = sha256Hex(bodyBytes);
|
|
20099
|
+
const headers = normalizeHeaders({
|
|
20100
|
+
...options.headers ?? {},
|
|
20101
|
+
host: url2.host,
|
|
20102
|
+
"x-amz-date": amzDate,
|
|
20103
|
+
"x-amz-content-sha256": options.headers?.["x-amz-content-sha256"] ?? payloadHash,
|
|
20104
|
+
...options.credentials.sessionToken ? { "x-amz-security-token": options.credentials.sessionToken } : {}
|
|
20105
|
+
});
|
|
20106
|
+
const signedHeaderNames = Object.keys(headers).sort();
|
|
20107
|
+
const canonicalHeaders = signedHeaderNames.map((name) => `${name}:${headers[name]}
|
|
20108
|
+
`).join("");
|
|
20109
|
+
const canonicalQuery = canonicalizeQuery(url2.searchParams);
|
|
20110
|
+
const canonicalRequest = [
|
|
20111
|
+
options.method.toUpperCase(),
|
|
20112
|
+
encodeUriPath(url2.pathname),
|
|
20113
|
+
canonicalQuery,
|
|
20114
|
+
canonicalHeaders,
|
|
20115
|
+
signedHeaderNames.join(";"),
|
|
20116
|
+
headers["x-amz-content-sha256"]
|
|
20117
|
+
].join(`
|
|
20118
|
+
`);
|
|
20119
|
+
const credentialScope = `${dateStamp}/${options.region}/${options.service}/aws4_request`;
|
|
20120
|
+
const stringToSign = [
|
|
20121
|
+
"AWS4-HMAC-SHA256",
|
|
20122
|
+
amzDate,
|
|
20123
|
+
credentialScope,
|
|
20124
|
+
sha256Hex(canonicalRequest)
|
|
20125
|
+
].join(`
|
|
20126
|
+
`);
|
|
20127
|
+
const signingKey = getAwsSigningKey(options.credentials.secretAccessKey, dateStamp, options.region, options.service);
|
|
20128
|
+
const signature = createHmac("sha256", signingKey).update(stringToSign).digest("hex");
|
|
20129
|
+
headers.authorization = [
|
|
20130
|
+
`AWS4-HMAC-SHA256 Credential=${options.credentials.accessKeyId}/${credentialScope}`,
|
|
20131
|
+
`SignedHeaders=${signedHeaderNames.join(";")}`,
|
|
20132
|
+
`Signature=${signature}`
|
|
20133
|
+
].join(", ");
|
|
20134
|
+
return { headers, canonicalRequest, stringToSign };
|
|
20135
|
+
}
|
|
20136
|
+
function buildSkillsS3ObjectUrl(params) {
|
|
20137
|
+
const region = params.region ?? "us-east-1";
|
|
20138
|
+
const key = params.key.split("/").map(encodeURIComponent).join("/");
|
|
20139
|
+
if (params.endpoint) {
|
|
20140
|
+
const endpoint = params.endpoint.replace(/\/+$/, "");
|
|
20141
|
+
return params.forcePathStyle ? `${endpoint}/${encodeURIComponent(params.bucket)}/${key}` : `${endpoint}/${key}`;
|
|
20142
|
+
}
|
|
20143
|
+
return params.forcePathStyle ? `https://s3.${region}.amazonaws.com/${encodeURIComponent(params.bucket)}/${key}` : `https://${params.bucket}.s3.${region}.amazonaws.com/${key}`;
|
|
20144
|
+
}
|
|
20145
|
+
function parseMode(value) {
|
|
20146
|
+
const normalized = value?.trim().toLowerCase();
|
|
20147
|
+
if (normalized === "remote" || normalized === "hybrid")
|
|
20148
|
+
return normalized;
|
|
20149
|
+
return "local";
|
|
20150
|
+
}
|
|
20151
|
+
function readStorageEnv(env, key) {
|
|
20152
|
+
const primaryName = SKILLS_NATIVE_STORAGE_ENV[key];
|
|
20153
|
+
const primaryValue = cleanOptional(env[primaryName]);
|
|
20154
|
+
if (primaryValue !== undefined)
|
|
20155
|
+
return { name: primaryName, value: primaryValue };
|
|
20156
|
+
const fallbackName = SKILLS_NATIVE_STORAGE_FALLBACK_ENV[key];
|
|
20157
|
+
const fallbackValue = cleanOptional(env[fallbackName]);
|
|
20158
|
+
if (fallbackValue !== undefined)
|
|
20159
|
+
return { name: fallbackName, value: fallbackValue };
|
|
20160
|
+
return { name: primaryName };
|
|
20161
|
+
}
|
|
20162
|
+
function cleanOptional(value) {
|
|
20163
|
+
const cleaned = value?.trim();
|
|
20164
|
+
return cleaned ? cleaned : undefined;
|
|
20165
|
+
}
|
|
20166
|
+
function parseBoolean(value) {
|
|
20167
|
+
if (value === undefined)
|
|
20168
|
+
return;
|
|
20169
|
+
const normalized = value.trim().toLowerCase();
|
|
20170
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
20171
|
+
return true;
|
|
20172
|
+
if (["0", "false", "no", "off"].includes(normalized))
|
|
20173
|
+
return false;
|
|
20174
|
+
return;
|
|
20175
|
+
}
|
|
20176
|
+
function parsePositiveInteger(value) {
|
|
20177
|
+
if (!value)
|
|
20178
|
+
return;
|
|
20179
|
+
const parsed = Number.parseInt(value, 10);
|
|
20180
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
20181
|
+
}
|
|
20182
|
+
function walkFiles2(dir) {
|
|
20183
|
+
const files = [];
|
|
20184
|
+
for (const entry of readdirSync5(dir)) {
|
|
20185
|
+
const full = join12(dir, entry);
|
|
20186
|
+
const stats = statSync4(full);
|
|
20187
|
+
if (stats.isDirectory())
|
|
20188
|
+
files.push(...walkFiles2(full));
|
|
20189
|
+
else
|
|
20190
|
+
files.push(full);
|
|
20191
|
+
}
|
|
20192
|
+
return files;
|
|
20193
|
+
}
|
|
20194
|
+
function resolveSnapshotPath(targetDir, snapshotPath) {
|
|
20195
|
+
const normalizedPath = normalize3(snapshotPath);
|
|
20196
|
+
if (normalizedPath.startsWith("..") || normalizedPath.includes(`${sep}..${sep}`) || normalizedPath.startsWith(sep)) {
|
|
20197
|
+
throw new Error(`Unsafe snapshot path: ${snapshotPath}`);
|
|
20198
|
+
}
|
|
20199
|
+
if (!toPosix(normalizedPath).startsWith(".skills/")) {
|
|
20200
|
+
throw new Error(`Snapshot path must stay inside .skills: ${snapshotPath}`);
|
|
20201
|
+
}
|
|
20202
|
+
return join12(targetDir, normalizedPath);
|
|
20203
|
+
}
|
|
20204
|
+
function normalizeS3Prefix(prefix) {
|
|
20205
|
+
return (prefix ?? "").trim().replace(/^\/+|\/+$/g, "");
|
|
20206
|
+
}
|
|
20207
|
+
function toPosix(path) {
|
|
20208
|
+
return path.split(/[\\/]+/).join("/");
|
|
20209
|
+
}
|
|
20210
|
+
function toIsoString(value) {
|
|
20211
|
+
const date5 = new Date(value);
|
|
20212
|
+
return Number.isNaN(date5.getTime()) ? value : date5.toISOString();
|
|
20213
|
+
}
|
|
20214
|
+
function sha256Hex(value) {
|
|
20215
|
+
return createHash2("sha256").update(value).digest("hex");
|
|
20216
|
+
}
|
|
20217
|
+
function normalizeHeaders(headers) {
|
|
20218
|
+
const result = {};
|
|
20219
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
20220
|
+
result[key.toLowerCase()] = String(value).trim().replace(/\s+/g, " ");
|
|
20221
|
+
}
|
|
20222
|
+
return result;
|
|
20223
|
+
}
|
|
20224
|
+
function canonicalizeQuery(params) {
|
|
20225
|
+
return [...params.entries()].sort(([aKey, aValue], [bKey, bValue]) => aKey.localeCompare(bKey) || aValue.localeCompare(bValue)).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
|
|
20226
|
+
}
|
|
20227
|
+
function encodeUriPath(pathname) {
|
|
20228
|
+
return pathname.split("/").map((segment) => encodeURIComponent(decodeURIComponent(segment)).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)).join("/");
|
|
20229
|
+
}
|
|
20230
|
+
function toAmzDate(date5) {
|
|
20231
|
+
return date5.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
20232
|
+
}
|
|
20233
|
+
function getAwsSigningKey(secret, dateStamp, region, service) {
|
|
20234
|
+
const kDate = createHmac("sha256", `AWS4${secret}`).update(dateStamp).digest();
|
|
20235
|
+
const kRegion = createHmac("sha256", kDate).update(region).digest();
|
|
20236
|
+
const kService = createHmac("sha256", kRegion).update(service).digest();
|
|
20237
|
+
return createHmac("sha256", kService).update("aws4_request").digest();
|
|
20238
|
+
}
|
|
20239
|
+
function contentTypeForPath(path) {
|
|
20240
|
+
const lower = path.toLowerCase();
|
|
20241
|
+
if (lower.endsWith(".json"))
|
|
20242
|
+
return "application/json";
|
|
20243
|
+
if (lower.endsWith(".log") || lower.endsWith(".txt") || lower.endsWith(".ndjson"))
|
|
20244
|
+
return "text/plain";
|
|
20245
|
+
if (lower.endsWith(".md"))
|
|
20246
|
+
return "text/markdown";
|
|
20247
|
+
if (lower.endsWith(".png"))
|
|
20248
|
+
return "image/png";
|
|
20249
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg"))
|
|
20250
|
+
return "image/jpeg";
|
|
20251
|
+
if (lower.endsWith(".webp"))
|
|
20252
|
+
return "image/webp";
|
|
20253
|
+
if (lower.endsWith(".pdf"))
|
|
20254
|
+
return "application/pdf";
|
|
20255
|
+
return "application/octet-stream";
|
|
20256
|
+
}
|
|
20257
|
+
function toArrayBuffer(bytes) {
|
|
20258
|
+
const buffer = new ArrayBuffer(bytes.byteLength);
|
|
20259
|
+
new Uint8Array(buffer).set(bytes);
|
|
20260
|
+
return buffer;
|
|
20261
|
+
}
|
|
19669
20262
|
export {
|
|
19670
20263
|
writeRunLogs,
|
|
19671
20264
|
writeRegistrySyncArtifact,
|
|
@@ -19675,11 +20268,14 @@ export {
|
|
|
19675
20268
|
validatePortableSkillDirectory,
|
|
19676
20269
|
validateCron,
|
|
19677
20270
|
validateBlogArticleRunOptions,
|
|
20271
|
+
uploadSkillsSnapshotFilesToS3,
|
|
19678
20272
|
updateSkillRun,
|
|
19679
20273
|
unpinSkill,
|
|
19680
20274
|
unpinProjectSkill,
|
|
19681
20275
|
summarizeMcpToolContract,
|
|
20276
|
+
skillsPostgresSyncSchemaSql,
|
|
19682
20277
|
skillExists,
|
|
20278
|
+
signSkillsAwsV4Request,
|
|
19683
20279
|
setSkillDisabled,
|
|
19684
20280
|
setScheduleEnabled,
|
|
19685
20281
|
searchSkills,
|
|
@@ -19690,6 +20286,8 @@ export {
|
|
|
19690
20286
|
sanitizePublicDiscoveryText,
|
|
19691
20287
|
runSkill,
|
|
19692
20288
|
runPortableSkill,
|
|
20289
|
+
resolveStorageConfig,
|
|
20290
|
+
resolveSkillsNativeStorageConfig,
|
|
19693
20291
|
resolveSkillAlias,
|
|
19694
20292
|
removeSkillForAgent,
|
|
19695
20293
|
removeSkill,
|
|
@@ -19701,6 +20299,7 @@ export {
|
|
|
19701
20299
|
publicDiscoveryDocumentation,
|
|
19702
20300
|
publicDiscoveryDependencies,
|
|
19703
20301
|
portPortableSkill,
|
|
20302
|
+
planSkillsS3SnapshotUpload,
|
|
19704
20303
|
pinSkill,
|
|
19705
20304
|
pinProjectSkill,
|
|
19706
20305
|
parseSkillFrontmatter,
|
|
@@ -19729,6 +20328,16 @@ export {
|
|
|
19729
20328
|
installSkillManifest,
|
|
19730
20329
|
installSkillForAgent,
|
|
19731
20330
|
installSkill,
|
|
20331
|
+
importSkillsLocalSnapshot,
|
|
20332
|
+
getStorageStatus,
|
|
20333
|
+
getStorageMode,
|
|
20334
|
+
getStorageDatabaseUrl,
|
|
20335
|
+
getStorageDatabaseEnv,
|
|
20336
|
+
getSkillsStorageStatus,
|
|
20337
|
+
getSkillsStorageMode,
|
|
20338
|
+
getSkillsStorageDatabaseUrl,
|
|
20339
|
+
getSkillsStorageDatabaseEnv,
|
|
20340
|
+
getSkillsNativeStorageStatus,
|
|
19732
20341
|
getSkillsByTag,
|
|
19733
20342
|
getSkillsByCategory,
|
|
19734
20343
|
getSkillRunCostCents,
|
|
@@ -19770,10 +20379,14 @@ export {
|
|
|
19770
20379
|
findSkillRun,
|
|
19771
20380
|
findSimilarSkills,
|
|
19772
20381
|
findPortableSkill,
|
|
20382
|
+
exportSkillsLocalSnapshot,
|
|
19773
20383
|
ensureProjectConfig,
|
|
19774
20384
|
enableSkill,
|
|
19775
20385
|
disableSkill,
|
|
19776
20386
|
describeMcpToolContracts,
|
|
20387
|
+
createSkillsSnapshotSyncRecord,
|
|
20388
|
+
createSkillsS3ObjectStore,
|
|
20389
|
+
createSkillsPostgresSyncStore,
|
|
19777
20390
|
createSkillRun,
|
|
19778
20391
|
createSkillMcpMetadata,
|
|
19779
20392
|
createRemoteSkillsClient,
|
|
@@ -19782,11 +20395,20 @@ export {
|
|
|
19782
20395
|
createLocalSkillManifest,
|
|
19783
20396
|
completeSkillRun,
|
|
19784
20397
|
clearRegistryCache,
|
|
20398
|
+
buildSkillsS3ObjectUrl,
|
|
19785
20399
|
buildSkillsApiUrl,
|
|
19786
20400
|
appendRunEvent,
|
|
19787
20401
|
addSchedule,
|
|
20402
|
+
SkillsS3ObjectStore,
|
|
20403
|
+
SkillsPostgresSyncStore,
|
|
20404
|
+
STORAGE_TABLES,
|
|
19788
20405
|
SKILL_ALIASES,
|
|
20406
|
+
SKILLS_STORAGE_TABLES,
|
|
20407
|
+
SKILLS_STORAGE_FALLBACK_ENV,
|
|
20408
|
+
SKILLS_STORAGE_ENV,
|
|
19789
20409
|
SKILLS_PROJECT_DIR,
|
|
20410
|
+
SKILLS_NATIVE_STORAGE_FALLBACK_ENV,
|
|
20411
|
+
SKILLS_NATIVE_STORAGE_ENV,
|
|
19790
20412
|
SKILLS_CLI_MCP_PARITY,
|
|
19791
20413
|
SKILLS,
|
|
19792
20414
|
RemoteSkillsClient,
|