md4ai 0.9.5 → 0.9.7

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.
@@ -233,6 +233,7 @@ import { resolve as resolve2, dirname, join as join2 } from "node:path";
233
233
  import { homedir as homedir2 } from "node:os";
234
234
  async function parseFileReferences(filePath, projectRoot) {
235
235
  const refs = [];
236
+ const brokenRefs = [];
236
237
  const content = await readFile2(filePath, "utf-8");
237
238
  const relPath = filePath.startsWith(projectRoot) ? filePath.slice(projectRoot.length + 1) : filePath;
238
239
  const markdownLinkPattern = /\[.*?\]\(([^)]+)\)/g;
@@ -251,23 +252,40 @@ async function parseFileReferences(filePath, projectRoot) {
251
252
  const target = match[1];
252
253
  if (!target || target.startsWith("http") || target.startsWith("#"))
253
254
  continue;
255
+ const baseName = target.split("/").pop() ?? target;
256
+ if (!target.includes("/") && /^[A-Z]/.test(baseName))
257
+ continue;
254
258
  const resolved = resolveTarget(target, filePath, projectRoot);
255
259
  if (resolved && existsSync2(resolved)) {
256
260
  addRef(refs, relPath, resolved, projectRoot);
261
+ } else if (resolved) {
262
+ const targetRel = resolved.startsWith(projectRoot) ? resolved.slice(projectRoot.length + 1) : resolved;
263
+ if (targetRel !== relPath) {
264
+ brokenRefs.push({ from: relPath, to: targetRel, rawRef: target });
265
+ }
257
266
  }
258
267
  }
259
268
  }
260
269
  if (filePath.endsWith(".json")) {
261
- parseJsonPathReferences(content, relPath, projectRoot, refs);
270
+ parseJsonPathReferences(content, relPath, projectRoot, refs, brokenRefs);
262
271
  }
263
272
  const seen = /* @__PURE__ */ new Set();
264
- return refs.filter((r) => {
273
+ const dedupedRefs = refs.filter((r) => {
265
274
  const key = `${r.from}->${r.to}`;
266
275
  if (seen.has(key))
267
276
  return false;
268
277
  seen.add(key);
269
278
  return true;
270
279
  });
280
+ const brokenSeen = /* @__PURE__ */ new Set();
281
+ const dedupedBroken = brokenRefs.filter((r) => {
282
+ const key = `${r.from}->${r.to}`;
283
+ if (brokenSeen.has(key))
284
+ return false;
285
+ brokenSeen.add(key);
286
+ return true;
287
+ });
288
+ return { refs: dedupedRefs, brokenRefs: dedupedBroken };
271
289
  }
272
290
  function resolveTarget(target, sourceFilePath, projectRoot) {
273
291
  if (target.startsWith("~")) {
@@ -279,6 +297,11 @@ function resolveTarget(target, sourceFilePath, projectRoot) {
279
297
  if (target.startsWith(".claude/") || target.startsWith("docs/")) {
280
298
  return join2(projectRoot, target);
281
299
  }
300
+ if (target.includes("/")) {
301
+ const fromRoot = join2(projectRoot, target);
302
+ if (existsSync2(fromRoot))
303
+ return fromRoot;
304
+ }
282
305
  return resolve2(dirname(sourceFilePath), target);
283
306
  }
284
307
  function addRef(refs, fromRel, resolvedAbsolute, projectRoot) {
@@ -287,7 +310,7 @@ function addRef(refs, fromRel, resolvedAbsolute, projectRoot) {
287
310
  refs.push({ from: fromRel, to: targetRel });
288
311
  }
289
312
  }
290
- function parseJsonPathReferences(content, fromRel, projectRoot, refs) {
313
+ function parseJsonPathReferences(content, fromRel, projectRoot, refs, brokenRefs) {
291
314
  let parsed;
292
315
  try {
293
316
  parsed = JSON.parse(content);
@@ -304,6 +327,8 @@ function parseJsonPathReferences(content, fromRel, projectRoot, refs) {
304
327
  const resolved = join2(projectRoot, relTarget);
305
328
  if (existsSync2(resolved)) {
306
329
  addRef(refs, fromRel, resolved, projectRoot);
330
+ } else {
331
+ brokenRefs.push({ from: fromRel, to: relTarget, rawRef: relTarget });
307
332
  }
308
333
  }
309
334
  }
@@ -316,6 +341,8 @@ function parseJsonPathReferences(content, fromRel, projectRoot, refs) {
316
341
  const resolved = join2(projectRoot, target);
317
342
  if (existsSync2(resolved)) {
318
343
  addRef(refs, fromRel, resolved, projectRoot);
344
+ } else {
345
+ brokenRefs.push({ from: fromRel, to: target, rawRef: target });
319
346
  }
320
347
  }
321
348
  });
@@ -1259,11 +1286,13 @@ async function scanProject(projectRoot) {
1259
1286
  const allFiles = await discoverFiles(projectRoot);
1260
1287
  const rootFiles = identifyRoots(allFiles, projectRoot);
1261
1288
  const allRefs = [];
1289
+ const allBrokenRefs = [];
1262
1290
  for (const file of allFiles) {
1263
1291
  const fullPath = file.startsWith("/") ? file : join10(projectRoot, file);
1264
1292
  try {
1265
- const refs = await parseFileReferences(fullPath, projectRoot);
1293
+ const { refs, brokenRefs: brokenRefs2 } = await parseFileReferences(fullPath, projectRoot);
1266
1294
  allRefs.push(...refs);
1295
+ allBrokenRefs.push(...brokenRefs2);
1267
1296
  } catch {
1268
1297
  }
1269
1298
  }
@@ -1273,11 +1302,32 @@ async function scanProject(projectRoot) {
1273
1302
  const skills = await parseSkills(projectRoot);
1274
1303
  const toolings = await detectToolings(projectRoot);
1275
1304
  const envManifest = await scanEnvManifest(projectRoot);
1276
- const scanData = JSON.stringify({ graph, orphans, skills, staleFiles, toolings, envManifest });
1305
+ const depthMap = /* @__PURE__ */ new Map();
1306
+ const queue = [...rootFiles];
1307
+ for (const r of queue)
1308
+ depthMap.set(r, 0);
1309
+ while (queue.length > 0) {
1310
+ const current = queue.shift();
1311
+ const currentDepth = depthMap.get(current);
1312
+ for (const ref of allRefs) {
1313
+ if (ref.from === current && !depthMap.has(ref.to)) {
1314
+ depthMap.set(ref.to, currentDepth + 1);
1315
+ queue.push(ref.to);
1316
+ }
1317
+ }
1318
+ }
1319
+ const filteredBroken = allBrokenRefs.filter((br) => classifyFileType(br.from) !== "plan");
1320
+ const brokenRefs = filteredBroken.map((br) => ({
1321
+ ...br,
1322
+ depth: depthMap.get(br.from) ?? 999
1323
+ }));
1324
+ brokenRefs.sort((a, b) => a.depth - b.depth || a.from.localeCompare(b.from));
1325
+ const scanData = JSON.stringify({ graph, orphans, brokenRefs, skills, staleFiles, toolings, envManifest });
1277
1326
  const dataHash = createHash("sha256").update(scanData).digest("hex");
1278
1327
  return {
1279
1328
  graph,
1280
1329
  orphans,
1330
+ brokenRefs,
1281
1331
  skills,
1282
1332
  staleFiles,
1283
1333
  toolings,
@@ -1595,7 +1645,7 @@ var CURRENT_VERSION;
1595
1645
  var init_check_update = __esm({
1596
1646
  "dist/check-update.js"() {
1597
1647
  "use strict";
1598
- CURRENT_VERSION = true ? "0.9.5" : "0.0.0-dev";
1648
+ CURRENT_VERSION = true ? "0.9.7" : "0.0.0-dev";
1599
1649
  }
1600
1650
  });
1601
1651
 
@@ -1764,12 +1814,23 @@ async function mapCommand(path, options) {
1764
1814
  const result = await scanProject(projectRoot);
1765
1815
  console.log(` Files found: ${result.graph.nodes.length}`);
1766
1816
  console.log(` References: ${result.graph.edges.length}`);
1817
+ console.log(` Broken refs: ${result.brokenRefs.length}`);
1767
1818
  console.log(` Orphans: ${result.orphans.length}`);
1768
1819
  console.log(` Stale files: ${result.staleFiles.length}`);
1769
1820
  console.log(` Skills: ${result.skills.length}`);
1770
1821
  console.log(` Toolings: ${result.toolings.length}`);
1771
1822
  console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
1772
1823
  console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
1824
+ if (result.brokenRefs.length > 0) {
1825
+ console.log(chalk11.red(`
1826
+ Warning: ${result.brokenRefs.length} broken reference(s) found:`));
1827
+ for (const br of result.brokenRefs.slice(0, 5)) {
1828
+ console.log(chalk11.red(` ${br.from} -> ${br.to}`));
1829
+ }
1830
+ if (result.brokenRefs.length > 5) {
1831
+ console.log(chalk11.red(` ... and ${result.brokenRefs.length - 5} more`));
1832
+ }
1833
+ }
1773
1834
  const outputDir = resolve3(projectRoot, "output");
1774
1835
  if (!existsSync7(outputDir)) {
1775
1836
  await mkdir2(outputDir, { recursive: true });
@@ -1823,6 +1884,7 @@ ${proposedFiles.length} file(s) proposed for deletion:
1823
1884
  const { error } = await supabase.from("claude_folders").update({
1824
1885
  graph_json: result.graph,
1825
1886
  orphans_json: result.orphans,
1887
+ broken_refs_json: result.brokenRefs,
1826
1888
  skills_table_json: result.skills,
1827
1889
  stale_files_json: result.staleFiles,
1828
1890
  env_manifest_json: result.envManifest,
@@ -1914,6 +1976,7 @@ ${proposedFiles.length} file(s) proposed for deletion:
1914
1976
  await sb.from("claude_folders").update({
1915
1977
  graph_json: result.graph,
1916
1978
  orphans_json: result.orphans,
1979
+ broken_refs_json: result.brokenRefs,
1917
1980
  skills_table_json: result.skills,
1918
1981
  stale_files_json: result.staleFiles,
1919
1982
  env_manifest_json: result.envManifest,
@@ -1989,6 +2052,7 @@ async function syncCommand(options) {
1989
2052
  await supabase.from("claude_folders").update({
1990
2053
  graph_json: result.graph,
1991
2054
  orphans_json: result.orphans,
2055
+ broken_refs_json: result.brokenRefs,
1992
2056
  skills_table_json: result.skills,
1993
2057
  stale_files_json: result.staleFiles,
1994
2058
  env_manifest_json: result.envManifest,
@@ -2024,6 +2088,7 @@ async function syncCommand(options) {
2024
2088
  await supabase.from("claude_folders").update({
2025
2089
  graph_json: result.graph,
2026
2090
  orphans_json: result.orphans,
2091
+ broken_refs_json: result.brokenRefs,
2027
2092
  skills_table_json: result.skills,
2028
2093
  stale_files_json: result.staleFiles,
2029
2094
  last_scanned: result.scannedAt,
@@ -2651,6 +2716,7 @@ async function checkPendingRescans(supabase, deviceId, deviceName) {
2651
2716
  await supabase.from("claude_folders").update({
2652
2717
  graph_json: result.graph,
2653
2718
  orphans_json: result.orphans,
2719
+ broken_refs_json: result.brokenRefs,
2654
2720
  skills_table_json: result.skills,
2655
2721
  stale_files_json: result.staleFiles,
2656
2722
  env_manifest_json: result.envManifest,
@@ -3066,6 +3132,7 @@ Linking "${folder.name}" to this device...
3066
3132
  const { error: scanErr } = await supabase.from("claude_folders").update({
3067
3133
  graph_json: result.graph,
3068
3134
  orphans_json: result.orphans,
3135
+ broken_refs_json: result.brokenRefs,
3069
3136
  skills_table_json: result.skills,
3070
3137
  stale_files_json: result.staleFiles,
3071
3138
  env_manifest_json: result.envManifest,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md4ai",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "description": "CLI for MD4AI — scan Claude projects and sync to your dashboard",
5
5
  "type": "module",
6
6
  "bin": {