@skill-map/cli 0.29.0 → 0.30.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/cli.js CHANGED
@@ -775,6 +775,53 @@ function link(source, target) {
775
775
  };
776
776
  }
777
777
 
778
+ // kernel/util/strip-code-blocks.ts
779
+ var FENCE_RE = /^(?<indent> {0,3})(?<fence>`{3,}|~{3,})/;
780
+ function stripCodeBlocks(input) {
781
+ if (!input) return input;
782
+ const fenceless = stripFences(input);
783
+ return stripInline(fenceless);
784
+ }
785
+ function stripFences(input) {
786
+ const out = [];
787
+ const lines = input.split("\n");
788
+ let openFence = null;
789
+ for (const line of lines) {
790
+ if (openFence) {
791
+ const closer = matchClosingFence(line, openFence);
792
+ if (closer) {
793
+ out.push(blank(line));
794
+ openFence = null;
795
+ } else {
796
+ out.push(blank(line));
797
+ }
798
+ continue;
799
+ }
800
+ const open = FENCE_RE.exec(line);
801
+ if (open?.groups) {
802
+ openFence = open.groups["fence"];
803
+ out.push(blank(line));
804
+ continue;
805
+ }
806
+ out.push(line);
807
+ }
808
+ return out.join("\n");
809
+ }
810
+ function matchClosingFence(line, openFence) {
811
+ const m = FENCE_RE.exec(line);
812
+ if (!m?.groups) return false;
813
+ const fence = m.groups["fence"];
814
+ return fence[0] === openFence[0] && fence.length >= openFence.length;
815
+ }
816
+ function stripInline(input) {
817
+ return input.replace(/(`+)([\s\S]*?)\1/g, (_full, ticks, body) => {
818
+ return ticks.replace(/`/g, " ") + blank(body) + ticks.replace(/`/g, " ");
819
+ });
820
+ }
821
+ function blank(s) {
822
+ return s.replace(/[^\s]/g, " ");
823
+ }
824
+
778
825
  // kernel/trigger-normalize.ts
779
826
  function normalizeTrigger(source) {
780
827
  let out = source.normalize("NFD");
@@ -787,21 +834,43 @@ function normalizeTrigger(source) {
787
834
 
788
835
  // plugins/core/extractors/at-directive/index.ts
789
836
  var ID2 = "at-directive";
790
- var AT_RE = /(?:^|[^A-Za-z0-9_@])(@[a-z0-9][a-z0-9_-]*(?:[/:][a-z0-9][a-z0-9_-]*)?)/gi;
837
+ var AT_RE = /(?:^|[^A-Za-z0-9_@])(@(?:\.{1,2}\/|\/)?[a-z0-9](?:[a-z0-9_\-./]*[a-z0-9_])?(?::[a-z0-9][a-z0-9_-]*)?)/gi;
838
+ var FILE_EXT_RE = /\.(md|mdx|js|jsx|ts|tsx|json|yml|yaml|toml|txt|html|css|scss|less|py|rb|go|rs|java|c|cpp|h|hpp|sh|sql|svg|png|jpg|jpeg|gif|webp|pdf)$/i;
791
839
  var atDirectiveExtractor = {
792
840
  id: ID2,
793
841
  pluginId: "core",
794
842
  kind: "extractor",
795
843
  version: "1.0.0",
796
- description: "Detects `@agent-name` mentions in a node's body and turns each one into an arrow between nodes in the graph.",
844
+ description: "Detects `@<token>` directives in a node's body. A bare handle (e.g. `@team`) becomes a `mentions` link; a file-flavoured token (e.g. `@docs/api.md`, `@./readme.md`) becomes a `references` link, matching how Claude Code / Gemini CLI / Cursor read the same syntax.",
797
845
  scope: "body",
798
846
  extract(ctx) {
799
- const seen = /* @__PURE__ */ new Set();
800
- for (const match of ctx.body.matchAll(AT_RE)) {
847
+ const seenMentions = /* @__PURE__ */ new Set();
848
+ const seenReferences = /* @__PURE__ */ new Set();
849
+ const body = stripCodeBlocks(ctx.body);
850
+ for (const match of body.matchAll(AT_RE)) {
801
851
  const original = match[1];
852
+ const bare = original.slice(1);
853
+ const isReference = bare.startsWith("./") || bare.startsWith("../") || bare.startsWith("/") || FILE_EXT_RE.test(bare);
854
+ if (isReference) {
855
+ const target = bare.replace(/^\.\//, "");
856
+ if (seenReferences.has(target)) continue;
857
+ seenReferences.add(target);
858
+ ctx.emitLink({
859
+ source: ctx.node.path,
860
+ target,
861
+ kind: "references",
862
+ confidence: "medium",
863
+ sources: [ID2],
864
+ trigger: {
865
+ originalTrigger: original,
866
+ normalizedTrigger: target.toLowerCase()
867
+ }
868
+ });
869
+ continue;
870
+ }
802
871
  const normalized = normalizeTrigger(original);
803
- if (seen.has(normalized)) continue;
804
- seen.add(normalized);
872
+ if (seenMentions.has(normalized)) continue;
873
+ seenMentions.add(normalized);
805
874
  ctx.emitLink({
806
875
  source: ctx.node.path,
807
876
  target: original,
@@ -993,8 +1062,12 @@ var slashExtractor = {
993
1062
  scope: "body",
994
1063
  extract(ctx) {
995
1064
  const seen = /* @__PURE__ */ new Set();
996
- for (const match of ctx.body.matchAll(SLASH_RE)) {
1065
+ const body = stripCodeBlocks(ctx.body);
1066
+ for (const match of body.matchAll(SLASH_RE)) {
997
1067
  const original = match[1];
1068
+ const endIdx = (match.index ?? 0) + match[0].length;
1069
+ const nextChar = body[endIdx];
1070
+ if (nextChar && /[A-Za-z0-9_/-]/.test(nextChar)) continue;
998
1071
  const normalized = normalizeTrigger(original);
999
1072
  if (seen.has(normalized)) continue;
1000
1073
  seen.add(normalized);
@@ -1176,7 +1249,7 @@ function tooltipFor(status) {
1176
1249
  }
1177
1250
 
1178
1251
  // plugins/core/analyzers/broken-ref/index.ts
1179
- import { resolve } from "path";
1252
+ import { posix as pathPosix2, resolve } from "path";
1180
1253
 
1181
1254
  // plugins/core/analyzers/broken-ref/text.ts
1182
1255
  var BROKEN_REF_TEXTS = {
@@ -1185,7 +1258,13 @@ var BROKEN_REF_TEXTS = {
1185
1258
  // Tooltips for the per-node view-contribution badges. Singular vs
1186
1259
  // plural keeps the count grammar correct without a sub-template.
1187
1260
  alertTooltipSingle: "This node has a broken reference. Open the inspector for details.",
1188
- alertTooltipMany: "This node has {{count}} broken references. Open the inspector for details."
1261
+ alertTooltipMany: "This node has {{count}} broken references. Open the inspector for details.",
1262
+ // Fix-summary copy when the broken trigger has a same-named file on
1263
+ // disk that does not advertise `name:` in its frontmatter. Two
1264
+ // variants for single vs multiple candidates; same template family
1265
+ // as the alert tooltips above.
1266
+ hintSummarySingle: "Add `name: {{name}}` to the frontmatter of {{candidate}} so this reference resolves.",
1267
+ hintSummaryMany: "Add `name: {{name}}` to the frontmatter of one of these files so this reference resolves: {{candidates}}."
1189
1268
  };
1190
1269
 
1191
1270
  // plugins/core/analyzers/broken-ref/index.ts
@@ -1225,13 +1304,15 @@ var brokenRefAnalyzer = {
1225
1304
  evaluate(ctx) {
1226
1305
  const byPath3 = new Set(ctx.nodes.map((n) => n.path));
1227
1306
  const byNormalizedName = indexByNormalizedName(ctx.nodes);
1307
+ const byBasenameWithoutName = indexByBasenameWithoutName(ctx.nodes);
1228
1308
  const refIndex = ctx.referenceablePaths && ctx.referenceablePaths.size > 0 && ctx.cwd ? { paths: ctx.referenceablePaths, cwd: ctx.cwd } : null;
1229
1309
  const issues = [];
1230
1310
  const perNode = /* @__PURE__ */ new Map();
1231
1311
  for (const link2 of ctx.links) {
1232
1312
  if (isResolved(link2, byPath3, byNormalizedName)) continue;
1233
1313
  if (refIndex && resolvesViaReferencePaths(link2, refIndex)) continue;
1234
- issues.push(buildIssue(link2));
1314
+ const candidates = findHintCandidates(link2, byBasenameWithoutName);
1315
+ issues.push(buildIssue(link2, candidates));
1235
1316
  perNode.set(link2.source, (perNode.get(link2.source) ?? 0) + 1);
1236
1317
  }
1237
1318
  for (const [nodePath, count] of perNode) {
@@ -1251,8 +1332,13 @@ var brokenRefAnalyzer = {
1251
1332
  return issues;
1252
1333
  }
1253
1334
  };
1254
- function buildIssue(link2) {
1255
- return {
1335
+ function buildIssue(link2, hintCandidates = []) {
1336
+ const data = {
1337
+ target: link2.target,
1338
+ kind: link2.kind,
1339
+ trigger: link2.trigger?.normalizedTrigger ?? null
1340
+ };
1341
+ const issue = {
1256
1342
  analyzerId: ID9,
1257
1343
  severity: "warn",
1258
1344
  nodeIds: [link2.source],
@@ -1261,12 +1347,28 @@ function buildIssue(link2) {
1261
1347
  source: link2.source,
1262
1348
  target: link2.target
1263
1349
  }),
1264
- data: {
1265
- target: link2.target,
1266
- kind: link2.kind,
1267
- trigger: link2.trigger?.normalizedTrigger ?? null
1268
- }
1350
+ data
1269
1351
  };
1352
+ if (hintCandidates.length > 0) {
1353
+ const suggestedName = (link2.trigger?.normalizedTrigger ?? "").replace(/^[/@]/, "").trim();
1354
+ const candidatePaths = hintCandidates.map((n) => n.path);
1355
+ data["hint"] = {
1356
+ kind: "missing-frontmatter-name",
1357
+ suggestedName,
1358
+ candidates: candidatePaths
1359
+ };
1360
+ issue.fix = {
1361
+ summary: candidatePaths.length === 1 ? tx(BROKEN_REF_TEXTS.hintSummarySingle, {
1362
+ name: suggestedName,
1363
+ candidate: candidatePaths[0]
1364
+ }) : tx(BROKEN_REF_TEXTS.hintSummaryMany, {
1365
+ name: suggestedName,
1366
+ candidates: candidatePaths.join(", ")
1367
+ }),
1368
+ autofixable: false
1369
+ };
1370
+ }
1371
+ return issue;
1270
1372
  }
1271
1373
  function resolvesViaReferencePaths(link2, refIndex) {
1272
1374
  if (!isPathStyleLink(link2)) return false;
@@ -1285,6 +1387,36 @@ function indexByNormalizedName(nodes) {
1285
1387
  }
1286
1388
  return out;
1287
1389
  }
1390
+ function basenameWithoutExt(path) {
1391
+ const base = pathPosix2.basename(path);
1392
+ const ext = pathPosix2.extname(base);
1393
+ return ext ? base.slice(0, -ext.length) : base;
1394
+ }
1395
+ function indexByBasenameWithoutName(nodes) {
1396
+ const out = /* @__PURE__ */ new Map();
1397
+ for (const node of nodes) {
1398
+ const raw = node.frontmatter?.["name"];
1399
+ const name = typeof raw === "string" ? raw : "";
1400
+ if (name) continue;
1401
+ const bare = basenameWithoutExt(node.path);
1402
+ if (!bare) continue;
1403
+ const key = normalizeTrigger(bare);
1404
+ if (!key) continue;
1405
+ const bucket = out.get(key) ?? [];
1406
+ bucket.push(node);
1407
+ out.set(key, bucket);
1408
+ }
1409
+ return out;
1410
+ }
1411
+ function findHintCandidates(link2, idx) {
1412
+ const normalized = link2.trigger?.normalizedTrigger;
1413
+ if (!normalized) return [];
1414
+ const sigil = normalized.charAt(0);
1415
+ if (sigil !== "/" && sigil !== "@") return [];
1416
+ const withoutSigil = normalized.slice(1).trim();
1417
+ if (!withoutSigil) return [];
1418
+ return idx.get(withoutSigil) ?? [];
1419
+ }
1288
1420
  function isResolved(link2, byPath3, byNormalizedName) {
1289
1421
  const normalized = link2.trigger?.normalizedTrigger;
1290
1422
  if (normalized) {
@@ -2831,7 +2963,7 @@ var UPDATE_CHECK_TEXTS = {
2831
2963
  // package.json
2832
2964
  var package_default = {
2833
2965
  name: "@skill-map/cli",
2834
- version: "0.29.0",
2966
+ version: "0.30.0",
2835
2967
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
2836
2968
  license: "MIT",
2837
2969
  type: "module",