md4ai 0.5.2 → 0.6.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.
@@ -1001,9 +1001,9 @@ function identifyRoots(allFiles, projectRoot) {
1001
1001
  return roots;
1002
1002
  }
1003
1003
  async function readClaudeConfigFiles(projectRoot) {
1004
- const { readFile: readFile7, stat, glob } = await import("node:fs/promises");
1005
- const { join: join11, relative: relative2 } = await import("node:path");
1006
- const { existsSync: existsSync10 } = await import("node:fs");
1004
+ const { readFile: readFile8, stat, glob } = await import("node:fs/promises");
1005
+ const { join: join12, relative: relative2 } = await import("node:path");
1006
+ const { existsSync: existsSync11 } = await import("node:fs");
1007
1007
  const configPatterns = [
1008
1008
  "CLAUDE.md",
1009
1009
  ".claude/CLAUDE.md",
@@ -1013,11 +1013,11 @@ async function readClaudeConfigFiles(projectRoot) {
1013
1013
  ];
1014
1014
  const files = [];
1015
1015
  for (const pattern of configPatterns) {
1016
- for await (const fullPath of glob(join11(projectRoot, pattern))) {
1017
- if (!existsSync10(fullPath))
1016
+ for await (const fullPath of glob(join12(projectRoot, pattern))) {
1017
+ if (!existsSync11(fullPath))
1018
1018
  continue;
1019
1019
  try {
1020
- const content = await readFile7(fullPath, "utf-8");
1020
+ const content = await readFile8(fullPath, "utf-8");
1021
1021
  const fileStat = await stat(fullPath);
1022
1022
  const lastMod = getGitLastModified(fullPath, projectRoot);
1023
1023
  files.push({
@@ -1161,7 +1161,7 @@ function escapeHtml(text) {
1161
1161
 
1162
1162
  // dist/check-update.js
1163
1163
  import chalk8 from "chalk";
1164
- var CURRENT_VERSION = "0.5.2";
1164
+ var CURRENT_VERSION = "0.6.1";
1165
1165
  async function checkForUpdate() {
1166
1166
  try {
1167
1167
  const controller = new AbortController();
@@ -1524,8 +1524,10 @@ async function syncCommand(options) {
1524
1524
 
1525
1525
  // dist/commands/link.js
1526
1526
  import { resolve as resolve4 } from "node:path";
1527
- import { hostname as hostname2, platform as platform2 } from "node:os";
1528
1527
  import chalk13 from "chalk";
1528
+
1529
+ // dist/device-utils.js
1530
+ import { hostname as hostname2, platform as platform2 } from "node:os";
1529
1531
  function detectOs2() {
1530
1532
  const p = platform2();
1531
1533
  if (p === "win32")
@@ -1542,6 +1544,22 @@ function detectDeviceName() {
1542
1544
  const osLabel = os.charAt(0).toUpperCase() + os.slice(1);
1543
1545
  return `${host}-${osLabel}`;
1544
1546
  }
1547
+ async function resolveDeviceId(supabase, userId) {
1548
+ const deviceName = detectDeviceName();
1549
+ const osType = detectOs2();
1550
+ await supabase.from("devices").upsert({
1551
+ user_id: userId,
1552
+ device_name: deviceName,
1553
+ os_type: osType
1554
+ }, { onConflict: "user_id,device_name" });
1555
+ const { data, error } = await supabase.from("devices").select("id").eq("user_id", userId).eq("device_name", deviceName).single();
1556
+ if (error || !data) {
1557
+ throw new Error(`Failed to resolve device ID: ${error?.message ?? "not found"}`);
1558
+ }
1559
+ return data.id;
1560
+ }
1561
+
1562
+ // dist/commands/link.js
1545
1563
  async function linkCommand(projectId) {
1546
1564
  const { supabase, userId } = await getAuthenticatedClient();
1547
1565
  const cwd = resolve4(process.cwd());
@@ -1982,6 +2000,394 @@ async function fetchGitHubVersions(repo, signal) {
1982
2000
  return { stable, beta };
1983
2001
  }
1984
2002
 
2003
+ // dist/commands/mcp-watch.js
2004
+ import chalk18 from "chalk";
2005
+
2006
+ // dist/mcp/read-configs.js
2007
+ import { readFile as readFile7 } from "node:fs/promises";
2008
+ import { join as join11 } from "node:path";
2009
+ import { homedir as homedir7 } from "node:os";
2010
+ import { existsSync as existsSync10 } from "node:fs";
2011
+ import { readdir as readdir3 } from "node:fs/promises";
2012
+ async function readJsonSafe(path) {
2013
+ try {
2014
+ if (!existsSync10(path))
2015
+ return null;
2016
+ const raw = await readFile7(path, "utf-8");
2017
+ return JSON.parse(raw);
2018
+ } catch {
2019
+ return null;
2020
+ }
2021
+ }
2022
+ function extractPackageName(args) {
2023
+ for (const arg of args) {
2024
+ if (arg.startsWith("-"))
2025
+ continue;
2026
+ if (arg.includes("mcp") || arg.startsWith("@"))
2027
+ return arg;
2028
+ }
2029
+ for (const arg of args) {
2030
+ if (!arg.startsWith("-"))
2031
+ return arg;
2032
+ }
2033
+ return null;
2034
+ }
2035
+ function parseServers(data, source) {
2036
+ if (!data?.mcpServers)
2037
+ return [];
2038
+ const entries = [];
2039
+ for (const [name, server] of Object.entries(data.mcpServers)) {
2040
+ const serverType = server.type === "http" ? "http" : "stdio";
2041
+ entries.push({
2042
+ name,
2043
+ source,
2044
+ type: serverType,
2045
+ command: server.command,
2046
+ args: server.args,
2047
+ env: server.env,
2048
+ url: server.url
2049
+ });
2050
+ }
2051
+ return entries;
2052
+ }
2053
+ function parseFlatConfig(data, source) {
2054
+ if (!data)
2055
+ return [];
2056
+ const entries = [];
2057
+ for (const [name, server] of Object.entries(data)) {
2058
+ if (typeof server !== "object" || server === null)
2059
+ continue;
2060
+ if (!server.command && !server.url)
2061
+ continue;
2062
+ const serverType = server.type === "http" ? "http" : "stdio";
2063
+ entries.push({
2064
+ name,
2065
+ source,
2066
+ type: serverType,
2067
+ command: server.command,
2068
+ args: server.args,
2069
+ env: server.env,
2070
+ url: server.url
2071
+ });
2072
+ }
2073
+ return entries;
2074
+ }
2075
+ async function readAllMcpConfigs() {
2076
+ const home = homedir7();
2077
+ const entries = [];
2078
+ const userConfig = await readJsonSafe(join11(home, ".claude.json"));
2079
+ entries.push(...parseServers(userConfig, "global"));
2080
+ const globalMcp = await readJsonSafe(join11(home, ".claude", "mcp.json"));
2081
+ entries.push(...parseServers(globalMcp, "global"));
2082
+ const cwdMcp = await readJsonSafe(join11(process.cwd(), ".mcp.json"));
2083
+ entries.push(...parseServers(cwdMcp, "project"));
2084
+ const pluginsBase = join11(home, ".claude", "plugins", "marketplaces");
2085
+ if (existsSync10(pluginsBase)) {
2086
+ try {
2087
+ const marketplaces = await readdir3(pluginsBase, { withFileTypes: true });
2088
+ for (const mp of marketplaces) {
2089
+ if (!mp.isDirectory())
2090
+ continue;
2091
+ const extDir = join11(pluginsBase, mp.name, "external_plugins");
2092
+ if (!existsSync10(extDir))
2093
+ continue;
2094
+ const plugins = await readdir3(extDir, { withFileTypes: true });
2095
+ for (const plugin of plugins) {
2096
+ if (!plugin.isDirectory())
2097
+ continue;
2098
+ const pluginMcp = await readJsonSafe(join11(extDir, plugin.name, ".mcp.json"));
2099
+ entries.push(...parseServers(pluginMcp, "plugin"));
2100
+ }
2101
+ }
2102
+ } catch {
2103
+ }
2104
+ }
2105
+ const cacheBase = join11(home, ".claude", "plugins", "cache");
2106
+ if (existsSync10(cacheBase)) {
2107
+ try {
2108
+ const registries = await readdir3(cacheBase, { withFileTypes: true });
2109
+ for (const reg of registries) {
2110
+ if (!reg.isDirectory())
2111
+ continue;
2112
+ const regDir = join11(cacheBase, reg.name);
2113
+ const plugins = await readdir3(regDir, { withFileTypes: true });
2114
+ for (const plugin of plugins) {
2115
+ if (!plugin.isDirectory())
2116
+ continue;
2117
+ const versionDirs = await readdir3(join11(regDir, plugin.name), { withFileTypes: true });
2118
+ for (const ver of versionDirs) {
2119
+ if (!ver.isDirectory())
2120
+ continue;
2121
+ const mcpPath = join11(regDir, plugin.name, ver.name, ".mcp.json");
2122
+ const flatData = await readJsonSafe(mcpPath);
2123
+ if (flatData) {
2124
+ entries.push(...parseFlatConfig(flatData, "plugin"));
2125
+ break;
2126
+ }
2127
+ }
2128
+ }
2129
+ }
2130
+ } catch {
2131
+ }
2132
+ }
2133
+ const seen = /* @__PURE__ */ new Set();
2134
+ const deduped = [];
2135
+ for (const entry of entries) {
2136
+ if (seen.has(entry.name))
2137
+ continue;
2138
+ seen.add(entry.name);
2139
+ deduped.push(entry);
2140
+ }
2141
+ return deduped;
2142
+ }
2143
+
2144
+ // dist/mcp/scan-processes.js
2145
+ import { execFileSync as execFileSync4 } from "node:child_process";
2146
+ function parsePsOutput(output) {
2147
+ const lines = output.trim().split("\n").slice(1);
2148
+ const entries = [];
2149
+ for (const line of lines) {
2150
+ const trimmed = line.trim();
2151
+ const match = trimmed.match(/^\s*(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(.+)$/);
2152
+ if (!match)
2153
+ continue;
2154
+ entries.push({
2155
+ pid: parseInt(match[1], 10),
2156
+ tty: match[2],
2157
+ etimeRaw: match[3],
2158
+ rss: parseInt(match[4], 10),
2159
+ args: match[5]
2160
+ });
2161
+ }
2162
+ return entries;
2163
+ }
2164
+ function parseEtime(etime) {
2165
+ let days = 0;
2166
+ let rest = etime;
2167
+ if (rest.includes("-")) {
2168
+ const [d, r] = rest.split("-");
2169
+ days = parseInt(d, 10);
2170
+ rest = r;
2171
+ }
2172
+ const parts = rest.split(":").map(Number);
2173
+ if (parts.length === 3) {
2174
+ return days * 86400 + parts[0] * 3600 + parts[1] * 60 + parts[2];
2175
+ } else if (parts.length === 2) {
2176
+ return days * 86400 + parts[0] * 60 + parts[1];
2177
+ }
2178
+ return days * 86400 + (parts[0] ?? 0);
2179
+ }
2180
+ function findProcessesForConfig(config, processes) {
2181
+ if (config.type === "http")
2182
+ return [];
2183
+ const packageName = config.args ? extractPackageName(config.args) : null;
2184
+ const matches = [];
2185
+ for (const proc of processes) {
2186
+ let matched = false;
2187
+ if (packageName && proc.args.includes(packageName)) {
2188
+ matched = true;
2189
+ } else if (config.command === "node" && config.args?.[0]) {
2190
+ if (proc.args.includes(config.args[0])) {
2191
+ matched = true;
2192
+ }
2193
+ }
2194
+ if (matched) {
2195
+ matches.push({
2196
+ pid: proc.pid,
2197
+ tty: proc.tty === "?" ? "" : proc.tty,
2198
+ uptimeSeconds: parseEtime(proc.etimeRaw),
2199
+ memoryMb: Math.round(proc.rss / 1024),
2200
+ commandLine: proc.args
2201
+ });
2202
+ }
2203
+ }
2204
+ return matches;
2205
+ }
2206
+ function getProcessTable() {
2207
+ try {
2208
+ const output = execFileSync4("ps", ["-eo", "pid,tty,etime,rss,args"], {
2209
+ encoding: "utf-8",
2210
+ timeout: 5e3
2211
+ });
2212
+ return parsePsOutput(output);
2213
+ } catch {
2214
+ return [];
2215
+ }
2216
+ }
2217
+
2218
+ // dist/commands/mcp-watch.js
2219
+ var POLL_INTERVAL_MS = 3e4;
2220
+ function checkEnvVars(config) {
2221
+ const required = config.env ? Object.keys(config.env) : [];
2222
+ const missing = required.filter((key) => !process.env[key]);
2223
+ return { required, missing };
2224
+ }
2225
+ function buildRows(configs) {
2226
+ const processes = getProcessTable();
2227
+ const rows = [];
2228
+ for (const config of configs) {
2229
+ const { required, missing } = checkEnvVars(config);
2230
+ const packageName = config.args ? extractPackageName(config.args) : null;
2231
+ if (config.type === "http") {
2232
+ rows.push({
2233
+ server_name: config.name,
2234
+ config_source: config.source,
2235
+ server_type: "http",
2236
+ command: null,
2237
+ package_name: null,
2238
+ http_url: config.url ?? null,
2239
+ status: "stopped",
2240
+ pid: null,
2241
+ session_tty: null,
2242
+ uptime_seconds: null,
2243
+ memory_mb: null,
2244
+ env_vars_required: required.length ? required : null,
2245
+ env_vars_missing: missing.length ? missing : null,
2246
+ error_detail: "HTTP server \u2014 cannot verify from CLI"
2247
+ });
2248
+ continue;
2249
+ }
2250
+ if (missing.length > 0) {
2251
+ rows.push({
2252
+ server_name: config.name,
2253
+ config_source: config.source,
2254
+ server_type: "stdio",
2255
+ command: config.command ?? null,
2256
+ package_name: packageName,
2257
+ http_url: null,
2258
+ status: "error",
2259
+ pid: null,
2260
+ session_tty: null,
2261
+ uptime_seconds: null,
2262
+ memory_mb: null,
2263
+ env_vars_required: required,
2264
+ env_vars_missing: missing,
2265
+ error_detail: `Missing env: ${missing.join(", ")}`
2266
+ });
2267
+ continue;
2268
+ }
2269
+ const matches = findProcessesForConfig(config, processes);
2270
+ if (matches.length === 0) {
2271
+ rows.push({
2272
+ server_name: config.name,
2273
+ config_source: config.source,
2274
+ server_type: "stdio",
2275
+ command: config.command ?? null,
2276
+ package_name: packageName,
2277
+ http_url: null,
2278
+ status: "stopped",
2279
+ pid: null,
2280
+ session_tty: null,
2281
+ uptime_seconds: null,
2282
+ memory_mb: null,
2283
+ env_vars_required: required.length ? required : null,
2284
+ env_vars_missing: null,
2285
+ error_detail: null
2286
+ });
2287
+ } else {
2288
+ for (const match of matches) {
2289
+ rows.push({
2290
+ server_name: config.name,
2291
+ config_source: config.source,
2292
+ server_type: "stdio",
2293
+ command: config.command ?? null,
2294
+ package_name: packageName,
2295
+ http_url: null,
2296
+ status: "running",
2297
+ pid: match.pid,
2298
+ session_tty: match.tty || null,
2299
+ uptime_seconds: match.uptimeSeconds,
2300
+ memory_mb: match.memoryMb,
2301
+ env_vars_required: required.length ? required : null,
2302
+ env_vars_missing: null,
2303
+ error_detail: null
2304
+ });
2305
+ }
2306
+ }
2307
+ }
2308
+ return rows;
2309
+ }
2310
+ function printTable(rows, deviceName) {
2311
+ process.stdout.write("\x1B[2J\x1B[H");
2312
+ console.log(chalk18.bold.cyan(`
2313
+ MCP Monitor \u2014 ${deviceName}`));
2314
+ console.log(chalk18.dim(` ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \xB7 refreshes every 30s \xB7 Ctrl+C to stop
2315
+ `));
2316
+ const running = rows.filter((r) => r.status === "running");
2317
+ const stopped = rows.filter((r) => r.status === "stopped");
2318
+ const errored = rows.filter((r) => r.status === "error");
2319
+ const byTty = /* @__PURE__ */ new Map();
2320
+ for (const r of running) {
2321
+ const key = r.session_tty ?? "unknown";
2322
+ const list = byTty.get(key) ?? [];
2323
+ list.push(r);
2324
+ byTty.set(key, list);
2325
+ }
2326
+ if (byTty.size > 0) {
2327
+ for (const [tty, servers] of byTty) {
2328
+ const totalMem = servers.reduce((sum, s) => sum + (s.memory_mb ?? 0), 0);
2329
+ console.log(chalk18.green(` Session ${tty}`) + chalk18.dim(` \u2014 ${servers.length} server${servers.length !== 1 ? "s" : ""} \xB7 ${totalMem} MB`));
2330
+ for (const s of servers) {
2331
+ const uptime = formatUptime(s.uptime_seconds ?? 0);
2332
+ console.log(` ${chalk18.green("\u25CF")} ${s.server_name.padEnd(20)} ${chalk18.dim((s.package_name ?? "").padEnd(30))} ${String(s.memory_mb ?? 0).padStart(4)} MB ${uptime}`);
2333
+ }
2334
+ console.log("");
2335
+ }
2336
+ }
2337
+ if (stopped.length > 0 || errored.length > 0) {
2338
+ const notRunning = [...stopped, ...errored];
2339
+ console.log(chalk18.yellow(` Not Running (${notRunning.length})`));
2340
+ for (const s of notRunning) {
2341
+ const icon = s.status === "error" ? chalk18.red("\u2717") : chalk18.yellow("\u25CB");
2342
+ const detail = s.error_detail ? chalk18.dim(` \u2014 ${s.error_detail}`) : "";
2343
+ console.log(` ${icon} ${s.server_name.padEnd(20)} ${chalk18.dim(s.config_source.padEnd(10))}${detail}`);
2344
+ }
2345
+ console.log("");
2346
+ }
2347
+ if (rows.length === 0) {
2348
+ console.log(chalk18.yellow(" No MCP servers configured."));
2349
+ console.log(chalk18.dim(" Configure servers in ~/.claude/mcp.json or .mcp.json\n"));
2350
+ }
2351
+ }
2352
+ function formatUptime(seconds) {
2353
+ if (seconds < 60)
2354
+ return `${seconds}s`;
2355
+ if (seconds < 3600)
2356
+ return `${Math.floor(seconds / 60)}m`;
2357
+ const h = Math.floor(seconds / 3600);
2358
+ const m = Math.floor(seconds % 3600 / 60);
2359
+ return `${h}h ${m}m`;
2360
+ }
2361
+ async function mcpWatchCommand() {
2362
+ const { supabase, userId } = await getAuthenticatedClient();
2363
+ const deviceId = await resolveDeviceId(supabase, userId);
2364
+ const deviceName = detectDeviceName();
2365
+ console.log(chalk18.blue(`Starting MCP monitor for ${deviceName}...`));
2366
+ async function cycle() {
2367
+ const configs = await readAllMcpConfigs();
2368
+ const rows = buildRows(configs);
2369
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2370
+ await supabase.from("mcp_server_status").delete().eq("device_id", deviceId);
2371
+ if (rows.length > 0) {
2372
+ await supabase.from("mcp_server_status").insert(rows.map((row) => ({
2373
+ device_id: deviceId,
2374
+ ...row,
2375
+ checked_at: now
2376
+ })));
2377
+ }
2378
+ printTable(rows, deviceName);
2379
+ }
2380
+ await cycle();
2381
+ const interval = setInterval(cycle, POLL_INTERVAL_MS);
2382
+ const shutdown = () => {
2383
+ clearInterval(interval);
2384
+ console.log(chalk18.dim("\nMCP monitor stopped."));
2385
+ process.exit(0);
2386
+ };
2387
+ process.on("SIGINT", shutdown);
2388
+ process.on("SIGTERM", shutdown);
2389
+ }
2390
+
1985
2391
  // dist/index.js
1986
2392
  var program = new Command();
1987
2393
  program.name("md4ai").description("MD4AI \u2014 Claude tooling visualiser").version(CURRENT_VERSION).addHelpText("after", `
@@ -2003,6 +2409,7 @@ program.command("print <title>").description("Generate a printable wall-chart HT
2003
2409
  program.command("sync").description("Re-push latest scan data to Supabase").option("--all", "Sync all folders on all devices").action(syncCommand);
2004
2410
  program.command("import <zipfile>").description("Import a Claude setup bundle exported from the web dashboard").action(importBundleCommand);
2005
2411
  program.command("check-update").description("Check if a newer version of md4ai is available").action(checkForUpdate);
2412
+ program.command("mcp-watch").description("Monitor MCP server status on this device (runs until Ctrl+C)").action(mcpWatchCommand);
2006
2413
  var admin = program.command("admin").description("Admin commands for managing the tools registry");
2007
2414
  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);
2008
2415
  admin.command("list-tools").description("List all tools in the master registry").action(adminListToolsCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md4ai",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "description": "CLI for MD4AI — scan Claude projects and sync to your dashboard",
5
5
  "type": "module",
6
6
  "bin": {