md4ai 0.10.3 → 0.10.4

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.
package/README.md CHANGED
@@ -1,100 +1,136 @@
1
- # md4ai
1
+ <p align="center">
2
+ <strong><span style="color:#38bdf8">MD4</span>AI</strong><br/>
3
+ <em>See what Claude actually reads</em>
4
+ </p>
2
5
 
3
- CLI for [MD4AI](https://www.md4ai.com) — scan your Claude Code projects and sync results to a web dashboard.
6
+ <p align="center">
7
+ <a href="https://www.npmjs.com/package/md4ai"><img src="https://img.shields.io/npm/v/md4ai?color=38bdf8&label=npm" alt="npm version"></a>
8
+ <a href="https://www.npmjs.com/package/md4ai"><img src="https://img.shields.io/npm/dm/md4ai?color=10b981" alt="monthly downloads"></a>
9
+ <a href="https://github.com/Media-HQ-2-Ltd/MD4AI/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Media-HQ-2-Ltd/MD4AI" alt="licence"></a>
10
+ <a href="https://www.md4ai.com"><img src="https://img.shields.io/badge/dashboard-md4ai.com-38bdf8" alt="web dashboard"></a>
11
+ </p>
4
12
 
5
- Discovers Claude configuration files, builds dependency graphs, detects orphans and broken references, catalogues skills and marketplace plugins, and pushes everything to a shared dashboard for team visibility.
13
+ ---
6
14
 
7
- ## Installation
15
+ Scan your Claude Code configuration, visualise the dependency graph, catch orphan files, and track software versions — across every device and every project.
16
+
17
+ **Free for individual developers.** Teams get shared dashboards and invite-based collaboration.
18
+
19
+ <p align="center">
20
+ <img src="https://www.md4ai.com/screenshots/project-overview.png" alt="MD4AI project dashboard" width="700"/>
21
+ </p>
22
+
23
+ ## Why MD4AI?
24
+
25
+ As your Claude Code setup grows — CLAUDE.md, skills, hooks, MCP servers, memory files, plan docs — it becomes hard to see the full picture. MD4AI scans everything and shows you:
26
+
27
+ - **Dependency graph** — which files reference which, interactively
28
+ - **Orphan detection** — config files nothing points to
29
+ - **Broken references** — links to files that don't exist
30
+ - **Skills catalogue** — project-specific, machine-wide, and marketplace plugins
31
+ - **Stale files** — anything untouched for 90+ days
32
+ - **Environment drift** — compare `.env`, Vercel, and GitHub Secrets
33
+ - **Software versions** — frameworks and tools detected vs latest releases
34
+
35
+ ## Quick Start
8
36
 
9
37
  ```bash
10
38
  npm install -g md4ai
11
39
  ```
12
40
 
13
- Requires **Node.js 22** or later.
41
+ > Requires **Node.js 22** or later. Works on **Windows (WSL2)**, **Linux**, and **macOS**.
14
42
 
15
- ## Setup
43
+ ```bash
44
+ # 1. Create an account at md4ai.com and set your API key
45
+ export MD4AI_SUPABASE_ANON_KEY="your-anon-key"
16
46
 
17
- 1. **Create an account** at [md4ai.com](https://www.md4ai.com) and create a project.
47
+ # 2. Log in
48
+ md4ai login
18
49
 
19
- 2. **Set the Supabase key** in your shell profile:
50
+ # 3. Link a project (get the ID from the dashboard URL)
51
+ cd /path/to/your-claude-project
52
+ md4ai link <project-id>
20
53
 
21
- ```bash
22
- export MD4AI_SUPABASE_ANON_KEY="your-anon-key"
23
- ```
54
+ # 4. That's it — your scan results are live at md4ai.com
55
+ ```
24
56
 
25
- 3. **Log in:**
57
+ Or scan without an account:
26
58
 
27
- ```bash
28
- md4ai login
29
- ```
59
+ ```bash
60
+ md4ai scan --offline # generates output/index.html
61
+ ```
30
62
 
31
- 4. **Link your project:**
63
+ ## What You Get
32
64
 
33
- ```bash
34
- cd /path/to/your-claude-project
35
- md4ai link <project-id>
36
- ```
65
+ ### Dependency Graph
37
66
 
38
- The project ID is in the URL when viewing your project on the dashboard.
67
+ See how your CLAUDE.md, skills, hooks, and config files connect. Search, zoom, and print wall sheets.
39
68
 
40
- ## Commands
69
+ <p align="center">
70
+ <img src="https://www.md4ai.com/screenshots/dependency-graph.png" alt="Dependency graph" width="600"/>
71
+ </p>
41
72
 
42
- ### Scanning & Syncing
73
+ ### Orphan Detection
43
74
 
44
- | Command | Description |
45
- |---------|-------------|
46
- | `md4ai scan [path]` | Scan a Claude project and push results to the dashboard. Defaults to current directory. |
47
- | `md4ai scan --offline` | Scan locally only — generates `output/index.html` without pushing to Supabase. |
48
- | `md4ai sync` | Re-push the most recent scan data for the current project. |
49
- | `md4ai sync --all` | Re-scan and sync all linked projects on this device. |
50
- | `md4ai link <project-id>` | Link the current directory to a dashboard project and run an initial scan. |
75
+ Find configuration files not reachable from any root. Grouped by folder with modification dates.
51
76
 
52
- ### Analysis
77
+ <p align="center">
78
+ <img src="https://www.md4ai.com/screenshots/orphan-files.png" alt="Orphan files" width="600"/>
79
+ </p>
53
80
 
54
- | Command | Description |
55
- |---------|-------------|
56
- | `md4ai simulate <prompt>` | Show which files Claude Code would load for a given prompt. |
57
- | `md4ai print <title>` | Generate a printable A3 wall-chart HTML with the dependency graph and skills table. |
58
- | `md4ai init-manifest` | Scaffold an `env-manifest.md` from detected `.env` files in the project. |
81
+ ### Skills Comparison
59
82
 
60
- ### Account & Device Management
83
+ Every skill and plugin at a glance — machine-wide vs project-specific, with current status.
84
+
85
+ <p align="center">
86
+ <img src="https://www.md4ai.com/screenshots/skills-table.png" alt="Skills comparison" width="600"/>
87
+ </p>
88
+
89
+ ### Software Versions
90
+
91
+ Track detected tool versions compared against latest stable and beta releases.
92
+
93
+ <p align="center">
94
+ <img src="https://www.md4ai.com/screenshots/software-versions.png" alt="Software versions" width="600"/>
95
+ </p>
96
+
97
+ ## Commands
98
+
99
+ ### Scanning & Syncing
61
100
 
62
101
  | Command | Description |
63
102
  |---------|-------------|
64
- | `md4ai login` | Authenticate with email and password. |
65
- | `md4ai logout` | Clear stored credentials. |
66
- | `md4ai status` | Show login status, linked folders, and last sync time. |
67
- | `md4ai add-folder` | Create a new project folder on the dashboard. |
68
- | `md4ai add-device` | Add a device path to an existing project. |
69
- | `md4ai list-devices` | List all devices and their linked projects. |
103
+ | `md4ai scan [path]` | Scan a Claude project and push results to the dashboard |
104
+ | `md4ai scan --offline` | Scan locally — generates `output/index.html` without pushing |
105
+ | `md4ai sync --all` | Re-scan and sync all linked projects on this device |
106
+ | `md4ai link <project-id>` | Link cwd to a dashboard project and run initial scan |
70
107
 
71
- ### Monitoring
108
+ ### Analysis
72
109
 
73
110
  | Command | Description |
74
111
  |---------|-------------|
75
- | `md4ai mcp-watch` | Monitor MCP server status on this device (runs until Ctrl+C, polls every 30s). |
112
+ | `md4ai simulate <prompt>` | Show which files Claude would load for a given prompt |
113
+ | `md4ai print <title>` | Generate a printable A3 wall-chart HTML |
114
+ | `md4ai init-manifest` | Scaffold an `env-manifest.md` from detected `.env` files |
76
115
 
77
- ### Other
116
+ ### Account & Devices
78
117
 
79
118
  | Command | Description |
80
119
  |---------|-------------|
81
- | `md4ai import <zipfile>` | Import an exported team bundle. |
82
- | `md4ai update` | Check for updates and install if available. |
83
- | `md4ai config set <key> <value>` | Set a configuration value (e.g. `vercel-token`). |
120
+ | `md4ai login` | Authenticate with email and password |
121
+ | `md4ai status` | Show login status, linked folders, and last sync |
122
+ | `md4ai list-devices` | List all devices and their linked projects |
123
+ | `md4ai mcp-watch` | Monitor MCP server status (runs until Ctrl+C) |
84
124
 
85
- ## What Gets Scanned
125
+ ### One Command to Rule Them All
86
126
 
87
- Running `md4ai scan` discovers files in `.claude/`, `CLAUDE.md`, `skills.md`, and `docs/plans/`. It then:
127
+ ```bash
128
+ md4ai start # or just: md4ai
129
+ ```
88
130
 
89
- - **Builds a dependency graph** by parsing markdown links, bare file paths, `$CLAUDE_PROJECT_DIR` references, and JSON hook configurations.
90
- - **Detects orphans** — files not reachable from any root configuration.
91
- - **Finds broken references** — links pointing to files that don't exist on disk.
92
- - **Catalogues skills** — both project-specific and machine-wide, including marketplace plugins.
93
- - **Flags stale files** — anything not modified in over 90 days.
94
- - **Scans environment variables** — if an `env-manifest.md` is present, checks local `.env` files, Vercel, and GitHub Secrets for drift.
95
- - **Detects tooling** — frameworks, runtimes, and packages from `package.json` and MCP settings.
131
+ Checks for updates, scans the current project, and starts MCP monitoring all in one go.
96
132
 
97
- Scan output:
133
+ ## Scan Output
98
134
 
99
135
  ```
100
136
  Files found: 41
@@ -109,15 +145,6 @@ Plugins: 17 (17 skills)
109
145
  Data hash: 9868c4a8b50f...
110
146
  ```
111
147
 
112
- ## File Locations
113
-
114
- | Item | Path |
115
- |------|------|
116
- | Credentials | `~/.md4ai/credentials.json` |
117
- | State | `~/.md4ai/state.json` |
118
- | Local scan preview | `output/index.html` (in scanned project) |
119
- | Print exports | `output/print-<timestamp>.html` |
120
-
121
148
  ## Web Dashboard
122
149
 
123
150
  All scan data syncs to [md4ai.com](https://www.md4ai.com) where you can:
@@ -129,16 +156,11 @@ All scan data syncs to [md4ai.com](https://www.md4ai.com) where you can:
129
156
  - Share projects with team members
130
157
  - Compare skills across machines
131
158
 
132
- ## Tech Stack
133
-
134
- - **TypeScript** (strict, ESM)
135
- - **Commander** for CLI parsing
136
- - **Supabase** for auth and data storage
137
- - **esbuild** for bundling
138
-
139
159
  ## Support
140
160
 
141
- For questions, feedback, or bug reports: [richard@mediahq2.com](mailto:richard@mediahq2.com)
161
+ Questions, feedback, or bugs: [richard@mediahq2.com](mailto:richard@mediahq2.com)
162
+
163
+ Built by [Testate Technologies Ltd](https://www.md4ai.com) · [Changelog](https://www.md4ai.com/changelog)
142
164
 
143
165
  ## Licence
144
166
 
@@ -9,6 +9,74 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // dist/check-update.js
13
+ import chalk from "chalk";
14
+ async function fetchLatest() {
15
+ try {
16
+ const controller = new AbortController();
17
+ const timeout = setTimeout(() => controller.abort(), 3e3);
18
+ const res = await fetch("https://registry.npmjs.org/md4ai/latest", {
19
+ signal: controller.signal
20
+ });
21
+ clearTimeout(timeout);
22
+ if (!res.ok)
23
+ return null;
24
+ const data = await res.json();
25
+ return data.version ?? null;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+ function printUpdateBanner(latest) {
31
+ console.log("");
32
+ console.log(chalk.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"));
33
+ console.log(chalk.yellow("\u2502") + chalk.bold(" Update available! ") + chalk.dim(`${CURRENT_VERSION}`) + chalk.white(" \u2192 ") + chalk.green.bold(`${latest}`) + " " + chalk.yellow("\u2502"));
34
+ console.log(chalk.yellow("\u2502") + " " + chalk.yellow("\u2502"));
35
+ console.log(chalk.yellow("\u2502") + " Run: " + chalk.cyan("md4ai update") + " " + chalk.yellow("\u2502"));
36
+ console.log(chalk.yellow("\u2502") + " " + chalk.yellow("\u2502"));
37
+ console.log(chalk.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
38
+ console.log("");
39
+ }
40
+ async function checkForUpdate() {
41
+ const latest = await fetchLatest();
42
+ if (!latest)
43
+ return;
44
+ if (latest !== CURRENT_VERSION && isNewer(latest, CURRENT_VERSION)) {
45
+ printUpdateBanner(latest);
46
+ } else {
47
+ console.log(chalk.green(`md4ai v${CURRENT_VERSION} \u2014 you're on the latest version.`));
48
+ }
49
+ }
50
+ async function autoCheckForUpdate() {
51
+ try {
52
+ const latest = await fetchLatest();
53
+ if (!latest)
54
+ return;
55
+ if (latest !== CURRENT_VERSION && isNewer(latest, CURRENT_VERSION)) {
56
+ printUpdateBanner(latest);
57
+ }
58
+ } catch {
59
+ }
60
+ }
61
+ function isNewer(a, b) {
62
+ const pa = a.split(".").map(Number);
63
+ const pb = b.split(".").map(Number);
64
+ for (let i = 0; i < 3; i++) {
65
+ if ((pa[i] ?? 0) > (pb[i] ?? 0))
66
+ return true;
67
+ if ((pa[i] ?? 0) < (pb[i] ?? 0))
68
+ return false;
69
+ }
70
+ return false;
71
+ }
72
+ var CURRENT_VERSION;
73
+ var init_check_update = __esm({
74
+ "dist/check-update.js"() {
75
+ "use strict";
76
+ CURRENT_VERSION = true ? "0.10.4" : "0.0.0-dev";
77
+ }
78
+ });
79
+
12
80
  // ../packages/shared/dist/types.js
13
81
  var init_types = __esm({
14
82
  "../packages/shared/dist/types.js"() {
@@ -152,10 +220,10 @@ var login_exports = {};
152
220
  __export(login_exports, {
153
221
  loginCommand: () => loginCommand
154
222
  });
155
- import chalk from "chalk";
223
+ import chalk2 from "chalk";
156
224
  import { input, password } from "@inquirer/prompts";
157
225
  async function loginCommand() {
158
- console.log(chalk.blue("MD4AI Login\n"));
226
+ console.log(chalk2.blue("MD4AI Login\n"));
159
227
  const email = await input({ message: "Email:" });
160
228
  const pwd = await password({ message: "Password:" });
161
229
  const anonKey = getAnonKey();
@@ -165,7 +233,7 @@ async function loginCommand() {
165
233
  password: pwd
166
234
  });
167
235
  if (error) {
168
- console.error(chalk.red(`Login failed: ${error.message}`));
236
+ console.error(chalk2.red(`Login failed: ${error.message}`));
169
237
  process.exit(1);
170
238
  }
171
239
  await saveCredentials({
@@ -175,7 +243,7 @@ async function loginCommand() {
175
243
  userId: data.user.id,
176
244
  email: data.user.email ?? email
177
245
  });
178
- console.log(chalk.green(`
246
+ console.log(chalk2.green(`
179
247
  Logged in as ${data.user.email}`));
180
248
  }
181
249
  var init_login = __esm({
@@ -187,14 +255,14 @@ var init_login = __esm({
187
255
  });
188
256
 
189
257
  // dist/auth.js
190
- import chalk4 from "chalk";
258
+ import chalk5 from "chalk";
191
259
  import { confirm, input as input2, password as password2 } from "@inquirer/prompts";
192
260
  async function getAuthenticatedClient() {
193
261
  const creds = await loadCredentials();
194
262
  const needsLogin = !creds?.accessToken || Date.now() > creds.expiresAt;
195
263
  if (needsLogin) {
196
264
  const reason = !creds?.accessToken ? "Not logged in." : "Session expired.";
197
- console.log(chalk4.yellow(`${reason}`));
265
+ console.log(chalk5.yellow(`${reason}`));
198
266
  const shouldLogin = await confirm({ message: "Would you like to log in now?" });
199
267
  if (!shouldLogin) {
200
268
  process.exit(0);
@@ -226,7 +294,7 @@ async function refreshSession() {
226
294
  async function getLongLivedClient() {
227
295
  const creds = await loadCredentials();
228
296
  if (!creds?.accessToken || !creds?.refreshToken) {
229
- console.log(chalk4.yellow("Not logged in."));
297
+ console.log(chalk5.yellow("Not logged in."));
230
298
  const shouldLogin = await confirm({ message: "Would you like to log in now?" });
231
299
  if (!shouldLogin)
232
300
  process.exit(0);
@@ -243,7 +311,7 @@ async function getLongLivedClient() {
243
311
  });
244
312
  return { supabase: result.client, userId: result.userId };
245
313
  } catch {
246
- console.log(chalk4.yellow("Session expired \u2014 please log in again."));
314
+ console.log(chalk5.yellow("Session expired \u2014 please log in again."));
247
315
  const shouldLogin = await confirm({ message: "Would you like to log in now?" });
248
316
  if (!shouldLogin)
249
317
  process.exit(0);
@@ -261,7 +329,7 @@ async function promptLogin() {
261
329
  password: pwd
262
330
  });
263
331
  if (error) {
264
- console.error(chalk4.red(`Login failed: ${error.message}`));
332
+ console.error(chalk5.red(`Login failed: ${error.message}`));
265
333
  process.exit(1);
266
334
  }
267
335
  await saveCredentials({
@@ -271,7 +339,7 @@ async function promptLogin() {
271
339
  userId: data.user.id,
272
340
  email: data.user.email ?? email
273
341
  });
274
- console.log(chalk4.green(`Logged in as ${data.user.email}
342
+ console.log(chalk5.green(`Logged in as ${data.user.email}
275
343
  `));
276
344
  const authedSupabase = createSupabaseClient(anonKey, data.session.access_token);
277
345
  return { supabase: authedSupabase, userId: data.user.id };
@@ -1082,7 +1150,7 @@ import { execFile } from "node:child_process";
1082
1150
  import { promisify } from "node:util";
1083
1151
  import { join as join9, relative as relative2 } from "node:path";
1084
1152
  import { existsSync as existsSync4 } from "node:fs";
1085
- import chalk8 from "chalk";
1153
+ import chalk9 from "chalk";
1086
1154
  async function scanEnvManifest(projectRoot) {
1087
1155
  const manifestFullPath = join9(projectRoot, MANIFEST_PATH);
1088
1156
  if (!existsSync4(manifestFullPath)) {
@@ -1143,16 +1211,16 @@ function tokenFixInstructions(source) {
1143
1211
  async function checkVercelEnvVars(projectRoot, variables) {
1144
1212
  const tokenResult = await resolveVercelTokenWithSource();
1145
1213
  if (!tokenResult) {
1146
- console.log(chalk8.dim(" Vercel checks skipped (no token configured)"));
1147
- console.log(chalk8.dim(" To enable: md4ai config set vercel-token <token>"));
1148
- console.log(chalk8.dim(" Generate a token at https://vercel.com/account/tokens"));
1214
+ console.log(chalk9.dim(" Vercel checks skipped (no token configured)"));
1215
+ console.log(chalk9.dim(" To enable: md4ai config set vercel-token <token>"));
1216
+ console.log(chalk9.dim(" Generate a token at https://vercel.com/account/tokens"));
1149
1217
  return null;
1150
1218
  }
1151
1219
  const { token, source } = tokenResult;
1152
1220
  const projects = await discoverVercelProjects(projectRoot);
1153
1221
  if (projects.length === 0)
1154
1222
  return null;
1155
- console.log(chalk8.dim(` Checking ${projects.length} Vercel project(s)...`));
1223
+ console.log(chalk9.dim(` Checking ${projects.length} Vercel project(s)...`));
1156
1224
  const discovered = [];
1157
1225
  const manifestVarNames = new Set(variables.map((v) => v.name));
1158
1226
  let tokenErrorShown = false;
@@ -1174,23 +1242,23 @@ async function checkVercelEnvVars(projectRoot, variables) {
1174
1242
  mv.vercelStatus = {};
1175
1243
  mv.vercelStatus[proj.projectName] = vercelKeySet.has(mv.name) ? "present" : "missing";
1176
1244
  }
1177
- console.log(chalk8.dim(` ${proj.projectName}: ${vars.length} var(s)`));
1245
+ console.log(chalk9.dim(` ${proj.projectName}: ${vars.length} var(s)`));
1178
1246
  } catch (err) {
1179
1247
  if (err instanceof VercelApiError && err.isInvalidToken && !tokenErrorShown) {
1180
1248
  tokenErrorShown = true;
1181
- console.log(chalk8.red(` ${proj.projectName}: Token is invalid or expired`));
1182
- console.log(chalk8.yellow(` Token source: ${tokenSourceLabel(source)}`));
1183
- console.log(chalk8.yellow(" To fix:"));
1249
+ console.log(chalk9.red(` ${proj.projectName}: Token is invalid or expired`));
1250
+ console.log(chalk9.yellow(` Token source: ${tokenSourceLabel(source)}`));
1251
+ console.log(chalk9.yellow(" To fix:"));
1184
1252
  for (const step of tokenFixInstructions(source)) {
1185
- console.log(chalk8.yellow(` \u2192 ${step}`));
1253
+ console.log(chalk9.yellow(` \u2192 ${step}`));
1186
1254
  }
1187
1255
  } else if (err instanceof VercelApiError && err.statusCode === 403) {
1188
- console.log(chalk8.yellow(` ${proj.projectName}: ${err.message}`));
1189
- console.log(chalk8.dim(` Token source: ${tokenSourceLabel(source)}`));
1190
- console.log(chalk8.dim(" Ensure the token has access to this team/project"));
1191
- console.log(chalk8.dim(" Check: https://vercel.com/account/tokens"));
1256
+ console.log(chalk9.yellow(` ${proj.projectName}: ${err.message}`));
1257
+ console.log(chalk9.dim(` Token source: ${tokenSourceLabel(source)}`));
1258
+ console.log(chalk9.dim(" Ensure the token has access to this team/project"));
1259
+ console.log(chalk9.dim(" Check: https://vercel.com/account/tokens"));
1192
1260
  } else {
1193
- console.log(chalk8.yellow(` ${proj.projectName}: ${err instanceof Error ? err.message : "API error"}`));
1261
+ console.log(chalk9.yellow(` ${proj.projectName}: ${err instanceof Error ? err.message : "API error"}`));
1194
1262
  }
1195
1263
  }
1196
1264
  }
@@ -1229,16 +1297,16 @@ async function listGitHubSecretNames(repoSlug) {
1229
1297
  async function checkGitHubSecrets(projectRoot, variables) {
1230
1298
  const slug = await detectGitHubRepoSlug(projectRoot);
1231
1299
  if (!slug) {
1232
- console.log(chalk8.dim(" GitHub checks skipped (no GitHub remote detected)"));
1300
+ console.log(chalk9.dim(" GitHub checks skipped (no GitHub remote detected)"));
1233
1301
  return null;
1234
1302
  }
1235
1303
  const secretNames = await listGitHubSecretNames(slug);
1236
1304
  if (!secretNames) {
1237
- console.log(chalk8.dim(" GitHub checks skipped (gh CLI not available or not authenticated)"));
1305
+ console.log(chalk9.dim(" GitHub checks skipped (gh CLI not available or not authenticated)"));
1238
1306
  return slug;
1239
1307
  }
1240
1308
  const secretSet = new Set(secretNames);
1241
- console.log(chalk8.dim(` GitHub secrets: ${secretNames.length} found in ${slug}`));
1309
+ console.log(chalk9.dim(` GitHub secrets: ${secretNames.length} found in ${slug}`));
1242
1310
  for (const v of variables) {
1243
1311
  if (v.requiredIn.github) {
1244
1312
  v.githubStatus = secretSet.has(v.name) ? "present" : "missing";
@@ -1803,74 +1871,6 @@ var init_html_generator = __esm({
1803
1871
  }
1804
1872
  });
1805
1873
 
1806
- // dist/check-update.js
1807
- import chalk9 from "chalk";
1808
- async function fetchLatest() {
1809
- try {
1810
- const controller = new AbortController();
1811
- const timeout = setTimeout(() => controller.abort(), 3e3);
1812
- const res = await fetch("https://registry.npmjs.org/md4ai/latest", {
1813
- signal: controller.signal
1814
- });
1815
- clearTimeout(timeout);
1816
- if (!res.ok)
1817
- return null;
1818
- const data = await res.json();
1819
- return data.version ?? null;
1820
- } catch {
1821
- return null;
1822
- }
1823
- }
1824
- function printUpdateBanner(latest) {
1825
- console.log("");
1826
- console.log(chalk9.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"));
1827
- console.log(chalk9.yellow("\u2502") + chalk9.bold(" Update available! ") + chalk9.dim(`${CURRENT_VERSION}`) + chalk9.white(" \u2192 ") + chalk9.green.bold(`${latest}`) + " " + chalk9.yellow("\u2502"));
1828
- console.log(chalk9.yellow("\u2502") + " " + chalk9.yellow("\u2502"));
1829
- console.log(chalk9.yellow("\u2502") + " Run: " + chalk9.cyan("md4ai update") + " " + chalk9.yellow("\u2502"));
1830
- console.log(chalk9.yellow("\u2502") + " " + chalk9.yellow("\u2502"));
1831
- console.log(chalk9.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"));
1832
- console.log("");
1833
- }
1834
- async function checkForUpdate() {
1835
- const latest = await fetchLatest();
1836
- if (!latest)
1837
- return;
1838
- if (latest !== CURRENT_VERSION && isNewer(latest, CURRENT_VERSION)) {
1839
- printUpdateBanner(latest);
1840
- } else {
1841
- console.log(chalk9.green(`md4ai v${CURRENT_VERSION} \u2014 you're on the latest version.`));
1842
- }
1843
- }
1844
- async function autoCheckForUpdate() {
1845
- try {
1846
- const latest = await fetchLatest();
1847
- if (!latest)
1848
- return;
1849
- if (latest !== CURRENT_VERSION && isNewer(latest, CURRENT_VERSION)) {
1850
- printUpdateBanner(latest);
1851
- }
1852
- } catch {
1853
- }
1854
- }
1855
- function isNewer(a, b) {
1856
- const pa = a.split(".").map(Number);
1857
- const pb = b.split(".").map(Number);
1858
- for (let i = 0; i < 3; i++) {
1859
- if ((pa[i] ?? 0) > (pb[i] ?? 0))
1860
- return true;
1861
- if ((pa[i] ?? 0) < (pb[i] ?? 0))
1862
- return false;
1863
- }
1864
- return false;
1865
- }
1866
- var CURRENT_VERSION;
1867
- var init_check_update = __esm({
1868
- "dist/check-update.js"() {
1869
- "use strict";
1870
- CURRENT_VERSION = true ? "0.10.3" : "0.0.0-dev";
1871
- }
1872
- });
1873
-
1874
1874
  // dist/commands/push-toolings.js
1875
1875
  async function pushToolings(supabase, folderId, toolings) {
1876
1876
  if (!toolings.length)
@@ -3116,28 +3116,71 @@ var init_mcp_watch = __esm({
3116
3116
  });
3117
3117
 
3118
3118
  // dist/index.js
3119
- init_login();
3120
3119
  import { Command } from "commander";
3121
3120
 
3121
+ // dist/sentry.js
3122
+ init_check_update();
3123
+ import * as Sentry from "@sentry/node";
3124
+ var DSN = process.env.MD4AI_SENTRY_DSN;
3125
+ function initSentry() {
3126
+ if (!DSN)
3127
+ return;
3128
+ Sentry.init({
3129
+ dsn: DSN,
3130
+ release: `md4ai-cli@${CURRENT_VERSION}`,
3131
+ environment: process.env.NODE_ENV === "development" ? "development" : "production",
3132
+ tracesSampleRate: 0,
3133
+ beforeSend(event) {
3134
+ if (event.exception?.values) {
3135
+ for (const exc of event.exception.values) {
3136
+ if (exc.stacktrace?.frames) {
3137
+ for (const frame of exc.stacktrace.frames) {
3138
+ if (frame.filename) {
3139
+ const idx = frame.filename.indexOf("md4ai");
3140
+ if (idx >= 0)
3141
+ frame.filename = frame.filename.slice(idx);
3142
+ }
3143
+ }
3144
+ }
3145
+ }
3146
+ }
3147
+ return event;
3148
+ }
3149
+ });
3150
+ }
3151
+ function captureException2(err) {
3152
+ if (!DSN)
3153
+ return;
3154
+ Sentry.captureException(err);
3155
+ }
3156
+ async function flushSentry() {
3157
+ if (!DSN)
3158
+ return;
3159
+ await Sentry.flush(2e3);
3160
+ }
3161
+
3162
+ // dist/index.js
3163
+ init_login();
3164
+
3122
3165
  // dist/commands/logout.js
3123
3166
  init_config();
3124
- import chalk2 from "chalk";
3167
+ import chalk3 from "chalk";
3125
3168
  async function logoutCommand() {
3126
3169
  await clearCredentials();
3127
- console.log(chalk2.green("Logged out successfully."));
3170
+ console.log(chalk3.green("Logged out successfully."));
3128
3171
  }
3129
3172
 
3130
3173
  // dist/commands/status.js
3131
3174
  init_dist();
3132
3175
  init_config();
3133
- import chalk3 from "chalk";
3176
+ import chalk4 from "chalk";
3134
3177
  async function statusCommand() {
3135
3178
  const creds = await loadCredentials();
3136
3179
  if (!creds?.accessToken) {
3137
- console.log(chalk3.yellow("Not logged in. Run: md4ai login"));
3180
+ console.log(chalk4.yellow("Not logged in. Run: md4ai login"));
3138
3181
  return;
3139
3182
  }
3140
- console.log(chalk3.blue("MD4AI Status\n"));
3183
+ console.log(chalk4.blue("MD4AI Status\n"));
3141
3184
  console.log(` User: ${creds.email}`);
3142
3185
  console.log(` Expires: ${new Date(creds.expiresAt).toLocaleString()}`);
3143
3186
  try {
@@ -3150,13 +3193,13 @@ async function statusCommand() {
3150
3193
  const state = await loadState();
3151
3194
  console.log(` Last sync: ${state.lastSyncAt ?? "never"}`);
3152
3195
  } catch {
3153
- console.log(chalk3.yellow(" (Could not fetch remote data)"));
3196
+ console.log(chalk4.yellow(" (Could not fetch remote data)"));
3154
3197
  }
3155
3198
  }
3156
3199
 
3157
3200
  // dist/commands/add-folder.js
3158
3201
  init_auth();
3159
- import chalk5 from "chalk";
3202
+ import chalk6 from "chalk";
3160
3203
  import { input as input3, select } from "@inquirer/prompts";
3161
3204
  async function addFolderCommand() {
3162
3205
  const { supabase, userId } = await getAuthenticatedClient();
@@ -3195,11 +3238,11 @@ async function addFolderCommand() {
3195
3238
  team_id: teamId
3196
3239
  }).select().single();
3197
3240
  if (error) {
3198
- console.error(chalk5.red(`Failed to create folder: ${error.message}`));
3241
+ console.error(chalk6.red(`Failed to create folder: ${error.message}`));
3199
3242
  process.exit(1);
3200
3243
  }
3201
3244
  const teamLabel = teamId ? `(team: ${allTeams.get(teamId)})` : "(personal)";
3202
- console.log(chalk5.green(`
3245
+ console.log(chalk6.green(`
3203
3246
  Folder "${name}" created ${teamLabel} (${data.id})`));
3204
3247
  }
3205
3248
 
@@ -3207,7 +3250,7 @@ Folder "${name}" created ${teamLabel} (${data.id})`));
3207
3250
  init_auth();
3208
3251
  import { resolve } from "node:path";
3209
3252
  import { hostname, platform } from "node:os";
3210
- import chalk6 from "chalk";
3253
+ import chalk7 from "chalk";
3211
3254
  import { input as input4, select as select2 } from "@inquirer/prompts";
3212
3255
  function detectOs() {
3213
3256
  const p = platform();
@@ -3228,14 +3271,14 @@ function suggestDeviceName() {
3228
3271
  async function addDeviceCommand() {
3229
3272
  const { supabase, userId } = await getAuthenticatedClient();
3230
3273
  const cwd = resolve(process.cwd());
3231
- console.log(chalk6.blue.bold("\n\u{1F4C2} Where is this Claude project?\n"));
3274
+ console.log(chalk7.blue.bold("\n\u{1F4C2} Where is this Claude project?\n"));
3232
3275
  const localPath = await input4({
3233
3276
  message: "Local project path:",
3234
3277
  default: cwd
3235
3278
  });
3236
3279
  const { data: folders, error: foldersErr } = await supabase.from("claude_folders").select("id, name").order("name");
3237
3280
  if (foldersErr || !folders?.length) {
3238
- console.error(chalk6.red("No folders found. Run: md4ai add-folder"));
3281
+ console.error(chalk7.red("No folders found. Run: md4ai add-folder"));
3239
3282
  process.exit(1);
3240
3283
  }
3241
3284
  const folderId = await select2({
@@ -3268,16 +3311,16 @@ async function addDeviceCommand() {
3268
3311
  description: description || null
3269
3312
  });
3270
3313
  if (error) {
3271
- console.error(chalk6.red(`Failed to add device: ${error.message}`));
3314
+ console.error(chalk7.red(`Failed to add device: ${error.message}`));
3272
3315
  process.exit(1);
3273
3316
  }
3274
- console.log(chalk6.green(`
3317
+ console.log(chalk7.green(`
3275
3318
  Device "${deviceName}" added to folder.`));
3276
3319
  }
3277
3320
 
3278
3321
  // dist/commands/list-devices.js
3279
3322
  init_auth();
3280
- import chalk7 from "chalk";
3323
+ import chalk8 from "chalk";
3281
3324
  async function listDevicesCommand() {
3282
3325
  const { supabase } = await getAuthenticatedClient();
3283
3326
  const { data: devices, error } = await supabase.from("device_paths").select(`
@@ -3285,11 +3328,11 @@ async function listDevicesCommand() {
3285
3328
  claude_folders!inner ( name )
3286
3329
  `).order("device_name");
3287
3330
  if (error) {
3288
- console.error(chalk7.red(`Failed to list devices: ${error.message}`));
3331
+ console.error(chalk8.red(`Failed to list devices: ${error.message}`));
3289
3332
  process.exit(1);
3290
3333
  }
3291
3334
  if (!devices?.length) {
3292
- console.log(chalk7.yellow("No devices found. Run: md4ai add-device"));
3335
+ console.log(chalk8.yellow("No devices found. Run: md4ai add-device"));
3293
3336
  return;
3294
3337
  }
3295
3338
  const grouped = /* @__PURE__ */ new Map();
@@ -3300,12 +3343,12 @@ async function listDevicesCommand() {
3300
3343
  }
3301
3344
  for (const [deviceName, entries] of grouped) {
3302
3345
  const first = entries[0];
3303
- console.log(chalk7.bold(`
3304
- ${deviceName}`) + chalk7.dim(` (${first.os_type})`));
3346
+ console.log(chalk8.bold(`
3347
+ ${deviceName}`) + chalk8.dim(` (${first.os_type})`));
3305
3348
  for (const entry of entries) {
3306
3349
  const folderName = entry.claude_folders?.name ?? "unknown";
3307
3350
  const synced = entry.last_synced ? new Date(entry.last_synced).toLocaleString() : "never";
3308
- console.log(` ${chalk7.cyan(folderName)} \u2192 ${entry.path}`);
3351
+ console.log(` ${chalk8.cyan(folderName)} \u2192 ${entry.path}`);
3309
3352
  console.log(` Last synced: ${synced}`);
3310
3353
  }
3311
3354
  }
@@ -4318,6 +4361,7 @@ var admin = program.command("admin").description("Admin commands for managing th
4318
4361
  admin.command("update-tool").description("Add or update a tool in the master registry").requiredOption("--name <name>", "Canonical tool name (e.g. next, playwright)").option("--display <display>", 'Human-friendly display name (e.g. "Next.js")').option("--category <category>", "Tool category (framework|runtime|cli|mcp|package|database|other)").option("--stable <version>", "Latest stable version").option("--beta <version>", "Latest beta/RC version").option("--source <url>", "Source of truth URL for checking versions").option("--install <url>", "Download/install link").option("--notes <text>", "Compatibility notes or warnings").action(adminUpdateToolCommand);
4319
4362
  admin.command("list-tools").description("List all tools in the master registry").action(adminListToolsCommand);
4320
4363
  admin.command("fetch-versions").description("Fetch latest stable and beta versions from npm and GitHub").action(adminFetchVersionsCommand);
4364
+ initSentry();
4321
4365
  if (process.argv.length <= 2) {
4322
4366
  void startCommand();
4323
4367
  } else {
@@ -4328,3 +4372,11 @@ if (process.argv.length <= 2) {
4328
4372
  autoCheckForUpdate();
4329
4373
  }
4330
4374
  }
4375
+ process.on("uncaughtException", async (err) => {
4376
+ captureException2(err);
4377
+ await flushSentry();
4378
+ process.exit(1);
4379
+ });
4380
+ process.on("unhandledRejection", (reason) => {
4381
+ captureException2(reason);
4382
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md4ai",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "description": "CLI for MD4AI — scan Claude projects and sync to your dashboard",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  "build": "tsc",
15
15
  "bundle": "node esbuild.config.js",
16
16
  "prepublishOnly": "pnpm build && pnpm bundle",
17
+ "test": "vitest run",
17
18
  "dev": "tsc --watch",
18
19
  "clean": "rm -rf dist"
19
20
  },
@@ -34,6 +35,7 @@
34
35
  },
35
36
  "dependencies": {
36
37
  "@inquirer/prompts": "^8.3.0",
38
+ "@sentry/node": "^10.42.0",
37
39
  "@supabase/supabase-js": "^2.98.0",
38
40
  "chalk": "^5.6.2",
39
41
  "commander": "^14.0.3",
@@ -43,6 +45,7 @@
43
45
  "@md4ai/shared": "workspace:*",
44
46
  "@types/node": "^22.19.15",
45
47
  "esbuild": "^0.27.3",
46
- "typescript": "^5.7.0"
48
+ "typescript": "^5.7.0",
49
+ "vitest": "^4.0.18"
47
50
  }
48
51
  }