@jpkme/diff-review 0.1.12 → 0.1.14

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.
Files changed (126) hide show
  1. package/README.md +1 -1
  2. package/bin/diff-review.mjs +72 -53
  3. package/lib/acp-policy.mjs +99 -12
  4. package/lib/acp-policy.test.mjs +56 -3
  5. package/lib/acp-shell.mjs +38 -8
  6. package/lib/acp-shell.test.mjs +32 -1
  7. package/lib/agent-login.mjs +105 -105
  8. package/lib/check-report.mjs +1 -0
  9. package/lib/check-report.test.mjs +1 -0
  10. package/lib/extract-cache.test.mjs +162 -162
  11. package/lib/model-label.mjs +263 -263
  12. package/lib/model-label.test.mjs +77 -77
  13. package/lib/models-cache.mjs +75 -75
  14. package/lib/models-cache.test.mjs +50 -50
  15. package/lib/prompt.mjs +8 -6
  16. package/lib/review-prep.mjs +63 -63
  17. package/package.json +1 -1
  18. package/web/build/client/_app/immutable/assets/{0.CLQiq3Ed.css → 0.BKRG1f36.css} +1 -1
  19. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.br +0 -0
  20. package/web/build/client/_app/immutable/assets/{0.CLQiq3Ed.css.gz → 0.BKRG1f36.css.gz} +0 -0
  21. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css +1 -0
  22. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.br +0 -0
  23. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.gz +0 -0
  24. package/web/build/client/_app/immutable/assets/3.BT54aVKb.css +1 -0
  25. package/web/build/client/_app/immutable/assets/3.BT54aVKb.css.br +0 -0
  26. package/web/build/client/_app/immutable/assets/3.BT54aVKb.css.gz +0 -0
  27. package/web/build/client/_app/immutable/assets/storage.C8XGESwn.css +1 -0
  28. package/web/build/client/_app/immutable/assets/storage.C8XGESwn.css.br +0 -0
  29. package/web/build/client/_app/immutable/assets/storage.C8XGESwn.css.gz +0 -0
  30. package/web/build/client/_app/immutable/chunks/C0hrdiUt.js +2 -0
  31. package/web/build/client/_app/immutable/chunks/C0hrdiUt.js.br +0 -0
  32. package/web/build/client/_app/immutable/chunks/C0hrdiUt.js.gz +0 -0
  33. package/web/build/client/_app/immutable/chunks/CpeIO6nz.js +210 -0
  34. package/web/build/client/_app/immutable/chunks/CpeIO6nz.js.br +0 -0
  35. package/web/build/client/_app/immutable/chunks/CpeIO6nz.js.gz +0 -0
  36. package/web/build/client/_app/immutable/chunks/{CPyWH418.js → FOc6rgIW.js} +1 -1
  37. package/web/build/client/_app/immutable/chunks/FOc6rgIW.js.br +2 -0
  38. package/web/build/client/_app/immutable/chunks/FOc6rgIW.js.gz +0 -0
  39. package/web/build/client/_app/immutable/entry/{app.BiBohpyO.js → app.DcAtSJ3n.js} +2 -2
  40. package/web/build/client/_app/immutable/entry/app.DcAtSJ3n.js.br +0 -0
  41. package/web/build/client/_app/immutable/entry/app.DcAtSJ3n.js.gz +0 -0
  42. package/web/build/client/_app/immutable/entry/start.CO5l0Zbx.js +1 -0
  43. package/web/build/client/_app/immutable/entry/start.CO5l0Zbx.js.br +0 -0
  44. package/web/build/client/_app/immutable/entry/start.CO5l0Zbx.js.gz +0 -0
  45. package/web/build/client/_app/immutable/nodes/{1.BzPXaJJj.js → 1.E-Hs7ura.js} +1 -1
  46. package/web/build/client/_app/immutable/nodes/1.E-Hs7ura.js.br +4 -0
  47. package/web/build/client/_app/immutable/nodes/1.E-Hs7ura.js.gz +0 -0
  48. package/web/build/client/_app/immutable/nodes/2.s1_irV1Q.js +10 -0
  49. package/web/build/client/_app/immutable/nodes/2.s1_irV1Q.js.br +0 -0
  50. package/web/build/client/_app/immutable/nodes/2.s1_irV1Q.js.gz +0 -0
  51. package/web/build/client/_app/immutable/nodes/3.pgwe-_5b.js +171 -0
  52. package/web/build/client/_app/immutable/nodes/3.pgwe-_5b.js.br +0 -0
  53. package/web/build/client/_app/immutable/nodes/3.pgwe-_5b.js.gz +0 -0
  54. package/web/build/client/_app/version.json +1 -1
  55. package/web/build/client/_app/version.json.br +0 -0
  56. package/web/build/client/_app/version.json.gz +0 -0
  57. package/web/build/handler.js +3 -3
  58. package/web/build/index.js +3 -3
  59. package/web/build/server/chunks/{0-BYok4D0n.js → 0-DUStDOsr.js} +3 -3
  60. package/web/build/server/chunks/{0-BYok4D0n.js.map → 0-DUStDOsr.js.map} +1 -1
  61. package/web/build/server/chunks/1-BFhhV5JB.js +7 -0
  62. package/web/build/server/chunks/{1-C7zcB0q0.js.map → 1-BFhhV5JB.js.map} +1 -1
  63. package/web/build/server/chunks/2-BZ_MVqj8.js +7 -0
  64. package/web/build/server/chunks/2-BZ_MVqj8.js.map +1 -0
  65. package/web/build/server/chunks/3-BV-TIUp1.js +7 -0
  66. package/web/build/server/chunks/{3-DAAmG6VX.js.map → 3-BV-TIUp1.js.map} +1 -1
  67. package/web/build/server/chunks/{_server.ts-qYVrMYPB.js → _server.ts-CdFoXw36.js} +4 -4
  68. package/web/build/server/chunks/{_server.ts-qYVrMYPB.js.map → _server.ts-CdFoXw36.js.map} +1 -1
  69. package/web/build/server/chunks/{agent-sessions-DSqGavSn.js → agent-sessions-D0WwtWgR.js} +223 -51
  70. package/web/build/server/chunks/agent-sessions-D0WwtWgR.js.map +1 -0
  71. package/web/build/server/chunks/{git-review-CpO_G8z8.js → git-review-CgcBIbvM.js} +7 -7
  72. package/web/build/server/chunks/{git-review-CpO_G8z8.js.map → git-review-CgcBIbvM.js.map} +1 -1
  73. package/web/build/server/chunks/{handler-Di1wJQuU.js → handler-DbxjtK9M.js} +3 -3
  74. package/web/build/server/chunks/{handler-Di1wJQuU.js.map → handler-DbxjtK9M.js.map} +1 -1
  75. package/web/build/server/chunks/{hooks.server-BLOHobEK.js → hooks.server-Dg6gYUqs.js} +3 -3
  76. package/web/build/server/chunks/{hooks.server-BLOHobEK.js.map → hooks.server-Dg6gYUqs.js.map} +1 -1
  77. package/web/build/server/chunks/{index.js-yLYmrAWy.js → index.js-NNne-njN.js} +876 -879
  78. package/web/build/server/chunks/index.js-NNne-njN.js.map +1 -0
  79. package/web/build/server/chunks/{manifest.js-CXj8IIo3.js → manifest.js-V8ViI5bl.js} +9 -9
  80. package/web/build/server/chunks/{manifest.js-CXj8IIo3.js.map → manifest.js-V8ViI5bl.js.map} +1 -1
  81. package/web/build/server/chunks/{remote-1g4ayag-BiJeU0Pk.js → remote-1g4ayag-CCsG2QXw.js} +4 -4
  82. package/web/build/server/chunks/{remote-1g4ayag-BiJeU0Pk.js.map → remote-1g4ayag-CCsG2QXw.js.map} +1 -1
  83. package/web/build/server/chunks/{remote-wh6t6j-CHanXurm.js → remote-wh6t6j-BCvmX37z.js} +5 -5
  84. package/web/build/server/chunks/{remote-wh6t6j-CHanXurm.js.map → remote-wh6t6j-BCvmX37z.js.map} +1 -1
  85. package/web/build/server/chunks/{validate-BsTKyhsq.js → validate-CAQ31C7k.js} +2 -2
  86. package/web/build/server/chunks/{validate-BsTKyhsq.js.map → validate-CAQ31C7k.js.map} +1 -1
  87. package/web/build/client/_app/immutable/assets/0.CLQiq3Ed.css.br +0 -0
  88. package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css +0 -1
  89. package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css.br +0 -0
  90. package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css.gz +0 -0
  91. package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css +0 -1
  92. package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css.br +0 -0
  93. package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css.gz +0 -0
  94. package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css +0 -1
  95. package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css.br +0 -0
  96. package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css.gz +0 -0
  97. package/web/build/client/_app/immutable/chunks/B5ByWcPE.js +0 -2
  98. package/web/build/client/_app/immutable/chunks/B5ByWcPE.js.br +0 -0
  99. package/web/build/client/_app/immutable/chunks/B5ByWcPE.js.gz +0 -0
  100. package/web/build/client/_app/immutable/chunks/CPyWH418.js.br +0 -0
  101. package/web/build/client/_app/immutable/chunks/CPyWH418.js.gz +0 -0
  102. package/web/build/client/_app/immutable/chunks/NwkMsx0h.js +0 -210
  103. package/web/build/client/_app/immutable/chunks/NwkMsx0h.js.br +0 -0
  104. package/web/build/client/_app/immutable/chunks/NwkMsx0h.js.gz +0 -0
  105. package/web/build/client/_app/immutable/entry/app.BiBohpyO.js.br +0 -0
  106. package/web/build/client/_app/immutable/entry/app.BiBohpyO.js.gz +0 -0
  107. package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js +0 -1
  108. package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js.br +0 -0
  109. package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js.gz +0 -0
  110. package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.br +0 -2
  111. package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.gz +0 -0
  112. package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js +0 -10
  113. package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js.br +0 -0
  114. package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js.gz +0 -0
  115. package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js +0 -171
  116. package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js.br +0 -0
  117. package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js.gz +0 -0
  118. package/web/build/server/chunks/1-C7zcB0q0.js +0 -7
  119. package/web/build/server/chunks/2-v6ejI_0-.js +0 -7
  120. package/web/build/server/chunks/2-v6ejI_0-.js.map +0 -1
  121. package/web/build/server/chunks/3-DAAmG6VX.js +0 -7
  122. package/web/build/server/chunks/agent-sessions-DSqGavSn.js.map +0 -1
  123. package/web/build/server/chunks/index.js-yLYmrAWy.js.map +0 -1
  124. /package/web/build/client/_app/immutable/nodes/{0.Cak4ehiE.js → 0.Dj97JW46.js} +0 -0
  125. /package/web/build/client/_app/immutable/nodes/{0.Cak4ehiE.js.br → 0.Dj97JW46.js.br} +0 -0
  126. /package/web/build/client/_app/immutable/nodes/{0.Cak4ehiE.js.gz → 0.Dj97JW46.js.gz} +0 -0
@@ -1,77 +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
- });
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
+ });
@@ -1,75 +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
- }
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
+ }
@@ -1,50 +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
- });
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/prompt.mjs CHANGED
@@ -91,7 +91,7 @@ const RULES = `- Ids: themes use \`t1\`, \`t2\`, …; findings use \`f1\`, \`f2\
91
91
  function workflowLocal(base, branch, ctx = {}) {
92
92
  if (ctx.hasDiff) {
93
93
  return `Workflow:
94
- 1. Read the three-dot diff from the path given in "Precomputed git context" below. Do not re-run \`git diff\` unless that file is missing.
94
+ 1. The host already computed the three-dot diff and wrote it to disk (see "Precomputed git context"). Read that file with your file tools. Do not run \`git diff\`, \`git merge-base\`, or rebuild the patch yourself unless that file is missing.
95
95
  2. Read enough surrounding code (types, callers, tests) to judge behavior, not only the hunk lines.
96
96
  3. Emit the JSON file. Do not modify the repo.
97
97
  4. Validate it with the command in the output section. If it prints INVALID, follow "How to continue" and re-run until it prints OK.`;
@@ -115,7 +115,7 @@ function workflowUrl(base, branch, ctx = {}) {
115
115
  if (ctx.hasDiff) {
116
116
  return `Workflow:
117
117
  1. If the repo is not local, clone it (or use whatever access you have). Do not modify it.
118
- 2. Read the three-dot diff from the path given in "Precomputed git context" below. Do not re-run \`git diff\` unless that file is missing.
118
+ 2. The host already computed the three-dot diff and wrote it to disk (see "Precomputed git context"). Read that file with your file tools. Do not run \`git diff\`, \`git merge-base\`, or rebuild the patch yourself unless that file is missing.
119
119
  3. Read enough surrounding code (types, callers, tests) to judge behavior, not only the hunk lines.
120
120
  4. Emit the JSON file.
121
121
  5. Validate it with the command in the output section. If it prints INVALID, follow "How to continue" and re-run until it prints OK.`;
@@ -133,7 +133,7 @@ function workflowUrl(base, branch, ctx = {}) {
133
133
  function workflowLocalAcp(ctx = {}) {
134
134
  if (ctx.hasDiff) {
135
135
  return `Workflow:
136
- 1. Read the three-dot diff from the path given in "Precomputed git context" below. Do not re-run \`git diff\` unless that file is missing.
136
+ 1. The host already computed the three-dot diff and wrote it to disk (see "Precomputed git context"). Read that file with your file tools. Do not run \`git diff\`, \`git merge-base\`, or rebuild the patch yourself unless that file is missing.
137
137
  2. Read enough surrounding code (types, callers, tests) to judge behavior, not only the hunk lines.
138
138
  3. Write the JSON with your file Write/Edit tool (never shell redirection). Do not modify other repo files. The host validates when your turn ends — do not run the validate CLI.`;
139
139
  }
@@ -147,7 +147,7 @@ function workflowUrlAcp(ctx = {}) {
147
147
  if (ctx.hasDiff) {
148
148
  return `Workflow:
149
149
  1. If the repo is not local, clone it (or use whatever access you have). Do not modify it.
150
- 2. Read the three-dot diff from the path given in "Precomputed git context" below. Do not re-run \`git diff\` unless that file is missing.
150
+ 2. The host already computed the three-dot diff and wrote it to disk (see "Precomputed git context"). Read that file with your file tools. Do not run \`git diff\`, \`git merge-base\`, or rebuild the patch yourself unless that file is missing.
151
151
  3. Read enough surrounding code (types, callers, tests) to judge behavior, not only the hunk lines.
152
152
  4. Write the JSON with your file Write/Edit tool (never shell redirection). The host validates when your turn ends — do not run the validate CLI.`;
153
153
  }
@@ -197,13 +197,15 @@ export function outputSchemaBlock(meta) {
197
197
  * }} ctx
198
198
  */
199
199
  function formatPrecomputedContext(ctx) {
200
- const lines = ["Precomputed git context (do not rediscover):"];
200
+ const lines = [
201
+ "Precomputed git context (provided by the host — do not rediscover or recompute):",
202
+ ];
201
203
  if (ctx.mergeBase) lines.push(`- Merge-base SHA: \`${ctx.mergeBase}\``);
202
204
  if (ctx.branchSha) lines.push(`- Branch tip SHA: \`${ctx.branchSha}\``);
203
205
  if (ctx.baseSha) lines.push(`- Base tip SHA: \`${ctx.baseSha}\``);
204
206
  if (ctx.diffRelPath) {
205
207
  lines.push(
206
- `- Diff file (relative to repo root): \`${ctx.diffRelPath}\` — read this file; do not re-run \`git diff\` unless it is missing.`
208
+ `- Diff file (relative to repo root): \`${ctx.diffRelPath}\` — Read this file for the full patch. Do not run \`git diff\` / \`git merge-base\` to rebuild it unless the file is missing.`
207
209
  );
208
210
  }
209
211
  const files = Array.isArray(ctx.files) ? ctx.files : [];
@@ -1,63 +1,63 @@
1
- /**
2
- * Precomputa contexto git para el prompt de review (diff cache + lista de archivos).
3
- */
4
- import { loadReviewConfig } from "./config.mjs";
5
- import { extractBranchDiff } from "./extract.mjs";
6
- import { outputRelPath } from "./output-name.mjs";
7
- import { ensureReviewDir, readDiffCache, writeDiffCache } from "./review-dir.mjs";
8
-
9
- /**
10
- * Cachea el three-dot diff y arma las opciones de `buildPrompt` (tips, files, ignores).
11
- * @param {{
12
- * repo: string,
13
- * branch: string,
14
- * base?: string,
15
- * outputFilename: string,
16
- * acp?: boolean,
17
- * }} input
18
- */
19
- export function prepareReviewPromptOpts(input) {
20
- const repo = String(input.repo || "").trim();
21
- const branch = String(input.branch || "").trim();
22
- const base = String(input.base || "develop").trim() || "develop";
23
- const outputFilename = String(input.outputFilename || "").trim();
24
- if (!repo || !branch || !outputFilename) {
25
- throw new Error("prepareReviewPromptOpts requiere repo, branch y outputFilename.");
26
- }
27
-
28
- ensureReviewDir(repo);
29
- writeDiffCache(repo, outputFilename, { branch, base });
30
- const cached = readDiffCache(repo, outputFilename);
31
-
32
- let extracted;
33
- try {
34
- extracted = extractBranchDiff({
35
- repo,
36
- branch,
37
- base,
38
- ...(cached ? { cachedDiff: cached } : {}),
39
- });
40
- } catch {
41
- extracted = extractBranchDiff({ repo, branch, base });
42
- }
43
-
44
- const config = loadReviewConfig(repo);
45
- const diffRelPath = cached
46
- ? outputRelPath(outputFilename).replace(/\.json$/i, ".diff")
47
- : null;
48
-
49
- return {
50
- outputFilename,
51
- acp: Boolean(input.acp),
52
- diffRelPath,
53
- mergeBase: extracted.mergeBase || undefined,
54
- branchSha: extracted.branchSha || undefined,
55
- baseSha: extracted.baseSha || undefined,
56
- files: (extracted.files || []).map((f) => ({
57
- path: f.path,
58
- changeType: f.changeType,
59
- })),
60
- ignored: extracted.context?.ignored || [],
61
- ignoreGlobs: config.ignore,
62
- };
63
- }
1
+ /**
2
+ * Precomputa contexto git para el prompt de review (diff cache + lista de archivos).
3
+ */
4
+ import { loadReviewConfig } from "./config.mjs";
5
+ import { extractBranchDiff } from "./extract.mjs";
6
+ import { outputRelPath } from "./output-name.mjs";
7
+ import { ensureReviewDir, readDiffCache, writeDiffCache } from "./review-dir.mjs";
8
+
9
+ /**
10
+ * Cachea el three-dot diff y arma las opciones de `buildPrompt` (tips, files, ignores).
11
+ * @param {{
12
+ * repo: string,
13
+ * branch: string,
14
+ * base?: string,
15
+ * outputFilename: string,
16
+ * acp?: boolean,
17
+ * }} input
18
+ */
19
+ export function prepareReviewPromptOpts(input) {
20
+ const repo = String(input.repo || "").trim();
21
+ const branch = String(input.branch || "").trim();
22
+ const base = String(input.base || "develop").trim() || "develop";
23
+ const outputFilename = String(input.outputFilename || "").trim();
24
+ if (!repo || !branch || !outputFilename) {
25
+ throw new Error("prepareReviewPromptOpts requiere repo, branch y outputFilename.");
26
+ }
27
+
28
+ ensureReviewDir(repo);
29
+ writeDiffCache(repo, outputFilename, { branch, base });
30
+ const cached = readDiffCache(repo, outputFilename);
31
+
32
+ let extracted;
33
+ try {
34
+ extracted = extractBranchDiff({
35
+ repo,
36
+ branch,
37
+ base,
38
+ ...(cached ? { cachedDiff: cached } : {}),
39
+ });
40
+ } catch {
41
+ extracted = extractBranchDiff({ repo, branch, base });
42
+ }
43
+
44
+ const config = loadReviewConfig(repo);
45
+ const diffRelPath = cached
46
+ ? outputRelPath(outputFilename).replace(/\.json$/i, ".diff")
47
+ : null;
48
+
49
+ return {
50
+ outputFilename,
51
+ acp: Boolean(input.acp),
52
+ diffRelPath,
53
+ mergeBase: extracted.mergeBase || undefined,
54
+ branchSha: extracted.branchSha || undefined,
55
+ baseSha: extracted.baseSha || undefined,
56
+ files: (extracted.files || []).map((f) => ({
57
+ path: f.path,
58
+ changeType: f.changeType,
59
+ })),
60
+ ignored: extracted.context?.ignored || [],
61
+ ignoreGlobs: config.ignore,
62
+ };
63
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jpkme/diff-review",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Local UI to prompt a coding agent for a branch review and browse the result.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +1 @@
1
- :root{--bg:#0b0e14;--bg-elev:#11151d;--bg-card:#151a24;--bg-card-hover:#1a2130;--border:#242b3a;--border-soft:#1c2230;--text:#e6e9ef;--text-dim:#9aa4b8;--text-faint:#5f6b82;--accent:#6ea8fe;--accent-soft:#1c2b45;--ok:#7ee2a8;--ok-bg:#0d2a1c;--warn:#e2c67e;--danger:#ff9a9a;--danger-bg:#2a1414;--danger-border:#4a1e1e;--mono:ui-monospace, "JetBrains Mono", "Fira Code", Consolas, "SFMono-Regular", Menlo, monospace;--sans:-apple-system, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;--btn-primary-bg:#1c2b45;--btn-primary-border:#3a5a8f;--btn-primary-text:#cfe0ff;--kind-feat:#7ee2a8;--kind-fix:#ff9a9a;--kind-refactor:#c9a5ff;--kind-perf:#e2c67e;--kind-test:#6ea8fe;--kind-chore:#9aa4b8;--kind-docs:#7fd6e0;--kind-infra:#f0a868;--sev-high:#ff9a9a;--sev-med:#f08a5a;--sev-low:#c4b86a;--sev-nit:#7f8aa3;--diff-add-bg:#0f2419;--diff-add-fg:#7ee2a8;--diff-del-bg:#2a1418;--diff-del-fg:#ff9a9a;--diff-hunk-bg:#141924;--diff-gutter:#46516a;--diff-mark:#2b3f63;--diff-mark-edge:#6ea8fe;--diff-add-strong:#1f6b3c;--diff-del-strong:#6d2530;--diff-blank-bg:repeating-linear-gradient(135deg, #11151d 0 6px, #151a24 6px 12px);--fix-bg:#0e1a13;--fix-border:#2f6b46;--fix-text:#7ee2a8;--publish-bg:#0e1622;--publish-border:#3a5a8f;--publish-text:#7db8ff;--scrollbar-thumb:#2a3348;--scrollbar-thumb-hover:#3a4660;--selection-bg:#33507a;--header-bg:#0b0e14eb}:root[data-theme=light]{--bg:#fff;--bg-elev:#f6f7f9;--bg-card:#eef0f3;--bg-card-hover:#e4e7ec;--border:#d7dbe2;--border-soft:#e4e7ec;--text:#16181d;--text-dim:#4b5563;--text-faint:#6b7280;--accent:#2f6fe0;--accent-soft:#dce8ff;--ok:#1f9d55;--ok-bg:#e3f8ea;--warn:#a5720a;--danger:#c92a2a;--danger-bg:#fbe4e4;--danger-border:#f3b4b4;--btn-primary-bg:#2f6fe0;--btn-primary-border:#2358b8;--btn-primary-text:#fff;--kind-feat:#1f9d55;--kind-fix:#c92a2a;--kind-refactor:#7c4dd6;--kind-perf:#a5720a;--kind-test:#2f6fe0;--kind-chore:#5b6472;--kind-docs:#0f8fa3;--kind-infra:#c1690f;--sev-high:#c92a2a;--sev-med:#c44d1a;--sev-low:#8b7d2e;--sev-nit:#6b7280;--diff-add-bg:#e6f7ec;--diff-add-fg:#1f9d55;--diff-del-bg:#fbe9e9;--diff-del-fg:#c92a2a;--diff-hunk-bg:#eef1f5;--diff-gutter:#9aa3b2;--diff-mark:#cfe0ff;--diff-mark-edge:#2f6fe0;--diff-add-strong:#a7ebbd;--diff-del-strong:#f9b8b8;--diff-blank-bg:repeating-linear-gradient(135deg, #f6f7f9 0 6px, #eef0f3 6px 12px);--fix-bg:#e6f7ec;--fix-border:#1f9d55;--fix-text:#157a41;--publish-bg:#e8f0fe;--publish-border:#2f6fe0;--publish-text:#1d4fa8;--scrollbar-thumb:#c7cbd4;--scrollbar-thumb-hover:#aab0bb;--selection-bg:#cfe0ff;--header-bg:#ffffffeb}.code .chg{border-radius:2px}.code.del .chg{background:var(--diff-del-strong)}.code.add .chg{background:var(--diff-add-strong)}.hljs-comment,.hljs-quote{color:#6b7689;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-literal,.hljs-doctag,.hljs-formula{color:#c9a5ff}.hljs-string,.hljs-regexp,.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string{color:#a5d6a7}.hljs-number,.hljs-variable,.hljs-template-variable,.hljs-type,.hljs-selector-class,.hljs-selector-attr,.hljs-selector-pseudo{color:#f0a868}.hljs-title,.hljs-section,.hljs-title.class_,.hljs-title.function_{color:#7fd6e0}.hljs-attr,.hljs-property,.hljs-symbol,.hljs-bullet,.hljs-link{color:#6ea8fe}.hljs-built_in,.hljs-class,.hljs-params{color:#e6e9ef}.hljs-meta,.hljs-tag{color:#9aa4b8}:root[data-theme=light] .hljs-comment,:root[data-theme=light] .hljs-quote{color:#6b7280}:root[data-theme=light] .hljs-keyword,:root[data-theme=light] .hljs-selector-tag,:root[data-theme=light] .hljs-literal,:root[data-theme=light] .hljs-doctag,:root[data-theme=light] .hljs-formula{color:#7c4dd6}:root[data-theme=light] .hljs-string,:root[data-theme=light] .hljs-regexp,:root[data-theme=light] .hljs-addition,:root[data-theme=light] .hljs-attribute,:root[data-theme=light] .hljs-meta .hljs-string{color:#1f9d55}:root[data-theme=light] .hljs-number,:root[data-theme=light] .hljs-variable,:root[data-theme=light] .hljs-template-variable,:root[data-theme=light] .hljs-type,:root[data-theme=light] .hljs-selector-class,:root[data-theme=light] .hljs-selector-attr,:root[data-theme=light] .hljs-selector-pseudo{color:#b5650f}:root[data-theme=light] .hljs-title,:root[data-theme=light] .hljs-section,:root[data-theme=light] .hljs-title.class_,:root[data-theme=light] .hljs-title.function_{color:#0f8fa3}:root[data-theme=light] .hljs-attr,:root[data-theme=light] .hljs-property,:root[data-theme=light] .hljs-symbol,:root[data-theme=light] .hljs-bullet,:root[data-theme=light] .hljs-link{color:#2f6fe0}:root[data-theme=light] .hljs-built_in,:root[data-theme=light] .hljs-class,:root[data-theme=light] .hljs-params{color:#16181d}:root[data-theme=light] .hljs-meta,:root[data-theme=light] .hljs-tag{color:#5b6472}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.anchor-flash{animation:1.4s ease-out anchor-flash}@keyframes anchor-flash{0%{box-shadow:0 0 0 2px var(--accent)}to{box-shadow:0 0 0 2px #0000}}*{box-sizing:border-box}html,body{background:var(--bg);color:var(--text);font-family:var(--sans);min-height:100%;margin:0;padding:0}body{-webkit-font-smoothing:antialiased}button,input,select{font:inherit;color:inherit}button{cursor:pointer}::selection{background:var(--selection-bg)}html,body,*{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) transparent}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);background-clip:padding-box;border:2px solid #0000;border-radius:999px}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover);background-clip:padding-box;border:2px solid #0000}::-webkit-scrollbar-corner{background:0 0}.boot.svelte-12qhfyh{color:var(--text-dim);font-family:var(--mono);padding:48px 28px;font-size:13px}.boot.svelte-12qhfyh button:where(.svelte-12qhfyh){background:var(--btn-primary-bg);border:1px solid var(--btn-primary-border);color:var(--btn-primary-text);border-radius:8px;margin-top:12px;padding:8px 14px}
1
+ :root{--bg:#0b0e14;--bg-elev:#11151d;--bg-card:#151a24;--bg-card-hover:#1a2130;--border:#242b3a;--border-soft:#1c2230;--text:#e6e9ef;--text-dim:#9aa4b8;--text-faint:#5f6b82;--accent:#6ea8fe;--accent-soft:#1c2b45;--ok:#7ee2a8;--ok-bg:#0d2a1c;--warn:#e2c67e;--danger:#ff9a9a;--danger-bg:#2a1414;--danger-border:#4a1e1e;--mono:ui-monospace, "JetBrains Mono", "Fira Code", Consolas, "SFMono-Regular", Menlo, monospace;--sans:-apple-system, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;--btn-primary-bg:#1c2b45;--btn-primary-border:#3a5a8f;--btn-primary-text:#cfe0ff;--kind-feat:#7ee2a8;--kind-fix:#ff9a9a;--kind-refactor:#c9a5ff;--kind-perf:#e2c67e;--kind-test:#6ea8fe;--kind-chore:#9aa4b8;--kind-docs:#7fd6e0;--kind-infra:#f0a868;--sev-high:#ff9a9a;--sev-med:#f08a5a;--sev-low:#c4b86a;--sev-nit:#7f8aa3;--diff-add-bg:#0f2419;--diff-add-fg:#7ee2a8;--diff-del-bg:#2a1418;--diff-del-fg:#ff9a9a;--diff-hunk-bg:#141924;--diff-gutter:#46516a;--diff-mark:#2b3f63;--diff-mark-edge:#6ea8fe;--diff-add-strong:#1f6b3c;--diff-del-strong:#6d2530;--diff-blank-bg:repeating-linear-gradient(135deg, #11151d 0 6px, #151a24 6px 12px);--fix-bg:#0e1a13;--fix-border:#2f6b46;--fix-text:#7ee2a8;--publish-bg:#0e1622;--publish-border:#3a5a8f;--publish-text:#7db8ff;--scrollbar-thumb:#2a3348;--scrollbar-thumb-hover:#3a4660;--selection-bg:#33507a;--header-bg:#0b0e14eb}:root[data-theme=light]{--bg:#fff;--bg-elev:#f6f7f9;--bg-card:#eef0f3;--bg-card-hover:#e4e7ec;--border:#d7dbe2;--border-soft:#e4e7ec;--text:#16181d;--text-dim:#4b5563;--text-faint:#6b7280;--accent:#2f6fe0;--accent-soft:#dce8ff;--ok:#1f9d55;--ok-bg:#e3f8ea;--warn:#a5720a;--danger:#c92a2a;--danger-bg:#fbe4e4;--danger-border:#f3b4b4;--btn-primary-bg:#2f6fe0;--btn-primary-border:#2358b8;--btn-primary-text:#fff;--kind-feat:#1f9d55;--kind-fix:#c92a2a;--kind-refactor:#7c4dd6;--kind-perf:#a5720a;--kind-test:#2f6fe0;--kind-chore:#5b6472;--kind-docs:#0f8fa3;--kind-infra:#c1690f;--sev-high:#c92a2a;--sev-med:#c44d1a;--sev-low:#8b7d2e;--sev-nit:#6b7280;--diff-add-bg:#e6f7ec;--diff-add-fg:#1f9d55;--diff-del-bg:#fbe9e9;--diff-del-fg:#c92a2a;--diff-hunk-bg:#eef1f5;--diff-gutter:#9aa3b2;--diff-mark:#cfe0ff;--diff-mark-edge:#2f6fe0;--diff-add-strong:#a7ebbd;--diff-del-strong:#f9b8b8;--diff-blank-bg:repeating-linear-gradient(135deg, #f6f7f9 0 6px, #eef0f3 6px 12px);--fix-bg:#e6f7ec;--fix-border:#1f9d55;--fix-text:#157a41;--publish-bg:#e8f0fe;--publish-border:#2f6fe0;--publish-text:#1d4fa8;--scrollbar-thumb:#c7cbd4;--scrollbar-thumb-hover:#aab0bb;--selection-bg:#cfe0ff;--header-bg:#ffffffeb}.code .chg{border-radius:2px}.code.del .chg{background:var(--diff-del-strong)}.code.add .chg{background:var(--diff-add-strong)}.hljs-comment,.hljs-quote{color:#6b7689;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-literal,.hljs-doctag,.hljs-formula{color:#c9a5ff}.hljs-string,.hljs-regexp,.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string{color:#a5d6a7}.hljs-number,.hljs-variable,.hljs-template-variable,.hljs-type,.hljs-selector-class,.hljs-selector-attr,.hljs-selector-pseudo{color:#f0a868}.hljs-title,.hljs-section,.hljs-title.class_,.hljs-title.function_{color:#7fd6e0}.hljs-attr,.hljs-property,.hljs-symbol,.hljs-bullet,.hljs-link{color:#6ea8fe}.hljs-built_in,.hljs-class,.hljs-params{color:#e6e9ef}.hljs-meta,.hljs-tag{color:#9aa4b8}:root[data-theme=light] .hljs-comment,:root[data-theme=light] .hljs-quote{color:#6b7280}:root[data-theme=light] .hljs-keyword,:root[data-theme=light] .hljs-selector-tag,:root[data-theme=light] .hljs-literal,:root[data-theme=light] .hljs-doctag,:root[data-theme=light] .hljs-formula{color:#7c4dd6}:root[data-theme=light] .hljs-string,:root[data-theme=light] .hljs-regexp,:root[data-theme=light] .hljs-addition,:root[data-theme=light] .hljs-attribute,:root[data-theme=light] .hljs-meta .hljs-string{color:#1f9d55}:root[data-theme=light] .hljs-number,:root[data-theme=light] .hljs-variable,:root[data-theme=light] .hljs-template-variable,:root[data-theme=light] .hljs-type,:root[data-theme=light] .hljs-selector-class,:root[data-theme=light] .hljs-selector-attr,:root[data-theme=light] .hljs-selector-pseudo{color:#b5650f}:root[data-theme=light] .hljs-title,:root[data-theme=light] .hljs-section,:root[data-theme=light] .hljs-title.class_,:root[data-theme=light] .hljs-title.function_{color:#0f8fa3}:root[data-theme=light] .hljs-attr,:root[data-theme=light] .hljs-property,:root[data-theme=light] .hljs-symbol,:root[data-theme=light] .hljs-bullet,:root[data-theme=light] .hljs-link{color:#2f6fe0}:root[data-theme=light] .hljs-built_in,:root[data-theme=light] .hljs-class,:root[data-theme=light] .hljs-params{color:#16181d}:root[data-theme=light] .hljs-meta,:root[data-theme=light] .hljs-tag{color:#5b6472}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.anchor-flash{animation:1.4s ease-out anchor-flash}@keyframes anchor-flash{0%{box-shadow:0 0 0 2px var(--accent)}to{box-shadow:0 0 0 2px #0000}}*{box-sizing:border-box}html,body{background:var(--bg);color:var(--text);font-family:var(--sans);min-height:100%;margin:0;padding:0}body{-webkit-font-smoothing:antialiased}button,input,select{font:inherit;color:inherit}button{cursor:pointer}::selection{background:var(--selection-bg)}html,body,*{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) transparent}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);background-clip:padding-box;border:2px solid #0000;border-radius:999px}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover);background-clip:padding-box;border:2px solid #0000}::-webkit-scrollbar-corner{background:0 0}.boot.svelte-12qhfyh{color:var(--text-dim);font-family:var(--mono);padding:48px 28px;font-size:13px}.boot.svelte-12qhfyh button:where(.svelte-12qhfyh){background:var(--btn-primary-bg);border:1px solid var(--btn-primary-border);color:var(--btn-primary-text);border-radius:0;margin-top:12px;padding:8px 14px}