md4ai 0.9.5 → 0.9.6

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;
@@ -254,20 +255,34 @@ async function parseFileReferences(filePath, projectRoot) {
254
255
  const resolved = resolveTarget(target, filePath, projectRoot);
255
256
  if (resolved && existsSync2(resolved)) {
256
257
  addRef(refs, relPath, resolved, projectRoot);
258
+ } else if (resolved) {
259
+ const targetRel = resolved.startsWith(projectRoot) ? resolved.slice(projectRoot.length + 1) : resolved;
260
+ if (targetRel !== relPath) {
261
+ brokenRefs.push({ from: relPath, to: targetRel, rawRef: target });
262
+ }
257
263
  }
258
264
  }
259
265
  }
260
266
  if (filePath.endsWith(".json")) {
261
- parseJsonPathReferences(content, relPath, projectRoot, refs);
267
+ parseJsonPathReferences(content, relPath, projectRoot, refs, brokenRefs);
262
268
  }
263
269
  const seen = /* @__PURE__ */ new Set();
264
- return refs.filter((r) => {
270
+ const dedupedRefs = refs.filter((r) => {
265
271
  const key = `${r.from}->${r.to}`;
266
272
  if (seen.has(key))
267
273
  return false;
268
274
  seen.add(key);
269
275
  return true;
270
276
  });
277
+ const brokenSeen = /* @__PURE__ */ new Set();
278
+ const dedupedBroken = brokenRefs.filter((r) => {
279
+ const key = `${r.from}->${r.to}`;
280
+ if (brokenSeen.has(key))
281
+ return false;
282
+ brokenSeen.add(key);
283
+ return true;
284
+ });
285
+ return { refs: dedupedRefs, brokenRefs: dedupedBroken };
271
286
  }
272
287
  function resolveTarget(target, sourceFilePath, projectRoot) {
273
288
  if (target.startsWith("~")) {
@@ -287,7 +302,7 @@ function addRef(refs, fromRel, resolvedAbsolute, projectRoot) {
287
302
  refs.push({ from: fromRel, to: targetRel });
288
303
  }
289
304
  }
290
- function parseJsonPathReferences(content, fromRel, projectRoot, refs) {
305
+ function parseJsonPathReferences(content, fromRel, projectRoot, refs, brokenRefs) {
291
306
  let parsed;
292
307
  try {
293
308
  parsed = JSON.parse(content);
@@ -304,6 +319,8 @@ function parseJsonPathReferences(content, fromRel, projectRoot, refs) {
304
319
  const resolved = join2(projectRoot, relTarget);
305
320
  if (existsSync2(resolved)) {
306
321
  addRef(refs, fromRel, resolved, projectRoot);
322
+ } else {
323
+ brokenRefs.push({ from: fromRel, to: relTarget, rawRef: relTarget });
307
324
  }
308
325
  }
309
326
  }
@@ -316,6 +333,8 @@ function parseJsonPathReferences(content, fromRel, projectRoot, refs) {
316
333
  const resolved = join2(projectRoot, target);
317
334
  if (existsSync2(resolved)) {
318
335
  addRef(refs, fromRel, resolved, projectRoot);
336
+ } else {
337
+ brokenRefs.push({ from: fromRel, to: target, rawRef: target });
319
338
  }
320
339
  }
321
340
  });
@@ -1259,11 +1278,13 @@ async function scanProject(projectRoot) {
1259
1278
  const allFiles = await discoverFiles(projectRoot);
1260
1279
  const rootFiles = identifyRoots(allFiles, projectRoot);
1261
1280
  const allRefs = [];
1281
+ const allBrokenRefs = [];
1262
1282
  for (const file of allFiles) {
1263
1283
  const fullPath = file.startsWith("/") ? file : join10(projectRoot, file);
1264
1284
  try {
1265
- const refs = await parseFileReferences(fullPath, projectRoot);
1285
+ const { refs, brokenRefs: brokenRefs2 } = await parseFileReferences(fullPath, projectRoot);
1266
1286
  allRefs.push(...refs);
1287
+ allBrokenRefs.push(...brokenRefs2);
1267
1288
  } catch {
1268
1289
  }
1269
1290
  }
@@ -1273,11 +1294,31 @@ async function scanProject(projectRoot) {
1273
1294
  const skills = await parseSkills(projectRoot);
1274
1295
  const toolings = await detectToolings(projectRoot);
1275
1296
  const envManifest = await scanEnvManifest(projectRoot);
1276
- const scanData = JSON.stringify({ graph, orphans, skills, staleFiles, toolings, envManifest });
1297
+ const depthMap = /* @__PURE__ */ new Map();
1298
+ const queue = [...rootFiles];
1299
+ for (const r of queue)
1300
+ depthMap.set(r, 0);
1301
+ while (queue.length > 0) {
1302
+ const current = queue.shift();
1303
+ const currentDepth = depthMap.get(current);
1304
+ for (const ref of allRefs) {
1305
+ if (ref.from === current && !depthMap.has(ref.to)) {
1306
+ depthMap.set(ref.to, currentDepth + 1);
1307
+ queue.push(ref.to);
1308
+ }
1309
+ }
1310
+ }
1311
+ const brokenRefs = allBrokenRefs.map((br) => ({
1312
+ ...br,
1313
+ depth: depthMap.get(br.from) ?? 999
1314
+ }));
1315
+ brokenRefs.sort((a, b) => a.depth - b.depth || a.from.localeCompare(b.from));
1316
+ const scanData = JSON.stringify({ graph, orphans, brokenRefs, skills, staleFiles, toolings, envManifest });
1277
1317
  const dataHash = createHash("sha256").update(scanData).digest("hex");
1278
1318
  return {
1279
1319
  graph,
1280
1320
  orphans,
1321
+ brokenRefs,
1281
1322
  skills,
1282
1323
  staleFiles,
1283
1324
  toolings,
@@ -1595,7 +1636,7 @@ var CURRENT_VERSION;
1595
1636
  var init_check_update = __esm({
1596
1637
  "dist/check-update.js"() {
1597
1638
  "use strict";
1598
- CURRENT_VERSION = true ? "0.9.5" : "0.0.0-dev";
1639
+ CURRENT_VERSION = true ? "0.9.6" : "0.0.0-dev";
1599
1640
  }
1600
1641
  });
1601
1642
 
@@ -1764,12 +1805,23 @@ async function mapCommand(path, options) {
1764
1805
  const result = await scanProject(projectRoot);
1765
1806
  console.log(` Files found: ${result.graph.nodes.length}`);
1766
1807
  console.log(` References: ${result.graph.edges.length}`);
1808
+ console.log(` Broken refs: ${result.brokenRefs.length}`);
1767
1809
  console.log(` Orphans: ${result.orphans.length}`);
1768
1810
  console.log(` Stale files: ${result.staleFiles.length}`);
1769
1811
  console.log(` Skills: ${result.skills.length}`);
1770
1812
  console.log(` Toolings: ${result.toolings.length}`);
1771
1813
  console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
1772
1814
  console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
1815
+ if (result.brokenRefs.length > 0) {
1816
+ console.log(chalk11.red(`
1817
+ Warning: ${result.brokenRefs.length} broken reference(s) found:`));
1818
+ for (const br of result.brokenRefs.slice(0, 5)) {
1819
+ console.log(chalk11.red(` ${br.from} -> ${br.to}`));
1820
+ }
1821
+ if (result.brokenRefs.length > 5) {
1822
+ console.log(chalk11.red(` ... and ${result.brokenRefs.length - 5} more`));
1823
+ }
1824
+ }
1773
1825
  const outputDir = resolve3(projectRoot, "output");
1774
1826
  if (!existsSync7(outputDir)) {
1775
1827
  await mkdir2(outputDir, { recursive: true });
@@ -1823,6 +1875,7 @@ ${proposedFiles.length} file(s) proposed for deletion:
1823
1875
  const { error } = await supabase.from("claude_folders").update({
1824
1876
  graph_json: result.graph,
1825
1877
  orphans_json: result.orphans,
1878
+ broken_refs_json: result.brokenRefs,
1826
1879
  skills_table_json: result.skills,
1827
1880
  stale_files_json: result.staleFiles,
1828
1881
  env_manifest_json: result.envManifest,
@@ -1914,6 +1967,7 @@ ${proposedFiles.length} file(s) proposed for deletion:
1914
1967
  await sb.from("claude_folders").update({
1915
1968
  graph_json: result.graph,
1916
1969
  orphans_json: result.orphans,
1970
+ broken_refs_json: result.brokenRefs,
1917
1971
  skills_table_json: result.skills,
1918
1972
  stale_files_json: result.staleFiles,
1919
1973
  env_manifest_json: result.envManifest,
@@ -1989,6 +2043,7 @@ async function syncCommand(options) {
1989
2043
  await supabase.from("claude_folders").update({
1990
2044
  graph_json: result.graph,
1991
2045
  orphans_json: result.orphans,
2046
+ broken_refs_json: result.brokenRefs,
1992
2047
  skills_table_json: result.skills,
1993
2048
  stale_files_json: result.staleFiles,
1994
2049
  env_manifest_json: result.envManifest,
@@ -2024,6 +2079,7 @@ async function syncCommand(options) {
2024
2079
  await supabase.from("claude_folders").update({
2025
2080
  graph_json: result.graph,
2026
2081
  orphans_json: result.orphans,
2082
+ broken_refs_json: result.brokenRefs,
2027
2083
  skills_table_json: result.skills,
2028
2084
  stale_files_json: result.staleFiles,
2029
2085
  last_scanned: result.scannedAt,
@@ -2651,6 +2707,7 @@ async function checkPendingRescans(supabase, deviceId, deviceName) {
2651
2707
  await supabase.from("claude_folders").update({
2652
2708
  graph_json: result.graph,
2653
2709
  orphans_json: result.orphans,
2710
+ broken_refs_json: result.brokenRefs,
2654
2711
  skills_table_json: result.skills,
2655
2712
  stale_files_json: result.staleFiles,
2656
2713
  env_manifest_json: result.envManifest,
@@ -3066,6 +3123,7 @@ Linking "${folder.name}" to this device...
3066
3123
  const { error: scanErr } = await supabase.from("claude_folders").update({
3067
3124
  graph_json: result.graph,
3068
3125
  orphans_json: result.orphans,
3126
+ broken_refs_json: result.brokenRefs,
3069
3127
  skills_table_json: result.skills,
3070
3128
  stale_files_json: result.staleFiles,
3071
3129
  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.6",
4
4
  "description": "CLI for MD4AI — scan Claude projects and sync to your dashboard",
5
5
  "type": "module",
6
6
  "bin": {