md4ai 0.17.2 → 0.18.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 +330 -281
  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.2" : "0.0.0-dev";
115
+ CURRENT_VERSION = true ? "0.18.0" : "0.0.0-dev";
126
116
  }
127
117
  });
128
118
 
@@ -365,14 +355,26 @@ import chalk5 from "chalk";
365
355
  import { confirm, input as input2, password as password2 } from "@inquirer/prompts";
366
356
  async function getAuthenticatedClient() {
367
357
  const creds = await loadCredentials();
368
- const needsLogin = !creds?.accessToken || Date.now() > creds.expiresAt;
369
- if (needsLogin) {
370
- const reason = !creds?.accessToken ? "Not logged in." : "Session expired.";
371
- console.log(chalk5.yellow(`${reason}`));
358
+ if (!creds?.accessToken) {
359
+ console.log(chalk5.yellow("Not logged in."));
372
360
  const shouldLogin = await confirm({ message: "Would you like to log in now?" });
373
- if (!shouldLogin) {
361
+ if (!shouldLogin)
374
362
  process.exit(0);
363
+ return promptLogin();
364
+ }
365
+ if (creds.refreshToken) {
366
+ const refreshed = await refreshSession();
367
+ if (refreshed) {
368
+ updateDeviceCliVersion(refreshed.supabase, refreshed.userId).catch(() => {
369
+ });
370
+ return refreshed;
375
371
  }
372
+ }
373
+ if (Date.now() > creds.expiresAt) {
374
+ console.log(chalk5.yellow("Session expired."));
375
+ const shouldLogin = await confirm({ message: "Would you like to log in now?" });
376
+ if (!shouldLogin)
377
+ process.exit(0);
376
378
  return promptLogin();
377
379
  }
378
380
  const anonKey = getAnonKey();
@@ -2069,6 +2071,11 @@ var init_doppler_scanner = __esm({
2069
2071
  });
2070
2072
 
2071
2073
  // dist/scanner/index.js
2074
+ var scanner_exports = {};
2075
+ __export(scanner_exports, {
2076
+ readClaudeConfigFiles: () => readClaudeConfigFiles,
2077
+ scanProject: () => scanProject
2078
+ });
2072
2079
  import { readdir as readdir5 } from "node:fs/promises";
2073
2080
  import { join as join12, relative as relative3 } from "node:path";
2074
2081
  import { existsSync as existsSync7 } from "node:fs";
@@ -2619,7 +2626,7 @@ ${proposedFiles.length} file(s) proposed for deletion:
2619
2626
  }
2620
2627
  }
2621
2628
  if (deviceId) {
2622
- await supabase.from("device_scans").upsert({
2629
+ const scanPayload = {
2623
2630
  folder_id,
2624
2631
  device_id: deviceId,
2625
2632
  user_id: userId,
@@ -2635,7 +2642,14 @@ ${proposedFiles.length} file(s) proposed for deletion:
2635
2642
  data_hash: result.dataHash,
2636
2643
  scanned_at: result.scannedAt,
2637
2644
  cli_version: CURRENT_VERSION
2638
- }, { onConflict: "folder_id,device_id" });
2645
+ };
2646
+ const { data: updated, error: updateErr } = await supabase.from("device_scans").update(scanPayload).eq("folder_id", folder_id).eq("device_id", deviceId).select("id");
2647
+ if (updateErr || !updated?.length) {
2648
+ const { error: insertErr } = await supabase.from("device_scans").insert(scanPayload);
2649
+ if (insertErr) {
2650
+ console.error(chalk12.red(` Failed to save scan data: ${insertErr.message}`));
2651
+ }
2652
+ }
2639
2653
  }
2640
2654
  await pushToolings(supabase, folder_id, result.toolings, deviceId);
2641
2655
  const manifestForHealth = result.envManifest ?? storedManifest;
@@ -2785,139 +2799,15 @@ var init_map = __esm({
2785
2799
  }
2786
2800
  });
2787
2801
 
2788
- // dist/commands/sync.js
2789
- var sync_exports = {};
2790
- __export(sync_exports, {
2791
- syncCommand: () => syncCommand
2792
- });
2793
- import { resolve as resolve5 } from "node:path";
2794
- import { existsSync as existsSync11 } from "node:fs";
2795
- import chalk15 from "chalk";
2796
- function isValidProjectPath(p) {
2797
- const resolved = resolve5(p);
2798
- return resolved.startsWith("/") && !resolved.includes("..") && existsSync11(resolved);
2799
- }
2800
- async function syncCommand(options) {
2801
- const { supabase, userId } = await getAuthenticatedClient();
2802
- if (options.all) {
2803
- const currentDeviceName = detectDeviceName();
2804
- const { data: devices, error } = await supabase.from("device_paths").select("folder_id, device_name, path").eq("device_name", currentDeviceName);
2805
- if (error || !devices?.length) {
2806
- console.log(chalk15.yellow(`
2807
- No projects linked on ${currentDeviceName} yet.
2808
- `));
2809
- console.log(chalk15.dim(" To link a project, cd into its folder and run:"));
2810
- console.log(chalk15.cyan(" md4ai scan"));
2811
- console.log(chalk15.dim(" Or link an existing project from the dashboard:"));
2812
- console.log(chalk15.cyan(" md4ai link <project-id>\n"));
2813
- return;
2814
- }
2815
- for (const device of devices) {
2816
- if (!isValidProjectPath(device.path)) {
2817
- console.error(chalk15.red(` Skipping invalid path: ${device.path}`));
2818
- continue;
2819
- }
2820
- console.log(chalk15.blue(`Syncing: ${device.path}`));
2821
- const { data: proposedAll } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
2822
- if (proposedAll?.length) {
2823
- console.log(chalk15.yellow(` ${proposedAll.length} file(s) proposed for deletion \u2014 run \`md4ai scan\` to review.`));
2824
- }
2825
- try {
2826
- const result = await scanProject(device.path, device.folder_id);
2827
- const { data: allDeviceRow } = await supabase.from("devices").select("id").eq("user_id", userId).eq("device_name", device.device_name).single();
2828
- const allDeviceId = allDeviceRow?.id;
2829
- if (allDeviceId) {
2830
- await supabase.from("device_scans").upsert({
2831
- folder_id: device.folder_id,
2832
- device_id: allDeviceId,
2833
- user_id: userId,
2834
- graph_json: result.graph,
2835
- orphans_json: result.orphans,
2836
- skills_table_json: result.skills,
2837
- stale_files_json: result.staleFiles,
2838
- broken_refs_json: result.brokenRefs,
2839
- env_manifest_json: result.envManifest,
2840
- plugin_versions_json: result.pluginVersions,
2841
- data_hash: result.dataHash,
2842
- scanned_at: result.scannedAt,
2843
- cli_version: CURRENT_VERSION
2844
- }, { onConflict: "folder_id,device_id" });
2845
- }
2846
- await pushToolings(supabase, device.folder_id, result.toolings, allDeviceId);
2847
- await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
2848
- console.log(chalk15.green(` Done: ${device.device_name}`));
2849
- } catch (err) {
2850
- console.error(chalk15.red(` Failed: ${device.path}: ${err}`));
2851
- }
2852
- }
2853
- await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
2854
- console.log(chalk15.green("\nAll devices synced."));
2855
- } else {
2856
- const state = await loadState();
2857
- if (!state.lastFolderId) {
2858
- console.error(chalk15.yellow("No recent sync. Use: md4ai sync --all, or md4ai scan <path> first."));
2859
- process.exit(1);
2860
- }
2861
- 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();
2862
- if (!device) {
2863
- console.error(chalk15.red("Could not find last synced device/folder."));
2864
- process.exit(1);
2865
- }
2866
- if (!isValidProjectPath(device.path)) {
2867
- console.error(chalk15.red(`Invalid project path: ${device.path}`));
2868
- process.exit(1);
2869
- }
2870
- console.log(chalk15.blue(`Syncing: ${device.path}`));
2871
- const { data: proposedSingle } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
2872
- if (proposedSingle?.length) {
2873
- console.log(chalk15.yellow(` ${proposedSingle.length} file(s) proposed for deletion \u2014 run \`md4ai scan\` to review.`));
2874
- }
2875
- const result = await scanProject(device.path, device.folder_id);
2876
- const { data: singleDeviceRow } = await supabase.from("devices").select("id").eq("user_id", userId).eq("device_name", device.device_name).single();
2877
- const singleDeviceId = singleDeviceRow?.id;
2878
- if (singleDeviceId) {
2879
- await supabase.from("device_scans").upsert({
2880
- folder_id: device.folder_id,
2881
- device_id: singleDeviceId,
2882
- user_id: userId,
2883
- graph_json: result.graph,
2884
- orphans_json: result.orphans,
2885
- skills_table_json: result.skills,
2886
- stale_files_json: result.staleFiles,
2887
- broken_refs_json: result.brokenRefs,
2888
- plugin_versions_json: result.pluginVersions,
2889
- data_hash: result.dataHash,
2890
- scanned_at: result.scannedAt,
2891
- cli_version: CURRENT_VERSION
2892
- }, { onConflict: "folder_id,device_id" });
2893
- }
2894
- await pushToolings(supabase, device.folder_id, result.toolings, singleDeviceId);
2895
- await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
2896
- await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
2897
- console.log(chalk15.green("Synced."));
2898
- }
2899
- }
2900
- var init_sync = __esm({
2901
- "dist/commands/sync.js"() {
2902
- "use strict";
2903
- init_auth();
2904
- init_config();
2905
- init_scanner();
2906
- init_push_toolings();
2907
- init_check_update();
2908
- init_device_utils();
2909
- }
2910
- });
2911
-
2912
2802
  // dist/mcp/read-configs.js
2913
2803
  import { readFile as readFile12 } from "node:fs/promises";
2914
2804
  import { join as join15 } from "node:path";
2915
2805
  import { homedir as homedir9 } from "node:os";
2916
- import { existsSync as existsSync13 } from "node:fs";
2806
+ import { existsSync as existsSync12 } from "node:fs";
2917
2807
  import { readdir as readdir6 } from "node:fs/promises";
2918
2808
  async function readJsonSafe(path) {
2919
2809
  try {
2920
- if (!existsSync13(path))
2810
+ if (!existsSync12(path))
2921
2811
  return null;
2922
2812
  const raw = await readFile12(path, "utf-8");
2923
2813
  return JSON.parse(raw);
@@ -2988,14 +2878,14 @@ async function readAllMcpConfigs() {
2988
2878
  const cwdMcp = await readJsonSafe(join15(process.cwd(), ".mcp.json"));
2989
2879
  entries.push(...parseServers(cwdMcp, "project"));
2990
2880
  const pluginsBase = join15(home, ".claude", "plugins", "marketplaces");
2991
- if (existsSync13(pluginsBase)) {
2881
+ if (existsSync12(pluginsBase)) {
2992
2882
  try {
2993
2883
  const marketplaces = await readdir6(pluginsBase, { withFileTypes: true });
2994
2884
  for (const mp of marketplaces) {
2995
2885
  if (!mp.isDirectory())
2996
2886
  continue;
2997
2887
  const extDir = join15(pluginsBase, mp.name, "external_plugins");
2998
- if (!existsSync13(extDir))
2888
+ if (!existsSync12(extDir))
2999
2889
  continue;
3000
2890
  const plugins = await readdir6(extDir, { withFileTypes: true });
3001
2891
  for (const plugin of plugins) {
@@ -3009,7 +2899,7 @@ async function readAllMcpConfigs() {
3009
2899
  }
3010
2900
  }
3011
2901
  const cacheBase = join15(home, ".claude", "plugins", "cache");
3012
- if (existsSync13(cacheBase)) {
2902
+ if (existsSync12(cacheBase)) {
3013
2903
  try {
3014
2904
  const registries = await readdir6(cacheBase, { withFileTypes: true });
3015
2905
  for (const reg of registries) {
@@ -3174,7 +3064,7 @@ var mcp_watch_exports = {};
3174
3064
  __export(mcp_watch_exports, {
3175
3065
  mcpWatchCommand: () => mcpWatchCommand
3176
3066
  });
3177
- import chalk21 from "chalk";
3067
+ import chalk20 from "chalk";
3178
3068
  import { execFileSync as execFileSync6 } from "node:child_process";
3179
3069
  import { createHash as createHash2 } from "node:crypto";
3180
3070
  function detectTty() {
@@ -3328,20 +3218,20 @@ function buildRows(configs, httpResults, cdpStatus) {
3328
3218
  }
3329
3219
  function printTable(rows, deviceName, watcherPid, cdpResult) {
3330
3220
  process.stdout.write("\x1B[3J\x1B[2J\x1B[H");
3331
- console.log(chalk21.bold.cyan(`
3332
- MCP Monitor v${CURRENT_VERSION} \u2014 ${deviceName}`) + chalk21.dim(` (PID ${watcherPid})`));
3333
- console.log(chalk21.dim(` ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \xB7 refreshes every 30s \xB7 Ctrl+C to stop
3221
+ console.log(chalk20.bold.cyan(`
3222
+ MCP Monitor v${CURRENT_VERSION} \u2014 ${deviceName}`) + chalk20.dim(` (PID ${watcherPid})`));
3223
+ console.log(chalk20.dim(` ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \xB7 refreshes every 30s \xB7 Ctrl+C to stop
3334
3224
  `));
3335
3225
  if (cdpResult) {
3336
3226
  if (cdpResult.status === "reachable") {
3337
3227
  const browserInfo = cdpResult.browser ? ` (${cdpResult.browser})` : "";
3338
- console.log(chalk21.green(" \u2714 Chrome browser connected") + chalk21.dim(browserInfo));
3339
- console.log(chalk21.dim(" Claude Code can control Chrome via DevTools Protocol on localhost:9222"));
3228
+ console.log(chalk20.green(" \u2714 Chrome browser connected") + chalk20.dim(browserInfo));
3229
+ console.log(chalk20.dim(" Claude Code can control Chrome via DevTools Protocol on localhost:9222"));
3340
3230
  } else {
3341
- console.log(chalk21.red(" \u2717 Chrome browser not connected"));
3342
- console.log(chalk21.dim(" Claude Code cannot reach Chrome. To fix:"));
3343
- console.log(chalk21.dim(" 1. Open Chrome with: google-chrome --remote-debugging-port=9222"));
3344
- console.log(chalk21.dim(" 2. Or add --remote-debugging-port=9222 to your Chrome shortcut"));
3231
+ console.log(chalk20.red(" \u2717 Chrome browser not connected"));
3232
+ console.log(chalk20.dim(" Claude Code cannot reach Chrome. To fix:"));
3233
+ console.log(chalk20.dim(" 1. Open Chrome with: google-chrome --remote-debugging-port=9222"));
3234
+ console.log(chalk20.dim(" 2. Or add --remote-debugging-port=9222 to your Chrome shortcut"));
3345
3235
  }
3346
3236
  console.log("");
3347
3237
  }
@@ -3362,41 +3252,41 @@ function printTable(rows, deviceName, watcherPid, cdpResult) {
3362
3252
  sessionNum++;
3363
3253
  const totalMem = servers.reduce((sum, s) => sum + (s.memory_mb ?? 0), 0);
3364
3254
  const label = byTty.size === 1 ? "Claude Code session" : `Claude Code session ${sessionNum}`;
3365
- console.log(chalk21.green(` ${label}`) + chalk21.dim(` \u2014 ${servers.length} server${servers.length !== 1 ? "s" : ""} \xB7 ${totalMem} MB \xB7 terminal ${tty}`));
3255
+ console.log(chalk20.green(` ${label}`) + chalk20.dim(` \u2014 ${servers.length} server${servers.length !== 1 ? "s" : ""} \xB7 ${totalMem} MB \xB7 terminal ${tty}`));
3366
3256
  for (const s of servers) {
3367
3257
  const uptime = formatUptime(s.uptime_seconds ?? 0);
3368
- const source = chalk21.dim(`[${s.config_source}]`);
3369
- 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}`);
3258
+ const source = chalk20.dim(`[${s.config_source}]`);
3259
+ 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}`);
3370
3260
  }
3371
3261
  console.log("");
3372
3262
  }
3373
3263
  if (byTty.size > 1) {
3374
- console.log(chalk21.dim(" Each open Claude Code window runs its own set of MCP servers.\n"));
3264
+ console.log(chalk20.dim(" Each open Claude Code window runs its own set of MCP servers.\n"));
3375
3265
  }
3376
3266
  }
3377
3267
  if (runningHttp.length > 0) {
3378
- console.log(chalk21.blue(` Remote Services (${runningHttp.length})`) + chalk21.dim(" \u2014 HTTP endpoints reachable"));
3268
+ console.log(chalk20.blue(` Remote Services (${runningHttp.length})`) + chalk20.dim(" \u2014 HTTP endpoints reachable"));
3379
3269
  for (const s of runningHttp) {
3380
- console.log(` ${chalk21.blue("\u25CF")} ${s.server_name.padEnd(20)} ${chalk21.dim((s.http_url ?? "").padEnd(30))}`);
3270
+ console.log(` ${chalk20.blue("\u25CF")} ${s.server_name.padEnd(20)} ${chalk20.dim((s.http_url ?? "").padEnd(30))}`);
3381
3271
  }
3382
3272
  console.log("");
3383
3273
  }
3384
3274
  if (stopped.length > 0 || errored.length > 0) {
3385
3275
  const notRunning = [...stopped, ...errored];
3386
- console.log(chalk21.yellow(` Not Running (${notRunning.length})`) + chalk21.dim(" \u2014 configured but no process detected"));
3276
+ console.log(chalk20.yellow(` Not Running (${notRunning.length})`) + chalk20.dim(" \u2014 configured but no process detected"));
3387
3277
  for (const s of notRunning) {
3388
- const icon = s.status === "error" ? chalk21.red("\u2717") : chalk21.yellow("\u25CB");
3389
- const source = chalk21.dim(`[${s.config_source}]`);
3390
- const detail = s.error_detail ? chalk21.dim(` \u2014 ${s.error_detail}`) : "";
3278
+ const icon = s.status === "error" ? chalk20.red("\u2717") : chalk20.yellow("\u25CB");
3279
+ const source = chalk20.dim(`[${s.config_source}]`);
3280
+ const detail = s.error_detail ? chalk20.dim(` \u2014 ${s.error_detail}`) : "";
3391
3281
  console.log(` ${icon} ${s.server_name.padEnd(20)} ${source}${detail}`);
3392
3282
  }
3393
3283
  console.log("");
3394
3284
  }
3395
3285
  if (rows.length === 0) {
3396
- console.log(chalk21.yellow(" No MCP servers configured."));
3397
- console.log(chalk21.dim(" Configure servers in ~/.claude/mcp.json or .mcp.json\n"));
3286
+ console.log(chalk20.yellow(" No MCP servers configured."));
3287
+ console.log(chalk20.dim(" Configure servers in ~/.claude/mcp.json or .mcp.json\n"));
3398
3288
  }
3399
- console.log(chalk21.bgYellow.black.bold(" \u26A0 DO NOT CLOSE THIS WINDOW \u2014 it feeds live data to the dashboard \u26A0 "));
3289
+ console.log(chalk20.bgYellow.black.bold(" \u26A0 DO NOT CLOSE THIS WINDOW \u2014 it feeds live data to the dashboard \u26A0 "));
3400
3290
  console.log("");
3401
3291
  }
3402
3292
  function formatUptime(seconds) {
@@ -3426,7 +3316,7 @@ async function mcpWatchCommand() {
3426
3316
  const { data: existingWatchers } = await supabase.from("mcp_watchers").select("pid, tty, cli_version, started_at").eq("device_id", deviceId);
3427
3317
  if (existingWatchers && existingWatchers.length > 0) {
3428
3318
  console.log("");
3429
- console.log(chalk21.yellow(` Replacing ${existingWatchers.length} existing watcher${existingWatchers.length !== 1 ? "s" : ""} on this device...`));
3319
+ console.log(chalk20.yellow(` Replacing ${existingWatchers.length} existing watcher${existingWatchers.length !== 1 ? "s" : ""} on this device...`));
3430
3320
  for (const w of existingWatchers) {
3431
3321
  try {
3432
3322
  if (typeof w.pid === "number" && w.pid > 1 && w.pid !== process.pid) {
@@ -3437,15 +3327,15 @@ async function mcpWatchCommand() {
3437
3327
  }
3438
3328
  await supabase.from("mcp_watchers").delete().eq("device_id", deviceId);
3439
3329
  await new Promise((r) => setTimeout(r, 1e3));
3440
- console.log(chalk21.dim(" Previous watcher stopped.\n"));
3330
+ console.log(chalk20.dim(" Previous watcher stopped.\n"));
3441
3331
  }
3442
3332
  process.stdout.write(`\x1B]0;MCP mon\x07`);
3443
- console.log(chalk21.blue(`Starting MCP monitor for ${deviceName}...`));
3333
+ console.log(chalk20.blue(`Starting MCP monitor for ${deviceName}...`));
3444
3334
  console.log("");
3445
- console.log(chalk21.dim(" View this in the online dashboard at:"));
3446
- console.log(chalk21.cyan(` https://www.md4ai.com/device/${deviceId}`));
3335
+ console.log(chalk20.dim(" View this in the online dashboard at:"));
3336
+ console.log(chalk20.cyan(` https://www.md4ai.com/device/${deviceId}`));
3447
3337
  console.log("");
3448
- console.log(chalk21.yellow(" Please note, closing this window will stop ALL watch reports."));
3338
+ console.log(chalk20.yellow(" Please note, closing this window will stop ALL watch reports."));
3449
3339
  console.log("");
3450
3340
  await supabase.from("mcp_watchers").upsert({
3451
3341
  device_id: deviceId,
@@ -3475,7 +3365,7 @@ async function mcpWatchCommand() {
3475
3365
  return;
3476
3366
  }
3477
3367
  if (deleteError) {
3478
- console.error(chalk21.red(` [debug] Failed to delete old status: ${deleteError.message}`));
3368
+ console.error(chalk20.red(` [debug] Failed to delete old status: ${deleteError.message}`));
3479
3369
  }
3480
3370
  if (rows.length > 0) {
3481
3371
  const { error: insertError } = await supabase.from("mcp_server_status").insert(rows.map((row) => ({
@@ -3488,7 +3378,7 @@ async function mcpWatchCommand() {
3488
3378
  return;
3489
3379
  }
3490
3380
  if (insertError) {
3491
- console.error(chalk21.red(` [debug] Failed to write MCP status: ${insertError.message}`));
3381
+ console.error(chalk20.red(` [debug] Failed to write MCP status: ${insertError.message}`));
3492
3382
  }
3493
3383
  }
3494
3384
  const { error: heartbeatError } = await supabase.from("mcp_watchers").update({ last_heartbeat: now }).eq("device_id", deviceId).eq("pid", myPid);
@@ -3506,11 +3396,11 @@ async function mcpWatchCommand() {
3506
3396
  return;
3507
3397
  }
3508
3398
  jwtRefreshAttempted = true;
3509
- console.log(chalk21.yellow("\n Session token expired \u2014 attempting to refresh..."));
3399
+ console.log(chalk20.yellow("\n Session token expired \u2014 attempting to refresh..."));
3510
3400
  const refreshed = await refreshSession();
3511
3401
  if (refreshed) {
3512
3402
  supabase = refreshed.supabase;
3513
- console.log(chalk21.green(" Session refreshed successfully. Resuming monitoring.\n"));
3403
+ console.log(chalk20.green(" Session refreshed successfully. Resuming monitoring.\n"));
3514
3404
  jwtRefreshAttempted = false;
3515
3405
  await supabase.from("mcp_watchers").upsert({
3516
3406
  device_id: deviceId,
@@ -3528,14 +3418,14 @@ async function mcpWatchCommand() {
3528
3418
  function printSessionExpired() {
3529
3419
  process.stdout.write("\x1B[3J\x1B[2J\x1B[H");
3530
3420
  console.log("");
3531
- console.log(chalk21.bgRed.white.bold(" \u2717 SESSION EXPIRED \u2014 this watcher is no longer sending data to the dashboard \u2717 "));
3421
+ console.log(chalk20.bgRed.white.bold(" \u2717 SESSION EXPIRED \u2014 this watcher is no longer sending data to the dashboard \u2717 "));
3532
3422
  console.log("");
3533
- console.log(chalk21.yellow(" Your authentication token has expired and could not be refreshed."));
3534
- console.log(chalk21.yellow(' The dashboard will show "No watchers" because this process can no longer update it.'));
3423
+ console.log(chalk20.yellow(" Your authentication token has expired and could not be refreshed."));
3424
+ console.log(chalk20.yellow(' The dashboard will show "No watchers" because this process can no longer update it.'));
3535
3425
  console.log("");
3536
- console.log(chalk21.white(" To fix this:"));
3537
- console.log(chalk21.cyan(" 1. Run: md4ai login"));
3538
- console.log(chalk21.cyan(" 2. Then restart the watcher: md4ai mcp-watch"));
3426
+ console.log(chalk20.white(" To fix this:"));
3427
+ console.log(chalk20.cyan(" 1. Run: md4ai login"));
3428
+ console.log(chalk20.cyan(" 2. Then restart the watcher: md4ai mcp-watch"));
3539
3429
  console.log("");
3540
3430
  }
3541
3431
  let interval;
@@ -3578,7 +3468,7 @@ async function mcpWatchCommand() {
3578
3468
  clearInterval(interval);
3579
3469
  clearInterval(envInterval);
3580
3470
  await supabase.from("mcp_watchers").delete().eq("device_id", deviceId).eq("pid", myPid);
3581
- console.log(chalk21.dim("\nMCP monitor stopped."));
3471
+ console.log(chalk20.dim("\nMCP monitor stopped."));
3582
3472
  process.exit(0);
3583
3473
  };
3584
3474
  process.on("SIGINT", () => {
@@ -3593,12 +3483,13 @@ async function checkPendingRescans(supabase, deviceId, deviceName) {
3593
3483
  if (!paths?.length)
3594
3484
  return;
3595
3485
  const folderIds = paths.map((p) => p.folder_id);
3596
- const { data: folders } = await supabase.from("claude_folders").select("id, last_scanned, rescan_requested_at").in("id", folderIds).not("rescan_requested_at", "is", null);
3486
+ const { data: folders } = await supabase.from("claude_folders").select("id, rescan_requested_at").in("id", folderIds).not("rescan_requested_at", "is", null);
3597
3487
  if (!folders?.length)
3598
3488
  return;
3599
3489
  for (const folder of folders) {
3600
3490
  const requested = new Date(folder.rescan_requested_at).getTime();
3601
- const scanned = folder.last_scanned ? new Date(folder.last_scanned).getTime() : 0;
3491
+ const { data: scanRow } = await supabase.from("device_scans").select("scanned_at").eq("folder_id", folder.id).eq("device_id", deviceId).maybeSingle();
3492
+ const scanned = scanRow?.scanned_at ? new Date(scanRow.scanned_at).getTime() : 0;
3602
3493
  if (requested <= scanned)
3603
3494
  continue;
3604
3495
  const dp = paths.find((p) => p.folder_id === folder.id);
@@ -3606,24 +3497,46 @@ async function checkPendingRescans(supabase, deviceId, deviceName) {
3606
3497
  continue;
3607
3498
  try {
3608
3499
  const result = await scanProject(dp.path);
3609
- await supabase.from("device_scans").upsert({
3500
+ const userId = (await supabase.auth.getUser()).data.user.id;
3501
+ const scanPayload = {
3610
3502
  folder_id: folder.id,
3611
3503
  device_id: deviceId,
3612
- user_id: (await supabase.auth.getUser()).data.user.id,
3504
+ user_id: userId,
3613
3505
  graph_json: result.graph,
3614
3506
  orphans_json: result.orphans,
3615
3507
  skills_table_json: result.skills,
3616
3508
  stale_files_json: result.staleFiles,
3617
3509
  broken_refs_json: result.brokenRefs,
3618
3510
  env_manifest_json: result.envManifest,
3511
+ doppler_json: result.doppler,
3512
+ marketplace_plugins_json: result.marketplacePlugins,
3513
+ plugin_versions_json: result.pluginVersions,
3619
3514
  data_hash: result.dataHash,
3620
3515
  scanned_at: result.scannedAt,
3621
3516
  cli_version: CURRENT_VERSION
3622
- }, { onConflict: "folder_id,device_id" });
3517
+ };
3518
+ const { data: updated } = await supabase.from("device_scans").update(scanPayload).eq("folder_id", folder.id).eq("device_id", deviceId).select("id");
3519
+ if (!updated?.length) {
3520
+ await supabase.from("device_scans").insert(scanPayload);
3521
+ }
3623
3522
  await supabase.from("claude_folders").update({
3624
3523
  rescan_requested_at: null
3625
3524
  }).eq("id", folder.id);
3626
3525
  await pushToolings(supabase, folder.id, result.toolings, deviceId);
3526
+ const graphPaths = result.graph.nodes.map((n) => n.filePath);
3527
+ const { readClaudeConfigFiles: readClaudeConfigFiles2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
3528
+ const configFiles = await readClaudeConfigFiles2(dp.path, graphPaths);
3529
+ for (const cf of configFiles) {
3530
+ await supabase.from("folder_files").upsert({
3531
+ folder_id: folder.id,
3532
+ file_path: cf.filePath,
3533
+ content: cf.content,
3534
+ size_bytes: cf.sizeBytes,
3535
+ last_modified: cf.lastModified,
3536
+ synced_at: (/* @__PURE__ */ new Date()).toISOString(),
3537
+ device_id: deviceId
3538
+ }, { onConflict: "folder_id,file_path,device_id" });
3539
+ }
3627
3540
  await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder.id).eq("device_name", deviceName);
3628
3541
  } catch {
3629
3542
  await supabase.from("claude_folders").update({ rescan_requested_at: null }).eq("id", folder.id);
@@ -3648,6 +3561,130 @@ var init_mcp_watch = __esm({
3648
3561
  }
3649
3562
  });
3650
3563
 
3564
+ // dist/commands/sync.js
3565
+ var sync_exports = {};
3566
+ __export(sync_exports, {
3567
+ syncCommand: () => syncCommand
3568
+ });
3569
+ import { resolve as resolve8 } from "node:path";
3570
+ import { existsSync as existsSync14 } from "node:fs";
3571
+ import chalk22 from "chalk";
3572
+ function isValidProjectPath(p) {
3573
+ const resolved = resolve8(p);
3574
+ return resolved.startsWith("/") && !resolved.includes("..") && existsSync14(resolved);
3575
+ }
3576
+ async function syncCommand(options) {
3577
+ const { supabase, userId } = await getAuthenticatedClient();
3578
+ if (options.all) {
3579
+ const currentDeviceName = detectDeviceName();
3580
+ const { data: devices, error } = await supabase.from("device_paths").select("folder_id, device_name, path").eq("device_name", currentDeviceName);
3581
+ if (error || !devices?.length) {
3582
+ console.log(chalk22.yellow(`
3583
+ No projects linked on ${currentDeviceName} yet.
3584
+ `));
3585
+ console.log(chalk22.dim(" To link a project, cd into its folder and run:"));
3586
+ console.log(chalk22.cyan(" md4ai scan"));
3587
+ console.log(chalk22.dim(" Or link an existing project from the dashboard:"));
3588
+ console.log(chalk22.cyan(" md4ai link <project-id>\n"));
3589
+ return;
3590
+ }
3591
+ for (const device of devices) {
3592
+ if (!isValidProjectPath(device.path)) {
3593
+ console.error(chalk22.red(` Skipping invalid path: ${device.path}`));
3594
+ continue;
3595
+ }
3596
+ console.log(chalk22.blue(`Syncing: ${device.path}`));
3597
+ const { data: proposedAll } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
3598
+ if (proposedAll?.length) {
3599
+ console.log(chalk22.yellow(` ${proposedAll.length} file(s) proposed for deletion \u2014 run \`md4ai scan\` to review.`));
3600
+ }
3601
+ try {
3602
+ const result = await scanProject(device.path, device.folder_id);
3603
+ const { data: allDeviceRow } = await supabase.from("devices").select("id").eq("user_id", userId).eq("device_name", device.device_name).single();
3604
+ const allDeviceId = allDeviceRow?.id;
3605
+ if (allDeviceId) {
3606
+ await supabase.from("device_scans").upsert({
3607
+ folder_id: device.folder_id,
3608
+ device_id: allDeviceId,
3609
+ user_id: userId,
3610
+ graph_json: result.graph,
3611
+ orphans_json: result.orphans,
3612
+ skills_table_json: result.skills,
3613
+ stale_files_json: result.staleFiles,
3614
+ broken_refs_json: result.brokenRefs,
3615
+ env_manifest_json: result.envManifest,
3616
+ plugin_versions_json: result.pluginVersions,
3617
+ data_hash: result.dataHash,
3618
+ scanned_at: result.scannedAt,
3619
+ cli_version: CURRENT_VERSION
3620
+ }, { onConflict: "folder_id,device_id" });
3621
+ }
3622
+ await pushToolings(supabase, device.folder_id, result.toolings, allDeviceId);
3623
+ await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
3624
+ console.log(chalk22.green(` Done: ${device.device_name}`));
3625
+ } catch (err) {
3626
+ console.error(chalk22.red(` Failed: ${device.path}: ${err}`));
3627
+ }
3628
+ }
3629
+ await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
3630
+ console.log(chalk22.green("\nAll devices synced."));
3631
+ } else {
3632
+ const state = await loadState();
3633
+ if (!state.lastFolderId) {
3634
+ console.error(chalk22.yellow("No recent sync. Use: md4ai sync --all, or md4ai scan <path> first."));
3635
+ process.exit(1);
3636
+ }
3637
+ 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();
3638
+ if (!device) {
3639
+ console.error(chalk22.red("Could not find last synced device/folder."));
3640
+ process.exit(1);
3641
+ }
3642
+ if (!isValidProjectPath(device.path)) {
3643
+ console.error(chalk22.red(`Invalid project path: ${device.path}`));
3644
+ process.exit(1);
3645
+ }
3646
+ console.log(chalk22.blue(`Syncing: ${device.path}`));
3647
+ const { data: proposedSingle } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
3648
+ if (proposedSingle?.length) {
3649
+ console.log(chalk22.yellow(` ${proposedSingle.length} file(s) proposed for deletion \u2014 run \`md4ai scan\` to review.`));
3650
+ }
3651
+ const result = await scanProject(device.path, device.folder_id);
3652
+ const { data: singleDeviceRow } = await supabase.from("devices").select("id").eq("user_id", userId).eq("device_name", device.device_name).single();
3653
+ const singleDeviceId = singleDeviceRow?.id;
3654
+ if (singleDeviceId) {
3655
+ await supabase.from("device_scans").upsert({
3656
+ folder_id: device.folder_id,
3657
+ device_id: singleDeviceId,
3658
+ user_id: userId,
3659
+ graph_json: result.graph,
3660
+ orphans_json: result.orphans,
3661
+ skills_table_json: result.skills,
3662
+ stale_files_json: result.staleFiles,
3663
+ broken_refs_json: result.brokenRefs,
3664
+ plugin_versions_json: result.pluginVersions,
3665
+ data_hash: result.dataHash,
3666
+ scanned_at: result.scannedAt,
3667
+ cli_version: CURRENT_VERSION
3668
+ }, { onConflict: "folder_id,device_id" });
3669
+ }
3670
+ await pushToolings(supabase, device.folder_id, result.toolings, singleDeviceId);
3671
+ await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
3672
+ await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
3673
+ console.log(chalk22.green("Synced."));
3674
+ }
3675
+ }
3676
+ var init_sync = __esm({
3677
+ "dist/commands/sync.js"() {
3678
+ "use strict";
3679
+ init_auth();
3680
+ init_config();
3681
+ init_scanner();
3682
+ init_push_toolings();
3683
+ init_check_update();
3684
+ init_device_utils();
3685
+ }
3686
+ });
3687
+
3651
3688
  // dist/index.js
3652
3689
  import { Command } from "commander";
3653
3690
 
@@ -4012,9 +4049,6 @@ function generatePrintHtml(result, title) {
4012
4049
  </html>`;
4013
4050
  }
4014
4051
 
4015
- // dist/index.js
4016
- init_sync();
4017
-
4018
4052
  // dist/commands/link.js
4019
4053
  init_auth();
4020
4054
  init_config();
@@ -4022,23 +4056,23 @@ init_scanner();
4022
4056
  init_push_toolings();
4023
4057
  init_device_utils();
4024
4058
  init_check_update();
4025
- import { resolve as resolve6 } from "node:path";
4026
- import chalk16 from "chalk";
4059
+ import { resolve as resolve5 } from "node:path";
4060
+ import chalk15 from "chalk";
4027
4061
  async function linkCommand(projectId) {
4028
4062
  const shouldContinue = await promptUpdateIfAvailable(["link", projectId]);
4029
4063
  if (!shouldContinue)
4030
4064
  return;
4031
4065
  const { supabase, userId } = await getAuthenticatedClient();
4032
- const cwd = resolve6(process.cwd());
4066
+ const cwd = resolve5(process.cwd());
4033
4067
  const deviceName = detectDeviceName();
4034
4068
  const osType = detectOs();
4035
4069
  const { data: folder, error: folderErr } = await supabase.from("claude_folders").select("id, name").eq("id", projectId).single();
4036
4070
  if (folderErr || !folder) {
4037
- console.error(chalk16.red("Project not found, or you do not have access."));
4038
- console.error(chalk16.yellow("Check the project ID in the MD4AI web dashboard."));
4071
+ console.error(chalk15.red("Project not found, or you do not have access."));
4072
+ console.error(chalk15.yellow("Check the project ID in the MD4AI web dashboard."));
4039
4073
  process.exit(1);
4040
4074
  }
4041
- console.log(chalk16.blue(`
4075
+ console.log(chalk15.blue(`
4042
4076
  Linking "${folder.name}" to this device...
4043
4077
  `));
4044
4078
  console.log(` Project: ${folder.name}`);
@@ -4064,12 +4098,12 @@ Linking "${folder.name}" to this device...
4064
4098
  path: cwd
4065
4099
  });
4066
4100
  if (pathErr) {
4067
- console.error(chalk16.red(`Failed to link: ${pathErr.message}`));
4101
+ console.error(chalk15.red(`Failed to link: ${pathErr.message}`));
4068
4102
  process.exit(1);
4069
4103
  }
4070
4104
  }
4071
- console.log(chalk16.green("\nLinked successfully."));
4072
- console.log(chalk16.blue("\nRunning initial scan...\n"));
4105
+ console.log(chalk15.green("\nLinked successfully."));
4106
+ console.log(chalk15.blue("\nRunning initial scan...\n"));
4073
4107
  const result = await scanProject(cwd, folder.id);
4074
4108
  console.log(` Files: ${result.graph.nodes.length}`);
4075
4109
  console.log(` References:${result.graph.edges.length}`);
@@ -4110,7 +4144,7 @@ Linking "${folder.name}" to this device...
4110
4144
  device_id: deviceId
4111
4145
  }, { onConflict: "folder_id,file_path,device_id" });
4112
4146
  }
4113
- console.log(chalk16.green(` Uploaded ${configFiles.length} config file(s).`));
4147
+ console.log(chalk15.green(` Uploaded ${configFiles.length} config file(s).`));
4114
4148
  }
4115
4149
  await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder.id).eq("device_name", deviceName);
4116
4150
  await saveState({
@@ -4119,28 +4153,28 @@ Linking "${folder.name}" to this device...
4119
4153
  lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
4120
4154
  });
4121
4155
  const projectUrl = `https://www.md4ai.com/project/${folder.id}`;
4122
- console.log(chalk16.green("\nDone! Project linked and scanned."));
4123
- console.log(chalk16.cyan(`
4156
+ console.log(chalk15.green("\nDone! Project linked and scanned."));
4157
+ console.log(chalk15.cyan(`
4124
4158
  ${projectUrl}
4125
4159
  `));
4126
- console.log(chalk16.grey('Run "md4ai scan" to rescan at any time.'));
4160
+ console.log(chalk15.grey('Run "md4ai scan" to rescan at any time.'));
4127
4161
  }
4128
4162
 
4129
4163
  // dist/commands/import-bundle.js
4130
4164
  import { readFile as readFile11, writeFile as writeFile4, mkdir as mkdir3, stat as fsStat } from "node:fs/promises";
4131
- import { dirname as dirname3, resolve as resolve7 } from "node:path";
4132
- import { existsSync as existsSync12 } from "node:fs";
4133
- import chalk17 from "chalk";
4165
+ import { dirname as dirname3, resolve as resolve6 } from "node:path";
4166
+ import { existsSync as existsSync11 } from "node:fs";
4167
+ import chalk16 from "chalk";
4134
4168
  import { confirm as confirm3, input as input6 } from "@inquirer/prompts";
4135
4169
  var MAX_BUNDLE_SIZE_BYTES = 50 * 1024 * 1024;
4136
4170
  async function importBundleCommand(zipPath) {
4137
- if (!existsSync12(zipPath)) {
4138
- console.error(chalk17.red(`File not found: ${zipPath}`));
4171
+ if (!existsSync11(zipPath)) {
4172
+ console.error(chalk16.red(`File not found: ${zipPath}`));
4139
4173
  process.exit(1);
4140
4174
  }
4141
4175
  const fileStat = await fsStat(zipPath);
4142
4176
  if (fileStat.size > MAX_BUNDLE_SIZE_BYTES) {
4143
- console.error(chalk17.red(`Bundle too large (${Math.round(fileStat.size / 1024 / 1024)} MB). Maximum is 50 MB.`));
4177
+ console.error(chalk16.red(`Bundle too large (${Math.round(fileStat.size / 1024 / 1024)} MB). Maximum is 50 MB.`));
4144
4178
  process.exit(1);
4145
4179
  }
4146
4180
  const JSZip = (await import("jszip")).default;
@@ -4148,11 +4182,11 @@ async function importBundleCommand(zipPath) {
4148
4182
  const zip = await JSZip.loadAsync(zipData);
4149
4183
  const manifestFile = zip.file("manifest.json");
4150
4184
  if (!manifestFile) {
4151
- console.error(chalk17.red("Invalid bundle: missing manifest.json"));
4185
+ console.error(chalk16.red("Invalid bundle: missing manifest.json"));
4152
4186
  process.exit(1);
4153
4187
  }
4154
4188
  const manifest = JSON.parse(await manifestFile.async("string"));
4155
- console.log(chalk17.blue(`
4189
+ console.log(chalk16.blue(`
4156
4190
  Bundle: ${manifest.folderName}`));
4157
4191
  console.log(` Exported by: ${manifest.ownerEmail}`);
4158
4192
  console.log(` Exported at: ${manifest.exportedAt}`);
@@ -4166,10 +4200,10 @@ Bundle: ${manifest.folderName}`));
4166
4200
  }
4167
4201
  }
4168
4202
  if (files.length === 0) {
4169
- console.log(chalk17.yellow("No Claude config files found in bundle."));
4203
+ console.log(chalk16.yellow("No Claude config files found in bundle."));
4170
4204
  return;
4171
4205
  }
4172
- console.log(chalk17.blue(`
4206
+ console.log(chalk16.blue(`
4173
4207
  Files to extract:`));
4174
4208
  for (const f of files) {
4175
4209
  console.log(` ${f.filePath}`);
@@ -4182,39 +4216,39 @@ Files to extract:`));
4182
4216
  message: `Extract ${files.length} file(s) to ${targetDir}?`
4183
4217
  });
4184
4218
  if (!proceed) {
4185
- console.log(chalk17.yellow("Cancelled."));
4219
+ console.log(chalk16.yellow("Cancelled."));
4186
4220
  return;
4187
4221
  }
4188
- const resolvedTarget = resolve7(targetDir);
4222
+ const resolvedTarget = resolve6(targetDir);
4189
4223
  for (const file of files) {
4190
- const fullPath = resolve7(targetDir, file.filePath);
4224
+ const fullPath = resolve6(targetDir, file.filePath);
4191
4225
  if (!fullPath.startsWith(resolvedTarget + "/") && fullPath !== resolvedTarget) {
4192
- console.error(chalk17.red(` Blocked path traversal: ${file.filePath}`));
4226
+ console.error(chalk16.red(` Blocked path traversal: ${file.filePath}`));
4193
4227
  continue;
4194
4228
  }
4195
4229
  const dir = dirname3(fullPath);
4196
- if (!existsSync12(dir)) {
4230
+ if (!existsSync11(dir)) {
4197
4231
  await mkdir3(dir, { recursive: true });
4198
4232
  }
4199
4233
  await writeFile4(fullPath, file.content, "utf-8");
4200
- console.log(chalk17.green(` \u2713 ${file.filePath}`));
4234
+ console.log(chalk16.green(` \u2713 ${file.filePath}`));
4201
4235
  }
4202
- console.log(chalk17.green(`
4236
+ console.log(chalk16.green(`
4203
4237
  Done! ${files.length} file(s) extracted to ${targetDir}`));
4204
4238
  }
4205
4239
 
4206
4240
  // dist/commands/admin-update-tool.js
4207
4241
  init_auth();
4208
- import chalk18 from "chalk";
4242
+ import chalk17 from "chalk";
4209
4243
  var VALID_CATEGORIES = ["framework", "runtime", "cli", "mcp", "package", "database", "other"];
4210
4244
  async function adminUpdateToolCommand(options) {
4211
4245
  const { supabase } = await getAuthenticatedClient();
4212
4246
  if (!options.name) {
4213
- console.error(chalk18.red("--name is required."));
4247
+ console.error(chalk17.red("--name is required."));
4214
4248
  process.exit(1);
4215
4249
  }
4216
4250
  if (options.category && !VALID_CATEGORIES.includes(options.category)) {
4217
- console.error(chalk18.red(`Invalid category. Must be one of: ${VALID_CATEGORIES.join(", ")}`));
4251
+ console.error(chalk17.red(`Invalid category. Must be one of: ${VALID_CATEGORIES.join(", ")}`));
4218
4252
  process.exit(1);
4219
4253
  }
4220
4254
  const { data: existing } = await supabase.from("tools_registry").select("id, name, display_name, category").eq("name", options.name).maybeSingle();
@@ -4236,13 +4270,13 @@ async function adminUpdateToolCommand(options) {
4236
4270
  updates.notes = options.notes;
4237
4271
  const { error } = await supabase.from("tools_registry").update(updates).eq("id", existing.id);
4238
4272
  if (error) {
4239
- console.error(chalk18.red(`Failed to update: ${error.message}`));
4273
+ console.error(chalk17.red(`Failed to update: ${error.message}`));
4240
4274
  process.exit(1);
4241
4275
  }
4242
- console.log(chalk18.green(`Updated "${existing.display_name}" in the registry.`));
4276
+ console.log(chalk17.green(`Updated "${existing.display_name}" in the registry.`));
4243
4277
  } else {
4244
4278
  if (!options.display || !options.category) {
4245
- console.error(chalk18.red("New tools require --display and --category."));
4279
+ console.error(chalk17.red("New tools require --display and --category."));
4246
4280
  process.exit(1);
4247
4281
  }
4248
4282
  const { error } = await supabase.from("tools_registry").insert({
@@ -4256,25 +4290,25 @@ async function adminUpdateToolCommand(options) {
4256
4290
  notes: options.notes ?? null
4257
4291
  });
4258
4292
  if (error) {
4259
- console.error(chalk18.red(`Failed to create: ${error.message}`));
4293
+ console.error(chalk17.red(`Failed to create: ${error.message}`));
4260
4294
  process.exit(1);
4261
4295
  }
4262
- console.log(chalk18.green(`Added "${options.display}" to the registry.`));
4296
+ console.log(chalk17.green(`Added "${options.display}" to the registry.`));
4263
4297
  }
4264
4298
  }
4265
4299
 
4266
4300
  // dist/commands/admin-list-tools.js
4267
4301
  init_auth();
4268
- import chalk19 from "chalk";
4302
+ import chalk18 from "chalk";
4269
4303
  async function adminListToolsCommand() {
4270
4304
  const { supabase } = await getAuthenticatedClient();
4271
4305
  const { data: tools, error } = await supabase.from("tools_registry").select("*").order("category").order("display_name");
4272
4306
  if (error) {
4273
- console.error(chalk19.red(`Failed to fetch tools: ${error.message}`));
4307
+ console.error(chalk18.red(`Failed to fetch tools: ${error.message}`));
4274
4308
  process.exit(1);
4275
4309
  }
4276
4310
  if (!tools?.length) {
4277
- console.log(chalk19.yellow("No tools in the registry."));
4311
+ console.log(chalk18.yellow("No tools in the registry."));
4278
4312
  return;
4279
4313
  }
4280
4314
  const nameW = Math.max(16, ...tools.map((t) => t.display_name.length));
@@ -4288,7 +4322,7 @@ async function adminListToolsCommand() {
4288
4322
  "Beta".padEnd(betaW),
4289
4323
  "Last Checked"
4290
4324
  ].join(" ");
4291
- console.log(chalk19.bold(header));
4325
+ console.log(chalk18.bold(header));
4292
4326
  console.log("\u2500".repeat(header.length));
4293
4327
  for (const tool of tools) {
4294
4328
  const lastChecked = tool.updated_at ? formatRelative(new Date(tool.updated_at)) : "\u2014";
@@ -4301,7 +4335,7 @@ async function adminListToolsCommand() {
4301
4335
  ].join(" ");
4302
4336
  console.log(row);
4303
4337
  }
4304
- console.log(chalk19.grey(`
4338
+ console.log(chalk18.grey(`
4305
4339
  ${tools.length} tool(s) in registry.`));
4306
4340
  }
4307
4341
  function formatRelative(date) {
@@ -4319,7 +4353,7 @@ function formatRelative(date) {
4319
4353
 
4320
4354
  // dist/commands/admin-fetch-versions.js
4321
4355
  init_auth();
4322
- import chalk20 from "chalk";
4356
+ import chalk19 from "chalk";
4323
4357
 
4324
4358
  // dist/commands/version-sources.js
4325
4359
  var VERSION_SOURCES = {
@@ -4340,7 +4374,15 @@ var VERSION_SOURCES = {
4340
4374
  "turborepo": { type: "npm", package: "turbo" },
4341
4375
  "npm": { type: "npm", package: "npm" },
4342
4376
  "node": { type: "github", repo: "nodejs/node" },
4343
- "supabase-cli": { type: "npm", package: "supabase" }
4377
+ "supabase-cli": { type: "npm", package: "supabase" },
4378
+ "vercel-cli": { type: "npm", package: "vercel" },
4379
+ "@sentry/node": { type: "npm", package: "@sentry/node" },
4380
+ "react-markdown": { type: "npm", package: "react-markdown" },
4381
+ "rehype-sanitize": { type: "npm", package: "rehype-sanitize" },
4382
+ "@react-email/render": { type: "npm", package: "@react-email/render" },
4383
+ "@anthropic-ai/sdk": { type: "npm", package: "@anthropic-ai/sdk" },
4384
+ "@xyflow/react": { type: "npm", package: "@xyflow/react" },
4385
+ "fflate": { type: "npm", package: "fflate" }
4344
4386
  };
4345
4387
 
4346
4388
  // dist/commands/admin-fetch-versions.js
@@ -4348,11 +4390,11 @@ async function adminFetchVersionsCommand() {
4348
4390
  const { supabase } = await getAuthenticatedClient();
4349
4391
  const { data: tools, error } = await supabase.from("tools_registry").select("*").order("display_name");
4350
4392
  if (error) {
4351
- console.error(chalk20.red(`Failed to fetch tools: ${error.message}`));
4393
+ console.error(chalk19.red(`Failed to fetch tools: ${error.message}`));
4352
4394
  process.exit(1);
4353
4395
  }
4354
4396
  if (!tools?.length) {
4355
- console.log(chalk20.yellow("No tools in the registry."));
4397
+ console.log(chalk19.yellow("No tools in the registry."));
4356
4398
  }
4357
4399
  const { data: allProjectToolings } = await supabase.from("project_toolings").select("tool_name, detection_source").is("tool_id", null);
4358
4400
  const registeredNames = new Set((tools ?? []).map((t) => t.name));
@@ -4363,7 +4405,7 @@ async function adminFetchVersionsCommand() {
4363
4405
  }
4364
4406
  }
4365
4407
  if (unregisteredNames.size > 0) {
4366
- console.log(chalk20.blue(`Auto-registering ${unregisteredNames.size} unverified package(s)...
4408
+ console.log(chalk19.blue(`Auto-registering ${unregisteredNames.size} unverified package(s)...
4367
4409
  `));
4368
4410
  for (const name of unregisteredNames) {
4369
4411
  const displayName = name;
@@ -4381,10 +4423,10 @@ async function adminFetchVersionsCommand() {
4381
4423
  }
4382
4424
  }
4383
4425
  if (!tools?.length) {
4384
- console.log(chalk20.yellow("No tools to fetch."));
4426
+ console.log(chalk19.yellow("No tools to fetch."));
4385
4427
  return;
4386
4428
  }
4387
- console.log(chalk20.bold(`Fetching versions for ${tools.length} tool(s)...
4429
+ console.log(chalk19.bold(`Fetching versions for ${tools.length} tool(s)...
4388
4430
  `));
4389
4431
  const results = await Promise.all(tools.map(async (tool) => {
4390
4432
  const source = VERSION_SOURCES[tool.name] ?? { type: "npm", package: tool.name };
@@ -4423,10 +4465,10 @@ async function adminFetchVersionsCommand() {
4423
4465
  "Beta".padEnd(betaW),
4424
4466
  "Status".padEnd(statusW)
4425
4467
  ].join(" ");
4426
- console.log(chalk20.bold(header));
4468
+ console.log(chalk19.bold(header));
4427
4469
  console.log("\u2500".repeat(header.length));
4428
4470
  for (const result of results) {
4429
- const statusColour = result.status === "updated" ? chalk20.green : result.status === "unchanged" ? chalk20.grey : result.status === "failed" ? chalk20.red : chalk20.yellow;
4471
+ const statusColour = result.status === "updated" ? chalk19.green : result.status === "unchanged" ? chalk19.grey : result.status === "failed" ? chalk19.red : chalk19.yellow;
4430
4472
  const row = [
4431
4473
  result.displayName.padEnd(nameW),
4432
4474
  (result.stable ?? "\u2014").padEnd(stableW),
@@ -4439,8 +4481,8 @@ async function adminFetchVersionsCommand() {
4439
4481
  const unchanged = results.filter((r) => r.status === "unchanged").length;
4440
4482
  const failed = results.filter((r) => r.status === "failed").length;
4441
4483
  const noSource = results.filter((r) => r.status === "no source").length;
4442
- console.log(chalk20.grey(`
4443
- ${results.length} tool(s): `) + chalk20.green(`${updated} updated`) + ", " + chalk20.grey(`${unchanged} unchanged`) + ", " + chalk20.red(`${failed} failed`) + ", " + chalk20.yellow(`${noSource} no source`));
4484
+ console.log(chalk19.grey(`
4485
+ ${results.length} tool(s): `) + chalk19.green(`${updated} updated`) + ", " + chalk19.grey(`${unchanged} unchanged`) + ", " + chalk19.red(`${failed} failed`) + ", " + chalk19.yellow(`${noSource} no source`));
4444
4486
  }
4445
4487
  async function fetchVersions(source) {
4446
4488
  const controller = new AbortController();
@@ -4498,10 +4540,10 @@ async function fetchGitHubVersions(repo, signal) {
4498
4540
  init_mcp_watch();
4499
4541
 
4500
4542
  // dist/commands/init-manifest.js
4501
- import { resolve as resolve8, join as join16, relative as relative4, dirname as dirname4 } from "node:path";
4543
+ import { resolve as resolve7, join as join16, relative as relative4, dirname as dirname4 } from "node:path";
4502
4544
  import { readFile as readFile13, writeFile as writeFile5, mkdir as mkdir4 } from "node:fs/promises";
4503
- import { existsSync as existsSync14 } from "node:fs";
4504
- import chalk22 from "chalk";
4545
+ import { existsSync as existsSync13 } from "node:fs";
4546
+ import chalk21 from "chalk";
4505
4547
  var SECRET_PATTERNS = [
4506
4548
  /_KEY$/i,
4507
4549
  /_SECRET$/i,
@@ -4518,7 +4560,7 @@ async function discoverEnvFiles(projectRoot) {
4518
4560
  const apps = [];
4519
4561
  for (const envName of [".env", ".env.local", ".env.example"]) {
4520
4562
  const envPath = join16(projectRoot, envName);
4521
- if (existsSync14(envPath)) {
4563
+ if (existsSync13(envPath)) {
4522
4564
  const vars = await extractVarNames(envPath);
4523
4565
  if (vars.length > 0) {
4524
4566
  apps.push({ name: "root", envFilePath: envName, vars });
@@ -4529,11 +4571,11 @@ async function discoverEnvFiles(projectRoot) {
4529
4571
  const subdirs = ["web", "cli", "api", "app", "server", "packages"];
4530
4572
  for (const sub of subdirs) {
4531
4573
  const subDir = join16(projectRoot, sub);
4532
- if (!existsSync14(subDir))
4574
+ if (!existsSync13(subDir))
4533
4575
  continue;
4534
4576
  for (const envName of [".env.local", ".env", ".env.example"]) {
4535
4577
  const envPath = join16(subDir, envName);
4536
- if (existsSync14(envPath)) {
4578
+ if (existsSync13(envPath)) {
4537
4579
  const vars = await extractVarNames(envPath);
4538
4580
  if (vars.length > 0) {
4539
4581
  apps.push({
@@ -4593,33 +4635,33 @@ function generateManifest(apps) {
4593
4635
  return lines.join("\n");
4594
4636
  }
4595
4637
  async function initManifestCommand() {
4596
- const projectRoot = resolve8(process.cwd());
4638
+ const projectRoot = resolve7(process.cwd());
4597
4639
  const manifestPath = join16(projectRoot, "docs", "reference", "env-manifest.md");
4598
- if (existsSync14(manifestPath)) {
4599
- console.log(chalk22.yellow(`Manifest already exists: ${relative4(projectRoot, manifestPath)}`));
4600
- console.log(chalk22.dim("Edit it directly to make changes."));
4640
+ if (existsSync13(manifestPath)) {
4641
+ console.log(chalk21.yellow(`Manifest already exists: ${relative4(projectRoot, manifestPath)}`));
4642
+ console.log(chalk21.dim("Edit it directly to make changes."));
4601
4643
  return;
4602
4644
  }
4603
- console.log(chalk22.blue("Scanning for .env files...\n"));
4645
+ console.log(chalk21.blue("Scanning for .env files...\n"));
4604
4646
  const apps = await discoverEnvFiles(projectRoot);
4605
4647
  if (apps.length === 0) {
4606
- console.log(chalk22.yellow("No .env files found. Create a manifest manually at:"));
4607
- console.log(chalk22.cyan(` docs/reference/env-manifest.md`));
4648
+ console.log(chalk21.yellow("No .env files found. Create a manifest manually at:"));
4649
+ console.log(chalk21.cyan(` docs/reference/env-manifest.md`));
4608
4650
  return;
4609
4651
  }
4610
4652
  for (const app of apps) {
4611
- console.log(` ${chalk22.green(app.name)}: ${app.envFilePath} (${app.vars.length} vars)`);
4653
+ console.log(` ${chalk21.green(app.name)}: ${app.envFilePath} (${app.vars.length} vars)`);
4612
4654
  }
4613
4655
  const content = generateManifest(apps);
4614
4656
  const dir = dirname4(manifestPath);
4615
- if (!existsSync14(dir)) {
4657
+ if (!existsSync13(dir)) {
4616
4658
  await mkdir4(dir, { recursive: true });
4617
4659
  }
4618
4660
  await writeFile5(manifestPath, content, "utf-8");
4619
- console.log(chalk22.green(`
4661
+ console.log(chalk21.green(`
4620
4662
  Manifest created: ${relative4(projectRoot, manifestPath)}`));
4621
- console.log(chalk22.dim("Review and edit the file \u2014 it is your source of truth."));
4622
- console.log(chalk22.dim("Then run `md4ai scan` to verify against your environments."));
4663
+ console.log(chalk21.dim("Review and edit the file \u2014 it is your source of truth."));
4664
+ console.log(chalk21.dim("Then run `md4ai scan` to verify against your environments."));
4623
4665
  }
4624
4666
 
4625
4667
  // dist/commands/update.js
@@ -5028,32 +5070,39 @@ init_check_update();
5028
5070
  var program = new Command();
5029
5071
  program.name("md4ai").description("MD4AI \u2014 Claude tooling visualiser").version(CURRENT_VERSION).addHelpText("after", `
5030
5072
  Quick start:
5031
- md4ai start Check for updates, scan project, and start MCP monitoring
5032
- md4ai Same as md4ai start (when run in a linked project folder)`);
5073
+ md4ai login Log in with email and password
5074
+ md4ai link <project-id> Link this folder to a dashboard project
5075
+ md4ai scan Scan project and push to dashboard
5076
+
5077
+ Daily use:
5078
+ md4ai Scan + start MCP monitoring (all-in-one)
5079
+
5080
+ More:
5081
+ md4ai config ... Configuration (tokens, settings)
5082
+ md4ai doppler ... Doppler secrets integration
5083
+ md4ai admin ... Advanced tools (simulate, print, import, etc.)`);
5033
5084
  program.command("start").description("Check for updates, scan project, and start MCP monitoring \u2014 the all-in-one command").action(startCommand);
5034
5085
  program.command("login").description("Log in to MD4AI with email and password").action(loginCommand);
5035
5086
  program.command("logout").description("Log out and clear stored credentials").action(logoutCommand);
5036
5087
  program.command("status").description("Show login status, device count, folder count, last sync").action(statusCommand);
5037
- program.command("add-folder").description("Create a new Claude folder").action(addFolderCommand);
5038
- program.command("add-device").description("Add a device path to a Claude folder").action(addDeviceCommand);
5039
- program.command("list-devices").description("List all devices and their linked folders").action(listDevicesCommand);
5040
5088
  program.command("link <project-id>").description("Link the current directory to a project created in the web dashboard").action(linkCommand);
5041
5089
  program.command("scan [path]").description("Scan Claude project files and push results to your dashboard").option("--offline", "Skip pushing to Supabase").action(mapCommand);
5042
- program.command("simulate <prompt>").description("Show which files Claude would load for a given prompt").action(simulateCommand);
5043
- program.command("print <title>").description("Generate a printable wall-chart HTML from the last scan").action(printCommand);
5044
- program.command("sync").description("Re-push latest scan data to Supabase").option("--all", "Sync all folders on all devices").action(syncCommand);
5045
- program.command("import <zipfile>").description("Import a Claude setup bundle exported from the web dashboard").action(importBundleCommand);
5046
- 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);
5047
- program.command("check-update").description("Check if a newer version of md4ai is available").action(checkForUpdate);
5048
5090
  program.command("mcp-watch").description("Monitor MCP server status on this device (runs until Ctrl+C)").action(mcpWatchCommand);
5049
- program.command("init-manifest").description("Scaffold a starter env-manifest.md from detected .env files").action(initManifestCommand);
5091
+ 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);
5050
5092
  var config = program.command("config").description("Manage CLI configuration");
5051
5093
  config.command("set <key> <value>").description("Set a configuration value (e.g. vercel-token)").action(configSetCommand);
5052
5094
  var doppler = program.command("doppler").description("Manage Doppler integration");
5053
5095
  doppler.command("connect").description("Configure Doppler token for secrets scanning").action(dopplerConnectCommand);
5054
5096
  doppler.command("disconnect").description("Remove stored Doppler token").action(dopplerDisconnectCommand);
5055
5097
  doppler.command("set-project <slug>").description("Override Doppler project slug for the current linked project").action(dopplerSetProjectCommand);
5056
- var admin = program.command("admin").description("Admin commands for managing the tools registry");
5098
+ var admin = program.command("admin").description("Advanced tools \u2014 simulate, print, import, device management, registry");
5099
+ admin.command("simulate <prompt>").description("Show which files Claude would load for a given prompt").action(simulateCommand);
5100
+ admin.command("print <title>").description("Generate a printable wall-chart HTML from the last scan").action(printCommand);
5101
+ admin.command("import <zipfile>").description("Import a Claude setup bundle exported from the web dashboard").action(importBundleCommand);
5102
+ admin.command("init-manifest").description("Scaffold a starter env-manifest.md from detected .env files").action(initManifestCommand);
5103
+ admin.command("list-devices").description("List all devices and their linked folders").action(listDevicesCommand);
5104
+ admin.command("add-folder").description("Create a new Claude folder").action(addFolderCommand);
5105
+ admin.command("add-device").description("Add a device path to a Claude folder").action(addDeviceCommand);
5057
5106
  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);
5058
5107
  admin.command("list-tools").description("List all tools in the master registry").action(adminListToolsCommand);
5059
5108
  admin.command("fetch-versions").description("Fetch latest stable and beta versions from npm and GitHub").action(adminFetchVersionsCommand);
@@ -5063,7 +5112,7 @@ if (process.argv.length <= 2) {
5063
5112
  } else {
5064
5113
  program.parseAsync().then(() => {
5065
5114
  const ran = program.args[0];
5066
- const skipAutoCheck = ["update", "check-update", "mcp-watch", "start"];
5115
+ const skipAutoCheck = ["update", "mcp-watch", "start"];
5067
5116
  if (!skipAutoCheck.includes(ran)) {
5068
5117
  autoCheckForUpdate();
5069
5118
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md4ai",
3
- "version": "0.17.2",
3
+ "version": "0.18.0",
4
4
  "description": "CLI for MD4AI — scan Claude projects and sync to your dashboard",
5
5
  "type": "module",
6
6
  "bin": {