ai-code-map 0.1.0-alpha.2 → 0.1.0-alpha.3

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
@@ -7,7 +7,7 @@ import { Command, CommanderError, Option } from "commander";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "ai-code-map",
10
- version: "0.1.0-alpha.2",
10
+ version: "0.1.0-alpha.3",
11
11
  description: "Local, deterministic semantic code maps for AI coding agents",
12
12
  type: "module",
13
13
  sideEffects: false,
@@ -47,6 +47,8 @@ var package_default = {
47
47
  "docs",
48
48
  "examples",
49
49
  "README.md",
50
+ "AGENTS.md",
51
+ "skills",
50
52
  "LICENSE",
51
53
  "CONTRIBUTING.md",
52
54
  "SECURITY.md",
@@ -64,6 +66,7 @@ var package_default = {
64
66
  "schema:check": "node --import tsx scripts/generate-schemas.ts --check",
65
67
  benchmark: "node --import tsx scripts/benchmark-parsers.ts",
66
68
  "benchmark:real-world": "node --import tsx scripts/benchmark-real-world.ts",
69
+ "benchmark:tokens": "node --import tsx scripts/benchmark-tokens.ts",
67
70
  prepack: "pnpm schema:check && pnpm build"
68
71
  },
69
72
  dependencies: {
@@ -1055,9 +1058,9 @@ var CodeMapSchema = CodeMapShapeSchema.superRefine((map, context) => {
1055
1058
  }
1056
1059
  }
1057
1060
  });
1058
- function addDuplicateIssue(context, seen, value, path16) {
1061
+ function addDuplicateIssue(context, seen, value, path18) {
1059
1062
  if (seen.has(value)) {
1060
- context.addIssue({ code: "custom", path: path16, message: `duplicate value ${value}` });
1063
+ context.addIssue({ code: "custom", path: path18, message: `duplicate value ${value}` });
1061
1064
  }
1062
1065
  seen.add(value);
1063
1066
  }
@@ -3351,8 +3354,8 @@ function makeSymbol(context, node, kind, options = {}) {
3351
3354
  };
3352
3355
  }
3353
3356
  function compactText(value, maxLength = 500) {
3354
- const compact = value.replace(/\s+/g, " ").trim();
3355
- return compact.length > maxLength ? `${compact.slice(0, maxLength - 1)}\u2026` : compact;
3357
+ const compact2 = value.replace(/\s+/g, " ").trim();
3358
+ return compact2.length > maxLength ? `${compact2.slice(0, maxLength - 1)}\u2026` : compact2;
3356
3359
  }
3357
3360
  function visit(node, callback) {
3358
3361
  callback(node);
@@ -5203,11 +5206,11 @@ function collectComponentFacts(context, name, parameters, body, profile) {
5203
5206
  if (props.length > 0) {
5204
5207
  facts.props = `${name.toLowerCase()}props:${props.join(",")}`;
5205
5208
  }
5206
- const behavior = collectComponentBehavior(context, body, profile);
5207
- if (behavior.state.length > 0) facts.state = behavior.state.join(",");
5208
- if (behavior.memo.length > 0) facts.memo = behavior.memo.join(",");
5209
- if (behavior.effects.length > 0) facts.effects = behavior.effects.join(",");
5210
- if (behavior.customHooks.length > 0) facts.hooks = behavior.customHooks.join(",");
5209
+ const behavior2 = collectComponentBehavior(context, body, profile);
5210
+ if (behavior2.state.length > 0) facts.state = behavior2.state.join(",");
5211
+ if (behavior2.memo.length > 0) facts.memo = behavior2.memo.join(",");
5212
+ if (behavior2.effects.length > 0) facts.effects = behavior2.effects.join(",");
5213
+ if (behavior2.customHooks.length > 0) facts.hooks = behavior2.customHooks.join(",");
5211
5214
  return facts;
5212
5215
  }
5213
5216
  function collectProps(context, parameter) {
@@ -6685,6 +6688,120 @@ function usesDefaultAnalysisPipeline(options) {
6685
6688
  return !options.analyzers && (!options.parser || isBuiltInTypeScriptParserAdapter(options.parser)) && !options.semanticResolver;
6686
6689
  }
6687
6690
 
6691
+ // src/search.ts
6692
+ var DEFAULT_LIMIT = 10;
6693
+ var MAX_LIMIT = 50;
6694
+ function findSymbols(map, query, options = {}) {
6695
+ const normalized = query.trim();
6696
+ if (!normalized) throw new Error("Symbol search query must not be empty.");
6697
+ const limit = options.limit ?? DEFAULT_LIMIT;
6698
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIMIT) {
6699
+ throw new RangeError(`Symbol search limit must be an integer from 1 to ${MAX_LIMIT}.`);
6700
+ }
6701
+ const qualified = splitQualifiedSelector(normalized);
6702
+ const queryWords = [...new Set(words(normalized))];
6703
+ const ranked = map.symbols.map((symbol) => {
6704
+ const nameWords = new Set(words(symbol.name));
6705
+ const symbolWords = /* @__PURE__ */ new Set([...nameWords, ...words(symbol.file)]);
6706
+ const unmatchedTerms = queryWords.filter((word) => !symbolWords.has(word));
6707
+ const matched = queryWords.length - unmatchedTerms.length;
6708
+ const nameMatches = queryWords.filter((word) => nameWords.has(word)).length;
6709
+ let rank = matchRank(
6710
+ symbol,
6711
+ normalized,
6712
+ qualified,
6713
+ queryWords.length > 0 && unmatchedTerms.length === 0
6714
+ );
6715
+ if (rank === void 0 && !qualified && /\s/.test(normalized) && queryWords.length >= 3 && matched >= Math.max(2, Math.ceil(queryWords.length / 2)) && nameMatches > 0)
6716
+ rank = 9;
6717
+ return rank === void 0 ? void 0 : { symbol, rank, matched, nameMatches, unmatchedTerms };
6718
+ }).filter((candidate) => candidate !== void 0).sort(
6719
+ (left, right) => left.rank - right.rank || right.matched - left.matched || right.nameMatches - left.nameMatches || compareText(left.symbol.id, right.symbol.id)
6720
+ );
6721
+ const matches = ranked.slice(0, limit).map(({ symbol, rank, unmatchedTerms }) => ({
6722
+ ...toMatch(symbol),
6723
+ ...rank === 9 ? { unmatchedTerms } : {}
6724
+ }));
6725
+ return {
6726
+ query: normalized,
6727
+ total: ranked.length,
6728
+ truncated: ranked.length > matches.length,
6729
+ matches
6730
+ };
6731
+ }
6732
+ function matchRank(symbol, query, qualified, allWordsMatch) {
6733
+ if (symbol.id === query) return 0;
6734
+ if (symbol.name === query) return 1;
6735
+ const lowerQuery = query.toLowerCase();
6736
+ if (qualified) {
6737
+ if (symbol.file !== qualified.file) return void 0;
6738
+ const terminalName = qualified.member.split(".").at(-1) ?? qualified.member;
6739
+ if (symbol.name === qualified.member || symbol.name === terminalName) return 2;
6740
+ if (symbol.name.toLowerCase() === qualified.member.toLowerCase() || symbol.name.toLowerCase() === terminalName.toLowerCase()) {
6741
+ return 3;
6742
+ }
6743
+ return void 0;
6744
+ }
6745
+ const lowerName = symbol.name.toLowerCase();
6746
+ const lowerFile = symbol.file.toLowerCase();
6747
+ if (lowerName === lowerQuery) return 3;
6748
+ if (lowerName.startsWith(lowerQuery)) return 4;
6749
+ if (lowerName.includes(lowerQuery)) return 5;
6750
+ if (lowerFile.includes(lowerQuery)) return 6;
6751
+ if (allWordsMatch) return 7;
6752
+ if (/^[a-z0-9]+$/i.test(query)) {
6753
+ const compactQuery = compact(query);
6754
+ const compactName = compact(symbol.name);
6755
+ if (compactQuery.length >= 4 && compactQuery.length <= 128 && compactName.length >= 4 && compactName.length <= 128 && editDistance(compactQuery, compactName) <= (compactQuery.length >= 8 ? 2 : 1)) {
6756
+ return 8;
6757
+ }
6758
+ }
6759
+ return void 0;
6760
+ }
6761
+ function splitQualifiedSelector(value) {
6762
+ const index = value.indexOf("#");
6763
+ if (index <= 0 || index === value.length - 1) return void 0;
6764
+ return { file: value.slice(0, index), member: value.slice(index + 1) };
6765
+ }
6766
+ function words(value) {
6767
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).map((word) => {
6768
+ if (word.length > 4 && word.endsWith("ies")) return `${word.slice(0, -3)}y`;
6769
+ if (word.length > 3 && word.endsWith("s") && !/(ss|us|is)$/.test(word))
6770
+ return word.slice(0, -1);
6771
+ return word;
6772
+ });
6773
+ }
6774
+ function compact(value) {
6775
+ return value.replace(/[^a-z0-9]/gi, "").toLowerCase();
6776
+ }
6777
+ function editDistance(left, right) {
6778
+ if (Math.abs(left.length - right.length) > 2) return 3;
6779
+ const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
6780
+ for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
6781
+ let diagonal = previous[0] ?? 0;
6782
+ previous[0] = leftIndex;
6783
+ for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
6784
+ const above = previous[rightIndex] ?? 0;
6785
+ previous[rightIndex] = Math.min(
6786
+ (previous[rightIndex - 1] ?? 0) + 1,
6787
+ above + 1,
6788
+ diagonal + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1)
6789
+ );
6790
+ diagonal = above;
6791
+ }
6792
+ }
6793
+ return previous[right.length] ?? 0;
6794
+ }
6795
+ function toMatch(symbol) {
6796
+ return {
6797
+ id: symbol.id,
6798
+ name: symbol.name,
6799
+ kind: symbol.kind,
6800
+ file: symbol.file,
6801
+ range: symbol.range
6802
+ };
6803
+ }
6804
+
6688
6805
  // src/show.ts
6689
6806
  var KIND_ALIASES = {
6690
6807
  component: "react-component",
@@ -6747,7 +6864,74 @@ function showSymbol(map, selector) {
6747
6864
  }).filter((match) => match !== void 0)
6748
6865
  };
6749
6866
  }
6750
- function formatShowResult(result) {
6867
+ function formatShowResult(result, options = {}) {
6868
+ const limit = options.limit ?? 12;
6869
+ if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
6870
+ throw new RangeError("show limit must be an integer between 1 and 50");
6871
+ }
6872
+ if (options.full) return formatFullShowResult(result);
6873
+ let remaining = limit;
6874
+ const matches = result.matches.slice(0, 5);
6875
+ const blocks = matches.map(({ symbol, inbound, outbound }) => {
6876
+ const { startLine, startColumn, endLine, endColumn } = symbol.range;
6877
+ const lines = [
6878
+ `${symbol.name} [${symbol.kind}]`,
6879
+ `id: ${symbol.id}`,
6880
+ `path: ${symbol.file}:${startLine}:${startColumn}-${endLine}:${endColumn}`
6881
+ ];
6882
+ if (symbol.signature) {
6883
+ const signature = symbol.signature.replace(/\s+/g, " ");
6884
+ lines.push(
6885
+ `signature: ${signature.length > 240 ? `${signature.slice(0, 240)}\u2026 [truncated]` : signature}`
6886
+ );
6887
+ }
6888
+ const entries = [
6889
+ ...outbound.map((relation2) => ({ relation: relation2, direction: "->", target: relation2.to })),
6890
+ ...inbound.map((relation2) => ({ relation: relation2, direction: "<-", target: relation2.from }))
6891
+ ];
6892
+ const eligible = entries.filter(
6893
+ ({ relation: relation2, target }) => !["heuristic", "derived"].includes(relation2.origin.classification) && !["contains", "exports", "used-by"].includes(relation2.type) && !target.startsWith("unresolved:")
6894
+ );
6895
+ const actionable = new Set(
6896
+ eligible.filter(({ relation: relation2 }) => relation2.type !== "references").map(({ direction, target }) => `${direction}:${target}`)
6897
+ );
6898
+ const groups = /* @__PURE__ */ new Map();
6899
+ let filtered = entries.length - eligible.length;
6900
+ for (const { relation: relation2, direction, target } of eligible) {
6901
+ if (relation2.type === "references" && actionable.has(`${direction}:${target}`)) {
6902
+ filtered++;
6903
+ continue;
6904
+ }
6905
+ const key = `${direction}:${relation2.type}:${target}`;
6906
+ const group = groups.get(key);
6907
+ if (group) group.count++;
6908
+ else groups.set(key, { direction, target, type: relation2.type, count: 1 });
6909
+ }
6910
+ const ranked = [...groups.values()].sort(
6911
+ (a, b) => Number(a.target.startsWith("external:")) - Number(b.target.startsWith("external:")) || Number(a.type === "references") - Number(b.type === "references") || compareText(`${a.direction}:${a.type}:${a.target}`, `${b.direction}:${b.type}:${b.target}`)
6912
+ );
6913
+ const shown = ranked.slice(0, remaining);
6914
+ remaining -= shown.length;
6915
+ lines.push(
6916
+ `relations: ${shown.length}/${ranked.length} groups shown; ${ranked.length - shown.length} omitted; ${filtered} records filtered`
6917
+ );
6918
+ lines.push(
6919
+ ...shown.map(
6920
+ ({ direction, type, target, count }) => ` ${direction} ${type} ${direction} ${target}${count > 1 ? ` (${count} occurrences)` : ""}`
6921
+ )
6922
+ );
6923
+ return lines.join("\n");
6924
+ });
6925
+ if (matches.length < result.matches.length) {
6926
+ blocks.push(
6927
+ `Symbols: ${matches.length}/${result.matches.length} shown; ${result.matches.length - matches.length} omitted. Use an exact ID to narrow the selector.`
6928
+ );
6929
+ }
6930
+ if (matches.length)
6931
+ blocks.push("Compact view; use --full or --json for all details and evidence.");
6932
+ return blocks.join("\n\n");
6933
+ }
6934
+ function formatFullShowResult(result) {
6751
6935
  return result.matches.map(({ symbol, inbound, outbound }) => {
6752
6936
  const lines = [
6753
6937
  `${symbol.name} [${symbol.kind}]`,
@@ -6786,6 +6970,495 @@ function formatFactValue(value) {
6786
6970
  return String(value);
6787
6971
  }
6788
6972
 
6973
+ // src/read.ts
6974
+ import path16 from "path";
6975
+
6976
+ // src/read-navigation.ts
6977
+ function buildReadNavigation(map, id) {
6978
+ const symbols = new Map(map.symbols.map((symbol) => [symbol.id, symbol]));
6979
+ const candidates = [];
6980
+ const tests = /* @__PURE__ */ new Map();
6981
+ for (const relation2 of map.relations) {
6982
+ if (relation2.from !== id && relation2.to !== id) continue;
6983
+ const classification = relation2.origin.classification;
6984
+ if (classification !== "exact" && classification !== "framework-derived") continue;
6985
+ if (["contains", "exports", "used-by"].includes(relation2.type)) continue;
6986
+ const outgoing = relation2.from === id;
6987
+ const target = outgoing ? relation2.to : relation2.from;
6988
+ if (target === id) continue;
6989
+ if (!outgoing && relation2.type === "calls" && classification === "exact" && /(?:^|\/)__tests__\/|\.(?:test|spec)\.[^/]+$/.test(relation2.origin.file)) {
6990
+ const site = { file: relation2.origin.file, line: relation2.origin.line };
6991
+ tests.set(`${site.file}:${site.line}`, site);
6992
+ continue;
6993
+ }
6994
+ const symbol = symbols.get(target);
6995
+ if (!symbol) continue;
6996
+ candidates.push({
6997
+ direction: outgoing ? "outgoing" : "incoming",
6998
+ type: relation2.type,
6999
+ id: symbol.id,
7000
+ file: symbol.file,
7001
+ startLine: symbol.range.startLine,
7002
+ endLine: symbol.range.endLine,
7003
+ classification
7004
+ });
7005
+ }
7006
+ candidates.sort(
7007
+ (a, b) => Number(a.type === "references") - Number(b.type === "references") || Number(a.direction === "incoming") - Number(b.direction === "incoming") || Number(a.classification !== "exact") - Number(b.classification !== "exact") || compareText(`${a.type}:${a.id}`, `${b.type}:${b.id}`)
7008
+ );
7009
+ const links = /* @__PURE__ */ new Map();
7010
+ for (const link of candidates) {
7011
+ const key = `${link.direction}:${link.id}`;
7012
+ if (!links.has(key)) links.set(key, link);
7013
+ }
7014
+ const result = { links: [], tests: [], omittedLinks: 0, omittedTests: 0 };
7015
+ let remaining = 3600;
7016
+ for (const site of [...tests.values()].sort(
7017
+ (a, b) => compareText(a.file, b.file) || a.line - b.line
7018
+ )) {
7019
+ const size = JSON.stringify(site).length;
7020
+ if (result.tests.length < 4 && size <= Math.min(remaining, 900)) {
7021
+ result.tests.push(site);
7022
+ remaining -= size;
7023
+ } else result.omittedTests++;
7024
+ }
7025
+ for (const link of links.values()) {
7026
+ const size = JSON.stringify(link).length;
7027
+ if (result.links.length < 8 && size <= remaining) {
7028
+ result.links.push(link);
7029
+ remaining -= size;
7030
+ } else result.omittedLinks++;
7031
+ }
7032
+ return result;
7033
+ }
7034
+ function formatReadNavigation(navigation) {
7035
+ return [
7036
+ `Map links: ${navigation.links.length} shown; ${navigation.omittedLinks} omitted. Read an ID only if needed.`,
7037
+ ...navigation.links.map(
7038
+ (link) => ` ${link.direction === "outgoing" ? "->" : "<-"} ${link.type} ${link.id} [lines ${link.startLine}-${link.endLine}; ${link.classification}]`
7039
+ ),
7040
+ `Test call sites: ${navigation.tests.length} shown; ${navigation.omittedTests} omitted (path-based; not a coverage claim).`,
7041
+ ...navigation.tests.map((site) => ` ${site.file}:${site.line}`)
7042
+ ];
7043
+ }
7044
+
7045
+ // src/read.ts
7046
+ async function readSymbolSource(map, id, options) {
7047
+ const limit = options.limit ?? 80;
7048
+ const offset = options.offset ?? 0;
7049
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200)
7050
+ throw new RangeError("read limit must be an integer between 1 and 200");
7051
+ if (!Number.isSafeInteger(offset) || offset < 0)
7052
+ throw new RangeError("read offset must be a nonnegative safe integer");
7053
+ const symbol = map.symbols.find((item) => item.id === id);
7054
+ if (!symbol)
7055
+ throw new CodeMapError(
7056
+ "SYMBOL_NOT_FOUND",
7057
+ "Use an exact symbol ID returned by ai-code-map find."
7058
+ );
7059
+ const file = map.files.find((item) => item.path === symbol.file);
7060
+ const absolute = path16.resolve(options.root, symbol.file);
7061
+ if (!file)
7062
+ throw new CodeMapError("SOURCE_NOT_FOUND", "Symbol has no indexed file. Run ai-code-map scan.");
7063
+ const bytes = await readFileInsideRoot(options.root, absolute);
7064
+ if (hashText(bytes) !== file.hash)
7065
+ throw new CodeMapError(
7066
+ "STALE_SOURCE",
7067
+ `Source ${symbol.file} changed. Run ai-code-map scan before reading its mapped range.`
7068
+ );
7069
+ const lines = bytes.toString("utf8").split(/\r\n|\n|\r/);
7070
+ if (lines.at(-1) === "") lines.pop();
7071
+ const first = Math.max(1, symbol.range.startLine - 2);
7072
+ const last = Math.min(lines.length, symbol.range.endLine + 2);
7073
+ if (symbol.range.startLine < 1 || symbol.range.endLine > lines.length || first > last)
7074
+ throw new CodeMapError(
7075
+ "INVALID_SOURCE_RANGE",
7076
+ "Mapped range is outside the source. Run ai-code-map scan."
7077
+ );
7078
+ if (offset >= last - first + 1) throw new RangeError("read offset is outside the symbol window");
7079
+ const startLine = first + offset;
7080
+ const endLine = Math.min(last, startLine + limit - 1);
7081
+ const source = lines.slice(startLine - 1, endLine).join("\n");
7082
+ if (source.length > 12e3)
7083
+ throw new CodeMapError(
7084
+ "SOURCE_WINDOW_TOO_LARGE",
7085
+ "Source window exceeds 12000 characters. Reduce --limit; for a single oversized line use targeted source search."
7086
+ );
7087
+ const truncated = endLine < last;
7088
+ return {
7089
+ id: symbol.id,
7090
+ file: symbol.file,
7091
+ symbolRange: symbol.range,
7092
+ startLine,
7093
+ endLine,
7094
+ source,
7095
+ truncated,
7096
+ ...truncated ? { nextOffset: offset + endLine - startLine + 1 } : {},
7097
+ ...offset === 0 ? { navigation: buildReadNavigation(map, id) } : {}
7098
+ };
7099
+ }
7100
+
7101
+ // src/context.ts
7102
+ import path17 from "path";
7103
+ var behavior = /* @__PURE__ */ new Set(["function", "method", "react-hook", "react-component", "class"]);
7104
+ function budget(value, min, max, name) {
7105
+ if (!Number.isInteger(value) || value < min || value > max)
7106
+ throw new RangeError(`${name} must be an integer between ${min} and ${max}`);
7107
+ return value;
7108
+ }
7109
+ async function buildSourceContext(map, query, options) {
7110
+ const maxLines = budget(options.maxLines ?? 800, 1, 2e3, "context maxLines");
7111
+ const maxChars = budget(options.maxChars ?? 32e3, 256, 64e3, "context maxChars");
7112
+ const seeds = budget(options.seeds ?? 3, 1, 5, "context seeds");
7113
+ const files = /* @__PURE__ */ new Map();
7114
+ const indexedFiles = new Map(map.files.map((f) => [f.path, f]));
7115
+ const sourceFile = async (symbol) => {
7116
+ let file = files.get(symbol.file);
7117
+ if (!file) {
7118
+ const indexed = indexedFiles.get(symbol.file);
7119
+ if (!indexed) throw new CodeMapError("SOURCE_NOT_FOUND", `No indexed file for ${symbol.id}`);
7120
+ const bytes = await readFileInsideRoot(options.root, path17.resolve(options.root, symbol.file));
7121
+ if (hashText(bytes) !== indexed.hash)
7122
+ throw new CodeMapError(
7123
+ "STALE_SOURCE",
7124
+ `Source ${symbol.file} changed. Run ai-code-map scan.`
7125
+ );
7126
+ const lines = bytes.toString("utf8").split(/\r\n|\n|\r/);
7127
+ if (lines.at(-1) === "") lines.pop();
7128
+ file = { lines, included: /* @__PURE__ */ new Set() };
7129
+ files.set(symbol.file, file);
7130
+ }
7131
+ validateRange(symbol, file.lines.length);
7132
+ return file;
7133
+ };
7134
+ const found = findSymbols(map, query, { limit: 50 });
7135
+ const exact = found.matches.filter((s) => s.id === query.trim() || s.name === query.trim());
7136
+ let ranked = exact.length ? exact : [...found.matches].sort(
7137
+ (a, b) => (a.unmatchedTerms?.length ?? 0) - (b.unmatchedTerms?.length ?? 0) || Number(!behavior.has(a.kind)) - Number(!behavior.has(b.kind))
7138
+ );
7139
+ const missing = ranked[0]?.unmatchedTerms?.length ?? 0;
7140
+ if (!exact.length && missing > 0) {
7141
+ let best = 0;
7142
+ let promoted = [];
7143
+ for (const match of ranked) {
7144
+ if (!behavior.has(match.kind) || match.kind === "class" || match.unmatchedTerms?.length !== missing)
7145
+ continue;
7146
+ const file = await sourceFile(match);
7147
+ const body = file.lines.slice(match.range.startLine - 1, match.range.endLine).join("\n").toLowerCase();
7148
+ let score = 0;
7149
+ for (const term of match.unmatchedTerms) {
7150
+ let hits = 0;
7151
+ for (let at = body.indexOf(term); at >= 0 && hits < 3; at = body.indexOf(term, at + term.length))
7152
+ hits++;
7153
+ if (hits) score += 10 + hits;
7154
+ }
7155
+ if (score > best) {
7156
+ best = score;
7157
+ promoted = [];
7158
+ }
7159
+ if (score === best) promoted.push(match);
7160
+ }
7161
+ if (best > 0) ranked = promoted;
7162
+ }
7163
+ const matches = ranked.slice(0, seeds);
7164
+ const result = {
7165
+ query,
7166
+ matches,
7167
+ moreMatches: found.total - matches.length,
7168
+ selected: [],
7169
+ snippets: [],
7170
+ omitted: [],
7171
+ fileReferences: [],
7172
+ tests: [],
7173
+ omittedNavigation: 0,
7174
+ usedLines: 0,
7175
+ usedChars: 0,
7176
+ limits: { maxLines, maxChars, seeds }
7177
+ };
7178
+ const byId = new Map(map.symbols.map((s) => [s.id, s]));
7179
+ const selected = new Map(matches.map((s) => [s.id, "match"]));
7180
+ const outgoingNeighbors = /* @__PURE__ */ new Set();
7181
+ const tests = /* @__PURE__ */ new Map();
7182
+ for (const match of matches) {
7183
+ const nav = buildReadNavigation(map, match.id);
7184
+ result.omittedNavigation += nav.omittedLinks + nav.omittedTests;
7185
+ for (const link of nav.links) {
7186
+ if (link.direction === "outgoing") outgoingNeighbors.add(link.id);
7187
+ if (selected.has(link.id)) continue;
7188
+ if (link.type === "references" && byId.get(link.id)?.kind === "class") {
7189
+ result.omittedNavigation++;
7190
+ continue;
7191
+ }
7192
+ selected.set(link.id, "neighbor");
7193
+ }
7194
+ for (const site of nav.tests) tests.set(`${site.file}:${site.line}`, site);
7195
+ }
7196
+ const testSites = [...tests.values()].sort(
7197
+ (a, b) => compareText(a.file, b.file) || a.line - b.line
7198
+ );
7199
+ result.tests = testSites.slice(0, 8);
7200
+ result.omittedNavigation += Math.max(0, testSites.length - result.tests.length);
7201
+ const roots = new Set(matches.map((match) => match.id));
7202
+ const filesById = new Map(map.files.map((file) => [file.id, file]));
7203
+ const references = /* @__PURE__ */ new Map();
7204
+ for (const relation2 of map.relations) {
7205
+ if (!roots.has(relation2.to) || !["calls", "references"].includes(relation2.type)) continue;
7206
+ const origin = relation2.origin;
7207
+ if (!["exact", "framework-derived"].includes(origin.classification)) continue;
7208
+ const file = filesById.get(relation2.from);
7209
+ if (!file || file.path !== origin.file || /(?:^|\/)__tests__\/|\.(?:test|spec)\.[^/]+$/.test(file.path))
7210
+ continue;
7211
+ references.set(`${file.path}:${origin.line}`, { file: file.path, line: origin.line });
7212
+ }
7213
+ const fileSites = [...references.values()].sort(
7214
+ (a, b) => compareText(a.file, b.file) || a.line - b.line
7215
+ );
7216
+ result.fileReferences = fileSites.slice(0, 4);
7217
+ result.omittedNavigation += Math.max(0, fileSites.length - result.fileReferences.length);
7218
+ const requested = [];
7219
+ const includeWindow = (id, name, file, first, last) => {
7220
+ requested.push({ id, file: name, startLine: first, endLine: last });
7221
+ for (let line = first; line <= last; line++) {
7222
+ if (file.included.has(line)) continue;
7223
+ const chars = (file.lines[line - 1]?.length ?? 0) + 1;
7224
+ if (result.usedLines >= maxLines || result.usedChars + chars > maxChars) break;
7225
+ file.included.add(line);
7226
+ result.usedLines++;
7227
+ result.usedChars += chars;
7228
+ }
7229
+ };
7230
+ for (const [id] of selected) {
7231
+ const symbol = byId.get(id);
7232
+ if (!symbol) throw new CodeMapError("SYMBOL_NOT_FOUND", `Missing context symbol: ${id}`);
7233
+ const file = await sourceFile(symbol);
7234
+ includeWindow(
7235
+ id,
7236
+ symbol.file,
7237
+ file,
7238
+ Math.max(1, symbol.range.startLine - 2),
7239
+ Math.min(file.lines.length, symbol.range.endLine + 2)
7240
+ );
7241
+ }
7242
+ for (const site of result.fileReferences) {
7243
+ const indexed = indexedFiles.get(site.file);
7244
+ if (!indexed) throw new CodeMapError("SOURCE_NOT_FOUND", `No indexed file for ${site.file}`);
7245
+ const file = await sourceFile({
7246
+ id: indexed.id,
7247
+ file: site.file,
7248
+ range: { startLine: site.line, endLine: site.line, startColumn: 1, endColumn: 1 }
7249
+ });
7250
+ const small2 = file.lines.length <= 80;
7251
+ includeWindow(
7252
+ indexed.id,
7253
+ site.file,
7254
+ file,
7255
+ small2 ? 1 : Math.max(1, site.line - 8),
7256
+ small2 ? file.lines.length : Math.min(file.lines.length, site.line + 8)
7257
+ );
7258
+ }
7259
+ const dependencies = /* @__PURE__ */ new Map();
7260
+ for (const relation2 of map.relations) {
7261
+ if (!outgoingNeighbors.has(relation2.from) || selected.get(relation2.from) !== "neighbor" || selected.has(relation2.to) || !["calls", "references"].includes(relation2.type) || !["exact", "framework-derived"].includes(relation2.origin.classification))
7262
+ continue;
7263
+ const symbol = byId.get(relation2.to);
7264
+ if (!symbol || symbol.kind === "class" || !behavior.has(symbol.kind) && symbol.kind !== "variable")
7265
+ continue;
7266
+ const call = relation2.type === "calls";
7267
+ if (!dependencies.has(symbol.id) || call) dependencies.set(symbol.id, { symbol, call });
7268
+ }
7269
+ const small = [...dependencies.values()].filter(
7270
+ ({ symbol }) => symbol.range.endLine - symbol.range.startLine + 1 <= 80
7271
+ );
7272
+ small.sort((a, b) => Number(b.call) - Number(a.call) || compareText(a.symbol.id, b.symbol.id));
7273
+ result.omittedNavigation += dependencies.size - Math.min(small.length, 4);
7274
+ for (const { symbol } of small.slice(0, 4)) {
7275
+ const file = await sourceFile(symbol);
7276
+ selected.set(symbol.id, "neighbor");
7277
+ includeWindow(
7278
+ symbol.id,
7279
+ symbol.file,
7280
+ file,
7281
+ Math.max(1, symbol.range.startLine - 2),
7282
+ Math.min(file.lines.length, symbol.range.endLine + 2)
7283
+ );
7284
+ }
7285
+ result.selected = [...selected].map(([id, role]) => ({ id, role }));
7286
+ for (const request of requested) {
7287
+ const file = files.get(request.file);
7288
+ if (!file) continue;
7289
+ const last = request.endLine;
7290
+ let missingStart;
7291
+ for (let line = request.startLine; line <= last + 1; line++) {
7292
+ const missing2 = line <= last && !file.included.has(line);
7293
+ if (missing2 && missingStart === void 0) missingStart = line;
7294
+ if (!missing2 && missingStart !== void 0) {
7295
+ result.omitted.push({ ...request, startLine: missingStart, endLine: line - 1 });
7296
+ missingStart = void 0;
7297
+ }
7298
+ }
7299
+ }
7300
+ for (const [name, file] of files) {
7301
+ const lines = [...file.included].sort((a, b) => a - b);
7302
+ let start, end = 0;
7303
+ const append = () => {
7304
+ if (start === void 0) return;
7305
+ result.snippets.push({
7306
+ file: name,
7307
+ startLine: start,
7308
+ endLine: end,
7309
+ source: file.lines.slice(start - 1, end).join("\n")
7310
+ });
7311
+ };
7312
+ for (const line of lines) {
7313
+ if (start === void 0 || line !== end + 1) {
7314
+ append();
7315
+ start = line;
7316
+ }
7317
+ end = line;
7318
+ }
7319
+ append();
7320
+ }
7321
+ return result;
7322
+ }
7323
+ function validateRange(symbol, length) {
7324
+ const { startLine, endLine } = symbol.range;
7325
+ if (!Number.isInteger(startLine) || !Number.isInteger(endLine) || startLine < 1 || endLine < startLine || endLine > length)
7326
+ throw new CodeMapError(
7327
+ "INVALID_SOURCE_RANGE",
7328
+ `Mapped range is outside ${symbol.file}. Run ai-code-map scan.`
7329
+ );
7330
+ }
7331
+ function formatSourceContext(result) {
7332
+ if (!result.matches.length)
7333
+ return "No indexed matches. Include action and subject words, or use find/targeted rg. No source was read.";
7334
+ const lines = [
7335
+ `Context: ${result.selected.length} selected symbols, ${result.usedLines}/${result.limits.maxLines} source lines; ${result.usedChars}/${result.limits.maxChars} source characters.`,
7336
+ `Matches: ${result.matches.map((m) => `${m.name}${m.unmatchedTerms?.length ? ` (unmatched: ${m.unmatchedTerms.join(", ")})` : ""}`).join(", ")}.`,
7337
+ "Scope: matches, direct exact/framework-derived neighbors, file-level reference sites and up to four small outgoing dependencies; reference-only classes are not expanded. Source determines behavior."
7338
+ ];
7339
+ if (result.fileReferences.length)
7340
+ lines.push(
7341
+ `File reference sites: ${result.fileReferences.map((site) => `${site.file}:${site.line}`).join(", ")}.`
7342
+ );
7343
+ for (const snippet of result.snippets) {
7344
+ lines.push(`
7345
+ ${snippet.file}:${snippet.startLine}-${snippet.endLine}`);
7346
+ lines.push(...snippet.source.split("\n").map((line, i) => `${snippet.startLine + i}: ${line}`));
7347
+ }
7348
+ if (result.tests.length)
7349
+ lines.push(
7350
+ "\nTest call sites (navigation hints, not coverage):",
7351
+ ...result.tests.map((s) => `${s.file}:${s.line}`)
7352
+ );
7353
+ if (result.omitted.length)
7354
+ lines.push(
7355
+ "\nBudget omitted source; read only a needed missing range:",
7356
+ ...result.omitted.slice(0, 12).map((o) => `${o.file}:${o.startLine}-${o.endLine} (${o.id})`),
7357
+ ...result.omitted.length > 12 ? [`${result.omitted.length - 12} further omitted ranges; use --json for all.`] : []
7358
+ );
7359
+ lines.push(
7360
+ `
7361
+ Other search matches: ${result.moreMatches}; navigation entries omitted: ${result.omittedNavigation}. Follow up only for a missing fact; reuse source already returned.`
7362
+ );
7363
+ return lines.join("\n");
7364
+ }
7365
+
7366
+ // src/project-map.ts
7367
+ function buildProjectMap(map) {
7368
+ const refs = /* @__PURE__ */ new Map();
7369
+ const ref = (id) => {
7370
+ const value = refs.get(id);
7371
+ if (!value) throw new Error(`Unknown map endpoint: ${id}`);
7372
+ return value;
7373
+ };
7374
+ const files = [...map.files].sort((a, b) => compareText(a.path, b.path)).map((file, index) => {
7375
+ const key = `f${index + 1}`;
7376
+ refs.set(file.id, key);
7377
+ return { ref: key, id: file.id, path: file.path };
7378
+ });
7379
+ const symbols = [...map.symbols].sort((a, b) => compareText(a.id, b.id)).map((symbol, index) => {
7380
+ const key = `s${index + 1}`;
7381
+ refs.set(symbol.id, key);
7382
+ return {
7383
+ ref: key,
7384
+ id: symbol.id,
7385
+ name: symbol.name,
7386
+ kind: symbol.kind,
7387
+ file: ref(`file:${symbol.file}`),
7388
+ range: { ...symbol.range }
7389
+ };
7390
+ });
7391
+ const otherIds = /* @__PURE__ */ new Set();
7392
+ for (const relation2 of map.relations) {
7393
+ if (!refs.has(relation2.from)) otherIds.add(relation2.from);
7394
+ if (!refs.has(relation2.to)) otherIds.add(relation2.to);
7395
+ }
7396
+ const endpoints = [...otherIds].sort(compareText).map((id, index) => {
7397
+ const key = `e${index + 1}`;
7398
+ refs.set(id, key);
7399
+ return { ref: key, id };
7400
+ });
7401
+ const groups = /* @__PURE__ */ new Map();
7402
+ for (const relation2 of map.relations) {
7403
+ const from = ref(relation2.from);
7404
+ const to = ref(relation2.to);
7405
+ const classification = relation2.origin.classification;
7406
+ const key = JSON.stringify([from, relation2.type, classification]);
7407
+ let group = groups.get(key);
7408
+ if (!group) {
7409
+ group = { from, type: relation2.type, classification, targets: /* @__PURE__ */ new Map() };
7410
+ groups.set(key, group);
7411
+ }
7412
+ group.targets.set(to, (group.targets.get(to) ?? 0) + 1);
7413
+ }
7414
+ const relations = [...groups.entries()].sort(([a], [b]) => compareText(a, b)).map(([, group]) => ({
7415
+ ...group,
7416
+ targets: [...group.targets.entries()].sort(([a], [b]) => compareText(a, b)).map(([ref2, count]) => ({ ref: ref2, count }))
7417
+ }));
7418
+ return {
7419
+ project: map.project.name,
7420
+ counts: {
7421
+ files: files.length,
7422
+ symbols: symbols.length,
7423
+ endpoints: endpoints.length,
7424
+ relationGroups: relations.length,
7425
+ relationRecords: map.relations.length
7426
+ },
7427
+ files,
7428
+ symbols,
7429
+ endpoints,
7430
+ relations
7431
+ };
7432
+ }
7433
+ function formatProjectMap(map) {
7434
+ const { counts } = map;
7435
+ return [
7436
+ `Complete structural map: ${map.project}`,
7437
+ `${counts.files} files; ${counts.symbols} symbols; ${counts.endpoints} other endpoints; ${counts.relationRecords} relation records in ${counts.relationGroups} adjacency groups.`,
7438
+ "All indexed nodes and relations included. No source, signatures, facts or per-occurrence evidence.",
7439
+ "f/s/e refs are local to this output. Read source using a symbol's stable ID, not its short ref.",
7440
+ "Relations: FROM TYPE [evidence classification] -> TARGET[*occurrences]. Heuristic/derived links are not exact calls.",
7441
+ "",
7442
+ "Files (ref path)",
7443
+ ...map.files.map((file) => `${file.ref} ${file.path}`),
7444
+ "",
7445
+ "Symbols (ref file:start-end kind stable-ID)",
7446
+ ...map.symbols.map(
7447
+ (symbol) => `${symbol.ref} ${symbol.file}:${symbol.range.startLine}-${symbol.range.endLine} ${symbol.kind} ${symbol.id}`
7448
+ ),
7449
+ "",
7450
+ "Other endpoints (ref ID)",
7451
+ ...map.endpoints.map((endpoint) => `${endpoint.ref} ${endpoint.id}`),
7452
+ "",
7453
+ "Relations",
7454
+ ...map.relations.map(
7455
+ (group) => `${group.from} ${group.type} [${group.classification}] -> ${group.targets.map((target) => `${target.ref}${target.count > 1 ? `*${target.count}` : ""}`).join(",")}`
7456
+ ),
7457
+ "",
7458
+ "End of map. If this marker is missing, the calling tool may have truncated the output."
7459
+ ].join("\n");
7460
+ }
7461
+
6789
7462
  // src/cli.ts
6790
7463
  async function runCli(argv = process.argv) {
6791
7464
  const program = createProgram();
@@ -6841,20 +7514,112 @@ ${result.wrote ? `Updated ${result.outputDir}.` : "Map was already current; no a
6841
7514
  if (!result.current) exitCode = 2;
6842
7515
  });
6843
7516
  addCommonOptions(checkCommand);
6844
- const showCommand = program.command("show").description("Show one indexed symbol by stable ID, kind:name, path, or exact name").argument("<symbol>", "symbol selector").addOption(new Option("--json", "print machine-readable output")).action(async (selector, _options, command) => {
7517
+ const mapCommand = program.command("map").description("Check freshness and print the complete structural map in one call").option("--json", "print all nodes and grouped relations as JSON").action(async (_options, command) => {
6845
7518
  const common = collectOptions(command);
6846
- const map = await loadCodeMap(loadOptions(common));
6847
- const result = showSymbol(map, selector);
6848
- if (result.matches.length === 0) {
6849
- throw new CodeMapError(
6850
- "SYMBOL_NOT_FOUND",
6851
- `No indexed symbol matches ${selector}. Run ai-code-map scan, then use an exact name or stable ID.`
6852
- );
7519
+ const inspection = await inspectFreshness(loadOptions(common));
7520
+ if (!inspection.result.current || !inspection.map) {
7521
+ if (common.json) printJson(inspection.result);
7522
+ else
7523
+ process.stdout.write(
7524
+ `Map unavailable: ${inspection.result.reasons.join(" ")} Run ai-code-map scan.
7525
+ `
7526
+ );
7527
+ exitCode = 2;
7528
+ return;
6853
7529
  }
6854
- if (common.json) printJson(result);
6855
- else process.stdout.write(`${formatShowResult(result)}
7530
+ const view = buildProjectMap(inspection.map);
7531
+ if (common.json) printJson(view);
7532
+ else process.stdout.write(`${formatProjectMap(view)}
6856
7533
  `);
6857
7534
  });
7535
+ addCommonOptions(mapCommand);
7536
+ const findCommand = program.command("find").description("Find indexed symbols by name, path, words, or a likely typo").argument("<query>", "search query (quote multiple words)").option("--limit <number>", "maximum matches, 1..50", parseLimit, 10).option("--json", "print machine-readable output").action(
7537
+ async (query, options, command) => {
7538
+ const map = await loadCodeMap(loadOptions(collectOptions(command)));
7539
+ const result = findSymbols(map, query, { limit: options.limit });
7540
+ if (options.json) printJson(result);
7541
+ else process.stdout.write(`${formatSearchResult(result)}
7542
+ `);
7543
+ }
7544
+ );
7545
+ addCommonOptions(findCommand);
7546
+ const contextCommand = program.command("context").description(
7547
+ "Collect bounded current source for lexical matches and their direct graph neighbors"
7548
+ ).argument("<query>", "quoted search words or an exact symbol ID").option("--max-lines <number>", "shared source-line budget, 1..2000", Number, 800).option("--max-chars <number>", "shared source-character budget, 256..64000", Number, 32e3).option("--seeds <number>", "maximum matches to expand, 1..5", Number, 3).option("--json", "print structured context, budgets and omitted ranges").action(
7549
+ async (query, options, command) => {
7550
+ const common = collectOptions(command);
7551
+ const inspection = await inspectFreshness(loadOptions(common));
7552
+ if (!inspection.result.current || !inspection.map) {
7553
+ if (common.json) printJson(inspection.result);
7554
+ else
7555
+ process.stdout.write(
7556
+ `Map unavailable: ${inspection.result.reasons.join(" ")} Run ai-code-map scan.
7557
+ `
7558
+ );
7559
+ exitCode = 2;
7560
+ return;
7561
+ }
7562
+ const result = await buildSourceContext(inspection.map, query, {
7563
+ root: inspection.result.root,
7564
+ maxLines: options.maxLines,
7565
+ maxChars: options.maxChars,
7566
+ seeds: options.seeds
7567
+ });
7568
+ if (common.json) printJson(result);
7569
+ else process.stdout.write(`${formatSourceContext(result)}
7570
+ `);
7571
+ }
7572
+ );
7573
+ addCommonOptions(contextCommand);
7574
+ const readCommand = program.command("read").description("Read a bounded source range using an exact symbol ID from the map").argument("<id>", "exact stable symbol ID from find").option("--limit <number>", "maximum source lines, 1..200", Number, 80).option("--offset <number>", "line offset within the symbol window", Number, 0).option("--json", "print source and continuation metadata as JSON").action(
7575
+ async (id, options, command) => {
7576
+ const common = collectOptions(command);
7577
+ const resolved = await resolveConfiguration(loadOptions(common));
7578
+ const map = await loadCodeMapFrom(resolved.outputDir);
7579
+ const result = await readSymbolSource(map, id, {
7580
+ root: resolved.root,
7581
+ limit: options.limit,
7582
+ offset: options.offset
7583
+ });
7584
+ if (options.json) printJson(result);
7585
+ else {
7586
+ const location = ["root", "config", "output"];
7587
+ const suffix = location.map((key) => {
7588
+ const value = common[key];
7589
+ return value ? ` --${key} ${shellQuote(value)}` : "";
7590
+ }).join("");
7591
+ const numbered = result.source.split("\n").map((line, index) => `${result.startLine + index}: ${line}`);
7592
+ process.stdout.write(
7593
+ [
7594
+ `${result.id}
7595
+ ${result.file}:${result.startLine}-${result.endLine}`,
7596
+ ...numbered,
7597
+ result.truncated ? `More source remains. Continue: ai-code-map read ${shellQuote(result.id)} --offset ${result.nextOffset} --limit ${options.limit}${suffix}` : "End of symbol window.",
7598
+ ...result.navigation ? formatReadNavigation(result.navigation) : []
7599
+ ].join("\n") + "\n"
7600
+ );
7601
+ }
7602
+ }
7603
+ );
7604
+ addCommonOptions(readCommand);
7605
+ const showCommand = program.command("show").description("Show one indexed symbol by stable ID, kind:name, path, or exact name").argument("<symbol>", "symbol selector").option("--full", "print verbose symbol details and relations").option("--limit <number>", "maximum relation groups in compact text, 1..50", parseLimit, 12).addOption(new Option("--json", "print machine-readable output")).action(
7606
+ async (selector, options, command) => {
7607
+ const common = collectOptions(command);
7608
+ const map = await loadCodeMap(loadOptions(common));
7609
+ const result = showSymbol(map, selector);
7610
+ if (result.matches.length === 0) {
7611
+ const suggestions = selector.trim() ? findSymbols(map, selector, { limit: 5 }) : void 0;
7612
+ throw new CodeMapError(
7613
+ "SYMBOL_NOT_FOUND",
7614
+ `No indexed symbol matches ${selector}. ${suggestions?.matches.length ? `Did you mean:
7615
+ ${formatSearchResult(suggestions)}` : 'Use ai-code-map find "words" to discover a selector, or rg if the symbol is not indexed.'}`
7616
+ );
7617
+ }
7618
+ if (common.json) printJson(result);
7619
+ else process.stdout.write(`${formatShowResult(result, { ...options })}
7620
+ `);
7621
+ }
7622
+ );
6858
7623
  addCommonOptions(showCommand);
6859
7624
  try {
6860
7625
  await program.parseAsync(argv);
@@ -6891,10 +7656,30 @@ function loadOptions(options) {
6891
7656
  ...options.output ? { output: options.output } : {}
6892
7657
  };
6893
7658
  }
7659
+ function shellQuote(value) {
7660
+ return "'" + value.replaceAll("'", "'\\''") + "'";
7661
+ }
6894
7662
  function printJson(value) {
6895
7663
  process.stdout.write(`${JSON.stringify(value, null, 2)}
6896
7664
  `);
6897
7665
  }
7666
+ function parseLimit(value) {
7667
+ const limit = Number(value);
7668
+ if (!/^\d+$/.test(value) || !Number.isInteger(limit) || limit < 1 || limit > 50) {
7669
+ throw new RangeError("limit must be an integer between 1 and 50");
7670
+ }
7671
+ return limit;
7672
+ }
7673
+ function formatSearchResult(result) {
7674
+ return [
7675
+ `Symbols: ${result.matches.length}/${result.total} shown${result.truncated ? "; refine the query for omitted matches" : ""}`,
7676
+ ...result.matches.map(
7677
+ ({ id, kind, file, range, unmatchedTerms }) => `${id} [${kind}]${unmatchedTerms?.length ? ` (partial; unmatched: ${unmatchedTerms.join(", ")})` : ""}
7678
+ ${file}:${range.startLine}:${range.startColumn}-${range.endLine}:${range.endColumn}`
7679
+ ),
7680
+ result.matches.length ? "Use ai-code-map read '<id>' for bounded source; show '<id>' for relations." : "No matches. Try other words or use rg; only indexed symbols are searchable."
7681
+ ].join("\n");
7682
+ }
6898
7683
  var invokedPath = process.argv[1] ? pathToFileURL2(process.argv[1]).href : void 0;
6899
7684
  if (invokedPath === import.meta.url) {
6900
7685
  process.exitCode = await runCli();