@jpkme/diff-review 0.1.10 → 0.1.12
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/README.md +38 -15
- package/bin/diff-review.mjs +151 -34
- package/lib/acp-policy.mjs +136 -0
- package/lib/acp-policy.test.mjs +83 -0
- package/lib/acp-shell.mjs +379 -0
- package/lib/acp-shell.test.mjs +127 -0
- package/lib/acp.mjs +368 -0
- package/lib/acp.test.mjs +58 -0
- package/lib/agent-login.mjs +105 -0
- package/lib/agents.mjs +87 -14
- package/lib/agents.test.mjs +4 -6
- package/lib/check-report.mjs +29 -11
- package/lib/extract-cache.test.mjs +162 -0
- package/lib/extract.mjs +197 -1
- package/lib/model-label.mjs +263 -0
- package/lib/model-label.test.mjs +77 -0
- package/lib/models-cache.mjs +75 -0
- package/lib/models-cache.test.mjs +50 -0
- package/lib/output-name.mjs +8 -0
- package/lib/prompt.mjs +168 -22
- package/lib/review-dir.mjs +108 -0
- package/lib/review-meta.mjs +5 -3
- package/lib/review-meta.test.mjs +11 -0
- package/lib/review-prep.mjs +63 -0
- package/lib/validate.mjs +2 -2
- package/lib/wait-output.mjs +46 -23
- package/lib/wait-output.test.mjs +24 -1
- package/package.json +3 -1
- package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css +1 -0
- package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css +1 -0
- package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css +1 -0
- package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css.br +0 -0
- package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{BBWAo_yW.js → B5ByWcPE.js} +2 -2
- package/web/build/client/_app/immutable/chunks/B5ByWcPE.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/B5ByWcPE.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/CPyWH418.js +1 -0
- package/web/build/client/_app/immutable/chunks/CPyWH418.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/CPyWH418.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/D8WOD_eS.js +3 -0
- package/web/build/client/_app/immutable/chunks/D8WOD_eS.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/D8WOD_eS.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/NwkMsx0h.js +210 -0
- package/web/build/client/_app/immutable/chunks/NwkMsx0h.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/NwkMsx0h.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js +1 -0
- package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.BiBohpyO.js +2 -0
- package/web/build/client/_app/immutable/entry/app.BiBohpyO.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.BiBohpyO.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js +1 -0
- package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.Cak4ehiE.js +1 -0
- package/web/build/client/_app/immutable/nodes/0.Cak4ehiE.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/0.Cak4ehiE.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js +1 -0
- package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.br +2 -0
- package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js +10 -0
- package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js +171 -0
- package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js.gz +0 -0
- package/web/build/client/_app/version.json +1 -1
- package/web/build/client/_app/version.json.br +0 -0
- package/web/build/client/_app/version.json.gz +0 -0
- package/web/build/handler.js +3 -3
- package/web/build/index.js +3 -3
- package/web/build/server/chunks/{0-SseDy__O.js → 0-BYok4D0n.js} +2 -2
- package/web/build/server/chunks/{0-SseDy__O.js.map → 0-BYok4D0n.js.map} +1 -1
- package/web/build/server/chunks/1-C7zcB0q0.js +7 -0
- package/web/build/server/chunks/1-C7zcB0q0.js.map +1 -0
- package/web/build/server/chunks/2-v6ejI_0-.js +7 -0
- package/web/build/server/chunks/2-v6ejI_0-.js.map +1 -0
- package/web/build/server/chunks/3-DAAmG6VX.js +7 -0
- package/web/build/server/chunks/3-DAAmG6VX.js.map +1 -0
- package/web/build/server/chunks/_server.ts-qYVrMYPB.js +62 -0
- package/web/build/server/chunks/_server.ts-qYVrMYPB.js.map +1 -0
- package/web/build/server/chunks/agent-sessions-DSqGavSn.js +2688 -0
- package/web/build/server/chunks/agent-sessions-DSqGavSn.js.map +1 -0
- package/web/build/server/chunks/{remote-1g4ayag-B67QMR_2.js → git-review-CpO_G8z8.js} +70 -704
- package/web/build/server/chunks/git-review-CpO_G8z8.js.map +1 -0
- package/web/build/server/chunks/{handler-B5P65tLk.js → handler-Di1wJQuU.js} +3 -3
- package/web/build/server/chunks/{handler-B5P65tLk.js.map → handler-Di1wJQuU.js.map} +1 -1
- package/web/build/server/chunks/hooks.server-BLOHobEK.js +42 -0
- package/web/build/server/chunks/hooks.server-BLOHobEK.js.map +1 -0
- package/web/build/server/chunks/{index.js-CL95e0Gl.js → index.js-yLYmrAWy.js} +3 -5
- package/web/build/server/chunks/index.js-yLYmrAWy.js.map +1 -0
- package/web/build/server/chunks/manifest.js-CXj8IIo3.js +58 -0
- package/web/build/server/chunks/manifest.js-CXj8IIo3.js.map +1 -0
- package/web/build/server/chunks/remote-1g4ayag-BiJeU0Pk.js +144 -0
- package/web/build/server/chunks/remote-1g4ayag-BiJeU0Pk.js.map +1 -0
- package/web/build/server/chunks/remote-wh6t6j-CHanXurm.js +162 -0
- package/web/build/server/chunks/remote-wh6t6j-CHanXurm.js.map +1 -0
- package/web/build/server/chunks/validate-BsTKyhsq.js +1073 -0
- package/web/build/server/chunks/validate-BsTKyhsq.js.map +1 -0
- package/web/build/client/_app/immutable/assets/2.CNFDEnjx.css +0 -1
- package/web/build/client/_app/immutable/assets/2.CNFDEnjx.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.CNFDEnjx.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.PHTPLlZ7.css +0 -1
- package/web/build/client/_app/immutable/assets/3.PHTPLlZ7.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.PHTPLlZ7.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/ThemeToggle.DcT79vPK.css +0 -1
- package/web/build/client/_app/immutable/assets/ThemeToggle.DcT79vPK.css.br +0 -0
- package/web/build/client/_app/immutable/assets/ThemeToggle.DcT79vPK.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/BBWAo_yW.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/BBWAo_yW.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/Dur-vdUf.js +0 -3
- package/web/build/client/_app/immutable/chunks/Dur-vdUf.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/Dur-vdUf.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/QdZF3nkf.js +0 -176
- package/web/build/client/_app/immutable/chunks/QdZF3nkf.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/QdZF3nkf.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/tYeNsyIK.js +0 -1
- package/web/build/client/_app/immutable/chunks/tYeNsyIK.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/tYeNsyIK.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.Dce6uJU3.js +0 -2
- package/web/build/client/_app/immutable/entry/app.Dce6uJU3.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.Dce6uJU3.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.DnBSC38l.js +0 -1
- package/web/build/client/_app/immutable/entry/start.DnBSC38l.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.DnBSC38l.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.s5xmjXoS.js +0 -1
- package/web/build/client/_app/immutable/nodes/0.s5xmjXoS.js.br +0 -1
- package/web/build/client/_app/immutable/nodes/0.s5xmjXoS.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.5Iv4ssDT.js +0 -1
- package/web/build/client/_app/immutable/nodes/1.5Iv4ssDT.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/1.5Iv4ssDT.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.BomzYVBF.js +0 -66
- package/web/build/client/_app/immutable/nodes/2.BomzYVBF.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.BomzYVBF.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.CeUs4lBX.js +0 -170
- package/web/build/client/_app/immutable/nodes/3.CeUs4lBX.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.CeUs4lBX.js.gz +0 -0
- package/web/build/server/chunks/1-2OO2bb7H.js +0 -7
- package/web/build/server/chunks/1-2OO2bb7H.js.map +0 -1
- package/web/build/server/chunks/2-BJvuqKN6.js +0 -7
- package/web/build/server/chunks/2-BJvuqKN6.js.map +0 -1
- package/web/build/server/chunks/3-ZIl1rHef.js +0 -7
- package/web/build/server/chunks/3-ZIl1rHef.js.map +0 -1
- package/web/build/server/chunks/hooks.server-DERFlOe1.js +0 -14
- package/web/build/server/chunks/hooks.server-DERFlOe1.js.map +0 -1
- package/web/build/server/chunks/index.js-CL95e0Gl.js.map +0 -1
- package/web/build/server/chunks/manifest.js-uD_a0079.js +0 -50
- package/web/build/server/chunks/manifest.js-uD_a0079.js.map +0 -1
- package/web/build/server/chunks/remote-1g4ayag-B67QMR_2.js.map +0 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/** Compara nombres de modelo ignorando mayúsculas, espacios y puntuación. */
|
|
2
|
+
export function modelSlug(s) {
|
|
3
|
+
return String(s || "")
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
6
|
+
.replace(/^-+|-+$/g, "");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Parece un id técnico (claude-opus-5-5) más que un nombre legible. */
|
|
10
|
+
function isSlugLike(s) {
|
|
11
|
+
const t = String(s || "").trim();
|
|
12
|
+
if (!t) return true;
|
|
13
|
+
if (/\s/.test(t)) return false;
|
|
14
|
+
if (/[A-Z]/.test(t) && /[a-z]/.test(t) && !t.includes("-")) return false; // "Sonnet", "Auto"
|
|
15
|
+
return /^[a-z0-9][a-z0-9._|-]*$/i.test(t) && (t.includes("-") || t.includes(".") || t.includes("["));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* claude-opus-5-5 → Claude Opus 5.5; gpt-5.6-sol → GPT-5.6 Sol; gemini-3.8-flash → Gemini 3.8 Flash
|
|
20
|
+
* @param {string} id
|
|
21
|
+
*/
|
|
22
|
+
export function humanizeModelId(id) {
|
|
23
|
+
const base = String(id || "")
|
|
24
|
+
.trim()
|
|
25
|
+
.replace(/\[.*\]$/, "");
|
|
26
|
+
if (!base) return "";
|
|
27
|
+
const parts = base.split(/[-_]/).filter(Boolean);
|
|
28
|
+
const out = [];
|
|
29
|
+
for (let i = 0; i < parts.length; i++) {
|
|
30
|
+
const p = parts[i];
|
|
31
|
+
const lower = p.toLowerCase();
|
|
32
|
+
if (lower === "gpt" || lower === "gpt5") {
|
|
33
|
+
out.push(lower === "gpt5" ? "GPT-5" : "GPT");
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (lower === "claude" || lower === "gemini" || lower === "grok" || lower === "composer" || lower === "muse") {
|
|
37
|
+
out.push(lower.charAt(0).toUpperCase() + lower.slice(1));
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (lower === "sonnet" || lower === "opus" || lower === "haiku" || lower === "flash" || lower === "codex") {
|
|
41
|
+
out.push(lower.charAt(0).toUpperCase() + lower.slice(1));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// 5-5 → 5.5 (versión mayor-menor)
|
|
45
|
+
if (/^\d+$/.test(p) && i + 1 < parts.length && /^\d+$/.test(parts[i + 1])) {
|
|
46
|
+
out.push(`${p}.${parts[i + 1]}`);
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (/^\d+(\.\d+)?$/.test(p)) {
|
|
51
|
+
out.push(p);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
out.push(p.charAt(0).toUpperCase() + p.slice(1).toLowerCase());
|
|
55
|
+
}
|
|
56
|
+
return out.join(" ");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* context=300k,effort=medium,fast=false → ["300k", "medium"]
|
|
61
|
+
* @param {string} raw
|
|
62
|
+
* @returns {string[]}
|
|
63
|
+
*/
|
|
64
|
+
export function compactModelMeta(raw) {
|
|
65
|
+
const s = String(raw || "").trim();
|
|
66
|
+
if (!s) return [];
|
|
67
|
+
/** @type {string[]} */
|
|
68
|
+
const badges = [];
|
|
69
|
+
for (const piece of s.split(",")) {
|
|
70
|
+
const part = piece.trim();
|
|
71
|
+
if (!part) continue;
|
|
72
|
+
const eq = part.indexOf("=");
|
|
73
|
+
if (eq < 0) {
|
|
74
|
+
// bare token: "1m", "1M"
|
|
75
|
+
badges.push(part);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const key = part.slice(0, eq).trim().toLowerCase();
|
|
79
|
+
const val = part.slice(eq + 1).trim();
|
|
80
|
+
if (!val || val === "false" || val === "0") continue;
|
|
81
|
+
if (key === "fast" || key === "thinking") {
|
|
82
|
+
if (val === "true" || val === "1") badges.push(key);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (key === "context" || key === "ctx") {
|
|
86
|
+
badges.push(val);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (key === "effort" || key === "reasoning_effort" || key === "reasoning") {
|
|
90
|
+
badges.push(val);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
badges.push(val);
|
|
94
|
+
}
|
|
95
|
+
return badges;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {string} name
|
|
100
|
+
* @param {string} value
|
|
101
|
+
* @returns {{ title: string, badges: string[], label: string, hint?: string }}
|
|
102
|
+
*/
|
|
103
|
+
export function formatModelOption(name, value) {
|
|
104
|
+
const n = String(name ?? "").trim();
|
|
105
|
+
const v = String(value ?? "").trim();
|
|
106
|
+
if (!v && !n) return { title: "", badges: [], label: "" };
|
|
107
|
+
if (/^default(\[\])?$/i.test(v) && n) {
|
|
108
|
+
return { title: n, badges: [], label: n };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const bracket = /^(.+?)\[(.*)\]$/.exec(v);
|
|
112
|
+
const base = bracket ? bracket[1].trim() : v;
|
|
113
|
+
const metaRaw = bracket ? bracket[2].trim() : "";
|
|
114
|
+
let badges = compactModelMeta(metaRaw);
|
|
115
|
+
|
|
116
|
+
let title;
|
|
117
|
+
if (n && !isSlugLike(n)) {
|
|
118
|
+
title = n;
|
|
119
|
+
} else if (n && modelSlug(n) === modelSlug(base)) {
|
|
120
|
+
title = isSlugLike(n) ? humanizeModelId(base || n) : n;
|
|
121
|
+
} else if (n && isSlugLike(n)) {
|
|
122
|
+
title = humanizeModelId(base || n);
|
|
123
|
+
} else if (n) {
|
|
124
|
+
title = n;
|
|
125
|
+
} else {
|
|
126
|
+
title = humanizeModelId(base) || base || v;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Si el título ya menciona el contexto (p. ej. "Sonnet (1M context)"), no repetir el badge "1m".
|
|
130
|
+
if (badges.length && /\(\s*\d+\s*m\b/i.test(title)) {
|
|
131
|
+
badges = badges.filter((b) => !/^\d+m$/i.test(b));
|
|
132
|
+
}
|
|
133
|
+
const titleSlug = modelSlug(title);
|
|
134
|
+
if (badges.length && titleSlug.includes(modelSlug(badges.join("")))) {
|
|
135
|
+
/* keep badges anyway for effort/fast */
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const hint = badges.length ? `[ ${badges.join(", ")} ]` : undefined;
|
|
139
|
+
const label = hint ? `${title} · ${hint}` : title;
|
|
140
|
+
return { title, badges, label, ...(hint ? { hint } : {}) };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Etiqueta compacta (compat / un solo string).
|
|
145
|
+
* @param {string} name
|
|
146
|
+
* @param {string} value
|
|
147
|
+
*/
|
|
148
|
+
export function modelOptionLabel(name, value) {
|
|
149
|
+
return formatModelOption(name, value).label || String(name || value || "").trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Etiqueta del ítem "usar el default del agente".
|
|
154
|
+
* @param {{ value: string, name: string } | undefined} current
|
|
155
|
+
* @param {string} [configuredOptionLabel]
|
|
156
|
+
*/
|
|
157
|
+
export function defaultModelOptionLabel(current, configuredOptionLabel) {
|
|
158
|
+
const name = String(current?.name ?? "").trim();
|
|
159
|
+
const value = String(current?.value ?? "").trim();
|
|
160
|
+
const generic =
|
|
161
|
+
!name ||
|
|
162
|
+
value === "default" ||
|
|
163
|
+
value === "auto" ||
|
|
164
|
+
/^default(\b|\s|\()/i.test(name) ||
|
|
165
|
+
/^auto(\b|\s|\()/i.test(name) ||
|
|
166
|
+
/\(recommended\)/i.test(name);
|
|
167
|
+
|
|
168
|
+
if (generic) {
|
|
169
|
+
const configured = String(configuredOptionLabel ?? "").trim();
|
|
170
|
+
if (configured && configured !== "Por defecto del agente") return configured;
|
|
171
|
+
return "Por defecto del agente";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const { title, hint } = formatModelOption(name, value);
|
|
175
|
+
return hint ? `Por defecto · ${title} · ${hint}` : `Por defecto · ${title}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Orden de la lista: default (`""`) y `auto` primero, resto A→Z por label.
|
|
180
|
+
* @template {{ value: string, label?: string, title?: string }} T
|
|
181
|
+
* @param {T[]} options
|
|
182
|
+
* @returns {T[]}
|
|
183
|
+
*/
|
|
184
|
+
export function sortModelSelectOptions(options) {
|
|
185
|
+
const list = Array.isArray(options) ? [...options] : [];
|
|
186
|
+
|
|
187
|
+
/** @param {{ value: string, label?: string, title?: string }} o */
|
|
188
|
+
const isAuto = (o) => {
|
|
189
|
+
const v = String(o.value ?? "").toLowerCase();
|
|
190
|
+
if (v === "auto" || v.startsWith("auto[")) return true;
|
|
191
|
+
const t = String(o.title || o.label || "").trim();
|
|
192
|
+
return /^auto\b/i.test(t);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const defaults = list.filter((o) => o.value === "");
|
|
196
|
+
const autos = list.filter((o) => o.value !== "" && isAuto(o));
|
|
197
|
+
const middle = list.filter((o) => o.value !== "" && !isAuto(o));
|
|
198
|
+
|
|
199
|
+
/** @param {{ label?: string, title?: string, value: string }} a */
|
|
200
|
+
/** @param {{ label?: string, title?: string, value: string }} b */
|
|
201
|
+
const byLabel = (a, b) =>
|
|
202
|
+
String(a.label || a.title || a.value).localeCompare(String(b.label || b.title || b.value), "es", {
|
|
203
|
+
sensitivity: "base",
|
|
204
|
+
numeric: true,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
autos.sort(byLabel);
|
|
208
|
+
middle.sort(byLabel);
|
|
209
|
+
return [...defaults, ...autos, ...middle];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Misma lista que la UI: default + aliases fijos + modelos ACP.
|
|
214
|
+
* Clack: `label` = título amigable, `hint` = `[ 300k, medium ]`.
|
|
215
|
+
* UI: también `title` + `badges` para chips.
|
|
216
|
+
*
|
|
217
|
+
* @param {{ models: { value: string, name: string }[], currentModel: string }} live
|
|
218
|
+
* @param {{ options: { value: string, label: string, hint?: string }[], initialValue?: string }} fixed
|
|
219
|
+
* @returns {{ options: { value: string, label: string, hint?: string, title?: string, badges?: string[] }[], initialValue: string }}
|
|
220
|
+
*/
|
|
221
|
+
export function buildAcpModelSelectOptions(live, fixed) {
|
|
222
|
+
const fixedOptions = fixed?.options ?? [];
|
|
223
|
+
const fallback = fixedOptions.filter((m) => m.value !== "");
|
|
224
|
+
const configuredLabel = fixedOptions.find((m) => m.value === "")?.label;
|
|
225
|
+
const current = live.models.find((m) => m.value === live.currentModel);
|
|
226
|
+
const seen = new Set(["", "default"]);
|
|
227
|
+
/** @type {{ value: string, label: string, hint?: string, title?: string, badges?: string[] }[]} */
|
|
228
|
+
const options = [
|
|
229
|
+
{
|
|
230
|
+
value: "",
|
|
231
|
+
label: defaultModelOptionLabel(current, configuredLabel),
|
|
232
|
+
title: defaultModelOptionLabel(current, configuredLabel),
|
|
233
|
+
badges: [],
|
|
234
|
+
},
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
for (const m of fallback) {
|
|
238
|
+
if (seen.has(m.value)) continue;
|
|
239
|
+
seen.add(m.value);
|
|
240
|
+
options.push({
|
|
241
|
+
value: m.value,
|
|
242
|
+
label: m.label,
|
|
243
|
+
title: m.label,
|
|
244
|
+
badges: [],
|
|
245
|
+
...(m.hint ? { hint: m.hint } : {}),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const m of live.models) {
|
|
250
|
+
if (seen.has(m.value)) continue;
|
|
251
|
+
seen.add(m.value);
|
|
252
|
+
const formatted = formatModelOption(m.name, m.value);
|
|
253
|
+
options.push({
|
|
254
|
+
value: m.value,
|
|
255
|
+
label: formatted.title,
|
|
256
|
+
title: formatted.title,
|
|
257
|
+
badges: formatted.badges,
|
|
258
|
+
...(formatted.hint ? { hint: formatted.hint } : {}),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { options: sortModelSelectOptions(options), initialValue: "" };
|
|
263
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
buildAcpModelSelectOptions,
|
|
5
|
+
compactModelMeta,
|
|
6
|
+
formatModelOption,
|
|
7
|
+
humanizeModelId,
|
|
8
|
+
modelOptionLabel,
|
|
9
|
+
} from "./model-label.mjs";
|
|
10
|
+
|
|
11
|
+
test("humanizeModelId", () => {
|
|
12
|
+
assert.equal(humanizeModelId("claude-opus-5-5"), "Claude Opus 5.5");
|
|
13
|
+
assert.equal(humanizeModelId("claude-sonnet-5.5"), "Claude Sonnet 5.5");
|
|
14
|
+
assert.equal(humanizeModelId("gemini-3.8-flash"), "Gemini 3.8 Flash");
|
|
15
|
+
assert.equal(humanizeModelId("gpt-5.6-sol"), "GPT 5.6 Sol");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("compactModelMeta", () => {
|
|
19
|
+
assert.deepEqual(compactModelMeta("context=300k,effort=medium,fast=false"), ["300k", "medium"]);
|
|
20
|
+
assert.deepEqual(compactModelMeta("thinking=true,context=300k,effort=high,fast=false"), [
|
|
21
|
+
"thinking",
|
|
22
|
+
"300k",
|
|
23
|
+
"high",
|
|
24
|
+
]);
|
|
25
|
+
assert.deepEqual(compactModelMeta("1m"), ["1m"]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("formatModelOption compacto y amigable", () => {
|
|
29
|
+
const a = formatModelOption("claude-opus-5-5", "claude-opus-5-5[context=300k,effort=medium,fast=false]");
|
|
30
|
+
assert.equal(a.title, "Claude Opus 5.5");
|
|
31
|
+
assert.deepEqual(a.badges, ["300k", "medium"]);
|
|
32
|
+
assert.equal(a.label, "Claude Opus 5.5 · [ 300k, medium ]");
|
|
33
|
+
assert.equal(a.hint, "[ 300k, medium ]");
|
|
34
|
+
|
|
35
|
+
const b = formatModelOption("Claude Sonnet 5.5", "claude-sonnet-5.5");
|
|
36
|
+
assert.equal(b.title, "Claude Sonnet 5.5");
|
|
37
|
+
assert.deepEqual(b.badges, []);
|
|
38
|
+
assert.equal(b.label, "Claude Sonnet 5.5");
|
|
39
|
+
|
|
40
|
+
const c = formatModelOption("Sonnet (1M context)", "sonnet[1m]");
|
|
41
|
+
assert.equal(c.title, "Sonnet (1M context)");
|
|
42
|
+
assert.deepEqual(c.badges, []);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("buildAcpModelSelectOptions fusiona aliases y formatea ACP", () => {
|
|
46
|
+
const { options } = buildAcpModelSelectOptions(
|
|
47
|
+
{
|
|
48
|
+
currentModel: "default",
|
|
49
|
+
models: [
|
|
50
|
+
{ value: "default", name: "Default (recommended)" },
|
|
51
|
+
{
|
|
52
|
+
value: "claude-opus-5-5[context=300k,effort=medium,fast=false]",
|
|
53
|
+
name: "claude-opus-5-5",
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
options: [
|
|
59
|
+
{ value: "", label: "Por defecto (opus)" },
|
|
60
|
+
{ value: "opus", label: "opus", hint: "el más capaz" },
|
|
61
|
+
],
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
assert.equal(options[0].label, "Por defecto (opus)");
|
|
65
|
+
assert.ok(options.some((o) => o.value === "opus"));
|
|
66
|
+
const opusAcp = options.find((o) => o.value.startsWith("claude-opus-5-5"));
|
|
67
|
+
assert.equal(opusAcp?.title, "Claude Opus 5.5");
|
|
68
|
+
assert.deepEqual(opusAcp?.badges, ["300k", "medium"]);
|
|
69
|
+
assert.equal(opusAcp?.hint, "[ 300k, medium ]");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("modelOptionLabel compat", () => {
|
|
73
|
+
assert.equal(
|
|
74
|
+
modelOptionLabel("grok-4.7", "grok-4.7[context=256k,fast=true]"),
|
|
75
|
+
"Grok 4.7 · [ 256k, fast ]"
|
|
76
|
+
);
|
|
77
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caché en disco de las listas de modelos ACP por agente.
|
|
3
|
+
* TTL 24h: los catálogos cambian de a poco (semanas); un día evita spawnear el CLI en cada review
|
|
4
|
+
* y sigue fresco si aparece un modelo nuevo.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
|
|
10
|
+
/** 24 horas. */
|
|
11
|
+
export const MODELS_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
/** @returns {string} */
|
|
14
|
+
export function modelsCachePath(home = homedir()) {
|
|
15
|
+
return join(home, ".diff-review", "models-cache.json");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} agentId
|
|
20
|
+
* @param {{ home?: string, now?: number, ttlMs?: number }} [opts]
|
|
21
|
+
* @returns {{ models: { value: string, name: string }[], currentModel: string, fetchedAt: string } | null}
|
|
22
|
+
*/
|
|
23
|
+
export function readModelsCache(agentId, opts = {}) {
|
|
24
|
+
const id = String(agentId || "").trim();
|
|
25
|
+
if (!id) return null;
|
|
26
|
+
const path = modelsCachePath(opts.home);
|
|
27
|
+
let root;
|
|
28
|
+
try {
|
|
29
|
+
root = JSON.parse(readFileSync(path, "utf8"));
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const entry = root?.agents?.[id];
|
|
34
|
+
if (!entry || !Array.isArray(entry.models)) return null;
|
|
35
|
+
const fetchedAt = Date.parse(String(entry.fetchedAt || ""));
|
|
36
|
+
if (!Number.isFinite(fetchedAt)) return null;
|
|
37
|
+
const ttl = opts.ttlMs ?? MODELS_CACHE_TTL_MS;
|
|
38
|
+
const now = opts.now ?? Date.now();
|
|
39
|
+
if (now - fetchedAt > ttl) return null;
|
|
40
|
+
return {
|
|
41
|
+
models: entry.models
|
|
42
|
+
.filter((m) => m && typeof m.value === "string")
|
|
43
|
+
.map((m) => ({ value: m.value, name: String(m.name ?? m.value) })),
|
|
44
|
+
currentModel: String(entry.currentModel ?? ""),
|
|
45
|
+
fetchedAt: new Date(fetchedAt).toISOString(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} agentId
|
|
51
|
+
* @param {{ models: { value: string, name: string }[], currentModel?: string }} data
|
|
52
|
+
* @param {{ home?: string, now?: number }} [opts]
|
|
53
|
+
*/
|
|
54
|
+
export function writeModelsCache(agentId, data, opts = {}) {
|
|
55
|
+
const id = String(agentId || "").trim();
|
|
56
|
+
if (!id || !Array.isArray(data?.models) || !data.models.length) return;
|
|
57
|
+
const path = modelsCachePath(opts.home);
|
|
58
|
+
let root = { version: 1, agents: {} };
|
|
59
|
+
try {
|
|
60
|
+
if (existsSync(path)) root = { ...root, ...JSON.parse(readFileSync(path, "utf8")) };
|
|
61
|
+
} catch {
|
|
62
|
+
/* archivo nuevo */
|
|
63
|
+
}
|
|
64
|
+
if (!root.agents || typeof root.agents !== "object") root.agents = {};
|
|
65
|
+
root.agents[id] = {
|
|
66
|
+
fetchedAt: new Date(opts.now ?? Date.now()).toISOString(),
|
|
67
|
+
currentModel: String(data.currentModel ?? ""),
|
|
68
|
+
models: data.models.map((m) => ({
|
|
69
|
+
value: String(m.value),
|
|
70
|
+
name: String(m.name ?? m.value),
|
|
71
|
+
})),
|
|
72
|
+
};
|
|
73
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
74
|
+
writeFileSync(path, `${JSON.stringify(root, null, 2)}\n`);
|
|
75
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, readFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import {
|
|
7
|
+
MODELS_CACHE_TTL_MS,
|
|
8
|
+
modelsCachePath,
|
|
9
|
+
readModelsCache,
|
|
10
|
+
writeModelsCache,
|
|
11
|
+
} from "./models-cache.mjs";
|
|
12
|
+
import { sortModelSelectOptions } from "./model-label.mjs";
|
|
13
|
+
|
|
14
|
+
test("sortModelSelectOptions: default y auto primero, resto lexico", () => {
|
|
15
|
+
const sorted = sortModelSelectOptions([
|
|
16
|
+
{ value: "zulu", label: "Zulu" },
|
|
17
|
+
{ value: "auto", label: "Auto" },
|
|
18
|
+
{ value: "alpha", label: "Alpha" },
|
|
19
|
+
{ value: "", label: "Por defecto" },
|
|
20
|
+
{ value: "bravo", label: "Bravo" },
|
|
21
|
+
]);
|
|
22
|
+
assert.deepEqual(
|
|
23
|
+
sorted.map((o) => o.value),
|
|
24
|
+
["", "auto", "alpha", "bravo", "zulu"]
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("models cache roundtrip + TTL", () => {
|
|
29
|
+
const home = mkdtempSync(join(tmpdir(), "diff-review-mc-"));
|
|
30
|
+
const now = Date.parse("2026-01-01T12:00:00.000Z");
|
|
31
|
+
writeModelsCache(
|
|
32
|
+
"claude",
|
|
33
|
+
{
|
|
34
|
+
models: [{ value: "opus", name: "Opus" }],
|
|
35
|
+
currentModel: "default",
|
|
36
|
+
},
|
|
37
|
+
{ home, now }
|
|
38
|
+
);
|
|
39
|
+
assert.ok(readFileSync(modelsCachePath(home), "utf8").includes("opus"));
|
|
40
|
+
|
|
41
|
+
const hit = readModelsCache("claude", { home, now: now + 1000 });
|
|
42
|
+
assert.equal(hit?.currentModel, "default");
|
|
43
|
+
assert.equal(hit?.models[0].value, "opus");
|
|
44
|
+
|
|
45
|
+
const expired = readModelsCache("claude", {
|
|
46
|
+
home,
|
|
47
|
+
now: now + MODELS_CACHE_TTL_MS + 1,
|
|
48
|
+
});
|
|
49
|
+
assert.equal(expired, null);
|
|
50
|
+
});
|
package/lib/output-name.mjs
CHANGED
|
@@ -11,6 +11,14 @@ export const OUTPUT_PREFIX = "diff-rev";
|
|
|
11
11
|
export const OUTPUT_PREFIX_LEGACY = "diff-review-output";
|
|
12
12
|
export const OUTPUT_PREFIX_REPORT = "diff-report";
|
|
13
13
|
|
|
14
|
+
/** Carpeta (en la raíz del repo revisado) donde va el JSON, el diff y la sesión del agente. */
|
|
15
|
+
export const OUTPUT_DIR = ".diff-review";
|
|
16
|
+
|
|
17
|
+
/** Path relativo a la raíz del repo, con `/` (el que se le da al agente): `.diff-review/<nombre>`. */
|
|
18
|
+
export function outputRelPath(filename) {
|
|
19
|
+
return `${OUTPUT_DIR}/${String(filename || "").replace(/^.*[\\/]/, "")}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
/** Nombre legacy fijo. */
|
|
15
23
|
export const OUTPUT_FILENAME_LEGACY = `${OUTPUT_PREFIX_LEGACY}.json`;
|
|
16
24
|
|