md4ai 0.10.2 → 0.10.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.
@@ -9,6 +9,74 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // dist/check-update.js
13
+ import chalk from "chalk";
14
+ async function fetchLatest() {
15
+ try {
16
+ const controller = new AbortController();
17
+ const timeout = setTimeout(() => controller.abort(), 3e3);
18
+ const res = await fetch("https://registry.npmjs.org/md4ai/latest", {
19
+ signal: controller.signal
20
+ });
21
+ clearTimeout(timeout);
22
+ if (!res.ok)
23
+ return null;
24
+ const data = await res.json();
25
+ return data.version ?? null;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+ function printUpdateBanner(latest) {
31
+ console.log("");
32
+ console.log(chalk.yellow("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
33
+ console.log(chalk.yellow("\u2502") + chalk.bold(" Update available! ") + chalk.dim(`${CURRENT_VERSION}`) + chalk.white(" \u2192 ") + chalk.green.bold(`${latest}`) + " " + chalk.yellow("\u2502"));
34
+ console.log(chalk.yellow("\u2502") + " " + chalk.yellow("\u2502"));
35
+ console.log(chalk.yellow("\u2502") + " Run: " + chalk.cyan("md4ai update") + " " + chalk.yellow("\u2502"));
36
+ console.log(chalk.yellow("\u2502") + " " + chalk.yellow("\u2502"));
37
+ console.log(chalk.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
38
+ console.log("");
39
+ }
40
+ async function checkForUpdate() {
41
+ const latest = await fetchLatest();
42
+ if (!latest)
43
+ return;
44
+ if (latest !== CURRENT_VERSION && isNewer(latest, CURRENT_VERSION)) {
45
+ printUpdateBanner(latest);
46
+ } else {
47
+ console.log(chalk.green(`md4ai v${CURRENT_VERSION} \u2014 you're on the latest version.`));
48
+ }
49
+ }
50
+ async function autoCheckForUpdate() {
51
+ try {
52
+ const latest = await fetchLatest();
53
+ if (!latest)
54
+ return;
55
+ if (latest !== CURRENT_VERSION && isNewer(latest, CURRENT_VERSION)) {
56
+ printUpdateBanner(latest);
57
+ }
58
+ } catch {
59
+ }
60
+ }
61
+ function isNewer(a, b) {
62
+ const pa = a.split(".").map(Number);
63
+ const pb = b.split(".").map(Number);
64
+ for (let i = 0; i < 3; i++) {
65
+ if ((pa[i] ?? 0) > (pb[i] ?? 0))
66
+ return true;
67
+ if ((pa[i] ?? 0) < (pb[i] ?? 0))
68
+ return false;
69
+ }
70
+ return false;
71
+ }
72
+ var CURRENT_VERSION;
73
+ var init_check_update = __esm({
74
+ "dist/check-update.js"() {
75
+ "use strict";
76
+ CURRENT_VERSION = true ? "0.10.4" : "0.0.0-dev";
77
+ }
78
+ });
79
+
12
80
  // ../packages/shared/dist/types.js
13
81
  var init_types = __esm({
14
82
  "../packages/shared/dist/types.js"() {
@@ -83,12 +151,8 @@ var init_dist = __esm({
83
151
  import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
84
152
  import { join } from "node:path";
85
153
  import { homedir } from "node:os";
86
- import { existsSync } from "node:fs";
87
154
  async function ensureConfigDir() {
88
- if (!existsSync(configPath)) {
89
- await mkdir(configPath, { recursive: true });
90
- await chmod(configPath, 448);
91
- }
155
+ await mkdir(configPath, { recursive: true, mode: 448 });
92
156
  }
93
157
  async function saveCredentials(creds) {
94
158
  await ensureConfigDir();
@@ -115,7 +179,7 @@ async function mergeCredentials(partial) {
115
179
  }
116
180
  async function clearCredentials() {
117
181
  try {
118
- await writeFile(credentialsPath, "{}", "utf-8");
182
+ await writeFile(credentialsPath, "{}", { mode: 384 });
119
183
  } catch {
120
184
  }
121
185
  }
@@ -156,10 +220,10 @@ var login_exports = {};
156
220
  __export(login_exports, {
157
221
  loginCommand: () => loginCommand
158
222
  });
159
- import chalk from "chalk";
223
+ import chalk2 from "chalk";
160
224
  import { input, password } from "@inquirer/prompts";
161
225
  async function loginCommand() {
162
- console.log(chalk.blue("MD4AI Login\n"));
226
+ console.log(chalk2.blue("MD4AI Login\n"));
163
227
  const email = await input({ message: "Email:" });
164
228
  const pwd = await password({ message: "Password:" });
165
229
  const anonKey = getAnonKey();
@@ -169,7 +233,7 @@ async function loginCommand() {
169
233
  password: pwd
170
234
  });
171
235
  if (error) {
172
- console.error(chalk.red(`Login failed: ${error.message}`));
236
+ console.error(chalk2.red(`Login failed: ${error.message}`));
173
237
  process.exit(1);
174
238
  }
175
239
  await saveCredentials({
@@ -179,7 +243,7 @@ async function loginCommand() {
179
243
  userId: data.user.id,
180
244
  email: data.user.email ?? email
181
245
  });
182
- console.log(chalk.green(`
246
+ console.log(chalk2.green(`
183
247
  Logged in as ${data.user.email}`));
184
248
  }
185
249
  var init_login = __esm({
@@ -191,14 +255,14 @@ var init_login = __esm({
191
255
  });
192
256
 
193
257
  // dist/auth.js
194
- import chalk4 from "chalk";
258
+ import chalk5 from "chalk";
195
259
  import { confirm, input as input2, password as password2 } from "@inquirer/prompts";
196
260
  async function getAuthenticatedClient() {
197
261
  const creds = await loadCredentials();
198
262
  const needsLogin = !creds?.accessToken || Date.now() > creds.expiresAt;
199
263
  if (needsLogin) {
200
264
  const reason = !creds?.accessToken ? "Not logged in." : "Session expired.";
201
- console.log(chalk4.yellow(`${reason}`));
265
+ console.log(chalk5.yellow(`${reason}`));
202
266
  const shouldLogin = await confirm({ message: "Would you like to log in now?" });
203
267
  if (!shouldLogin) {
204
268
  process.exit(0);
@@ -230,7 +294,7 @@ async function refreshSession() {
230
294
  async function getLongLivedClient() {
231
295
  const creds = await loadCredentials();
232
296
  if (!creds?.accessToken || !creds?.refreshToken) {
233
- console.log(chalk4.yellow("Not logged in."));
297
+ console.log(chalk5.yellow("Not logged in."));
234
298
  const shouldLogin = await confirm({ message: "Would you like to log in now?" });
235
299
  if (!shouldLogin)
236
300
  process.exit(0);
@@ -247,7 +311,7 @@ async function getLongLivedClient() {
247
311
  });
248
312
  return { supabase: result.client, userId: result.userId };
249
313
  } catch {
250
- console.log(chalk4.yellow("Session expired \u2014 please log in again."));
314
+ console.log(chalk5.yellow("Session expired \u2014 please log in again."));
251
315
  const shouldLogin = await confirm({ message: "Would you like to log in now?" });
252
316
  if (!shouldLogin)
253
317
  process.exit(0);
@@ -265,7 +329,7 @@ async function promptLogin() {
265
329
  password: pwd
266
330
  });
267
331
  if (error) {
268
- console.error(chalk4.red(`Login failed: ${error.message}`));
332
+ console.error(chalk5.red(`Login failed: ${error.message}`));
269
333
  process.exit(1);
270
334
  }
271
335
  await saveCredentials({
@@ -275,7 +339,7 @@ async function promptLogin() {
275
339
  userId: data.user.id,
276
340
  email: data.user.email ?? email
277
341
  });
278
- console.log(chalk4.green(`Logged in as ${data.user.email}
342
+ console.log(chalk5.green(`Logged in as ${data.user.email}
279
343
  `));
280
344
  const authedSupabase = createSupabaseClient(anonKey, data.session.access_token);
281
345
  return { supabase: authedSupabase, userId: data.user.id };
@@ -290,7 +354,7 @@ var init_auth = __esm({
290
354
 
291
355
  // dist/scanner/file-parser.js
292
356
  import { readFile as readFile2 } from "node:fs/promises";
293
- import { existsSync as existsSync2 } from "node:fs";
357
+ import { existsSync } from "node:fs";
294
358
  import { resolve as resolve2, dirname, join as join2 } from "node:path";
295
359
  import { homedir as homedir2 } from "node:os";
296
360
  async function parseFileReferences(filePath, projectRoot) {
@@ -318,7 +382,7 @@ async function parseFileReferences(filePath, projectRoot) {
318
382
  if (!target.includes("/") && /^[A-Z]/.test(baseName))
319
383
  continue;
320
384
  const resolved = resolveTarget(target, filePath, projectRoot);
321
- if (resolved && existsSync2(resolved)) {
385
+ if (resolved && existsSync(resolved)) {
322
386
  addRef(refs, relPath, resolved, projectRoot);
323
387
  } else if (resolved) {
324
388
  const targetRel = resolved.startsWith(projectRoot) ? resolved.slice(projectRoot.length + 1) : resolved;
@@ -361,7 +425,7 @@ function resolveTarget(target, sourceFilePath, projectRoot) {
361
425
  }
362
426
  if (target.includes("/")) {
363
427
  const fromRoot = join2(projectRoot, target);
364
- if (existsSync2(fromRoot))
428
+ if (existsSync(fromRoot))
365
429
  return fromRoot;
366
430
  }
367
431
  return resolve2(dirname(sourceFilePath), target);
@@ -387,7 +451,7 @@ function parseJsonPathReferences(content, fromRel, projectRoot, refs, brokenRefs
387
451
  while ((match = pattern.exec(value)) !== null) {
388
452
  const relTarget = match[1];
389
453
  const resolved = join2(projectRoot, relTarget);
390
- if (existsSync2(resolved)) {
454
+ if (existsSync(resolved)) {
391
455
  addRef(refs, fromRel, resolved, projectRoot);
392
456
  } else {
393
457
  brokenRefs.push({ from: fromRel, to: relTarget, rawRef: relTarget });
@@ -401,7 +465,7 @@ function parseJsonPathReferences(content, fromRel, projectRoot, refs, brokenRefs
401
465
  if (!target || target.startsWith("http"))
402
466
  continue;
403
467
  const resolved = join2(projectRoot, target);
404
- if (existsSync2(resolved)) {
468
+ if (existsSync(resolved)) {
405
469
  addRef(refs, fromRel, resolved, projectRoot);
406
470
  } else {
407
471
  brokenRefs.push({ from: fromRel, to: target, rawRef: target });
@@ -609,7 +673,7 @@ var init_orphan_detector = __esm({
609
673
  // dist/scanner/skills-parser.js
610
674
  import { execFileSync as execFileSync2 } from "node:child_process";
611
675
  import { readFile as readFile3 } from "node:fs/promises";
612
- import { existsSync as existsSync3 } from "node:fs";
676
+ import { existsSync as existsSync2 } from "node:fs";
613
677
  import { join as join5 } from "node:path";
614
678
  import { homedir as homedir3 } from "node:os";
615
679
  async function parseSkills(projectRoot) {
@@ -638,7 +702,7 @@ async function parseSkills(projectRoot) {
638
702
  await parseSettingsForPlugins(projectSettings, skills, false);
639
703
  await parseSettingsForPlugins(projectLocalSettings, skills, false);
640
704
  const skillsMd = join5(projectRoot, "skills.md");
641
- if (existsSync3(skillsMd)) {
705
+ if (existsSync2(skillsMd)) {
642
706
  const content = await readFile3(skillsMd, "utf-8");
643
707
  const skillNames = content.match(/^#+\s+(.+)$/gm);
644
708
  if (skillNames) {
@@ -661,7 +725,7 @@ async function parseSkills(projectRoot) {
661
725
  return Array.from(skills.values());
662
726
  }
663
727
  async function parseSettingsForPlugins(settingsPath, skills, isMachineWide) {
664
- if (!existsSync3(settingsPath))
728
+ if (!existsSync2(settingsPath))
665
729
  return;
666
730
  try {
667
731
  const content = await readFile3(settingsPath, "utf-8");
@@ -698,7 +762,7 @@ var init_skills_parser = __esm({
698
762
 
699
763
  // dist/scanner/tooling-detector.js
700
764
  import { readFile as readFile4, readdir } from "node:fs/promises";
701
- import { existsSync as existsSync4 } from "node:fs";
765
+ import { existsSync as existsSync3 } from "node:fs";
702
766
  import { join as join6 } from "node:path";
703
767
  import { execFileSync as execFileSync3 } from "node:child_process";
704
768
  import { homedir as homedir4 } from "node:os";
@@ -715,7 +779,7 @@ async function detectToolings(projectRoot) {
715
779
  async function detectFromPackageJson(projectRoot) {
716
780
  const toolings = [];
717
781
  const pkgPath = join6(projectRoot, "package.json");
718
- if (!existsSync4(pkgPath))
782
+ if (!existsSync3(pkgPath))
719
783
  return toolings;
720
784
  const resolvedVersions = await getResolvedVersions(projectRoot);
721
785
  const seen = /* @__PURE__ */ new Set();
@@ -759,7 +823,7 @@ async function discoverWorkspacePackageJsons(projectRoot) {
759
823
  const cleanPattern = pattern.replace(/\/\*\*?$/, "");
760
824
  if (isGlob) {
761
825
  const parentDir = join6(projectRoot, cleanPattern);
762
- if (!existsSync4(parentDir))
826
+ if (!existsSync3(parentDir))
763
827
  continue;
764
828
  try {
765
829
  const entries = await readdir(parentDir, { withFileTypes: true });
@@ -767,7 +831,7 @@ async function discoverWorkspacePackageJsons(projectRoot) {
767
831
  if (!entry.isDirectory())
768
832
  continue;
769
833
  const pkgPath = join6(parentDir, entry.name, "package.json");
770
- if (existsSync4(pkgPath)) {
834
+ if (existsSync3(pkgPath)) {
771
835
  results.push(pkgPath);
772
836
  }
773
837
  }
@@ -775,7 +839,7 @@ async function discoverWorkspacePackageJsons(projectRoot) {
775
839
  }
776
840
  } else {
777
841
  const pkgPath = join6(projectRoot, cleanPattern, "package.json");
778
- if (existsSync4(pkgPath)) {
842
+ if (existsSync3(pkgPath)) {
779
843
  results.push(pkgPath);
780
844
  }
781
845
  }
@@ -784,7 +848,7 @@ async function discoverWorkspacePackageJsons(projectRoot) {
784
848
  }
785
849
  async function getWorkspacePatterns(projectRoot) {
786
850
  const pnpmWorkspace = join6(projectRoot, "pnpm-workspace.yaml");
787
- if (existsSync4(pnpmWorkspace)) {
851
+ if (existsSync3(pnpmWorkspace)) {
788
852
  try {
789
853
  const content = await readFile4(pnpmWorkspace, "utf-8");
790
854
  const patterns = [];
@@ -827,7 +891,7 @@ function stripVersionPrefix(version) {
827
891
  async function getResolvedVersions(projectRoot) {
828
892
  const versions = /* @__PURE__ */ new Map();
829
893
  const pnpmLock = join6(projectRoot, "pnpm-lock.yaml");
830
- if (existsSync4(pnpmLock)) {
894
+ if (existsSync3(pnpmLock)) {
831
895
  try {
832
896
  const content = await readFile4(pnpmLock, "utf-8");
833
897
  const versionPattern = /^\s{4}'?(@?[^@\s:]+)(?:@[^:]+)?'?:\s*(?:version:\s*)?'?(\d+\.\d+[^'\s]*)/gm;
@@ -843,7 +907,7 @@ async function getResolvedVersions(projectRoot) {
843
907
  }
844
908
  if (versions.size === 0) {
845
909
  const npmLock = join6(projectRoot, "package-lock.json");
846
- if (existsSync4(npmLock)) {
910
+ if (existsSync3(npmLock)) {
847
911
  try {
848
912
  const content = await readFile4(npmLock, "utf-8");
849
913
  const lock = JSON.parse(content);
@@ -864,7 +928,7 @@ async function getResolvedVersions(projectRoot) {
864
928
  function detectFromCli(projectRoot) {
865
929
  const toolings = [];
866
930
  for (const { name, command, args, conditionFile } of CLI_VERSION_COMMANDS) {
867
- if (conditionFile && !existsSync4(join6(projectRoot, conditionFile))) {
931
+ if (conditionFile && !existsSync3(join6(projectRoot, conditionFile))) {
868
932
  continue;
869
933
  }
870
934
  try {
@@ -897,7 +961,7 @@ async function detectFromMcpSettings(projectRoot) {
897
961
  ];
898
962
  const seen = /* @__PURE__ */ new Set();
899
963
  for (const settingsPath of settingsPaths) {
900
- if (!existsSync4(settingsPath))
964
+ if (!existsSync3(settingsPath))
901
965
  continue;
902
966
  try {
903
967
  const content = await readFile4(settingsPath, "utf-8");
@@ -1042,13 +1106,24 @@ async function fetchVercelEnvVars(projectId, orgId, token) {
1042
1106
  }
1043
1107
  const data = await res.json();
1044
1108
  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
- }));
1109
+ const merged = /* @__PURE__ */ new Map();
1110
+ for (const e of envs) {
1111
+ const targets = e.target ?? ["production", "preview", "development"];
1112
+ const existing = merged.get(e.key);
1113
+ if (existing) {
1114
+ const targetSet = /* @__PURE__ */ new Set([...existing.targets, ...targets]);
1115
+ existing.targets = Array.from(targetSet);
1116
+ } else {
1117
+ merged.set(e.key, {
1118
+ key: e.key,
1119
+ targets: [...targets],
1120
+ type: e.type ?? "plain",
1121
+ inManifest: false
1122
+ // populated later by the scanner
1123
+ });
1124
+ }
1125
+ }
1126
+ return Array.from(merged.values());
1052
1127
  }
1053
1128
  var VercelApiError;
1054
1129
  var init_fetch_env_vars = __esm({
@@ -1074,11 +1149,11 @@ import { readFile as readFile7, glob as glob2 } from "node:fs/promises";
1074
1149
  import { execFile } from "node:child_process";
1075
1150
  import { promisify } from "node:util";
1076
1151
  import { join as join9, relative as relative2 } from "node:path";
1077
- import { existsSync as existsSync5 } from "node:fs";
1078
- import chalk8 from "chalk";
1152
+ import { existsSync as existsSync4 } from "node:fs";
1153
+ import chalk9 from "chalk";
1079
1154
  async function scanEnvManifest(projectRoot) {
1080
1155
  const manifestFullPath = join9(projectRoot, MANIFEST_PATH);
1081
- if (!existsSync5(manifestFullPath)) {
1156
+ if (!existsSync4(manifestFullPath)) {
1082
1157
  return null;
1083
1158
  }
1084
1159
  const manifestContent = await readFile7(manifestFullPath, "utf-8");
@@ -1136,16 +1211,16 @@ function tokenFixInstructions(source) {
1136
1211
  async function checkVercelEnvVars(projectRoot, variables) {
1137
1212
  const tokenResult = await resolveVercelTokenWithSource();
1138
1213
  if (!tokenResult) {
1139
- console.log(chalk8.dim(" Vercel checks skipped (no token configured)"));
1140
- console.log(chalk8.dim(" To enable: md4ai config set vercel-token <token>"));
1141
- console.log(chalk8.dim(" Generate a token at https://vercel.com/account/tokens"));
1214
+ console.log(chalk9.dim(" Vercel checks skipped (no token configured)"));
1215
+ console.log(chalk9.dim(" To enable: md4ai config set vercel-token <token>"));
1216
+ console.log(chalk9.dim(" Generate a token at https://vercel.com/account/tokens"));
1142
1217
  return null;
1143
1218
  }
1144
1219
  const { token, source } = tokenResult;
1145
1220
  const projects = await discoverVercelProjects(projectRoot);
1146
1221
  if (projects.length === 0)
1147
1222
  return null;
1148
- console.log(chalk8.dim(` Checking ${projects.length} Vercel project(s)...`));
1223
+ console.log(chalk9.dim(` Checking ${projects.length} Vercel project(s)...`));
1149
1224
  const discovered = [];
1150
1225
  const manifestVarNames = new Set(variables.map((v) => v.name));
1151
1226
  let tokenErrorShown = false;
@@ -1167,23 +1242,23 @@ async function checkVercelEnvVars(projectRoot, variables) {
1167
1242
  mv.vercelStatus = {};
1168
1243
  mv.vercelStatus[proj.projectName] = vercelKeySet.has(mv.name) ? "present" : "missing";
1169
1244
  }
1170
- console.log(chalk8.dim(` ${proj.projectName}: ${vars.length} var(s)`));
1245
+ console.log(chalk9.dim(` ${proj.projectName}: ${vars.length} var(s)`));
1171
1246
  } catch (err) {
1172
1247
  if (err instanceof VercelApiError && err.isInvalidToken && !tokenErrorShown) {
1173
1248
  tokenErrorShown = true;
1174
- console.log(chalk8.red(` ${proj.projectName}: Token is invalid or expired`));
1175
- console.log(chalk8.yellow(` Token source: ${tokenSourceLabel(source)}`));
1176
- console.log(chalk8.yellow(" To fix:"));
1249
+ console.log(chalk9.red(` ${proj.projectName}: Token is invalid or expired`));
1250
+ console.log(chalk9.yellow(` Token source: ${tokenSourceLabel(source)}`));
1251
+ console.log(chalk9.yellow(" To fix:"));
1177
1252
  for (const step of tokenFixInstructions(source)) {
1178
- console.log(chalk8.yellow(` \u2192 ${step}`));
1253
+ console.log(chalk9.yellow(` \u2192 ${step}`));
1179
1254
  }
1180
1255
  } else if (err instanceof VercelApiError && err.statusCode === 403) {
1181
- console.log(chalk8.yellow(` ${proj.projectName}: ${err.message}`));
1182
- console.log(chalk8.dim(` Token source: ${tokenSourceLabel(source)}`));
1183
- console.log(chalk8.dim(" Ensure the token has access to this team/project"));
1184
- console.log(chalk8.dim(" Check: https://vercel.com/account/tokens"));
1256
+ console.log(chalk9.yellow(` ${proj.projectName}: ${err.message}`));
1257
+ console.log(chalk9.dim(` Token source: ${tokenSourceLabel(source)}`));
1258
+ console.log(chalk9.dim(" Ensure the token has access to this team/project"));
1259
+ console.log(chalk9.dim(" Check: https://vercel.com/account/tokens"));
1185
1260
  } else {
1186
- console.log(chalk8.yellow(` ${proj.projectName}: ${err instanceof Error ? err.message : "API error"}`));
1261
+ console.log(chalk9.yellow(` ${proj.projectName}: ${err instanceof Error ? err.message : "API error"}`));
1187
1262
  }
1188
1263
  }
1189
1264
  }
@@ -1222,16 +1297,16 @@ async function listGitHubSecretNames(repoSlug) {
1222
1297
  async function checkGitHubSecrets(projectRoot, variables) {
1223
1298
  const slug = await detectGitHubRepoSlug(projectRoot);
1224
1299
  if (!slug) {
1225
- console.log(chalk8.dim(" GitHub checks skipped (no GitHub remote detected)"));
1300
+ console.log(chalk9.dim(" GitHub checks skipped (no GitHub remote detected)"));
1226
1301
  return null;
1227
1302
  }
1228
1303
  const secretNames = await listGitHubSecretNames(slug);
1229
1304
  if (!secretNames) {
1230
- console.log(chalk8.dim(" GitHub checks skipped (gh CLI not available or not authenticated)"));
1305
+ console.log(chalk9.dim(" GitHub checks skipped (gh CLI not available or not authenticated)"));
1231
1306
  return slug;
1232
1307
  }
1233
1308
  const secretSet = new Set(secretNames);
1234
- console.log(chalk8.dim(` GitHub secrets: ${secretNames.length} found in ${slug}`));
1309
+ console.log(chalk9.dim(` GitHub secrets: ${secretNames.length} found in ${slug}`));
1235
1310
  for (const v of variables) {
1236
1311
  if (v.requiredIn.github) {
1237
1312
  v.githubStatus = secretSet.has(v.name) ? "present" : "missing";
@@ -1320,7 +1395,7 @@ async function checkLocalEnvFiles(projectRoot, apps) {
1320
1395
  for (const app of apps) {
1321
1396
  const envPath = join9(projectRoot, app.envFilePath);
1322
1397
  const varNames = /* @__PURE__ */ new Set();
1323
- if (existsSync5(envPath)) {
1398
+ if (existsSync4(envPath)) {
1324
1399
  const content = await readFile7(envPath, "utf-8");
1325
1400
  for (const line of content.split("\n")) {
1326
1401
  const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
@@ -1335,7 +1410,7 @@ async function checkLocalEnvFiles(projectRoot, apps) {
1335
1410
  }
1336
1411
  async function parseWorkflowSecrets(projectRoot) {
1337
1412
  const workflowsDir = join9(projectRoot, ".github", "workflows");
1338
- if (!existsSync5(workflowsDir))
1413
+ if (!existsSync4(workflowsDir))
1339
1414
  return [];
1340
1415
  const refs = [];
1341
1416
  const secretRe = /\$\{\{\s*secrets\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
@@ -1416,11 +1491,111 @@ var init_env_manifest_scanner = __esm({
1416
1491
  }
1417
1492
  });
1418
1493
 
1494
+ // dist/scanner/marketplace-scanner.js
1495
+ import { readFile as readFile8, readdir as readdir2, stat } from "node:fs/promises";
1496
+ import { join as join10, resolve as resolve3 } from "node:path";
1497
+ import { existsSync as existsSync5 } from "node:fs";
1498
+ import { homedir as homedir6 } from "node:os";
1499
+ async function scanMarketplacePlugins() {
1500
+ const pluginsDir = join10(homedir6(), ".claude", "plugins");
1501
+ const installedPath = join10(pluginsDir, "installed_plugins.json");
1502
+ const marketplacesPath = join10(pluginsDir, "known_marketplaces.json");
1503
+ if (!existsSync5(installedPath))
1504
+ return [];
1505
+ let installed;
1506
+ let knownMarketplaces = {};
1507
+ try {
1508
+ installed = JSON.parse(await readFile8(installedPath, "utf-8"));
1509
+ } catch {
1510
+ return [];
1511
+ }
1512
+ try {
1513
+ if (existsSync5(marketplacesPath)) {
1514
+ knownMarketplaces = JSON.parse(await readFile8(marketplacesPath, "utf-8"));
1515
+ }
1516
+ } catch {
1517
+ }
1518
+ const results = [];
1519
+ const expectedBase = resolve3(pluginsDir);
1520
+ for (const [key, entries] of Object.entries(installed.plugins ?? {})) {
1521
+ if (!entries?.length)
1522
+ continue;
1523
+ const entry = entries[0];
1524
+ const resolvedInstallPath = resolve3(entry.installPath);
1525
+ if (!resolvedInstallPath.startsWith(expectedBase + "/"))
1526
+ continue;
1527
+ const atIdx = key.lastIndexOf("@");
1528
+ if (atIdx === -1)
1529
+ continue;
1530
+ const pluginName = key.slice(0, atIdx);
1531
+ const marketplace = key.slice(atIdx + 1);
1532
+ const homepageUrl = buildHomepageUrl(knownMarketplaces, marketplace, pluginName);
1533
+ const skills = await discoverSkills(entry.installPath);
1534
+ results.push({
1535
+ marketplace,
1536
+ pluginName,
1537
+ version: entry.version ?? null,
1538
+ homepageUrl,
1539
+ installedAt: entry.installedAt ?? null,
1540
+ lastUpdated: entry.lastUpdated ?? null,
1541
+ skills
1542
+ });
1543
+ }
1544
+ results.sort((a, b) => a.marketplace.localeCompare(b.marketplace) || a.pluginName.localeCompare(b.pluginName));
1545
+ return results;
1546
+ }
1547
+ function buildHomepageUrl(knownMarketplaces, marketplace, pluginName) {
1548
+ const mp = knownMarketplaces[marketplace];
1549
+ if (!mp?.source?.repo)
1550
+ return null;
1551
+ const repo = mp.source.repo;
1552
+ const baseUrl = `https://github.com/${repo}`;
1553
+ if (marketplace === "claude-plugins-official") {
1554
+ return `${baseUrl}/tree/main/plugins/${pluginName}`;
1555
+ }
1556
+ return baseUrl;
1557
+ }
1558
+ async function discoverSkills(installPath) {
1559
+ const skillsDir = join10(installPath, "skills");
1560
+ if (!existsSync5(skillsDir))
1561
+ return [];
1562
+ const skills = [];
1563
+ try {
1564
+ const entries = await readdir2(skillsDir, { withFileTypes: true });
1565
+ for (const entry of entries) {
1566
+ if (!entry.isDirectory())
1567
+ continue;
1568
+ const skillMd = join10(skillsDir, entry.name, "SKILL.md");
1569
+ if (!existsSync5(skillMd))
1570
+ continue;
1571
+ let lastModified = null;
1572
+ try {
1573
+ const fileStat = await stat(skillMd);
1574
+ lastModified = fileStat.mtime.toISOString();
1575
+ } catch {
1576
+ }
1577
+ skills.push({
1578
+ name: entry.name,
1579
+ skillPath: `skills/${entry.name}/SKILL.md`,
1580
+ lastModified
1581
+ });
1582
+ }
1583
+ } catch {
1584
+ }
1585
+ skills.sort((a, b) => a.name.localeCompare(b.name));
1586
+ return skills;
1587
+ }
1588
+ var init_marketplace_scanner = __esm({
1589
+ "dist/scanner/marketplace-scanner.js"() {
1590
+ "use strict";
1591
+ }
1592
+ });
1593
+
1419
1594
  // dist/scanner/index.js
1420
- import { readdir as readdir2 } from "node:fs/promises";
1421
- import { join as join10, relative as relative3 } from "node:path";
1595
+ import { readdir as readdir3 } from "node:fs/promises";
1596
+ import { join as join11, relative as relative3 } from "node:path";
1422
1597
  import { existsSync as existsSync6 } from "node:fs";
1423
- import { homedir as homedir6 } from "node:os";
1598
+ import { homedir as homedir7 } from "node:os";
1424
1599
  import { createHash } from "node:crypto";
1425
1600
  async function scanProject(projectRoot) {
1426
1601
  const allFiles = await discoverFiles(projectRoot);
@@ -1428,7 +1603,7 @@ async function scanProject(projectRoot) {
1428
1603
  const allRefs = [];
1429
1604
  const allBrokenRefs = [];
1430
1605
  for (const file of allFiles) {
1431
- const fullPath = file.startsWith("/") ? file : join10(projectRoot, file);
1606
+ const fullPath = file.startsWith("/") ? file : join11(projectRoot, file);
1432
1607
  try {
1433
1608
  const { refs, brokenRefs: brokenRefs2 } = await parseFileReferences(fullPath, projectRoot);
1434
1609
  allRefs.push(...refs);
@@ -1442,6 +1617,7 @@ async function scanProject(projectRoot) {
1442
1617
  const skills = await parseSkills(projectRoot);
1443
1618
  const toolings = await detectToolings(projectRoot);
1444
1619
  const envManifest = await scanEnvManifest(projectRoot);
1620
+ const marketplacePlugins = await scanMarketplacePlugins();
1445
1621
  const depthMap = /* @__PURE__ */ new Map();
1446
1622
  const queue = [...rootFiles];
1447
1623
  for (const r of queue)
@@ -1462,7 +1638,7 @@ async function scanProject(projectRoot) {
1462
1638
  depth: depthMap.get(br.from) ?? 999
1463
1639
  }));
1464
1640
  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 });
1641
+ const scanData = JSON.stringify({ graph, orphans, brokenRefs, skills, staleFiles, toolings, envManifest, marketplacePlugins });
1466
1642
  const dataHash = createHash("sha256").update(scanData).digest("hex");
1467
1643
  return {
1468
1644
  graph,
@@ -1472,32 +1648,33 @@ async function scanProject(projectRoot) {
1472
1648
  staleFiles,
1473
1649
  toolings,
1474
1650
  envManifest,
1651
+ marketplacePlugins,
1475
1652
  scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
1476
1653
  dataHash
1477
1654
  };
1478
1655
  }
1479
1656
  async function discoverFiles(projectRoot) {
1480
1657
  const files = [];
1481
- const claudeDir = join10(projectRoot, ".claude");
1658
+ const claudeDir = join11(projectRoot, ".claude");
1482
1659
  if (existsSync6(claudeDir)) {
1483
1660
  await walkDir(claudeDir, projectRoot, files);
1484
1661
  }
1485
- if (existsSync6(join10(projectRoot, "CLAUDE.md"))) {
1662
+ if (existsSync6(join11(projectRoot, "CLAUDE.md"))) {
1486
1663
  files.push("CLAUDE.md");
1487
1664
  }
1488
- if (existsSync6(join10(projectRoot, "skills.md"))) {
1665
+ if (existsSync6(join11(projectRoot, "skills.md"))) {
1489
1666
  files.push("skills.md");
1490
1667
  }
1491
- const plansDir = join10(projectRoot, "docs", "plans");
1668
+ const plansDir = join11(projectRoot, "docs", "plans");
1492
1669
  if (existsSync6(plansDir)) {
1493
1670
  await walkDir(plansDir, projectRoot, files);
1494
1671
  }
1495
1672
  return [...new Set(files)];
1496
1673
  }
1497
1674
  async function walkDir(dir, projectRoot, files) {
1498
- const entries = await readdir2(dir, { withFileTypes: true });
1675
+ const entries = await readdir3(dir, { withFileTypes: true });
1499
1676
  for (const entry of entries) {
1500
- const fullPath = join10(dir, entry.name);
1677
+ const fullPath = join11(dir, entry.name);
1501
1678
  if (entry.isDirectory()) {
1502
1679
  if (["node_modules", ".git", ".turbo", "cache", "session-env"].includes(entry.name))
1503
1680
  continue;
@@ -1516,71 +1693,43 @@ function identifyRoots(allFiles, projectRoot) {
1516
1693
  }
1517
1694
  }
1518
1695
  for (const globalFile of GLOBAL_ROOT_FILES) {
1519
- const expanded = globalFile.replace("~", homedir6());
1696
+ const expanded = globalFile.replace("~", homedir7());
1520
1697
  if (existsSync6(expanded)) {
1521
1698
  roots.push(globalFile);
1522
1699
  }
1523
1700
  }
1524
1701
  return roots;
1525
1702
  }
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
- ];
1703
+ async function readClaudeConfigFiles(projectRoot, graphFilePaths) {
1704
+ const { readFile: readFile13, stat: stat2 } = await import("node:fs/promises");
1705
+ const { existsSync: existsSync14 } = await import("node:fs");
1706
+ const filePaths = graphFilePaths ?? await discoverFiles(projectRoot);
1537
1707
  const files = [];
1538
1708
  const seen = /* @__PURE__ */ new Set();
1539
- async function addFile(fullPath) {
1540
- const relPath = relative5(projectRoot, fullPath);
1709
+ for (const relPath of filePaths) {
1541
1710
  if (seen.has(relPath))
1542
- return;
1711
+ continue;
1712
+ if (relPath.startsWith("~") || relPath.startsWith("/"))
1713
+ continue;
1543
1714
  seen.add(relPath);
1715
+ const fullPath = join11(projectRoot, relPath);
1716
+ if (!existsSync14(fullPath))
1717
+ continue;
1544
1718
  try {
1545
- const content = await readFile12(fullPath, "utf-8");
1546
- const fileStat = await stat(fullPath);
1719
+ const content = await readFile13(fullPath, "utf-8");
1720
+ const fileStat = await stat2(fullPath);
1547
1721
  const lastMod = getGitLastModified(fullPath, projectRoot);
1548
1722
  files.push({ filePath: relPath, content, sizeBytes: fileStat.size, lastModified: lastMod });
1549
1723
  } catch {
1550
1724
  }
1551
1725
  }
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
1726
  return files;
1578
1727
  }
1579
1728
  function detectStaleFiles(allFiles, projectRoot) {
1580
1729
  const stale = [];
1581
1730
  const now = Date.now();
1582
1731
  for (const file of allFiles) {
1583
- const fullPath = join10(projectRoot, file);
1732
+ const fullPath = join11(projectRoot, file);
1584
1733
  const lastMod = getGitLastModified(fullPath, projectRoot);
1585
1734
  if (lastMod) {
1586
1735
  const days = Math.floor((now - new Date(lastMod).getTime()) / (1e3 * 60 * 60 * 24));
@@ -1602,6 +1751,7 @@ var init_scanner = __esm({
1602
1751
  init_git_dates();
1603
1752
  init_tooling_detector();
1604
1753
  init_env_manifest_scanner();
1754
+ init_marketplace_scanner();
1605
1755
  }
1606
1756
  });
1607
1757
 
@@ -1708,7 +1858,7 @@ function generateOfflineHtml(result, projectRoot) {
1708
1858
  }
1709
1859
  </script>
1710
1860
 
1711
- <script type="application/json" id="scan-data">${dataJson}</script>
1861
+ <script type="application/json" id="scan-data">${dataJson.replace(/<\//g, "<\\/")}</script>
1712
1862
  </body>
1713
1863
  </html>`;
1714
1864
  }
@@ -1721,74 +1871,6 @@ var init_html_generator = __esm({
1721
1871
  }
1722
1872
  });
1723
1873
 
1724
- // dist/check-update.js
1725
- import chalk9 from "chalk";
1726
- async function fetchLatest() {
1727
- try {
1728
- const controller = new AbortController();
1729
- const timeout = setTimeout(() => controller.abort(), 3e3);
1730
- const res = await fetch("https://registry.npmjs.org/md4ai/latest", {
1731
- signal: controller.signal
1732
- });
1733
- clearTimeout(timeout);
1734
- if (!res.ok)
1735
- return null;
1736
- const data = await res.json();
1737
- return data.version ?? null;
1738
- } catch {
1739
- return null;
1740
- }
1741
- }
1742
- function printUpdateBanner(latest) {
1743
- console.log("");
1744
- console.log(chalk9.yellow("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
1745
- console.log(chalk9.yellow("\u2502") + chalk9.bold(" Update available! ") + chalk9.dim(`${CURRENT_VERSION}`) + chalk9.white(" \u2192 ") + chalk9.green.bold(`${latest}`) + " " + chalk9.yellow("\u2502"));
1746
- console.log(chalk9.yellow("\u2502") + " " + chalk9.yellow("\u2502"));
1747
- console.log(chalk9.yellow("\u2502") + " Run: " + chalk9.cyan("md4ai update") + " " + chalk9.yellow("\u2502"));
1748
- console.log(chalk9.yellow("\u2502") + " " + chalk9.yellow("\u2502"));
1749
- console.log(chalk9.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
1750
- console.log("");
1751
- }
1752
- async function checkForUpdate() {
1753
- const latest = await fetchLatest();
1754
- if (!latest)
1755
- return;
1756
- if (latest !== CURRENT_VERSION && isNewer(latest, CURRENT_VERSION)) {
1757
- printUpdateBanner(latest);
1758
- } else {
1759
- console.log(chalk9.green(`md4ai v${CURRENT_VERSION} \u2014 you're on the latest version.`));
1760
- }
1761
- }
1762
- async function autoCheckForUpdate() {
1763
- try {
1764
- const latest = await fetchLatest();
1765
- if (!latest)
1766
- return;
1767
- if (latest !== CURRENT_VERSION && isNewer(latest, CURRENT_VERSION)) {
1768
- printUpdateBanner(latest);
1769
- }
1770
- } catch {
1771
- }
1772
- }
1773
- function isNewer(a, b) {
1774
- const pa = a.split(".").map(Number);
1775
- const pb = b.split(".").map(Number);
1776
- for (let i = 0; i < 3; i++) {
1777
- if ((pa[i] ?? 0) > (pb[i] ?? 0))
1778
- return true;
1779
- if ((pa[i] ?? 0) < (pb[i] ?? 0))
1780
- return false;
1781
- }
1782
- return false;
1783
- }
1784
- var CURRENT_VERSION;
1785
- var init_check_update = __esm({
1786
- "dist/check-update.js"() {
1787
- "use strict";
1788
- CURRENT_VERSION = true ? "0.10.2" : "0.0.0-dev";
1789
- }
1790
- });
1791
-
1792
1874
  // dist/commands/push-toolings.js
1793
1875
  async function pushToolings(supabase, folderId, toolings) {
1794
1876
  if (!toolings.length)
@@ -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
  }
@@ -2981,28 +3116,71 @@ var init_mcp_watch = __esm({
2981
3116
  });
2982
3117
 
2983
3118
  // dist/index.js
2984
- init_login();
2985
3119
  import { Command } from "commander";
2986
3120
 
3121
+ // dist/sentry.js
3122
+ init_check_update();
3123
+ import * as Sentry from "@sentry/node";
3124
+ var DSN = process.env.MD4AI_SENTRY_DSN;
3125
+ function initSentry() {
3126
+ if (!DSN)
3127
+ return;
3128
+ Sentry.init({
3129
+ dsn: DSN,
3130
+ release: `md4ai-cli@${CURRENT_VERSION}`,
3131
+ environment: process.env.NODE_ENV === "development" ? "development" : "production",
3132
+ tracesSampleRate: 0,
3133
+ beforeSend(event) {
3134
+ if (event.exception?.values) {
3135
+ for (const exc of event.exception.values) {
3136
+ if (exc.stacktrace?.frames) {
3137
+ for (const frame of exc.stacktrace.frames) {
3138
+ if (frame.filename) {
3139
+ const idx = frame.filename.indexOf("md4ai");
3140
+ if (idx >= 0)
3141
+ frame.filename = frame.filename.slice(idx);
3142
+ }
3143
+ }
3144
+ }
3145
+ }
3146
+ }
3147
+ return event;
3148
+ }
3149
+ });
3150
+ }
3151
+ function captureException2(err) {
3152
+ if (!DSN)
3153
+ return;
3154
+ Sentry.captureException(err);
3155
+ }
3156
+ async function flushSentry() {
3157
+ if (!DSN)
3158
+ return;
3159
+ await Sentry.flush(2e3);
3160
+ }
3161
+
3162
+ // dist/index.js
3163
+ init_login();
3164
+
2987
3165
  // dist/commands/logout.js
2988
3166
  init_config();
2989
- import chalk2 from "chalk";
3167
+ import chalk3 from "chalk";
2990
3168
  async function logoutCommand() {
2991
3169
  await clearCredentials();
2992
- console.log(chalk2.green("Logged out successfully."));
3170
+ console.log(chalk3.green("Logged out successfully."));
2993
3171
  }
2994
3172
 
2995
3173
  // dist/commands/status.js
2996
3174
  init_dist();
2997
3175
  init_config();
2998
- import chalk3 from "chalk";
3176
+ import chalk4 from "chalk";
2999
3177
  async function statusCommand() {
3000
3178
  const creds = await loadCredentials();
3001
3179
  if (!creds?.accessToken) {
3002
- console.log(chalk3.yellow("Not logged in. Run: md4ai login"));
3180
+ console.log(chalk4.yellow("Not logged in. Run: md4ai login"));
3003
3181
  return;
3004
3182
  }
3005
- console.log(chalk3.blue("MD4AI Status\n"));
3183
+ console.log(chalk4.blue("MD4AI Status\n"));
3006
3184
  console.log(` User: ${creds.email}`);
3007
3185
  console.log(` Expires: ${new Date(creds.expiresAt).toLocaleString()}`);
3008
3186
  try {
@@ -3015,13 +3193,13 @@ async function statusCommand() {
3015
3193
  const state = await loadState();
3016
3194
  console.log(` Last sync: ${state.lastSyncAt ?? "never"}`);
3017
3195
  } catch {
3018
- console.log(chalk3.yellow(" (Could not fetch remote data)"));
3196
+ console.log(chalk4.yellow(" (Could not fetch remote data)"));
3019
3197
  }
3020
3198
  }
3021
3199
 
3022
3200
  // dist/commands/add-folder.js
3023
3201
  init_auth();
3024
- import chalk5 from "chalk";
3202
+ import chalk6 from "chalk";
3025
3203
  import { input as input3, select } from "@inquirer/prompts";
3026
3204
  async function addFolderCommand() {
3027
3205
  const { supabase, userId } = await getAuthenticatedClient();
@@ -3060,11 +3238,11 @@ async function addFolderCommand() {
3060
3238
  team_id: teamId
3061
3239
  }).select().single();
3062
3240
  if (error) {
3063
- console.error(chalk5.red(`Failed to create folder: ${error.message}`));
3241
+ console.error(chalk6.red(`Failed to create folder: ${error.message}`));
3064
3242
  process.exit(1);
3065
3243
  }
3066
3244
  const teamLabel = teamId ? `(team: ${allTeams.get(teamId)})` : "(personal)";
3067
- console.log(chalk5.green(`
3245
+ console.log(chalk6.green(`
3068
3246
  Folder "${name}" created ${teamLabel} (${data.id})`));
3069
3247
  }
3070
3248
 
@@ -3072,7 +3250,7 @@ Folder "${name}" created ${teamLabel} (${data.id})`));
3072
3250
  init_auth();
3073
3251
  import { resolve } from "node:path";
3074
3252
  import { hostname, platform } from "node:os";
3075
- import chalk6 from "chalk";
3253
+ import chalk7 from "chalk";
3076
3254
  import { input as input4, select as select2 } from "@inquirer/prompts";
3077
3255
  function detectOs() {
3078
3256
  const p = platform();
@@ -3093,14 +3271,14 @@ function suggestDeviceName() {
3093
3271
  async function addDeviceCommand() {
3094
3272
  const { supabase, userId } = await getAuthenticatedClient();
3095
3273
  const cwd = resolve(process.cwd());
3096
- console.log(chalk6.blue.bold("\n\u{1F4C2} Where is this Claude project?\n"));
3274
+ console.log(chalk7.blue.bold("\n\u{1F4C2} Where is this Claude project?\n"));
3097
3275
  const localPath = await input4({
3098
3276
  message: "Local project path:",
3099
3277
  default: cwd
3100
3278
  });
3101
3279
  const { data: folders, error: foldersErr } = await supabase.from("claude_folders").select("id, name").order("name");
3102
3280
  if (foldersErr || !folders?.length) {
3103
- console.error(chalk6.red("No folders found. Run: md4ai add-folder"));
3281
+ console.error(chalk7.red("No folders found. Run: md4ai add-folder"));
3104
3282
  process.exit(1);
3105
3283
  }
3106
3284
  const folderId = await select2({
@@ -3133,16 +3311,16 @@ async function addDeviceCommand() {
3133
3311
  description: description || null
3134
3312
  });
3135
3313
  if (error) {
3136
- console.error(chalk6.red(`Failed to add device: ${error.message}`));
3314
+ console.error(chalk7.red(`Failed to add device: ${error.message}`));
3137
3315
  process.exit(1);
3138
3316
  }
3139
- console.log(chalk6.green(`
3317
+ console.log(chalk7.green(`
3140
3318
  Device "${deviceName}" added to folder.`));
3141
3319
  }
3142
3320
 
3143
3321
  // dist/commands/list-devices.js
3144
3322
  init_auth();
3145
- import chalk7 from "chalk";
3323
+ import chalk8 from "chalk";
3146
3324
  async function listDevicesCommand() {
3147
3325
  const { supabase } = await getAuthenticatedClient();
3148
3326
  const { data: devices, error } = await supabase.from("device_paths").select(`
@@ -3150,11 +3328,11 @@ async function listDevicesCommand() {
3150
3328
  claude_folders!inner ( name )
3151
3329
  `).order("device_name");
3152
3330
  if (error) {
3153
- console.error(chalk7.red(`Failed to list devices: ${error.message}`));
3331
+ console.error(chalk8.red(`Failed to list devices: ${error.message}`));
3154
3332
  process.exit(1);
3155
3333
  }
3156
3334
  if (!devices?.length) {
3157
- console.log(chalk7.yellow("No devices found. Run: md4ai add-device"));
3335
+ console.log(chalk8.yellow("No devices found. Run: md4ai add-device"));
3158
3336
  return;
3159
3337
  }
3160
3338
  const grouped = /* @__PURE__ */ new Map();
@@ -3165,12 +3343,12 @@ async function listDevicesCommand() {
3165
3343
  }
3166
3344
  for (const [deviceName, entries] of grouped) {
3167
3345
  const first = entries[0];
3168
- console.log(chalk7.bold(`
3169
- ${deviceName}`) + chalk7.dim(` (${first.os_type})`));
3346
+ console.log(chalk8.bold(`
3347
+ ${deviceName}`) + chalk8.dim(` (${first.os_type})`));
3170
3348
  for (const entry of entries) {
3171
3349
  const folderName = entry.claude_folders?.name ?? "unknown";
3172
3350
  const synced = entry.last_synced ? new Date(entry.last_synced).toLocaleString() : "never";
3173
- console.log(` ${chalk7.cyan(folderName)} \u2192 ${entry.path}`);
3351
+ console.log(` ${chalk8.cyan(folderName)} \u2192 ${entry.path}`);
3174
3352
  console.log(` Last synced: ${synced}`);
3175
3353
  }
3176
3354
  }
@@ -3181,41 +3359,41 @@ init_map();
3181
3359
 
3182
3360
  // dist/commands/simulate.js
3183
3361
  init_dist();
3184
- import { join as join11 } from "node:path";
3362
+ import { join as join12 } from "node:path";
3185
3363
  import { existsSync as existsSync8 } from "node:fs";
3186
- import { homedir as homedir7 } from "node:os";
3364
+ import { homedir as homedir8 } from "node:os";
3187
3365
  import chalk12 from "chalk";
3188
3366
  async function simulateCommand(prompt) {
3189
3367
  const projectRoot = process.cwd();
3190
3368
  console.log(chalk12.blue(`Simulating prompt: "${prompt}"
3191
3369
  `));
3192
3370
  console.log(chalk12.dim("Files Claude would load:\n"));
3193
- const globalClaude = join11(homedir7(), ".claude", "CLAUDE.md");
3371
+ const globalClaude = join12(homedir8(), ".claude", "CLAUDE.md");
3194
3372
  if (existsSync8(globalClaude)) {
3195
3373
  console.log(chalk12.green(" \u2713 ~/.claude/CLAUDE.md (global)"));
3196
3374
  }
3197
3375
  for (const rootFile of ROOT_FILES) {
3198
- const fullPath = join11(projectRoot, rootFile);
3376
+ const fullPath = join12(projectRoot, rootFile);
3199
3377
  if (existsSync8(fullPath)) {
3200
3378
  console.log(chalk12.green(` \u2713 ${rootFile} (project root)`));
3201
3379
  }
3202
3380
  }
3203
3381
  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")
3382
+ join12(homedir8(), ".claude", "settings.json"),
3383
+ join12(homedir8(), ".claude", "settings.local.json"),
3384
+ join12(projectRoot, ".claude", "settings.json"),
3385
+ join12(projectRoot, ".claude", "settings.local.json")
3208
3386
  ];
3209
3387
  for (const sf of settingsFiles) {
3210
3388
  if (existsSync8(sf)) {
3211
- const display = sf.startsWith(homedir7()) ? sf.replace(homedir7(), "~") : sf.replace(projectRoot + "/", "");
3389
+ const display = sf.startsWith(homedir8()) ? sf.replace(homedir8(), "~") : sf.replace(projectRoot + "/", "");
3212
3390
  console.log(chalk12.green(` \u2713 ${display} (settings)`));
3213
3391
  }
3214
3392
  }
3215
3393
  const words = prompt.split(/\s+/);
3216
3394
  for (const word of words) {
3217
3395
  const cleaned = word.replace(/['"]/g, "");
3218
- const candidatePath = join11(projectRoot, cleaned);
3396
+ const candidatePath = join12(projectRoot, cleaned);
3219
3397
  if (existsSync8(candidatePath) && cleaned.includes("/")) {
3220
3398
  console.log(chalk12.cyan(` \u2192 ${cleaned} (referenced in prompt)`));
3221
3399
  }
@@ -3224,18 +3402,18 @@ async function simulateCommand(prompt) {
3224
3402
  }
3225
3403
 
3226
3404
  // dist/commands/print.js
3227
- import { join as join12 } from "node:path";
3228
- import { readFile as readFile8, writeFile as writeFile3 } from "node:fs/promises";
3405
+ import { join as join13 } from "node:path";
3406
+ import { readFile as readFile9, writeFile as writeFile3 } from "node:fs/promises";
3229
3407
  import { existsSync as existsSync9 } from "node:fs";
3230
3408
  import chalk13 from "chalk";
3231
3409
  async function printCommand(title) {
3232
3410
  const projectRoot = process.cwd();
3233
- const scanDataPath = join12(projectRoot, "output", "index.html");
3411
+ const scanDataPath = join13(projectRoot, "output", "index.html");
3234
3412
  if (!existsSync9(scanDataPath)) {
3235
3413
  console.error(chalk13.red("No scan data found. Run: md4ai scan"));
3236
3414
  process.exit(1);
3237
3415
  }
3238
- const html = await readFile8(scanDataPath, "utf-8");
3416
+ const html = await readFile9(scanDataPath, "utf-8");
3239
3417
  const match = html.match(/<script type="application\/json" id="scan-data">([\s\S]*?)<\/script>/);
3240
3418
  if (!match) {
3241
3419
  console.error(chalk13.red("Could not extract scan data from output/index.html"));
@@ -3243,7 +3421,7 @@ async function printCommand(title) {
3243
3421
  }
3244
3422
  const result = JSON.parse(match[1]);
3245
3423
  const printHtml = generatePrintHtml(result, title);
3246
- const outputPath = join12(projectRoot, "output", `print-${Date.now()}.html`);
3424
+ const outputPath = join13(projectRoot, "output", `print-${Date.now()}.html`);
3247
3425
  await writeFile3(outputPath, printHtml, "utf-8");
3248
3426
  console.log(chalk13.green(`Print-ready wall sheet: ${outputPath}`));
3249
3427
  }
@@ -3310,11 +3488,11 @@ init_config();
3310
3488
  init_scanner();
3311
3489
  init_push_toolings();
3312
3490
  init_device_utils();
3313
- import { resolve as resolve4 } from "node:path";
3491
+ import { resolve as resolve6 } from "node:path";
3314
3492
  import chalk15 from "chalk";
3315
3493
  async function linkCommand(projectId) {
3316
3494
  const { supabase, userId } = await getAuthenticatedClient();
3317
- const cwd = resolve4(process.cwd());
3495
+ const cwd = resolve6(process.cwd());
3318
3496
  const deviceName = detectDeviceName();
3319
3497
  const osType = detectOs2();
3320
3498
  const { data: folder, error: folderErr } = await supabase.from("claude_folders").select("id, name").eq("id", projectId).single();
@@ -3360,6 +3538,7 @@ Linking "${folder.name}" to this device...
3360
3538
  console.log(` Skills: ${result.skills.length}`);
3361
3539
  console.log(` Toolings: ${result.toolings.length}`);
3362
3540
  console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
3541
+ console.log(` Plugins: ${result.marketplacePlugins.length} (${result.marketplacePlugins.reduce((n, p) => n + p.skills.length, 0)} skills)`);
3363
3542
  const { error: scanErr } = await supabase.from("claude_folders").update({
3364
3543
  graph_json: result.graph,
3365
3544
  orphans_json: result.orphans,
@@ -3367,6 +3546,7 @@ Linking "${folder.name}" to this device...
3367
3546
  skills_table_json: result.skills,
3368
3547
  stale_files_json: result.staleFiles,
3369
3548
  env_manifest_json: result.envManifest,
3549
+ marketplace_plugins_json: result.marketplacePlugins,
3370
3550
  last_scanned: result.scannedAt,
3371
3551
  data_hash: result.dataHash
3372
3552
  }).eq("id", folder.id);
@@ -3374,7 +3554,8 @@ Linking "${folder.name}" to this device...
3374
3554
  console.error(chalk15.yellow(`Scan upload warning: ${scanErr.message}`));
3375
3555
  }
3376
3556
  await pushToolings(supabase, folder.id, result.toolings);
3377
- const configFiles = await readClaudeConfigFiles(cwd);
3557
+ const graphPaths = result.graph.nodes.map((n) => n.filePath);
3558
+ const configFiles = await readClaudeConfigFiles(cwd, graphPaths);
3378
3559
  if (configFiles.length > 0) {
3379
3560
  for (const file of configFiles) {
3380
3561
  await supabase.from("folder_files").upsert({
@@ -3402,18 +3583,24 @@ Linking "${folder.name}" to this device...
3402
3583
  }
3403
3584
 
3404
3585
  // 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";
3586
+ import { readFile as readFile10, writeFile as writeFile4, mkdir as mkdir3, stat as fsStat } from "node:fs/promises";
3587
+ import { dirname as dirname3, resolve as resolve7 } from "node:path";
3588
+ import { existsSync as existsSync11 } from "node:fs";
3408
3589
  import chalk16 from "chalk";
3409
3590
  import { confirm as confirm3, input as input6 } from "@inquirer/prompts";
3591
+ var MAX_BUNDLE_SIZE_BYTES = 50 * 1024 * 1024;
3410
3592
  async function importBundleCommand(zipPath) {
3411
- if (!existsSync10(zipPath)) {
3593
+ if (!existsSync11(zipPath)) {
3412
3594
  console.error(chalk16.red(`File not found: ${zipPath}`));
3413
3595
  process.exit(1);
3414
3596
  }
3597
+ const fileStat = await fsStat(zipPath);
3598
+ if (fileStat.size > MAX_BUNDLE_SIZE_BYTES) {
3599
+ console.error(chalk16.red(`Bundle too large (${Math.round(fileStat.size / 1024 / 1024)} MB). Maximum is 50 MB.`));
3600
+ process.exit(1);
3601
+ }
3415
3602
  const JSZip = (await import("jszip")).default;
3416
- const zipData = await readFile9(zipPath);
3603
+ const zipData = await readFile10(zipPath);
3417
3604
  const zip = await JSZip.loadAsync(zipData);
3418
3605
  const manifestFile = zip.file("manifest.json");
3419
3606
  if (!manifestFile) {
@@ -3454,10 +3641,15 @@ Files to extract:`));
3454
3641
  console.log(chalk16.yellow("Cancelled."));
3455
3642
  return;
3456
3643
  }
3644
+ const resolvedTarget = resolve7(targetDir);
3457
3645
  for (const file of files) {
3458
- const fullPath = join13(targetDir, file.filePath);
3646
+ const fullPath = resolve7(targetDir, file.filePath);
3647
+ if (!fullPath.startsWith(resolvedTarget + "/") && fullPath !== resolvedTarget) {
3648
+ console.error(chalk16.red(` Blocked path traversal: ${file.filePath}`));
3649
+ continue;
3650
+ }
3459
3651
  const dir = dirname3(fullPath);
3460
- if (!existsSync10(dir)) {
3652
+ if (!existsSync11(dir)) {
3461
3653
  await mkdir3(dir, { recursive: true });
3462
3654
  }
3463
3655
  await writeFile4(fullPath, file.content, "utf-8");
@@ -3762,9 +3954,9 @@ async function fetchGitHubVersions(repo, signal) {
3762
3954
  init_mcp_watch();
3763
3955
 
3764
3956
  // 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";
3957
+ import { resolve as resolve8, join as join15, relative as relative4, dirname as dirname4 } from "node:path";
3958
+ import { readFile as readFile12, writeFile as writeFile5, mkdir as mkdir4 } from "node:fs/promises";
3959
+ import { existsSync as existsSync13 } from "node:fs";
3768
3960
  import chalk21 from "chalk";
3769
3961
  var SECRET_PATTERNS = [
3770
3962
  /_KEY$/i,
@@ -3782,7 +3974,7 @@ async function discoverEnvFiles(projectRoot) {
3782
3974
  const apps = [];
3783
3975
  for (const envName of [".env", ".env.local", ".env.example"]) {
3784
3976
  const envPath = join15(projectRoot, envName);
3785
- if (existsSync12(envPath)) {
3977
+ if (existsSync13(envPath)) {
3786
3978
  const vars = await extractVarNames(envPath);
3787
3979
  if (vars.length > 0) {
3788
3980
  apps.push({ name: "root", envFilePath: envName, vars });
@@ -3793,11 +3985,11 @@ async function discoverEnvFiles(projectRoot) {
3793
3985
  const subdirs = ["web", "cli", "api", "app", "server", "packages"];
3794
3986
  for (const sub of subdirs) {
3795
3987
  const subDir = join15(projectRoot, sub);
3796
- if (!existsSync12(subDir))
3988
+ if (!existsSync13(subDir))
3797
3989
  continue;
3798
3990
  for (const envName of [".env.local", ".env", ".env.example"]) {
3799
3991
  const envPath = join15(subDir, envName);
3800
- if (existsSync12(envPath)) {
3992
+ if (existsSync13(envPath)) {
3801
3993
  const vars = await extractVarNames(envPath);
3802
3994
  if (vars.length > 0) {
3803
3995
  apps.push({
@@ -3813,7 +4005,7 @@ async function discoverEnvFiles(projectRoot) {
3813
4005
  return apps;
3814
4006
  }
3815
4007
  async function extractVarNames(envPath) {
3816
- const content = await readFile11(envPath, "utf-8");
4008
+ const content = await readFile12(envPath, "utf-8");
3817
4009
  const vars = [];
3818
4010
  for (const line of content.split("\n")) {
3819
4011
  const trimmed = line.trim();
@@ -3857,9 +4049,9 @@ function generateManifest(apps) {
3857
4049
  return lines.join("\n");
3858
4050
  }
3859
4051
  async function initManifestCommand() {
3860
- const projectRoot = resolve5(process.cwd());
4052
+ const projectRoot = resolve8(process.cwd());
3861
4053
  const manifestPath = join15(projectRoot, "docs", "reference", "env-manifest.md");
3862
- if (existsSync12(manifestPath)) {
4054
+ if (existsSync13(manifestPath)) {
3863
4055
  console.log(chalk21.yellow(`Manifest already exists: ${relative4(projectRoot, manifestPath)}`));
3864
4056
  console.log(chalk21.dim("Edit it directly to make changes."));
3865
4057
  return;
@@ -3876,7 +4068,7 @@ async function initManifestCommand() {
3876
4068
  }
3877
4069
  const content = generateManifest(apps);
3878
4070
  const dir = dirname4(manifestPath);
3879
- if (!existsSync12(dir)) {
4071
+ if (!existsSync13(dir)) {
3880
4072
  await mkdir4(dir, { recursive: true });
3881
4073
  }
3882
4074
  await writeFile5(manifestPath, content, "utf-8");
@@ -4036,7 +4228,7 @@ init_check_update();
4036
4228
  init_map();
4037
4229
  init_mcp_watch();
4038
4230
  import chalk23 from "chalk";
4039
- import { resolve as resolve6 } from "node:path";
4231
+ import { resolve as resolve9 } from "node:path";
4040
4232
  async function fetchLatestVersion2() {
4041
4233
  try {
4042
4234
  const controller = new AbortController();
@@ -4065,7 +4257,7 @@ function isNewer3(a, b) {
4065
4257
  return false;
4066
4258
  }
4067
4259
  async function startCommand() {
4068
- const projectRoot = resolve6(process.cwd());
4260
+ const projectRoot = resolve9(process.cwd());
4069
4261
  console.log("");
4070
4262
  console.log(chalk23.bold.cyan(` MD4AI v${CURRENT_VERSION}`));
4071
4263
  console.log(chalk23.dim(` ${projectRoot}`));
@@ -4169,6 +4361,7 @@ var admin = program.command("admin").description("Admin commands for managing th
4169
4361
  admin.command("update-tool").description("Add or update a tool in the master registry").requiredOption("--name <name>", "Canonical tool name (e.g. next, playwright)").option("--display <display>", 'Human-friendly display name (e.g. "Next.js")').option("--category <category>", "Tool category (framework|runtime|cli|mcp|package|database|other)").option("--stable <version>", "Latest stable version").option("--beta <version>", "Latest beta/RC version").option("--source <url>", "Source of truth URL for checking versions").option("--install <url>", "Download/install link").option("--notes <text>", "Compatibility notes or warnings").action(adminUpdateToolCommand);
4170
4362
  admin.command("list-tools").description("List all tools in the master registry").action(adminListToolsCommand);
4171
4363
  admin.command("fetch-versions").description("Fetch latest stable and beta versions from npm and GitHub").action(adminFetchVersionsCommand);
4364
+ initSentry();
4172
4365
  if (process.argv.length <= 2) {
4173
4366
  void startCommand();
4174
4367
  } else {
@@ -4179,3 +4372,11 @@ if (process.argv.length <= 2) {
4179
4372
  autoCheckForUpdate();
4180
4373
  }
4181
4374
  }
4375
+ process.on("uncaughtException", async (err) => {
4376
+ captureException2(err);
4377
+ await flushSentry();
4378
+ process.exit(1);
4379
+ });
4380
+ process.on("unhandledRejection", (reason) => {
4381
+ captureException2(reason);
4382
+ });