@jpkme/diff-review 0.1.16 → 0.1.17

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 (154) hide show
  1. package/README.md +22 -14
  2. package/bin/diff-review.mjs +192 -21
  3. package/lib/acp.mjs +86 -10
  4. package/lib/agent-login.mjs +848 -36
  5. package/lib/agent-login.test.mjs +538 -0
  6. package/lib/agents.mjs +0 -39
  7. package/lib/agents.test.mjs +17 -24
  8. package/lib/extract-cache.test.mjs +162 -162
  9. package/lib/git.mjs +25 -9
  10. package/lib/model-label.mjs +20 -4
  11. package/lib/model-label.test.mjs +15 -2
  12. package/lib/prompt.mjs +12 -10
  13. package/lib/prompt.test.mjs +12 -0
  14. package/lib/review-meta.mjs +11 -3
  15. package/lib/review-meta.test.mjs +19 -2
  16. package/lib/review-prep.mjs +63 -63
  17. package/lib/validate.mjs +1 -1
  18. package/lib/wait-output.test.mjs +1 -1
  19. package/package.json +2 -2
  20. package/web/build/client/_app/immutable/assets/0.B04dzd8P.css +1 -0
  21. package/web/build/client/_app/immutable/assets/0.B04dzd8P.css.br +0 -0
  22. package/web/build/client/_app/immutable/assets/0.B04dzd8P.css.gz +0 -0
  23. package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css +1 -0
  24. package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css.br +0 -0
  25. package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css.gz +0 -0
  26. package/web/build/client/_app/immutable/assets/3.BVJgADrM.css +1 -0
  27. package/web/build/client/_app/immutable/assets/3.BVJgADrM.css.br +0 -0
  28. package/web/build/client/_app/immutable/assets/3.BVJgADrM.css.gz +0 -0
  29. package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css +1 -0
  30. package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css.br +0 -0
  31. package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css.gz +0 -0
  32. package/web/build/client/_app/immutable/chunks/B-2kfLeA.js +1 -0
  33. package/web/build/client/_app/immutable/chunks/B-2kfLeA.js.br +0 -0
  34. package/web/build/client/_app/immutable/chunks/B-2kfLeA.js.gz +0 -0
  35. package/web/build/client/_app/immutable/chunks/{DHO7NTh1.js → B4SiKXUO.js} +2 -2
  36. package/web/build/client/_app/immutable/chunks/B4SiKXUO.js.br +0 -0
  37. package/web/build/client/_app/immutable/chunks/B4SiKXUO.js.gz +0 -0
  38. package/web/build/client/_app/immutable/chunks/{CXhIC34H.js → DA0YnpGR.js} +3 -3
  39. package/web/build/client/_app/immutable/chunks/DA0YnpGR.js.br +0 -0
  40. package/web/build/client/_app/immutable/chunks/DA0YnpGR.js.gz +0 -0
  41. package/web/build/client/_app/immutable/chunks/{cYiFfRa3.js → Dv85Hz5p.js} +1 -1
  42. package/web/build/client/_app/immutable/chunks/Dv85Hz5p.js.br +2 -0
  43. package/web/build/client/_app/immutable/chunks/Dv85Hz5p.js.gz +0 -0
  44. package/web/build/client/_app/immutable/chunks/b-BaDYIc.js +215 -0
  45. package/web/build/client/_app/immutable/chunks/b-BaDYIc.js.br +0 -0
  46. package/web/build/client/_app/immutable/chunks/b-BaDYIc.js.gz +0 -0
  47. package/web/build/client/_app/immutable/entry/app.DPUME9Z_.js +2 -0
  48. package/web/build/client/_app/immutable/entry/app.DPUME9Z_.js.br +0 -0
  49. package/web/build/client/_app/immutable/entry/app.DPUME9Z_.js.gz +0 -0
  50. package/web/build/client/_app/immutable/entry/start.hH87UZsD.js +1 -0
  51. package/web/build/client/_app/immutable/entry/start.hH87UZsD.js.br +0 -0
  52. package/web/build/client/_app/immutable/entry/start.hH87UZsD.js.gz +0 -0
  53. package/web/build/client/_app/immutable/nodes/0.BhHA-Ohy.js +1 -0
  54. package/web/build/client/_app/immutable/nodes/0.BhHA-Ohy.js.br +0 -0
  55. package/web/build/client/_app/immutable/nodes/0.BhHA-Ohy.js.gz +0 -0
  56. package/web/build/client/_app/immutable/nodes/1.jjvGFUxu.js +1 -0
  57. package/web/build/client/_app/immutable/nodes/1.jjvGFUxu.js.br +2 -0
  58. package/web/build/client/_app/immutable/nodes/1.jjvGFUxu.js.gz +0 -0
  59. package/web/build/client/_app/immutable/nodes/2.CJTh6CGA.js +12 -0
  60. package/web/build/client/_app/immutable/nodes/2.CJTh6CGA.js.br +0 -0
  61. package/web/build/client/_app/immutable/nodes/2.CJTh6CGA.js.gz +0 -0
  62. package/web/build/client/_app/immutable/nodes/3.DYAPRc_2.js +340 -0
  63. package/web/build/client/_app/immutable/nodes/3.DYAPRc_2.js.br +0 -0
  64. package/web/build/client/_app/immutable/nodes/3.DYAPRc_2.js.gz +0 -0
  65. package/web/build/client/_app/version.json +1 -1
  66. package/web/build/client/_app/version.json.br +0 -0
  67. package/web/build/client/_app/version.json.gz +0 -0
  68. package/web/build/handler.js +3 -3
  69. package/web/build/index.js +3 -3
  70. package/web/build/server/chunks/0-8NUeB9cB.js +11 -0
  71. package/web/build/server/chunks/{0-D7NSxn1-.js.map → 0-8NUeB9cB.js.map} +1 -1
  72. package/web/build/server/chunks/1-BApN-ILJ.js +7 -0
  73. package/web/build/server/chunks/{1-DboYz8Fw.js.map → 1-BApN-ILJ.js.map} +1 -1
  74. package/web/build/server/chunks/2-Bq7iMeQx.js +7 -0
  75. package/web/build/server/chunks/2-Bq7iMeQx.js.map +1 -0
  76. package/web/build/server/chunks/3-W0cqofyE.js +7 -0
  77. package/web/build/server/chunks/{3-DgJ_LmTL.js.map → 3-W0cqofyE.js.map} +1 -1
  78. package/web/build/server/chunks/_server.ts-Bu1fdGSB.js +123 -0
  79. package/web/build/server/chunks/_server.ts-Bu1fdGSB.js.map +1 -0
  80. package/web/build/server/chunks/{_server.ts-CP5cRLhW.js → _server.ts-BuUNechu.js} +5 -4
  81. package/web/build/server/chunks/_server.ts-BuUNechu.js.map +1 -0
  82. package/web/build/server/chunks/{agent-sessions-2XGojSJ_.js → agent-sessions-BArYLTW6.js} +1023 -135
  83. package/web/build/server/chunks/agent-sessions-BArYLTW6.js.map +1 -0
  84. package/web/build/server/chunks/{git-review-jZ_JJYfv.js → git-review-BJ_JrFNP.js} +54 -21
  85. package/web/build/server/chunks/{git-review-jZ_JJYfv.js.map → git-review-BJ_JrFNP.js.map} +1 -1
  86. package/web/build/server/chunks/{handler-B_8yUXji.js → handler-Df_bJiCq.js} +3 -3
  87. package/web/build/server/chunks/{handler-B_8yUXji.js.map → handler-Df_bJiCq.js.map} +1 -1
  88. package/web/build/server/chunks/{hooks.server-rF_eBCff.js → hooks.server-BEl7rVad.js} +4 -3
  89. package/web/build/server/chunks/{hooks.server-rF_eBCff.js.map → hooks.server-BEl7rVad.js.map} +1 -1
  90. package/web/build/server/chunks/{index.js-CGw8_Q7V.js → index.js-Bb0CZLZ_.js} +4 -4
  91. package/web/build/server/chunks/index.js-Bb0CZLZ_.js.map +1 -0
  92. package/web/build/server/chunks/manifest.js-j442duM7.js +65 -0
  93. package/web/build/server/chunks/manifest.js-j442duM7.js.map +1 -0
  94. package/web/build/server/chunks/{remote-1g4ayag-BMpjk8UD.js → remote-1g4ayag-BxKnZQPn.js} +6 -6
  95. package/web/build/server/chunks/remote-1g4ayag-BxKnZQPn.js.map +1 -0
  96. package/web/build/server/chunks/{remote-wh6t6j-dYLCg8ks.js → remote-wh6t6j-B-AHrGaN.js} +44 -10
  97. package/web/build/server/chunks/remote-wh6t6j-B-AHrGaN.js.map +1 -0
  98. package/web/build/server/chunks/{validate-Da6e71V3.js → validate-CXjTRGnR.js} +34 -17
  99. package/web/build/server/chunks/validate-CXjTRGnR.js.map +1 -0
  100. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css +0 -1
  101. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.br +0 -0
  102. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.gz +0 -0
  103. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css +0 -1
  104. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.br +0 -0
  105. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.gz +0 -0
  106. package/web/build/client/_app/immutable/assets/3.DB-bJJq2.css +0 -1
  107. package/web/build/client/_app/immutable/assets/3.DB-bJJq2.css.br +0 -0
  108. package/web/build/client/_app/immutable/assets/3.DB-bJJq2.css.gz +0 -0
  109. package/web/build/client/_app/immutable/assets/storage.BxR6930R.css +0 -1
  110. package/web/build/client/_app/immutable/assets/storage.BxR6930R.css.br +0 -0
  111. package/web/build/client/_app/immutable/assets/storage.BxR6930R.css.gz +0 -0
  112. package/web/build/client/_app/immutable/chunks/BLMWhW3J.js +0 -1
  113. package/web/build/client/_app/immutable/chunks/BLMWhW3J.js.br +0 -1
  114. package/web/build/client/_app/immutable/chunks/BLMWhW3J.js.gz +0 -0
  115. package/web/build/client/_app/immutable/chunks/CXhIC34H.js.br +0 -0
  116. package/web/build/client/_app/immutable/chunks/CXhIC34H.js.gz +0 -0
  117. package/web/build/client/_app/immutable/chunks/DHO7NTh1.js.br +0 -0
  118. package/web/build/client/_app/immutable/chunks/DHO7NTh1.js.gz +0 -0
  119. package/web/build/client/_app/immutable/chunks/DpxY75GB.js +0 -215
  120. package/web/build/client/_app/immutable/chunks/DpxY75GB.js.br +0 -0
  121. package/web/build/client/_app/immutable/chunks/DpxY75GB.js.gz +0 -0
  122. package/web/build/client/_app/immutable/chunks/cYiFfRa3.js.br +0 -1
  123. package/web/build/client/_app/immutable/chunks/cYiFfRa3.js.gz +0 -0
  124. package/web/build/client/_app/immutable/entry/app.CCv57mvp.js +0 -2
  125. package/web/build/client/_app/immutable/entry/app.CCv57mvp.js.br +0 -0
  126. package/web/build/client/_app/immutable/entry/app.CCv57mvp.js.gz +0 -0
  127. package/web/build/client/_app/immutable/entry/start.B012TSbW.js +0 -1
  128. package/web/build/client/_app/immutable/entry/start.B012TSbW.js.br +0 -1
  129. package/web/build/client/_app/immutable/entry/start.B012TSbW.js.gz +0 -0
  130. package/web/build/client/_app/immutable/nodes/0.slrnBioC.js +0 -1
  131. package/web/build/client/_app/immutable/nodes/0.slrnBioC.js.br +0 -0
  132. package/web/build/client/_app/immutable/nodes/0.slrnBioC.js.gz +0 -0
  133. package/web/build/client/_app/immutable/nodes/1.Bs5JdO4l.js +0 -1
  134. package/web/build/client/_app/immutable/nodes/1.Bs5JdO4l.js.br +0 -0
  135. package/web/build/client/_app/immutable/nodes/1.Bs5JdO4l.js.gz +0 -0
  136. package/web/build/client/_app/immutable/nodes/2.D5ULH3EX.js +0 -10
  137. package/web/build/client/_app/immutable/nodes/2.D5ULH3EX.js.br +0 -0
  138. package/web/build/client/_app/immutable/nodes/2.D5ULH3EX.js.gz +0 -0
  139. package/web/build/client/_app/immutable/nodes/3.hjzQQgyj.js +0 -173
  140. package/web/build/client/_app/immutable/nodes/3.hjzQQgyj.js.br +0 -0
  141. package/web/build/client/_app/immutable/nodes/3.hjzQQgyj.js.gz +0 -0
  142. package/web/build/server/chunks/0-D7NSxn1-.js +0 -11
  143. package/web/build/server/chunks/1-DboYz8Fw.js +0 -7
  144. package/web/build/server/chunks/2-B7LX817Q.js +0 -7
  145. package/web/build/server/chunks/2-B7LX817Q.js.map +0 -1
  146. package/web/build/server/chunks/3-DgJ_LmTL.js +0 -7
  147. package/web/build/server/chunks/_server.ts-CP5cRLhW.js.map +0 -1
  148. package/web/build/server/chunks/agent-sessions-2XGojSJ_.js.map +0 -1
  149. package/web/build/server/chunks/index.js-CGw8_Q7V.js.map +0 -1
  150. package/web/build/server/chunks/manifest.js-CImZFS6G.js +0 -58
  151. package/web/build/server/chunks/manifest.js-CImZFS6G.js.map +0 -1
  152. package/web/build/server/chunks/remote-1g4ayag-BMpjk8UD.js.map +0 -1
  153. package/web/build/server/chunks/remote-wh6t6j-dYLCg8ks.js.map +0 -1
  154. package/web/build/server/chunks/validate-Da6e71V3.js.map +0 -1
@@ -1,10 +1,11 @@
1
- import { p as getRemoteUrl, e as ensureReviewDir, w as writeDiffCache, q as readDiffCache, u as extractBranchDiff, v as loadReviewConfig, x as outputRelPath, m as makeOutputFilename, O as OUTPUT_DIR, y as writeSessionRef, c as companionPaths, d as decodePayload, t as tryResolveGitRoot, f as loadBranchDiff, z as validateAgainst, A as reviewIssues, R as REVIEW_SCHEMA, C as globMatch, D as writeDiffText, E as writeHandoffChat } from './validate-Da6e71V3.js';
2
- import { existsSync, readFileSync, statSync, accessSync, constants, readdirSync, mkdirSync, writeFileSync, copyFileSync, rmSync, realpathSync } from 'node:fs';
3
- import { spawn } from 'node:child_process';
1
+ import { p as getRemoteUrl, e as ensureReviewDir, w as writeDiffCache, q as readDiffCache, u as extractBranchDiff, v as loadReviewConfig, x as outputRelPath, m as makeOutputFilename, O as OUTPUT_DIR, y as writeSessionRef, c as companionPaths, d as decodePayload, t as tryResolveGitRoot, f as loadBranchDiff, z as validateAgainst, A as reviewIssues, R as REVIEW_SCHEMA, C as globMatch, D as writeDiffText, E as writeHandoffChat } from './validate-CXjTRGnR.js';
2
+ import { existsSync, statSync, accessSync, constants, readFileSync, readdirSync, mkdirSync, writeFileSync, copyFileSync, rmSync, realpathSync, unlinkSync } from 'node:fs';
3
+ import { spawnSync, spawn } from 'node:child_process';
4
4
  import { homedir, tmpdir } from 'node:os';
5
5
  import { join, delimiter, isAbsolute, resolve, basename, dirname, normalize } from 'node:path';
6
6
  import { Writable, Readable } from 'node:stream';
7
7
  import * as acp from '@agentclientprotocol/sdk';
8
+ import { randomBytes } from 'node:crypto';
8
9
 
9
10
  //#region ../lib/agents.mjs
10
11
  /**
@@ -40,15 +41,6 @@ function readJson(file) {
40
41
  return null;
41
42
  }
42
43
  }
43
- /** Primer `model = "..."` de nivel raíz de un TOML (antes de cualquier `[sección]`). */
44
- function topLevelTomlModel(text) {
45
- for (const line of String(text).split(/\r?\n/)) {
46
- if (/^\s*\[/.test(line)) break;
47
- const m = line.match(/^\s*model\s*=\s*["']([^"']+)["']/);
48
- if (m) return m[1];
49
- }
50
- return null;
51
- }
52
44
  /** Versiones tipo `2.1.286` de mayor a menor (las que no parsean, al final). */
53
45
  function byVersionDesc(a, b) {
54
46
  const pa = a.split(".").map(Number);
@@ -182,30 +174,6 @@ var AGENTS = [
182
174
  };
183
175
  }
184
176
  },
185
- {
186
- id: "codex",
187
- label: "Codex",
188
- bins: ["codex"],
189
- models: [],
190
- configuredModel: ({ home }) => {
191
- try {
192
- const model = topLevelTomlModel(readFileSync(join(home, ".codex", "config.toml"), "utf8"));
193
- return model ? {
194
- model,
195
- source: "~/.codex/config.toml"
196
- } : null;
197
- } catch {
198
- return null;
199
- }
200
- },
201
- args: ({ model }) => [
202
- "exec",
203
- "--sandbox",
204
- "workspace-write",
205
- ...model ? ["-m", model] : [],
206
- "-"
207
- ]
208
- },
209
177
  {
210
178
  id: "cursor",
211
179
  label: "Cursor CLI",
@@ -266,21 +234,6 @@ var AGENTS = [
266
234
  file: path,
267
235
  args: ["--acp"]
268
236
  })
269
- },
270
- {
271
- id: "gemini",
272
- label: "Gemini CLI",
273
- bins: ["gemini"],
274
- models: [],
275
- configuredModel: ({ home }) => {
276
- const model = readJson(join(home, ".gemini", "settings.json"))?.model;
277
- const name = typeof model === "string" ? model : model?.name;
278
- return typeof name === "string" && name ? {
279
- model: name,
280
- source: "~/.gemini/settings.json"
281
- } : null;
282
- },
283
- args: ({ model }) => [...model ? ["-m", model] : [], "--yolo"]
284
237
  }
285
238
  ];
286
239
  /** @param {string} file @param {NodeJS.Platform} platform */
@@ -1066,48 +1019,139 @@ function permissionOutcome(options, allow, opts = {}) {
1066
1019
  //#endregion
1067
1020
  //#region ../lib/agent-login.mjs
1068
1021
  /**
1069
- * Login de agentes ACP: mensajes claros + abrir una terminal interactiva cuando se pueda.
1022
+ * Login de agentes ACP: clasificación de authMethods, mensajes claros y lanzamiento del CLI de auth.
1023
+ *
1024
+ * Preferencia de launch:
1025
+ * 1. Misma TTY (stdio inherit) si stdin/stdout son TTY — CLI interactiva, exit code real.
1026
+ * 2. Proceso hijo oculto (sin ventana de terminal) si no hay TTY — p. ej. web; OAuth abre el browser.
1027
+ * 3. Terminal externa solo con `external: true` (raro: login que exige TTY interactiva).
1028
+ *
1029
+ * En Windows, `cmd /c start /wait` **no** propaga el exit code del proceso iniciado (siempre 0
1030
+ * hacia Node). El fallback externo usa un archivo sentinel, igual que macOS/Linux.
1031
+ */
1032
+ /**
1033
+ * @typedef {{ bin: string, args: string[], display: string, env?: Record<string, string> }} LoginCommand
1034
+ * @typedef {'agent' | 'terminal' | 'terminal-meta' | 'unknown'} AuthMethodKind
1035
+ * @typedef {'inherit' | 'hidden' | 'external'} LoginLaunchMode
1070
1036
  */
1071
1037
  /**
1072
- * @typedef {{ bin: string, args: string[], display: string }} LoginCommand
1038
+ * Hostname de Copilot (Enterprise). Vacío / `github.com` = cuenta en github.com.
1039
+ * @param {string | null | undefined} host
1040
+ * @returns {string}
1073
1041
  */
1042
+ function normalizeCopilotLoginHost(host) {
1043
+ const h = String(host || "").trim().replace(/^https?:\/\//i, "").replace(/\/.*$/, "");
1044
+ if (!h || /^github\.com$/i.test(h)) return "";
1045
+ return h;
1046
+ }
1074
1047
  /**
1075
- * Comando de login interactivo por agente (null = no hay CLI de login, p. ej. Cursor app).
1048
+ * Agrega `--host` a un comando `copilot login` cuando el usuario eligió Enterprise.
1049
+ * No fuerza nada por env: el host lo elige quien llama (CLI/UI).
1050
+ * @param {LoginCommand} cmd
1051
+ * @param {string | null | undefined} host
1052
+ * @returns {LoginCommand}
1053
+ */
1054
+ function withCopilotLoginHost(cmd, host) {
1055
+ if (!cmd || cmd.bin == null) return cmd;
1056
+ const h = normalizeCopilotLoginHost(host);
1057
+ if (!h) return cmd;
1058
+ const args = Array.isArray(cmd.args) ? [...cmd.args] : [];
1059
+ const hostIdx = args.findIndex((a) => a === "--host");
1060
+ if (hostIdx >= 0) args[hostIdx + 1] = h;
1061
+ else args.push("--host", h);
1062
+ const withoutHost = String(cmd.display || "").replace(/\s+--host\s+\S+/g, "").trim();
1063
+ return {
1064
+ ...cmd,
1065
+ args,
1066
+ display: `${withoutHost} --host ${h}`.trim()
1067
+ };
1068
+ }
1069
+ /**
1070
+ * Comando de login interactivo por agente (fallback si no hay authMethod usable).
1076
1071
  * @param {string} agentId
1072
+ * @param {{ host?: string | null }} [opts] solo Copilot: host Enterprise opcional
1077
1073
  * @returns {LoginCommand | null}
1078
1074
  */
1079
- function loginCommand(agentId) {
1075
+ function loginCommand(agentId, opts = {}) {
1080
1076
  switch (agentId) {
1081
1077
  case "claude": return {
1082
1078
  bin: "claude",
1083
- args: ["/login"],
1084
- display: "claude /login"
1085
- };
1086
- case "copilot": return {
1087
- bin: "gh",
1088
1079
  args: ["auth", "login"],
1089
- display: "gh auth login"
1080
+ display: "claude auth login"
1090
1081
  };
1091
- case "codex": return {
1092
- bin: "codex",
1082
+ case "copilot": return withCopilotLoginHost({
1083
+ bin: "copilot",
1093
1084
  args: ["login"],
1094
- display: "codex login"
1095
- };
1096
- case "gemini": return {
1097
- bin: "gemini",
1098
- args: [],
1099
- display: "gemini"
1085
+ display: "copilot login"
1086
+ }, opts.host);
1087
+ case "cursor": return {
1088
+ bin: "cursor-agent",
1089
+ args: ["login"],
1090
+ display: "cursor-agent login"
1100
1091
  };
1101
1092
  default: return null;
1102
1093
  }
1103
1094
  }
1095
+ /**
1096
+ * Args de `claude auth login …` a partir de un authMethod ACP.
1097
+ * El adaptador anuncia `["--cli", "auth", "login", "--claudeai"]` (o lo mismo tras el path del
1098
+ * adapter en `_meta.terminal-auth.args`); `--cli` solo entiende el wrapper ACP, no `claude`.
1099
+ * @param {any} [method]
1100
+ * @returns {string[]}
1101
+ */
1102
+ function claudeCliLoginArgs(method) {
1103
+ const meta = method?._meta?.["terminal-auth"];
1104
+ const raw = Array.isArray(meta?.args) ? meta.args.map(String) : Array.isArray(method?.args) ? method.args.map(String) : [];
1105
+ const cliIdx = raw.indexOf("--cli");
1106
+ const after = cliIdx >= 0 ? raw.slice(cliIdx + 1) : raw;
1107
+ if (after[0] === "auth") return after;
1108
+ return [
1109
+ "auth",
1110
+ "login",
1111
+ ...after.filter((a) => a.startsWith("--"))
1112
+ ];
1113
+ }
1114
+ /**
1115
+ * Clasifica un authMethod de `initialize` según ACP v1 + extensión Copilot `_meta.terminal-auth`.
1116
+ * @param {any} method
1117
+ * @returns {AuthMethodKind}
1118
+ */
1119
+ function classifyAuthMethod(method) {
1120
+ if (!method || typeof method.id !== "string" || !method.id) return "unknown";
1121
+ if (method._meta != null && method._meta["terminal-auth"] != null) return "terminal-meta";
1122
+ if (method.type === "terminal") return "terminal";
1123
+ if (!method.type || method.type === "agent") return "agent";
1124
+ return "unknown";
1125
+ }
1126
+ /**
1127
+ * @param {any[]} authMethods
1128
+ * @returns {{ agent: any[], terminal: any[], terminalMeta: any[], unknown: any[] }}
1129
+ */
1130
+ function groupAuthMethods(authMethods) {
1131
+ /** @type {{ agent: any[], terminal: any[], terminalMeta: any[], unknown: any[] }} */
1132
+ const groups = {
1133
+ agent: [],
1134
+ terminal: [],
1135
+ terminalMeta: [],
1136
+ unknown: []
1137
+ };
1138
+ for (const method of authMethods || []) {
1139
+ const kind = classifyAuthMethod(method);
1140
+ if (kind === "agent") groups.agent.push(method);
1141
+ else if (kind === "terminal") groups.terminal.push(method);
1142
+ else if (kind === "terminal-meta") groups.terminalMeta.push(method);
1143
+ else groups.unknown.push(method);
1144
+ }
1145
+ return groups;
1146
+ }
1104
1147
  /** @param {string} text */
1105
1148
  function looksLikeAuthFailure(text) {
1106
1149
  const s = String(text || "").toLowerCase();
1107
- return /(not\s+)?auth|login|unauthor|not logged|sign[\s-]?in|credentials|token.*(invalid|expired|missing)|please\s+run/.test(s);
1150
+ return /(not\s+)?auth|login|unauthor|not logged|sign[\s-]?in|credentials|token.*(invalid|expired|missing)|please\s+run|auth_required/.test(s);
1108
1151
  }
1109
1152
  /**
1110
1153
  * Mensaje en español si el agente murió / falló por falta de login.
1154
+ * Sin comandos CLI: el CTA abre el login por el usuario.
1111
1155
  * @param {string} agentId
1112
1156
  * @param {string} agentLabel
1113
1157
  * @param {string} detail
@@ -1115,38 +1159,280 @@ function looksLikeAuthFailure(text) {
1115
1159
  */
1116
1160
  function authFailureMessage$1(agentId, agentLabel, detail) {
1117
1161
  if (!looksLikeAuthFailure(detail)) return null;
1118
- const cmd = loginCommand(agentId);
1119
- if (agentId === "cursor") return `${agentLabel} no está autenticado. Iniciá sesión en la app de Cursor y reintentá.`;
1120
- if (cmd) return `${agentLabel} no está autenticado. Corré \`${cmd.display}\` en una terminal y reintentá.`;
1121
- return `${agentLabel} no está autenticado. Revisá el login del agente y reintentá.`;
1162
+ return `Necesitás iniciar sesión en ${String(agentLabel || "").trim() || "el agente"}.`;
1163
+ }
1164
+ /** Fallo del proceso de login (exit ≠ 0 o cancelado a medias). */
1165
+ function loginIncompleteMessage() {
1166
+ return "No se pudo completar el inicio de sesión. Probá de nuevo.";
1167
+ }
1168
+ /** Estado mientras corre el login (TTY, proceso oculto o terminal externa). */
1169
+ function loginWaitingMessage() {
1170
+ return "Esperando que completes el inicio de sesión…";
1122
1171
  }
1123
1172
  /**
1124
- * Abre una terminal nueva con el login del agente (Windows / macOS / Linux).
1125
- * @param {string} agentId
1173
+ * Método interactivo a abrir: `_meta.terminal-auth` (Copilot) o `type: terminal` (Claude).
1174
+ * Si solo hay métodos `agent` (Cursor), devolvemos null → `resolveLoginCommand` usa el fallback CLI.
1175
+ * @param {any[]} [methods]
1176
+ * @returns {any | null}
1177
+ */
1178
+ function pickLoginAuthMethod(methods) {
1179
+ const grouped = groupAuthMethods(methods || []);
1180
+ return grouped.terminalMeta[0] ?? grouped.terminal[0] ?? null;
1181
+ }
1182
+ /**
1183
+ * ¿Podemos correr el login en la misma terminal (stdio inherit)?
1184
+ * La web / procesos sin TTY usan proceso oculto (o `external: true` → ventana).
1185
+ * @param {{ external?: boolean }} [opts]
1186
+ */
1187
+ function canInheritLoginTty(opts = {}) {
1188
+ if (opts.external) return false;
1189
+ return Boolean(process.stdin?.isTTY && process.stdout?.isTTY);
1190
+ }
1191
+ /**
1192
+ * Modo de launch: inherit (CLI con TTY) → hidden (web / OAuth en browser) → external (forzado).
1193
+ * @param {{ wait?: boolean, external?: boolean }} [opts]
1194
+ * @returns {LoginLaunchMode}
1195
+ */
1196
+ function resolveLoginLaunchMode(opts = {}) {
1197
+ if (Boolean(opts.wait) && canInheritLoginTty(opts)) return "inherit";
1198
+ if (opts.external) return "external";
1199
+ return "hidden";
1200
+ }
1201
+ /**
1202
+ * Arma el comando de login a partir de un authMethod (o fallback por agentId).
1203
+ * @param {{ def: { id: string, acp?: Function }, path: string }} agent
1204
+ * @param {any} [method]
1205
+ * @returns {LoginCommand}
1206
+ */
1207
+ function resolveLoginCommand(agent, method) {
1208
+ const kind = method ? classifyAuthMethod(method) : "unknown";
1209
+ const meta = method?._meta?.["terminal-auth"];
1210
+ if (agent?.def?.id === "claude" && method && (kind === "terminal" || kind === "terminal-meta")) {
1211
+ const args = claudeCliLoginArgs(method);
1212
+ return {
1213
+ bin: typeof agent.path === "string" && agent.path.trim() ? agent.path : "claude",
1214
+ args,
1215
+ display: `claude ${args.join(" ")}`.trim(),
1216
+ ...method?.env && typeof method.env === "object" ? { env: method.env } : {}
1217
+ };
1218
+ }
1219
+ if ((kind === "terminal-meta" || kind === "terminal") && meta && typeof meta === "object") {
1220
+ const command = typeof meta.command === "string" ? meta.command.trim() : "";
1221
+ if (command) {
1222
+ const args = Array.isArray(meta.args) ? meta.args.map(String) : [];
1223
+ return {
1224
+ bin: command,
1225
+ args,
1226
+ display: (typeof meta.label === "string" && meta.label.trim() ? meta.label.trim() : "") || `${command}${args.length ? ` ${args.join(" ")}` : ""}`.trim(),
1227
+ ...method?.env && typeof method.env === "object" ? { env: method.env } : {}
1228
+ };
1229
+ }
1230
+ }
1231
+ if (kind === "terminal" && method && typeof agent?.def?.acp === "function") {
1232
+ const launch = agent.def.acp({ path: agent.path });
1233
+ const extra = Array.isArray(method.args) ? method.args.map(String) : [];
1234
+ const env = {
1235
+ ...launch.env && typeof launch.env === "object" ? launch.env : {},
1236
+ ...method.env && typeof method.env === "object" ? method.env : {}
1237
+ };
1238
+ const args = [...launch.args || [], ...extra];
1239
+ return {
1240
+ bin: launch.file,
1241
+ args,
1242
+ display: `${launch.file}${args.length ? ` ${args.join(" ")}` : ""}`.trim(),
1243
+ ...Object.keys(env).length ? { env } : {}
1244
+ };
1245
+ }
1246
+ const fallback = loginCommand(agent?.def?.id);
1247
+ if (fallback) {
1248
+ if (agent?.def?.id === "claude" && typeof agent.path === "string" && agent.path.trim()) return {
1249
+ ...fallback,
1250
+ bin: agent.path
1251
+ };
1252
+ return fallback;
1253
+ }
1254
+ throw new Error("Este agente no tiene un inicio de sesión conocido.");
1255
+ }
1256
+ /** @param {import('node:child_process').ChildProcess} child */
1257
+ function waitExit(child) {
1258
+ return new Promise((resolve, reject) => {
1259
+ child.once("error", reject);
1260
+ child.once("exit", (code, signal) => resolve(signal ? 1 : code ?? 1));
1261
+ });
1262
+ }
1263
+ /**
1264
+ * Lanza el login en background (proceso oculto; no espera). Compat con UI/CTA simples (web sin sesión).
1265
+ * @param {string | LoginCommand} agentIdOrCmd
1126
1266
  * @returns {{ display: string }}
1127
1267
  */
1128
- function openLoginTerminal(agentId) {
1129
- const cmd = loginCommand(agentId);
1130
- if (!cmd) throw new Error(agentId === "cursor" ? "Cursor se autentica desde la app de escritorio, no desde una terminal." : "Este agente no tiene un comando de login conocido.");
1131
- const platform = process.platform;
1132
- if (platform === "win32") spawn("cmd.exe", [
1133
- "/c",
1134
- "start",
1135
- `${cmd.bin} login`,
1136
- "cmd.exe",
1137
- "/k",
1138
- cmd.bin,
1139
- ...cmd.args
1140
- ], {
1141
- detached: true,
1268
+ function openLoginTerminal(agentIdOrCmd) {
1269
+ const cmd = typeof agentIdOrCmd === "string" ? loginCommand(agentIdOrCmd) : agentIdOrCmd;
1270
+ if (!cmd) throw new Error((typeof agentIdOrCmd === "string" ? agentIdOrCmd : "") === "cursor" ? "Necesitás iniciar sesión en Cursor." : "Este agente no tiene un inicio de sesión conocido.");
1271
+ launchLoginProcess(cmd, {
1272
+ wait: false,
1273
+ mode: "hidden"
1274
+ });
1275
+ return { display: cmd.display };
1276
+ }
1277
+ /**
1278
+ * Lanza el login. Con TTY y `wait: true`, corre en esta terminal (inherit); si no, proceso oculto
1279
+ * (OAuth en browser). Solo con `external: true` abre una ventana de terminal del OS.
1280
+ * @param {LoginCommand} cmd
1281
+ * @param {{ wait?: boolean, cwd?: string, external?: boolean }} [opts]
1282
+ * @returns {Promise<{ display: string, code: number | null, mode: LoginLaunchMode }>}
1283
+ */
1284
+ async function runLoginTerminal(cmd, opts = {}) {
1285
+ const wait = Boolean(opts.wait);
1286
+ const mode = resolveLoginLaunchMode(opts);
1287
+ const code = await launchLoginProcess(cmd, {
1288
+ wait,
1289
+ cwd: opts.cwd,
1290
+ mode
1291
+ });
1292
+ return {
1293
+ display: cmd.display,
1294
+ code: wait ? code : null,
1295
+ mode
1296
+ };
1297
+ }
1298
+ /** @param {string} arg */
1299
+ function quoteBatArg(arg) {
1300
+ const s = String(arg);
1301
+ if (/^[\w./:\\@=-]+$/.test(s)) return s;
1302
+ return `"${s.replace(/"/g, "\"\"")}"`;
1303
+ }
1304
+ /**
1305
+ * Espera a que el sentinel tenga un exit code (macOS/Linux/Windows externo).
1306
+ * @param {string} sentinel
1307
+ * @param {string} display
1308
+ * @returns {Promise<number>}
1309
+ */
1310
+ async function waitForExitSentinel(sentinel, display) {
1311
+ const deadline = Date.now() + 1800 * 1e3;
1312
+ while (Date.now() < deadline) {
1313
+ try {
1314
+ const raw = readFileSync(sentinel, "utf8").trim();
1315
+ if (raw !== "") {
1316
+ const code = Number.parseInt(raw, 10);
1317
+ try {
1318
+ unlinkSync(sentinel);
1319
+ } catch {}
1320
+ return Number.isFinite(code) ? code : 1;
1321
+ }
1322
+ } catch {}
1323
+ await new Promise((r) => setTimeout(r, 400));
1324
+ }
1325
+ try {
1326
+ unlinkSync(sentinel);
1327
+ } catch {}
1328
+ throw new Error(`Timeout esperando el login (${display}).`);
1329
+ }
1330
+ /**
1331
+ * Proceso hijo sin ventana de terminal (web / OAuth). Exit code real vía waitExit.
1332
+ * @param {LoginCommand} cmd
1333
+ * @param {{ wait: boolean, cwd?: string, env: NodeJS.ProcessEnv }} opts
1334
+ * @returns {Promise<number>}
1335
+ */
1336
+ async function launchHiddenLogin(cmd, { wait, cwd, env }) {
1337
+ const child = spawnCommand(cmd.bin, cmd.args, {
1338
+ cwd,
1339
+ env,
1142
1340
  stdio: "ignore",
1143
- windowsHide: false
1144
- }).unref();
1145
- else if (platform === "darwin") spawn("osascript", ["-e", `tell application "Terminal" to do script ${JSON.stringify(`${cmd.bin} ${cmd.args.join(" ")}`.trim())}`], {
1146
- detached: true,
1147
- stdio: "ignore"
1148
- }).unref();
1149
- else {
1341
+ windowsHide: true,
1342
+ ...wait ? {} : { detached: true }
1343
+ });
1344
+ if (!wait) {
1345
+ child.unref();
1346
+ return 0;
1347
+ }
1348
+ return waitExit(child);
1349
+ }
1350
+ /**
1351
+ * @param {LoginCommand} cmd
1352
+ * @param {{ wait: boolean, cwd?: string, mode: LoginLaunchMode }} opts
1353
+ * @returns {Promise<number>}
1354
+ */
1355
+ async function launchLoginProcess(cmd, { wait, cwd, mode }) {
1356
+ const env = {
1357
+ ...process.env,
1358
+ ...cmd.env || {}
1359
+ };
1360
+ if (mode === "inherit") return waitExit(spawnCommand(cmd.bin, cmd.args, {
1361
+ cwd,
1362
+ env,
1363
+ stdio: "inherit"
1364
+ }));
1365
+ if (mode === "hidden") return launchHiddenLogin(cmd, {
1366
+ wait,
1367
+ cwd,
1368
+ env
1369
+ });
1370
+ const platform = process.platform;
1371
+ if (platform === "win32") {
1372
+ if (!wait) {
1373
+ spawn("cmd.exe", [
1374
+ "/c",
1375
+ "start",
1376
+ "diff-review login",
1377
+ "cmd.exe",
1378
+ "/k",
1379
+ cmd.bin,
1380
+ ...cmd.args
1381
+ ], {
1382
+ cwd,
1383
+ env,
1384
+ detached: true,
1385
+ stdio: "ignore",
1386
+ windowsHide: false
1387
+ }).unref();
1388
+ return 0;
1389
+ }
1390
+ const id = randomBytes(8).toString("hex");
1391
+ const sentinel = join(tmpdir(), `diff-review-login-${id}.exit`);
1392
+ const bat = join(tmpdir(), `diff-review-login-${id}.bat`);
1393
+ const line = [quoteBatArg(cmd.bin), ...cmd.args.map(quoteBatArg)].join(" ");
1394
+ const sentPath = sentinel.replace(/"/g, "\"\"");
1395
+ writeFileSync(bat, [
1396
+ `@echo off`,
1397
+ `call ${line}`,
1398
+ `> "${sentPath}" echo %ERRORLEVEL%`,
1399
+ ``
1400
+ ].join("\r\n"));
1401
+ try {
1402
+ writeFileSync(sentinel, "");
1403
+ } catch {}
1404
+ const child = spawn("cmd.exe", [
1405
+ "/c",
1406
+ "start",
1407
+ "/wait",
1408
+ "diff-review login",
1409
+ "cmd.exe",
1410
+ "/c",
1411
+ bat
1412
+ ], {
1413
+ cwd,
1414
+ env,
1415
+ stdio: "ignore",
1416
+ windowsHide: false
1417
+ });
1418
+ try {
1419
+ await waitExit(child);
1420
+ return await waitForExitSentinel(sentinel, cmd.display);
1421
+ } finally {
1422
+ try {
1423
+ unlinkSync(bat);
1424
+ } catch {}
1425
+ }
1426
+ }
1427
+ if (!wait) {
1428
+ if (platform === "darwin") {
1429
+ spawn("osascript", ["-e", `tell application "Terminal" to do script ${JSON.stringify(`${cmd.bin} ${cmd.args.join(" ")}`.trim())}`], {
1430
+ detached: true,
1431
+ stdio: "ignore",
1432
+ env
1433
+ }).unref();
1434
+ return 0;
1435
+ }
1150
1436
  const line = `${cmd.bin} ${cmd.args.join(" ")}`.trim();
1151
1437
  const tried = [
1152
1438
  ["x-terminal-emulator", [
@@ -1174,18 +1460,375 @@ function openLoginTerminal(agentId) {
1174
1460
  `${line}; exec bash`
1175
1461
  ]]
1176
1462
  ];
1177
- let ok = false;
1178
1463
  for (const [bin, args] of tried) try {
1179
1464
  spawn(bin, args, {
1180
1465
  detached: true,
1181
- stdio: "ignore"
1466
+ stdio: "ignore",
1467
+ env,
1468
+ cwd
1182
1469
  }).unref();
1470
+ return 0;
1471
+ } catch {}
1472
+ throw new Error(`No pude abrir una terminal. Corré a mano: ${cmd.display}`);
1473
+ }
1474
+ const sentinel = join(tmpdir(), `diff-review-login-${randomBytes(8).toString("hex")}.exit`);
1475
+ try {
1476
+ writeFileSync(sentinel, "");
1477
+ } catch {}
1478
+ const line = `${[cmd.bin, ...cmd.args].map((a) => JSON.stringify(a)).join(" ")}; echo $? > ${JSON.stringify(sentinel)}`;
1479
+ if (platform === "darwin") spawn("osascript", ["-e", `tell application "Terminal" to do script ${JSON.stringify(line)}`], {
1480
+ stdio: "ignore",
1481
+ env,
1482
+ cwd
1483
+ });
1484
+ else {
1485
+ const tried = [
1486
+ ["gnome-terminal", [
1487
+ "--wait",
1488
+ "--",
1489
+ "bash",
1490
+ "-lc",
1491
+ line
1492
+ ]],
1493
+ ["x-terminal-emulator", [
1494
+ "-e",
1495
+ "bash",
1496
+ "-lc",
1497
+ line
1498
+ ]],
1499
+ ["konsole", [
1500
+ "-e",
1501
+ "bash",
1502
+ "-lc",
1503
+ line
1504
+ ]],
1505
+ ["xterm", [
1506
+ "-e",
1507
+ "bash",
1508
+ "-lc",
1509
+ line
1510
+ ]]
1511
+ ];
1512
+ let ok = false;
1513
+ for (const [bin, args] of tried) try {
1514
+ await waitExit(spawn(bin, args, {
1515
+ stdio: "ignore",
1516
+ env,
1517
+ cwd
1518
+ }));
1183
1519
  ok = true;
1184
1520
  break;
1185
1521
  } catch {}
1186
1522
  if (!ok) throw new Error(`No pude abrir una terminal. Corré a mano: ${cmd.display}`);
1187
1523
  }
1188
- return { display: cmd.display };
1524
+ return waitForExitSentinel(sentinel, cmd.display);
1525
+ }
1526
+ /**
1527
+ * Cuenta activa del agente (para el header de la web).
1528
+ * @param {string} agentId
1529
+ * @param {{ env?: NodeJS.ProcessEnv, run?: typeof spawnSync }} [opts]
1530
+ * @returns {{
1531
+ * agentId: string,
1532
+ * loggedIn: boolean,
1533
+ * accountLabel: string | null,
1534
+ * detail: string | null,
1535
+ * source: 'gh' | 'claude' | 'env' | 'unknown' | null,
1536
+ * warnings: string[],
1537
+ * }}
1538
+ */
1539
+ function agentAccountStatus(agentId, opts = {}) {
1540
+ const id = String(agentId || "").trim();
1541
+ const env = opts.env || process.env;
1542
+ const run = opts.run || spawnSync;
1543
+ /** @type {string[]} */
1544
+ const warnings = [];
1545
+ if (id === "copilot") {
1546
+ const envHits = copilotTokenEnvOverrides(env);
1547
+ if (envHits.length) {
1548
+ warnings.push(`${envHits.join(", ")} en el entorno (pisa otras cuentas).`);
1549
+ return {
1550
+ agentId: id,
1551
+ loggedIn: true,
1552
+ accountLabel: "Token de entorno",
1553
+ detail: envHits.join(", "),
1554
+ source: "env",
1555
+ warnings
1556
+ };
1557
+ }
1558
+ const accounts = listGhAuthAccounts({
1559
+ env,
1560
+ run
1561
+ });
1562
+ const active = accounts.find((a) => a.active) || accounts[0] || null;
1563
+ if (active) return {
1564
+ agentId: id,
1565
+ loggedIn: true,
1566
+ accountLabel: active.user,
1567
+ detail: active.host,
1568
+ source: "gh",
1569
+ warnings
1570
+ };
1571
+ return {
1572
+ agentId: id,
1573
+ loggedIn: false,
1574
+ accountLabel: null,
1575
+ detail: null,
1576
+ source: null,
1577
+ warnings
1578
+ };
1579
+ }
1580
+ if (id === "claude") {
1581
+ const r = run("claude", [
1582
+ "auth",
1583
+ "status",
1584
+ "--json"
1585
+ ], {
1586
+ encoding: "utf8",
1587
+ env,
1588
+ windowsHide: true
1589
+ });
1590
+ if (r.error) return {
1591
+ agentId: id,
1592
+ loggedIn: false,
1593
+ accountLabel: null,
1594
+ detail: null,
1595
+ source: null,
1596
+ warnings: [r.error.message]
1597
+ };
1598
+ try {
1599
+ const parsed = JSON.parse(String(r.stdout || "{}"));
1600
+ const loggedIn = Boolean(parsed?.loggedIn);
1601
+ const email = String(parsed?.email || "").trim() || null;
1602
+ const org = String(parsed?.orgName || "").trim() || null;
1603
+ return {
1604
+ agentId: id,
1605
+ loggedIn,
1606
+ accountLabel: loggedIn ? email || "Claude" : null,
1607
+ detail: loggedIn ? org || String(parsed?.authMethod || "") || null : null,
1608
+ source: loggedIn ? "claude" : null,
1609
+ warnings
1610
+ };
1611
+ } catch {
1612
+ return {
1613
+ agentId: id,
1614
+ loggedIn: false,
1615
+ accountLabel: null,
1616
+ detail: null,
1617
+ source: null,
1618
+ warnings: ["No pude leer el estado de Claude."]
1619
+ };
1620
+ }
1621
+ }
1622
+ if (id === "cursor") return {
1623
+ agentId: id,
1624
+ loggedIn: true,
1625
+ accountLabel: "Cursor",
1626
+ detail: "Sesión del CLI / app",
1627
+ source: "unknown",
1628
+ warnings
1629
+ };
1630
+ return {
1631
+ agentId: id,
1632
+ loggedIn: false,
1633
+ accountLabel: null,
1634
+ detail: null,
1635
+ source: null,
1636
+ warnings
1637
+ };
1638
+ }
1639
+ /**
1640
+ * Env vars que Copilot usa antes que OAuth / gh (impiden un cambio de cuenta real).
1641
+ * @param {NodeJS.ProcessEnv} [env]
1642
+ * @returns {string[]}
1643
+ */
1644
+ function copilotTokenEnvOverrides(env = process.env) {
1645
+ /** @type {string[]} */
1646
+ const hit = [];
1647
+ for (const key of [
1648
+ "COPILOT_GITHUB_TOKEN",
1649
+ "GH_TOKEN",
1650
+ "GITHUB_TOKEN"
1651
+ ]) if (String(env[key] || "").trim()) hit.push(key);
1652
+ return hit;
1653
+ }
1654
+ /**
1655
+ * Cuentas conocidas por `gh auth status --json hosts`.
1656
+ * @param {{ env?: NodeJS.ProcessEnv, run?: typeof spawnSync }} [opts]
1657
+ * @returns {{ host: string, user: string, active: boolean }[]}
1658
+ */
1659
+ function listGhAuthAccounts(opts = {}) {
1660
+ const env = opts.env || process.env;
1661
+ const r = (opts.run || spawnSync)("gh", [
1662
+ "auth",
1663
+ "status",
1664
+ "--json",
1665
+ "hosts"
1666
+ ], {
1667
+ encoding: "utf8",
1668
+ env,
1669
+ windowsHide: true
1670
+ });
1671
+ if (r.error) return [];
1672
+ try {
1673
+ const parsed = JSON.parse(String(r.stdout || "{}"));
1674
+ const hosts = parsed?.hosts && typeof parsed.hosts === "object" ? parsed.hosts : {};
1675
+ /** @type {{ host: string, user: string, active: boolean }[]} */
1676
+ const out = [];
1677
+ for (const [host, accounts] of Object.entries(hosts)) {
1678
+ const list = Array.isArray(accounts) ? accounts : [];
1679
+ for (const acc of list) {
1680
+ const user = String(acc?.login || acc?.user || "").trim();
1681
+ if (!user) continue;
1682
+ out.push({
1683
+ host: String(host),
1684
+ user,
1685
+ active: Boolean(acc?.active)
1686
+ });
1687
+ }
1688
+ }
1689
+ return out;
1690
+ } catch {
1691
+ return [];
1692
+ }
1693
+ }
1694
+ /**
1695
+ * Cierra sesiones de `gh` (Copilot las reusa si no hay OAuth propio).
1696
+ * @param {{ env?: NodeJS.ProcessEnv, run?: typeof spawnSync, host?: string | null }} [opts]
1697
+ * @returns {{ ok: boolean, loggedOut: { host: string, user: string }[], warnings: string[] }}
1698
+ */
1699
+ function logoutGhAuthAccounts(opts = {}) {
1700
+ const env = opts.env || process.env;
1701
+ const run = opts.run || spawnSync;
1702
+ const onlyHost = normalizeCopilotLoginHost(opts.host) || null;
1703
+ const accounts = listGhAuthAccounts({
1704
+ env,
1705
+ run
1706
+ }).filter((a) => onlyHost ? a.host === onlyHost || a.host === `https://${onlyHost}` : true);
1707
+ /** @type {{ host: string, user: string }[]} */
1708
+ const loggedOut = [];
1709
+ /** @type {string[]} */
1710
+ const warnings = [];
1711
+ for (const acc of accounts) {
1712
+ const r = run("gh", [
1713
+ "auth",
1714
+ "logout",
1715
+ "--hostname",
1716
+ acc.host,
1717
+ "--user",
1718
+ acc.user
1719
+ ], {
1720
+ encoding: "utf8",
1721
+ env,
1722
+ windowsHide: true
1723
+ });
1724
+ if (r.error) {
1725
+ warnings.push(`No pude cerrar ${acc.user}@${acc.host}: ${r.error.message}`);
1726
+ continue;
1727
+ }
1728
+ if (r.status !== 0) {
1729
+ const err = String(r.stderr || r.stdout || "").trim() || `exit ${r.status}`;
1730
+ warnings.push(`No pude cerrar ${acc.user}@${acc.host}: ${err}`);
1731
+ continue;
1732
+ }
1733
+ loggedOut.push({
1734
+ host: acc.host,
1735
+ user: acc.user
1736
+ });
1737
+ }
1738
+ const envHits = copilotTokenEnvOverrides(env);
1739
+ if (envHits.length) warnings.push(`Hay ${envHits.join(", ")} en el entorno: Copilot seguirá usando ese token hasta que lo saques.`);
1740
+ return {
1741
+ ok: warnings.length === 0 || loggedOut.length > 0,
1742
+ loggedOut,
1743
+ warnings
1744
+ };
1745
+ }
1746
+ /**
1747
+ * Comando de logout CLI por agente (sin depender de gh).
1748
+ * @param {string} agentId
1749
+ * @returns {LoginCommand | null}
1750
+ */
1751
+ function logoutCommand(agentId) {
1752
+ switch (agentId) {
1753
+ case "claude": return {
1754
+ bin: "claude",
1755
+ args: ["auth", "logout"],
1756
+ display: "claude auth logout"
1757
+ };
1758
+ default: return null;
1759
+ }
1760
+ }
1761
+ /**
1762
+ * Cierra la sesión del agente para poder elegir otra cuenta.
1763
+ * Copilot: también desloguea cuentas de `gh` (si solo hacés logout OAuth, gh sigue ganando).
1764
+ *
1765
+ * @param {string} agentId
1766
+ * @param {{
1767
+ * host?: string | null,
1768
+ * env?: NodeJS.ProcessEnv,
1769
+ * run?: typeof spawnSync,
1770
+ * runTerminal?: typeof runLoginTerminal,
1771
+ * cwd?: string,
1772
+ * }} [opts]
1773
+ * @returns {Promise<{ ok: boolean, steps: string[], warnings: string[] }>}
1774
+ */
1775
+ async function runAgentLogout(agentId, opts = {}) {
1776
+ const id = String(agentId || "").trim();
1777
+ /** @type {string[]} */
1778
+ const steps = [];
1779
+ /** @type {string[]} */
1780
+ const warnings = [];
1781
+ const env = opts.env || process.env;
1782
+ const run = opts.run || spawnSync;
1783
+ const runTerm = opts.runTerminal || runLoginTerminal;
1784
+ if (id === "copilot") {
1785
+ const gh = logoutGhAuthAccounts({
1786
+ env,
1787
+ run,
1788
+ host: opts.host
1789
+ });
1790
+ for (const a of gh.loggedOut) steps.push(`Cerré ${a.user} en ${a.host} (GitHub CLI)`);
1791
+ warnings.push(...gh.warnings);
1792
+ if (!gh.loggedOut.length && !gh.warnings.length) steps.push("No había cuentas de GitHub CLI activas.");
1793
+ return {
1794
+ ok: true,
1795
+ steps,
1796
+ warnings
1797
+ };
1798
+ }
1799
+ const cmd = logoutCommand(id);
1800
+ if (!cmd) return {
1801
+ ok: false,
1802
+ steps,
1803
+ warnings: ["Este agente no tiene cierre de sesión desde la app."]
1804
+ };
1805
+ try {
1806
+ const result = await runTerm(cmd, {
1807
+ wait: true,
1808
+ cwd: opts.cwd
1809
+ });
1810
+ if (result.code !== 0) {
1811
+ warnings.push(`El logout terminó con código ${result.code ?? "?"}.`);
1812
+ return {
1813
+ ok: false,
1814
+ steps,
1815
+ warnings
1816
+ };
1817
+ }
1818
+ steps.push(`Cerré la sesión (${cmd.display}).`);
1819
+ return {
1820
+ ok: true,
1821
+ steps,
1822
+ warnings
1823
+ };
1824
+ } catch (err) {
1825
+ warnings.push(err instanceof Error ? err.message : String(err));
1826
+ return {
1827
+ ok: false,
1828
+ steps,
1829
+ warnings
1830
+ };
1831
+ }
1189
1832
  }
1190
1833
  /** @returns {string} */
1191
1834
  function modelsCachePath(home = homedir()) {
@@ -1261,6 +1904,23 @@ function writeModelsCache(agentId, data, opts = {}) {
1261
1904
  * review y después para las preguntas y los cambios que se le pidan desde la web. Cada pedido de permiso pasa por
1262
1905
  * `decide` (ver `acp-policy.mjs`): no hay nadie mirando una terminal para aprobar.
1263
1906
  */
1907
+ /**
1908
+ * session/new (u otra op) requiere login interactivo (terminal / terminal-meta).
1909
+ * No es un fallo genérico: la UI debe abrir el login y reintentar.
1910
+ */
1911
+ var AcpAuthRequiredError = class extends Error {
1912
+ /**
1913
+ * @param {string} message
1914
+ * @param {{ methods?: any[], agentId?: string, agentLabel?: string }} [info]
1915
+ */
1916
+ constructor(message, info = {}) {
1917
+ super(message);
1918
+ this.name = "AcpAuthRequiredError";
1919
+ this.methods = Array.isArray(info.methods) ? info.methods : [];
1920
+ this.agentId = info.agentId || "";
1921
+ this.agentLabel = info.agentLabel || "";
1922
+ }
1923
+ };
1264
1924
  /** El agente tiene un modo ACP declarado (no todos los agentes lo traen). */
1265
1925
  function supportsAcp(agent) {
1266
1926
  return typeof agent?.def?.acp === "function";
@@ -1291,7 +1951,7 @@ var ACP_START_TIMEOUT_MS = 9e4;
1291
1951
  * @returns {Promise<AcpSession>}
1292
1952
  */
1293
1953
  async function startAcpSession(agent, opts) {
1294
- if (!supportsAcp(agent)) throw new Error(`${agent.def.label} no tiene modo ACP.`);
1954
+ if (!supportsAcp(agent)) throw new Error(`${agent.def.label} no se puede usar desde la web.`);
1295
1955
  const { file, args, env, sessionMeta } = agent.def.acp({ path: agent.path });
1296
1956
  const child = spawnCommand(file, args, {
1297
1957
  cwd: opts.cwd,
@@ -1369,16 +2029,21 @@ async function startAcpSession(agent, opts) {
1369
2029
  readTextFile: false,
1370
2030
  writeTextFile: false
1371
2031
  },
1372
- terminal: false
2032
+ terminal: false,
2033
+ auth: { terminal: true },
2034
+ _meta: { "terminal-auth": true }
1373
2035
  },
1374
2036
  clientInfo: {
1375
2037
  name: "diff-review",
1376
2038
  version: "0"
1377
2039
  }
1378
2040
  });
1379
- /** Métodos donde el agente maneja el login por `authenticate` (no terminal interactiva). */
1380
- const agentAuthMethods = (Array.isArray(init.authMethods) ? init.authMethods : []).filter((m) => m && (!m.type || m.type === "agent") && m.id);
1381
- /** Si session/new falla por auth, pedimos `authenticate` una vez y reintentamos. */
2041
+ const authMethods = Array.isArray(init.authMethods) ? init.authMethods : [];
2042
+ const grouped = groupAuthMethods(authMethods);
2043
+ /** Solo type agent (p. ej. Cursor `cursor_login`). Nunca terminal ni Copilot `_meta.terminal-auth`. */
2044
+ const agentAuthMethods = grouped.agent;
2045
+ const interactiveAuthMethods = [...grouped.terminal, ...grouped.terminalMeta];
2046
+ /** Si session/new falla por auth, pedimos `authenticate` una vez (métodos agent) y reintentamos. */
1382
2047
  const tryAuthenticate = async () => {
1383
2048
  for (const method of agentAuthMethods) try {
1384
2049
  await conn.agent.request(acp.methods.agent.authenticate, { methodId: method.id });
@@ -1386,6 +2051,14 @@ async function startAcpSession(agent, opts) {
1386
2051
  } catch {}
1387
2052
  return false;
1388
2053
  };
2054
+ const authRequired = (msg) => {
2055
+ const label = agent.def.label;
2056
+ return new AcpAuthRequiredError(authFailureMessage$1(agent.def.id, label, msg) || `Necesitás iniciar sesión en ${label}.`, {
2057
+ methods: interactiveAuthMethods.length ? interactiveAuthMethods : authMethods,
2058
+ agentId: agent.def.id,
2059
+ agentLabel: label
2060
+ });
2061
+ };
1389
2062
  const openNewSession = async () => {
1390
2063
  try {
1391
2064
  return await conn.agent.request(acp.methods.agent.session.new, {
@@ -1395,11 +2068,13 @@ async function startAcpSession(agent, opts) {
1395
2068
  });
1396
2069
  } catch (err) {
1397
2070
  const msg = err instanceof Error ? err.message : String(err);
1398
- if (agentAuthMethods.length && looksLikeAuthFailure(msg) && await tryAuthenticate()) return await conn.agent.request(acp.methods.agent.session.new, {
2071
+ if (!looksLikeAuthFailure(msg)) throw err;
2072
+ if (agentAuthMethods.length && await tryAuthenticate()) return await conn.agent.request(acp.methods.agent.session.new, {
1399
2073
  cwd: opts.cwd,
1400
2074
  mcpServers: [],
1401
2075
  ...sessionMeta ? { _meta: sessionMeta } : {}
1402
2076
  });
2077
+ if (interactiveAuthMethods.length || agentAuthMethods.length || looksLikeAuthFailure(msg)) throw authRequired(msg);
1403
2078
  throw err;
1404
2079
  }
1405
2080
  };
@@ -1431,7 +2106,7 @@ async function startAcpSession(agent, opts) {
1431
2106
  if (!wanted || wanted === currentModel) return { currentModel };
1432
2107
  if (!models) return {
1433
2108
  currentModel,
1434
- warning: `El agente no deja elegir el modelo por ACP; usa el suyo.`
2109
+ warning: `El agente no deja elegir el modelo; usa el suyo.`
1435
2110
  };
1436
2111
  if (!models.options.some((o) => o.value === wanted)) return {
1437
2112
  currentModel,
@@ -1451,6 +2126,7 @@ async function startAcpSession(agent, opts) {
1451
2126
  const applied = await setModel(initialWanted);
1452
2127
  if (applied.warning) modelWarning = applied.warning;
1453
2128
  }
2129
+ const supportsLogout = Boolean(init.agentCapabilities?.auth?.logout);
1454
2130
  clearTimeout(timer);
1455
2131
  return {
1456
2132
  sessionId,
@@ -1459,6 +2135,7 @@ async function startAcpSession(agent, opts) {
1459
2135
  get currentModel() {
1460
2136
  return currentModel;
1461
2137
  },
2138
+ supportsLogout,
1462
2139
  ...modelWarning ? { modelWarning } : {},
1463
2140
  setModel,
1464
2141
  prompt: async (text) => {
@@ -1472,6 +2149,11 @@ async function startAcpSession(agent, opts) {
1472
2149
  return { stopReason: String(res.stopReason) };
1473
2150
  },
1474
2151
  cancel: () => conn.agent.notify(acp.methods.agent.session.cancel, { sessionId }),
2152
+ /** ACP `logout` si el agente lo anunció; no garantiza nada sobre credenciales de `gh`. */
2153
+ logout: async () => {
2154
+ if (!supportsLogout) throw new Error(`${agent.def.label} no admite cerrar sesión desde acá.`);
2155
+ await conn.agent.request(acp.methods.agent.logout, {});
2156
+ },
1475
2157
  close,
1476
2158
  closed: conn.closed,
1477
2159
  stderrTail: () => stderr
@@ -1479,13 +2161,24 @@ async function startAcpSession(agent, opts) {
1479
2161
  } catch (err) {
1480
2162
  clearTimeout(timer);
1481
2163
  close();
2164
+ if (err instanceof AcpAuthRequiredError) throw err;
1482
2165
  const tail = stderr.trim().split(/\r?\n/).slice(-8).join("\n");
1483
2166
  if (timedOut) {
1484
2167
  const msg = `Timeout (${Math.round(timeoutMs / 1e3)}s) al lanzar ${agent.def.label}. ¿Está autenticado el CLI?`;
1485
- throw new Error(tail ? `${msg}\n${tail}` : msg);
2168
+ const full = tail ? `${msg}\n${tail}` : msg;
2169
+ if (looksLikeAuthFailure(full)) throw new AcpAuthRequiredError(authFailureMessage$1(agent.def.id, agent.def.label, full) || full, {
2170
+ agentId: agent.def.id,
2171
+ agentLabel: agent.def.label
2172
+ });
2173
+ throw new Error(full);
1486
2174
  }
1487
2175
  const reason = err instanceof Error ? err.message : String(err);
1488
- throw new Error(tail ? `${reason}\n${tail}` : reason);
2176
+ const full = tail ? `${reason}\n${tail}` : reason;
2177
+ if (looksLikeAuthFailure(full)) throw new AcpAuthRequiredError(authFailureMessage$1(agent.def.id, agent.def.label, full) || full, {
2178
+ agentId: agent.def.id,
2179
+ agentLabel: agent.def.label
2180
+ });
2181
+ throw new Error(full);
1489
2182
  }
1490
2183
  }
1491
2184
  /**
@@ -1701,12 +2394,15 @@ function formatCheckReport(file, command, result) {
1701
2394
  /**
1702
2395
  * `agentSession` = la sesión ACP que generó la review, para seguir conversando con el agente desde la web.
1703
2396
  * @param {string} filePath
1704
- * @param {{ agent?: string, model?: string, agentSession?: { agentId: string, sessionId: string, outputFilename: string } }} provenance
2397
+ * `modelLabel` = nombre legible que el agente da al modelo ("Haiku 5.5"): `model` puede ser un alias
2398
+ * (`haiku`) que más adelante apunte a otra versión.
2399
+ * @param {{ agent?: string, model?: string, modelLabel?: string, agentSession?: { agentId: string, sessionId: string, outputFilename: string } }} provenance
1705
2400
  * @returns {boolean}
1706
2401
  */
1707
- function stampReviewProvenance(filePath, { agent, model, agentSession } = {}) {
2402
+ function stampReviewProvenance(filePath, { agent, model, modelLabel, agentSession } = {}) {
1708
2403
  const agentName = String(agent || "").trim();
1709
2404
  const modelId = String(model || "").trim();
2405
+ const label = String(modelLabel || "").trim();
1710
2406
  if (!agentName && !modelId && !agentSession) return false;
1711
2407
  let payload;
1712
2408
  try {
@@ -1718,7 +2414,11 @@ function stampReviewProvenance(filePath, { agent, model, agentSession } = {}) {
1718
2414
  const current = payload.meta;
1719
2415
  const meta = current && typeof current === "object" && !Array.isArray(current) ? { ...current } : {};
1720
2416
  if (agentName) meta.agent = agentName;
1721
- if (modelId) meta.model = modelId;
2417
+ if (modelId) {
2418
+ meta.model = modelId;
2419
+ if (label && label !== modelId) meta.modelLabel = label;
2420
+ else delete meta.modelLabel;
2421
+ }
1722
2422
  if (agentSession) meta.agentSession = { ...agentSession };
1723
2423
  payload.meta = meta;
1724
2424
  writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
@@ -1747,7 +2447,7 @@ var OUTPUT_SCHEMA_BLOCK = `{
1747
2447
  "id": "t1",
1748
2448
  "kind": "feat|fix|refactor|perf|test|chore|docs|infra",
1749
2449
  "title": "string, aim for ≤72 chars, imperative, no kind prefix",
1750
- "intent": "string"
2450
+ "intent": "string: why this theme exists + what it changes for the reader (not a restatement of the title)"
1751
2451
  }
1752
2452
  ],
1753
2453
  "blocks": [
@@ -1760,8 +2460,8 @@ var OUTPUT_SCHEMA_BLOCK = `{
1760
2460
  "start": 40,
1761
2461
  "end": 58,
1762
2462
  "op": "add|mod|del|rename",
1763
- "what": "string, aim for ≤120 chars: concrete change + effect",
1764
- "why": "string, aim for ≤120 chars",
2463
+ "what": "string: concrete change + effect (aim ≤120 chars; longer OK if needed for clarity)",
2464
+ "why": "string: motivation / why this change belongs in the branch (aim ≤120 chars)",
1765
2465
  "source": "code|commit|pr|issue|inferred"
1766
2466
  }
1767
2467
  ],
@@ -1775,8 +2475,8 @@ var OUTPUT_SCHEMA_BLOCK = `{
1775
2475
  "file": "relative/path.ext",
1776
2476
  "line": 87,
1777
2477
  "block": "b1",
1778
- "what": "string, aim for ≤120 chars: concrete cause + observable consequence (what fails / risk / cost)",
1779
- "fix": "string, aim for ≤120 chars: suggested change + what the reader gains or avoids if applied"
2478
+ "what": "string: cause + why it is a problem + observable consequence (aim ≤120 chars; prefer clarity over brevity)",
2479
+ "fix": "string: suggested change + why that change resolves the problem / what it avoids (aim ≤120 chars)"
1780
2480
  }
1781
2481
  ],
1782
2482
  "skipped": [
@@ -1790,13 +2490,15 @@ var RULES = `- Ids: themes use \`t1\`, \`t2\`, …; findings use \`f1\`, \`f2\`,
1790
2490
  - Line anchors: "lines"/"start"/"end"/"line" MUST match the chosen "side" ("new" = post-image / right side of @@; "old" = pre-image, typically deletions). One block per coherent change span, not one per line. Point findings at that block; leave "block" empty only if cross-cutting.
1791
2491
  - Do NOT report: pre-existing code outside the diff; linter/formatter noise; naming/style prefs with no real consequence; intentional branch behavior; silenced lint rules; "missing docs/coverage" without a concrete broken scenario.
1792
2492
  - Quality findings must name a concrete cost, not a vague feeling.
1793
- - Finding "what": state the concrete cause and the observable consequence (what fails, the risk, or the cost) — not only a symptom.
1794
- - Finding "fix": state the suggested change and what the reader gains or avoids if they apply it.
2493
+ - Theme "intent": explain why the theme exists and what it changes for the reader — not a paraphrase of "title". One short sentence is too little; aim for 1–3 sentences with cause and scope.
2494
+ - Top-level "intent": 2–4 sentences on what the branch solves and why it matters.
2495
+ - Finding "what": cover (1) the concrete cause in the diff, (2) why it is a problem (failure mode, risk, or cost), and (3) the observable consequence. Do not stop at a symptom label.
2496
+ - Finding "fix": cover (1) the suggested change and (2) why that change resolves the problem / what the reader gains or avoids. Vague advice ("mejorar el manejo") is not enough.
1795
2497
  - Signal over volume: prefer fewer high-signal findings. Order findings by severity (high, then nit). Use "nit" sparingly.
1796
2498
  - "blocking": true ONLY if the branch should not merge as-is. Use only with class "risk".
1797
- - When several read-only lookups are independent (files, searches), batch them in the same turn if your tools allow parallel calls.
2499
+ - Parallelize: when several read-only lookups are independent (Read, Grep/search, Glob, etc.), issue them in the same turn if your tools allow parallel calls. Do not serialize independent work — finish faster by batching.
1798
2500
  - Write human-readable strings (intent, title, what, why, fix, notes) in Spanish. Keep enums/ids/paths/JSON keys exactly as in the schema.
1799
- - Length is a soft target, not a check: aim for title ≤72 characters and what/why/fix ≤120 characters. Prefer clarity over shortening. Longer text is still valid. Do not rewrite the file only to shorten it.
2501
+ - Length is a soft target, not a check: aim for title ≤72 characters and what/why/fix ≤120 characters when possible. Prefer clarity over shortening — thin one-liners that omit cause, impact, or why-the-fix-works are worse than a slightly longer explanation. Longer text is still valid. Do not rewrite the file only to shorten it.
1800
2502
  - If the output JSON already exists and findings have \`resolution\` ({ kind: ignored|fix|corrected, detail?: string }), preserve those fields on rewrite (coverage / edit / regenerate). Keep existing finding ids when possible; do not drop or reshuffle resolutions.`;
1801
2503
  /** @param {{ hasDiff?: boolean }} [ctx] */
1802
2504
  function workflowLocal(base, branch, ctx = {}) {
@@ -1946,7 +2648,7 @@ Write the result to \`${outputPath}\` (relative to the repo root; use forward sl
1946
2648
 
1947
2649
  ${outputSchemaBlock(meta)}
1948
2650
 
1949
- Include "meta" with the source/repo/branch/base above (and origin "remoteUrl" when local). "agent" and "model" are required: set "agent" to the product you are (Claude Code, Cursor, Codex, Copilot, Gemini, …) and "model" to the exact model id you are running. Do not leave placeholders or omit them; validation fails without both. "generatedAt" is optional (ISO-8601 now).
2651
+ Include "meta" with the source/repo/branch/base above (and origin "remoteUrl" when local). "agent" and "model" are required: set "agent" to the product you are (Claude Code, Cursor, Copilot, …) and "model" to the exact model id you are running. Do not leave placeholders or omit them; validation fails without both. "generatedAt" is optional (ISO-8601 now).
1950
2652
 
1951
2653
  Do NOT include diffs or a "files" array. The tool loads the companion \`.diff\` (or computes with git if missing) when opening the report. Do not invent hunks or paste patches into the JSON. If you are rewriting an existing review JSON, copy through any per-finding \`resolution\` already present.
1952
2654
 
@@ -2114,7 +2816,7 @@ function agentModels(agentId, cwd, opts) {
2114
2816
  function findAgent(agentId) {
2115
2817
  const agent = detectAgents().find((a) => a.def.id === agentId);
2116
2818
  if (!agent) throw new Error(`No encontré el agente "${agentId}" instalado.`);
2117
- if (!supportsAcp(agent)) throw new Error(`${agent.def.label} no tiene modo ACP.`);
2819
+ if (!supportsAcp(agent)) throw new Error(`${agent.def.label} no se puede usar desde la web.`);
2118
2820
  return agent;
2119
2821
  }
2120
2822
  function info(entry) {
@@ -2138,9 +2840,137 @@ function info(entry) {
2138
2840
  function authFailureMessage(agentId, agentLabel, detail) {
2139
2841
  return authFailureMessage$1(agentId, agentLabel, detail);
2140
2842
  }
2141
- /** Abre una terminal con el login del agente (Claude `/login`, `gh auth login`, …). */
2142
- function openAgentLogin(agentId) {
2143
- return openLoginTerminal(String(agentId || ""));
2843
+ /**
2844
+ * Abre el login del agente.
2845
+ * - Con `sessionId`: comando según authMethods ACP (proceso oculto sin TTY), espera exit 0, reconecta y retoma.
2846
+ * - Solo `agentId`: lanza el comando fallback en background (p. ej. fallo al listar modelos).
2847
+ */
2848
+ async function openAgentLogin(input) {
2849
+ const sessionId = String(input?.sessionId || "").trim();
2850
+ const agentId = String(input?.agentId || "").trim();
2851
+ const host = input?.host != null ? String(input.host) : void 0;
2852
+ if (sessionId) {
2853
+ const entry = sessions.get(sessionId);
2854
+ if (!entry) throw new Error("La sesión del agente ya no existe. Recargá la página.");
2855
+ const method = pickLoginAuthMethod(entry.pendingAuthMethods || []);
2856
+ let cmd;
2857
+ try {
2858
+ cmd = resolveLoginCommand(entry.agent, method);
2859
+ } catch {
2860
+ cmd = loginCommand(entry.agentId, { host });
2861
+ if (!cmd) throw new Error("Este agente no tiene un inicio de sesión conocido.");
2862
+ }
2863
+ if (entry.agentId === "copilot" || agentId === "copilot") cmd = withCopilotLoginHost(cmd, host);
2864
+ setStatus(entry, "starting", loginWaitingMessage());
2865
+ entry.busy = true;
2866
+ try {
2867
+ const result = await runLoginTerminal(cmd, {
2868
+ wait: true,
2869
+ cwd: entry.repo
2870
+ });
2871
+ if (result.code !== 0) {
2872
+ entry.busy = false;
2873
+ const msg = loginIncompleteMessage();
2874
+ setStatus(entry, "needs_auth", msg);
2875
+ throw new Error(msg);
2876
+ }
2877
+ try {
2878
+ entry.acp?.close();
2879
+ } catch {}
2880
+ entry.acp = null;
2881
+ entry.pendingAuthMethods = null;
2882
+ const resume = entry.authResume || "chat";
2883
+ entry.authResume = null;
2884
+ entry.busy = false;
2885
+ if (resume === "review") {
2886
+ setStatus(entry, "starting", "Login ok. Reintentando la review…");
2887
+ continueReviewAfterDiff(entry, {
2888
+ repo: entry.repo,
2889
+ branch: entry.branch,
2890
+ base: entry.base,
2891
+ outputFilename: entry.outputFilename,
2892
+ model: entry.model || void 0
2893
+ });
2894
+ } else {
2895
+ setStatus(entry, "starting", "Login ok. Reconectando con el agente…");
2896
+ try {
2897
+ await ensureAcp(entry, entry.model || void 0);
2898
+ setStatus(entry, "idle");
2899
+ } catch (err) {
2900
+ entry.busy = false;
2901
+ if (isAuthError(err)) markNeedsAuth(entry, err, "chat");
2902
+ else {
2903
+ const raw = err instanceof Error ? err.message : String(err);
2904
+ setStatus(entry, "error", authFailureMessage(entry.agentId, entry.agentLabel, raw) || raw);
2905
+ }
2906
+ throw err;
2907
+ }
2908
+ }
2909
+ return {
2910
+ display: result.display,
2911
+ resumed: true,
2912
+ code: result.code
2913
+ };
2914
+ } catch (err) {
2915
+ entry.busy = false;
2916
+ if (entry.status !== "needs_auth" && entry.status !== "error" && entry.status !== "starting") setStatus(entry, "needs_auth", err instanceof Error ? err.message : String(err));
2917
+ throw err;
2918
+ }
2919
+ }
2920
+ if (!agentId) throw new Error("Falta agentId o sessionId.");
2921
+ const cmd = loginCommand(agentId, { host });
2922
+ if (!cmd) throw new Error("Este agente no tiene un inicio de sesión conocido.");
2923
+ return openLoginTerminal(cmd);
2924
+ }
2925
+ /**
2926
+ * Cierra la sesión del agente (y cuentas `gh` si es Copilot) para poder elegir otra cuenta.
2927
+ * Deja la entry en `needs_auth` lista para un nuevo login.
2928
+ */
2929
+ async function openAgentLogout(input) {
2930
+ const sessionId = String(input?.sessionId || "").trim();
2931
+ const host = input?.host != null ? String(input.host) : void 0;
2932
+ let agentId = String(input?.agentId || "").trim();
2933
+ /** @type {Entry | undefined} */
2934
+ let entry;
2935
+ if (sessionId) {
2936
+ entry = sessions.get(sessionId);
2937
+ if (!entry) throw new Error("La sesión del agente ya no existe. Recargá la página.");
2938
+ agentId = entry.agentId || agentId;
2939
+ }
2940
+ if (!agentId) throw new Error("Falta agentId o sessionId.");
2941
+ /** @type {string[]} */
2942
+ const steps = [];
2943
+ /** @type {string[]} */
2944
+ const warnings = [];
2945
+ if (entry?.acp?.supportsLogout) try {
2946
+ await entry.acp.logout();
2947
+ steps.push("Cerré la sesión del agente.");
2948
+ } catch (err) {
2949
+ warnings.push(err instanceof Error ? err.message : String(err));
2950
+ }
2951
+ try {
2952
+ entry?.acp?.close();
2953
+ } catch {}
2954
+ if (entry) {
2955
+ entry.acp = null;
2956
+ entry.starting = null;
2957
+ entry.busy = false;
2958
+ entry.pendingAuthMethods = null;
2959
+ entry.authResume = entry.authResume || (entry.seed || entry.diffText ? "review" : "chat");
2960
+ }
2961
+ const cli = await runAgentLogout(agentId, {
2962
+ host,
2963
+ cwd: entry?.repo
2964
+ });
2965
+ steps.push(...cli.steps);
2966
+ warnings.push(...cli.warnings);
2967
+ const msg = warnings.length && !cli.ok ? warnings[0] : "Sesión cerrada. Elegí con qué cuenta iniciar de nuevo.";
2968
+ if (entry) setStatus(entry, "needs_auth", msg);
2969
+ return {
2970
+ ok: cli.ok || steps.length > 0,
2971
+ steps,
2972
+ warnings
2973
+ };
2144
2974
  }
2145
2975
  /** Escribe seed (JSON + .diff) en `.diff-review/` para que el agente los lea. */
2146
2976
  function materializeSeed(entry) {
@@ -2291,14 +3121,36 @@ function emit(entry, event) {
2291
3121
  }
2292
3122
  function setStatus(entry, status, message) {
2293
3123
  entry.status = status;
2294
- if (status === "error") entry.statusMessage = message || entry.statusMessage || "Error del agente.";
3124
+ if (status === "error" || status === "needs_auth") entry.statusMessage = message || entry.statusMessage || "Error del agente.";
2295
3125
  else if (message !== void 0) delete entry.statusMessage;
2296
3126
  emit(entry, {
2297
3127
  type: "status",
2298
3128
  status,
2299
- ...message || status === "error" ? { message: message || entry.statusMessage } : {}
3129
+ ...message || status === "error" || status === "needs_auth" ? { message: message || entry.statusMessage } : {}
2300
3130
  });
2301
3131
  }
3132
+ function markNeedsAuth(entry, err, resume) {
3133
+ const authErr = err instanceof AcpAuthRequiredError ? err : null;
3134
+ const raw = err instanceof Error ? err.message : String(err);
3135
+ const msg = authErr?.message || authFailureMessage(entry.agentId, entry.agentLabel, raw) || raw;
3136
+ entry.pendingAuthMethods = authErr?.methods?.length ? authErr.methods : entry.pendingAuthMethods;
3137
+ entry.authResume = resume;
3138
+ entry.acp = null;
3139
+ setStatus(entry, "needs_auth", msg);
3140
+ }
3141
+ function isAuthError(err) {
3142
+ if (err instanceof AcpAuthRequiredError) return true;
3143
+ return looksLikeAuthFailure(err instanceof Error ? err.message : String(err));
3144
+ }
3145
+ /** Sin sesión CLI y con login desde la app (Claude / Copilot / Cursor). */
3146
+ function needsInteractiveLogin(agentId) {
3147
+ if (!loginCommand(agentId)) return false;
3148
+ try {
3149
+ return !agentAccountStatus(agentId).loggedIn;
3150
+ } catch {
3151
+ return false;
3152
+ }
3153
+ }
2302
3154
  /** SessionUpdate de ACP → evento de la web (lo que no se muestra se descarta). */
2303
3155
  function toEvent(update) {
2304
3156
  switch (update.sessionUpdate) {
@@ -2382,13 +3234,13 @@ async function ensureAcp(entry, model) {
2382
3234
  entry.busy = false;
2383
3235
  const auth = authFailureMessage(entry.agentId, entry.agentLabel, stderr);
2384
3236
  if (auth) {
2385
- setStatus(entry, "error", auth);
3237
+ markNeedsAuth(entry, new Error(auth), entry.mode === "review" ? "review" : "chat");
2386
3238
  return;
2387
3239
  }
2388
3240
  const raw = stderr.trim() || `código ${code ?? "?"}`;
2389
3241
  const authFromCode = authFailureMessage(entry.agentId, entry.agentLabel, raw);
2390
3242
  if (authFromCode) {
2391
- setStatus(entry, "error", authFromCode);
3243
+ markNeedsAuth(entry, new Error(authFromCode), entry.mode === "review" ? "review" : "chat");
2392
3244
  return;
2393
3245
  }
2394
3246
  const tail = stderr.trim().split(/\r?\n/).slice(-3).join(" · ");
@@ -2506,6 +3358,7 @@ async function checkOutput(entry, since, retriesLeft) {
2506
3358
  stampReviewProvenance(path, {
2507
3359
  agent: entry.agentLabel,
2508
3360
  model: entry.model,
3361
+ modelLabel: entry.acp?.models.find((o) => o.value === entry.model)?.name,
2509
3362
  agentSession: {
2510
3363
  agentId: entry.agentId,
2511
3364
  sessionId: entry.acpSessionId,
@@ -2573,6 +3426,11 @@ async function runTurn(entry, text, mode, opts) {
2573
3426
  mode: mode === "review" ? "review" : mode
2574
3427
  });
2575
3428
  try {
3429
+ if (!entry.acp && needsInteractiveLogin(entry.agentId)) {
3430
+ entry.busy = false;
3431
+ markNeedsAuth(entry, /* @__PURE__ */ new Error("not logged in"), mode === "review" ? "review" : "chat");
3432
+ return;
3433
+ }
2576
3434
  if (!entry.acp) setStatus(entry, "starting", `Lanzando ${entry.agentLabel}…`);
2577
3435
  const acp = await ensureAcp(entry, opts.model);
2578
3436
  if (entry.cancelled) {
@@ -2604,6 +3462,10 @@ async function runTurn(entry, text, mode, opts) {
2604
3462
  setStatus(entry, "idle", "Cancelado.");
2605
3463
  return;
2606
3464
  }
3465
+ if (isAuthError(err)) {
3466
+ markNeedsAuth(entry, err, mode === "review" ? "review" : "chat");
3467
+ return;
3468
+ }
2607
3469
  const raw = err instanceof Error ? err.message : String(err);
2608
3470
  setStatus(entry, "error", authFailureMessage(entry.agentId, entry.agentLabel, raw) || raw);
2609
3471
  } finally {
@@ -2641,7 +3503,9 @@ function newEntry(agent, fields) {
2641
3503
  diffPaths: null,
2642
3504
  pendingPermissions: /* @__PURE__ */ new Map(),
2643
3505
  sessionAllowDenied: false,
2644
- cancelled: false
3506
+ cancelled: false,
3507
+ pendingAuthMethods: null,
3508
+ authResume: null
2645
3509
  };
2646
3510
  sessions.set(entry.id, entry);
2647
3511
  return entry;
@@ -2746,13 +3610,20 @@ async function openChat(input) {
2746
3610
  if (handoffNote) entry.handoffNote = handoffNote;
2747
3611
  if (handoffChat != null) entry.handoffChatMarkdown = handoffChat;
2748
3612
  if (!sessionId) entry.needsContext = true;
3613
+ if (needsInteractiveLogin(entry.agentId)) {
3614
+ markNeedsAuth(entry, /* @__PURE__ */ new Error("not logged in"), "chat");
3615
+ return info(entry);
3616
+ }
2749
3617
  try {
2750
3618
  if (!entry.acp) setStatus(entry, "starting", `Lanzando ${entry.agentLabel}…`);
2751
3619
  await ensureAcp(entry, input.model);
2752
3620
  if (entry.status === "starting") setStatus(entry, "idle");
2753
3621
  } catch (err) {
2754
- const raw = err instanceof Error ? err.message : String(err);
2755
- setStatus(entry, "error", authFailureMessage(entry.agentId, entry.agentLabel, raw) || raw);
3622
+ if (isAuthError(err)) markNeedsAuth(entry, err, "chat");
3623
+ else {
3624
+ const raw = err instanceof Error ? err.message : String(err);
3625
+ setStatus(entry, "error", authFailureMessage(entry.agentId, entry.agentLabel, raw) || raw);
3626
+ }
2756
3627
  }
2757
3628
  return info(entry);
2758
3629
  }
@@ -2787,6 +3658,23 @@ function retryReview(id) {
2787
3658
  const entry = sessions.get(String(id || ""));
2788
3659
  if (!entry) throw new Error("La sesión del agente ya no existe. Recargá la página.");
2789
3660
  if (entry.busy) throw new Error("El agente todavía está trabajando en el pedido anterior.");
3661
+ if (entry.status === "needs_auth" || entry.status === "error" && looksLikeAuthFailure(entry.statusMessage || "")) {
3662
+ entry.pendingAuthMethods = null;
3663
+ entry.authResume = null;
3664
+ try {
3665
+ entry.acp?.close();
3666
+ } catch {}
3667
+ entry.acp = null;
3668
+ setStatus(entry, "starting", "Reintentando la review…");
3669
+ continueReviewAfterDiff(entry, {
3670
+ repo: entry.repo,
3671
+ branch: entry.branch,
3672
+ base: entry.base,
3673
+ outputFilename: entry.outputFilename,
3674
+ model: entry.model || void 0
3675
+ });
3676
+ return info(entry);
3677
+ }
2790
3678
  if (entry.status === "error") throw new Error(entry.statusMessage || "La sesión está en error; iniciá una nueva review.");
2791
3679
  const posixRel = outputRelPath(entry.outputFilename).replace(/\\/g, "/");
2792
3680
  const existing = checkEntryReport(entry);
@@ -2945,5 +3833,5 @@ function startPendingAgentRun() {
2945
3833
  }
2946
3834
  }
2947
3835
 
2948
- export { agentModels as a, startReview as b, sendMessage as c, setPermissionTrust as d, cancelTurn as e, closeChat as f, retryReview as g, openAgentLogin as h, loginCommand as i, subscribe as j, startPendingAgentRun as k, listAcpAgents as l, openChat as o, respondPermission as r, sessionInfo as s };
2949
- //# sourceMappingURL=agent-sessions-2XGojSJ_.js.map
3836
+ export { agentModels as a, startReview as b, sendMessage as c, setPermissionTrust as d, cancelTurn as e, closeChat as f, retryReview as g, agentAccountStatus as h, openAgentLogin as i, openAgentLogout as j, subscribe as k, listAcpAgents as l, startPendingAgentRun as m, openChat as o, respondPermission as r, sessionInfo as s };
3837
+ //# sourceMappingURL=agent-sessions-BArYLTW6.js.map