@weareikko/code-review 0.9.5 → 0.9.7
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-tIo5jQvJ.js → cli-m4pS6_5q.js} +361 -110
- package/dist/cli-m4pS6_5q.js.map +1 -0
- package/dist/cli.d.ts +8 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/config.d.ts +20 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/diagnostics.d.ts +2 -2
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/gitlab-review.d.ts +8 -7
- package/dist/gitlab-review.d.ts.map +1 -1
- package/dist/marketplaces.d.ts +1 -1
- package/dist/marketplaces.d.ts.map +1 -1
- package/dist/otel.d.ts.map +1 -1
- package/dist/review.d.ts +3 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +2 -2
- package/dist/skill-links.d.ts +52 -0
- package/dist/skill-links.d.ts.map +1 -0
- package/dist/skills.d.ts +35 -2
- package/dist/skills.d.ts.map +1 -1
- package/package.json +15 -15
- package/dist/cli-tIo5jQvJ.js.map +0 -1
|
@@ -35,7 +35,7 @@ var GitlabReviewError = class extends Error {
|
|
|
35
35
|
};
|
|
36
36
|
/**
|
|
37
37
|
* Patterns that identify a provider credit/quota-exhaustion error across
|
|
38
|
-
* providers (Anthropic, OpenAI,
|
|
38
|
+
* providers (Anthropic, OpenAI, OpenRouter, …). Matched against the
|
|
39
39
|
* provider's error message. Deliberately excludes transient rate limits (429),
|
|
40
40
|
* which are retryable rather than a billing dead-end.
|
|
41
41
|
*/
|
|
@@ -399,7 +399,7 @@ async function git$1(args, options = {}) {
|
|
|
399
399
|
try {
|
|
400
400
|
const { stdout } = await exec$1("git", args, {
|
|
401
401
|
cwd: options.cwd,
|
|
402
|
-
maxBuffer:
|
|
402
|
+
maxBuffer: 52428800
|
|
403
403
|
});
|
|
404
404
|
return stdout;
|
|
405
405
|
} catch (error) {
|
|
@@ -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,
|
|
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
|
-
|
|
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),
|
|
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,
|
|
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
|
}
|
|
@@ -781,7 +791,8 @@ function redactUrl(url) {
|
|
|
781
791
|
}
|
|
782
792
|
/** Base directory for cached git-skill clones (honours `XDG_CACHE_HOME`). */
|
|
783
793
|
function resolveSkillCacheDir() {
|
|
784
|
-
|
|
794
|
+
const base = process.env.XDG_CACHE_HOME?.trim() || join(homedir(), ".cache");
|
|
795
|
+
return join(base, "code-review", "skills");
|
|
785
796
|
}
|
|
786
797
|
/**
|
|
787
798
|
* Stable cache-directory name for a git skill. Keyed on the clone URL plus the
|
|
@@ -898,13 +909,20 @@ async function loadNamedSkill(spec, cwd, options = {}) {
|
|
|
898
909
|
const pkgRef = parsed.subpath ? `${parsed.packageName} (subpath "${parsed.subpath}")` : parsed.packageName;
|
|
899
910
|
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
911
|
}
|
|
901
|
-
const skill = await loadSkillFromDir(dir,
|
|
912
|
+
const skill = await loadSkillFromDir(dir, {
|
|
913
|
+
kind: "npm",
|
|
914
|
+
packageName: parsed.packageName,
|
|
915
|
+
subpath: parsed.subpath
|
|
916
|
+
});
|
|
902
917
|
if (!skill) throw new ConfigError(`Cannot load skill: "${spec}"`, { hint: `The package at ${dir} does not contain a valid SKILL.md.` });
|
|
903
918
|
return skill;
|
|
904
919
|
}
|
|
905
920
|
if (parsed.protocol === "file") {
|
|
906
921
|
const resolvedPath = parsed.path.startsWith("/") ? parsed.path : join(cwd, parsed.path);
|
|
907
|
-
const skill = await loadSkillFromDir(resolvedPath,
|
|
922
|
+
const skill = await loadSkillFromDir(resolvedPath, {
|
|
923
|
+
kind: "file",
|
|
924
|
+
path: resolvedPath
|
|
925
|
+
});
|
|
908
926
|
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
927
|
return skill;
|
|
910
928
|
}
|
|
@@ -922,7 +940,12 @@ async function loadNamedSkill(spec, cwd, options = {}) {
|
|
|
922
940
|
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
941
|
});
|
|
924
942
|
}
|
|
925
|
-
const skill = await loadSkillFromDir(parsed.subpath ? join(repoDir, parsed.subpath) : repoDir,
|
|
943
|
+
const skill = await loadSkillFromDir(parsed.subpath ? join(repoDir, parsed.subpath) : repoDir, {
|
|
944
|
+
kind: "git",
|
|
945
|
+
url: parsed.url,
|
|
946
|
+
ref: parsed.ref,
|
|
947
|
+
path: parsed.subpath
|
|
948
|
+
});
|
|
926
949
|
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
950
|
return skill;
|
|
928
951
|
}
|
|
@@ -943,7 +966,7 @@ var DEFAULT_MARKETPLACE_FORMAT = "anthropic";
|
|
|
943
966
|
* (`npm:`, `file:`, `git:`) or the bare-name builtin lookup, and so cannot be
|
|
944
967
|
* used as a marketplace name — `<name>:<plugin>/<skill>` would be ambiguous.
|
|
945
968
|
*/
|
|
946
|
-
var RESERVED_MARKETPLACE_NAMES = new Set([
|
|
969
|
+
var RESERVED_MARKETPLACE_NAMES = /* @__PURE__ */ new Set([
|
|
947
970
|
"npm",
|
|
948
971
|
"file",
|
|
949
972
|
"git",
|
|
@@ -1061,7 +1084,7 @@ async function resolveAnthropicSkill(repoDir, mp, spec) {
|
|
|
1061
1084
|
const pluginDir = resolvePluginDir(repoDir, manifest, entry.source, mp, spec);
|
|
1062
1085
|
const realPluginDir = await resolveInside(realRoot, pluginDir, ref);
|
|
1063
1086
|
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
|
|
1087
|
+
const skill = await findSkillInBases(realRoot, bases, pluginDir, spec, ref, mp);
|
|
1065
1088
|
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
1089
|
return skill;
|
|
1067
1090
|
}
|
|
@@ -1121,14 +1144,15 @@ function resolveUnderPlugin(repoDir, pluginDir, rel, ref) {
|
|
|
1121
1144
|
* symlinks and reject any directory that escapes the cloned marketplace before a
|
|
1122
1145
|
* file is read.
|
|
1123
1146
|
*/
|
|
1124
|
-
async function findSkillInBases(realRoot, bases, pluginDir,
|
|
1147
|
+
async function findSkillInBases(realRoot, bases, pluginDir, spec, ref, mp) {
|
|
1148
|
+
const skillName = spec.skill;
|
|
1125
1149
|
for (const base of bases) {
|
|
1126
|
-
const container = await loadSkillIfInside(realRoot, join(base, skillName), ref);
|
|
1150
|
+
const container = await loadSkillIfInside(realRoot, join(base, skillName), ref, mp, spec);
|
|
1127
1151
|
if (container) return container;
|
|
1128
|
-
const single = await loadSkillIfInside(realRoot, base, ref);
|
|
1152
|
+
const single = await loadSkillIfInside(realRoot, base, ref, mp, spec);
|
|
1129
1153
|
if (single && single.name === skillName) return single;
|
|
1130
1154
|
}
|
|
1131
|
-
const root = await loadSkillIfInside(realRoot, pluginDir, ref);
|
|
1155
|
+
const root = await loadSkillIfInside(realRoot, pluginDir, ref, mp, spec);
|
|
1132
1156
|
return root && root.name === skillName ? root : null;
|
|
1133
1157
|
}
|
|
1134
1158
|
/**
|
|
@@ -1173,11 +1197,18 @@ async function readTextInside(realRoot, filePath, ref) {
|
|
|
1173
1197
|
* resolve within `realRoot`. Guarding the file (not just the directory) closes
|
|
1174
1198
|
* the case where a legitimate in-repo dir holds a `SKILL.md` symlinked outside.
|
|
1175
1199
|
*/
|
|
1176
|
-
async function loadSkillIfInside(realRoot, dir, ref) {
|
|
1200
|
+
async function loadSkillIfInside(realRoot, dir, ref, mp, spec) {
|
|
1177
1201
|
const real = await resolveInside(realRoot, dir, ref);
|
|
1178
1202
|
if (!real) return null;
|
|
1179
1203
|
if (await readTextInside(realRoot, join(real, "SKILL.md"), ref) === null) return null;
|
|
1180
|
-
return loadSkillFromDir(real,
|
|
1204
|
+
return loadSkillFromDir(real, {
|
|
1205
|
+
kind: "marketplace",
|
|
1206
|
+
marketplace: mp.name,
|
|
1207
|
+
plugin: spec.plugin,
|
|
1208
|
+
url: mp.url,
|
|
1209
|
+
ref: mp.ref,
|
|
1210
|
+
path: toPosixPath(relative(realRoot, real))
|
|
1211
|
+
});
|
|
1181
1212
|
}
|
|
1182
1213
|
/**
|
|
1183
1214
|
* Resolve a plugin entry's `source` to an absolute directory inside the cloned
|
|
@@ -1304,13 +1335,24 @@ function extractExistingFingerprints(discussions) {
|
|
|
1304
1335
|
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
1336
|
return set;
|
|
1306
1337
|
}
|
|
1338
|
+
//#endregion
|
|
1339
|
+
//#region src/product.ts
|
|
1340
|
+
/**
|
|
1341
|
+
* The published package name, shown in review footers. Independent of the review
|
|
1342
|
+
* platform: the same tool posts to GitLab and GitHub, so the footer identifies
|
|
1343
|
+
* the tool, not the backend. Kept as a single source of truth so the inline and
|
|
1344
|
+
* summary footers can never drift apart.
|
|
1345
|
+
*/
|
|
1346
|
+
var PRODUCT_NAME = "@weareikko/code-review";
|
|
1347
|
+
/** Canonical project URL used in the footer's markdown link. */
|
|
1348
|
+
var PRODUCT_URL = "https://github.com/weareikko/code-review";
|
|
1307
1349
|
/**
|
|
1308
1350
|
* The `[name](url)` markdown link used verbatim in both the inline comment footer
|
|
1309
1351
|
* ({@link buildCommentBody}) and the reviewed-commit summary footer
|
|
1310
1352
|
* ({@link buildReviewedCommitFooter}). Changing this changes the reviewed-commit
|
|
1311
1353
|
* footer format, which is guarded by a migration test.
|
|
1312
1354
|
*/
|
|
1313
|
-
var PRODUCT_LINK = `[
|
|
1355
|
+
var PRODUCT_LINK = `[${PRODUCT_NAME}](${PRODUCT_URL})`;
|
|
1314
1356
|
//#endregion
|
|
1315
1357
|
//#region src/posting.ts
|
|
1316
1358
|
var SUMMARY_MARKER = "<!-- code-review:summary -->";
|
|
@@ -1390,7 +1432,7 @@ function buildSummaryBody(summary, costFooter, options = {}) {
|
|
|
1390
1432
|
return `${withFooter}\n\n${buildSummaryHistoryBlock(historyEntries)}`;
|
|
1391
1433
|
}
|
|
1392
1434
|
function buildReviewedCommitFooter(commitSha) {
|
|
1393
|
-
return `Reviewed by ${PRODUCT_LINK} v0.9.
|
|
1435
|
+
return `Reviewed by ${PRODUCT_LINK} v0.9.7 for commit ${commitSha}.`;
|
|
1394
1436
|
}
|
|
1395
1437
|
function extractReviewedCommitSha(body) {
|
|
1396
1438
|
return REVIEWED_COMMIT_FOOTER_PATTERN.exec(body)?.[1] ?? null;
|
|
@@ -1610,8 +1652,9 @@ async function createDraftsConcurrently(gitlab, project, mr, fresh) {
|
|
|
1610
1652
|
next += 1;
|
|
1611
1653
|
if (index >= fresh.length) return;
|
|
1612
1654
|
const item = fresh[index];
|
|
1655
|
+
const draft = await gitlab.createDraftNote(project, mr, item.payload);
|
|
1613
1656
|
records[index] = {
|
|
1614
|
-
id:
|
|
1657
|
+
id: draft.id,
|
|
1615
1658
|
fingerprints: item.fingerprints
|
|
1616
1659
|
};
|
|
1617
1660
|
}
|
|
@@ -1742,8 +1785,8 @@ var RESERVED_ENV_SUFFIX_SET = new Set(RESERVED_ENV_SUFFIXES);
|
|
|
1742
1785
|
* CI-wide variable of the same name.
|
|
1743
1786
|
*
|
|
1744
1787
|
* This lets credentials and infra vars that `@earendil-works/pi-ai` reads
|
|
1745
|
-
* (`ANTHROPIC_API_KEY`, `
|
|
1746
|
-
*
|
|
1788
|
+
* (`ANTHROPIC_API_KEY`, `OPENROUTER_API_KEY`, `OLLAMA_HOST`,
|
|
1789
|
+
* ambient AWS/Vertex creds, …) — and the GitLab tokens — be
|
|
1747
1790
|
* scoped under `CODE_REVIEW_` in shared CI without enumerating pi-ai's
|
|
1748
1791
|
* provider list.
|
|
1749
1792
|
*
|
|
@@ -1771,8 +1814,8 @@ function applyCodeReviewEnvPrefix(env = process.env) {
|
|
|
1771
1814
|
/**
|
|
1772
1815
|
* Default pi-ai's prompt-cache retention to `long` when the caller has not set
|
|
1773
1816
|
* it. pi-ai reads `PI_CACHE_RETENTION` from `process.env` at request time; `long`
|
|
1774
|
-
* asks providers that support it (e.g. OpenAI's `openai-responses` API
|
|
1775
|
-
*
|
|
1817
|
+
* asks providers that support it (e.g. OpenAI's `openai-responses` API) to keep
|
|
1818
|
+
* the cached system-prompt prefix for up
|
|
1776
1819
|
* to 24h so reviews spaced hours apart still reuse it. It is a safe no-op for
|
|
1777
1820
|
* providers/models without long-retention support (e.g. Anthropic), where it
|
|
1778
1821
|
* behaves exactly like the default `short`.
|
|
@@ -1784,7 +1827,7 @@ function applyDefaultCacheRetention(env = process.env) {
|
|
|
1784
1827
|
if (!env.PI_CACHE_RETENTION) env.PI_CACHE_RETENTION = "long";
|
|
1785
1828
|
return env;
|
|
1786
1829
|
}
|
|
1787
|
-
var BOOLEAN_FLAGS = new Set([
|
|
1830
|
+
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
1788
1831
|
"dry-run",
|
|
1789
1832
|
"no-post",
|
|
1790
1833
|
"no-summary",
|
|
@@ -1795,7 +1838,7 @@ var BOOLEAN_FLAGS = new Set([
|
|
|
1795
1838
|
"help",
|
|
1796
1839
|
"version"
|
|
1797
1840
|
]);
|
|
1798
|
-
var MULTI_FLAGS = new Set(["skill", "marketplace"]);
|
|
1841
|
+
var MULTI_FLAGS = /* @__PURE__ */ new Set(["skill", "marketplace"]);
|
|
1799
1842
|
function parseArgs(argv) {
|
|
1800
1843
|
const args = {};
|
|
1801
1844
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -2035,6 +2078,16 @@ function detectPlatform(args, env, readEventFile = readEventFileSync) {
|
|
|
2035
2078
|
if (hasGitLab && !hasGitHub) return "gitlab";
|
|
2036
2079
|
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
2080
|
}
|
|
2081
|
+
/**
|
|
2082
|
+
* Web URL of the repository under review, as the CI environment reports it:
|
|
2083
|
+
* GitLab exposes it directly, GitHub composes it from the server and the
|
|
2084
|
+
* `owner/repo` slug. Returns `undefined` outside CI, where no such URL exists.
|
|
2085
|
+
* Shared by the config (skill source links) and the OTel attribute builder.
|
|
2086
|
+
*/
|
|
2087
|
+
function resolveProjectWebUrl(env = process.env) {
|
|
2088
|
+
if (env.CI_PROJECT_URL) return env.CI_PROJECT_URL.replace(/\/$/, "");
|
|
2089
|
+
if (env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY) return `${env.GITHUB_SERVER_URL.replace(/\/$/, "")}/${env.GITHUB_REPOSITORY}`;
|
|
2090
|
+
}
|
|
2038
2091
|
function resolveConfig(argv = process.argv.slice(2), env = process.env) {
|
|
2039
2092
|
const args = parseArgs(argv);
|
|
2040
2093
|
const platform = detectPlatform(args, env);
|
|
@@ -2042,6 +2095,7 @@ function resolveConfig(argv = process.argv.slice(2), env = process.env) {
|
|
|
2042
2095
|
const token = resolveGitLabToken(args, env);
|
|
2043
2096
|
const githubApiUrl = String(args.githubApiUrl ?? env.GITHUB_API_URL ?? "https://api.github.com").replace(/\/$/, "");
|
|
2044
2097
|
const githubServerUrl = String(args.githubServerUrl ?? env.GITHUB_SERVER_URL ?? "https://github.com").replace(/\/$/, "");
|
|
2098
|
+
const githubRepository = String(args.githubRepository ?? env.GITHUB_REPOSITORY ?? "");
|
|
2045
2099
|
const model = String(args.model ?? env.CODE_REVIEW_MODEL ?? "");
|
|
2046
2100
|
const apiKey = String(args.apiKey ?? resolveProviderApiKey(model) ?? "");
|
|
2047
2101
|
const baseUrl = String(args.baseUrl ?? first(env.CODE_REVIEW_BASE_URL, resolveOllamaBaseUrl(model, env)) ?? "");
|
|
@@ -2060,11 +2114,12 @@ function resolveConfig(argv = process.argv.slice(2), env = process.env) {
|
|
|
2060
2114
|
gitlabUrl,
|
|
2061
2115
|
gitlabToken: token.token,
|
|
2062
2116
|
gitlabAuthHeader: token.header,
|
|
2063
|
-
githubRepository
|
|
2117
|
+
githubRepository,
|
|
2064
2118
|
githubPr: resolveGitHubPr(args, env),
|
|
2065
2119
|
githubToken: String(args.githubToken ?? env.GITHUB_TOKEN ?? ""),
|
|
2066
2120
|
githubApiUrl,
|
|
2067
2121
|
githubServerUrl,
|
|
2122
|
+
projectWebUrl: resolveProjectWebUrl(env) ?? "",
|
|
2068
2123
|
model,
|
|
2069
2124
|
modelPool: resolveModelPool(args, env),
|
|
2070
2125
|
minSeverity: normalizeChoice(args.minSeverity ?? env.CODE_REVIEW_MIN_SEVERITY ?? "info"),
|
|
@@ -2393,7 +2448,8 @@ function createGitTools(dir, options = {}) {
|
|
|
2393
2448
|
dir,
|
|
2394
2449
|
oid
|
|
2395
2450
|
});
|
|
2396
|
-
const
|
|
2451
|
+
const parent = commit.parent[0] ?? null;
|
|
2452
|
+
const diff = await diffCommits(dir, fs, parent, oid);
|
|
2397
2453
|
return text(`commit ${oid}\nAuthor: ${commit.author.name} <${commit.author.email}>\n\n${commit.message.trim()}\n\n${diff}`);
|
|
2398
2454
|
}
|
|
2399
2455
|
},
|
|
@@ -2406,7 +2462,9 @@ function createGitTools(dir, options = {}) {
|
|
|
2406
2462
|
to: Type.String({ description: "Target ref/sha." })
|
|
2407
2463
|
}),
|
|
2408
2464
|
async execute(_id, params) {
|
|
2409
|
-
|
|
2465
|
+
const fromOid = await resolveOid(dir, fs, params.from);
|
|
2466
|
+
const toOid = await resolveOid(dir, fs, params.to);
|
|
2467
|
+
return text(await diffCommits(dir, fs, fromOid, toOid));
|
|
2410
2468
|
}
|
|
2411
2469
|
}
|
|
2412
2470
|
];
|
|
@@ -2572,6 +2630,80 @@ function removeAtIndex(text, start, count) {
|
|
|
2572
2630
|
function endsWithCommaOrNewline(text) {
|
|
2573
2631
|
return /[,\n][ \t\r]*$/.test(text);
|
|
2574
2632
|
}
|
|
2633
|
+
var namedHtmlEntities = {
|
|
2634
|
+
""": "\"",
|
|
2635
|
+
"&": "&",
|
|
2636
|
+
"<": "<",
|
|
2637
|
+
">": ">",
|
|
2638
|
+
"'": "'"
|
|
2639
|
+
};
|
|
2640
|
+
/**
|
|
2641
|
+
* Try to match an HTML entity at the start of the given fragment. The fragment
|
|
2642
|
+
* is a small slice of text that begins exactly at the candidate '&'. Returns the
|
|
2643
|
+
* decoded character and the number of characters consumed, or null when there
|
|
2644
|
+
* is no complete, valid entity (for example a truncated """ without ';').
|
|
2645
|
+
*/
|
|
2646
|
+
function matchHtmlEntity(fragment) {
|
|
2647
|
+
if (fragment.charAt(0) !== "&") return null;
|
|
2648
|
+
const semicolon = fragment.indexOf(";");
|
|
2649
|
+
if (semicolon === -1) return null;
|
|
2650
|
+
const entity = fragment.substring(0, semicolon + 1);
|
|
2651
|
+
const named = namedHtmlEntities[entity];
|
|
2652
|
+
if (named !== void 0) return {
|
|
2653
|
+
char: named,
|
|
2654
|
+
length: entity.length
|
|
2655
|
+
};
|
|
2656
|
+
if (fragment.charAt(1) === "#") {
|
|
2657
|
+
const body = fragment.substring(2, semicolon);
|
|
2658
|
+
const hex = body.charAt(0) === "x" || body.charAt(0) === "X";
|
|
2659
|
+
const digits = hex ? body.substring(1) : body;
|
|
2660
|
+
if (digits.length > 0) {
|
|
2661
|
+
const code = Number.parseInt(digits, hex ? 16 : 10);
|
|
2662
|
+
if (!Number.isNaN(code) && code >= 0 && code <= 1114111) return {
|
|
2663
|
+
char: String.fromCodePoint(code),
|
|
2664
|
+
length: entity.length
|
|
2665
|
+
};
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
return null;
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* Test whether a matched HTML entity decodes to a double quote character
|
|
2672
|
+
*/
|
|
2673
|
+
function isDoubleQuoteEntity(match) {
|
|
2674
|
+
return match !== null && match.char === "\"";
|
|
2675
|
+
}
|
|
2676
|
+
/**
|
|
2677
|
+
* Test whether a matched HTML entity decodes to a single quote character
|
|
2678
|
+
*/
|
|
2679
|
+
function isSingleQuoteEntity(match) {
|
|
2680
|
+
return match !== null && match.char === "'";
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* Count the number of occurrences of a single character in a string
|
|
2684
|
+
*/
|
|
2685
|
+
function countOccurrences(text, char) {
|
|
2686
|
+
let count = 0;
|
|
2687
|
+
for (let i = 0; i < text.length; i++) if (text.charAt(i) === char) count++;
|
|
2688
|
+
return count;
|
|
2689
|
+
}
|
|
2690
|
+
/**
|
|
2691
|
+
* Test whether `closeChar` is a closing bracket and `text` still contains an
|
|
2692
|
+
* unmatched opening bracket of the same kind. This indicates that the end of
|
|
2693
|
+
* `text` is located inside the brackets, for example the quote in
|
|
2694
|
+
* `"a (b") c"` is followed by `)` while `(` is still unclosed.
|
|
2695
|
+
*
|
|
2696
|
+
* Note that the (potentially expensive) counting is only performed when
|
|
2697
|
+
* `closeChar` actually is a closing bracket.
|
|
2698
|
+
*/
|
|
2699
|
+
function isInsideUnclosedBracket(text, closeChar) {
|
|
2700
|
+
switch (closeChar) {
|
|
2701
|
+
case ")": return countOccurrences(text, "(") > countOccurrences(text, ")");
|
|
2702
|
+
case "]": return countOccurrences(text, "[") > countOccurrences(text, "]");
|
|
2703
|
+
case "}": return countOccurrences(text, "{") > countOccurrences(text, "}");
|
|
2704
|
+
default: return false;
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2575
2707
|
//#endregion
|
|
2576
2708
|
//#region node_modules/jsonrepair/lib/esm/regular/jsonrepair.js
|
|
2577
2709
|
var controlCharacters = {
|
|
@@ -2744,23 +2876,26 @@ function jsonrepair(text) {
|
|
|
2744
2876
|
processedComma = parseCharacter(",");
|
|
2745
2877
|
if (!processedComma) output = insertBeforeLastWhitespace(output, ",");
|
|
2746
2878
|
parseWhitespaceAndSkipComments();
|
|
2747
|
-
} else
|
|
2748
|
-
processedComma = true;
|
|
2749
|
-
initial = false;
|
|
2750
|
-
}
|
|
2879
|
+
} else processedComma = true;
|
|
2751
2880
|
skipEllipsis();
|
|
2752
2881
|
if (!(parseString() || parseUnquotedString(true))) {
|
|
2753
|
-
if (text[i] === "}" || text[i] === "{" || text[i] === "]" || text[i] === "[" || text[i] === void 0)
|
|
2754
|
-
|
|
2882
|
+
if (text[i] === "}" || text[i] === "{" || text[i] === "]" || text[i] === "[" || text[i] === void 0) {
|
|
2883
|
+
if (!initial) output = stripLastOccurrence(output, ",");
|
|
2884
|
+
} else throwObjectKeyExpected();
|
|
2755
2885
|
break;
|
|
2756
2886
|
}
|
|
2757
2887
|
parseWhitespaceAndSkipComments();
|
|
2758
2888
|
const processedColon = parseCharacter(":");
|
|
2759
2889
|
const truncatedText = i >= text.length;
|
|
2760
|
-
if (!processedColon)
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2890
|
+
if (!processedColon) {
|
|
2891
|
+
if (isStartOfValue(text[i]) || truncatedText) output = insertBeforeLastWhitespace(output, ":");
|
|
2892
|
+
else throwColonExpected();
|
|
2893
|
+
}
|
|
2894
|
+
if (!parseValue()) {
|
|
2895
|
+
if (processedColon || truncatedText) output += "null";
|
|
2896
|
+
else throwColonExpected();
|
|
2897
|
+
}
|
|
2898
|
+
initial = false;
|
|
2764
2899
|
}
|
|
2765
2900
|
if (text[i] === "}") {
|
|
2766
2901
|
output += "}";
|
|
@@ -2783,12 +2918,13 @@ function jsonrepair(text) {
|
|
|
2783
2918
|
while (i < text.length && text[i] !== "]") {
|
|
2784
2919
|
if (!initial) {
|
|
2785
2920
|
if (!parseCharacter(",")) output = insertBeforeLastWhitespace(output, ",");
|
|
2786
|
-
}
|
|
2921
|
+
}
|
|
2787
2922
|
skipEllipsis();
|
|
2788
2923
|
if (!parseValue()) {
|
|
2789
|
-
output = stripLastOccurrence(output, ",");
|
|
2924
|
+
if (!initial) output = stripLastOccurrence(output, ",");
|
|
2790
2925
|
break;
|
|
2791
2926
|
}
|
|
2927
|
+
initial = false;
|
|
2792
2928
|
}
|
|
2793
2929
|
if (text[i] === "]") {
|
|
2794
2930
|
output += "]";
|
|
@@ -2830,17 +2966,19 @@ function jsonrepair(text) {
|
|
|
2830
2966
|
function parseString() {
|
|
2831
2967
|
let stopAtDelimiter = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : false;
|
|
2832
2968
|
let stopAtIndex = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : -1;
|
|
2833
|
-
|
|
2969
|
+
const skipEscapeChars = text[i] === "\\";
|
|
2834
2970
|
if (skipEscapeChars) {
|
|
2835
2971
|
i++;
|
|
2836
|
-
|
|
2972
|
+
if (!isQuote(text[i])) throwUnexpectedCharacter();
|
|
2837
2973
|
}
|
|
2838
|
-
|
|
2974
|
+
const openEntity = text[i] === "&" ? matchHtmlEntity(text.slice(i, i + 12)) : null;
|
|
2975
|
+
const openedByEntity = isDoubleQuoteEntity(openEntity) || isSingleQuoteEntity(openEntity);
|
|
2976
|
+
if (isQuote(text[i]) || openedByEntity) {
|
|
2839
2977
|
const isEndQuote = isDoubleQuote(text[i]) ? isDoubleQuote : isSingleQuote(text[i]) ? isSingleQuote : isSingleQuoteLike(text[i]) ? isSingleQuoteLike : isDoubleQuoteLike;
|
|
2840
2978
|
const iBefore = i;
|
|
2841
2979
|
const oBefore = output.length;
|
|
2842
2980
|
let str = "\"";
|
|
2843
|
-
i
|
|
2981
|
+
i += openedByEntity && openEntity ? openEntity.length : 1;
|
|
2844
2982
|
while (true) {
|
|
2845
2983
|
if (i >= text.length) {
|
|
2846
2984
|
const iPrev = prevNonWhitespaceIndex(i - 1);
|
|
@@ -2858,17 +2996,19 @@ function jsonrepair(text) {
|
|
|
2858
2996
|
output += str;
|
|
2859
2997
|
return true;
|
|
2860
2998
|
}
|
|
2861
|
-
|
|
2999
|
+
const entity = openedByEntity && text[i] === "&" ? matchHtmlEntity(text.slice(i, i + 12)) : null;
|
|
3000
|
+
if (entity && openEntity ? entity.char === openEntity.char : isEndQuote(text[i])) {
|
|
2862
3001
|
const iQuote = i;
|
|
2863
3002
|
const oQuote = str.length;
|
|
2864
3003
|
str += "\"";
|
|
2865
|
-
i
|
|
3004
|
+
i += entity ? entity.length : 1;
|
|
2866
3005
|
output += str;
|
|
2867
3006
|
parseWhitespaceAndSkipComments(false);
|
|
2868
|
-
if (stopAtDelimiter || i >= text.length || isDelimiter(text[i]) || isQuote(text[i]) || isDigit(text[i])) {
|
|
3007
|
+
if (stopAtDelimiter || i >= text.length || isDelimiter(text[i]) && !isInsideUnclosedBracket(str, text[i]) || isQuote(text[i]) && !nextQuoteIsEndQuote(i) || isDigit(text[i])) {
|
|
2869
3008
|
parseConcatenatedString();
|
|
2870
3009
|
return true;
|
|
2871
3010
|
}
|
|
3011
|
+
if (text[i] === "\\") throwUnexpectedCharacter();
|
|
2872
3012
|
const iPrevChar = prevNonWhitespaceIndex(iQuote - 1);
|
|
2873
3013
|
const prevChar = text.charAt(iPrevChar);
|
|
2874
3014
|
if (prevChar === ",") {
|
|
@@ -2882,7 +3022,7 @@ function jsonrepair(text) {
|
|
|
2882
3022
|
return parseString(true);
|
|
2883
3023
|
}
|
|
2884
3024
|
output = output.substring(0, oBefore);
|
|
2885
|
-
i = iQuote + 1;
|
|
3025
|
+
i = iQuote + (entity ? entity.length : 1);
|
|
2886
3026
|
str = `${str.substring(0, oQuote)}\\${str.substring(oQuote)}`;
|
|
2887
3027
|
} else if (stopAtDelimiter && isUnquotedStringDelimiter(text[i])) {
|
|
2888
3028
|
if (text[i - 1] === ":" && regexUrlStart.test(text.substring(iBefore + 1, i + 2))) while (i < text.length && regexUrlChar.test(text[i])) {
|
|
@@ -2893,6 +3033,12 @@ function jsonrepair(text) {
|
|
|
2893
3033
|
output += str;
|
|
2894
3034
|
parseConcatenatedString();
|
|
2895
3035
|
return true;
|
|
3036
|
+
} else if (entity) {
|
|
3037
|
+
const char = entity.char;
|
|
3038
|
+
if (char === "\"") str += "\\\"";
|
|
3039
|
+
else if (isControlCharacter(char)) str += controlCharacters[char];
|
|
3040
|
+
else str += char;
|
|
3041
|
+
i += entity.length;
|
|
2896
3042
|
} else if (text[i] === "\\") {
|
|
2897
3043
|
const char = text.charAt(i + 1);
|
|
2898
3044
|
if (escapeCharacters[char] !== void 0) {
|
|
@@ -2954,51 +3100,48 @@ function jsonrepair(text) {
|
|
|
2954
3100
|
*/
|
|
2955
3101
|
function parseNumber() {
|
|
2956
3102
|
const start = i;
|
|
3103
|
+
let num = "";
|
|
3104
|
+
let invalid = false;
|
|
2957
3105
|
if (text[i] === "-") {
|
|
3106
|
+
num += text[i];
|
|
3107
|
+
i++;
|
|
3108
|
+
if (!isDigit(text[i]) && atEndOfNumber()) num += "0";
|
|
3109
|
+
}
|
|
3110
|
+
if (text[i] === "0" && isDigit(text[i + 1])) invalid = true;
|
|
3111
|
+
while (isDigit(text[i])) {
|
|
3112
|
+
num += text[i];
|
|
2958
3113
|
i++;
|
|
2959
|
-
if (atEndOfNumber()) {
|
|
2960
|
-
repairNumberEndingWithNumericSymbol(start);
|
|
2961
|
-
return true;
|
|
2962
|
-
}
|
|
2963
|
-
if (!isDigit(text[i])) {
|
|
2964
|
-
i = start;
|
|
2965
|
-
return false;
|
|
2966
|
-
}
|
|
2967
3114
|
}
|
|
2968
|
-
while (isDigit(text[i])) i++;
|
|
2969
3115
|
if (text[i] === ".") {
|
|
3116
|
+
if (num === "" || num === "-") num += "0";
|
|
3117
|
+
num += text[i];
|
|
2970
3118
|
i++;
|
|
2971
|
-
if (
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
if (!isDigit(text[i])) {
|
|
2976
|
-
i = start;
|
|
2977
|
-
return false;
|
|
3119
|
+
if (!isDigit(text[i])) num += "0";
|
|
3120
|
+
while (isDigit(text[i])) {
|
|
3121
|
+
num += text[i];
|
|
3122
|
+
i++;
|
|
2978
3123
|
}
|
|
2979
|
-
while (isDigit(text[i])) i++;
|
|
2980
3124
|
}
|
|
2981
|
-
if (
|
|
2982
|
-
i
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
3125
|
+
if (i > start) {
|
|
3126
|
+
if (text[i] === "e" || text[i] === "E") {
|
|
3127
|
+
if (num === "-") invalid = true;
|
|
3128
|
+
num += text[i];
|
|
3129
|
+
i++;
|
|
3130
|
+
if (text[i] === "-" || text[i] === "+") {
|
|
3131
|
+
num += text[i];
|
|
3132
|
+
i++;
|
|
3133
|
+
}
|
|
3134
|
+
if (!isDigit(text[i])) num += "0";
|
|
3135
|
+
while (isDigit(text[i])) {
|
|
3136
|
+
num += text[i];
|
|
3137
|
+
i++;
|
|
3138
|
+
}
|
|
2987
3139
|
}
|
|
2988
|
-
if (!
|
|
3140
|
+
if (!atEndOfNumber()) {
|
|
2989
3141
|
i = start;
|
|
2990
3142
|
return false;
|
|
2991
3143
|
}
|
|
2992
|
-
|
|
2993
|
-
}
|
|
2994
|
-
if (!atEndOfNumber()) {
|
|
2995
|
-
i = start;
|
|
2996
|
-
return false;
|
|
2997
|
-
}
|
|
2998
|
-
if (i > start) {
|
|
2999
|
-
const num = text.slice(start, i);
|
|
3000
|
-
const hasInvalidLeadingZero = /^0\d/.test(num);
|
|
3001
|
-
output += hasInvalidLeadingZero ? `"${num}"` : num;
|
|
3144
|
+
output += invalid ? `"${text.substring(start, i)}"` : num;
|
|
3002
3145
|
return true;
|
|
3003
3146
|
}
|
|
3004
3147
|
return false;
|
|
@@ -3011,7 +3154,7 @@ function jsonrepair(text) {
|
|
|
3011
3154
|
return parseKeyword("true", "true") || parseKeyword("false", "false") || parseKeyword("null", "null") || parseKeyword("True", "true") || parseKeyword("False", "false") || parseKeyword("None", "null");
|
|
3012
3155
|
}
|
|
3013
3156
|
function parseKeyword(name, value) {
|
|
3014
|
-
if (text.slice(i, i + name.length) === name) {
|
|
3157
|
+
if (text.slice(i, i + name.length) === name && !isFunctionNameChar(text[i + name.length])) {
|
|
3015
3158
|
output += value;
|
|
3016
3159
|
i += name.length;
|
|
3017
3160
|
return true;
|
|
@@ -3064,12 +3207,14 @@ function jsonrepair(text) {
|
|
|
3064
3207
|
while (prev > 0 && isWhitespace(text, prev)) prev--;
|
|
3065
3208
|
return prev;
|
|
3066
3209
|
}
|
|
3210
|
+
function nextQuoteIsEndQuote(index) {
|
|
3211
|
+
let next = index + 1;
|
|
3212
|
+
while (next < text.length && isWhitespace(text, next)) next++;
|
|
3213
|
+
return next >= text.length || isDelimiter(text[next]);
|
|
3214
|
+
}
|
|
3067
3215
|
function atEndOfNumber() {
|
|
3068
3216
|
return i >= text.length || isDelimiter(text[i]) || isWhitespace(text, i);
|
|
3069
3217
|
}
|
|
3070
|
-
function repairNumberEndingWithNumericSymbol(start) {
|
|
3071
|
-
output += `${text.slice(start, i)}0`;
|
|
3072
|
-
}
|
|
3073
3218
|
function throwInvalidCharacter(char) {
|
|
3074
3219
|
throw new JSONRepairError(`Invalid character ${JSON.stringify(char)}`, i);
|
|
3075
3220
|
}
|
|
@@ -3713,7 +3858,8 @@ function slugify(path) {
|
|
|
3713
3858
|
*/
|
|
3714
3859
|
async function writeSkippedDiffs(cwd, sections) {
|
|
3715
3860
|
if (sections.length === 0) return [];
|
|
3716
|
-
|
|
3861
|
+
const dir = join(cwd, SKIPPED_DIFF_DIR);
|
|
3862
|
+
await mkdir(dir, { recursive: true });
|
|
3717
3863
|
const files = [];
|
|
3718
3864
|
for (const { path, section } of sections) {
|
|
3719
3865
|
const relative = join(SKIPPED_DIFF_DIR, slugify(path));
|
|
@@ -3785,7 +3931,7 @@ function normalizeSubject(body) {
|
|
|
3785
3931
|
var MAX_LINE_DELTA = 2;
|
|
3786
3932
|
/** Min token-set Jaccard similarity of normalised subjects required to merge. */
|
|
3787
3933
|
var SUBJECT_SIMILARITY_THRESHOLD = .6;
|
|
3788
|
-
var STOP_WORDS = new Set([
|
|
3934
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
3789
3935
|
"a",
|
|
3790
3936
|
"an",
|
|
3791
3937
|
"the",
|
|
@@ -3908,7 +4054,7 @@ function triageFindings(groups) {
|
|
|
3908
4054
|
}
|
|
3909
4055
|
//#endregion
|
|
3910
4056
|
//#region src/gitlab-review.ts
|
|
3911
|
-
var DEFAULT_REVIEW_TIMEOUT_MS =
|
|
4057
|
+
var DEFAULT_REVIEW_TIMEOUT_MS = 6e5;
|
|
3912
4058
|
var DEFAULT_MAX_DIFF_CHARS = 1e5;
|
|
3913
4059
|
var CONVENTION_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
3914
4060
|
var REVIEW_RULE_FILES = ["REVIEW.md"];
|
|
@@ -3926,7 +4072,7 @@ var NOISE_PATH_PATTERNS = [
|
|
|
3926
4072
|
/\.d\.ts$/,
|
|
3927
4073
|
/\.(js|css)\.map$/
|
|
3928
4074
|
];
|
|
3929
|
-
var LOCKFILE_BASENAMES = new Set([
|
|
4075
|
+
var LOCKFILE_BASENAMES = /* @__PURE__ */ new Set([
|
|
3930
4076
|
"package-lock.json",
|
|
3931
4077
|
"npm-shrinkwrap.json",
|
|
3932
4078
|
"yarn.lock",
|
|
@@ -3967,12 +4113,11 @@ var exec = promisify(execFile);
|
|
|
3967
4113
|
*
|
|
3968
4114
|
* pi-ai >=0.82 requires an explicit stream function (earlier versions built one
|
|
3969
4115
|
* internally from model + getApiKey); `streamSimple` is the drop-in. Critically,
|
|
3970
|
-
* 0.83 also moved
|
|
3971
|
-
*
|
|
3972
|
-
*
|
|
3973
|
-
*
|
|
3974
|
-
*
|
|
3975
|
-
* `env` is harmless for providers whose base URL has no placeholders.
|
|
4116
|
+
* 0.83 also moved base-URL placeholder substitution from a direct `process.env`
|
|
4117
|
+
* read to an explicit `env` on the stream options. Providers whose base URL
|
|
4118
|
+
* contains `{VAR}` placeholders keep them literal — and fail every request —
|
|
4119
|
+
* unless `env` is threaded through. Passing `env` is harmless for providers
|
|
4120
|
+
* whose base URL has none.
|
|
3976
4121
|
*
|
|
3977
4122
|
* `stream` is injectable so the env threading can be unit-tested without a live call.
|
|
3978
4123
|
*/
|
|
@@ -4734,7 +4879,10 @@ async function runReview(config, options) {
|
|
|
4734
4879
|
tokens: aggregated.tokens,
|
|
4735
4880
|
cost: aggregated.cost,
|
|
4736
4881
|
byModel: buildByModelUsage(aggregated),
|
|
4737
|
-
skills: context.skills.map((s) =>
|
|
4882
|
+
skills: context.skills.map((s) => ({
|
|
4883
|
+
name: s.name,
|
|
4884
|
+
origin: s.origin
|
|
4885
|
+
})),
|
|
4738
4886
|
sizeNotice
|
|
4739
4887
|
});
|
|
4740
4888
|
let outputText;
|
|
@@ -5348,7 +5496,8 @@ async function startOtelBridge(options = {}) {
|
|
|
5348
5496
|
createAgentTelemetry(runId) {
|
|
5349
5497
|
const reviewerEntry = openByRun.get(runId)?.get(GEN_AI_PHASE);
|
|
5350
5498
|
if (!reviewerEntry || reviewerEntry.closed) return void 0;
|
|
5351
|
-
|
|
5499
|
+
const reviewerSpanCtx = trace.setSpan(context.active(), reviewerEntry.span);
|
|
5500
|
+
return buildAgentSubscriber(tracer, tokenUsage, operationCost, timeToFirstToken, reviewerSpanCtx, {
|
|
5352
5501
|
ciAttrs,
|
|
5353
5502
|
runId,
|
|
5354
5503
|
configuredModel: runMeta.get(runId)?.model,
|
|
@@ -5638,7 +5787,7 @@ async function loadDefaultRuntime() {
|
|
|
5638
5787
|
const [sdkNode, resources, semconv] = modules;
|
|
5639
5788
|
const serviceResource = resources.resourceFromAttributes({
|
|
5640
5789
|
[semconv.ATTR_SERVICE_NAME ?? "service.name"]: SERVICE_NAME,
|
|
5641
|
-
[semconv.ATTR_SERVICE_VERSION ?? "service.version"]: "0.9.
|
|
5790
|
+
[semconv.ATTR_SERVICE_VERSION ?? "service.version"]: "0.9.7"
|
|
5642
5791
|
});
|
|
5643
5792
|
applyOtelExporterDefaults(process.env);
|
|
5644
5793
|
const sdk = new sdkNode.NodeSDK({ resource: resources.defaultResource().merge(serviceResource) });
|
|
@@ -5822,7 +5971,7 @@ function buildCiSpanAttrs(env) {
|
|
|
5822
5971
|
if (taskRunId) attrs["cicd.pipeline.task.run.id"] = taskRunId;
|
|
5823
5972
|
const pipelineRunId = env.CI_PIPELINE_ID ?? env.GITHUB_RUN_ID;
|
|
5824
5973
|
if (pipelineRunId) attrs["cicd.pipeline.run.id"] = pipelineRunId;
|
|
5825
|
-
const repositoryUrl =
|
|
5974
|
+
const repositoryUrl = resolveProjectWebUrl(env);
|
|
5826
5975
|
if (repositoryUrl) attrs["vcs.repository.url.full"] = repositoryUrl;
|
|
5827
5976
|
return attrs;
|
|
5828
5977
|
}
|
|
@@ -6110,7 +6259,7 @@ function boldCommentTitle(body) {
|
|
|
6110
6259
|
*/
|
|
6111
6260
|
function buildCommentBody(body, commitSha, confidence) {
|
|
6112
6261
|
const confidenceLine = `_Confidence: ${confidence}._`;
|
|
6113
|
-
const footer = `<sub>Reviewed by ${PRODUCT_LINK} v0.9.
|
|
6262
|
+
const footer = `<sub>Reviewed by ${PRODUCT_LINK} v0.9.7 for commit ${commitSha}.</sub>`;
|
|
6114
6263
|
return `${boldCommentTitle(body.trim())}\n\n${confidenceLine}\n\n---\n\n${footer}`;
|
|
6115
6264
|
}
|
|
6116
6265
|
function buildPayload(comment, body, refs, resolved) {
|
|
@@ -6683,6 +6832,99 @@ function createPlatform(config) {
|
|
|
6683
6832
|
return new GitLabPlatform(config);
|
|
6684
6833
|
}
|
|
6685
6834
|
//#endregion
|
|
6835
|
+
//#region src/skill-links.ts
|
|
6836
|
+
/**
|
|
6837
|
+
* Hosts that serve blobs at `/<repo>/blob/<ref>/<path>`. Everything else is
|
|
6838
|
+
* assumed to be GitLab (`/<repo>/-/blob/<ref>/<path>`), which covers both
|
|
6839
|
+
* gitlab.com and the self-hosted instances this tool mostly runs against.
|
|
6840
|
+
*/
|
|
6841
|
+
var GITHUB_HOSTS = /* @__PURE__ */ new Set(["github.com", "www.github.com"]);
|
|
6842
|
+
/** Percent-encode each path segment while keeping the `/` separators intact. */
|
|
6843
|
+
function encodePath(path) {
|
|
6844
|
+
return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
|
|
6845
|
+
}
|
|
6846
|
+
/**
|
|
6847
|
+
* Build a blob URL for `path` at `ref` inside the repository served at
|
|
6848
|
+
* `repoWebUrl`. An empty ref resolves to `HEAD`, which both GitHub and GitLab
|
|
6849
|
+
* accept, so a skill pinned to a remote's default branch still links.
|
|
6850
|
+
*/
|
|
6851
|
+
function blobUrl(repoWebUrl, ref, path) {
|
|
6852
|
+
let parsed;
|
|
6853
|
+
try {
|
|
6854
|
+
parsed = new URL(repoWebUrl);
|
|
6855
|
+
} catch {
|
|
6856
|
+
return;
|
|
6857
|
+
}
|
|
6858
|
+
return `${`${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`}/${GITHUB_HOSTS.has(parsed.host) ? "blob" : "-/blob"}/${encodeURIComponent(ref || "HEAD")}/${encodePath(path)}`;
|
|
6859
|
+
}
|
|
6860
|
+
/**
|
|
6861
|
+
* Convert a git clone URL to the repository's web URL: drop any transport
|
|
6862
|
+
* marker, embedded credentials, and the `.git` suffix, and serve it over HTTPS.
|
|
6863
|
+
* Accepts `https://`, `ssh://`, `git+<transport>://`, and scp-style
|
|
6864
|
+
* (`git@host:group/repo.git`) forms. Returns `undefined` for anything it cannot
|
|
6865
|
+
* parse, so an exotic remote simply yields an unlinked skill.
|
|
6866
|
+
*/
|
|
6867
|
+
function gitRepoWebUrl(cloneUrl) {
|
|
6868
|
+
let raw = cloneUrl.trim();
|
|
6869
|
+
if (!raw) return void 0;
|
|
6870
|
+
if (raw.startsWith("git+")) raw = raw.slice(4);
|
|
6871
|
+
const scp = /^(?:[^@/]+@)?([^/:]+):(?!\/)(.+)$/.exec(raw);
|
|
6872
|
+
if (scp) raw = `ssh://${scp[1]}/${scp[2]}`;
|
|
6873
|
+
let parsed;
|
|
6874
|
+
try {
|
|
6875
|
+
parsed = new URL(raw);
|
|
6876
|
+
} catch {
|
|
6877
|
+
return;
|
|
6878
|
+
}
|
|
6879
|
+
if (!parsed.host) return void 0;
|
|
6880
|
+
const path = parsed.pathname.replace(/\.git\/?$/, "").replace(/\/+$/, "");
|
|
6881
|
+
if (!path || path === "/") return void 0;
|
|
6882
|
+
return `https://${parsed.host}${path}`;
|
|
6883
|
+
}
|
|
6884
|
+
/**
|
|
6885
|
+
* Resolve a link to a skill's `SKILL.md`, or `undefined` when the source is not
|
|
6886
|
+
* reachable from a browser (a `file:` path, or an in-repo skill on a run with no
|
|
6887
|
+
* CI project URL). Built-in skills link to the published tag of this package, so
|
|
6888
|
+
* the link always shows the skill exactly as the run used it.
|
|
6889
|
+
*/
|
|
6890
|
+
function skillSourceUrl(origin, context = {}) {
|
|
6891
|
+
switch (origin.kind) {
|
|
6892
|
+
case "builtin": return blobUrl(PRODUCT_URL, "0.9.7", `skills/${origin.name}/SKILL.md`);
|
|
6893
|
+
case "project":
|
|
6894
|
+
if (!context.projectWebUrl || !context.commitSha) return void 0;
|
|
6895
|
+
return blobUrl(context.projectWebUrl, context.commitSha, `${origin.path}/SKILL.md`);
|
|
6896
|
+
case "npm": return `https://www.npmjs.com/package/${origin.packageName}`;
|
|
6897
|
+
case "file": return;
|
|
6898
|
+
case "git":
|
|
6899
|
+
case "marketplace": {
|
|
6900
|
+
const repoWebUrl = gitRepoWebUrl(origin.url);
|
|
6901
|
+
if (!repoWebUrl) return void 0;
|
|
6902
|
+
const path = origin.path ? `${origin.path}/SKILL.md` : "SKILL.md";
|
|
6903
|
+
return blobUrl(repoWebUrl, origin.ref, path);
|
|
6904
|
+
}
|
|
6905
|
+
}
|
|
6906
|
+
}
|
|
6907
|
+
/**
|
|
6908
|
+
* The name a skill is shown under in the footer. Marketplace skills carry their
|
|
6909
|
+
* full selector (`<marketplace>:<plugin>/<skill>`) because a bare skill name says
|
|
6910
|
+
* nothing about which marketplace and plugin it came from — and that selector is
|
|
6911
|
+
* exactly what a developer would put in `CODE_REVIEW_SKILLS` to use it. Every
|
|
6912
|
+
* other source shows the skill's own name.
|
|
6913
|
+
*/
|
|
6914
|
+
function skillDisplayName(skill) {
|
|
6915
|
+
return skill.origin.kind === "marketplace" ? `${skill.origin.marketplace}:${skill.origin.plugin}/${skill.name}` : skill.name;
|
|
6916
|
+
}
|
|
6917
|
+
/**
|
|
6918
|
+
* Render one skill for the summary footer: a link to its source when one can be
|
|
6919
|
+
* resolved, otherwise the bare name. The name keeps its code span either way so
|
|
6920
|
+
* linked and unlinked skills read the same.
|
|
6921
|
+
*/
|
|
6922
|
+
function formatSkillLink(skill, context = {}) {
|
|
6923
|
+
const name = skillDisplayName(skill);
|
|
6924
|
+
const url = skillSourceUrl(skill.origin, context);
|
|
6925
|
+
return url ? `[\`${name}\`](${url})` : `\`${name}\``;
|
|
6926
|
+
}
|
|
6927
|
+
//#endregion
|
|
6686
6928
|
//#region src/summary-carryover.ts
|
|
6687
6929
|
var FINGERPRINT_MARKER_GLOBAL_RE = new RegExp(FINGERPRINT_MARKER_PATTERN, "gi");
|
|
6688
6930
|
var RISK_RANK = {
|
|
@@ -7058,7 +7300,10 @@ async function run(config, bridges) {
|
|
|
7058
7300
|
const summaryBody = withCarriedOverFindings(parsed.summary, discussions, currentFingerprints);
|
|
7059
7301
|
const result = await platform.upsertSummary(summaryBody, discussions, {
|
|
7060
7302
|
costFooter: [formatUsageLine(usage), formatPerModelUsage(usage)].filter(Boolean).join("\n\n"),
|
|
7061
|
-
skillsFooter: formatSkillsFooter(usage.skills
|
|
7303
|
+
skillsFooter: formatSkillsFooter(usage.skills, {
|
|
7304
|
+
projectWebUrl: config.projectWebUrl,
|
|
7305
|
+
commitSha: refs.head_sha
|
|
7306
|
+
}),
|
|
7062
7307
|
reviewedCommitSha: refs.head_sha,
|
|
7063
7308
|
runId,
|
|
7064
7309
|
sizeNotice: usage.sizeNotice
|
|
@@ -7125,9 +7370,15 @@ function zeroReviewUsage(model, thinkingLevel) {
|
|
|
7125
7370
|
sizeNotice: { sizeSkippedFiles: [] }
|
|
7126
7371
|
};
|
|
7127
7372
|
}
|
|
7128
|
-
|
|
7373
|
+
/**
|
|
7374
|
+
* The summary footer's skills line. Each skill links to its `SKILL.md` source so
|
|
7375
|
+
* a developer reading the review can check what the reviewer was told; skills
|
|
7376
|
+
* with no reachable source (a `file:` path, or an in-repo skill on a run without
|
|
7377
|
+
* CI project coordinates) stay as plain names.
|
|
7378
|
+
*/
|
|
7379
|
+
function formatSkillsFooter(skills, context = {}) {
|
|
7129
7380
|
if (skills.length === 0) return void 0;
|
|
7130
|
-
return `Skills: ${skills.map((s) =>
|
|
7381
|
+
return `Skills: ${skills.map((s) => formatSkillLink(s, context)).join(", ")}`;
|
|
7131
7382
|
}
|
|
7132
7383
|
function formatUsageLine(usage) {
|
|
7133
7384
|
const formatter = new Intl.NumberFormat("en-US");
|
|
@@ -7217,10 +7468,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
7217
7468
|
return;
|
|
7218
7469
|
}
|
|
7219
7470
|
if (argv.includes("--version") || argv.includes("-v")) {
|
|
7220
|
-
console.log("0.9.
|
|
7471
|
+
console.log("0.9.7");
|
|
7221
7472
|
return;
|
|
7222
7473
|
}
|
|
7223
|
-
process.stderr.write(`[code-review] @weareikko/code-review v0.9.
|
|
7474
|
+
process.stderr.write(`[code-review] @weareikko/code-review v0.9.7\n`);
|
|
7224
7475
|
assertNodeVersion();
|
|
7225
7476
|
applyCodeReviewEnvPrefix();
|
|
7226
7477
|
applyDefaultCacheRetention();
|
|
@@ -7241,6 +7492,6 @@ if (isDirectRun()) main().catch((error) => {
|
|
|
7241
7492
|
process.exitCode = 1;
|
|
7242
7493
|
});
|
|
7243
7494
|
//#endregion
|
|
7244
|
-
export {
|
|
7495
|
+
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
7496
|
|
|
7246
|
-
//# sourceMappingURL=cli-
|
|
7497
|
+
//# sourceMappingURL=cli-m4pS6_5q.js.map
|