md4ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1353 @@
1
+ #!/usr/bin/env node
2
+
3
+ // dist/index.js
4
+ import { Command } from "commander";
5
+
6
+ // ../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";
22
+
23
+ // ../packages/shared/dist/supabase.js
24
+ import { createClient } from "@supabase/supabase-js";
25
+ function createSupabaseClient(anonKey, accessToken) {
26
+ const options = accessToken ? { global: { headers: { Authorization: `Bearer ${accessToken}` } } } : {};
27
+ return createClient(SUPABASE_URL, anonKey, options);
28
+ }
29
+
30
+ // dist/config.js
31
+ import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
32
+ import { join } from "node:path";
33
+ import { homedir } from "node:os";
34
+ 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
+ async function ensureConfigDir() {
39
+ if (!existsSync(configPath)) {
40
+ await mkdir(configPath, { recursive: true });
41
+ await chmod(configPath, 448);
42
+ }
43
+ }
44
+ async function saveCredentials(creds) {
45
+ await ensureConfigDir();
46
+ await writeFile(credentialsPath, JSON.stringify(creds, null, 2), "utf-8");
47
+ await chmod(credentialsPath, 384);
48
+ }
49
+ async function loadCredentials() {
50
+ try {
51
+ const data = await readFile(credentialsPath, "utf-8");
52
+ const parsed = JSON.parse(data);
53
+ if (!parsed.accessToken)
54
+ return null;
55
+ return parsed;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+ async function clearCredentials() {
61
+ try {
62
+ await writeFile(credentialsPath, "{}", "utf-8");
63
+ } catch {
64
+ }
65
+ }
66
+ async function saveState(state) {
67
+ await ensureConfigDir();
68
+ const existing = await loadState();
69
+ const merged = { ...existing, ...state };
70
+ await writeFile(statePath, JSON.stringify(merged, null, 2), "utf-8");
71
+ }
72
+ async function loadState() {
73
+ try {
74
+ const data = await readFile(statePath, "utf-8");
75
+ return JSON.parse(data);
76
+ } catch {
77
+ return { lastFolderId: null, lastDeviceName: null, lastSyncAt: null };
78
+ }
79
+ }
80
+ function getAnonKey() {
81
+ const key = process.env.MD4AI_SUPABASE_ANON_KEY;
82
+ if (!key) {
83
+ throw new Error("MD4AI_SUPABASE_ANON_KEY environment variable not set.\nSet it in your shell profile or .env file.");
84
+ }
85
+ return key;
86
+ }
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)"));
145
+ }
146
+ }
147
+
148
+ // dist/auth.js
149
+ import chalk4 from "chalk";
150
+ async function getAuthenticatedClient() {
151
+ const creds = await loadCredentials();
152
+ if (!creds?.accessToken) {
153
+ console.error(chalk4.red("Not logged in. Run: md4ai login"));
154
+ process.exit(1);
155
+ }
156
+ if (Date.now() > creds.expiresAt) {
157
+ console.error(chalk4.red("Session expired. Run: md4ai login"));
158
+ process.exit(1);
159
+ }
160
+ const anonKey = getAnonKey();
161
+ const supabase = createSupabaseClient(anonKey, creds.accessToken);
162
+ return { supabase, userId: creds.userId };
163
+ }
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
+ }
316
+ }
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 existsSync5 } from "node:fs";
323
+ import chalk8 from "chalk";
324
+
325
+ // dist/scanner/index.js
326
+ import { readdir } from "node:fs/promises";
327
+ import { join as join6, relative } from "node:path";
328
+ import { existsSync as existsSync4 } from "node:fs";
329
+ import { homedir as homedir4 } from "node:os";
330
+ import { createHash } from "node:crypto";
331
+
332
+ // dist/scanner/file-parser.js
333
+ import { readFile as readFile2 } from "node:fs/promises";
334
+ import { existsSync as existsSync2 } from "node:fs";
335
+ import { resolve as resolve2, dirname, join as join2 } from "node:path";
336
+ import { homedir as homedir2 } from "node:os";
337
+ var FILE_EXT_PATTERN = "md|json|ya?ml|ts|js|sh|py|bash";
338
+ async function parseFileReferences(filePath, projectRoot) {
339
+ const refs = [];
340
+ const content = await readFile2(filePath, "utf-8");
341
+ const relPath = filePath.startsWith(projectRoot) ? filePath.slice(projectRoot.length + 1) : filePath;
342
+ const markdownLinkPattern = /\[.*?\]\(([^)]+)\)/g;
343
+ const bareFilePattern = new RegExp(`(?:^|\\s)((?:\\.\\.?\\/)?[\\w./-]+\\.(?:${FILE_EXT_PATTERN}))\\b`, "gm");
344
+ const claudeDirQuotedPattern = new RegExp(`"\\$CLAUDE_PROJECT_DIR"\\/([\\w./-]+\\.(?:${FILE_EXT_PATTERN}))`, "g");
345
+ const claudeDirUnquotedPattern = new RegExp(`\\$CLAUDE_PROJECT_DIR\\/([\\w./-]+\\.(?:${FILE_EXT_PATTERN}))`, "g");
346
+ const patterns = [
347
+ markdownLinkPattern,
348
+ bareFilePattern,
349
+ claudeDirQuotedPattern,
350
+ claudeDirUnquotedPattern
351
+ ];
352
+ for (const pattern of patterns) {
353
+ let match;
354
+ while ((match = pattern.exec(content)) !== null) {
355
+ const target = match[1];
356
+ if (!target || target.startsWith("http") || target.startsWith("#"))
357
+ continue;
358
+ const resolved = resolveTarget(target, filePath, projectRoot);
359
+ if (resolved && existsSync2(resolved)) {
360
+ addRef(refs, relPath, resolved, projectRoot);
361
+ }
362
+ }
363
+ }
364
+ if (filePath.endsWith(".json")) {
365
+ parseJsonPathReferences(content, relPath, projectRoot, refs);
366
+ }
367
+ const seen = /* @__PURE__ */ new Set();
368
+ return refs.filter((r) => {
369
+ const key = `${r.from}->${r.to}`;
370
+ if (seen.has(key))
371
+ return false;
372
+ seen.add(key);
373
+ return true;
374
+ });
375
+ }
376
+ function resolveTarget(target, sourceFilePath, projectRoot) {
377
+ if (target.startsWith("~")) {
378
+ return target.replace("~", homedir2());
379
+ }
380
+ if (target.startsWith("/")) {
381
+ return target;
382
+ }
383
+ if (target.startsWith(".claude/") || target.startsWith("docs/")) {
384
+ return join2(projectRoot, target);
385
+ }
386
+ return resolve2(dirname(sourceFilePath), target);
387
+ }
388
+ function addRef(refs, fromRel, resolvedAbsolute, projectRoot) {
389
+ const targetRel = resolvedAbsolute.startsWith(projectRoot) ? resolvedAbsolute.slice(projectRoot.length + 1) : resolvedAbsolute;
390
+ if (targetRel !== fromRel) {
391
+ refs.push({ from: fromRel, to: targetRel });
392
+ }
393
+ }
394
+ function parseJsonPathReferences(content, fromRel, projectRoot, refs) {
395
+ let parsed;
396
+ try {
397
+ parsed = JSON.parse(content);
398
+ } catch {
399
+ return;
400
+ }
401
+ walkJsonValues(parsed, (value) => {
402
+ const quotedPattern = new RegExp(`"\\$CLAUDE_PROJECT_DIR"\\/([\\w./-]+\\.(?:${FILE_EXT_PATTERN}))`, "g");
403
+ const unquotedPattern = new RegExp(`\\$CLAUDE_PROJECT_DIR\\/([\\w./-]+\\.(?:${FILE_EXT_PATTERN}))`, "g");
404
+ for (const pattern of [quotedPattern, unquotedPattern]) {
405
+ let match;
406
+ while ((match = pattern.exec(value)) !== null) {
407
+ const relTarget = match[1];
408
+ const resolved = join2(projectRoot, relTarget);
409
+ if (existsSync2(resolved)) {
410
+ addRef(refs, fromRel, resolved, projectRoot);
411
+ }
412
+ }
413
+ }
414
+ const barePattern = new RegExp(`(?:^|[\\s":/])((?:\\.?\\.?\\/)?[\\w./-]+\\.(?:${FILE_EXT_PATTERN}))(?:[\\s"']|$)`, "g");
415
+ let bareMatch;
416
+ while ((bareMatch = barePattern.exec(value)) !== null) {
417
+ const target = bareMatch[1];
418
+ if (!target || target.startsWith("http"))
419
+ continue;
420
+ const resolved = join2(projectRoot, target);
421
+ if (existsSync2(resolved)) {
422
+ addRef(refs, fromRel, resolved, projectRoot);
423
+ }
424
+ }
425
+ });
426
+ }
427
+ function walkJsonValues(obj, callback) {
428
+ if (typeof obj === "string") {
429
+ callback(obj);
430
+ } else if (Array.isArray(obj)) {
431
+ for (const item of obj) {
432
+ walkJsonValues(item, callback);
433
+ }
434
+ } else if (obj !== null && typeof obj === "object") {
435
+ for (const value of Object.values(obj)) {
436
+ walkJsonValues(value, callback);
437
+ }
438
+ }
439
+ }
440
+ function classifyFileType(filePath) {
441
+ const lower = filePath.toLowerCase();
442
+ if (lower === "claude.md" || lower.endsWith("/claude.md"))
443
+ return "root";
444
+ if (lower.includes("settings.json") || lower.includes("settings.local.json"))
445
+ return "config";
446
+ if (lower.includes("skill") || lower.includes("plugin"))
447
+ return "skill";
448
+ if (lower.includes("memory") || lower.includes("memory.md"))
449
+ return "memory";
450
+ if (lower.includes("plan") || lower.includes("docs/plans"))
451
+ return "plan";
452
+ return "other";
453
+ }
454
+
455
+ // dist/scanner/git-dates.js
456
+ import { execFileSync } from "node:child_process";
457
+ import { statSync } from "node:fs";
458
+ function getGitLastModified(filePath, cwd) {
459
+ try {
460
+ const result = execFileSync("git", ["log", "-1", "--format=%cI", "--", filePath], { cwd, encoding: "utf-8", timeout: 5e3 }).trim();
461
+ return result || null;
462
+ } catch {
463
+ try {
464
+ return statSync(filePath).mtime.toISOString();
465
+ } catch {
466
+ return null;
467
+ }
468
+ }
469
+ }
470
+ function getGitCreationDate(filePath, cwd) {
471
+ try {
472
+ const result = execFileSync("git", ["log", "--diff-filter=A", "--format=%cI", "--", filePath], { cwd, encoding: "utf-8", timeout: 5e3 }).trim();
473
+ return result || null;
474
+ } catch {
475
+ try {
476
+ return statSync(filePath).birthtime.toISOString();
477
+ } catch {
478
+ return null;
479
+ }
480
+ }
481
+ }
482
+
483
+ // dist/scanner/graph-builder.js
484
+ import { statSync as statSync2 } from "node:fs";
485
+ import { join as join3 } from "node:path";
486
+ function buildGraph(files, references, projectRoot) {
487
+ const nodeMap = /* @__PURE__ */ new Map();
488
+ for (const file of files) {
489
+ const fullPath = file.startsWith("/") ? file : join3(projectRoot, file);
490
+ let sizeBytes = 0;
491
+ try {
492
+ sizeBytes = statSync2(fullPath).size;
493
+ } catch {
494
+ }
495
+ nodeMap.set(file, {
496
+ id: sanitiseId(file),
497
+ label: file.split("/").pop() ?? file,
498
+ filePath: file,
499
+ type: classifyFileType(file),
500
+ lastModified: getGitLastModified(fullPath, projectRoot),
501
+ createdAt: getGitCreationDate(fullPath, projectRoot),
502
+ sizeBytes
503
+ });
504
+ }
505
+ for (const ref of references) {
506
+ if (!nodeMap.has(ref.to)) {
507
+ const fullPath = ref.to.startsWith("/") ? ref.to : join3(projectRoot, ref.to);
508
+ let sizeBytes = 0;
509
+ try {
510
+ sizeBytes = statSync2(fullPath).size;
511
+ } catch {
512
+ }
513
+ nodeMap.set(ref.to, {
514
+ id: sanitiseId(ref.to),
515
+ label: ref.to.split("/").pop() ?? ref.to,
516
+ filePath: ref.to,
517
+ type: classifyFileType(ref.to),
518
+ lastModified: getGitLastModified(fullPath, projectRoot),
519
+ createdAt: getGitCreationDate(fullPath, projectRoot),
520
+ sizeBytes
521
+ });
522
+ }
523
+ }
524
+ const nodes = Array.from(nodeMap.values());
525
+ const edges = references.map((ref) => ({
526
+ from: sanitiseId(ref.from),
527
+ to: sanitiseId(ref.to),
528
+ label: ref.label
529
+ }));
530
+ const mermaidLines = ["graph TD"];
531
+ for (const node of nodes) {
532
+ const escapedLabel = node.label.replace(/"/g, "#quot;");
533
+ const shape = node.type === "root" ? `[["${escapedLabel}"]]` : node.type === "config" ? `[("${escapedLabel}")]` : `["${escapedLabel}"]`;
534
+ mermaidLines.push(` ${node.id}${shape}`);
535
+ }
536
+ for (const edge of edges) {
537
+ const label = edge.label ? `|${edge.label}|` : "";
538
+ mermaidLines.push(` ${edge.from} -->${label} ${edge.to}`);
539
+ }
540
+ const rootNodes = nodes.filter((n) => n.type === "root");
541
+ if (rootNodes.length) {
542
+ for (const rn of rootNodes) {
543
+ mermaidLines.push(` style ${rn.id} fill:#f9f,stroke:#333`);
544
+ }
545
+ }
546
+ return {
547
+ mermaid: mermaidLines.join("\n"),
548
+ nodes,
549
+ edges
550
+ };
551
+ }
552
+ function sanitiseId(path) {
553
+ return path.replace(/[^a-zA-Z0-9]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "").slice(0, 64);
554
+ }
555
+
556
+ // dist/scanner/orphan-detector.js
557
+ import { statSync as statSync3 } from "node:fs";
558
+ import { join as join4 } from "node:path";
559
+ function detectOrphans(allFiles, references, rootFiles, projectRoot) {
560
+ const reachable = new Set(rootFiles);
561
+ const refMap = /* @__PURE__ */ new Map();
562
+ for (const ref of references) {
563
+ const targets = refMap.get(ref.from) ?? [];
564
+ targets.push(ref.to);
565
+ refMap.set(ref.from, targets);
566
+ }
567
+ const queue = [...rootFiles];
568
+ while (queue.length > 0) {
569
+ const current = queue.pop();
570
+ const targets = refMap.get(current) ?? [];
571
+ for (const target of targets) {
572
+ if (!reachable.has(target)) {
573
+ reachable.add(target);
574
+ queue.push(target);
575
+ }
576
+ }
577
+ }
578
+ return allFiles.filter((f) => !reachable.has(f)).map((f) => {
579
+ const fullPath = join4(projectRoot, f);
580
+ let sizeBytes = 0;
581
+ try {
582
+ sizeBytes = statSync3(fullPath).size;
583
+ } catch {
584
+ }
585
+ return {
586
+ path: f,
587
+ lastModified: getGitLastModified(fullPath, projectRoot),
588
+ sizeBytes
589
+ };
590
+ });
591
+ }
592
+
593
+ // dist/scanner/skills-parser.js
594
+ import { execFileSync as execFileSync2 } from "node:child_process";
595
+ import { readFile as readFile3 } from "node:fs/promises";
596
+ import { existsSync as existsSync3 } from "node:fs";
597
+ import { join as join5 } from "node:path";
598
+ import { homedir as homedir3 } from "node:os";
599
+ async function parseSkills(projectRoot) {
600
+ const skills = /* @__PURE__ */ new Map();
601
+ try {
602
+ const output = execFileSync2("claude", ["plugin", "list"], { encoding: "utf-8", timeout: 1e4 }).trim();
603
+ if (output) {
604
+ for (const line of output.split("\n")) {
605
+ const name = line.trim().split(/\s+/)[0];
606
+ if (name) {
607
+ skills.set(name, {
608
+ name,
609
+ source: "machine",
610
+ machineWide: true,
611
+ projectSpecific: false
612
+ });
613
+ }
614
+ }
615
+ }
616
+ } catch {
617
+ }
618
+ const globalSettingsPath = join5(homedir3(), ".claude", "settings.json");
619
+ await parseSettingsForPlugins(globalSettingsPath, skills, true);
620
+ const projectSettings = join5(projectRoot, ".claude", "settings.json");
621
+ const projectLocalSettings = join5(projectRoot, ".claude", "settings.local.json");
622
+ await parseSettingsForPlugins(projectSettings, skills, false);
623
+ await parseSettingsForPlugins(projectLocalSettings, skills, false);
624
+ const skillsMd = join5(projectRoot, "skills.md");
625
+ if (existsSync3(skillsMd)) {
626
+ const content = await readFile3(skillsMd, "utf-8");
627
+ const skillNames = content.match(/^#+\s+(.+)$/gm);
628
+ if (skillNames) {
629
+ for (const match of skillNames) {
630
+ const name = match.replace(/^#+\s+/, "").trim();
631
+ const existing = skills.get(name);
632
+ if (existing) {
633
+ existing.projectSpecific = true;
634
+ } else {
635
+ skills.set(name, {
636
+ name,
637
+ source: "skills.md",
638
+ machineWide: false,
639
+ projectSpecific: true
640
+ });
641
+ }
642
+ }
643
+ }
644
+ }
645
+ return Array.from(skills.values());
646
+ }
647
+ async function parseSettingsForPlugins(settingsPath, skills, isMachineWide) {
648
+ if (!existsSync3(settingsPath))
649
+ return;
650
+ try {
651
+ const content = await readFile3(settingsPath, "utf-8");
652
+ const settings = JSON.parse(content);
653
+ const sections = [settings.plugins, settings.mcpServers, settings.permissions];
654
+ for (const section of sections) {
655
+ if (!section || typeof section !== "object")
656
+ continue;
657
+ for (const name of Object.keys(section)) {
658
+ const existing = skills.get(name);
659
+ if (existing) {
660
+ if (isMachineWide)
661
+ existing.machineWide = true;
662
+ else
663
+ existing.projectSpecific = true;
664
+ } else {
665
+ skills.set(name, {
666
+ name,
667
+ source: isMachineWide ? "global settings" : "project settings",
668
+ machineWide: isMachineWide,
669
+ projectSpecific: !isMachineWide
670
+ });
671
+ }
672
+ }
673
+ }
674
+ } catch {
675
+ }
676
+ }
677
+
678
+ // dist/scanner/index.js
679
+ async function scanProject(projectRoot) {
680
+ const allFiles = await discoverFiles(projectRoot);
681
+ const rootFiles = identifyRoots(allFiles, projectRoot);
682
+ const allRefs = [];
683
+ for (const file of allFiles) {
684
+ const fullPath = file.startsWith("/") ? file : join6(projectRoot, file);
685
+ try {
686
+ const refs = await parseFileReferences(fullPath, projectRoot);
687
+ allRefs.push(...refs);
688
+ } catch {
689
+ }
690
+ }
691
+ const graph = buildGraph(allFiles, allRefs, projectRoot);
692
+ const orphans = detectOrphans(allFiles, allRefs, rootFiles, projectRoot);
693
+ const staleFiles = detectStaleFiles(allFiles, projectRoot);
694
+ const skills = await parseSkills(projectRoot);
695
+ const scanData = JSON.stringify({ graph, orphans, skills, staleFiles });
696
+ const dataHash = createHash("sha256").update(scanData).digest("hex");
697
+ return {
698
+ graph,
699
+ orphans,
700
+ skills,
701
+ staleFiles,
702
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
703
+ dataHash
704
+ };
705
+ }
706
+ async function discoverFiles(projectRoot) {
707
+ const files = [];
708
+ const claudeDir = join6(projectRoot, ".claude");
709
+ if (existsSync4(claudeDir)) {
710
+ await walkDir(claudeDir, projectRoot, files);
711
+ }
712
+ if (existsSync4(join6(projectRoot, "CLAUDE.md"))) {
713
+ files.push("CLAUDE.md");
714
+ }
715
+ if (existsSync4(join6(projectRoot, "skills.md"))) {
716
+ files.push("skills.md");
717
+ }
718
+ const plansDir = join6(projectRoot, "docs", "plans");
719
+ if (existsSync4(plansDir)) {
720
+ await walkDir(plansDir, projectRoot, files);
721
+ }
722
+ return [...new Set(files)];
723
+ }
724
+ async function walkDir(dir, projectRoot, files) {
725
+ const entries = await readdir(dir, { withFileTypes: true });
726
+ for (const entry of entries) {
727
+ const fullPath = join6(dir, entry.name);
728
+ if (entry.isDirectory()) {
729
+ if (["node_modules", ".git", ".turbo", "cache", "session-env"].includes(entry.name))
730
+ continue;
731
+ await walkDir(fullPath, projectRoot, files);
732
+ } else {
733
+ const relPath = relative(projectRoot, fullPath);
734
+ files.push(relPath);
735
+ }
736
+ }
737
+ }
738
+ function identifyRoots(allFiles, projectRoot) {
739
+ const roots = [];
740
+ for (const rootFile of ROOT_FILES) {
741
+ if (allFiles.includes(rootFile)) {
742
+ roots.push(rootFile);
743
+ }
744
+ }
745
+ for (const globalFile of GLOBAL_ROOT_FILES) {
746
+ const expanded = globalFile.replace("~", homedir4());
747
+ if (existsSync4(expanded)) {
748
+ roots.push(globalFile);
749
+ }
750
+ }
751
+ return roots;
752
+ }
753
+ async function readClaudeConfigFiles(projectRoot) {
754
+ const { readFile: readFile6, stat } = await import("node:fs/promises");
755
+ const { join: join10 } = await import("node:path");
756
+ const { existsSync: existsSync9 } = await import("node:fs");
757
+ const { glob } = await import("glob");
758
+ const configPatterns = [
759
+ "CLAUDE.md",
760
+ ".claude/CLAUDE.md",
761
+ ".claude/settings.json",
762
+ ".claude/commands/*.md",
763
+ ".claude/skills/*.md"
764
+ ];
765
+ const files = [];
766
+ for (const pattern of configPatterns) {
767
+ const matches = await glob(pattern, { cwd: projectRoot, nodir: true });
768
+ for (const match of matches) {
769
+ const fullPath = join10(projectRoot, match);
770
+ if (!existsSync9(fullPath))
771
+ continue;
772
+ try {
773
+ const content = await readFile6(fullPath, "utf-8");
774
+ const fileStat = await stat(fullPath);
775
+ const lastMod = getGitLastModified(fullPath, projectRoot);
776
+ files.push({
777
+ filePath: match,
778
+ content,
779
+ sizeBytes: fileStat.size,
780
+ lastModified: lastMod
781
+ });
782
+ } catch {
783
+ }
784
+ }
785
+ }
786
+ return files;
787
+ }
788
+ function detectStaleFiles(allFiles, projectRoot) {
789
+ const stale = [];
790
+ const now = Date.now();
791
+ for (const file of allFiles) {
792
+ const fullPath = join6(projectRoot, file);
793
+ const lastMod = getGitLastModified(fullPath, projectRoot);
794
+ if (lastMod) {
795
+ const days = Math.floor((now - new Date(lastMod).getTime()) / (1e3 * 60 * 60 * 24));
796
+ if (days > STALE_THRESHOLD_DAYS) {
797
+ stale.push({ path: file, lastModified: lastMod, daysSinceModified: days });
798
+ }
799
+ }
800
+ }
801
+ return stale;
802
+ }
803
+
804
+ // dist/output/html-generator.js
805
+ function generateOfflineHtml(result, projectRoot) {
806
+ const title = projectRoot.split("/").pop() ?? "MD4AI";
807
+ const dataJson = JSON.stringify(result);
808
+ 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");
809
+ 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");
810
+ 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");
811
+ 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");
812
+ return `<!DOCTYPE html>
813
+ <html lang="en">
814
+ <head>
815
+ <meta charset="UTF-8">
816
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
817
+ <title>MD4AI \u2014 ${escapeHtml(title)}</title>
818
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
819
+ <style>
820
+ * { margin: 0; padding: 0; box-sizing: border-box; }
821
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
822
+ h1 { color: #38bdf8; margin-bottom: 0.5rem; }
823
+ .subtitle { color: #94a3b8; margin-bottom: 2rem; }
824
+ .section { background: #1e293b; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
825
+ .section h2 { color: #38bdf8; margin-bottom: 1rem; font-size: 1.2rem; }
826
+ .mermaid { background: #0f172a; border-radius: 8px; padding: 1rem; overflow-x: auto; }
827
+ .badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.85rem; margin-left: 0.5rem; }
828
+ .badge-orphan { background: #ef4444; color: white; }
829
+ .badge-stale { background: #f59e0b; color: black; }
830
+ table { width: 100%; border-collapse: collapse; }
831
+ th, td { padding: 0.5rem 1rem; text-align: left; border-bottom: 1px solid #334155; }
832
+ th { color: #94a3b8; font-weight: 600; }
833
+ .check { color: #22c55e; }
834
+ .meta { color: #94a3b8; }
835
+ .stale-meta { color: #f59e0b; }
836
+ .stats { display: flex; gap: 2rem; flex-wrap: wrap; margin-bottom: 1rem; }
837
+ .stat { text-align: center; }
838
+ .stat-value { font-size: 2rem; font-weight: bold; color: #38bdf8; }
839
+ .stat-label { color: #94a3b8; font-size: 0.85rem; }
840
+ .file-list { list-style: none; }
841
+ .file-list li { padding: 0.3rem 0; color: #cbd5e1; font-family: monospace; font-size: 0.9rem; }
842
+ #search { width: 100%; padding: 0.75rem 1rem; background: #0f172a; border: 1px solid #334155; border-radius: 8px; color: #e2e8f0; font-size: 1rem; margin-bottom: 1rem; }
843
+ #search:focus { outline: none; border-color: #38bdf8; }
844
+ @media print { body { background: white; color: black; } .section { border: 1px solid #ccc; } }
845
+ </style>
846
+ </head>
847
+ <body>
848
+ <h1>MD4AI \u2014 ${escapeHtml(title)}</h1>
849
+ <p class="subtitle">Scanned: ${new Date(result.scannedAt).toLocaleString()} | Hash: ${result.dataHash.slice(0, 12)}</p>
850
+
851
+ <div class="stats">
852
+ <div class="stat"><div class="stat-value">${result.graph.nodes.length}</div><div class="stat-label">Files</div></div>
853
+ <div class="stat"><div class="stat-value">${result.graph.edges.length}</div><div class="stat-label">References</div></div>
854
+ <div class="stat"><div class="stat-value">${result.orphans.length}</div><div class="stat-label">Orphans</div></div>
855
+ <div class="stat"><div class="stat-value">${result.staleFiles.length}</div><div class="stat-label">Stale</div></div>
856
+ <div class="stat"><div class="stat-value">${result.skills.length}</div><div class="stat-label">Skills</div></div>
857
+ </div>
858
+
859
+ <input type="text" id="search" placeholder="Search files..." oninput="filterFiles(this.value)">
860
+
861
+ <div class="section">
862
+ <h2>Dependency Graph</h2>
863
+ <pre class="mermaid">${escapeHtml(result.graph.mermaid)}</pre>
864
+ </div>
865
+
866
+ ${result.orphans.length > 0 ? `
867
+ <div class="section">
868
+ <h2>Orphan Files <span class="badge badge-orphan">${result.orphans.length}</span></h2>
869
+ <p class="meta" style="margin-bottom: 1rem;">Files not reachable from any root configuration file.</p>
870
+ <ul class="file-list" id="orphan-list">${orphanItems}</ul>
871
+ </div>
872
+ ` : ""}
873
+
874
+ ${result.staleFiles.length > 0 ? `
875
+ <div class="section">
876
+ <h2>Stale Files <span class="badge badge-stale">${result.staleFiles.length}</span></h2>
877
+ <p class="meta" style="margin-bottom: 1rem;">Files not modified in over 90 days.</p>
878
+ <ul class="file-list" id="stale-list">${staleItems}</ul>
879
+ </div>
880
+ ` : ""}
881
+
882
+ <div class="section">
883
+ <h2>Skills Comparison</h2>
884
+ <table>
885
+ <thead><tr><th>Skill/Plugin</th><th>Entire Machine</th><th>Project Specific</th><th>Source</th></tr></thead>
886
+ <tbody>${skillRows}</tbody>
887
+ </table>
888
+ </div>
889
+
890
+ <div class="section">
891
+ <h2>All Files</h2>
892
+ <ul class="file-list" id="all-files">${fileItems}</ul>
893
+ </div>
894
+
895
+ <script>
896
+ mermaid.initialize({ startOnLoad: true, theme: 'dark' });
897
+
898
+ function filterFiles(query) {
899
+ var q = query.toLowerCase();
900
+ document.querySelectorAll('.file-list li').forEach(function(li) {
901
+ var path = li.getAttribute('data-path') || '';
902
+ li.style.display = path.toLowerCase().indexOf(q) >= 0 ? '' : 'none';
903
+ });
904
+ }
905
+ </script>
906
+
907
+ <script type="application/json" id="scan-data">${dataJson}</script>
908
+ </body>
909
+ </html>`;
910
+ }
911
+ function escapeHtml(text) {
912
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
913
+ }
914
+
915
+ // dist/commands/map.js
916
+ async function mapCommand(path, options) {
917
+ const projectRoot = resolve3(path ?? process.cwd());
918
+ if (!existsSync5(projectRoot)) {
919
+ console.error(chalk8.red(`Path not found: ${projectRoot}`));
920
+ process.exit(1);
921
+ }
922
+ console.log(chalk8.blue(`Scanning: ${projectRoot}
923
+ `));
924
+ const result = await scanProject(projectRoot);
925
+ console.log(` Files found: ${result.graph.nodes.length}`);
926
+ console.log(` References: ${result.graph.edges.length}`);
927
+ console.log(` Orphans: ${result.orphans.length}`);
928
+ console.log(` Stale files: ${result.staleFiles.length}`);
929
+ console.log(` Skills: ${result.skills.length}`);
930
+ console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
931
+ const outputDir = resolve3(projectRoot, "output");
932
+ if (!existsSync5(outputDir)) {
933
+ await mkdir2(outputDir, { recursive: true });
934
+ }
935
+ const htmlPath = resolve3(outputDir, "index.html");
936
+ const html = generateOfflineHtml(result, projectRoot);
937
+ await writeFile2(htmlPath, html, "utf-8");
938
+ console.log(chalk8.green(`
939
+ Local preview: ${htmlPath}`));
940
+ if (!options.offline) {
941
+ try {
942
+ const { supabase } = await getAuthenticatedClient();
943
+ const { data: devicePaths } = await supabase.from("device_paths").select("folder_id, device_name").eq("path", projectRoot);
944
+ if (devicePaths?.length) {
945
+ const { folder_id, device_name } = devicePaths[0];
946
+ const { error } = await supabase.from("claude_folders").update({
947
+ graph_json: result.graph,
948
+ orphans_json: result.orphans,
949
+ skills_table_json: result.skills,
950
+ stale_files_json: result.staleFiles,
951
+ last_scanned: result.scannedAt,
952
+ data_hash: result.dataHash
953
+ }).eq("id", folder_id);
954
+ if (error) {
955
+ console.error(chalk8.yellow(`Sync warning: ${error.message}`));
956
+ } else {
957
+ await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder_id).eq("device_name", device_name);
958
+ await saveState({
959
+ lastFolderId: folder_id,
960
+ lastDeviceName: device_name,
961
+ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
962
+ });
963
+ console.log(chalk8.green("Synced to Supabase."));
964
+ console.log(chalk8.cyan(`
965
+ https://www.md4ai.com/project/${folder_id}
966
+ `));
967
+ const configFiles = await readClaudeConfigFiles(projectRoot);
968
+ if (configFiles.length > 0) {
969
+ for (const file of configFiles) {
970
+ await supabase.from("folder_files").upsert({
971
+ folder_id,
972
+ file_path: file.filePath,
973
+ content: file.content,
974
+ size_bytes: file.sizeBytes,
975
+ last_modified: file.lastModified
976
+ }, { onConflict: "folder_id,file_path" });
977
+ }
978
+ console.log(chalk8.green(` Uploaded ${configFiles.length} config file(s).`));
979
+ }
980
+ }
981
+ } else {
982
+ console.log(chalk8.yellow("No device path matches this folder. Run: md4ai add-device\n(Local preview still generated.)"));
983
+ }
984
+ } catch {
985
+ console.log(chalk8.yellow("Not logged in \u2014 local preview only."));
986
+ }
987
+ }
988
+ }
989
+
990
+ // dist/commands/simulate.js
991
+ import { join as join7 } from "node:path";
992
+ import { existsSync as existsSync6 } from "node:fs";
993
+ import { homedir as homedir5 } from "node:os";
994
+ import chalk9 from "chalk";
995
+ async function simulateCommand(prompt) {
996
+ const projectRoot = process.cwd();
997
+ console.log(chalk9.blue(`Simulating prompt: "${prompt}"
998
+ `));
999
+ console.log(chalk9.dim("Files Claude would load:\n"));
1000
+ const globalClaude = join7(homedir5(), ".claude", "CLAUDE.md");
1001
+ if (existsSync6(globalClaude)) {
1002
+ console.log(chalk9.green(" \u2713 ~/.claude/CLAUDE.md (global)"));
1003
+ }
1004
+ for (const rootFile of ROOT_FILES) {
1005
+ const fullPath = join7(projectRoot, rootFile);
1006
+ if (existsSync6(fullPath)) {
1007
+ console.log(chalk9.green(` \u2713 ${rootFile} (project root)`));
1008
+ }
1009
+ }
1010
+ const settingsFiles = [
1011
+ join7(homedir5(), ".claude", "settings.json"),
1012
+ join7(homedir5(), ".claude", "settings.local.json"),
1013
+ join7(projectRoot, ".claude", "settings.json"),
1014
+ join7(projectRoot, ".claude", "settings.local.json")
1015
+ ];
1016
+ for (const sf of settingsFiles) {
1017
+ if (existsSync6(sf)) {
1018
+ const display = sf.startsWith(homedir5()) ? sf.replace(homedir5(), "~") : sf.replace(projectRoot + "/", "");
1019
+ console.log(chalk9.green(` \u2713 ${display} (settings)`));
1020
+ }
1021
+ }
1022
+ const words = prompt.split(/\s+/);
1023
+ for (const word of words) {
1024
+ const cleaned = word.replace(/['"]/g, "");
1025
+ const candidatePath = join7(projectRoot, cleaned);
1026
+ if (existsSync6(candidatePath) && cleaned.includes("/")) {
1027
+ console.log(chalk9.cyan(` \u2192 ${cleaned} (referenced in prompt)`));
1028
+ }
1029
+ }
1030
+ console.log(chalk9.dim("\nNote: This is an approximation. Actual file loading depends on Claude Code internals."));
1031
+ }
1032
+
1033
+ // dist/commands/print.js
1034
+ import { join as join8 } from "node:path";
1035
+ import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
1036
+ import { existsSync as existsSync7 } from "node:fs";
1037
+ import chalk10 from "chalk";
1038
+ async function printCommand(title) {
1039
+ const projectRoot = process.cwd();
1040
+ const scanDataPath = join8(projectRoot, "output", "index.html");
1041
+ if (!existsSync7(scanDataPath)) {
1042
+ console.error(chalk10.red("No scan data found. Run: md4ai update"));
1043
+ process.exit(1);
1044
+ }
1045
+ const html = await readFile4(scanDataPath, "utf-8");
1046
+ const match = html.match(/<script type="application\/json" id="scan-data">([\s\S]*?)<\/script>/);
1047
+ if (!match) {
1048
+ console.error(chalk10.red("Could not extract scan data from output/index.html"));
1049
+ process.exit(1);
1050
+ }
1051
+ const result = JSON.parse(match[1]);
1052
+ const printHtml = generatePrintHtml(result, title);
1053
+ const outputPath = join8(projectRoot, "output", `print-${Date.now()}.html`);
1054
+ await writeFile3(outputPath, printHtml, "utf-8");
1055
+ console.log(chalk10.green(`Print-ready wall sheet: ${outputPath}`));
1056
+ }
1057
+ function escapeHtml2(text) {
1058
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1059
+ }
1060
+ function generatePrintHtml(result, title) {
1061
+ 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");
1062
+ const orphanItems = result.orphans.map((o) => `<li class="orphan">${escapeHtml2(o.path)}</li>`).join("\n");
1063
+ const staleItems = result.staleFiles.map((s) => `<li class="stale">${escapeHtml2(s.path)} (${s.daysSinceModified}d)</li>`).join("\n");
1064
+ return `<!DOCTYPE html>
1065
+ <html lang="en">
1066
+ <head>
1067
+ <meta charset="UTF-8">
1068
+ <title>${escapeHtml2(title)} \u2014 MD4AI Wall Sheet</title>
1069
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
1070
+ <style>
1071
+ @page { size: A3 landscape; margin: 1cm; }
1072
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 1rem; }
1073
+ h1 { font-size: 2rem; margin-bottom: 0.5rem; }
1074
+ .subtitle { color: #666; margin-bottom: 2rem; }
1075
+ .mermaid { margin: 2rem 0; }
1076
+ .columns { display: flex; gap: 2rem; }
1077
+ .column { flex: 1; }
1078
+ table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
1079
+ th, td { padding: 0.3rem 0.5rem; text-align: left; border-bottom: 1px solid #ddd; }
1080
+ th { background: #f5f5f5; }
1081
+ .orphan { color: #dc2626; }
1082
+ .stale { color: #d97706; }
1083
+ .check { color: #16a34a; font-weight: bold; }
1084
+ </style>
1085
+ </head>
1086
+ <body>
1087
+ <h1>${escapeHtml2(title)}</h1>
1088
+ <p class="subtitle">Generated: ${(/* @__PURE__ */ new Date()).toLocaleString()} | Files: ${result.graph.nodes.length} | Orphans: ${result.orphans.length}</p>
1089
+
1090
+ <pre class="mermaid">${escapeHtml2(result.graph.mermaid)}</pre>
1091
+
1092
+ <div class="columns">
1093
+ <div class="column">
1094
+ <h2>Skills Comparison</h2>
1095
+ <table>
1096
+ <tr><th>Skill</th><th>Machine</th><th>Project</th></tr>
1097
+ ${skillRows}
1098
+ </table>
1099
+ </div>
1100
+ <div class="column">
1101
+ ${result.orphans.length ? `<h2>Orphans</h2><ul>${orphanItems}</ul>` : ""}
1102
+ ${result.staleFiles.length ? `<h2>Stale Files</h2><ul>${staleItems}</ul>` : ""}
1103
+ </div>
1104
+ </div>
1105
+
1106
+ <script>mermaid.initialize({ startOnLoad: true, theme: 'default' });</script>
1107
+ </body>
1108
+ </html>`;
1109
+ }
1110
+
1111
+ // dist/commands/sync.js
1112
+ import chalk11 from "chalk";
1113
+ async function syncCommand(options) {
1114
+ const { supabase } = await getAuthenticatedClient();
1115
+ if (options.all) {
1116
+ const { data: devices, error } = await supabase.from("device_paths").select("folder_id, device_name, path");
1117
+ if (error || !devices?.length) {
1118
+ console.error(chalk11.red("No devices found."));
1119
+ process.exit(1);
1120
+ }
1121
+ for (const device of devices) {
1122
+ console.log(chalk11.blue(`Syncing: ${device.path}`));
1123
+ try {
1124
+ const result = await scanProject(device.path);
1125
+ await supabase.from("claude_folders").update({
1126
+ graph_json: result.graph,
1127
+ orphans_json: result.orphans,
1128
+ skills_table_json: result.skills,
1129
+ stale_files_json: result.staleFiles,
1130
+ last_scanned: result.scannedAt,
1131
+ data_hash: result.dataHash
1132
+ }).eq("id", device.folder_id);
1133
+ await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
1134
+ console.log(chalk11.green(` Done: ${device.device_name}`));
1135
+ } catch (err) {
1136
+ console.error(chalk11.red(` Failed: ${device.path}: ${err}`));
1137
+ }
1138
+ }
1139
+ await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
1140
+ console.log(chalk11.green("\nAll devices synced."));
1141
+ } else {
1142
+ const state = await loadState();
1143
+ if (!state.lastFolderId) {
1144
+ console.error(chalk11.yellow("No recent sync. Use: md4ai sync --all, or md4ai update <path> first."));
1145
+ process.exit(1);
1146
+ }
1147
+ const { data: device } = await supabase.from("device_paths").select("folder_id, device_name, path").eq("folder_id", state.lastFolderId).eq("device_name", state.lastDeviceName).single();
1148
+ if (!device) {
1149
+ console.error(chalk11.red("Could not find last synced device/folder."));
1150
+ process.exit(1);
1151
+ }
1152
+ console.log(chalk11.blue(`Syncing: ${device.path}`));
1153
+ const result = await scanProject(device.path);
1154
+ await supabase.from("claude_folders").update({
1155
+ graph_json: result.graph,
1156
+ orphans_json: result.orphans,
1157
+ skills_table_json: result.skills,
1158
+ stale_files_json: result.staleFiles,
1159
+ last_scanned: result.scannedAt,
1160
+ data_hash: result.dataHash
1161
+ }).eq("id", device.folder_id);
1162
+ await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
1163
+ await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
1164
+ console.log(chalk11.green("Synced."));
1165
+ }
1166
+ }
1167
+
1168
+ // dist/commands/link.js
1169
+ import { resolve as resolve4 } from "node:path";
1170
+ import { hostname as hostname2, platform as platform2 } from "node:os";
1171
+ import chalk12 from "chalk";
1172
+ function detectOs2() {
1173
+ const p = platform2();
1174
+ if (p === "win32")
1175
+ return "windows";
1176
+ if (p === "darwin")
1177
+ return "macos";
1178
+ if (p === "linux")
1179
+ return "linux";
1180
+ return "other";
1181
+ }
1182
+ function detectDeviceName() {
1183
+ const os = detectOs2();
1184
+ const host = hostname2().split(".")[0];
1185
+ const osLabel = os.charAt(0).toUpperCase() + os.slice(1);
1186
+ return `${host}-${osLabel}`;
1187
+ }
1188
+ async function linkCommand(projectId) {
1189
+ const { supabase, userId } = await getAuthenticatedClient();
1190
+ const cwd = resolve4(process.cwd());
1191
+ const deviceName = detectDeviceName();
1192
+ const osType = detectOs2();
1193
+ const { data: folder, error: folderErr } = await supabase.from("claude_folders").select("id, name").eq("id", projectId).single();
1194
+ if (folderErr || !folder) {
1195
+ console.error(chalk12.red("Project not found, or you do not have access."));
1196
+ console.error(chalk12.yellow("Check the project ID in the MD4AI web dashboard."));
1197
+ process.exit(1);
1198
+ }
1199
+ console.log(chalk12.blue(`
1200
+ Linking "${folder.name}" to this device...
1201
+ `));
1202
+ console.log(` Project: ${folder.name}`);
1203
+ console.log(` Path: ${cwd}`);
1204
+ console.log(` Device: ${deviceName}`);
1205
+ console.log(` OS: ${osType}`);
1206
+ await supabase.from("devices").upsert({
1207
+ user_id: userId,
1208
+ device_name: deviceName,
1209
+ os_type: osType
1210
+ }, { onConflict: "user_id,device_name" });
1211
+ const { data: existing } = await supabase.from("device_paths").select("id").eq("folder_id", folder.id).eq("device_name", deviceName).maybeSingle();
1212
+ if (existing) {
1213
+ await supabase.from("device_paths").update({ path: cwd, os_type: osType, last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", existing.id);
1214
+ } else {
1215
+ const { error: pathErr } = await supabase.from("device_paths").insert({
1216
+ user_id: userId,
1217
+ folder_id: folder.id,
1218
+ device_name: deviceName,
1219
+ os_type: osType,
1220
+ path: cwd
1221
+ });
1222
+ if (pathErr) {
1223
+ console.error(chalk12.red(`Failed to link: ${pathErr.message}`));
1224
+ process.exit(1);
1225
+ }
1226
+ }
1227
+ console.log(chalk12.green("\nLinked successfully."));
1228
+ console.log(chalk12.blue("\nRunning initial scan...\n"));
1229
+ const result = await scanProject(cwd);
1230
+ console.log(` Files: ${result.graph.nodes.length}`);
1231
+ console.log(` References:${result.graph.edges.length}`);
1232
+ console.log(` Orphans: ${result.orphans.length}`);
1233
+ console.log(` Skills: ${result.skills.length}`);
1234
+ const { error: scanErr } = await supabase.from("claude_folders").update({
1235
+ graph_json: result.graph,
1236
+ orphans_json: result.orphans,
1237
+ skills_table_json: result.skills,
1238
+ stale_files_json: result.staleFiles,
1239
+ last_scanned: result.scannedAt,
1240
+ data_hash: result.dataHash
1241
+ }).eq("id", folder.id);
1242
+ if (scanErr) {
1243
+ console.error(chalk12.yellow(`Scan upload warning: ${scanErr.message}`));
1244
+ }
1245
+ const configFiles = await readClaudeConfigFiles(cwd);
1246
+ if (configFiles.length > 0) {
1247
+ for (const file of configFiles) {
1248
+ await supabase.from("folder_files").upsert({
1249
+ folder_id: folder.id,
1250
+ file_path: file.filePath,
1251
+ content: file.content,
1252
+ size_bytes: file.sizeBytes,
1253
+ last_modified: file.lastModified
1254
+ }, { onConflict: "folder_id,file_path" });
1255
+ }
1256
+ console.log(chalk12.green(` Uploaded ${configFiles.length} config file(s).`));
1257
+ }
1258
+ await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder.id).eq("device_name", deviceName);
1259
+ await saveState({
1260
+ lastFolderId: folder.id,
1261
+ lastDeviceName: deviceName,
1262
+ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
1263
+ });
1264
+ const projectUrl = `https://www.md4ai.com/project/${folder.id}`;
1265
+ console.log(chalk12.green("\nDone! Project linked and scanned."));
1266
+ console.log(chalk12.cyan(`
1267
+ ${projectUrl}
1268
+ `));
1269
+ console.log(chalk12.grey('Run "md4ai update" to rescan at any time.'));
1270
+ }
1271
+
1272
+ // dist/commands/import-bundle.js
1273
+ import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir3 } from "node:fs/promises";
1274
+ import { join as join9, dirname as dirname2 } from "node:path";
1275
+ import { existsSync as existsSync8 } from "node:fs";
1276
+ import chalk13 from "chalk";
1277
+ import { confirm, input as input4 } from "@inquirer/prompts";
1278
+ async function importBundleCommand(zipPath) {
1279
+ if (!existsSync8(zipPath)) {
1280
+ console.error(chalk13.red(`File not found: ${zipPath}`));
1281
+ process.exit(1);
1282
+ }
1283
+ const JSZip = (await import("jszip")).default;
1284
+ const zipData = await readFile5(zipPath);
1285
+ const zip = await JSZip.loadAsync(zipData);
1286
+ const manifestFile = zip.file("manifest.json");
1287
+ if (!manifestFile) {
1288
+ console.error(chalk13.red("Invalid bundle: missing manifest.json"));
1289
+ process.exit(1);
1290
+ }
1291
+ const manifest = JSON.parse(await manifestFile.async("string"));
1292
+ console.log(chalk13.blue(`
1293
+ Bundle: ${manifest.folderName}`));
1294
+ console.log(` Exported by: ${manifest.ownerEmail}`);
1295
+ console.log(` Exported at: ${manifest.exportedAt}`);
1296
+ console.log(` Files: ${manifest.fileCount}`);
1297
+ const files = [];
1298
+ for (const [path, file] of Object.entries(zip.files)) {
1299
+ if (path.startsWith("claude-files/") && !file.dir) {
1300
+ const relativePath = path.replace("claude-files/", "");
1301
+ const content = await file.async("string");
1302
+ files.push({ filePath: relativePath, content });
1303
+ }
1304
+ }
1305
+ if (files.length === 0) {
1306
+ console.log(chalk13.yellow("No Claude config files found in bundle."));
1307
+ return;
1308
+ }
1309
+ console.log(chalk13.blue(`
1310
+ Files to extract:`));
1311
+ for (const f of files) {
1312
+ console.log(` ${f.filePath}`);
1313
+ }
1314
+ const targetDir = await input4({
1315
+ message: "Extract to directory:",
1316
+ default: process.cwd()
1317
+ });
1318
+ const proceed = await confirm({
1319
+ message: `Extract ${files.length} file(s) to ${targetDir}?`
1320
+ });
1321
+ if (!proceed) {
1322
+ console.log(chalk13.yellow("Cancelled."));
1323
+ return;
1324
+ }
1325
+ for (const file of files) {
1326
+ const fullPath = join9(targetDir, file.filePath);
1327
+ const dir = dirname2(fullPath);
1328
+ if (!existsSync8(dir)) {
1329
+ await mkdir3(dir, { recursive: true });
1330
+ }
1331
+ await writeFile4(fullPath, file.content, "utf-8");
1332
+ console.log(chalk13.green(` \u2713 ${file.filePath}`));
1333
+ }
1334
+ console.log(chalk13.green(`
1335
+ Done! ${files.length} file(s) extracted to ${targetDir}`));
1336
+ }
1337
+
1338
+ // dist/index.js
1339
+ var program = new Command();
1340
+ program.name("md4ai").description("MD4AI \u2014 Claude tooling visualiser").version("0.1.0");
1341
+ program.command("login").description("Log in to MD4AI with email and password").action(loginCommand);
1342
+ program.command("logout").description("Log out and clear stored credentials").action(logoutCommand);
1343
+ program.command("status").description("Show login status, device count, folder count, last sync").action(statusCommand);
1344
+ program.command("add-folder").description("Create a new Claude folder").action(addFolderCommand);
1345
+ program.command("add-device").description("Add a device path to a Claude folder").action(addDeviceCommand);
1346
+ program.command("list-devices").description("List all devices and their linked folders").action(listDevicesCommand);
1347
+ program.command("link <project-id>").description("Link the current directory to a project created in the web dashboard").action(linkCommand);
1348
+ program.command("update [path]").description("Rescan a Claude project folder and push updated data to your dashboard").option("--offline", "Skip pushing to Supabase").action(mapCommand);
1349
+ program.command("simulate <prompt>").description("Show which files Claude would load for a given prompt").action(simulateCommand);
1350
+ program.command("print <title>").description("Generate a printable wall-chart HTML from the last scan").action(printCommand);
1351
+ program.command("sync").description("Re-push latest scan data to Supabase").option("--all", "Sync all folders on all devices").action(syncCommand);
1352
+ program.command("import <zipfile>").description("Import a Claude setup bundle exported from the web dashboard").action(importBundleCommand);
1353
+ program.parse();