@hasna/instructions 0.3.1 → 0.4.1

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 (63) hide show
  1. package/README.md +43 -12
  2. package/dashboard/README.md +73 -0
  3. package/dist/cli/index.js +1305 -837
  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/index.d.ts +6 -8
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +908 -492
  13. package/dist/lib/apply.d.ts +2 -2
  14. package/dist/lib/apply.d.ts.map +1 -1
  15. package/dist/lib/export.d.ts +2 -2
  16. package/dist/lib/export.d.ts.map +1 -1
  17. package/dist/lib/import.d.ts +2 -2
  18. package/dist/lib/import.d.ts.map +1 -1
  19. package/dist/lib/package-manager-guard.d.ts +24 -0
  20. package/dist/lib/package-manager-guard.d.ts.map +1 -0
  21. package/dist/lib/package-manager-guard.test.d.ts +2 -0
  22. package/dist/lib/package-manager-guard.test.d.ts.map +1 -0
  23. package/dist/lib/platform-profiles.d.ts +2 -2
  24. package/dist/lib/platform-profiles.d.ts.map +1 -1
  25. package/dist/lib/project-dashboard-standard.d.ts +2 -2
  26. package/dist/lib/project-dashboard-standard.d.ts.map +1 -1
  27. package/dist/lib/redact.d.ts.map +1 -1
  28. package/dist/lib/sync-dir.d.ts +3 -3
  29. package/dist/lib/sync-dir.d.ts.map +1 -1
  30. package/dist/lib/sync.d.ts +6 -6
  31. package/dist/lib/sync.d.ts.map +1 -1
  32. package/dist/mcp/http.d.ts +0 -13
  33. package/dist/mcp/http.d.ts.map +1 -1
  34. package/dist/mcp/index.js +661 -574
  35. package/dist/mcp/server.d.ts.map +1 -1
  36. package/dist/server/index.d.ts.map +1 -1
  37. package/dist/server/index.js +1757 -17541
  38. package/dist/server/v1.d.ts.map +1 -1
  39. package/dist/status.d.ts +2 -2
  40. package/dist/status.d.ts.map +1 -1
  41. package/dist/storage/cloud-store.d.ts +21 -1
  42. package/dist/storage/cloud-store.d.ts.map +1 -1
  43. package/dist/storage/schema.d.ts.map +1 -1
  44. package/package.json +4 -7
  45. package/dashboard/dist/assets/index-D7p6fFQw.js +0 -11
  46. package/dashboard/dist/assets/index-DQ3P1g1z.css +0 -1
  47. package/dashboard/dist/index.html +0 -14
  48. package/dashboard/dist/vite.svg +0 -1
  49. package/dist/cli/storage.d.ts +0 -3
  50. package/dist/cli/storage.d.ts.map +0 -1
  51. package/dist/cli/storage.test.d.ts +0 -2
  52. package/dist/cli/storage.test.d.ts.map +0 -1
  53. package/dist/db/remote-storage.d.ts +0 -13
  54. package/dist/db/remote-storage.d.ts.map +0 -1
  55. package/dist/db/storage-sync.d.ts +0 -53
  56. package/dist/db/storage-sync.d.ts.map +0 -1
  57. package/dist/db/storage-sync.test.d.ts +0 -2
  58. package/dist/db/storage-sync.test.d.ts.map +0 -1
  59. package/dist/server/server.test.d.ts +0 -2
  60. package/dist/server/server.test.d.ts.map +0 -1
  61. package/dist/storage.d.ts +0 -5
  62. package/dist/storage.d.ts.map +0 -1
  63. 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 {
@@ -98,6 +132,22 @@ function ensureFeedbackTable(db) {
98
132
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
99
133
  )
100
134
  `);
135
+ const existing = new Set(db.query("PRAGMA table_info(feedback)").all().map((r) => r.name));
136
+ const required = [
137
+ ["email", "TEXT"],
138
+ ["category", "TEXT DEFAULT 'general'"],
139
+ ["version", "TEXT"],
140
+ ["machine_id", "TEXT"],
141
+ ["created_at", "TEXT"]
142
+ ];
143
+ for (const [name, def] of required) {
144
+ if (!existing.has(name))
145
+ db.exec(`ALTER TABLE feedback ADD COLUMN ${name} ${def}`);
146
+ }
147
+ }
148
+ function insertFeedback(input, db) {
149
+ const d = db || getDatabase();
150
+ d.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [input.message, input.email ?? null, input.category ?? "general", input.version ?? null]);
101
151
  }
102
152
  function migrateDotfile() {
103
153
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
@@ -189,46 +239,7 @@ var init_database = __esm(() => {
189
239
  ];
190
240
  });
191
241
 
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
242
  // 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
243
  function rowToConfig(row) {
233
244
  let outputs = [];
234
245
  try {
@@ -413,26 +424,6 @@ var init_configs = __esm(() => {
413
424
  init_database();
414
425
  });
415
426
 
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
427
  // src/lib/template.ts
437
428
  var exports_template = {};
438
429
  __export(exports_template, {
@@ -588,6 +579,527 @@ var init_machine = __esm(() => {
588
579
  init_template();
589
580
  });
590
581
 
582
+ // src/db/profiles.ts
583
+ function rowToProfile(row) {
584
+ return {
585
+ ...row,
586
+ selectors: JSON.parse(row.selectors || "{}"),
587
+ variables: JSON.parse(row.variables || "{}")
588
+ };
589
+ }
590
+ function uniqueProfileSlug(name, db, excludeId) {
591
+ const base = slugify(name);
592
+ let slug = base;
593
+ let i = 1;
594
+ while (true) {
595
+ const existing = db.query("SELECT id FROM profiles WHERE slug = ?").get(slug);
596
+ if (!existing || existing.id === excludeId)
597
+ return slug;
598
+ slug = `${base}-${i++}`;
599
+ }
600
+ }
601
+ function createProfile(input, db) {
602
+ const d = db || getDatabase();
603
+ const id = uuid();
604
+ const ts = now();
605
+ const slug = uniqueProfileSlug(input.name, d);
606
+ d.run("INSERT INTO profiles (id, name, slug, description, selectors, variables, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
607
+ id,
608
+ input.name,
609
+ slug,
610
+ input.description ?? null,
611
+ JSON.stringify(input.selectors ?? {}),
612
+ JSON.stringify(input.variables ?? {}),
613
+ ts,
614
+ ts
615
+ ]);
616
+ return getProfile(id, d);
617
+ }
618
+ function getProfile(idOrSlug, db) {
619
+ const d = db || getDatabase();
620
+ const row = d.query("SELECT * FROM profiles WHERE id = ? OR slug = ?").get(idOrSlug, idOrSlug);
621
+ if (!row)
622
+ throw new ProfileNotFoundError(idOrSlug);
623
+ return rowToProfile(row);
624
+ }
625
+ function listProfiles(db) {
626
+ const d = db || getDatabase();
627
+ return d.query("SELECT * FROM profiles ORDER BY name").all().map(rowToProfile);
628
+ }
629
+ function updateProfile(idOrSlug, input, db) {
630
+ const d = db || getDatabase();
631
+ const existing = getProfile(idOrSlug, d);
632
+ const ts = now();
633
+ const updates = ["updated_at = ?"];
634
+ const params = [ts];
635
+ if (input.name !== undefined) {
636
+ updates.push("name = ?", "slug = ?");
637
+ params.push(input.name, uniqueProfileSlug(input.name, d, existing.id));
638
+ }
639
+ if (input.description !== undefined) {
640
+ updates.push("description = ?");
641
+ params.push(input.description);
642
+ }
643
+ if (input.selectors !== undefined) {
644
+ updates.push("selectors = ?");
645
+ params.push(JSON.stringify(input.selectors));
646
+ }
647
+ if (input.variables !== undefined) {
648
+ updates.push("variables = ?");
649
+ params.push(JSON.stringify(input.variables));
650
+ }
651
+ params.push(existing.id);
652
+ d.run(`UPDATE profiles SET ${updates.join(", ")} WHERE id = ?`, params);
653
+ return getProfile(existing.id, d);
654
+ }
655
+ function deleteProfile(idOrSlug, db) {
656
+ const d = db || getDatabase();
657
+ const existing = getProfile(idOrSlug, d);
658
+ d.run("DELETE FROM profiles WHERE id = ?", [existing.id]);
659
+ }
660
+ function addConfigToProfile(profileIdOrSlug, configId, db) {
661
+ const d = db || getDatabase();
662
+ const profile = getProfile(profileIdOrSlug, d);
663
+ const maxRow = d.query("SELECT MAX(sort_order) as max_order FROM profile_configs WHERE profile_id = ?").get(profile.id);
664
+ const order = (maxRow?.max_order ?? -1) + 1;
665
+ d.run("INSERT OR IGNORE INTO profile_configs (profile_id, config_id, sort_order) VALUES (?, ?, ?)", [profile.id, configId, order]);
666
+ }
667
+ function removeConfigFromProfile(profileIdOrSlug, configId, db) {
668
+ const d = db || getDatabase();
669
+ const profile = getProfile(profileIdOrSlug, d);
670
+ d.run("DELETE FROM profile_configs WHERE profile_id = ? AND config_id = ?", [profile.id, configId]);
671
+ }
672
+ function getProfileConfigs(profileIdOrSlug, db) {
673
+ const d = db || getDatabase();
674
+ const profile = getProfile(profileIdOrSlug, d);
675
+ const rows = d.query("SELECT config_id FROM profile_configs WHERE profile_id = ? ORDER BY sort_order").all(profile.id);
676
+ if (rows.length === 0)
677
+ return [];
678
+ const ids = rows.map((r) => r.config_id);
679
+ return listConfigs(undefined, d).filter((c) => ids.includes(c.id));
680
+ }
681
+ function profileHasSelectors(profile) {
682
+ const selectors = profile.selectors ?? {};
683
+ return (selectors.os?.length ?? 0) > 0 || (selectors.arch?.length ?? 0) > 0 || (selectors.hostnames?.length ?? 0) > 0;
684
+ }
685
+ function profileMatchesMachine(profile, machine) {
686
+ const selectors = profile.selectors ?? {};
687
+ const osMatches = !selectors.os?.length || selectors.os.some((candidate) => {
688
+ const value = candidate.trim().toLowerCase();
689
+ return value === machine.os_family || value === (machine.os ?? "").trim().toLowerCase() || normalizeOsFamily(candidate) === machine.os_family;
690
+ });
691
+ const archMatches = !selectors.arch?.length || selectors.arch.some((candidate) => candidate.trim().toLowerCase() === (machine.arch ?? "").trim().toLowerCase());
692
+ const hostnameMatches = !selectors.hostnames?.length || selectors.hostnames.some((candidate) => candidate.trim().toLowerCase() === machine.hostname.trim().toLowerCase());
693
+ return osMatches && archMatches && hostnameMatches;
694
+ }
695
+ function resolveProfileForMachine(machine = detectMachineContext(), db) {
696
+ const profiles = listProfiles(db).filter(profileHasSelectors);
697
+ const matches = profiles.filter((profile) => profileMatchesMachine(profile, machine)).map((profile) => {
698
+ const selectors = profile.selectors;
699
+ const score = (selectors.hostnames?.length ? 100 : 0) + (selectors.os?.length ? 10 : 0) + (selectors.arch?.length ? 10 : 0);
700
+ return { profile, score };
701
+ }).sort((a, b) => b.score - a.score || a.profile.name.localeCompare(b.profile.name));
702
+ return matches[0]?.profile ?? null;
703
+ }
704
+ var init_profiles = __esm(() => {
705
+ init_types();
706
+ init_database();
707
+ init_configs();
708
+ init_machine();
709
+ });
710
+
711
+ // src/db/snapshots.ts
712
+ function createSnapshot(configId, content, version, db) {
713
+ const d = db || getDatabase();
714
+ const id = uuid();
715
+ const ts = now();
716
+ d.run("INSERT INTO config_snapshots (id, config_id, content, version, created_at) VALUES (?, ?, ?, ?, ?)", [id, configId, content, version, ts]);
717
+ return { id, config_id: configId, content, version, created_at: ts };
718
+ }
719
+ function listSnapshots(configId, db) {
720
+ const d = db || getDatabase();
721
+ return d.query("SELECT * FROM config_snapshots WHERE config_id = ? ORDER BY version DESC").all(configId);
722
+ }
723
+ function getSnapshot(id, db) {
724
+ const d = db || getDatabase();
725
+ return d.query("SELECT * FROM config_snapshots WHERE id = ?").get(id);
726
+ }
727
+ function getSnapshotByVersion(configId, version, db) {
728
+ const d = db || getDatabase();
729
+ return d.query("SELECT * FROM config_snapshots WHERE config_id = ? AND version = ?").get(configId, version);
730
+ }
731
+ function pruneSnapshots(configId, keep = 10, db) {
732
+ const d = db || getDatabase();
733
+ const result = d.run(`DELETE FROM config_snapshots WHERE config_id = ? AND id NOT IN (
734
+ SELECT id FROM config_snapshots WHERE config_id = ? ORDER BY version DESC LIMIT ?
735
+ )`, [configId, configId, keep]);
736
+ return result.changes;
737
+ }
738
+ var init_snapshots = __esm(() => {
739
+ init_database();
740
+ });
741
+
742
+ // src/db/machines.ts
743
+ import { arch, hostname, type } from "os";
744
+ function currentHostname2() {
745
+ return hostname();
746
+ }
747
+ function currentOs() {
748
+ return type();
749
+ }
750
+ function currentArch2() {
751
+ return arch();
752
+ }
753
+ function registerMachine(hostnameStr, os, archStr, db) {
754
+ const d = db || getDatabase();
755
+ const h = hostnameStr ?? currentHostname2();
756
+ const o = os ?? currentOs();
757
+ const a = archStr ?? currentArch2();
758
+ const existing = d.query("SELECT * FROM machines WHERE hostname = ?").get(h);
759
+ if (existing) {
760
+ if (existing.os !== o || existing.arch !== a) {
761
+ d.run("UPDATE machines SET os = ?, arch = ? WHERE hostname = ?", [o, a, h]);
762
+ return d.query("SELECT * FROM machines WHERE hostname = ?").get(h);
763
+ }
764
+ return existing;
765
+ }
766
+ const id = uuid();
767
+ const ts = now();
768
+ d.run("INSERT INTO machines (id, hostname, os, arch, last_applied_at, created_at) VALUES (?, ?, ?, ?, NULL, ?)", [id, h, o, a, ts]);
769
+ return d.query("SELECT * FROM machines WHERE id = ?").get(id);
770
+ }
771
+ function updateMachineApplied(hostnameStr, db) {
772
+ const d = db || getDatabase();
773
+ const h = hostnameStr ?? currentHostname2();
774
+ d.run("UPDATE machines SET last_applied_at = ? WHERE hostname = ?", [now(), h]);
775
+ }
776
+ function listMachines(db) {
777
+ const d = db || getDatabase();
778
+ return d.query("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST").all();
779
+ }
780
+ var init_machines = __esm(() => {
781
+ init_database();
782
+ });
783
+
784
+ // src/data/config-store.ts
785
+ import { randomUUID as randomUUID2 } from "crypto";
786
+ function resolveCloudConfig(env = process.env) {
787
+ const apiUrl = env[API_URL_ENV]?.trim();
788
+ const apiKey = env[API_KEY_ENV]?.trim();
789
+ if (!apiUrl && !apiKey)
790
+ return null;
791
+ if (!apiUrl || !apiKey) {
792
+ 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.`);
793
+ }
794
+ return { apiUrl, apiKey };
795
+ }
796
+
797
+ class LocalConfigStore {
798
+ db;
799
+ mode = "local";
800
+ constructor(db) {
801
+ this.db = db;
802
+ }
803
+ async listConfigs(filter) {
804
+ return listConfigs(filter, this.db);
805
+ }
806
+ async getConfig(idOrSlug) {
807
+ return getConfig(idOrSlug, this.db);
808
+ }
809
+ async getConfigById(id) {
810
+ return getConfigById(id, this.db);
811
+ }
812
+ async createConfig(input) {
813
+ return createConfig(input, this.db);
814
+ }
815
+ async updateConfig(idOrSlug, input) {
816
+ return updateConfig(idOrSlug, input, this.db);
817
+ }
818
+ async deleteConfig(idOrSlug) {
819
+ deleteConfig(idOrSlug, this.db);
820
+ }
821
+ async getConfigStats() {
822
+ return getConfigStats(this.db);
823
+ }
824
+ async listSnapshots(configId) {
825
+ return listSnapshots(configId, this.db);
826
+ }
827
+ async getSnapshot(id) {
828
+ return getSnapshot(id, this.db);
829
+ }
830
+ async getSnapshotByVersion(configId, version) {
831
+ return getSnapshotByVersion(configId, version, this.db);
832
+ }
833
+ async createSnapshot(configId, content, version) {
834
+ return createSnapshot(configId, content, version, this.db);
835
+ }
836
+ async pruneSnapshots(configId, keep = 10) {
837
+ return pruneSnapshots(configId, keep, this.db);
838
+ }
839
+ async listProfiles() {
840
+ return listProfiles(this.db);
841
+ }
842
+ async getProfile(idOrSlug) {
843
+ return getProfile(idOrSlug, this.db);
844
+ }
845
+ async getProfileConfigs(idOrSlug) {
846
+ return getProfileConfigs(idOrSlug, this.db);
847
+ }
848
+ async createProfile(input) {
849
+ return createProfile(input, this.db);
850
+ }
851
+ async updateProfile(idOrSlug, input) {
852
+ return updateProfile(idOrSlug, input, this.db);
853
+ }
854
+ async deleteProfile(idOrSlug) {
855
+ deleteProfile(idOrSlug, this.db);
856
+ }
857
+ async addConfigToProfile(profileIdOrSlug, configId) {
858
+ addConfigToProfile(profileIdOrSlug, configId, this.db);
859
+ }
860
+ async removeConfigFromProfile(profileIdOrSlug, configId) {
861
+ removeConfigFromProfile(profileIdOrSlug, configId, this.db);
862
+ }
863
+ async resolveProfileForMachine(machine) {
864
+ return machine ? resolveProfileForMachine(machine, this.db) : resolveProfileForMachine(undefined, this.db);
865
+ }
866
+ async registerMachine(hostname2, os, arch2) {
867
+ return registerMachine(hostname2, os, arch2, this.db);
868
+ }
869
+ async updateMachineApplied(hostname2) {
870
+ updateMachineApplied(hostname2, this.db);
871
+ }
872
+ async listMachines() {
873
+ return listMachines(this.db);
874
+ }
875
+ async sendFeedback(input) {
876
+ insertFeedback(input, this.db);
877
+ }
878
+ async reset() {
879
+ resetLocalDatabase();
880
+ }
881
+ }
882
+
883
+ class CloudConfigStore {
884
+ mode = "api";
885
+ base;
886
+ apiKey;
887
+ timeoutMs;
888
+ constructor(config) {
889
+ this.base = `${config.apiUrl.replace(/\/+$/, "")}/v1`;
890
+ this.apiKey = config.apiKey;
891
+ this.timeoutMs = config.timeoutMs ?? 30000;
892
+ }
893
+ async request(method, path, body, opts = {}) {
894
+ const controller = new AbortController;
895
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
896
+ const headers = {
897
+ Authorization: `Bearer ${this.apiKey}`,
898
+ Accept: "application/json"
899
+ };
900
+ if (body !== undefined)
901
+ headers["Content-Type"] = "application/json";
902
+ if (opts.idempotent)
903
+ headers["Idempotency-Key"] = randomUUID2();
904
+ try {
905
+ const res = await fetch(`${this.base}${path}`, {
906
+ method,
907
+ headers,
908
+ body: body === undefined ? undefined : JSON.stringify(body),
909
+ signal: controller.signal
910
+ });
911
+ if (res.status === 404 && opts.allow404)
912
+ return { status: 404, data: null };
913
+ const text = await res.text();
914
+ let parsed = null;
915
+ if (text) {
916
+ try {
917
+ parsed = JSON.parse(text);
918
+ } catch {
919
+ parsed = text;
920
+ }
921
+ }
922
+ if (!res.ok) {
923
+ const message = (parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : undefined) ?? `HTTP ${res.status} on ${method} ${path}`;
924
+ throw new CloudHttpError(res.status, message, parsed);
925
+ }
926
+ return { status: res.status, data: parsed };
927
+ } finally {
928
+ clearTimeout(timer);
929
+ }
930
+ }
931
+ async listConfigs(filter = {}) {
932
+ const params = new URLSearchParams;
933
+ if (filter.category)
934
+ params.set("category", filter.category);
935
+ if (filter.agent)
936
+ params.set("agent", filter.agent);
937
+ if (filter.kind)
938
+ params.set("kind", filter.kind);
939
+ if (filter.search)
940
+ params.set("search", filter.search);
941
+ const qs = params.toString();
942
+ const { data } = await this.request("GET", `/configs${qs ? `?${qs}` : ""}`);
943
+ let configs = data?.configs ?? [];
944
+ if (filter.tags && filter.tags.length > 0) {
945
+ configs = configs.filter((c) => filter.tags.every((t) => c.tags.includes(t)));
946
+ }
947
+ if (filter.is_template !== undefined) {
948
+ configs = configs.filter((c) => c.is_template === filter.is_template);
949
+ }
950
+ return configs;
951
+ }
952
+ async getConfig(idOrSlug) {
953
+ const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
954
+ if (status === 404 || !data?.config)
955
+ throw new ConfigNotFoundError(idOrSlug);
956
+ return data.config;
957
+ }
958
+ async getConfigById(id) {
959
+ return this.getConfig(id);
960
+ }
961
+ async createConfig(input) {
962
+ const { data } = await this.request("POST", "/configs", input, {
963
+ idempotent: true
964
+ });
965
+ return data.config;
966
+ }
967
+ async updateConfig(idOrSlug, input) {
968
+ const { data } = await this.request("PATCH", `/configs/${encodeURIComponent(idOrSlug)}`, input);
969
+ return data.config;
970
+ }
971
+ async deleteConfig(idOrSlug) {
972
+ const { status } = await this.request("DELETE", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
973
+ if (status === 404)
974
+ throw new ConfigNotFoundError(idOrSlug);
975
+ }
976
+ async getConfigStats() {
977
+ const { data } = await this.request("GET", "/stats");
978
+ return data ?? { total: 0 };
979
+ }
980
+ async listSnapshots(configId) {
981
+ const { data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots`);
982
+ return data?.snapshots ?? [];
983
+ }
984
+ async getSnapshot(id) {
985
+ const { status, data } = await this.request("GET", `/snapshots/${encodeURIComponent(id)}`, undefined, { allow404: true });
986
+ if (status === 404 || !data?.snapshot)
987
+ return null;
988
+ return data.snapshot;
989
+ }
990
+ async getSnapshotByVersion(configId, version) {
991
+ const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots/${version}`, undefined, { allow404: true });
992
+ if (status === 404 || !data?.snapshot)
993
+ return null;
994
+ return data.snapshot;
995
+ }
996
+ async createSnapshot(configId, content, version) {
997
+ const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots`, { content, version }, { idempotent: true });
998
+ return data.snapshot;
999
+ }
1000
+ async pruneSnapshots(configId, keep = 10) {
1001
+ const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots/prune`, { keep });
1002
+ return data?.pruned ?? 0;
1003
+ }
1004
+ async listProfiles() {
1005
+ const { data } = await this.request("GET", "/profiles");
1006
+ return data?.profiles ?? [];
1007
+ }
1008
+ async getProfile(idOrSlug) {
1009
+ const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
1010
+ if (status === 404 || !data?.profile)
1011
+ throw new ProfileNotFoundError(idOrSlug);
1012
+ const { configs: _configs, ...profile } = data.profile;
1013
+ return profile;
1014
+ }
1015
+ async getProfileConfigs(idOrSlug) {
1016
+ const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
1017
+ if (status === 404 || !data?.profile)
1018
+ throw new ProfileNotFoundError(idOrSlug);
1019
+ return data.profile.configs ?? [];
1020
+ }
1021
+ async createProfile(input) {
1022
+ const { data } = await this.request("POST", "/profiles", input, {
1023
+ idempotent: true
1024
+ });
1025
+ return data.profile;
1026
+ }
1027
+ async updateProfile(idOrSlug, input) {
1028
+ const { data } = await this.request("PATCH", `/profiles/${encodeURIComponent(idOrSlug)}`, input);
1029
+ return data.profile;
1030
+ }
1031
+ async deleteProfile(idOrSlug) {
1032
+ const { status } = await this.request("DELETE", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
1033
+ if (status === 404)
1034
+ throw new ProfileNotFoundError(idOrSlug);
1035
+ }
1036
+ async addConfigToProfile(profileIdOrSlug, configId) {
1037
+ await this.request("POST", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs`, { config_id: configId }, { idempotent: true });
1038
+ }
1039
+ async removeConfigFromProfile(profileIdOrSlug, configId) {
1040
+ await this.request("DELETE", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs/${encodeURIComponent(configId)}`, undefined, { allow404: true });
1041
+ }
1042
+ async resolveProfileForMachine(machine) {
1043
+ const params = new URLSearchParams;
1044
+ if (machine?.hostname)
1045
+ params.set("hostname", machine.hostname);
1046
+ if (machine?.os)
1047
+ params.set("os", machine.os);
1048
+ if (machine?.arch)
1049
+ params.set("arch", machine.arch);
1050
+ const qs = params.toString();
1051
+ const { status, data } = await this.request("GET", `/profiles/resolve${qs ? `?${qs}` : ""}`, undefined, { allow404: true });
1052
+ if (status === 404 || !data?.profile)
1053
+ return null;
1054
+ return data.profile;
1055
+ }
1056
+ async registerMachine(hostname2, os, arch2) {
1057
+ const { data } = await this.request("POST", "/machines", { hostname: hostname2, os, arch: arch2 }, { idempotent: true });
1058
+ return data.machine;
1059
+ }
1060
+ async updateMachineApplied(hostname2) {
1061
+ await this.request("POST", "/machines/applied", { hostname: hostname2 });
1062
+ }
1063
+ async listMachines() {
1064
+ const { data } = await this.request("GET", "/machines");
1065
+ return data?.machines ?? [];
1066
+ }
1067
+ async sendFeedback(input) {
1068
+ await this.request("POST", "/feedback", {
1069
+ message: input.message,
1070
+ email: input.email ?? undefined,
1071
+ category: input.category ?? undefined,
1072
+ version: input.version ?? undefined
1073
+ });
1074
+ }
1075
+ async reset() {
1076
+ 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.");
1077
+ }
1078
+ }
1079
+ function resolveConfigStore(env = process.env) {
1080
+ const cloud = resolveCloudConfig(env);
1081
+ return cloud ? new CloudConfigStore(cloud) : new LocalConfigStore;
1082
+ }
1083
+ var CloudHttpError, API_URL_ENV = "HASNA_INSTRUCTIONS_API_URL", API_KEY_ENV = "HASNA_INSTRUCTIONS_API_KEY";
1084
+ var init_config_store = __esm(() => {
1085
+ init_configs();
1086
+ init_profiles();
1087
+ init_snapshots();
1088
+ init_machines();
1089
+ init_database();
1090
+ init_types();
1091
+ CloudHttpError = class CloudHttpError extends Error {
1092
+ status;
1093
+ body;
1094
+ constructor(status, message, body) {
1095
+ super(message);
1096
+ this.status = status;
1097
+ this.body = body;
1098
+ this.name = "CloudHttpError";
1099
+ }
1100
+ };
1101
+ });
1102
+
591
1103
  // src/lib/transforms.ts
592
1104
  import { basename, extname } from "path";
593
1105
  function ensureTrailingNewline(content) {
@@ -755,8 +1267,8 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
755
1267
  mkdirSync2(dir, { recursive: true });
756
1268
  }
757
1269
  if (previousContent !== null && changed) {
758
- const db = opts.db || getDatabase();
759
- createSnapshot(config.id, previousContent, config.version, db);
1270
+ const store = opts.store ?? resolveConfigStore();
1271
+ await store.createSnapshot(config.id, previousContent, config.version);
760
1272
  }
761
1273
  writeFileSync(path, renderedContent, "utf-8");
762
1274
  }
@@ -782,8 +1294,8 @@ async function applyConfig(config, opts = {}) {
782
1294
  if (config.kind === "reference" || (!config.target_path || !shouldApplyPrimary) && selectedOutputs.length === 0) {
783
1295
  throw new ConfigApplyError(`Config "${config.name}" is a reference (kind=reference) and has no target_path \u2014 cannot apply to disk.`);
784
1296
  }
785
- const db = opts.db || getDatabase();
786
- const contextConfigs = selectedOutputs.length > 0 || config.target_path ? listConfigs(undefined, db) : [config];
1297
+ const store = opts.store ?? resolveConfigStore();
1298
+ const contextConfigs = selectedOutputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
787
1299
  if (isGeneratedOutputTarget(config, contextConfigs)) {
788
1300
  throw new ConfigApplyError(`Config "${config.name}" targets a generated output path. Apply the canonical source config instead.`);
789
1301
  }
@@ -804,7 +1316,7 @@ async function applyConfig(config, opts = {}) {
804
1316
  };
805
1317
  }
806
1318
  if (!opts.dryRun) {
807
- updateConfig(config.id, { synced_at: now() }, db);
1319
+ await store.updateConfig(config.id, { synced_at: new Date().toISOString() });
808
1320
  }
809
1321
  return result;
810
1322
  }
@@ -826,9 +1338,7 @@ async function applyConfigs(configs, opts = {}) {
826
1338
  }
827
1339
  var init_apply = __esm(() => {
828
1340
  init_types();
829
- init_database();
830
- init_configs();
831
- init_snapshots();
1341
+ init_config_store();
832
1342
  init_machine();
833
1343
  init_transforms();
834
1344
  });
@@ -922,9 +1432,9 @@ function redactIni(content) {
922
1432
  for (let i = 0;i < lines.length; i++) {
923
1433
  const line = lines[i];
924
1434
  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}}`);
1435
+ if (authM && !isReferenceValue(authM[2].trim())) {
1436
+ redacted.push({ varName: "NPM_TOKEN", line: i + 1, reason: "npm auth token" });
1437
+ out.push(`${authM[1]}\${NPM_TOKEN}`);
928
1438
  continue;
929
1439
  }
930
1440
  const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
@@ -964,6 +1474,8 @@ function redactGeneric(content) {
964
1474
  function shouldRedactKeyValue(key, value) {
965
1475
  if (!value || value.startsWith("{{"))
966
1476
  return false;
1477
+ if (isReferenceValue(value.trim()))
1478
+ return false;
967
1479
  if (value.length < MIN_SECRET_VALUE_LEN)
968
1480
  return false;
969
1481
  if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
@@ -985,6 +1497,9 @@ function reasonFor(key, value) {
985
1497
  }
986
1498
  return "secret value pattern";
987
1499
  }
1500
+ function isReferenceValue(value) {
1501
+ 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);
1502
+ }
988
1503
  function redactContent(content, format) {
989
1504
  switch (format) {
990
1505
  case "shell":
@@ -1088,11 +1603,11 @@ function isKnownGeneratedTargetPath(targetPath) {
1088
1603
  return hasClaudeRuleSourceForCursorTarget(targetPath);
1089
1604
  }
1090
1605
  async function syncProject(opts) {
1091
- const d = opts.db || getDatabase();
1606
+ const store = opts.store ?? resolveConfigStore();
1092
1607
  const absDir = expandPath(opts.projectDir);
1093
1608
  const projectName = absDir.split("/").pop() || "project";
1094
1609
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
1095
- const allConfigs = listConfigs(undefined, d);
1610
+ const allConfigs = await store.listConfigs();
1096
1611
  const machine = detectMachineContext();
1097
1612
  for (const pf of PROJECT_CONFIG_FILES) {
1098
1613
  const abs = join3(absDir, pf.file);
@@ -1114,11 +1629,11 @@ async function syncProject(opts) {
1114
1629
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
1115
1630
  if (!existing) {
1116
1631
  if (!opts.dryRun)
1117
- createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 }, d);
1632
+ await store.createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 });
1118
1633
  result.added++;
1119
1634
  } else if (existing.content !== content) {
1120
1635
  if (!opts.dryRun)
1121
- updateConfig(existing.id, { content, is_template: isTemplate2 }, d);
1636
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
1122
1637
  result.updated++;
1123
1638
  } else {
1124
1639
  result.unchanged++;
@@ -1143,11 +1658,11 @@ async function syncProject(opts) {
1143
1658
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
1144
1659
  if (!existing) {
1145
1660
  if (!opts.dryRun)
1146
- createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 }, d);
1661
+ await store.createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 });
1147
1662
  result.added++;
1148
1663
  } else if (existing.content !== content) {
1149
1664
  if (!opts.dryRun)
1150
- updateConfig(existing.id, { content, is_template: isTemplate2 }, d);
1665
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
1151
1666
  result.updated++;
1152
1667
  } else {
1153
1668
  result.unchanged++;
@@ -1157,7 +1672,7 @@ async function syncProject(opts) {
1157
1672
  return result;
1158
1673
  }
1159
1674
  async function syncKnown(opts = {}) {
1160
- const d = opts.db || getDatabase();
1675
+ const store = opts.store ?? resolveConfigStore();
1161
1676
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
1162
1677
  const home = getConfigHome();
1163
1678
  const machine = detectMachineContext();
@@ -1166,7 +1681,7 @@ async function syncKnown(opts = {}) {
1166
1681
  targets = targets.filter((k) => k.agent === opts.agent);
1167
1682
  if (opts.category)
1168
1683
  targets = targets.filter((k) => k.category === opts.category);
1169
- const allConfigs = listConfigs(undefined, d);
1684
+ const allConfigs = await store.listConfigs();
1170
1685
  const existingOutputOwners = outputOwnerIdsByTarget(allConfigs);
1171
1686
  for (const known of targets) {
1172
1687
  if (known.rulesDir) {
@@ -1195,15 +1710,15 @@ async function syncKnown(opts = {}) {
1195
1710
  const outputs = known.agent === "claude" ? claudeRuleOutputs(f) : known.outputs;
1196
1711
  if (!existing) {
1197
1712
  if (!opts.dryRun)
1198
- createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs }, d);
1713
+ await store.createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs });
1199
1714
  result.added++;
1200
1715
  } else if (existing.content !== content) {
1201
1716
  if (!opts.dryRun)
1202
- updateConfig(existing.id, { content, is_template: isTemplate2, outputs }, d);
1717
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs });
1203
1718
  result.updated++;
1204
1719
  } else if (!outputsEqual(existing.outputs, outputs)) {
1205
1720
  if (!opts.dryRun)
1206
- updateConfig(existing.id, { outputs }, d);
1721
+ await store.updateConfig(existing.id, { outputs });
1207
1722
  result.updated++;
1208
1723
  } else {
1209
1724
  result.unchanged++;
@@ -1235,7 +1750,7 @@ async function syncKnown(opts = {}) {
1235
1750
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === known.name);
1236
1751
  if (!existing) {
1237
1752
  if (!opts.dryRun) {
1238
- createConfig({
1753
+ await store.createConfig({
1239
1754
  name: known.name,
1240
1755
  category: known.category,
1241
1756
  agent: known.agent,
@@ -1246,16 +1761,16 @@ async function syncKnown(opts = {}) {
1246
1761
  description: known.description,
1247
1762
  is_template: isTemplate2,
1248
1763
  outputs: known.outputs
1249
- }, d);
1764
+ });
1250
1765
  }
1251
1766
  result.added++;
1252
1767
  } else if (existing.content !== content) {
1253
1768
  if (!opts.dryRun)
1254
- updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs }, d);
1769
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs });
1255
1770
  result.updated++;
1256
1771
  } else if (!outputsEqual(existing.outputs, known.outputs)) {
1257
1772
  if (!opts.dryRun)
1258
- updateConfig(existing.id, { outputs: known.outputs }, d);
1773
+ await store.updateConfig(existing.id, { outputs: known.outputs });
1259
1774
  result.updated++;
1260
1775
  } else {
1261
1776
  result.unchanged++;
@@ -1267,9 +1782,9 @@ async function syncKnown(opts = {}) {
1267
1782
  return result;
1268
1783
  }
1269
1784
  async function syncToDisk(opts = {}) {
1270
- const d = opts.db || getDatabase();
1785
+ const store = opts.store ?? resolveConfigStore();
1271
1786
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
1272
- const allFileConfigs = listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} }, d);
1787
+ const allFileConfigs = await store.listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} });
1273
1788
  const outputOwners = outputOwnerIdsByTarget(allFileConfigs);
1274
1789
  let configs = allFileConfigs.filter((config) => {
1275
1790
  return !isGeneratedOutputTarget2(config, outputOwners);
@@ -1282,7 +1797,7 @@ async function syncToDisk(opts = {}) {
1282
1797
  if (!config.target_path && config.outputs.length === 0)
1283
1798
  continue;
1284
1799
  try {
1285
- const r = await applyConfig(config, { dryRun: opts.dryRun, db: d, outputAgent: opts.agent });
1800
+ const r = await applyConfig(config, { dryRun: opts.dryRun, store, outputAgent: opts.agent });
1286
1801
  r.changed ? result.updated++ : result.unchanged++;
1287
1802
  } catch {
1288
1803
  result.skipped.push(config.target_path ?? config.id);
@@ -1319,12 +1834,12 @@ function buildDiff(expectedContent, targetPath) {
1319
1834
  return lines.join(`
1320
1835
  `);
1321
1836
  }
1322
- function diffConfig(config, opts = {}) {
1837
+ async function diffConfig(config, opts = {}) {
1323
1838
  if (!config.target_path && config.outputs.length === 0)
1324
1839
  return "(reference \u2014 no target path)";
1325
1840
  const diffs = [];
1326
- const db = opts.db || getDatabase();
1327
- const contextConfigs = config.outputs.length > 0 || config.target_path ? listConfigs(undefined, db) : [config];
1841
+ const store = opts.store ?? resolveConfigStore();
1842
+ const contextConfigs = config.outputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
1328
1843
  if (isGeneratedOutputTarget2(config, outputOwnerIdsByTarget(contextConfigs))) {
1329
1844
  return "(generated output \u2014 managed by fan-out)";
1330
1845
  }
@@ -1403,8 +1918,7 @@ function detectFormat(filePath) {
1403
1918
  }
1404
1919
  var CLAUDE_PROMPT_OUTPUTS, KNOWN_CONFIGS, PROJECT_CONFIG_FILES;
1405
1920
  var init_sync = __esm(() => {
1406
- init_database();
1407
- init_configs();
1921
+ init_config_store();
1408
1922
  init_apply();
1409
1923
  init_redact();
1410
1924
  init_machine();
@@ -1467,14 +1981,14 @@ function shouldSkip(p) {
1467
1981
  return SKIP.some((s) => p.includes(s));
1468
1982
  }
1469
1983
  async function syncFromDir(dir, opts = {}) {
1470
- const d = opts.db || getDatabase();
1984
+ const store = opts.store ?? resolveConfigStore();
1471
1985
  const absDir = expandPath(dir);
1472
1986
  if (!existsSync5(absDir))
1473
1987
  return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
1474
1988
  const files = opts.recursive !== false ? walkDir(absDir) : readdirSync2(absDir).map((f) => join4(absDir, f)).filter((f) => statSync2(f).isFile());
1475
1989
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
1476
1990
  const home = homedir3();
1477
- const allConfigs = listConfigs(undefined, d);
1991
+ const allConfigs = await store.listConfigs();
1478
1992
  for (const file of files) {
1479
1993
  if (shouldSkip(file)) {
1480
1994
  result.skipped.push(file);
@@ -1490,11 +2004,11 @@ async function syncFromDir(dir, opts = {}) {
1490
2004
  const existing = allConfigs.find((c) => c.target_path === targetPath);
1491
2005
  if (!existing) {
1492
2006
  if (!opts.dryRun)
1493
- createConfig({ name: relative(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content }, d);
2007
+ await store.createConfig({ name: relative(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content });
1494
2008
  result.added++;
1495
2009
  } else if (existing.content !== content) {
1496
2010
  if (!opts.dryRun)
1497
- updateConfig(existing.id, { content }, d);
2011
+ await store.updateConfig(existing.id, { content });
1498
2012
  result.updated++;
1499
2013
  } else {
1500
2014
  result.unchanged++;
@@ -1506,17 +2020,17 @@ async function syncFromDir(dir, opts = {}) {
1506
2020
  return result;
1507
2021
  }
1508
2022
  async function syncToDir(dir, opts = {}) {
1509
- const d = opts.db || getDatabase();
2023
+ const store = opts.store ?? resolveConfigStore();
1510
2024
  const home = homedir3();
1511
2025
  const absDir = expandPath(dir);
1512
2026
  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)));
2027
+ const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
1514
2028
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
1515
2029
  for (const config of configs) {
1516
2030
  if (config.kind === "reference")
1517
2031
  continue;
1518
2032
  try {
1519
- const r = await applyConfig(config, { dryRun: opts.dryRun, db: d });
2033
+ const r = await applyConfig(config, { dryRun: opts.dryRun, store });
1520
2034
  r.changed ? result.updated++ : result.unchanged++;
1521
2035
  } catch {
1522
2036
  result.skipped.push(config.target_path || config.id);
@@ -1538,8 +2052,7 @@ function walkDir(dir, files = []) {
1538
2052
  }
1539
2053
  var SKIP;
1540
2054
  var init_sync_dir = __esm(() => {
1541
- init_database();
1542
- init_configs();
2055
+ init_config_store();
1543
2056
  init_apply();
1544
2057
  init_sync();
1545
2058
  SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
@@ -1549,7 +2062,7 @@ var init_sync_dir = __esm(() => {
1549
2062
  var require_package = __commonJS((exports, module) => {
1550
2063
  module.exports = {
1551
2064
  name: "@hasna/instructions",
1552
- version: "0.3.1",
2065
+ version: "0.4.1",
1553
2066
  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
2067
  type: "module",
1555
2068
  main: "dist/index.js",
@@ -1564,10 +2077,6 @@ var require_package = __commonJS((exports, module) => {
1564
2077
  ".": {
1565
2078
  types: "./dist/index.d.ts",
1566
2079
  import: "./dist/index.js"
1567
- },
1568
- "./storage": {
1569
- types: "./dist/storage.d.ts",
1570
- import: "./dist/storage.js"
1571
2080
  }
1572
2081
  },
1573
2082
  files: [
@@ -1578,14 +2087,15 @@ var require_package = __commonJS((exports, module) => {
1578
2087
  ],
1579
2088
  scripts: {
1580
2089
  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",
2090
+ 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",
2091
+ "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
2092
  "build:dashboard": "cd dashboard && bun run build",
1584
2093
  migrate: "bun run src/server/index.ts migrate",
1585
2094
  "generate:sdk": "bun run scripts/generate-sdk.ts",
1586
2095
  "kit:check": "bunx @hasna/contracts vendor-kit --check",
1587
2096
  typecheck: "tsc --noEmit",
1588
2097
  test: "bun test",
2098
+ "check:package-secrets": "bun run src/cli/index.tsx package-manager-scan --fail-on-findings --home .",
1589
2099
  "dev:cli": "bun run src/cli/index.tsx",
1590
2100
  "dev:mcp": "bun run src/mcp/index.ts",
1591
2101
  "dev:serve": "bun run src/server/index.ts",
@@ -1649,420 +2159,12 @@ var require_package = __commonJS((exports, module) => {
1649
2159
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1650
2160
 
1651
2161
  // 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();
2162
+ init_config_store();
2003
2163
  init_apply();
2004
2164
  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
2165
  init_machine();
2166
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2167
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
2066
2168
 
2067
2169
  // src/lib/compact-output.ts
2068
2170
  var DEFAULT_LIST_LIMIT = 20;
@@ -2208,11 +2310,7 @@ var ALL_LEAN_TOOLS = [
2208
2310
  { name: "heartbeat", description: "Update last_seen_at.", inputSchema: { type: "object", properties: { agent_id: { type: "string" } }, required: ["agent_id"] } },
2209
2311
  { name: "set_focus", description: "Set active project context.", inputSchema: { type: "object", properties: { agent_id: { type: "string" }, project_id: { type: "string" } }, required: ["agent_id"] } },
2210
2312
  { 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" } } } } }
2313
+ { 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
2314
  ];
2217
2315
  function ok(data) {
2218
2316
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
@@ -2227,10 +2325,11 @@ function buildServer() {
2227
2325
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: LEAN_TOOLS }));
2228
2326
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
2229
2327
  const { name, arguments: args = {} } = req.params;
2328
+ const store = resolveConfigStore();
2230
2329
  try {
2231
2330
  switch (name) {
2232
2331
  case "list_configs": {
2233
- const configs = listConfigs({
2332
+ const configs = await store.listConfigs({
2234
2333
  category: args["category"] || undefined,
2235
2334
  agent: args["agent"] || undefined,
2236
2335
  kind: args["kind"] || undefined,
@@ -2244,11 +2343,11 @@ function buildServer() {
2244
2343
  }));
2245
2344
  }
2246
2345
  case "get_config": {
2247
- const c = getConfig(args["id_or_slug"]);
2346
+ const c = await store.getConfig(args["id_or_slug"]);
2248
2347
  return ok(c);
2249
2348
  }
2250
2349
  case "create_config": {
2251
- const c = createConfig({
2350
+ const c = await store.createConfig({
2252
2351
  name: args["name"],
2253
2352
  content: args["content"],
2254
2353
  category: args["category"],
@@ -2264,7 +2363,7 @@ function buildServer() {
2264
2363
  return ok({ id: c.id, slug: c.slug, name: c.name });
2265
2364
  }
2266
2365
  case "update_config": {
2267
- const c = updateConfig(args["id_or_slug"], {
2366
+ const c = await store.updateConfig(args["id_or_slug"], {
2268
2367
  content: args["content"],
2269
2368
  name: args["name"],
2270
2369
  tags: args["tags"],
@@ -2277,23 +2376,22 @@ function buildServer() {
2277
2376
  return ok({ id: c.id, slug: c.slug, version: c.version });
2278
2377
  }
2279
2378
  case "delete_config": {
2280
- const { deleteConfig: deleteConfig2 } = await Promise.resolve().then(() => (init_configs(), exports_configs));
2281
- deleteConfig2(args["id_or_slug"]);
2379
+ await store.deleteConfig(args["id_or_slug"]);
2282
2380
  return ok({ deleted: true });
2283
2381
  }
2284
2382
  case "apply_config": {
2285
- const config = getConfig(args["id_or_slug"]);
2286
- const result = await applyConfig(config, { dryRun: args["dry_run"] });
2383
+ const config = await store.getConfig(args["id_or_slug"]);
2384
+ const result = await applyConfig(config, { dryRun: args["dry_run"], store });
2287
2385
  return ok(args["verbose"] ? result : summarizeApplyResult(result));
2288
2386
  }
2289
2387
  case "sync_directory": {
2290
2388
  const dir = args["dir"];
2291
2389
  const direction = args["direction"] || "from_disk";
2292
- const result = direction === "to_disk" ? await syncToDir(dir) : await syncFromDir(dir);
2390
+ const result = direction === "to_disk" ? await syncToDir(dir, { store }) : await syncFromDir(dir, { store });
2293
2391
  return ok(result);
2294
2392
  }
2295
2393
  case "list_profiles": {
2296
- const profiles = listProfiles().map((profile) => summarizeProfile(profile, { verbose: Boolean(args["verbose"]) }));
2394
+ const profiles = (await store.listProfiles()).map((profile) => summarizeProfile(profile, { verbose: Boolean(args["verbose"]) }));
2297
2395
  return ok(pagedPayload(profiles, {
2298
2396
  limit: args["limit"],
2299
2397
  cursor: args["cursor"],
@@ -2308,12 +2406,12 @@ function buildServer() {
2308
2406
  });
2309
2407
  if (!args["auto"] && !args["id_or_slug"])
2310
2408
  return err("id_or_slug is required unless auto=true");
2311
- const profile = args["auto"] ? resolveProfileForMachine(machine) : getProfile(args["id_or_slug"]);
2409
+ const profile = args["auto"] ? await store.resolveProfileForMachine(machine) : await store.getProfile(args["id_or_slug"]);
2312
2410
  if (!profile)
2313
2411
  return err("No matching machine-aware profile found");
2314
- const configs = getProfileConfigs(profile.id);
2412
+ const configs = await store.getProfileConfigs(profile.id);
2315
2413
  const vars = resolveProfileVariables(profile, machine);
2316
- const results = await applyConfigs(configs, { dryRun: args["dry_run"], vars });
2414
+ const results = await applyConfigs(configs, { dryRun: args["dry_run"], vars, store });
2317
2415
  return ok({
2318
2416
  profile: summarizeProfile(profile, { verbose: Boolean(args["verbose"]) }),
2319
2417
  machine: {
@@ -2328,17 +2426,17 @@ function buildServer() {
2328
2426
  });
2329
2427
  }
2330
2428
  case "get_snapshot": {
2331
- const config = getConfig(args["config_id_or_slug"]);
2429
+ const config = await store.getConfig(args["config_id_or_slug"]);
2332
2430
  if (args["version"]) {
2333
- const snap = getSnapshotByVersion(config.id, args["version"]);
2431
+ const snap = await store.getSnapshotByVersion(config.id, args["version"]);
2334
2432
  return snap ? ok(snap) : err("Snapshot not found");
2335
2433
  }
2336
- const snaps = listSnapshots(config.id);
2434
+ const snaps = await store.listSnapshots(config.id);
2337
2435
  return ok(snaps[0] ?? null);
2338
2436
  }
2339
2437
  case "get_status": {
2340
- const stats = getConfigStats();
2341
- const allConfigs = listConfigs({ kind: "file" });
2438
+ const stats = await store.getConfigStats();
2439
+ const allConfigs = await store.listConfigs({ kind: "file" });
2342
2440
  const { existsSync: ex, readFileSync: rf } = await import("fs");
2343
2441
  const { expandPath: expandPath2 } = await Promise.resolve().then(() => (init_apply(), exports_apply));
2344
2442
  const { redactContent: redactContent2 } = await Promise.resolve().then(() => (init_redact(), exports_redact));
@@ -2374,6 +2472,7 @@ function buildServer() {
2374
2472
  case "sync_known": {
2375
2473
  const { syncKnown: syncKnown2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
2376
2474
  const result = await syncKnown2({
2475
+ store,
2377
2476
  agent: args["agent"] || undefined,
2378
2477
  category: args["category"] || undefined
2379
2478
  });
@@ -2382,12 +2481,12 @@ function buildServer() {
2382
2481
  case "sync_project": {
2383
2482
  const { syncProject: syncProject2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
2384
2483
  const dir = args["project_dir"] || process.cwd();
2385
- const result = await syncProject2({ projectDir: dir });
2484
+ const result = await syncProject2({ store, projectDir: dir });
2386
2485
  return ok(result);
2387
2486
  }
2388
2487
  case "render_template": {
2389
2488
  const { renderTemplate: renderTemplate2 } = await Promise.resolve().then(() => (init_template(), exports_template));
2390
- const config = getConfig(args["id_or_slug"]);
2489
+ const config = await store.getConfig(args["id_or_slug"]);
2391
2490
  const vars = args["vars"] || {};
2392
2491
  if (args["use_env"]) {
2393
2492
  const { extractTemplateVars: extractTemplateVars2 } = await Promise.resolve().then(() => (init_template(), exports_template));
@@ -2402,7 +2501,7 @@ function buildServer() {
2402
2501
  }
2403
2502
  case "scan_secrets": {
2404
2503
  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" });
2504
+ const configs = args["id_or_slug"] ? [await store.getConfig(args["id_or_slug"])] : await store.listConfigs({ kind: "file" });
2406
2505
  const findings = [];
2407
2506
  for (const c of configs) {
2408
2507
  const fmt = c.format;
@@ -2411,7 +2510,7 @@ function buildServer() {
2411
2510
  findings.push({ slug: c.slug, secrets: secrets.length, vars: secrets.map((s) => s.varName) });
2412
2511
  if (args["fix"]) {
2413
2512
  const { content, isTemplate: isTemplate2 } = redactContent2(c.content, fmt);
2414
- updateConfig(c.id, { content, is_template: isTemplate2 });
2513
+ await store.updateConfig(c.id, { content, is_template: isTemplate2 });
2415
2514
  }
2416
2515
  }
2417
2516
  }
@@ -2467,27 +2566,15 @@ function buildServer() {
2467
2566
  return ok([..._cfgAgents.values()]);
2468
2567
  }
2469
2568
  case "send_feedback": {
2470
- const { getDatabase: getDatabase2 } = await Promise.resolve().then(() => (init_database(), exports_database));
2471
- const db = getDatabase2();
2472
2569
  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]);
2570
+ await store.sendFeedback({
2571
+ message: args["message"],
2572
+ email: args["email"] || null,
2573
+ category: args["category"] || "general",
2574
+ version: pkg.version
2575
+ });
2474
2576
  return ok({ message: "Feedback saved. Thank you!" });
2475
2577
  }
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
2578
  default:
2492
2579
  return err(`Unknown tool: ${name}`);
2493
2580
  }