@hasna/configs 0.2.3 → 0.2.4

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 (2) hide show
  1. package/dist/mcp/index.js +558 -63
  2. package/package.json +1 -1
package/dist/mcp/index.js CHANGED
@@ -1,32 +1,39 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
-
4
- // src/mcp/index.ts
5
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all)
6
+ __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true,
9
+ configurable: true,
10
+ set: (newValue) => all[name] = () => newValue
11
+ });
12
+ };
13
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
8
14
 
9
15
  // src/types/index.ts
10
- class ConfigNotFoundError extends Error {
11
- constructor(id) {
12
- super(`Config not found: ${id}`);
13
- this.name = "ConfigNotFoundError";
14
- }
15
- }
16
-
17
- class ProfileNotFoundError extends Error {
18
- constructor(id) {
19
- super(`Profile not found: ${id}`);
20
- this.name = "ProfileNotFoundError";
21
- }
22
- }
23
-
24
- class ConfigApplyError extends Error {
25
- constructor(message) {
26
- super(message);
27
- this.name = "ConfigApplyError";
28
- }
29
- }
16
+ var ConfigNotFoundError, ProfileNotFoundError, ConfigApplyError;
17
+ var init_types = __esm(() => {
18
+ ConfigNotFoundError = class ConfigNotFoundError extends Error {
19
+ constructor(id) {
20
+ super(`Config not found: ${id}`);
21
+ this.name = "ConfigNotFoundError";
22
+ }
23
+ };
24
+ ProfileNotFoundError = class ProfileNotFoundError extends Error {
25
+ constructor(id) {
26
+ super(`Profile not found: ${id}`);
27
+ this.name = "ProfileNotFoundError";
28
+ }
29
+ };
30
+ ConfigApplyError = class ConfigApplyError extends Error {
31
+ constructor(message) {
32
+ super(message);
33
+ this.name = "ConfigApplyError";
34
+ }
35
+ };
36
+ });
30
37
 
31
38
  // src/db/database.ts
32
39
  import { Database } from "bun:sqlite";
@@ -57,8 +64,35 @@ function now() {
57
64
  function slugify(name) {
58
65
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
59
66
  }
60
- var MIGRATIONS = [
61
- `
67
+ function getDatabase(path) {
68
+ if (_db)
69
+ return _db;
70
+ const dbPath = path || getDbPath();
71
+ ensureDir(dbPath);
72
+ const db = new Database(dbPath);
73
+ db.run("PRAGMA journal_mode = WAL");
74
+ db.run("PRAGMA foreign_keys = ON");
75
+ applyMigrations(db);
76
+ _db = db;
77
+ return db;
78
+ }
79
+ function applyMigrations(db) {
80
+ let currentVersion = 0;
81
+ try {
82
+ const row = db.query("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get();
83
+ currentVersion = row?.version ?? 0;
84
+ } catch {
85
+ currentVersion = 0;
86
+ }
87
+ for (let i = currentVersion;i < MIGRATIONS.length; i++) {
88
+ db.run(MIGRATIONS[i]);
89
+ db.run(`INSERT OR REPLACE INTO schema_version (version) VALUES (${i + 1})`);
90
+ }
91
+ }
92
+ var MIGRATIONS, _db = null;
93
+ var init_database = __esm(() => {
94
+ MIGRATIONS = [
95
+ `
62
96
  CREATE TABLE IF NOT EXISTS configs (
63
97
  id TEXT PRIMARY KEY,
64
98
  name TEXT NOT NULL,
@@ -116,33 +150,8 @@ var MIGRATIONS = [
116
150
 
117
151
  INSERT OR IGNORE INTO schema_version (version) VALUES (1);
118
152
  `
119
- ];
120
- var _db = null;
121
- function getDatabase(path) {
122
- if (_db)
123
- return _db;
124
- const dbPath = path || getDbPath();
125
- ensureDir(dbPath);
126
- const db = new Database(dbPath);
127
- db.run("PRAGMA journal_mode = WAL");
128
- db.run("PRAGMA foreign_keys = ON");
129
- applyMigrations(db);
130
- _db = db;
131
- return db;
132
- }
133
- function applyMigrations(db) {
134
- let currentVersion = 0;
135
- try {
136
- const row = db.query("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get();
137
- currentVersion = row?.version ?? 0;
138
- } catch {
139
- currentVersion = 0;
140
- }
141
- for (let i = currentVersion;i < MIGRATIONS.length; i++) {
142
- db.run(MIGRATIONS[i]);
143
- db.run(`INSERT OR REPLACE INTO schema_version (version) VALUES (${i + 1})`);
144
- }
145
- }
153
+ ];
154
+ });
146
155
 
147
156
  // src/db/configs.ts
148
157
  function rowToConfig(row) {
@@ -295,11 +304,20 @@ function updateConfig(idOrSlug, input, db) {
295
304
  d.run(`UPDATE configs SET ${updates.join(", ")} WHERE id = ?`, params);
296
305
  return getConfigById(existing.id, d);
297
306
  }
298
-
299
- // src/lib/apply.ts
300
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
301
- import { dirname as dirname2, resolve as resolve2 } from "path";
302
- import { homedir } from "os";
307
+ function getConfigStats(db) {
308
+ const d = db || getDatabase();
309
+ const rows = d.query("SELECT category, COUNT(*) as count FROM configs GROUP BY category").all();
310
+ const stats = { total: 0 };
311
+ for (const row of rows) {
312
+ stats[row.category] = row.count;
313
+ stats["total"] = (stats["total"] || 0) + row.count;
314
+ }
315
+ return stats;
316
+ }
317
+ var init_configs = __esm(() => {
318
+ init_types();
319
+ init_database();
320
+ });
303
321
 
304
322
  // src/db/snapshots.ts
305
323
  function createSnapshot(configId, content, version, db) {
@@ -317,8 +335,14 @@ function getSnapshotByVersion(configId, version, db) {
317
335
  const d = db || getDatabase();
318
336
  return d.query("SELECT * FROM config_snapshots WHERE config_id = ? AND version = ?").get(configId, version);
319
337
  }
338
+ var init_snapshots = __esm(() => {
339
+ init_database();
340
+ });
320
341
 
321
342
  // src/lib/apply.ts
343
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
344
+ import { dirname as dirname2, resolve as resolve2 } from "path";
345
+ import { homedir } from "os";
322
346
  function expandPath(p) {
323
347
  if (p.startsWith("~/")) {
324
348
  return resolve2(homedir(), p.slice(2));
@@ -363,16 +387,192 @@ async function applyConfigs(configs, opts = {}) {
363
387
  }
364
388
  return results;
365
389
  }
390
+ var init_apply = __esm(() => {
391
+ init_types();
392
+ init_database();
393
+ init_configs();
394
+ init_snapshots();
395
+ });
366
396
 
367
- // src/lib/sync.ts
368
- import { extname, join as join3 } from "path";
369
- import { homedir as homedir3 } from "os";
397
+ // src/lib/redact.ts
398
+ function redactShell(content) {
399
+ const redacted = [];
400
+ const lines = content.split(`
401
+ `);
402
+ const out = [];
403
+ for (let i = 0;i < lines.length; i++) {
404
+ const line = lines[i];
405
+ const m = line.match(/^(\s*(?:export\s+)?)([A-Z][A-Z0-9_]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
406
+ if (m) {
407
+ const [, prefix, key, eq, quote, value] = m;
408
+ if (shouldRedactKeyValue(key, value)) {
409
+ const reason = reasonFor(key, value);
410
+ redacted.push({ varName: key, line: i + 1, reason });
411
+ out.push(`${prefix}${key}${eq}${quote}{{${key}}}${quote}`);
412
+ continue;
413
+ }
414
+ }
415
+ out.push(line);
416
+ }
417
+ return { content: out.join(`
418
+ `), redacted, isTemplate: redacted.length > 0 };
419
+ }
420
+ function redactJson(content) {
421
+ const redacted = [];
422
+ const lines = content.split(`
423
+ `);
424
+ const out = [];
425
+ for (let i = 0;i < lines.length; i++) {
426
+ const line = lines[i];
427
+ const m = line.match(/^(\s*"([^"]+)"\s*:\s*)"([^"]+)"(,?)(\s*)$/);
428
+ if (m) {
429
+ const [, prefix, key, value, comma, trail] = m;
430
+ if (shouldRedactKeyValue(key, value)) {
431
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
432
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
433
+ out.push(`${prefix}"{{${varName}}}"${comma}${trail}`);
434
+ continue;
435
+ }
436
+ }
437
+ let newLine = line;
438
+ for (const { re, reason } of VALUE_PATTERNS) {
439
+ newLine = newLine.replace(re, (match) => {
440
+ const varName = `REDACTED_${reason.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
441
+ redacted.push({ varName, line: i + 1, reason });
442
+ return `{{${varName}}}`;
443
+ });
444
+ }
445
+ out.push(newLine);
446
+ }
447
+ return { content: out.join(`
448
+ `), redacted, isTemplate: redacted.length > 0 };
449
+ }
450
+ function redactToml(content) {
451
+ const redacted = [];
452
+ const lines = content.split(`
453
+ `);
454
+ const out = [];
455
+ for (let i = 0;i < lines.length; i++) {
456
+ const line = lines[i];
457
+ const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(['"]?)(.+?)\4\s*$/);
458
+ if (m) {
459
+ const [, indent, key, eq, quote, value] = m;
460
+ if (shouldRedactKeyValue(key, value)) {
461
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
462
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
463
+ out.push(`${indent}${key}${eq}${quote}{{${varName}}}${quote}`);
464
+ continue;
465
+ }
466
+ }
467
+ out.push(line);
468
+ }
469
+ return { content: out.join(`
470
+ `), redacted, isTemplate: redacted.length > 0 };
471
+ }
472
+ function redactIni(content) {
473
+ const redacted = [];
474
+ const lines = content.split(`
475
+ `);
476
+ const out = [];
477
+ for (let i = 0;i < lines.length; i++) {
478
+ const line = lines[i];
479
+ const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
480
+ if (authM && !authM[2].startsWith("{{")) {
481
+ redacted.push({ varName: "NPM_AUTH_TOKEN", line: i + 1, reason: "npm auth token" });
482
+ out.push(`${authM[1]}{{NPM_AUTH_TOKEN}}`);
483
+ continue;
484
+ }
485
+ const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
486
+ if (m) {
487
+ const [, indent, key, eq, value] = m;
488
+ if (shouldRedactKeyValue(key, value)) {
489
+ const varName = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
490
+ redacted.push({ varName, line: i + 1, reason: reasonFor(key, value) });
491
+ out.push(`${indent}${key}${eq}{{${varName}}}`);
492
+ continue;
493
+ }
494
+ }
495
+ out.push(line);
496
+ }
497
+ return { content: out.join(`
498
+ `), redacted, isTemplate: redacted.length > 0 };
499
+ }
500
+ function redactGeneric(content) {
501
+ const redacted = [];
502
+ const lines = content.split(`
503
+ `);
504
+ const out = [];
505
+ for (let i = 0;i < lines.length; i++) {
506
+ let line = lines[i];
507
+ for (const { re, reason } of VALUE_PATTERNS) {
508
+ line = line.replace(re, (match) => {
509
+ const varName = reason.toUpperCase().replace(/[^A-Z0-9]/g, "_");
510
+ redacted.push({ varName, line: i + 1, reason });
511
+ return `{{${varName}}}`;
512
+ });
513
+ }
514
+ out.push(line);
515
+ }
516
+ return { content: out.join(`
517
+ `), redacted, isTemplate: redacted.length > 0 };
518
+ }
519
+ function shouldRedactKeyValue(key, value) {
520
+ if (!value || value.startsWith("{{"))
521
+ return false;
522
+ if (value.length < MIN_SECRET_VALUE_LEN)
523
+ return false;
524
+ if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
525
+ return false;
526
+ if (SECRET_KEY_PATTERN.test(key))
527
+ return true;
528
+ for (const { re } of VALUE_PATTERNS) {
529
+ if (re.test(value))
530
+ return true;
531
+ }
532
+ return false;
533
+ }
534
+ function reasonFor(key, value) {
535
+ if (SECRET_KEY_PATTERN.test(key))
536
+ return `secret key name: ${key}`;
537
+ for (const { re, reason } of VALUE_PATTERNS) {
538
+ if (re.test(value))
539
+ return reason;
540
+ }
541
+ return "secret value pattern";
542
+ }
543
+ function redactContent(content, format) {
544
+ switch (format) {
545
+ case "shell":
546
+ return redactShell(content);
547
+ case "json":
548
+ return redactJson(content);
549
+ case "toml":
550
+ return redactToml(content);
551
+ case "ini":
552
+ return redactIni(content);
553
+ default:
554
+ return redactGeneric(content);
555
+ }
556
+ }
557
+ var SECRET_KEY_PATTERN, VALUE_PATTERNS, MIN_SECRET_VALUE_LEN = 8;
558
+ var init_redact = __esm(() => {
559
+ SECRET_KEY_PATTERN = /^(.*_?API_?KEY|.*_?TOKEN|.*_?SECRET|.*_?PASSWORD|.*_?PASSWD|.*_?CREDENTIAL|.*_?AUTH(?:_TOKEN|_KEY|ORIZATION)?|.*_?PRIVATE_?KEY|.*_?ACCESS_?KEY|.*_?CLIENT_?SECRET|.*_?SIGNING_?KEY|.*_?ENCRYPTION_?KEY|.*_AUTH_TOKEN)$/i;
560
+ VALUE_PATTERNS = [
561
+ { re: /npm_[A-Za-z0-9]{36,}/, reason: "npm token" },
562
+ { re: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
563
+ { re: /sk-ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
564
+ { re: /sk-[A-Za-z0-9]{48,}/, reason: "OpenAI API key" },
565
+ { re: /xoxb-[0-9]+-[A-Za-z0-9\-]+/, reason: "Slack bot token" },
566
+ { re: /AIza[0-9A-Za-z\-_]{35}/, reason: "Google API key" },
567
+ { re: /ey[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\./, reason: "JWT token" },
568
+ { re: /AKIA[0-9A-Z]{16}/, reason: "AWS access key" }
569
+ ];
570
+ });
370
571
 
371
572
  // src/lib/sync-dir.ts
372
573
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
373
574
  import { join as join2, relative } from "path";
374
575
  import { homedir as homedir2 } from "os";
375
- var SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
376
576
  function shouldSkip(p) {
377
577
  return SKIP.some((s) => p.includes(s));
378
578
  }
@@ -446,7 +646,225 @@ function walkDir(dir, files = []) {
446
646
  }
447
647
  return files;
448
648
  }
649
+ var SKIP;
650
+ var init_sync_dir = __esm(() => {
651
+ init_database();
652
+ init_configs();
653
+ init_apply();
654
+ init_sync();
655
+ SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
656
+ });
657
+
449
658
  // src/lib/sync.ts
659
+ var exports_sync = {};
660
+ __export(exports_sync, {
661
+ syncToDisk: () => syncToDisk,
662
+ syncToDir: () => syncToDir,
663
+ syncProject: () => syncProject,
664
+ syncKnown: () => syncKnown,
665
+ syncFromDir: () => syncFromDir,
666
+ diffConfig: () => diffConfig,
667
+ detectFormat: () => detectFormat,
668
+ detectCategory: () => detectCategory,
669
+ detectAgent: () => detectAgent,
670
+ PROJECT_CONFIG_FILES: () => PROJECT_CONFIG_FILES,
671
+ KNOWN_CONFIGS: () => KNOWN_CONFIGS
672
+ });
673
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
674
+ import { extname, join as join3 } from "path";
675
+ import { homedir as homedir3 } from "os";
676
+ async function syncProject(opts) {
677
+ const d = opts.db || getDatabase();
678
+ const absDir = expandPath(opts.projectDir);
679
+ const projectName = absDir.split("/").pop() || "project";
680
+ const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
681
+ const allConfigs = listConfigs(undefined, d);
682
+ for (const pf of PROJECT_CONFIG_FILES) {
683
+ const abs = join3(absDir, pf.file);
684
+ if (!existsSync4(abs))
685
+ continue;
686
+ try {
687
+ const rawContent = readFileSync3(abs, "utf-8");
688
+ if (rawContent.length > 500000) {
689
+ result.skipped.push(pf.file);
690
+ continue;
691
+ }
692
+ const { content, isTemplate } = redactContent(rawContent, pf.format);
693
+ const name = `${projectName}/${pf.file}`;
694
+ const targetPath = abs.replace(homedir3(), "~");
695
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
696
+ const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
697
+ if (!existing) {
698
+ if (!opts.dryRun)
699
+ createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate }, d);
700
+ result.added++;
701
+ } else if (existing.content !== content) {
702
+ if (!opts.dryRun)
703
+ updateConfig(existing.id, { content, is_template: isTemplate }, d);
704
+ result.updated++;
705
+ } else {
706
+ result.unchanged++;
707
+ }
708
+ } catch {
709
+ result.skipped.push(pf.file);
710
+ }
711
+ }
712
+ const rulesDir = join3(absDir, ".claude", "rules");
713
+ if (existsSync4(rulesDir)) {
714
+ const mdFiles = readdirSync2(rulesDir).filter((f) => f.endsWith(".md"));
715
+ for (const f of mdFiles) {
716
+ const abs = join3(rulesDir, f);
717
+ const raw = readFileSync3(abs, "utf-8");
718
+ const { content, isTemplate } = redactContent(raw, "markdown");
719
+ const name = `${projectName}/rules/${f}`;
720
+ const targetPath = abs.replace(homedir3(), "~");
721
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
722
+ const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
723
+ if (!existing) {
724
+ if (!opts.dryRun)
725
+ createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate }, d);
726
+ result.added++;
727
+ } else if (existing.content !== content) {
728
+ if (!opts.dryRun)
729
+ updateConfig(existing.id, { content, is_template: isTemplate }, d);
730
+ result.updated++;
731
+ } else {
732
+ result.unchanged++;
733
+ }
734
+ }
735
+ }
736
+ return result;
737
+ }
738
+ async function syncKnown(opts = {}) {
739
+ const d = opts.db || getDatabase();
740
+ const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
741
+ const home = homedir3();
742
+ let targets = KNOWN_CONFIGS;
743
+ if (opts.agent)
744
+ targets = targets.filter((k) => k.agent === opts.agent);
745
+ if (opts.category)
746
+ targets = targets.filter((k) => k.category === opts.category);
747
+ const allConfigs = listConfigs(undefined, d);
748
+ for (const known of targets) {
749
+ if (known.rulesDir) {
750
+ const absDir = expandPath(known.rulesDir);
751
+ if (!existsSync4(absDir)) {
752
+ result.skipped.push(known.rulesDir);
753
+ continue;
754
+ }
755
+ const mdFiles = readdirSync2(absDir).filter((f) => f.endsWith(".md"));
756
+ for (const f of mdFiles) {
757
+ const abs2 = join3(absDir, f);
758
+ const targetPath = abs2.replace(home, "~");
759
+ const raw = readFileSync3(abs2, "utf-8");
760
+ const { content, isTemplate } = redactContent(raw, "markdown");
761
+ const name = `claude-rules-${f}`;
762
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
763
+ const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
764
+ if (!existing) {
765
+ if (!opts.dryRun)
766
+ createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate }, d);
767
+ result.added++;
768
+ } else if (existing.content !== content) {
769
+ if (!opts.dryRun)
770
+ updateConfig(existing.id, { content, is_template: isTemplate }, d);
771
+ result.updated++;
772
+ } else {
773
+ result.unchanged++;
774
+ }
775
+ }
776
+ continue;
777
+ }
778
+ const abs = expandPath(known.path);
779
+ if (!existsSync4(abs)) {
780
+ result.skipped.push(known.path);
781
+ continue;
782
+ }
783
+ try {
784
+ const rawContent = readFileSync3(abs, "utf-8");
785
+ if (rawContent.length > 500000) {
786
+ result.skipped.push(known.path + " (too large)");
787
+ continue;
788
+ }
789
+ const fmt = known.format ?? detectFormat(abs);
790
+ const { content, isTemplate } = redactContent(rawContent, fmt);
791
+ const targetPath = abs.replace(home, "~");
792
+ const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === known.name);
793
+ if (!existing) {
794
+ if (!opts.dryRun) {
795
+ createConfig({
796
+ name: known.name,
797
+ category: known.category,
798
+ agent: known.agent,
799
+ format: fmt,
800
+ content,
801
+ target_path: known.kind === "reference" ? null : targetPath,
802
+ kind: known.kind ?? "file",
803
+ description: known.description,
804
+ is_template: isTemplate
805
+ }, d);
806
+ }
807
+ result.added++;
808
+ } else if (existing.content !== content) {
809
+ if (!opts.dryRun)
810
+ updateConfig(existing.id, { content, is_template: isTemplate }, d);
811
+ result.updated++;
812
+ } else {
813
+ result.unchanged++;
814
+ }
815
+ } catch {
816
+ result.skipped.push(known.path);
817
+ }
818
+ }
819
+ return result;
820
+ }
821
+ async function syncToDisk(opts = {}) {
822
+ const d = opts.db || getDatabase();
823
+ const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
824
+ let configs = listConfigs({ kind: "file", ...opts.agent ? { agent: opts.agent } : {}, ...opts.category ? { category: opts.category } : {} }, d);
825
+ for (const config of configs) {
826
+ if (!config.target_path)
827
+ continue;
828
+ try {
829
+ const r = await applyConfig(config, { dryRun: opts.dryRun, db: d });
830
+ r.changed ? result.updated++ : result.unchanged++;
831
+ } catch {
832
+ result.skipped.push(config.target_path);
833
+ }
834
+ }
835
+ return result;
836
+ }
837
+ function diffConfig(config) {
838
+ if (!config.target_path)
839
+ return "(reference \u2014 no target path)";
840
+ const path = expandPath(config.target_path);
841
+ if (!existsSync4(path))
842
+ return `(file not found on disk: ${path})`;
843
+ const diskContent = readFileSync3(path, "utf-8");
844
+ if (diskContent === config.content)
845
+ return "(no diff \u2014 identical)";
846
+ const stored = config.content.split(`
847
+ `);
848
+ const disk = diskContent.split(`
849
+ `);
850
+ const lines = [`--- stored (DB)`, `+++ disk (${path})`];
851
+ const maxLen = Math.max(stored.length, disk.length);
852
+ for (let i = 0;i < maxLen; i++) {
853
+ const s = stored[i];
854
+ const dk = disk[i];
855
+ if (s === dk) {
856
+ if (s !== undefined)
857
+ lines.push(` ${s}`);
858
+ } else {
859
+ if (s !== undefined)
860
+ lines.push(`-${s}`);
861
+ if (dk !== undefined)
862
+ lines.push(`+${dk}`);
863
+ }
864
+ }
865
+ return lines.join(`
866
+ `);
867
+ }
450
868
  function detectCategory(filePath) {
451
869
  const p = filePath.toLowerCase().replace(homedir3(), "~");
452
870
  if (p.includes("/.claude/rules/") || p.endsWith("claude.md") || p.endsWith("agents.md") || p.endsWith("gemini.md"))
@@ -495,8 +913,56 @@ function detectFormat(filePath) {
495
913
  return "ini";
496
914
  return "text";
497
915
  }
916
+ var KNOWN_CONFIGS, PROJECT_CONFIG_FILES;
917
+ var init_sync = __esm(() => {
918
+ init_database();
919
+ init_configs();
920
+ init_apply();
921
+ init_redact();
922
+ init_sync_dir();
923
+ KNOWN_CONFIGS = [
924
+ { path: "~/.claude/CLAUDE.md", name: "claude-claude-md", category: "rules", agent: "claude", format: "markdown" },
925
+ { path: "~/.claude/settings.json", name: "claude-settings", category: "agent", agent: "claude", format: "json" },
926
+ { path: "~/.claude/settings.local.json", name: "claude-settings-local", category: "agent", agent: "claude", format: "json" },
927
+ { path: "~/.claude/keybindings.json", name: "claude-keybindings", category: "agent", agent: "claude", format: "json" },
928
+ { path: "~/.claude/rules", name: "claude-rules", category: "rules", agent: "claude", rulesDir: "~/.claude/rules" },
929
+ { path: "~/.codex/config.toml", name: "codex-config", category: "agent", agent: "codex", format: "toml" },
930
+ { path: "~/.codex/AGENTS.md", name: "codex-agents-md", category: "rules", agent: "codex", format: "markdown" },
931
+ { path: "~/.gemini/settings.json", name: "gemini-settings", category: "agent", agent: "gemini", format: "json" },
932
+ { path: "~/.gemini/GEMINI.md", name: "gemini-gemini-md", category: "rules", agent: "gemini", format: "markdown" },
933
+ { path: "~/.claude.json", name: "claude-json", category: "mcp", agent: "claude", format: "json", description: "Claude Code global config (includes MCP server entries)" },
934
+ { path: "~/.zshrc", name: "zshrc", category: "shell", agent: "zsh" },
935
+ { path: "~/.zprofile", name: "zprofile", category: "shell", agent: "zsh" },
936
+ { path: "~/.bashrc", name: "bashrc", category: "shell", agent: "zsh" },
937
+ { path: "~/.bash_profile", name: "bash-profile", category: "shell", agent: "zsh" },
938
+ { path: "~/.gitconfig", name: "gitconfig", category: "git", agent: "git", format: "ini" },
939
+ { path: "~/.gitignore_global", name: "gitignore-global", category: "git", agent: "git" },
940
+ { path: "~/.npmrc", name: "npmrc", category: "tools", agent: "npm", format: "ini" },
941
+ { path: "~/.bunfig.toml", name: "bunfig", category: "tools", agent: "global", format: "toml" }
942
+ ];
943
+ PROJECT_CONFIG_FILES = [
944
+ { file: "CLAUDE.md", category: "rules", agent: "claude", format: "markdown" },
945
+ { file: ".claude/settings.json", category: "agent", agent: "claude", format: "json" },
946
+ { file: ".claude/settings.local.json", category: "agent", agent: "claude", format: "json" },
947
+ { file: ".mcp.json", category: "mcp", agent: "claude", format: "json" },
948
+ { file: "AGENTS.md", category: "rules", agent: "codex", format: "markdown" },
949
+ { file: ".codex/AGENTS.md", category: "rules", agent: "codex", format: "markdown" },
950
+ { file: "GEMINI.md", category: "rules", agent: "gemini", format: "markdown" }
951
+ ];
952
+ });
953
+
954
+ // src/mcp/index.ts
955
+ init_configs();
956
+ init_apply();
957
+ init_sync();
958
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
959
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
960
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
498
961
 
499
962
  // src/db/profiles.ts
963
+ init_types();
964
+ init_database();
965
+ init_configs();
500
966
  function rowToProfile(row) {
501
967
  return { ...row };
502
968
  }
@@ -522,6 +988,8 @@ function getProfileConfigs(profileIdOrSlug, db) {
522
988
  }
523
989
 
524
990
  // src/mcp/index.ts
991
+ init_apply();
992
+ init_snapshots();
525
993
  var TOOL_DOCS = {
526
994
  list_configs: "List configs. Params: category?, agent?, kind?, search?. Returns array of config objects.",
527
995
  get_config: "Get a config by id or slug. Returns full config including content.",
@@ -532,6 +1000,8 @@ var TOOL_DOCS = {
532
1000
  list_profiles: "List all profiles. Returns array of profile objects.",
533
1001
  apply_profile: "Apply all configs in a profile to disk. Params: id_or_slug, dry_run?. Returns array of apply results.",
534
1002
  get_snapshot: "Get snapshot(s) for a config. Params: config_id_or_slug, version?. Returns latest snapshot or specific version.",
1003
+ get_status: "Single-call orientation. Returns: total configs, counts by category, drifted count, unredacted secrets, templates, DB path.",
1004
+ sync_known: "Sync all known config files from disk into DB. Params: agent?, category?. Replaces sync_directory for standard use.",
535
1005
  search_tools: "Search tool descriptions. Params: query. Returns matching tool names and descriptions.",
536
1006
  describe_tools: "Get full descriptions for tools. Params: names? (array). Returns tool docs."
537
1007
  };
@@ -545,6 +1015,8 @@ var LEAN_TOOLS = [
545
1015
  { name: "list_profiles", inputSchema: { type: "object", properties: {} } },
546
1016
  { name: "apply_profile", inputSchema: { type: "object", properties: { id_or_slug: { type: "string" }, dry_run: { type: "boolean" } }, required: ["id_or_slug"] } },
547
1017
  { name: "get_snapshot", inputSchema: { type: "object", properties: { config_id_or_slug: { type: "string" }, version: { type: "number" } }, required: ["config_id_or_slug"] } },
1018
+ { name: "get_status", inputSchema: { type: "object", properties: {} } },
1019
+ { name: "sync_known", inputSchema: { type: "object", properties: { agent: { type: "string" }, category: { type: "string" } } } },
548
1020
  { name: "search_tools", inputSchema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] } },
549
1021
  { name: "describe_tools", inputSchema: { type: "object", properties: { names: { type: "array", items: { type: "string" } } } } }
550
1022
  ];
@@ -628,6 +1100,29 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
628
1100
  const snaps = listSnapshots(config.id);
629
1101
  return ok(snaps[0] ?? null);
630
1102
  }
1103
+ case "get_status": {
1104
+ const stats = getConfigStats();
1105
+ const allConfigs = listConfigs({ kind: "file" });
1106
+ let drifted = 0, secrets = 0, templates = 0;
1107
+ for (const c of allConfigs) {
1108
+ if (c.is_template)
1109
+ templates++;
1110
+ }
1111
+ return ok({
1112
+ total: stats["total"] || 0,
1113
+ by_category: Object.fromEntries(Object.entries(stats).filter(([k]) => k !== "total")),
1114
+ templates,
1115
+ db_path: process.env["CONFIGS_DB_PATH"] || "~/.configs/configs.db"
1116
+ });
1117
+ }
1118
+ case "sync_known": {
1119
+ const { syncKnown: syncKnown2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
1120
+ const result = await syncKnown2({
1121
+ agent: args["agent"] || undefined,
1122
+ category: args["category"] || undefined
1123
+ });
1124
+ return ok(result);
1125
+ }
631
1126
  case "search_tools": {
632
1127
  const query = (args["query"] || "").toLowerCase();
633
1128
  const matches = Object.entries(TOOL_DOCS).filter(([k, v]) => k.includes(query) || v.toLowerCase().includes(query)).map(([name2, description]) => ({ name: name2, description }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/configs",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "AI coding agent configuration manager — store, version, apply, and share all your AI coding configs. CLI + MCP + REST API + Dashboard.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",