md4ai 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.bundled.js +423 -70
  2. package/package.json +7 -2
@@ -319,13 +319,13 @@ ${deviceName}`) + chalk7.dim(` (${first.os_type})`));
319
319
  // dist/commands/map.js
320
320
  import { resolve as resolve3 } from "node:path";
321
321
  import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
322
- import { existsSync as existsSync6 } from "node:fs";
322
+ import { existsSync as existsSync7 } from "node:fs";
323
323
  import chalk9 from "chalk";
324
324
 
325
325
  // dist/scanner/index.js
326
326
  import { readdir as readdir2 } from "node:fs/promises";
327
- import { join as join7, relative } from "node:path";
328
- import { existsSync as existsSync5 } from "node:fs";
327
+ import { join as join8, relative as relative2 } from "node:path";
328
+ import { existsSync as existsSync6 } from "node:fs";
329
329
  import { homedir as homedir5 } from "node:os";
330
330
  import { createHash } from "node:crypto";
331
331
 
@@ -923,13 +923,211 @@ async function detectFromMcpSettings(projectRoot) {
923
923
  return toolings;
924
924
  }
925
925
 
926
+ // dist/scanner/env-manifest-scanner.js
927
+ import { readFile as readFile5, glob } from "node:fs/promises";
928
+ import { join as join7, relative } from "node:path";
929
+ import { existsSync as existsSync5 } from "node:fs";
930
+ var MANIFEST_PATH = "docs/reference/env-manifest.md";
931
+ async function scanEnvManifest(projectRoot) {
932
+ const manifestFullPath = join7(projectRoot, MANIFEST_PATH);
933
+ if (!existsSync5(manifestFullPath)) {
934
+ return null;
935
+ }
936
+ const manifestContent = await readFile5(manifestFullPath, "utf-8");
937
+ const apps = parseApps(manifestContent);
938
+ const variables = parseVariables(manifestContent);
939
+ const localPresence = await checkLocalEnvFiles(projectRoot, apps);
940
+ for (const v of variables) {
941
+ v.localStatus = {};
942
+ for (const app of v.requiredIn.local) {
943
+ v.localStatus[app] = localPresence.get(app)?.has(v.name) ? "present" : "missing";
944
+ }
945
+ }
946
+ const workflowRefs = await parseWorkflowSecrets(projectRoot);
947
+ const drift = computeDrift(variables, apps, localPresence, workflowRefs);
948
+ return {
949
+ manifestFound: true,
950
+ manifestPath: MANIFEST_PATH,
951
+ apps: apps.map((a) => a.name),
952
+ variables,
953
+ drift,
954
+ workflowRefs,
955
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
956
+ };
957
+ }
958
+ function parseApps(content) {
959
+ const apps = [];
960
+ const appsSection = extractSection(content, "Apps");
961
+ if (!appsSection)
962
+ return apps;
963
+ const lineRe = /^-\s+(\w+):\s+`([^`]+)`/;
964
+ for (const line of appsSection.split("\n")) {
965
+ const m = line.match(lineRe);
966
+ if (m) {
967
+ apps.push({ name: m[1], envFilePath: m[2] });
968
+ }
969
+ }
970
+ return apps;
971
+ }
972
+ function parseVariables(content) {
973
+ const variables = [];
974
+ const varsSection = extractSection(content, "Variables");
975
+ if (!varsSection)
976
+ return variables;
977
+ const lines = varsSection.split("\n");
978
+ let tableStarted = false;
979
+ for (const line of lines) {
980
+ const trimmed = line.trim();
981
+ if (!trimmed.startsWith("|"))
982
+ continue;
983
+ if (trimmed.includes("Variable") && trimmed.includes("Public")) {
984
+ tableStarted = true;
985
+ continue;
986
+ }
987
+ if (/^\|[\s-|]+\|$/.test(trimmed)) {
988
+ continue;
989
+ }
990
+ if (!tableStarted)
991
+ continue;
992
+ const cells = trimmed.split("|").slice(1, -1).map((c) => c.trim());
993
+ if (cells.length < 7)
994
+ continue;
995
+ const [nameCell, publicCell, localCell, vercelCell, githubCell, supabaseCell, notesCell] = cells;
996
+ const nameMatch = nameCell.match(/`([^`]+)`/);
997
+ if (!nameMatch)
998
+ continue;
999
+ const name = nameMatch[1];
1000
+ const isPublic = publicCell.toLowerCase() === "yes";
1001
+ const parseAppList = (cell) => {
1002
+ if (cell === "---" || cell === "")
1003
+ return [];
1004
+ return cell.split(",").map((s) => s.trim()).filter(Boolean);
1005
+ };
1006
+ const localApps = parseAppList(localCell);
1007
+ const vercelProjects = parseAppList(vercelCell);
1008
+ const githubNeeded = githubCell.toLowerCase() === "yes" || githubCell.includes("\u2713") || githubCell.includes("\u2714");
1009
+ const supabaseNeeded = supabaseCell.toLowerCase() === "source" || supabaseCell.toLowerCase() === "yes";
1010
+ variables.push({
1011
+ name,
1012
+ isPublic,
1013
+ requiredIn: {
1014
+ local: localApps,
1015
+ vercel: vercelProjects,
1016
+ github: githubNeeded,
1017
+ supabase: supabaseNeeded
1018
+ },
1019
+ localStatus: {},
1020
+ notes: notesCell || ""
1021
+ });
1022
+ }
1023
+ return variables;
1024
+ }
1025
+ function extractSection(content, heading) {
1026
+ const re = new RegExp(`^##\\s+${heading}\\s*$`, "m");
1027
+ const match = re.exec(content);
1028
+ if (!match)
1029
+ return null;
1030
+ const start = match.index + match[0].length;
1031
+ const nextHeading = content.indexOf("\n## ", start);
1032
+ return nextHeading === -1 ? content.slice(start) : content.slice(start, nextHeading);
1033
+ }
1034
+ async function checkLocalEnvFiles(projectRoot, apps) {
1035
+ const result = /* @__PURE__ */ new Map();
1036
+ for (const app of apps) {
1037
+ const envPath = join7(projectRoot, app.envFilePath);
1038
+ const varNames = /* @__PURE__ */ new Set();
1039
+ if (existsSync5(envPath)) {
1040
+ const content = await readFile5(envPath, "utf-8");
1041
+ for (const line of content.split("\n")) {
1042
+ const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
1043
+ if (m) {
1044
+ varNames.add(m[1]);
1045
+ }
1046
+ }
1047
+ }
1048
+ result.set(app.name, varNames);
1049
+ }
1050
+ return result;
1051
+ }
1052
+ async function parseWorkflowSecrets(projectRoot) {
1053
+ const workflowsDir = join7(projectRoot, ".github", "workflows");
1054
+ if (!existsSync5(workflowsDir))
1055
+ return [];
1056
+ const refs = [];
1057
+ const secretRe = /\$\{\{\s*secrets\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
1058
+ for (const ext of ["*.yml", "*.yaml"]) {
1059
+ for await (const filePath of glob(join7(workflowsDir, ext))) {
1060
+ const content = await readFile5(filePath, "utf-8");
1061
+ const secrets = /* @__PURE__ */ new Set();
1062
+ let m;
1063
+ while ((m = secretRe.exec(content)) !== null) {
1064
+ secrets.add(m[1]);
1065
+ }
1066
+ secretRe.lastIndex = 0;
1067
+ if (secrets.size > 0) {
1068
+ refs.push({
1069
+ workflowFile: relative(projectRoot, filePath),
1070
+ secretsUsed: [...secrets].sort()
1071
+ });
1072
+ }
1073
+ }
1074
+ }
1075
+ return refs;
1076
+ }
1077
+ function computeDrift(variables, apps, localPresence, workflowRefs) {
1078
+ const missingFromLocal = [];
1079
+ const undocumented = [];
1080
+ const documentedNames = new Set(variables.map((v) => v.name));
1081
+ for (const v of variables) {
1082
+ for (const app of v.requiredIn.local) {
1083
+ if (v.localStatus[app] === "missing") {
1084
+ missingFromLocal.push({ variable: v.name, app });
1085
+ }
1086
+ }
1087
+ }
1088
+ for (const app of apps) {
1089
+ const present = localPresence.get(app.name);
1090
+ if (!present)
1091
+ continue;
1092
+ for (const varName of present) {
1093
+ if (!documentedNames.has(varName)) {
1094
+ undocumented.push({ variable: varName, app: app.name });
1095
+ }
1096
+ }
1097
+ }
1098
+ const allWorkflowSecrets = /* @__PURE__ */ new Set();
1099
+ for (const ref of workflowRefs) {
1100
+ for (const s of ref.secretsUsed) {
1101
+ allWorkflowSecrets.add(s);
1102
+ }
1103
+ }
1104
+ const workflowDeadSecrets = [];
1105
+ for (const v of variables) {
1106
+ if (v.requiredIn.github && !allWorkflowSecrets.has(v.name)) {
1107
+ workflowDeadSecrets.push(v.name);
1108
+ }
1109
+ }
1110
+ const workflowBrokenRefs = [];
1111
+ for (const secret of allWorkflowSecrets) {
1112
+ if (!documentedNames.has(secret)) {
1113
+ workflowBrokenRefs.push(secret);
1114
+ }
1115
+ }
1116
+ return {
1117
+ missingFromLocal,
1118
+ undocumented,
1119
+ workflowDeadSecrets: workflowDeadSecrets.sort(),
1120
+ workflowBrokenRefs: workflowBrokenRefs.sort()
1121
+ };
1122
+ }
1123
+
926
1124
  // dist/scanner/index.js
927
1125
  async function scanProject(projectRoot) {
928
1126
  const allFiles = await discoverFiles(projectRoot);
929
1127
  const rootFiles = identifyRoots(allFiles, projectRoot);
930
1128
  const allRefs = [];
931
1129
  for (const file of allFiles) {
932
- const fullPath = file.startsWith("/") ? file : join7(projectRoot, file);
1130
+ const fullPath = file.startsWith("/") ? file : join8(projectRoot, file);
933
1131
  try {
934
1132
  const refs = await parseFileReferences(fullPath, projectRoot);
935
1133
  allRefs.push(...refs);
@@ -941,7 +1139,8 @@ async function scanProject(projectRoot) {
941
1139
  const staleFiles = detectStaleFiles(allFiles, projectRoot);
942
1140
  const skills = await parseSkills(projectRoot);
943
1141
  const toolings = await detectToolings(projectRoot);
944
- const scanData = JSON.stringify({ graph, orphans, skills, staleFiles, toolings });
1142
+ const envManifest = await scanEnvManifest(projectRoot);
1143
+ const scanData = JSON.stringify({ graph, orphans, skills, staleFiles, toolings, envManifest });
945
1144
  const dataHash = createHash("sha256").update(scanData).digest("hex");
946
1145
  return {
947
1146
  graph,
@@ -949,24 +1148,25 @@ async function scanProject(projectRoot) {
949
1148
  skills,
950
1149
  staleFiles,
951
1150
  toolings,
1151
+ envManifest,
952
1152
  scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
953
1153
  dataHash
954
1154
  };
955
1155
  }
956
1156
  async function discoverFiles(projectRoot) {
957
1157
  const files = [];
958
- const claudeDir = join7(projectRoot, ".claude");
959
- if (existsSync5(claudeDir)) {
1158
+ const claudeDir = join8(projectRoot, ".claude");
1159
+ if (existsSync6(claudeDir)) {
960
1160
  await walkDir(claudeDir, projectRoot, files);
961
1161
  }
962
- if (existsSync5(join7(projectRoot, "CLAUDE.md"))) {
1162
+ if (existsSync6(join8(projectRoot, "CLAUDE.md"))) {
963
1163
  files.push("CLAUDE.md");
964
1164
  }
965
- if (existsSync5(join7(projectRoot, "skills.md"))) {
1165
+ if (existsSync6(join8(projectRoot, "skills.md"))) {
966
1166
  files.push("skills.md");
967
1167
  }
968
- const plansDir = join7(projectRoot, "docs", "plans");
969
- if (existsSync5(plansDir)) {
1168
+ const plansDir = join8(projectRoot, "docs", "plans");
1169
+ if (existsSync6(plansDir)) {
970
1170
  await walkDir(plansDir, projectRoot, files);
971
1171
  }
972
1172
  return [...new Set(files)];
@@ -974,13 +1174,13 @@ async function discoverFiles(projectRoot) {
974
1174
  async function walkDir(dir, projectRoot, files) {
975
1175
  const entries = await readdir2(dir, { withFileTypes: true });
976
1176
  for (const entry of entries) {
977
- const fullPath = join7(dir, entry.name);
1177
+ const fullPath = join8(dir, entry.name);
978
1178
  if (entry.isDirectory()) {
979
1179
  if (["node_modules", ".git", ".turbo", "cache", "session-env"].includes(entry.name))
980
1180
  continue;
981
1181
  await walkDir(fullPath, projectRoot, files);
982
1182
  } else {
983
- const relPath = relative(projectRoot, fullPath);
1183
+ const relPath = relative2(projectRoot, fullPath);
984
1184
  files.push(relPath);
985
1185
  }
986
1186
  }
@@ -994,16 +1194,16 @@ function identifyRoots(allFiles, projectRoot) {
994
1194
  }
995
1195
  for (const globalFile of GLOBAL_ROOT_FILES) {
996
1196
  const expanded = globalFile.replace("~", homedir5());
997
- if (existsSync5(expanded)) {
1197
+ if (existsSync6(expanded)) {
998
1198
  roots.push(globalFile);
999
1199
  }
1000
1200
  }
1001
1201
  return roots;
1002
1202
  }
1003
1203
  async function readClaudeConfigFiles(projectRoot) {
1004
- const { readFile: readFile8, stat, glob } = await import("node:fs/promises");
1005
- const { join: join12, relative: relative2 } = await import("node:path");
1006
- const { existsSync: existsSync11 } = await import("node:fs");
1204
+ const { readFile: readFile10, stat, glob: glob2 } = await import("node:fs/promises");
1205
+ const { join: join14, relative: relative4 } = await import("node:path");
1206
+ const { existsSync: existsSync13 } = await import("node:fs");
1007
1207
  const configPatterns = [
1008
1208
  "CLAUDE.md",
1009
1209
  ".claude/CLAUDE.md",
@@ -1013,15 +1213,15 @@ async function readClaudeConfigFiles(projectRoot) {
1013
1213
  ];
1014
1214
  const files = [];
1015
1215
  for (const pattern of configPatterns) {
1016
- for await (const fullPath of glob(join12(projectRoot, pattern))) {
1017
- if (!existsSync11(fullPath))
1216
+ for await (const fullPath of glob2(join14(projectRoot, pattern))) {
1217
+ if (!existsSync13(fullPath))
1018
1218
  continue;
1019
1219
  try {
1020
- const content = await readFile8(fullPath, "utf-8");
1220
+ const content = await readFile10(fullPath, "utf-8");
1021
1221
  const fileStat = await stat(fullPath);
1022
1222
  const lastMod = getGitLastModified(fullPath, projectRoot);
1023
1223
  files.push({
1024
- filePath: relative2(projectRoot, fullPath),
1224
+ filePath: relative4(projectRoot, fullPath),
1025
1225
  content,
1026
1226
  sizeBytes: fileStat.size,
1027
1227
  lastModified: lastMod
@@ -1036,7 +1236,7 @@ function detectStaleFiles(allFiles, projectRoot) {
1036
1236
  const stale = [];
1037
1237
  const now = Date.now();
1038
1238
  for (const file of allFiles) {
1039
- const fullPath = join7(projectRoot, file);
1239
+ const fullPath = join8(projectRoot, file);
1040
1240
  const lastMod = getGitLastModified(fullPath, projectRoot);
1041
1241
  if (lastMod) {
1042
1242
  const days = Math.floor((now - new Date(lastMod).getTime()) / (1e3 * 60 * 60 * 24));
@@ -1161,7 +1361,7 @@ function escapeHtml(text) {
1161
1361
 
1162
1362
  // dist/check-update.js
1163
1363
  import chalk8 from "chalk";
1164
- var CURRENT_VERSION = "0.6.2";
1364
+ var CURRENT_VERSION = true ? "0.7.1" : "0.0.0-dev";
1165
1365
  async function checkForUpdate() {
1166
1366
  try {
1167
1367
  const controller = new AbortController();
@@ -1227,7 +1427,7 @@ async function pushToolings(supabase, folderId, toolings) {
1227
1427
  async function mapCommand(path, options) {
1228
1428
  await checkForUpdate();
1229
1429
  const projectRoot = resolve3(path ?? process.cwd());
1230
- if (!existsSync6(projectRoot)) {
1430
+ if (!existsSync7(projectRoot)) {
1231
1431
  console.error(chalk9.red(`Path not found: ${projectRoot}`));
1232
1432
  process.exit(1);
1233
1433
  }
@@ -1240,9 +1440,10 @@ async function mapCommand(path, options) {
1240
1440
  console.log(` Stale files: ${result.staleFiles.length}`);
1241
1441
  console.log(` Skills: ${result.skills.length}`);
1242
1442
  console.log(` Toolings: ${result.toolings.length}`);
1443
+ console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
1243
1444
  console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
1244
1445
  const outputDir = resolve3(projectRoot, "output");
1245
- if (!existsSync6(outputDir)) {
1446
+ if (!existsSync7(outputDir)) {
1246
1447
  await mkdir2(outputDir, { recursive: true });
1247
1448
  }
1248
1449
  const htmlPath = resolve3(outputDir, "index.html");
@@ -1294,6 +1495,7 @@ ${proposedFiles.length} file(s) proposed for deletion:
1294
1495
  orphans_json: result.orphans,
1295
1496
  skills_table_json: result.skills,
1296
1497
  stale_files_json: result.staleFiles,
1498
+ env_manifest_json: result.envManifest,
1297
1499
  last_scanned: result.scannedAt,
1298
1500
  data_hash: result.dataHash
1299
1501
  }).eq("id", folder_id);
@@ -1335,8 +1537,8 @@ ${proposedFiles.length} file(s) proposed for deletion:
1335
1537
  }
1336
1538
 
1337
1539
  // dist/commands/simulate.js
1338
- import { join as join8 } from "node:path";
1339
- import { existsSync as existsSync7 } from "node:fs";
1540
+ import { join as join9 } from "node:path";
1541
+ import { existsSync as existsSync8 } from "node:fs";
1340
1542
  import { homedir as homedir6 } from "node:os";
1341
1543
  import chalk10 from "chalk";
1342
1544
  async function simulateCommand(prompt) {
@@ -1344,24 +1546,24 @@ async function simulateCommand(prompt) {
1344
1546
  console.log(chalk10.blue(`Simulating prompt: "${prompt}"
1345
1547
  `));
1346
1548
  console.log(chalk10.dim("Files Claude would load:\n"));
1347
- const globalClaude = join8(homedir6(), ".claude", "CLAUDE.md");
1348
- if (existsSync7(globalClaude)) {
1549
+ const globalClaude = join9(homedir6(), ".claude", "CLAUDE.md");
1550
+ if (existsSync8(globalClaude)) {
1349
1551
  console.log(chalk10.green(" \u2713 ~/.claude/CLAUDE.md (global)"));
1350
1552
  }
1351
1553
  for (const rootFile of ROOT_FILES) {
1352
- const fullPath = join8(projectRoot, rootFile);
1353
- if (existsSync7(fullPath)) {
1554
+ const fullPath = join9(projectRoot, rootFile);
1555
+ if (existsSync8(fullPath)) {
1354
1556
  console.log(chalk10.green(` \u2713 ${rootFile} (project root)`));
1355
1557
  }
1356
1558
  }
1357
1559
  const settingsFiles = [
1358
- join8(homedir6(), ".claude", "settings.json"),
1359
- join8(homedir6(), ".claude", "settings.local.json"),
1360
- join8(projectRoot, ".claude", "settings.json"),
1361
- join8(projectRoot, ".claude", "settings.local.json")
1560
+ join9(homedir6(), ".claude", "settings.json"),
1561
+ join9(homedir6(), ".claude", "settings.local.json"),
1562
+ join9(projectRoot, ".claude", "settings.json"),
1563
+ join9(projectRoot, ".claude", "settings.local.json")
1362
1564
  ];
1363
1565
  for (const sf of settingsFiles) {
1364
- if (existsSync7(sf)) {
1566
+ if (existsSync8(sf)) {
1365
1567
  const display = sf.startsWith(homedir6()) ? sf.replace(homedir6(), "~") : sf.replace(projectRoot + "/", "");
1366
1568
  console.log(chalk10.green(` \u2713 ${display} (settings)`));
1367
1569
  }
@@ -1369,8 +1571,8 @@ async function simulateCommand(prompt) {
1369
1571
  const words = prompt.split(/\s+/);
1370
1572
  for (const word of words) {
1371
1573
  const cleaned = word.replace(/['"]/g, "");
1372
- const candidatePath = join8(projectRoot, cleaned);
1373
- if (existsSync7(candidatePath) && cleaned.includes("/")) {
1574
+ const candidatePath = join9(projectRoot, cleaned);
1575
+ if (existsSync8(candidatePath) && cleaned.includes("/")) {
1374
1576
  console.log(chalk10.cyan(` \u2192 ${cleaned} (referenced in prompt)`));
1375
1577
  }
1376
1578
  }
@@ -1378,18 +1580,18 @@ async function simulateCommand(prompt) {
1378
1580
  }
1379
1581
 
1380
1582
  // dist/commands/print.js
1381
- import { join as join9 } from "node:path";
1382
- import { readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
1383
- import { existsSync as existsSync8 } from "node:fs";
1583
+ import { join as join10 } from "node:path";
1584
+ import { readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
1585
+ import { existsSync as existsSync9 } from "node:fs";
1384
1586
  import chalk11 from "chalk";
1385
1587
  async function printCommand(title) {
1386
1588
  const projectRoot = process.cwd();
1387
- const scanDataPath = join9(projectRoot, "output", "index.html");
1388
- if (!existsSync8(scanDataPath)) {
1589
+ const scanDataPath = join10(projectRoot, "output", "index.html");
1590
+ if (!existsSync9(scanDataPath)) {
1389
1591
  console.error(chalk11.red("No scan data found. Run: md4ai scan"));
1390
1592
  process.exit(1);
1391
1593
  }
1392
- const html = await readFile5(scanDataPath, "utf-8");
1594
+ const html = await readFile6(scanDataPath, "utf-8");
1393
1595
  const match = html.match(/<script type="application\/json" id="scan-data">([\s\S]*?)<\/script>/);
1394
1596
  if (!match) {
1395
1597
  console.error(chalk11.red("Could not extract scan data from output/index.html"));
@@ -1397,7 +1599,7 @@ async function printCommand(title) {
1397
1599
  }
1398
1600
  const result = JSON.parse(match[1]);
1399
1601
  const printHtml = generatePrintHtml(result, title);
1400
- const outputPath = join9(projectRoot, "output", `print-${Date.now()}.html`);
1602
+ const outputPath = join10(projectRoot, "output", `print-${Date.now()}.html`);
1401
1603
  await writeFile3(outputPath, printHtml, "utf-8");
1402
1604
  console.log(chalk11.green(`Print-ready wall sheet: ${outputPath}`));
1403
1605
  }
@@ -1478,6 +1680,7 @@ async function syncCommand(options) {
1478
1680
  orphans_json: result.orphans,
1479
1681
  skills_table_json: result.skills,
1480
1682
  stale_files_json: result.staleFiles,
1683
+ env_manifest_json: result.envManifest,
1481
1684
  last_scanned: result.scannedAt,
1482
1685
  data_hash: result.dataHash
1483
1686
  }).eq("id", device.folder_id);
@@ -1607,11 +1810,13 @@ Linking "${folder.name}" to this device...
1607
1810
  console.log(` Orphans: ${result.orphans.length}`);
1608
1811
  console.log(` Skills: ${result.skills.length}`);
1609
1812
  console.log(` Toolings: ${result.toolings.length}`);
1813
+ console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
1610
1814
  const { error: scanErr } = await supabase.from("claude_folders").update({
1611
1815
  graph_json: result.graph,
1612
1816
  orphans_json: result.orphans,
1613
1817
  skills_table_json: result.skills,
1614
1818
  stale_files_json: result.staleFiles,
1819
+ env_manifest_json: result.envManifest,
1615
1820
  last_scanned: result.scannedAt,
1616
1821
  data_hash: result.dataHash
1617
1822
  }).eq("id", folder.id);
@@ -1647,18 +1852,18 @@ Linking "${folder.name}" to this device...
1647
1852
  }
1648
1853
 
1649
1854
  // dist/commands/import-bundle.js
1650
- import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir3 } from "node:fs/promises";
1651
- import { join as join10, dirname as dirname2 } from "node:path";
1652
- import { existsSync as existsSync9 } from "node:fs";
1855
+ import { readFile as readFile7, writeFile as writeFile4, mkdir as mkdir3 } from "node:fs/promises";
1856
+ import { join as join11, dirname as dirname2 } from "node:path";
1857
+ import { existsSync as existsSync10 } from "node:fs";
1653
1858
  import chalk14 from "chalk";
1654
1859
  import { confirm, input as input4 } from "@inquirer/prompts";
1655
1860
  async function importBundleCommand(zipPath) {
1656
- if (!existsSync9(zipPath)) {
1861
+ if (!existsSync10(zipPath)) {
1657
1862
  console.error(chalk14.red(`File not found: ${zipPath}`));
1658
1863
  process.exit(1);
1659
1864
  }
1660
1865
  const JSZip = (await import("jszip")).default;
1661
- const zipData = await readFile6(zipPath);
1866
+ const zipData = await readFile7(zipPath);
1662
1867
  const zip = await JSZip.loadAsync(zipData);
1663
1868
  const manifestFile = zip.file("manifest.json");
1664
1869
  if (!manifestFile) {
@@ -1700,9 +1905,9 @@ Files to extract:`));
1700
1905
  return;
1701
1906
  }
1702
1907
  for (const file of files) {
1703
- const fullPath = join10(targetDir, file.filePath);
1908
+ const fullPath = join11(targetDir, file.filePath);
1704
1909
  const dir = dirname2(fullPath);
1705
- if (!existsSync9(dir)) {
1910
+ if (!existsSync10(dir)) {
1706
1911
  await mkdir3(dir, { recursive: true });
1707
1912
  }
1708
1913
  await writeFile4(fullPath, file.content, "utf-8");
@@ -2005,16 +2210,16 @@ import chalk18 from "chalk";
2005
2210
  import { execFileSync as execFileSync5 } from "node:child_process";
2006
2211
 
2007
2212
  // dist/mcp/read-configs.js
2008
- import { readFile as readFile7 } from "node:fs/promises";
2009
- import { join as join11 } from "node:path";
2213
+ import { readFile as readFile8 } from "node:fs/promises";
2214
+ import { join as join12 } from "node:path";
2010
2215
  import { homedir as homedir7 } from "node:os";
2011
- import { existsSync as existsSync10 } from "node:fs";
2216
+ import { existsSync as existsSync11 } from "node:fs";
2012
2217
  import { readdir as readdir3 } from "node:fs/promises";
2013
2218
  async function readJsonSafe(path) {
2014
2219
  try {
2015
- if (!existsSync10(path))
2220
+ if (!existsSync11(path))
2016
2221
  return null;
2017
- const raw = await readFile7(path, "utf-8");
2222
+ const raw = await readFile8(path, "utf-8");
2018
2223
  return JSON.parse(raw);
2019
2224
  } catch {
2020
2225
  return null;
@@ -2076,50 +2281,50 @@ function parseFlatConfig(data, source) {
2076
2281
  async function readAllMcpConfigs() {
2077
2282
  const home = homedir7();
2078
2283
  const entries = [];
2079
- const userConfig = await readJsonSafe(join11(home, ".claude.json"));
2284
+ const userConfig = await readJsonSafe(join12(home, ".claude.json"));
2080
2285
  entries.push(...parseServers(userConfig, "global"));
2081
- const globalMcp = await readJsonSafe(join11(home, ".claude", "mcp.json"));
2286
+ const globalMcp = await readJsonSafe(join12(home, ".claude", "mcp.json"));
2082
2287
  entries.push(...parseServers(globalMcp, "global"));
2083
- const cwdMcp = await readJsonSafe(join11(process.cwd(), ".mcp.json"));
2288
+ const cwdMcp = await readJsonSafe(join12(process.cwd(), ".mcp.json"));
2084
2289
  entries.push(...parseServers(cwdMcp, "project"));
2085
- const pluginsBase = join11(home, ".claude", "plugins", "marketplaces");
2086
- if (existsSync10(pluginsBase)) {
2290
+ const pluginsBase = join12(home, ".claude", "plugins", "marketplaces");
2291
+ if (existsSync11(pluginsBase)) {
2087
2292
  try {
2088
2293
  const marketplaces = await readdir3(pluginsBase, { withFileTypes: true });
2089
2294
  for (const mp of marketplaces) {
2090
2295
  if (!mp.isDirectory())
2091
2296
  continue;
2092
- const extDir = join11(pluginsBase, mp.name, "external_plugins");
2093
- if (!existsSync10(extDir))
2297
+ const extDir = join12(pluginsBase, mp.name, "external_plugins");
2298
+ if (!existsSync11(extDir))
2094
2299
  continue;
2095
2300
  const plugins = await readdir3(extDir, { withFileTypes: true });
2096
2301
  for (const plugin of plugins) {
2097
2302
  if (!plugin.isDirectory())
2098
2303
  continue;
2099
- const pluginMcp = await readJsonSafe(join11(extDir, plugin.name, ".mcp.json"));
2304
+ const pluginMcp = await readJsonSafe(join12(extDir, plugin.name, ".mcp.json"));
2100
2305
  entries.push(...parseServers(pluginMcp, "plugin"));
2101
2306
  }
2102
2307
  }
2103
2308
  } catch {
2104
2309
  }
2105
2310
  }
2106
- const cacheBase = join11(home, ".claude", "plugins", "cache");
2107
- if (existsSync10(cacheBase)) {
2311
+ const cacheBase = join12(home, ".claude", "plugins", "cache");
2312
+ if (existsSync11(cacheBase)) {
2108
2313
  try {
2109
2314
  const registries = await readdir3(cacheBase, { withFileTypes: true });
2110
2315
  for (const reg of registries) {
2111
2316
  if (!reg.isDirectory())
2112
2317
  continue;
2113
- const regDir = join11(cacheBase, reg.name);
2318
+ const regDir = join12(cacheBase, reg.name);
2114
2319
  const plugins = await readdir3(regDir, { withFileTypes: true });
2115
2320
  for (const plugin of plugins) {
2116
2321
  if (!plugin.isDirectory())
2117
2322
  continue;
2118
- const versionDirs = await readdir3(join11(regDir, plugin.name), { withFileTypes: true });
2323
+ const versionDirs = await readdir3(join12(regDir, plugin.name), { withFileTypes: true });
2119
2324
  for (const ver of versionDirs) {
2120
2325
  if (!ver.isDirectory())
2121
2326
  continue;
2122
- const mcpPath = join11(regDir, plugin.name, ver.name, ".mcp.json");
2327
+ const mcpPath = join12(regDir, plugin.name, ver.name, ".mcp.json");
2123
2328
  const flatData = await readJsonSafe(mcpPath);
2124
2329
  if (flatData) {
2125
2330
  entries.push(...parseFlatConfig(flatData, "plugin"));
@@ -2217,7 +2422,9 @@ function getProcessTable() {
2217
2422
  }
2218
2423
 
2219
2424
  // dist/commands/mcp-watch.js
2425
+ import { createHash as createHash2 } from "node:crypto";
2220
2426
  var POLL_INTERVAL_MS = 3e4;
2427
+ var ENV_POLL_INTERVAL_MS = 3e5;
2221
2428
  function detectTty() {
2222
2429
  try {
2223
2430
  const result = execFileSync5("ps", ["-o", "tty=", "-p", String(process.pid)], {
@@ -2403,9 +2610,29 @@ async function mcpWatchCommand() {
2403
2610
  printTable(rows, deviceName);
2404
2611
  }
2405
2612
  await cycle();
2613
+ let lastEnvHash = "";
2614
+ async function envCycle() {
2615
+ const state = await loadState();
2616
+ if (!state.lastFolderId)
2617
+ return;
2618
+ const { data: dp } = await supabase.from("device_paths").select("path").eq("folder_id", state.lastFolderId).eq("device_name", deviceName).maybeSingle();
2619
+ if (!dp?.path)
2620
+ return;
2621
+ const envManifest = await scanEnvManifest(dp.path);
2622
+ if (!envManifest)
2623
+ return;
2624
+ const hash = createHash2("sha256").update(JSON.stringify(envManifest)).digest("hex");
2625
+ if (hash === lastEnvHash)
2626
+ return;
2627
+ lastEnvHash = hash;
2628
+ await supabase.from("claude_folders").update({ env_manifest_json: envManifest }).eq("id", state.lastFolderId);
2629
+ }
2630
+ await envCycle();
2631
+ const envInterval = setInterval(envCycle, ENV_POLL_INTERVAL_MS);
2406
2632
  const interval = setInterval(cycle, POLL_INTERVAL_MS);
2407
2633
  const shutdown = async () => {
2408
2634
  clearInterval(interval);
2635
+ clearInterval(envInterval);
2409
2636
  await supabase.from("mcp_watchers").delete().eq("device_id", deviceId).eq("pid", myPid);
2410
2637
  console.log(chalk18.dim("\nMCP monitor stopped."));
2411
2638
  process.exit(0);
@@ -2418,6 +2645,131 @@ async function mcpWatchCommand() {
2418
2645
  });
2419
2646
  }
2420
2647
 
2648
+ // dist/commands/init-manifest.js
2649
+ import { resolve as resolve5, join as join13, relative as relative3, dirname as dirname3 } from "node:path";
2650
+ import { readFile as readFile9, writeFile as writeFile5, mkdir as mkdir4 } from "node:fs/promises";
2651
+ import { existsSync as existsSync12 } from "node:fs";
2652
+ import chalk19 from "chalk";
2653
+ var SECRET_PATTERNS = [
2654
+ /_KEY$/i,
2655
+ /_SECRET$/i,
2656
+ /_TOKEN$/i,
2657
+ /_PASSWORD$/i,
2658
+ /^SECRET_/i,
2659
+ /^PRIVATE_/i,
2660
+ /SERVICE_ROLE/i
2661
+ ];
2662
+ function isLikelySecret(name) {
2663
+ return SECRET_PATTERNS.some((p) => p.test(name));
2664
+ }
2665
+ async function discoverEnvFiles(projectRoot) {
2666
+ const apps = [];
2667
+ for (const envName of [".env", ".env.local", ".env.example"]) {
2668
+ const envPath = join13(projectRoot, envName);
2669
+ if (existsSync12(envPath)) {
2670
+ const vars = await extractVarNames(envPath);
2671
+ if (vars.length > 0) {
2672
+ apps.push({ name: "root", envFilePath: envName, vars });
2673
+ }
2674
+ break;
2675
+ }
2676
+ }
2677
+ const subdirs = ["web", "cli", "api", "app", "server", "packages"];
2678
+ for (const sub of subdirs) {
2679
+ const subDir = join13(projectRoot, sub);
2680
+ if (!existsSync12(subDir))
2681
+ continue;
2682
+ for (const envName of [".env.local", ".env", ".env.example"]) {
2683
+ const envPath = join13(subDir, envName);
2684
+ if (existsSync12(envPath)) {
2685
+ const vars = await extractVarNames(envPath);
2686
+ if (vars.length > 0) {
2687
+ apps.push({
2688
+ name: sub,
2689
+ envFilePath: `${sub}/${envName}`,
2690
+ vars
2691
+ });
2692
+ }
2693
+ break;
2694
+ }
2695
+ }
2696
+ }
2697
+ return apps;
2698
+ }
2699
+ async function extractVarNames(envPath) {
2700
+ const content = await readFile9(envPath, "utf-8");
2701
+ const vars = [];
2702
+ for (const line of content.split("\n")) {
2703
+ const trimmed = line.trim();
2704
+ if (trimmed.startsWith("#") || !trimmed.includes("="))
2705
+ continue;
2706
+ const name = trimmed.split("=")[0].trim();
2707
+ if (name)
2708
+ vars.push(name);
2709
+ }
2710
+ return vars;
2711
+ }
2712
+ function generateManifest(apps) {
2713
+ const lines = ["# Environment Variable Manifest", ""];
2714
+ lines.push("## Apps");
2715
+ for (const app of apps) {
2716
+ lines.push(`- ${app.name}: \`${app.envFilePath}\``);
2717
+ }
2718
+ lines.push("");
2719
+ const allVars = /* @__PURE__ */ new Map();
2720
+ for (const app of apps) {
2721
+ for (const v of app.vars) {
2722
+ if (!allVars.has(v))
2723
+ allVars.set(v, /* @__PURE__ */ new Set());
2724
+ allVars.get(v).add(app.name);
2725
+ }
2726
+ }
2727
+ lines.push("## Variables");
2728
+ lines.push("");
2729
+ lines.push("| Variable | Public? | Local | Vercel | GitHub | Supabase | Notes |");
2730
+ lines.push("|----------|---------|-------|--------|--------|----------|-------|");
2731
+ for (const [varName, appNames] of allVars) {
2732
+ const secret = isLikelySecret(varName);
2733
+ const publicCol = secret ? "**SECRET**" : "Yes";
2734
+ const localCol = [...appNames].join(", ");
2735
+ const vercelCol = "---";
2736
+ const githubCol = secret ? "yes" : "---";
2737
+ const supabaseCol = "---";
2738
+ lines.push(`| \`${varName}\` | ${publicCol} | ${localCol} | ${vercelCol} | ${githubCol} | ${supabaseCol} | |`);
2739
+ }
2740
+ lines.push("");
2741
+ return lines.join("\n");
2742
+ }
2743
+ async function initManifestCommand() {
2744
+ const projectRoot = resolve5(process.cwd());
2745
+ const manifestPath = join13(projectRoot, "docs", "reference", "env-manifest.md");
2746
+ if (existsSync12(manifestPath)) {
2747
+ console.log(chalk19.yellow(`Manifest already exists: ${relative3(projectRoot, manifestPath)}`));
2748
+ console.log(chalk19.dim("Edit it directly to make changes."));
2749
+ return;
2750
+ }
2751
+ console.log(chalk19.blue("Scanning for .env files...\n"));
2752
+ const apps = await discoverEnvFiles(projectRoot);
2753
+ if (apps.length === 0) {
2754
+ console.log(chalk19.yellow("No .env files found. Create a manifest manually at:"));
2755
+ console.log(chalk19.cyan(` docs/reference/env-manifest.md`));
2756
+ return;
2757
+ }
2758
+ for (const app of apps) {
2759
+ console.log(` ${chalk19.green(app.name)}: ${app.envFilePath} (${app.vars.length} vars)`);
2760
+ }
2761
+ const content = generateManifest(apps);
2762
+ const dir = dirname3(manifestPath);
2763
+ if (!existsSync12(dir)) {
2764
+ await mkdir4(dir, { recursive: true });
2765
+ }
2766
+ await writeFile5(manifestPath, content, "utf-8");
2767
+ console.log(chalk19.green(`
2768
+ Manifest created: ${relative3(projectRoot, manifestPath)}`));
2769
+ console.log(chalk19.dim("Review and edit the file \u2014 it is your source of truth."));
2770
+ console.log(chalk19.dim("Then run `md4ai scan` to verify against your environments."));
2771
+ }
2772
+
2421
2773
  // dist/index.js
2422
2774
  var program = new Command();
2423
2775
  program.name("md4ai").description("MD4AI \u2014 Claude tooling visualiser").version(CURRENT_VERSION).addHelpText("after", `
@@ -2440,6 +2792,7 @@ program.command("sync").description("Re-push latest scan data to Supabase").opti
2440
2792
  program.command("import <zipfile>").description("Import a Claude setup bundle exported from the web dashboard").action(importBundleCommand);
2441
2793
  program.command("check-update").description("Check if a newer version of md4ai is available").action(checkForUpdate);
2442
2794
  program.command("mcp-watch").description("Monitor MCP server status on this device (runs until Ctrl+C)").action(mcpWatchCommand);
2795
+ program.command("init-manifest").description("Scaffold a starter env-manifest.md from detected .env files").action(initManifestCommand);
2443
2796
  var admin = program.command("admin").description("Admin commands for managing the tools registry");
2444
2797
  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);
2445
2798
  admin.command("list-tools").description("List all tools in the master registry").action(adminListToolsCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md4ai",
3
- "version": "0.6.2",
3
+ "version": "0.7.1",
4
4
  "description": "CLI for MD4AI — scan Claude projects and sync to your dashboard",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,7 +17,12 @@
17
17
  "dev": "tsc --watch",
18
18
  "clean": "rm -rf dist"
19
19
  },
20
- "keywords": ["claude", "ai", "project-scanner", "dependency-graph"],
20
+ "keywords": [
21
+ "claude",
22
+ "ai",
23
+ "project-scanner",
24
+ "dependency-graph"
25
+ ],
21
26
  "license": "MIT",
22
27
  "repository": {
23
28
  "type": "git",