@jpkme/diff-review 0.1.16 → 0.1.18
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 +220 -29
- 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.DdOl5Uve.css +1 -0
- package/web/build/client/_app/immutable/assets/0.DdOl5Uve.css.br +0 -0
- package/web/build/client/_app/immutable/assets/0.DdOl5Uve.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/{cYiFfRa3.js → -cMMFbSG.js} +1 -1
- package/web/build/client/_app/immutable/chunks/-cMMFbSG.js.br +2 -0
- package/web/build/client/_app/immutable/chunks/-cMMFbSG.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/0vBZPdKl.js +215 -0
- package/web/build/client/_app/immutable/chunks/0vBZPdKl.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/0vBZPdKl.js.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 → BMoYpzpA.js} +2 -2
- package/web/build/client/_app/immutable/chunks/BMoYpzpA.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/BMoYpzpA.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/entry/app.CT6P0WMK.js +2 -0
- package/web/build/client/_app/immutable/entry/app.CT6P0WMK.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.CT6P0WMK.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.U0TDB-jm.js +1 -0
- package/web/build/client/_app/immutable/entry/start.U0TDB-jm.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.U0TDB-jm.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.ii5weUjH.js +1 -0
- package/web/build/client/_app/immutable/nodes/0.ii5weUjH.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/0.ii5weUjH.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.CM10dYkJ.js +1 -0
- package/web/build/client/_app/immutable/nodes/1.CM10dYkJ.js.br +2 -0
- package/web/build/client/_app/immutable/nodes/1.CM10dYkJ.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.B1LHk3LQ.js +12 -0
- package/web/build/client/_app/immutable/nodes/2.B1LHk3LQ.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.B1LHk3LQ.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.BQOdjQBQ.js +340 -0
- package/web/build/client/_app/immutable/nodes/3.BQOdjQBQ.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.BQOdjQBQ.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-BUdytb9b.js +11 -0
- package/web/build/server/chunks/{0-D7NSxn1-.js.map → 0-BUdytb9b.js.map} +1 -1
- package/web/build/server/chunks/1-DRokSBkY.js +7 -0
- package/web/build/server/chunks/{1-DboYz8Fw.js.map → 1-DRokSBkY.js.map} +1 -1
- package/web/build/server/chunks/2-BxyzaJTR.js +7 -0
- package/web/build/server/chunks/2-BxyzaJTR.js.map +1 -0
- package/web/build/server/chunks/3-D8o0VWkj.js +7 -0
- package/web/build/server/chunks/{3-DgJ_LmTL.js.map → 3-D8o0VWkj.js.map} +1 -1
- package/web/build/server/chunks/{_server.ts-CP5cRLhW.js → _server.ts-kax5Szn0.js} +5 -4
- package/web/build/server/chunks/_server.ts-kax5Szn0.js.map +1 -0
- package/web/build/server/chunks/_server.ts-vS3A9t3f.js +123 -0
- package/web/build/server/chunks/_server.ts-vS3A9t3f.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-hZSqk_1P.js} +54 -21
- package/web/build/server/chunks/{git-review-jZ_JJYfv.js.map → git-review-hZSqk_1P.js.map} +1 -1
- package/web/build/server/chunks/{handler-B_8yUXji.js → handler-DbFqzg7l.js} +3 -3
- package/web/build/server/chunks/{handler-B_8yUXji.js.map → handler-DbFqzg7l.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-BB9DZBdR.js} +4 -4
- package/web/build/server/chunks/index.js-BB9DZBdR.js.map +1 -0
- package/web/build/server/chunks/manifest.js-Dk6YX1jw.js +65 -0
- package/web/build/server/chunks/manifest.js-Dk6YX1jw.js.map +1 -0
- package/web/build/server/chunks/{remote-1g4ayag-BMpjk8UD.js → remote-1g4ayag-Bz1vj8X1.js} +6 -6
- package/web/build/server/chunks/remote-1g4ayag-Bz1vj8X1.js.map +1 -0
- package/web/build/server/chunks/{remote-wh6t6j-dYLCg8ks.js → remote-wh6t6j-DSwM3S9h.js} +44 -10
- package/web/build/server/chunks/remote-wh6t6j-DSwM3S9h.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
|
@@ -13,13 +13,15 @@
|
|
|
13
13
|
|
|
14
14
|
import { spawn, spawnSync } from 'node:child_process';
|
|
15
15
|
import { randomUUID } from 'node:crypto';
|
|
16
|
-
import { existsSync } from 'node:fs';
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
17
|
import { createServer } from 'node:net';
|
|
18
18
|
import { dirname, join } from 'node:path';
|
|
19
19
|
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 { readModelsCache } from '../lib/models-cache.mjs';
|
|
24
|
+
import { isAuthRequiredError, withAgentLoginRetry } from '../lib/agent-login.mjs';
|
|
23
25
|
import { buildAcpModelSelectOptions, sortModelSelectOptions } from '../lib/model-label.mjs';
|
|
24
26
|
import { autocompleteEs } from '../lib/autocomplete-es.mjs';
|
|
25
27
|
import { copyTextToClipboard } from '../lib/clipboard.mjs';
|
|
@@ -32,6 +34,46 @@ import { existingReviewOutput, waitForReviewOutput } from '../lib/wait-output.mj
|
|
|
32
34
|
import { stampReviewProvenance } from '../lib/review-meta.mjs';
|
|
33
35
|
import { runValidateCli } from '../lib/check-report.mjs';
|
|
34
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Copilot: elegir github.com vs Enterprise (el usuario elige; no se fuerza por env).
|
|
39
|
+
* @returns {Promise<string | false>} host vacío = github.com; false = canceló
|
|
40
|
+
*/
|
|
41
|
+
async function pickCopilotLoginHost() {
|
|
42
|
+
const where = await p.select({
|
|
43
|
+
message: '¿Con qué cuenta de GitHub querés iniciar sesión?',
|
|
44
|
+
options: [
|
|
45
|
+
{
|
|
46
|
+
value: 'github.com',
|
|
47
|
+
label: 'GitHub.com',
|
|
48
|
+
hint: 'Personal u organización en github.com — elegí la cuenta en el browser'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
value: 'enterprise',
|
|
52
|
+
label: 'GitHub Enterprise',
|
|
53
|
+
hint: 'Host tipo empresa.ghe.com'
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
initialValue: 'github.com'
|
|
57
|
+
});
|
|
58
|
+
if (p.isCancel(where)) {
|
|
59
|
+
p.cancel('Cancelado.');
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (where === 'enterprise') {
|
|
63
|
+
const host = await p.text({
|
|
64
|
+
message: 'Hostname de Enterprise',
|
|
65
|
+
placeholder: 'empresa.ghe.com',
|
|
66
|
+
validate: (v) => (String(v || '').trim() ? undefined : 'Ingresá el hostname')
|
|
67
|
+
});
|
|
68
|
+
if (p.isCancel(host)) {
|
|
69
|
+
p.cancel('Cancelado.');
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return String(host).trim();
|
|
73
|
+
}
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
|
|
35
77
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
36
78
|
const packageRoot = join(here, '..');
|
|
37
79
|
// `DIFF_REVIEW_OUT=build-next` cuando `web/build` está bloqueado por un server en marcha (EBUSY en Windows).
|
|
@@ -43,6 +85,19 @@ const buildDir = (() => {
|
|
|
43
85
|
})();
|
|
44
86
|
const entry = join(buildDir, 'index.js');
|
|
45
87
|
|
|
88
|
+
/** Versión del paquete instalado (la del package.json que viaja con el CLI). */
|
|
89
|
+
const VERSION = (() => {
|
|
90
|
+
try {
|
|
91
|
+
return String(JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')).version || '');
|
|
92
|
+
} catch {
|
|
93
|
+
return '';
|
|
94
|
+
}
|
|
95
|
+
})();
|
|
96
|
+
const TITLE = VERSION ? `diff-review v${VERSION}` : 'diff-review';
|
|
97
|
+
|
|
98
|
+
/** Agentes cuyos modelos ya se consultaron en vivo en esta corrida: la sesión ACP anda y no hace falta otro probe. */
|
|
99
|
+
const liveModelsOk = new Set();
|
|
100
|
+
|
|
46
101
|
const DEFAULT_HOST = '127.0.0.1';
|
|
47
102
|
const DEFAULT_PORT = 5190;
|
|
48
103
|
|
|
@@ -62,7 +117,7 @@ function resolveNodeBin() {
|
|
|
62
117
|
}
|
|
63
118
|
|
|
64
119
|
function printHelp() {
|
|
65
|
-
console.log(
|
|
120
|
+
console.log(`${TITLE} — review de branch con tu agente + UI local.
|
|
66
121
|
|
|
67
122
|
Usage:
|
|
68
123
|
diff-review [options]
|
|
@@ -75,9 +130,10 @@ un solo select lista el portapapeles y cada agente detectado.
|
|
|
75
130
|
Si pasás --branch y --base juntos, saltea los selects.
|
|
76
131
|
Con --agent: saltea esa lista; elegís un agente instalado y el modelo,
|
|
77
132
|
y diff-review lo lanza con el prompt.
|
|
78
|
-
|
|
79
|
-
proceso en vivo y después podés preguntarle o
|
|
80
|
-
|
|
133
|
+
Los agentes integrados (Claude Code, Cursor CLI, Copilot) corren por ACP
|
|
134
|
+
desde la UI web: ves el proceso en vivo y después podés preguntarle o
|
|
135
|
+
pedirle cambios al reporte. Si falta login, la CLI ofrece abrirlo en una
|
|
136
|
+
terminal y reintentar. Con --headless corren en la terminal.
|
|
81
137
|
Con --ui: abre la UI de inmediato (web-first).
|
|
82
138
|
|
|
83
139
|
Options:
|
|
@@ -89,6 +145,7 @@ Options:
|
|
|
89
145
|
--agent Lanzar un agente instalado (${AGENTS.map((a) => a.bins[0]).join(', ')}) con el prompt
|
|
90
146
|
--headless Con agente: correrlo en la terminal (sin ACP ni chat en la web)
|
|
91
147
|
--no-open No abrir el navegador
|
|
148
|
+
-v, --version Mostrar la versión
|
|
92
149
|
-h, --help Esta ayuda
|
|
93
150
|
|
|
94
151
|
Requiere git en PATH. Los reportes viven en localStorage del navegador.
|
|
@@ -101,6 +158,7 @@ function parseArgs(argv) {
|
|
|
101
158
|
host: DEFAULT_HOST,
|
|
102
159
|
open: true,
|
|
103
160
|
help: false,
|
|
161
|
+
version: false,
|
|
104
162
|
ui: false,
|
|
105
163
|
agent: false,
|
|
106
164
|
headless: false,
|
|
@@ -110,6 +168,7 @@ function parseArgs(argv) {
|
|
|
110
168
|
for (let i = 0; i < argv.length; i++) {
|
|
111
169
|
const a = argv[i];
|
|
112
170
|
if (a === '-h' || a === '--help') opts.help = true;
|
|
171
|
+
else if (a === '-v' || a === '--version') opts.version = true;
|
|
113
172
|
else if (a === '--no-open') opts.open = false;
|
|
114
173
|
else if (a === '--ui') opts.ui = true;
|
|
115
174
|
else if (a === '--agent') opts.agent = true;
|
|
@@ -257,7 +316,7 @@ async function resolveBranchBase(launch, opts, pickOpts = {}) {
|
|
|
257
316
|
|
|
258
317
|
const options = branchOptions(launch.listed);
|
|
259
318
|
if (!pickOpts.skipIntro) {
|
|
260
|
-
p.intro(
|
|
319
|
+
p.intro(TITLE);
|
|
261
320
|
p.log.info(launch.repo);
|
|
262
321
|
}
|
|
263
322
|
|
|
@@ -405,6 +464,7 @@ function copyPromptOrWarn(prompt) {
|
|
|
405
464
|
|
|
406
465
|
/**
|
|
407
466
|
* Opciones de modelo: las que ofrece el agente por ACP (si va a correr así) o, si no, la lista fija.
|
|
467
|
+
* 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
468
|
* @param {import('../lib/agents.mjs').DetectedAgent} agent
|
|
409
469
|
* @param {{ acpCwd?: string }} opts
|
|
410
470
|
*/
|
|
@@ -415,26 +475,82 @@ async function modelOptions(agent, { acpCwd }) {
|
|
|
415
475
|
options: sortModelSelectOptions(fixed.options)
|
|
416
476
|
};
|
|
417
477
|
if (!acpCwd) return sortedFixed;
|
|
478
|
+
|
|
479
|
+
// Siempre se le pregunta al agente (tarda unos segundos, pero la lista es la real); la caché solo
|
|
480
|
+
// sirve de respaldo si la consulta falla.
|
|
481
|
+
const fetchModels = () => listAcpModelsCached(agent, { cwd: acpCwd, force: true });
|
|
482
|
+
|
|
483
|
+
/** Última lista conocida, aunque tenga más de 24 h; si no hay, la fija. */
|
|
484
|
+
const fallback = () => {
|
|
485
|
+
const hit = readModelsCache(agent.def.id, { ttlMs: Infinity });
|
|
486
|
+
if (!hit?.models.length) {
|
|
487
|
+
p.log.warn('Uso la lista fija de modelos.');
|
|
488
|
+
return sortedFixed;
|
|
489
|
+
}
|
|
490
|
+
p.log.warn(`Uso la última lista conocida (${hit.fetchedAt.slice(0, 10)}).`);
|
|
491
|
+
return buildAcpModelSelectOptions({ models: hit.models, currentModel: hit.currentModel }, fixed);
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
/** @param {{ models: { value: string, name: string }[], currentModel: string }} result */
|
|
495
|
+
const fromLive = (result) => {
|
|
496
|
+
liveModelsOk.add(agent.def.id);
|
|
497
|
+
if (!result.models.length) return sortedFixed;
|
|
498
|
+
return buildAcpModelSelectOptions(
|
|
499
|
+
{ models: result.models, currentModel: result.currentModel },
|
|
500
|
+
fixed
|
|
501
|
+
);
|
|
502
|
+
};
|
|
503
|
+
|
|
418
504
|
const spin = p.spinner();
|
|
419
505
|
spin.start(`Consultando los modelos de ${agent.def.label}`);
|
|
420
506
|
try {
|
|
421
|
-
const
|
|
422
|
-
spin.stop(
|
|
423
|
-
|
|
424
|
-
? `Modelos de ${agent.def.label} (caché)`
|
|
425
|
-
: `Modelos de ${agent.def.label}`
|
|
426
|
-
);
|
|
427
|
-
if (!models.length) return sortedFixed;
|
|
428
|
-
// Misma fusión/labels/orden que la UI.
|
|
429
|
-
return buildAcpModelSelectOptions({ models, currentModel }, fixed);
|
|
507
|
+
const result = await fetchModels();
|
|
508
|
+
spin.stop(`Modelos de ${agent.def.label} (${result.models.length})`);
|
|
509
|
+
return fromLive(result);
|
|
430
510
|
} catch (err) {
|
|
431
|
-
|
|
432
|
-
|
|
511
|
+
const detail = err instanceof Error ? err.message.split('\n')[0] : String(err);
|
|
512
|
+
spin.stop(`No pude consultar los modelos (${detail}).`);
|
|
513
|
+
|
|
514
|
+
if (!isInteractive() || !isAuthRequiredError(err)) return fallback();
|
|
515
|
+
|
|
516
|
+
const outcome = await withAgentLoginRetry(() => fetchModels(), {
|
|
517
|
+
agent,
|
|
518
|
+
cwd: acpCwd,
|
|
519
|
+
priorError: err,
|
|
520
|
+
askToLogin: async ({ message, agentLabel, displayCmd }) => {
|
|
521
|
+
p.log.warn(message);
|
|
522
|
+
const answer = await p.confirm({
|
|
523
|
+
message: `¿Querés iniciar sesión en ${agentLabel} ahora?`,
|
|
524
|
+
initialValue: true
|
|
525
|
+
});
|
|
526
|
+
if (p.isCancel(answer)) {
|
|
527
|
+
p.cancel('Cancelado.');
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
if (displayCmd) p.log.step(displayCmd);
|
|
531
|
+
return Boolean(answer);
|
|
532
|
+
},
|
|
533
|
+
pickCopilotHost: pickCopilotLoginHost,
|
|
534
|
+
onStatus: (msg) => p.log.info(msg)
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
if (outcome.ok) {
|
|
538
|
+
p.log.success(`Modelos de ${agent.def.label} (${outcome.value.models.length})`);
|
|
539
|
+
return fromLive(outcome.value);
|
|
540
|
+
}
|
|
541
|
+
if (outcome.cancelled) {
|
|
542
|
+
p.log.warn('Podés autenticarte después desde la web.');
|
|
543
|
+
return fallback();
|
|
544
|
+
}
|
|
545
|
+
const after = outcome.error instanceof Error ? outcome.error.message.split('\n')[0] : String(outcome.error);
|
|
546
|
+
p.log.warn(`No pude consultar los modelos tras el login (${after}).`);
|
|
547
|
+
return fallback();
|
|
433
548
|
}
|
|
434
549
|
}
|
|
435
550
|
|
|
436
551
|
/**
|
|
437
552
|
* Modelo del agente: autocomplete con búsqueda; sin TTY, el default (string vacío).
|
|
553
|
+
* `""` es válido (= "Por defecto del agente", no se pasa --model); no usar ensureValue.
|
|
438
554
|
* @param {import('../lib/agents.mjs').DetectedAgent} agent
|
|
439
555
|
* @param {{ acpCwd?: string }} [opts] con `acpCwd`, la lista sale del agente por ACP
|
|
440
556
|
*/
|
|
@@ -442,17 +558,17 @@ async function pickModel(agent, opts = {}) {
|
|
|
442
558
|
if (!isInteractive()) return '';
|
|
443
559
|
|
|
444
560
|
const { options, initialValue } = await modelOptions(agent, opts);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
);
|
|
561
|
+
const value = await autocompleteEs({
|
|
562
|
+
message: 'Modelo',
|
|
563
|
+
options,
|
|
564
|
+
initialValue,
|
|
565
|
+
placeholder: 'opus, sonnet, gpt…'
|
|
566
|
+
});
|
|
567
|
+
if (p.isCancel(value)) {
|
|
568
|
+
p.cancel('Cancelado.');
|
|
569
|
+
process.exit(0);
|
|
570
|
+
}
|
|
571
|
+
return /** @type {string} */ (value ?? '');
|
|
456
572
|
}
|
|
457
573
|
|
|
458
574
|
/**
|
|
@@ -572,6 +688,76 @@ async function runAgentAndWait(agent, { repo, prompt, model, outputFilename, use
|
|
|
572
688
|
return /** @type {{ path: string }} */ (result);
|
|
573
689
|
}
|
|
574
690
|
|
|
691
|
+
/**
|
|
692
|
+
* Antes de abrir la web: forzar un probe ACP (sin caché) y, si hace falta auth, login in-flow en esta terminal.
|
|
693
|
+
* Así no llegamos a LiveReviewView sin sesión cuando el usuario ya eligió el agente en la CLI.
|
|
694
|
+
* @param {import('../lib/agents.mjs').DetectedAgent} agent
|
|
695
|
+
* @param {string} repo
|
|
696
|
+
* @param {boolean} useClack
|
|
697
|
+
* @returns {Promise<boolean>} true si el probe quedó ok (logueado o ya lo estaba)
|
|
698
|
+
*/
|
|
699
|
+
async function ensureAgentAuthBeforeUi(agent, repo, useClack) {
|
|
700
|
+
// Si recién listamos los modelos en vivo, la sesión ya anduvo: no hace falta arrancar el agente otra vez.
|
|
701
|
+
if (liveModelsOk.has(agent.def.id)) return true;
|
|
702
|
+
const probe = () => listAcpModelsCached(agent, { cwd: repo, force: true });
|
|
703
|
+
try {
|
|
704
|
+
await probe();
|
|
705
|
+
return true;
|
|
706
|
+
} catch (err) {
|
|
707
|
+
if (!isInteractive() || !isAuthRequiredError(err)) {
|
|
708
|
+
const detail = err instanceof Error ? err.message.split('\n')[0] : String(err);
|
|
709
|
+
if (useClack) p.log.warn(detail);
|
|
710
|
+
else console.warn(detail);
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const outcome = await withAgentLoginRetry(() => probe(), {
|
|
715
|
+
agent,
|
|
716
|
+
cwd: repo,
|
|
717
|
+
priorError: err,
|
|
718
|
+
askToLogin: async ({ message, agentLabel }) => {
|
|
719
|
+
if (useClack) {
|
|
720
|
+
p.log.warn(message);
|
|
721
|
+
const answer = await p.confirm({
|
|
722
|
+
message: `¿Querés iniciar sesión en ${agentLabel} ahora?`,
|
|
723
|
+
initialValue: true
|
|
724
|
+
});
|
|
725
|
+
if (p.isCancel(answer)) {
|
|
726
|
+
p.cancel('Cancelado.');
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
return Boolean(answer);
|
|
730
|
+
}
|
|
731
|
+
console.warn(message);
|
|
732
|
+
return true;
|
|
733
|
+
},
|
|
734
|
+
pickCopilotHost: useClack ? pickCopilotLoginHost : async () => '',
|
|
735
|
+
onStatus: (msg) => {
|
|
736
|
+
if (useClack) p.log.info(msg);
|
|
737
|
+
else console.log(msg);
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
if (outcome.ok) {
|
|
742
|
+
if (useClack) p.log.success(`Sesión de ${agent.def.label} lista.`);
|
|
743
|
+
else console.log(`Sesión de ${agent.def.label} lista.`);
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
if (outcome.cancelled) {
|
|
747
|
+
const warn =
|
|
748
|
+
'Sin iniciar sesión: la web va a pedir autenticación al lanzar el agente.';
|
|
749
|
+
if (useClack) p.log.warn(warn);
|
|
750
|
+
else console.warn(warn);
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
const after =
|
|
754
|
+
outcome.error instanceof Error ? outcome.error.message.split('\n')[0] : String(outcome.error);
|
|
755
|
+
if (useClack) p.log.warn(after);
|
|
756
|
+
else console.warn(after);
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
575
761
|
/**
|
|
576
762
|
* El agente corre dentro del server de la UI (ACP): la web muestra el proceso y abre el reporte; la terminal ve un
|
|
577
763
|
* resumen. La sesión queda viva para preguntas y cambios desde el reporte.
|
|
@@ -580,6 +766,7 @@ async function runAgentAndWait(agent, { repo, prompt, model, outputFilename, use
|
|
|
580
766
|
* @param {{ host: string, port: number, open: boolean }} opts
|
|
581
767
|
*/
|
|
582
768
|
async function runAgentInUi(agent, { repo, branch, base, model, outputFilename, useClack }, opts) {
|
|
769
|
+
await ensureAgentAuthBeforeUi(agent, repo, useClack);
|
|
583
770
|
const id = randomUUID();
|
|
584
771
|
const msg = `Lanzando ${agent.def.label} (${model || 'modelo por defecto'}). El progreso se ve en la web; Ctrl+C corta.`;
|
|
585
772
|
if (useClack) p.outro(msg);
|
|
@@ -746,6 +933,10 @@ async function main() {
|
|
|
746
933
|
}
|
|
747
934
|
|
|
748
935
|
const opts = parseArgs(rawArgv);
|
|
936
|
+
if (opts.version) {
|
|
937
|
+
console.log(VERSION || 'desconocida');
|
|
938
|
+
process.exit(0);
|
|
939
|
+
}
|
|
749
940
|
if (opts.help) {
|
|
750
941
|
printHelp();
|
|
751
942
|
process.exit(0);
|
|
@@ -844,7 +1035,7 @@ async function main() {
|
|
|
844
1035
|
const existing = isInteractive() ? existingReviewOutput(launch.repo, outputFilename) : null;
|
|
845
1036
|
const useClack = prompted || (isInteractive() && (detected.length > 0 || existing != null));
|
|
846
1037
|
// Con flags de branch/base no hubo intro, pero estos selects abren sesión de Clack igual.
|
|
847
|
-
if (useClack && !prompted) p.intro(
|
|
1038
|
+
if (useClack && !prompted) p.intro(TITLE);
|
|
848
1039
|
|
|
849
1040
|
let result = existing ? await reuseExistingReview(existing, outputFilename) : null;
|
|
850
1041
|
/** @type {import('../lib/agents.mjs').DetectedAgent | null} */
|
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
|
|