@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.
- package/README.md +22 -14
- package/bin/diff-review.mjs +192 -21
- package/lib/acp.mjs +86 -10
- package/lib/agent-login.mjs +848 -36
- package/lib/agent-login.test.mjs +538 -0
- package/lib/agents.mjs +0 -39
- package/lib/agents.test.mjs +17 -24
- package/lib/extract-cache.test.mjs +162 -162
- package/lib/git.mjs +25 -9
- package/lib/model-label.mjs +20 -4
- package/lib/model-label.test.mjs +15 -2
- package/lib/prompt.mjs +12 -10
- package/lib/prompt.test.mjs +12 -0
- package/lib/review-meta.mjs +11 -3
- package/lib/review-meta.test.mjs +19 -2
- package/lib/review-prep.mjs +63 -63
- package/lib/validate.mjs +1 -1
- package/lib/wait-output.test.mjs +1 -1
- package/package.json +2 -2
- package/web/build/client/_app/immutable/assets/0.B04dzd8P.css +1 -0
- package/web/build/client/_app/immutable/assets/0.B04dzd8P.css.br +0 -0
- package/web/build/client/_app/immutable/assets/0.B04dzd8P.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css +1 -0
- package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.BVJgADrM.css +1 -0
- package/web/build/client/_app/immutable/assets/3.BVJgADrM.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.BVJgADrM.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css +1 -0
- package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css.br +0 -0
- package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/B-2kfLeA.js +1 -0
- package/web/build/client/_app/immutable/chunks/B-2kfLeA.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/B-2kfLeA.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{DHO7NTh1.js → B4SiKXUO.js} +2 -2
- package/web/build/client/_app/immutable/chunks/B4SiKXUO.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/B4SiKXUO.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{CXhIC34H.js → DA0YnpGR.js} +3 -3
- package/web/build/client/_app/immutable/chunks/DA0YnpGR.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/DA0YnpGR.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{cYiFfRa3.js → Dv85Hz5p.js} +1 -1
- package/web/build/client/_app/immutable/chunks/Dv85Hz5p.js.br +2 -0
- package/web/build/client/_app/immutable/chunks/Dv85Hz5p.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/b-BaDYIc.js +215 -0
- package/web/build/client/_app/immutable/chunks/b-BaDYIc.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/b-BaDYIc.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.DPUME9Z_.js +2 -0
- package/web/build/client/_app/immutable/entry/app.DPUME9Z_.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.DPUME9Z_.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.hH87UZsD.js +1 -0
- package/web/build/client/_app/immutable/entry/start.hH87UZsD.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.hH87UZsD.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.BhHA-Ohy.js +1 -0
- package/web/build/client/_app/immutable/nodes/0.BhHA-Ohy.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/0.BhHA-Ohy.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.jjvGFUxu.js +1 -0
- package/web/build/client/_app/immutable/nodes/1.jjvGFUxu.js.br +2 -0
- package/web/build/client/_app/immutable/nodes/1.jjvGFUxu.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.CJTh6CGA.js +12 -0
- package/web/build/client/_app/immutable/nodes/2.CJTh6CGA.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.CJTh6CGA.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.DYAPRc_2.js +340 -0
- package/web/build/client/_app/immutable/nodes/3.DYAPRc_2.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.DYAPRc_2.js.gz +0 -0
- package/web/build/client/_app/version.json +1 -1
- package/web/build/client/_app/version.json.br +0 -0
- package/web/build/client/_app/version.json.gz +0 -0
- package/web/build/handler.js +3 -3
- package/web/build/index.js +3 -3
- package/web/build/server/chunks/0-8NUeB9cB.js +11 -0
- package/web/build/server/chunks/{0-D7NSxn1-.js.map → 0-8NUeB9cB.js.map} +1 -1
- package/web/build/server/chunks/1-BApN-ILJ.js +7 -0
- package/web/build/server/chunks/{1-DboYz8Fw.js.map → 1-BApN-ILJ.js.map} +1 -1
- package/web/build/server/chunks/2-Bq7iMeQx.js +7 -0
- package/web/build/server/chunks/2-Bq7iMeQx.js.map +1 -0
- package/web/build/server/chunks/3-W0cqofyE.js +7 -0
- package/web/build/server/chunks/{3-DgJ_LmTL.js.map → 3-W0cqofyE.js.map} +1 -1
- package/web/build/server/chunks/_server.ts-Bu1fdGSB.js +123 -0
- package/web/build/server/chunks/_server.ts-Bu1fdGSB.js.map +1 -0
- package/web/build/server/chunks/{_server.ts-CP5cRLhW.js → _server.ts-BuUNechu.js} +5 -4
- package/web/build/server/chunks/_server.ts-BuUNechu.js.map +1 -0
- package/web/build/server/chunks/{agent-sessions-2XGojSJ_.js → agent-sessions-BArYLTW6.js} +1023 -135
- package/web/build/server/chunks/agent-sessions-BArYLTW6.js.map +1 -0
- package/web/build/server/chunks/{git-review-jZ_JJYfv.js → git-review-BJ_JrFNP.js} +54 -21
- package/web/build/server/chunks/{git-review-jZ_JJYfv.js.map → git-review-BJ_JrFNP.js.map} +1 -1
- package/web/build/server/chunks/{handler-B_8yUXji.js → handler-Df_bJiCq.js} +3 -3
- package/web/build/server/chunks/{handler-B_8yUXji.js.map → handler-Df_bJiCq.js.map} +1 -1
- package/web/build/server/chunks/{hooks.server-rF_eBCff.js → hooks.server-BEl7rVad.js} +4 -3
- package/web/build/server/chunks/{hooks.server-rF_eBCff.js.map → hooks.server-BEl7rVad.js.map} +1 -1
- package/web/build/server/chunks/{index.js-CGw8_Q7V.js → index.js-Bb0CZLZ_.js} +4 -4
- package/web/build/server/chunks/index.js-Bb0CZLZ_.js.map +1 -0
- package/web/build/server/chunks/manifest.js-j442duM7.js +65 -0
- package/web/build/server/chunks/manifest.js-j442duM7.js.map +1 -0
- package/web/build/server/chunks/{remote-1g4ayag-BMpjk8UD.js → remote-1g4ayag-BxKnZQPn.js} +6 -6
- package/web/build/server/chunks/remote-1g4ayag-BxKnZQPn.js.map +1 -0
- package/web/build/server/chunks/{remote-wh6t6j-dYLCg8ks.js → remote-wh6t6j-B-AHrGaN.js} +44 -10
- package/web/build/server/chunks/remote-wh6t6j-B-AHrGaN.js.map +1 -0
- package/web/build/server/chunks/{validate-Da6e71V3.js → validate-CXjTRGnR.js} +34 -17
- package/web/build/server/chunks/validate-CXjTRGnR.js.map +1 -0
- package/web/build/client/_app/immutable/assets/0.BKRG1f36.css +0 -1
- package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.br +0 -0
- package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/2.C4whE4xz.css +0 -1
- package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.DB-bJJq2.css +0 -1
- package/web/build/client/_app/immutable/assets/3.DB-bJJq2.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.DB-bJJq2.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/storage.BxR6930R.css +0 -1
- package/web/build/client/_app/immutable/assets/storage.BxR6930R.css.br +0 -0
- package/web/build/client/_app/immutable/assets/storage.BxR6930R.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/BLMWhW3J.js +0 -1
- package/web/build/client/_app/immutable/chunks/BLMWhW3J.js.br +0 -1
- package/web/build/client/_app/immutable/chunks/BLMWhW3J.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/CXhIC34H.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/CXhIC34H.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/DHO7NTh1.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/DHO7NTh1.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/DpxY75GB.js +0 -215
- package/web/build/client/_app/immutable/chunks/DpxY75GB.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/DpxY75GB.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/cYiFfRa3.js.br +0 -1
- package/web/build/client/_app/immutable/chunks/cYiFfRa3.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.CCv57mvp.js +0 -2
- package/web/build/client/_app/immutable/entry/app.CCv57mvp.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.CCv57mvp.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.B012TSbW.js +0 -1
- package/web/build/client/_app/immutable/entry/start.B012TSbW.js.br +0 -1
- package/web/build/client/_app/immutable/entry/start.B012TSbW.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.slrnBioC.js +0 -1
- package/web/build/client/_app/immutable/nodes/0.slrnBioC.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/0.slrnBioC.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.Bs5JdO4l.js +0 -1
- package/web/build/client/_app/immutable/nodes/1.Bs5JdO4l.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/1.Bs5JdO4l.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.D5ULH3EX.js +0 -10
- package/web/build/client/_app/immutable/nodes/2.D5ULH3EX.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.D5ULH3EX.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.hjzQQgyj.js +0 -173
- package/web/build/client/_app/immutable/nodes/3.hjzQQgyj.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.hjzQQgyj.js.gz +0 -0
- package/web/build/server/chunks/0-D7NSxn1-.js +0 -11
- package/web/build/server/chunks/1-DboYz8Fw.js +0 -7
- package/web/build/server/chunks/2-B7LX817Q.js +0 -7
- package/web/build/server/chunks/2-B7LX817Q.js.map +0 -1
- package/web/build/server/chunks/3-DgJ_LmTL.js +0 -7
- package/web/build/server/chunks/_server.ts-CP5cRLhW.js.map +0 -1
- package/web/build/server/chunks/agent-sessions-2XGojSJ_.js.map +0 -1
- package/web/build/server/chunks/index.js-CGw8_Q7V.js.map +0 -1
- package/web/build/server/chunks/manifest.js-CImZFS6G.js +0 -58
- package/web/build/server/chunks/manifest.js-CImZFS6G.js.map +0 -1
- package/web/build/server/chunks/remote-1g4ayag-BMpjk8UD.js.map +0 -1
- package/web/build/server/chunks/remote-wh6t6j-dYLCg8ks.js.map +0 -1
- package/web/build/server/chunks/validate-Da6e71V3.js.map +0 -1
package/README.md
CHANGED
|
@@ -47,30 +47,38 @@ diff-review --agent
|
|
|
47
47
|
|
|
48
48
|
Al enviarlo a un agente, en vez de copiar el prompt pregunta el modelo (con `--agent`, primero el agente):
|
|
49
49
|
|
|
50
|
-
1. **Agente**: los instalados que encuentra (Claude Code,
|
|
50
|
+
1. **Agente**: los instalados que encuentra (Claude Code, Cursor CLI, GitHub Copilot CLI). Si ya lo elegiste en la lista anterior, este paso no se repite. Se buscan por nombre en el `PATH` y en algunas rutas fijas: en Windows, el Claude Code que trae la app de
|
|
51
51
|
escritorio (`%APPDATA%\Claude\claude-code\<versión>\…\claude.exe`) y `%LOCALAPPDATA%\cursor-agent\cursor-agent.cmd`;
|
|
52
52
|
en Linux/macOS, `~/.local/bin/claude`, `~/.claude/local/claude` y `~/.local/bin/cursor-agent`.
|
|
53
53
|
2. **Modelo**: arranca en el default del agente (el configurado en `~/.claude/settings.json` /
|
|
54
|
-
`ANTHROPIC_MODEL`, `~/.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
`ANTHROPIC_MODEL`, `~/.cursor/cli-config.json` o `~/.copilot/settings.json`; si no hay, no se pasa `--model`).
|
|
55
|
+
También lista los conocidos (para Claude, los alias `opus` / `sonnet` / `haiku`; para Copilot, `auto`) y "Otro…"
|
|
56
|
+
para escribir un id. Por ACP, la lista sale de lo que ofrece el agente.
|
|
57
|
+
|
|
58
|
+
Por defecto el agente corre por ACP en la web. Con `--headless`, el prompt va por stdin y la salida se ve en la
|
|
59
|
+
terminal (Claude/Copilot con permisos acotados; Cursor con `--force`). Si no hay agentes instalados, sigue con el
|
|
60
|
+
portapapeles. Sin terminal interactiva usa el primer agente con su modelo por defecto.
|
|
61
|
+
|
|
62
|
+
Si el agente no está autenticado, la CLI pregunta **¿Querés iniciar sesión en {agente} ahora?** (al listar
|
|
63
|
+
modelos y otra vez **antes de abrir la web** si ya elegiste agente), corre el login **en la misma terminal**,
|
|
64
|
+
espera exit 0 y reintenta; si cancelás, sigue (lista fija de modelos / web con CTA). Con **Copilot** podés
|
|
65
|
+
elegir **GitHub.com** o **Enterprise** (hostname); no se fuerza un host por env. En github.com, si tenés
|
|
66
|
+
varias cuentas, elegí la corporativa en el browser del OAuth. En el header (arriba a la derecha) se muestra la **cuenta activa** del agente; el menú tiene
|
|
67
|
+
**Cambiar cuenta** (en Copilot también cierra sesiones de `gh`, porque si no el logout OAuth no alcanza).
|
|
68
|
+
Ojo: si tenés `GH_TOKEN` / `GITHUB_TOKEN` / `COPILOT_GITHUB_TOKEN` en el entorno, hay que sacarlos a mano.
|
|
65
69
|
|
|
66
70
|
#### Por ACP, con la web (Claude Code, Cursor CLI, GitHub Copilot CLI)
|
|
67
71
|
|
|
68
|
-
|
|
72
|
+
Los agentes integrados corren por ACP: la CLI abre la UI enseguida y el agente vive dentro del server local. Claude Code
|
|
69
73
|
va por el adaptador `@agentclientprotocol/claude-agent-acp` (se baja con `npx` la primera vez); Cursor
|
|
70
|
-
(`cursor-agent acp`) y Copilot (`copilot --acp`) lo traen de fábrica.
|
|
74
|
+
(`cursor-agent acp`) y Copilot (`copilot --acp`) lo traen de fábrica.
|
|
71
75
|
|
|
72
76
|
- El select de modelo muestra los que ofrece cada agente (se consultan por ACP la primera vez).
|
|
73
77
|
- La web muestra lo que va haciendo (comandos, archivos que lee, plan). La terminal solo avisa fallos duros o JSON válido.
|
|
78
|
+
- Autenticación ACP: el client anuncia `auth.terminal`; Cursor usa `authenticate`, Claude/Copilot abren login
|
|
79
|
+
interactivo (métodos `type: terminal` o `_meta.terminal-auth`) en la misma TTY desde la CLI (modelos + justo
|
|
80
|
+
antes de abrir la web), o como proceso oculto + browser OAuth desde la web vía **Iniciar sesión en {agente}**.
|
|
81
|
+
Si hace falta más tarde, la web usa **needs_auth**.
|
|
74
82
|
- Al terminar cada turno, el server valida el JSON; si falta algo se lo pide al agente hasta dos veces y, cuando valida,
|
|
75
83
|
la web abre el reporte.
|
|
76
84
|
- Cada pedido de permiso pasa por una política fija: leer dentro del repo, escribir únicamente el JSON del review
|
package/bin/diff-review.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
20
20
|
import * as p from '@clack/prompts';
|
|
21
21
|
import { AGENTS, detectAgents, modelChoices, resolvedRunModel, runAgent } from '../lib/agents.mjs';
|
|
22
22
|
import { listAcpModelsCached, supportsAcp } from '../lib/acp.mjs';
|
|
23
|
+
import { isAuthRequiredError, withAgentLoginRetry } from '../lib/agent-login.mjs';
|
|
23
24
|
import { buildAcpModelSelectOptions, sortModelSelectOptions } from '../lib/model-label.mjs';
|
|
24
25
|
import { autocompleteEs } from '../lib/autocomplete-es.mjs';
|
|
25
26
|
import { copyTextToClipboard } from '../lib/clipboard.mjs';
|
|
@@ -32,6 +33,46 @@ import { existingReviewOutput, waitForReviewOutput } from '../lib/wait-output.mj
|
|
|
32
33
|
import { stampReviewProvenance } from '../lib/review-meta.mjs';
|
|
33
34
|
import { runValidateCli } from '../lib/check-report.mjs';
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Copilot: elegir github.com vs Enterprise (el usuario elige; no se fuerza por env).
|
|
38
|
+
* @returns {Promise<string | false>} host vacío = github.com; false = canceló
|
|
39
|
+
*/
|
|
40
|
+
async function pickCopilotLoginHost() {
|
|
41
|
+
const where = await p.select({
|
|
42
|
+
message: '¿Con qué cuenta de GitHub querés iniciar sesión?',
|
|
43
|
+
options: [
|
|
44
|
+
{
|
|
45
|
+
value: 'github.com',
|
|
46
|
+
label: 'GitHub.com',
|
|
47
|
+
hint: 'Personal u organización en github.com — elegí la cuenta en el browser'
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
value: 'enterprise',
|
|
51
|
+
label: 'GitHub Enterprise',
|
|
52
|
+
hint: 'Host tipo empresa.ghe.com'
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
initialValue: 'github.com'
|
|
56
|
+
});
|
|
57
|
+
if (p.isCancel(where)) {
|
|
58
|
+
p.cancel('Cancelado.');
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (where === 'enterprise') {
|
|
62
|
+
const host = await p.text({
|
|
63
|
+
message: 'Hostname de Enterprise',
|
|
64
|
+
placeholder: 'empresa.ghe.com',
|
|
65
|
+
validate: (v) => (String(v || '').trim() ? undefined : 'Ingresá el hostname')
|
|
66
|
+
});
|
|
67
|
+
if (p.isCancel(host)) {
|
|
68
|
+
p.cancel('Cancelado.');
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return String(host).trim();
|
|
72
|
+
}
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
|
|
35
76
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
36
77
|
const packageRoot = join(here, '..');
|
|
37
78
|
// `DIFF_REVIEW_OUT=build-next` cuando `web/build` está bloqueado por un server en marcha (EBUSY en Windows).
|
|
@@ -75,9 +116,10 @@ un solo select lista el portapapeles y cada agente detectado.
|
|
|
75
116
|
Si pasás --branch y --base juntos, saltea los selects.
|
|
76
117
|
Con --agent: saltea esa lista; elegís un agente instalado y el modelo,
|
|
77
118
|
y diff-review lo lanza con el prompt.
|
|
78
|
-
|
|
79
|
-
proceso en vivo y después podés preguntarle o
|
|
80
|
-
|
|
119
|
+
Los agentes integrados (Claude Code, Cursor CLI, Copilot) corren por ACP
|
|
120
|
+
desde la UI web: ves el proceso en vivo y después podés preguntarle o
|
|
121
|
+
pedirle cambios al reporte. Si falta login, la CLI ofrece abrirlo en una
|
|
122
|
+
terminal y reintentar. Con --headless corren en la terminal.
|
|
81
123
|
Con --ui: abre la UI de inmediato (web-first).
|
|
82
124
|
|
|
83
125
|
Options:
|
|
@@ -405,6 +447,7 @@ function copyPromptOrWarn(prompt) {
|
|
|
405
447
|
|
|
406
448
|
/**
|
|
407
449
|
* Opciones de modelo: las que ofrece el agente por ACP (si va a correr así) o, si no, la lista fija.
|
|
450
|
+
* Si ACP falla por auth, ofrece el login (misma TTY o proceso oculto), espera exit 0 y reintenta (misma lógica que la web).
|
|
408
451
|
* @param {import('../lib/agents.mjs').DetectedAgent} agent
|
|
409
452
|
* @param {{ acpCwd?: string }} opts
|
|
410
453
|
*/
|
|
@@ -415,26 +458,85 @@ async function modelOptions(agent, { acpCwd }) {
|
|
|
415
458
|
options: sortModelSelectOptions(fixed.options)
|
|
416
459
|
};
|
|
417
460
|
if (!acpCwd) return sortedFixed;
|
|
461
|
+
|
|
462
|
+
/** @param {boolean} [force] */
|
|
463
|
+
const fetchModels = (force = false) => listAcpModelsCached(agent, { cwd: acpCwd, force });
|
|
464
|
+
|
|
418
465
|
const spin = p.spinner();
|
|
419
466
|
spin.start(`Consultando los modelos de ${agent.def.label}`);
|
|
420
|
-
|
|
421
|
-
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* @param {{ models: { value: string, name: string }[], currentModel: string, fromCache: boolean }} result
|
|
470
|
+
*/
|
|
471
|
+
const finishOk = (result) => {
|
|
422
472
|
spin.stop(
|
|
423
|
-
fromCache
|
|
473
|
+
result.fromCache
|
|
424
474
|
? `Modelos de ${agent.def.label} (caché)`
|
|
425
475
|
: `Modelos de ${agent.def.label}`
|
|
426
476
|
);
|
|
427
|
-
if (!models.length) return sortedFixed;
|
|
428
|
-
|
|
429
|
-
|
|
477
|
+
if (!result.models.length) return sortedFixed;
|
|
478
|
+
return buildAcpModelSelectOptions(
|
|
479
|
+
{ models: result.models, currentModel: result.currentModel },
|
|
480
|
+
fixed
|
|
481
|
+
);
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
return finishOk(await fetchModels(false));
|
|
430
486
|
} catch (err) {
|
|
431
|
-
|
|
487
|
+
const detail = err instanceof Error ? err.message.split('\n')[0] : String(err);
|
|
488
|
+
spin.stop(`No pude consultar los modelos (${detail}).`);
|
|
489
|
+
|
|
490
|
+
// Sin TTY o no es auth: misma degradación de antes.
|
|
491
|
+
if (!isInteractive() || !isAuthRequiredError(err)) return sortedFixed;
|
|
492
|
+
|
|
493
|
+
const outcome = await withAgentLoginRetry(() => fetchModels(true), {
|
|
494
|
+
agent,
|
|
495
|
+
cwd: acpCwd,
|
|
496
|
+
priorError: err,
|
|
497
|
+
askToLogin: async ({ message, agentLabel, displayCmd }) => {
|
|
498
|
+
p.log.warn(message);
|
|
499
|
+
const answer = await p.confirm({
|
|
500
|
+
message: `¿Querés iniciar sesión en ${agentLabel} ahora?`,
|
|
501
|
+
initialValue: true
|
|
502
|
+
});
|
|
503
|
+
if (p.isCancel(answer)) {
|
|
504
|
+
p.cancel('Cancelado.');
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
if (displayCmd) p.log.step(displayCmd);
|
|
508
|
+
return Boolean(answer);
|
|
509
|
+
},
|
|
510
|
+
pickCopilotHost: pickCopilotLoginHost,
|
|
511
|
+
onStatus: (msg) => p.log.info(msg)
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
if (outcome.ok) {
|
|
515
|
+
const result = outcome.value;
|
|
516
|
+
p.log.success(
|
|
517
|
+
result.fromCache
|
|
518
|
+
? `Modelos de ${agent.def.label} (caché)`
|
|
519
|
+
: `Modelos de ${agent.def.label}`
|
|
520
|
+
);
|
|
521
|
+
if (!result.models.length) return sortedFixed;
|
|
522
|
+
return buildAcpModelSelectOptions(
|
|
523
|
+
{ models: result.models, currentModel: result.currentModel },
|
|
524
|
+
fixed
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
if (outcome.cancelled) {
|
|
528
|
+
p.log.warn('Siguiendo con la lista fija de modelos. Podés autenticarte después desde la web.');
|
|
529
|
+
return sortedFixed;
|
|
530
|
+
}
|
|
531
|
+
const after = outcome.error instanceof Error ? outcome.error.message.split('\n')[0] : String(outcome.error);
|
|
532
|
+
p.log.warn(`No pude consultar los modelos tras el login (${after}). Sigo con la lista fija.`);
|
|
432
533
|
return sortedFixed;
|
|
433
534
|
}
|
|
434
535
|
}
|
|
435
536
|
|
|
436
537
|
/**
|
|
437
538
|
* Modelo del agente: autocomplete con búsqueda; sin TTY, el default (string vacío).
|
|
539
|
+
* `""` es válido (= "Por defecto del agente", no se pasa --model); no usar ensureValue.
|
|
438
540
|
* @param {import('../lib/agents.mjs').DetectedAgent} agent
|
|
439
541
|
* @param {{ acpCwd?: string }} [opts] con `acpCwd`, la lista sale del agente por ACP
|
|
440
542
|
*/
|
|
@@ -442,17 +544,17 @@ async function pickModel(agent, opts = {}) {
|
|
|
442
544
|
if (!isInteractive()) return '';
|
|
443
545
|
|
|
444
546
|
const { options, initialValue } = await modelOptions(agent, opts);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
);
|
|
547
|
+
const value = await autocompleteEs({
|
|
548
|
+
message: 'Modelo',
|
|
549
|
+
options,
|
|
550
|
+
initialValue,
|
|
551
|
+
placeholder: 'opus, sonnet, gpt…'
|
|
552
|
+
});
|
|
553
|
+
if (p.isCancel(value)) {
|
|
554
|
+
p.cancel('Cancelado.');
|
|
555
|
+
process.exit(0);
|
|
556
|
+
}
|
|
557
|
+
return /** @type {string} */ (value ?? '');
|
|
456
558
|
}
|
|
457
559
|
|
|
458
560
|
/**
|
|
@@ -572,6 +674,74 @@ async function runAgentAndWait(agent, { repo, prompt, model, outputFilename, use
|
|
|
572
674
|
return /** @type {{ path: string }} */ (result);
|
|
573
675
|
}
|
|
574
676
|
|
|
677
|
+
/**
|
|
678
|
+
* Antes de abrir la web: forzar un probe ACP (sin caché) y, si hace falta auth, login in-flow en esta terminal.
|
|
679
|
+
* Así no llegamos a LiveReviewView sin sesión cuando el usuario ya eligió el agente en la CLI.
|
|
680
|
+
* @param {import('../lib/agents.mjs').DetectedAgent} agent
|
|
681
|
+
* @param {string} repo
|
|
682
|
+
* @param {boolean} useClack
|
|
683
|
+
* @returns {Promise<boolean>} true si el probe quedó ok (logueado o ya lo estaba)
|
|
684
|
+
*/
|
|
685
|
+
async function ensureAgentAuthBeforeUi(agent, repo, useClack) {
|
|
686
|
+
const probe = () => listAcpModelsCached(agent, { cwd: repo, force: true });
|
|
687
|
+
try {
|
|
688
|
+
await probe();
|
|
689
|
+
return true;
|
|
690
|
+
} catch (err) {
|
|
691
|
+
if (!isInteractive() || !isAuthRequiredError(err)) {
|
|
692
|
+
const detail = err instanceof Error ? err.message.split('\n')[0] : String(err);
|
|
693
|
+
if (useClack) p.log.warn(detail);
|
|
694
|
+
else console.warn(detail);
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const outcome = await withAgentLoginRetry(() => probe(), {
|
|
699
|
+
agent,
|
|
700
|
+
cwd: repo,
|
|
701
|
+
priorError: err,
|
|
702
|
+
askToLogin: async ({ message, agentLabel }) => {
|
|
703
|
+
if (useClack) {
|
|
704
|
+
p.log.warn(message);
|
|
705
|
+
const answer = await p.confirm({
|
|
706
|
+
message: `¿Querés iniciar sesión en ${agentLabel} ahora?`,
|
|
707
|
+
initialValue: true
|
|
708
|
+
});
|
|
709
|
+
if (p.isCancel(answer)) {
|
|
710
|
+
p.cancel('Cancelado.');
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
return Boolean(answer);
|
|
714
|
+
}
|
|
715
|
+
console.warn(message);
|
|
716
|
+
return true;
|
|
717
|
+
},
|
|
718
|
+
pickCopilotHost: useClack ? pickCopilotLoginHost : async () => '',
|
|
719
|
+
onStatus: (msg) => {
|
|
720
|
+
if (useClack) p.log.info(msg);
|
|
721
|
+
else console.log(msg);
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
if (outcome.ok) {
|
|
726
|
+
if (useClack) p.log.success(`Sesión de ${agent.def.label} lista.`);
|
|
727
|
+
else console.log(`Sesión de ${agent.def.label} lista.`);
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
if (outcome.cancelled) {
|
|
731
|
+
const warn =
|
|
732
|
+
'Sin iniciar sesión: la web va a pedir autenticación al lanzar el agente.';
|
|
733
|
+
if (useClack) p.log.warn(warn);
|
|
734
|
+
else console.warn(warn);
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
const after =
|
|
738
|
+
outcome.error instanceof Error ? outcome.error.message.split('\n')[0] : String(outcome.error);
|
|
739
|
+
if (useClack) p.log.warn(after);
|
|
740
|
+
else console.warn(after);
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
575
745
|
/**
|
|
576
746
|
* El agente corre dentro del server de la UI (ACP): la web muestra el proceso y abre el reporte; la terminal ve un
|
|
577
747
|
* resumen. La sesión queda viva para preguntas y cambios desde el reporte.
|
|
@@ -580,6 +750,7 @@ async function runAgentAndWait(agent, { repo, prompt, model, outputFilename, use
|
|
|
580
750
|
* @param {{ host: string, port: number, open: boolean }} opts
|
|
581
751
|
*/
|
|
582
752
|
async function runAgentInUi(agent, { repo, branch, base, model, outputFilename, useClack }, opts) {
|
|
753
|
+
await ensureAgentAuthBeforeUi(agent, repo, useClack);
|
|
583
754
|
const id = randomUUID();
|
|
584
755
|
const msg = `Lanzando ${agent.def.label} (${model || 'modelo por defecto'}). El progreso se ve en la web; Ctrl+C corta.`;
|
|
585
756
|
if (useClack) p.outro(msg);
|
package/lib/acp.mjs
CHANGED
|
@@ -9,9 +9,27 @@ import { Readable, Writable } from "node:stream";
|
|
|
9
9
|
import * as acp from "@agentclientprotocol/sdk";
|
|
10
10
|
import { spawnCommand } from "./agents.mjs";
|
|
11
11
|
import { permissionOutcome } from "./acp-policy.mjs";
|
|
12
|
-
import { looksLikeAuthFailure } from "./agent-login.mjs";
|
|
12
|
+
import { authFailureMessage, groupAuthMethods, looksLikeAuthFailure } from "./agent-login.mjs";
|
|
13
13
|
import { readModelsCache, writeModelsCache } from "./models-cache.mjs";
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* session/new (u otra op) requiere login interactivo (terminal / terminal-meta).
|
|
17
|
+
* No es un fallo genérico: la UI debe abrir el login y reintentar.
|
|
18
|
+
*/
|
|
19
|
+
export class AcpAuthRequiredError extends Error {
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} message
|
|
22
|
+
* @param {{ methods?: any[], agentId?: string, agentLabel?: string }} [info]
|
|
23
|
+
*/
|
|
24
|
+
constructor(message, info = {}) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "AcpAuthRequiredError";
|
|
27
|
+
this.methods = Array.isArray(info.methods) ? info.methods : [];
|
|
28
|
+
this.agentId = info.agentId || "";
|
|
29
|
+
this.agentLabel = info.agentLabel || "";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
15
33
|
/**
|
|
16
34
|
* @typedef {import('./agents.mjs').DetectedAgent} DetectedAgent
|
|
17
35
|
* @typedef {import('@agentclientprotocol/sdk').SessionUpdate} SessionUpdate
|
|
@@ -40,9 +58,11 @@ import { readModelsCache, writeModelsCache } from "./models-cache.mjs";
|
|
|
40
58
|
* models: ModelOption[],
|
|
41
59
|
* currentModel: string,
|
|
42
60
|
* modelWarning?: string,
|
|
61
|
+
* supportsLogout: boolean,
|
|
43
62
|
* setModel: (model: string) => Promise<{ currentModel: string, warning?: string }>,
|
|
44
63
|
* prompt: (text: string) => Promise<{ stopReason: string }>,
|
|
45
64
|
* cancel: () => Promise<void>,
|
|
65
|
+
* logout: () => Promise<void>,
|
|
46
66
|
* close: () => void,
|
|
47
67
|
* closed: Promise<void>,
|
|
48
68
|
* stderrTail: () => string,
|
|
@@ -84,7 +104,7 @@ const ACP_START_TIMEOUT_MS = 90_000;
|
|
|
84
104
|
* @returns {Promise<AcpSession>}
|
|
85
105
|
*/
|
|
86
106
|
export async function startAcpSession(agent, opts) {
|
|
87
|
-
if (!supportsAcp(agent)) throw new Error(`${agent.def.label} no
|
|
107
|
+
if (!supportsAcp(agent)) throw new Error(`${agent.def.label} no se puede usar desde la web.`);
|
|
88
108
|
const { file, args, env, sessionMeta } = agent.def.acp({ path: agent.path });
|
|
89
109
|
const child = spawnCommand(file, args, {
|
|
90
110
|
cwd: opts.cwd,
|
|
@@ -163,15 +183,27 @@ export async function startAcpSession(agent, opts) {
|
|
|
163
183
|
try {
|
|
164
184
|
const init = await conn.agent.request(acp.methods.agent.initialize, {
|
|
165
185
|
protocolVersion: acp.PROTOCOL_VERSION,
|
|
166
|
-
clientCapabilities: {
|
|
186
|
+
clientCapabilities: {
|
|
187
|
+
fs: { readTextFile: false, writeTextFile: false },
|
|
188
|
+
// terminal* ACP (PTY del agente): no. auth.terminal = login interactivo en una terminal nuestra.
|
|
189
|
+
terminal: false,
|
|
190
|
+
auth: {
|
|
191
|
+
terminal: true,
|
|
192
|
+
},
|
|
193
|
+
// Claude mira `clientCapabilities._meta["terminal-auth"]` (no `auth._meta`) para rellenar
|
|
194
|
+
// method._meta["terminal-auth"] con node+adapter --cli …; sin esto solo manda type:terminal + args.
|
|
195
|
+
_meta: { "terminal-auth": true },
|
|
196
|
+
},
|
|
167
197
|
clientInfo: { name: "diff-review", version: "0" },
|
|
168
198
|
});
|
|
169
199
|
|
|
170
200
|
const authMethods = Array.isArray(init.authMethods) ? init.authMethods : [];
|
|
171
|
-
|
|
172
|
-
|
|
201
|
+
const grouped = groupAuthMethods(authMethods);
|
|
202
|
+
/** Solo type agent (p. ej. Cursor `cursor_login`). Nunca terminal ni Copilot `_meta.terminal-auth`. */
|
|
203
|
+
const agentAuthMethods = grouped.agent;
|
|
204
|
+
const interactiveAuthMethods = [...grouped.terminal, ...grouped.terminalMeta];
|
|
173
205
|
|
|
174
|
-
/** Si session/new falla por auth, pedimos `authenticate` una vez y reintentamos. */
|
|
206
|
+
/** Si session/new falla por auth, pedimos `authenticate` una vez (métodos agent) y reintentamos. */
|
|
175
207
|
const tryAuthenticate = async () => {
|
|
176
208
|
for (const method of agentAuthMethods) {
|
|
177
209
|
try {
|
|
@@ -184,6 +216,18 @@ export async function startAcpSession(agent, opts) {
|
|
|
184
216
|
return false;
|
|
185
217
|
};
|
|
186
218
|
|
|
219
|
+
const authRequired = (msg) => {
|
|
220
|
+
const label = agent.def.label;
|
|
221
|
+
const friendly =
|
|
222
|
+
authFailureMessage(agent.def.id, label, msg) ||
|
|
223
|
+
`Necesitás iniciar sesión en ${label}.`;
|
|
224
|
+
return new AcpAuthRequiredError(friendly, {
|
|
225
|
+
methods: interactiveAuthMethods.length ? interactiveAuthMethods : authMethods,
|
|
226
|
+
agentId: agent.def.id,
|
|
227
|
+
agentLabel: label,
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
|
|
187
231
|
const openNewSession = async () => {
|
|
188
232
|
try {
|
|
189
233
|
return await conn.agent.request(acp.methods.agent.session.new, {
|
|
@@ -193,13 +237,20 @@ export async function startAcpSession(agent, opts) {
|
|
|
193
237
|
});
|
|
194
238
|
} catch (err) {
|
|
195
239
|
const msg = err instanceof Error ? err.message : String(err);
|
|
196
|
-
if (
|
|
240
|
+
if (!looksLikeAuthFailure(msg)) throw err;
|
|
241
|
+
|
|
242
|
+
// Cursor (y similares): authenticate + retry. No llamar authenticate para terminal / Copilot.
|
|
243
|
+
if (agentAuthMethods.length && (await tryAuthenticate())) {
|
|
197
244
|
return await conn.agent.request(acp.methods.agent.session.new, {
|
|
198
245
|
cwd: opts.cwd,
|
|
199
246
|
mcpServers: [],
|
|
200
247
|
...(sessionMeta ? { _meta: sessionMeta } : {}),
|
|
201
248
|
});
|
|
202
249
|
}
|
|
250
|
+
|
|
251
|
+
if (interactiveAuthMethods.length || agentAuthMethods.length || looksLikeAuthFailure(msg)) {
|
|
252
|
+
throw authRequired(msg);
|
|
253
|
+
}
|
|
203
254
|
throw err;
|
|
204
255
|
}
|
|
205
256
|
};
|
|
@@ -237,7 +288,7 @@ export async function startAcpSession(agent, opts) {
|
|
|
237
288
|
const wanted = String(model || "").trim();
|
|
238
289
|
if (!wanted || wanted === currentModel) return { currentModel };
|
|
239
290
|
if (!models) {
|
|
240
|
-
return { currentModel, warning: `El agente no deja elegir el modelo
|
|
291
|
+
return { currentModel, warning: `El agente no deja elegir el modelo; usa el suyo.` };
|
|
241
292
|
}
|
|
242
293
|
if (!models.options.some((o) => o.value === wanted)) {
|
|
243
294
|
return {
|
|
@@ -261,6 +312,8 @@ export async function startAcpSession(agent, opts) {
|
|
|
261
312
|
if (applied.warning) modelWarning = applied.warning;
|
|
262
313
|
}
|
|
263
314
|
|
|
315
|
+
const supportsLogout = Boolean(init.agentCapabilities?.auth?.logout);
|
|
316
|
+
|
|
264
317
|
clearTimeout(timer);
|
|
265
318
|
return {
|
|
266
319
|
sessionId,
|
|
@@ -269,6 +322,7 @@ export async function startAcpSession(agent, opts) {
|
|
|
269
322
|
get currentModel() {
|
|
270
323
|
return currentModel;
|
|
271
324
|
},
|
|
325
|
+
supportsLogout,
|
|
272
326
|
...(modelWarning ? { modelWarning } : {}),
|
|
273
327
|
setModel,
|
|
274
328
|
prompt: async (text) => {
|
|
@@ -279,6 +333,13 @@ export async function startAcpSession(agent, opts) {
|
|
|
279
333
|
return { stopReason: String(res.stopReason) };
|
|
280
334
|
},
|
|
281
335
|
cancel: () => conn.agent.notify(acp.methods.agent.session.cancel, { sessionId }),
|
|
336
|
+
/** ACP `logout` si el agente lo anunció; no garantiza nada sobre credenciales de `gh`. */
|
|
337
|
+
logout: async () => {
|
|
338
|
+
if (!supportsLogout) {
|
|
339
|
+
throw new Error(`${agent.def.label} no admite cerrar sesión desde acá.`);
|
|
340
|
+
}
|
|
341
|
+
await conn.agent.request(acp.methods.agent.logout, {});
|
|
342
|
+
},
|
|
282
343
|
close,
|
|
283
344
|
closed: conn.closed,
|
|
284
345
|
stderrTail: () => stderr,
|
|
@@ -286,13 +347,28 @@ export async function startAcpSession(agent, opts) {
|
|
|
286
347
|
} catch (err) {
|
|
287
348
|
clearTimeout(timer);
|
|
288
349
|
close();
|
|
350
|
+
if (err instanceof AcpAuthRequiredError) throw err;
|
|
289
351
|
const tail = stderr.trim().split(/\r?\n/).slice(-8).join("\n");
|
|
290
352
|
if (timedOut) {
|
|
291
353
|
const msg = `Timeout (${Math.round(timeoutMs / 1000)}s) al lanzar ${agent.def.label}. ¿Está autenticado el CLI?`;
|
|
292
|
-
|
|
354
|
+
const full = tail ? `${msg}\n${tail}` : msg;
|
|
355
|
+
if (looksLikeAuthFailure(full)) {
|
|
356
|
+
throw new AcpAuthRequiredError(
|
|
357
|
+
authFailureMessage(agent.def.id, agent.def.label, full) || full,
|
|
358
|
+
{ agentId: agent.def.id, agentLabel: agent.def.label }
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
throw new Error(full);
|
|
293
362
|
}
|
|
294
363
|
const reason = err instanceof Error ? err.message : String(err);
|
|
295
|
-
|
|
364
|
+
const full = tail ? `${reason}\n${tail}` : reason;
|
|
365
|
+
if (looksLikeAuthFailure(full)) {
|
|
366
|
+
throw new AcpAuthRequiredError(
|
|
367
|
+
authFailureMessage(agent.def.id, agent.def.label, full) || full,
|
|
368
|
+
{ agentId: agent.def.id, agentLabel: agent.def.label }
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
throw new Error(full);
|
|
296
372
|
}
|
|
297
373
|
}
|
|
298
374
|
|