@weareikko/code-review 0.9.5 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -541,6 +541,10 @@ function summarizeDiff(diff) {
541
541
  }
542
542
  //#endregion
543
543
  //#region src/skills.ts
544
+ /** Normalize a filesystem-relative path to the POSIX form used in URLs. */
545
+ function toPosixPath(path) {
546
+ return sep === "/" ? path : path.split(sep).join("/");
547
+ }
544
548
  var SKILL_DIRS = [".agents/skills", ".claude/skills"];
545
549
  var RESOURCE_DIRS = ["references"];
546
550
  function parseFrontmatter(content) {
@@ -563,7 +567,7 @@ function parseFrontmatter(content) {
563
567
  description: trimmedDescription
564
568
  };
565
569
  }
566
- async function loadSkillFromDir(dirPath, source) {
570
+ async function loadSkillFromDir(dirPath, origin) {
567
571
  const skillMdPath = join(dirPath, "SKILL.md");
568
572
  let content;
569
573
  try {
@@ -580,14 +584,17 @@ async function loadSkillFromDir(dirPath, source) {
580
584
  filePath: skillMdPath,
581
585
  rootDir: dirPath,
582
586
  resourceDirs,
583
- source
587
+ origin
584
588
  };
585
589
  }
586
590
  function resolveBuiltinSkillsDir() {
587
591
  return join(dirname(fileURLToPath(import.meta.url)), "..", "skills");
588
592
  }
589
593
  async function loadBuiltinSkill(name) {
590
- return loadSkillFromDir(join(resolveBuiltinSkillsDir(), name), "builtin");
594
+ return loadSkillFromDir(join(resolveBuiltinSkillsDir(), name), {
595
+ kind: "builtin",
596
+ name
597
+ });
591
598
  }
592
599
  async function loadAutoDiscoveredSkills(cwd, gitRoot, warn) {
593
600
  const dirs = [];
@@ -610,7 +617,10 @@ async function loadAutoDiscoveredSkills(cwd, gitRoot, warn) {
610
617
  }
611
618
  for (const entry of entries) {
612
619
  const entryPath = join(skillsPath, entry);
613
- const skill = await loadSkillFromDir(entryPath, "project");
620
+ const skill = await loadSkillFromDir(entryPath, {
621
+ kind: "project",
622
+ path: toPosixPath(relative(gitRoot, entryPath))
623
+ });
614
624
  if (skill) found.set(skill.name, skill);
615
625
  else if (warn && existsSync(join(entryPath, "SKILL.md"))) warn(`Skill at ${entryPath} has a SKILL.md but is missing required frontmatter fields (name, description) — skill not loaded.`);
616
626
  }
@@ -898,13 +908,20 @@ async function loadNamedSkill(spec, cwd, options = {}) {
898
908
  const pkgRef = parsed.subpath ? `${parsed.packageName} (subpath "${parsed.subpath}")` : parsed.packageName;
899
909
  throw new ConfigError(`Cannot load skill: "${spec}"`, { hint: `Package ${pkgRef} was not found in node_modules. Run \`npm install ${parsed.packageName}\` in the project.` });
900
910
  }
901
- const skill = await loadSkillFromDir(dir, "npm");
911
+ const skill = await loadSkillFromDir(dir, {
912
+ kind: "npm",
913
+ packageName: parsed.packageName,
914
+ subpath: parsed.subpath
915
+ });
902
916
  if (!skill) throw new ConfigError(`Cannot load skill: "${spec}"`, { hint: `The package at ${dir} does not contain a valid SKILL.md.` });
903
917
  return skill;
904
918
  }
905
919
  if (parsed.protocol === "file") {
906
920
  const resolvedPath = parsed.path.startsWith("/") ? parsed.path : join(cwd, parsed.path);
907
- const skill = await loadSkillFromDir(resolvedPath, "file");
921
+ const skill = await loadSkillFromDir(resolvedPath, {
922
+ kind: "file",
923
+ path: resolvedPath
924
+ });
908
925
  if (!skill) throw new ConfigError(`Cannot load skill: "${spec}"`, { hint: `No valid SKILL.md was found at "${resolvedPath}". Check that the path points to a skill directory.` });
909
926
  return skill;
910
927
  }
@@ -922,7 +939,12 @@ async function loadNamedSkill(spec, cwd, options = {}) {
922
939
  hint: `Failed to clone "${redactUrl(parsed.url)}"${atRef}. Check the URL, the ref, and your git credentials. For GitLab, prefer the SSH form: git+ssh://git@host/group/project.git`
923
940
  });
924
941
  }
925
- const skill = await loadSkillFromDir(parsed.subpath ? join(repoDir, parsed.subpath) : repoDir, "git");
942
+ const skill = await loadSkillFromDir(parsed.subpath ? join(repoDir, parsed.subpath) : repoDir, {
943
+ kind: "git",
944
+ url: parsed.url,
945
+ ref: parsed.ref,
946
+ path: parsed.subpath
947
+ });
926
948
  if (!skill) throw new ConfigError(`Cannot load skill: "${spec}"`, { hint: parsed.subpath ? `The cloned repository has no valid SKILL.md at subpath "${parsed.subpath}".` : "The cloned repository has no valid SKILL.md at its root. If the skill lives in a subdirectory, point at it with \"#<ref>/<subpath>\"." });
927
949
  return skill;
928
950
  }
@@ -1061,7 +1083,7 @@ async function resolveAnthropicSkill(repoDir, mp, spec) {
1061
1083
  const pluginDir = resolvePluginDir(repoDir, manifest, entry.source, mp, spec);
1062
1084
  const realPluginDir = await resolveInside(realRoot, pluginDir, ref);
1063
1085
  const bases = computeSkillBases(repoDir, pluginDir, entry, entry.strict !== false && realPluginDir ? await readPluginManifest(realRoot, realPluginDir, ref) : null, ref);
1064
- const skill = await findSkillInBases(realRoot, bases, pluginDir, spec.skill, ref);
1086
+ const skill = await findSkillInBases(realRoot, bases, pluginDir, spec, ref, mp);
1065
1087
  if (!skill) throw new ConfigError(`Cannot load skill: "${ref}"`, { hint: `No skill "${spec.skill}" found in plugin "${spec.plugin}". Looked under the default skills/ directory${bases.length > 1 ? " and the plugin's custom \"skills\" paths" : ""}. Check the skill name against the marketplace.` });
1066
1088
  return skill;
1067
1089
  }
@@ -1121,14 +1143,15 @@ function resolveUnderPlugin(repoDir, pluginDir, rel, ref) {
1121
1143
  * symlinks and reject any directory that escapes the cloned marketplace before a
1122
1144
  * file is read.
1123
1145
  */
1124
- async function findSkillInBases(realRoot, bases, pluginDir, skillName, ref) {
1146
+ async function findSkillInBases(realRoot, bases, pluginDir, spec, ref, mp) {
1147
+ const skillName = spec.skill;
1125
1148
  for (const base of bases) {
1126
- const container = await loadSkillIfInside(realRoot, join(base, skillName), ref);
1149
+ const container = await loadSkillIfInside(realRoot, join(base, skillName), ref, mp, spec);
1127
1150
  if (container) return container;
1128
- const single = await loadSkillIfInside(realRoot, base, ref);
1151
+ const single = await loadSkillIfInside(realRoot, base, ref, mp, spec);
1129
1152
  if (single && single.name === skillName) return single;
1130
1153
  }
1131
- const root = await loadSkillIfInside(realRoot, pluginDir, ref);
1154
+ const root = await loadSkillIfInside(realRoot, pluginDir, ref, mp, spec);
1132
1155
  return root && root.name === skillName ? root : null;
1133
1156
  }
1134
1157
  /**
@@ -1173,11 +1196,18 @@ async function readTextInside(realRoot, filePath, ref) {
1173
1196
  * resolve within `realRoot`. Guarding the file (not just the directory) closes
1174
1197
  * the case where a legitimate in-repo dir holds a `SKILL.md` symlinked outside.
1175
1198
  */
1176
- async function loadSkillIfInside(realRoot, dir, ref) {
1199
+ async function loadSkillIfInside(realRoot, dir, ref, mp, spec) {
1177
1200
  const real = await resolveInside(realRoot, dir, ref);
1178
1201
  if (!real) return null;
1179
1202
  if (await readTextInside(realRoot, join(real, "SKILL.md"), ref) === null) return null;
1180
- return loadSkillFromDir(real, "marketplace");
1203
+ return loadSkillFromDir(real, {
1204
+ kind: "marketplace",
1205
+ marketplace: mp.name,
1206
+ plugin: spec.plugin,
1207
+ url: mp.url,
1208
+ ref: mp.ref,
1209
+ path: toPosixPath(relative(realRoot, real))
1210
+ });
1181
1211
  }
1182
1212
  /**
1183
1213
  * Resolve a plugin entry's `source` to an absolute directory inside the cloned
@@ -1304,13 +1334,24 @@ function extractExistingFingerprints(discussions) {
1304
1334
  for (const discussion of discussions) for (const note of discussion.notes ?? []) for (const match of String(note.body ?? "").matchAll(FINGERPRINT_MARKER_RE$2)) set.add(match[1]);
1305
1335
  return set;
1306
1336
  }
1337
+ //#endregion
1338
+ //#region src/product.ts
1339
+ /**
1340
+ * The published package name, shown in review footers. Independent of the review
1341
+ * platform: the same tool posts to GitLab and GitHub, so the footer identifies
1342
+ * the tool, not the backend. Kept as a single source of truth so the inline and
1343
+ * summary footers can never drift apart.
1344
+ */
1345
+ var PRODUCT_NAME = "@weareikko/code-review";
1346
+ /** Canonical project URL used in the footer's markdown link. */
1347
+ var PRODUCT_URL = "https://github.com/weareikko/code-review";
1307
1348
  /**
1308
1349
  * The `[name](url)` markdown link used verbatim in both the inline comment footer
1309
1350
  * ({@link buildCommentBody}) and the reviewed-commit summary footer
1310
1351
  * ({@link buildReviewedCommitFooter}). Changing this changes the reviewed-commit
1311
1352
  * footer format, which is guarded by a migration test.
1312
1353
  */
1313
- var PRODUCT_LINK = `[@weareikko/code-review](https://github.com/weareikko/code-review)`;
1354
+ var PRODUCT_LINK = `[${PRODUCT_NAME}](${PRODUCT_URL})`;
1314
1355
  //#endregion
1315
1356
  //#region src/posting.ts
1316
1357
  var SUMMARY_MARKER = "<!-- code-review:summary -->";
@@ -1390,7 +1431,7 @@ function buildSummaryBody(summary, costFooter, options = {}) {
1390
1431
  return `${withFooter}\n\n${buildSummaryHistoryBlock(historyEntries)}`;
1391
1432
  }
1392
1433
  function buildReviewedCommitFooter(commitSha) {
1393
- return `Reviewed by ${PRODUCT_LINK} v0.9.5 for commit ${commitSha}.`;
1434
+ return `Reviewed by ${PRODUCT_LINK} v0.9.6 for commit ${commitSha}.`;
1394
1435
  }
1395
1436
  function extractReviewedCommitSha(body) {
1396
1437
  return REVIEWED_COMMIT_FOOTER_PATTERN.exec(body)?.[1] ?? null;
@@ -2035,6 +2076,16 @@ function detectPlatform(args, env, readEventFile = readEventFileSync) {
2035
2076
  if (hasGitLab && !hasGitHub) return "gitlab";
2036
2077
  throw new ConfigError(hasGitHub && hasGitLab ? "Ambiguous review platform: both GitHub and GitLab identifiers are present." : "Could not detect the review platform from the environment.", { hint: `Set --platform (or CODE_REVIEW_PLATFORM) to one of: ${PLATFORMS.join(", ")}.` });
2037
2078
  }
2079
+ /**
2080
+ * Web URL of the repository under review, as the CI environment reports it:
2081
+ * GitLab exposes it directly, GitHub composes it from the server and the
2082
+ * `owner/repo` slug. Returns `undefined` outside CI, where no such URL exists.
2083
+ * Shared by the config (skill source links) and the OTel attribute builder.
2084
+ */
2085
+ function resolveProjectWebUrl(env = process.env) {
2086
+ if (env.CI_PROJECT_URL) return env.CI_PROJECT_URL.replace(/\/$/, "");
2087
+ if (env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY) return `${env.GITHUB_SERVER_URL.replace(/\/$/, "")}/${env.GITHUB_REPOSITORY}`;
2088
+ }
2038
2089
  function resolveConfig(argv = process.argv.slice(2), env = process.env) {
2039
2090
  const args = parseArgs(argv);
2040
2091
  const platform = detectPlatform(args, env);
@@ -2042,6 +2093,7 @@ function resolveConfig(argv = process.argv.slice(2), env = process.env) {
2042
2093
  const token = resolveGitLabToken(args, env);
2043
2094
  const githubApiUrl = String(args.githubApiUrl ?? env.GITHUB_API_URL ?? "https://api.github.com").replace(/\/$/, "");
2044
2095
  const githubServerUrl = String(args.githubServerUrl ?? env.GITHUB_SERVER_URL ?? "https://github.com").replace(/\/$/, "");
2096
+ const githubRepository = String(args.githubRepository ?? env.GITHUB_REPOSITORY ?? "");
2045
2097
  const model = String(args.model ?? env.CODE_REVIEW_MODEL ?? "");
2046
2098
  const apiKey = String(args.apiKey ?? resolveProviderApiKey(model) ?? "");
2047
2099
  const baseUrl = String(args.baseUrl ?? first(env.CODE_REVIEW_BASE_URL, resolveOllamaBaseUrl(model, env)) ?? "");
@@ -2060,11 +2112,12 @@ function resolveConfig(argv = process.argv.slice(2), env = process.env) {
2060
2112
  gitlabUrl,
2061
2113
  gitlabToken: token.token,
2062
2114
  gitlabAuthHeader: token.header,
2063
- githubRepository: String(args.githubRepository ?? env.GITHUB_REPOSITORY ?? ""),
2115
+ githubRepository,
2064
2116
  githubPr: resolveGitHubPr(args, env),
2065
2117
  githubToken: String(args.githubToken ?? env.GITHUB_TOKEN ?? ""),
2066
2118
  githubApiUrl,
2067
2119
  githubServerUrl,
2120
+ projectWebUrl: resolveProjectWebUrl(env) ?? "",
2068
2121
  model,
2069
2122
  modelPool: resolveModelPool(args, env),
2070
2123
  minSeverity: normalizeChoice(args.minSeverity ?? env.CODE_REVIEW_MIN_SEVERITY ?? "info"),
@@ -4734,7 +4787,10 @@ async function runReview(config, options) {
4734
4787
  tokens: aggregated.tokens,
4735
4788
  cost: aggregated.cost,
4736
4789
  byModel: buildByModelUsage(aggregated),
4737
- skills: context.skills.map((s) => s.name),
4790
+ skills: context.skills.map((s) => ({
4791
+ name: s.name,
4792
+ origin: s.origin
4793
+ })),
4738
4794
  sizeNotice
4739
4795
  });
4740
4796
  let outputText;
@@ -5638,7 +5694,7 @@ async function loadDefaultRuntime() {
5638
5694
  const [sdkNode, resources, semconv] = modules;
5639
5695
  const serviceResource = resources.resourceFromAttributes({
5640
5696
  [semconv.ATTR_SERVICE_NAME ?? "service.name"]: SERVICE_NAME,
5641
- [semconv.ATTR_SERVICE_VERSION ?? "service.version"]: "0.9.5"
5697
+ [semconv.ATTR_SERVICE_VERSION ?? "service.version"]: "0.9.6"
5642
5698
  });
5643
5699
  applyOtelExporterDefaults(process.env);
5644
5700
  const sdk = new sdkNode.NodeSDK({ resource: resources.defaultResource().merge(serviceResource) });
@@ -5822,7 +5878,7 @@ function buildCiSpanAttrs(env) {
5822
5878
  if (taskRunId) attrs["cicd.pipeline.task.run.id"] = taskRunId;
5823
5879
  const pipelineRunId = env.CI_PIPELINE_ID ?? env.GITHUB_RUN_ID;
5824
5880
  if (pipelineRunId) attrs["cicd.pipeline.run.id"] = pipelineRunId;
5825
- const repositoryUrl = env.CI_PROJECT_URL ?? (env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}` : void 0);
5881
+ const repositoryUrl = resolveProjectWebUrl(env);
5826
5882
  if (repositoryUrl) attrs["vcs.repository.url.full"] = repositoryUrl;
5827
5883
  return attrs;
5828
5884
  }
@@ -6110,7 +6166,7 @@ function boldCommentTitle(body) {
6110
6166
  */
6111
6167
  function buildCommentBody(body, commitSha, confidence) {
6112
6168
  const confidenceLine = `_Confidence: ${confidence}._`;
6113
- const footer = `<sub>Reviewed by ${PRODUCT_LINK} v0.9.5 for commit ${commitSha}.</sub>`;
6169
+ const footer = `<sub>Reviewed by ${PRODUCT_LINK} v0.9.6 for commit ${commitSha}.</sub>`;
6114
6170
  return `${boldCommentTitle(body.trim())}\n\n${confidenceLine}\n\n---\n\n${footer}`;
6115
6171
  }
6116
6172
  function buildPayload(comment, body, refs, resolved) {
@@ -6683,6 +6739,99 @@ function createPlatform(config) {
6683
6739
  return new GitLabPlatform(config);
6684
6740
  }
6685
6741
  //#endregion
6742
+ //#region src/skill-links.ts
6743
+ /**
6744
+ * Hosts that serve blobs at `/<repo>/blob/<ref>/<path>`. Everything else is
6745
+ * assumed to be GitLab (`/<repo>/-/blob/<ref>/<path>`), which covers both
6746
+ * gitlab.com and the self-hosted instances this tool mostly runs against.
6747
+ */
6748
+ var GITHUB_HOSTS = new Set(["github.com", "www.github.com"]);
6749
+ /** Percent-encode each path segment while keeping the `/` separators intact. */
6750
+ function encodePath(path) {
6751
+ return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
6752
+ }
6753
+ /**
6754
+ * Build a blob URL for `path` at `ref` inside the repository served at
6755
+ * `repoWebUrl`. An empty ref resolves to `HEAD`, which both GitHub and GitLab
6756
+ * accept, so a skill pinned to a remote's default branch still links.
6757
+ */
6758
+ function blobUrl(repoWebUrl, ref, path) {
6759
+ let parsed;
6760
+ try {
6761
+ parsed = new URL(repoWebUrl);
6762
+ } catch {
6763
+ return;
6764
+ }
6765
+ return `${`${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`}/${GITHUB_HOSTS.has(parsed.host) ? "blob" : "-/blob"}/${encodeURIComponent(ref || "HEAD")}/${encodePath(path)}`;
6766
+ }
6767
+ /**
6768
+ * Convert a git clone URL to the repository's web URL: drop any transport
6769
+ * marker, embedded credentials, and the `.git` suffix, and serve it over HTTPS.
6770
+ * Accepts `https://`, `ssh://`, `git+<transport>://`, and scp-style
6771
+ * (`git@host:group/repo.git`) forms. Returns `undefined` for anything it cannot
6772
+ * parse, so an exotic remote simply yields an unlinked skill.
6773
+ */
6774
+ function gitRepoWebUrl(cloneUrl) {
6775
+ let raw = cloneUrl.trim();
6776
+ if (!raw) return void 0;
6777
+ if (raw.startsWith("git+")) raw = raw.slice(4);
6778
+ const scp = /^(?:[^@/]+@)?([^/:]+):(?!\/)(.+)$/.exec(raw);
6779
+ if (scp) raw = `ssh://${scp[1]}/${scp[2]}`;
6780
+ let parsed;
6781
+ try {
6782
+ parsed = new URL(raw);
6783
+ } catch {
6784
+ return;
6785
+ }
6786
+ if (!parsed.host) return void 0;
6787
+ const path = parsed.pathname.replace(/\.git\/?$/, "").replace(/\/+$/, "");
6788
+ if (!path || path === "/") return void 0;
6789
+ return `https://${parsed.host}${path}`;
6790
+ }
6791
+ /**
6792
+ * Resolve a link to a skill's `SKILL.md`, or `undefined` when the source is not
6793
+ * reachable from a browser (a `file:` path, or an in-repo skill on a run with no
6794
+ * CI project URL). Built-in skills link to the published tag of this package, so
6795
+ * the link always shows the skill exactly as the run used it.
6796
+ */
6797
+ function skillSourceUrl(origin, context = {}) {
6798
+ switch (origin.kind) {
6799
+ case "builtin": return blobUrl(PRODUCT_URL, "0.9.6", `skills/${origin.name}/SKILL.md`);
6800
+ case "project":
6801
+ if (!context.projectWebUrl || !context.commitSha) return void 0;
6802
+ return blobUrl(context.projectWebUrl, context.commitSha, `${origin.path}/SKILL.md`);
6803
+ case "npm": return `https://www.npmjs.com/package/${origin.packageName}`;
6804
+ case "file": return;
6805
+ case "git":
6806
+ case "marketplace": {
6807
+ const repoWebUrl = gitRepoWebUrl(origin.url);
6808
+ if (!repoWebUrl) return void 0;
6809
+ const path = origin.path ? `${origin.path}/SKILL.md` : "SKILL.md";
6810
+ return blobUrl(repoWebUrl, origin.ref, path);
6811
+ }
6812
+ }
6813
+ }
6814
+ /**
6815
+ * The name a skill is shown under in the footer. Marketplace skills carry their
6816
+ * full selector (`<marketplace>:<plugin>/<skill>`) because a bare skill name says
6817
+ * nothing about which marketplace and plugin it came from — and that selector is
6818
+ * exactly what a developer would put in `CODE_REVIEW_SKILLS` to use it. Every
6819
+ * other source shows the skill's own name.
6820
+ */
6821
+ function skillDisplayName(skill) {
6822
+ return skill.origin.kind === "marketplace" ? `${skill.origin.marketplace}:${skill.origin.plugin}/${skill.name}` : skill.name;
6823
+ }
6824
+ /**
6825
+ * Render one skill for the summary footer: a link to its source when one can be
6826
+ * resolved, otherwise the bare name. The name keeps its code span either way so
6827
+ * linked and unlinked skills read the same.
6828
+ */
6829
+ function formatSkillLink(skill, context = {}) {
6830
+ const name = skillDisplayName(skill);
6831
+ const url = skillSourceUrl(skill.origin, context);
6832
+ return url ? `[\`${name}\`](${url})` : `\`${name}\``;
6833
+ }
6834
+ //#endregion
6686
6835
  //#region src/summary-carryover.ts
6687
6836
  var FINGERPRINT_MARKER_GLOBAL_RE = new RegExp(FINGERPRINT_MARKER_PATTERN, "gi");
6688
6837
  var RISK_RANK = {
@@ -7058,7 +7207,10 @@ async function run(config, bridges) {
7058
7207
  const summaryBody = withCarriedOverFindings(parsed.summary, discussions, currentFingerprints);
7059
7208
  const result = await platform.upsertSummary(summaryBody, discussions, {
7060
7209
  costFooter: [formatUsageLine(usage), formatPerModelUsage(usage)].filter(Boolean).join("\n\n"),
7061
- skillsFooter: formatSkillsFooter(usage.skills),
7210
+ skillsFooter: formatSkillsFooter(usage.skills, {
7211
+ projectWebUrl: config.projectWebUrl,
7212
+ commitSha: refs.head_sha
7213
+ }),
7062
7214
  reviewedCommitSha: refs.head_sha,
7063
7215
  runId,
7064
7216
  sizeNotice: usage.sizeNotice
@@ -7125,9 +7277,15 @@ function zeroReviewUsage(model, thinkingLevel) {
7125
7277
  sizeNotice: { sizeSkippedFiles: [] }
7126
7278
  };
7127
7279
  }
7128
- function formatSkillsFooter(skills) {
7280
+ /**
7281
+ * The summary footer's skills line. Each skill links to its `SKILL.md` source so
7282
+ * a developer reading the review can check what the reviewer was told; skills
7283
+ * with no reachable source (a `file:` path, or an in-repo skill on a run without
7284
+ * CI project coordinates) stay as plain names.
7285
+ */
7286
+ function formatSkillsFooter(skills, context = {}) {
7129
7287
  if (skills.length === 0) return void 0;
7130
- return `Skills: ${skills.map((s) => `\`${s}\``).join(", ")}`;
7288
+ return `Skills: ${skills.map((s) => formatSkillLink(s, context)).join(", ")}`;
7131
7289
  }
7132
7290
  function formatUsageLine(usage) {
7133
7291
  const formatter = new Intl.NumberFormat("en-US");
@@ -7217,10 +7375,10 @@ async function main(argv = process.argv.slice(2)) {
7217
7375
  return;
7218
7376
  }
7219
7377
  if (argv.includes("--version") || argv.includes("-v")) {
7220
- console.log("0.9.5");
7378
+ console.log("0.9.6");
7221
7379
  return;
7222
7380
  }
7223
- process.stderr.write(`[code-review] @weareikko/code-review v0.9.5\n`);
7381
+ process.stderr.write(`[code-review] @weareikko/code-review v0.9.6\n`);
7224
7382
  assertNodeVersion();
7225
7383
  applyCodeReviewEnvPrefix();
7226
7384
  applyDefaultCacheRetention();
@@ -7241,6 +7399,6 @@ if (isDirectRun()) main().catch((error) => {
7241
7399
  process.exitCode = 1;
7242
7400
  });
7243
7401
  //#endregion
7244
- export { resolveNpmSkillDir as $, SUMMARY_MARKER as A, findExistingSummaryNoteId as B, normalizeSeverity as C, SUMMARY_HISTORY_ENTRY_START as D, SUMMARY_HISTORY_ENTRY_END as E, buildSummaryHistoryEntries as F, extractDiffHunkContext as G, stripSummaryMarker as H, extractReviewedCommitSha as I, normalizeBody as J, extractExistingFingerprints as K, extractSummaryHistoryEntries as L, buildReviewedCommitFooter as M, buildSizeNoticeBlock as N, SUMMARY_HISTORY_LIMIT as O, buildSummaryBody as P, parseSkillSpec as Q, findExistingReviewedCommitSha as R, traceDiagnosticPhase as S, SUMMARY_HISTORY_END as T, upsertSummaryNote as U, stripSummaryHistory as V, appendFingerprintMarkers as W, gitSkillCacheKey as X, sha256 as Y, loadNamedSkill as Z, DIAGNOSTIC_CHANNEL_PREFIX as _, main as a, diagnosticChannels as b, buildGeneratedComments as c, startOtelBridge as d, resolveSkillCacheDir as et, filterDiff as f, DIAGNOSTIC_CHANNEL_NAMES as g, parseReviewMarkdownWithWarnings as h, formatUsageLine as i, buildArchivedSummaryEntry as j, SUMMARY_HISTORY_START as k, buildPayload as l, parseReviewMarkdown as m, formatPerModelUsage as n, run as o, runReview as p, fingerprints as q, formatSkillsFooter as r, withHttpStamping as s, countPostedBySeverity as t, isOtelEnabled as u, createDiagnosticContext as v, toGitLabReviewSeverity as w, traceDiagnostic as x, createDiagnosticRunId as y, findExistingSummaryNote as z };
7402
+ export { normalizeBody as $, SUMMARY_HISTORY_END as A, buildSummaryHistoryEntries as B, createDiagnosticContext as C, traceDiagnosticPhase as D, traceDiagnostic as E, SUMMARY_MARKER as F, findExistingSummaryNoteId as G, extractSummaryHistoryEntries as H, buildArchivedSummaryEntry as I, upsertSummaryNote as J, stripSummaryHistory as K, buildReviewedCommitFooter as L, SUMMARY_HISTORY_ENTRY_START as M, SUMMARY_HISTORY_LIMIT as N, normalizeSeverity as O, SUMMARY_HISTORY_START as P, fingerprints as Q, buildSizeNoticeBlock as R, DIAGNOSTIC_CHANNEL_PREFIX as S, diagnosticChannels as T, findExistingReviewedCommitSha as U, extractReviewedCommitSha as V, findExistingSummaryNote as W, extractDiffHunkContext as X, appendFingerprintMarkers as Y, extractExistingFingerprints as Z, filterDiff as _, main as a, resolveSkillCacheDir as at, parseReviewMarkdownWithWarnings as b, blobUrl as c, skillDisplayName as d, sha256 as et, skillSourceUrl as f, startOtelBridge as g, isOtelEnabled as h, formatUsageLine as i, resolveNpmSkillDir as it, SUMMARY_HISTORY_ENTRY_END as j, toGitLabReviewSeverity as k, formatSkillLink as l, buildPayload as m, formatPerModelUsage as n, loadNamedSkill as nt, run as o, buildGeneratedComments as p, stripSummaryMarker as q, formatSkillsFooter as r, parseSkillSpec as rt, withHttpStamping as s, countPostedBySeverity as t, gitSkillCacheKey as tt, gitRepoWebUrl as u, runReview as v, createDiagnosticRunId as w, DIAGNOSTIC_CHANNEL_NAMES as x, parseReviewMarkdown as y, buildSummaryBody as z };
7245
7403
 
7246
- //# sourceMappingURL=cli-tIo5jQvJ.js.map
7404
+ //# sourceMappingURL=cli-D6vNV3kA.js.map