@skill-map/cli 0.70.0 → 0.71.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
@@ -1,6 +1,6 @@
1
1
  // cli/entry.ts
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="ca8113af-a7ac-5cd7-b45c-03a5974bca86")}catch(e){}}();
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="81f53ca6-a8bf-5fc1-909d-ce926a83f056")}catch(e){}}();
4
4
  import { existsSync as existsSync34 } from "fs";
5
5
  import { Builtins, Cli as Cli2 } from "clipanion";
6
6
 
@@ -250,7 +250,7 @@ function bucketByKind(kind, instance, bag) {
250
250
  // package.json
251
251
  var package_default = {
252
252
  name: "@skill-map/cli",
253
- version: "0.70.0",
253
+ version: "0.71.0",
254
254
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
255
255
  license: "MIT",
256
256
  type: "module",
@@ -581,7 +581,7 @@ var command_schema_default = {
581
581
  // plugins/ids.ts
582
582
  var CORE_PLUGIN_ID = "core";
583
583
  var CLAUDE_PLUGIN_ID = "claude";
584
- var OPENAI_PLUGIN_ID = "codex";
584
+ var CODEX_PLUGIN_ID = "codex";
585
585
  var ANTIGRAVITY_PLUGIN_ID = "antigravity";
586
586
  var AGENT_SKILLS_PLUGIN_ID = "agent-skills";
587
587
 
@@ -808,9 +808,6 @@ var claudeProvider = {
808
808
  }
809
809
  };
810
810
 
811
- // plugins/claude/extractors/at-directive/index.ts
812
- import { posix as pathPosix } from "path";
813
-
814
811
  // kernel/util/strip-code-blocks.ts
815
812
  var FENCE_RE = /^(?<indent> {0,3})(?<fence>`{3,}|~{3,})/;
816
813
  function stripCodeBlocks(input) {
@@ -818,6 +815,28 @@ function stripCodeBlocks(input) {
818
815
  const fenceless = stripFences(input);
819
816
  return stripInline(fenceless);
820
817
  }
818
+ function findBacktickImbalance(body) {
819
+ if (!body) return null;
820
+ const { stripped, openFenceLine } = scanFences(body);
821
+ if (openFenceLine > 0) {
822
+ return { kind: "fence", line: openFenceLine, sourceLine: sourceLineAt(body, openFenceLine) };
823
+ }
824
+ const survivor = stripInline(maskEscapes(stripped)).indexOf("`");
825
+ if (survivor < 0) return null;
826
+ const line = lineOfIndex(stripped, survivor);
827
+ return { kind: "inline", line, sourceLine: sourceLineAt(body, line) };
828
+ }
829
+ function maskEscapes(text) {
830
+ return text.replace(/\\[^\n]/g, " ");
831
+ }
832
+ function lineOfIndex(text, idx) {
833
+ let line = 1;
834
+ for (let i = 0; i < idx; i++) if (text[i] === "\n") line++;
835
+ return line;
836
+ }
837
+ function sourceLineAt(text, line) {
838
+ return text.split("\n")[line - 1]?.trim() ?? "";
839
+ }
821
840
  function extractCodeRegions(input) {
822
841
  if (!input) return input;
823
842
  const stripped = stripCodeBlocks(input);
@@ -837,30 +856,31 @@ function stripHtml(input) {
837
856
  function stripCodeAndHtml(input) {
838
857
  return stripHtml(stripCodeBlocks(input));
839
858
  }
840
- function stripFences(input) {
859
+ function scanFences(input) {
841
860
  const out = [];
842
861
  const lines = input.split("\n");
843
862
  let openFence = null;
844
- for (const line of lines) {
863
+ let openFenceLine = 0;
864
+ for (let i = 0; i < lines.length; i++) {
865
+ const line = lines[i];
845
866
  if (openFence) {
846
- const closer = matchClosingFence(line, openFence);
847
- if (closer) {
848
- out.push(blank(line));
849
- openFence = null;
850
- } else {
851
- out.push(blank(line));
852
- }
867
+ if (matchClosingFence(line, openFence)) openFence = null;
868
+ out.push(blank(line));
853
869
  continue;
854
870
  }
855
871
  const open3 = FENCE_RE.exec(line);
856
872
  if (open3?.groups) {
857
873
  openFence = open3.groups["fence"];
874
+ openFenceLine = i + 1;
858
875
  out.push(blank(line));
859
876
  continue;
860
877
  }
861
878
  out.push(line);
862
879
  }
863
- return out.join("\n");
880
+ return { stripped: out.join("\n"), openFenceLine: openFence ? openFenceLine : 0 };
881
+ }
882
+ function stripFences(input) {
883
+ return scanFences(input).stripped;
864
884
  }
865
885
  function matchClosingFence(line, openFence) {
866
886
  const m = FENCE_RE.exec(line);
@@ -906,82 +926,61 @@ function normalizeTrigger(source) {
906
926
  return out.trim();
907
927
  }
908
928
 
929
+ // kernel/util/at-token.ts
930
+ import { posix as pathPosix } from "path";
931
+ var AT_TOKEN_RE = /(?:^|[^A-Za-z0-9_@])(@(?:(?:\.{1,2}\/)+|\/)?[a-z0-9](?:[a-z0-9_\-./]*[a-z0-9_])?(?::[a-z0-9][a-z0-9_-]*)?)/gi;
932
+ 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;
933
+ function classifyAtFileToken(bare) {
934
+ if (bare.startsWith("/")) return null;
935
+ if (bare.startsWith("./") || bare.startsWith("../")) return "relative path prefix";
936
+ if (FILE_EXT_RE.test(bare)) return "known file extension";
937
+ return null;
938
+ }
939
+ function resolveAtFileTarget(sourceDir, bare) {
940
+ const joined = sourceDir === "." ? bare : `${sourceDir}/${bare}`;
941
+ return pathPosix.normalize(joined);
942
+ }
943
+
909
944
  // plugins/claude/extractors/at-directive/index.ts
910
945
  var ID = "at-directive";
911
- 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;
912
- 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;
913
946
  var atDirectiveExtractor = {
914
947
  id: ID,
915
948
  pluginId: CLAUDE_PLUGIN_ID,
916
949
  kind: "extractor",
917
- description: "Detects `@<token>` directives in a node's body using Claude Code rules, choosing the link kind by token shape. Example: a bare handle `@team` becomes a `mentions` link, while a file-flavoured token `@docs/api.md` becomes a `references` link.",
950
+ description: "Detects bare `@<handle>` mentions in a node's body using Claude Code rules and emits a `mentions` link to the named entity. Example: `@team` becomes a `mentions` link. File-shaped tokens like `@docs/api.md` are handled by `core/at-file` instead.",
918
951
  scope: "body",
919
- // Claude-only. This is Claude's `@<name>` grammar, where a bare handle is
920
- // an agent / entity MENTION. OpenAI Codex's `@` is a file-path picker, not
921
- // a mention grammar, so codex is NOT gated here; the codex-owned `at-file`
922
- // extractor covers `@`-as-file-reference under the codex lens.
952
+ // Claude-only. This is Claude's bare `@<handle>` MENTION grammar. The
953
+ // file-reference half of `@` (file-shaped tokens) is the shared
954
+ // `core/at-file` extractor, which also serves codex / antigravity; this
955
+ // extractor defers file-shaped tokens to it.
923
956
  precondition: { provider: ["claude"] },
924
- // eslint-disable-next-line complexity
925
957
  extract(ctx) {
926
958
  const seenMentions = /* @__PURE__ */ new Set();
927
- const seenReferences = /* @__PURE__ */ new Set();
928
959
  const body = stripCodeAndHtml(ctx.body);
929
960
  const lineStarts = computeLineStarts(body);
930
- const sourceDir = pathPosix.dirname(ctx.node.path);
931
- for (const match of body.matchAll(AT_RE)) {
961
+ for (const match of body.matchAll(AT_TOKEN_RE)) {
932
962
  const original = match[1];
933
963
  const bare = original.slice(1);
934
- const captureOffset = (match.index ?? 0) + match[0].indexOf(original);
935
- const line = lineFor(lineStarts, captureOffset);
936
- const range = { start: captureOffset, end: captureOffset + original.length, line };
937
964
  if (bare.startsWith("/")) continue;
938
- const isReference = bare.startsWith("./") || bare.startsWith("../") || FILE_EXT_RE.test(bare);
939
- if (isReference) {
940
- const target = resolveSourceRelative(sourceDir, bare);
941
- const dedupKey = target.toLowerCase();
942
- if (seenReferences.has(dedupKey)) continue;
943
- seenReferences.add(dedupKey);
944
- ctx.emitSignal({
945
- source: ctx.node.path,
946
- scope: "body",
947
- range,
948
- raw: original,
949
- candidates: [
950
- {
951
- extractorId: ID,
952
- kind: "references",
953
- target,
954
- // 0.85: strong file signal (path prefix `./` / `../` OR
955
- // a known file extension on the tail). One degree of
956
- // inference (the runtime still resolves the path).
957
- confidence: 0.85,
958
- rationale: bare.startsWith("./") || bare.startsWith("../") ? "relative path prefix" : "known file extension",
959
- trigger: {
960
- originalTrigger: original,
961
- normalizedTrigger: target
962
- }
963
- }
964
- ]
965
- });
966
- continue;
967
- }
965
+ if (classifyAtFileToken(bare) !== null) continue;
968
966
  const normalized = normalizeTrigger(original);
969
967
  if (seenMentions.has(normalized)) continue;
970
968
  seenMentions.add(normalized);
969
+ const captureOffset = (match.index ?? 0) + match[0].indexOf(original);
970
+ const line = lineFor(lineStarts, captureOffset);
971
971
  ctx.emitSignal({
972
972
  source: ctx.node.path,
973
973
  scope: "body",
974
- range,
974
+ range: { start: captureOffset, end: captureOffset + original.length, line },
975
975
  raw: original,
976
976
  candidates: [
977
977
  {
978
978
  extractorId: ID,
979
979
  kind: "mentions",
980
980
  target: original,
981
- // 0.5: genuine ambiguity. A bare `@handle` (no extension, no
982
- // path prefix) could be an agent, a handle, or generic prose.
983
- // The runtime decides at invocation time; the extractor leaves
984
- // the question open.
981
+ // 0.5: genuine ambiguity. A bare `@handle` (no extension, no path
982
+ // prefix) could be an agent, a handle, or generic prose. The
983
+ // runtime decides at invocation time; the extractor leaves it open.
985
984
  confidence: 0.5,
986
985
  rationale: "no extension, no path prefix",
987
986
  trigger: {
@@ -994,72 +993,9 @@ var atDirectiveExtractor = {
994
993
  }
995
994
  }
996
995
  };
997
- function resolveSourceRelative(sourceDir, bare) {
998
- const joined = sourceDir === "." ? bare : `${sourceDir}/${bare}`;
999
- return pathPosix.normalize(joined);
1000
- }
1001
-
1002
- // plugins/claude/extractors/slash-command/index.ts
1003
- var ID2 = "slash-command";
1004
- var SLASH_RE = /(?<![A-Za-z0-9_/.:?#=&])(\/[a-z0-9][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)/gi;
1005
- var slashCommandExtractor = {
1006
- id: ID2,
1007
- pluginId: CLAUDE_PLUGIN_ID,
1008
- kind: "extractor",
1009
- description: "Turns `/command` invocations in a node's body into arrows that point at the resolved slash command or skill, using Claude Code routing rules. Example: `/deploy` in the body draws an arrow to the `deploy` command.",
1010
- scope: "body",
1011
- // Also authorised under the antigravity lens, which shares the `/command`
1012
- // grammar: a workflow / skill / AGENTS.md body's `/name` tokens resolve to
1013
- // BOTH skills and workflows (`invokes: ['skill', 'workflow']`), since
1014
- // Antigravity invokes either by the same slash. NOT gated under codex:
1015
- // OpenAI Codex reserves `/` for its OWN built-in commands (`/model`,
1016
- // `/init`, ...) and invokes user skills with `$` instead (parsed by the
1017
- // codex `dollar-skill` extractor). A lens that declares no `invokes`
1018
- // resolution leaves the signals unresolved (no spurious edges).
1019
- precondition: { provider: ["claude", "antigravity"] },
1020
- extract(ctx) {
1021
- const seen = /* @__PURE__ */ new Set();
1022
- const body = stripCodeAndHtml(ctx.body);
1023
- const lineStarts = computeLineStarts(body);
1024
- for (const match of body.matchAll(SLASH_RE)) {
1025
- const original = match[1];
1026
- const endIdx = (match.index ?? 0) + match[0].length;
1027
- const nextChar = body[endIdx];
1028
- if (nextChar && /[A-Za-z0-9_/-]/.test(nextChar)) continue;
1029
- const normalized = normalizeTrigger(original);
1030
- if (seen.has(normalized)) continue;
1031
- seen.add(normalized);
1032
- const captureOffset = (match.index ?? 0) + match[0].indexOf(original);
1033
- const line = lineFor(lineStarts, captureOffset);
1034
- ctx.emitSignal({
1035
- source: ctx.node.path,
1036
- scope: "body",
1037
- range: { start: captureOffset, end: captureOffset + original.length, line },
1038
- raw: original,
1039
- candidates: [
1040
- {
1041
- extractorId: ID2,
1042
- kind: "invokes",
1043
- target: original,
1044
- // 0.8: clean `/command` match after code-block strip. The
1045
- // post-match path guard above filters URL / file-path noise,
1046
- // so a hit is unambiguous syntax. Resolution against the
1047
- // live skill / command catalog happens downstream.
1048
- confidence: 0.8,
1049
- rationale: "unambiguous slash syntax post code-block strip",
1050
- trigger: {
1051
- originalTrigger: original,
1052
- normalizedTrigger: normalized
1053
- }
1054
- }
1055
- ]
1056
- });
1057
- }
1058
- }
1059
- };
1060
996
 
1061
997
  // plugins/claude/extractors/tools-counter/index.ts
1062
- var ID3 = "tools-counter";
998
+ var ID2 = "tools-counter";
1063
999
  var count = {
1064
1000
  slot: "card.footer.left",
1065
1001
  icon: "pi-wrench",
@@ -1069,7 +1005,7 @@ var count = {
1069
1005
  };
1070
1006
  var TOOLTIP_MAX = 255;
1071
1007
  var toolsCounterExtractor = {
1072
- id: ID3,
1008
+ id: ID2,
1073
1009
  pluginId: CLAUDE_PLUGIN_ID,
1074
1010
  kind: "extractor",
1075
1011
  description: "Counts the tools an agent declares in its frontmatter and shows the count on the agent card. Example: an agent with `tools: [Bash, Read, Grep]` shows a count of 3.",
@@ -1355,10 +1291,15 @@ var antigravityProvider = {
1355
1291
  schemaJson: workflow_schema_default,
1356
1292
  ui: {
1357
1293
  label: "Workflows",
1358
- // Antigravity violet, so a workflow node reads as Antigravity's own
1359
- // (skills keep the normalised cross-provider green of COMMONS_KINDS).
1360
- color: "#7c3aed",
1361
- colorDark: "#a78bfa",
1294
+ // A workflow is Antigravity's command-equivalent (its own slash-
1295
+ // invocable procedural kind, the slot Claude fills with `command`),
1296
+ // so it adopts the SAME color as Claude's `command` kind (amber) for a
1297
+ // uniform cross-provider vocabulary, the same way skills normalise to
1298
+ // the cross-provider green of COMMONS_KINDS. The Antigravity violet
1299
+ // stays on the provider-level `presentation` (the lens identity), not
1300
+ // on this kind.
1301
+ color: "#f59e0b",
1302
+ colorDark: "#fbbf24",
1362
1303
  icon: { kind: "pi", id: "pi-sitemap" }
1363
1304
  },
1364
1305
  // The handle is ALWAYS the filename stem (`/<name>`): Antigravity
@@ -1370,7 +1311,12 @@ var antigravityProvider = {
1370
1311
  // `/<name>` slash invocations resolve to BOTH skills and workflows: under
1371
1312
  // the antigravity lens a `/deploy` links to either `.agents/skills/deploy`
1372
1313
  // or `.agent/workflows/deploy.md`. Overrides the open-standard default
1373
- // (`invokes: ['skill']`) to add the own `workflow` kind.
1314
+ // (`invokes: ['skill']`) to add the own `workflow` kind. Antigravity's OTHER
1315
+ // connector, `@filename` file references (the documented rules / skill /
1316
+ // workflow file pointer, a file-picker grammar like Codex's, distinct from
1317
+ // Claude's `@`-agent-mention), needs no entry here: the shared `core/at-file`
1318
+ // extractor (gated to claude / codex / antigravity) emits them as
1319
+ // `references` resolved by PATH, lens-independent.
1374
1320
  resolution: { invokes: ["skill", "workflow"] },
1375
1321
  classify(path) {
1376
1322
  if (/^\.agent\/workflows\/[^/]+\.md$/.test(path.toLowerCase())) return "workflow";
@@ -1461,7 +1407,7 @@ var agent_schema_default2 = {
1461
1407
  // plugins/codex/providers/codex/index.ts
1462
1408
  var codexProvider = {
1463
1409
  id: "codex",
1464
- pluginId: OPENAI_PLUGIN_ID,
1410
+ pluginId: CODEX_PLUGIN_ID,
1465
1411
  kind: "provider",
1466
1412
  description: "Classifies `.codex/agents/*.toml` as OpenAI Codex CLI sub-agents and `.agents/skills/*/SKILL.md` as Codex skills (open standard).",
1467
1413
  // Provider identity for the active-lens dropdown, the topbar lens chip,
@@ -1562,78 +1508,12 @@ var codexProvider = {
1562
1508
  }
1563
1509
  };
1564
1510
 
1565
- // plugins/codex/extractors/at-file/index.ts
1566
- import { posix as pathPosix2 } from "path";
1567
- var ID4 = "at-file";
1568
- var AT_RE2 = /(?:^|[^A-Za-z0-9_@])(@(?:\.{1,2}\/|\/)?[a-z0-9](?:[a-z0-9_\-./]*[a-z0-9_])?(?::[a-z0-9][a-z0-9_-]*)?)/gi;
1569
- var FILE_EXT_RE2 = /\.(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;
1570
- var atFileExtractor = {
1571
- id: ID4,
1572
- pluginId: OPENAI_PLUGIN_ID,
1573
- kind: "extractor",
1574
- description: "Detects `@<file>` references in a node's body under the OpenAI Codex lens, where `@` is a file picker. A path- or extension-shaped token becomes a `references` link to that file; a bare `@handle` forms no edge. Example: `@builder.toml` in an agent's prompt draws an arrow to the `builder` agent file.",
1575
- scope: "body",
1576
- // Codex-only: under the codex lens `@` is a file-path picker, so only
1577
- // file-shaped tokens form references. The claude `at-directive` (bare
1578
- // `@handle` → agent mention) is NOT gated under codex.
1579
- precondition: { provider: ["codex"] },
1580
- extract(ctx) {
1581
- const seenReferences = /* @__PURE__ */ new Set();
1582
- const body = stripCodeAndHtml(ctx.body);
1583
- const lineStarts = computeLineStarts(body);
1584
- const sourceDir = pathPosix2.dirname(ctx.node.path);
1585
- for (const match of body.matchAll(AT_RE2)) {
1586
- const original = match[1];
1587
- const bare = original.slice(1);
1588
- const rationale = classifyAtToken(bare);
1589
- if (rationale === null) continue;
1590
- const target = resolveSourceRelative2(sourceDir, bare);
1591
- const dedupKey = target.toLowerCase();
1592
- if (seenReferences.has(dedupKey)) continue;
1593
- seenReferences.add(dedupKey);
1594
- const captureOffset = (match.index ?? 0) + match[0].indexOf(original);
1595
- const line = lineFor(lineStarts, captureOffset);
1596
- ctx.emitSignal({
1597
- source: ctx.node.path,
1598
- scope: "body",
1599
- range: { start: captureOffset, end: captureOffset + original.length, line },
1600
- raw: original,
1601
- candidates: [
1602
- {
1603
- extractorId: ID4,
1604
- kind: "references",
1605
- target,
1606
- // 0.85: strong file signal (path prefix or known extension),
1607
- // one degree of inference (the runtime resolves the path).
1608
- confidence: 0.85,
1609
- rationale,
1610
- trigger: {
1611
- originalTrigger: original,
1612
- normalizedTrigger: target
1613
- }
1614
- }
1615
- ]
1616
- });
1617
- }
1618
- }
1619
- };
1620
- function classifyAtToken(bare) {
1621
- if (bare.startsWith("/")) return null;
1622
- if (bare.startsWith("./") || bare.startsWith("../")) return "relative path prefix";
1623
- if (FILE_EXT_RE2.test(bare)) return "known file extension";
1624
- return null;
1625
- }
1626
- function resolveSourceRelative2(sourceDir, bare) {
1627
- const joined = sourceDir === "." ? bare : `${sourceDir}/${bare}`;
1628
- return pathPosix2.normalize(joined);
1629
- }
1630
-
1631
1511
  // plugins/codex/extractors/dollar-skill/index.ts
1632
- var ID5 = "dollar-skill";
1512
+ var ID3 = "dollar-skill";
1633
1513
  var DOLLAR_RE = /(?<![A-Za-z0-9_$])(\$[a-z][a-z0-9_-]*)/g;
1634
1514
  var dollarSkillExtractor = {
1635
- id: ID5,
1636
- pluginId: OPENAI_PLUGIN_ID,
1515
+ id: ID3,
1516
+ pluginId: CODEX_PLUGIN_ID,
1637
1517
  kind: "extractor",
1638
1518
  description: "Turns `$skill` invocations in a node's body into arrows that point at the resolved Codex skill, using OpenAI Codex routing rules. Example: `$check-links` in the body draws an arrow to the `check-links` skill.",
1639
1519
  scope: "body",
@@ -1659,7 +1539,7 @@ var dollarSkillExtractor = {
1659
1539
  raw: original,
1660
1540
  candidates: [
1661
1541
  {
1662
- extractorId: ID5,
1542
+ extractorId: ID3,
1663
1543
  kind: "invokes",
1664
1544
  target: original,
1665
1545
  // 0.8: clean `$skill` match after code-block strip. The
@@ -1765,12 +1645,68 @@ var coreMarkdownProvider = {
1765
1645
  }
1766
1646
  };
1767
1647
 
1648
+ // plugins/core/extractors/at-file/index.ts
1649
+ import { posix as pathPosix2 } from "path";
1650
+ var ID4 = "at-file";
1651
+ var atFileExtractor = {
1652
+ id: ID4,
1653
+ pluginId: CORE_PLUGIN_ID,
1654
+ kind: "extractor",
1655
+ description: "Detects `@<file>` references in a node's body for the `@`-file-picker lenses (Codex, Antigravity, Claude), where `@` points at a workspace file. A path- or extension-shaped token becomes a `references` link to that file; a bare `@handle` forms no edge. Example: `@builder.toml` in a body draws an arrow to the `builder` file.",
1656
+ scope: "body",
1657
+ // The lenses whose runtime reads `@` as a file-path picker. Codex (`@` is a
1658
+ // file picker, `$` invokes skills), Antigravity (documented `@filename` file
1659
+ // refs), and Claude (`@file.md` file ref; its bare-handle MENTION half lives
1660
+ // in `claude/at-directive`). NOT gated under `agent-skills` / `markdown`,
1661
+ // whose runtimes read `@` as nothing (no phantom edges).
1662
+ precondition: { provider: ["claude", "codex", "antigravity"] },
1663
+ extract(ctx) {
1664
+ const seenReferences = /* @__PURE__ */ new Set();
1665
+ const body = stripCodeAndHtml(ctx.body);
1666
+ const lineStarts = computeLineStarts(body);
1667
+ const sourceDir = pathPosix2.dirname(ctx.node.path);
1668
+ for (const match of body.matchAll(AT_TOKEN_RE)) {
1669
+ const original = match[1];
1670
+ const bare = original.slice(1);
1671
+ const rationale = classifyAtFileToken(bare);
1672
+ if (rationale === null) continue;
1673
+ const target = resolveAtFileTarget(sourceDir, bare);
1674
+ const dedupKey = target.toLowerCase();
1675
+ if (seenReferences.has(dedupKey)) continue;
1676
+ seenReferences.add(dedupKey);
1677
+ const captureOffset = (match.index ?? 0) + match[0].indexOf(original);
1678
+ const line = lineFor(lineStarts, captureOffset);
1679
+ ctx.emitSignal({
1680
+ source: ctx.node.path,
1681
+ scope: "body",
1682
+ range: { start: captureOffset, end: captureOffset + original.length, line },
1683
+ raw: original,
1684
+ candidates: [
1685
+ {
1686
+ extractorId: ID4,
1687
+ kind: "references",
1688
+ target,
1689
+ // 0.85: strong file signal (path prefix or known extension),
1690
+ // one degree of inference (the runtime resolves the path).
1691
+ confidence: 0.85,
1692
+ rationale,
1693
+ trigger: {
1694
+ originalTrigger: original,
1695
+ normalizedTrigger: target
1696
+ }
1697
+ }
1698
+ ]
1699
+ });
1700
+ }
1701
+ }
1702
+ };
1703
+
1768
1704
  // plugins/core/extractors/backtick-path/index.ts
1769
1705
  import { posix as pathPosix3 } from "path";
1770
- var ID6 = "backtick-path";
1706
+ var ID5 = "backtick-path";
1771
1707
  var PATH_RE = /(?<![\w/:.-])(?:\.{1,2}\/)?[\w][\w.-]*(?:\/[\w.-]+)*\.md\b(?![\w/])/g;
1772
1708
  var backtickPathExtractor = {
1773
- id: ID6,
1709
+ id: ID5,
1774
1710
  pluginId: CORE_PLUGIN_ID,
1775
1711
  kind: "extractor",
1776
1712
  description: "Turns relative .md paths written inside code spans and fenced blocks into arrows between nodes in the graph. Example: a backticked `references/rules.md` path draws an arrow to that file.",
@@ -1795,7 +1731,7 @@ var backtickPathExtractor = {
1795
1731
  raw: original,
1796
1732
  candidates: [
1797
1733
  {
1798
- extractorId: ID6,
1734
+ extractorId: ID5,
1799
1735
  kind: "points",
1800
1736
  target: resolved,
1801
1737
  // 0.85: a strong file signal with one degree of inference,
@@ -1824,7 +1760,7 @@ function resolveTarget(sourceDir, raw) {
1824
1760
  }
1825
1761
 
1826
1762
  // plugins/core/extractors/external-url-counter/index.ts
1827
- var ID7 = "external-url-counter";
1763
+ var ID6 = "external-url-counter";
1828
1764
  var count2 = {
1829
1765
  slot: "card.footer.left",
1830
1766
  icon: "pi-link",
@@ -1844,7 +1780,7 @@ var settings = {
1844
1780
  var URL_RE = /https?:\/\/[^\s<>"'`)\]]+/g;
1845
1781
  var TRAILING_PUNCT = /[.,;:!?]+$/;
1846
1782
  var externalUrlCounterExtractor = {
1847
- id: ID7,
1783
+ id: ID6,
1848
1784
  pluginId: CORE_PLUGIN_ID,
1849
1785
  kind: "extractor",
1850
1786
  description: "Counts the distinct external URLs in a node's body and shows the count on the card. Example: a body linking `https://example.com` and `https://docs.rs` shows a count of 2.",
@@ -1896,7 +1832,7 @@ var externalUrlCounterExtractor = {
1896
1832
  raw: original,
1897
1833
  candidates: [
1898
1834
  {
1899
- extractorId: ID7,
1835
+ extractorId: ID6,
1900
1836
  kind: "references",
1901
1837
  target: normalized.href,
1902
1838
  confidence: 0.3,
@@ -1938,11 +1874,11 @@ function normalizeUrl(raw) {
1938
1874
 
1939
1875
  // plugins/core/extractors/markdown-link/index.ts
1940
1876
  import { posix as pathPosix4 } from "path";
1941
- var ID8 = "markdown-link";
1877
+ var ID7 = "markdown-link";
1942
1878
  var LINK_RE = /(?<!!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
1943
1879
  var URL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
1944
1880
  var markdownLinkExtractor = {
1945
- id: ID8,
1881
+ id: ID7,
1946
1882
  pluginId: CORE_PLUGIN_ID,
1947
1883
  kind: "extractor",
1948
1884
  description: "Turns markdown links (`[text](path)`) in a node's body into arrows between nodes in the graph. Example: `[the guide](docs/guide.md)` draws an arrow to `docs/guide.md`.",
@@ -1967,7 +1903,7 @@ var markdownLinkExtractor = {
1967
1903
  raw: match[0],
1968
1904
  candidates: [
1969
1905
  {
1970
- extractorId: ID8,
1906
+ extractorId: ID7,
1971
1907
  kind: "references",
1972
1908
  target: resolved,
1973
1909
  // 0.95: the `[text](path)` syntax is unambiguous (the spec's
@@ -2002,10 +1938,10 @@ function resolveTarget2(sourceDir, raw) {
2002
1938
  }
2003
1939
 
2004
1940
  // plugins/core/extractors/mcp-tools/index.ts
2005
- var ID9 = "mcp-tools";
1941
+ var ID8 = "mcp-tools";
2006
1942
  var MCP_PATTERN = /^mcp__([a-z0-9][a-z0-9_-]*)__[a-z0-9_-]+$/i;
2007
1943
  var mcpToolsExtractor = {
2008
- id: ID9,
1944
+ id: ID8,
2009
1945
  pluginId: CORE_PLUGIN_ID,
2010
1946
  kind: "extractor",
2011
1947
  description: "Turns `tools: [mcp__<server>__<tool>]` entries in a node's frontmatter into an MCP node per unique server and an arrow from the source to each one. Example: `tools: [mcp__github__create_pr]` adds an `mcp://github` node and an arrow to it.",
@@ -2036,7 +1972,7 @@ var mcpToolsExtractor = {
2036
1972
  raw: `mcp__${server}__*`,
2037
1973
  candidates: [
2038
1974
  {
2039
- extractorId: ID9,
1975
+ extractorId: ID8,
2040
1976
  kind: "references",
2041
1977
  target: mcpPath,
2042
1978
  confidence: 0.85,
@@ -2066,6 +2002,65 @@ function collectMcpServers(tools) {
2066
2002
  return out;
2067
2003
  }
2068
2004
 
2005
+ // plugins/core/extractors/slash-command/index.ts
2006
+ var ID9 = "slash-command";
2007
+ var SLASH_RE = /(?<![A-Za-z0-9_/.:?#=&])(\/[a-z0-9][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)/gi;
2008
+ var slashCommandExtractor = {
2009
+ id: ID9,
2010
+ pluginId: CORE_PLUGIN_ID,
2011
+ kind: "extractor",
2012
+ description: "Turns `/command` invocations in a node's body into arrows that point at the resolved slash command, skill, or workflow, using the `/`-grammar shared by Claude and Antigravity. Example: `/deploy` in the body draws an arrow to the `deploy` command.",
2013
+ scope: "body",
2014
+ // Also authorised under the antigravity lens, which shares the `/command`
2015
+ // grammar: a workflow / skill / AGENTS.md body's `/name` tokens resolve to
2016
+ // BOTH skills and workflows (`invokes: ['skill', 'workflow']`), since
2017
+ // Antigravity invokes either by the same slash. NOT gated under codex:
2018
+ // OpenAI Codex reserves `/` for its OWN built-in commands (`/model`,
2019
+ // `/init`, ...) and invokes user skills with `$` instead (parsed by the
2020
+ // codex `dollar-skill` extractor). A lens that declares no `invokes`
2021
+ // resolution leaves the signals unresolved (no spurious edges).
2022
+ precondition: { provider: ["claude", "antigravity"] },
2023
+ extract(ctx) {
2024
+ const seen = /* @__PURE__ */ new Set();
2025
+ const body = stripCodeAndHtml(ctx.body);
2026
+ const lineStarts = computeLineStarts(body);
2027
+ for (const match of body.matchAll(SLASH_RE)) {
2028
+ const original = match[1];
2029
+ const endIdx = (match.index ?? 0) + match[0].length;
2030
+ const nextChar = body[endIdx];
2031
+ if (nextChar && /[A-Za-z0-9_/-]/.test(nextChar)) continue;
2032
+ const normalized = normalizeTrigger(original);
2033
+ if (seen.has(normalized)) continue;
2034
+ seen.add(normalized);
2035
+ const captureOffset = (match.index ?? 0) + match[0].indexOf(original);
2036
+ const line = lineFor(lineStarts, captureOffset);
2037
+ ctx.emitSignal({
2038
+ source: ctx.node.path,
2039
+ scope: "body",
2040
+ range: { start: captureOffset, end: captureOffset + original.length, line },
2041
+ raw: original,
2042
+ candidates: [
2043
+ {
2044
+ extractorId: ID9,
2045
+ kind: "invokes",
2046
+ target: original,
2047
+ // 0.8: clean `/command` match after code-block strip. The
2048
+ // post-match path guard above filters URL / file-path noise,
2049
+ // so a hit is unambiguous syntax. Resolution against the
2050
+ // live skill / command catalog happens downstream.
2051
+ confidence: 0.8,
2052
+ rationale: "unambiguous slash syntax post code-block strip",
2053
+ trigger: {
2054
+ originalTrigger: original,
2055
+ normalizedTrigger: normalized
2056
+ }
2057
+ }
2058
+ ]
2059
+ });
2060
+ }
2061
+ }
2062
+ };
2063
+
2069
2064
  // plugins/core/analyzers/annotation-field-unknown/index.ts
2070
2065
  import { readFileSync } from "fs";
2071
2066
  import { dirname, resolve } from "path";
@@ -4651,18 +4646,18 @@ var updateCheckHook = {
4651
4646
  // plugins/built-ins.ts
4652
4647
  var claudeProvider2 = { ...claudeProvider, pluginId: "claude", version: VERSION };
4653
4648
  var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "claude", version: VERSION };
4654
- var slashCommandExtractor2 = { ...slashCommandExtractor, pluginId: "claude", version: VERSION };
4655
4649
  var toolsCounterExtractor2 = { ...toolsCounterExtractor, pluginId: "claude", version: VERSION };
4656
4650
  var antigravityProvider2 = { ...antigravityProvider, pluginId: "antigravity", version: VERSION };
4657
4651
  var codexProvider2 = { ...codexProvider, pluginId: "codex", version: VERSION };
4658
- var atFileExtractor2 = { ...atFileExtractor, pluginId: "codex", version: VERSION };
4659
4652
  var dollarSkillExtractor2 = { ...dollarSkillExtractor, pluginId: "codex", version: VERSION };
4660
4653
  var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills", version: VERSION };
4661
4654
  var coreMarkdownProvider2 = { ...coreMarkdownProvider, pluginId: "core", version: VERSION };
4655
+ var atFileExtractor2 = { ...atFileExtractor, pluginId: "core", version: VERSION };
4662
4656
  var backtickPathExtractor2 = { ...backtickPathExtractor, pluginId: "core", version: VERSION };
4663
4657
  var externalUrlCounterExtractor2 = { ...externalUrlCounterExtractor, pluginId: "core", version: VERSION };
4664
4658
  var markdownLinkExtractor2 = { ...markdownLinkExtractor, pluginId: "core", version: VERSION };
4665
4659
  var mcpToolsExtractor2 = { ...mcpToolsExtractor, pluginId: "core", version: VERSION };
4660
+ var slashCommandExtractor2 = { ...slashCommandExtractor, pluginId: "core", version: VERSION };
4666
4661
  var annotationFieldUnknownAnalyzer2 = { ...annotationFieldUnknownAnalyzer, pluginId: "core", version: VERSION };
4667
4662
  var annotationOrphanAnalyzer2 = { ...annotationOrphanAnalyzer, pluginId: "core", version: VERSION };
4668
4663
  var annotationStaleAnalyzer2 = { ...annotationStaleAnalyzer, pluginId: "core", version: VERSION };
@@ -4691,7 +4686,6 @@ var builtInPlugins = [
4691
4686
  extensions: [
4692
4687
  claudeProvider2,
4693
4688
  atDirectiveExtractor2,
4694
- slashCommandExtractor2,
4695
4689
  toolsCounterExtractor2
4696
4690
  ]
4697
4691
  },
@@ -4707,7 +4701,6 @@ var builtInPlugins = [
4707
4701
  description: "OpenAI Codex CLI platform integration. Classifies TOML sub-agent definitions under `.codex/agents/*.toml`.",
4708
4702
  extensions: [
4709
4703
  codexProvider2,
4710
- atFileExtractor2,
4711
4704
  dollarSkillExtractor2
4712
4705
  ]
4713
4706
  },
@@ -4723,10 +4716,12 @@ var builtInPlugins = [
4723
4716
  description: "Core extensions shared across providers: parsers, extractors, analyzers, actions, hooks, formatters, and the universal `.md` fallback provider.",
4724
4717
  extensions: [
4725
4718
  coreMarkdownProvider2,
4719
+ atFileExtractor2,
4726
4720
  backtickPathExtractor2,
4727
4721
  externalUrlCounterExtractor2,
4728
4722
  markdownLinkExtractor2,
4729
4723
  mcpToolsExtractor2,
4724
+ slashCommandExtractor2,
4730
4725
  annotationFieldUnknownAnalyzer2,
4731
4726
  annotationOrphanAnalyzer2,
4732
4727
  annotationStaleAnalyzer2,
@@ -16538,6 +16533,8 @@ var ORCHESTRATOR_TEXTS = {
16538
16533
  frontmatterMalformedPasteWithIndent: "Frontmatter fence in {{path}} appears indented; YAML frontmatter MUST start with `---` at column 0. The file was scanned as body-only; the metadata block was silently lost. Move the `---` lines to the start of the line.",
16539
16534
  frontmatterMalformedByteOrderMark: "Frontmatter fence in {{path}} is preceded by a UTF-8 byte-order mark (BOM); the file was scanned as body-only. Re-save the file as UTF-8 without BOM. The metadata block was silently lost.",
16540
16535
  frontmatterMalformedMissingClose: "Frontmatter in {{path}} opens with `---` but never closes (no matching `---` line at column 0 was found). The file was scanned as body-only and every metadata field was silently lost. Add a closing `---` line below the metadata block.",
16536
+ bodyBacktickUnclosedFence: "Body of {{path}} has an unclosed fenced code block opened at body line {{line}} (no matching closing ``` or ~~~). The code-strip policy then reads the rest of the file as code, so prose extractors stop emitting edges past it. Close the fence.",
16537
+ bodyBacktickUnclosedInline: "Body of {{path}} has an unclosed inline backtick at body line {{line}} (the backtick run has no equal-length closer). Close the inline span with a matching backtick run, or escape a literal backtick with a backslash.",
16541
16538
  extensionErrorLinkKindNotDeclared: 'Extractor "{{extractorId}}" emitted a link of kind "{{linkKind}}" outside its declared `emitsLinkKinds` set [{{declaredKinds}}]. Link dropped.',
16542
16539
  extensionErrorIssueInvalidSeverity: `Rule "{{analyzerId}}" emitted an issue with invalid severity {{severity}} (allowed: 'error' | 'warn' | 'info'). Issue dropped.`,
16543
16540
  extensionErrorContributionUndeclaredRef: 'Extension "{{extractorId}}" emitted a view contribution on {{nodePath}} whose object is not one declared in its `ui` map (pass the declared const by reference, do not spread or inline it). Contribution dropped.',
@@ -17223,6 +17220,11 @@ var FRONTMATTER_ISSUE_ANALYZERS = /* @__PURE__ */ new Set([
17223
17220
  "frontmatter-malformed",
17224
17221
  "frontmatter-parse-error"
17225
17222
  ]);
17223
+ var BACKTICK_ISSUE_ID = "backtick-unbalanced";
17224
+ var CACHED_KERNEL_ISSUE_ANALYZERS = /* @__PURE__ */ new Set([
17225
+ ...FRONTMATTER_ISSUE_ANALYZERS,
17226
+ BACKTICK_ISSUE_ID
17227
+ ]);
17226
17228
 
17227
17229
  // kernel/orchestrator/cache.ts
17228
17230
  function indexPriorSnapshot(prior) {
@@ -17252,7 +17254,7 @@ function indexPriorLinks(links, byOriginating) {
17252
17254
  }
17253
17255
  function indexPriorFrontmatterIssues(issues, byNode) {
17254
17256
  for (const issue of issues) {
17255
- if (!FRONTMATTER_ISSUE_ANALYZERS.has(issue.analyzerId)) continue;
17257
+ if (!CACHED_KERNEL_ISSUE_ANALYZERS.has(issue.analyzerId)) continue;
17256
17258
  if (issue.nodeIds.length !== 1) continue;
17257
17259
  const path = issue.nodeIds[0];
17258
17260
  const list = byNode.get(path);
@@ -17996,6 +17998,21 @@ function malformedMessage(hint, path) {
17996
17998
  }
17997
17999
  }
17998
18000
 
18001
+ // kernel/orchestrator/body-syntax.ts
18002
+ function detectUnclosedBacktick(body, path, strict) {
18003
+ const imbalance = findBacktickImbalance(body);
18004
+ if (!imbalance) return null;
18005
+ const template = imbalance.kind === "fence" ? ORCHESTRATOR_TEXTS.bodyBacktickUnclosedFence : ORCHESTRATOR_TEXTS.bodyBacktickUnclosedInline;
18006
+ return {
18007
+ analyzerId: BACKTICK_ISSUE_ID,
18008
+ severity: strict ? "error" : "warn",
18009
+ nodeIds: [path],
18010
+ message: tx(template, { path, line: imbalance.line }),
18011
+ detail: imbalance.sourceLine,
18012
+ data: { kind: imbalance.kind, line: imbalance.line }
18013
+ };
18014
+ }
18015
+
17999
18016
  // kernel/orchestrator/node-build.ts
18000
18017
  function buildNode(args2) {
18001
18018
  const bytesFrontmatter = Buffer.byteLength(args2.frontmatterRaw, "utf8");
@@ -18122,6 +18139,9 @@ function relativePathFromRoots(absolutePath, roots) {
18122
18139
  }
18123
18140
  return absolutePath;
18124
18141
  }
18142
+ function pushIssue(list, issue) {
18143
+ if (issue) list.push(issue);
18144
+ }
18125
18145
  function buildFreshNodeAndValidateFrontmatter(opts) {
18126
18146
  const node = buildNode({
18127
18147
  path: opts.raw.path,
@@ -18149,19 +18169,27 @@ function buildFreshNodeAndValidateFrontmatter(opts) {
18149
18169
  }
18150
18170
  }
18151
18171
  if (opts.raw.frontmatterRaw.length > 0) {
18152
- const fmIssue = validateFrontmatter(
18153
- opts.providerFrontmatter,
18154
- opts.provider,
18155
- opts.kind,
18156
- opts.raw.frontmatter,
18157
- opts.raw.path,
18158
- opts.strict
18172
+ pushIssue(
18173
+ frontmatterIssues,
18174
+ validateFrontmatter(
18175
+ opts.providerFrontmatter,
18176
+ opts.provider,
18177
+ opts.kind,
18178
+ opts.raw.frontmatter,
18179
+ opts.raw.path,
18180
+ opts.strict
18181
+ )
18159
18182
  );
18160
- if (fmIssue) frontmatterIssues.push(fmIssue);
18161
18183
  } else {
18162
- const malformed = detectMalformedFrontmatter(opts.raw.body, opts.raw.path, opts.strict);
18163
- if (malformed) frontmatterIssues.push(malformed);
18184
+ pushIssue(
18185
+ frontmatterIssues,
18186
+ detectMalformedFrontmatter(opts.raw.body, opts.raw.path, opts.strict)
18187
+ );
18164
18188
  }
18189
+ pushIssue(
18190
+ frontmatterIssues,
18191
+ detectUnclosedBacktick(opts.raw.body, opts.raw.path, opts.strict)
18192
+ );
18165
18193
  return { node, frontmatterIssues };
18166
18194
  }
18167
18195
 
@@ -32575,15 +32603,20 @@ var TUTORIAL_TEXTS = {
32575
32603
  writtenLabelEs: "Espa\xF1ol",
32576
32604
  // Destination-provider prompt (interactive stdin, no `--for`). Header
32577
32605
  // uses a yellow `?` glyph; options are a numbered list of the provider's
32578
- // vendor name (for a provider with an `aka`, the aka vendor leads and the
32579
- // provider label follows in parentheses), with a `(default)` marker on
32580
- // the first option (Claude). The destination folder is deliberately NOT
32581
- // shown: several providers share `.agents/skills`, so the folder does not
32582
- // identify the lens. The input line accepts a number, a provider id, or
32583
- // an empty answer (which takes the default).
32606
+ // label (for a provider with an `aka`, the standard label leads and the
32607
+ // supporting vendors follow in parentheses, closed by `akaOthers` to mark
32608
+ // the set as open), with a `(default)` marker on the first option (Claude).
32609
+ // The destination folder is deliberately NOT shown: several providers share
32610
+ // `.agents/skills`, so the folder does not identify the lens. The input line
32611
+ // accepts a number, a provider id, or an empty answer (which takes the
32612
+ // default).
32584
32613
  promptHeader: "{{glyph}} Which agent should host the tutorial skill?",
32585
32614
  promptOption: " {{index}}) {{label}}{{marker}}",
32586
32615
  promptDefaultMarker: " (default)",
32616
+ // Trailing token appended to the `aka` vendor list in the open-lens label
32617
+ // ("... (Google's Antigravity, others)"), signalling the open standard is
32618
+ // not tied to a single vendor and more will support it.
32619
+ akaOthers: "others",
32587
32620
  promptInput: " Enter the number or provider id [default {{index}}]: ",
32588
32621
  // Prompt answer matched neither an index nor an id. Goes to stderr,
32589
32622
  // exit code 2. Mirrors the error shape: glyph + headline + dim hint.
@@ -32833,7 +32866,7 @@ function listScaffoldTargets(includeExperimental = false) {
32833
32866
  return out;
32834
32867
  }
32835
32868
  function labelWithAka(target) {
32836
- return target.aka.length > 0 ? `${target.aka.join(", ")} (${target.label})` : target.label;
32869
+ return target.aka.length > 0 ? `${target.label} (${[...target.aka, TUTORIAL_TEXTS.akaOthers].join(", ")})` : target.label;
32837
32870
  }
32838
32871
  function renderTargetLines(targets, def, glyph) {
32839
32872
  const lines = [tx(TUTORIAL_TEXTS.promptHeader, { glyph })];
@@ -33128,4 +33161,4 @@ function resolveBareDefault() {
33128
33161
  process.exit(ExitCode.Error);
33129
33162
  }
33130
33163
  //# sourceMappingURL=cli.js.map
33131
- //# debugId=ca8113af-a7ac-5cd7-b45c-03a5974bca86
33164
+ //# debugId=81f53ca6-a8bf-5fc1-909d-ce926a83f056