@hasna/skills 0.1.43 → 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.
@@ -0,0 +1,854 @@
1
+ // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/lib/native-storage.ts
18
+ import { createHash, createHmac } from "crypto";
19
+ import {
20
+ existsSync as existsSync3,
21
+ mkdirSync as mkdirSync3,
22
+ readFileSync as readFileSync3,
23
+ readdirSync,
24
+ statSync,
25
+ writeFileSync as writeFileSync3
26
+ } from "fs";
27
+ import { dirname as dirname2, join as join3, normalize, relative, sep } from "path";
28
+
29
+ // src/lib/config.ts
30
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "fs";
31
+ import { join, dirname } from "path";
32
+ import { homedir } from "os";
33
+ var ENUM_KEYS = {
34
+ defaultAgent: ["claude", "codex", "gemini", "pi", "opencode", "all"],
35
+ defaultScope: ["global", "project"],
36
+ format: ["compact", "json", "csv"]
37
+ };
38
+ var STRING_KEYS = ["apiUrl"];
39
+ var MODE_VALUES = ["local", "hosted"];
40
+ var MODE_ALIASES = {
41
+ local: "local",
42
+ offline: "local",
43
+ hosted: "hosted",
44
+ remote: "hosted",
45
+ "skills.md": "hosted",
46
+ skillsmd: "hosted"
47
+ };
48
+ function validKeys() {
49
+ return ["mode", ...Object.keys(ENUM_KEYS), ...STRING_KEYS];
50
+ }
51
+ function allowedValues(key) {
52
+ if (key === "mode")
53
+ return MODE_VALUES;
54
+ return ENUM_KEYS[key];
55
+ }
56
+ function normalizeConfigValue(key, value) {
57
+ if (typeof value !== "string")
58
+ return;
59
+ if (key === "mode")
60
+ return MODE_ALIASES[value.trim().toLowerCase()];
61
+ const allowed = allowedValues(key);
62
+ if (allowed)
63
+ return allowed.includes(value) ? value : undefined;
64
+ if (key === "apiUrl") {
65
+ try {
66
+ const url = new URL(value);
67
+ if (url.protocol !== "http:" && url.protocol !== "https:")
68
+ return;
69
+ return value.replace(/\/+$/, "");
70
+ } catch {
71
+ return;
72
+ }
73
+ }
74
+ return;
75
+ }
76
+ function getDataDir() {
77
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
78
+ const newDir = join(home, ".hasna", "skills");
79
+ const oldConfigFile = join(home, ".skillsrc");
80
+ if (existsSync(oldConfigFile) && !existsSync(join(newDir, "config.json"))) {
81
+ mkdirSync(newDir, { recursive: true });
82
+ try {
83
+ copyFileSync(oldConfigFile, join(newDir, "config.json"));
84
+ } catch {}
85
+ }
86
+ mkdirSync(newDir, { recursive: true });
87
+ return newDir;
88
+ }
89
+ function getConfigPath(scope) {
90
+ if (scope === "global") {
91
+ return join(getDataDir(), "config.json");
92
+ }
93
+ return join(process.cwd(), "skills.config.json");
94
+ }
95
+ function readConfigFile(path) {
96
+ if (!existsSync(path))
97
+ return {};
98
+ try {
99
+ const raw = readFileSync(path, "utf-8");
100
+ const parsed = JSON.parse(raw);
101
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
102
+ return {};
103
+ const config = {};
104
+ for (const key of validKeys()) {
105
+ const value = normalizeConfigValue(key, parsed[key]);
106
+ if (value !== undefined)
107
+ config[key] = value;
108
+ }
109
+ return config;
110
+ } catch {
111
+ return {};
112
+ }
113
+ }
114
+ function loadConfig() {
115
+ const globalConfig = readConfigFile(getConfigPath("global"));
116
+ const projectConfig = readConfigFile(getConfigPath("project"));
117
+ return { ...globalConfig, ...projectConfig };
118
+ }
119
+ function saveConfig(key, value, scope = "project") {
120
+ if (!validKeys().includes(key)) {
121
+ throw new Error(`Unknown config key: ${key}. Valid keys: ${validKeys().join(", ")}`);
122
+ }
123
+ const normalized = normalizeConfigValue(key, value);
124
+ if (normalized === undefined) {
125
+ const allowed = allowedValues(key);
126
+ throw new Error(allowed ? `Invalid value '${value}' for ${key}. Allowed: ${allowed.join(", ")}` : `Invalid value '${value}' for ${key}. Expected an http(s) URL`);
127
+ }
128
+ const filePath = getConfigPath(scope);
129
+ let existing = {};
130
+ if (existsSync(filePath)) {
131
+ try {
132
+ existing = JSON.parse(readFileSync(filePath, "utf-8"));
133
+ if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
134
+ existing = {};
135
+ }
136
+ } catch {
137
+ existing = {};
138
+ }
139
+ } else {
140
+ const dir = dirname(filePath);
141
+ if (!existsSync(dir)) {
142
+ mkdirSync(dir, { recursive: true });
143
+ }
144
+ }
145
+ existing[key] = normalized;
146
+ writeFileSync(filePath, JSON.stringify(existing, null, 2) + `
147
+ `);
148
+ }
149
+
150
+ // src/lib/project-state.ts
151
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
152
+ import { join as join2 } from "path";
153
+
154
+ // src/lib/utils.ts
155
+ function normalizeSkillName(name) {
156
+ return name;
157
+ }
158
+
159
+ // src/lib/project-state.ts
160
+ var SKILLS_PROJECT_DIR = ".skills";
161
+ var PROJECT_CONFIG_FILE = "project.json";
162
+ var DEFAULT_EXPORT_DIR = ".skills/exports";
163
+ function getProjectStateDir(targetDir = process.cwd()) {
164
+ return join2(targetDir, SKILLS_PROJECT_DIR);
165
+ }
166
+ function getProjectConfigPath(targetDir = process.cwd()) {
167
+ return join2(getProjectStateDir(targetDir), PROJECT_CONFIG_FILE);
168
+ }
169
+ function loadProjectConfig(targetDir = process.cwd()) {
170
+ const path = getProjectConfigPath(targetDir);
171
+ if (!existsSync2(path))
172
+ return null;
173
+ try {
174
+ return normalizeProjectConfig(JSON.parse(readFileSync2(path, "utf-8")));
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
179
+ function ensureProjectConfig(targetDir = process.cwd()) {
180
+ const existing = loadProjectConfig(targetDir);
181
+ if (existing)
182
+ return existing;
183
+ const now = new Date().toISOString();
184
+ return {
185
+ version: 1,
186
+ defaultExportDir: DEFAULT_EXPORT_DIR,
187
+ pinnedSkills: [],
188
+ pins: {},
189
+ createdAt: now,
190
+ updatedAt: now
191
+ };
192
+ }
193
+ function saveProjectConfig(config, targetDir = process.cwd()) {
194
+ const dir = getProjectStateDir(targetDir);
195
+ mkdirSync2(dir, { recursive: true });
196
+ const normalized = normalizeProjectConfig({ ...config, updatedAt: new Date().toISOString() });
197
+ writeFileSync2(getProjectConfigPath(targetDir), JSON.stringify(normalized, null, 2) + `
198
+ `);
199
+ }
200
+ function pinProjectSkill(name, details = {}, targetDir = process.cwd()) {
201
+ const skillName = normalizeSkillName(name);
202
+ const config = ensureProjectConfig(targetDir);
203
+ const alreadyPinned = config.pinnedSkills.includes(skillName);
204
+ if (!alreadyPinned)
205
+ config.pinnedSkills.push(skillName);
206
+ config.pinnedSkills = [...new Set(config.pinnedSkills)].sort();
207
+ config.pins[skillName] = config.pins[skillName] ?? {
208
+ name: skillName,
209
+ pinnedAt: new Date().toISOString(),
210
+ version: details.version ?? "unknown",
211
+ source: details.source ?? "official"
212
+ };
213
+ config.pins[skillName] = {
214
+ ...config.pins[skillName],
215
+ version: details.version ?? config.pins[skillName].version ?? "unknown",
216
+ source: details.source ?? config.pins[skillName].source ?? "official"
217
+ };
218
+ saveProjectConfig(config, targetDir);
219
+ return { pinned: !alreadyPinned, config };
220
+ }
221
+ function unpinProjectSkill(name, targetDir = process.cwd()) {
222
+ const skillName = normalizeSkillName(name);
223
+ const config = loadProjectConfig(targetDir);
224
+ if (!config)
225
+ return { unpinned: false, config: null };
226
+ const before = config.pinnedSkills.length;
227
+ config.pinnedSkills = config.pinnedSkills.filter((skill) => skill !== skillName);
228
+ delete config.pins[skillName];
229
+ if (config.disabledSkills) {
230
+ config.disabledSkills = config.disabledSkills.filter((skill) => skill !== skillName);
231
+ }
232
+ const unpinned = config.pinnedSkills.length !== before;
233
+ if (unpinned)
234
+ saveProjectConfig(config, targetDir);
235
+ return { unpinned, config };
236
+ }
237
+ function listPinnedSkills(targetDir = process.cwd()) {
238
+ return loadProjectConfig(targetDir)?.pinnedSkills ?? [];
239
+ }
240
+ function setSkillDisabled(name, disabled, targetDir = process.cwd()) {
241
+ const skillName = normalizeSkillName(name);
242
+ const config = loadProjectConfig(targetDir);
243
+ if (!config?.pinnedSkills.includes(skillName))
244
+ return false;
245
+ const disabledSet = new Set(config.disabledSkills ?? []);
246
+ const changed = disabled ? !disabledSet.has(skillName) : disabledSet.has(skillName);
247
+ if (disabled)
248
+ disabledSet.add(skillName);
249
+ else
250
+ disabledSet.delete(skillName);
251
+ config.disabledSkills = [...disabledSet].sort();
252
+ if (changed)
253
+ saveProjectConfig(config, targetDir);
254
+ return changed;
255
+ }
256
+ function getDisabledProjectSkills(targetDir = process.cwd()) {
257
+ return loadProjectConfig(targetDir)?.disabledSkills ?? [];
258
+ }
259
+ function normalizeProjectConfig(raw) {
260
+ const now = new Date().toISOString();
261
+ const pinnedSkills = Array.isArray(raw.pinnedSkills) ? [...new Set(raw.pinnedSkills.map((name) => normalizeSkillName(String(name))))].sort() : [];
262
+ const pins = {};
263
+ const rawPins = raw.pins && typeof raw.pins === "object" ? raw.pins : {};
264
+ for (const name of pinnedSkills) {
265
+ const pin = rawPins[name];
266
+ pins[name] = {
267
+ name,
268
+ pinnedAt: typeof pin?.pinnedAt === "string" ? pin.pinnedAt : now,
269
+ version: typeof pin?.version === "string" ? pin.version : "unknown",
270
+ source: isPinSource(pin?.source) ? pin.source : "official"
271
+ };
272
+ }
273
+ return {
274
+ version: 1,
275
+ defaultExportDir: typeof raw.defaultExportDir === "string" ? raw.defaultExportDir : DEFAULT_EXPORT_DIR,
276
+ pinnedSkills,
277
+ pins,
278
+ disabledSkills: Array.isArray(raw.disabledSkills) ? [...new Set(raw.disabledSkills.map((name) => normalizeSkillName(String(name))))].sort() : [],
279
+ createdAt: typeof raw.createdAt === "string" ? raw.createdAt : now,
280
+ updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : now
281
+ };
282
+ }
283
+ function isPinSource(value) {
284
+ return value === "official" || value === "custom" || value === "remote" || value === "local";
285
+ }
286
+
287
+ // src/lib/native-storage.ts
288
+ var SKILLS_STORAGE_TABLES = [
289
+ "skills_sync_records",
290
+ "skills_sync_cursors"
291
+ ];
292
+ var STORAGE_TABLES = SKILLS_STORAGE_TABLES;
293
+ var SKILLS_NATIVE_STORAGE_ENV = {
294
+ mode: "HASNA_SKILLS_STORAGE_MODE",
295
+ databaseUrl: "HASNA_SKILLS_DATABASE_URL",
296
+ databaseSsl: "HASNA_SKILLS_DATABASE_SSL",
297
+ databaseSchema: "HASNA_SKILLS_DATABASE_SCHEMA",
298
+ s3Bucket: "HASNA_SKILLS_S3_BUCKET",
299
+ s3Prefix: "HASNA_SKILLS_S3_PREFIX",
300
+ awsRegion: "HASNA_SKILLS_AWS_REGION",
301
+ s3Endpoint: "HASNA_SKILLS_S3_ENDPOINT",
302
+ s3ForcePathStyle: "HASNA_SKILLS_S3_FORCE_PATH_STYLE",
303
+ s3AccessKeyId: "HASNA_SKILLS_S3_ACCESS_KEY_ID",
304
+ s3SecretAccessKey: "HASNA_SKILLS_S3_SECRET_ACCESS_KEY",
305
+ s3SessionToken: "HASNA_SKILLS_S3_SESSION_TOKEN",
306
+ syncBatchSize: "HASNA_SKILLS_SYNC_BATCH_SIZE",
307
+ dryRun: "HASNA_SKILLS_SYNC_DRY_RUN"
308
+ };
309
+ var SKILLS_NATIVE_STORAGE_FALLBACK_ENV = {
310
+ mode: "SKILLS_STORAGE_MODE",
311
+ databaseUrl: "SKILLS_DATABASE_URL",
312
+ databaseSsl: "SKILLS_DATABASE_SSL",
313
+ databaseSchema: "SKILLS_DATABASE_SCHEMA",
314
+ s3Bucket: "SKILLS_S3_BUCKET",
315
+ s3Prefix: "SKILLS_S3_PREFIX",
316
+ awsRegion: "SKILLS_AWS_REGION",
317
+ s3Endpoint: "SKILLS_S3_ENDPOINT",
318
+ s3ForcePathStyle: "SKILLS_S3_FORCE_PATH_STYLE",
319
+ s3AccessKeyId: "SKILLS_S3_ACCESS_KEY_ID",
320
+ s3SecretAccessKey: "SKILLS_S3_SECRET_ACCESS_KEY",
321
+ s3SessionToken: "SKILLS_S3_SESSION_TOKEN",
322
+ syncBatchSize: "SKILLS_SYNC_BATCH_SIZE",
323
+ dryRun: "SKILLS_SYNC_DRY_RUN"
324
+ };
325
+ var SKILLS_STORAGE_ENV = SKILLS_NATIVE_STORAGE_ENV;
326
+ var SKILLS_STORAGE_FALLBACK_ENV = SKILLS_NATIVE_STORAGE_FALLBACK_ENV;
327
+ function resolveSkillsNativeStorageConfig(env = process.env) {
328
+ const mode = getSkillsStorageMode(env);
329
+ return {
330
+ mode,
331
+ databaseUrl: getSkillsStorageDatabaseUrl(env),
332
+ databaseSsl: parseBoolean(readStorageEnv(env, "databaseSsl").value),
333
+ databaseSchema: readStorageEnv(env, "databaseSchema").value,
334
+ s3Bucket: readStorageEnv(env, "s3Bucket").value,
335
+ s3Prefix: readStorageEnv(env, "s3Prefix").value,
336
+ awsRegion: readStorageEnv(env, "awsRegion").value ?? "us-east-1",
337
+ s3Endpoint: readStorageEnv(env, "s3Endpoint").value,
338
+ s3ForcePathStyle: parseBoolean(readStorageEnv(env, "s3ForcePathStyle").value) ?? false,
339
+ syncBatchSize: parsePositiveInteger(readStorageEnv(env, "syncBatchSize").value) ?? 500,
340
+ dryRun: parseBoolean(readStorageEnv(env, "dryRun").value) ?? true
341
+ };
342
+ }
343
+ function resolveStorageConfig(env = process.env) {
344
+ return resolveSkillsNativeStorageConfig(env);
345
+ }
346
+ function getSkillsStorageMode(env = process.env) {
347
+ return parseMode(readStorageEnv(env, "mode").value);
348
+ }
349
+ function getStorageMode(env = process.env) {
350
+ return getSkillsStorageMode(env);
351
+ }
352
+ function getSkillsStorageDatabaseEnv(env = process.env) {
353
+ return readStorageEnv(env, "databaseUrl").name;
354
+ }
355
+ function getStorageDatabaseEnv(env = process.env) {
356
+ return getSkillsStorageDatabaseEnv(env);
357
+ }
358
+ function getSkillsStorageDatabaseUrl(env = process.env) {
359
+ return readStorageEnv(env, "databaseUrl").value;
360
+ }
361
+ function getStorageDatabaseUrl(env = process.env) {
362
+ return getSkillsStorageDatabaseUrl(env);
363
+ }
364
+ function getSkillsNativeStorageStatus(options = {}) {
365
+ const env = options.env ?? process.env;
366
+ const config = resolveSkillsNativeStorageConfig(env);
367
+ const modeEnv = readStorageEnv(env, "mode");
368
+ const databaseEnv = readStorageEnv(env, "databaseUrl");
369
+ const s3BucketEnv = readStorageEnv(env, "s3Bucket");
370
+ const targetDir = options.targetDir ?? process.cwd();
371
+ return {
372
+ package: "open-skills",
373
+ mode: config.mode,
374
+ tables: [...SKILLS_STORAGE_TABLES],
375
+ env: {
376
+ mode: SKILLS_NATIVE_STORAGE_ENV.mode,
377
+ databaseUrl: SKILLS_NATIVE_STORAGE_ENV.databaseUrl,
378
+ s3Bucket: SKILLS_NATIVE_STORAGE_ENV.s3Bucket
379
+ },
380
+ local: {
381
+ dataDir: getDataDir(),
382
+ projectStateDir: getProjectStateDir(targetDir),
383
+ feedbackDbPath: join3(getDataDir(), "skills.db")
384
+ },
385
+ remote: {
386
+ databaseConfigured: Boolean(config.databaseUrl),
387
+ s3Configured: Boolean(config.s3Bucket),
388
+ databaseEnv: SKILLS_NATIVE_STORAGE_ENV.databaseUrl,
389
+ s3BucketEnv: SKILLS_NATIVE_STORAGE_ENV.s3Bucket,
390
+ activeModeEnv: modeEnv.name,
391
+ activeDatabaseEnv: databaseEnv.name,
392
+ activeS3BucketEnv: s3BucketEnv.name,
393
+ region: config.awsRegion ?? "us-east-1",
394
+ dryRun: config.dryRun
395
+ }
396
+ };
397
+ }
398
+ function getSkillsStorageStatus(options = {}) {
399
+ return getSkillsNativeStorageStatus(options);
400
+ }
401
+ function getStorageStatus(options = {}) {
402
+ return getSkillsNativeStorageStatus(options);
403
+ }
404
+ function exportSkillsLocalSnapshot(targetDir = process.cwd(), options = {}) {
405
+ const projectStateDir = getProjectStateDir(targetDir);
406
+ const files = [];
407
+ if (existsSync3(projectStateDir)) {
408
+ for (const filePath of walkFiles(projectStateDir)) {
409
+ const bytes = readFileSync3(filePath);
410
+ const relativePath = toPosix(relative(targetDir, filePath));
411
+ files.push({
412
+ path: relativePath,
413
+ sizeBytes: bytes.byteLength,
414
+ sha256: createHash("sha256").update(bytes).digest("hex"),
415
+ ...options.includeFileContents ? { contentBase64: Buffer.from(bytes).toString("base64") } : {}
416
+ });
417
+ }
418
+ }
419
+ return {
420
+ schemaVersion: 1,
421
+ exportedAt: new Date().toISOString(),
422
+ files: files.sort((a, b) => a.path.localeCompare(b.path))
423
+ };
424
+ }
425
+ function importSkillsLocalSnapshot(snapshot, targetDir = process.cwd(), options = {}) {
426
+ let written = 0;
427
+ let skipped = 0;
428
+ for (const file of snapshot.files) {
429
+ if (!file.contentBase64) {
430
+ skipped += 1;
431
+ continue;
432
+ }
433
+ const absolutePath = resolveSnapshotPath(targetDir, file.path);
434
+ if (existsSync3(absolutePath) && !options.overwrite) {
435
+ skipped += 1;
436
+ continue;
437
+ }
438
+ const bytes = Buffer.from(file.contentBase64, "base64");
439
+ const hash = createHash("sha256").update(bytes).digest("hex");
440
+ if (hash !== file.sha256) {
441
+ throw new Error(`Snapshot file checksum mismatch: ${file.path}`);
442
+ }
443
+ mkdirSync3(dirname2(absolutePath), { recursive: true });
444
+ writeFileSync3(absolutePath, bytes);
445
+ written += 1;
446
+ }
447
+ return { written, skipped };
448
+ }
449
+ var skillsPostgresSyncSchemaSql = `
450
+ CREATE TABLE IF NOT EXISTS skills_sync_records (
451
+ scope TEXT NOT NULL,
452
+ kind TEXT NOT NULL,
453
+ id TEXT NOT NULL,
454
+ updated_at TIMESTAMPTZ NOT NULL,
455
+ deleted_at TIMESTAMPTZ,
456
+ source TEXT,
457
+ payload JSONB NOT NULL,
458
+ PRIMARY KEY (scope, kind, id)
459
+ );
460
+
461
+ CREATE INDEX IF NOT EXISTS skills_sync_records_updated_at_idx
462
+ ON skills_sync_records (updated_at);
463
+
464
+ CREATE TABLE IF NOT EXISTS skills_sync_cursors (
465
+ scope TEXT NOT NULL,
466
+ cursor_name TEXT NOT NULL,
467
+ value TEXT NOT NULL,
468
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
469
+ PRIMARY KEY (scope, cursor_name)
470
+ );
471
+ `.trim();
472
+
473
+ class SkillsPostgresSyncStore {
474
+ client;
475
+ constructor(client) {
476
+ this.client = client;
477
+ }
478
+ async ensureSchema() {
479
+ await this.client.query(skillsPostgresSyncSchemaSql);
480
+ }
481
+ async upsertRecords(records) {
482
+ let count = 0;
483
+ for (const record of records) {
484
+ await this.client.query([
485
+ "INSERT INTO skills_sync_records",
486
+ "(scope, kind, id, updated_at, deleted_at, source, payload)",
487
+ "VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)",
488
+ "ON CONFLICT (scope, kind, id) DO UPDATE SET",
489
+ "updated_at = EXCLUDED.updated_at,",
490
+ "deleted_at = EXCLUDED.deleted_at,",
491
+ "source = EXCLUDED.source,",
492
+ "payload = EXCLUDED.payload"
493
+ ].join(" "), [
494
+ record.scope,
495
+ record.kind,
496
+ record.id,
497
+ record.updatedAt,
498
+ record.deletedAt ?? null,
499
+ record.source ?? null,
500
+ JSON.stringify(record.payload)
501
+ ]);
502
+ count += 1;
503
+ }
504
+ return count;
505
+ }
506
+ async pullUpdatedSince(params) {
507
+ const limit = params.limit ?? 500;
508
+ const result = await this.client.query([
509
+ "SELECT scope, kind, id, updated_at, deleted_at, source, payload",
510
+ "FROM skills_sync_records",
511
+ "WHERE scope = $1 AND updated_at > $2",
512
+ "ORDER BY updated_at ASC",
513
+ "LIMIT $3"
514
+ ].join(" "), [params.scope, params.since ?? "1970-01-01T00:00:00.000Z", limit]);
515
+ return result.rows.map((row) => ({
516
+ scope: row.scope,
517
+ kind: row.kind,
518
+ id: row.id,
519
+ updatedAt: toIsoString(row.updated_at),
520
+ deletedAt: row.deleted_at ? toIsoString(row.deleted_at) : null,
521
+ source: row.source,
522
+ payload: typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload
523
+ }));
524
+ }
525
+ async getCursor(scope, cursorName) {
526
+ const result = await this.client.query("SELECT value FROM skills_sync_cursors WHERE scope = $1 AND cursor_name = $2", [scope, cursorName]);
527
+ return result.rows[0]?.value ?? null;
528
+ }
529
+ async setCursor(scope, cursorName, value) {
530
+ await this.client.query([
531
+ "INSERT INTO skills_sync_cursors (scope, cursor_name, value, updated_at)",
532
+ "VALUES ($1, $2, $3, now())",
533
+ "ON CONFLICT (scope, cursor_name) DO UPDATE SET",
534
+ "value = EXCLUDED.value, updated_at = EXCLUDED.updated_at"
535
+ ].join(" "), [scope, cursorName, value]);
536
+ }
537
+ }
538
+ function createSkillsPostgresSyncStore(client) {
539
+ return new SkillsPostgresSyncStore(client);
540
+ }
541
+ function createSkillsSnapshotSyncRecord(snapshot, options = {}) {
542
+ return {
543
+ scope: options.scope ?? "default",
544
+ kind: "local-snapshot",
545
+ id: options.id ?? "project-state",
546
+ updatedAt: snapshot.exportedAt,
547
+ source: options.source ?? "open-skills",
548
+ payload: snapshot
549
+ };
550
+ }
551
+
552
+ class SkillsS3ObjectStore {
553
+ options;
554
+ fetchImpl;
555
+ region;
556
+ prefix;
557
+ constructor(options) {
558
+ this.options = options;
559
+ this.fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
560
+ this.region = options.region ?? "us-east-1";
561
+ this.prefix = normalizeS3Prefix(options.prefix);
562
+ }
563
+ objectKey(path) {
564
+ const cleaned = toPosix(path).replace(/^\.?\//, "").replace(/^\/+/, "");
565
+ return [this.prefix, cleaned].filter(Boolean).join("/");
566
+ }
567
+ objectUrl(key) {
568
+ return buildSkillsS3ObjectUrl({
569
+ bucket: this.options.bucket,
570
+ key,
571
+ region: this.region,
572
+ endpoint: this.options.endpoint,
573
+ forcePathStyle: this.options.forcePathStyle
574
+ });
575
+ }
576
+ async putObject(params) {
577
+ const key = this.objectKey(params.key);
578
+ const body = typeof params.body === "string" ? new TextEncoder().encode(params.body) : params.body;
579
+ const url = this.objectUrl(key);
580
+ const headers = {
581
+ "content-type": params.contentType ?? "application/octet-stream",
582
+ "x-amz-content-sha256": sha256Hex(body)
583
+ };
584
+ const signed = signSkillsAwsV4Request({
585
+ method: "PUT",
586
+ url,
587
+ region: this.region,
588
+ service: "s3",
589
+ headers,
590
+ body,
591
+ credentials: this.options.credentials
592
+ });
593
+ const response = await this.fetchImpl(url, {
594
+ method: "PUT",
595
+ headers: signed.headers,
596
+ body: toArrayBuffer(body)
597
+ });
598
+ if (!response.ok) {
599
+ throw new Error(`S3 put failed for ${key}: ${response.status} ${response.statusText}`.trim());
600
+ }
601
+ return {
602
+ key,
603
+ url,
604
+ etag: response.headers.get("etag"),
605
+ sizeBytes: body.byteLength
606
+ };
607
+ }
608
+ async getObject(key) {
609
+ const objectKey = this.objectKey(key);
610
+ const url = this.objectUrl(objectKey);
611
+ const signed = signSkillsAwsV4Request({
612
+ method: "GET",
613
+ url,
614
+ region: this.region,
615
+ service: "s3",
616
+ headers: { "x-amz-content-sha256": sha256Hex(new Uint8Array) },
617
+ credentials: this.options.credentials
618
+ });
619
+ const response = await this.fetchImpl(url, {
620
+ method: "GET",
621
+ headers: signed.headers
622
+ });
623
+ if (!response.ok) {
624
+ throw new Error(`S3 get failed for ${objectKey}: ${response.status} ${response.statusText}`.trim());
625
+ }
626
+ return new Uint8Array(await response.arrayBuffer());
627
+ }
628
+ }
629
+ function createSkillsS3ObjectStore(options) {
630
+ return new SkillsS3ObjectStore(options);
631
+ }
632
+ function planSkillsS3SnapshotUpload(snapshot, options = {}) {
633
+ const prefix = normalizeS3Prefix(options.prefix);
634
+ return snapshot.files.map((file) => ({
635
+ path: file.path,
636
+ key: [prefix, file.path.replace(/^\.?\//, "")].filter(Boolean).join("/"),
637
+ sizeBytes: file.sizeBytes,
638
+ sha256: file.sha256
639
+ }));
640
+ }
641
+ async function uploadSkillsSnapshotFilesToS3(snapshot, store) {
642
+ const uploaded = [];
643
+ for (const file of snapshot.files) {
644
+ if (!file.contentBase64)
645
+ continue;
646
+ uploaded.push(await store.putObject({
647
+ key: file.path,
648
+ body: Buffer.from(file.contentBase64, "base64"),
649
+ contentType: contentTypeForPath(file.path)
650
+ }));
651
+ }
652
+ return uploaded;
653
+ }
654
+ function signSkillsAwsV4Request(options) {
655
+ const now = options.now ?? new Date;
656
+ const amzDate = toAmzDate(now);
657
+ const dateStamp = amzDate.slice(0, 8);
658
+ const url = new URL(options.url);
659
+ const bodyBytes = typeof options.body === "string" ? new TextEncoder().encode(options.body) : options.body ?? new Uint8Array;
660
+ const payloadHash = sha256Hex(bodyBytes);
661
+ const headers = normalizeHeaders({
662
+ ...options.headers ?? {},
663
+ host: url.host,
664
+ "x-amz-date": amzDate,
665
+ "x-amz-content-sha256": options.headers?.["x-amz-content-sha256"] ?? payloadHash,
666
+ ...options.credentials.sessionToken ? { "x-amz-security-token": options.credentials.sessionToken } : {}
667
+ });
668
+ const signedHeaderNames = Object.keys(headers).sort();
669
+ const canonicalHeaders = signedHeaderNames.map((name) => `${name}:${headers[name]}
670
+ `).join("");
671
+ const canonicalQuery = canonicalizeQuery(url.searchParams);
672
+ const canonicalRequest = [
673
+ options.method.toUpperCase(),
674
+ encodeUriPath(url.pathname),
675
+ canonicalQuery,
676
+ canonicalHeaders,
677
+ signedHeaderNames.join(";"),
678
+ headers["x-amz-content-sha256"]
679
+ ].join(`
680
+ `);
681
+ const credentialScope = `${dateStamp}/${options.region}/${options.service}/aws4_request`;
682
+ const stringToSign = [
683
+ "AWS4-HMAC-SHA256",
684
+ amzDate,
685
+ credentialScope,
686
+ sha256Hex(canonicalRequest)
687
+ ].join(`
688
+ `);
689
+ const signingKey = getAwsSigningKey(options.credentials.secretAccessKey, dateStamp, options.region, options.service);
690
+ const signature = createHmac("sha256", signingKey).update(stringToSign).digest("hex");
691
+ headers.authorization = [
692
+ `AWS4-HMAC-SHA256 Credential=${options.credentials.accessKeyId}/${credentialScope}`,
693
+ `SignedHeaders=${signedHeaderNames.join(";")}`,
694
+ `Signature=${signature}`
695
+ ].join(", ");
696
+ return { headers, canonicalRequest, stringToSign };
697
+ }
698
+ function buildSkillsS3ObjectUrl(params) {
699
+ const region = params.region ?? "us-east-1";
700
+ const key = params.key.split("/").map(encodeURIComponent).join("/");
701
+ if (params.endpoint) {
702
+ const endpoint = params.endpoint.replace(/\/+$/, "");
703
+ return params.forcePathStyle ? `${endpoint}/${encodeURIComponent(params.bucket)}/${key}` : `${endpoint}/${key}`;
704
+ }
705
+ return params.forcePathStyle ? `https://s3.${region}.amazonaws.com/${encodeURIComponent(params.bucket)}/${key}` : `https://${params.bucket}.s3.${region}.amazonaws.com/${key}`;
706
+ }
707
+ function parseMode(value) {
708
+ const normalized = value?.trim().toLowerCase();
709
+ if (normalized === "remote" || normalized === "hybrid")
710
+ return normalized;
711
+ return "local";
712
+ }
713
+ function readStorageEnv(env, key) {
714
+ const primaryName = SKILLS_NATIVE_STORAGE_ENV[key];
715
+ const primaryValue = cleanOptional(env[primaryName]);
716
+ if (primaryValue !== undefined)
717
+ return { name: primaryName, value: primaryValue };
718
+ const fallbackName = SKILLS_NATIVE_STORAGE_FALLBACK_ENV[key];
719
+ const fallbackValue = cleanOptional(env[fallbackName]);
720
+ if (fallbackValue !== undefined)
721
+ return { name: fallbackName, value: fallbackValue };
722
+ return { name: primaryName };
723
+ }
724
+ function cleanOptional(value) {
725
+ const cleaned = value?.trim();
726
+ return cleaned ? cleaned : undefined;
727
+ }
728
+ function parseBoolean(value) {
729
+ if (value === undefined)
730
+ return;
731
+ const normalized = value.trim().toLowerCase();
732
+ if (["1", "true", "yes", "on"].includes(normalized))
733
+ return true;
734
+ if (["0", "false", "no", "off"].includes(normalized))
735
+ return false;
736
+ return;
737
+ }
738
+ function parsePositiveInteger(value) {
739
+ if (!value)
740
+ return;
741
+ const parsed = Number.parseInt(value, 10);
742
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
743
+ }
744
+ function walkFiles(dir) {
745
+ const files = [];
746
+ for (const entry of readdirSync(dir)) {
747
+ const full = join3(dir, entry);
748
+ const stats = statSync(full);
749
+ if (stats.isDirectory())
750
+ files.push(...walkFiles(full));
751
+ else
752
+ files.push(full);
753
+ }
754
+ return files;
755
+ }
756
+ function resolveSnapshotPath(targetDir, snapshotPath) {
757
+ const normalizedPath = normalize(snapshotPath);
758
+ if (normalizedPath.startsWith("..") || normalizedPath.includes(`${sep}..${sep}`) || normalizedPath.startsWith(sep)) {
759
+ throw new Error(`Unsafe snapshot path: ${snapshotPath}`);
760
+ }
761
+ if (!toPosix(normalizedPath).startsWith(".skills/")) {
762
+ throw new Error(`Snapshot path must stay inside .skills: ${snapshotPath}`);
763
+ }
764
+ return join3(targetDir, normalizedPath);
765
+ }
766
+ function normalizeS3Prefix(prefix) {
767
+ return (prefix ?? "").trim().replace(/^\/+|\/+$/g, "");
768
+ }
769
+ function toPosix(path) {
770
+ return path.split(/[\\/]+/).join("/");
771
+ }
772
+ function toIsoString(value) {
773
+ const date = new Date(value);
774
+ return Number.isNaN(date.getTime()) ? value : date.toISOString();
775
+ }
776
+ function sha256Hex(value) {
777
+ return createHash("sha256").update(value).digest("hex");
778
+ }
779
+ function normalizeHeaders(headers) {
780
+ const result = {};
781
+ for (const [key, value] of Object.entries(headers)) {
782
+ result[key.toLowerCase()] = String(value).trim().replace(/\s+/g, " ");
783
+ }
784
+ return result;
785
+ }
786
+ function canonicalizeQuery(params) {
787
+ return [...params.entries()].sort(([aKey, aValue], [bKey, bValue]) => aKey.localeCompare(bKey) || aValue.localeCompare(bValue)).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
788
+ }
789
+ function encodeUriPath(pathname) {
790
+ return pathname.split("/").map((segment) => encodeURIComponent(decodeURIComponent(segment)).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)).join("/");
791
+ }
792
+ function toAmzDate(date) {
793
+ return date.toISOString().replace(/[:-]|\.\d{3}/g, "");
794
+ }
795
+ function getAwsSigningKey(secret, dateStamp, region, service) {
796
+ const kDate = createHmac("sha256", `AWS4${secret}`).update(dateStamp).digest();
797
+ const kRegion = createHmac("sha256", kDate).update(region).digest();
798
+ const kService = createHmac("sha256", kRegion).update(service).digest();
799
+ return createHmac("sha256", kService).update("aws4_request").digest();
800
+ }
801
+ function contentTypeForPath(path) {
802
+ const lower = path.toLowerCase();
803
+ if (lower.endsWith(".json"))
804
+ return "application/json";
805
+ if (lower.endsWith(".log") || lower.endsWith(".txt") || lower.endsWith(".ndjson"))
806
+ return "text/plain";
807
+ if (lower.endsWith(".md"))
808
+ return "text/markdown";
809
+ if (lower.endsWith(".png"))
810
+ return "image/png";
811
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg"))
812
+ return "image/jpeg";
813
+ if (lower.endsWith(".webp"))
814
+ return "image/webp";
815
+ if (lower.endsWith(".pdf"))
816
+ return "application/pdf";
817
+ return "application/octet-stream";
818
+ }
819
+ function toArrayBuffer(bytes) {
820
+ const buffer = new ArrayBuffer(bytes.byteLength);
821
+ new Uint8Array(buffer).set(bytes);
822
+ return buffer;
823
+ }
824
+ export {
825
+ uploadSkillsSnapshotFilesToS3,
826
+ skillsPostgresSyncSchemaSql,
827
+ signSkillsAwsV4Request,
828
+ resolveStorageConfig,
829
+ resolveSkillsNativeStorageConfig,
830
+ planSkillsS3SnapshotUpload,
831
+ importSkillsLocalSnapshot,
832
+ getStorageStatus,
833
+ getStorageMode,
834
+ getStorageDatabaseUrl,
835
+ getStorageDatabaseEnv,
836
+ getSkillsStorageStatus,
837
+ getSkillsStorageMode,
838
+ getSkillsStorageDatabaseUrl,
839
+ getSkillsStorageDatabaseEnv,
840
+ getSkillsNativeStorageStatus,
841
+ exportSkillsLocalSnapshot,
842
+ createSkillsSnapshotSyncRecord,
843
+ createSkillsS3ObjectStore,
844
+ createSkillsPostgresSyncStore,
845
+ buildSkillsS3ObjectUrl,
846
+ SkillsS3ObjectStore,
847
+ SkillsPostgresSyncStore,
848
+ STORAGE_TABLES,
849
+ SKILLS_STORAGE_TABLES,
850
+ SKILLS_STORAGE_FALLBACK_ENV,
851
+ SKILLS_STORAGE_ENV,
852
+ SKILLS_NATIVE_STORAGE_FALLBACK_ENV,
853
+ SKILLS_NATIVE_STORAGE_ENV
854
+ };