md4ai 0.6.2 → 0.7.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.
- package/dist/index.bundled.js +422 -69
- package/package.json +7 -2
package/dist/index.bundled.js
CHANGED
|
@@ -319,13 +319,13 @@ ${deviceName}`) + chalk7.dim(` (${first.os_type})`));
|
|
|
319
319
|
// dist/commands/map.js
|
|
320
320
|
import { resolve as resolve3 } from "node:path";
|
|
321
321
|
import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
|
|
322
|
-
import { existsSync as
|
|
322
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
323
323
|
import chalk9 from "chalk";
|
|
324
324
|
|
|
325
325
|
// dist/scanner/index.js
|
|
326
326
|
import { readdir as readdir2 } from "node:fs/promises";
|
|
327
|
-
import { join as
|
|
328
|
-
import { existsSync as
|
|
327
|
+
import { join as join8, relative as relative2 } from "node:path";
|
|
328
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
329
329
|
import { homedir as homedir5 } from "node:os";
|
|
330
330
|
import { createHash } from "node:crypto";
|
|
331
331
|
|
|
@@ -923,13 +923,211 @@ async function detectFromMcpSettings(projectRoot) {
|
|
|
923
923
|
return toolings;
|
|
924
924
|
}
|
|
925
925
|
|
|
926
|
+
// dist/scanner/env-manifest-scanner.js
|
|
927
|
+
import { readFile as readFile5, glob } from "node:fs/promises";
|
|
928
|
+
import { join as join7, relative } from "node:path";
|
|
929
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
930
|
+
var MANIFEST_PATH = "docs/reference/env-manifest.md";
|
|
931
|
+
async function scanEnvManifest(projectRoot) {
|
|
932
|
+
const manifestFullPath = join7(projectRoot, MANIFEST_PATH);
|
|
933
|
+
if (!existsSync5(manifestFullPath)) {
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
const manifestContent = await readFile5(manifestFullPath, "utf-8");
|
|
937
|
+
const apps = parseApps(manifestContent);
|
|
938
|
+
const variables = parseVariables(manifestContent);
|
|
939
|
+
const localPresence = await checkLocalEnvFiles(projectRoot, apps);
|
|
940
|
+
for (const v of variables) {
|
|
941
|
+
v.localStatus = {};
|
|
942
|
+
for (const app of v.requiredIn.local) {
|
|
943
|
+
v.localStatus[app] = localPresence.get(app)?.has(v.name) ? "present" : "missing";
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const workflowRefs = await parseWorkflowSecrets(projectRoot);
|
|
947
|
+
const drift = computeDrift(variables, apps, localPresence, workflowRefs);
|
|
948
|
+
return {
|
|
949
|
+
manifestFound: true,
|
|
950
|
+
manifestPath: MANIFEST_PATH,
|
|
951
|
+
apps: apps.map((a) => a.name),
|
|
952
|
+
variables,
|
|
953
|
+
drift,
|
|
954
|
+
workflowRefs,
|
|
955
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
function parseApps(content) {
|
|
959
|
+
const apps = [];
|
|
960
|
+
const appsSection = extractSection(content, "Apps");
|
|
961
|
+
if (!appsSection)
|
|
962
|
+
return apps;
|
|
963
|
+
const lineRe = /^-\s+(\w+):\s+`([^`]+)`/;
|
|
964
|
+
for (const line of appsSection.split("\n")) {
|
|
965
|
+
const m = line.match(lineRe);
|
|
966
|
+
if (m) {
|
|
967
|
+
apps.push({ name: m[1], envFilePath: m[2] });
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return apps;
|
|
971
|
+
}
|
|
972
|
+
function parseVariables(content) {
|
|
973
|
+
const variables = [];
|
|
974
|
+
const varsSection = extractSection(content, "Variables");
|
|
975
|
+
if (!varsSection)
|
|
976
|
+
return variables;
|
|
977
|
+
const lines = varsSection.split("\n");
|
|
978
|
+
let tableStarted = false;
|
|
979
|
+
for (const line of lines) {
|
|
980
|
+
const trimmed = line.trim();
|
|
981
|
+
if (!trimmed.startsWith("|"))
|
|
982
|
+
continue;
|
|
983
|
+
if (trimmed.includes("Variable") && trimmed.includes("Public")) {
|
|
984
|
+
tableStarted = true;
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
if (/^\|[\s-|]+\|$/.test(trimmed)) {
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
if (!tableStarted)
|
|
991
|
+
continue;
|
|
992
|
+
const cells = trimmed.split("|").slice(1, -1).map((c) => c.trim());
|
|
993
|
+
if (cells.length < 7)
|
|
994
|
+
continue;
|
|
995
|
+
const [nameCell, publicCell, localCell, vercelCell, githubCell, supabaseCell, notesCell] = cells;
|
|
996
|
+
const nameMatch = nameCell.match(/`([^`]+)`/);
|
|
997
|
+
if (!nameMatch)
|
|
998
|
+
continue;
|
|
999
|
+
const name = nameMatch[1];
|
|
1000
|
+
const isPublic = publicCell.toLowerCase() === "yes";
|
|
1001
|
+
const parseAppList = (cell) => {
|
|
1002
|
+
if (cell === "---" || cell === "")
|
|
1003
|
+
return [];
|
|
1004
|
+
return cell.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1005
|
+
};
|
|
1006
|
+
const localApps = parseAppList(localCell);
|
|
1007
|
+
const vercelProjects = parseAppList(vercelCell);
|
|
1008
|
+
const githubNeeded = githubCell.toLowerCase() === "yes" || githubCell.includes("\u2713") || githubCell.includes("\u2714");
|
|
1009
|
+
const supabaseNeeded = supabaseCell.toLowerCase() === "source" || supabaseCell.toLowerCase() === "yes";
|
|
1010
|
+
variables.push({
|
|
1011
|
+
name,
|
|
1012
|
+
isPublic,
|
|
1013
|
+
requiredIn: {
|
|
1014
|
+
local: localApps,
|
|
1015
|
+
vercel: vercelProjects,
|
|
1016
|
+
github: githubNeeded,
|
|
1017
|
+
supabase: supabaseNeeded
|
|
1018
|
+
},
|
|
1019
|
+
localStatus: {},
|
|
1020
|
+
notes: notesCell || ""
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
return variables;
|
|
1024
|
+
}
|
|
1025
|
+
function extractSection(content, heading) {
|
|
1026
|
+
const re = new RegExp(`^##\\s+${heading}\\s*$`, "m");
|
|
1027
|
+
const match = re.exec(content);
|
|
1028
|
+
if (!match)
|
|
1029
|
+
return null;
|
|
1030
|
+
const start = match.index + match[0].length;
|
|
1031
|
+
const nextHeading = content.indexOf("\n## ", start);
|
|
1032
|
+
return nextHeading === -1 ? content.slice(start) : content.slice(start, nextHeading);
|
|
1033
|
+
}
|
|
1034
|
+
async function checkLocalEnvFiles(projectRoot, apps) {
|
|
1035
|
+
const result = /* @__PURE__ */ new Map();
|
|
1036
|
+
for (const app of apps) {
|
|
1037
|
+
const envPath = join7(projectRoot, app.envFilePath);
|
|
1038
|
+
const varNames = /* @__PURE__ */ new Set();
|
|
1039
|
+
if (existsSync5(envPath)) {
|
|
1040
|
+
const content = await readFile5(envPath, "utf-8");
|
|
1041
|
+
for (const line of content.split("\n")) {
|
|
1042
|
+
const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
1043
|
+
if (m) {
|
|
1044
|
+
varNames.add(m[1]);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
result.set(app.name, varNames);
|
|
1049
|
+
}
|
|
1050
|
+
return result;
|
|
1051
|
+
}
|
|
1052
|
+
async function parseWorkflowSecrets(projectRoot) {
|
|
1053
|
+
const workflowsDir = join7(projectRoot, ".github", "workflows");
|
|
1054
|
+
if (!existsSync5(workflowsDir))
|
|
1055
|
+
return [];
|
|
1056
|
+
const refs = [];
|
|
1057
|
+
const secretRe = /\$\{\{\s*secrets\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
|
|
1058
|
+
for (const ext of ["*.yml", "*.yaml"]) {
|
|
1059
|
+
for await (const filePath of glob(join7(workflowsDir, ext))) {
|
|
1060
|
+
const content = await readFile5(filePath, "utf-8");
|
|
1061
|
+
const secrets = /* @__PURE__ */ new Set();
|
|
1062
|
+
let m;
|
|
1063
|
+
while ((m = secretRe.exec(content)) !== null) {
|
|
1064
|
+
secrets.add(m[1]);
|
|
1065
|
+
}
|
|
1066
|
+
secretRe.lastIndex = 0;
|
|
1067
|
+
if (secrets.size > 0) {
|
|
1068
|
+
refs.push({
|
|
1069
|
+
workflowFile: relative(projectRoot, filePath),
|
|
1070
|
+
secretsUsed: [...secrets].sort()
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
return refs;
|
|
1076
|
+
}
|
|
1077
|
+
function computeDrift(variables, apps, localPresence, workflowRefs) {
|
|
1078
|
+
const missingFromLocal = [];
|
|
1079
|
+
const undocumented = [];
|
|
1080
|
+
const documentedNames = new Set(variables.map((v) => v.name));
|
|
1081
|
+
for (const v of variables) {
|
|
1082
|
+
for (const app of v.requiredIn.local) {
|
|
1083
|
+
if (v.localStatus[app] === "missing") {
|
|
1084
|
+
missingFromLocal.push({ variable: v.name, app });
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
for (const app of apps) {
|
|
1089
|
+
const present = localPresence.get(app.name);
|
|
1090
|
+
if (!present)
|
|
1091
|
+
continue;
|
|
1092
|
+
for (const varName of present) {
|
|
1093
|
+
if (!documentedNames.has(varName)) {
|
|
1094
|
+
undocumented.push({ variable: varName, app: app.name });
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
const allWorkflowSecrets = /* @__PURE__ */ new Set();
|
|
1099
|
+
for (const ref of workflowRefs) {
|
|
1100
|
+
for (const s of ref.secretsUsed) {
|
|
1101
|
+
allWorkflowSecrets.add(s);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
const workflowDeadSecrets = [];
|
|
1105
|
+
for (const v of variables) {
|
|
1106
|
+
if (v.requiredIn.github && !allWorkflowSecrets.has(v.name)) {
|
|
1107
|
+
workflowDeadSecrets.push(v.name);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
const workflowBrokenRefs = [];
|
|
1111
|
+
for (const secret of allWorkflowSecrets) {
|
|
1112
|
+
if (!documentedNames.has(secret)) {
|
|
1113
|
+
workflowBrokenRefs.push(secret);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return {
|
|
1117
|
+
missingFromLocal,
|
|
1118
|
+
undocumented,
|
|
1119
|
+
workflowDeadSecrets: workflowDeadSecrets.sort(),
|
|
1120
|
+
workflowBrokenRefs: workflowBrokenRefs.sort()
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
|
|
926
1124
|
// dist/scanner/index.js
|
|
927
1125
|
async function scanProject(projectRoot) {
|
|
928
1126
|
const allFiles = await discoverFiles(projectRoot);
|
|
929
1127
|
const rootFiles = identifyRoots(allFiles, projectRoot);
|
|
930
1128
|
const allRefs = [];
|
|
931
1129
|
for (const file of allFiles) {
|
|
932
|
-
const fullPath = file.startsWith("/") ? file :
|
|
1130
|
+
const fullPath = file.startsWith("/") ? file : join8(projectRoot, file);
|
|
933
1131
|
try {
|
|
934
1132
|
const refs = await parseFileReferences(fullPath, projectRoot);
|
|
935
1133
|
allRefs.push(...refs);
|
|
@@ -941,7 +1139,8 @@ async function scanProject(projectRoot) {
|
|
|
941
1139
|
const staleFiles = detectStaleFiles(allFiles, projectRoot);
|
|
942
1140
|
const skills = await parseSkills(projectRoot);
|
|
943
1141
|
const toolings = await detectToolings(projectRoot);
|
|
944
|
-
const
|
|
1142
|
+
const envManifest = await scanEnvManifest(projectRoot);
|
|
1143
|
+
const scanData = JSON.stringify({ graph, orphans, skills, staleFiles, toolings, envManifest });
|
|
945
1144
|
const dataHash = createHash("sha256").update(scanData).digest("hex");
|
|
946
1145
|
return {
|
|
947
1146
|
graph,
|
|
@@ -949,24 +1148,25 @@ async function scanProject(projectRoot) {
|
|
|
949
1148
|
skills,
|
|
950
1149
|
staleFiles,
|
|
951
1150
|
toolings,
|
|
1151
|
+
envManifest,
|
|
952
1152
|
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
953
1153
|
dataHash
|
|
954
1154
|
};
|
|
955
1155
|
}
|
|
956
1156
|
async function discoverFiles(projectRoot) {
|
|
957
1157
|
const files = [];
|
|
958
|
-
const claudeDir =
|
|
959
|
-
if (
|
|
1158
|
+
const claudeDir = join8(projectRoot, ".claude");
|
|
1159
|
+
if (existsSync6(claudeDir)) {
|
|
960
1160
|
await walkDir(claudeDir, projectRoot, files);
|
|
961
1161
|
}
|
|
962
|
-
if (
|
|
1162
|
+
if (existsSync6(join8(projectRoot, "CLAUDE.md"))) {
|
|
963
1163
|
files.push("CLAUDE.md");
|
|
964
1164
|
}
|
|
965
|
-
if (
|
|
1165
|
+
if (existsSync6(join8(projectRoot, "skills.md"))) {
|
|
966
1166
|
files.push("skills.md");
|
|
967
1167
|
}
|
|
968
|
-
const plansDir =
|
|
969
|
-
if (
|
|
1168
|
+
const plansDir = join8(projectRoot, "docs", "plans");
|
|
1169
|
+
if (existsSync6(plansDir)) {
|
|
970
1170
|
await walkDir(plansDir, projectRoot, files);
|
|
971
1171
|
}
|
|
972
1172
|
return [...new Set(files)];
|
|
@@ -974,13 +1174,13 @@ async function discoverFiles(projectRoot) {
|
|
|
974
1174
|
async function walkDir(dir, projectRoot, files) {
|
|
975
1175
|
const entries = await readdir2(dir, { withFileTypes: true });
|
|
976
1176
|
for (const entry of entries) {
|
|
977
|
-
const fullPath =
|
|
1177
|
+
const fullPath = join8(dir, entry.name);
|
|
978
1178
|
if (entry.isDirectory()) {
|
|
979
1179
|
if (["node_modules", ".git", ".turbo", "cache", "session-env"].includes(entry.name))
|
|
980
1180
|
continue;
|
|
981
1181
|
await walkDir(fullPath, projectRoot, files);
|
|
982
1182
|
} else {
|
|
983
|
-
const relPath =
|
|
1183
|
+
const relPath = relative2(projectRoot, fullPath);
|
|
984
1184
|
files.push(relPath);
|
|
985
1185
|
}
|
|
986
1186
|
}
|
|
@@ -994,16 +1194,16 @@ function identifyRoots(allFiles, projectRoot) {
|
|
|
994
1194
|
}
|
|
995
1195
|
for (const globalFile of GLOBAL_ROOT_FILES) {
|
|
996
1196
|
const expanded = globalFile.replace("~", homedir5());
|
|
997
|
-
if (
|
|
1197
|
+
if (existsSync6(expanded)) {
|
|
998
1198
|
roots.push(globalFile);
|
|
999
1199
|
}
|
|
1000
1200
|
}
|
|
1001
1201
|
return roots;
|
|
1002
1202
|
}
|
|
1003
1203
|
async function readClaudeConfigFiles(projectRoot) {
|
|
1004
|
-
const { readFile:
|
|
1005
|
-
const { join:
|
|
1006
|
-
const { existsSync:
|
|
1204
|
+
const { readFile: readFile10, stat, glob: glob2 } = await import("node:fs/promises");
|
|
1205
|
+
const { join: join14, relative: relative4 } = await import("node:path");
|
|
1206
|
+
const { existsSync: existsSync13 } = await import("node:fs");
|
|
1007
1207
|
const configPatterns = [
|
|
1008
1208
|
"CLAUDE.md",
|
|
1009
1209
|
".claude/CLAUDE.md",
|
|
@@ -1013,15 +1213,15 @@ async function readClaudeConfigFiles(projectRoot) {
|
|
|
1013
1213
|
];
|
|
1014
1214
|
const files = [];
|
|
1015
1215
|
for (const pattern of configPatterns) {
|
|
1016
|
-
for await (const fullPath of
|
|
1017
|
-
if (!
|
|
1216
|
+
for await (const fullPath of glob2(join14(projectRoot, pattern))) {
|
|
1217
|
+
if (!existsSync13(fullPath))
|
|
1018
1218
|
continue;
|
|
1019
1219
|
try {
|
|
1020
|
-
const content = await
|
|
1220
|
+
const content = await readFile10(fullPath, "utf-8");
|
|
1021
1221
|
const fileStat = await stat(fullPath);
|
|
1022
1222
|
const lastMod = getGitLastModified(fullPath, projectRoot);
|
|
1023
1223
|
files.push({
|
|
1024
|
-
filePath:
|
|
1224
|
+
filePath: relative4(projectRoot, fullPath),
|
|
1025
1225
|
content,
|
|
1026
1226
|
sizeBytes: fileStat.size,
|
|
1027
1227
|
lastModified: lastMod
|
|
@@ -1036,7 +1236,7 @@ function detectStaleFiles(allFiles, projectRoot) {
|
|
|
1036
1236
|
const stale = [];
|
|
1037
1237
|
const now = Date.now();
|
|
1038
1238
|
for (const file of allFiles) {
|
|
1039
|
-
const fullPath =
|
|
1239
|
+
const fullPath = join8(projectRoot, file);
|
|
1040
1240
|
const lastMod = getGitLastModified(fullPath, projectRoot);
|
|
1041
1241
|
if (lastMod) {
|
|
1042
1242
|
const days = Math.floor((now - new Date(lastMod).getTime()) / (1e3 * 60 * 60 * 24));
|
|
@@ -1227,7 +1427,7 @@ async function pushToolings(supabase, folderId, toolings) {
|
|
|
1227
1427
|
async function mapCommand(path, options) {
|
|
1228
1428
|
await checkForUpdate();
|
|
1229
1429
|
const projectRoot = resolve3(path ?? process.cwd());
|
|
1230
|
-
if (!
|
|
1430
|
+
if (!existsSync7(projectRoot)) {
|
|
1231
1431
|
console.error(chalk9.red(`Path not found: ${projectRoot}`));
|
|
1232
1432
|
process.exit(1);
|
|
1233
1433
|
}
|
|
@@ -1240,9 +1440,10 @@ async function mapCommand(path, options) {
|
|
|
1240
1440
|
console.log(` Stale files: ${result.staleFiles.length}`);
|
|
1241
1441
|
console.log(` Skills: ${result.skills.length}`);
|
|
1242
1442
|
console.log(` Toolings: ${result.toolings.length}`);
|
|
1443
|
+
console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
|
|
1243
1444
|
console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
|
|
1244
1445
|
const outputDir = resolve3(projectRoot, "output");
|
|
1245
|
-
if (!
|
|
1446
|
+
if (!existsSync7(outputDir)) {
|
|
1246
1447
|
await mkdir2(outputDir, { recursive: true });
|
|
1247
1448
|
}
|
|
1248
1449
|
const htmlPath = resolve3(outputDir, "index.html");
|
|
@@ -1294,6 +1495,7 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
1294
1495
|
orphans_json: result.orphans,
|
|
1295
1496
|
skills_table_json: result.skills,
|
|
1296
1497
|
stale_files_json: result.staleFiles,
|
|
1498
|
+
env_manifest_json: result.envManifest,
|
|
1297
1499
|
last_scanned: result.scannedAt,
|
|
1298
1500
|
data_hash: result.dataHash
|
|
1299
1501
|
}).eq("id", folder_id);
|
|
@@ -1335,8 +1537,8 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
1335
1537
|
}
|
|
1336
1538
|
|
|
1337
1539
|
// dist/commands/simulate.js
|
|
1338
|
-
import { join as
|
|
1339
|
-
import { existsSync as
|
|
1540
|
+
import { join as join9 } from "node:path";
|
|
1541
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
1340
1542
|
import { homedir as homedir6 } from "node:os";
|
|
1341
1543
|
import chalk10 from "chalk";
|
|
1342
1544
|
async function simulateCommand(prompt) {
|
|
@@ -1344,24 +1546,24 @@ async function simulateCommand(prompt) {
|
|
|
1344
1546
|
console.log(chalk10.blue(`Simulating prompt: "${prompt}"
|
|
1345
1547
|
`));
|
|
1346
1548
|
console.log(chalk10.dim("Files Claude would load:\n"));
|
|
1347
|
-
const globalClaude =
|
|
1348
|
-
if (
|
|
1549
|
+
const globalClaude = join9(homedir6(), ".claude", "CLAUDE.md");
|
|
1550
|
+
if (existsSync8(globalClaude)) {
|
|
1349
1551
|
console.log(chalk10.green(" \u2713 ~/.claude/CLAUDE.md (global)"));
|
|
1350
1552
|
}
|
|
1351
1553
|
for (const rootFile of ROOT_FILES) {
|
|
1352
|
-
const fullPath =
|
|
1353
|
-
if (
|
|
1554
|
+
const fullPath = join9(projectRoot, rootFile);
|
|
1555
|
+
if (existsSync8(fullPath)) {
|
|
1354
1556
|
console.log(chalk10.green(` \u2713 ${rootFile} (project root)`));
|
|
1355
1557
|
}
|
|
1356
1558
|
}
|
|
1357
1559
|
const settingsFiles = [
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1560
|
+
join9(homedir6(), ".claude", "settings.json"),
|
|
1561
|
+
join9(homedir6(), ".claude", "settings.local.json"),
|
|
1562
|
+
join9(projectRoot, ".claude", "settings.json"),
|
|
1563
|
+
join9(projectRoot, ".claude", "settings.local.json")
|
|
1362
1564
|
];
|
|
1363
1565
|
for (const sf of settingsFiles) {
|
|
1364
|
-
if (
|
|
1566
|
+
if (existsSync8(sf)) {
|
|
1365
1567
|
const display = sf.startsWith(homedir6()) ? sf.replace(homedir6(), "~") : sf.replace(projectRoot + "/", "");
|
|
1366
1568
|
console.log(chalk10.green(` \u2713 ${display} (settings)`));
|
|
1367
1569
|
}
|
|
@@ -1369,8 +1571,8 @@ async function simulateCommand(prompt) {
|
|
|
1369
1571
|
const words = prompt.split(/\s+/);
|
|
1370
1572
|
for (const word of words) {
|
|
1371
1573
|
const cleaned = word.replace(/['"]/g, "");
|
|
1372
|
-
const candidatePath =
|
|
1373
|
-
if (
|
|
1574
|
+
const candidatePath = join9(projectRoot, cleaned);
|
|
1575
|
+
if (existsSync8(candidatePath) && cleaned.includes("/")) {
|
|
1374
1576
|
console.log(chalk10.cyan(` \u2192 ${cleaned} (referenced in prompt)`));
|
|
1375
1577
|
}
|
|
1376
1578
|
}
|
|
@@ -1378,18 +1580,18 @@ async function simulateCommand(prompt) {
|
|
|
1378
1580
|
}
|
|
1379
1581
|
|
|
1380
1582
|
// dist/commands/print.js
|
|
1381
|
-
import { join as
|
|
1382
|
-
import { readFile as
|
|
1383
|
-
import { existsSync as
|
|
1583
|
+
import { join as join10 } from "node:path";
|
|
1584
|
+
import { readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
|
|
1585
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
1384
1586
|
import chalk11 from "chalk";
|
|
1385
1587
|
async function printCommand(title) {
|
|
1386
1588
|
const projectRoot = process.cwd();
|
|
1387
|
-
const scanDataPath =
|
|
1388
|
-
if (!
|
|
1589
|
+
const scanDataPath = join10(projectRoot, "output", "index.html");
|
|
1590
|
+
if (!existsSync9(scanDataPath)) {
|
|
1389
1591
|
console.error(chalk11.red("No scan data found. Run: md4ai scan"));
|
|
1390
1592
|
process.exit(1);
|
|
1391
1593
|
}
|
|
1392
|
-
const html = await
|
|
1594
|
+
const html = await readFile6(scanDataPath, "utf-8");
|
|
1393
1595
|
const match = html.match(/<script type="application\/json" id="scan-data">([\s\S]*?)<\/script>/);
|
|
1394
1596
|
if (!match) {
|
|
1395
1597
|
console.error(chalk11.red("Could not extract scan data from output/index.html"));
|
|
@@ -1397,7 +1599,7 @@ async function printCommand(title) {
|
|
|
1397
1599
|
}
|
|
1398
1600
|
const result = JSON.parse(match[1]);
|
|
1399
1601
|
const printHtml = generatePrintHtml(result, title);
|
|
1400
|
-
const outputPath =
|
|
1602
|
+
const outputPath = join10(projectRoot, "output", `print-${Date.now()}.html`);
|
|
1401
1603
|
await writeFile3(outputPath, printHtml, "utf-8");
|
|
1402
1604
|
console.log(chalk11.green(`Print-ready wall sheet: ${outputPath}`));
|
|
1403
1605
|
}
|
|
@@ -1478,6 +1680,7 @@ async function syncCommand(options) {
|
|
|
1478
1680
|
orphans_json: result.orphans,
|
|
1479
1681
|
skills_table_json: result.skills,
|
|
1480
1682
|
stale_files_json: result.staleFiles,
|
|
1683
|
+
env_manifest_json: result.envManifest,
|
|
1481
1684
|
last_scanned: result.scannedAt,
|
|
1482
1685
|
data_hash: result.dataHash
|
|
1483
1686
|
}).eq("id", device.folder_id);
|
|
@@ -1607,11 +1810,13 @@ Linking "${folder.name}" to this device...
|
|
|
1607
1810
|
console.log(` Orphans: ${result.orphans.length}`);
|
|
1608
1811
|
console.log(` Skills: ${result.skills.length}`);
|
|
1609
1812
|
console.log(` Toolings: ${result.toolings.length}`);
|
|
1813
|
+
console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
|
|
1610
1814
|
const { error: scanErr } = await supabase.from("claude_folders").update({
|
|
1611
1815
|
graph_json: result.graph,
|
|
1612
1816
|
orphans_json: result.orphans,
|
|
1613
1817
|
skills_table_json: result.skills,
|
|
1614
1818
|
stale_files_json: result.staleFiles,
|
|
1819
|
+
env_manifest_json: result.envManifest,
|
|
1615
1820
|
last_scanned: result.scannedAt,
|
|
1616
1821
|
data_hash: result.dataHash
|
|
1617
1822
|
}).eq("id", folder.id);
|
|
@@ -1647,18 +1852,18 @@ Linking "${folder.name}" to this device...
|
|
|
1647
1852
|
}
|
|
1648
1853
|
|
|
1649
1854
|
// dist/commands/import-bundle.js
|
|
1650
|
-
import { readFile as
|
|
1651
|
-
import { join as
|
|
1652
|
-
import { existsSync as
|
|
1855
|
+
import { readFile as readFile7, writeFile as writeFile4, mkdir as mkdir3 } from "node:fs/promises";
|
|
1856
|
+
import { join as join11, dirname as dirname2 } from "node:path";
|
|
1857
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
1653
1858
|
import chalk14 from "chalk";
|
|
1654
1859
|
import { confirm, input as input4 } from "@inquirer/prompts";
|
|
1655
1860
|
async function importBundleCommand(zipPath) {
|
|
1656
|
-
if (!
|
|
1861
|
+
if (!existsSync10(zipPath)) {
|
|
1657
1862
|
console.error(chalk14.red(`File not found: ${zipPath}`));
|
|
1658
1863
|
process.exit(1);
|
|
1659
1864
|
}
|
|
1660
1865
|
const JSZip = (await import("jszip")).default;
|
|
1661
|
-
const zipData = await
|
|
1866
|
+
const zipData = await readFile7(zipPath);
|
|
1662
1867
|
const zip = await JSZip.loadAsync(zipData);
|
|
1663
1868
|
const manifestFile = zip.file("manifest.json");
|
|
1664
1869
|
if (!manifestFile) {
|
|
@@ -1700,9 +1905,9 @@ Files to extract:`));
|
|
|
1700
1905
|
return;
|
|
1701
1906
|
}
|
|
1702
1907
|
for (const file of files) {
|
|
1703
|
-
const fullPath =
|
|
1908
|
+
const fullPath = join11(targetDir, file.filePath);
|
|
1704
1909
|
const dir = dirname2(fullPath);
|
|
1705
|
-
if (!
|
|
1910
|
+
if (!existsSync10(dir)) {
|
|
1706
1911
|
await mkdir3(dir, { recursive: true });
|
|
1707
1912
|
}
|
|
1708
1913
|
await writeFile4(fullPath, file.content, "utf-8");
|
|
@@ -2005,16 +2210,16 @@ import chalk18 from "chalk";
|
|
|
2005
2210
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
2006
2211
|
|
|
2007
2212
|
// dist/mcp/read-configs.js
|
|
2008
|
-
import { readFile as
|
|
2009
|
-
import { join as
|
|
2213
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
2214
|
+
import { join as join12 } from "node:path";
|
|
2010
2215
|
import { homedir as homedir7 } from "node:os";
|
|
2011
|
-
import { existsSync as
|
|
2216
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
2012
2217
|
import { readdir as readdir3 } from "node:fs/promises";
|
|
2013
2218
|
async function readJsonSafe(path) {
|
|
2014
2219
|
try {
|
|
2015
|
-
if (!
|
|
2220
|
+
if (!existsSync11(path))
|
|
2016
2221
|
return null;
|
|
2017
|
-
const raw = await
|
|
2222
|
+
const raw = await readFile8(path, "utf-8");
|
|
2018
2223
|
return JSON.parse(raw);
|
|
2019
2224
|
} catch {
|
|
2020
2225
|
return null;
|
|
@@ -2076,50 +2281,50 @@ function parseFlatConfig(data, source) {
|
|
|
2076
2281
|
async function readAllMcpConfigs() {
|
|
2077
2282
|
const home = homedir7();
|
|
2078
2283
|
const entries = [];
|
|
2079
|
-
const userConfig = await readJsonSafe(
|
|
2284
|
+
const userConfig = await readJsonSafe(join12(home, ".claude.json"));
|
|
2080
2285
|
entries.push(...parseServers(userConfig, "global"));
|
|
2081
|
-
const globalMcp = await readJsonSafe(
|
|
2286
|
+
const globalMcp = await readJsonSafe(join12(home, ".claude", "mcp.json"));
|
|
2082
2287
|
entries.push(...parseServers(globalMcp, "global"));
|
|
2083
|
-
const cwdMcp = await readJsonSafe(
|
|
2288
|
+
const cwdMcp = await readJsonSafe(join12(process.cwd(), ".mcp.json"));
|
|
2084
2289
|
entries.push(...parseServers(cwdMcp, "project"));
|
|
2085
|
-
const pluginsBase =
|
|
2086
|
-
if (
|
|
2290
|
+
const pluginsBase = join12(home, ".claude", "plugins", "marketplaces");
|
|
2291
|
+
if (existsSync11(pluginsBase)) {
|
|
2087
2292
|
try {
|
|
2088
2293
|
const marketplaces = await readdir3(pluginsBase, { withFileTypes: true });
|
|
2089
2294
|
for (const mp of marketplaces) {
|
|
2090
2295
|
if (!mp.isDirectory())
|
|
2091
2296
|
continue;
|
|
2092
|
-
const extDir =
|
|
2093
|
-
if (!
|
|
2297
|
+
const extDir = join12(pluginsBase, mp.name, "external_plugins");
|
|
2298
|
+
if (!existsSync11(extDir))
|
|
2094
2299
|
continue;
|
|
2095
2300
|
const plugins = await readdir3(extDir, { withFileTypes: true });
|
|
2096
2301
|
for (const plugin of plugins) {
|
|
2097
2302
|
if (!plugin.isDirectory())
|
|
2098
2303
|
continue;
|
|
2099
|
-
const pluginMcp = await readJsonSafe(
|
|
2304
|
+
const pluginMcp = await readJsonSafe(join12(extDir, plugin.name, ".mcp.json"));
|
|
2100
2305
|
entries.push(...parseServers(pluginMcp, "plugin"));
|
|
2101
2306
|
}
|
|
2102
2307
|
}
|
|
2103
2308
|
} catch {
|
|
2104
2309
|
}
|
|
2105
2310
|
}
|
|
2106
|
-
const cacheBase =
|
|
2107
|
-
if (
|
|
2311
|
+
const cacheBase = join12(home, ".claude", "plugins", "cache");
|
|
2312
|
+
if (existsSync11(cacheBase)) {
|
|
2108
2313
|
try {
|
|
2109
2314
|
const registries = await readdir3(cacheBase, { withFileTypes: true });
|
|
2110
2315
|
for (const reg of registries) {
|
|
2111
2316
|
if (!reg.isDirectory())
|
|
2112
2317
|
continue;
|
|
2113
|
-
const regDir =
|
|
2318
|
+
const regDir = join12(cacheBase, reg.name);
|
|
2114
2319
|
const plugins = await readdir3(regDir, { withFileTypes: true });
|
|
2115
2320
|
for (const plugin of plugins) {
|
|
2116
2321
|
if (!plugin.isDirectory())
|
|
2117
2322
|
continue;
|
|
2118
|
-
const versionDirs = await readdir3(
|
|
2323
|
+
const versionDirs = await readdir3(join12(regDir, plugin.name), { withFileTypes: true });
|
|
2119
2324
|
for (const ver of versionDirs) {
|
|
2120
2325
|
if (!ver.isDirectory())
|
|
2121
2326
|
continue;
|
|
2122
|
-
const mcpPath =
|
|
2327
|
+
const mcpPath = join12(regDir, plugin.name, ver.name, ".mcp.json");
|
|
2123
2328
|
const flatData = await readJsonSafe(mcpPath);
|
|
2124
2329
|
if (flatData) {
|
|
2125
2330
|
entries.push(...parseFlatConfig(flatData, "plugin"));
|
|
@@ -2217,7 +2422,9 @@ function getProcessTable() {
|
|
|
2217
2422
|
}
|
|
2218
2423
|
|
|
2219
2424
|
// dist/commands/mcp-watch.js
|
|
2425
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2220
2426
|
var POLL_INTERVAL_MS = 3e4;
|
|
2427
|
+
var ENV_POLL_INTERVAL_MS = 3e5;
|
|
2221
2428
|
function detectTty() {
|
|
2222
2429
|
try {
|
|
2223
2430
|
const result = execFileSync5("ps", ["-o", "tty=", "-p", String(process.pid)], {
|
|
@@ -2403,9 +2610,29 @@ async function mcpWatchCommand() {
|
|
|
2403
2610
|
printTable(rows, deviceName);
|
|
2404
2611
|
}
|
|
2405
2612
|
await cycle();
|
|
2613
|
+
let lastEnvHash = "";
|
|
2614
|
+
async function envCycle() {
|
|
2615
|
+
const state = await loadState();
|
|
2616
|
+
if (!state.lastFolderId)
|
|
2617
|
+
return;
|
|
2618
|
+
const { data: dp } = await supabase.from("device_paths").select("path").eq("folder_id", state.lastFolderId).eq("device_name", deviceName).maybeSingle();
|
|
2619
|
+
if (!dp?.path)
|
|
2620
|
+
return;
|
|
2621
|
+
const envManifest = await scanEnvManifest(dp.path);
|
|
2622
|
+
if (!envManifest)
|
|
2623
|
+
return;
|
|
2624
|
+
const hash = createHash2("sha256").update(JSON.stringify(envManifest)).digest("hex");
|
|
2625
|
+
if (hash === lastEnvHash)
|
|
2626
|
+
return;
|
|
2627
|
+
lastEnvHash = hash;
|
|
2628
|
+
await supabase.from("claude_folders").update({ env_manifest_json: envManifest }).eq("id", state.lastFolderId);
|
|
2629
|
+
}
|
|
2630
|
+
await envCycle();
|
|
2631
|
+
const envInterval = setInterval(envCycle, ENV_POLL_INTERVAL_MS);
|
|
2406
2632
|
const interval = setInterval(cycle, POLL_INTERVAL_MS);
|
|
2407
2633
|
const shutdown = async () => {
|
|
2408
2634
|
clearInterval(interval);
|
|
2635
|
+
clearInterval(envInterval);
|
|
2409
2636
|
await supabase.from("mcp_watchers").delete().eq("device_id", deviceId).eq("pid", myPid);
|
|
2410
2637
|
console.log(chalk18.dim("\nMCP monitor stopped."));
|
|
2411
2638
|
process.exit(0);
|
|
@@ -2418,6 +2645,131 @@ async function mcpWatchCommand() {
|
|
|
2418
2645
|
});
|
|
2419
2646
|
}
|
|
2420
2647
|
|
|
2648
|
+
// dist/commands/init-manifest.js
|
|
2649
|
+
import { resolve as resolve5, join as join13, relative as relative3, dirname as dirname3 } from "node:path";
|
|
2650
|
+
import { readFile as readFile9, writeFile as writeFile5, mkdir as mkdir4 } from "node:fs/promises";
|
|
2651
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
2652
|
+
import chalk19 from "chalk";
|
|
2653
|
+
var SECRET_PATTERNS = [
|
|
2654
|
+
/_KEY$/i,
|
|
2655
|
+
/_SECRET$/i,
|
|
2656
|
+
/_TOKEN$/i,
|
|
2657
|
+
/_PASSWORD$/i,
|
|
2658
|
+
/^SECRET_/i,
|
|
2659
|
+
/^PRIVATE_/i,
|
|
2660
|
+
/SERVICE_ROLE/i
|
|
2661
|
+
];
|
|
2662
|
+
function isLikelySecret(name) {
|
|
2663
|
+
return SECRET_PATTERNS.some((p) => p.test(name));
|
|
2664
|
+
}
|
|
2665
|
+
async function discoverEnvFiles(projectRoot) {
|
|
2666
|
+
const apps = [];
|
|
2667
|
+
for (const envName of [".env", ".env.local", ".env.example"]) {
|
|
2668
|
+
const envPath = join13(projectRoot, envName);
|
|
2669
|
+
if (existsSync12(envPath)) {
|
|
2670
|
+
const vars = await extractVarNames(envPath);
|
|
2671
|
+
if (vars.length > 0) {
|
|
2672
|
+
apps.push({ name: "root", envFilePath: envName, vars });
|
|
2673
|
+
}
|
|
2674
|
+
break;
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
const subdirs = ["web", "cli", "api", "app", "server", "packages"];
|
|
2678
|
+
for (const sub of subdirs) {
|
|
2679
|
+
const subDir = join13(projectRoot, sub);
|
|
2680
|
+
if (!existsSync12(subDir))
|
|
2681
|
+
continue;
|
|
2682
|
+
for (const envName of [".env.local", ".env", ".env.example"]) {
|
|
2683
|
+
const envPath = join13(subDir, envName);
|
|
2684
|
+
if (existsSync12(envPath)) {
|
|
2685
|
+
const vars = await extractVarNames(envPath);
|
|
2686
|
+
if (vars.length > 0) {
|
|
2687
|
+
apps.push({
|
|
2688
|
+
name: sub,
|
|
2689
|
+
envFilePath: `${sub}/${envName}`,
|
|
2690
|
+
vars
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
break;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
return apps;
|
|
2698
|
+
}
|
|
2699
|
+
async function extractVarNames(envPath) {
|
|
2700
|
+
const content = await readFile9(envPath, "utf-8");
|
|
2701
|
+
const vars = [];
|
|
2702
|
+
for (const line of content.split("\n")) {
|
|
2703
|
+
const trimmed = line.trim();
|
|
2704
|
+
if (trimmed.startsWith("#") || !trimmed.includes("="))
|
|
2705
|
+
continue;
|
|
2706
|
+
const name = trimmed.split("=")[0].trim();
|
|
2707
|
+
if (name)
|
|
2708
|
+
vars.push(name);
|
|
2709
|
+
}
|
|
2710
|
+
return vars;
|
|
2711
|
+
}
|
|
2712
|
+
function generateManifest(apps) {
|
|
2713
|
+
const lines = ["# Environment Variable Manifest", ""];
|
|
2714
|
+
lines.push("## Apps");
|
|
2715
|
+
for (const app of apps) {
|
|
2716
|
+
lines.push(`- ${app.name}: \`${app.envFilePath}\``);
|
|
2717
|
+
}
|
|
2718
|
+
lines.push("");
|
|
2719
|
+
const allVars = /* @__PURE__ */ new Map();
|
|
2720
|
+
for (const app of apps) {
|
|
2721
|
+
for (const v of app.vars) {
|
|
2722
|
+
if (!allVars.has(v))
|
|
2723
|
+
allVars.set(v, /* @__PURE__ */ new Set());
|
|
2724
|
+
allVars.get(v).add(app.name);
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
lines.push("## Variables");
|
|
2728
|
+
lines.push("");
|
|
2729
|
+
lines.push("| Variable | Public? | Local | Vercel | GitHub | Supabase | Notes |");
|
|
2730
|
+
lines.push("|----------|---------|-------|--------|--------|----------|-------|");
|
|
2731
|
+
for (const [varName, appNames] of allVars) {
|
|
2732
|
+
const secret = isLikelySecret(varName);
|
|
2733
|
+
const publicCol = secret ? "**SECRET**" : "Yes";
|
|
2734
|
+
const localCol = [...appNames].join(", ");
|
|
2735
|
+
const vercelCol = "---";
|
|
2736
|
+
const githubCol = secret ? "yes" : "---";
|
|
2737
|
+
const supabaseCol = "---";
|
|
2738
|
+
lines.push(`| \`${varName}\` | ${publicCol} | ${localCol} | ${vercelCol} | ${githubCol} | ${supabaseCol} | |`);
|
|
2739
|
+
}
|
|
2740
|
+
lines.push("");
|
|
2741
|
+
return lines.join("\n");
|
|
2742
|
+
}
|
|
2743
|
+
async function initManifestCommand() {
|
|
2744
|
+
const projectRoot = resolve5(process.cwd());
|
|
2745
|
+
const manifestPath = join13(projectRoot, "docs", "reference", "env-manifest.md");
|
|
2746
|
+
if (existsSync12(manifestPath)) {
|
|
2747
|
+
console.log(chalk19.yellow(`Manifest already exists: ${relative3(projectRoot, manifestPath)}`));
|
|
2748
|
+
console.log(chalk19.dim("Edit it directly to make changes."));
|
|
2749
|
+
return;
|
|
2750
|
+
}
|
|
2751
|
+
console.log(chalk19.blue("Scanning for .env files...\n"));
|
|
2752
|
+
const apps = await discoverEnvFiles(projectRoot);
|
|
2753
|
+
if (apps.length === 0) {
|
|
2754
|
+
console.log(chalk19.yellow("No .env files found. Create a manifest manually at:"));
|
|
2755
|
+
console.log(chalk19.cyan(` docs/reference/env-manifest.md`));
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
for (const app of apps) {
|
|
2759
|
+
console.log(` ${chalk19.green(app.name)}: ${app.envFilePath} (${app.vars.length} vars)`);
|
|
2760
|
+
}
|
|
2761
|
+
const content = generateManifest(apps);
|
|
2762
|
+
const dir = dirname3(manifestPath);
|
|
2763
|
+
if (!existsSync12(dir)) {
|
|
2764
|
+
await mkdir4(dir, { recursive: true });
|
|
2765
|
+
}
|
|
2766
|
+
await writeFile5(manifestPath, content, "utf-8");
|
|
2767
|
+
console.log(chalk19.green(`
|
|
2768
|
+
Manifest created: ${relative3(projectRoot, manifestPath)}`));
|
|
2769
|
+
console.log(chalk19.dim("Review and edit the file \u2014 it is your source of truth."));
|
|
2770
|
+
console.log(chalk19.dim("Then run `md4ai scan` to verify against your environments."));
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2421
2773
|
// dist/index.js
|
|
2422
2774
|
var program = new Command();
|
|
2423
2775
|
program.name("md4ai").description("MD4AI \u2014 Claude tooling visualiser").version(CURRENT_VERSION).addHelpText("after", `
|
|
@@ -2440,6 +2792,7 @@ program.command("sync").description("Re-push latest scan data to Supabase").opti
|
|
|
2440
2792
|
program.command("import <zipfile>").description("Import a Claude setup bundle exported from the web dashboard").action(importBundleCommand);
|
|
2441
2793
|
program.command("check-update").description("Check if a newer version of md4ai is available").action(checkForUpdate);
|
|
2442
2794
|
program.command("mcp-watch").description("Monitor MCP server status on this device (runs until Ctrl+C)").action(mcpWatchCommand);
|
|
2795
|
+
program.command("init-manifest").description("Scaffold a starter env-manifest.md from detected .env files").action(initManifestCommand);
|
|
2443
2796
|
var admin = program.command("admin").description("Admin commands for managing the tools registry");
|
|
2444
2797
|
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);
|
|
2445
2798
|
admin.command("list-tools").description("List all tools in the master registry").action(adminListToolsCommand);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "md4ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "CLI for MD4AI — scan Claude projects and sync to your dashboard",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,12 @@
|
|
|
17
17
|
"dev": "tsc --watch",
|
|
18
18
|
"clean": "rm -rf dist"
|
|
19
19
|
},
|
|
20
|
-
"keywords": [
|
|
20
|
+
"keywords": [
|
|
21
|
+
"claude",
|
|
22
|
+
"ai",
|
|
23
|
+
"project-scanner",
|
|
24
|
+
"dependency-graph"
|
|
25
|
+
],
|
|
21
26
|
"license": "MIT",
|
|
22
27
|
"repository": {
|
|
23
28
|
"type": "git",
|