md4ai 0.4.0 → 0.5.0

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 +621 -56
  2. package/package.json +1 -1
@@ -319,14 +319,14 @@ ${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 existsSync5 } from "node:fs";
322
+ import { existsSync as existsSync6 } from "node:fs";
323
323
  import chalk9 from "chalk";
324
324
 
325
325
  // dist/scanner/index.js
326
- import { readdir } from "node:fs/promises";
327
- import { join as join6, relative } from "node:path";
328
- import { existsSync as existsSync4 } from "node:fs";
329
- import { homedir as homedir4 } from "node:os";
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";
329
+ import { homedir as homedir5 } from "node:os";
330
330
  import { createHash } from "node:crypto";
331
331
 
332
332
  // dist/scanner/file-parser.js
@@ -582,9 +582,15 @@ function detectOrphans(allFiles, references, rootFiles, projectRoot) {
582
582
  sizeBytes = statSync3(fullPath).size;
583
583
  } catch {
584
584
  }
585
+ let createdAt = null;
586
+ try {
587
+ createdAt = statSync3(fullPath).birthtime.toISOString();
588
+ } catch {
589
+ }
585
590
  return {
586
591
  path: f,
587
592
  lastModified: getGitLastModified(fullPath, projectRoot),
593
+ createdAt,
588
594
  sizeBytes
589
595
  };
590
596
  });
@@ -675,13 +681,253 @@ async function parseSettingsForPlugins(settingsPath, skills, isMachineWide) {
675
681
  }
676
682
  }
677
683
 
684
+ // dist/scanner/tooling-detector.js
685
+ import { readFile as readFile4, readdir } from "node:fs/promises";
686
+ import { existsSync as existsSync4 } from "node:fs";
687
+ import { join as join6 } from "node:path";
688
+ import { execFileSync as execFileSync3 } from "node:child_process";
689
+ import { homedir as homedir4 } from "node:os";
690
+ var CLI_VERSION_COMMANDS = [
691
+ { name: "node", command: "node", args: ["--version"] },
692
+ { name: "npm", command: "npm", args: ["--version"] },
693
+ { name: "pnpm", command: "pnpm", args: ["--version"], conditionFile: "pnpm-lock.yaml" },
694
+ { name: "supabase-cli", command: "npx", args: ["supabase", "--version"] },
695
+ { name: "claude-code", command: "claude", args: ["--version"] }
696
+ ];
697
+ async function detectToolings(projectRoot) {
698
+ const toolings = [];
699
+ const pkgToolings = await detectFromPackageJson(projectRoot);
700
+ toolings.push(...pkgToolings);
701
+ const cliToolings = detectFromCli(projectRoot);
702
+ toolings.push(...cliToolings);
703
+ const mcpToolings = await detectFromMcpSettings(projectRoot);
704
+ toolings.push(...mcpToolings);
705
+ return toolings;
706
+ }
707
+ async function detectFromPackageJson(projectRoot) {
708
+ const toolings = [];
709
+ const pkgPath = join6(projectRoot, "package.json");
710
+ if (!existsSync4(pkgPath))
711
+ return toolings;
712
+ const resolvedVersions = await getResolvedVersions(projectRoot);
713
+ const seen = /* @__PURE__ */ new Set();
714
+ const pkgPaths = [pkgPath];
715
+ const workspacePaths = await discoverWorkspacePackageJsons(projectRoot);
716
+ pkgPaths.push(...workspacePaths);
717
+ for (const path of pkgPaths) {
718
+ try {
719
+ const content = await readFile4(path, "utf-8");
720
+ const pkg = JSON.parse(content);
721
+ const allDeps = {
722
+ ...pkg.dependencies ?? {},
723
+ ...pkg.devDependencies ?? {}
724
+ };
725
+ for (const [name, specifier] of Object.entries(allDeps)) {
726
+ if (seen.has(name))
727
+ continue;
728
+ seen.add(name);
729
+ if (specifier.startsWith("workspace:"))
730
+ continue;
731
+ const resolved = resolvedVersions.get(name);
732
+ const version = resolved ?? stripVersionPrefix(specifier);
733
+ toolings.push({
734
+ tool_name: name,
735
+ detected_version: version,
736
+ detection_source: "package.json"
737
+ });
738
+ }
739
+ } catch {
740
+ }
741
+ }
742
+ return toolings;
743
+ }
744
+ async function discoverWorkspacePackageJsons(projectRoot) {
745
+ const patterns = await getWorkspacePatterns(projectRoot);
746
+ if (patterns.length === 0)
747
+ return [];
748
+ const results = [];
749
+ for (const pattern of patterns) {
750
+ const isGlob = /\/\*\*?$/.test(pattern) || pattern.includes("*");
751
+ const cleanPattern = pattern.replace(/\/\*\*?$/, "");
752
+ if (isGlob) {
753
+ const parentDir = join6(projectRoot, cleanPattern);
754
+ if (!existsSync4(parentDir))
755
+ continue;
756
+ try {
757
+ const entries = await readdir(parentDir, { withFileTypes: true });
758
+ for (const entry of entries) {
759
+ if (!entry.isDirectory())
760
+ continue;
761
+ const pkgPath = join6(parentDir, entry.name, "package.json");
762
+ if (existsSync4(pkgPath)) {
763
+ results.push(pkgPath);
764
+ }
765
+ }
766
+ } catch {
767
+ }
768
+ } else {
769
+ const pkgPath = join6(projectRoot, cleanPattern, "package.json");
770
+ if (existsSync4(pkgPath)) {
771
+ results.push(pkgPath);
772
+ }
773
+ }
774
+ }
775
+ return results;
776
+ }
777
+ async function getWorkspacePatterns(projectRoot) {
778
+ const pnpmWorkspace = join6(projectRoot, "pnpm-workspace.yaml");
779
+ if (existsSync4(pnpmWorkspace)) {
780
+ try {
781
+ const content = await readFile4(pnpmWorkspace, "utf-8");
782
+ const patterns = [];
783
+ let inPackages = false;
784
+ for (const line of content.split("\n")) {
785
+ const trimmed = line.trim();
786
+ if (trimmed === "packages:") {
787
+ inPackages = true;
788
+ continue;
789
+ }
790
+ if (inPackages) {
791
+ if (trimmed.startsWith("- ")) {
792
+ patterns.push(trimmed.slice(2).replace(/^["']|["']$/g, ""));
793
+ } else if (trimmed && !trimmed.startsWith("#")) {
794
+ break;
795
+ }
796
+ }
797
+ }
798
+ if (patterns.length > 0)
799
+ return patterns;
800
+ } catch {
801
+ }
802
+ }
803
+ const pkgPath = join6(projectRoot, "package.json");
804
+ try {
805
+ const content = await readFile4(pkgPath, "utf-8");
806
+ const pkg = JSON.parse(content);
807
+ const workspaces = pkg.workspaces;
808
+ if (Array.isArray(workspaces))
809
+ return workspaces;
810
+ if (workspaces?.packages && Array.isArray(workspaces.packages))
811
+ return workspaces.packages;
812
+ } catch {
813
+ }
814
+ return [];
815
+ }
816
+ function stripVersionPrefix(version) {
817
+ return version.replace(/^[\^~>=<]+/, "").split(" ")[0];
818
+ }
819
+ async function getResolvedVersions(projectRoot) {
820
+ const versions = /* @__PURE__ */ new Map();
821
+ const pnpmLock = join6(projectRoot, "pnpm-lock.yaml");
822
+ if (existsSync4(pnpmLock)) {
823
+ try {
824
+ const content = await readFile4(pnpmLock, "utf-8");
825
+ const versionPattern = /^\s{4}'?(@?[^@\s:]+)(?:@[^:]+)?'?:\s*(?:version:\s*)?'?(\d+\.\d+[^'\s]*)/gm;
826
+ let match;
827
+ while ((match = versionPattern.exec(content)) !== null) {
828
+ const [, name, version] = match;
829
+ if (name && version && !versions.has(name)) {
830
+ versions.set(name, version);
831
+ }
832
+ }
833
+ } catch {
834
+ }
835
+ }
836
+ if (versions.size === 0) {
837
+ const npmLock = join6(projectRoot, "package-lock.json");
838
+ if (existsSync4(npmLock)) {
839
+ try {
840
+ const content = await readFile4(npmLock, "utf-8");
841
+ const lock = JSON.parse(content);
842
+ const packages = lock.packages ?? lock.dependencies ?? {};
843
+ for (const [key, value] of Object.entries(packages)) {
844
+ const name = key.replace(/^node_modules\//, "");
845
+ const ver = value.version;
846
+ if (name && ver && !versions.has(name)) {
847
+ versions.set(name, ver);
848
+ }
849
+ }
850
+ } catch {
851
+ }
852
+ }
853
+ }
854
+ return versions;
855
+ }
856
+ function detectFromCli(projectRoot) {
857
+ const toolings = [];
858
+ for (const { name, command, args, conditionFile } of CLI_VERSION_COMMANDS) {
859
+ if (conditionFile && !existsSync4(join6(projectRoot, conditionFile))) {
860
+ continue;
861
+ }
862
+ try {
863
+ const output = execFileSync3(command, args, {
864
+ encoding: "utf-8",
865
+ timeout: 3e3,
866
+ stdio: ["pipe", "pipe", "pipe"]
867
+ }).trim();
868
+ const version = output.replace(/^v/, "").split("\n")[0].trim();
869
+ if (version) {
870
+ toolings.push({
871
+ tool_name: name,
872
+ detected_version: version,
873
+ detection_source: "cli"
874
+ });
875
+ }
876
+ } catch {
877
+ }
878
+ }
879
+ return toolings;
880
+ }
881
+ async function detectFromMcpSettings(projectRoot) {
882
+ const toolings = [];
883
+ const settingsPaths = [
884
+ join6(projectRoot, ".claude", "settings.json"),
885
+ join6(projectRoot, ".claude", "settings.local.json"),
886
+ join6(homedir4(), ".claude", "settings.json")
887
+ ];
888
+ const seen = /* @__PURE__ */ new Set();
889
+ for (const settingsPath of settingsPaths) {
890
+ if (!existsSync4(settingsPath))
891
+ continue;
892
+ try {
893
+ const content = await readFile4(settingsPath, "utf-8");
894
+ const settings = JSON.parse(content);
895
+ const mcpServers = settings.mcpServers ?? {};
896
+ for (const serverName of Object.keys(mcpServers)) {
897
+ if (seen.has(serverName))
898
+ continue;
899
+ seen.add(serverName);
900
+ let version = null;
901
+ const config = mcpServers[serverName];
902
+ if (config?.command && typeof config.command === "string") {
903
+ const args = config.args ?? [];
904
+ for (const arg of args) {
905
+ const versionMatch = arg.match(/@(\d+\.\d+[^\s]*?)$/);
906
+ if (versionMatch) {
907
+ version = versionMatch[1];
908
+ break;
909
+ }
910
+ }
911
+ }
912
+ toolings.push({
913
+ tool_name: serverName,
914
+ detected_version: version,
915
+ detection_source: "mcp_settings"
916
+ });
917
+ }
918
+ } catch {
919
+ }
920
+ }
921
+ return toolings;
922
+ }
923
+
678
924
  // dist/scanner/index.js
679
925
  async function scanProject(projectRoot) {
680
926
  const allFiles = await discoverFiles(projectRoot);
681
927
  const rootFiles = identifyRoots(allFiles, projectRoot);
682
928
  const allRefs = [];
683
929
  for (const file of allFiles) {
684
- const fullPath = file.startsWith("/") ? file : join6(projectRoot, file);
930
+ const fullPath = file.startsWith("/") ? file : join7(projectRoot, file);
685
931
  try {
686
932
  const refs = await parseFileReferences(fullPath, projectRoot);
687
933
  allRefs.push(...refs);
@@ -692,39 +938,41 @@ async function scanProject(projectRoot) {
692
938
  const orphans = detectOrphans(allFiles, allRefs, rootFiles, projectRoot);
693
939
  const staleFiles = detectStaleFiles(allFiles, projectRoot);
694
940
  const skills = await parseSkills(projectRoot);
695
- const scanData = JSON.stringify({ graph, orphans, skills, staleFiles });
941
+ const toolings = await detectToolings(projectRoot);
942
+ const scanData = JSON.stringify({ graph, orphans, skills, staleFiles, toolings });
696
943
  const dataHash = createHash("sha256").update(scanData).digest("hex");
697
944
  return {
698
945
  graph,
699
946
  orphans,
700
947
  skills,
701
948
  staleFiles,
949
+ toolings,
702
950
  scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
703
951
  dataHash
704
952
  };
705
953
  }
706
954
  async function discoverFiles(projectRoot) {
707
955
  const files = [];
708
- const claudeDir = join6(projectRoot, ".claude");
709
- if (existsSync4(claudeDir)) {
956
+ const claudeDir = join7(projectRoot, ".claude");
957
+ if (existsSync5(claudeDir)) {
710
958
  await walkDir(claudeDir, projectRoot, files);
711
959
  }
712
- if (existsSync4(join6(projectRoot, "CLAUDE.md"))) {
960
+ if (existsSync5(join7(projectRoot, "CLAUDE.md"))) {
713
961
  files.push("CLAUDE.md");
714
962
  }
715
- if (existsSync4(join6(projectRoot, "skills.md"))) {
963
+ if (existsSync5(join7(projectRoot, "skills.md"))) {
716
964
  files.push("skills.md");
717
965
  }
718
- const plansDir = join6(projectRoot, "docs", "plans");
719
- if (existsSync4(plansDir)) {
966
+ const plansDir = join7(projectRoot, "docs", "plans");
967
+ if (existsSync5(plansDir)) {
720
968
  await walkDir(plansDir, projectRoot, files);
721
969
  }
722
970
  return [...new Set(files)];
723
971
  }
724
972
  async function walkDir(dir, projectRoot, files) {
725
- const entries = await readdir(dir, { withFileTypes: true });
973
+ const entries = await readdir2(dir, { withFileTypes: true });
726
974
  for (const entry of entries) {
727
- const fullPath = join6(dir, entry.name);
975
+ const fullPath = join7(dir, entry.name);
728
976
  if (entry.isDirectory()) {
729
977
  if (["node_modules", ".git", ".turbo", "cache", "session-env"].includes(entry.name))
730
978
  continue;
@@ -743,17 +991,17 @@ function identifyRoots(allFiles, projectRoot) {
743
991
  }
744
992
  }
745
993
  for (const globalFile of GLOBAL_ROOT_FILES) {
746
- const expanded = globalFile.replace("~", homedir4());
747
- if (existsSync4(expanded)) {
994
+ const expanded = globalFile.replace("~", homedir5());
995
+ if (existsSync5(expanded)) {
748
996
  roots.push(globalFile);
749
997
  }
750
998
  }
751
999
  return roots;
752
1000
  }
753
1001
  async function readClaudeConfigFiles(projectRoot) {
754
- const { readFile: readFile6, stat, glob } = await import("node:fs/promises");
755
- const { join: join10, relative: relative2 } = await import("node:path");
756
- const { existsSync: existsSync9 } = await import("node:fs");
1002
+ const { readFile: readFile7, stat, glob } = await import("node:fs/promises");
1003
+ const { join: join11, relative: relative2 } = await import("node:path");
1004
+ const { existsSync: existsSync10 } = await import("node:fs");
757
1005
  const configPatterns = [
758
1006
  "CLAUDE.md",
759
1007
  ".claude/CLAUDE.md",
@@ -763,11 +1011,11 @@ async function readClaudeConfigFiles(projectRoot) {
763
1011
  ];
764
1012
  const files = [];
765
1013
  for (const pattern of configPatterns) {
766
- for await (const fullPath of glob(join10(projectRoot, pattern))) {
767
- if (!existsSync9(fullPath))
1014
+ for await (const fullPath of glob(join11(projectRoot, pattern))) {
1015
+ if (!existsSync10(fullPath))
768
1016
  continue;
769
1017
  try {
770
- const content = await readFile6(fullPath, "utf-8");
1018
+ const content = await readFile7(fullPath, "utf-8");
771
1019
  const fileStat = await stat(fullPath);
772
1020
  const lastMod = getGitLastModified(fullPath, projectRoot);
773
1021
  files.push({
@@ -786,7 +1034,7 @@ function detectStaleFiles(allFiles, projectRoot) {
786
1034
  const stale = [];
787
1035
  const now = Date.now();
788
1036
  for (const file of allFiles) {
789
- const fullPath = join6(projectRoot, file);
1037
+ const fullPath = join7(projectRoot, file);
790
1038
  const lastMod = getGitLastModified(fullPath, projectRoot);
791
1039
  if (lastMod) {
792
1040
  const days = Math.floor((now - new Date(lastMod).getTime()) / (1e3 * 60 * 60 * 24));
@@ -911,7 +1159,7 @@ function escapeHtml(text) {
911
1159
 
912
1160
  // dist/check-update.js
913
1161
  import chalk8 from "chalk";
914
- var CURRENT_VERSION = "0.4.0";
1162
+ var CURRENT_VERSION = "0.5.0";
915
1163
  async function checkForUpdate() {
916
1164
  try {
917
1165
  const controller = new AbortController();
@@ -954,11 +1202,30 @@ function isNewer(a, b) {
954
1202
  return false;
955
1203
  }
956
1204
 
1205
+ // dist/commands/push-toolings.js
1206
+ async function pushToolings(supabase, folderId, toolings) {
1207
+ if (!toolings.length)
1208
+ return;
1209
+ const { data: registry } = await supabase.from("tools_registry").select("id, name");
1210
+ const registryMap = new Map((registry ?? []).map((r) => [r.name, r.id]));
1211
+ for (const tooling of toolings) {
1212
+ const toolId = registryMap.get(tooling.tool_name) ?? null;
1213
+ await supabase.from("project_toolings").upsert({
1214
+ folder_id: folderId,
1215
+ tool_id: toolId,
1216
+ tool_name: tooling.tool_name,
1217
+ detected_version: tooling.detected_version,
1218
+ detection_source: tooling.detection_source,
1219
+ detected_at: (/* @__PURE__ */ new Date()).toISOString()
1220
+ }, { onConflict: "folder_id,tool_name,detection_source" });
1221
+ }
1222
+ }
1223
+
957
1224
  // dist/commands/map.js
958
1225
  async function mapCommand(path, options) {
959
1226
  await checkForUpdate();
960
1227
  const projectRoot = resolve3(path ?? process.cwd());
961
- if (!existsSync5(projectRoot)) {
1228
+ if (!existsSync6(projectRoot)) {
962
1229
  console.error(chalk9.red(`Path not found: ${projectRoot}`));
963
1230
  process.exit(1);
964
1231
  }
@@ -970,9 +1237,10 @@ async function mapCommand(path, options) {
970
1237
  console.log(` Orphans: ${result.orphans.length}`);
971
1238
  console.log(` Stale files: ${result.staleFiles.length}`);
972
1239
  console.log(` Skills: ${result.skills.length}`);
1240
+ console.log(` Toolings: ${result.toolings.length}`);
973
1241
  console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
974
1242
  const outputDir = resolve3(projectRoot, "output");
975
- if (!existsSync5(outputDir)) {
1243
+ if (!existsSync6(outputDir)) {
976
1244
  await mkdir2(outputDir, { recursive: true });
977
1245
  }
978
1246
  const htmlPath = resolve3(outputDir, "index.html");
@@ -1030,6 +1298,7 @@ ${proposedFiles.length} file(s) proposed for deletion:
1030
1298
  if (error) {
1031
1299
  console.error(chalk9.yellow(`Sync warning: ${error.message}`));
1032
1300
  } else {
1301
+ await pushToolings(supabase, folder_id, result.toolings);
1033
1302
  await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder_id).eq("device_name", device_name);
1034
1303
  await saveState({
1035
1304
  lastFolderId: folder_id,
@@ -1064,42 +1333,42 @@ ${proposedFiles.length} file(s) proposed for deletion:
1064
1333
  }
1065
1334
 
1066
1335
  // dist/commands/simulate.js
1067
- import { join as join7 } from "node:path";
1068
- import { existsSync as existsSync6 } from "node:fs";
1069
- import { homedir as homedir5 } from "node:os";
1336
+ import { join as join8 } from "node:path";
1337
+ import { existsSync as existsSync7 } from "node:fs";
1338
+ import { homedir as homedir6 } from "node:os";
1070
1339
  import chalk10 from "chalk";
1071
1340
  async function simulateCommand(prompt) {
1072
1341
  const projectRoot = process.cwd();
1073
1342
  console.log(chalk10.blue(`Simulating prompt: "${prompt}"
1074
1343
  `));
1075
1344
  console.log(chalk10.dim("Files Claude would load:\n"));
1076
- const globalClaude = join7(homedir5(), ".claude", "CLAUDE.md");
1077
- if (existsSync6(globalClaude)) {
1345
+ const globalClaude = join8(homedir6(), ".claude", "CLAUDE.md");
1346
+ if (existsSync7(globalClaude)) {
1078
1347
  console.log(chalk10.green(" \u2713 ~/.claude/CLAUDE.md (global)"));
1079
1348
  }
1080
1349
  for (const rootFile of ROOT_FILES) {
1081
- const fullPath = join7(projectRoot, rootFile);
1082
- if (existsSync6(fullPath)) {
1350
+ const fullPath = join8(projectRoot, rootFile);
1351
+ if (existsSync7(fullPath)) {
1083
1352
  console.log(chalk10.green(` \u2713 ${rootFile} (project root)`));
1084
1353
  }
1085
1354
  }
1086
1355
  const settingsFiles = [
1087
- join7(homedir5(), ".claude", "settings.json"),
1088
- join7(homedir5(), ".claude", "settings.local.json"),
1089
- join7(projectRoot, ".claude", "settings.json"),
1090
- join7(projectRoot, ".claude", "settings.local.json")
1356
+ join8(homedir6(), ".claude", "settings.json"),
1357
+ join8(homedir6(), ".claude", "settings.local.json"),
1358
+ join8(projectRoot, ".claude", "settings.json"),
1359
+ join8(projectRoot, ".claude", "settings.local.json")
1091
1360
  ];
1092
1361
  for (const sf of settingsFiles) {
1093
- if (existsSync6(sf)) {
1094
- const display = sf.startsWith(homedir5()) ? sf.replace(homedir5(), "~") : sf.replace(projectRoot + "/", "");
1362
+ if (existsSync7(sf)) {
1363
+ const display = sf.startsWith(homedir6()) ? sf.replace(homedir6(), "~") : sf.replace(projectRoot + "/", "");
1095
1364
  console.log(chalk10.green(` \u2713 ${display} (settings)`));
1096
1365
  }
1097
1366
  }
1098
1367
  const words = prompt.split(/\s+/);
1099
1368
  for (const word of words) {
1100
1369
  const cleaned = word.replace(/['"]/g, "");
1101
- const candidatePath = join7(projectRoot, cleaned);
1102
- if (existsSync6(candidatePath) && cleaned.includes("/")) {
1370
+ const candidatePath = join8(projectRoot, cleaned);
1371
+ if (existsSync7(candidatePath) && cleaned.includes("/")) {
1103
1372
  console.log(chalk10.cyan(` \u2192 ${cleaned} (referenced in prompt)`));
1104
1373
  }
1105
1374
  }
@@ -1107,18 +1376,18 @@ async function simulateCommand(prompt) {
1107
1376
  }
1108
1377
 
1109
1378
  // dist/commands/print.js
1110
- import { join as join8 } from "node:path";
1111
- import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
1112
- import { existsSync as existsSync7 } from "node:fs";
1379
+ import { join as join9 } from "node:path";
1380
+ import { readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
1381
+ import { existsSync as existsSync8 } from "node:fs";
1113
1382
  import chalk11 from "chalk";
1114
1383
  async function printCommand(title) {
1115
1384
  const projectRoot = process.cwd();
1116
- const scanDataPath = join8(projectRoot, "output", "index.html");
1117
- if (!existsSync7(scanDataPath)) {
1385
+ const scanDataPath = join9(projectRoot, "output", "index.html");
1386
+ if (!existsSync8(scanDataPath)) {
1118
1387
  console.error(chalk11.red("No scan data found. Run: md4ai scan"));
1119
1388
  process.exit(1);
1120
1389
  }
1121
- const html = await readFile4(scanDataPath, "utf-8");
1390
+ const html = await readFile5(scanDataPath, "utf-8");
1122
1391
  const match = html.match(/<script type="application\/json" id="scan-data">([\s\S]*?)<\/script>/);
1123
1392
  if (!match) {
1124
1393
  console.error(chalk11.red("Could not extract scan data from output/index.html"));
@@ -1126,7 +1395,7 @@ async function printCommand(title) {
1126
1395
  }
1127
1396
  const result = JSON.parse(match[1]);
1128
1397
  const printHtml = generatePrintHtml(result, title);
1129
- const outputPath = join8(projectRoot, "output", `print-${Date.now()}.html`);
1398
+ const outputPath = join9(projectRoot, "output", `print-${Date.now()}.html`);
1130
1399
  await writeFile3(outputPath, printHtml, "utf-8");
1131
1400
  console.log(chalk11.green(`Print-ready wall sheet: ${outputPath}`));
1132
1401
  }
@@ -1210,6 +1479,7 @@ async function syncCommand(options) {
1210
1479
  last_scanned: result.scannedAt,
1211
1480
  data_hash: result.dataHash
1212
1481
  }).eq("id", device.folder_id);
1482
+ await pushToolings(supabase, device.folder_id, result.toolings);
1213
1483
  await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
1214
1484
  console.log(chalk12.green(` Done: ${device.device_name}`));
1215
1485
  } catch (err) {
@@ -1243,6 +1513,7 @@ async function syncCommand(options) {
1243
1513
  last_scanned: result.scannedAt,
1244
1514
  data_hash: result.dataHash
1245
1515
  }).eq("id", device.folder_id);
1516
+ await pushToolings(supabase, device.folder_id, result.toolings);
1246
1517
  await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
1247
1518
  await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
1248
1519
  console.log(chalk12.green("Synced."));
@@ -1315,6 +1586,7 @@ Linking "${folder.name}" to this device...
1315
1586
  console.log(` References:${result.graph.edges.length}`);
1316
1587
  console.log(` Orphans: ${result.orphans.length}`);
1317
1588
  console.log(` Skills: ${result.skills.length}`);
1589
+ console.log(` Toolings: ${result.toolings.length}`);
1318
1590
  const { error: scanErr } = await supabase.from("claude_folders").update({
1319
1591
  graph_json: result.graph,
1320
1592
  orphans_json: result.orphans,
@@ -1326,6 +1598,7 @@ Linking "${folder.name}" to this device...
1326
1598
  if (scanErr) {
1327
1599
  console.error(chalk13.yellow(`Scan upload warning: ${scanErr.message}`));
1328
1600
  }
1601
+ await pushToolings(supabase, folder.id, result.toolings);
1329
1602
  const configFiles = await readClaudeConfigFiles(cwd);
1330
1603
  if (configFiles.length > 0) {
1331
1604
  for (const file of configFiles) {
@@ -1354,18 +1627,18 @@ Linking "${folder.name}" to this device...
1354
1627
  }
1355
1628
 
1356
1629
  // dist/commands/import-bundle.js
1357
- import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir3 } from "node:fs/promises";
1358
- import { join as join9, dirname as dirname2 } from "node:path";
1359
- import { existsSync as existsSync8 } from "node:fs";
1630
+ import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir3 } from "node:fs/promises";
1631
+ import { join as join10, dirname as dirname2 } from "node:path";
1632
+ import { existsSync as existsSync9 } from "node:fs";
1360
1633
  import chalk14 from "chalk";
1361
1634
  import { confirm, input as input4 } from "@inquirer/prompts";
1362
1635
  async function importBundleCommand(zipPath) {
1363
- if (!existsSync8(zipPath)) {
1636
+ if (!existsSync9(zipPath)) {
1364
1637
  console.error(chalk14.red(`File not found: ${zipPath}`));
1365
1638
  process.exit(1);
1366
1639
  }
1367
1640
  const JSZip = (await import("jszip")).default;
1368
- const zipData = await readFile5(zipPath);
1641
+ const zipData = await readFile6(zipPath);
1369
1642
  const zip = await JSZip.loadAsync(zipData);
1370
1643
  const manifestFile = zip.file("manifest.json");
1371
1644
  if (!manifestFile) {
@@ -1407,9 +1680,9 @@ Files to extract:`));
1407
1680
  return;
1408
1681
  }
1409
1682
  for (const file of files) {
1410
- const fullPath = join9(targetDir, file.filePath);
1683
+ const fullPath = join10(targetDir, file.filePath);
1411
1684
  const dir = dirname2(fullPath);
1412
- if (!existsSync8(dir)) {
1685
+ if (!existsSync9(dir)) {
1413
1686
  await mkdir3(dir, { recursive: true });
1414
1687
  }
1415
1688
  await writeFile4(fullPath, file.content, "utf-8");
@@ -1419,6 +1692,294 @@ Files to extract:`));
1419
1692
  Done! ${files.length} file(s) extracted to ${targetDir}`));
1420
1693
  }
1421
1694
 
1695
+ // dist/commands/admin-update-tool.js
1696
+ import chalk15 from "chalk";
1697
+ var VALID_CATEGORIES = ["framework", "runtime", "cli", "mcp", "package", "database", "other"];
1698
+ async function adminUpdateToolCommand(options) {
1699
+ const { supabase } = await getAuthenticatedClient();
1700
+ if (!options.name) {
1701
+ console.error(chalk15.red("--name is required."));
1702
+ process.exit(1);
1703
+ }
1704
+ if (options.category && !VALID_CATEGORIES.includes(options.category)) {
1705
+ console.error(chalk15.red(`Invalid category. Must be one of: ${VALID_CATEGORIES.join(", ")}`));
1706
+ process.exit(1);
1707
+ }
1708
+ const { data: existing } = await supabase.from("tools_registry").select("id, name, display_name, category").eq("name", options.name).maybeSingle();
1709
+ if (existing) {
1710
+ const updates = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
1711
+ if (options.display)
1712
+ updates.display_name = options.display;
1713
+ if (options.category)
1714
+ updates.category = options.category;
1715
+ if (options.stable)
1716
+ updates.latest_stable = options.stable;
1717
+ if (options.beta)
1718
+ updates.latest_beta = options.beta;
1719
+ if (options.source)
1720
+ updates.source_url = options.source;
1721
+ if (options.install)
1722
+ updates.install_url = options.install;
1723
+ if (options.notes)
1724
+ updates.notes = options.notes;
1725
+ const { error } = await supabase.from("tools_registry").update(updates).eq("id", existing.id);
1726
+ if (error) {
1727
+ console.error(chalk15.red(`Failed to update: ${error.message}`));
1728
+ process.exit(1);
1729
+ }
1730
+ console.log(chalk15.green(`Updated "${existing.display_name}" in the registry.`));
1731
+ } else {
1732
+ if (!options.display || !options.category) {
1733
+ console.error(chalk15.red("New tools require --display and --category."));
1734
+ process.exit(1);
1735
+ }
1736
+ const { error } = await supabase.from("tools_registry").insert({
1737
+ name: options.name,
1738
+ display_name: options.display,
1739
+ category: options.category,
1740
+ latest_stable: options.stable ?? null,
1741
+ latest_beta: options.beta ?? null,
1742
+ source_url: options.source ?? null,
1743
+ install_url: options.install ?? null,
1744
+ notes: options.notes ?? null
1745
+ });
1746
+ if (error) {
1747
+ console.error(chalk15.red(`Failed to create: ${error.message}`));
1748
+ process.exit(1);
1749
+ }
1750
+ console.log(chalk15.green(`Added "${options.display}" to the registry.`));
1751
+ }
1752
+ }
1753
+
1754
+ // dist/commands/admin-list-tools.js
1755
+ import chalk16 from "chalk";
1756
+ async function adminListToolsCommand() {
1757
+ const { supabase } = await getAuthenticatedClient();
1758
+ const { data: tools, error } = await supabase.from("tools_registry").select("*").order("category").order("display_name");
1759
+ if (error) {
1760
+ console.error(chalk16.red(`Failed to fetch tools: ${error.message}`));
1761
+ process.exit(1);
1762
+ }
1763
+ if (!tools?.length) {
1764
+ console.log(chalk16.yellow("No tools in the registry."));
1765
+ return;
1766
+ }
1767
+ const nameW = Math.max(16, ...tools.map((t) => t.display_name.length));
1768
+ const catW = Math.max(10, ...tools.map((t) => t.category.length));
1769
+ const stableW = Math.max(8, ...tools.map((t) => (t.latest_stable ?? "\u2014").length));
1770
+ const betaW = Math.max(8, ...tools.map((t) => (t.latest_beta ?? "\u2014").length));
1771
+ const header = [
1772
+ "Name".padEnd(nameW),
1773
+ "Category".padEnd(catW),
1774
+ "Stable".padEnd(stableW),
1775
+ "Beta".padEnd(betaW),
1776
+ "Last Checked"
1777
+ ].join(" ");
1778
+ console.log(chalk16.bold(header));
1779
+ console.log("\u2500".repeat(header.length));
1780
+ for (const tool of tools) {
1781
+ const lastChecked = tool.updated_at ? formatRelative(new Date(tool.updated_at)) : "\u2014";
1782
+ const row = [
1783
+ tool.display_name.padEnd(nameW),
1784
+ tool.category.padEnd(catW),
1785
+ (tool.latest_stable ?? "\u2014").padEnd(stableW),
1786
+ (tool.latest_beta ?? "\u2014").padEnd(betaW),
1787
+ lastChecked
1788
+ ].join(" ");
1789
+ console.log(row);
1790
+ }
1791
+ console.log(chalk16.grey(`
1792
+ ${tools.length} tool(s) in registry.`));
1793
+ }
1794
+ function formatRelative(date) {
1795
+ const diff = Date.now() - date.getTime();
1796
+ const hours = Math.floor(diff / (1e3 * 60 * 60));
1797
+ if (hours < 1)
1798
+ return "just now";
1799
+ if (hours < 24)
1800
+ return `${hours}h ago`;
1801
+ const days = Math.floor(hours / 24);
1802
+ if (days < 7)
1803
+ return `${days}d ago`;
1804
+ return date.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
1805
+ }
1806
+
1807
+ // dist/commands/admin-fetch-versions.js
1808
+ import chalk17 from "chalk";
1809
+
1810
+ // dist/commands/version-sources.js
1811
+ var VERSION_SOURCES = {
1812
+ "next": { type: "npm", package: "next" },
1813
+ "react": { type: "npm", package: "react" },
1814
+ "react-dom": { type: "npm", package: "react-dom" },
1815
+ "typescript": { type: "npm", package: "typescript" },
1816
+ "tailwindcss": { type: "npm", package: "tailwindcss" },
1817
+ "esbuild": { type: "npm", package: "esbuild" },
1818
+ "chalk": { type: "npm", package: "chalk" },
1819
+ "commander": { type: "npm", package: "commander" },
1820
+ "inquirer": { type: "npm", package: "inquirer" },
1821
+ "playwright": { type: "npm", package: "playwright" },
1822
+ "resend": { type: "npm", package: "resend" },
1823
+ "@supabase/supabase-js": { type: "npm", package: "@supabase/supabase-js" },
1824
+ "pnpm": { type: "npm", package: "pnpm" },
1825
+ "claude-code": { type: "npm", package: "@anthropic-ai/claude-code" },
1826
+ "turborepo": { type: "npm", package: "turbo" },
1827
+ "npm": { type: "npm", package: "npm" },
1828
+ "node": { type: "github", repo: "nodejs/node" },
1829
+ "supabase-cli": { type: "github", repo: "supabase/cli" }
1830
+ };
1831
+
1832
+ // dist/commands/admin-fetch-versions.js
1833
+ async function adminFetchVersionsCommand() {
1834
+ const { supabase } = await getAuthenticatedClient();
1835
+ const { data: tools, error } = await supabase.from("tools_registry").select("*").order("display_name");
1836
+ if (error) {
1837
+ console.error(chalk17.red(`Failed to fetch tools: ${error.message}`));
1838
+ process.exit(1);
1839
+ }
1840
+ if (!tools?.length) {
1841
+ console.log(chalk17.yellow("No tools in the registry."));
1842
+ }
1843
+ const { data: allProjectToolings } = await supabase.from("project_toolings").select("tool_name, detection_source").is("tool_id", null);
1844
+ const registeredNames = new Set((tools ?? []).map((t) => t.name));
1845
+ const unregisteredNames = /* @__PURE__ */ new Set();
1846
+ for (const pt of allProjectToolings ?? []) {
1847
+ if (!registeredNames.has(pt.tool_name) && pt.detection_source === "package.json") {
1848
+ unregisteredNames.add(pt.tool_name);
1849
+ }
1850
+ }
1851
+ if (unregisteredNames.size > 0) {
1852
+ console.log(chalk17.blue(`Auto-registering ${unregisteredNames.size} unverified package(s)...
1853
+ `));
1854
+ for (const name of unregisteredNames) {
1855
+ const displayName = name;
1856
+ const { data: inserted, error: insertError } = await supabase.from("tools_registry").upsert({
1857
+ name,
1858
+ display_name: displayName,
1859
+ category: "package",
1860
+ source_url: `https://www.npmjs.com/package/${name}`,
1861
+ install_url: `https://www.npmjs.com/package/${name}`
1862
+ }, { onConflict: "name" }).select().single();
1863
+ if (!insertError && inserted) {
1864
+ tools.push(inserted);
1865
+ await supabase.from("project_toolings").update({ tool_id: inserted.id }).eq("tool_name", name).is("tool_id", null);
1866
+ }
1867
+ }
1868
+ }
1869
+ if (!tools?.length) {
1870
+ console.log(chalk17.yellow("No tools to fetch."));
1871
+ return;
1872
+ }
1873
+ console.log(chalk17.bold(`Fetching versions for ${tools.length} tool(s)...
1874
+ `));
1875
+ const results = await Promise.all(tools.map(async (tool) => {
1876
+ const source = VERSION_SOURCES[tool.name] ?? { type: "npm", package: tool.name };
1877
+ try {
1878
+ const { stable, beta } = await fetchVersions(source);
1879
+ const stableChanged = stable !== tool.latest_stable;
1880
+ const betaChanged = beta !== tool.latest_beta;
1881
+ const updates = {
1882
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1883
+ };
1884
+ if (stableChanged)
1885
+ updates.latest_stable = stable;
1886
+ if (betaChanged)
1887
+ updates.latest_beta = beta;
1888
+ const { error: updateError } = await supabase.from("tools_registry").update(updates).eq("id", tool.id);
1889
+ if (updateError) {
1890
+ return { displayName: tool.display_name, stable, beta, status: "failed" };
1891
+ }
1892
+ return {
1893
+ displayName: tool.display_name,
1894
+ stable,
1895
+ beta,
1896
+ status: stableChanged || betaChanged ? "updated" : "unchanged"
1897
+ };
1898
+ } catch {
1899
+ return { displayName: tool.display_name, stable: null, beta: null, status: "failed" };
1900
+ }
1901
+ }));
1902
+ const nameW = Math.max(16, ...results.map((r) => r.displayName.length));
1903
+ const stableW = Math.max(8, ...results.map((r) => (r.stable ?? "\u2014").length));
1904
+ const betaW = Math.max(8, ...results.map((r) => (r.beta ?? "\u2014").length));
1905
+ const statusW = 10;
1906
+ const header = [
1907
+ "Tool".padEnd(nameW),
1908
+ "Stable".padEnd(stableW),
1909
+ "Beta".padEnd(betaW),
1910
+ "Status".padEnd(statusW)
1911
+ ].join(" ");
1912
+ console.log(chalk17.bold(header));
1913
+ console.log("\u2500".repeat(header.length));
1914
+ for (const result of results) {
1915
+ const statusColour = result.status === "updated" ? chalk17.green : result.status === "unchanged" ? chalk17.grey : result.status === "failed" ? chalk17.red : chalk17.yellow;
1916
+ const row = [
1917
+ result.displayName.padEnd(nameW),
1918
+ (result.stable ?? "\u2014").padEnd(stableW),
1919
+ (result.beta ?? "\u2014").padEnd(betaW),
1920
+ statusColour(result.status.padEnd(statusW))
1921
+ ].join(" ");
1922
+ console.log(row);
1923
+ }
1924
+ const updated = results.filter((r) => r.status === "updated").length;
1925
+ const unchanged = results.filter((r) => r.status === "unchanged").length;
1926
+ const failed = results.filter((r) => r.status === "failed").length;
1927
+ const noSource = results.filter((r) => r.status === "no source").length;
1928
+ console.log(chalk17.grey(`
1929
+ ${results.length} tool(s): `) + chalk17.green(`${updated} updated`) + ", " + chalk17.grey(`${unchanged} unchanged`) + ", " + chalk17.red(`${failed} failed`) + ", " + chalk17.yellow(`${noSource} no source`));
1930
+ }
1931
+ async function fetchVersions(source) {
1932
+ const controller = new AbortController();
1933
+ const timeout = setTimeout(() => controller.abort(), 3e3);
1934
+ try {
1935
+ if (source.type === "npm") {
1936
+ return await fetchNpmVersions(source.package, controller.signal);
1937
+ } else {
1938
+ return await fetchGitHubVersions(source.repo, controller.signal);
1939
+ }
1940
+ } finally {
1941
+ clearTimeout(timeout);
1942
+ }
1943
+ }
1944
+ async function fetchNpmVersions(packageName, signal) {
1945
+ const url = `https://registry.npmjs.org/-/package/${encodeURIComponent(packageName)}/dist-tags`;
1946
+ const res = await fetch(url, { signal });
1947
+ if (!res.ok) {
1948
+ throw new Error(`npm registry returned ${res.status}`);
1949
+ }
1950
+ const tags = await res.json();
1951
+ const stable = tags.latest ?? null;
1952
+ const beta = tags.next ?? tags.beta ?? tags.rc ?? null;
1953
+ return { stable, beta };
1954
+ }
1955
+ async function fetchGitHubVersions(repo, signal) {
1956
+ const url = `https://api.github.com/repos/${repo}/releases?per_page=20`;
1957
+ const res = await fetch(url, {
1958
+ signal,
1959
+ headers: { Accept: "application/vnd.github+json" }
1960
+ });
1961
+ if (!res.ok) {
1962
+ throw new Error(`GitHub API returned ${res.status}`);
1963
+ }
1964
+ const releases = await res.json();
1965
+ let stable = null;
1966
+ let beta = null;
1967
+ for (const release of releases) {
1968
+ if (release.draft)
1969
+ continue;
1970
+ const version = release.tag_name.replace(/^v/, "");
1971
+ if (!release.prerelease && !stable) {
1972
+ stable = version;
1973
+ }
1974
+ if (release.prerelease && !beta) {
1975
+ beta = version;
1976
+ }
1977
+ if (stable && beta)
1978
+ break;
1979
+ }
1980
+ return { stable, beta };
1981
+ }
1982
+
1422
1983
  // dist/index.js
1423
1984
  var program = new Command();
1424
1985
  program.name("md4ai").description("MD4AI \u2014 Claude tooling visualiser").version(CURRENT_VERSION).addHelpText("after", `
@@ -1440,4 +2001,8 @@ program.command("print <title>").description("Generate a printable wall-chart HT
1440
2001
  program.command("sync").description("Re-push latest scan data to Supabase").option("--all", "Sync all folders on all devices").action(syncCommand);
1441
2002
  program.command("import <zipfile>").description("Import a Claude setup bundle exported from the web dashboard").action(importBundleCommand);
1442
2003
  program.command("check-update").description("Check if a newer version of md4ai is available").action(checkForUpdate);
2004
+ var admin = program.command("admin").description("Admin commands for managing the tools registry");
2005
+ 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);
2006
+ admin.command("list-tools").description("List all tools in the master registry").action(adminListToolsCommand);
2007
+ admin.command("fetch-versions").description("Fetch latest stable and beta versions from npm and GitHub").action(adminFetchVersionsCommand);
1443
2008
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md4ai",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "CLI for MD4AI — scan Claude projects and sync to your dashboard",
5
5
  "type": "module",
6
6
  "bin": {