@hasna/instructions 0.3.0 → 0.4.0

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.
Files changed (60) hide show
  1. package/README.md +43 -12
  2. package/dashboard/README.md +73 -0
  3. package/dist/cli/index.js +1321 -1101
  4. package/dist/data/config-store.d.ts +134 -0
  5. package/dist/data/config-store.d.ts.map +1 -0
  6. package/dist/data/config-store.test.d.ts +2 -0
  7. package/dist/data/config-store.test.d.ts.map +1 -0
  8. package/dist/db/database.d.ts +15 -0
  9. package/dist/db/database.d.ts.map +1 -1
  10. package/dist/generated/storage-kit/index.d.ts +1 -1
  11. package/dist/index.d.ts +6 -8
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +896 -492
  14. package/dist/lib/apply.d.ts +2 -2
  15. package/dist/lib/apply.d.ts.map +1 -1
  16. package/dist/lib/export.d.ts +2 -2
  17. package/dist/lib/export.d.ts.map +1 -1
  18. package/dist/lib/import.d.ts +2 -2
  19. package/dist/lib/import.d.ts.map +1 -1
  20. package/dist/lib/package-manager-guard.d.ts +24 -0
  21. package/dist/lib/package-manager-guard.d.ts.map +1 -0
  22. package/dist/lib/package-manager-guard.test.d.ts +2 -0
  23. package/dist/lib/package-manager-guard.test.d.ts.map +1 -0
  24. package/dist/lib/platform-profiles.d.ts +2 -2
  25. package/dist/lib/platform-profiles.d.ts.map +1 -1
  26. package/dist/lib/project-dashboard-standard.d.ts +2 -2
  27. package/dist/lib/project-dashboard-standard.d.ts.map +1 -1
  28. package/dist/lib/redact.d.ts.map +1 -1
  29. package/dist/lib/sync-dir.d.ts +3 -3
  30. package/dist/lib/sync-dir.d.ts.map +1 -1
  31. package/dist/lib/sync.d.ts +6 -6
  32. package/dist/lib/sync.d.ts.map +1 -1
  33. package/dist/mcp/http.d.ts +0 -13
  34. package/dist/mcp/http.d.ts.map +1 -1
  35. package/dist/mcp/index.js +650 -575
  36. package/dist/mcp/server.d.ts.map +1 -1
  37. package/dist/server/index.d.ts.map +1 -1
  38. package/dist/server/index.js +1756 -17542
  39. package/dist/server/v1.d.ts.map +1 -1
  40. package/dist/status.d.ts +2 -2
  41. package/dist/status.d.ts.map +1 -1
  42. package/dist/storage/cloud-store.d.ts +21 -1
  43. package/dist/storage/cloud-store.d.ts.map +1 -1
  44. package/dist/storage/schema.d.ts.map +1 -1
  45. package/package.json +5 -8
  46. package/dist/cli/storage.d.ts +0 -3
  47. package/dist/cli/storage.d.ts.map +0 -1
  48. package/dist/cli/storage.test.d.ts +0 -2
  49. package/dist/cli/storage.test.d.ts.map +0 -1
  50. package/dist/db/remote-storage.d.ts +0 -13
  51. package/dist/db/remote-storage.d.ts.map +0 -1
  52. package/dist/db/storage-sync.d.ts +0 -53
  53. package/dist/db/storage-sync.d.ts.map +0 -1
  54. package/dist/db/storage-sync.test.d.ts +0 -2
  55. package/dist/db/storage-sync.test.d.ts.map +0 -1
  56. package/dist/server/server.test.d.ts +0 -2
  57. package/dist/server/server.test.d.ts.map +0 -1
  58. package/dist/storage.d.ts +0 -5
  59. package/dist/storage.d.ts.map +0 -1
  60. package/dist/storage.js +0 -537
package/dist/mcp/index.js CHANGED
@@ -18,17 +18,38 @@ var __export = (target, all) => {
18
18
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
19
19
  var __require = import.meta.require;
20
20
 
21
- // src/db/database.ts
22
- var exports_database = {};
23
- __export(exports_database, {
24
- uuid: () => uuid,
25
- slugify: () => slugify,
26
- resetDatabase: () => resetDatabase,
27
- now: () => now,
28
- getDatabase: () => getDatabase
21
+ // src/types/index.ts
22
+ var ConfigNotFoundError, ProfileNotFoundError, ConfigApplyError, TemplateRenderError;
23
+ var init_types = __esm(() => {
24
+ ConfigNotFoundError = class ConfigNotFoundError extends Error {
25
+ constructor(id) {
26
+ super(`Config not found: ${id}`);
27
+ this.name = "ConfigNotFoundError";
28
+ }
29
+ };
30
+ ProfileNotFoundError = class ProfileNotFoundError extends Error {
31
+ constructor(id) {
32
+ super(`Profile not found: ${id}`);
33
+ this.name = "ProfileNotFoundError";
34
+ }
35
+ };
36
+ ConfigApplyError = class ConfigApplyError extends Error {
37
+ constructor(message) {
38
+ super(message);
39
+ this.name = "ConfigApplyError";
40
+ }
41
+ };
42
+ TemplateRenderError = class TemplateRenderError extends Error {
43
+ constructor(message) {
44
+ super(message);
45
+ this.name = "TemplateRenderError";
46
+ }
47
+ };
29
48
  });
49
+
50
+ // src/db/database.ts
30
51
  import { Database } from "bun:sqlite";
31
- import { cpSync, existsSync, mkdirSync, statSync } from "fs";
52
+ import { cpSync, existsSync, mkdirSync, rmSync, statSync } from "fs";
32
53
  import { join } from "path";
33
54
  import { randomUUID } from "crypto";
34
55
  function getDbPath() {
@@ -56,6 +77,9 @@ function slugify(name) {
56
77
  function getDatabase(path) {
57
78
  if (_db)
58
79
  return _db;
80
+ if (!path && process.env["HASNA_INSTRUCTIONS_API_URL"] && process.env["HASNA_INSTRUCTIONS_API_KEY"]) {
81
+ throw new Error("instructions is in self_hosted (cloud) mode: this command is not wired to the cloud API yet. " + "Unset HASNA_INSTRUCTIONS_API_URL / HASNA_INSTRUCTIONS_API_KEY to use it against the local store.");
82
+ }
59
83
  const dbPath = path || getDbPath();
60
84
  const db = new Database(dbPath);
61
85
  db.run("PRAGMA journal_mode = WAL");
@@ -73,6 +97,16 @@ function resetDatabase() {
73
97
  }
74
98
  _db = null;
75
99
  }
100
+ function resetLocalDatabase() {
101
+ resetDatabase();
102
+ const dbPath = getDbPath();
103
+ if (dbPath === ":memory:")
104
+ return;
105
+ for (const p of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
106
+ if (existsSync(p))
107
+ rmSync(p);
108
+ }
109
+ }
76
110
  function applyMigrations(db) {
77
111
  let currentVersion = 0;
78
112
  try {
@@ -99,6 +133,10 @@ function ensureFeedbackTable(db) {
99
133
  )
100
134
  `);
101
135
  }
136
+ function insertFeedback(input, db) {
137
+ const d = db || getDatabase();
138
+ d.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [input.message, input.email ?? null, input.category ?? "general", input.version ?? null]);
139
+ }
102
140
  function migrateDotfile() {
103
141
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
104
142
  const oldDirs = [join(home, ".open-configs"), join(home, ".configs")];
@@ -189,46 +227,7 @@ var init_database = __esm(() => {
189
227
  ];
190
228
  });
191
229
 
192
- // src/types/index.ts
193
- var ConfigNotFoundError, ProfileNotFoundError, ConfigApplyError, TemplateRenderError;
194
- var init_types = __esm(() => {
195
- ConfigNotFoundError = class ConfigNotFoundError extends Error {
196
- constructor(id) {
197
- super(`Config not found: ${id}`);
198
- this.name = "ConfigNotFoundError";
199
- }
200
- };
201
- ProfileNotFoundError = class ProfileNotFoundError extends Error {
202
- constructor(id) {
203
- super(`Profile not found: ${id}`);
204
- this.name = "ProfileNotFoundError";
205
- }
206
- };
207
- ConfigApplyError = class ConfigApplyError extends Error {
208
- constructor(message) {
209
- super(message);
210
- this.name = "ConfigApplyError";
211
- }
212
- };
213
- TemplateRenderError = class TemplateRenderError extends Error {
214
- constructor(message) {
215
- super(message);
216
- this.name = "TemplateRenderError";
217
- }
218
- };
219
- });
220
-
221
230
  // src/db/configs.ts
222
- var exports_configs = {};
223
- __export(exports_configs, {
224
- updateConfig: () => updateConfig,
225
- listConfigs: () => listConfigs,
226
- getConfigStats: () => getConfigStats,
227
- getConfigById: () => getConfigById,
228
- getConfig: () => getConfig,
229
- deleteConfig: () => deleteConfig,
230
- createConfig: () => createConfig
231
- });
232
231
  function rowToConfig(row) {
233
232
  let outputs = [];
234
233
  try {
@@ -413,26 +412,6 @@ var init_configs = __esm(() => {
413
412
  init_database();
414
413
  });
415
414
 
416
- // src/db/snapshots.ts
417
- function createSnapshot(configId, content, version, db) {
418
- const d = db || getDatabase();
419
- const id = uuid();
420
- const ts = now();
421
- d.run("INSERT INTO config_snapshots (id, config_id, content, version, created_at) VALUES (?, ?, ?, ?, ?)", [id, configId, content, version, ts]);
422
- return { id, config_id: configId, content, version, created_at: ts };
423
- }
424
- function listSnapshots(configId, db) {
425
- const d = db || getDatabase();
426
- return d.query("SELECT * FROM config_snapshots WHERE config_id = ? ORDER BY version DESC").all(configId);
427
- }
428
- function getSnapshotByVersion(configId, version, db) {
429
- const d = db || getDatabase();
430
- return d.query("SELECT * FROM config_snapshots WHERE config_id = ? AND version = ?").get(configId, version);
431
- }
432
- var init_snapshots = __esm(() => {
433
- init_database();
434
- });
435
-
436
415
  // src/lib/template.ts
437
416
  var exports_template = {};
438
417
  __export(exports_template, {
@@ -588,6 +567,527 @@ var init_machine = __esm(() => {
588
567
  init_template();
589
568
  });
590
569
 
570
+ // src/db/profiles.ts
571
+ function rowToProfile(row) {
572
+ return {
573
+ ...row,
574
+ selectors: JSON.parse(row.selectors || "{}"),
575
+ variables: JSON.parse(row.variables || "{}")
576
+ };
577
+ }
578
+ function uniqueProfileSlug(name, db, excludeId) {
579
+ const base = slugify(name);
580
+ let slug = base;
581
+ let i = 1;
582
+ while (true) {
583
+ const existing = db.query("SELECT id FROM profiles WHERE slug = ?").get(slug);
584
+ if (!existing || existing.id === excludeId)
585
+ return slug;
586
+ slug = `${base}-${i++}`;
587
+ }
588
+ }
589
+ function createProfile(input, db) {
590
+ const d = db || getDatabase();
591
+ const id = uuid();
592
+ const ts = now();
593
+ const slug = uniqueProfileSlug(input.name, d);
594
+ d.run("INSERT INTO profiles (id, name, slug, description, selectors, variables, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
595
+ id,
596
+ input.name,
597
+ slug,
598
+ input.description ?? null,
599
+ JSON.stringify(input.selectors ?? {}),
600
+ JSON.stringify(input.variables ?? {}),
601
+ ts,
602
+ ts
603
+ ]);
604
+ return getProfile(id, d);
605
+ }
606
+ function getProfile(idOrSlug, db) {
607
+ const d = db || getDatabase();
608
+ const row = d.query("SELECT * FROM profiles WHERE id = ? OR slug = ?").get(idOrSlug, idOrSlug);
609
+ if (!row)
610
+ throw new ProfileNotFoundError(idOrSlug);
611
+ return rowToProfile(row);
612
+ }
613
+ function listProfiles(db) {
614
+ const d = db || getDatabase();
615
+ return d.query("SELECT * FROM profiles ORDER BY name").all().map(rowToProfile);
616
+ }
617
+ function updateProfile(idOrSlug, input, db) {
618
+ const d = db || getDatabase();
619
+ const existing = getProfile(idOrSlug, d);
620
+ const ts = now();
621
+ const updates = ["updated_at = ?"];
622
+ const params = [ts];
623
+ if (input.name !== undefined) {
624
+ updates.push("name = ?", "slug = ?");
625
+ params.push(input.name, uniqueProfileSlug(input.name, d, existing.id));
626
+ }
627
+ if (input.description !== undefined) {
628
+ updates.push("description = ?");
629
+ params.push(input.description);
630
+ }
631
+ if (input.selectors !== undefined) {
632
+ updates.push("selectors = ?");
633
+ params.push(JSON.stringify(input.selectors));
634
+ }
635
+ if (input.variables !== undefined) {
636
+ updates.push("variables = ?");
637
+ params.push(JSON.stringify(input.variables));
638
+ }
639
+ params.push(existing.id);
640
+ d.run(`UPDATE profiles SET ${updates.join(", ")} WHERE id = ?`, params);
641
+ return getProfile(existing.id, d);
642
+ }
643
+ function deleteProfile(idOrSlug, db) {
644
+ const d = db || getDatabase();
645
+ const existing = getProfile(idOrSlug, d);
646
+ d.run("DELETE FROM profiles WHERE id = ?", [existing.id]);
647
+ }
648
+ function addConfigToProfile(profileIdOrSlug, configId, db) {
649
+ const d = db || getDatabase();
650
+ const profile = getProfile(profileIdOrSlug, d);
651
+ const maxRow = d.query("SELECT MAX(sort_order) as max_order FROM profile_configs WHERE profile_id = ?").get(profile.id);
652
+ const order = (maxRow?.max_order ?? -1) + 1;
653
+ d.run("INSERT OR IGNORE INTO profile_configs (profile_id, config_id, sort_order) VALUES (?, ?, ?)", [profile.id, configId, order]);
654
+ }
655
+ function removeConfigFromProfile(profileIdOrSlug, configId, db) {
656
+ const d = db || getDatabase();
657
+ const profile = getProfile(profileIdOrSlug, d);
658
+ d.run("DELETE FROM profile_configs WHERE profile_id = ? AND config_id = ?", [profile.id, configId]);
659
+ }
660
+ function getProfileConfigs(profileIdOrSlug, db) {
661
+ const d = db || getDatabase();
662
+ const profile = getProfile(profileIdOrSlug, d);
663
+ const rows = d.query("SELECT config_id FROM profile_configs WHERE profile_id = ? ORDER BY sort_order").all(profile.id);
664
+ if (rows.length === 0)
665
+ return [];
666
+ const ids = rows.map((r) => r.config_id);
667
+ return listConfigs(undefined, d).filter((c) => ids.includes(c.id));
668
+ }
669
+ function profileHasSelectors(profile) {
670
+ const selectors = profile.selectors ?? {};
671
+ return (selectors.os?.length ?? 0) > 0 || (selectors.arch?.length ?? 0) > 0 || (selectors.hostnames?.length ?? 0) > 0;
672
+ }
673
+ function profileMatchesMachine(profile, machine) {
674
+ const selectors = profile.selectors ?? {};
675
+ const osMatches = !selectors.os?.length || selectors.os.some((candidate) => {
676
+ const value = candidate.trim().toLowerCase();
677
+ return value === machine.os_family || value === (machine.os ?? "").trim().toLowerCase() || normalizeOsFamily(candidate) === machine.os_family;
678
+ });
679
+ const archMatches = !selectors.arch?.length || selectors.arch.some((candidate) => candidate.trim().toLowerCase() === (machine.arch ?? "").trim().toLowerCase());
680
+ const hostnameMatches = !selectors.hostnames?.length || selectors.hostnames.some((candidate) => candidate.trim().toLowerCase() === machine.hostname.trim().toLowerCase());
681
+ return osMatches && archMatches && hostnameMatches;
682
+ }
683
+ function resolveProfileForMachine(machine = detectMachineContext(), db) {
684
+ const profiles = listProfiles(db).filter(profileHasSelectors);
685
+ const matches = profiles.filter((profile) => profileMatchesMachine(profile, machine)).map((profile) => {
686
+ const selectors = profile.selectors;
687
+ const score = (selectors.hostnames?.length ? 100 : 0) + (selectors.os?.length ? 10 : 0) + (selectors.arch?.length ? 10 : 0);
688
+ return { profile, score };
689
+ }).sort((a, b) => b.score - a.score || a.profile.name.localeCompare(b.profile.name));
690
+ return matches[0]?.profile ?? null;
691
+ }
692
+ var init_profiles = __esm(() => {
693
+ init_types();
694
+ init_database();
695
+ init_configs();
696
+ init_machine();
697
+ });
698
+
699
+ // src/db/snapshots.ts
700
+ function createSnapshot(configId, content, version, db) {
701
+ const d = db || getDatabase();
702
+ const id = uuid();
703
+ const ts = now();
704
+ d.run("INSERT INTO config_snapshots (id, config_id, content, version, created_at) VALUES (?, ?, ?, ?, ?)", [id, configId, content, version, ts]);
705
+ return { id, config_id: configId, content, version, created_at: ts };
706
+ }
707
+ function listSnapshots(configId, db) {
708
+ const d = db || getDatabase();
709
+ return d.query("SELECT * FROM config_snapshots WHERE config_id = ? ORDER BY version DESC").all(configId);
710
+ }
711
+ function getSnapshot(id, db) {
712
+ const d = db || getDatabase();
713
+ return d.query("SELECT * FROM config_snapshots WHERE id = ?").get(id);
714
+ }
715
+ function getSnapshotByVersion(configId, version, db) {
716
+ const d = db || getDatabase();
717
+ return d.query("SELECT * FROM config_snapshots WHERE config_id = ? AND version = ?").get(configId, version);
718
+ }
719
+ function pruneSnapshots(configId, keep = 10, db) {
720
+ const d = db || getDatabase();
721
+ const result = d.run(`DELETE FROM config_snapshots WHERE config_id = ? AND id NOT IN (
722
+ SELECT id FROM config_snapshots WHERE config_id = ? ORDER BY version DESC LIMIT ?
723
+ )`, [configId, configId, keep]);
724
+ return result.changes;
725
+ }
726
+ var init_snapshots = __esm(() => {
727
+ init_database();
728
+ });
729
+
730
+ // src/db/machines.ts
731
+ import { arch, hostname, type } from "os";
732
+ function currentHostname2() {
733
+ return hostname();
734
+ }
735
+ function currentOs() {
736
+ return type();
737
+ }
738
+ function currentArch2() {
739
+ return arch();
740
+ }
741
+ function registerMachine(hostnameStr, os, archStr, db) {
742
+ const d = db || getDatabase();
743
+ const h = hostnameStr ?? currentHostname2();
744
+ const o = os ?? currentOs();
745
+ const a = archStr ?? currentArch2();
746
+ const existing = d.query("SELECT * FROM machines WHERE hostname = ?").get(h);
747
+ if (existing) {
748
+ if (existing.os !== o || existing.arch !== a) {
749
+ d.run("UPDATE machines SET os = ?, arch = ? WHERE hostname = ?", [o, a, h]);
750
+ return d.query("SELECT * FROM machines WHERE hostname = ?").get(h);
751
+ }
752
+ return existing;
753
+ }
754
+ const id = uuid();
755
+ const ts = now();
756
+ d.run("INSERT INTO machines (id, hostname, os, arch, last_applied_at, created_at) VALUES (?, ?, ?, ?, NULL, ?)", [id, h, o, a, ts]);
757
+ return d.query("SELECT * FROM machines WHERE id = ?").get(id);
758
+ }
759
+ function updateMachineApplied(hostnameStr, db) {
760
+ const d = db || getDatabase();
761
+ const h = hostnameStr ?? currentHostname2();
762
+ d.run("UPDATE machines SET last_applied_at = ? WHERE hostname = ?", [now(), h]);
763
+ }
764
+ function listMachines(db) {
765
+ const d = db || getDatabase();
766
+ return d.query("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST").all();
767
+ }
768
+ var init_machines = __esm(() => {
769
+ init_database();
770
+ });
771
+
772
+ // src/data/config-store.ts
773
+ import { randomUUID as randomUUID2 } from "crypto";
774
+ function resolveCloudConfig(env = process.env) {
775
+ const apiUrl = env[API_URL_ENV]?.trim();
776
+ const apiKey = env[API_KEY_ENV]?.trim();
777
+ if (!apiUrl && !apiKey)
778
+ return null;
779
+ if (!apiUrl || !apiKey) {
780
+ throw new Error(`API mode requires BOTH ${API_URL_ENV} and ${API_KEY_ENV}; only ` + `${apiUrl ? API_URL_ENV : API_KEY_ENV} is set. Set both to use the cloud API, ` + `or unset both to use the local store.`);
781
+ }
782
+ return { apiUrl, apiKey };
783
+ }
784
+
785
+ class LocalConfigStore {
786
+ db;
787
+ mode = "local";
788
+ constructor(db) {
789
+ this.db = db;
790
+ }
791
+ async listConfigs(filter) {
792
+ return listConfigs(filter, this.db);
793
+ }
794
+ async getConfig(idOrSlug) {
795
+ return getConfig(idOrSlug, this.db);
796
+ }
797
+ async getConfigById(id) {
798
+ return getConfigById(id, this.db);
799
+ }
800
+ async createConfig(input) {
801
+ return createConfig(input, this.db);
802
+ }
803
+ async updateConfig(idOrSlug, input) {
804
+ return updateConfig(idOrSlug, input, this.db);
805
+ }
806
+ async deleteConfig(idOrSlug) {
807
+ deleteConfig(idOrSlug, this.db);
808
+ }
809
+ async getConfigStats() {
810
+ return getConfigStats(this.db);
811
+ }
812
+ async listSnapshots(configId) {
813
+ return listSnapshots(configId, this.db);
814
+ }
815
+ async getSnapshot(id) {
816
+ return getSnapshot(id, this.db);
817
+ }
818
+ async getSnapshotByVersion(configId, version) {
819
+ return getSnapshotByVersion(configId, version, this.db);
820
+ }
821
+ async createSnapshot(configId, content, version) {
822
+ return createSnapshot(configId, content, version, this.db);
823
+ }
824
+ async pruneSnapshots(configId, keep = 10) {
825
+ return pruneSnapshots(configId, keep, this.db);
826
+ }
827
+ async listProfiles() {
828
+ return listProfiles(this.db);
829
+ }
830
+ async getProfile(idOrSlug) {
831
+ return getProfile(idOrSlug, this.db);
832
+ }
833
+ async getProfileConfigs(idOrSlug) {
834
+ return getProfileConfigs(idOrSlug, this.db);
835
+ }
836
+ async createProfile(input) {
837
+ return createProfile(input, this.db);
838
+ }
839
+ async updateProfile(idOrSlug, input) {
840
+ return updateProfile(idOrSlug, input, this.db);
841
+ }
842
+ async deleteProfile(idOrSlug) {
843
+ deleteProfile(idOrSlug, this.db);
844
+ }
845
+ async addConfigToProfile(profileIdOrSlug, configId) {
846
+ addConfigToProfile(profileIdOrSlug, configId, this.db);
847
+ }
848
+ async removeConfigFromProfile(profileIdOrSlug, configId) {
849
+ removeConfigFromProfile(profileIdOrSlug, configId, this.db);
850
+ }
851
+ async resolveProfileForMachine(machine) {
852
+ return machine ? resolveProfileForMachine(machine, this.db) : resolveProfileForMachine(undefined, this.db);
853
+ }
854
+ async registerMachine(hostname2, os, arch2) {
855
+ return registerMachine(hostname2, os, arch2, this.db);
856
+ }
857
+ async updateMachineApplied(hostname2) {
858
+ updateMachineApplied(hostname2, this.db);
859
+ }
860
+ async listMachines() {
861
+ return listMachines(this.db);
862
+ }
863
+ async sendFeedback(input) {
864
+ insertFeedback(input, this.db);
865
+ }
866
+ async reset() {
867
+ resetLocalDatabase();
868
+ }
869
+ }
870
+
871
+ class CloudConfigStore {
872
+ mode = "api";
873
+ base;
874
+ apiKey;
875
+ timeoutMs;
876
+ constructor(config) {
877
+ this.base = `${config.apiUrl.replace(/\/+$/, "")}/v1`;
878
+ this.apiKey = config.apiKey;
879
+ this.timeoutMs = config.timeoutMs ?? 30000;
880
+ }
881
+ async request(method, path, body, opts = {}) {
882
+ const controller = new AbortController;
883
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
884
+ const headers = {
885
+ Authorization: `Bearer ${this.apiKey}`,
886
+ Accept: "application/json"
887
+ };
888
+ if (body !== undefined)
889
+ headers["Content-Type"] = "application/json";
890
+ if (opts.idempotent)
891
+ headers["Idempotency-Key"] = randomUUID2();
892
+ try {
893
+ const res = await fetch(`${this.base}${path}`, {
894
+ method,
895
+ headers,
896
+ body: body === undefined ? undefined : JSON.stringify(body),
897
+ signal: controller.signal
898
+ });
899
+ if (res.status === 404 && opts.allow404)
900
+ return { status: 404, data: null };
901
+ const text = await res.text();
902
+ let parsed = null;
903
+ if (text) {
904
+ try {
905
+ parsed = JSON.parse(text);
906
+ } catch {
907
+ parsed = text;
908
+ }
909
+ }
910
+ if (!res.ok) {
911
+ const message = (parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : undefined) ?? `HTTP ${res.status} on ${method} ${path}`;
912
+ throw new CloudHttpError(res.status, message, parsed);
913
+ }
914
+ return { status: res.status, data: parsed };
915
+ } finally {
916
+ clearTimeout(timer);
917
+ }
918
+ }
919
+ async listConfigs(filter = {}) {
920
+ const params = new URLSearchParams;
921
+ if (filter.category)
922
+ params.set("category", filter.category);
923
+ if (filter.agent)
924
+ params.set("agent", filter.agent);
925
+ if (filter.kind)
926
+ params.set("kind", filter.kind);
927
+ if (filter.search)
928
+ params.set("search", filter.search);
929
+ const qs = params.toString();
930
+ const { data } = await this.request("GET", `/configs${qs ? `?${qs}` : ""}`);
931
+ let configs = data?.configs ?? [];
932
+ if (filter.tags && filter.tags.length > 0) {
933
+ configs = configs.filter((c) => filter.tags.every((t) => c.tags.includes(t)));
934
+ }
935
+ if (filter.is_template !== undefined) {
936
+ configs = configs.filter((c) => c.is_template === filter.is_template);
937
+ }
938
+ return configs;
939
+ }
940
+ async getConfig(idOrSlug) {
941
+ const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
942
+ if (status === 404 || !data?.config)
943
+ throw new ConfigNotFoundError(idOrSlug);
944
+ return data.config;
945
+ }
946
+ async getConfigById(id) {
947
+ return this.getConfig(id);
948
+ }
949
+ async createConfig(input) {
950
+ const { data } = await this.request("POST", "/configs", input, {
951
+ idempotent: true
952
+ });
953
+ return data.config;
954
+ }
955
+ async updateConfig(idOrSlug, input) {
956
+ const { data } = await this.request("PATCH", `/configs/${encodeURIComponent(idOrSlug)}`, input);
957
+ return data.config;
958
+ }
959
+ async deleteConfig(idOrSlug) {
960
+ const { status } = await this.request("DELETE", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
961
+ if (status === 404)
962
+ throw new ConfigNotFoundError(idOrSlug);
963
+ }
964
+ async getConfigStats() {
965
+ const { data } = await this.request("GET", "/stats");
966
+ return data ?? { total: 0 };
967
+ }
968
+ async listSnapshots(configId) {
969
+ const { data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots`);
970
+ return data?.snapshots ?? [];
971
+ }
972
+ async getSnapshot(id) {
973
+ const { status, data } = await this.request("GET", `/snapshots/${encodeURIComponent(id)}`, undefined, { allow404: true });
974
+ if (status === 404 || !data?.snapshot)
975
+ return null;
976
+ return data.snapshot;
977
+ }
978
+ async getSnapshotByVersion(configId, version) {
979
+ const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots/${version}`, undefined, { allow404: true });
980
+ if (status === 404 || !data?.snapshot)
981
+ return null;
982
+ return data.snapshot;
983
+ }
984
+ async createSnapshot(configId, content, version) {
985
+ const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots`, { content, version }, { idempotent: true });
986
+ return data.snapshot;
987
+ }
988
+ async pruneSnapshots(configId, keep = 10) {
989
+ const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots/prune`, { keep });
990
+ return data?.pruned ?? 0;
991
+ }
992
+ async listProfiles() {
993
+ const { data } = await this.request("GET", "/profiles");
994
+ return data?.profiles ?? [];
995
+ }
996
+ async getProfile(idOrSlug) {
997
+ const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
998
+ if (status === 404 || !data?.profile)
999
+ throw new ProfileNotFoundError(idOrSlug);
1000
+ const { configs: _configs, ...profile } = data.profile;
1001
+ return profile;
1002
+ }
1003
+ async getProfileConfigs(idOrSlug) {
1004
+ const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
1005
+ if (status === 404 || !data?.profile)
1006
+ throw new ProfileNotFoundError(idOrSlug);
1007
+ return data.profile.configs ?? [];
1008
+ }
1009
+ async createProfile(input) {
1010
+ const { data } = await this.request("POST", "/profiles", input, {
1011
+ idempotent: true
1012
+ });
1013
+ return data.profile;
1014
+ }
1015
+ async updateProfile(idOrSlug, input) {
1016
+ const { data } = await this.request("PATCH", `/profiles/${encodeURIComponent(idOrSlug)}`, input);
1017
+ return data.profile;
1018
+ }
1019
+ async deleteProfile(idOrSlug) {
1020
+ const { status } = await this.request("DELETE", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
1021
+ if (status === 404)
1022
+ throw new ProfileNotFoundError(idOrSlug);
1023
+ }
1024
+ async addConfigToProfile(profileIdOrSlug, configId) {
1025
+ await this.request("POST", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs`, { config_id: configId }, { idempotent: true });
1026
+ }
1027
+ async removeConfigFromProfile(profileIdOrSlug, configId) {
1028
+ await this.request("DELETE", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs/${encodeURIComponent(configId)}`, undefined, { allow404: true });
1029
+ }
1030
+ async resolveProfileForMachine(machine) {
1031
+ const params = new URLSearchParams;
1032
+ if (machine?.hostname)
1033
+ params.set("hostname", machine.hostname);
1034
+ if (machine?.os)
1035
+ params.set("os", machine.os);
1036
+ if (machine?.arch)
1037
+ params.set("arch", machine.arch);
1038
+ const qs = params.toString();
1039
+ const { status, data } = await this.request("GET", `/profiles/resolve${qs ? `?${qs}` : ""}`, undefined, { allow404: true });
1040
+ if (status === 404 || !data?.profile)
1041
+ return null;
1042
+ return data.profile;
1043
+ }
1044
+ async registerMachine(hostname2, os, arch2) {
1045
+ const { data } = await this.request("POST", "/machines", { hostname: hostname2, os, arch: arch2 }, { idempotent: true });
1046
+ return data.machine;
1047
+ }
1048
+ async updateMachineApplied(hostname2) {
1049
+ await this.request("POST", "/machines/applied", { hostname: hostname2 });
1050
+ }
1051
+ async listMachines() {
1052
+ const { data } = await this.request("GET", "/machines");
1053
+ return data?.machines ?? [];
1054
+ }
1055
+ async sendFeedback(input) {
1056
+ await this.request("POST", "/feedback", {
1057
+ message: input.message,
1058
+ email: input.email ?? undefined,
1059
+ category: input.category ?? undefined,
1060
+ version: input.version ?? undefined
1061
+ });
1062
+ }
1063
+ async reset() {
1064
+ throw new Error("`init --force` cannot wipe the shared cloud store from a client. " + "Unset HASNA_INSTRUCTIONS_API_URL / HASNA_INSTRUCTIONS_API_KEY to reset the local store instead.");
1065
+ }
1066
+ }
1067
+ function resolveConfigStore(env = process.env) {
1068
+ const cloud = resolveCloudConfig(env);
1069
+ return cloud ? new CloudConfigStore(cloud) : new LocalConfigStore;
1070
+ }
1071
+ var CloudHttpError, API_URL_ENV = "HASNA_INSTRUCTIONS_API_URL", API_KEY_ENV = "HASNA_INSTRUCTIONS_API_KEY";
1072
+ var init_config_store = __esm(() => {
1073
+ init_configs();
1074
+ init_profiles();
1075
+ init_snapshots();
1076
+ init_machines();
1077
+ init_database();
1078
+ init_types();
1079
+ CloudHttpError = class CloudHttpError extends Error {
1080
+ status;
1081
+ body;
1082
+ constructor(status, message, body) {
1083
+ super(message);
1084
+ this.status = status;
1085
+ this.body = body;
1086
+ this.name = "CloudHttpError";
1087
+ }
1088
+ };
1089
+ });
1090
+
591
1091
  // src/lib/transforms.ts
592
1092
  import { basename, extname } from "path";
593
1093
  function ensureTrailingNewline(content) {
@@ -755,8 +1255,8 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
755
1255
  mkdirSync2(dir, { recursive: true });
756
1256
  }
757
1257
  if (previousContent !== null && changed) {
758
- const db = opts.db || getDatabase();
759
- createSnapshot(config.id, previousContent, config.version, db);
1258
+ const store = opts.store ?? resolveConfigStore();
1259
+ await store.createSnapshot(config.id, previousContent, config.version);
760
1260
  }
761
1261
  writeFileSync(path, renderedContent, "utf-8");
762
1262
  }
@@ -782,8 +1282,8 @@ async function applyConfig(config, opts = {}) {
782
1282
  if (config.kind === "reference" || (!config.target_path || !shouldApplyPrimary) && selectedOutputs.length === 0) {
783
1283
  throw new ConfigApplyError(`Config "${config.name}" is a reference (kind=reference) and has no target_path \u2014 cannot apply to disk.`);
784
1284
  }
785
- const db = opts.db || getDatabase();
786
- const contextConfigs = selectedOutputs.length > 0 || config.target_path ? listConfigs(undefined, db) : [config];
1285
+ const store = opts.store ?? resolveConfigStore();
1286
+ const contextConfigs = selectedOutputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
787
1287
  if (isGeneratedOutputTarget(config, contextConfigs)) {
788
1288
  throw new ConfigApplyError(`Config "${config.name}" targets a generated output path. Apply the canonical source config instead.`);
789
1289
  }
@@ -804,7 +1304,7 @@ async function applyConfig(config, opts = {}) {
804
1304
  };
805
1305
  }
806
1306
  if (!opts.dryRun) {
807
- updateConfig(config.id, { synced_at: now() }, db);
1307
+ await store.updateConfig(config.id, { synced_at: new Date().toISOString() });
808
1308
  }
809
1309
  return result;
810
1310
  }
@@ -826,9 +1326,7 @@ async function applyConfigs(configs, opts = {}) {
826
1326
  }
827
1327
  var init_apply = __esm(() => {
828
1328
  init_types();
829
- init_database();
830
- init_configs();
831
- init_snapshots();
1329
+ init_config_store();
832
1330
  init_machine();
833
1331
  init_transforms();
834
1332
  });
@@ -922,9 +1420,9 @@ function redactIni(content) {
922
1420
  for (let i = 0;i < lines.length; i++) {
923
1421
  const line = lines[i];
924
1422
  const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
925
- if (authM && !authM[2].startsWith("{{")) {
926
- redacted.push({ varName: "NPM_AUTH_TOKEN", line: i + 1, reason: "npm auth token" });
927
- out.push(`${authM[1]}{{NPM_AUTH_TOKEN}}`);
1423
+ if (authM && !isReferenceValue(authM[2].trim())) {
1424
+ redacted.push({ varName: "NPM_TOKEN", line: i + 1, reason: "npm auth token" });
1425
+ out.push(`${authM[1]}\${NPM_TOKEN}`);
928
1426
  continue;
929
1427
  }
930
1428
  const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
@@ -964,6 +1462,8 @@ function redactGeneric(content) {
964
1462
  function shouldRedactKeyValue(key, value) {
965
1463
  if (!value || value.startsWith("{{"))
966
1464
  return false;
1465
+ if (isReferenceValue(value.trim()))
1466
+ return false;
967
1467
  if (value.length < MIN_SECRET_VALUE_LEN)
968
1468
  return false;
969
1469
  if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
@@ -985,6 +1485,9 @@ function reasonFor(key, value) {
985
1485
  }
986
1486
  return "secret value pattern";
987
1487
  }
1488
+ function isReferenceValue(value) {
1489
+ return /^\{\{[A-Z][A-Z0-9_]*\}\}$/.test(value) || /^\$\{[A-Z][A-Z0-9_]*\}$/.test(value) || /^\$[A-Z][A-Z0-9_]*$/.test(value) || /^%[A-Z][A-Z0-9_]*%$/.test(value);
1490
+ }
988
1491
  function redactContent(content, format) {
989
1492
  switch (format) {
990
1493
  case "shell":
@@ -1088,11 +1591,11 @@ function isKnownGeneratedTargetPath(targetPath) {
1088
1591
  return hasClaudeRuleSourceForCursorTarget(targetPath);
1089
1592
  }
1090
1593
  async function syncProject(opts) {
1091
- const d = opts.db || getDatabase();
1594
+ const store = opts.store ?? resolveConfigStore();
1092
1595
  const absDir = expandPath(opts.projectDir);
1093
1596
  const projectName = absDir.split("/").pop() || "project";
1094
1597
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
1095
- const allConfigs = listConfigs(undefined, d);
1598
+ const allConfigs = await store.listConfigs();
1096
1599
  const machine = detectMachineContext();
1097
1600
  for (const pf of PROJECT_CONFIG_FILES) {
1098
1601
  const abs = join3(absDir, pf.file);
@@ -1114,11 +1617,11 @@ async function syncProject(opts) {
1114
1617
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
1115
1618
  if (!existing) {
1116
1619
  if (!opts.dryRun)
1117
- createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 }, d);
1620
+ await store.createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 });
1118
1621
  result.added++;
1119
1622
  } else if (existing.content !== content) {
1120
1623
  if (!opts.dryRun)
1121
- updateConfig(existing.id, { content, is_template: isTemplate2 }, d);
1624
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
1122
1625
  result.updated++;
1123
1626
  } else {
1124
1627
  result.unchanged++;
@@ -1143,11 +1646,11 @@ async function syncProject(opts) {
1143
1646
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
1144
1647
  if (!existing) {
1145
1648
  if (!opts.dryRun)
1146
- createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 }, d);
1649
+ await store.createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 });
1147
1650
  result.added++;
1148
1651
  } else if (existing.content !== content) {
1149
1652
  if (!opts.dryRun)
1150
- updateConfig(existing.id, { content, is_template: isTemplate2 }, d);
1653
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
1151
1654
  result.updated++;
1152
1655
  } else {
1153
1656
  result.unchanged++;
@@ -1157,7 +1660,7 @@ async function syncProject(opts) {
1157
1660
  return result;
1158
1661
  }
1159
1662
  async function syncKnown(opts = {}) {
1160
- const d = opts.db || getDatabase();
1663
+ const store = opts.store ?? resolveConfigStore();
1161
1664
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
1162
1665
  const home = getConfigHome();
1163
1666
  const machine = detectMachineContext();
@@ -1166,7 +1669,7 @@ async function syncKnown(opts = {}) {
1166
1669
  targets = targets.filter((k) => k.agent === opts.agent);
1167
1670
  if (opts.category)
1168
1671
  targets = targets.filter((k) => k.category === opts.category);
1169
- const allConfigs = listConfigs(undefined, d);
1672
+ const allConfigs = await store.listConfigs();
1170
1673
  const existingOutputOwners = outputOwnerIdsByTarget(allConfigs);
1171
1674
  for (const known of targets) {
1172
1675
  if (known.rulesDir) {
@@ -1195,15 +1698,15 @@ async function syncKnown(opts = {}) {
1195
1698
  const outputs = known.agent === "claude" ? claudeRuleOutputs(f) : known.outputs;
1196
1699
  if (!existing) {
1197
1700
  if (!opts.dryRun)
1198
- createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs }, d);
1701
+ await store.createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs });
1199
1702
  result.added++;
1200
1703
  } else if (existing.content !== content) {
1201
1704
  if (!opts.dryRun)
1202
- updateConfig(existing.id, { content, is_template: isTemplate2, outputs }, d);
1705
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs });
1203
1706
  result.updated++;
1204
1707
  } else if (!outputsEqual(existing.outputs, outputs)) {
1205
1708
  if (!opts.dryRun)
1206
- updateConfig(existing.id, { outputs }, d);
1709
+ await store.updateConfig(existing.id, { outputs });
1207
1710
  result.updated++;
1208
1711
  } else {
1209
1712
  result.unchanged++;
@@ -1235,7 +1738,7 @@ async function syncKnown(opts = {}) {
1235
1738
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === known.name);
1236
1739
  if (!existing) {
1237
1740
  if (!opts.dryRun) {
1238
- createConfig({
1741
+ await store.createConfig({
1239
1742
  name: known.name,
1240
1743
  category: known.category,
1241
1744
  agent: known.agent,
@@ -1246,16 +1749,16 @@ async function syncKnown(opts = {}) {
1246
1749
  description: known.description,
1247
1750
  is_template: isTemplate2,
1248
1751
  outputs: known.outputs
1249
- }, d);
1752
+ });
1250
1753
  }
1251
1754
  result.added++;
1252
1755
  } else if (existing.content !== content) {
1253
1756
  if (!opts.dryRun)
1254
- updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs }, d);
1757
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs });
1255
1758
  result.updated++;
1256
1759
  } else if (!outputsEqual(existing.outputs, known.outputs)) {
1257
1760
  if (!opts.dryRun)
1258
- updateConfig(existing.id, { outputs: known.outputs }, d);
1761
+ await store.updateConfig(existing.id, { outputs: known.outputs });
1259
1762
  result.updated++;
1260
1763
  } else {
1261
1764
  result.unchanged++;
@@ -1267,9 +1770,9 @@ async function syncKnown(opts = {}) {
1267
1770
  return result;
1268
1771
  }
1269
1772
  async function syncToDisk(opts = {}) {
1270
- const d = opts.db || getDatabase();
1773
+ const store = opts.store ?? resolveConfigStore();
1271
1774
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
1272
- const allFileConfigs = listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} }, d);
1775
+ const allFileConfigs = await store.listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} });
1273
1776
  const outputOwners = outputOwnerIdsByTarget(allFileConfigs);
1274
1777
  let configs = allFileConfigs.filter((config) => {
1275
1778
  return !isGeneratedOutputTarget2(config, outputOwners);
@@ -1282,7 +1785,7 @@ async function syncToDisk(opts = {}) {
1282
1785
  if (!config.target_path && config.outputs.length === 0)
1283
1786
  continue;
1284
1787
  try {
1285
- const r = await applyConfig(config, { dryRun: opts.dryRun, db: d, outputAgent: opts.agent });
1788
+ const r = await applyConfig(config, { dryRun: opts.dryRun, store, outputAgent: opts.agent });
1286
1789
  r.changed ? result.updated++ : result.unchanged++;
1287
1790
  } catch {
1288
1791
  result.skipped.push(config.target_path ?? config.id);
@@ -1319,12 +1822,12 @@ function buildDiff(expectedContent, targetPath) {
1319
1822
  return lines.join(`
1320
1823
  `);
1321
1824
  }
1322
- function diffConfig(config, opts = {}) {
1825
+ async function diffConfig(config, opts = {}) {
1323
1826
  if (!config.target_path && config.outputs.length === 0)
1324
1827
  return "(reference \u2014 no target path)";
1325
1828
  const diffs = [];
1326
- const db = opts.db || getDatabase();
1327
- const contextConfigs = config.outputs.length > 0 || config.target_path ? listConfigs(undefined, db) : [config];
1829
+ const store = opts.store ?? resolveConfigStore();
1830
+ const contextConfigs = config.outputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
1328
1831
  if (isGeneratedOutputTarget2(config, outputOwnerIdsByTarget(contextConfigs))) {
1329
1832
  return "(generated output \u2014 managed by fan-out)";
1330
1833
  }
@@ -1403,8 +1906,7 @@ function detectFormat(filePath) {
1403
1906
  }
1404
1907
  var CLAUDE_PROMPT_OUTPUTS, KNOWN_CONFIGS, PROJECT_CONFIG_FILES;
1405
1908
  var init_sync = __esm(() => {
1406
- init_database();
1407
- init_configs();
1909
+ init_config_store();
1408
1910
  init_apply();
1409
1911
  init_redact();
1410
1912
  init_machine();
@@ -1467,14 +1969,14 @@ function shouldSkip(p) {
1467
1969
  return SKIP.some((s) => p.includes(s));
1468
1970
  }
1469
1971
  async function syncFromDir(dir, opts = {}) {
1470
- const d = opts.db || getDatabase();
1972
+ const store = opts.store ?? resolveConfigStore();
1471
1973
  const absDir = expandPath(dir);
1472
1974
  if (!existsSync5(absDir))
1473
1975
  return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
1474
1976
  const files = opts.recursive !== false ? walkDir(absDir) : readdirSync2(absDir).map((f) => join4(absDir, f)).filter((f) => statSync2(f).isFile());
1475
1977
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
1476
1978
  const home = homedir3();
1477
- const allConfigs = listConfigs(undefined, d);
1979
+ const allConfigs = await store.listConfigs();
1478
1980
  for (const file of files) {
1479
1981
  if (shouldSkip(file)) {
1480
1982
  result.skipped.push(file);
@@ -1490,11 +1992,11 @@ async function syncFromDir(dir, opts = {}) {
1490
1992
  const existing = allConfigs.find((c) => c.target_path === targetPath);
1491
1993
  if (!existing) {
1492
1994
  if (!opts.dryRun)
1493
- createConfig({ name: relative(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content }, d);
1995
+ await store.createConfig({ name: relative(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content });
1494
1996
  result.added++;
1495
1997
  } else if (existing.content !== content) {
1496
1998
  if (!opts.dryRun)
1497
- updateConfig(existing.id, { content }, d);
1999
+ await store.updateConfig(existing.id, { content });
1498
2000
  result.updated++;
1499
2001
  } else {
1500
2002
  result.unchanged++;
@@ -1506,17 +2008,17 @@ async function syncFromDir(dir, opts = {}) {
1506
2008
  return result;
1507
2009
  }
1508
2010
  async function syncToDir(dir, opts = {}) {
1509
- const d = opts.db || getDatabase();
2011
+ const store = opts.store ?? resolveConfigStore();
1510
2012
  const home = homedir3();
1511
2013
  const absDir = expandPath(dir);
1512
2014
  const normalized = dir.startsWith("~/") ? dir : absDir.replace(home, "~");
1513
- const configs = listConfigs(undefined, d).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
2015
+ const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
1514
2016
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
1515
2017
  for (const config of configs) {
1516
2018
  if (config.kind === "reference")
1517
2019
  continue;
1518
2020
  try {
1519
- const r = await applyConfig(config, { dryRun: opts.dryRun, db: d });
2021
+ const r = await applyConfig(config, { dryRun: opts.dryRun, store });
1520
2022
  r.changed ? result.updated++ : result.unchanged++;
1521
2023
  } catch {
1522
2024
  result.skipped.push(config.target_path || config.id);
@@ -1538,8 +2040,7 @@ function walkDir(dir, files = []) {
1538
2040
  }
1539
2041
  var SKIP;
1540
2042
  var init_sync_dir = __esm(() => {
1541
- init_database();
1542
- init_configs();
2043
+ init_config_store();
1543
2044
  init_apply();
1544
2045
  init_sync();
1545
2046
  SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
@@ -1549,7 +2050,7 @@ var init_sync_dir = __esm(() => {
1549
2050
  var require_package = __commonJS((exports, module) => {
1550
2051
  module.exports = {
1551
2052
  name: "@hasna/instructions",
1552
- version: "0.3.0",
2053
+ version: "0.4.0",
1553
2054
  description: "AI coding agent instruction & configuration manager \u2014 store, version, apply, and share all your AI coding configs. CLI + MCP + HTTP API (instructions-serve) + generated SDK + Dashboard.",
1554
2055
  type: "module",
1555
2056
  main: "dist/index.js",
@@ -1564,10 +2065,6 @@ var require_package = __commonJS((exports, module) => {
1564
2065
  ".": {
1565
2066
  types: "./dist/index.d.ts",
1566
2067
  import: "./dist/index.js"
1567
- },
1568
- "./storage": {
1569
- types: "./dist/storage.d.ts",
1570
- import: "./dist/storage.js"
1571
2068
  }
1572
2069
  },
1573
2070
  files: [
@@ -1578,14 +2075,15 @@ var require_package = __commonJS((exports, module) => {
1578
2075
  ],
1579
2076
  scripts: {
1580
2077
  clean: "rm -rf dist",
1581
- build: "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external pg --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external pg --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun --external pg && bun build src/index.ts src/storage.ts --outdir dist --target bun --external pg && tsc --emitDeclarationOnly --outDir dist",
1582
- "build:server": "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external pg --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external pg --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun --external pg && bun build src/index.ts src/storage.ts --outdir dist --target bun --external pg",
2078
+ build: "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external pg --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external pg --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun --external pg && bun build src/index.ts --outdir dist --target bun --external pg && tsc --emitDeclarationOnly --outDir dist",
2079
+ "build:server": "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external pg --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external pg --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun --external pg && bun build src/index.ts --outdir dist --target bun --external pg",
1583
2080
  "build:dashboard": "cd dashboard && bun run build",
1584
2081
  migrate: "bun run src/server/index.ts migrate",
1585
2082
  "generate:sdk": "bun run scripts/generate-sdk.ts",
1586
2083
  "kit:check": "bunx @hasna/contracts vendor-kit --check",
1587
2084
  typecheck: "tsc --noEmit",
1588
2085
  test: "bun test",
2086
+ "check:package-secrets": "bun run src/cli/index.tsx package-manager-scan --fail-on-findings --home .",
1589
2087
  "dev:cli": "bun run src/cli/index.tsx",
1590
2088
  "dev:mcp": "bun run src/mcp/index.ts",
1591
2089
  "dev:serve": "bun run src/server/index.ts",
@@ -1625,7 +2123,7 @@ var require_package = __commonJS((exports, module) => {
1625
2123
  author: "Andrei Hasna <andrei@hasna.com>",
1626
2124
  license: "Apache-2.0",
1627
2125
  dependencies: {
1628
- "@hasna/contracts": "^0.4.1",
2126
+ "@hasna/contracts": "0.4.2",
1629
2127
  "@hasna/events": "^0.1.6",
1630
2128
  "@modelcontextprotocol/sdk": "^1.12.1",
1631
2129
  chalk: "^5.4.1",
@@ -1649,420 +2147,12 @@ var require_package = __commonJS((exports, module) => {
1649
2147
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1650
2148
 
1651
2149
  // src/mcp/server.ts
1652
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1653
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
1654
-
1655
- // src/db/storage-sync.ts
1656
- init_database();
1657
-
1658
- // src/db/pg-migrations.ts
1659
- var PG_MIGRATIONS = [
1660
- `CREATE TABLE IF NOT EXISTS configs (
1661
- id TEXT PRIMARY KEY,
1662
- name TEXT NOT NULL,
1663
- slug TEXT NOT NULL UNIQUE,
1664
- kind TEXT NOT NULL DEFAULT 'file',
1665
- category TEXT NOT NULL,
1666
- agent TEXT NOT NULL DEFAULT 'global',
1667
- target_path TEXT,
1668
- outputs TEXT NOT NULL DEFAULT '[]',
1669
- format TEXT NOT NULL DEFAULT 'text',
1670
- content TEXT NOT NULL DEFAULT '',
1671
- description TEXT,
1672
- tags TEXT NOT NULL DEFAULT '[]',
1673
- is_template BOOLEAN NOT NULL DEFAULT FALSE,
1674
- version INTEGER NOT NULL DEFAULT 1,
1675
- created_at TEXT NOT NULL,
1676
- updated_at TEXT NOT NULL,
1677
- synced_at TEXT
1678
- )`,
1679
- `CREATE TABLE IF NOT EXISTS config_snapshots (
1680
- id TEXT PRIMARY KEY,
1681
- config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
1682
- content TEXT NOT NULL,
1683
- version INTEGER NOT NULL,
1684
- created_at TEXT NOT NULL
1685
- )`,
1686
- `CREATE TABLE IF NOT EXISTS profiles (
1687
- id TEXT PRIMARY KEY,
1688
- name TEXT NOT NULL,
1689
- slug TEXT NOT NULL UNIQUE,
1690
- description TEXT,
1691
- selectors TEXT NOT NULL DEFAULT '{}',
1692
- variables TEXT NOT NULL DEFAULT '{}',
1693
- created_at TEXT NOT NULL,
1694
- updated_at TEXT NOT NULL
1695
- )`,
1696
- `CREATE TABLE IF NOT EXISTS profile_configs (
1697
- profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
1698
- config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
1699
- sort_order INTEGER NOT NULL DEFAULT 0,
1700
- PRIMARY KEY (profile_id, config_id)
1701
- )`,
1702
- `CREATE TABLE IF NOT EXISTS machines (
1703
- id TEXT PRIMARY KEY,
1704
- hostname TEXT NOT NULL UNIQUE,
1705
- os TEXT,
1706
- arch TEXT,
1707
- last_applied_at TEXT,
1708
- created_at TEXT NOT NULL
1709
- )`,
1710
- `CREATE TABLE IF NOT EXISTS feedback (
1711
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
1712
- message TEXT NOT NULL,
1713
- email TEXT,
1714
- category TEXT DEFAULT 'general',
1715
- version TEXT,
1716
- machine_id TEXT,
1717
- created_at TEXT NOT NULL DEFAULT NOW()::text
1718
- )`,
1719
- `ALTER TABLE configs ADD COLUMN IF NOT EXISTS outputs TEXT NOT NULL DEFAULT '[]'`
1720
- ];
1721
-
1722
- // src/db/remote-storage.ts
1723
- import pg from "pg";
1724
- var DISABLED_SSL_MODE = "disable";
1725
- function translatePlaceholders(sql) {
1726
- let index = 0;
1727
- return sql.replace(/\?/g, () => `$${++index}`);
1728
- }
1729
- function normalizeParams(params) {
1730
- const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
1731
- return flat.map((value) => value === undefined ? null : value);
1732
- }
1733
- function normalizeHost(hostname) {
1734
- const stripped = hostname.replace(/^\[/, "").replace(/\]$/, "");
1735
- try {
1736
- return decodeURIComponent(stripped).toLowerCase();
1737
- } catch {
1738
- return stripped.toLowerCase();
1739
- }
1740
- }
1741
- function isLocalPostgresHost(hostname) {
1742
- const host = normalizeHost(hostname);
1743
- return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "" || host.startsWith("/");
1744
- }
1745
- function effectivePgHost(url) {
1746
- const hosts = url.searchParams.getAll("host");
1747
- const finalHost = hosts.length > 0 ? hosts[hosts.length - 1] : null;
1748
- return finalHost?.trim() ? finalHost : url.hostname;
1749
- }
1750
- function buildPgPoolConfig(connectionString) {
1751
- let url;
1752
- try {
1753
- url = new URL(connectionString);
1754
- } catch {
1755
- throw new Error("Invalid PostgreSQL connection string");
1756
- }
1757
- const sslMode = url.searchParams.get("sslmode")?.trim().toLowerCase();
1758
- const sslValue = url.searchParams.get("ssl")?.trim().toLowerCase();
1759
- const isLocal = isLocalPostgresHost(effectivePgHost(url));
1760
- const hasDisabledSsl = sslMode === DISABLED_SSL_MODE || sslValue === "false";
1761
- if (!isLocal && hasDisabledSsl) {
1762
- throw new Error("Refusing remote PostgreSQL connection with TLS disabled");
1763
- }
1764
- const shouldUseSsl = !isLocal || sslMode === "require" || sslMode === "verify-ca" || sslMode === "verify-full" || sslValue === "true";
1765
- url.searchParams.delete("sslmode");
1766
- url.searchParams.delete("ssl");
1767
- return {
1768
- connectionString: url.toString(),
1769
- ssl: shouldUseSsl ? { rejectUnauthorized: true } : undefined
1770
- };
1771
- }
1772
-
1773
- class PgAdapterAsync {
1774
- pool;
1775
- constructor(connectionString) {
1776
- this.pool = new pg.Pool(buildPgPoolConfig(connectionString));
1777
- }
1778
- async run(sql, ...params) {
1779
- const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
1780
- return { changes: result.rowCount ?? 0 };
1781
- }
1782
- async all(sql, ...params) {
1783
- const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
1784
- return result.rows;
1785
- }
1786
- async close() {
1787
- await this.pool.end();
1788
- }
1789
- }
1790
-
1791
- // src/db/storage-sync.ts
1792
- var STORAGE_TABLES = ["configs", "config_snapshots", "profiles", "profile_configs", "machines", "feedback"];
1793
- var PRIMARY_KEYS = {
1794
- configs: ["id"],
1795
- config_snapshots: ["id"],
1796
- profiles: ["id"],
1797
- profile_configs: ["profile_id", "config_id"],
1798
- machines: ["id"],
1799
- feedback: ["id"]
1800
- };
1801
- var CONFIGS_STORAGE_ENV = "HASNA_CONFIGS_DATABASE_URL";
1802
- var CONFIGS_STORAGE_FALLBACK_ENV = "CONFIGS_DATABASE_URL";
1803
- var CONFIGS_STORAGE_MODE_ENV = "HASNA_CONFIGS_STORAGE_MODE";
1804
- var CONFIGS_STORAGE_MODE_FALLBACK_ENV = "CONFIGS_STORAGE_MODE";
1805
- var STORAGE_DATABASE_ENV = [CONFIGS_STORAGE_ENV, CONFIGS_STORAGE_FALLBACK_ENV];
1806
- var STORAGE_MODE_ENV = [CONFIGS_STORAGE_MODE_ENV, CONFIGS_STORAGE_MODE_FALLBACK_ENV];
1807
- function firstEnv(names) {
1808
- for (const name of names) {
1809
- const value = process.env[name];
1810
- if (value)
1811
- return value;
1812
- }
1813
- return null;
1814
- }
1815
- function normalizeStorageMode(value) {
1816
- const normalized = value?.trim().toLowerCase();
1817
- if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
1818
- return normalized;
1819
- return;
1820
- }
1821
- function getStorageDatabaseUrl() {
1822
- return firstEnv(STORAGE_DATABASE_ENV);
1823
- }
1824
- function getStorageMode() {
1825
- const mode = normalizeStorageMode(firstEnv(STORAGE_MODE_ENV));
1826
- if (mode)
1827
- return mode;
1828
- return getStorageDatabaseUrl() ? "hybrid" : "local";
1829
- }
1830
- async function getStoragePg() {
1831
- const url = getStorageDatabaseUrl();
1832
- if (!url)
1833
- throw new Error("Missing HASNA_CONFIGS_DATABASE_URL or CONFIGS_DATABASE_URL");
1834
- return new PgAdapterAsync(url);
1835
- }
1836
- async function runStorageMigrations(remote) {
1837
- await remote.run("CREATE EXTENSION IF NOT EXISTS pgcrypto");
1838
- for (const sql of PG_MIGRATIONS)
1839
- await remote.run(sql);
1840
- }
1841
- async function storagePush(options) {
1842
- const remote = await getStoragePg();
1843
- const db = getDatabase();
1844
- try {
1845
- await runStorageMigrations(remote);
1846
- const results = [];
1847
- for (const table of resolveTables(options?.tables))
1848
- results.push(await pushTable(db, remote, table));
1849
- recordSyncMeta(db, "push", results);
1850
- return results;
1851
- } finally {
1852
- await remote.close();
1853
- }
1854
- }
1855
- async function storagePull(options) {
1856
- const remote = await getStoragePg();
1857
- const db = getDatabase();
1858
- try {
1859
- await runStorageMigrations(remote);
1860
- const results = [];
1861
- for (const table of resolveTables(options?.tables))
1862
- results.push(await pullTable(remote, db, table));
1863
- recordSyncMeta(db, "pull", results);
1864
- return results;
1865
- } finally {
1866
- await remote.close();
1867
- }
1868
- }
1869
- async function storageSync(options) {
1870
- const pull = await storagePull(options);
1871
- const push = await storagePush(options);
1872
- return { pull, push };
1873
- }
1874
- function getStorageSyncMetaAll() {
1875
- const db = getDatabase();
1876
- ensureSyncMetaTable(db);
1877
- return db.query("SELECT table_name, last_synced_at, direction FROM _configs_sync_meta ORDER BY table_name, direction").all();
1878
- }
1879
- function getStorageStatus() {
1880
- return {
1881
- configured: Boolean(getStorageDatabaseUrl()),
1882
- mode: getStorageMode(),
1883
- env: STORAGE_DATABASE_ENV,
1884
- service: "configs",
1885
- tables: STORAGE_TABLES,
1886
- sync: getStorageSyncMetaAll()
1887
- };
1888
- }
1889
- function resolveTables(tables) {
1890
- if (!tables || tables.length === 0)
1891
- return [...STORAGE_TABLES];
1892
- const allowed = new Set(STORAGE_TABLES);
1893
- const requested = tables.map((table) => table.trim()).filter(Boolean);
1894
- const invalid = requested.filter((table) => !allowed.has(table));
1895
- if (invalid.length > 0)
1896
- throw new Error(`Unknown configs sync table(s): ${invalid.join(", ")}`);
1897
- return requested;
1898
- }
1899
- async function pushTable(db, remote, table) {
1900
- const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
1901
- try {
1902
- const rows = db.query(`SELECT * FROM ${quoteIdent(table)}`).all();
1903
- result.rowsRead = rows.length;
1904
- if (rows.length === 0)
1905
- return result;
1906
- const columns = await filterRemoteColumns(remote, table, Object.keys(rows[0]));
1907
- result.rowsWritten = await upsertPg(remote, table, columns, rows);
1908
- } catch (error) {
1909
- result.errors.push(error instanceof Error ? error.message : String(error));
1910
- }
1911
- return result;
1912
- }
1913
- async function pullTable(remote, db, table) {
1914
- const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
1915
- try {
1916
- const rows = await remote.all(`SELECT * FROM ${quoteIdent(table)}`);
1917
- result.rowsRead = rows.length;
1918
- if (rows.length === 0)
1919
- return result;
1920
- const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
1921
- result.rowsWritten = upsertSqlite(db, table, columns, rows);
1922
- } catch (error) {
1923
- result.errors.push(error instanceof Error ? error.message : String(error));
1924
- }
1925
- return result;
1926
- }
1927
- async function filterRemoteColumns(remote, table, columns) {
1928
- const rows = await remote.all("SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ?", table);
1929
- if (rows.length === 0)
1930
- return columns;
1931
- const allowed = new Set(rows.map((row) => row.column_name));
1932
- return columns.filter((column) => allowed.has(column));
1933
- }
1934
- function filterLocalColumns(db, table, columns) {
1935
- const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all();
1936
- const allowed = new Set(rows.map((row) => row.name));
1937
- return columns.filter((column) => allowed.has(column));
1938
- }
1939
- async function upsertPg(remote, table, columns, rows) {
1940
- if (columns.length === 0)
1941
- return 0;
1942
- const primaryKeys = PRIMARY_KEYS[table];
1943
- const columnList = columns.map(quoteIdent).join(", ");
1944
- const placeholders = columns.map(() => "?").join(", ");
1945
- const keyList = primaryKeys.map(quoteIdent).join(", ");
1946
- const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
1947
- const fallbackKey = primaryKeys[0];
1948
- const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
1949
- for (const row of rows) {
1950
- await remote.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`, ...columns.map((column) => row[column] ?? null));
1951
- }
1952
- return rows.length;
1953
- }
1954
- function upsertSqlite(db, table, columns, rows) {
1955
- if (columns.length === 0)
1956
- return 0;
1957
- const primaryKeys = PRIMARY_KEYS[table];
1958
- const columnList = columns.map(quoteIdent).join(", ");
1959
- const placeholders = columns.map(() => "?").join(", ");
1960
- const keyList = primaryKeys.map(quoteIdent).join(", ");
1961
- const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
1962
- const fallbackKey = primaryKeys[0];
1963
- const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = excluded.${quoteIdent(fallbackKey)}`;
1964
- const statement = db.prepare(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`);
1965
- db.transaction((batch) => {
1966
- for (const row of batch)
1967
- statement.run(...columns.map((column) => coerceForSqlite(row[column])));
1968
- })(rows);
1969
- return rows.length;
1970
- }
1971
- function recordSyncMeta(db, direction, results) {
1972
- ensureSyncMetaTable(db);
1973
- const now2 = new Date().toISOString();
1974
- const statement = db.prepare("INSERT INTO _configs_sync_meta (table_name, last_synced_at, direction) VALUES (?, ?, ?) ON CONFLICT(table_name, direction) DO UPDATE SET last_synced_at = excluded.last_synced_at");
1975
- for (const result of results) {
1976
- if (result.errors.length > 0)
1977
- continue;
1978
- statement.run(result.table, now2, direction);
1979
- }
1980
- }
1981
- function ensureSyncMetaTable(db) {
1982
- db.exec("CREATE TABLE IF NOT EXISTS _configs_sync_meta (table_name TEXT NOT NULL, last_synced_at TEXT, direction TEXT NOT NULL CHECK(direction IN ('push', 'pull')), PRIMARY KEY (table_name, direction))");
1983
- }
1984
- function quoteIdent(identifier) {
1985
- return `"${identifier.replace(/"/g, '""')}"`;
1986
- }
1987
- function coerceForSqlite(value) {
1988
- if (value === undefined || value === null)
1989
- return null;
1990
- if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
1991
- return value;
1992
- if (value instanceof Date)
1993
- return value.toISOString();
1994
- if (Buffer.isBuffer(value) || value instanceof Uint8Array)
1995
- return value;
1996
- if (typeof value === "object")
1997
- return JSON.stringify(value);
1998
- return String(value);
1999
- }
2000
-
2001
- // src/mcp/server.ts
2002
- init_configs();
2150
+ init_config_store();
2003
2151
  init_apply();
2004
2152
  init_sync_dir();
2005
-
2006
- // src/db/profiles.ts
2007
- init_types();
2008
- init_database();
2009
- init_configs();
2010
- init_machine();
2011
- function rowToProfile(row) {
2012
- return {
2013
- ...row,
2014
- selectors: JSON.parse(row.selectors || "{}"),
2015
- variables: JSON.parse(row.variables || "{}")
2016
- };
2017
- }
2018
- function getProfile(idOrSlug, db) {
2019
- const d = db || getDatabase();
2020
- const row = d.query("SELECT * FROM profiles WHERE id = ? OR slug = ?").get(idOrSlug, idOrSlug);
2021
- if (!row)
2022
- throw new ProfileNotFoundError(idOrSlug);
2023
- return rowToProfile(row);
2024
- }
2025
- function listProfiles(db) {
2026
- const d = db || getDatabase();
2027
- return d.query("SELECT * FROM profiles ORDER BY name").all().map(rowToProfile);
2028
- }
2029
- function getProfileConfigs(profileIdOrSlug, db) {
2030
- const d = db || getDatabase();
2031
- const profile = getProfile(profileIdOrSlug, d);
2032
- const rows = d.query("SELECT config_id FROM profile_configs WHERE profile_id = ? ORDER BY sort_order").all(profile.id);
2033
- if (rows.length === 0)
2034
- return [];
2035
- const ids = rows.map((r) => r.config_id);
2036
- return listConfigs(undefined, d).filter((c) => ids.includes(c.id));
2037
- }
2038
- function profileHasSelectors(profile) {
2039
- const selectors = profile.selectors ?? {};
2040
- return (selectors.os?.length ?? 0) > 0 || (selectors.arch?.length ?? 0) > 0 || (selectors.hostnames?.length ?? 0) > 0;
2041
- }
2042
- function profileMatchesMachine(profile, machine) {
2043
- const selectors = profile.selectors ?? {};
2044
- const osMatches = !selectors.os?.length || selectors.os.some((candidate) => {
2045
- const value = candidate.trim().toLowerCase();
2046
- return value === machine.os_family || value === (machine.os ?? "").trim().toLowerCase() || normalizeOsFamily(candidate) === machine.os_family;
2047
- });
2048
- const archMatches = !selectors.arch?.length || selectors.arch.some((candidate) => candidate.trim().toLowerCase() === (machine.arch ?? "").trim().toLowerCase());
2049
- const hostnameMatches = !selectors.hostnames?.length || selectors.hostnames.some((candidate) => candidate.trim().toLowerCase() === machine.hostname.trim().toLowerCase());
2050
- return osMatches && archMatches && hostnameMatches;
2051
- }
2052
- function resolveProfileForMachine(machine = detectMachineContext(), db) {
2053
- const profiles = listProfiles(db).filter(profileHasSelectors);
2054
- const matches = profiles.filter((profile) => profileMatchesMachine(profile, machine)).map((profile) => {
2055
- const selectors = profile.selectors;
2056
- const score = (selectors.hostnames?.length ? 100 : 0) + (selectors.os?.length ? 10 : 0) + (selectors.arch?.length ? 10 : 0);
2057
- return { profile, score };
2058
- }).sort((a, b) => b.score - a.score || a.profile.name.localeCompare(b.profile.name));
2059
- return matches[0]?.profile ?? null;
2060
- }
2061
-
2062
- // src/mcp/server.ts
2063
- init_apply();
2064
- init_snapshots();
2065
2153
  init_machine();
2154
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2155
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
2066
2156
 
2067
2157
  // src/lib/compact-output.ts
2068
2158
  var DEFAULT_LIST_LIMIT = 20;
@@ -2208,11 +2298,7 @@ var ALL_LEAN_TOOLS = [
2208
2298
  { name: "heartbeat", description: "Update last_seen_at.", inputSchema: { type: "object", properties: { agent_id: { type: "string" } }, required: ["agent_id"] } },
2209
2299
  { name: "set_focus", description: "Set active project context.", inputSchema: { type: "object", properties: { agent_id: { type: "string" }, project_id: { type: "string" } }, required: ["agent_id"] } },
2210
2300
  { name: "list_agents", description: "List all registered agents.", inputSchema: { type: "object", properties: {} } },
2211
- { name: "send_feedback", description: "Send feedback about this service", inputSchema: { type: "object", properties: { message: { type: "string" }, email: { type: "string" }, category: { type: "string", enum: ["bug", "feature", "general"] } }, required: ["message"] } },
2212
- { name: "storage_status", description: "Show storage sync configuration and local sync history.", inputSchema: { type: "object", properties: {} } },
2213
- { name: "storage_push", description: "Push local configs data to storage PostgreSQL.", inputSchema: { type: "object", properties: { tables: { type: "array", items: { type: "string" } } } } },
2214
- { name: "storage_pull", description: "Pull configs data from storage PostgreSQL to local SQLite.", inputSchema: { type: "object", properties: { tables: { type: "array", items: { type: "string" } } } } },
2215
- { name: "storage_sync", description: "Bidirectional configs sync: pull then push.", inputSchema: { type: "object", properties: { tables: { type: "array", items: { type: "string" } } } } }
2301
+ { name: "send_feedback", description: "Send feedback about this service", inputSchema: { type: "object", properties: { message: { type: "string" }, email: { type: "string" }, category: { type: "string", enum: ["bug", "feature", "general"] } }, required: ["message"] } }
2216
2302
  ];
2217
2303
  function ok(data) {
2218
2304
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
@@ -2227,10 +2313,11 @@ function buildServer() {
2227
2313
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: LEAN_TOOLS }));
2228
2314
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
2229
2315
  const { name, arguments: args = {} } = req.params;
2316
+ const store = resolveConfigStore();
2230
2317
  try {
2231
2318
  switch (name) {
2232
2319
  case "list_configs": {
2233
- const configs = listConfigs({
2320
+ const configs = await store.listConfigs({
2234
2321
  category: args["category"] || undefined,
2235
2322
  agent: args["agent"] || undefined,
2236
2323
  kind: args["kind"] || undefined,
@@ -2244,11 +2331,11 @@ function buildServer() {
2244
2331
  }));
2245
2332
  }
2246
2333
  case "get_config": {
2247
- const c = getConfig(args["id_or_slug"]);
2334
+ const c = await store.getConfig(args["id_or_slug"]);
2248
2335
  return ok(c);
2249
2336
  }
2250
2337
  case "create_config": {
2251
- const c = createConfig({
2338
+ const c = await store.createConfig({
2252
2339
  name: args["name"],
2253
2340
  content: args["content"],
2254
2341
  category: args["category"],
@@ -2264,7 +2351,7 @@ function buildServer() {
2264
2351
  return ok({ id: c.id, slug: c.slug, name: c.name });
2265
2352
  }
2266
2353
  case "update_config": {
2267
- const c = updateConfig(args["id_or_slug"], {
2354
+ const c = await store.updateConfig(args["id_or_slug"], {
2268
2355
  content: args["content"],
2269
2356
  name: args["name"],
2270
2357
  tags: args["tags"],
@@ -2277,23 +2364,22 @@ function buildServer() {
2277
2364
  return ok({ id: c.id, slug: c.slug, version: c.version });
2278
2365
  }
2279
2366
  case "delete_config": {
2280
- const { deleteConfig: deleteConfig2 } = await Promise.resolve().then(() => (init_configs(), exports_configs));
2281
- deleteConfig2(args["id_or_slug"]);
2367
+ await store.deleteConfig(args["id_or_slug"]);
2282
2368
  return ok({ deleted: true });
2283
2369
  }
2284
2370
  case "apply_config": {
2285
- const config = getConfig(args["id_or_slug"]);
2286
- const result = await applyConfig(config, { dryRun: args["dry_run"] });
2371
+ const config = await store.getConfig(args["id_or_slug"]);
2372
+ const result = await applyConfig(config, { dryRun: args["dry_run"], store });
2287
2373
  return ok(args["verbose"] ? result : summarizeApplyResult(result));
2288
2374
  }
2289
2375
  case "sync_directory": {
2290
2376
  const dir = args["dir"];
2291
2377
  const direction = args["direction"] || "from_disk";
2292
- const result = direction === "to_disk" ? await syncToDir(dir) : await syncFromDir(dir);
2378
+ const result = direction === "to_disk" ? await syncToDir(dir, { store }) : await syncFromDir(dir, { store });
2293
2379
  return ok(result);
2294
2380
  }
2295
2381
  case "list_profiles": {
2296
- const profiles = listProfiles().map((profile) => summarizeProfile(profile, { verbose: Boolean(args["verbose"]) }));
2382
+ const profiles = (await store.listProfiles()).map((profile) => summarizeProfile(profile, { verbose: Boolean(args["verbose"]) }));
2297
2383
  return ok(pagedPayload(profiles, {
2298
2384
  limit: args["limit"],
2299
2385
  cursor: args["cursor"],
@@ -2308,12 +2394,12 @@ function buildServer() {
2308
2394
  });
2309
2395
  if (!args["auto"] && !args["id_or_slug"])
2310
2396
  return err("id_or_slug is required unless auto=true");
2311
- const profile = args["auto"] ? resolveProfileForMachine(machine) : getProfile(args["id_or_slug"]);
2397
+ const profile = args["auto"] ? await store.resolveProfileForMachine(machine) : await store.getProfile(args["id_or_slug"]);
2312
2398
  if (!profile)
2313
2399
  return err("No matching machine-aware profile found");
2314
- const configs = getProfileConfigs(profile.id);
2400
+ const configs = await store.getProfileConfigs(profile.id);
2315
2401
  const vars = resolveProfileVariables(profile, machine);
2316
- const results = await applyConfigs(configs, { dryRun: args["dry_run"], vars });
2402
+ const results = await applyConfigs(configs, { dryRun: args["dry_run"], vars, store });
2317
2403
  return ok({
2318
2404
  profile: summarizeProfile(profile, { verbose: Boolean(args["verbose"]) }),
2319
2405
  machine: {
@@ -2328,17 +2414,17 @@ function buildServer() {
2328
2414
  });
2329
2415
  }
2330
2416
  case "get_snapshot": {
2331
- const config = getConfig(args["config_id_or_slug"]);
2417
+ const config = await store.getConfig(args["config_id_or_slug"]);
2332
2418
  if (args["version"]) {
2333
- const snap = getSnapshotByVersion(config.id, args["version"]);
2419
+ const snap = await store.getSnapshotByVersion(config.id, args["version"]);
2334
2420
  return snap ? ok(snap) : err("Snapshot not found");
2335
2421
  }
2336
- const snaps = listSnapshots(config.id);
2422
+ const snaps = await store.listSnapshots(config.id);
2337
2423
  return ok(snaps[0] ?? null);
2338
2424
  }
2339
2425
  case "get_status": {
2340
- const stats = getConfigStats();
2341
- const allConfigs = listConfigs({ kind: "file" });
2426
+ const stats = await store.getConfigStats();
2427
+ const allConfigs = await store.listConfigs({ kind: "file" });
2342
2428
  const { existsSync: ex, readFileSync: rf } = await import("fs");
2343
2429
  const { expandPath: expandPath2 } = await Promise.resolve().then(() => (init_apply(), exports_apply));
2344
2430
  const { redactContent: redactContent2 } = await Promise.resolve().then(() => (init_redact(), exports_redact));
@@ -2374,6 +2460,7 @@ function buildServer() {
2374
2460
  case "sync_known": {
2375
2461
  const { syncKnown: syncKnown2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
2376
2462
  const result = await syncKnown2({
2463
+ store,
2377
2464
  agent: args["agent"] || undefined,
2378
2465
  category: args["category"] || undefined
2379
2466
  });
@@ -2382,12 +2469,12 @@ function buildServer() {
2382
2469
  case "sync_project": {
2383
2470
  const { syncProject: syncProject2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
2384
2471
  const dir = args["project_dir"] || process.cwd();
2385
- const result = await syncProject2({ projectDir: dir });
2472
+ const result = await syncProject2({ store, projectDir: dir });
2386
2473
  return ok(result);
2387
2474
  }
2388
2475
  case "render_template": {
2389
2476
  const { renderTemplate: renderTemplate2 } = await Promise.resolve().then(() => (init_template(), exports_template));
2390
- const config = getConfig(args["id_or_slug"]);
2477
+ const config = await store.getConfig(args["id_or_slug"]);
2391
2478
  const vars = args["vars"] || {};
2392
2479
  if (args["use_env"]) {
2393
2480
  const { extractTemplateVars: extractTemplateVars2 } = await Promise.resolve().then(() => (init_template(), exports_template));
@@ -2402,7 +2489,7 @@ function buildServer() {
2402
2489
  }
2403
2490
  case "scan_secrets": {
2404
2491
  const { scanSecrets: scanSecrets2, redactContent: redactContent2 } = await Promise.resolve().then(() => (init_redact(), exports_redact));
2405
- const configs = args["id_or_slug"] ? [getConfig(args["id_or_slug"])] : listConfigs({ kind: "file" });
2492
+ const configs = args["id_or_slug"] ? [await store.getConfig(args["id_or_slug"])] : await store.listConfigs({ kind: "file" });
2406
2493
  const findings = [];
2407
2494
  for (const c of configs) {
2408
2495
  const fmt = c.format;
@@ -2411,7 +2498,7 @@ function buildServer() {
2411
2498
  findings.push({ slug: c.slug, secrets: secrets.length, vars: secrets.map((s) => s.varName) });
2412
2499
  if (args["fix"]) {
2413
2500
  const { content, isTemplate: isTemplate2 } = redactContent2(c.content, fmt);
2414
- updateConfig(c.id, { content, is_template: isTemplate2 });
2501
+ await store.updateConfig(c.id, { content, is_template: isTemplate2 });
2415
2502
  }
2416
2503
  }
2417
2504
  }
@@ -2467,27 +2554,15 @@ function buildServer() {
2467
2554
  return ok([..._cfgAgents.values()]);
2468
2555
  }
2469
2556
  case "send_feedback": {
2470
- const { getDatabase: getDatabase2 } = await Promise.resolve().then(() => (init_database(), exports_database));
2471
- const db = getDatabase2();
2472
2557
  const pkg = require_package();
2473
- db.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [args["message"], args["email"] || null, args["category"] || "general", pkg.version]);
2558
+ await store.sendFeedback({
2559
+ message: args["message"],
2560
+ email: args["email"] || null,
2561
+ category: args["category"] || "general",
2562
+ version: pkg.version
2563
+ });
2474
2564
  return ok({ message: "Feedback saved. Thank you!" });
2475
2565
  }
2476
- case "storage_status": {
2477
- return ok(getStorageStatus());
2478
- }
2479
- case "storage_push": {
2480
- const tables = Array.isArray(args["tables"]) ? args["tables"] : undefined;
2481
- return ok(await storagePush(tables ? { tables } : undefined));
2482
- }
2483
- case "storage_pull": {
2484
- const tables = Array.isArray(args["tables"]) ? args["tables"] : undefined;
2485
- return ok(await storagePull(tables ? { tables } : undefined));
2486
- }
2487
- case "storage_sync": {
2488
- const tables = Array.isArray(args["tables"]) ? args["tables"] : undefined;
2489
- return ok(await storageSync(tables ? { tables } : undefined));
2490
- }
2491
2566
  default:
2492
2567
  return err(`Unknown tool: ${name}`);
2493
2568
  }