md4ai 0.7.1 → 0.7.3

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 +1777 -1451
  2. package/package.json +1 -1
@@ -1,24 +1,43 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
2
11
 
3
- // dist/index.js
4
- import { Command } from "commander";
12
+ // ../packages/shared/dist/types.js
13
+ var init_types = __esm({
14
+ "../packages/shared/dist/types.js"() {
15
+ "use strict";
16
+ }
17
+ });
5
18
 
6
19
  // ../packages/shared/dist/constants.js
7
- var SUPABASE_URL = "https://gkrfwmwlfnixeffhwwju.supabase.co";
8
- var ROOT_FILES = [
9
- "CLAUDE.md",
10
- ".claude/settings.json",
11
- ".claude/settings.local.json"
12
- ];
13
- var GLOBAL_ROOT_FILES = [
14
- "~/.claude/CLAUDE.md",
15
- "~/.claude/settings.json",
16
- "~/.claude/settings.local.json"
17
- ];
18
- var STALE_THRESHOLD_DAYS = 90;
19
- var CONFIG_DIR = ".md4ai";
20
- var CREDENTIALS_FILE = "credentials.json";
21
- var STATE_FILE = "state.json";
20
+ var SUPABASE_URL, ROOT_FILES, GLOBAL_ROOT_FILES, STALE_THRESHOLD_DAYS, CONFIG_DIR, CREDENTIALS_FILE, STATE_FILE;
21
+ var init_constants = __esm({
22
+ "../packages/shared/dist/constants.js"() {
23
+ "use strict";
24
+ SUPABASE_URL = "https://gkrfwmwlfnixeffhwwju.supabase.co";
25
+ ROOT_FILES = [
26
+ "CLAUDE.md",
27
+ ".claude/settings.json",
28
+ ".claude/settings.local.json"
29
+ ];
30
+ GLOBAL_ROOT_FILES = [
31
+ "~/.claude/CLAUDE.md",
32
+ "~/.claude/settings.json",
33
+ "~/.claude/settings.local.json"
34
+ ];
35
+ STALE_THRESHOLD_DAYS = 90;
36
+ CONFIG_DIR = ".md4ai";
37
+ CREDENTIALS_FILE = "credentials.json";
38
+ STATE_FILE = "state.json";
39
+ }
40
+ });
22
41
 
23
42
  // ../packages/shared/dist/supabase.js
24
43
  import { createClient } from "@supabase/supabase-js";
@@ -26,15 +45,28 @@ function createSupabaseClient(anonKey, accessToken) {
26
45
  const options = accessToken ? { global: { headers: { Authorization: `Bearer ${accessToken}` } } } : {};
27
46
  return createClient(SUPABASE_URL, anonKey, options);
28
47
  }
48
+ var init_supabase = __esm({
49
+ "../packages/shared/dist/supabase.js"() {
50
+ "use strict";
51
+ init_constants();
52
+ }
53
+ });
54
+
55
+ // ../packages/shared/dist/index.js
56
+ var init_dist = __esm({
57
+ "../packages/shared/dist/index.js"() {
58
+ "use strict";
59
+ init_types();
60
+ init_constants();
61
+ init_supabase();
62
+ }
63
+ });
29
64
 
30
65
  // dist/config.js
31
66
  import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
32
67
  import { join } from "node:path";
33
68
  import { homedir } from "node:os";
34
69
  import { existsSync } from "node:fs";
35
- var configPath = join(homedir(), CONFIG_DIR);
36
- var credentialsPath = join(configPath, CREDENTIALS_FILE);
37
- var statePath = join(configPath, STATE_FILE);
38
70
  async function ensureConfigDir() {
39
71
  if (!existsSync(configPath)) {
40
72
  await mkdir(configPath, { recursive: true });
@@ -84,66 +116,16 @@ function getAnonKey() {
84
116
  }
85
117
  return key;
86
118
  }
87
-
88
- // dist/commands/login.js
89
- import chalk from "chalk";
90
- import { input, password } from "@inquirer/prompts";
91
- async function loginCommand() {
92
- console.log(chalk.blue("MD4AI Login\n"));
93
- const email = await input({ message: "Email:" });
94
- const pwd = await password({ message: "Password:" });
95
- const anonKey = getAnonKey();
96
- const supabase = createSupabaseClient(anonKey);
97
- const { data, error } = await supabase.auth.signInWithPassword({
98
- email,
99
- password: pwd
100
- });
101
- if (error) {
102
- console.error(chalk.red(`Login failed: ${error.message}`));
103
- process.exit(1);
104
- }
105
- await saveCredentials({
106
- accessToken: data.session.access_token,
107
- refreshToken: data.session.refresh_token,
108
- expiresAt: Date.now() + data.session.expires_in * 1e3,
109
- userId: data.user.id,
110
- email: data.user.email ?? email
111
- });
112
- console.log(chalk.green(`
113
- Logged in as ${data.user.email}`));
114
- }
115
-
116
- // dist/commands/logout.js
117
- import chalk2 from "chalk";
118
- async function logoutCommand() {
119
- await clearCredentials();
120
- console.log(chalk2.green("Logged out successfully."));
121
- }
122
-
123
- // dist/commands/status.js
124
- import chalk3 from "chalk";
125
- async function statusCommand() {
126
- const creds = await loadCredentials();
127
- if (!creds?.accessToken) {
128
- console.log(chalk3.yellow("Not logged in. Run: md4ai login"));
129
- return;
130
- }
131
- console.log(chalk3.blue("MD4AI Status\n"));
132
- console.log(` User: ${creds.email}`);
133
- console.log(` Expires: ${new Date(creds.expiresAt).toLocaleString()}`);
134
- try {
135
- const anonKey = getAnonKey();
136
- const supabase = createSupabaseClient(anonKey, creds.accessToken);
137
- const { count: folderCount } = await supabase.from("claude_folders").select("*", { count: "exact", head: true });
138
- const { count: deviceCount } = await supabase.from("device_paths").select("*", { count: "exact", head: true });
139
- console.log(` Folders: ${folderCount ?? 0}`);
140
- console.log(` Devices: ${deviceCount ?? 0}`);
141
- const state = await loadState();
142
- console.log(` Last sync: ${state.lastSyncAt ?? "never"}`);
143
- } catch {
144
- console.log(chalk3.yellow(" (Could not fetch remote data)"));
119
+ var configPath, credentialsPath, statePath;
120
+ var init_config = __esm({
121
+ "dist/config.js"() {
122
+ "use strict";
123
+ init_dist();
124
+ configPath = join(homedir(), CONFIG_DIR);
125
+ credentialsPath = join(configPath, CREDENTIALS_FILE);
126
+ statePath = join(configPath, STATE_FILE);
145
127
  }
146
- }
128
+ });
147
129
 
148
130
  // dist/auth.js
149
131
  import chalk4 from "chalk";
@@ -161,180 +143,19 @@ async function getAuthenticatedClient() {
161
143
  const supabase = createSupabaseClient(anonKey, creds.accessToken);
162
144
  return { supabase, userId: creds.userId };
163
145
  }
164
-
165
- // dist/commands/add-folder.js
166
- import chalk5 from "chalk";
167
- import { input as input2, select } from "@inquirer/prompts";
168
- async function addFolderCommand() {
169
- const { supabase, userId } = await getAuthenticatedClient();
170
- const name = await input2({ message: "Folder name:" });
171
- const description = await input2({ message: "Description (optional):" });
172
- const { data: teams } = await supabase.from("team_members").select("team_id, teams(id, name)").eq("user_id", userId);
173
- const { data: ownedTeams } = await supabase.from("teams").select("id, name").eq("created_by", userId);
174
- const allTeams = /* @__PURE__ */ new Map();
175
- for (const t of ownedTeams ?? []) {
176
- allTeams.set(t.id, t.name);
177
- }
178
- for (const t of teams ?? []) {
179
- const team = t.teams;
180
- if (team)
181
- allTeams.set(team.id, team.name);
182
- }
183
- let teamId = null;
184
- if (allTeams.size > 0) {
185
- const choices = [
186
- { name: "Personal (only you)", value: "__personal__" },
187
- ...Array.from(allTeams.entries()).map(([id, teamName]) => ({
188
- name: teamName,
189
- value: id
190
- }))
191
- ];
192
- const selected = await select({
193
- message: "Assign to a team?",
194
- choices
195
- });
196
- teamId = selected === "__personal__" ? null : selected;
197
- }
198
- const { data, error } = await supabase.from("claude_folders").insert({
199
- user_id: userId,
200
- name,
201
- description: description || null,
202
- team_id: teamId
203
- }).select().single();
204
- if (error) {
205
- console.error(chalk5.red(`Failed to create folder: ${error.message}`));
206
- process.exit(1);
207
- }
208
- const teamLabel = teamId ? `(team: ${allTeams.get(teamId)})` : "(personal)";
209
- console.log(chalk5.green(`
210
- Folder "${name}" created ${teamLabel} (${data.id})`));
211
- }
212
-
213
- // dist/commands/add-device.js
214
- import { resolve } from "node:path";
215
- import { hostname, platform } from "node:os";
216
- import chalk6 from "chalk";
217
- import { input as input3, select as select2 } from "@inquirer/prompts";
218
- function detectOs() {
219
- const p = platform();
220
- if (p === "win32")
221
- return "windows";
222
- if (p === "darwin")
223
- return "macos";
224
- if (p === "linux")
225
- return "linux";
226
- return "other";
227
- }
228
- function suggestDeviceName() {
229
- const os = detectOs();
230
- const host = hostname().split(".")[0];
231
- const osLabel = os.charAt(0).toUpperCase() + os.slice(1);
232
- return `${host}-${osLabel}`;
233
- }
234
- async function addDeviceCommand() {
235
- const { supabase, userId } = await getAuthenticatedClient();
236
- const cwd = resolve(process.cwd());
237
- console.log(chalk6.blue.bold("\n\u{1F4C2} Where is this Claude project?\n"));
238
- const localPath = await input3({
239
- message: "Local project path:",
240
- default: cwd
241
- });
242
- const { data: folders, error: foldersErr } = await supabase.from("claude_folders").select("id, name").order("name");
243
- if (foldersErr || !folders?.length) {
244
- console.error(chalk6.red("No folders found. Run: md4ai add-folder"));
245
- process.exit(1);
246
- }
247
- const folderId = await select2({
248
- message: "Select folder:",
249
- choices: folders.map((f) => ({ name: f.name, value: f.id }))
250
- });
251
- const suggested = suggestDeviceName();
252
- const deviceName = await input3({
253
- message: "Device name:",
254
- default: suggested
255
- });
256
- const osType = await select2({
257
- message: "OS type:",
258
- choices: [
259
- { name: "Windows", value: "windows" },
260
- { name: "macOS", value: "macos" },
261
- { name: "Ubuntu", value: "ubuntu" },
262
- { name: "Linux", value: "linux" },
263
- { name: "Other", value: "other" }
264
- ],
265
- default: detectOs()
266
- });
267
- const description = await input3({ message: "Description (optional):" });
268
- const { error } = await supabase.from("device_paths").insert({
269
- user_id: userId,
270
- folder_id: folderId,
271
- device_name: deviceName,
272
- os_type: osType,
273
- path: localPath,
274
- description: description || null
275
- });
276
- if (error) {
277
- console.error(chalk6.red(`Failed to add device: ${error.message}`));
278
- process.exit(1);
279
- }
280
- console.log(chalk6.green(`
281
- Device "${deviceName}" added to folder.`));
282
- }
283
-
284
- // dist/commands/list-devices.js
285
- import chalk7 from "chalk";
286
- async function listDevicesCommand() {
287
- const { supabase } = await getAuthenticatedClient();
288
- const { data: devices, error } = await supabase.from("device_paths").select(`
289
- id, device_name, os_type, path, last_synced, description,
290
- claude_folders!inner ( name )
291
- `).order("device_name");
292
- if (error) {
293
- console.error(chalk7.red(`Failed to list devices: ${error.message}`));
294
- process.exit(1);
295
- }
296
- if (!devices?.length) {
297
- console.log(chalk7.yellow("No devices found. Run: md4ai add-device"));
298
- return;
299
- }
300
- const grouped = /* @__PURE__ */ new Map();
301
- for (const d of devices) {
302
- const list = grouped.get(d.device_name) ?? [];
303
- list.push(d);
304
- grouped.set(d.device_name, list);
305
- }
306
- for (const [deviceName, entries] of grouped) {
307
- const first = entries[0];
308
- console.log(chalk7.bold(`
309
- ${deviceName}`) + chalk7.dim(` (${first.os_type})`));
310
- for (const entry of entries) {
311
- const folderName = entry.claude_folders?.name ?? "unknown";
312
- const synced = entry.last_synced ? new Date(entry.last_synced).toLocaleString() : "never";
313
- console.log(` ${chalk7.cyan(folderName)} \u2192 ${entry.path}`);
314
- console.log(` Last synced: ${synced}`);
315
- }
146
+ var init_auth = __esm({
147
+ "dist/auth.js"() {
148
+ "use strict";
149
+ init_dist();
150
+ init_config();
316
151
  }
317
- }
318
-
319
- // dist/commands/map.js
320
- import { resolve as resolve3 } from "node:path";
321
- import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
322
- import { existsSync as existsSync7 } from "node:fs";
323
- import chalk9 from "chalk";
324
-
325
- // dist/scanner/index.js
326
- import { readdir as readdir2 } from "node:fs/promises";
327
- import { join as join8, relative as relative2 } from "node:path";
328
- import { existsSync as existsSync6 } from "node:fs";
329
- import { homedir as homedir5 } from "node:os";
330
- import { createHash } from "node:crypto";
152
+ });
331
153
 
332
154
  // dist/scanner/file-parser.js
333
155
  import { readFile as readFile2 } from "node:fs/promises";
334
156
  import { existsSync as existsSync2 } from "node:fs";
335
157
  import { resolve as resolve2, dirname, join as join2 } from "node:path";
336
158
  import { homedir as homedir2 } from "node:os";
337
- var FILE_EXT_PATTERN = "md|json|ya?ml|ts|js|sh|py|bash";
338
159
  async function parseFileReferences(filePath, projectRoot) {
339
160
  const refs = [];
340
161
  const content = await readFile2(filePath, "utf-8");
@@ -451,6 +272,13 @@ function classifyFileType(filePath) {
451
272
  return "plan";
452
273
  return "other";
453
274
  }
275
+ var FILE_EXT_PATTERN;
276
+ var init_file_parser = __esm({
277
+ "dist/scanner/file-parser.js"() {
278
+ "use strict";
279
+ FILE_EXT_PATTERN = "md|json|ya?ml|ts|js|sh|py|bash";
280
+ }
281
+ });
454
282
 
455
283
  // dist/scanner/git-dates.js
456
284
  import { execFileSync } from "node:child_process";
@@ -479,6 +307,11 @@ function getGitCreationDate(filePath, cwd) {
479
307
  }
480
308
  }
481
309
  }
310
+ var init_git_dates = __esm({
311
+ "dist/scanner/git-dates.js"() {
312
+ "use strict";
313
+ }
314
+ });
482
315
 
483
316
  // dist/scanner/graph-builder.js
484
317
  import { statSync as statSync2 } from "node:fs";
@@ -552,6 +385,13 @@ function buildGraph(files, references, projectRoot) {
552
385
  function sanitiseId(path) {
553
386
  return path.replace(/[^a-zA-Z0-9]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "").slice(0, 64);
554
387
  }
388
+ var init_graph_builder = __esm({
389
+ "dist/scanner/graph-builder.js"() {
390
+ "use strict";
391
+ init_file_parser();
392
+ init_git_dates();
393
+ }
394
+ });
555
395
 
556
396
  // dist/scanner/orphan-detector.js
557
397
  import { statSync as statSync3 } from "node:fs";
@@ -595,6 +435,12 @@ function detectOrphans(allFiles, references, rootFiles, projectRoot) {
595
435
  };
596
436
  });
597
437
  }
438
+ var init_orphan_detector = __esm({
439
+ "dist/scanner/orphan-detector.js"() {
440
+ "use strict";
441
+ init_git_dates();
442
+ }
443
+ });
598
444
 
599
445
  // dist/scanner/skills-parser.js
600
446
  import { execFileSync as execFileSync2 } from "node:child_process";
@@ -680,6 +526,11 @@ async function parseSettingsForPlugins(settingsPath, skills, isMachineWide) {
680
526
  } catch {
681
527
  }
682
528
  }
529
+ var init_skills_parser = __esm({
530
+ "dist/scanner/skills-parser.js"() {
531
+ "use strict";
532
+ }
533
+ });
683
534
 
684
535
  // dist/scanner/tooling-detector.js
685
536
  import { readFile as readFile4, readdir } from "node:fs/promises";
@@ -687,13 +538,6 @@ import { existsSync as existsSync4 } from "node:fs";
687
538
  import { join as join6 } from "node:path";
688
539
  import { execFileSync as execFileSync3 } from "node:child_process";
689
540
  import { homedir as homedir4 } from "node:os";
690
- var CLI_VERSION_COMMANDS = [
691
- { name: "node", command: "node", args: ["--version"] },
692
- { name: "npm", command: "npm", args: ["--version"] },
693
- { name: "pnpm", command: "pnpm", args: ["--version"], conditionFile: "pnpm-lock.yaml" },
694
- { name: "supabase-cli", command: "npx", args: ["supabase", "--version"] },
695
- { name: "claude-code", command: "claude", args: ["--version"] }
696
- ];
697
541
  async function detectToolings(projectRoot) {
698
542
  const toolings = [];
699
543
  const pkgToolings = await detectFromPackageJson(projectRoot);
@@ -922,12 +766,24 @@ async function detectFromMcpSettings(projectRoot) {
922
766
  }
923
767
  return toolings;
924
768
  }
769
+ var CLI_VERSION_COMMANDS;
770
+ var init_tooling_detector = __esm({
771
+ "dist/scanner/tooling-detector.js"() {
772
+ "use strict";
773
+ CLI_VERSION_COMMANDS = [
774
+ { name: "node", command: "node", args: ["--version"] },
775
+ { name: "npm", command: "npm", args: ["--version"] },
776
+ { name: "pnpm", command: "pnpm", args: ["--version"], conditionFile: "pnpm-lock.yaml" },
777
+ { name: "supabase-cli", command: "npx", args: ["supabase", "--version"] },
778
+ { name: "claude-code", command: "claude", args: ["--version"] }
779
+ ];
780
+ }
781
+ });
925
782
 
926
783
  // dist/scanner/env-manifest-scanner.js
927
784
  import { readFile as readFile5, glob } from "node:fs/promises";
928
785
  import { join as join7, relative } from "node:path";
929
786
  import { existsSync as existsSync5 } from "node:fs";
930
- var MANIFEST_PATH = "docs/reference/env-manifest.md";
931
787
  async function scanEnvManifest(projectRoot) {
932
788
  const manifestFullPath = join7(projectRoot, MANIFEST_PATH);
933
789
  if (!existsSync5(manifestFullPath)) {
@@ -1120,8 +976,20 @@ function computeDrift(variables, apps, localPresence, workflowRefs) {
1120
976
  workflowBrokenRefs: workflowBrokenRefs.sort()
1121
977
  };
1122
978
  }
979
+ var MANIFEST_PATH;
980
+ var init_env_manifest_scanner = __esm({
981
+ "dist/scanner/env-manifest-scanner.js"() {
982
+ "use strict";
983
+ MANIFEST_PATH = "docs/reference/env-manifest.md";
984
+ }
985
+ });
1123
986
 
1124
987
  // dist/scanner/index.js
988
+ import { readdir as readdir2 } from "node:fs/promises";
989
+ import { join as join8, relative as relative2 } from "node:path";
990
+ import { existsSync as existsSync6 } from "node:fs";
991
+ import { homedir as homedir5 } from "node:os";
992
+ import { createHash } from "node:crypto";
1125
993
  async function scanProject(projectRoot) {
1126
994
  const allFiles = await discoverFiles(projectRoot);
1127
995
  const rootFiles = identifyRoots(allFiles, projectRoot);
@@ -1247,122 +1115,23 @@ function detectStaleFiles(allFiles, projectRoot) {
1247
1115
  }
1248
1116
  return stale;
1249
1117
  }
1250
-
1251
- // dist/output/html-generator.js
1252
- function generateOfflineHtml(result, projectRoot) {
1253
- const title = projectRoot.split("/").pop() ?? "MD4AI";
1254
- const dataJson = JSON.stringify(result);
1255
- const orphanItems = result.orphans.map((o) => `<li data-path="${escapeHtml(o.path)}">${escapeHtml(o.path)} <span class="meta">(${o.sizeBytes} bytes)</span></li>`).join("\n");
1256
- const staleItems = result.staleFiles.map((s) => `<li data-path="${escapeHtml(s.path)}">${escapeHtml(s.path)} <span class="stale-meta">(${s.daysSinceModified} days)</span></li>`).join("\n");
1257
- const skillRows = result.skills.map((s) => `<tr><td>${escapeHtml(s.name)}</td><td>${s.machineWide ? '<span class="check">\u2713</span>' : ""}</td><td>${s.projectSpecific ? '<span class="check">\u2713</span>' : ""}</td><td class="meta">${escapeHtml(s.source)}</td></tr>`).join("\n");
1258
- const fileItems = result.graph.nodes.map((n) => `<li data-path="${escapeHtml(n.filePath)}" data-type="${n.type}">${escapeHtml(n.filePath)} <span class="meta">(${n.type})</span></li>`).join("\n");
1259
- return `<!DOCTYPE html>
1260
- <html lang="en">
1261
- <head>
1262
- <meta charset="UTF-8">
1263
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1264
- <title>MD4AI \u2014 ${escapeHtml(title)}</title>
1265
- <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
1266
- <style>
1267
- * { margin: 0; padding: 0; box-sizing: border-box; }
1268
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
1269
- h1 { color: #38bdf8; margin-bottom: 0.5rem; }
1270
- .subtitle { color: #94a3b8; margin-bottom: 2rem; }
1271
- .section { background: #1e293b; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
1272
- .section h2 { color: #38bdf8; margin-bottom: 1rem; font-size: 1.2rem; }
1273
- .mermaid { background: #0f172a; border-radius: 8px; padding: 1rem; overflow-x: auto; }
1274
- .badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.85rem; margin-left: 0.5rem; }
1275
- .badge-orphan { background: #ef4444; color: white; }
1276
- .badge-stale { background: #f59e0b; color: black; }
1277
- table { width: 100%; border-collapse: collapse; }
1278
- th, td { padding: 0.5rem 1rem; text-align: left; border-bottom: 1px solid #334155; }
1279
- th { color: #94a3b8; font-weight: 600; }
1280
- .check { color: #22c55e; }
1281
- .meta { color: #94a3b8; }
1282
- .stale-meta { color: #f59e0b; }
1283
- .stats { display: flex; gap: 2rem; flex-wrap: wrap; margin-bottom: 1rem; }
1284
- .stat { text-align: center; }
1285
- .stat-value { font-size: 2rem; font-weight: bold; color: #38bdf8; }
1286
- .stat-label { color: #94a3b8; font-size: 0.85rem; }
1287
- .file-list { list-style: none; }
1288
- .file-list li { padding: 0.3rem 0; color: #cbd5e1; font-family: monospace; font-size: 0.9rem; }
1289
- #search { width: 100%; padding: 0.75rem 1rem; background: #0f172a; border: 1px solid #334155; border-radius: 8px; color: #e2e8f0; font-size: 1rem; margin-bottom: 1rem; }
1290
- #search:focus { outline: none; border-color: #38bdf8; }
1291
- @media print { body { background: white; color: black; } .section { border: 1px solid #ccc; } }
1292
- </style>
1293
- </head>
1294
- <body>
1295
- <h1>MD4AI \u2014 ${escapeHtml(title)}</h1>
1296
- <p class="subtitle">Scanned: ${new Date(result.scannedAt).toLocaleString()} | Hash: ${result.dataHash.slice(0, 12)}</p>
1297
-
1298
- <div class="stats">
1299
- <div class="stat"><div class="stat-value">${result.graph.nodes.length}</div><div class="stat-label">Files</div></div>
1300
- <div class="stat"><div class="stat-value">${result.graph.edges.length}</div><div class="stat-label">References</div></div>
1301
- <div class="stat"><div class="stat-value">${result.orphans.length}</div><div class="stat-label">Orphans</div></div>
1302
- <div class="stat"><div class="stat-value">${result.staleFiles.length}</div><div class="stat-label">Stale</div></div>
1303
- <div class="stat"><div class="stat-value">${result.skills.length}</div><div class="stat-label">Skills</div></div>
1304
- </div>
1305
-
1306
- <input type="text" id="search" placeholder="Search files..." oninput="filterFiles(this.value)">
1307
-
1308
- <div class="section">
1309
- <h2>Dependency Graph</h2>
1310
- <pre class="mermaid">${escapeHtml(result.graph.mermaid)}</pre>
1311
- </div>
1312
-
1313
- ${result.orphans.length > 0 ? `
1314
- <div class="section">
1315
- <h2>Orphan Files <span class="badge badge-orphan">${result.orphans.length}</span></h2>
1316
- <p class="meta" style="margin-bottom: 1rem;">Files not reachable from any root configuration file.</p>
1317
- <ul class="file-list" id="orphan-list">${orphanItems}</ul>
1318
- </div>
1319
- ` : ""}
1320
-
1321
- ${result.staleFiles.length > 0 ? `
1322
- <div class="section">
1323
- <h2>Stale Files <span class="badge badge-stale">${result.staleFiles.length}</span></h2>
1324
- <p class="meta" style="margin-bottom: 1rem;">Files not modified in over 90 days.</p>
1325
- <ul class="file-list" id="stale-list">${staleItems}</ul>
1326
- </div>
1327
- ` : ""}
1328
-
1329
- <div class="section">
1330
- <h2>Skills Comparison</h2>
1331
- <table>
1332
- <thead><tr><th>Skill/Plugin</th><th>Entire Machine</th><th>Project Specific</th><th>Source</th></tr></thead>
1333
- <tbody>${skillRows}</tbody>
1334
- </table>
1335
- </div>
1336
-
1337
- <div class="section">
1338
- <h2>All Files</h2>
1339
- <ul class="file-list" id="all-files">${fileItems}</ul>
1340
- </div>
1341
-
1342
- <script>
1343
- mermaid.initialize({ startOnLoad: true, theme: 'dark' });
1344
-
1345
- function filterFiles(query) {
1346
- var q = query.toLowerCase();
1347
- document.querySelectorAll('.file-list li').forEach(function(li) {
1348
- var path = li.getAttribute('data-path') || '';
1349
- li.style.display = path.toLowerCase().indexOf(q) >= 0 ? '' : 'none';
1350
- });
1351
- }
1352
- </script>
1353
-
1354
- <script type="application/json" id="scan-data">${dataJson}</script>
1355
- </body>
1356
- </html>`;
1357
- }
1358
- function escapeHtml(text) {
1359
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1360
- }
1118
+ var init_scanner = __esm({
1119
+ "dist/scanner/index.js"() {
1120
+ "use strict";
1121
+ init_dist();
1122
+ init_file_parser();
1123
+ init_graph_builder();
1124
+ init_orphan_detector();
1125
+ init_skills_parser();
1126
+ init_git_dates();
1127
+ init_tooling_detector();
1128
+ init_env_manifest_scanner();
1129
+ }
1130
+ });
1361
1131
 
1362
1132
  // dist/check-update.js
1363
1133
  import chalk8 from "chalk";
1364
- var CURRENT_VERSION = true ? "0.7.1" : "0.0.0-dev";
1365
- async function checkForUpdate() {
1134
+ async function fetchLatest() {
1366
1135
  try {
1367
1136
  const controller = new AbortController();
1368
1137
  const timeout = setTimeout(() => controller.abort(), 3e3);
@@ -1371,23 +1140,40 @@ async function checkForUpdate() {
1371
1140
  });
1372
1141
  clearTimeout(timeout);
1373
1142
  if (!res.ok)
1374
- return;
1143
+ return null;
1375
1144
  const data = await res.json();
1376
- const latest = data.version;
1145
+ return data.version ?? null;
1146
+ } catch {
1147
+ return null;
1148
+ }
1149
+ }
1150
+ function printUpdateBanner(latest) {
1151
+ console.log("");
1152
+ console.log(chalk8.yellow("\u250C\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\u2510"));
1153
+ console.log(chalk8.yellow("\u2502") + chalk8.bold(" Update available! ") + chalk8.dim(`${CURRENT_VERSION}`) + chalk8.white(" \u2192 ") + chalk8.green.bold(`${latest}`) + " " + chalk8.yellow("\u2502"));
1154
+ console.log(chalk8.yellow("\u2502") + " " + chalk8.yellow("\u2502"));
1155
+ console.log(chalk8.yellow("\u2502") + " Run: " + chalk8.cyan("md4ai update") + " " + chalk8.yellow("\u2502"));
1156
+ console.log(chalk8.yellow("\u2502") + " " + chalk8.yellow("\u2502"));
1157
+ console.log(chalk8.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"));
1158
+ console.log("");
1159
+ }
1160
+ async function checkForUpdate() {
1161
+ const latest = await fetchLatest();
1162
+ if (!latest)
1163
+ return;
1164
+ if (latest !== CURRENT_VERSION && isNewer(latest, CURRENT_VERSION)) {
1165
+ printUpdateBanner(latest);
1166
+ } else {
1167
+ console.log(chalk8.green(`md4ai v${CURRENT_VERSION} \u2014 you're on the latest version.`));
1168
+ }
1169
+ }
1170
+ async function autoCheckForUpdate() {
1171
+ try {
1172
+ const latest = await fetchLatest();
1377
1173
  if (!latest)
1378
1174
  return;
1379
1175
  if (latest !== CURRENT_VERSION && isNewer(latest, CURRENT_VERSION)) {
1380
- console.log("");
1381
- console.log(chalk8.yellow("\u250C\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\u2500\u2500\u2500\u2510"));
1382
- console.log(chalk8.yellow("\u2502") + chalk8.bold(" Update available! ") + chalk8.dim(`${CURRENT_VERSION}`) + chalk8.white(" \u2192 ") + chalk8.green.bold(`${latest}`) + " " + chalk8.yellow("\u2502"));
1383
- console.log(chalk8.yellow("\u2502") + " " + chalk8.yellow("\u2502"));
1384
- console.log(chalk8.yellow("\u2502") + " Run: " + chalk8.cyan("npm install -g md4ai") + " " + chalk8.yellow("\u2502"));
1385
- console.log(chalk8.yellow("\u2502") + " " + chalk8.yellow("\u2502"));
1386
- console.log(chalk8.yellow("\u2502") + chalk8.dim(" Changelog: https://www.md4ai.com/how-it-works") + " " + chalk8.yellow("\u2502"));
1387
- console.log(chalk8.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\u2500\u2500\u2500\u2518"));
1388
- console.log("");
1389
- } else {
1390
- console.log(chalk8.green(`md4ai v${CURRENT_VERSION} \u2014 you're on the latest version.`));
1176
+ printUpdateBanner(latest);
1391
1177
  }
1392
1178
  } catch {
1393
1179
  }
@@ -1403,6 +1189,13 @@ function isNewer(a, b) {
1403
1189
  }
1404
1190
  return false;
1405
1191
  }
1192
+ var CURRENT_VERSION;
1193
+ var init_check_update = __esm({
1194
+ "dist/check-update.js"() {
1195
+ "use strict";
1196
+ CURRENT_VERSION = true ? "0.7.3" : "0.0.0-dev";
1197
+ }
1198
+ });
1406
1199
 
1407
1200
  // dist/commands/push-toolings.js
1408
1201
  async function pushToolings(supabase, folderId, toolings) {
@@ -1422,75 +1215,35 @@ async function pushToolings(supabase, folderId, toolings) {
1422
1215
  }, { onConflict: "folder_id,tool_name,detection_source" });
1423
1216
  }
1424
1217
  }
1425
-
1426
- // dist/commands/map.js
1427
- async function mapCommand(path, options) {
1428
- await checkForUpdate();
1429
- const projectRoot = resolve3(path ?? process.cwd());
1430
- if (!existsSync7(projectRoot)) {
1431
- console.error(chalk9.red(`Path not found: ${projectRoot}`));
1432
- process.exit(1);
1433
- }
1434
- console.log(chalk9.blue(`Scanning: ${projectRoot}
1435
- `));
1436
- const result = await scanProject(projectRoot);
1437
- console.log(` Files found: ${result.graph.nodes.length}`);
1438
- console.log(` References: ${result.graph.edges.length}`);
1439
- console.log(` Orphans: ${result.orphans.length}`);
1440
- console.log(` Stale files: ${result.staleFiles.length}`);
1441
- console.log(` Skills: ${result.skills.length}`);
1442
- console.log(` Toolings: ${result.toolings.length}`);
1443
- console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
1444
- console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
1445
- const outputDir = resolve3(projectRoot, "output");
1446
- if (!existsSync7(outputDir)) {
1447
- await mkdir2(outputDir, { recursive: true });
1218
+ var init_push_toolings = __esm({
1219
+ "dist/commands/push-toolings.js"() {
1220
+ "use strict";
1448
1221
  }
1449
- const htmlPath = resolve3(outputDir, "index.html");
1450
- const html = generateOfflineHtml(result, projectRoot);
1451
- await writeFile2(htmlPath, html, "utf-8");
1452
- console.log(chalk9.green(`
1453
- Local preview: ${htmlPath}`));
1454
- if (!options.offline) {
1455
- try {
1456
- const { supabase } = await getAuthenticatedClient();
1457
- const { data: devicePaths } = await supabase.from("device_paths").select("folder_id, device_name").eq("path", projectRoot);
1458
- if (devicePaths?.length) {
1459
- const { folder_id, device_name } = devicePaths[0];
1460
- const { data: proposedFiles } = await supabase.from("folder_files").select("id, file_path, proposed_at").eq("folder_id", folder_id).eq("proposed_for_deletion", true);
1461
- if (proposedFiles?.length) {
1462
- const { checkbox } = await import("@inquirer/prompts");
1463
- console.log(chalk9.yellow(`
1464
- ${proposedFiles.length} file(s) proposed for deletion:
1465
- `));
1466
- const toDelete = await checkbox({
1467
- message: "Select files to delete (space to toggle, enter to confirm)",
1468
- choices: proposedFiles.map((f) => ({
1469
- name: `${f.file_path}${f.proposed_at ? ` (proposed ${new Date(f.proposed_at).toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short", year: "numeric" })})` : ""}`,
1470
- value: f
1471
- }))
1472
- });
1473
- for (const file of toDelete) {
1474
- const fullPath = resolve3(projectRoot, file.file_path);
1475
- try {
1476
- const { unlink } = await import("node:fs/promises");
1477
- await unlink(fullPath);
1478
- await supabase.from("folder_files").delete().eq("id", file.id);
1479
- console.log(chalk9.green(` Deleted: ${file.file_path}`));
1480
- } catch (err) {
1481
- console.error(chalk9.red(` Failed to delete ${file.file_path}: ${err}`));
1482
- }
1483
- }
1484
- const keptIds = proposedFiles.filter((f) => !toDelete.some((d) => d.id === f.id)).map((f) => f.id);
1485
- if (keptIds.length > 0) {
1486
- for (const id of keptIds) {
1487
- await supabase.from("folder_files").update({ proposed_for_deletion: false, proposed_at: null, proposed_by: null }).eq("id", id);
1488
- }
1489
- console.log(chalk9.cyan(` Kept ${keptIds.length} file(s) \u2014 proposals cleared.`));
1490
- }
1491
- console.log("");
1492
- }
1493
- const { error } = await supabase.from("claude_folders").update({
1222
+ });
1223
+
1224
+ // dist/commands/sync.js
1225
+ var sync_exports = {};
1226
+ __export(sync_exports, {
1227
+ syncCommand: () => syncCommand
1228
+ });
1229
+ import chalk12 from "chalk";
1230
+ async function syncCommand(options) {
1231
+ const { supabase } = await getAuthenticatedClient();
1232
+ if (options.all) {
1233
+ const { data: devices, error } = await supabase.from("device_paths").select("folder_id, device_name, path");
1234
+ if (error || !devices?.length) {
1235
+ console.error(chalk12.red("No devices found."));
1236
+ process.exit(1);
1237
+ }
1238
+ for (const device of devices) {
1239
+ console.log(chalk12.blue(`Syncing: ${device.path}`));
1240
+ const { data: proposedAll } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
1241
+ if (proposedAll?.length) {
1242
+ console.log(chalk12.yellow(` ${proposedAll.length} file(s) proposed for deletion \u2014 run \`md4ai scan\` to review.`));
1243
+ }
1244
+ try {
1245
+ const result = await scanProject(device.path);
1246
+ await supabase.from("claude_folders").update({
1494
1247
  graph_json: result.graph,
1495
1248
  orphans_json: result.orphans,
1496
1249
  skills_table_json: result.skills,
@@ -1498,197 +1251,12 @@ ${proposedFiles.length} file(s) proposed for deletion:
1498
1251
  env_manifest_json: result.envManifest,
1499
1252
  last_scanned: result.scannedAt,
1500
1253
  data_hash: result.dataHash
1501
- }).eq("id", folder_id);
1502
- if (error) {
1503
- console.error(chalk9.yellow(`Sync warning: ${error.message}`));
1504
- } else {
1505
- await pushToolings(supabase, folder_id, result.toolings);
1506
- await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder_id).eq("device_name", device_name);
1507
- await saveState({
1508
- lastFolderId: folder_id,
1509
- lastDeviceName: device_name,
1510
- lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
1511
- });
1512
- console.log(chalk9.green("Synced to Supabase."));
1513
- console.log(chalk9.cyan(`
1514
- https://www.md4ai.com/project/${folder_id}
1515
- `));
1516
- const configFiles = await readClaudeConfigFiles(projectRoot);
1517
- if (configFiles.length > 0) {
1518
- for (const file of configFiles) {
1519
- await supabase.from("folder_files").upsert({
1520
- folder_id,
1521
- file_path: file.filePath,
1522
- content: file.content,
1523
- size_bytes: file.sizeBytes,
1524
- last_modified: file.lastModified
1525
- }, { onConflict: "folder_id,file_path" });
1526
- }
1527
- console.log(chalk9.green(` Uploaded ${configFiles.length} config file(s).`));
1528
- }
1529
- }
1530
- } else {
1531
- console.log(chalk9.yellow("No device path matches this folder. Run: md4ai add-device\n(Local preview still generated.)"));
1532
- }
1533
- } catch {
1534
- console.log(chalk9.yellow("Not logged in \u2014 local preview only."));
1535
- }
1536
- }
1537
- }
1538
-
1539
- // dist/commands/simulate.js
1540
- import { join as join9 } from "node:path";
1541
- import { existsSync as existsSync8 } from "node:fs";
1542
- import { homedir as homedir6 } from "node:os";
1543
- import chalk10 from "chalk";
1544
- async function simulateCommand(prompt) {
1545
- const projectRoot = process.cwd();
1546
- console.log(chalk10.blue(`Simulating prompt: "${prompt}"
1547
- `));
1548
- console.log(chalk10.dim("Files Claude would load:\n"));
1549
- const globalClaude = join9(homedir6(), ".claude", "CLAUDE.md");
1550
- if (existsSync8(globalClaude)) {
1551
- console.log(chalk10.green(" \u2713 ~/.claude/CLAUDE.md (global)"));
1552
- }
1553
- for (const rootFile of ROOT_FILES) {
1554
- const fullPath = join9(projectRoot, rootFile);
1555
- if (existsSync8(fullPath)) {
1556
- console.log(chalk10.green(` \u2713 ${rootFile} (project root)`));
1557
- }
1558
- }
1559
- const settingsFiles = [
1560
- join9(homedir6(), ".claude", "settings.json"),
1561
- join9(homedir6(), ".claude", "settings.local.json"),
1562
- join9(projectRoot, ".claude", "settings.json"),
1563
- join9(projectRoot, ".claude", "settings.local.json")
1564
- ];
1565
- for (const sf of settingsFiles) {
1566
- if (existsSync8(sf)) {
1567
- const display = sf.startsWith(homedir6()) ? sf.replace(homedir6(), "~") : sf.replace(projectRoot + "/", "");
1568
- console.log(chalk10.green(` \u2713 ${display} (settings)`));
1569
- }
1570
- }
1571
- const words = prompt.split(/\s+/);
1572
- for (const word of words) {
1573
- const cleaned = word.replace(/['"]/g, "");
1574
- const candidatePath = join9(projectRoot, cleaned);
1575
- if (existsSync8(candidatePath) && cleaned.includes("/")) {
1576
- console.log(chalk10.cyan(` \u2192 ${cleaned} (referenced in prompt)`));
1577
- }
1578
- }
1579
- console.log(chalk10.dim("\nNote: This is an approximation. Actual file loading depends on Claude Code internals."));
1580
- }
1581
-
1582
- // dist/commands/print.js
1583
- import { join as join10 } from "node:path";
1584
- import { readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
1585
- import { existsSync as existsSync9 } from "node:fs";
1586
- import chalk11 from "chalk";
1587
- async function printCommand(title) {
1588
- const projectRoot = process.cwd();
1589
- const scanDataPath = join10(projectRoot, "output", "index.html");
1590
- if (!existsSync9(scanDataPath)) {
1591
- console.error(chalk11.red("No scan data found. Run: md4ai scan"));
1592
- process.exit(1);
1593
- }
1594
- const html = await readFile6(scanDataPath, "utf-8");
1595
- const match = html.match(/<script type="application\/json" id="scan-data">([\s\S]*?)<\/script>/);
1596
- if (!match) {
1597
- console.error(chalk11.red("Could not extract scan data from output/index.html"));
1598
- process.exit(1);
1599
- }
1600
- const result = JSON.parse(match[1]);
1601
- const printHtml = generatePrintHtml(result, title);
1602
- const outputPath = join10(projectRoot, "output", `print-${Date.now()}.html`);
1603
- await writeFile3(outputPath, printHtml, "utf-8");
1604
- console.log(chalk11.green(`Print-ready wall sheet: ${outputPath}`));
1605
- }
1606
- function escapeHtml2(text) {
1607
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1608
- }
1609
- function generatePrintHtml(result, title) {
1610
- const skillRows = result.skills.map((s) => `<tr><td>${escapeHtml2(s.name)}</td><td>${s.machineWide ? '<span class="check">\u2713</span>' : ""}</td><td>${s.projectSpecific ? '<span class="check">\u2713</span>' : ""}</td></tr>`).join("\n");
1611
- const orphanItems = result.orphans.map((o) => `<li class="orphan">${escapeHtml2(o.path)}</li>`).join("\n");
1612
- const staleItems = result.staleFiles.map((s) => `<li class="stale">${escapeHtml2(s.path)} (${s.daysSinceModified}d)</li>`).join("\n");
1613
- return `<!DOCTYPE html>
1614
- <html lang="en">
1615
- <head>
1616
- <meta charset="UTF-8">
1617
- <title>${escapeHtml2(title)} \u2014 MD4AI Wall Sheet</title>
1618
- <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
1619
- <style>
1620
- @page { size: A3 landscape; margin: 1cm; }
1621
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 1rem; }
1622
- h1 { font-size: 2rem; margin-bottom: 0.5rem; }
1623
- .subtitle { color: #666; margin-bottom: 2rem; }
1624
- .mermaid { margin: 2rem 0; }
1625
- .columns { display: flex; gap: 2rem; }
1626
- .column { flex: 1; }
1627
- table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
1628
- th, td { padding: 0.3rem 0.5rem; text-align: left; border-bottom: 1px solid #ddd; }
1629
- th { background: #f5f5f5; }
1630
- .orphan { color: #dc2626; }
1631
- .stale { color: #d97706; }
1632
- .check { color: #16a34a; font-weight: bold; }
1633
- </style>
1634
- </head>
1635
- <body>
1636
- <h1>${escapeHtml2(title)}</h1>
1637
- <p class="subtitle">Generated: ${(/* @__PURE__ */ new Date()).toLocaleString()} | Files: ${result.graph.nodes.length} | Orphans: ${result.orphans.length}</p>
1638
-
1639
- <pre class="mermaid">${escapeHtml2(result.graph.mermaid)}</pre>
1640
-
1641
- <div class="columns">
1642
- <div class="column">
1643
- <h2>Skills Comparison</h2>
1644
- <table>
1645
- <tr><th>Skill</th><th>Machine</th><th>Project</th></tr>
1646
- ${skillRows}
1647
- </table>
1648
- </div>
1649
- <div class="column">
1650
- ${result.orphans.length ? `<h2>Orphans</h2><ul>${orphanItems}</ul>` : ""}
1651
- ${result.staleFiles.length ? `<h2>Stale Files</h2><ul>${staleItems}</ul>` : ""}
1652
- </div>
1653
- </div>
1654
-
1655
- <script>mermaid.initialize({ startOnLoad: true, theme: 'default' });</script>
1656
- </body>
1657
- </html>`;
1658
- }
1659
-
1660
- // dist/commands/sync.js
1661
- import chalk12 from "chalk";
1662
- async function syncCommand(options) {
1663
- const { supabase } = await getAuthenticatedClient();
1664
- if (options.all) {
1665
- const { data: devices, error } = await supabase.from("device_paths").select("folder_id, device_name, path");
1666
- if (error || !devices?.length) {
1667
- console.error(chalk12.red("No devices found."));
1668
- process.exit(1);
1669
- }
1670
- for (const device of devices) {
1671
- console.log(chalk12.blue(`Syncing: ${device.path}`));
1672
- const { data: proposedAll } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
1673
- if (proposedAll?.length) {
1674
- console.log(chalk12.yellow(` ${proposedAll.length} file(s) proposed for deletion \u2014 run \`md4ai scan\` to review.`));
1675
- }
1676
- try {
1677
- const result = await scanProject(device.path);
1678
- await supabase.from("claude_folders").update({
1679
- graph_json: result.graph,
1680
- orphans_json: result.orphans,
1681
- skills_table_json: result.skills,
1682
- stale_files_json: result.staleFiles,
1683
- env_manifest_json: result.envManifest,
1684
- last_scanned: result.scannedAt,
1685
- data_hash: result.dataHash
1686
- }).eq("id", device.folder_id);
1687
- await pushToolings(supabase, device.folder_id, result.toolings);
1688
- await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
1689
- console.log(chalk12.green(` Done: ${device.device_name}`));
1690
- } catch (err) {
1691
- console.error(chalk12.red(` Failed: ${device.path}: ${err}`));
1254
+ }).eq("id", device.folder_id);
1255
+ await pushToolings(supabase, device.folder_id, result.toolings);
1256
+ await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
1257
+ console.log(chalk12.green(` Done: ${device.device_name}`));
1258
+ } catch (err) {
1259
+ console.error(chalk12.red(` Failed: ${device.path}: ${err}`));
1692
1260
  }
1693
1261
  }
1694
1262
  await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
@@ -1724,10 +1292,15 @@ async function syncCommand(options) {
1724
1292
  console.log(chalk12.green("Synced."));
1725
1293
  }
1726
1294
  }
1727
-
1728
- // dist/commands/link.js
1729
- import { resolve as resolve4 } from "node:path";
1730
- import chalk13 from "chalk";
1295
+ var init_sync = __esm({
1296
+ "dist/commands/sync.js"() {
1297
+ "use strict";
1298
+ init_auth();
1299
+ init_config();
1300
+ init_scanner();
1301
+ init_push_toolings();
1302
+ }
1303
+ });
1731
1304
 
1732
1305
  // dist/device-utils.js
1733
1306
  import { hostname as hostname2, platform as platform2 } from "node:os";
@@ -1761,890 +1334,1519 @@ async function resolveDeviceId(supabase, userId) {
1761
1334
  }
1762
1335
  return data.id;
1763
1336
  }
1764
-
1765
- // dist/commands/link.js
1766
- async function linkCommand(projectId) {
1767
- const { supabase, userId } = await getAuthenticatedClient();
1768
- const cwd = resolve4(process.cwd());
1769
- const deviceName = detectDeviceName();
1770
- const osType = detectOs2();
1771
- const { data: folder, error: folderErr } = await supabase.from("claude_folders").select("id, name").eq("id", projectId).single();
1772
- if (folderErr || !folder) {
1773
- console.error(chalk13.red("Project not found, or you do not have access."));
1774
- console.error(chalk13.yellow("Check the project ID in the MD4AI web dashboard."));
1775
- process.exit(1);
1337
+ var init_device_utils = __esm({
1338
+ "dist/device-utils.js"() {
1339
+ "use strict";
1776
1340
  }
1777
- console.log(chalk13.blue(`
1778
- Linking "${folder.name}" to this device...
1779
- `));
1780
- console.log(` Project: ${folder.name}`);
1781
- console.log(` Path: ${cwd}`);
1782
- console.log(` Device: ${deviceName}`);
1783
- console.log(` OS: ${osType}`);
1784
- await supabase.from("devices").upsert({
1785
- user_id: userId,
1786
- device_name: deviceName,
1787
- os_type: osType
1788
- }, { onConflict: "user_id,device_name" });
1789
- const { data: existing } = await supabase.from("device_paths").select("id").eq("folder_id", folder.id).eq("device_name", deviceName).maybeSingle();
1790
- if (existing) {
1791
- await supabase.from("device_paths").update({ path: cwd, os_type: osType, last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", existing.id);
1792
- } else {
1793
- const { error: pathErr } = await supabase.from("device_paths").insert({
1794
- user_id: userId,
1795
- folder_id: folder.id,
1796
- device_name: deviceName,
1797
- os_type: osType,
1798
- path: cwd
1799
- });
1800
- if (pathErr) {
1801
- console.error(chalk13.red(`Failed to link: ${pathErr.message}`));
1802
- process.exit(1);
1803
- }
1341
+ });
1342
+
1343
+ // dist/mcp/read-configs.js
1344
+ import { readFile as readFile8 } from "node:fs/promises";
1345
+ import { join as join12 } from "node:path";
1346
+ import { homedir as homedir7 } from "node:os";
1347
+ import { existsSync as existsSync11 } from "node:fs";
1348
+ import { readdir as readdir3 } from "node:fs/promises";
1349
+ async function readJsonSafe(path) {
1350
+ try {
1351
+ if (!existsSync11(path))
1352
+ return null;
1353
+ const raw = await readFile8(path, "utf-8");
1354
+ return JSON.parse(raw);
1355
+ } catch {
1356
+ return null;
1804
1357
  }
1805
- console.log(chalk13.green("\nLinked successfully."));
1806
- console.log(chalk13.blue("\nRunning initial scan...\n"));
1807
- const result = await scanProject(cwd);
1808
- console.log(` Files: ${result.graph.nodes.length}`);
1809
- console.log(` References:${result.graph.edges.length}`);
1810
- console.log(` Orphans: ${result.orphans.length}`);
1811
- console.log(` Skills: ${result.skills.length}`);
1812
- console.log(` Toolings: ${result.toolings.length}`);
1813
- console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
1814
- const { error: scanErr } = await supabase.from("claude_folders").update({
1815
- graph_json: result.graph,
1816
- orphans_json: result.orphans,
1817
- skills_table_json: result.skills,
1818
- stale_files_json: result.staleFiles,
1819
- env_manifest_json: result.envManifest,
1820
- last_scanned: result.scannedAt,
1821
- data_hash: result.dataHash
1822
- }).eq("id", folder.id);
1823
- if (scanErr) {
1824
- console.error(chalk13.yellow(`Scan upload warning: ${scanErr.message}`));
1358
+ }
1359
+ function extractPackageName(args) {
1360
+ for (const arg of args) {
1361
+ if (arg.startsWith("-"))
1362
+ continue;
1363
+ if (arg.includes("mcp") || arg.startsWith("@"))
1364
+ return arg;
1825
1365
  }
1826
- await pushToolings(supabase, folder.id, result.toolings);
1827
- const configFiles = await readClaudeConfigFiles(cwd);
1828
- if (configFiles.length > 0) {
1829
- for (const file of configFiles) {
1830
- await supabase.from("folder_files").upsert({
1831
- folder_id: folder.id,
1832
- file_path: file.filePath,
1833
- content: file.content,
1834
- size_bytes: file.sizeBytes,
1835
- last_modified: file.lastModified
1836
- }, { onConflict: "folder_id,file_path" });
1837
- }
1838
- console.log(chalk13.green(` Uploaded ${configFiles.length} config file(s).`));
1366
+ for (const arg of args) {
1367
+ if (!arg.startsWith("-"))
1368
+ return arg;
1839
1369
  }
1840
- await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder.id).eq("device_name", deviceName);
1841
- await saveState({
1842
- lastFolderId: folder.id,
1843
- lastDeviceName: deviceName,
1844
- lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
1845
- });
1846
- const projectUrl = `https://www.md4ai.com/project/${folder.id}`;
1847
- console.log(chalk13.green("\nDone! Project linked and scanned."));
1848
- console.log(chalk13.cyan(`
1849
- ${projectUrl}
1850
- `));
1851
- console.log(chalk13.grey('Run "md4ai scan" to rescan at any time.'));
1370
+ return null;
1852
1371
  }
1853
-
1854
- // dist/commands/import-bundle.js
1855
- import { readFile as readFile7, writeFile as writeFile4, mkdir as mkdir3 } from "node:fs/promises";
1856
- import { join as join11, dirname as dirname2 } from "node:path";
1857
- import { existsSync as existsSync10 } from "node:fs";
1858
- import chalk14 from "chalk";
1859
- import { confirm, input as input4 } from "@inquirer/prompts";
1860
- async function importBundleCommand(zipPath) {
1861
- if (!existsSync10(zipPath)) {
1862
- console.error(chalk14.red(`File not found: ${zipPath}`));
1863
- process.exit(1);
1372
+ function parseServers(data, source) {
1373
+ if (!data?.mcpServers)
1374
+ return [];
1375
+ const entries = [];
1376
+ for (const [name, server] of Object.entries(data.mcpServers)) {
1377
+ const serverType = server.type === "http" ? "http" : "stdio";
1378
+ entries.push({
1379
+ name,
1380
+ source,
1381
+ type: serverType,
1382
+ command: server.command,
1383
+ args: server.args,
1384
+ env: server.env,
1385
+ url: server.url
1386
+ });
1864
1387
  }
1865
- const JSZip = (await import("jszip")).default;
1866
- const zipData = await readFile7(zipPath);
1867
- const zip = await JSZip.loadAsync(zipData);
1868
- const manifestFile = zip.file("manifest.json");
1869
- if (!manifestFile) {
1870
- console.error(chalk14.red("Invalid bundle: missing manifest.json"));
1871
- process.exit(1);
1388
+ return entries;
1389
+ }
1390
+ function parseFlatConfig(data, source) {
1391
+ if (!data)
1392
+ return [];
1393
+ const entries = [];
1394
+ for (const [name, server] of Object.entries(data)) {
1395
+ if (typeof server !== "object" || server === null)
1396
+ continue;
1397
+ if (!server.command && !server.url)
1398
+ continue;
1399
+ const serverType = server.type === "http" ? "http" : "stdio";
1400
+ entries.push({
1401
+ name,
1402
+ source,
1403
+ type: serverType,
1404
+ command: server.command,
1405
+ args: server.args,
1406
+ env: server.env,
1407
+ url: server.url
1408
+ });
1872
1409
  }
1873
- const manifest = JSON.parse(await manifestFile.async("string"));
1874
- console.log(chalk14.blue(`
1875
- Bundle: ${manifest.folderName}`));
1876
- console.log(` Exported by: ${manifest.ownerEmail}`);
1877
- console.log(` Exported at: ${manifest.exportedAt}`);
1878
- console.log(` Files: ${manifest.fileCount}`);
1879
- const files = [];
1880
- for (const [path, file] of Object.entries(zip.files)) {
1881
- if (path.startsWith("claude-files/") && !file.dir) {
1882
- const relativePath = path.replace("claude-files/", "");
1883
- const content = await file.async("string");
1884
- files.push({ filePath: relativePath, content });
1410
+ return entries;
1411
+ }
1412
+ async function readAllMcpConfigs() {
1413
+ const home = homedir7();
1414
+ const entries = [];
1415
+ const userConfig = await readJsonSafe(join12(home, ".claude.json"));
1416
+ entries.push(...parseServers(userConfig, "global"));
1417
+ const globalMcp = await readJsonSafe(join12(home, ".claude", "mcp.json"));
1418
+ entries.push(...parseServers(globalMcp, "global"));
1419
+ const cwdMcp = await readJsonSafe(join12(process.cwd(), ".mcp.json"));
1420
+ entries.push(...parseServers(cwdMcp, "project"));
1421
+ const pluginsBase = join12(home, ".claude", "plugins", "marketplaces");
1422
+ if (existsSync11(pluginsBase)) {
1423
+ try {
1424
+ const marketplaces = await readdir3(pluginsBase, { withFileTypes: true });
1425
+ for (const mp of marketplaces) {
1426
+ if (!mp.isDirectory())
1427
+ continue;
1428
+ const extDir = join12(pluginsBase, mp.name, "external_plugins");
1429
+ if (!existsSync11(extDir))
1430
+ continue;
1431
+ const plugins = await readdir3(extDir, { withFileTypes: true });
1432
+ for (const plugin of plugins) {
1433
+ if (!plugin.isDirectory())
1434
+ continue;
1435
+ const pluginMcp = await readJsonSafe(join12(extDir, plugin.name, ".mcp.json"));
1436
+ entries.push(...parseServers(pluginMcp, "plugin"));
1437
+ }
1438
+ }
1439
+ } catch {
1885
1440
  }
1886
1441
  }
1887
- if (files.length === 0) {
1888
- console.log(chalk14.yellow("No Claude config files found in bundle."));
1889
- return;
1442
+ const cacheBase = join12(home, ".claude", "plugins", "cache");
1443
+ if (existsSync11(cacheBase)) {
1444
+ try {
1445
+ const registries = await readdir3(cacheBase, { withFileTypes: true });
1446
+ for (const reg of registries) {
1447
+ if (!reg.isDirectory())
1448
+ continue;
1449
+ const regDir = join12(cacheBase, reg.name);
1450
+ const plugins = await readdir3(regDir, { withFileTypes: true });
1451
+ for (const plugin of plugins) {
1452
+ if (!plugin.isDirectory())
1453
+ continue;
1454
+ const versionDirs = await readdir3(join12(regDir, plugin.name), { withFileTypes: true });
1455
+ for (const ver of versionDirs) {
1456
+ if (!ver.isDirectory())
1457
+ continue;
1458
+ const mcpPath = join12(regDir, plugin.name, ver.name, ".mcp.json");
1459
+ const flatData = await readJsonSafe(mcpPath);
1460
+ if (flatData) {
1461
+ entries.push(...parseFlatConfig(flatData, "plugin"));
1462
+ break;
1463
+ }
1464
+ }
1465
+ }
1466
+ }
1467
+ } catch {
1468
+ }
1890
1469
  }
1891
- console.log(chalk14.blue(`
1892
- Files to extract:`));
1893
- for (const f of files) {
1894
- console.log(` ${f.filePath}`);
1470
+ const seen = /* @__PURE__ */ new Set();
1471
+ const deduped = [];
1472
+ for (const entry of entries) {
1473
+ if (seen.has(entry.name))
1474
+ continue;
1475
+ seen.add(entry.name);
1476
+ deduped.push(entry);
1895
1477
  }
1896
- const targetDir = await input4({
1897
- message: "Extract to directory:",
1898
- default: process.cwd()
1899
- });
1900
- const proceed = await confirm({
1901
- message: `Extract ${files.length} file(s) to ${targetDir}?`
1902
- });
1903
- if (!proceed) {
1904
- console.log(chalk14.yellow("Cancelled."));
1905
- return;
1478
+ return deduped;
1479
+ }
1480
+ var init_read_configs = __esm({
1481
+ "dist/mcp/read-configs.js"() {
1482
+ "use strict";
1906
1483
  }
1907
- for (const file of files) {
1908
- const fullPath = join11(targetDir, file.filePath);
1909
- const dir = dirname2(fullPath);
1910
- if (!existsSync10(dir)) {
1911
- await mkdir3(dir, { recursive: true });
1912
- }
1913
- await writeFile4(fullPath, file.content, "utf-8");
1914
- console.log(chalk14.green(` \u2713 ${file.filePath}`));
1484
+ });
1485
+
1486
+ // dist/mcp/scan-processes.js
1487
+ import { execFileSync as execFileSync4 } from "node:child_process";
1488
+ function parsePsOutput(output) {
1489
+ const lines = output.trim().split("\n").slice(1);
1490
+ const entries = [];
1491
+ for (const line of lines) {
1492
+ const trimmed = line.trim();
1493
+ const match = trimmed.match(/^\s*(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(.+)$/);
1494
+ if (!match)
1495
+ continue;
1496
+ entries.push({
1497
+ pid: parseInt(match[1], 10),
1498
+ tty: match[2],
1499
+ etimeRaw: match[3],
1500
+ rss: parseInt(match[4], 10),
1501
+ args: match[5]
1502
+ });
1915
1503
  }
1916
- console.log(chalk14.green(`
1917
- Done! ${files.length} file(s) extracted to ${targetDir}`));
1504
+ return entries;
1918
1505
  }
1919
-
1920
- // dist/commands/admin-update-tool.js
1921
- import chalk15 from "chalk";
1922
- var VALID_CATEGORIES = ["framework", "runtime", "cli", "mcp", "package", "database", "other"];
1923
- async function adminUpdateToolCommand(options) {
1924
- const { supabase } = await getAuthenticatedClient();
1925
- if (!options.name) {
1926
- console.error(chalk15.red("--name is required."));
1927
- process.exit(1);
1506
+ function parseEtime(etime) {
1507
+ let days = 0;
1508
+ let rest = etime;
1509
+ if (rest.includes("-")) {
1510
+ const [d, r] = rest.split("-");
1511
+ days = parseInt(d, 10);
1512
+ rest = r;
1928
1513
  }
1929
- if (options.category && !VALID_CATEGORIES.includes(options.category)) {
1930
- console.error(chalk15.red(`Invalid category. Must be one of: ${VALID_CATEGORIES.join(", ")}`));
1931
- process.exit(1);
1514
+ const parts = rest.split(":").map(Number);
1515
+ if (parts.length === 3) {
1516
+ return days * 86400 + parts[0] * 3600 + parts[1] * 60 + parts[2];
1517
+ } else if (parts.length === 2) {
1518
+ return days * 86400 + parts[0] * 60 + parts[1];
1932
1519
  }
1933
- const { data: existing } = await supabase.from("tools_registry").select("id, name, display_name, category").eq("name", options.name).maybeSingle();
1934
- if (existing) {
1935
- const updates = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
1936
- if (options.display)
1937
- updates.display_name = options.display;
1938
- if (options.category)
1939
- updates.category = options.category;
1940
- if (options.stable)
1941
- updates.latest_stable = options.stable;
1942
- if (options.beta)
1943
- updates.latest_beta = options.beta;
1944
- if (options.source)
1945
- updates.source_url = options.source;
1946
- if (options.install)
1947
- updates.install_url = options.install;
1948
- if (options.notes)
1949
- updates.notes = options.notes;
1950
- const { error } = await supabase.from("tools_registry").update(updates).eq("id", existing.id);
1951
- if (error) {
1952
- console.error(chalk15.red(`Failed to update: ${error.message}`));
1953
- process.exit(1);
1954
- }
1955
- console.log(chalk15.green(`Updated "${existing.display_name}" in the registry.`));
1956
- } else {
1957
- if (!options.display || !options.category) {
1958
- console.error(chalk15.red("New tools require --display and --category."));
1959
- process.exit(1);
1520
+ return days * 86400 + (parts[0] ?? 0);
1521
+ }
1522
+ function findProcessesForConfig(config, processes) {
1523
+ if (config.type === "http")
1524
+ return [];
1525
+ const packageName = config.args ? extractPackageName(config.args) : null;
1526
+ const matches = [];
1527
+ for (const proc of processes) {
1528
+ let matched = false;
1529
+ if (packageName && proc.args.includes(packageName)) {
1530
+ matched = true;
1531
+ } else if (config.command === "node" && config.args?.[0]) {
1532
+ if (proc.args.includes(config.args[0])) {
1533
+ matched = true;
1534
+ }
1960
1535
  }
1961
- const { error } = await supabase.from("tools_registry").insert({
1962
- name: options.name,
1963
- display_name: options.display,
1964
- category: options.category,
1965
- latest_stable: options.stable ?? null,
1966
- latest_beta: options.beta ?? null,
1967
- source_url: options.source ?? null,
1968
- install_url: options.install ?? null,
1969
- notes: options.notes ?? null
1970
- });
1971
- if (error) {
1972
- console.error(chalk15.red(`Failed to create: ${error.message}`));
1973
- process.exit(1);
1536
+ if (matched) {
1537
+ matches.push({
1538
+ pid: proc.pid,
1539
+ tty: proc.tty === "?" ? "" : proc.tty,
1540
+ uptimeSeconds: parseEtime(proc.etimeRaw),
1541
+ memoryMb: Math.round(proc.rss / 1024),
1542
+ commandLine: proc.args
1543
+ });
1974
1544
  }
1975
- console.log(chalk15.green(`Added "${options.display}" to the registry.`));
1976
1545
  }
1546
+ return matches;
1977
1547
  }
1978
-
1979
- // dist/commands/admin-list-tools.js
1980
- import chalk16 from "chalk";
1981
- async function adminListToolsCommand() {
1982
- const { supabase } = await getAuthenticatedClient();
1983
- const { data: tools, error } = await supabase.from("tools_registry").select("*").order("category").order("display_name");
1984
- if (error) {
1985
- console.error(chalk16.red(`Failed to fetch tools: ${error.message}`));
1986
- process.exit(1);
1548
+ function getProcessTable() {
1549
+ try {
1550
+ const output = execFileSync4("ps", ["-eo", "pid,tty,etime,rss,args"], {
1551
+ encoding: "utf-8",
1552
+ timeout: 5e3
1553
+ });
1554
+ return parsePsOutput(output);
1555
+ } catch {
1556
+ return [];
1987
1557
  }
1988
- if (!tools?.length) {
1989
- console.log(chalk16.yellow("No tools in the registry."));
1990
- return;
1558
+ }
1559
+ var init_scan_processes = __esm({
1560
+ "dist/mcp/scan-processes.js"() {
1561
+ "use strict";
1562
+ init_read_configs();
1991
1563
  }
1992
- const nameW = Math.max(16, ...tools.map((t) => t.display_name.length));
1993
- const catW = Math.max(10, ...tools.map((t) => t.category.length));
1994
- const stableW = Math.max(8, ...tools.map((t) => (t.latest_stable ?? "\u2014").length));
1995
- const betaW = Math.max(8, ...tools.map((t) => (t.latest_beta ?? "\u2014").length));
1996
- const header = [
1997
- "Name".padEnd(nameW),
1998
- "Category".padEnd(catW),
1999
- "Stable".padEnd(stableW),
2000
- "Beta".padEnd(betaW),
2001
- "Last Checked"
2002
- ].join(" ");
2003
- console.log(chalk16.bold(header));
2004
- console.log("\u2500".repeat(header.length));
2005
- for (const tool of tools) {
2006
- const lastChecked = tool.updated_at ? formatRelative(new Date(tool.updated_at)) : "\u2014";
2007
- const row = [
2008
- tool.display_name.padEnd(nameW),
2009
- tool.category.padEnd(catW),
2010
- (tool.latest_stable ?? "\u2014").padEnd(stableW),
2011
- (tool.latest_beta ?? "\u2014").padEnd(betaW),
2012
- lastChecked
2013
- ].join(" ");
2014
- console.log(row);
1564
+ });
1565
+
1566
+ // dist/commands/mcp-watch.js
1567
+ var mcp_watch_exports = {};
1568
+ __export(mcp_watch_exports, {
1569
+ mcpWatchCommand: () => mcpWatchCommand
1570
+ });
1571
+ import chalk18 from "chalk";
1572
+ import { execFileSync as execFileSync5 } from "node:child_process";
1573
+ import { createHash as createHash2 } from "node:crypto";
1574
+ function detectTty() {
1575
+ try {
1576
+ const result = execFileSync5("ps", ["-o", "tty=", "-p", String(process.pid)], {
1577
+ encoding: "utf-8",
1578
+ timeout: 3e3
1579
+ }).trim();
1580
+ return result && result !== "?" ? result : null;
1581
+ } catch {
1582
+ return null;
2015
1583
  }
2016
- console.log(chalk16.grey(`
2017
- ${tools.length} tool(s) in registry.`));
2018
1584
  }
2019
- function formatRelative(date) {
2020
- const diff = Date.now() - date.getTime();
2021
- const hours = Math.floor(diff / (1e3 * 60 * 60));
2022
- if (hours < 1)
2023
- return "just now";
2024
- if (hours < 24)
2025
- return `${hours}h ago`;
2026
- const days = Math.floor(hours / 24);
2027
- if (days < 7)
2028
- return `${days}d ago`;
2029
- return date.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
1585
+ function checkEnvVars(config) {
1586
+ const required = config.env ? Object.keys(config.env) : [];
1587
+ const missing = required.filter((key) => !process.env[key]);
1588
+ return { required, missing };
2030
1589
  }
2031
-
2032
- // dist/commands/admin-fetch-versions.js
2033
- import chalk17 from "chalk";
2034
-
2035
- // dist/commands/version-sources.js
2036
- var VERSION_SOURCES = {
2037
- "next": { type: "npm", package: "next" },
2038
- "react": { type: "npm", package: "react" },
2039
- "react-dom": { type: "npm", package: "react-dom" },
2040
- "typescript": { type: "npm", package: "typescript" },
2041
- "tailwindcss": { type: "npm", package: "tailwindcss" },
2042
- "esbuild": { type: "npm", package: "esbuild" },
2043
- "chalk": { type: "npm", package: "chalk" },
2044
- "commander": { type: "npm", package: "commander" },
2045
- "inquirer": { type: "npm", package: "inquirer" },
2046
- "playwright": { type: "npm", package: "playwright" },
2047
- "resend": { type: "npm", package: "resend" },
2048
- "@supabase/supabase-js": { type: "npm", package: "@supabase/supabase-js" },
2049
- "pnpm": { type: "npm", package: "pnpm" },
2050
- "claude-code": { type: "npm", package: "@anthropic-ai/claude-code" },
2051
- "turborepo": { type: "npm", package: "turbo" },
2052
- "npm": { type: "npm", package: "npm" },
2053
- "node": { type: "github", repo: "nodejs/node" },
2054
- "supabase-cli": { type: "npm", package: "supabase" }
2055
- };
2056
-
2057
- // dist/commands/admin-fetch-versions.js
2058
- async function adminFetchVersionsCommand() {
2059
- const { supabase } = await getAuthenticatedClient();
2060
- const { data: tools, error } = await supabase.from("tools_registry").select("*").order("display_name");
2061
- if (error) {
2062
- console.error(chalk17.red(`Failed to fetch tools: ${error.message}`));
2063
- process.exit(1);
2064
- }
2065
- if (!tools?.length) {
2066
- console.log(chalk17.yellow("No tools in the registry."));
2067
- }
2068
- const { data: allProjectToolings } = await supabase.from("project_toolings").select("tool_name, detection_source").is("tool_id", null);
2069
- const registeredNames = new Set((tools ?? []).map((t) => t.name));
2070
- const unregisteredNames = /* @__PURE__ */ new Set();
2071
- for (const pt of allProjectToolings ?? []) {
2072
- if (!registeredNames.has(pt.tool_name) && pt.detection_source === "package.json") {
2073
- unregisteredNames.add(pt.tool_name);
1590
+ function buildRows(configs) {
1591
+ const processes = getProcessTable();
1592
+ const rows = [];
1593
+ for (const config of configs) {
1594
+ const { required, missing } = checkEnvVars(config);
1595
+ const packageName = config.args ? extractPackageName(config.args) : null;
1596
+ if (config.type === "http") {
1597
+ rows.push({
1598
+ server_name: config.name,
1599
+ config_source: config.source,
1600
+ server_type: "http",
1601
+ command: null,
1602
+ package_name: null,
1603
+ http_url: config.url ?? null,
1604
+ status: "stopped",
1605
+ pid: null,
1606
+ session_tty: null,
1607
+ uptime_seconds: null,
1608
+ memory_mb: null,
1609
+ env_vars_required: required.length ? required : null,
1610
+ env_vars_missing: missing.length ? missing : null,
1611
+ error_detail: "HTTP server \u2014 cannot verify from CLI"
1612
+ });
1613
+ continue;
2074
1614
  }
2075
- }
2076
- if (unregisteredNames.size > 0) {
2077
- console.log(chalk17.blue(`Auto-registering ${unregisteredNames.size} unverified package(s)...
2078
- `));
2079
- for (const name of unregisteredNames) {
2080
- const displayName = name;
2081
- const { data: inserted, error: insertError } = await supabase.from("tools_registry").upsert({
2082
- name,
2083
- display_name: displayName,
2084
- category: "package",
2085
- source_url: `https://www.npmjs.com/package/${name}`,
2086
- install_url: `https://www.npmjs.com/package/${name}`
2087
- }, { onConflict: "name" }).select().single();
2088
- if (!insertError && inserted) {
2089
- tools.push(inserted);
2090
- await supabase.from("project_toolings").update({ tool_id: inserted.id }).eq("tool_name", name).is("tool_id", null);
1615
+ if (missing.length > 0) {
1616
+ rows.push({
1617
+ server_name: config.name,
1618
+ config_source: config.source,
1619
+ server_type: "stdio",
1620
+ command: config.command ?? null,
1621
+ package_name: packageName,
1622
+ http_url: null,
1623
+ status: "error",
1624
+ pid: null,
1625
+ session_tty: null,
1626
+ uptime_seconds: null,
1627
+ memory_mb: null,
1628
+ env_vars_required: required,
1629
+ env_vars_missing: missing,
1630
+ error_detail: `Missing env: ${missing.join(", ")}`
1631
+ });
1632
+ continue;
1633
+ }
1634
+ const matches = findProcessesForConfig(config, processes);
1635
+ if (matches.length === 0) {
1636
+ rows.push({
1637
+ server_name: config.name,
1638
+ config_source: config.source,
1639
+ server_type: "stdio",
1640
+ command: config.command ?? null,
1641
+ package_name: packageName,
1642
+ http_url: null,
1643
+ status: "stopped",
1644
+ pid: null,
1645
+ session_tty: null,
1646
+ uptime_seconds: null,
1647
+ memory_mb: null,
1648
+ env_vars_required: required.length ? required : null,
1649
+ env_vars_missing: null,
1650
+ error_detail: null
1651
+ });
1652
+ } else {
1653
+ for (const match of matches) {
1654
+ rows.push({
1655
+ server_name: config.name,
1656
+ config_source: config.source,
1657
+ server_type: "stdio",
1658
+ command: config.command ?? null,
1659
+ package_name: packageName,
1660
+ http_url: null,
1661
+ status: "running",
1662
+ pid: match.pid,
1663
+ session_tty: match.tty || null,
1664
+ uptime_seconds: match.uptimeSeconds,
1665
+ memory_mb: match.memoryMb,
1666
+ env_vars_required: required.length ? required : null,
1667
+ env_vars_missing: null,
1668
+ error_detail: null
1669
+ });
2091
1670
  }
2092
1671
  }
2093
1672
  }
2094
- if (!tools?.length) {
2095
- console.log(chalk17.yellow("No tools to fetch."));
2096
- return;
2097
- }
2098
- console.log(chalk17.bold(`Fetching versions for ${tools.length} tool(s)...
1673
+ return rows;
1674
+ }
1675
+ function printTable(rows, deviceName) {
1676
+ process.stdout.write("\x1B[2J\x1B[H");
1677
+ console.log(chalk18.bold.cyan(`
1678
+ MCP Monitor \u2014 ${deviceName}`));
1679
+ console.log(chalk18.dim(` ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \xB7 refreshes every 30s \xB7 Ctrl+C to stop
2099
1680
  `));
2100
- const results = await Promise.all(tools.map(async (tool) => {
2101
- const source = VERSION_SOURCES[tool.name] ?? { type: "npm", package: tool.name };
2102
- try {
2103
- const { stable, beta } = await fetchVersions(source);
2104
- const stableChanged = stable !== tool.latest_stable;
2105
- const betaChanged = beta !== tool.latest_beta;
2106
- const updates = {
2107
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
2108
- };
2109
- if (stableChanged)
2110
- updates.latest_stable = stable;
2111
- if (betaChanged)
2112
- updates.latest_beta = beta;
2113
- const { error: updateError } = await supabase.from("tools_registry").update(updates).eq("id", tool.id);
2114
- if (updateError) {
2115
- return { displayName: tool.display_name, stable, beta, status: "failed" };
1681
+ const running = rows.filter((r) => r.status === "running");
1682
+ const stopped = rows.filter((r) => r.status === "stopped");
1683
+ const errored = rows.filter((r) => r.status === "error");
1684
+ const byTty = /* @__PURE__ */ new Map();
1685
+ for (const r of running) {
1686
+ const key = r.session_tty ?? "unknown";
1687
+ const list = byTty.get(key) ?? [];
1688
+ list.push(r);
1689
+ byTty.set(key, list);
1690
+ }
1691
+ if (byTty.size > 0) {
1692
+ for (const [tty, servers] of byTty) {
1693
+ const totalMem = servers.reduce((sum, s) => sum + (s.memory_mb ?? 0), 0);
1694
+ console.log(chalk18.green(` Session ${tty}`) + chalk18.dim(` \u2014 ${servers.length} server${servers.length !== 1 ? "s" : ""} \xB7 ${totalMem} MB`));
1695
+ for (const s of servers) {
1696
+ const uptime = formatUptime(s.uptime_seconds ?? 0);
1697
+ 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}`);
2116
1698
  }
2117
- return {
2118
- displayName: tool.display_name,
2119
- stable,
2120
- beta,
2121
- status: stableChanged || betaChanged ? "updated" : "unchanged"
2122
- };
2123
- } catch {
2124
- return { displayName: tool.display_name, stable: null, beta: null, status: "failed" };
1699
+ console.log("");
2125
1700
  }
2126
- }));
2127
- const nameW = Math.max(16, ...results.map((r) => r.displayName.length));
2128
- const stableW = Math.max(8, ...results.map((r) => (r.stable ?? "\u2014").length));
2129
- const betaW = Math.max(8, ...results.map((r) => (r.beta ?? "\u2014").length));
2130
- const statusW = 10;
2131
- const header = [
2132
- "Tool".padEnd(nameW),
2133
- "Stable".padEnd(stableW),
2134
- "Beta".padEnd(betaW),
2135
- "Status".padEnd(statusW)
2136
- ].join(" ");
2137
- console.log(chalk17.bold(header));
2138
- console.log("\u2500".repeat(header.length));
2139
- for (const result of results) {
2140
- const statusColour = result.status === "updated" ? chalk17.green : result.status === "unchanged" ? chalk17.grey : result.status === "failed" ? chalk17.red : chalk17.yellow;
2141
- const row = [
2142
- result.displayName.padEnd(nameW),
2143
- (result.stable ?? "\u2014").padEnd(stableW),
2144
- (result.beta ?? "\u2014").padEnd(betaW),
2145
- statusColour(result.status.padEnd(statusW))
2146
- ].join(" ");
2147
- console.log(row);
2148
1701
  }
2149
- const updated = results.filter((r) => r.status === "updated").length;
2150
- const unchanged = results.filter((r) => r.status === "unchanged").length;
2151
- const failed = results.filter((r) => r.status === "failed").length;
2152
- const noSource = results.filter((r) => r.status === "no source").length;
2153
- console.log(chalk17.grey(`
2154
- ${results.length} tool(s): `) + chalk17.green(`${updated} updated`) + ", " + chalk17.grey(`${unchanged} unchanged`) + ", " + chalk17.red(`${failed} failed`) + ", " + chalk17.yellow(`${noSource} no source`));
2155
- }
2156
- async function fetchVersions(source) {
2157
- const controller = new AbortController();
2158
- const timeout = setTimeout(() => controller.abort(), 3e3);
2159
- try {
2160
- if (source.type === "npm") {
2161
- return await fetchNpmVersions(source.package, controller.signal);
2162
- } else {
2163
- return await fetchGitHubVersions(source.repo, controller.signal);
1702
+ if (stopped.length > 0 || errored.length > 0) {
1703
+ const notRunning = [...stopped, ...errored];
1704
+ console.log(chalk18.yellow(` Not Running (${notRunning.length})`));
1705
+ for (const s of notRunning) {
1706
+ const icon = s.status === "error" ? chalk18.red("\u2717") : chalk18.yellow("\u25CB");
1707
+ const detail = s.error_detail ? chalk18.dim(` \u2014 ${s.error_detail}`) : "";
1708
+ console.log(` ${icon} ${s.server_name.padEnd(20)} ${chalk18.dim(s.config_source.padEnd(10))}${detail}`);
2164
1709
  }
2165
- } finally {
2166
- clearTimeout(timeout);
1710
+ console.log("");
2167
1711
  }
2168
- }
2169
- async function fetchNpmVersions(packageName, signal) {
2170
- const url = `https://registry.npmjs.org/-/package/${encodeURIComponent(packageName)}/dist-tags`;
2171
- const res = await fetch(url, { signal });
2172
- if (!res.ok) {
2173
- throw new Error(`npm registry returned ${res.status}`);
1712
+ if (rows.length === 0) {
1713
+ console.log(chalk18.yellow(" No MCP servers configured."));
1714
+ console.log(chalk18.dim(" Configure servers in ~/.claude/mcp.json or .mcp.json\n"));
2174
1715
  }
2175
- const tags = await res.json();
2176
- const stable = tags.latest ?? null;
2177
- const beta = tags.next ?? tags.beta ?? tags.rc ?? null;
2178
- return { stable, beta };
2179
1716
  }
2180
- async function fetchGitHubVersions(repo, signal) {
2181
- const url = `https://api.github.com/repos/${repo}/releases?per_page=20`;
2182
- const res = await fetch(url, {
2183
- signal,
2184
- headers: { Accept: "application/vnd.github+json" }
2185
- });
2186
- if (!res.ok) {
2187
- throw new Error(`GitHub API returned ${res.status}`);
2188
- }
2189
- const releases = await res.json();
2190
- let stable = null;
2191
- let beta = null;
2192
- for (const release of releases) {
2193
- if (release.draft)
2194
- continue;
2195
- const version = release.tag_name.replace(/^v/, "");
2196
- if (!release.prerelease && !stable) {
2197
- stable = version;
2198
- }
2199
- if (release.prerelease && !beta) {
2200
- beta = version;
1717
+ function formatUptime(seconds) {
1718
+ if (seconds < 60)
1719
+ return `${seconds}s`;
1720
+ if (seconds < 3600)
1721
+ return `${Math.floor(seconds / 60)}m`;
1722
+ const h = Math.floor(seconds / 3600);
1723
+ const m = Math.floor(seconds % 3600 / 60);
1724
+ return `${h}h ${m}m`;
1725
+ }
1726
+ async function mcpWatchCommand() {
1727
+ const { supabase, userId } = await getAuthenticatedClient();
1728
+ const deviceId = await resolveDeviceId(supabase, userId);
1729
+ const deviceName = detectDeviceName();
1730
+ const myPid = process.pid;
1731
+ const myTty = detectTty();
1732
+ console.log(chalk18.blue(`Starting MCP monitor for ${deviceName}...`));
1733
+ await supabase.from("mcp_watchers").upsert({
1734
+ device_id: deviceId,
1735
+ pid: myPid,
1736
+ tty: myTty,
1737
+ cli_version: CURRENT_VERSION,
1738
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
1739
+ last_heartbeat: (/* @__PURE__ */ new Date()).toISOString()
1740
+ }, { onConflict: "device_id,pid" });
1741
+ const staleThreshold = new Date(Date.now() - 12e4).toISOString();
1742
+ await supabase.from("mcp_watchers").delete().eq("device_id", deviceId).lt("last_heartbeat", staleThreshold);
1743
+ async function cycle() {
1744
+ const configs = await readAllMcpConfigs();
1745
+ const rows = buildRows(configs);
1746
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1747
+ await supabase.from("mcp_server_status").delete().eq("device_id", deviceId);
1748
+ if (rows.length > 0) {
1749
+ await supabase.from("mcp_server_status").insert(rows.map((row) => ({
1750
+ device_id: deviceId,
1751
+ ...row,
1752
+ checked_at: now
1753
+ })));
2201
1754
  }
2202
- if (stable && beta)
2203
- break;
1755
+ await supabase.from("mcp_watchers").update({ last_heartbeat: now }).eq("device_id", deviceId).eq("pid", myPid);
1756
+ printTable(rows, deviceName);
2204
1757
  }
2205
- return { stable, beta };
1758
+ await cycle();
1759
+ let lastEnvHash = "";
1760
+ async function envCycle() {
1761
+ const state = await loadState();
1762
+ if (!state.lastFolderId)
1763
+ return;
1764
+ const { data: dp } = await supabase.from("device_paths").select("path").eq("folder_id", state.lastFolderId).eq("device_name", deviceName).maybeSingle();
1765
+ if (!dp?.path)
1766
+ return;
1767
+ const envManifest = await scanEnvManifest(dp.path);
1768
+ if (!envManifest)
1769
+ return;
1770
+ const hash = createHash2("sha256").update(JSON.stringify(envManifest)).digest("hex");
1771
+ if (hash === lastEnvHash)
1772
+ return;
1773
+ lastEnvHash = hash;
1774
+ await supabase.from("claude_folders").update({ env_manifest_json: envManifest }).eq("id", state.lastFolderId);
1775
+ }
1776
+ await envCycle();
1777
+ const envInterval = setInterval(envCycle, ENV_POLL_INTERVAL_MS);
1778
+ const interval = setInterval(cycle, POLL_INTERVAL_MS);
1779
+ const shutdown = async () => {
1780
+ clearInterval(interval);
1781
+ clearInterval(envInterval);
1782
+ await supabase.from("mcp_watchers").delete().eq("device_id", deviceId).eq("pid", myPid);
1783
+ console.log(chalk18.dim("\nMCP monitor stopped."));
1784
+ process.exit(0);
1785
+ };
1786
+ process.on("SIGINT", () => {
1787
+ void shutdown();
1788
+ });
1789
+ process.on("SIGTERM", () => {
1790
+ void shutdown();
1791
+ });
2206
1792
  }
1793
+ var POLL_INTERVAL_MS, ENV_POLL_INTERVAL_MS;
1794
+ var init_mcp_watch = __esm({
1795
+ "dist/commands/mcp-watch.js"() {
1796
+ "use strict";
1797
+ init_auth();
1798
+ init_device_utils();
1799
+ init_read_configs();
1800
+ init_scan_processes();
1801
+ init_check_update();
1802
+ init_env_manifest_scanner();
1803
+ init_config();
1804
+ POLL_INTERVAL_MS = 3e4;
1805
+ ENV_POLL_INTERVAL_MS = 3e5;
1806
+ }
1807
+ });
2207
1808
 
2208
- // dist/commands/mcp-watch.js
2209
- import chalk18 from "chalk";
2210
- import { execFileSync as execFileSync5 } from "node:child_process";
1809
+ // dist/index.js
1810
+ import { Command } from "commander";
2211
1811
 
2212
- // dist/mcp/read-configs.js
2213
- import { readFile as readFile8 } from "node:fs/promises";
2214
- import { join as join12 } from "node:path";
2215
- import { homedir as homedir7 } from "node:os";
2216
- import { existsSync as existsSync11 } from "node:fs";
2217
- import { readdir as readdir3 } from "node:fs/promises";
2218
- async function readJsonSafe(path) {
2219
- try {
2220
- if (!existsSync11(path))
2221
- return null;
2222
- const raw = await readFile8(path, "utf-8");
2223
- return JSON.parse(raw);
2224
- } catch {
2225
- return null;
1812
+ // dist/commands/login.js
1813
+ init_dist();
1814
+ init_config();
1815
+ import chalk from "chalk";
1816
+ import { input, password } from "@inquirer/prompts";
1817
+ async function loginCommand() {
1818
+ console.log(chalk.blue("MD4AI Login\n"));
1819
+ const email = await input({ message: "Email:" });
1820
+ const pwd = await password({ message: "Password:" });
1821
+ const anonKey = getAnonKey();
1822
+ const supabase = createSupabaseClient(anonKey);
1823
+ const { data, error } = await supabase.auth.signInWithPassword({
1824
+ email,
1825
+ password: pwd
1826
+ });
1827
+ if (error) {
1828
+ console.error(chalk.red(`Login failed: ${error.message}`));
1829
+ process.exit(1);
2226
1830
  }
1831
+ await saveCredentials({
1832
+ accessToken: data.session.access_token,
1833
+ refreshToken: data.session.refresh_token,
1834
+ expiresAt: Date.now() + data.session.expires_in * 1e3,
1835
+ userId: data.user.id,
1836
+ email: data.user.email ?? email
1837
+ });
1838
+ console.log(chalk.green(`
1839
+ Logged in as ${data.user.email}`));
2227
1840
  }
2228
- function extractPackageName(args) {
2229
- for (const arg of args) {
2230
- if (arg.startsWith("-"))
2231
- continue;
2232
- if (arg.includes("mcp") || arg.startsWith("@"))
2233
- return arg;
2234
- }
2235
- for (const arg of args) {
2236
- if (!arg.startsWith("-"))
2237
- return arg;
2238
- }
2239
- return null;
1841
+
1842
+ // dist/commands/logout.js
1843
+ init_config();
1844
+ import chalk2 from "chalk";
1845
+ async function logoutCommand() {
1846
+ await clearCredentials();
1847
+ console.log(chalk2.green("Logged out successfully."));
2240
1848
  }
2241
- function parseServers(data, source) {
2242
- if (!data?.mcpServers)
2243
- return [];
2244
- const entries = [];
2245
- for (const [name, server] of Object.entries(data.mcpServers)) {
2246
- const serverType = server.type === "http" ? "http" : "stdio";
2247
- entries.push({
2248
- name,
2249
- source,
2250
- type: serverType,
2251
- command: server.command,
2252
- args: server.args,
2253
- env: server.env,
2254
- url: server.url
1849
+
1850
+ // dist/commands/status.js
1851
+ init_dist();
1852
+ init_config();
1853
+ import chalk3 from "chalk";
1854
+ async function statusCommand() {
1855
+ const creds = await loadCredentials();
1856
+ if (!creds?.accessToken) {
1857
+ console.log(chalk3.yellow("Not logged in. Run: md4ai login"));
1858
+ return;
1859
+ }
1860
+ console.log(chalk3.blue("MD4AI Status\n"));
1861
+ console.log(` User: ${creds.email}`);
1862
+ console.log(` Expires: ${new Date(creds.expiresAt).toLocaleString()}`);
1863
+ try {
1864
+ const anonKey = getAnonKey();
1865
+ const supabase = createSupabaseClient(anonKey, creds.accessToken);
1866
+ const { count: folderCount } = await supabase.from("claude_folders").select("*", { count: "exact", head: true });
1867
+ const { count: deviceCount } = await supabase.from("device_paths").select("*", { count: "exact", head: true });
1868
+ console.log(` Folders: ${folderCount ?? 0}`);
1869
+ console.log(` Devices: ${deviceCount ?? 0}`);
1870
+ const state = await loadState();
1871
+ console.log(` Last sync: ${state.lastSyncAt ?? "never"}`);
1872
+ } catch {
1873
+ console.log(chalk3.yellow(" (Could not fetch remote data)"));
1874
+ }
1875
+ }
1876
+
1877
+ // dist/commands/add-folder.js
1878
+ init_auth();
1879
+ import chalk5 from "chalk";
1880
+ import { input as input2, select } from "@inquirer/prompts";
1881
+ async function addFolderCommand() {
1882
+ const { supabase, userId } = await getAuthenticatedClient();
1883
+ const name = await input2({ message: "Folder name:" });
1884
+ const description = await input2({ message: "Description (optional):" });
1885
+ const { data: teams } = await supabase.from("team_members").select("team_id, teams(id, name)").eq("user_id", userId);
1886
+ const { data: ownedTeams } = await supabase.from("teams").select("id, name").eq("created_by", userId);
1887
+ const allTeams = /* @__PURE__ */ new Map();
1888
+ for (const t of ownedTeams ?? []) {
1889
+ allTeams.set(t.id, t.name);
1890
+ }
1891
+ for (const t of teams ?? []) {
1892
+ const team = t.teams;
1893
+ if (team)
1894
+ allTeams.set(team.id, team.name);
1895
+ }
1896
+ let teamId = null;
1897
+ if (allTeams.size > 0) {
1898
+ const choices = [
1899
+ { name: "Personal (only you)", value: "__personal__" },
1900
+ ...Array.from(allTeams.entries()).map(([id, teamName]) => ({
1901
+ name: teamName,
1902
+ value: id
1903
+ }))
1904
+ ];
1905
+ const selected = await select({
1906
+ message: "Assign to a team?",
1907
+ choices
1908
+ });
1909
+ teamId = selected === "__personal__" ? null : selected;
1910
+ }
1911
+ const { data, error } = await supabase.from("claude_folders").insert({
1912
+ user_id: userId,
1913
+ name,
1914
+ description: description || null,
1915
+ team_id: teamId
1916
+ }).select().single();
1917
+ if (error) {
1918
+ console.error(chalk5.red(`Failed to create folder: ${error.message}`));
1919
+ process.exit(1);
1920
+ }
1921
+ const teamLabel = teamId ? `(team: ${allTeams.get(teamId)})` : "(personal)";
1922
+ console.log(chalk5.green(`
1923
+ Folder "${name}" created ${teamLabel} (${data.id})`));
1924
+ }
1925
+
1926
+ // dist/commands/add-device.js
1927
+ init_auth();
1928
+ import { resolve } from "node:path";
1929
+ import { hostname, platform } from "node:os";
1930
+ import chalk6 from "chalk";
1931
+ import { input as input3, select as select2 } from "@inquirer/prompts";
1932
+ function detectOs() {
1933
+ const p = platform();
1934
+ if (p === "win32")
1935
+ return "windows";
1936
+ if (p === "darwin")
1937
+ return "macos";
1938
+ if (p === "linux")
1939
+ return "linux";
1940
+ return "other";
1941
+ }
1942
+ function suggestDeviceName() {
1943
+ const os = detectOs();
1944
+ const host = hostname().split(".")[0];
1945
+ const osLabel = os.charAt(0).toUpperCase() + os.slice(1);
1946
+ return `${host}-${osLabel}`;
1947
+ }
1948
+ async function addDeviceCommand() {
1949
+ const { supabase, userId } = await getAuthenticatedClient();
1950
+ const cwd = resolve(process.cwd());
1951
+ console.log(chalk6.blue.bold("\n\u{1F4C2} Where is this Claude project?\n"));
1952
+ const localPath = await input3({
1953
+ message: "Local project path:",
1954
+ default: cwd
1955
+ });
1956
+ const { data: folders, error: foldersErr } = await supabase.from("claude_folders").select("id, name").order("name");
1957
+ if (foldersErr || !folders?.length) {
1958
+ console.error(chalk6.red("No folders found. Run: md4ai add-folder"));
1959
+ process.exit(1);
1960
+ }
1961
+ const folderId = await select2({
1962
+ message: "Select folder:",
1963
+ choices: folders.map((f) => ({ name: f.name, value: f.id }))
1964
+ });
1965
+ const suggested = suggestDeviceName();
1966
+ const deviceName = await input3({
1967
+ message: "Device name:",
1968
+ default: suggested
1969
+ });
1970
+ const osType = await select2({
1971
+ message: "OS type:",
1972
+ choices: [
1973
+ { name: "Windows", value: "windows" },
1974
+ { name: "macOS", value: "macos" },
1975
+ { name: "Ubuntu", value: "ubuntu" },
1976
+ { name: "Linux", value: "linux" },
1977
+ { name: "Other", value: "other" }
1978
+ ],
1979
+ default: detectOs()
1980
+ });
1981
+ const description = await input3({ message: "Description (optional):" });
1982
+ const { error } = await supabase.from("device_paths").insert({
1983
+ user_id: userId,
1984
+ folder_id: folderId,
1985
+ device_name: deviceName,
1986
+ os_type: osType,
1987
+ path: localPath,
1988
+ description: description || null
1989
+ });
1990
+ if (error) {
1991
+ console.error(chalk6.red(`Failed to add device: ${error.message}`));
1992
+ process.exit(1);
1993
+ }
1994
+ console.log(chalk6.green(`
1995
+ Device "${deviceName}" added to folder.`));
1996
+ }
1997
+
1998
+ // dist/commands/list-devices.js
1999
+ init_auth();
2000
+ import chalk7 from "chalk";
2001
+ async function listDevicesCommand() {
2002
+ const { supabase } = await getAuthenticatedClient();
2003
+ const { data: devices, error } = await supabase.from("device_paths").select(`
2004
+ id, device_name, os_type, path, last_synced, description,
2005
+ claude_folders!inner ( name )
2006
+ `).order("device_name");
2007
+ if (error) {
2008
+ console.error(chalk7.red(`Failed to list devices: ${error.message}`));
2009
+ process.exit(1);
2010
+ }
2011
+ if (!devices?.length) {
2012
+ console.log(chalk7.yellow("No devices found. Run: md4ai add-device"));
2013
+ return;
2014
+ }
2015
+ const grouped = /* @__PURE__ */ new Map();
2016
+ for (const d of devices) {
2017
+ const list = grouped.get(d.device_name) ?? [];
2018
+ list.push(d);
2019
+ grouped.set(d.device_name, list);
2020
+ }
2021
+ for (const [deviceName, entries] of grouped) {
2022
+ const first = entries[0];
2023
+ console.log(chalk7.bold(`
2024
+ ${deviceName}`) + chalk7.dim(` (${first.os_type})`));
2025
+ for (const entry of entries) {
2026
+ const folderName = entry.claude_folders?.name ?? "unknown";
2027
+ const synced = entry.last_synced ? new Date(entry.last_synced).toLocaleString() : "never";
2028
+ console.log(` ${chalk7.cyan(folderName)} \u2192 ${entry.path}`);
2029
+ console.log(` Last synced: ${synced}`);
2030
+ }
2031
+ }
2032
+ }
2033
+
2034
+ // dist/commands/map.js
2035
+ init_scanner();
2036
+ init_auth();
2037
+ init_config();
2038
+ import { resolve as resolve3 } from "node:path";
2039
+ import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
2040
+ import { existsSync as existsSync7 } from "node:fs";
2041
+ import chalk9 from "chalk";
2042
+
2043
+ // dist/output/html-generator.js
2044
+ function generateOfflineHtml(result, projectRoot) {
2045
+ const title = projectRoot.split("/").pop() ?? "MD4AI";
2046
+ const dataJson = JSON.stringify(result);
2047
+ const orphanItems = result.orphans.map((o) => `<li data-path="${escapeHtml(o.path)}">${escapeHtml(o.path)} <span class="meta">(${o.sizeBytes} bytes)</span></li>`).join("\n");
2048
+ const staleItems = result.staleFiles.map((s) => `<li data-path="${escapeHtml(s.path)}">${escapeHtml(s.path)} <span class="stale-meta">(${s.daysSinceModified} days)</span></li>`).join("\n");
2049
+ const skillRows = result.skills.map((s) => `<tr><td>${escapeHtml(s.name)}</td><td>${s.machineWide ? '<span class="check">\u2713</span>' : ""}</td><td>${s.projectSpecific ? '<span class="check">\u2713</span>' : ""}</td><td class="meta">${escapeHtml(s.source)}</td></tr>`).join("\n");
2050
+ const fileItems = result.graph.nodes.map((n) => `<li data-path="${escapeHtml(n.filePath)}" data-type="${n.type}">${escapeHtml(n.filePath)} <span class="meta">(${n.type})</span></li>`).join("\n");
2051
+ return `<!DOCTYPE html>
2052
+ <html lang="en">
2053
+ <head>
2054
+ <meta charset="UTF-8">
2055
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2056
+ <title>MD4AI \u2014 ${escapeHtml(title)}</title>
2057
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
2058
+ <style>
2059
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2060
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
2061
+ h1 { color: #38bdf8; margin-bottom: 0.5rem; }
2062
+ .subtitle { color: #94a3b8; margin-bottom: 2rem; }
2063
+ .section { background: #1e293b; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
2064
+ .section h2 { color: #38bdf8; margin-bottom: 1rem; font-size: 1.2rem; }
2065
+ .mermaid { background: #0f172a; border-radius: 8px; padding: 1rem; overflow-x: auto; }
2066
+ .badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.85rem; margin-left: 0.5rem; }
2067
+ .badge-orphan { background: #ef4444; color: white; }
2068
+ .badge-stale { background: #f59e0b; color: black; }
2069
+ table { width: 100%; border-collapse: collapse; }
2070
+ th, td { padding: 0.5rem 1rem; text-align: left; border-bottom: 1px solid #334155; }
2071
+ th { color: #94a3b8; font-weight: 600; }
2072
+ .check { color: #22c55e; }
2073
+ .meta { color: #94a3b8; }
2074
+ .stale-meta { color: #f59e0b; }
2075
+ .stats { display: flex; gap: 2rem; flex-wrap: wrap; margin-bottom: 1rem; }
2076
+ .stat { text-align: center; }
2077
+ .stat-value { font-size: 2rem; font-weight: bold; color: #38bdf8; }
2078
+ .stat-label { color: #94a3b8; font-size: 0.85rem; }
2079
+ .file-list { list-style: none; }
2080
+ .file-list li { padding: 0.3rem 0; color: #cbd5e1; font-family: monospace; font-size: 0.9rem; }
2081
+ #search { width: 100%; padding: 0.75rem 1rem; background: #0f172a; border: 1px solid #334155; border-radius: 8px; color: #e2e8f0; font-size: 1rem; margin-bottom: 1rem; }
2082
+ #search:focus { outline: none; border-color: #38bdf8; }
2083
+ @media print { body { background: white; color: black; } .section { border: 1px solid #ccc; } }
2084
+ </style>
2085
+ </head>
2086
+ <body>
2087
+ <h1>MD4AI \u2014 ${escapeHtml(title)}</h1>
2088
+ <p class="subtitle">Scanned: ${new Date(result.scannedAt).toLocaleString()} | Hash: ${result.dataHash.slice(0, 12)}</p>
2089
+
2090
+ <div class="stats">
2091
+ <div class="stat"><div class="stat-value">${result.graph.nodes.length}</div><div class="stat-label">Files</div></div>
2092
+ <div class="stat"><div class="stat-value">${result.graph.edges.length}</div><div class="stat-label">References</div></div>
2093
+ <div class="stat"><div class="stat-value">${result.orphans.length}</div><div class="stat-label">Orphans</div></div>
2094
+ <div class="stat"><div class="stat-value">${result.staleFiles.length}</div><div class="stat-label">Stale</div></div>
2095
+ <div class="stat"><div class="stat-value">${result.skills.length}</div><div class="stat-label">Skills</div></div>
2096
+ </div>
2097
+
2098
+ <input type="text" id="search" placeholder="Search files..." oninput="filterFiles(this.value)">
2099
+
2100
+ <div class="section">
2101
+ <h2>Dependency Graph</h2>
2102
+ <pre class="mermaid">${escapeHtml(result.graph.mermaid)}</pre>
2103
+ </div>
2104
+
2105
+ ${result.orphans.length > 0 ? `
2106
+ <div class="section">
2107
+ <h2>Orphan Files <span class="badge badge-orphan">${result.orphans.length}</span></h2>
2108
+ <p class="meta" style="margin-bottom: 1rem;">Files not reachable from any root configuration file.</p>
2109
+ <ul class="file-list" id="orphan-list">${orphanItems}</ul>
2110
+ </div>
2111
+ ` : ""}
2112
+
2113
+ ${result.staleFiles.length > 0 ? `
2114
+ <div class="section">
2115
+ <h2>Stale Files <span class="badge badge-stale">${result.staleFiles.length}</span></h2>
2116
+ <p class="meta" style="margin-bottom: 1rem;">Files not modified in over 90 days.</p>
2117
+ <ul class="file-list" id="stale-list">${staleItems}</ul>
2118
+ </div>
2119
+ ` : ""}
2120
+
2121
+ <div class="section">
2122
+ <h2>Skills Comparison</h2>
2123
+ <table>
2124
+ <thead><tr><th>Skill/Plugin</th><th>Entire Machine</th><th>Project Specific</th><th>Source</th></tr></thead>
2125
+ <tbody>${skillRows}</tbody>
2126
+ </table>
2127
+ </div>
2128
+
2129
+ <div class="section">
2130
+ <h2>All Files</h2>
2131
+ <ul class="file-list" id="all-files">${fileItems}</ul>
2132
+ </div>
2133
+
2134
+ <script>
2135
+ mermaid.initialize({ startOnLoad: true, theme: 'dark' });
2136
+
2137
+ function filterFiles(query) {
2138
+ var q = query.toLowerCase();
2139
+ document.querySelectorAll('.file-list li').forEach(function(li) {
2140
+ var path = li.getAttribute('data-path') || '';
2141
+ li.style.display = path.toLowerCase().indexOf(q) >= 0 ? '' : 'none';
2142
+ });
2143
+ }
2144
+ </script>
2145
+
2146
+ <script type="application/json" id="scan-data">${dataJson}</script>
2147
+ </body>
2148
+ </html>`;
2149
+ }
2150
+ function escapeHtml(text) {
2151
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2152
+ }
2153
+
2154
+ // dist/commands/map.js
2155
+ init_check_update();
2156
+ init_push_toolings();
2157
+ async function mapCommand(path, options) {
2158
+ await checkForUpdate();
2159
+ const projectRoot = resolve3(path ?? process.cwd());
2160
+ if (!existsSync7(projectRoot)) {
2161
+ console.error(chalk9.red(`Path not found: ${projectRoot}`));
2162
+ process.exit(1);
2163
+ }
2164
+ console.log(chalk9.blue(`Scanning: ${projectRoot}
2165
+ `));
2166
+ const result = await scanProject(projectRoot);
2167
+ console.log(` Files found: ${result.graph.nodes.length}`);
2168
+ console.log(` References: ${result.graph.edges.length}`);
2169
+ console.log(` Orphans: ${result.orphans.length}`);
2170
+ console.log(` Stale files: ${result.staleFiles.length}`);
2171
+ console.log(` Skills: ${result.skills.length}`);
2172
+ console.log(` Toolings: ${result.toolings.length}`);
2173
+ console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
2174
+ console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
2175
+ const outputDir = resolve3(projectRoot, "output");
2176
+ if (!existsSync7(outputDir)) {
2177
+ await mkdir2(outputDir, { recursive: true });
2178
+ }
2179
+ const htmlPath = resolve3(outputDir, "index.html");
2180
+ const html = generateOfflineHtml(result, projectRoot);
2181
+ await writeFile2(htmlPath, html, "utf-8");
2182
+ console.log(chalk9.green(`
2183
+ Local preview: ${htmlPath}`));
2184
+ if (!options.offline) {
2185
+ try {
2186
+ const { supabase } = await getAuthenticatedClient();
2187
+ const { data: devicePaths } = await supabase.from("device_paths").select("folder_id, device_name").eq("path", projectRoot);
2188
+ if (devicePaths?.length) {
2189
+ const { folder_id, device_name } = devicePaths[0];
2190
+ const { data: proposedFiles } = await supabase.from("folder_files").select("id, file_path, proposed_at").eq("folder_id", folder_id).eq("proposed_for_deletion", true);
2191
+ if (proposedFiles?.length) {
2192
+ const { checkbox } = await import("@inquirer/prompts");
2193
+ console.log(chalk9.yellow(`
2194
+ ${proposedFiles.length} file(s) proposed for deletion:
2195
+ `));
2196
+ const toDelete = await checkbox({
2197
+ message: "Select files to delete (space to toggle, enter to confirm)",
2198
+ choices: proposedFiles.map((f) => ({
2199
+ name: `${f.file_path}${f.proposed_at ? ` (proposed ${new Date(f.proposed_at).toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short", year: "numeric" })})` : ""}`,
2200
+ value: f
2201
+ }))
2202
+ });
2203
+ for (const file of toDelete) {
2204
+ const fullPath = resolve3(projectRoot, file.file_path);
2205
+ try {
2206
+ const { unlink } = await import("node:fs/promises");
2207
+ await unlink(fullPath);
2208
+ await supabase.from("folder_files").delete().eq("id", file.id);
2209
+ console.log(chalk9.green(` Deleted: ${file.file_path}`));
2210
+ } catch (err) {
2211
+ console.error(chalk9.red(` Failed to delete ${file.file_path}: ${err}`));
2212
+ }
2213
+ }
2214
+ const keptIds = proposedFiles.filter((f) => !toDelete.some((d) => d.id === f.id)).map((f) => f.id);
2215
+ if (keptIds.length > 0) {
2216
+ for (const id of keptIds) {
2217
+ await supabase.from("folder_files").update({ proposed_for_deletion: false, proposed_at: null, proposed_by: null }).eq("id", id);
2218
+ }
2219
+ console.log(chalk9.cyan(` Kept ${keptIds.length} file(s) \u2014 proposals cleared.`));
2220
+ }
2221
+ console.log("");
2222
+ }
2223
+ const { error } = await supabase.from("claude_folders").update({
2224
+ graph_json: result.graph,
2225
+ orphans_json: result.orphans,
2226
+ skills_table_json: result.skills,
2227
+ stale_files_json: result.staleFiles,
2228
+ env_manifest_json: result.envManifest,
2229
+ last_scanned: result.scannedAt,
2230
+ data_hash: result.dataHash
2231
+ }).eq("id", folder_id);
2232
+ if (error) {
2233
+ console.error(chalk9.yellow(`Sync warning: ${error.message}`));
2234
+ } else {
2235
+ await pushToolings(supabase, folder_id, result.toolings);
2236
+ await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder_id).eq("device_name", device_name);
2237
+ await saveState({
2238
+ lastFolderId: folder_id,
2239
+ lastDeviceName: device_name,
2240
+ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
2241
+ });
2242
+ console.log(chalk9.green("Synced to Supabase."));
2243
+ console.log(chalk9.cyan(`
2244
+ https://www.md4ai.com/project/${folder_id}
2245
+ `));
2246
+ const configFiles = await readClaudeConfigFiles(projectRoot);
2247
+ if (configFiles.length > 0) {
2248
+ for (const file of configFiles) {
2249
+ await supabase.from("folder_files").upsert({
2250
+ folder_id,
2251
+ file_path: file.filePath,
2252
+ content: file.content,
2253
+ size_bytes: file.sizeBytes,
2254
+ last_modified: file.lastModified
2255
+ }, { onConflict: "folder_id,file_path" });
2256
+ }
2257
+ console.log(chalk9.green(` Uploaded ${configFiles.length} config file(s).`));
2258
+ }
2259
+ }
2260
+ } else {
2261
+ console.log(chalk9.yellow("No device path matches this folder. Run: md4ai add-device\n(Local preview still generated.)"));
2262
+ }
2263
+ } catch {
2264
+ console.log(chalk9.yellow("Not logged in \u2014 local preview only."));
2265
+ }
2266
+ }
2267
+ }
2268
+
2269
+ // dist/commands/simulate.js
2270
+ init_dist();
2271
+ import { join as join9 } from "node:path";
2272
+ import { existsSync as existsSync8 } from "node:fs";
2273
+ import { homedir as homedir6 } from "node:os";
2274
+ import chalk10 from "chalk";
2275
+ async function simulateCommand(prompt) {
2276
+ const projectRoot = process.cwd();
2277
+ console.log(chalk10.blue(`Simulating prompt: "${prompt}"
2278
+ `));
2279
+ console.log(chalk10.dim("Files Claude would load:\n"));
2280
+ const globalClaude = join9(homedir6(), ".claude", "CLAUDE.md");
2281
+ if (existsSync8(globalClaude)) {
2282
+ console.log(chalk10.green(" \u2713 ~/.claude/CLAUDE.md (global)"));
2283
+ }
2284
+ for (const rootFile of ROOT_FILES) {
2285
+ const fullPath = join9(projectRoot, rootFile);
2286
+ if (existsSync8(fullPath)) {
2287
+ console.log(chalk10.green(` \u2713 ${rootFile} (project root)`));
2288
+ }
2289
+ }
2290
+ const settingsFiles = [
2291
+ join9(homedir6(), ".claude", "settings.json"),
2292
+ join9(homedir6(), ".claude", "settings.local.json"),
2293
+ join9(projectRoot, ".claude", "settings.json"),
2294
+ join9(projectRoot, ".claude", "settings.local.json")
2295
+ ];
2296
+ for (const sf of settingsFiles) {
2297
+ if (existsSync8(sf)) {
2298
+ const display = sf.startsWith(homedir6()) ? sf.replace(homedir6(), "~") : sf.replace(projectRoot + "/", "");
2299
+ console.log(chalk10.green(` \u2713 ${display} (settings)`));
2300
+ }
2301
+ }
2302
+ const words = prompt.split(/\s+/);
2303
+ for (const word of words) {
2304
+ const cleaned = word.replace(/['"]/g, "");
2305
+ const candidatePath = join9(projectRoot, cleaned);
2306
+ if (existsSync8(candidatePath) && cleaned.includes("/")) {
2307
+ console.log(chalk10.cyan(` \u2192 ${cleaned} (referenced in prompt)`));
2308
+ }
2309
+ }
2310
+ console.log(chalk10.dim("\nNote: This is an approximation. Actual file loading depends on Claude Code internals."));
2311
+ }
2312
+
2313
+ // dist/commands/print.js
2314
+ import { join as join10 } from "node:path";
2315
+ import { readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
2316
+ import { existsSync as existsSync9 } from "node:fs";
2317
+ import chalk11 from "chalk";
2318
+ async function printCommand(title) {
2319
+ const projectRoot = process.cwd();
2320
+ const scanDataPath = join10(projectRoot, "output", "index.html");
2321
+ if (!existsSync9(scanDataPath)) {
2322
+ console.error(chalk11.red("No scan data found. Run: md4ai scan"));
2323
+ process.exit(1);
2324
+ }
2325
+ const html = await readFile6(scanDataPath, "utf-8");
2326
+ const match = html.match(/<script type="application\/json" id="scan-data">([\s\S]*?)<\/script>/);
2327
+ if (!match) {
2328
+ console.error(chalk11.red("Could not extract scan data from output/index.html"));
2329
+ process.exit(1);
2330
+ }
2331
+ const result = JSON.parse(match[1]);
2332
+ const printHtml = generatePrintHtml(result, title);
2333
+ const outputPath = join10(projectRoot, "output", `print-${Date.now()}.html`);
2334
+ await writeFile3(outputPath, printHtml, "utf-8");
2335
+ console.log(chalk11.green(`Print-ready wall sheet: ${outputPath}`));
2336
+ }
2337
+ function escapeHtml2(text) {
2338
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2339
+ }
2340
+ function generatePrintHtml(result, title) {
2341
+ const skillRows = result.skills.map((s) => `<tr><td>${escapeHtml2(s.name)}</td><td>${s.machineWide ? '<span class="check">\u2713</span>' : ""}</td><td>${s.projectSpecific ? '<span class="check">\u2713</span>' : ""}</td></tr>`).join("\n");
2342
+ const orphanItems = result.orphans.map((o) => `<li class="orphan">${escapeHtml2(o.path)}</li>`).join("\n");
2343
+ const staleItems = result.staleFiles.map((s) => `<li class="stale">${escapeHtml2(s.path)} (${s.daysSinceModified}d)</li>`).join("\n");
2344
+ return `<!DOCTYPE html>
2345
+ <html lang="en">
2346
+ <head>
2347
+ <meta charset="UTF-8">
2348
+ <title>${escapeHtml2(title)} \u2014 MD4AI Wall Sheet</title>
2349
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
2350
+ <style>
2351
+ @page { size: A3 landscape; margin: 1cm; }
2352
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 1rem; }
2353
+ h1 { font-size: 2rem; margin-bottom: 0.5rem; }
2354
+ .subtitle { color: #666; margin-bottom: 2rem; }
2355
+ .mermaid { margin: 2rem 0; }
2356
+ .columns { display: flex; gap: 2rem; }
2357
+ .column { flex: 1; }
2358
+ table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
2359
+ th, td { padding: 0.3rem 0.5rem; text-align: left; border-bottom: 1px solid #ddd; }
2360
+ th { background: #f5f5f5; }
2361
+ .orphan { color: #dc2626; }
2362
+ .stale { color: #d97706; }
2363
+ .check { color: #16a34a; font-weight: bold; }
2364
+ </style>
2365
+ </head>
2366
+ <body>
2367
+ <h1>${escapeHtml2(title)}</h1>
2368
+ <p class="subtitle">Generated: ${(/* @__PURE__ */ new Date()).toLocaleString()} | Files: ${result.graph.nodes.length} | Orphans: ${result.orphans.length}</p>
2369
+
2370
+ <pre class="mermaid">${escapeHtml2(result.graph.mermaid)}</pre>
2371
+
2372
+ <div class="columns">
2373
+ <div class="column">
2374
+ <h2>Skills Comparison</h2>
2375
+ <table>
2376
+ <tr><th>Skill</th><th>Machine</th><th>Project</th></tr>
2377
+ ${skillRows}
2378
+ </table>
2379
+ </div>
2380
+ <div class="column">
2381
+ ${result.orphans.length ? `<h2>Orphans</h2><ul>${orphanItems}</ul>` : ""}
2382
+ ${result.staleFiles.length ? `<h2>Stale Files</h2><ul>${staleItems}</ul>` : ""}
2383
+ </div>
2384
+ </div>
2385
+
2386
+ <script>mermaid.initialize({ startOnLoad: true, theme: 'default' });</script>
2387
+ </body>
2388
+ </html>`;
2389
+ }
2390
+
2391
+ // dist/index.js
2392
+ init_sync();
2393
+
2394
+ // dist/commands/link.js
2395
+ init_auth();
2396
+ init_config();
2397
+ init_scanner();
2398
+ init_push_toolings();
2399
+ init_device_utils();
2400
+ import { resolve as resolve4 } from "node:path";
2401
+ import chalk13 from "chalk";
2402
+ async function linkCommand(projectId) {
2403
+ const { supabase, userId } = await getAuthenticatedClient();
2404
+ const cwd = resolve4(process.cwd());
2405
+ const deviceName = detectDeviceName();
2406
+ const osType = detectOs2();
2407
+ const { data: folder, error: folderErr } = await supabase.from("claude_folders").select("id, name").eq("id", projectId).single();
2408
+ if (folderErr || !folder) {
2409
+ console.error(chalk13.red("Project not found, or you do not have access."));
2410
+ console.error(chalk13.yellow("Check the project ID in the MD4AI web dashboard."));
2411
+ process.exit(1);
2412
+ }
2413
+ console.log(chalk13.blue(`
2414
+ Linking "${folder.name}" to this device...
2415
+ `));
2416
+ console.log(` Project: ${folder.name}`);
2417
+ console.log(` Path: ${cwd}`);
2418
+ console.log(` Device: ${deviceName}`);
2419
+ console.log(` OS: ${osType}`);
2420
+ await supabase.from("devices").upsert({
2421
+ user_id: userId,
2422
+ device_name: deviceName,
2423
+ os_type: osType
2424
+ }, { onConflict: "user_id,device_name" });
2425
+ const { data: existing } = await supabase.from("device_paths").select("id").eq("folder_id", folder.id).eq("device_name", deviceName).maybeSingle();
2426
+ if (existing) {
2427
+ await supabase.from("device_paths").update({ path: cwd, os_type: osType, last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", existing.id);
2428
+ } else {
2429
+ const { error: pathErr } = await supabase.from("device_paths").insert({
2430
+ user_id: userId,
2431
+ folder_id: folder.id,
2432
+ device_name: deviceName,
2433
+ os_type: osType,
2434
+ path: cwd
2255
2435
  });
2436
+ if (pathErr) {
2437
+ console.error(chalk13.red(`Failed to link: ${pathErr.message}`));
2438
+ process.exit(1);
2439
+ }
2440
+ }
2441
+ console.log(chalk13.green("\nLinked successfully."));
2442
+ console.log(chalk13.blue("\nRunning initial scan...\n"));
2443
+ const result = await scanProject(cwd);
2444
+ console.log(` Files: ${result.graph.nodes.length}`);
2445
+ console.log(` References:${result.graph.edges.length}`);
2446
+ console.log(` Orphans: ${result.orphans.length}`);
2447
+ console.log(` Skills: ${result.skills.length}`);
2448
+ console.log(` Toolings: ${result.toolings.length}`);
2449
+ console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
2450
+ const { error: scanErr } = await supabase.from("claude_folders").update({
2451
+ graph_json: result.graph,
2452
+ orphans_json: result.orphans,
2453
+ skills_table_json: result.skills,
2454
+ stale_files_json: result.staleFiles,
2455
+ env_manifest_json: result.envManifest,
2456
+ last_scanned: result.scannedAt,
2457
+ data_hash: result.dataHash
2458
+ }).eq("id", folder.id);
2459
+ if (scanErr) {
2460
+ console.error(chalk13.yellow(`Scan upload warning: ${scanErr.message}`));
2256
2461
  }
2257
- return entries;
2258
- }
2259
- function parseFlatConfig(data, source) {
2260
- if (!data)
2261
- return [];
2262
- const entries = [];
2263
- for (const [name, server] of Object.entries(data)) {
2264
- if (typeof server !== "object" || server === null)
2265
- continue;
2266
- if (!server.command && !server.url)
2267
- continue;
2268
- const serverType = server.type === "http" ? "http" : "stdio";
2269
- entries.push({
2270
- name,
2271
- source,
2272
- type: serverType,
2273
- command: server.command,
2274
- args: server.args,
2275
- env: server.env,
2276
- url: server.url
2277
- });
2462
+ await pushToolings(supabase, folder.id, result.toolings);
2463
+ const configFiles = await readClaudeConfigFiles(cwd);
2464
+ if (configFiles.length > 0) {
2465
+ for (const file of configFiles) {
2466
+ await supabase.from("folder_files").upsert({
2467
+ folder_id: folder.id,
2468
+ file_path: file.filePath,
2469
+ content: file.content,
2470
+ size_bytes: file.sizeBytes,
2471
+ last_modified: file.lastModified
2472
+ }, { onConflict: "folder_id,file_path" });
2473
+ }
2474
+ console.log(chalk13.green(` Uploaded ${configFiles.length} config file(s).`));
2278
2475
  }
2279
- return entries;
2476
+ await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder.id).eq("device_name", deviceName);
2477
+ await saveState({
2478
+ lastFolderId: folder.id,
2479
+ lastDeviceName: deviceName,
2480
+ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
2481
+ });
2482
+ const projectUrl = `https://www.md4ai.com/project/${folder.id}`;
2483
+ console.log(chalk13.green("\nDone! Project linked and scanned."));
2484
+ console.log(chalk13.cyan(`
2485
+ ${projectUrl}
2486
+ `));
2487
+ console.log(chalk13.grey('Run "md4ai scan" to rescan at any time.'));
2280
2488
  }
2281
- async function readAllMcpConfigs() {
2282
- const home = homedir7();
2283
- const entries = [];
2284
- const userConfig = await readJsonSafe(join12(home, ".claude.json"));
2285
- entries.push(...parseServers(userConfig, "global"));
2286
- const globalMcp = await readJsonSafe(join12(home, ".claude", "mcp.json"));
2287
- entries.push(...parseServers(globalMcp, "global"));
2288
- const cwdMcp = await readJsonSafe(join12(process.cwd(), ".mcp.json"));
2289
- entries.push(...parseServers(cwdMcp, "project"));
2290
- const pluginsBase = join12(home, ".claude", "plugins", "marketplaces");
2291
- if (existsSync11(pluginsBase)) {
2292
- try {
2293
- const marketplaces = await readdir3(pluginsBase, { withFileTypes: true });
2294
- for (const mp of marketplaces) {
2295
- if (!mp.isDirectory())
2296
- continue;
2297
- const extDir = join12(pluginsBase, mp.name, "external_plugins");
2298
- if (!existsSync11(extDir))
2299
- continue;
2300
- const plugins = await readdir3(extDir, { withFileTypes: true });
2301
- for (const plugin of plugins) {
2302
- if (!plugin.isDirectory())
2303
- continue;
2304
- const pluginMcp = await readJsonSafe(join12(extDir, plugin.name, ".mcp.json"));
2305
- entries.push(...parseServers(pluginMcp, "plugin"));
2306
- }
2307
- }
2308
- } catch {
2309
- }
2489
+
2490
+ // dist/commands/import-bundle.js
2491
+ import { readFile as readFile7, writeFile as writeFile4, mkdir as mkdir3 } from "node:fs/promises";
2492
+ import { join as join11, dirname as dirname2 } from "node:path";
2493
+ import { existsSync as existsSync10 } from "node:fs";
2494
+ import chalk14 from "chalk";
2495
+ import { confirm, input as input4 } from "@inquirer/prompts";
2496
+ async function importBundleCommand(zipPath) {
2497
+ if (!existsSync10(zipPath)) {
2498
+ console.error(chalk14.red(`File not found: ${zipPath}`));
2499
+ process.exit(1);
2310
2500
  }
2311
- const cacheBase = join12(home, ".claude", "plugins", "cache");
2312
- if (existsSync11(cacheBase)) {
2313
- try {
2314
- const registries = await readdir3(cacheBase, { withFileTypes: true });
2315
- for (const reg of registries) {
2316
- if (!reg.isDirectory())
2317
- continue;
2318
- const regDir = join12(cacheBase, reg.name);
2319
- const plugins = await readdir3(regDir, { withFileTypes: true });
2320
- for (const plugin of plugins) {
2321
- if (!plugin.isDirectory())
2322
- continue;
2323
- const versionDirs = await readdir3(join12(regDir, plugin.name), { withFileTypes: true });
2324
- for (const ver of versionDirs) {
2325
- if (!ver.isDirectory())
2326
- continue;
2327
- const mcpPath = join12(regDir, plugin.name, ver.name, ".mcp.json");
2328
- const flatData = await readJsonSafe(mcpPath);
2329
- if (flatData) {
2330
- entries.push(...parseFlatConfig(flatData, "plugin"));
2331
- break;
2332
- }
2333
- }
2334
- }
2335
- }
2336
- } catch {
2501
+ const JSZip = (await import("jszip")).default;
2502
+ const zipData = await readFile7(zipPath);
2503
+ const zip = await JSZip.loadAsync(zipData);
2504
+ const manifestFile = zip.file("manifest.json");
2505
+ if (!manifestFile) {
2506
+ console.error(chalk14.red("Invalid bundle: missing manifest.json"));
2507
+ process.exit(1);
2508
+ }
2509
+ const manifest = JSON.parse(await manifestFile.async("string"));
2510
+ console.log(chalk14.blue(`
2511
+ Bundle: ${manifest.folderName}`));
2512
+ console.log(` Exported by: ${manifest.ownerEmail}`);
2513
+ console.log(` Exported at: ${manifest.exportedAt}`);
2514
+ console.log(` Files: ${manifest.fileCount}`);
2515
+ const files = [];
2516
+ for (const [path, file] of Object.entries(zip.files)) {
2517
+ if (path.startsWith("claude-files/") && !file.dir) {
2518
+ const relativePath = path.replace("claude-files/", "");
2519
+ const content = await file.async("string");
2520
+ files.push({ filePath: relativePath, content });
2337
2521
  }
2338
2522
  }
2339
- const seen = /* @__PURE__ */ new Set();
2340
- const deduped = [];
2341
- for (const entry of entries) {
2342
- if (seen.has(entry.name))
2343
- continue;
2344
- seen.add(entry.name);
2345
- deduped.push(entry);
2523
+ if (files.length === 0) {
2524
+ console.log(chalk14.yellow("No Claude config files found in bundle."));
2525
+ return;
2346
2526
  }
2347
- return deduped;
2348
- }
2349
-
2350
- // dist/mcp/scan-processes.js
2351
- import { execFileSync as execFileSync4 } from "node:child_process";
2352
- function parsePsOutput(output) {
2353
- const lines = output.trim().split("\n").slice(1);
2354
- const entries = [];
2355
- for (const line of lines) {
2356
- const trimmed = line.trim();
2357
- const match = trimmed.match(/^\s*(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(.+)$/);
2358
- if (!match)
2359
- continue;
2360
- entries.push({
2361
- pid: parseInt(match[1], 10),
2362
- tty: match[2],
2363
- etimeRaw: match[3],
2364
- rss: parseInt(match[4], 10),
2365
- args: match[5]
2366
- });
2527
+ console.log(chalk14.blue(`
2528
+ Files to extract:`));
2529
+ for (const f of files) {
2530
+ console.log(` ${f.filePath}`);
2367
2531
  }
2368
- return entries;
2369
- }
2370
- function parseEtime(etime) {
2371
- let days = 0;
2372
- let rest = etime;
2373
- if (rest.includes("-")) {
2374
- const [d, r] = rest.split("-");
2375
- days = parseInt(d, 10);
2376
- rest = r;
2532
+ const targetDir = await input4({
2533
+ message: "Extract to directory:",
2534
+ default: process.cwd()
2535
+ });
2536
+ const proceed = await confirm({
2537
+ message: `Extract ${files.length} file(s) to ${targetDir}?`
2538
+ });
2539
+ if (!proceed) {
2540
+ console.log(chalk14.yellow("Cancelled."));
2541
+ return;
2377
2542
  }
2378
- const parts = rest.split(":").map(Number);
2379
- if (parts.length === 3) {
2380
- return days * 86400 + parts[0] * 3600 + parts[1] * 60 + parts[2];
2381
- } else if (parts.length === 2) {
2382
- return days * 86400 + parts[0] * 60 + parts[1];
2543
+ for (const file of files) {
2544
+ const fullPath = join11(targetDir, file.filePath);
2545
+ const dir = dirname2(fullPath);
2546
+ if (!existsSync10(dir)) {
2547
+ await mkdir3(dir, { recursive: true });
2548
+ }
2549
+ await writeFile4(fullPath, file.content, "utf-8");
2550
+ console.log(chalk14.green(` \u2713 ${file.filePath}`));
2383
2551
  }
2384
- return days * 86400 + (parts[0] ?? 0);
2552
+ console.log(chalk14.green(`
2553
+ Done! ${files.length} file(s) extracted to ${targetDir}`));
2385
2554
  }
2386
- function findProcessesForConfig(config, processes) {
2387
- if (config.type === "http")
2388
- return [];
2389
- const packageName = config.args ? extractPackageName(config.args) : null;
2390
- const matches = [];
2391
- for (const proc of processes) {
2392
- let matched = false;
2393
- if (packageName && proc.args.includes(packageName)) {
2394
- matched = true;
2395
- } else if (config.command === "node" && config.args?.[0]) {
2396
- if (proc.args.includes(config.args[0])) {
2397
- matched = true;
2398
- }
2399
- }
2400
- if (matched) {
2401
- matches.push({
2402
- pid: proc.pid,
2403
- tty: proc.tty === "?" ? "" : proc.tty,
2404
- uptimeSeconds: parseEtime(proc.etimeRaw),
2405
- memoryMb: Math.round(proc.rss / 1024),
2406
- commandLine: proc.args
2407
- });
2555
+
2556
+ // dist/commands/admin-update-tool.js
2557
+ init_auth();
2558
+ import chalk15 from "chalk";
2559
+ var VALID_CATEGORIES = ["framework", "runtime", "cli", "mcp", "package", "database", "other"];
2560
+ async function adminUpdateToolCommand(options) {
2561
+ const { supabase } = await getAuthenticatedClient();
2562
+ if (!options.name) {
2563
+ console.error(chalk15.red("--name is required."));
2564
+ process.exit(1);
2565
+ }
2566
+ if (options.category && !VALID_CATEGORIES.includes(options.category)) {
2567
+ console.error(chalk15.red(`Invalid category. Must be one of: ${VALID_CATEGORIES.join(", ")}`));
2568
+ process.exit(1);
2569
+ }
2570
+ const { data: existing } = await supabase.from("tools_registry").select("id, name, display_name, category").eq("name", options.name).maybeSingle();
2571
+ if (existing) {
2572
+ const updates = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
2573
+ if (options.display)
2574
+ updates.display_name = options.display;
2575
+ if (options.category)
2576
+ updates.category = options.category;
2577
+ if (options.stable)
2578
+ updates.latest_stable = options.stable;
2579
+ if (options.beta)
2580
+ updates.latest_beta = options.beta;
2581
+ if (options.source)
2582
+ updates.source_url = options.source;
2583
+ if (options.install)
2584
+ updates.install_url = options.install;
2585
+ if (options.notes)
2586
+ updates.notes = options.notes;
2587
+ const { error } = await supabase.from("tools_registry").update(updates).eq("id", existing.id);
2588
+ if (error) {
2589
+ console.error(chalk15.red(`Failed to update: ${error.message}`));
2590
+ process.exit(1);
2408
2591
  }
2409
- }
2410
- return matches;
2411
- }
2412
- function getProcessTable() {
2413
- try {
2414
- const output = execFileSync4("ps", ["-eo", "pid,tty,etime,rss,args"], {
2415
- encoding: "utf-8",
2416
- timeout: 5e3
2592
+ console.log(chalk15.green(`Updated "${existing.display_name}" in the registry.`));
2593
+ } else {
2594
+ if (!options.display || !options.category) {
2595
+ console.error(chalk15.red("New tools require --display and --category."));
2596
+ process.exit(1);
2597
+ }
2598
+ const { error } = await supabase.from("tools_registry").insert({
2599
+ name: options.name,
2600
+ display_name: options.display,
2601
+ category: options.category,
2602
+ latest_stable: options.stable ?? null,
2603
+ latest_beta: options.beta ?? null,
2604
+ source_url: options.source ?? null,
2605
+ install_url: options.install ?? null,
2606
+ notes: options.notes ?? null
2417
2607
  });
2418
- return parsePsOutput(output);
2419
- } catch {
2420
- return [];
2608
+ if (error) {
2609
+ console.error(chalk15.red(`Failed to create: ${error.message}`));
2610
+ process.exit(1);
2611
+ }
2612
+ console.log(chalk15.green(`Added "${options.display}" to the registry.`));
2421
2613
  }
2422
2614
  }
2423
2615
 
2424
- // dist/commands/mcp-watch.js
2425
- import { createHash as createHash2 } from "node:crypto";
2426
- var POLL_INTERVAL_MS = 3e4;
2427
- var ENV_POLL_INTERVAL_MS = 3e5;
2428
- function detectTty() {
2429
- try {
2430
- const result = execFileSync5("ps", ["-o", "tty=", "-p", String(process.pid)], {
2431
- encoding: "utf-8",
2432
- timeout: 3e3
2433
- }).trim();
2434
- return result && result !== "?" ? result : null;
2435
- } catch {
2436
- return null;
2616
+ // dist/commands/admin-list-tools.js
2617
+ init_auth();
2618
+ import chalk16 from "chalk";
2619
+ async function adminListToolsCommand() {
2620
+ const { supabase } = await getAuthenticatedClient();
2621
+ const { data: tools, error } = await supabase.from("tools_registry").select("*").order("category").order("display_name");
2622
+ if (error) {
2623
+ console.error(chalk16.red(`Failed to fetch tools: ${error.message}`));
2624
+ process.exit(1);
2625
+ }
2626
+ if (!tools?.length) {
2627
+ console.log(chalk16.yellow("No tools in the registry."));
2628
+ return;
2629
+ }
2630
+ const nameW = Math.max(16, ...tools.map((t) => t.display_name.length));
2631
+ const catW = Math.max(10, ...tools.map((t) => t.category.length));
2632
+ const stableW = Math.max(8, ...tools.map((t) => (t.latest_stable ?? "\u2014").length));
2633
+ const betaW = Math.max(8, ...tools.map((t) => (t.latest_beta ?? "\u2014").length));
2634
+ const header = [
2635
+ "Name".padEnd(nameW),
2636
+ "Category".padEnd(catW),
2637
+ "Stable".padEnd(stableW),
2638
+ "Beta".padEnd(betaW),
2639
+ "Last Checked"
2640
+ ].join(" ");
2641
+ console.log(chalk16.bold(header));
2642
+ console.log("\u2500".repeat(header.length));
2643
+ for (const tool of tools) {
2644
+ const lastChecked = tool.updated_at ? formatRelative(new Date(tool.updated_at)) : "\u2014";
2645
+ const row = [
2646
+ tool.display_name.padEnd(nameW),
2647
+ tool.category.padEnd(catW),
2648
+ (tool.latest_stable ?? "\u2014").padEnd(stableW),
2649
+ (tool.latest_beta ?? "\u2014").padEnd(betaW),
2650
+ lastChecked
2651
+ ].join(" ");
2652
+ console.log(row);
2437
2653
  }
2654
+ console.log(chalk16.grey(`
2655
+ ${tools.length} tool(s) in registry.`));
2438
2656
  }
2439
- function checkEnvVars(config) {
2440
- const required = config.env ? Object.keys(config.env) : [];
2441
- const missing = required.filter((key) => !process.env[key]);
2442
- return { required, missing };
2657
+ function formatRelative(date) {
2658
+ const diff = Date.now() - date.getTime();
2659
+ const hours = Math.floor(diff / (1e3 * 60 * 60));
2660
+ if (hours < 1)
2661
+ return "just now";
2662
+ if (hours < 24)
2663
+ return `${hours}h ago`;
2664
+ const days = Math.floor(hours / 24);
2665
+ if (days < 7)
2666
+ return `${days}d ago`;
2667
+ return date.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
2443
2668
  }
2444
- function buildRows(configs) {
2445
- const processes = getProcessTable();
2446
- const rows = [];
2447
- for (const config of configs) {
2448
- const { required, missing } = checkEnvVars(config);
2449
- const packageName = config.args ? extractPackageName(config.args) : null;
2450
- if (config.type === "http") {
2451
- rows.push({
2452
- server_name: config.name,
2453
- config_source: config.source,
2454
- server_type: "http",
2455
- command: null,
2456
- package_name: null,
2457
- http_url: config.url ?? null,
2458
- status: "stopped",
2459
- pid: null,
2460
- session_tty: null,
2461
- uptime_seconds: null,
2462
- memory_mb: null,
2463
- env_vars_required: required.length ? required : null,
2464
- env_vars_missing: missing.length ? missing : null,
2465
- error_detail: "HTTP server \u2014 cannot verify from CLI"
2466
- });
2467
- continue;
2468
- }
2469
- if (missing.length > 0) {
2470
- rows.push({
2471
- server_name: config.name,
2472
- config_source: config.source,
2473
- server_type: "stdio",
2474
- command: config.command ?? null,
2475
- package_name: packageName,
2476
- http_url: null,
2477
- status: "error",
2478
- pid: null,
2479
- session_tty: null,
2480
- uptime_seconds: null,
2481
- memory_mb: null,
2482
- env_vars_required: required,
2483
- env_vars_missing: missing,
2484
- error_detail: `Missing env: ${missing.join(", ")}`
2485
- });
2486
- continue;
2487
- }
2488
- const matches = findProcessesForConfig(config, processes);
2489
- if (matches.length === 0) {
2490
- rows.push({
2491
- server_name: config.name,
2492
- config_source: config.source,
2493
- server_type: "stdio",
2494
- command: config.command ?? null,
2495
- package_name: packageName,
2496
- http_url: null,
2497
- status: "stopped",
2498
- pid: null,
2499
- session_tty: null,
2500
- uptime_seconds: null,
2501
- memory_mb: null,
2502
- env_vars_required: required.length ? required : null,
2503
- env_vars_missing: null,
2504
- error_detail: null
2505
- });
2506
- } else {
2507
- for (const match of matches) {
2508
- rows.push({
2509
- server_name: config.name,
2510
- config_source: config.source,
2511
- server_type: "stdio",
2512
- command: config.command ?? null,
2513
- package_name: packageName,
2514
- http_url: null,
2515
- status: "running",
2516
- pid: match.pid,
2517
- session_tty: match.tty || null,
2518
- uptime_seconds: match.uptimeSeconds,
2519
- memory_mb: match.memoryMb,
2520
- env_vars_required: required.length ? required : null,
2521
- env_vars_missing: null,
2522
- error_detail: null
2523
- });
2524
- }
2669
+
2670
+ // dist/commands/admin-fetch-versions.js
2671
+ init_auth();
2672
+ import chalk17 from "chalk";
2673
+
2674
+ // dist/commands/version-sources.js
2675
+ var VERSION_SOURCES = {
2676
+ "next": { type: "npm", package: "next" },
2677
+ "react": { type: "npm", package: "react" },
2678
+ "react-dom": { type: "npm", package: "react-dom" },
2679
+ "typescript": { type: "npm", package: "typescript" },
2680
+ "tailwindcss": { type: "npm", package: "tailwindcss" },
2681
+ "esbuild": { type: "npm", package: "esbuild" },
2682
+ "chalk": { type: "npm", package: "chalk" },
2683
+ "commander": { type: "npm", package: "commander" },
2684
+ "inquirer": { type: "npm", package: "inquirer" },
2685
+ "playwright": { type: "npm", package: "playwright" },
2686
+ "resend": { type: "npm", package: "resend" },
2687
+ "@supabase/supabase-js": { type: "npm", package: "@supabase/supabase-js" },
2688
+ "pnpm": { type: "npm", package: "pnpm" },
2689
+ "claude-code": { type: "npm", package: "@anthropic-ai/claude-code" },
2690
+ "turborepo": { type: "npm", package: "turbo" },
2691
+ "npm": { type: "npm", package: "npm" },
2692
+ "node": { type: "github", repo: "nodejs/node" },
2693
+ "supabase-cli": { type: "npm", package: "supabase" }
2694
+ };
2695
+
2696
+ // dist/commands/admin-fetch-versions.js
2697
+ async function adminFetchVersionsCommand() {
2698
+ const { supabase } = await getAuthenticatedClient();
2699
+ const { data: tools, error } = await supabase.from("tools_registry").select("*").order("display_name");
2700
+ if (error) {
2701
+ console.error(chalk17.red(`Failed to fetch tools: ${error.message}`));
2702
+ process.exit(1);
2703
+ }
2704
+ if (!tools?.length) {
2705
+ console.log(chalk17.yellow("No tools in the registry."));
2706
+ }
2707
+ const { data: allProjectToolings } = await supabase.from("project_toolings").select("tool_name, detection_source").is("tool_id", null);
2708
+ const registeredNames = new Set((tools ?? []).map((t) => t.name));
2709
+ const unregisteredNames = /* @__PURE__ */ new Set();
2710
+ for (const pt of allProjectToolings ?? []) {
2711
+ if (!registeredNames.has(pt.tool_name) && pt.detection_source === "package.json") {
2712
+ unregisteredNames.add(pt.tool_name);
2525
2713
  }
2526
2714
  }
2527
- return rows;
2528
- }
2529
- function printTable(rows, deviceName) {
2530
- process.stdout.write("\x1B[2J\x1B[H");
2531
- console.log(chalk18.bold.cyan(`
2532
- MCP Monitor \u2014 ${deviceName}`));
2533
- console.log(chalk18.dim(` ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \xB7 refreshes every 30s \xB7 Ctrl+C to stop
2715
+ if (unregisteredNames.size > 0) {
2716
+ console.log(chalk17.blue(`Auto-registering ${unregisteredNames.size} unverified package(s)...
2534
2717
  `));
2535
- const running = rows.filter((r) => r.status === "running");
2536
- const stopped = rows.filter((r) => r.status === "stopped");
2537
- const errored = rows.filter((r) => r.status === "error");
2538
- const byTty = /* @__PURE__ */ new Map();
2539
- for (const r of running) {
2540
- const key = r.session_tty ?? "unknown";
2541
- const list = byTty.get(key) ?? [];
2542
- list.push(r);
2543
- byTty.set(key, list);
2544
- }
2545
- if (byTty.size > 0) {
2546
- for (const [tty, servers] of byTty) {
2547
- const totalMem = servers.reduce((sum, s) => sum + (s.memory_mb ?? 0), 0);
2548
- console.log(chalk18.green(` Session ${tty}`) + chalk18.dim(` \u2014 ${servers.length} server${servers.length !== 1 ? "s" : ""} \xB7 ${totalMem} MB`));
2549
- for (const s of servers) {
2550
- const uptime = formatUptime(s.uptime_seconds ?? 0);
2551
- 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}`);
2718
+ for (const name of unregisteredNames) {
2719
+ const displayName = name;
2720
+ const { data: inserted, error: insertError } = await supabase.from("tools_registry").upsert({
2721
+ name,
2722
+ display_name: displayName,
2723
+ category: "package",
2724
+ source_url: `https://www.npmjs.com/package/${name}`,
2725
+ install_url: `https://www.npmjs.com/package/${name}`
2726
+ }, { onConflict: "name" }).select().single();
2727
+ if (!insertError && inserted) {
2728
+ tools.push(inserted);
2729
+ await supabase.from("project_toolings").update({ tool_id: inserted.id }).eq("tool_name", name).is("tool_id", null);
2552
2730
  }
2553
- console.log("");
2554
2731
  }
2555
2732
  }
2556
- if (stopped.length > 0 || errored.length > 0) {
2557
- const notRunning = [...stopped, ...errored];
2558
- console.log(chalk18.yellow(` Not Running (${notRunning.length})`));
2559
- for (const s of notRunning) {
2560
- const icon = s.status === "error" ? chalk18.red("\u2717") : chalk18.yellow("\u25CB");
2561
- const detail = s.error_detail ? chalk18.dim(` \u2014 ${s.error_detail}`) : "";
2562
- console.log(` ${icon} ${s.server_name.padEnd(20)} ${chalk18.dim(s.config_source.padEnd(10))}${detail}`);
2563
- }
2564
- console.log("");
2733
+ if (!tools?.length) {
2734
+ console.log(chalk17.yellow("No tools to fetch."));
2735
+ return;
2565
2736
  }
2566
- if (rows.length === 0) {
2567
- console.log(chalk18.yellow(" No MCP servers configured."));
2568
- console.log(chalk18.dim(" Configure servers in ~/.claude/mcp.json or .mcp.json\n"));
2737
+ console.log(chalk17.bold(`Fetching versions for ${tools.length} tool(s)...
2738
+ `));
2739
+ const results = await Promise.all(tools.map(async (tool) => {
2740
+ const source = VERSION_SOURCES[tool.name] ?? { type: "npm", package: tool.name };
2741
+ try {
2742
+ const { stable, beta } = await fetchVersions(source);
2743
+ const stableChanged = stable !== tool.latest_stable;
2744
+ const betaChanged = beta !== tool.latest_beta;
2745
+ const updates = {
2746
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
2747
+ };
2748
+ if (stableChanged)
2749
+ updates.latest_stable = stable;
2750
+ if (betaChanged)
2751
+ updates.latest_beta = beta;
2752
+ const { error: updateError } = await supabase.from("tools_registry").update(updates).eq("id", tool.id);
2753
+ if (updateError) {
2754
+ return { displayName: tool.display_name, stable, beta, status: "failed" };
2755
+ }
2756
+ return {
2757
+ displayName: tool.display_name,
2758
+ stable,
2759
+ beta,
2760
+ status: stableChanged || betaChanged ? "updated" : "unchanged"
2761
+ };
2762
+ } catch {
2763
+ return { displayName: tool.display_name, stable: null, beta: null, status: "failed" };
2764
+ }
2765
+ }));
2766
+ const nameW = Math.max(16, ...results.map((r) => r.displayName.length));
2767
+ const stableW = Math.max(8, ...results.map((r) => (r.stable ?? "\u2014").length));
2768
+ const betaW = Math.max(8, ...results.map((r) => (r.beta ?? "\u2014").length));
2769
+ const statusW = 10;
2770
+ const header = [
2771
+ "Tool".padEnd(nameW),
2772
+ "Stable".padEnd(stableW),
2773
+ "Beta".padEnd(betaW),
2774
+ "Status".padEnd(statusW)
2775
+ ].join(" ");
2776
+ console.log(chalk17.bold(header));
2777
+ console.log("\u2500".repeat(header.length));
2778
+ for (const result of results) {
2779
+ const statusColour = result.status === "updated" ? chalk17.green : result.status === "unchanged" ? chalk17.grey : result.status === "failed" ? chalk17.red : chalk17.yellow;
2780
+ const row = [
2781
+ result.displayName.padEnd(nameW),
2782
+ (result.stable ?? "\u2014").padEnd(stableW),
2783
+ (result.beta ?? "\u2014").padEnd(betaW),
2784
+ statusColour(result.status.padEnd(statusW))
2785
+ ].join(" ");
2786
+ console.log(row);
2569
2787
  }
2788
+ const updated = results.filter((r) => r.status === "updated").length;
2789
+ const unchanged = results.filter((r) => r.status === "unchanged").length;
2790
+ const failed = results.filter((r) => r.status === "failed").length;
2791
+ const noSource = results.filter((r) => r.status === "no source").length;
2792
+ console.log(chalk17.grey(`
2793
+ ${results.length} tool(s): `) + chalk17.green(`${updated} updated`) + ", " + chalk17.grey(`${unchanged} unchanged`) + ", " + chalk17.red(`${failed} failed`) + ", " + chalk17.yellow(`${noSource} no source`));
2570
2794
  }
2571
- function formatUptime(seconds) {
2572
- if (seconds < 60)
2573
- return `${seconds}s`;
2574
- if (seconds < 3600)
2575
- return `${Math.floor(seconds / 60)}m`;
2576
- const h = Math.floor(seconds / 3600);
2577
- const m = Math.floor(seconds % 3600 / 60);
2578
- return `${h}h ${m}m`;
2579
- }
2580
- async function mcpWatchCommand() {
2581
- const { supabase, userId } = await getAuthenticatedClient();
2582
- const deviceId = await resolveDeviceId(supabase, userId);
2583
- const deviceName = detectDeviceName();
2584
- const myPid = process.pid;
2585
- const myTty = detectTty();
2586
- console.log(chalk18.blue(`Starting MCP monitor for ${deviceName}...`));
2587
- await supabase.from("mcp_watchers").upsert({
2588
- device_id: deviceId,
2589
- pid: myPid,
2590
- tty: myTty,
2591
- cli_version: CURRENT_VERSION,
2592
- started_at: (/* @__PURE__ */ new Date()).toISOString(),
2593
- last_heartbeat: (/* @__PURE__ */ new Date()).toISOString()
2594
- }, { onConflict: "device_id,pid" });
2595
- const staleThreshold = new Date(Date.now() - 12e4).toISOString();
2596
- await supabase.from("mcp_watchers").delete().eq("device_id", deviceId).lt("last_heartbeat", staleThreshold);
2597
- async function cycle() {
2598
- const configs = await readAllMcpConfigs();
2599
- const rows = buildRows(configs);
2600
- const now = (/* @__PURE__ */ new Date()).toISOString();
2601
- await supabase.from("mcp_server_status").delete().eq("device_id", deviceId);
2602
- if (rows.length > 0) {
2603
- await supabase.from("mcp_server_status").insert(rows.map((row) => ({
2604
- device_id: deviceId,
2605
- ...row,
2606
- checked_at: now
2607
- })));
2795
+ async function fetchVersions(source) {
2796
+ const controller = new AbortController();
2797
+ const timeout = setTimeout(() => controller.abort(), 3e3);
2798
+ try {
2799
+ if (source.type === "npm") {
2800
+ return await fetchNpmVersions(source.package, controller.signal);
2801
+ } else {
2802
+ return await fetchGitHubVersions(source.repo, controller.signal);
2608
2803
  }
2609
- await supabase.from("mcp_watchers").update({ last_heartbeat: now }).eq("device_id", deviceId).eq("pid", myPid);
2610
- printTable(rows, deviceName);
2804
+ } finally {
2805
+ clearTimeout(timeout);
2611
2806
  }
2612
- await cycle();
2613
- let lastEnvHash = "";
2614
- async function envCycle() {
2615
- const state = await loadState();
2616
- if (!state.lastFolderId)
2617
- return;
2618
- const { data: dp } = await supabase.from("device_paths").select("path").eq("folder_id", state.lastFolderId).eq("device_name", deviceName).maybeSingle();
2619
- if (!dp?.path)
2620
- return;
2621
- const envManifest = await scanEnvManifest(dp.path);
2622
- if (!envManifest)
2623
- return;
2624
- const hash = createHash2("sha256").update(JSON.stringify(envManifest)).digest("hex");
2625
- if (hash === lastEnvHash)
2626
- return;
2627
- lastEnvHash = hash;
2628
- await supabase.from("claude_folders").update({ env_manifest_json: envManifest }).eq("id", state.lastFolderId);
2807
+ }
2808
+ async function fetchNpmVersions(packageName, signal) {
2809
+ const url = `https://registry.npmjs.org/-/package/${encodeURIComponent(packageName)}/dist-tags`;
2810
+ const res = await fetch(url, { signal });
2811
+ if (!res.ok) {
2812
+ throw new Error(`npm registry returned ${res.status}`);
2629
2813
  }
2630
- await envCycle();
2631
- const envInterval = setInterval(envCycle, ENV_POLL_INTERVAL_MS);
2632
- const interval = setInterval(cycle, POLL_INTERVAL_MS);
2633
- const shutdown = async () => {
2634
- clearInterval(interval);
2635
- clearInterval(envInterval);
2636
- await supabase.from("mcp_watchers").delete().eq("device_id", deviceId).eq("pid", myPid);
2637
- console.log(chalk18.dim("\nMCP monitor stopped."));
2638
- process.exit(0);
2639
- };
2640
- process.on("SIGINT", () => {
2641
- void shutdown();
2642
- });
2643
- process.on("SIGTERM", () => {
2644
- void shutdown();
2814
+ const tags = await res.json();
2815
+ const stable = tags.latest ?? null;
2816
+ const beta = tags.next ?? tags.beta ?? tags.rc ?? null;
2817
+ return { stable, beta };
2818
+ }
2819
+ async function fetchGitHubVersions(repo, signal) {
2820
+ const url = `https://api.github.com/repos/${repo}/releases?per_page=20`;
2821
+ const res = await fetch(url, {
2822
+ signal,
2823
+ headers: { Accept: "application/vnd.github+json" }
2645
2824
  });
2825
+ if (!res.ok) {
2826
+ throw new Error(`GitHub API returned ${res.status}`);
2827
+ }
2828
+ const releases = await res.json();
2829
+ let stable = null;
2830
+ let beta = null;
2831
+ for (const release of releases) {
2832
+ if (release.draft)
2833
+ continue;
2834
+ const version = release.tag_name.replace(/^v/, "");
2835
+ if (!release.prerelease && !stable) {
2836
+ stable = version;
2837
+ }
2838
+ if (release.prerelease && !beta) {
2839
+ beta = version;
2840
+ }
2841
+ if (stable && beta)
2842
+ break;
2843
+ }
2844
+ return { stable, beta };
2646
2845
  }
2647
2846
 
2847
+ // dist/index.js
2848
+ init_mcp_watch();
2849
+
2648
2850
  // dist/commands/init-manifest.js
2649
2851
  import { resolve as resolve5, join as join13, relative as relative3, dirname as dirname3 } from "node:path";
2650
2852
  import { readFile as readFile9, writeFile as writeFile5, mkdir as mkdir4 } from "node:fs/promises";
@@ -2770,14 +2972,132 @@ Manifest created: ${relative3(projectRoot, manifestPath)}`));
2770
2972
  console.log(chalk19.dim("Then run `md4ai scan` to verify against your environments."));
2771
2973
  }
2772
2974
 
2975
+ // dist/commands/update.js
2976
+ init_check_update();
2977
+ init_config();
2978
+ import chalk20 from "chalk";
2979
+ import { execFileSync as execFileSync6, spawn } from "node:child_process";
2980
+ async function fetchLatestVersion() {
2981
+ try {
2982
+ const controller = new AbortController();
2983
+ const timeout = setTimeout(() => controller.abort(), 5e3);
2984
+ const res = await fetch("https://registry.npmjs.org/md4ai/latest", {
2985
+ signal: controller.signal
2986
+ });
2987
+ clearTimeout(timeout);
2988
+ if (!res.ok)
2989
+ return null;
2990
+ const data = await res.json();
2991
+ return data.version ?? null;
2992
+ } catch {
2993
+ return null;
2994
+ }
2995
+ }
2996
+ function isNewer2(a, b) {
2997
+ const pa = a.split(".").map(Number);
2998
+ const pb = b.split(".").map(Number);
2999
+ for (let i = 0; i < 3; i++) {
3000
+ if ((pa[i] ?? 0) > (pb[i] ?? 0))
3001
+ return true;
3002
+ if ((pa[i] ?? 0) < (pb[i] ?? 0))
3003
+ return false;
3004
+ }
3005
+ return false;
3006
+ }
3007
+ function runNpmInstall() {
3008
+ try {
3009
+ execFileSync6("npm", ["install", "-g", "md4ai"], {
3010
+ stdio: "inherit",
3011
+ timeout: 6e4
3012
+ });
3013
+ return true;
3014
+ } catch {
3015
+ return false;
3016
+ }
3017
+ }
3018
+ function spawnPostUpdate() {
3019
+ const child = spawn("md4ai", ["update", "--post-update"], {
3020
+ stdio: "inherit",
3021
+ shell: false
3022
+ });
3023
+ child.on("exit", (code) => process.exit(code ?? 0));
3024
+ }
3025
+ async function postUpdateFlow() {
3026
+ const { confirm: confirm2 } = await import("@inquirer/prompts");
3027
+ console.log(chalk20.green(`
3028
+ md4ai v${CURRENT_VERSION} \u2014 you're on the latest version.
3029
+ `));
3030
+ const state = await loadState();
3031
+ const hasProject = !!state.lastFolderId;
3032
+ if (hasProject) {
3033
+ const wantScan = await confirm2({
3034
+ message: "Rescan your project and refresh the dashboard?",
3035
+ default: true
3036
+ });
3037
+ if (wantScan) {
3038
+ console.log("");
3039
+ const { syncCommand: syncCommand2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
3040
+ await syncCommand2({ all: false });
3041
+ console.log("");
3042
+ }
3043
+ }
3044
+ const wantWatch = await confirm2({
3045
+ message: "Start monitoring MCP servers? (runs until you press Ctrl+C)",
3046
+ default: true
3047
+ });
3048
+ if (wantWatch) {
3049
+ console.log("");
3050
+ const { mcpWatchCommand: mcpWatchCommand2 } = await Promise.resolve().then(() => (init_mcp_watch(), mcp_watch_exports));
3051
+ await mcpWatchCommand2();
3052
+ } else {
3053
+ console.log(chalk20.dim("\nYou can start monitoring later with: md4ai mcp-watch\n"));
3054
+ }
3055
+ }
3056
+ async function updateCommand(options) {
3057
+ if (options.postUpdate) {
3058
+ await postUpdateFlow();
3059
+ return;
3060
+ }
3061
+ console.log(chalk20.blue(`
3062
+ Current version: v${CURRENT_VERSION}`));
3063
+ console.log(chalk20.dim(" Checking for updates...\n"));
3064
+ const latest = await fetchLatestVersion();
3065
+ if (!latest) {
3066
+ console.log(chalk20.yellow(" Could not reach the npm registry. Check your internet connection."));
3067
+ process.exit(1);
3068
+ }
3069
+ if (!isNewer2(latest, CURRENT_VERSION)) {
3070
+ await postUpdateFlow();
3071
+ return;
3072
+ }
3073
+ console.log(chalk20.white(" Update available: ") + chalk20.dim(`v${CURRENT_VERSION}`) + chalk20.white(" \u2192 ") + chalk20.green.bold(`v${latest}
3074
+ `));
3075
+ const { confirm: confirm2 } = await import("@inquirer/prompts");
3076
+ const wantUpdate = await confirm2({
3077
+ message: `Install md4ai v${latest}?`,
3078
+ default: true
3079
+ });
3080
+ if (!wantUpdate) {
3081
+ console.log(chalk20.dim("\nUpdate skipped.\n"));
3082
+ return;
3083
+ }
3084
+ console.log(chalk20.blue("\n Installing...\n"));
3085
+ const ok = runNpmInstall();
3086
+ if (!ok) {
3087
+ console.log(chalk20.red("\n npm install failed. Try running manually:"));
3088
+ console.log(chalk20.cyan(" npm install -g md4ai\n"));
3089
+ process.exit(1);
3090
+ }
3091
+ console.log(chalk20.green("\n Updated successfully.\n"));
3092
+ spawnPostUpdate();
3093
+ }
3094
+
2773
3095
  // dist/index.js
3096
+ init_check_update();
2774
3097
  var program = new Command();
2775
3098
  program.name("md4ai").description("MD4AI \u2014 Claude tooling visualiser").version(CURRENT_VERSION).addHelpText("after", `
2776
3099
  Update to the latest version:
2777
- npm install -g md4ai
2778
-
2779
- Check for updates:
2780
- md4ai check-update`);
3100
+ md4ai update`);
2781
3101
  program.command("login").description("Log in to MD4AI with email and password").action(loginCommand);
2782
3102
  program.command("logout").description("Log out and clear stored credentials").action(logoutCommand);
2783
3103
  program.command("status").description("Show login status, device count, folder count, last sync").action(statusCommand);
@@ -2790,6 +3110,7 @@ program.command("simulate <prompt>").description("Show which files Claude would
2790
3110
  program.command("print <title>").description("Generate a printable wall-chart HTML from the last scan").action(printCommand);
2791
3111
  program.command("sync").description("Re-push latest scan data to Supabase").option("--all", "Sync all folders on all devices").action(syncCommand);
2792
3112
  program.command("import <zipfile>").description("Import a Claude setup bundle exported from the web dashboard").action(importBundleCommand);
3113
+ 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);
2793
3114
  program.command("check-update").description("Check if a newer version of md4ai is available").action(checkForUpdate);
2794
3115
  program.command("mcp-watch").description("Monitor MCP server status on this device (runs until Ctrl+C)").action(mcpWatchCommand);
2795
3116
  program.command("init-manifest").description("Scaffold a starter env-manifest.md from detected .env files").action(initManifestCommand);
@@ -2798,3 +3119,8 @@ admin.command("update-tool").description("Add or update a tool in the master reg
2798
3119
  admin.command("list-tools").description("List all tools in the master registry").action(adminListToolsCommand);
2799
3120
  admin.command("fetch-versions").description("Fetch latest stable and beta versions from npm and GitHub").action(adminFetchVersionsCommand);
2800
3121
  program.parse();
3122
+ var ran = program.args[0];
3123
+ var skipAutoCheck = ["update", "check-update", "mcp-watch"];
3124
+ if (!skipAutoCheck.includes(ran)) {
3125
+ autoCheckForUpdate();
3126
+ }