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 +98 -76
- package/dist/index.bundled.js +169 -117
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,100 +1,136 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
+
---
|
|
6
14
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
+
# 2. Log in
|
|
48
|
+
md4ai login
|
|
18
49
|
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
```
|
|
54
|
+
# 4. That's it — your scan results are live at md4ai.com
|
|
55
|
+
```
|
|
24
56
|
|
|
25
|
-
|
|
57
|
+
Or scan without an account:
|
|
26
58
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
59
|
+
```bash
|
|
60
|
+
md4ai scan --offline # generates output/index.html
|
|
61
|
+
```
|
|
30
62
|
|
|
31
|
-
|
|
63
|
+
## What You Get
|
|
32
64
|
|
|
33
|
-
|
|
34
|
-
cd /path/to/your-claude-project
|
|
35
|
-
md4ai link <project-id>
|
|
36
|
-
```
|
|
65
|
+
### Dependency Graph
|
|
37
66
|
|
|
38
|
-
|
|
67
|
+
See how your CLAUDE.md, skills, hooks, and config files connect. Search, zoom, and print wall sheets.
|
|
39
68
|
|
|
40
|
-
|
|
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
|
-
###
|
|
73
|
+
### Orphan Detection
|
|
43
74
|
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
65
|
-
| `md4ai
|
|
66
|
-
| `md4ai
|
|
67
|
-
| `md4ai
|
|
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
|
-
###
|
|
108
|
+
### Analysis
|
|
72
109
|
|
|
73
110
|
| Command | Description |
|
|
74
111
|
|---------|-------------|
|
|
75
|
-
| `md4ai
|
|
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
|
-
###
|
|
116
|
+
### Account & Devices
|
|
78
117
|
|
|
79
118
|
| Command | Description |
|
|
80
119
|
|---------|-------------|
|
|
81
|
-
| `md4ai
|
|
82
|
-
| `md4ai
|
|
83
|
-
| `md4ai
|
|
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
|
-
|
|
125
|
+
### One Command to Rule Them All
|
|
86
126
|
|
|
87
|
-
|
|
127
|
+
```bash
|
|
128
|
+
md4ai start # or just: md4ai
|
|
129
|
+
```
|
|
88
130
|
|
|
89
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
package/dist/index.bundled.js
CHANGED
|
@@ -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
|
|
223
|
+
import chalk2 from "chalk";
|
|
156
224
|
import { input, password } from "@inquirer/prompts";
|
|
157
225
|
async function loginCommand() {
|
|
158
|
-
console.log(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
1147
|
-
console.log(
|
|
1148
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
1182
|
-
console.log(
|
|
1183
|
-
console.log(
|
|
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(
|
|
1253
|
+
console.log(chalk9.yellow(` \u2192 ${step}`));
|
|
1186
1254
|
}
|
|
1187
1255
|
} else if (err instanceof VercelApiError && err.statusCode === 403) {
|
|
1188
|
-
console.log(
|
|
1189
|
-
console.log(
|
|
1190
|
-
console.log(
|
|
1191
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
3167
|
+
import chalk3 from "chalk";
|
|
3125
3168
|
async function logoutCommand() {
|
|
3126
3169
|
await clearCredentials();
|
|
3127
|
-
console.log(
|
|
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
|
|
3176
|
+
import chalk4 from "chalk";
|
|
3134
3177
|
async function statusCommand() {
|
|
3135
3178
|
const creds = await loadCredentials();
|
|
3136
3179
|
if (!creds?.accessToken) {
|
|
3137
|
-
console.log(
|
|
3180
|
+
console.log(chalk4.yellow("Not logged in. Run: md4ai login"));
|
|
3138
3181
|
return;
|
|
3139
3182
|
}
|
|
3140
|
-
console.log(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
3314
|
+
console.error(chalk7.red(`Failed to add device: ${error.message}`));
|
|
3272
3315
|
process.exit(1);
|
|
3273
3316
|
}
|
|
3274
|
-
console.log(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
3304
|
-
${deviceName}`) +
|
|
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(` ${
|
|
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
|
+
"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
|
}
|