md4ai 0.10.2 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # md4ai
2
+
3
+ CLI for [MD4AI](https://www.md4ai.com) — scan your Claude Code projects and sync results to a web dashboard.
4
+
5
+ Discovers Claude configuration files, builds dependency graphs, detects orphans and broken references, catalogues skills and marketplace plugins, and pushes everything to a shared dashboard for team visibility.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install -g md4ai
11
+ ```
12
+
13
+ Requires **Node.js 22** or later.
14
+
15
+ ## Setup
16
+
17
+ 1. **Create an account** at [md4ai.com](https://www.md4ai.com) and create a project.
18
+
19
+ 2. **Set the Supabase key** in your shell profile:
20
+
21
+ ```bash
22
+ export MD4AI_SUPABASE_ANON_KEY="your-anon-key"
23
+ ```
24
+
25
+ 3. **Log in:**
26
+
27
+ ```bash
28
+ md4ai login
29
+ ```
30
+
31
+ 4. **Link your project:**
32
+
33
+ ```bash
34
+ cd /path/to/your-claude-project
35
+ md4ai link <project-id>
36
+ ```
37
+
38
+ The project ID is in the URL when viewing your project on the dashboard.
39
+
40
+ ## Commands
41
+
42
+ ### Scanning & Syncing
43
+
44
+ | Command | Description |
45
+ |---------|-------------|
46
+ | `md4ai scan [path]` | Scan a Claude project and push results to the dashboard. Defaults to current directory. |
47
+ | `md4ai scan --offline` | Scan locally only — generates `output/index.html` without pushing to Supabase. |
48
+ | `md4ai sync` | Re-push the most recent scan data for the current project. |
49
+ | `md4ai sync --all` | Re-scan and sync all linked projects on this device. |
50
+ | `md4ai link <project-id>` | Link the current directory to a dashboard project and run an initial scan. |
51
+
52
+ ### Analysis
53
+
54
+ | Command | Description |
55
+ |---------|-------------|
56
+ | `md4ai simulate <prompt>` | Show which files Claude Code would load for a given prompt. |
57
+ | `md4ai print <title>` | Generate a printable A3 wall-chart HTML with the dependency graph and skills table. |
58
+ | `md4ai init-manifest` | Scaffold an `env-manifest.md` from detected `.env` files in the project. |
59
+
60
+ ### Account & Device Management
61
+
62
+ | Command | Description |
63
+ |---------|-------------|
64
+ | `md4ai login` | Authenticate with email and password. |
65
+ | `md4ai logout` | Clear stored credentials. |
66
+ | `md4ai status` | Show login status, linked folders, and last sync time. |
67
+ | `md4ai add-folder` | Create a new project folder on the dashboard. |
68
+ | `md4ai add-device` | Add a device path to an existing project. |
69
+ | `md4ai list-devices` | List all devices and their linked projects. |
70
+
71
+ ### Monitoring
72
+
73
+ | Command | Description |
74
+ |---------|-------------|
75
+ | `md4ai mcp-watch` | Monitor MCP server status on this device (runs until Ctrl+C, polls every 30s). |
76
+
77
+ ### Other
78
+
79
+ | Command | Description |
80
+ |---------|-------------|
81
+ | `md4ai import <zipfile>` | Import an exported team bundle. |
82
+ | `md4ai update` | Check for updates and install if available. |
83
+ | `md4ai config set <key> <value>` | Set a configuration value (e.g. `vercel-token`). |
84
+
85
+ ## What Gets Scanned
86
+
87
+ Running `md4ai scan` discovers files in `.claude/`, `CLAUDE.md`, `skills.md`, and `docs/plans/`. It then:
88
+
89
+ - **Builds a dependency graph** by parsing markdown links, bare file paths, `$CLAUDE_PROJECT_DIR` references, and JSON hook configurations.
90
+ - **Detects orphans** — files not reachable from any root configuration.
91
+ - **Finds broken references** — links pointing to files that don't exist on disk.
92
+ - **Catalogues skills** — both project-specific and machine-wide, including marketplace plugins.
93
+ - **Flags stale files** — anything not modified in over 90 days.
94
+ - **Scans environment variables** — if an `env-manifest.md` is present, checks local `.env` files, Vercel, and GitHub Secrets for drift.
95
+ - **Detects tooling** — frameworks, runtimes, and packages from `package.json` and MCP settings.
96
+
97
+ Scan output:
98
+
99
+ ```
100
+ Files found: 41
101
+ References: 41
102
+ Broken refs: 2
103
+ Orphans: 3
104
+ Stale files: 0
105
+ Skills: 19
106
+ Toolings: 55
107
+ Env Vars: 24 (manifest found)
108
+ Plugins: 17 (17 skills)
109
+ Data hash: 9868c4a8b50f...
110
+ ```
111
+
112
+ ## File Locations
113
+
114
+ | Item | Path |
115
+ |------|------|
116
+ | Credentials | `~/.md4ai/credentials.json` |
117
+ | State | `~/.md4ai/state.json` |
118
+ | Local scan preview | `output/index.html` (in scanned project) |
119
+ | Print exports | `output/print-<timestamp>.html` |
120
+
121
+ ## Web Dashboard
122
+
123
+ All scan data syncs to [md4ai.com](https://www.md4ai.com) where you can:
124
+
125
+ - Browse the dependency graph interactively
126
+ - View file contents with structure navigation
127
+ - Track environment variable drift across local, Vercel, and GitHub
128
+ - Monitor MCP server status per device
129
+ - Share projects with team members
130
+ - Compare skills across machines
131
+
132
+ ## Tech Stack
133
+
134
+ - **TypeScript** (strict, ESM)
135
+ - **Commander** for CLI parsing
136
+ - **Supabase** for auth and data storage
137
+ - **esbuild** for bundling
138
+
139
+ ## Support
140
+
141
+ For questions, feedback, or bug reports: [richard@mediahq2.com](mailto:richard@mediahq2.com)
142
+
143
+ ## Licence
144
+
145
+ MIT — see [LICENCE](https://github.com/Media-HQ-2-Ltd/MD4AI/blob/main/LICENSE) for details.
@@ -83,12 +83,8 @@ var init_dist = __esm({
83
83
  import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
84
84
  import { join } from "node:path";
85
85
  import { homedir } from "node:os";
86
- import { existsSync } from "node:fs";
87
86
  async function ensureConfigDir() {
88
- if (!existsSync(configPath)) {
89
- await mkdir(configPath, { recursive: true });
90
- await chmod(configPath, 448);
91
- }
87
+ await mkdir(configPath, { recursive: true, mode: 448 });
92
88
  }
93
89
  async function saveCredentials(creds) {
94
90
  await ensureConfigDir();
@@ -115,7 +111,7 @@ async function mergeCredentials(partial) {
115
111
  }
116
112
  async function clearCredentials() {
117
113
  try {
118
- await writeFile(credentialsPath, "{}", "utf-8");
114
+ await writeFile(credentialsPath, "{}", { mode: 384 });
119
115
  } catch {
120
116
  }
121
117
  }
@@ -290,7 +286,7 @@ var init_auth = __esm({
290
286
 
291
287
  // dist/scanner/file-parser.js
292
288
  import { readFile as readFile2 } from "node:fs/promises";
293
- import { existsSync as existsSync2 } from "node:fs";
289
+ import { existsSync } from "node:fs";
294
290
  import { resolve as resolve2, dirname, join as join2 } from "node:path";
295
291
  import { homedir as homedir2 } from "node:os";
296
292
  async function parseFileReferences(filePath, projectRoot) {
@@ -318,7 +314,7 @@ async function parseFileReferences(filePath, projectRoot) {
318
314
  if (!target.includes("/") && /^[A-Z]/.test(baseName))
319
315
  continue;
320
316
  const resolved = resolveTarget(target, filePath, projectRoot);
321
- if (resolved && existsSync2(resolved)) {
317
+ if (resolved && existsSync(resolved)) {
322
318
  addRef(refs, relPath, resolved, projectRoot);
323
319
  } else if (resolved) {
324
320
  const targetRel = resolved.startsWith(projectRoot) ? resolved.slice(projectRoot.length + 1) : resolved;
@@ -361,7 +357,7 @@ function resolveTarget(target, sourceFilePath, projectRoot) {
361
357
  }
362
358
  if (target.includes("/")) {
363
359
  const fromRoot = join2(projectRoot, target);
364
- if (existsSync2(fromRoot))
360
+ if (existsSync(fromRoot))
365
361
  return fromRoot;
366
362
  }
367
363
  return resolve2(dirname(sourceFilePath), target);
@@ -387,7 +383,7 @@ function parseJsonPathReferences(content, fromRel, projectRoot, refs, brokenRefs
387
383
  while ((match = pattern.exec(value)) !== null) {
388
384
  const relTarget = match[1];
389
385
  const resolved = join2(projectRoot, relTarget);
390
- if (existsSync2(resolved)) {
386
+ if (existsSync(resolved)) {
391
387
  addRef(refs, fromRel, resolved, projectRoot);
392
388
  } else {
393
389
  brokenRefs.push({ from: fromRel, to: relTarget, rawRef: relTarget });
@@ -401,7 +397,7 @@ function parseJsonPathReferences(content, fromRel, projectRoot, refs, brokenRefs
401
397
  if (!target || target.startsWith("http"))
402
398
  continue;
403
399
  const resolved = join2(projectRoot, target);
404
- if (existsSync2(resolved)) {
400
+ if (existsSync(resolved)) {
405
401
  addRef(refs, fromRel, resolved, projectRoot);
406
402
  } else {
407
403
  brokenRefs.push({ from: fromRel, to: target, rawRef: target });
@@ -609,7 +605,7 @@ var init_orphan_detector = __esm({
609
605
  // dist/scanner/skills-parser.js
610
606
  import { execFileSync as execFileSync2 } from "node:child_process";
611
607
  import { readFile as readFile3 } from "node:fs/promises";
612
- import { existsSync as existsSync3 } from "node:fs";
608
+ import { existsSync as existsSync2 } from "node:fs";
613
609
  import { join as join5 } from "node:path";
614
610
  import { homedir as homedir3 } from "node:os";
615
611
  async function parseSkills(projectRoot) {
@@ -638,7 +634,7 @@ async function parseSkills(projectRoot) {
638
634
  await parseSettingsForPlugins(projectSettings, skills, false);
639
635
  await parseSettingsForPlugins(projectLocalSettings, skills, false);
640
636
  const skillsMd = join5(projectRoot, "skills.md");
641
- if (existsSync3(skillsMd)) {
637
+ if (existsSync2(skillsMd)) {
642
638
  const content = await readFile3(skillsMd, "utf-8");
643
639
  const skillNames = content.match(/^#+\s+(.+)$/gm);
644
640
  if (skillNames) {
@@ -661,7 +657,7 @@ async function parseSkills(projectRoot) {
661
657
  return Array.from(skills.values());
662
658
  }
663
659
  async function parseSettingsForPlugins(settingsPath, skills, isMachineWide) {
664
- if (!existsSync3(settingsPath))
660
+ if (!existsSync2(settingsPath))
665
661
  return;
666
662
  try {
667
663
  const content = await readFile3(settingsPath, "utf-8");
@@ -698,7 +694,7 @@ var init_skills_parser = __esm({
698
694
 
699
695
  // dist/scanner/tooling-detector.js
700
696
  import { readFile as readFile4, readdir } from "node:fs/promises";
701
- import { existsSync as existsSync4 } from "node:fs";
697
+ import { existsSync as existsSync3 } from "node:fs";
702
698
  import { join as join6 } from "node:path";
703
699
  import { execFileSync as execFileSync3 } from "node:child_process";
704
700
  import { homedir as homedir4 } from "node:os";
@@ -715,7 +711,7 @@ async function detectToolings(projectRoot) {
715
711
  async function detectFromPackageJson(projectRoot) {
716
712
  const toolings = [];
717
713
  const pkgPath = join6(projectRoot, "package.json");
718
- if (!existsSync4(pkgPath))
714
+ if (!existsSync3(pkgPath))
719
715
  return toolings;
720
716
  const resolvedVersions = await getResolvedVersions(projectRoot);
721
717
  const seen = /* @__PURE__ */ new Set();
@@ -759,7 +755,7 @@ async function discoverWorkspacePackageJsons(projectRoot) {
759
755
  const cleanPattern = pattern.replace(/\/\*\*?$/, "");
760
756
  if (isGlob) {
761
757
  const parentDir = join6(projectRoot, cleanPattern);
762
- if (!existsSync4(parentDir))
758
+ if (!existsSync3(parentDir))
763
759
  continue;
764
760
  try {
765
761
  const entries = await readdir(parentDir, { withFileTypes: true });
@@ -767,7 +763,7 @@ async function discoverWorkspacePackageJsons(projectRoot) {
767
763
  if (!entry.isDirectory())
768
764
  continue;
769
765
  const pkgPath = join6(parentDir, entry.name, "package.json");
770
- if (existsSync4(pkgPath)) {
766
+ if (existsSync3(pkgPath)) {
771
767
  results.push(pkgPath);
772
768
  }
773
769
  }
@@ -775,7 +771,7 @@ async function discoverWorkspacePackageJsons(projectRoot) {
775
771
  }
776
772
  } else {
777
773
  const pkgPath = join6(projectRoot, cleanPattern, "package.json");
778
- if (existsSync4(pkgPath)) {
774
+ if (existsSync3(pkgPath)) {
779
775
  results.push(pkgPath);
780
776
  }
781
777
  }
@@ -784,7 +780,7 @@ async function discoverWorkspacePackageJsons(projectRoot) {
784
780
  }
785
781
  async function getWorkspacePatterns(projectRoot) {
786
782
  const pnpmWorkspace = join6(projectRoot, "pnpm-workspace.yaml");
787
- if (existsSync4(pnpmWorkspace)) {
783
+ if (existsSync3(pnpmWorkspace)) {
788
784
  try {
789
785
  const content = await readFile4(pnpmWorkspace, "utf-8");
790
786
  const patterns = [];
@@ -827,7 +823,7 @@ function stripVersionPrefix(version) {
827
823
  async function getResolvedVersions(projectRoot) {
828
824
  const versions = /* @__PURE__ */ new Map();
829
825
  const pnpmLock = join6(projectRoot, "pnpm-lock.yaml");
830
- if (existsSync4(pnpmLock)) {
826
+ if (existsSync3(pnpmLock)) {
831
827
  try {
832
828
  const content = await readFile4(pnpmLock, "utf-8");
833
829
  const versionPattern = /^\s{4}'?(@?[^@\s:]+)(?:@[^:]+)?'?:\s*(?:version:\s*)?'?(\d+\.\d+[^'\s]*)/gm;
@@ -843,7 +839,7 @@ async function getResolvedVersions(projectRoot) {
843
839
  }
844
840
  if (versions.size === 0) {
845
841
  const npmLock = join6(projectRoot, "package-lock.json");
846
- if (existsSync4(npmLock)) {
842
+ if (existsSync3(npmLock)) {
847
843
  try {
848
844
  const content = await readFile4(npmLock, "utf-8");
849
845
  const lock = JSON.parse(content);
@@ -864,7 +860,7 @@ async function getResolvedVersions(projectRoot) {
864
860
  function detectFromCli(projectRoot) {
865
861
  const toolings = [];
866
862
  for (const { name, command, args, conditionFile } of CLI_VERSION_COMMANDS) {
867
- if (conditionFile && !existsSync4(join6(projectRoot, conditionFile))) {
863
+ if (conditionFile && !existsSync3(join6(projectRoot, conditionFile))) {
868
864
  continue;
869
865
  }
870
866
  try {
@@ -897,7 +893,7 @@ async function detectFromMcpSettings(projectRoot) {
897
893
  ];
898
894
  const seen = /* @__PURE__ */ new Set();
899
895
  for (const settingsPath of settingsPaths) {
900
- if (!existsSync4(settingsPath))
896
+ if (!existsSync3(settingsPath))
901
897
  continue;
902
898
  try {
903
899
  const content = await readFile4(settingsPath, "utf-8");
@@ -1042,13 +1038,24 @@ async function fetchVercelEnvVars(projectId, orgId, token) {
1042
1038
  }
1043
1039
  const data = await res.json();
1044
1040
  const envs = data.envs ?? [];
1045
- return envs.map((e) => ({
1046
- key: e.key,
1047
- targets: e.target ?? ["production", "preview", "development"],
1048
- type: e.type ?? "plain",
1049
- inManifest: false
1050
- // populated later by the scanner
1051
- }));
1041
+ const merged = /* @__PURE__ */ new Map();
1042
+ for (const e of envs) {
1043
+ const targets = e.target ?? ["production", "preview", "development"];
1044
+ const existing = merged.get(e.key);
1045
+ if (existing) {
1046
+ const targetSet = /* @__PURE__ */ new Set([...existing.targets, ...targets]);
1047
+ existing.targets = Array.from(targetSet);
1048
+ } else {
1049
+ merged.set(e.key, {
1050
+ key: e.key,
1051
+ targets: [...targets],
1052
+ type: e.type ?? "plain",
1053
+ inManifest: false
1054
+ // populated later by the scanner
1055
+ });
1056
+ }
1057
+ }
1058
+ return Array.from(merged.values());
1052
1059
  }
1053
1060
  var VercelApiError;
1054
1061
  var init_fetch_env_vars = __esm({
@@ -1074,11 +1081,11 @@ import { readFile as readFile7, glob as glob2 } from "node:fs/promises";
1074
1081
  import { execFile } from "node:child_process";
1075
1082
  import { promisify } from "node:util";
1076
1083
  import { join as join9, relative as relative2 } from "node:path";
1077
- import { existsSync as existsSync5 } from "node:fs";
1084
+ import { existsSync as existsSync4 } from "node:fs";
1078
1085
  import chalk8 from "chalk";
1079
1086
  async function scanEnvManifest(projectRoot) {
1080
1087
  const manifestFullPath = join9(projectRoot, MANIFEST_PATH);
1081
- if (!existsSync5(manifestFullPath)) {
1088
+ if (!existsSync4(manifestFullPath)) {
1082
1089
  return null;
1083
1090
  }
1084
1091
  const manifestContent = await readFile7(manifestFullPath, "utf-8");
@@ -1320,7 +1327,7 @@ async function checkLocalEnvFiles(projectRoot, apps) {
1320
1327
  for (const app of apps) {
1321
1328
  const envPath = join9(projectRoot, app.envFilePath);
1322
1329
  const varNames = /* @__PURE__ */ new Set();
1323
- if (existsSync5(envPath)) {
1330
+ if (existsSync4(envPath)) {
1324
1331
  const content = await readFile7(envPath, "utf-8");
1325
1332
  for (const line of content.split("\n")) {
1326
1333
  const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
@@ -1335,7 +1342,7 @@ async function checkLocalEnvFiles(projectRoot, apps) {
1335
1342
  }
1336
1343
  async function parseWorkflowSecrets(projectRoot) {
1337
1344
  const workflowsDir = join9(projectRoot, ".github", "workflows");
1338
- if (!existsSync5(workflowsDir))
1345
+ if (!existsSync4(workflowsDir))
1339
1346
  return [];
1340
1347
  const refs = [];
1341
1348
  const secretRe = /\$\{\{\s*secrets\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
@@ -1416,11 +1423,111 @@ var init_env_manifest_scanner = __esm({
1416
1423
  }
1417
1424
  });
1418
1425
 
1426
+ // dist/scanner/marketplace-scanner.js
1427
+ import { readFile as readFile8, readdir as readdir2, stat } from "node:fs/promises";
1428
+ import { join as join10, resolve as resolve3 } from "node:path";
1429
+ import { existsSync as existsSync5 } from "node:fs";
1430
+ import { homedir as homedir6 } from "node:os";
1431
+ async function scanMarketplacePlugins() {
1432
+ const pluginsDir = join10(homedir6(), ".claude", "plugins");
1433
+ const installedPath = join10(pluginsDir, "installed_plugins.json");
1434
+ const marketplacesPath = join10(pluginsDir, "known_marketplaces.json");
1435
+ if (!existsSync5(installedPath))
1436
+ return [];
1437
+ let installed;
1438
+ let knownMarketplaces = {};
1439
+ try {
1440
+ installed = JSON.parse(await readFile8(installedPath, "utf-8"));
1441
+ } catch {
1442
+ return [];
1443
+ }
1444
+ try {
1445
+ if (existsSync5(marketplacesPath)) {
1446
+ knownMarketplaces = JSON.parse(await readFile8(marketplacesPath, "utf-8"));
1447
+ }
1448
+ } catch {
1449
+ }
1450
+ const results = [];
1451
+ const expectedBase = resolve3(pluginsDir);
1452
+ for (const [key, entries] of Object.entries(installed.plugins ?? {})) {
1453
+ if (!entries?.length)
1454
+ continue;
1455
+ const entry = entries[0];
1456
+ const resolvedInstallPath = resolve3(entry.installPath);
1457
+ if (!resolvedInstallPath.startsWith(expectedBase + "/"))
1458
+ continue;
1459
+ const atIdx = key.lastIndexOf("@");
1460
+ if (atIdx === -1)
1461
+ continue;
1462
+ const pluginName = key.slice(0, atIdx);
1463
+ const marketplace = key.slice(atIdx + 1);
1464
+ const homepageUrl = buildHomepageUrl(knownMarketplaces, marketplace, pluginName);
1465
+ const skills = await discoverSkills(entry.installPath);
1466
+ results.push({
1467
+ marketplace,
1468
+ pluginName,
1469
+ version: entry.version ?? null,
1470
+ homepageUrl,
1471
+ installedAt: entry.installedAt ?? null,
1472
+ lastUpdated: entry.lastUpdated ?? null,
1473
+ skills
1474
+ });
1475
+ }
1476
+ results.sort((a, b) => a.marketplace.localeCompare(b.marketplace) || a.pluginName.localeCompare(b.pluginName));
1477
+ return results;
1478
+ }
1479
+ function buildHomepageUrl(knownMarketplaces, marketplace, pluginName) {
1480
+ const mp = knownMarketplaces[marketplace];
1481
+ if (!mp?.source?.repo)
1482
+ return null;
1483
+ const repo = mp.source.repo;
1484
+ const baseUrl = `https://github.com/${repo}`;
1485
+ if (marketplace === "claude-plugins-official") {
1486
+ return `${baseUrl}/tree/main/plugins/${pluginName}`;
1487
+ }
1488
+ return baseUrl;
1489
+ }
1490
+ async function discoverSkills(installPath) {
1491
+ const skillsDir = join10(installPath, "skills");
1492
+ if (!existsSync5(skillsDir))
1493
+ return [];
1494
+ const skills = [];
1495
+ try {
1496
+ const entries = await readdir2(skillsDir, { withFileTypes: true });
1497
+ for (const entry of entries) {
1498
+ if (!entry.isDirectory())
1499
+ continue;
1500
+ const skillMd = join10(skillsDir, entry.name, "SKILL.md");
1501
+ if (!existsSync5(skillMd))
1502
+ continue;
1503
+ let lastModified = null;
1504
+ try {
1505
+ const fileStat = await stat(skillMd);
1506
+ lastModified = fileStat.mtime.toISOString();
1507
+ } catch {
1508
+ }
1509
+ skills.push({
1510
+ name: entry.name,
1511
+ skillPath: `skills/${entry.name}/SKILL.md`,
1512
+ lastModified
1513
+ });
1514
+ }
1515
+ } catch {
1516
+ }
1517
+ skills.sort((a, b) => a.name.localeCompare(b.name));
1518
+ return skills;
1519
+ }
1520
+ var init_marketplace_scanner = __esm({
1521
+ "dist/scanner/marketplace-scanner.js"() {
1522
+ "use strict";
1523
+ }
1524
+ });
1525
+
1419
1526
  // dist/scanner/index.js
1420
- import { readdir as readdir2 } from "node:fs/promises";
1421
- import { join as join10, relative as relative3 } from "node:path";
1527
+ import { readdir as readdir3 } from "node:fs/promises";
1528
+ import { join as join11, relative as relative3 } from "node:path";
1422
1529
  import { existsSync as existsSync6 } from "node:fs";
1423
- import { homedir as homedir6 } from "node:os";
1530
+ import { homedir as homedir7 } from "node:os";
1424
1531
  import { createHash } from "node:crypto";
1425
1532
  async function scanProject(projectRoot) {
1426
1533
  const allFiles = await discoverFiles(projectRoot);
@@ -1428,7 +1535,7 @@ async function scanProject(projectRoot) {
1428
1535
  const allRefs = [];
1429
1536
  const allBrokenRefs = [];
1430
1537
  for (const file of allFiles) {
1431
- const fullPath = file.startsWith("/") ? file : join10(projectRoot, file);
1538
+ const fullPath = file.startsWith("/") ? file : join11(projectRoot, file);
1432
1539
  try {
1433
1540
  const { refs, brokenRefs: brokenRefs2 } = await parseFileReferences(fullPath, projectRoot);
1434
1541
  allRefs.push(...refs);
@@ -1442,6 +1549,7 @@ async function scanProject(projectRoot) {
1442
1549
  const skills = await parseSkills(projectRoot);
1443
1550
  const toolings = await detectToolings(projectRoot);
1444
1551
  const envManifest = await scanEnvManifest(projectRoot);
1552
+ const marketplacePlugins = await scanMarketplacePlugins();
1445
1553
  const depthMap = /* @__PURE__ */ new Map();
1446
1554
  const queue = [...rootFiles];
1447
1555
  for (const r of queue)
@@ -1462,7 +1570,7 @@ async function scanProject(projectRoot) {
1462
1570
  depth: depthMap.get(br.from) ?? 999
1463
1571
  }));
1464
1572
  brokenRefs.sort((a, b) => a.depth - b.depth || a.from.localeCompare(b.from));
1465
- const scanData = JSON.stringify({ graph, orphans, brokenRefs, skills, staleFiles, toolings, envManifest });
1573
+ const scanData = JSON.stringify({ graph, orphans, brokenRefs, skills, staleFiles, toolings, envManifest, marketplacePlugins });
1466
1574
  const dataHash = createHash("sha256").update(scanData).digest("hex");
1467
1575
  return {
1468
1576
  graph,
@@ -1472,32 +1580,33 @@ async function scanProject(projectRoot) {
1472
1580
  staleFiles,
1473
1581
  toolings,
1474
1582
  envManifest,
1583
+ marketplacePlugins,
1475
1584
  scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
1476
1585
  dataHash
1477
1586
  };
1478
1587
  }
1479
1588
  async function discoverFiles(projectRoot) {
1480
1589
  const files = [];
1481
- const claudeDir = join10(projectRoot, ".claude");
1590
+ const claudeDir = join11(projectRoot, ".claude");
1482
1591
  if (existsSync6(claudeDir)) {
1483
1592
  await walkDir(claudeDir, projectRoot, files);
1484
1593
  }
1485
- if (existsSync6(join10(projectRoot, "CLAUDE.md"))) {
1594
+ if (existsSync6(join11(projectRoot, "CLAUDE.md"))) {
1486
1595
  files.push("CLAUDE.md");
1487
1596
  }
1488
- if (existsSync6(join10(projectRoot, "skills.md"))) {
1597
+ if (existsSync6(join11(projectRoot, "skills.md"))) {
1489
1598
  files.push("skills.md");
1490
1599
  }
1491
- const plansDir = join10(projectRoot, "docs", "plans");
1600
+ const plansDir = join11(projectRoot, "docs", "plans");
1492
1601
  if (existsSync6(plansDir)) {
1493
1602
  await walkDir(plansDir, projectRoot, files);
1494
1603
  }
1495
1604
  return [...new Set(files)];
1496
1605
  }
1497
1606
  async function walkDir(dir, projectRoot, files) {
1498
- const entries = await readdir2(dir, { withFileTypes: true });
1607
+ const entries = await readdir3(dir, { withFileTypes: true });
1499
1608
  for (const entry of entries) {
1500
- const fullPath = join10(dir, entry.name);
1609
+ const fullPath = join11(dir, entry.name);
1501
1610
  if (entry.isDirectory()) {
1502
1611
  if (["node_modules", ".git", ".turbo", "cache", "session-env"].includes(entry.name))
1503
1612
  continue;
@@ -1516,71 +1625,43 @@ function identifyRoots(allFiles, projectRoot) {
1516
1625
  }
1517
1626
  }
1518
1627
  for (const globalFile of GLOBAL_ROOT_FILES) {
1519
- const expanded = globalFile.replace("~", homedir6());
1628
+ const expanded = globalFile.replace("~", homedir7());
1520
1629
  if (existsSync6(expanded)) {
1521
1630
  roots.push(globalFile);
1522
1631
  }
1523
1632
  }
1524
1633
  return roots;
1525
1634
  }
1526
- async function readClaudeConfigFiles(projectRoot) {
1527
- const { readFile: readFile12, stat, glob: glob3 } = await import("node:fs/promises");
1528
- const { join: join16, relative: relative5 } = await import("node:path");
1529
- const { existsSync: existsSync13 } = await import("node:fs");
1530
- const configPatterns = [
1531
- "CLAUDE.md",
1532
- ".claude/CLAUDE.md",
1533
- ".claude/settings.json",
1534
- ".claude/commands/*.md",
1535
- ".claude/skills/*.md"
1536
- ];
1635
+ async function readClaudeConfigFiles(projectRoot, graphFilePaths) {
1636
+ const { readFile: readFile13, stat: stat2 } = await import("node:fs/promises");
1637
+ const { existsSync: existsSync14 } = await import("node:fs");
1638
+ const filePaths = graphFilePaths ?? await discoverFiles(projectRoot);
1537
1639
  const files = [];
1538
1640
  const seen = /* @__PURE__ */ new Set();
1539
- async function addFile(fullPath) {
1540
- const relPath = relative5(projectRoot, fullPath);
1641
+ for (const relPath of filePaths) {
1541
1642
  if (seen.has(relPath))
1542
- return;
1643
+ continue;
1644
+ if (relPath.startsWith("~") || relPath.startsWith("/"))
1645
+ continue;
1543
1646
  seen.add(relPath);
1647
+ const fullPath = join11(projectRoot, relPath);
1648
+ if (!existsSync14(fullPath))
1649
+ continue;
1544
1650
  try {
1545
- const content = await readFile12(fullPath, "utf-8");
1546
- const fileStat = await stat(fullPath);
1651
+ const content = await readFile13(fullPath, "utf-8");
1652
+ const fileStat = await stat2(fullPath);
1547
1653
  const lastMod = getGitLastModified(fullPath, projectRoot);
1548
1654
  files.push({ filePath: relPath, content, sizeBytes: fileStat.size, lastModified: lastMod });
1549
1655
  } catch {
1550
1656
  }
1551
1657
  }
1552
- for (const pattern of configPatterns) {
1553
- for await (const fullPath of glob3(join16(projectRoot, pattern))) {
1554
- if (!existsSync13(fullPath))
1555
- continue;
1556
- await addFile(fullPath);
1557
- }
1558
- }
1559
- const pkgPath = join16(projectRoot, "package.json");
1560
- if (existsSync13(pkgPath)) {
1561
- try {
1562
- const pkgContent = await readFile12(pkgPath, "utf-8");
1563
- const pkg = JSON.parse(pkgContent);
1564
- const preflightCmd = pkg.scripts?.preflight;
1565
- if (preflightCmd) {
1566
- const match = preflightCmd.match(/(?:bash\s+|sh\s+|\.\/)?(\S+\.(?:sh|bash|ts|js|mjs))/);
1567
- if (match) {
1568
- const scriptPath = join16(projectRoot, match[1]);
1569
- if (existsSync13(scriptPath)) {
1570
- await addFile(scriptPath);
1571
- }
1572
- }
1573
- }
1574
- } catch {
1575
- }
1576
- }
1577
1658
  return files;
1578
1659
  }
1579
1660
  function detectStaleFiles(allFiles, projectRoot) {
1580
1661
  const stale = [];
1581
1662
  const now = Date.now();
1582
1663
  for (const file of allFiles) {
1583
- const fullPath = join10(projectRoot, file);
1664
+ const fullPath = join11(projectRoot, file);
1584
1665
  const lastMod = getGitLastModified(fullPath, projectRoot);
1585
1666
  if (lastMod) {
1586
1667
  const days = Math.floor((now - new Date(lastMod).getTime()) / (1e3 * 60 * 60 * 24));
@@ -1602,6 +1683,7 @@ var init_scanner = __esm({
1602
1683
  init_git_dates();
1603
1684
  init_tooling_detector();
1604
1685
  init_env_manifest_scanner();
1686
+ init_marketplace_scanner();
1605
1687
  }
1606
1688
  });
1607
1689
 
@@ -1708,7 +1790,7 @@ function generateOfflineHtml(result, projectRoot) {
1708
1790
  }
1709
1791
  </script>
1710
1792
 
1711
- <script type="application/json" id="scan-data">${dataJson}</script>
1793
+ <script type="application/json" id="scan-data">${dataJson.replace(/<\//g, "<\\/")}</script>
1712
1794
  </body>
1713
1795
  </html>`;
1714
1796
  }
@@ -1785,7 +1867,7 @@ var CURRENT_VERSION;
1785
1867
  var init_check_update = __esm({
1786
1868
  "dist/check-update.js"() {
1787
1869
  "use strict";
1788
- CURRENT_VERSION = true ? "0.10.2" : "0.0.0-dev";
1870
+ CURRENT_VERSION = true ? "0.10.3" : "0.0.0-dev";
1789
1871
  }
1790
1872
  });
1791
1873
 
@@ -1820,7 +1902,7 @@ async function pushHealthResults(supabase, folderId, manifest) {
1820
1902
  if (!checks?.length)
1821
1903
  return;
1822
1904
  const results = [];
1823
- const ranAt = manifest.checkedAt;
1905
+ const ranAt = (/* @__PURE__ */ new Date()).toISOString();
1824
1906
  for (const chk of checks) {
1825
1907
  const config2 = chk.check_config;
1826
1908
  if (chk.check_type === "env_var_set") {
@@ -1885,6 +1967,22 @@ async function pushHealthResults(supabase, folderId, manifest) {
1885
1967
  console.error(chalk10.yellow(`Health results warning: ${error.message}`));
1886
1968
  } else {
1887
1969
  console.log(chalk10.green(` Updated ${results.length} health check result(s).`));
1970
+ const checkIds = results.map((r) => r.check_id);
1971
+ const { data: allResults } = await supabase.from("env_health_results").select("id, check_id, ran_at").in("check_id", checkIds).order("ran_at", { ascending: false });
1972
+ if (allResults) {
1973
+ const seen = /* @__PURE__ */ new Set();
1974
+ const toDelete = [];
1975
+ for (const r of allResults) {
1976
+ if (seen.has(r.check_id)) {
1977
+ toDelete.push(r.id);
1978
+ } else {
1979
+ seen.add(r.check_id);
1980
+ }
1981
+ }
1982
+ if (toDelete.length > 0) {
1983
+ await supabase.from("env_health_results").delete().in("id", toDelete);
1984
+ }
1985
+ }
1888
1986
  }
1889
1987
  }
1890
1988
  }
@@ -1937,14 +2035,14 @@ var map_exports = {};
1937
2035
  __export(map_exports, {
1938
2036
  mapCommand: () => mapCommand
1939
2037
  });
1940
- import { resolve as resolve3, basename } from "node:path";
2038
+ import { resolve as resolve4, basename } from "node:path";
1941
2039
  import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
1942
2040
  import { existsSync as existsSync7 } from "node:fs";
1943
2041
  import chalk11 from "chalk";
1944
2042
  import { select as select3, input as input5, confirm as confirm2 } from "@inquirer/prompts";
1945
2043
  async function mapCommand(path, options) {
1946
2044
  await checkForUpdate();
1947
- const projectRoot = resolve3(path ?? process.cwd());
2045
+ const projectRoot = resolve4(path ?? process.cwd());
1948
2046
  if (!existsSync7(projectRoot)) {
1949
2047
  console.error(chalk11.red(`Path not found: ${projectRoot}`));
1950
2048
  process.exit(1);
@@ -1960,6 +2058,7 @@ async function mapCommand(path, options) {
1960
2058
  console.log(` Skills: ${result.skills.length}`);
1961
2059
  console.log(` Toolings: ${result.toolings.length}`);
1962
2060
  console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
2061
+ console.log(` Plugins: ${result.marketplacePlugins.length} (${result.marketplacePlugins.reduce((n, p) => n + p.skills.length, 0)} skills)`);
1963
2062
  console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
1964
2063
  if (result.brokenRefs.length > 0) {
1965
2064
  console.log(chalk11.red(`
@@ -1971,11 +2070,11 @@ async function mapCommand(path, options) {
1971
2070
  console.log(chalk11.red(` ... and ${result.brokenRefs.length - 5} more`));
1972
2071
  }
1973
2072
  }
1974
- const outputDir = resolve3(projectRoot, "output");
2073
+ const outputDir = resolve4(projectRoot, "output");
1975
2074
  if (!existsSync7(outputDir)) {
1976
2075
  await mkdir2(outputDir, { recursive: true });
1977
2076
  }
1978
- const htmlPath = resolve3(outputDir, "index.html");
2077
+ const htmlPath = resolve4(outputDir, "index.html");
1979
2078
  const html = generateOfflineHtml(result, projectRoot);
1980
2079
  await writeFile2(htmlPath, html, "utf-8");
1981
2080
  console.log(chalk11.green(`
@@ -1999,15 +2098,20 @@ ${proposedFiles.length} file(s) proposed for deletion:
1999
2098
  value: f
2000
2099
  }))
2001
2100
  });
2101
+ const resolvedRoot = resolve4(projectRoot);
2002
2102
  for (const file of toDelete) {
2003
- const fullPath = resolve3(projectRoot, file.file_path);
2103
+ const fullPath = resolve4(projectRoot, file.file_path);
2104
+ if (!fullPath.startsWith(resolvedRoot + "/")) {
2105
+ console.error(chalk11.red(` Blocked path traversal: ${file.file_path}`));
2106
+ continue;
2107
+ }
2004
2108
  try {
2005
2109
  const { unlink } = await import("node:fs/promises");
2006
2110
  await unlink(fullPath);
2007
2111
  await supabase.from("folder_files").delete().eq("id", file.id);
2008
2112
  console.log(chalk11.green(` Deleted: ${file.file_path}`));
2009
2113
  } catch (err) {
2010
- console.error(chalk11.red(` Failed to delete ${file.file_path}: ${err}`));
2114
+ console.error(chalk11.red(` Failed to delete ${file.file_path}: ${err instanceof Error ? err.message : String(err)}`));
2011
2115
  }
2012
2116
  }
2013
2117
  const keptIds = proposedFiles.filter((f) => !toDelete.some((d) => d.id === f.id)).map((f) => f.id);
@@ -2021,22 +2125,34 @@ ${proposedFiles.length} file(s) proposed for deletion:
2021
2125
  } else if (proposedFiles?.length) {
2022
2126
  console.log(chalk11.dim(` ${proposedFiles.length} file(s) proposed for deletion \u2014 skipped (non-interactive mode).`));
2023
2127
  }
2024
- const { error } = await supabase.from("claude_folders").update({
2128
+ let storedManifest = null;
2129
+ if (!result.envManifest) {
2130
+ const { data: stored, error: fetchErr } = await supabase.from("claude_folders").select("env_manifest_json").eq("id", folder_id).single();
2131
+ if (!fetchErr && stored?.env_manifest_json) {
2132
+ storedManifest = stored.env_manifest_json;
2133
+ }
2134
+ }
2135
+ const updatePayload = {
2025
2136
  graph_json: result.graph,
2026
2137
  orphans_json: result.orphans,
2027
2138
  broken_refs_json: result.brokenRefs,
2028
2139
  skills_table_json: result.skills,
2029
2140
  stale_files_json: result.staleFiles,
2030
- env_manifest_json: result.envManifest,
2141
+ marketplace_plugins_json: result.marketplacePlugins,
2031
2142
  last_scanned: result.scannedAt,
2032
2143
  data_hash: result.dataHash
2033
- }).eq("id", folder_id);
2144
+ };
2145
+ if (result.envManifest) {
2146
+ updatePayload.env_manifest_json = result.envManifest;
2147
+ }
2148
+ const { error } = await supabase.from("claude_folders").update(updatePayload).eq("id", folder_id);
2034
2149
  if (error) {
2035
2150
  console.error(chalk11.yellow(`Sync warning: ${error.message}`));
2036
2151
  } else {
2037
2152
  await pushToolings(supabase, folder_id, result.toolings);
2038
- if (result.envManifest) {
2039
- await pushHealthResults(supabase, folder_id, result.envManifest);
2153
+ const manifestForHealth = result.envManifest ?? storedManifest;
2154
+ if (manifestForHealth) {
2155
+ await pushHealthResults(supabase, folder_id, manifestForHealth);
2040
2156
  }
2041
2157
  await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder_id).eq("device_name", device_name);
2042
2158
  await saveState({
@@ -2048,7 +2164,8 @@ ${proposedFiles.length} file(s) proposed for deletion:
2048
2164
  console.log(chalk11.cyan(`
2049
2165
  https://www.md4ai.com/project/${folder_id}
2050
2166
  `));
2051
- const configFiles = await readClaudeConfigFiles(projectRoot);
2167
+ const graphPaths = result.graph.nodes.map((n) => n.filePath);
2168
+ const configFiles = await readClaudeConfigFiles(projectRoot, graphPaths);
2052
2169
  if (configFiles.length > 0) {
2053
2170
  for (const file of configFiles) {
2054
2171
  await supabase.from("folder_files").upsert({
@@ -2120,6 +2237,7 @@ ${proposedFiles.length} file(s) proposed for deletion:
2120
2237
  skills_table_json: result.skills,
2121
2238
  stale_files_json: result.staleFiles,
2122
2239
  env_manifest_json: result.envManifest,
2240
+ marketplace_plugins_json: result.marketplacePlugins,
2123
2241
  last_scanned: result.scannedAt,
2124
2242
  data_hash: result.dataHash
2125
2243
  }).eq("id", folderId);
@@ -2127,7 +2245,8 @@ ${proposedFiles.length} file(s) proposed for deletion:
2127
2245
  if (result.envManifest) {
2128
2246
  await pushHealthResults(sb, folderId, result.envManifest);
2129
2247
  }
2130
- const configFiles = await readClaudeConfigFiles(projectRoot);
2248
+ const graphPaths2 = result.graph.nodes.map((n) => n.filePath);
2249
+ const configFiles = await readClaudeConfigFiles(projectRoot, graphPaths2);
2131
2250
  for (const file of configFiles) {
2132
2251
  await sb.from("folder_files").upsert({
2133
2252
  folder_id: folderId,
@@ -2172,7 +2291,13 @@ var sync_exports = {};
2172
2291
  __export(sync_exports, {
2173
2292
  syncCommand: () => syncCommand
2174
2293
  });
2294
+ import { resolve as resolve5 } from "node:path";
2295
+ import { existsSync as existsSync10 } from "node:fs";
2175
2296
  import chalk14 from "chalk";
2297
+ function isValidProjectPath(p) {
2298
+ const resolved = resolve5(p);
2299
+ return resolved.startsWith("/") && !resolved.includes("..") && existsSync10(resolved);
2300
+ }
2176
2301
  async function syncCommand(options) {
2177
2302
  const { supabase } = await getAuthenticatedClient();
2178
2303
  if (options.all) {
@@ -2182,6 +2307,10 @@ async function syncCommand(options) {
2182
2307
  process.exit(1);
2183
2308
  }
2184
2309
  for (const device of devices) {
2310
+ if (!isValidProjectPath(device.path)) {
2311
+ console.error(chalk14.red(` Skipping invalid path: ${device.path}`));
2312
+ continue;
2313
+ }
2185
2314
  console.log(chalk14.blue(`Syncing: ${device.path}`));
2186
2315
  const { data: proposedAll } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
2187
2316
  if (proposedAll?.length) {
@@ -2219,6 +2348,10 @@ async function syncCommand(options) {
2219
2348
  console.error(chalk14.red("Could not find last synced device/folder."));
2220
2349
  process.exit(1);
2221
2350
  }
2351
+ if (!isValidProjectPath(device.path)) {
2352
+ console.error(chalk14.red(`Invalid project path: ${device.path}`));
2353
+ process.exit(1);
2354
+ }
2222
2355
  console.log(chalk14.blue(`Syncing: ${device.path}`));
2223
2356
  const { data: proposedSingle } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
2224
2357
  if (proposedSingle?.length) {
@@ -2251,16 +2384,16 @@ var init_sync = __esm({
2251
2384
  });
2252
2385
 
2253
2386
  // dist/mcp/read-configs.js
2254
- import { readFile as readFile10 } from "node:fs/promises";
2387
+ import { readFile as readFile11 } from "node:fs/promises";
2255
2388
  import { join as join14 } from "node:path";
2256
- import { homedir as homedir8 } from "node:os";
2257
- import { existsSync as existsSync11 } from "node:fs";
2258
- import { readdir as readdir3 } from "node:fs/promises";
2389
+ import { homedir as homedir9 } from "node:os";
2390
+ import { existsSync as existsSync12 } from "node:fs";
2391
+ import { readdir as readdir4 } from "node:fs/promises";
2259
2392
  async function readJsonSafe(path) {
2260
2393
  try {
2261
- if (!existsSync11(path))
2394
+ if (!existsSync12(path))
2262
2395
  return null;
2263
- const raw = await readFile10(path, "utf-8");
2396
+ const raw = await readFile11(path, "utf-8");
2264
2397
  return JSON.parse(raw);
2265
2398
  } catch {
2266
2399
  return null;
@@ -2320,7 +2453,7 @@ function parseFlatConfig(data, source) {
2320
2453
  return entries;
2321
2454
  }
2322
2455
  async function readAllMcpConfigs() {
2323
- const home = homedir8();
2456
+ const home = homedir9();
2324
2457
  const entries = [];
2325
2458
  const userConfig = await readJsonSafe(join14(home, ".claude.json"));
2326
2459
  entries.push(...parseServers(userConfig, "global"));
@@ -2329,16 +2462,16 @@ async function readAllMcpConfigs() {
2329
2462
  const cwdMcp = await readJsonSafe(join14(process.cwd(), ".mcp.json"));
2330
2463
  entries.push(...parseServers(cwdMcp, "project"));
2331
2464
  const pluginsBase = join14(home, ".claude", "plugins", "marketplaces");
2332
- if (existsSync11(pluginsBase)) {
2465
+ if (existsSync12(pluginsBase)) {
2333
2466
  try {
2334
- const marketplaces = await readdir3(pluginsBase, { withFileTypes: true });
2467
+ const marketplaces = await readdir4(pluginsBase, { withFileTypes: true });
2335
2468
  for (const mp of marketplaces) {
2336
2469
  if (!mp.isDirectory())
2337
2470
  continue;
2338
2471
  const extDir = join14(pluginsBase, mp.name, "external_plugins");
2339
- if (!existsSync11(extDir))
2472
+ if (!existsSync12(extDir))
2340
2473
  continue;
2341
- const plugins = await readdir3(extDir, { withFileTypes: true });
2474
+ const plugins = await readdir4(extDir, { withFileTypes: true });
2342
2475
  for (const plugin of plugins) {
2343
2476
  if (!plugin.isDirectory())
2344
2477
  continue;
@@ -2350,18 +2483,18 @@ async function readAllMcpConfigs() {
2350
2483
  }
2351
2484
  }
2352
2485
  const cacheBase = join14(home, ".claude", "plugins", "cache");
2353
- if (existsSync11(cacheBase)) {
2486
+ if (existsSync12(cacheBase)) {
2354
2487
  try {
2355
- const registries = await readdir3(cacheBase, { withFileTypes: true });
2488
+ const registries = await readdir4(cacheBase, { withFileTypes: true });
2356
2489
  for (const reg of registries) {
2357
2490
  if (!reg.isDirectory())
2358
2491
  continue;
2359
2492
  const regDir = join14(cacheBase, reg.name);
2360
- const plugins = await readdir3(regDir, { withFileTypes: true });
2493
+ const plugins = await readdir4(regDir, { withFileTypes: true });
2361
2494
  for (const plugin of plugins) {
2362
2495
  if (!plugin.isDirectory())
2363
2496
  continue;
2364
- const versionDirs = await readdir3(join14(regDir, plugin.name), { withFileTypes: true });
2497
+ const versionDirs = await readdir4(join14(regDir, plugin.name), { withFileTypes: true });
2365
2498
  for (const ver of versionDirs) {
2366
2499
  if (!ver.isDirectory())
2367
2500
  continue;
@@ -2769,7 +2902,9 @@ async function mcpWatchCommand() {
2769
2902
  console.log(chalk20.yellow(` Replacing ${existingWatchers.length} existing watcher${existingWatchers.length !== 1 ? "s" : ""} on this device...`));
2770
2903
  for (const w of existingWatchers) {
2771
2904
  try {
2772
- process.kill(w.pid, "SIGTERM");
2905
+ if (typeof w.pid === "number" && w.pid > 1 && w.pid !== process.pid) {
2906
+ process.kill(w.pid, "SIGTERM");
2907
+ }
2773
2908
  } catch {
2774
2909
  }
2775
2910
  }
@@ -3181,41 +3316,41 @@ init_map();
3181
3316
 
3182
3317
  // dist/commands/simulate.js
3183
3318
  init_dist();
3184
- import { join as join11 } from "node:path";
3319
+ import { join as join12 } from "node:path";
3185
3320
  import { existsSync as existsSync8 } from "node:fs";
3186
- import { homedir as homedir7 } from "node:os";
3321
+ import { homedir as homedir8 } from "node:os";
3187
3322
  import chalk12 from "chalk";
3188
3323
  async function simulateCommand(prompt) {
3189
3324
  const projectRoot = process.cwd();
3190
3325
  console.log(chalk12.blue(`Simulating prompt: "${prompt}"
3191
3326
  `));
3192
3327
  console.log(chalk12.dim("Files Claude would load:\n"));
3193
- const globalClaude = join11(homedir7(), ".claude", "CLAUDE.md");
3328
+ const globalClaude = join12(homedir8(), ".claude", "CLAUDE.md");
3194
3329
  if (existsSync8(globalClaude)) {
3195
3330
  console.log(chalk12.green(" \u2713 ~/.claude/CLAUDE.md (global)"));
3196
3331
  }
3197
3332
  for (const rootFile of ROOT_FILES) {
3198
- const fullPath = join11(projectRoot, rootFile);
3333
+ const fullPath = join12(projectRoot, rootFile);
3199
3334
  if (existsSync8(fullPath)) {
3200
3335
  console.log(chalk12.green(` \u2713 ${rootFile} (project root)`));
3201
3336
  }
3202
3337
  }
3203
3338
  const settingsFiles = [
3204
- join11(homedir7(), ".claude", "settings.json"),
3205
- join11(homedir7(), ".claude", "settings.local.json"),
3206
- join11(projectRoot, ".claude", "settings.json"),
3207
- join11(projectRoot, ".claude", "settings.local.json")
3339
+ join12(homedir8(), ".claude", "settings.json"),
3340
+ join12(homedir8(), ".claude", "settings.local.json"),
3341
+ join12(projectRoot, ".claude", "settings.json"),
3342
+ join12(projectRoot, ".claude", "settings.local.json")
3208
3343
  ];
3209
3344
  for (const sf of settingsFiles) {
3210
3345
  if (existsSync8(sf)) {
3211
- const display = sf.startsWith(homedir7()) ? sf.replace(homedir7(), "~") : sf.replace(projectRoot + "/", "");
3346
+ const display = sf.startsWith(homedir8()) ? sf.replace(homedir8(), "~") : sf.replace(projectRoot + "/", "");
3212
3347
  console.log(chalk12.green(` \u2713 ${display} (settings)`));
3213
3348
  }
3214
3349
  }
3215
3350
  const words = prompt.split(/\s+/);
3216
3351
  for (const word of words) {
3217
3352
  const cleaned = word.replace(/['"]/g, "");
3218
- const candidatePath = join11(projectRoot, cleaned);
3353
+ const candidatePath = join12(projectRoot, cleaned);
3219
3354
  if (existsSync8(candidatePath) && cleaned.includes("/")) {
3220
3355
  console.log(chalk12.cyan(` \u2192 ${cleaned} (referenced in prompt)`));
3221
3356
  }
@@ -3224,18 +3359,18 @@ async function simulateCommand(prompt) {
3224
3359
  }
3225
3360
 
3226
3361
  // dist/commands/print.js
3227
- import { join as join12 } from "node:path";
3228
- import { readFile as readFile8, writeFile as writeFile3 } from "node:fs/promises";
3362
+ import { join as join13 } from "node:path";
3363
+ import { readFile as readFile9, writeFile as writeFile3 } from "node:fs/promises";
3229
3364
  import { existsSync as existsSync9 } from "node:fs";
3230
3365
  import chalk13 from "chalk";
3231
3366
  async function printCommand(title) {
3232
3367
  const projectRoot = process.cwd();
3233
- const scanDataPath = join12(projectRoot, "output", "index.html");
3368
+ const scanDataPath = join13(projectRoot, "output", "index.html");
3234
3369
  if (!existsSync9(scanDataPath)) {
3235
3370
  console.error(chalk13.red("No scan data found. Run: md4ai scan"));
3236
3371
  process.exit(1);
3237
3372
  }
3238
- const html = await readFile8(scanDataPath, "utf-8");
3373
+ const html = await readFile9(scanDataPath, "utf-8");
3239
3374
  const match = html.match(/<script type="application\/json" id="scan-data">([\s\S]*?)<\/script>/);
3240
3375
  if (!match) {
3241
3376
  console.error(chalk13.red("Could not extract scan data from output/index.html"));
@@ -3243,7 +3378,7 @@ async function printCommand(title) {
3243
3378
  }
3244
3379
  const result = JSON.parse(match[1]);
3245
3380
  const printHtml = generatePrintHtml(result, title);
3246
- const outputPath = join12(projectRoot, "output", `print-${Date.now()}.html`);
3381
+ const outputPath = join13(projectRoot, "output", `print-${Date.now()}.html`);
3247
3382
  await writeFile3(outputPath, printHtml, "utf-8");
3248
3383
  console.log(chalk13.green(`Print-ready wall sheet: ${outputPath}`));
3249
3384
  }
@@ -3310,11 +3445,11 @@ init_config();
3310
3445
  init_scanner();
3311
3446
  init_push_toolings();
3312
3447
  init_device_utils();
3313
- import { resolve as resolve4 } from "node:path";
3448
+ import { resolve as resolve6 } from "node:path";
3314
3449
  import chalk15 from "chalk";
3315
3450
  async function linkCommand(projectId) {
3316
3451
  const { supabase, userId } = await getAuthenticatedClient();
3317
- const cwd = resolve4(process.cwd());
3452
+ const cwd = resolve6(process.cwd());
3318
3453
  const deviceName = detectDeviceName();
3319
3454
  const osType = detectOs2();
3320
3455
  const { data: folder, error: folderErr } = await supabase.from("claude_folders").select("id, name").eq("id", projectId).single();
@@ -3360,6 +3495,7 @@ Linking "${folder.name}" to this device...
3360
3495
  console.log(` Skills: ${result.skills.length}`);
3361
3496
  console.log(` Toolings: ${result.toolings.length}`);
3362
3497
  console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
3498
+ console.log(` Plugins: ${result.marketplacePlugins.length} (${result.marketplacePlugins.reduce((n, p) => n + p.skills.length, 0)} skills)`);
3363
3499
  const { error: scanErr } = await supabase.from("claude_folders").update({
3364
3500
  graph_json: result.graph,
3365
3501
  orphans_json: result.orphans,
@@ -3367,6 +3503,7 @@ Linking "${folder.name}" to this device...
3367
3503
  skills_table_json: result.skills,
3368
3504
  stale_files_json: result.staleFiles,
3369
3505
  env_manifest_json: result.envManifest,
3506
+ marketplace_plugins_json: result.marketplacePlugins,
3370
3507
  last_scanned: result.scannedAt,
3371
3508
  data_hash: result.dataHash
3372
3509
  }).eq("id", folder.id);
@@ -3374,7 +3511,8 @@ Linking "${folder.name}" to this device...
3374
3511
  console.error(chalk15.yellow(`Scan upload warning: ${scanErr.message}`));
3375
3512
  }
3376
3513
  await pushToolings(supabase, folder.id, result.toolings);
3377
- const configFiles = await readClaudeConfigFiles(cwd);
3514
+ const graphPaths = result.graph.nodes.map((n) => n.filePath);
3515
+ const configFiles = await readClaudeConfigFiles(cwd, graphPaths);
3378
3516
  if (configFiles.length > 0) {
3379
3517
  for (const file of configFiles) {
3380
3518
  await supabase.from("folder_files").upsert({
@@ -3402,18 +3540,24 @@ Linking "${folder.name}" to this device...
3402
3540
  }
3403
3541
 
3404
3542
  // dist/commands/import-bundle.js
3405
- import { readFile as readFile9, writeFile as writeFile4, mkdir as mkdir3 } from "node:fs/promises";
3406
- import { join as join13, dirname as dirname3 } from "node:path";
3407
- import { existsSync as existsSync10 } from "node:fs";
3543
+ import { readFile as readFile10, writeFile as writeFile4, mkdir as mkdir3, stat as fsStat } from "node:fs/promises";
3544
+ import { dirname as dirname3, resolve as resolve7 } from "node:path";
3545
+ import { existsSync as existsSync11 } from "node:fs";
3408
3546
  import chalk16 from "chalk";
3409
3547
  import { confirm as confirm3, input as input6 } from "@inquirer/prompts";
3548
+ var MAX_BUNDLE_SIZE_BYTES = 50 * 1024 * 1024;
3410
3549
  async function importBundleCommand(zipPath) {
3411
- if (!existsSync10(zipPath)) {
3550
+ if (!existsSync11(zipPath)) {
3412
3551
  console.error(chalk16.red(`File not found: ${zipPath}`));
3413
3552
  process.exit(1);
3414
3553
  }
3554
+ const fileStat = await fsStat(zipPath);
3555
+ if (fileStat.size > MAX_BUNDLE_SIZE_BYTES) {
3556
+ console.error(chalk16.red(`Bundle too large (${Math.round(fileStat.size / 1024 / 1024)} MB). Maximum is 50 MB.`));
3557
+ process.exit(1);
3558
+ }
3415
3559
  const JSZip = (await import("jszip")).default;
3416
- const zipData = await readFile9(zipPath);
3560
+ const zipData = await readFile10(zipPath);
3417
3561
  const zip = await JSZip.loadAsync(zipData);
3418
3562
  const manifestFile = zip.file("manifest.json");
3419
3563
  if (!manifestFile) {
@@ -3454,10 +3598,15 @@ Files to extract:`));
3454
3598
  console.log(chalk16.yellow("Cancelled."));
3455
3599
  return;
3456
3600
  }
3601
+ const resolvedTarget = resolve7(targetDir);
3457
3602
  for (const file of files) {
3458
- const fullPath = join13(targetDir, file.filePath);
3603
+ const fullPath = resolve7(targetDir, file.filePath);
3604
+ if (!fullPath.startsWith(resolvedTarget + "/") && fullPath !== resolvedTarget) {
3605
+ console.error(chalk16.red(` Blocked path traversal: ${file.filePath}`));
3606
+ continue;
3607
+ }
3459
3608
  const dir = dirname3(fullPath);
3460
- if (!existsSync10(dir)) {
3609
+ if (!existsSync11(dir)) {
3461
3610
  await mkdir3(dir, { recursive: true });
3462
3611
  }
3463
3612
  await writeFile4(fullPath, file.content, "utf-8");
@@ -3762,9 +3911,9 @@ async function fetchGitHubVersions(repo, signal) {
3762
3911
  init_mcp_watch();
3763
3912
 
3764
3913
  // dist/commands/init-manifest.js
3765
- import { resolve as resolve5, join as join15, relative as relative4, dirname as dirname4 } from "node:path";
3766
- import { readFile as readFile11, writeFile as writeFile5, mkdir as mkdir4 } from "node:fs/promises";
3767
- import { existsSync as existsSync12 } from "node:fs";
3914
+ import { resolve as resolve8, join as join15, relative as relative4, dirname as dirname4 } from "node:path";
3915
+ import { readFile as readFile12, writeFile as writeFile5, mkdir as mkdir4 } from "node:fs/promises";
3916
+ import { existsSync as existsSync13 } from "node:fs";
3768
3917
  import chalk21 from "chalk";
3769
3918
  var SECRET_PATTERNS = [
3770
3919
  /_KEY$/i,
@@ -3782,7 +3931,7 @@ async function discoverEnvFiles(projectRoot) {
3782
3931
  const apps = [];
3783
3932
  for (const envName of [".env", ".env.local", ".env.example"]) {
3784
3933
  const envPath = join15(projectRoot, envName);
3785
- if (existsSync12(envPath)) {
3934
+ if (existsSync13(envPath)) {
3786
3935
  const vars = await extractVarNames(envPath);
3787
3936
  if (vars.length > 0) {
3788
3937
  apps.push({ name: "root", envFilePath: envName, vars });
@@ -3793,11 +3942,11 @@ async function discoverEnvFiles(projectRoot) {
3793
3942
  const subdirs = ["web", "cli", "api", "app", "server", "packages"];
3794
3943
  for (const sub of subdirs) {
3795
3944
  const subDir = join15(projectRoot, sub);
3796
- if (!existsSync12(subDir))
3945
+ if (!existsSync13(subDir))
3797
3946
  continue;
3798
3947
  for (const envName of [".env.local", ".env", ".env.example"]) {
3799
3948
  const envPath = join15(subDir, envName);
3800
- if (existsSync12(envPath)) {
3949
+ if (existsSync13(envPath)) {
3801
3950
  const vars = await extractVarNames(envPath);
3802
3951
  if (vars.length > 0) {
3803
3952
  apps.push({
@@ -3813,7 +3962,7 @@ async function discoverEnvFiles(projectRoot) {
3813
3962
  return apps;
3814
3963
  }
3815
3964
  async function extractVarNames(envPath) {
3816
- const content = await readFile11(envPath, "utf-8");
3965
+ const content = await readFile12(envPath, "utf-8");
3817
3966
  const vars = [];
3818
3967
  for (const line of content.split("\n")) {
3819
3968
  const trimmed = line.trim();
@@ -3857,9 +4006,9 @@ function generateManifest(apps) {
3857
4006
  return lines.join("\n");
3858
4007
  }
3859
4008
  async function initManifestCommand() {
3860
- const projectRoot = resolve5(process.cwd());
4009
+ const projectRoot = resolve8(process.cwd());
3861
4010
  const manifestPath = join15(projectRoot, "docs", "reference", "env-manifest.md");
3862
- if (existsSync12(manifestPath)) {
4011
+ if (existsSync13(manifestPath)) {
3863
4012
  console.log(chalk21.yellow(`Manifest already exists: ${relative4(projectRoot, manifestPath)}`));
3864
4013
  console.log(chalk21.dim("Edit it directly to make changes."));
3865
4014
  return;
@@ -3876,7 +4025,7 @@ async function initManifestCommand() {
3876
4025
  }
3877
4026
  const content = generateManifest(apps);
3878
4027
  const dir = dirname4(manifestPath);
3879
- if (!existsSync12(dir)) {
4028
+ if (!existsSync13(dir)) {
3880
4029
  await mkdir4(dir, { recursive: true });
3881
4030
  }
3882
4031
  await writeFile5(manifestPath, content, "utf-8");
@@ -4036,7 +4185,7 @@ init_check_update();
4036
4185
  init_map();
4037
4186
  init_mcp_watch();
4038
4187
  import chalk23 from "chalk";
4039
- import { resolve as resolve6 } from "node:path";
4188
+ import { resolve as resolve9 } from "node:path";
4040
4189
  async function fetchLatestVersion2() {
4041
4190
  try {
4042
4191
  const controller = new AbortController();
@@ -4065,7 +4214,7 @@ function isNewer3(a, b) {
4065
4214
  return false;
4066
4215
  }
4067
4216
  async function startCommand() {
4068
- const projectRoot = resolve6(process.cwd());
4217
+ const projectRoot = resolve9(process.cwd());
4069
4218
  console.log("");
4070
4219
  console.log(chalk23.bold.cyan(` MD4AI v${CURRENT_VERSION}`));
4071
4220
  console.log(chalk23.dim(` ${projectRoot}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md4ai",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "description": "CLI for MD4AI — scan Claude projects and sync to your dashboard",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "devDependencies": {
43
43
  "@md4ai/shared": "workspace:*",
44
- "@types/node": "^22.15.31",
44
+ "@types/node": "^22.19.15",
45
45
  "esbuild": "^0.27.3",
46
46
  "typescript": "^5.7.0"
47
47
  }