md4ai 0.17.3 → 0.18.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 +288 -275
  2. package/package.json +1 -1
@@ -49,16 +49,6 @@ function printUpdateBanner(latest) {
49
49
  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"));
50
50
  console.log("");
51
51
  }
52
- async function checkForUpdate() {
53
- const latest = await fetchLatest();
54
- if (!latest)
55
- return;
56
- if (latest !== CURRENT_VERSION && isNewer(latest, CURRENT_VERSION)) {
57
- printUpdateBanner(latest);
58
- } else {
59
- console.log(chalk.green(`md4ai v${CURRENT_VERSION} \u2014 you're on the latest version.`));
60
- }
61
- }
62
52
  async function autoCheckForUpdate() {
63
53
  try {
64
54
  const latest = await fetchLatest();
@@ -122,7 +112,7 @@ var CURRENT_VERSION;
122
112
  var init_check_update = __esm({
123
113
  "dist/check-update.js"() {
124
114
  "use strict";
125
- CURRENT_VERSION = true ? "0.17.3" : "0.0.0-dev";
115
+ CURRENT_VERSION = true ? "0.18.1" : "0.0.0-dev";
126
116
  }
127
117
  });
128
118
 
@@ -634,7 +624,7 @@ import { execFileSync as execFileSync2 } from "node:child_process";
634
624
  import { statSync } from "node:fs";
635
625
  function getGitLastModified(filePath, cwd) {
636
626
  try {
637
- const result = execFileSync2("git", ["log", "-1", "--format=%cI", "--", filePath], { cwd, encoding: "utf-8", timeout: 5e3 }).trim();
627
+ const result = execFileSync2("git", ["log", "-1", "--format=%cI", "--", filePath], { cwd, encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
638
628
  return result || null;
639
629
  } catch {
640
630
  try {
@@ -646,7 +636,7 @@ function getGitLastModified(filePath, cwd) {
646
636
  }
647
637
  function getGitCreationDate(filePath, cwd) {
648
638
  try {
649
- const result = execFileSync2("git", ["log", "--diff-filter=A", "--format=%cI", "--", filePath], { cwd, encoding: "utf-8", timeout: 5e3 }).trim();
639
+ const result = execFileSync2("git", ["log", "--diff-filter=A", "--format=%cI", "--", filePath], { cwd, encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
650
640
  return result || null;
651
641
  } catch {
652
642
  try {
@@ -2087,7 +2077,7 @@ __export(scanner_exports, {
2087
2077
  scanProject: () => scanProject
2088
2078
  });
2089
2079
  import { readdir as readdir5 } from "node:fs/promises";
2090
- import { join as join12, relative as relative3 } from "node:path";
2080
+ import { join as join12, relative as relative3, resolve as resolve4 } from "node:path";
2091
2081
  import { existsSync as existsSync7 } from "node:fs";
2092
2082
  import { homedir as homedir7 } from "node:os";
2093
2083
  import { createHash } from "node:crypto";
@@ -2097,7 +2087,7 @@ async function scanProject(projectRoot, folderId) {
2097
2087
  const allRefs = [];
2098
2088
  const allBrokenRefs = [];
2099
2089
  for (const file of allFiles) {
2100
- const fullPath = file.startsWith("/") ? file : join12(projectRoot, file);
2090
+ const fullPath = file.startsWith("/") ? file : resolveFilePath(file, projectRoot);
2101
2091
  try {
2102
2092
  const { refs, brokenRefs: brokenRefs2 } = await parseFileReferences(fullPath, projectRoot);
2103
2093
  allRefs.push(...refs);
@@ -2172,6 +2162,17 @@ async function discoverFiles(projectRoot) {
2172
2162
  if (existsSync7(plansDir)) {
2173
2163
  await walkDir(plansDir, projectRoot, files);
2174
2164
  }
2165
+ const absRoot = resolve4(projectRoot);
2166
+ const projectHash = absRoot.replace(/\//g, "-");
2167
+ const memoryDir = join12(homedir7(), ".claude", "projects", projectHash, "memory");
2168
+ if (existsSync7(memoryDir)) {
2169
+ const memEntries = await readdir5(memoryDir, { withFileTypes: true });
2170
+ for (const entry of memEntries) {
2171
+ if (entry.isFile() && entry.name.endsWith(".md")) {
2172
+ files.push(`.claude/memory/${entry.name}`);
2173
+ }
2174
+ }
2175
+ }
2175
2176
  return [...new Set(files)];
2176
2177
  }
2177
2178
  async function walkDir(dir, projectRoot, files) {
@@ -2215,7 +2216,7 @@ async function readClaudeConfigFiles(projectRoot, graphFilePaths) {
2215
2216
  if (relPath.startsWith("~") || relPath.startsWith("/"))
2216
2217
  continue;
2217
2218
  seen.add(relPath);
2218
- const fullPath = join12(projectRoot, relPath);
2219
+ const fullPath = resolveFilePath(relPath, projectRoot);
2219
2220
  if (!existsSync15(fullPath))
2220
2221
  continue;
2221
2222
  try {
@@ -2228,11 +2229,19 @@ async function readClaudeConfigFiles(projectRoot, graphFilePaths) {
2228
2229
  }
2229
2230
  return files;
2230
2231
  }
2232
+ function resolveFilePath(relPath, projectRoot) {
2233
+ if (relPath.startsWith(".claude/memory/")) {
2234
+ const absRoot = resolve4(projectRoot);
2235
+ const projectHash = absRoot.replace(/\//g, "-");
2236
+ return join12(homedir7(), ".claude", "projects", projectHash, "memory", relPath.replace(".claude/memory/", ""));
2237
+ }
2238
+ return join12(projectRoot, relPath);
2239
+ }
2231
2240
  function detectStaleFiles(allFiles, projectRoot) {
2232
2241
  const stale = [];
2233
2242
  const now = Date.now();
2234
2243
  for (const file of allFiles) {
2235
- const fullPath = join12(projectRoot, file);
2244
+ const fullPath = resolveFilePath(file, projectRoot);
2236
2245
  const lastMod = getGitLastModified(fullPath, projectRoot);
2237
2246
  if (lastMod) {
2238
2247
  const days = Math.floor((now - new Date(lastMod).getTime()) / (1e3 * 60 * 60 * 24));
@@ -2521,7 +2530,7 @@ var map_exports = {};
2521
2530
  __export(map_exports, {
2522
2531
  mapCommand: () => mapCommand
2523
2532
  });
2524
- import { resolve as resolve4, basename as basename2 } from "node:path";
2533
+ import { resolve as resolve5, basename as basename2 } from "node:path";
2525
2534
  import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
2526
2535
  import { existsSync as existsSync8 } from "node:fs";
2527
2536
  import chalk12 from "chalk";
@@ -2533,7 +2542,7 @@ async function mapCommand(path, options) {
2533
2542
  const shouldContinue = await promptUpdateIfAvailable(args);
2534
2543
  if (!shouldContinue)
2535
2544
  return;
2536
- const projectRoot = resolve4(path ?? process.cwd());
2545
+ const projectRoot = resolve5(path ?? process.cwd());
2537
2546
  if (!existsSync8(projectRoot)) {
2538
2547
  console.error(chalk12.red(`Path not found: ${projectRoot}`));
2539
2548
  process.exit(1);
@@ -2571,11 +2580,11 @@ async function mapCommand(path, options) {
2571
2580
  console.log(chalk12.red(` ... and ${result.brokenRefs.length - 5} more`));
2572
2581
  }
2573
2582
  }
2574
- const outputDir = resolve4(projectRoot, "output");
2583
+ const outputDir = resolve5(projectRoot, "output");
2575
2584
  if (!existsSync8(outputDir)) {
2576
2585
  await mkdir2(outputDir, { recursive: true });
2577
2586
  }
2578
- const htmlPath = resolve4(outputDir, "index.html");
2587
+ const htmlPath = resolve5(outputDir, "index.html");
2579
2588
  const html = generateOfflineHtml(result, projectRoot);
2580
2589
  await writeFile2(htmlPath, html, "utf-8");
2581
2590
  console.log(chalk12.green(`
@@ -2601,9 +2610,9 @@ ${proposedFiles.length} file(s) proposed for deletion:
2601
2610
  value: f
2602
2611
  }))
2603
2612
  });
2604
- const resolvedRoot = resolve4(projectRoot);
2613
+ const resolvedRoot = resolve5(projectRoot);
2605
2614
  for (const file of toDelete) {
2606
- const fullPath = resolve4(projectRoot, file.file_path);
2615
+ const fullPath = resolve5(projectRoot, file.file_path);
2607
2616
  if (!fullPath.startsWith(resolvedRoot + "/")) {
2608
2617
  console.error(chalk12.red(` Blocked path traversal: ${file.file_path}`));
2609
2618
  continue;
@@ -2809,139 +2818,15 @@ var init_map = __esm({
2809
2818
  }
2810
2819
  });
2811
2820
 
2812
- // dist/commands/sync.js
2813
- var sync_exports = {};
2814
- __export(sync_exports, {
2815
- syncCommand: () => syncCommand
2816
- });
2817
- import { resolve as resolve5 } from "node:path";
2818
- import { existsSync as existsSync11 } from "node:fs";
2819
- import chalk15 from "chalk";
2820
- function isValidProjectPath(p) {
2821
- const resolved = resolve5(p);
2822
- return resolved.startsWith("/") && !resolved.includes("..") && existsSync11(resolved);
2823
- }
2824
- async function syncCommand(options) {
2825
- const { supabase, userId } = await getAuthenticatedClient();
2826
- if (options.all) {
2827
- const currentDeviceName = detectDeviceName();
2828
- const { data: devices, error } = await supabase.from("device_paths").select("folder_id, device_name, path").eq("device_name", currentDeviceName);
2829
- if (error || !devices?.length) {
2830
- console.log(chalk15.yellow(`
2831
- No projects linked on ${currentDeviceName} yet.
2832
- `));
2833
- console.log(chalk15.dim(" To link a project, cd into its folder and run:"));
2834
- console.log(chalk15.cyan(" md4ai scan"));
2835
- console.log(chalk15.dim(" Or link an existing project from the dashboard:"));
2836
- console.log(chalk15.cyan(" md4ai link <project-id>\n"));
2837
- return;
2838
- }
2839
- for (const device of devices) {
2840
- if (!isValidProjectPath(device.path)) {
2841
- console.error(chalk15.red(` Skipping invalid path: ${device.path}`));
2842
- continue;
2843
- }
2844
- console.log(chalk15.blue(`Syncing: ${device.path}`));
2845
- const { data: proposedAll } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
2846
- if (proposedAll?.length) {
2847
- console.log(chalk15.yellow(` ${proposedAll.length} file(s) proposed for deletion \u2014 run \`md4ai scan\` to review.`));
2848
- }
2849
- try {
2850
- const result = await scanProject(device.path, device.folder_id);
2851
- const { data: allDeviceRow } = await supabase.from("devices").select("id").eq("user_id", userId).eq("device_name", device.device_name).single();
2852
- const allDeviceId = allDeviceRow?.id;
2853
- if (allDeviceId) {
2854
- await supabase.from("device_scans").upsert({
2855
- folder_id: device.folder_id,
2856
- device_id: allDeviceId,
2857
- user_id: userId,
2858
- graph_json: result.graph,
2859
- orphans_json: result.orphans,
2860
- skills_table_json: result.skills,
2861
- stale_files_json: result.staleFiles,
2862
- broken_refs_json: result.brokenRefs,
2863
- env_manifest_json: result.envManifest,
2864
- plugin_versions_json: result.pluginVersions,
2865
- data_hash: result.dataHash,
2866
- scanned_at: result.scannedAt,
2867
- cli_version: CURRENT_VERSION
2868
- }, { onConflict: "folder_id,device_id" });
2869
- }
2870
- await pushToolings(supabase, device.folder_id, result.toolings, allDeviceId);
2871
- await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
2872
- console.log(chalk15.green(` Done: ${device.device_name}`));
2873
- } catch (err) {
2874
- console.error(chalk15.red(` Failed: ${device.path}: ${err}`));
2875
- }
2876
- }
2877
- await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
2878
- console.log(chalk15.green("\nAll devices synced."));
2879
- } else {
2880
- const state = await loadState();
2881
- if (!state.lastFolderId) {
2882
- console.error(chalk15.yellow("No recent sync. Use: md4ai sync --all, or md4ai scan <path> first."));
2883
- process.exit(1);
2884
- }
2885
- const { data: device } = await supabase.from("device_paths").select("folder_id, device_name, path").eq("folder_id", state.lastFolderId).eq("device_name", state.lastDeviceName).single();
2886
- if (!device) {
2887
- console.error(chalk15.red("Could not find last synced device/folder."));
2888
- process.exit(1);
2889
- }
2890
- if (!isValidProjectPath(device.path)) {
2891
- console.error(chalk15.red(`Invalid project path: ${device.path}`));
2892
- process.exit(1);
2893
- }
2894
- console.log(chalk15.blue(`Syncing: ${device.path}`));
2895
- const { data: proposedSingle } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
2896
- if (proposedSingle?.length) {
2897
- console.log(chalk15.yellow(` ${proposedSingle.length} file(s) proposed for deletion \u2014 run \`md4ai scan\` to review.`));
2898
- }
2899
- const result = await scanProject(device.path, device.folder_id);
2900
- const { data: singleDeviceRow } = await supabase.from("devices").select("id").eq("user_id", userId).eq("device_name", device.device_name).single();
2901
- const singleDeviceId = singleDeviceRow?.id;
2902
- if (singleDeviceId) {
2903
- await supabase.from("device_scans").upsert({
2904
- folder_id: device.folder_id,
2905
- device_id: singleDeviceId,
2906
- user_id: userId,
2907
- graph_json: result.graph,
2908
- orphans_json: result.orphans,
2909
- skills_table_json: result.skills,
2910
- stale_files_json: result.staleFiles,
2911
- broken_refs_json: result.brokenRefs,
2912
- plugin_versions_json: result.pluginVersions,
2913
- data_hash: result.dataHash,
2914
- scanned_at: result.scannedAt,
2915
- cli_version: CURRENT_VERSION
2916
- }, { onConflict: "folder_id,device_id" });
2917
- }
2918
- await pushToolings(supabase, device.folder_id, result.toolings, singleDeviceId);
2919
- await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
2920
- await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
2921
- console.log(chalk15.green("Synced."));
2922
- }
2923
- }
2924
- var init_sync = __esm({
2925
- "dist/commands/sync.js"() {
2926
- "use strict";
2927
- init_auth();
2928
- init_config();
2929
- init_scanner();
2930
- init_push_toolings();
2931
- init_check_update();
2932
- init_device_utils();
2933
- }
2934
- });
2935
-
2936
2821
  // dist/mcp/read-configs.js
2937
2822
  import { readFile as readFile12 } from "node:fs/promises";
2938
2823
  import { join as join15 } from "node:path";
2939
2824
  import { homedir as homedir9 } from "node:os";
2940
- import { existsSync as existsSync13 } from "node:fs";
2825
+ import { existsSync as existsSync12 } from "node:fs";
2941
2826
  import { readdir as readdir6 } from "node:fs/promises";
2942
2827
  async function readJsonSafe(path) {
2943
2828
  try {
2944
- if (!existsSync13(path))
2829
+ if (!existsSync12(path))
2945
2830
  return null;
2946
2831
  const raw = await readFile12(path, "utf-8");
2947
2832
  return JSON.parse(raw);
@@ -3012,14 +2897,14 @@ async function readAllMcpConfigs() {
3012
2897
  const cwdMcp = await readJsonSafe(join15(process.cwd(), ".mcp.json"));
3013
2898
  entries.push(...parseServers(cwdMcp, "project"));
3014
2899
  const pluginsBase = join15(home, ".claude", "plugins", "marketplaces");
3015
- if (existsSync13(pluginsBase)) {
2900
+ if (existsSync12(pluginsBase)) {
3016
2901
  try {
3017
2902
  const marketplaces = await readdir6(pluginsBase, { withFileTypes: true });
3018
2903
  for (const mp of marketplaces) {
3019
2904
  if (!mp.isDirectory())
3020
2905
  continue;
3021
2906
  const extDir = join15(pluginsBase, mp.name, "external_plugins");
3022
- if (!existsSync13(extDir))
2907
+ if (!existsSync12(extDir))
3023
2908
  continue;
3024
2909
  const plugins = await readdir6(extDir, { withFileTypes: true });
3025
2910
  for (const plugin of plugins) {
@@ -3033,7 +2918,7 @@ async function readAllMcpConfigs() {
3033
2918
  }
3034
2919
  }
3035
2920
  const cacheBase = join15(home, ".claude", "plugins", "cache");
3036
- if (existsSync13(cacheBase)) {
2921
+ if (existsSync12(cacheBase)) {
3037
2922
  try {
3038
2923
  const registries = await readdir6(cacheBase, { withFileTypes: true });
3039
2924
  for (const reg of registries) {
@@ -3198,7 +3083,7 @@ var mcp_watch_exports = {};
3198
3083
  __export(mcp_watch_exports, {
3199
3084
  mcpWatchCommand: () => mcpWatchCommand
3200
3085
  });
3201
- import chalk21 from "chalk";
3086
+ import chalk20 from "chalk";
3202
3087
  import { execFileSync as execFileSync6 } from "node:child_process";
3203
3088
  import { createHash as createHash2 } from "node:crypto";
3204
3089
  function detectTty() {
@@ -3352,20 +3237,20 @@ function buildRows(configs, httpResults, cdpStatus) {
3352
3237
  }
3353
3238
  function printTable(rows, deviceName, watcherPid, cdpResult) {
3354
3239
  process.stdout.write("\x1B[3J\x1B[2J\x1B[H");
3355
- console.log(chalk21.bold.cyan(`
3356
- MCP Monitor v${CURRENT_VERSION} \u2014 ${deviceName}`) + chalk21.dim(` (PID ${watcherPid})`));
3357
- console.log(chalk21.dim(` ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \xB7 refreshes every 30s \xB7 Ctrl+C to stop
3240
+ console.log(chalk20.bold.cyan(`
3241
+ MCP Monitor v${CURRENT_VERSION} \u2014 ${deviceName}`) + chalk20.dim(` (PID ${watcherPid})`));
3242
+ console.log(chalk20.dim(` ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \xB7 refreshes every 30s \xB7 Ctrl+C to stop
3358
3243
  `));
3359
3244
  if (cdpResult) {
3360
3245
  if (cdpResult.status === "reachable") {
3361
3246
  const browserInfo = cdpResult.browser ? ` (${cdpResult.browser})` : "";
3362
- console.log(chalk21.green(" \u2714 Chrome browser connected") + chalk21.dim(browserInfo));
3363
- console.log(chalk21.dim(" Claude Code can control Chrome via DevTools Protocol on localhost:9222"));
3247
+ console.log(chalk20.green(" \u2714 Chrome browser connected") + chalk20.dim(browserInfo));
3248
+ console.log(chalk20.dim(" Claude Code can control Chrome via DevTools Protocol on localhost:9222"));
3364
3249
  } else {
3365
- console.log(chalk21.red(" \u2717 Chrome browser not connected"));
3366
- console.log(chalk21.dim(" Claude Code cannot reach Chrome. To fix:"));
3367
- console.log(chalk21.dim(" 1. Open Chrome with: google-chrome --remote-debugging-port=9222"));
3368
- console.log(chalk21.dim(" 2. Or add --remote-debugging-port=9222 to your Chrome shortcut"));
3250
+ console.log(chalk20.red(" \u2717 Chrome browser not connected"));
3251
+ console.log(chalk20.dim(" Claude Code cannot reach Chrome. To fix:"));
3252
+ console.log(chalk20.dim(" 1. Open Chrome with: google-chrome --remote-debugging-port=9222"));
3253
+ console.log(chalk20.dim(" 2. Or add --remote-debugging-port=9222 to your Chrome shortcut"));
3369
3254
  }
3370
3255
  console.log("");
3371
3256
  }
@@ -3386,41 +3271,41 @@ function printTable(rows, deviceName, watcherPid, cdpResult) {
3386
3271
  sessionNum++;
3387
3272
  const totalMem = servers.reduce((sum, s) => sum + (s.memory_mb ?? 0), 0);
3388
3273
  const label = byTty.size === 1 ? "Claude Code session" : `Claude Code session ${sessionNum}`;
3389
- console.log(chalk21.green(` ${label}`) + chalk21.dim(` \u2014 ${servers.length} server${servers.length !== 1 ? "s" : ""} \xB7 ${totalMem} MB \xB7 terminal ${tty}`));
3274
+ console.log(chalk20.green(` ${label}`) + chalk20.dim(` \u2014 ${servers.length} server${servers.length !== 1 ? "s" : ""} \xB7 ${totalMem} MB \xB7 terminal ${tty}`));
3390
3275
  for (const s of servers) {
3391
3276
  const uptime = formatUptime(s.uptime_seconds ?? 0);
3392
- const source = chalk21.dim(`[${s.config_source}]`);
3393
- console.log(` ${chalk21.green("\u25CF")} ${s.server_name.padEnd(20)} ${source} ${chalk21.dim((s.package_name ?? "").padEnd(25))} ${String(s.memory_mb ?? 0).padStart(4)} MB ${uptime}`);
3277
+ const source = chalk20.dim(`[${s.config_source}]`);
3278
+ console.log(` ${chalk20.green("\u25CF")} ${s.server_name.padEnd(20)} ${source} ${chalk20.dim((s.package_name ?? "").padEnd(25))} ${String(s.memory_mb ?? 0).padStart(4)} MB ${uptime}`);
3394
3279
  }
3395
3280
  console.log("");
3396
3281
  }
3397
3282
  if (byTty.size > 1) {
3398
- console.log(chalk21.dim(" Each open Claude Code window runs its own set of MCP servers.\n"));
3283
+ console.log(chalk20.dim(" Each open Claude Code window runs its own set of MCP servers.\n"));
3399
3284
  }
3400
3285
  }
3401
3286
  if (runningHttp.length > 0) {
3402
- console.log(chalk21.blue(` Remote Services (${runningHttp.length})`) + chalk21.dim(" \u2014 HTTP endpoints reachable"));
3287
+ console.log(chalk20.blue(` Remote Services (${runningHttp.length})`) + chalk20.dim(" \u2014 HTTP endpoints reachable"));
3403
3288
  for (const s of runningHttp) {
3404
- console.log(` ${chalk21.blue("\u25CF")} ${s.server_name.padEnd(20)} ${chalk21.dim((s.http_url ?? "").padEnd(30))}`);
3289
+ console.log(` ${chalk20.blue("\u25CF")} ${s.server_name.padEnd(20)} ${chalk20.dim((s.http_url ?? "").padEnd(30))}`);
3405
3290
  }
3406
3291
  console.log("");
3407
3292
  }
3408
3293
  if (stopped.length > 0 || errored.length > 0) {
3409
3294
  const notRunning = [...stopped, ...errored];
3410
- console.log(chalk21.yellow(` Not Running (${notRunning.length})`) + chalk21.dim(" \u2014 configured but no process detected"));
3295
+ console.log(chalk20.yellow(` Not Running (${notRunning.length})`) + chalk20.dim(" \u2014 configured but no process detected"));
3411
3296
  for (const s of notRunning) {
3412
- const icon = s.status === "error" ? chalk21.red("\u2717") : chalk21.yellow("\u25CB");
3413
- const source = chalk21.dim(`[${s.config_source}]`);
3414
- const detail = s.error_detail ? chalk21.dim(` \u2014 ${s.error_detail}`) : "";
3297
+ const icon = s.status === "error" ? chalk20.red("\u2717") : chalk20.yellow("\u25CB");
3298
+ const source = chalk20.dim(`[${s.config_source}]`);
3299
+ const detail = s.error_detail ? chalk20.dim(` \u2014 ${s.error_detail}`) : "";
3415
3300
  console.log(` ${icon} ${s.server_name.padEnd(20)} ${source}${detail}`);
3416
3301
  }
3417
3302
  console.log("");
3418
3303
  }
3419
3304
  if (rows.length === 0) {
3420
- console.log(chalk21.yellow(" No MCP servers configured."));
3421
- console.log(chalk21.dim(" Configure servers in ~/.claude/mcp.json or .mcp.json\n"));
3305
+ console.log(chalk20.yellow(" No MCP servers configured."));
3306
+ console.log(chalk20.dim(" Configure servers in ~/.claude/mcp.json or .mcp.json\n"));
3422
3307
  }
3423
- console.log(chalk21.bgYellow.black.bold(" \u26A0 DO NOT CLOSE THIS WINDOW \u2014 it feeds live data to the dashboard \u26A0 "));
3308
+ console.log(chalk20.bgYellow.black.bold(" \u26A0 DO NOT CLOSE THIS WINDOW \u2014 it feeds live data to the dashboard \u26A0 "));
3424
3309
  console.log("");
3425
3310
  }
3426
3311
  function formatUptime(seconds) {
@@ -3450,7 +3335,7 @@ async function mcpWatchCommand() {
3450
3335
  const { data: existingWatchers } = await supabase.from("mcp_watchers").select("pid, tty, cli_version, started_at").eq("device_id", deviceId);
3451
3336
  if (existingWatchers && existingWatchers.length > 0) {
3452
3337
  console.log("");
3453
- console.log(chalk21.yellow(` Replacing ${existingWatchers.length} existing watcher${existingWatchers.length !== 1 ? "s" : ""} on this device...`));
3338
+ console.log(chalk20.yellow(` Replacing ${existingWatchers.length} existing watcher${existingWatchers.length !== 1 ? "s" : ""} on this device...`));
3454
3339
  for (const w of existingWatchers) {
3455
3340
  try {
3456
3341
  if (typeof w.pid === "number" && w.pid > 1 && w.pid !== process.pid) {
@@ -3461,15 +3346,15 @@ async function mcpWatchCommand() {
3461
3346
  }
3462
3347
  await supabase.from("mcp_watchers").delete().eq("device_id", deviceId);
3463
3348
  await new Promise((r) => setTimeout(r, 1e3));
3464
- console.log(chalk21.dim(" Previous watcher stopped.\n"));
3349
+ console.log(chalk20.dim(" Previous watcher stopped.\n"));
3465
3350
  }
3466
3351
  process.stdout.write(`\x1B]0;MCP mon\x07`);
3467
- console.log(chalk21.blue(`Starting MCP monitor for ${deviceName}...`));
3352
+ console.log(chalk20.blue(`Starting MCP monitor for ${deviceName}...`));
3468
3353
  console.log("");
3469
- console.log(chalk21.dim(" View this in the online dashboard at:"));
3470
- console.log(chalk21.cyan(` https://www.md4ai.com/device/${deviceId}`));
3354
+ console.log(chalk20.dim(" View this in the online dashboard at:"));
3355
+ console.log(chalk20.cyan(` https://www.md4ai.com/device/${deviceId}`));
3471
3356
  console.log("");
3472
- console.log(chalk21.yellow(" Please note, closing this window will stop ALL watch reports."));
3357
+ console.log(chalk20.yellow(" Please note, closing this window will stop ALL watch reports."));
3473
3358
  console.log("");
3474
3359
  await supabase.from("mcp_watchers").upsert({
3475
3360
  device_id: deviceId,
@@ -3499,7 +3384,7 @@ async function mcpWatchCommand() {
3499
3384
  return;
3500
3385
  }
3501
3386
  if (deleteError) {
3502
- console.error(chalk21.red(` [debug] Failed to delete old status: ${deleteError.message}`));
3387
+ console.error(chalk20.red(` [debug] Failed to delete old status: ${deleteError.message}`));
3503
3388
  }
3504
3389
  if (rows.length > 0) {
3505
3390
  const { error: insertError } = await supabase.from("mcp_server_status").insert(rows.map((row) => ({
@@ -3512,7 +3397,7 @@ async function mcpWatchCommand() {
3512
3397
  return;
3513
3398
  }
3514
3399
  if (insertError) {
3515
- console.error(chalk21.red(` [debug] Failed to write MCP status: ${insertError.message}`));
3400
+ console.error(chalk20.red(` [debug] Failed to write MCP status: ${insertError.message}`));
3516
3401
  }
3517
3402
  }
3518
3403
  const { error: heartbeatError } = await supabase.from("mcp_watchers").update({ last_heartbeat: now }).eq("device_id", deviceId).eq("pid", myPid);
@@ -3530,11 +3415,11 @@ async function mcpWatchCommand() {
3530
3415
  return;
3531
3416
  }
3532
3417
  jwtRefreshAttempted = true;
3533
- console.log(chalk21.yellow("\n Session token expired \u2014 attempting to refresh..."));
3418
+ console.log(chalk20.yellow("\n Session token expired \u2014 attempting to refresh..."));
3534
3419
  const refreshed = await refreshSession();
3535
3420
  if (refreshed) {
3536
3421
  supabase = refreshed.supabase;
3537
- console.log(chalk21.green(" Session refreshed successfully. Resuming monitoring.\n"));
3422
+ console.log(chalk20.green(" Session refreshed successfully. Resuming monitoring.\n"));
3538
3423
  jwtRefreshAttempted = false;
3539
3424
  await supabase.from("mcp_watchers").upsert({
3540
3425
  device_id: deviceId,
@@ -3552,14 +3437,14 @@ async function mcpWatchCommand() {
3552
3437
  function printSessionExpired() {
3553
3438
  process.stdout.write("\x1B[3J\x1B[2J\x1B[H");
3554
3439
  console.log("");
3555
- console.log(chalk21.bgRed.white.bold(" \u2717 SESSION EXPIRED \u2014 this watcher is no longer sending data to the dashboard \u2717 "));
3440
+ console.log(chalk20.bgRed.white.bold(" \u2717 SESSION EXPIRED \u2014 this watcher is no longer sending data to the dashboard \u2717 "));
3556
3441
  console.log("");
3557
- console.log(chalk21.yellow(" Your authentication token has expired and could not be refreshed."));
3558
- console.log(chalk21.yellow(' The dashboard will show "No watchers" because this process can no longer update it.'));
3442
+ console.log(chalk20.yellow(" Your authentication token has expired and could not be refreshed."));
3443
+ console.log(chalk20.yellow(' The dashboard will show "No watchers" because this process can no longer update it.'));
3559
3444
  console.log("");
3560
- console.log(chalk21.white(" To fix this:"));
3561
- console.log(chalk21.cyan(" 1. Run: md4ai login"));
3562
- console.log(chalk21.cyan(" 2. Then restart the watcher: md4ai mcp-watch"));
3445
+ console.log(chalk20.white(" To fix this:"));
3446
+ console.log(chalk20.cyan(" 1. Run: md4ai login"));
3447
+ console.log(chalk20.cyan(" 2. Then restart the watcher: md4ai mcp-watch"));
3563
3448
  console.log("");
3564
3449
  }
3565
3450
  let interval;
@@ -3602,7 +3487,7 @@ async function mcpWatchCommand() {
3602
3487
  clearInterval(interval);
3603
3488
  clearInterval(envInterval);
3604
3489
  await supabase.from("mcp_watchers").delete().eq("device_id", deviceId).eq("pid", myPid);
3605
- console.log(chalk21.dim("\nMCP monitor stopped."));
3490
+ console.log(chalk20.dim("\nMCP monitor stopped."));
3606
3491
  process.exit(0);
3607
3492
  };
3608
3493
  process.on("SIGINT", () => {
@@ -3695,6 +3580,130 @@ var init_mcp_watch = __esm({
3695
3580
  }
3696
3581
  });
3697
3582
 
3583
+ // dist/commands/sync.js
3584
+ var sync_exports = {};
3585
+ __export(sync_exports, {
3586
+ syncCommand: () => syncCommand
3587
+ });
3588
+ import { resolve as resolve9 } from "node:path";
3589
+ import { existsSync as existsSync14 } from "node:fs";
3590
+ import chalk22 from "chalk";
3591
+ function isValidProjectPath(p) {
3592
+ const resolved = resolve9(p);
3593
+ return resolved.startsWith("/") && !resolved.includes("..") && existsSync14(resolved);
3594
+ }
3595
+ async function syncCommand(options) {
3596
+ const { supabase, userId } = await getAuthenticatedClient();
3597
+ if (options.all) {
3598
+ const currentDeviceName = detectDeviceName();
3599
+ const { data: devices, error } = await supabase.from("device_paths").select("folder_id, device_name, path").eq("device_name", currentDeviceName);
3600
+ if (error || !devices?.length) {
3601
+ console.log(chalk22.yellow(`
3602
+ No projects linked on ${currentDeviceName} yet.
3603
+ `));
3604
+ console.log(chalk22.dim(" To link a project, cd into its folder and run:"));
3605
+ console.log(chalk22.cyan(" md4ai scan"));
3606
+ console.log(chalk22.dim(" Or link an existing project from the dashboard:"));
3607
+ console.log(chalk22.cyan(" md4ai link <project-id>\n"));
3608
+ return;
3609
+ }
3610
+ for (const device of devices) {
3611
+ if (!isValidProjectPath(device.path)) {
3612
+ console.error(chalk22.red(` Skipping invalid path: ${device.path}`));
3613
+ continue;
3614
+ }
3615
+ console.log(chalk22.blue(`Syncing: ${device.path}`));
3616
+ const { data: proposedAll } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
3617
+ if (proposedAll?.length) {
3618
+ console.log(chalk22.yellow(` ${proposedAll.length} file(s) proposed for deletion \u2014 run \`md4ai scan\` to review.`));
3619
+ }
3620
+ try {
3621
+ const result = await scanProject(device.path, device.folder_id);
3622
+ const { data: allDeviceRow } = await supabase.from("devices").select("id").eq("user_id", userId).eq("device_name", device.device_name).single();
3623
+ const allDeviceId = allDeviceRow?.id;
3624
+ if (allDeviceId) {
3625
+ await supabase.from("device_scans").upsert({
3626
+ folder_id: device.folder_id,
3627
+ device_id: allDeviceId,
3628
+ user_id: userId,
3629
+ graph_json: result.graph,
3630
+ orphans_json: result.orphans,
3631
+ skills_table_json: result.skills,
3632
+ stale_files_json: result.staleFiles,
3633
+ broken_refs_json: result.brokenRefs,
3634
+ env_manifest_json: result.envManifest,
3635
+ plugin_versions_json: result.pluginVersions,
3636
+ data_hash: result.dataHash,
3637
+ scanned_at: result.scannedAt,
3638
+ cli_version: CURRENT_VERSION
3639
+ }, { onConflict: "folder_id,device_id" });
3640
+ }
3641
+ await pushToolings(supabase, device.folder_id, result.toolings, allDeviceId);
3642
+ await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
3643
+ console.log(chalk22.green(` Done: ${device.device_name}`));
3644
+ } catch (err) {
3645
+ console.error(chalk22.red(` Failed: ${device.path}: ${err}`));
3646
+ }
3647
+ }
3648
+ await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
3649
+ console.log(chalk22.green("\nAll devices synced."));
3650
+ } else {
3651
+ const state = await loadState();
3652
+ if (!state.lastFolderId) {
3653
+ console.error(chalk22.yellow("No recent sync. Use: md4ai sync --all, or md4ai scan <path> first."));
3654
+ process.exit(1);
3655
+ }
3656
+ const { data: device } = await supabase.from("device_paths").select("folder_id, device_name, path").eq("folder_id", state.lastFolderId).eq("device_name", state.lastDeviceName).single();
3657
+ if (!device) {
3658
+ console.error(chalk22.red("Could not find last synced device/folder."));
3659
+ process.exit(1);
3660
+ }
3661
+ if (!isValidProjectPath(device.path)) {
3662
+ console.error(chalk22.red(`Invalid project path: ${device.path}`));
3663
+ process.exit(1);
3664
+ }
3665
+ console.log(chalk22.blue(`Syncing: ${device.path}`));
3666
+ const { data: proposedSingle } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
3667
+ if (proposedSingle?.length) {
3668
+ console.log(chalk22.yellow(` ${proposedSingle.length} file(s) proposed for deletion \u2014 run \`md4ai scan\` to review.`));
3669
+ }
3670
+ const result = await scanProject(device.path, device.folder_id);
3671
+ const { data: singleDeviceRow } = await supabase.from("devices").select("id").eq("user_id", userId).eq("device_name", device.device_name).single();
3672
+ const singleDeviceId = singleDeviceRow?.id;
3673
+ if (singleDeviceId) {
3674
+ await supabase.from("device_scans").upsert({
3675
+ folder_id: device.folder_id,
3676
+ device_id: singleDeviceId,
3677
+ user_id: userId,
3678
+ graph_json: result.graph,
3679
+ orphans_json: result.orphans,
3680
+ skills_table_json: result.skills,
3681
+ stale_files_json: result.staleFiles,
3682
+ broken_refs_json: result.brokenRefs,
3683
+ plugin_versions_json: result.pluginVersions,
3684
+ data_hash: result.dataHash,
3685
+ scanned_at: result.scannedAt,
3686
+ cli_version: CURRENT_VERSION
3687
+ }, { onConflict: "folder_id,device_id" });
3688
+ }
3689
+ await pushToolings(supabase, device.folder_id, result.toolings, singleDeviceId);
3690
+ await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
3691
+ await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
3692
+ console.log(chalk22.green("Synced."));
3693
+ }
3694
+ }
3695
+ var init_sync = __esm({
3696
+ "dist/commands/sync.js"() {
3697
+ "use strict";
3698
+ init_auth();
3699
+ init_config();
3700
+ init_scanner();
3701
+ init_push_toolings();
3702
+ init_check_update();
3703
+ init_device_utils();
3704
+ }
3705
+ });
3706
+
3698
3707
  // dist/index.js
3699
3708
  import { Command } from "commander";
3700
3709
 
@@ -4059,9 +4068,6 @@ function generatePrintHtml(result, title) {
4059
4068
  </html>`;
4060
4069
  }
4061
4070
 
4062
- // dist/index.js
4063
- init_sync();
4064
-
4065
4071
  // dist/commands/link.js
4066
4072
  init_auth();
4067
4073
  init_config();
@@ -4070,7 +4076,7 @@ init_push_toolings();
4070
4076
  init_device_utils();
4071
4077
  init_check_update();
4072
4078
  import { resolve as resolve6 } from "node:path";
4073
- import chalk16 from "chalk";
4079
+ import chalk15 from "chalk";
4074
4080
  async function linkCommand(projectId) {
4075
4081
  const shouldContinue = await promptUpdateIfAvailable(["link", projectId]);
4076
4082
  if (!shouldContinue)
@@ -4081,11 +4087,11 @@ async function linkCommand(projectId) {
4081
4087
  const osType = detectOs();
4082
4088
  const { data: folder, error: folderErr } = await supabase.from("claude_folders").select("id, name").eq("id", projectId).single();
4083
4089
  if (folderErr || !folder) {
4084
- console.error(chalk16.red("Project not found, or you do not have access."));
4085
- console.error(chalk16.yellow("Check the project ID in the MD4AI web dashboard."));
4090
+ console.error(chalk15.red("Project not found, or you do not have access."));
4091
+ console.error(chalk15.yellow("Check the project ID in the MD4AI web dashboard."));
4086
4092
  process.exit(1);
4087
4093
  }
4088
- console.log(chalk16.blue(`
4094
+ console.log(chalk15.blue(`
4089
4095
  Linking "${folder.name}" to this device...
4090
4096
  `));
4091
4097
  console.log(` Project: ${folder.name}`);
@@ -4111,12 +4117,12 @@ Linking "${folder.name}" to this device...
4111
4117
  path: cwd
4112
4118
  });
4113
4119
  if (pathErr) {
4114
- console.error(chalk16.red(`Failed to link: ${pathErr.message}`));
4120
+ console.error(chalk15.red(`Failed to link: ${pathErr.message}`));
4115
4121
  process.exit(1);
4116
4122
  }
4117
4123
  }
4118
- console.log(chalk16.green("\nLinked successfully."));
4119
- console.log(chalk16.blue("\nRunning initial scan...\n"));
4124
+ console.log(chalk15.green("\nLinked successfully."));
4125
+ console.log(chalk15.blue("\nRunning initial scan...\n"));
4120
4126
  const result = await scanProject(cwd, folder.id);
4121
4127
  console.log(` Files: ${result.graph.nodes.length}`);
4122
4128
  console.log(` References:${result.graph.edges.length}`);
@@ -4157,7 +4163,7 @@ Linking "${folder.name}" to this device...
4157
4163
  device_id: deviceId
4158
4164
  }, { onConflict: "folder_id,file_path,device_id" });
4159
4165
  }
4160
- console.log(chalk16.green(` Uploaded ${configFiles.length} config file(s).`));
4166
+ console.log(chalk15.green(` Uploaded ${configFiles.length} config file(s).`));
4161
4167
  }
4162
4168
  await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder.id).eq("device_name", deviceName);
4163
4169
  await saveState({
@@ -4166,28 +4172,28 @@ Linking "${folder.name}" to this device...
4166
4172
  lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
4167
4173
  });
4168
4174
  const projectUrl = `https://www.md4ai.com/project/${folder.id}`;
4169
- console.log(chalk16.green("\nDone! Project linked and scanned."));
4170
- console.log(chalk16.cyan(`
4175
+ console.log(chalk15.green("\nDone! Project linked and scanned."));
4176
+ console.log(chalk15.cyan(`
4171
4177
  ${projectUrl}
4172
4178
  `));
4173
- console.log(chalk16.grey('Run "md4ai scan" to rescan at any time.'));
4179
+ console.log(chalk15.grey('Run "md4ai scan" to rescan at any time.'));
4174
4180
  }
4175
4181
 
4176
4182
  // dist/commands/import-bundle.js
4177
4183
  import { readFile as readFile11, writeFile as writeFile4, mkdir as mkdir3, stat as fsStat } from "node:fs/promises";
4178
4184
  import { dirname as dirname3, resolve as resolve7 } from "node:path";
4179
- import { existsSync as existsSync12 } from "node:fs";
4180
- import chalk17 from "chalk";
4185
+ import { existsSync as existsSync11 } from "node:fs";
4186
+ import chalk16 from "chalk";
4181
4187
  import { confirm as confirm3, input as input6 } from "@inquirer/prompts";
4182
4188
  var MAX_BUNDLE_SIZE_BYTES = 50 * 1024 * 1024;
4183
4189
  async function importBundleCommand(zipPath) {
4184
- if (!existsSync12(zipPath)) {
4185
- console.error(chalk17.red(`File not found: ${zipPath}`));
4190
+ if (!existsSync11(zipPath)) {
4191
+ console.error(chalk16.red(`File not found: ${zipPath}`));
4186
4192
  process.exit(1);
4187
4193
  }
4188
4194
  const fileStat = await fsStat(zipPath);
4189
4195
  if (fileStat.size > MAX_BUNDLE_SIZE_BYTES) {
4190
- console.error(chalk17.red(`Bundle too large (${Math.round(fileStat.size / 1024 / 1024)} MB). Maximum is 50 MB.`));
4196
+ console.error(chalk16.red(`Bundle too large (${Math.round(fileStat.size / 1024 / 1024)} MB). Maximum is 50 MB.`));
4191
4197
  process.exit(1);
4192
4198
  }
4193
4199
  const JSZip = (await import("jszip")).default;
@@ -4195,11 +4201,11 @@ async function importBundleCommand(zipPath) {
4195
4201
  const zip = await JSZip.loadAsync(zipData);
4196
4202
  const manifestFile = zip.file("manifest.json");
4197
4203
  if (!manifestFile) {
4198
- console.error(chalk17.red("Invalid bundle: missing manifest.json"));
4204
+ console.error(chalk16.red("Invalid bundle: missing manifest.json"));
4199
4205
  process.exit(1);
4200
4206
  }
4201
4207
  const manifest = JSON.parse(await manifestFile.async("string"));
4202
- console.log(chalk17.blue(`
4208
+ console.log(chalk16.blue(`
4203
4209
  Bundle: ${manifest.folderName}`));
4204
4210
  console.log(` Exported by: ${manifest.ownerEmail}`);
4205
4211
  console.log(` Exported at: ${manifest.exportedAt}`);
@@ -4213,10 +4219,10 @@ Bundle: ${manifest.folderName}`));
4213
4219
  }
4214
4220
  }
4215
4221
  if (files.length === 0) {
4216
- console.log(chalk17.yellow("No Claude config files found in bundle."));
4222
+ console.log(chalk16.yellow("No Claude config files found in bundle."));
4217
4223
  return;
4218
4224
  }
4219
- console.log(chalk17.blue(`
4225
+ console.log(chalk16.blue(`
4220
4226
  Files to extract:`));
4221
4227
  for (const f of files) {
4222
4228
  console.log(` ${f.filePath}`);
@@ -4229,39 +4235,39 @@ Files to extract:`));
4229
4235
  message: `Extract ${files.length} file(s) to ${targetDir}?`
4230
4236
  });
4231
4237
  if (!proceed) {
4232
- console.log(chalk17.yellow("Cancelled."));
4238
+ console.log(chalk16.yellow("Cancelled."));
4233
4239
  return;
4234
4240
  }
4235
4241
  const resolvedTarget = resolve7(targetDir);
4236
4242
  for (const file of files) {
4237
4243
  const fullPath = resolve7(targetDir, file.filePath);
4238
4244
  if (!fullPath.startsWith(resolvedTarget + "/") && fullPath !== resolvedTarget) {
4239
- console.error(chalk17.red(` Blocked path traversal: ${file.filePath}`));
4245
+ console.error(chalk16.red(` Blocked path traversal: ${file.filePath}`));
4240
4246
  continue;
4241
4247
  }
4242
4248
  const dir = dirname3(fullPath);
4243
- if (!existsSync12(dir)) {
4249
+ if (!existsSync11(dir)) {
4244
4250
  await mkdir3(dir, { recursive: true });
4245
4251
  }
4246
4252
  await writeFile4(fullPath, file.content, "utf-8");
4247
- console.log(chalk17.green(` \u2713 ${file.filePath}`));
4253
+ console.log(chalk16.green(` \u2713 ${file.filePath}`));
4248
4254
  }
4249
- console.log(chalk17.green(`
4255
+ console.log(chalk16.green(`
4250
4256
  Done! ${files.length} file(s) extracted to ${targetDir}`));
4251
4257
  }
4252
4258
 
4253
4259
  // dist/commands/admin-update-tool.js
4254
4260
  init_auth();
4255
- import chalk18 from "chalk";
4261
+ import chalk17 from "chalk";
4256
4262
  var VALID_CATEGORIES = ["framework", "runtime", "cli", "mcp", "package", "database", "other"];
4257
4263
  async function adminUpdateToolCommand(options) {
4258
4264
  const { supabase } = await getAuthenticatedClient();
4259
4265
  if (!options.name) {
4260
- console.error(chalk18.red("--name is required."));
4266
+ console.error(chalk17.red("--name is required."));
4261
4267
  process.exit(1);
4262
4268
  }
4263
4269
  if (options.category && !VALID_CATEGORIES.includes(options.category)) {
4264
- console.error(chalk18.red(`Invalid category. Must be one of: ${VALID_CATEGORIES.join(", ")}`));
4270
+ console.error(chalk17.red(`Invalid category. Must be one of: ${VALID_CATEGORIES.join(", ")}`));
4265
4271
  process.exit(1);
4266
4272
  }
4267
4273
  const { data: existing } = await supabase.from("tools_registry").select("id, name, display_name, category").eq("name", options.name).maybeSingle();
@@ -4283,13 +4289,13 @@ async function adminUpdateToolCommand(options) {
4283
4289
  updates.notes = options.notes;
4284
4290
  const { error } = await supabase.from("tools_registry").update(updates).eq("id", existing.id);
4285
4291
  if (error) {
4286
- console.error(chalk18.red(`Failed to update: ${error.message}`));
4292
+ console.error(chalk17.red(`Failed to update: ${error.message}`));
4287
4293
  process.exit(1);
4288
4294
  }
4289
- console.log(chalk18.green(`Updated "${existing.display_name}" in the registry.`));
4295
+ console.log(chalk17.green(`Updated "${existing.display_name}" in the registry.`));
4290
4296
  } else {
4291
4297
  if (!options.display || !options.category) {
4292
- console.error(chalk18.red("New tools require --display and --category."));
4298
+ console.error(chalk17.red("New tools require --display and --category."));
4293
4299
  process.exit(1);
4294
4300
  }
4295
4301
  const { error } = await supabase.from("tools_registry").insert({
@@ -4303,25 +4309,25 @@ async function adminUpdateToolCommand(options) {
4303
4309
  notes: options.notes ?? null
4304
4310
  });
4305
4311
  if (error) {
4306
- console.error(chalk18.red(`Failed to create: ${error.message}`));
4312
+ console.error(chalk17.red(`Failed to create: ${error.message}`));
4307
4313
  process.exit(1);
4308
4314
  }
4309
- console.log(chalk18.green(`Added "${options.display}" to the registry.`));
4315
+ console.log(chalk17.green(`Added "${options.display}" to the registry.`));
4310
4316
  }
4311
4317
  }
4312
4318
 
4313
4319
  // dist/commands/admin-list-tools.js
4314
4320
  init_auth();
4315
- import chalk19 from "chalk";
4321
+ import chalk18 from "chalk";
4316
4322
  async function adminListToolsCommand() {
4317
4323
  const { supabase } = await getAuthenticatedClient();
4318
4324
  const { data: tools, error } = await supabase.from("tools_registry").select("*").order("category").order("display_name");
4319
4325
  if (error) {
4320
- console.error(chalk19.red(`Failed to fetch tools: ${error.message}`));
4326
+ console.error(chalk18.red(`Failed to fetch tools: ${error.message}`));
4321
4327
  process.exit(1);
4322
4328
  }
4323
4329
  if (!tools?.length) {
4324
- console.log(chalk19.yellow("No tools in the registry."));
4330
+ console.log(chalk18.yellow("No tools in the registry."));
4325
4331
  return;
4326
4332
  }
4327
4333
  const nameW = Math.max(16, ...tools.map((t) => t.display_name.length));
@@ -4335,7 +4341,7 @@ async function adminListToolsCommand() {
4335
4341
  "Beta".padEnd(betaW),
4336
4342
  "Last Checked"
4337
4343
  ].join(" ");
4338
- console.log(chalk19.bold(header));
4344
+ console.log(chalk18.bold(header));
4339
4345
  console.log("\u2500".repeat(header.length));
4340
4346
  for (const tool of tools) {
4341
4347
  const lastChecked = tool.updated_at ? formatRelative(new Date(tool.updated_at)) : "\u2014";
@@ -4348,7 +4354,7 @@ async function adminListToolsCommand() {
4348
4354
  ].join(" ");
4349
4355
  console.log(row);
4350
4356
  }
4351
- console.log(chalk19.grey(`
4357
+ console.log(chalk18.grey(`
4352
4358
  ${tools.length} tool(s) in registry.`));
4353
4359
  }
4354
4360
  function formatRelative(date) {
@@ -4366,7 +4372,7 @@ function formatRelative(date) {
4366
4372
 
4367
4373
  // dist/commands/admin-fetch-versions.js
4368
4374
  init_auth();
4369
- import chalk20 from "chalk";
4375
+ import chalk19 from "chalk";
4370
4376
 
4371
4377
  // dist/commands/version-sources.js
4372
4378
  var VERSION_SOURCES = {
@@ -4403,11 +4409,11 @@ async function adminFetchVersionsCommand() {
4403
4409
  const { supabase } = await getAuthenticatedClient();
4404
4410
  const { data: tools, error } = await supabase.from("tools_registry").select("*").order("display_name");
4405
4411
  if (error) {
4406
- console.error(chalk20.red(`Failed to fetch tools: ${error.message}`));
4412
+ console.error(chalk19.red(`Failed to fetch tools: ${error.message}`));
4407
4413
  process.exit(1);
4408
4414
  }
4409
4415
  if (!tools?.length) {
4410
- console.log(chalk20.yellow("No tools in the registry."));
4416
+ console.log(chalk19.yellow("No tools in the registry."));
4411
4417
  }
4412
4418
  const { data: allProjectToolings } = await supabase.from("project_toolings").select("tool_name, detection_source").is("tool_id", null);
4413
4419
  const registeredNames = new Set((tools ?? []).map((t) => t.name));
@@ -4418,7 +4424,7 @@ async function adminFetchVersionsCommand() {
4418
4424
  }
4419
4425
  }
4420
4426
  if (unregisteredNames.size > 0) {
4421
- console.log(chalk20.blue(`Auto-registering ${unregisteredNames.size} unverified package(s)...
4427
+ console.log(chalk19.blue(`Auto-registering ${unregisteredNames.size} unverified package(s)...
4422
4428
  `));
4423
4429
  for (const name of unregisteredNames) {
4424
4430
  const displayName = name;
@@ -4436,10 +4442,10 @@ async function adminFetchVersionsCommand() {
4436
4442
  }
4437
4443
  }
4438
4444
  if (!tools?.length) {
4439
- console.log(chalk20.yellow("No tools to fetch."));
4445
+ console.log(chalk19.yellow("No tools to fetch."));
4440
4446
  return;
4441
4447
  }
4442
- console.log(chalk20.bold(`Fetching versions for ${tools.length} tool(s)...
4448
+ console.log(chalk19.bold(`Fetching versions for ${tools.length} tool(s)...
4443
4449
  `));
4444
4450
  const results = await Promise.all(tools.map(async (tool) => {
4445
4451
  const source = VERSION_SOURCES[tool.name] ?? { type: "npm", package: tool.name };
@@ -4478,10 +4484,10 @@ async function adminFetchVersionsCommand() {
4478
4484
  "Beta".padEnd(betaW),
4479
4485
  "Status".padEnd(statusW)
4480
4486
  ].join(" ");
4481
- console.log(chalk20.bold(header));
4487
+ console.log(chalk19.bold(header));
4482
4488
  console.log("\u2500".repeat(header.length));
4483
4489
  for (const result of results) {
4484
- const statusColour = result.status === "updated" ? chalk20.green : result.status === "unchanged" ? chalk20.grey : result.status === "failed" ? chalk20.red : chalk20.yellow;
4490
+ const statusColour = result.status === "updated" ? chalk19.green : result.status === "unchanged" ? chalk19.grey : result.status === "failed" ? chalk19.red : chalk19.yellow;
4485
4491
  const row = [
4486
4492
  result.displayName.padEnd(nameW),
4487
4493
  (result.stable ?? "\u2014").padEnd(stableW),
@@ -4494,8 +4500,8 @@ async function adminFetchVersionsCommand() {
4494
4500
  const unchanged = results.filter((r) => r.status === "unchanged").length;
4495
4501
  const failed = results.filter((r) => r.status === "failed").length;
4496
4502
  const noSource = results.filter((r) => r.status === "no source").length;
4497
- console.log(chalk20.grey(`
4498
- ${results.length} tool(s): `) + chalk20.green(`${updated} updated`) + ", " + chalk20.grey(`${unchanged} unchanged`) + ", " + chalk20.red(`${failed} failed`) + ", " + chalk20.yellow(`${noSource} no source`));
4503
+ console.log(chalk19.grey(`
4504
+ ${results.length} tool(s): `) + chalk19.green(`${updated} updated`) + ", " + chalk19.grey(`${unchanged} unchanged`) + ", " + chalk19.red(`${failed} failed`) + ", " + chalk19.yellow(`${noSource} no source`));
4499
4505
  }
4500
4506
  async function fetchVersions(source) {
4501
4507
  const controller = new AbortController();
@@ -4555,8 +4561,8 @@ init_mcp_watch();
4555
4561
  // dist/commands/init-manifest.js
4556
4562
  import { resolve as resolve8, join as join16, relative as relative4, dirname as dirname4 } from "node:path";
4557
4563
  import { readFile as readFile13, writeFile as writeFile5, mkdir as mkdir4 } from "node:fs/promises";
4558
- import { existsSync as existsSync14 } from "node:fs";
4559
- import chalk22 from "chalk";
4564
+ import { existsSync as existsSync13 } from "node:fs";
4565
+ import chalk21 from "chalk";
4560
4566
  var SECRET_PATTERNS = [
4561
4567
  /_KEY$/i,
4562
4568
  /_SECRET$/i,
@@ -4573,7 +4579,7 @@ async function discoverEnvFiles(projectRoot) {
4573
4579
  const apps = [];
4574
4580
  for (const envName of [".env", ".env.local", ".env.example"]) {
4575
4581
  const envPath = join16(projectRoot, envName);
4576
- if (existsSync14(envPath)) {
4582
+ if (existsSync13(envPath)) {
4577
4583
  const vars = await extractVarNames(envPath);
4578
4584
  if (vars.length > 0) {
4579
4585
  apps.push({ name: "root", envFilePath: envName, vars });
@@ -4584,11 +4590,11 @@ async function discoverEnvFiles(projectRoot) {
4584
4590
  const subdirs = ["web", "cli", "api", "app", "server", "packages"];
4585
4591
  for (const sub of subdirs) {
4586
4592
  const subDir = join16(projectRoot, sub);
4587
- if (!existsSync14(subDir))
4593
+ if (!existsSync13(subDir))
4588
4594
  continue;
4589
4595
  for (const envName of [".env.local", ".env", ".env.example"]) {
4590
4596
  const envPath = join16(subDir, envName);
4591
- if (existsSync14(envPath)) {
4597
+ if (existsSync13(envPath)) {
4592
4598
  const vars = await extractVarNames(envPath);
4593
4599
  if (vars.length > 0) {
4594
4600
  apps.push({
@@ -4650,31 +4656,31 @@ function generateManifest(apps) {
4650
4656
  async function initManifestCommand() {
4651
4657
  const projectRoot = resolve8(process.cwd());
4652
4658
  const manifestPath = join16(projectRoot, "docs", "reference", "env-manifest.md");
4653
- if (existsSync14(manifestPath)) {
4654
- console.log(chalk22.yellow(`Manifest already exists: ${relative4(projectRoot, manifestPath)}`));
4655
- console.log(chalk22.dim("Edit it directly to make changes."));
4659
+ if (existsSync13(manifestPath)) {
4660
+ console.log(chalk21.yellow(`Manifest already exists: ${relative4(projectRoot, manifestPath)}`));
4661
+ console.log(chalk21.dim("Edit it directly to make changes."));
4656
4662
  return;
4657
4663
  }
4658
- console.log(chalk22.blue("Scanning for .env files...\n"));
4664
+ console.log(chalk21.blue("Scanning for .env files...\n"));
4659
4665
  const apps = await discoverEnvFiles(projectRoot);
4660
4666
  if (apps.length === 0) {
4661
- console.log(chalk22.yellow("No .env files found. Create a manifest manually at:"));
4662
- console.log(chalk22.cyan(` docs/reference/env-manifest.md`));
4667
+ console.log(chalk21.yellow("No .env files found. Create a manifest manually at:"));
4668
+ console.log(chalk21.cyan(` docs/reference/env-manifest.md`));
4663
4669
  return;
4664
4670
  }
4665
4671
  for (const app of apps) {
4666
- console.log(` ${chalk22.green(app.name)}: ${app.envFilePath} (${app.vars.length} vars)`);
4672
+ console.log(` ${chalk21.green(app.name)}: ${app.envFilePath} (${app.vars.length} vars)`);
4667
4673
  }
4668
4674
  const content = generateManifest(apps);
4669
4675
  const dir = dirname4(manifestPath);
4670
- if (!existsSync14(dir)) {
4676
+ if (!existsSync13(dir)) {
4671
4677
  await mkdir4(dir, { recursive: true });
4672
4678
  }
4673
4679
  await writeFile5(manifestPath, content, "utf-8");
4674
- console.log(chalk22.green(`
4680
+ console.log(chalk21.green(`
4675
4681
  Manifest created: ${relative4(projectRoot, manifestPath)}`));
4676
- console.log(chalk22.dim("Review and edit the file \u2014 it is your source of truth."));
4677
- console.log(chalk22.dim("Then run `md4ai scan` to verify against your environments."));
4682
+ console.log(chalk21.dim("Review and edit the file \u2014 it is your source of truth."));
4683
+ console.log(chalk21.dim("Then run `md4ai scan` to verify against your environments."));
4678
4684
  }
4679
4685
 
4680
4686
  // dist/commands/update.js
@@ -4857,7 +4863,7 @@ init_check_update();
4857
4863
  init_map();
4858
4864
  init_mcp_watch();
4859
4865
  import chalk24 from "chalk";
4860
- import { resolve as resolve9 } from "node:path";
4866
+ import { resolve as resolve10 } from "node:path";
4861
4867
  async function fetchLatestVersion2() {
4862
4868
  try {
4863
4869
  const controller = new AbortController();
@@ -4886,7 +4892,7 @@ function isNewer3(a, b) {
4886
4892
  return false;
4887
4893
  }
4888
4894
  async function startCommand() {
4889
- const projectRoot = resolve9(process.cwd());
4895
+ const projectRoot = resolve10(process.cwd());
4890
4896
  console.log("");
4891
4897
  console.log(chalk24.bold.cyan(` MD4AI v${CURRENT_VERSION}`));
4892
4898
  console.log(chalk24.dim(` ${projectRoot}`));
@@ -5083,32 +5089,39 @@ init_check_update();
5083
5089
  var program = new Command();
5084
5090
  program.name("md4ai").description("MD4AI \u2014 Claude tooling visualiser").version(CURRENT_VERSION).addHelpText("after", `
5085
5091
  Quick start:
5086
- md4ai start Check for updates, scan project, and start MCP monitoring
5087
- md4ai Same as md4ai start (when run in a linked project folder)`);
5092
+ md4ai login Log in with email and password
5093
+ md4ai link <project-id> Link this folder to a dashboard project
5094
+ md4ai scan Scan project and push to dashboard
5095
+
5096
+ Daily use:
5097
+ md4ai Scan + start MCP monitoring (all-in-one)
5098
+
5099
+ More:
5100
+ md4ai config ... Configuration (tokens, settings)
5101
+ md4ai doppler ... Doppler secrets integration
5102
+ md4ai admin ... Advanced tools (simulate, print, import, etc.)`);
5088
5103
  program.command("start").description("Check for updates, scan project, and start MCP monitoring \u2014 the all-in-one command").action(startCommand);
5089
5104
  program.command("login").description("Log in to MD4AI with email and password").action(loginCommand);
5090
5105
  program.command("logout").description("Log out and clear stored credentials").action(logoutCommand);
5091
5106
  program.command("status").description("Show login status, device count, folder count, last sync").action(statusCommand);
5092
- program.command("add-folder").description("Create a new Claude folder").action(addFolderCommand);
5093
- program.command("add-device").description("Add a device path to a Claude folder").action(addDeviceCommand);
5094
- program.command("list-devices").description("List all devices and their linked folders").action(listDevicesCommand);
5095
5107
  program.command("link <project-id>").description("Link the current directory to a project created in the web dashboard").action(linkCommand);
5096
5108
  program.command("scan [path]").description("Scan Claude project files and push results to your dashboard").option("--offline", "Skip pushing to Supabase").action(mapCommand);
5097
- program.command("simulate <prompt>").description("Show which files Claude would load for a given prompt").action(simulateCommand);
5098
- program.command("print <title>").description("Generate a printable wall-chart HTML from the last scan").action(printCommand);
5099
- program.command("sync").description("Re-push latest scan data to Supabase").option("--all", "Sync all folders on all devices").action(syncCommand);
5100
- program.command("import <zipfile>").description("Import a Claude setup bundle exported from the web dashboard").action(importBundleCommand);
5101
- program.command("update").description("Check for updates, install, and optionally rescan and start watching").option("--post-update", "Internal: run post-update flow (called by the update command)").action(updateCommand);
5102
- program.command("check-update").description("Check if a newer version of md4ai is available").action(checkForUpdate);
5103
5109
  program.command("mcp-watch").description("Monitor MCP server status on this device (runs until Ctrl+C)").action(mcpWatchCommand);
5104
- program.command("init-manifest").description("Scaffold a starter env-manifest.md from detected .env files").action(initManifestCommand);
5110
+ program.command("update").description("Check for updates, install, and optionally rescan and start watching").option("--post-update", "Internal: run post-update flow (called by the update command)").action(updateCommand);
5105
5111
  var config = program.command("config").description("Manage CLI configuration");
5106
5112
  config.command("set <key> <value>").description("Set a configuration value (e.g. vercel-token)").action(configSetCommand);
5107
5113
  var doppler = program.command("doppler").description("Manage Doppler integration");
5108
5114
  doppler.command("connect").description("Configure Doppler token for secrets scanning").action(dopplerConnectCommand);
5109
5115
  doppler.command("disconnect").description("Remove stored Doppler token").action(dopplerDisconnectCommand);
5110
5116
  doppler.command("set-project <slug>").description("Override Doppler project slug for the current linked project").action(dopplerSetProjectCommand);
5111
- var admin = program.command("admin").description("Admin commands for managing the tools registry");
5117
+ var admin = program.command("admin").description("Advanced tools \u2014 simulate, print, import, device management, registry");
5118
+ admin.command("simulate <prompt>").description("Show which files Claude would load for a given prompt").action(simulateCommand);
5119
+ admin.command("print <title>").description("Generate a printable wall-chart HTML from the last scan").action(printCommand);
5120
+ admin.command("import <zipfile>").description("Import a Claude setup bundle exported from the web dashboard").action(importBundleCommand);
5121
+ admin.command("init-manifest").description("Scaffold a starter env-manifest.md from detected .env files").action(initManifestCommand);
5122
+ admin.command("list-devices").description("List all devices and their linked folders").action(listDevicesCommand);
5123
+ admin.command("add-folder").description("Create a new Claude folder").action(addFolderCommand);
5124
+ admin.command("add-device").description("Add a device path to a Claude folder").action(addDeviceCommand);
5112
5125
  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);
5113
5126
  admin.command("list-tools").description("List all tools in the master registry").action(adminListToolsCommand);
5114
5127
  admin.command("fetch-versions").description("Fetch latest stable and beta versions from npm and GitHub").action(adminFetchVersionsCommand);
@@ -5118,7 +5131,7 @@ if (process.argv.length <= 2) {
5118
5131
  } else {
5119
5132
  program.parseAsync().then(() => {
5120
5133
  const ran = program.args[0];
5121
- const skipAutoCheck = ["update", "check-update", "mcp-watch", "start"];
5134
+ const skipAutoCheck = ["update", "mcp-watch", "start"];
5122
5135
  if (!skipAutoCheck.includes(ran)) {
5123
5136
  autoCheckForUpdate();
5124
5137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md4ai",
3
- "version": "0.17.3",
3
+ "version": "0.18.1",
4
4
  "description": "CLI for MD4AI — scan Claude projects and sync to your dashboard",
5
5
  "type": "module",
6
6
  "bin": {