md4ai 0.7.0 → 0.7.2

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 +1665 -1362
  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,121 +1115,22 @@ 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 = "0.6.2";
1365
1134
  async function checkForUpdate() {
1366
1135
  try {
1367
1136
  const controller = new AbortController();
@@ -1378,13 +1147,12 @@ async function checkForUpdate() {
1378
1147
  return;
1379
1148
  if (latest !== CURRENT_VERSION && isNewer(latest, CURRENT_VERSION)) {
1380
1149
  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"));
1150
+ 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"));
1151
+ console.log(chalk8.yellow("\u2502") + chalk8.bold(" Update available! ") + chalk8.dim(`${CURRENT_VERSION}`) + chalk8.white(" \u2192 ") + chalk8.green.bold(`${latest}`) + " " + chalk8.yellow("\u2502"));
1152
+ console.log(chalk8.yellow("\u2502") + " " + chalk8.yellow("\u2502"));
1153
+ console.log(chalk8.yellow("\u2502") + " Run: " + chalk8.cyan("md4ai update") + " " + chalk8.yellow("\u2502"));
1154
+ console.log(chalk8.yellow("\u2502") + " " + chalk8.yellow("\u2502"));
1155
+ 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"));
1388
1156
  console.log("");
1389
1157
  } else {
1390
1158
  console.log(chalk8.green(`md4ai v${CURRENT_VERSION} \u2014 you're on the latest version.`));
@@ -1403,6 +1171,13 @@ function isNewer(a, b) {
1403
1171
  }
1404
1172
  return false;
1405
1173
  }
1174
+ var CURRENT_VERSION;
1175
+ var init_check_update = __esm({
1176
+ "dist/check-update.js"() {
1177
+ "use strict";
1178
+ CURRENT_VERSION = true ? "0.7.2" : "0.0.0-dev";
1179
+ }
1180
+ });
1406
1181
 
1407
1182
  // dist/commands/push-toolings.js
1408
1183
  async function pushToolings(supabase, folderId, toolings) {
@@ -1422,75 +1197,35 @@ async function pushToolings(supabase, folderId, toolings) {
1422
1197
  }, { onConflict: "folder_id,tool_name,detection_source" });
1423
1198
  }
1424
1199
  }
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 });
1200
+ var init_push_toolings = __esm({
1201
+ "dist/commands/push-toolings.js"() {
1202
+ "use strict";
1448
1203
  }
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({
1204
+ });
1205
+
1206
+ // dist/commands/sync.js
1207
+ var sync_exports = {};
1208
+ __export(sync_exports, {
1209
+ syncCommand: () => syncCommand
1210
+ });
1211
+ import chalk12 from "chalk";
1212
+ async function syncCommand(options) {
1213
+ const { supabase } = await getAuthenticatedClient();
1214
+ if (options.all) {
1215
+ const { data: devices, error } = await supabase.from("device_paths").select("folder_id, device_name, path");
1216
+ if (error || !devices?.length) {
1217
+ console.error(chalk12.red("No devices found."));
1218
+ process.exit(1);
1219
+ }
1220
+ for (const device of devices) {
1221
+ console.log(chalk12.blue(`Syncing: ${device.path}`));
1222
+ const { data: proposedAll } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
1223
+ if (proposedAll?.length) {
1224
+ console.log(chalk12.yellow(` ${proposedAll.length} file(s) proposed for deletion \u2014 run \`md4ai scan\` to review.`));
1225
+ }
1226
+ try {
1227
+ const result = await scanProject(device.path);
1228
+ await supabase.from("claude_folders").update({
1494
1229
  graph_json: result.graph,
1495
1230
  orphans_json: result.orphans,
1496
1231
  skills_table_json: result.skills,
@@ -1498,197 +1233,12 @@ ${proposedFiles.length} file(s) proposed for deletion:
1498
1233
  env_manifest_json: result.envManifest,
1499
1234
  last_scanned: result.scannedAt,
1500
1235
  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}`));
1236
+ }).eq("id", device.folder_id);
1237
+ await pushToolings(supabase, device.folder_id, result.toolings);
1238
+ await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
1239
+ console.log(chalk12.green(` Done: ${device.device_name}`));
1240
+ } catch (err) {
1241
+ console.error(chalk12.red(` Failed: ${device.path}: ${err}`));
1692
1242
  }
1693
1243
  }
1694
1244
  await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
@@ -1724,10 +1274,15 @@ async function syncCommand(options) {
1724
1274
  console.log(chalk12.green("Synced."));
1725
1275
  }
1726
1276
  }
1727
-
1728
- // dist/commands/link.js
1729
- import { resolve as resolve4 } from "node:path";
1730
- import chalk13 from "chalk";
1277
+ var init_sync = __esm({
1278
+ "dist/commands/sync.js"() {
1279
+ "use strict";
1280
+ init_auth();
1281
+ init_config();
1282
+ init_scanner();
1283
+ init_push_toolings();
1284
+ }
1285
+ });
1731
1286
 
1732
1287
  // dist/device-utils.js
1733
1288
  import { hostname as hostname2, platform as platform2 } from "node:os";
@@ -1761,677 +1316,250 @@ async function resolveDeviceId(supabase, userId) {
1761
1316
  }
1762
1317
  return data.id;
1763
1318
  }
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);
1319
+ var init_device_utils = __esm({
1320
+ "dist/device-utils.js"() {
1321
+ "use strict";
1776
1322
  }
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
- }
1323
+ });
1324
+
1325
+ // dist/mcp/read-configs.js
1326
+ import { readFile as readFile8 } from "node:fs/promises";
1327
+ import { join as join12 } from "node:path";
1328
+ import { homedir as homedir7 } from "node:os";
1329
+ import { existsSync as existsSync11 } from "node:fs";
1330
+ import { readdir as readdir3 } from "node:fs/promises";
1331
+ async function readJsonSafe(path) {
1332
+ try {
1333
+ if (!existsSync11(path))
1334
+ return null;
1335
+ const raw = await readFile8(path, "utf-8");
1336
+ return JSON.parse(raw);
1337
+ } catch {
1338
+ return null;
1804
1339
  }
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}`));
1340
+ }
1341
+ function extractPackageName(args) {
1342
+ for (const arg of args) {
1343
+ if (arg.startsWith("-"))
1344
+ continue;
1345
+ if (arg.includes("mcp") || arg.startsWith("@"))
1346
+ return arg;
1825
1347
  }
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).`));
1348
+ for (const arg of args) {
1349
+ if (!arg.startsWith("-"))
1350
+ return arg;
1839
1351
  }
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.'));
1352
+ return null;
1852
1353
  }
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);
1354
+ function parseServers(data, source) {
1355
+ if (!data?.mcpServers)
1356
+ return [];
1357
+ const entries = [];
1358
+ for (const [name, server] of Object.entries(data.mcpServers)) {
1359
+ const serverType = server.type === "http" ? "http" : "stdio";
1360
+ entries.push({
1361
+ name,
1362
+ source,
1363
+ type: serverType,
1364
+ command: server.command,
1365
+ args: server.args,
1366
+ env: server.env,
1367
+ url: server.url
1368
+ });
1864
1369
  }
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);
1370
+ return entries;
1371
+ }
1372
+ function parseFlatConfig(data, source) {
1373
+ if (!data)
1374
+ return [];
1375
+ const entries = [];
1376
+ for (const [name, server] of Object.entries(data)) {
1377
+ if (typeof server !== "object" || server === null)
1378
+ continue;
1379
+ if (!server.command && !server.url)
1380
+ continue;
1381
+ const serverType = server.type === "http" ? "http" : "stdio";
1382
+ entries.push({
1383
+ name,
1384
+ source,
1385
+ type: serverType,
1386
+ command: server.command,
1387
+ args: server.args,
1388
+ env: server.env,
1389
+ url: server.url
1390
+ });
1872
1391
  }
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 });
1392
+ return entries;
1393
+ }
1394
+ async function readAllMcpConfigs() {
1395
+ const home = homedir7();
1396
+ const entries = [];
1397
+ const userConfig = await readJsonSafe(join12(home, ".claude.json"));
1398
+ entries.push(...parseServers(userConfig, "global"));
1399
+ const globalMcp = await readJsonSafe(join12(home, ".claude", "mcp.json"));
1400
+ entries.push(...parseServers(globalMcp, "global"));
1401
+ const cwdMcp = await readJsonSafe(join12(process.cwd(), ".mcp.json"));
1402
+ entries.push(...parseServers(cwdMcp, "project"));
1403
+ const pluginsBase = join12(home, ".claude", "plugins", "marketplaces");
1404
+ if (existsSync11(pluginsBase)) {
1405
+ try {
1406
+ const marketplaces = await readdir3(pluginsBase, { withFileTypes: true });
1407
+ for (const mp of marketplaces) {
1408
+ if (!mp.isDirectory())
1409
+ continue;
1410
+ const extDir = join12(pluginsBase, mp.name, "external_plugins");
1411
+ if (!existsSync11(extDir))
1412
+ continue;
1413
+ const plugins = await readdir3(extDir, { withFileTypes: true });
1414
+ for (const plugin of plugins) {
1415
+ if (!plugin.isDirectory())
1416
+ continue;
1417
+ const pluginMcp = await readJsonSafe(join12(extDir, plugin.name, ".mcp.json"));
1418
+ entries.push(...parseServers(pluginMcp, "plugin"));
1419
+ }
1420
+ }
1421
+ } catch {
1885
1422
  }
1886
1423
  }
1887
- if (files.length === 0) {
1888
- console.log(chalk14.yellow("No Claude config files found in bundle."));
1889
- return;
1890
- }
1891
- console.log(chalk14.blue(`
1892
- Files to extract:`));
1893
- for (const f of files) {
1894
- console.log(` ${f.filePath}`);
1895
- }
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;
1906
- }
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 });
1424
+ const cacheBase = join12(home, ".claude", "plugins", "cache");
1425
+ if (existsSync11(cacheBase)) {
1426
+ try {
1427
+ const registries = await readdir3(cacheBase, { withFileTypes: true });
1428
+ for (const reg of registries) {
1429
+ if (!reg.isDirectory())
1430
+ continue;
1431
+ const regDir = join12(cacheBase, reg.name);
1432
+ const plugins = await readdir3(regDir, { withFileTypes: true });
1433
+ for (const plugin of plugins) {
1434
+ if (!plugin.isDirectory())
1435
+ continue;
1436
+ const versionDirs = await readdir3(join12(regDir, plugin.name), { withFileTypes: true });
1437
+ for (const ver of versionDirs) {
1438
+ if (!ver.isDirectory())
1439
+ continue;
1440
+ const mcpPath = join12(regDir, plugin.name, ver.name, ".mcp.json");
1441
+ const flatData = await readJsonSafe(mcpPath);
1442
+ if (flatData) {
1443
+ entries.push(...parseFlatConfig(flatData, "plugin"));
1444
+ break;
1445
+ }
1446
+ }
1447
+ }
1448
+ }
1449
+ } catch {
1912
1450
  }
1913
- await writeFile4(fullPath, file.content, "utf-8");
1914
- console.log(chalk14.green(` \u2713 ${file.filePath}`));
1915
1451
  }
1916
- console.log(chalk14.green(`
1917
- Done! ${files.length} file(s) extracted to ${targetDir}`));
1918
- }
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);
1452
+ const seen = /* @__PURE__ */ new Set();
1453
+ const deduped = [];
1454
+ for (const entry of entries) {
1455
+ if (seen.has(entry.name))
1456
+ continue;
1457
+ seen.add(entry.name);
1458
+ deduped.push(entry);
1928
1459
  }
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);
1460
+ return deduped;
1461
+ }
1462
+ var init_read_configs = __esm({
1463
+ "dist/mcp/read-configs.js"() {
1464
+ "use strict";
1932
1465
  }
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);
1960
- }
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
1466
+ });
1467
+
1468
+ // dist/mcp/scan-processes.js
1469
+ import { execFileSync as execFileSync4 } from "node:child_process";
1470
+ function parsePsOutput(output) {
1471
+ const lines = output.trim().split("\n").slice(1);
1472
+ const entries = [];
1473
+ for (const line of lines) {
1474
+ const trimmed = line.trim();
1475
+ const match = trimmed.match(/^\s*(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(.+)$/);
1476
+ if (!match)
1477
+ continue;
1478
+ entries.push({
1479
+ pid: parseInt(match[1], 10),
1480
+ tty: match[2],
1481
+ etimeRaw: match[3],
1482
+ rss: parseInt(match[4], 10),
1483
+ args: match[5]
1970
1484
  });
1971
- if (error) {
1972
- console.error(chalk15.red(`Failed to create: ${error.message}`));
1973
- process.exit(1);
1974
- }
1975
- console.log(chalk15.green(`Added "${options.display}" to the registry.`));
1976
1485
  }
1486
+ return entries;
1977
1487
  }
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);
1488
+ function parseEtime(etime) {
1489
+ let days = 0;
1490
+ let rest = etime;
1491
+ if (rest.includes("-")) {
1492
+ const [d, r] = rest.split("-");
1493
+ days = parseInt(d, 10);
1494
+ rest = r;
1987
1495
  }
1988
- if (!tools?.length) {
1989
- console.log(chalk16.yellow("No tools in the registry."));
1990
- return;
1496
+ const parts = rest.split(":").map(Number);
1497
+ if (parts.length === 3) {
1498
+ return days * 86400 + parts[0] * 3600 + parts[1] * 60 + parts[2];
1499
+ } else if (parts.length === 2) {
1500
+ return days * 86400 + parts[0] * 60 + parts[1];
1991
1501
  }
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);
2015
- }
2016
- console.log(chalk16.grey(`
2017
- ${tools.length} tool(s) in registry.`));
2018
- }
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" });
1502
+ return days * 86400 + (parts[0] ?? 0);
2030
1503
  }
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);
2074
- }
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);
1504
+ function findProcessesForConfig(config, processes) {
1505
+ if (config.type === "http")
1506
+ return [];
1507
+ const packageName = config.args ? extractPackageName(config.args) : null;
1508
+ const matches = [];
1509
+ for (const proc of processes) {
1510
+ let matched = false;
1511
+ if (packageName && proc.args.includes(packageName)) {
1512
+ matched = true;
1513
+ } else if (config.command === "node" && config.args?.[0]) {
1514
+ if (proc.args.includes(config.args[0])) {
1515
+ matched = true;
2091
1516
  }
2092
1517
  }
2093
- }
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)...
2099
- `));
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" };
2116
- }
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" };
1518
+ if (matched) {
1519
+ matches.push({
1520
+ pid: proc.pid,
1521
+ tty: proc.tty === "?" ? "" : proc.tty,
1522
+ uptimeSeconds: parseEtime(proc.etimeRaw),
1523
+ memoryMb: Math.round(proc.rss / 1024),
1524
+ commandLine: proc.args
1525
+ });
2125
1526
  }
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
1527
  }
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`));
1528
+ return matches;
2155
1529
  }
2156
- async function fetchVersions(source) {
2157
- const controller = new AbortController();
2158
- const timeout = setTimeout(() => controller.abort(), 3e3);
1530
+ function getProcessTable() {
2159
1531
  try {
2160
- if (source.type === "npm") {
2161
- return await fetchNpmVersions(source.package, controller.signal);
2162
- } else {
2163
- return await fetchGitHubVersions(source.repo, controller.signal);
2164
- }
2165
- } finally {
2166
- clearTimeout(timeout);
2167
- }
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}`);
1532
+ const output = execFileSync4("ps", ["-eo", "pid,tty,etime,rss,args"], {
1533
+ encoding: "utf-8",
1534
+ timeout: 5e3
1535
+ });
1536
+ return parsePsOutput(output);
1537
+ } catch {
1538
+ return [];
2174
1539
  }
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
1540
  }
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;
2201
- }
2202
- if (stable && beta)
2203
- break;
1541
+ var init_scan_processes = __esm({
1542
+ "dist/mcp/scan-processes.js"() {
1543
+ "use strict";
1544
+ init_read_configs();
2204
1545
  }
2205
- return { stable, beta };
2206
- }
1546
+ });
2207
1547
 
2208
1548
  // dist/commands/mcp-watch.js
1549
+ var mcp_watch_exports = {};
1550
+ __export(mcp_watch_exports, {
1551
+ mcpWatchCommand: () => mcpWatchCommand
1552
+ });
2209
1553
  import chalk18 from "chalk";
2210
1554
  import { execFileSync as execFileSync5 } from "node:child_process";
2211
-
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) {
1555
+ import { createHash as createHash2 } from "node:crypto";
1556
+ function detectTty() {
2219
1557
  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;
2226
- }
2227
- }
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;
2240
- }
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
2255
- });
2256
- }
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
- });
2278
- }
2279
- return entries;
2280
- }
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
- }
2310
- }
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 {
2337
- }
2338
- }
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);
2346
- }
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
- });
2367
- }
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;
2377
- }
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];
2383
- }
2384
- return days * 86400 + (parts[0] ?? 0);
2385
- }
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
- });
2408
- }
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
2417
- });
2418
- return parsePsOutput(output);
2419
- } catch {
2420
- return [];
2421
- }
2422
- }
2423
-
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;
1558
+ const result = execFileSync5("ps", ["-o", "tty=", "-p", String(process.pid)], {
1559
+ encoding: "utf-8",
1560
+ timeout: 3e3
1561
+ }).trim();
1562
+ return result && result !== "?" ? result : null;
2435
1563
  } catch {
2436
1564
  return null;
2437
1565
  }
@@ -2550,101 +1678,1157 @@ function printTable(rows, deviceName) {
2550
1678
  const uptime = formatUptime(s.uptime_seconds ?? 0);
2551
1679
  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}`);
2552
1680
  }
2553
- console.log("");
1681
+ console.log("");
1682
+ }
1683
+ }
1684
+ if (stopped.length > 0 || errored.length > 0) {
1685
+ const notRunning = [...stopped, ...errored];
1686
+ console.log(chalk18.yellow(` Not Running (${notRunning.length})`));
1687
+ for (const s of notRunning) {
1688
+ const icon = s.status === "error" ? chalk18.red("\u2717") : chalk18.yellow("\u25CB");
1689
+ const detail = s.error_detail ? chalk18.dim(` \u2014 ${s.error_detail}`) : "";
1690
+ console.log(` ${icon} ${s.server_name.padEnd(20)} ${chalk18.dim(s.config_source.padEnd(10))}${detail}`);
1691
+ }
1692
+ console.log("");
1693
+ }
1694
+ if (rows.length === 0) {
1695
+ console.log(chalk18.yellow(" No MCP servers configured."));
1696
+ console.log(chalk18.dim(" Configure servers in ~/.claude/mcp.json or .mcp.json\n"));
1697
+ }
1698
+ }
1699
+ function formatUptime(seconds) {
1700
+ if (seconds < 60)
1701
+ return `${seconds}s`;
1702
+ if (seconds < 3600)
1703
+ return `${Math.floor(seconds / 60)}m`;
1704
+ const h = Math.floor(seconds / 3600);
1705
+ const m = Math.floor(seconds % 3600 / 60);
1706
+ return `${h}h ${m}m`;
1707
+ }
1708
+ async function mcpWatchCommand() {
1709
+ const { supabase, userId } = await getAuthenticatedClient();
1710
+ const deviceId = await resolveDeviceId(supabase, userId);
1711
+ const deviceName = detectDeviceName();
1712
+ const myPid = process.pid;
1713
+ const myTty = detectTty();
1714
+ console.log(chalk18.blue(`Starting MCP monitor for ${deviceName}...`));
1715
+ await supabase.from("mcp_watchers").upsert({
1716
+ device_id: deviceId,
1717
+ pid: myPid,
1718
+ tty: myTty,
1719
+ cli_version: CURRENT_VERSION,
1720
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
1721
+ last_heartbeat: (/* @__PURE__ */ new Date()).toISOString()
1722
+ }, { onConflict: "device_id,pid" });
1723
+ const staleThreshold = new Date(Date.now() - 12e4).toISOString();
1724
+ await supabase.from("mcp_watchers").delete().eq("device_id", deviceId).lt("last_heartbeat", staleThreshold);
1725
+ async function cycle() {
1726
+ const configs = await readAllMcpConfigs();
1727
+ const rows = buildRows(configs);
1728
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1729
+ await supabase.from("mcp_server_status").delete().eq("device_id", deviceId);
1730
+ if (rows.length > 0) {
1731
+ await supabase.from("mcp_server_status").insert(rows.map((row) => ({
1732
+ device_id: deviceId,
1733
+ ...row,
1734
+ checked_at: now
1735
+ })));
1736
+ }
1737
+ await supabase.from("mcp_watchers").update({ last_heartbeat: now }).eq("device_id", deviceId).eq("pid", myPid);
1738
+ printTable(rows, deviceName);
1739
+ }
1740
+ await cycle();
1741
+ let lastEnvHash = "";
1742
+ async function envCycle() {
1743
+ const state = await loadState();
1744
+ if (!state.lastFolderId)
1745
+ return;
1746
+ const { data: dp } = await supabase.from("device_paths").select("path").eq("folder_id", state.lastFolderId).eq("device_name", deviceName).maybeSingle();
1747
+ if (!dp?.path)
1748
+ return;
1749
+ const envManifest = await scanEnvManifest(dp.path);
1750
+ if (!envManifest)
1751
+ return;
1752
+ const hash = createHash2("sha256").update(JSON.stringify(envManifest)).digest("hex");
1753
+ if (hash === lastEnvHash)
1754
+ return;
1755
+ lastEnvHash = hash;
1756
+ await supabase.from("claude_folders").update({ env_manifest_json: envManifest }).eq("id", state.lastFolderId);
1757
+ }
1758
+ await envCycle();
1759
+ const envInterval = setInterval(envCycle, ENV_POLL_INTERVAL_MS);
1760
+ const interval = setInterval(cycle, POLL_INTERVAL_MS);
1761
+ const shutdown = async () => {
1762
+ clearInterval(interval);
1763
+ clearInterval(envInterval);
1764
+ await supabase.from("mcp_watchers").delete().eq("device_id", deviceId).eq("pid", myPid);
1765
+ console.log(chalk18.dim("\nMCP monitor stopped."));
1766
+ process.exit(0);
1767
+ };
1768
+ process.on("SIGINT", () => {
1769
+ void shutdown();
1770
+ });
1771
+ process.on("SIGTERM", () => {
1772
+ void shutdown();
1773
+ });
1774
+ }
1775
+ var POLL_INTERVAL_MS, ENV_POLL_INTERVAL_MS;
1776
+ var init_mcp_watch = __esm({
1777
+ "dist/commands/mcp-watch.js"() {
1778
+ "use strict";
1779
+ init_auth();
1780
+ init_device_utils();
1781
+ init_read_configs();
1782
+ init_scan_processes();
1783
+ init_check_update();
1784
+ init_env_manifest_scanner();
1785
+ init_config();
1786
+ POLL_INTERVAL_MS = 3e4;
1787
+ ENV_POLL_INTERVAL_MS = 3e5;
1788
+ }
1789
+ });
1790
+
1791
+ // dist/index.js
1792
+ import { Command } from "commander";
1793
+
1794
+ // dist/commands/login.js
1795
+ init_dist();
1796
+ init_config();
1797
+ import chalk from "chalk";
1798
+ import { input, password } from "@inquirer/prompts";
1799
+ async function loginCommand() {
1800
+ console.log(chalk.blue("MD4AI Login\n"));
1801
+ const email = await input({ message: "Email:" });
1802
+ const pwd = await password({ message: "Password:" });
1803
+ const anonKey = getAnonKey();
1804
+ const supabase = createSupabaseClient(anonKey);
1805
+ const { data, error } = await supabase.auth.signInWithPassword({
1806
+ email,
1807
+ password: pwd
1808
+ });
1809
+ if (error) {
1810
+ console.error(chalk.red(`Login failed: ${error.message}`));
1811
+ process.exit(1);
1812
+ }
1813
+ await saveCredentials({
1814
+ accessToken: data.session.access_token,
1815
+ refreshToken: data.session.refresh_token,
1816
+ expiresAt: Date.now() + data.session.expires_in * 1e3,
1817
+ userId: data.user.id,
1818
+ email: data.user.email ?? email
1819
+ });
1820
+ console.log(chalk.green(`
1821
+ Logged in as ${data.user.email}`));
1822
+ }
1823
+
1824
+ // dist/commands/logout.js
1825
+ init_config();
1826
+ import chalk2 from "chalk";
1827
+ async function logoutCommand() {
1828
+ await clearCredentials();
1829
+ console.log(chalk2.green("Logged out successfully."));
1830
+ }
1831
+
1832
+ // dist/commands/status.js
1833
+ init_dist();
1834
+ init_config();
1835
+ import chalk3 from "chalk";
1836
+ async function statusCommand() {
1837
+ const creds = await loadCredentials();
1838
+ if (!creds?.accessToken) {
1839
+ console.log(chalk3.yellow("Not logged in. Run: md4ai login"));
1840
+ return;
1841
+ }
1842
+ console.log(chalk3.blue("MD4AI Status\n"));
1843
+ console.log(` User: ${creds.email}`);
1844
+ console.log(` Expires: ${new Date(creds.expiresAt).toLocaleString()}`);
1845
+ try {
1846
+ const anonKey = getAnonKey();
1847
+ const supabase = createSupabaseClient(anonKey, creds.accessToken);
1848
+ const { count: folderCount } = await supabase.from("claude_folders").select("*", { count: "exact", head: true });
1849
+ const { count: deviceCount } = await supabase.from("device_paths").select("*", { count: "exact", head: true });
1850
+ console.log(` Folders: ${folderCount ?? 0}`);
1851
+ console.log(` Devices: ${deviceCount ?? 0}`);
1852
+ const state = await loadState();
1853
+ console.log(` Last sync: ${state.lastSyncAt ?? "never"}`);
1854
+ } catch {
1855
+ console.log(chalk3.yellow(" (Could not fetch remote data)"));
1856
+ }
1857
+ }
1858
+
1859
+ // dist/commands/add-folder.js
1860
+ init_auth();
1861
+ import chalk5 from "chalk";
1862
+ import { input as input2, select } from "@inquirer/prompts";
1863
+ async function addFolderCommand() {
1864
+ const { supabase, userId } = await getAuthenticatedClient();
1865
+ const name = await input2({ message: "Folder name:" });
1866
+ const description = await input2({ message: "Description (optional):" });
1867
+ const { data: teams } = await supabase.from("team_members").select("team_id, teams(id, name)").eq("user_id", userId);
1868
+ const { data: ownedTeams } = await supabase.from("teams").select("id, name").eq("created_by", userId);
1869
+ const allTeams = /* @__PURE__ */ new Map();
1870
+ for (const t of ownedTeams ?? []) {
1871
+ allTeams.set(t.id, t.name);
1872
+ }
1873
+ for (const t of teams ?? []) {
1874
+ const team = t.teams;
1875
+ if (team)
1876
+ allTeams.set(team.id, team.name);
1877
+ }
1878
+ let teamId = null;
1879
+ if (allTeams.size > 0) {
1880
+ const choices = [
1881
+ { name: "Personal (only you)", value: "__personal__" },
1882
+ ...Array.from(allTeams.entries()).map(([id, teamName]) => ({
1883
+ name: teamName,
1884
+ value: id
1885
+ }))
1886
+ ];
1887
+ const selected = await select({
1888
+ message: "Assign to a team?",
1889
+ choices
1890
+ });
1891
+ teamId = selected === "__personal__" ? null : selected;
1892
+ }
1893
+ const { data, error } = await supabase.from("claude_folders").insert({
1894
+ user_id: userId,
1895
+ name,
1896
+ description: description || null,
1897
+ team_id: teamId
1898
+ }).select().single();
1899
+ if (error) {
1900
+ console.error(chalk5.red(`Failed to create folder: ${error.message}`));
1901
+ process.exit(1);
1902
+ }
1903
+ const teamLabel = teamId ? `(team: ${allTeams.get(teamId)})` : "(personal)";
1904
+ console.log(chalk5.green(`
1905
+ Folder "${name}" created ${teamLabel} (${data.id})`));
1906
+ }
1907
+
1908
+ // dist/commands/add-device.js
1909
+ init_auth();
1910
+ import { resolve } from "node:path";
1911
+ import { hostname, platform } from "node:os";
1912
+ import chalk6 from "chalk";
1913
+ import { input as input3, select as select2 } from "@inquirer/prompts";
1914
+ function detectOs() {
1915
+ const p = platform();
1916
+ if (p === "win32")
1917
+ return "windows";
1918
+ if (p === "darwin")
1919
+ return "macos";
1920
+ if (p === "linux")
1921
+ return "linux";
1922
+ return "other";
1923
+ }
1924
+ function suggestDeviceName() {
1925
+ const os = detectOs();
1926
+ const host = hostname().split(".")[0];
1927
+ const osLabel = os.charAt(0).toUpperCase() + os.slice(1);
1928
+ return `${host}-${osLabel}`;
1929
+ }
1930
+ async function addDeviceCommand() {
1931
+ const { supabase, userId } = await getAuthenticatedClient();
1932
+ const cwd = resolve(process.cwd());
1933
+ console.log(chalk6.blue.bold("\n\u{1F4C2} Where is this Claude project?\n"));
1934
+ const localPath = await input3({
1935
+ message: "Local project path:",
1936
+ default: cwd
1937
+ });
1938
+ const { data: folders, error: foldersErr } = await supabase.from("claude_folders").select("id, name").order("name");
1939
+ if (foldersErr || !folders?.length) {
1940
+ console.error(chalk6.red("No folders found. Run: md4ai add-folder"));
1941
+ process.exit(1);
1942
+ }
1943
+ const folderId = await select2({
1944
+ message: "Select folder:",
1945
+ choices: folders.map((f) => ({ name: f.name, value: f.id }))
1946
+ });
1947
+ const suggested = suggestDeviceName();
1948
+ const deviceName = await input3({
1949
+ message: "Device name:",
1950
+ default: suggested
1951
+ });
1952
+ const osType = await select2({
1953
+ message: "OS type:",
1954
+ choices: [
1955
+ { name: "Windows", value: "windows" },
1956
+ { name: "macOS", value: "macos" },
1957
+ { name: "Ubuntu", value: "ubuntu" },
1958
+ { name: "Linux", value: "linux" },
1959
+ { name: "Other", value: "other" }
1960
+ ],
1961
+ default: detectOs()
1962
+ });
1963
+ const description = await input3({ message: "Description (optional):" });
1964
+ const { error } = await supabase.from("device_paths").insert({
1965
+ user_id: userId,
1966
+ folder_id: folderId,
1967
+ device_name: deviceName,
1968
+ os_type: osType,
1969
+ path: localPath,
1970
+ description: description || null
1971
+ });
1972
+ if (error) {
1973
+ console.error(chalk6.red(`Failed to add device: ${error.message}`));
1974
+ process.exit(1);
1975
+ }
1976
+ console.log(chalk6.green(`
1977
+ Device "${deviceName}" added to folder.`));
1978
+ }
1979
+
1980
+ // dist/commands/list-devices.js
1981
+ init_auth();
1982
+ import chalk7 from "chalk";
1983
+ async function listDevicesCommand() {
1984
+ const { supabase } = await getAuthenticatedClient();
1985
+ const { data: devices, error } = await supabase.from("device_paths").select(`
1986
+ id, device_name, os_type, path, last_synced, description,
1987
+ claude_folders!inner ( name )
1988
+ `).order("device_name");
1989
+ if (error) {
1990
+ console.error(chalk7.red(`Failed to list devices: ${error.message}`));
1991
+ process.exit(1);
1992
+ }
1993
+ if (!devices?.length) {
1994
+ console.log(chalk7.yellow("No devices found. Run: md4ai add-device"));
1995
+ return;
1996
+ }
1997
+ const grouped = /* @__PURE__ */ new Map();
1998
+ for (const d of devices) {
1999
+ const list = grouped.get(d.device_name) ?? [];
2000
+ list.push(d);
2001
+ grouped.set(d.device_name, list);
2002
+ }
2003
+ for (const [deviceName, entries] of grouped) {
2004
+ const first = entries[0];
2005
+ console.log(chalk7.bold(`
2006
+ ${deviceName}`) + chalk7.dim(` (${first.os_type})`));
2007
+ for (const entry of entries) {
2008
+ const folderName = entry.claude_folders?.name ?? "unknown";
2009
+ const synced = entry.last_synced ? new Date(entry.last_synced).toLocaleString() : "never";
2010
+ console.log(` ${chalk7.cyan(folderName)} \u2192 ${entry.path}`);
2011
+ console.log(` Last synced: ${synced}`);
2012
+ }
2013
+ }
2014
+ }
2015
+
2016
+ // dist/commands/map.js
2017
+ init_scanner();
2018
+ init_auth();
2019
+ init_config();
2020
+ import { resolve as resolve3 } from "node:path";
2021
+ import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
2022
+ import { existsSync as existsSync7 } from "node:fs";
2023
+ import chalk9 from "chalk";
2024
+
2025
+ // dist/output/html-generator.js
2026
+ function generateOfflineHtml(result, projectRoot) {
2027
+ const title = projectRoot.split("/").pop() ?? "MD4AI";
2028
+ const dataJson = JSON.stringify(result);
2029
+ 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");
2030
+ 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");
2031
+ 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");
2032
+ 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");
2033
+ return `<!DOCTYPE html>
2034
+ <html lang="en">
2035
+ <head>
2036
+ <meta charset="UTF-8">
2037
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2038
+ <title>MD4AI \u2014 ${escapeHtml(title)}</title>
2039
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
2040
+ <style>
2041
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2042
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
2043
+ h1 { color: #38bdf8; margin-bottom: 0.5rem; }
2044
+ .subtitle { color: #94a3b8; margin-bottom: 2rem; }
2045
+ .section { background: #1e293b; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
2046
+ .section h2 { color: #38bdf8; margin-bottom: 1rem; font-size: 1.2rem; }
2047
+ .mermaid { background: #0f172a; border-radius: 8px; padding: 1rem; overflow-x: auto; }
2048
+ .badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.85rem; margin-left: 0.5rem; }
2049
+ .badge-orphan { background: #ef4444; color: white; }
2050
+ .badge-stale { background: #f59e0b; color: black; }
2051
+ table { width: 100%; border-collapse: collapse; }
2052
+ th, td { padding: 0.5rem 1rem; text-align: left; border-bottom: 1px solid #334155; }
2053
+ th { color: #94a3b8; font-weight: 600; }
2054
+ .check { color: #22c55e; }
2055
+ .meta { color: #94a3b8; }
2056
+ .stale-meta { color: #f59e0b; }
2057
+ .stats { display: flex; gap: 2rem; flex-wrap: wrap; margin-bottom: 1rem; }
2058
+ .stat { text-align: center; }
2059
+ .stat-value { font-size: 2rem; font-weight: bold; color: #38bdf8; }
2060
+ .stat-label { color: #94a3b8; font-size: 0.85rem; }
2061
+ .file-list { list-style: none; }
2062
+ .file-list li { padding: 0.3rem 0; color: #cbd5e1; font-family: monospace; font-size: 0.9rem; }
2063
+ #search { width: 100%; padding: 0.75rem 1rem; background: #0f172a; border: 1px solid #334155; border-radius: 8px; color: #e2e8f0; font-size: 1rem; margin-bottom: 1rem; }
2064
+ #search:focus { outline: none; border-color: #38bdf8; }
2065
+ @media print { body { background: white; color: black; } .section { border: 1px solid #ccc; } }
2066
+ </style>
2067
+ </head>
2068
+ <body>
2069
+ <h1>MD4AI \u2014 ${escapeHtml(title)}</h1>
2070
+ <p class="subtitle">Scanned: ${new Date(result.scannedAt).toLocaleString()} | Hash: ${result.dataHash.slice(0, 12)}</p>
2071
+
2072
+ <div class="stats">
2073
+ <div class="stat"><div class="stat-value">${result.graph.nodes.length}</div><div class="stat-label">Files</div></div>
2074
+ <div class="stat"><div class="stat-value">${result.graph.edges.length}</div><div class="stat-label">References</div></div>
2075
+ <div class="stat"><div class="stat-value">${result.orphans.length}</div><div class="stat-label">Orphans</div></div>
2076
+ <div class="stat"><div class="stat-value">${result.staleFiles.length}</div><div class="stat-label">Stale</div></div>
2077
+ <div class="stat"><div class="stat-value">${result.skills.length}</div><div class="stat-label">Skills</div></div>
2078
+ </div>
2079
+
2080
+ <input type="text" id="search" placeholder="Search files..." oninput="filterFiles(this.value)">
2081
+
2082
+ <div class="section">
2083
+ <h2>Dependency Graph</h2>
2084
+ <pre class="mermaid">${escapeHtml(result.graph.mermaid)}</pre>
2085
+ </div>
2086
+
2087
+ ${result.orphans.length > 0 ? `
2088
+ <div class="section">
2089
+ <h2>Orphan Files <span class="badge badge-orphan">${result.orphans.length}</span></h2>
2090
+ <p class="meta" style="margin-bottom: 1rem;">Files not reachable from any root configuration file.</p>
2091
+ <ul class="file-list" id="orphan-list">${orphanItems}</ul>
2092
+ </div>
2093
+ ` : ""}
2094
+
2095
+ ${result.staleFiles.length > 0 ? `
2096
+ <div class="section">
2097
+ <h2>Stale Files <span class="badge badge-stale">${result.staleFiles.length}</span></h2>
2098
+ <p class="meta" style="margin-bottom: 1rem;">Files not modified in over 90 days.</p>
2099
+ <ul class="file-list" id="stale-list">${staleItems}</ul>
2100
+ </div>
2101
+ ` : ""}
2102
+
2103
+ <div class="section">
2104
+ <h2>Skills Comparison</h2>
2105
+ <table>
2106
+ <thead><tr><th>Skill/Plugin</th><th>Entire Machine</th><th>Project Specific</th><th>Source</th></tr></thead>
2107
+ <tbody>${skillRows}</tbody>
2108
+ </table>
2109
+ </div>
2110
+
2111
+ <div class="section">
2112
+ <h2>All Files</h2>
2113
+ <ul class="file-list" id="all-files">${fileItems}</ul>
2114
+ </div>
2115
+
2116
+ <script>
2117
+ mermaid.initialize({ startOnLoad: true, theme: 'dark' });
2118
+
2119
+ function filterFiles(query) {
2120
+ var q = query.toLowerCase();
2121
+ document.querySelectorAll('.file-list li').forEach(function(li) {
2122
+ var path = li.getAttribute('data-path') || '';
2123
+ li.style.display = path.toLowerCase().indexOf(q) >= 0 ? '' : 'none';
2124
+ });
2125
+ }
2126
+ </script>
2127
+
2128
+ <script type="application/json" id="scan-data">${dataJson}</script>
2129
+ </body>
2130
+ </html>`;
2131
+ }
2132
+ function escapeHtml(text) {
2133
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2134
+ }
2135
+
2136
+ // dist/commands/map.js
2137
+ init_check_update();
2138
+ init_push_toolings();
2139
+ async function mapCommand(path, options) {
2140
+ await checkForUpdate();
2141
+ const projectRoot = resolve3(path ?? process.cwd());
2142
+ if (!existsSync7(projectRoot)) {
2143
+ console.error(chalk9.red(`Path not found: ${projectRoot}`));
2144
+ process.exit(1);
2145
+ }
2146
+ console.log(chalk9.blue(`Scanning: ${projectRoot}
2147
+ `));
2148
+ const result = await scanProject(projectRoot);
2149
+ console.log(` Files found: ${result.graph.nodes.length}`);
2150
+ console.log(` References: ${result.graph.edges.length}`);
2151
+ console.log(` Orphans: ${result.orphans.length}`);
2152
+ console.log(` Stale files: ${result.staleFiles.length}`);
2153
+ console.log(` Skills: ${result.skills.length}`);
2154
+ console.log(` Toolings: ${result.toolings.length}`);
2155
+ console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
2156
+ console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
2157
+ const outputDir = resolve3(projectRoot, "output");
2158
+ if (!existsSync7(outputDir)) {
2159
+ await mkdir2(outputDir, { recursive: true });
2160
+ }
2161
+ const htmlPath = resolve3(outputDir, "index.html");
2162
+ const html = generateOfflineHtml(result, projectRoot);
2163
+ await writeFile2(htmlPath, html, "utf-8");
2164
+ console.log(chalk9.green(`
2165
+ Local preview: ${htmlPath}`));
2166
+ if (!options.offline) {
2167
+ try {
2168
+ const { supabase } = await getAuthenticatedClient();
2169
+ const { data: devicePaths } = await supabase.from("device_paths").select("folder_id, device_name").eq("path", projectRoot);
2170
+ if (devicePaths?.length) {
2171
+ const { folder_id, device_name } = devicePaths[0];
2172
+ const { data: proposedFiles } = await supabase.from("folder_files").select("id, file_path, proposed_at").eq("folder_id", folder_id).eq("proposed_for_deletion", true);
2173
+ if (proposedFiles?.length) {
2174
+ const { checkbox } = await import("@inquirer/prompts");
2175
+ console.log(chalk9.yellow(`
2176
+ ${proposedFiles.length} file(s) proposed for deletion:
2177
+ `));
2178
+ const toDelete = await checkbox({
2179
+ message: "Select files to delete (space to toggle, enter to confirm)",
2180
+ choices: proposedFiles.map((f) => ({
2181
+ name: `${f.file_path}${f.proposed_at ? ` (proposed ${new Date(f.proposed_at).toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short", year: "numeric" })})` : ""}`,
2182
+ value: f
2183
+ }))
2184
+ });
2185
+ for (const file of toDelete) {
2186
+ const fullPath = resolve3(projectRoot, file.file_path);
2187
+ try {
2188
+ const { unlink } = await import("node:fs/promises");
2189
+ await unlink(fullPath);
2190
+ await supabase.from("folder_files").delete().eq("id", file.id);
2191
+ console.log(chalk9.green(` Deleted: ${file.file_path}`));
2192
+ } catch (err) {
2193
+ console.error(chalk9.red(` Failed to delete ${file.file_path}: ${err}`));
2194
+ }
2195
+ }
2196
+ const keptIds = proposedFiles.filter((f) => !toDelete.some((d) => d.id === f.id)).map((f) => f.id);
2197
+ if (keptIds.length > 0) {
2198
+ for (const id of keptIds) {
2199
+ await supabase.from("folder_files").update({ proposed_for_deletion: false, proposed_at: null, proposed_by: null }).eq("id", id);
2200
+ }
2201
+ console.log(chalk9.cyan(` Kept ${keptIds.length} file(s) \u2014 proposals cleared.`));
2202
+ }
2203
+ console.log("");
2204
+ }
2205
+ const { error } = await supabase.from("claude_folders").update({
2206
+ graph_json: result.graph,
2207
+ orphans_json: result.orphans,
2208
+ skills_table_json: result.skills,
2209
+ stale_files_json: result.staleFiles,
2210
+ env_manifest_json: result.envManifest,
2211
+ last_scanned: result.scannedAt,
2212
+ data_hash: result.dataHash
2213
+ }).eq("id", folder_id);
2214
+ if (error) {
2215
+ console.error(chalk9.yellow(`Sync warning: ${error.message}`));
2216
+ } else {
2217
+ await pushToolings(supabase, folder_id, result.toolings);
2218
+ await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder_id).eq("device_name", device_name);
2219
+ await saveState({
2220
+ lastFolderId: folder_id,
2221
+ lastDeviceName: device_name,
2222
+ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
2223
+ });
2224
+ console.log(chalk9.green("Synced to Supabase."));
2225
+ console.log(chalk9.cyan(`
2226
+ https://www.md4ai.com/project/${folder_id}
2227
+ `));
2228
+ const configFiles = await readClaudeConfigFiles(projectRoot);
2229
+ if (configFiles.length > 0) {
2230
+ for (const file of configFiles) {
2231
+ await supabase.from("folder_files").upsert({
2232
+ folder_id,
2233
+ file_path: file.filePath,
2234
+ content: file.content,
2235
+ size_bytes: file.sizeBytes,
2236
+ last_modified: file.lastModified
2237
+ }, { onConflict: "folder_id,file_path" });
2238
+ }
2239
+ console.log(chalk9.green(` Uploaded ${configFiles.length} config file(s).`));
2240
+ }
2241
+ }
2242
+ } else {
2243
+ console.log(chalk9.yellow("No device path matches this folder. Run: md4ai add-device\n(Local preview still generated.)"));
2244
+ }
2245
+ } catch {
2246
+ console.log(chalk9.yellow("Not logged in \u2014 local preview only."));
2247
+ }
2248
+ }
2249
+ }
2250
+
2251
+ // dist/commands/simulate.js
2252
+ init_dist();
2253
+ import { join as join9 } from "node:path";
2254
+ import { existsSync as existsSync8 } from "node:fs";
2255
+ import { homedir as homedir6 } from "node:os";
2256
+ import chalk10 from "chalk";
2257
+ async function simulateCommand(prompt) {
2258
+ const projectRoot = process.cwd();
2259
+ console.log(chalk10.blue(`Simulating prompt: "${prompt}"
2260
+ `));
2261
+ console.log(chalk10.dim("Files Claude would load:\n"));
2262
+ const globalClaude = join9(homedir6(), ".claude", "CLAUDE.md");
2263
+ if (existsSync8(globalClaude)) {
2264
+ console.log(chalk10.green(" \u2713 ~/.claude/CLAUDE.md (global)"));
2265
+ }
2266
+ for (const rootFile of ROOT_FILES) {
2267
+ const fullPath = join9(projectRoot, rootFile);
2268
+ if (existsSync8(fullPath)) {
2269
+ console.log(chalk10.green(` \u2713 ${rootFile} (project root)`));
2270
+ }
2271
+ }
2272
+ const settingsFiles = [
2273
+ join9(homedir6(), ".claude", "settings.json"),
2274
+ join9(homedir6(), ".claude", "settings.local.json"),
2275
+ join9(projectRoot, ".claude", "settings.json"),
2276
+ join9(projectRoot, ".claude", "settings.local.json")
2277
+ ];
2278
+ for (const sf of settingsFiles) {
2279
+ if (existsSync8(sf)) {
2280
+ const display = sf.startsWith(homedir6()) ? sf.replace(homedir6(), "~") : sf.replace(projectRoot + "/", "");
2281
+ console.log(chalk10.green(` \u2713 ${display} (settings)`));
2282
+ }
2283
+ }
2284
+ const words = prompt.split(/\s+/);
2285
+ for (const word of words) {
2286
+ const cleaned = word.replace(/['"]/g, "");
2287
+ const candidatePath = join9(projectRoot, cleaned);
2288
+ if (existsSync8(candidatePath) && cleaned.includes("/")) {
2289
+ console.log(chalk10.cyan(` \u2192 ${cleaned} (referenced in prompt)`));
2290
+ }
2291
+ }
2292
+ console.log(chalk10.dim("\nNote: This is an approximation. Actual file loading depends on Claude Code internals."));
2293
+ }
2294
+
2295
+ // dist/commands/print.js
2296
+ import { join as join10 } from "node:path";
2297
+ import { readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
2298
+ import { existsSync as existsSync9 } from "node:fs";
2299
+ import chalk11 from "chalk";
2300
+ async function printCommand(title) {
2301
+ const projectRoot = process.cwd();
2302
+ const scanDataPath = join10(projectRoot, "output", "index.html");
2303
+ if (!existsSync9(scanDataPath)) {
2304
+ console.error(chalk11.red("No scan data found. Run: md4ai scan"));
2305
+ process.exit(1);
2306
+ }
2307
+ const html = await readFile6(scanDataPath, "utf-8");
2308
+ const match = html.match(/<script type="application\/json" id="scan-data">([\s\S]*?)<\/script>/);
2309
+ if (!match) {
2310
+ console.error(chalk11.red("Could not extract scan data from output/index.html"));
2311
+ process.exit(1);
2312
+ }
2313
+ const result = JSON.parse(match[1]);
2314
+ const printHtml = generatePrintHtml(result, title);
2315
+ const outputPath = join10(projectRoot, "output", `print-${Date.now()}.html`);
2316
+ await writeFile3(outputPath, printHtml, "utf-8");
2317
+ console.log(chalk11.green(`Print-ready wall sheet: ${outputPath}`));
2318
+ }
2319
+ function escapeHtml2(text) {
2320
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2321
+ }
2322
+ function generatePrintHtml(result, title) {
2323
+ 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");
2324
+ const orphanItems = result.orphans.map((o) => `<li class="orphan">${escapeHtml2(o.path)}</li>`).join("\n");
2325
+ const staleItems = result.staleFiles.map((s) => `<li class="stale">${escapeHtml2(s.path)} (${s.daysSinceModified}d)</li>`).join("\n");
2326
+ return `<!DOCTYPE html>
2327
+ <html lang="en">
2328
+ <head>
2329
+ <meta charset="UTF-8">
2330
+ <title>${escapeHtml2(title)} \u2014 MD4AI Wall Sheet</title>
2331
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
2332
+ <style>
2333
+ @page { size: A3 landscape; margin: 1cm; }
2334
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 1rem; }
2335
+ h1 { font-size: 2rem; margin-bottom: 0.5rem; }
2336
+ .subtitle { color: #666; margin-bottom: 2rem; }
2337
+ .mermaid { margin: 2rem 0; }
2338
+ .columns { display: flex; gap: 2rem; }
2339
+ .column { flex: 1; }
2340
+ table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
2341
+ th, td { padding: 0.3rem 0.5rem; text-align: left; border-bottom: 1px solid #ddd; }
2342
+ th { background: #f5f5f5; }
2343
+ .orphan { color: #dc2626; }
2344
+ .stale { color: #d97706; }
2345
+ .check { color: #16a34a; font-weight: bold; }
2346
+ </style>
2347
+ </head>
2348
+ <body>
2349
+ <h1>${escapeHtml2(title)}</h1>
2350
+ <p class="subtitle">Generated: ${(/* @__PURE__ */ new Date()).toLocaleString()} | Files: ${result.graph.nodes.length} | Orphans: ${result.orphans.length}</p>
2351
+
2352
+ <pre class="mermaid">${escapeHtml2(result.graph.mermaid)}</pre>
2353
+
2354
+ <div class="columns">
2355
+ <div class="column">
2356
+ <h2>Skills Comparison</h2>
2357
+ <table>
2358
+ <tr><th>Skill</th><th>Machine</th><th>Project</th></tr>
2359
+ ${skillRows}
2360
+ </table>
2361
+ </div>
2362
+ <div class="column">
2363
+ ${result.orphans.length ? `<h2>Orphans</h2><ul>${orphanItems}</ul>` : ""}
2364
+ ${result.staleFiles.length ? `<h2>Stale Files</h2><ul>${staleItems}</ul>` : ""}
2365
+ </div>
2366
+ </div>
2367
+
2368
+ <script>mermaid.initialize({ startOnLoad: true, theme: 'default' });</script>
2369
+ </body>
2370
+ </html>`;
2371
+ }
2372
+
2373
+ // dist/index.js
2374
+ init_sync();
2375
+
2376
+ // dist/commands/link.js
2377
+ init_auth();
2378
+ init_config();
2379
+ init_scanner();
2380
+ init_push_toolings();
2381
+ init_device_utils();
2382
+ import { resolve as resolve4 } from "node:path";
2383
+ import chalk13 from "chalk";
2384
+ async function linkCommand(projectId) {
2385
+ const { supabase, userId } = await getAuthenticatedClient();
2386
+ const cwd = resolve4(process.cwd());
2387
+ const deviceName = detectDeviceName();
2388
+ const osType = detectOs2();
2389
+ const { data: folder, error: folderErr } = await supabase.from("claude_folders").select("id, name").eq("id", projectId).single();
2390
+ if (folderErr || !folder) {
2391
+ console.error(chalk13.red("Project not found, or you do not have access."));
2392
+ console.error(chalk13.yellow("Check the project ID in the MD4AI web dashboard."));
2393
+ process.exit(1);
2394
+ }
2395
+ console.log(chalk13.blue(`
2396
+ Linking "${folder.name}" to this device...
2397
+ `));
2398
+ console.log(` Project: ${folder.name}`);
2399
+ console.log(` Path: ${cwd}`);
2400
+ console.log(` Device: ${deviceName}`);
2401
+ console.log(` OS: ${osType}`);
2402
+ await supabase.from("devices").upsert({
2403
+ user_id: userId,
2404
+ device_name: deviceName,
2405
+ os_type: osType
2406
+ }, { onConflict: "user_id,device_name" });
2407
+ const { data: existing } = await supabase.from("device_paths").select("id").eq("folder_id", folder.id).eq("device_name", deviceName).maybeSingle();
2408
+ if (existing) {
2409
+ await supabase.from("device_paths").update({ path: cwd, os_type: osType, last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", existing.id);
2410
+ } else {
2411
+ const { error: pathErr } = await supabase.from("device_paths").insert({
2412
+ user_id: userId,
2413
+ folder_id: folder.id,
2414
+ device_name: deviceName,
2415
+ os_type: osType,
2416
+ path: cwd
2417
+ });
2418
+ if (pathErr) {
2419
+ console.error(chalk13.red(`Failed to link: ${pathErr.message}`));
2420
+ process.exit(1);
2421
+ }
2422
+ }
2423
+ console.log(chalk13.green("\nLinked successfully."));
2424
+ console.log(chalk13.blue("\nRunning initial scan...\n"));
2425
+ const result = await scanProject(cwd);
2426
+ console.log(` Files: ${result.graph.nodes.length}`);
2427
+ console.log(` References:${result.graph.edges.length}`);
2428
+ console.log(` Orphans: ${result.orphans.length}`);
2429
+ console.log(` Skills: ${result.skills.length}`);
2430
+ console.log(` Toolings: ${result.toolings.length}`);
2431
+ console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
2432
+ const { error: scanErr } = await supabase.from("claude_folders").update({
2433
+ graph_json: result.graph,
2434
+ orphans_json: result.orphans,
2435
+ skills_table_json: result.skills,
2436
+ stale_files_json: result.staleFiles,
2437
+ env_manifest_json: result.envManifest,
2438
+ last_scanned: result.scannedAt,
2439
+ data_hash: result.dataHash
2440
+ }).eq("id", folder.id);
2441
+ if (scanErr) {
2442
+ console.error(chalk13.yellow(`Scan upload warning: ${scanErr.message}`));
2443
+ }
2444
+ await pushToolings(supabase, folder.id, result.toolings);
2445
+ const configFiles = await readClaudeConfigFiles(cwd);
2446
+ if (configFiles.length > 0) {
2447
+ for (const file of configFiles) {
2448
+ await supabase.from("folder_files").upsert({
2449
+ folder_id: folder.id,
2450
+ file_path: file.filePath,
2451
+ content: file.content,
2452
+ size_bytes: file.sizeBytes,
2453
+ last_modified: file.lastModified
2454
+ }, { onConflict: "folder_id,file_path" });
2455
+ }
2456
+ console.log(chalk13.green(` Uploaded ${configFiles.length} config file(s).`));
2457
+ }
2458
+ await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder.id).eq("device_name", deviceName);
2459
+ await saveState({
2460
+ lastFolderId: folder.id,
2461
+ lastDeviceName: deviceName,
2462
+ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
2463
+ });
2464
+ const projectUrl = `https://www.md4ai.com/project/${folder.id}`;
2465
+ console.log(chalk13.green("\nDone! Project linked and scanned."));
2466
+ console.log(chalk13.cyan(`
2467
+ ${projectUrl}
2468
+ `));
2469
+ console.log(chalk13.grey('Run "md4ai scan" to rescan at any time.'));
2470
+ }
2471
+
2472
+ // dist/commands/import-bundle.js
2473
+ import { readFile as readFile7, writeFile as writeFile4, mkdir as mkdir3 } from "node:fs/promises";
2474
+ import { join as join11, dirname as dirname2 } from "node:path";
2475
+ import { existsSync as existsSync10 } from "node:fs";
2476
+ import chalk14 from "chalk";
2477
+ import { confirm, input as input4 } from "@inquirer/prompts";
2478
+ async function importBundleCommand(zipPath) {
2479
+ if (!existsSync10(zipPath)) {
2480
+ console.error(chalk14.red(`File not found: ${zipPath}`));
2481
+ process.exit(1);
2482
+ }
2483
+ const JSZip = (await import("jszip")).default;
2484
+ const zipData = await readFile7(zipPath);
2485
+ const zip = await JSZip.loadAsync(zipData);
2486
+ const manifestFile = zip.file("manifest.json");
2487
+ if (!manifestFile) {
2488
+ console.error(chalk14.red("Invalid bundle: missing manifest.json"));
2489
+ process.exit(1);
2490
+ }
2491
+ const manifest = JSON.parse(await manifestFile.async("string"));
2492
+ console.log(chalk14.blue(`
2493
+ Bundle: ${manifest.folderName}`));
2494
+ console.log(` Exported by: ${manifest.ownerEmail}`);
2495
+ console.log(` Exported at: ${manifest.exportedAt}`);
2496
+ console.log(` Files: ${manifest.fileCount}`);
2497
+ const files = [];
2498
+ for (const [path, file] of Object.entries(zip.files)) {
2499
+ if (path.startsWith("claude-files/") && !file.dir) {
2500
+ const relativePath = path.replace("claude-files/", "");
2501
+ const content = await file.async("string");
2502
+ files.push({ filePath: relativePath, content });
2503
+ }
2504
+ }
2505
+ if (files.length === 0) {
2506
+ console.log(chalk14.yellow("No Claude config files found in bundle."));
2507
+ return;
2508
+ }
2509
+ console.log(chalk14.blue(`
2510
+ Files to extract:`));
2511
+ for (const f of files) {
2512
+ console.log(` ${f.filePath}`);
2513
+ }
2514
+ const targetDir = await input4({
2515
+ message: "Extract to directory:",
2516
+ default: process.cwd()
2517
+ });
2518
+ const proceed = await confirm({
2519
+ message: `Extract ${files.length} file(s) to ${targetDir}?`
2520
+ });
2521
+ if (!proceed) {
2522
+ console.log(chalk14.yellow("Cancelled."));
2523
+ return;
2524
+ }
2525
+ for (const file of files) {
2526
+ const fullPath = join11(targetDir, file.filePath);
2527
+ const dir = dirname2(fullPath);
2528
+ if (!existsSync10(dir)) {
2529
+ await mkdir3(dir, { recursive: true });
2530
+ }
2531
+ await writeFile4(fullPath, file.content, "utf-8");
2532
+ console.log(chalk14.green(` \u2713 ${file.filePath}`));
2533
+ }
2534
+ console.log(chalk14.green(`
2535
+ Done! ${files.length} file(s) extracted to ${targetDir}`));
2536
+ }
2537
+
2538
+ // dist/commands/admin-update-tool.js
2539
+ init_auth();
2540
+ import chalk15 from "chalk";
2541
+ var VALID_CATEGORIES = ["framework", "runtime", "cli", "mcp", "package", "database", "other"];
2542
+ async function adminUpdateToolCommand(options) {
2543
+ const { supabase } = await getAuthenticatedClient();
2544
+ if (!options.name) {
2545
+ console.error(chalk15.red("--name is required."));
2546
+ process.exit(1);
2547
+ }
2548
+ if (options.category && !VALID_CATEGORIES.includes(options.category)) {
2549
+ console.error(chalk15.red(`Invalid category. Must be one of: ${VALID_CATEGORIES.join(", ")}`));
2550
+ process.exit(1);
2551
+ }
2552
+ const { data: existing } = await supabase.from("tools_registry").select("id, name, display_name, category").eq("name", options.name).maybeSingle();
2553
+ if (existing) {
2554
+ const updates = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
2555
+ if (options.display)
2556
+ updates.display_name = options.display;
2557
+ if (options.category)
2558
+ updates.category = options.category;
2559
+ if (options.stable)
2560
+ updates.latest_stable = options.stable;
2561
+ if (options.beta)
2562
+ updates.latest_beta = options.beta;
2563
+ if (options.source)
2564
+ updates.source_url = options.source;
2565
+ if (options.install)
2566
+ updates.install_url = options.install;
2567
+ if (options.notes)
2568
+ updates.notes = options.notes;
2569
+ const { error } = await supabase.from("tools_registry").update(updates).eq("id", existing.id);
2570
+ if (error) {
2571
+ console.error(chalk15.red(`Failed to update: ${error.message}`));
2572
+ process.exit(1);
2573
+ }
2574
+ console.log(chalk15.green(`Updated "${existing.display_name}" in the registry.`));
2575
+ } else {
2576
+ if (!options.display || !options.category) {
2577
+ console.error(chalk15.red("New tools require --display and --category."));
2578
+ process.exit(1);
2579
+ }
2580
+ const { error } = await supabase.from("tools_registry").insert({
2581
+ name: options.name,
2582
+ display_name: options.display,
2583
+ category: options.category,
2584
+ latest_stable: options.stable ?? null,
2585
+ latest_beta: options.beta ?? null,
2586
+ source_url: options.source ?? null,
2587
+ install_url: options.install ?? null,
2588
+ notes: options.notes ?? null
2589
+ });
2590
+ if (error) {
2591
+ console.error(chalk15.red(`Failed to create: ${error.message}`));
2592
+ process.exit(1);
2593
+ }
2594
+ console.log(chalk15.green(`Added "${options.display}" to the registry.`));
2595
+ }
2596
+ }
2597
+
2598
+ // dist/commands/admin-list-tools.js
2599
+ init_auth();
2600
+ import chalk16 from "chalk";
2601
+ async function adminListToolsCommand() {
2602
+ const { supabase } = await getAuthenticatedClient();
2603
+ const { data: tools, error } = await supabase.from("tools_registry").select("*").order("category").order("display_name");
2604
+ if (error) {
2605
+ console.error(chalk16.red(`Failed to fetch tools: ${error.message}`));
2606
+ process.exit(1);
2607
+ }
2608
+ if (!tools?.length) {
2609
+ console.log(chalk16.yellow("No tools in the registry."));
2610
+ return;
2611
+ }
2612
+ const nameW = Math.max(16, ...tools.map((t) => t.display_name.length));
2613
+ const catW = Math.max(10, ...tools.map((t) => t.category.length));
2614
+ const stableW = Math.max(8, ...tools.map((t) => (t.latest_stable ?? "\u2014").length));
2615
+ const betaW = Math.max(8, ...tools.map((t) => (t.latest_beta ?? "\u2014").length));
2616
+ const header = [
2617
+ "Name".padEnd(nameW),
2618
+ "Category".padEnd(catW),
2619
+ "Stable".padEnd(stableW),
2620
+ "Beta".padEnd(betaW),
2621
+ "Last Checked"
2622
+ ].join(" ");
2623
+ console.log(chalk16.bold(header));
2624
+ console.log("\u2500".repeat(header.length));
2625
+ for (const tool of tools) {
2626
+ const lastChecked = tool.updated_at ? formatRelative(new Date(tool.updated_at)) : "\u2014";
2627
+ const row = [
2628
+ tool.display_name.padEnd(nameW),
2629
+ tool.category.padEnd(catW),
2630
+ (tool.latest_stable ?? "\u2014").padEnd(stableW),
2631
+ (tool.latest_beta ?? "\u2014").padEnd(betaW),
2632
+ lastChecked
2633
+ ].join(" ");
2634
+ console.log(row);
2635
+ }
2636
+ console.log(chalk16.grey(`
2637
+ ${tools.length} tool(s) in registry.`));
2638
+ }
2639
+ function formatRelative(date) {
2640
+ const diff = Date.now() - date.getTime();
2641
+ const hours = Math.floor(diff / (1e3 * 60 * 60));
2642
+ if (hours < 1)
2643
+ return "just now";
2644
+ if (hours < 24)
2645
+ return `${hours}h ago`;
2646
+ const days = Math.floor(hours / 24);
2647
+ if (days < 7)
2648
+ return `${days}d ago`;
2649
+ return date.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
2650
+ }
2651
+
2652
+ // dist/commands/admin-fetch-versions.js
2653
+ init_auth();
2654
+ import chalk17 from "chalk";
2655
+
2656
+ // dist/commands/version-sources.js
2657
+ var VERSION_SOURCES = {
2658
+ "next": { type: "npm", package: "next" },
2659
+ "react": { type: "npm", package: "react" },
2660
+ "react-dom": { type: "npm", package: "react-dom" },
2661
+ "typescript": { type: "npm", package: "typescript" },
2662
+ "tailwindcss": { type: "npm", package: "tailwindcss" },
2663
+ "esbuild": { type: "npm", package: "esbuild" },
2664
+ "chalk": { type: "npm", package: "chalk" },
2665
+ "commander": { type: "npm", package: "commander" },
2666
+ "inquirer": { type: "npm", package: "inquirer" },
2667
+ "playwright": { type: "npm", package: "playwright" },
2668
+ "resend": { type: "npm", package: "resend" },
2669
+ "@supabase/supabase-js": { type: "npm", package: "@supabase/supabase-js" },
2670
+ "pnpm": { type: "npm", package: "pnpm" },
2671
+ "claude-code": { type: "npm", package: "@anthropic-ai/claude-code" },
2672
+ "turborepo": { type: "npm", package: "turbo" },
2673
+ "npm": { type: "npm", package: "npm" },
2674
+ "node": { type: "github", repo: "nodejs/node" },
2675
+ "supabase-cli": { type: "npm", package: "supabase" }
2676
+ };
2677
+
2678
+ // dist/commands/admin-fetch-versions.js
2679
+ async function adminFetchVersionsCommand() {
2680
+ const { supabase } = await getAuthenticatedClient();
2681
+ const { data: tools, error } = await supabase.from("tools_registry").select("*").order("display_name");
2682
+ if (error) {
2683
+ console.error(chalk17.red(`Failed to fetch tools: ${error.message}`));
2684
+ process.exit(1);
2685
+ }
2686
+ if (!tools?.length) {
2687
+ console.log(chalk17.yellow("No tools in the registry."));
2688
+ }
2689
+ const { data: allProjectToolings } = await supabase.from("project_toolings").select("tool_name, detection_source").is("tool_id", null);
2690
+ const registeredNames = new Set((tools ?? []).map((t) => t.name));
2691
+ const unregisteredNames = /* @__PURE__ */ new Set();
2692
+ for (const pt of allProjectToolings ?? []) {
2693
+ if (!registeredNames.has(pt.tool_name) && pt.detection_source === "package.json") {
2694
+ unregisteredNames.add(pt.tool_name);
2695
+ }
2696
+ }
2697
+ if (unregisteredNames.size > 0) {
2698
+ console.log(chalk17.blue(`Auto-registering ${unregisteredNames.size} unverified package(s)...
2699
+ `));
2700
+ for (const name of unregisteredNames) {
2701
+ const displayName = name;
2702
+ const { data: inserted, error: insertError } = await supabase.from("tools_registry").upsert({
2703
+ name,
2704
+ display_name: displayName,
2705
+ category: "package",
2706
+ source_url: `https://www.npmjs.com/package/${name}`,
2707
+ install_url: `https://www.npmjs.com/package/${name}`
2708
+ }, { onConflict: "name" }).select().single();
2709
+ if (!insertError && inserted) {
2710
+ tools.push(inserted);
2711
+ await supabase.from("project_toolings").update({ tool_id: inserted.id }).eq("tool_name", name).is("tool_id", null);
2712
+ }
2554
2713
  }
2555
2714
  }
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("");
2715
+ if (!tools?.length) {
2716
+ console.log(chalk17.yellow("No tools to fetch."));
2717
+ return;
2565
2718
  }
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"));
2719
+ console.log(chalk17.bold(`Fetching versions for ${tools.length} tool(s)...
2720
+ `));
2721
+ const results = await Promise.all(tools.map(async (tool) => {
2722
+ const source = VERSION_SOURCES[tool.name] ?? { type: "npm", package: tool.name };
2723
+ try {
2724
+ const { stable, beta } = await fetchVersions(source);
2725
+ const stableChanged = stable !== tool.latest_stable;
2726
+ const betaChanged = beta !== tool.latest_beta;
2727
+ const updates = {
2728
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
2729
+ };
2730
+ if (stableChanged)
2731
+ updates.latest_stable = stable;
2732
+ if (betaChanged)
2733
+ updates.latest_beta = beta;
2734
+ const { error: updateError } = await supabase.from("tools_registry").update(updates).eq("id", tool.id);
2735
+ if (updateError) {
2736
+ return { displayName: tool.display_name, stable, beta, status: "failed" };
2737
+ }
2738
+ return {
2739
+ displayName: tool.display_name,
2740
+ stable,
2741
+ beta,
2742
+ status: stableChanged || betaChanged ? "updated" : "unchanged"
2743
+ };
2744
+ } catch {
2745
+ return { displayName: tool.display_name, stable: null, beta: null, status: "failed" };
2746
+ }
2747
+ }));
2748
+ const nameW = Math.max(16, ...results.map((r) => r.displayName.length));
2749
+ const stableW = Math.max(8, ...results.map((r) => (r.stable ?? "\u2014").length));
2750
+ const betaW = Math.max(8, ...results.map((r) => (r.beta ?? "\u2014").length));
2751
+ const statusW = 10;
2752
+ const header = [
2753
+ "Tool".padEnd(nameW),
2754
+ "Stable".padEnd(stableW),
2755
+ "Beta".padEnd(betaW),
2756
+ "Status".padEnd(statusW)
2757
+ ].join(" ");
2758
+ console.log(chalk17.bold(header));
2759
+ console.log("\u2500".repeat(header.length));
2760
+ for (const result of results) {
2761
+ const statusColour = result.status === "updated" ? chalk17.green : result.status === "unchanged" ? chalk17.grey : result.status === "failed" ? chalk17.red : chalk17.yellow;
2762
+ const row = [
2763
+ result.displayName.padEnd(nameW),
2764
+ (result.stable ?? "\u2014").padEnd(stableW),
2765
+ (result.beta ?? "\u2014").padEnd(betaW),
2766
+ statusColour(result.status.padEnd(statusW))
2767
+ ].join(" ");
2768
+ console.log(row);
2569
2769
  }
2770
+ const updated = results.filter((r) => r.status === "updated").length;
2771
+ const unchanged = results.filter((r) => r.status === "unchanged").length;
2772
+ const failed = results.filter((r) => r.status === "failed").length;
2773
+ const noSource = results.filter((r) => r.status === "no source").length;
2774
+ console.log(chalk17.grey(`
2775
+ ${results.length} tool(s): `) + chalk17.green(`${updated} updated`) + ", " + chalk17.grey(`${unchanged} unchanged`) + ", " + chalk17.red(`${failed} failed`) + ", " + chalk17.yellow(`${noSource} no source`));
2570
2776
  }
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
- })));
2777
+ async function fetchVersions(source) {
2778
+ const controller = new AbortController();
2779
+ const timeout = setTimeout(() => controller.abort(), 3e3);
2780
+ try {
2781
+ if (source.type === "npm") {
2782
+ return await fetchNpmVersions(source.package, controller.signal);
2783
+ } else {
2784
+ return await fetchGitHubVersions(source.repo, controller.signal);
2608
2785
  }
2609
- await supabase.from("mcp_watchers").update({ last_heartbeat: now }).eq("device_id", deviceId).eq("pid", myPid);
2610
- printTable(rows, deviceName);
2786
+ } finally {
2787
+ clearTimeout(timeout);
2611
2788
  }
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);
2789
+ }
2790
+ async function fetchNpmVersions(packageName, signal) {
2791
+ const url = `https://registry.npmjs.org/-/package/${encodeURIComponent(packageName)}/dist-tags`;
2792
+ const res = await fetch(url, { signal });
2793
+ if (!res.ok) {
2794
+ throw new Error(`npm registry returned ${res.status}`);
2629
2795
  }
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();
2796
+ const tags = await res.json();
2797
+ const stable = tags.latest ?? null;
2798
+ const beta = tags.next ?? tags.beta ?? tags.rc ?? null;
2799
+ return { stable, beta };
2800
+ }
2801
+ async function fetchGitHubVersions(repo, signal) {
2802
+ const url = `https://api.github.com/repos/${repo}/releases?per_page=20`;
2803
+ const res = await fetch(url, {
2804
+ signal,
2805
+ headers: { Accept: "application/vnd.github+json" }
2645
2806
  });
2807
+ if (!res.ok) {
2808
+ throw new Error(`GitHub API returned ${res.status}`);
2809
+ }
2810
+ const releases = await res.json();
2811
+ let stable = null;
2812
+ let beta = null;
2813
+ for (const release of releases) {
2814
+ if (release.draft)
2815
+ continue;
2816
+ const version = release.tag_name.replace(/^v/, "");
2817
+ if (!release.prerelease && !stable) {
2818
+ stable = version;
2819
+ }
2820
+ if (release.prerelease && !beta) {
2821
+ beta = version;
2822
+ }
2823
+ if (stable && beta)
2824
+ break;
2825
+ }
2826
+ return { stable, beta };
2646
2827
  }
2647
2828
 
2829
+ // dist/index.js
2830
+ init_mcp_watch();
2831
+
2648
2832
  // dist/commands/init-manifest.js
2649
2833
  import { resolve as resolve5, join as join13, relative as relative3, dirname as dirname3 } from "node:path";
2650
2834
  import { readFile as readFile9, writeFile as writeFile5, mkdir as mkdir4 } from "node:fs/promises";
@@ -2770,14 +2954,132 @@ Manifest created: ${relative3(projectRoot, manifestPath)}`));
2770
2954
  console.log(chalk19.dim("Then run `md4ai scan` to verify against your environments."));
2771
2955
  }
2772
2956
 
2957
+ // dist/commands/update.js
2958
+ init_check_update();
2959
+ init_config();
2960
+ import chalk20 from "chalk";
2961
+ import { execFileSync as execFileSync6, spawn } from "node:child_process";
2962
+ async function fetchLatestVersion() {
2963
+ try {
2964
+ const controller = new AbortController();
2965
+ const timeout = setTimeout(() => controller.abort(), 5e3);
2966
+ const res = await fetch("https://registry.npmjs.org/md4ai/latest", {
2967
+ signal: controller.signal
2968
+ });
2969
+ clearTimeout(timeout);
2970
+ if (!res.ok)
2971
+ return null;
2972
+ const data = await res.json();
2973
+ return data.version ?? null;
2974
+ } catch {
2975
+ return null;
2976
+ }
2977
+ }
2978
+ function isNewer2(a, b) {
2979
+ const pa = a.split(".").map(Number);
2980
+ const pb = b.split(".").map(Number);
2981
+ for (let i = 0; i < 3; i++) {
2982
+ if ((pa[i] ?? 0) > (pb[i] ?? 0))
2983
+ return true;
2984
+ if ((pa[i] ?? 0) < (pb[i] ?? 0))
2985
+ return false;
2986
+ }
2987
+ return false;
2988
+ }
2989
+ function runNpmInstall() {
2990
+ try {
2991
+ execFileSync6("npm", ["install", "-g", "md4ai"], {
2992
+ stdio: "inherit",
2993
+ timeout: 6e4
2994
+ });
2995
+ return true;
2996
+ } catch {
2997
+ return false;
2998
+ }
2999
+ }
3000
+ function spawnPostUpdate() {
3001
+ const child = spawn("md4ai", ["update", "--post-update"], {
3002
+ stdio: "inherit",
3003
+ shell: false
3004
+ });
3005
+ child.on("exit", (code) => process.exit(code ?? 0));
3006
+ }
3007
+ async function postUpdateFlow() {
3008
+ const { confirm: confirm2 } = await import("@inquirer/prompts");
3009
+ console.log(chalk20.green(`
3010
+ md4ai v${CURRENT_VERSION} \u2014 you're on the latest version.
3011
+ `));
3012
+ const state = await loadState();
3013
+ const hasProject = !!state.lastFolderId;
3014
+ if (hasProject) {
3015
+ const wantScan = await confirm2({
3016
+ message: "Rescan your project and refresh the dashboard?",
3017
+ default: true
3018
+ });
3019
+ if (wantScan) {
3020
+ console.log("");
3021
+ const { syncCommand: syncCommand2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
3022
+ await syncCommand2({ all: false });
3023
+ console.log("");
3024
+ }
3025
+ }
3026
+ const wantWatch = await confirm2({
3027
+ message: "Start monitoring MCP servers? (runs until you press Ctrl+C)",
3028
+ default: true
3029
+ });
3030
+ if (wantWatch) {
3031
+ console.log("");
3032
+ const { mcpWatchCommand: mcpWatchCommand2 } = await Promise.resolve().then(() => (init_mcp_watch(), mcp_watch_exports));
3033
+ await mcpWatchCommand2();
3034
+ } else {
3035
+ console.log(chalk20.dim("\nYou can start monitoring later with: md4ai mcp-watch\n"));
3036
+ }
3037
+ }
3038
+ async function updateCommand(options) {
3039
+ if (options.postUpdate) {
3040
+ await postUpdateFlow();
3041
+ return;
3042
+ }
3043
+ console.log(chalk20.blue(`
3044
+ Current version: v${CURRENT_VERSION}`));
3045
+ console.log(chalk20.dim(" Checking for updates...\n"));
3046
+ const latest = await fetchLatestVersion();
3047
+ if (!latest) {
3048
+ console.log(chalk20.yellow(" Could not reach the npm registry. Check your internet connection."));
3049
+ process.exit(1);
3050
+ }
3051
+ if (!isNewer2(latest, CURRENT_VERSION)) {
3052
+ await postUpdateFlow();
3053
+ return;
3054
+ }
3055
+ console.log(chalk20.white(" Update available: ") + chalk20.dim(`v${CURRENT_VERSION}`) + chalk20.white(" \u2192 ") + chalk20.green.bold(`v${latest}
3056
+ `));
3057
+ const { confirm: confirm2 } = await import("@inquirer/prompts");
3058
+ const wantUpdate = await confirm2({
3059
+ message: `Install md4ai v${latest}?`,
3060
+ default: true
3061
+ });
3062
+ if (!wantUpdate) {
3063
+ console.log(chalk20.dim("\nUpdate skipped.\n"));
3064
+ return;
3065
+ }
3066
+ console.log(chalk20.blue("\n Installing...\n"));
3067
+ const ok = runNpmInstall();
3068
+ if (!ok) {
3069
+ console.log(chalk20.red("\n npm install failed. Try running manually:"));
3070
+ console.log(chalk20.cyan(" npm install -g md4ai\n"));
3071
+ process.exit(1);
3072
+ }
3073
+ console.log(chalk20.green("\n Updated successfully.\n"));
3074
+ spawnPostUpdate();
3075
+ }
3076
+
2773
3077
  // dist/index.js
3078
+ init_check_update();
2774
3079
  var program = new Command();
2775
3080
  program.name("md4ai").description("MD4AI \u2014 Claude tooling visualiser").version(CURRENT_VERSION).addHelpText("after", `
2776
3081
  Update to the latest version:
2777
- npm install -g md4ai
2778
-
2779
- Check for updates:
2780
- md4ai check-update`);
3082
+ md4ai update`);
2781
3083
  program.command("login").description("Log in to MD4AI with email and password").action(loginCommand);
2782
3084
  program.command("logout").description("Log out and clear stored credentials").action(logoutCommand);
2783
3085
  program.command("status").description("Show login status, device count, folder count, last sync").action(statusCommand);
@@ -2790,6 +3092,7 @@ program.command("simulate <prompt>").description("Show which files Claude would
2790
3092
  program.command("print <title>").description("Generate a printable wall-chart HTML from the last scan").action(printCommand);
2791
3093
  program.command("sync").description("Re-push latest scan data to Supabase").option("--all", "Sync all folders on all devices").action(syncCommand);
2792
3094
  program.command("import <zipfile>").description("Import a Claude setup bundle exported from the web dashboard").action(importBundleCommand);
3095
+ 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
3096
  program.command("check-update").description("Check if a newer version of md4ai is available").action(checkForUpdate);
2794
3097
  program.command("mcp-watch").description("Monitor MCP server status on this device (runs until Ctrl+C)").action(mcpWatchCommand);
2795
3098
  program.command("init-manifest").description("Scaffold a starter env-manifest.md from detected .env files").action(initManifestCommand);