@jpkme/diff-review 0.1.7 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -15
- package/bin/diff-review.mjs +156 -34
- package/lib/acp-policy.mjs +136 -0
- package/lib/acp-policy.test.mjs +83 -0
- package/lib/acp-shell.mjs +379 -0
- package/lib/acp-shell.test.mjs +127 -0
- package/lib/acp.mjs +368 -0
- package/lib/acp.test.mjs +58 -0
- package/lib/agent-login.mjs +105 -0
- package/lib/agents.mjs +102 -14
- package/lib/agents.test.mjs +4 -6
- package/lib/check-report.mjs +31 -12
- package/lib/check-report.test.mjs +1 -0
- package/lib/extract-cache.test.mjs +162 -0
- package/lib/extract.mjs +197 -1
- package/lib/model-label.mjs +263 -0
- package/lib/model-label.test.mjs +77 -0
- package/lib/models-cache.mjs +75 -0
- package/lib/models-cache.test.mjs +50 -0
- package/lib/output-name.mjs +8 -0
- package/lib/prompt.mjs +178 -31
- package/lib/review-dir.mjs +108 -0
- package/lib/review-meta.mjs +35 -0
- package/lib/review-meta.test.mjs +41 -0
- package/lib/review-prep.mjs +63 -0
- package/lib/review.test.mjs +1 -0
- package/lib/validate.mjs +28 -11
- package/lib/validate.test.mjs +25 -0
- package/lib/wait-output.mjs +46 -23
- package/lib/wait-output.test.mjs +25 -1
- package/package.json +3 -1
- package/schema.json +9 -10
- package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css +1 -0
- package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css +1 -0
- package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css +1 -0
- package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css.br +0 -0
- package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/B5ByWcPE.js +2 -0
- package/web/build/client/_app/immutable/chunks/B5ByWcPE.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/B5ByWcPE.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/CPyWH418.js +1 -0
- package/web/build/client/_app/immutable/chunks/CPyWH418.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/CPyWH418.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/D8WOD_eS.js +3 -0
- package/web/build/client/_app/immutable/chunks/D8WOD_eS.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/D8WOD_eS.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/NwkMsx0h.js +210 -0
- package/web/build/client/_app/immutable/chunks/NwkMsx0h.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/NwkMsx0h.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js +1 -0
- package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.BiBohpyO.js +2 -0
- package/web/build/client/_app/immutable/entry/app.BiBohpyO.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.BiBohpyO.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js +1 -0
- package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.Cak4ehiE.js +1 -0
- package/web/build/client/_app/immutable/nodes/0.Cak4ehiE.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/0.Cak4ehiE.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js +1 -0
- package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.br +2 -0
- package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js +10 -0
- package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js +171 -0
- package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js.gz +0 -0
- package/web/build/client/_app/version.json +1 -1
- package/web/build/client/_app/version.json.br +0 -0
- package/web/build/client/_app/version.json.gz +0 -0
- package/web/build/handler.js +3 -3
- package/web/build/index.js +3 -3
- package/web/build/server/chunks/{0-SseDy__O.js → 0-BYok4D0n.js} +2 -2
- package/web/build/server/chunks/{0-SseDy__O.js.map → 0-BYok4D0n.js.map} +1 -1
- package/web/build/server/chunks/1-C7zcB0q0.js +7 -0
- package/web/build/server/chunks/1-C7zcB0q0.js.map +1 -0
- package/web/build/server/chunks/2-v6ejI_0-.js +7 -0
- package/web/build/server/chunks/2-v6ejI_0-.js.map +1 -0
- package/web/build/server/chunks/3-DAAmG6VX.js +7 -0
- package/web/build/server/chunks/3-DAAmG6VX.js.map +1 -0
- package/web/build/server/chunks/_server.ts-qYVrMYPB.js +62 -0
- package/web/build/server/chunks/_server.ts-qYVrMYPB.js.map +1 -0
- package/web/build/server/chunks/agent-sessions-DSqGavSn.js +2688 -0
- package/web/build/server/chunks/agent-sessions-DSqGavSn.js.map +1 -0
- package/web/build/server/chunks/{remote-1g4ayag-B8KZPTkn.js → git-review-CpO_G8z8.js} +70 -704
- package/web/build/server/chunks/git-review-CpO_G8z8.js.map +1 -0
- package/web/build/server/chunks/{handler-cq3QoEzW.js → handler-Di1wJQuU.js} +3 -3
- package/web/build/server/chunks/{handler-cq3QoEzW.js.map → handler-Di1wJQuU.js.map} +1 -1
- package/web/build/server/chunks/hooks.server-BLOHobEK.js +42 -0
- package/web/build/server/chunks/hooks.server-BLOHobEK.js.map +1 -0
- package/web/build/server/chunks/{index.js-D4qkHe3r.js → index.js-yLYmrAWy.js} +3 -5
- package/web/build/server/chunks/index.js-yLYmrAWy.js.map +1 -0
- package/web/build/server/chunks/manifest.js-CXj8IIo3.js +58 -0
- package/web/build/server/chunks/manifest.js-CXj8IIo3.js.map +1 -0
- package/web/build/server/chunks/remote-1g4ayag-BiJeU0Pk.js +144 -0
- package/web/build/server/chunks/remote-1g4ayag-BiJeU0Pk.js.map +1 -0
- package/web/build/server/chunks/remote-wh6t6j-CHanXurm.js +162 -0
- package/web/build/server/chunks/remote-wh6t6j-CHanXurm.js.map +1 -0
- package/web/build/server/chunks/validate-BsTKyhsq.js +1073 -0
- package/web/build/server/chunks/validate-BsTKyhsq.js.map +1 -0
- package/web/build/client/_app/immutable/assets/2.COsJT9ly.css +0 -1
- package/web/build/client/_app/immutable/assets/2.COsJT9ly.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.COsJT9ly.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.qhPDz1xQ.css +0 -1
- package/web/build/client/_app/immutable/assets/3.qhPDz1xQ.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.qhPDz1xQ.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/ThemeToggle.DcT79vPK.css +0 -1
- package/web/build/client/_app/immutable/assets/ThemeToggle.DcT79vPK.css.br +0 -0
- package/web/build/client/_app/immutable/assets/ThemeToggle.DcT79vPK.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/C4--ziCr.js +0 -2
- package/web/build/client/_app/immutable/chunks/C4--ziCr.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/C4--ziCr.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/D8sAxZVo.js +0 -174
- package/web/build/client/_app/immutable/chunks/D8sAxZVo.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/D8sAxZVo.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/Dur-vdUf.js +0 -3
- package/web/build/client/_app/immutable/chunks/Dur-vdUf.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/Dur-vdUf.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/tYeNsyIK.js +0 -1
- package/web/build/client/_app/immutable/chunks/tYeNsyIK.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/tYeNsyIK.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.Dl9OTE-n.js +0 -2
- package/web/build/client/_app/immutable/entry/app.Dl9OTE-n.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.Dl9OTE-n.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.CDz8Qs8Q.js +0 -1
- package/web/build/client/_app/immutable/entry/start.CDz8Qs8Q.js.br +0 -1
- package/web/build/client/_app/immutable/entry/start.CDz8Qs8Q.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.s5xmjXoS.js +0 -1
- package/web/build/client/_app/immutable/nodes/0.s5xmjXoS.js.br +0 -1
- package/web/build/client/_app/immutable/nodes/0.s5xmjXoS.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.B1c4AH1e.js +0 -1
- package/web/build/client/_app/immutable/nodes/1.B1c4AH1e.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/1.B1c4AH1e.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.Dx-oOpEf.js +0 -66
- package/web/build/client/_app/immutable/nodes/2.Dx-oOpEf.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.Dx-oOpEf.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.C-USexyX.js +0 -170
- package/web/build/client/_app/immutable/nodes/3.C-USexyX.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.C-USexyX.js.gz +0 -0
- package/web/build/server/chunks/1-0X6Jp9nH.js +0 -7
- package/web/build/server/chunks/1-0X6Jp9nH.js.map +0 -1
- package/web/build/server/chunks/2-HlcVALg6.js +0 -7
- package/web/build/server/chunks/2-HlcVALg6.js.map +0 -1
- package/web/build/server/chunks/3-BN1eS7L2.js +0 -7
- package/web/build/server/chunks/3-BN1eS7L2.js.map +0 -1
- package/web/build/server/chunks/hooks.server-DERFlOe1.js +0 -14
- package/web/build/server/chunks/hooks.server-DERFlOe1.js.map +0 -1
- package/web/build/server/chunks/index.js-D4qkHe3r.js.map +0 -1
- package/web/build/server/chunks/manifest.js-DQHMKyJh.js +0 -50
- package/web/build/server/chunks/manifest.js-DQHMKyJh.js.map +0 -1
- package/web/build/server/chunks/remote-1g4ayag-B8KZPTkn.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# diff-review
|
|
2
2
|
|
|
3
|
-
App local para revisar un branch
|
|
4
|
-
|
|
5
|
-
los diffs con `git` y te deja triager hallazgos.
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
App local para revisar un branch con tu agente (Claude, Cursor, Copilot, etc.):
|
|
4
|
+
lanzás la review desde la UI o la CLI, el agente escribe el JSON, la UI completa
|
|
5
|
+
los diffs con `git` y te deja triager hallazgos. Por ACP ves el proceso en vivo y
|
|
6
|
+
después podés preguntarle o pedirle cambios al reporte desde la web. También podés
|
|
7
|
+
copiar el prompt y correrlo en otro agente a mano.
|
|
8
8
|
|
|
9
9
|
## Instalación
|
|
10
10
|
|
|
@@ -27,8 +27,8 @@ npx @jpkme/diff-review
|
|
|
27
27
|
Desde un **repo git**, el default es terminal-first:
|
|
28
28
|
|
|
29
29
|
1. Elegís **branch** y **base** con prompts en la terminal (`@clack/prompts`; tipeá para filtrar).
|
|
30
|
-
2.
|
|
31
|
-
3.
|
|
30
|
+
2. Si hay agentes instalados, el default es lanzar uno (ACP en la web cuando aplica); también podés copiar el prompt al portapapeles.
|
|
31
|
+
3. El agente escribe `diff-rev_{branchTip}_{baseTip}.json`; la CLI valida y abre el viewer.
|
|
32
32
|
|
|
33
33
|
Si pasás `--branch` y `--base` juntos, saltea los selects. Si quedan iguales (diff vacío), abre la UI.
|
|
34
34
|
|
|
@@ -38,8 +38,8 @@ caso el archivo viejo no cuenta hasta que el agente lo reescriba. Sin terminal i
|
|
|
38
38
|
|
|
39
39
|
### Lanzar el agente (`--agent`)
|
|
40
40
|
|
|
41
|
-
Si hay agentes instalados, después de branch y base un solo select lista
|
|
42
|
-
|
|
41
|
+
Si hay agentes instalados, después de branch y base un solo select lista cada agente detectado (default el primero)
|
|
42
|
+
y, al final, copiar el prompt al portapapeles. Con `--agent` saltea esa lista y va directo a elegir agente y modelo:
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
45
|
diff-review --agent
|
|
@@ -63,13 +63,38 @@ instalados, sigue con el portapapeles. Sin terminal interactiva usa el primer ag
|
|
|
63
63
|
|
|
64
64
|
El agente tiene que estar logueado (p. ej. `claude /login` una vez).
|
|
65
65
|
|
|
66
|
+
#### Por ACP, con la web (Claude Code, Cursor CLI, GitHub Copilot CLI)
|
|
67
|
+
|
|
68
|
+
Si el agente elegido habla ACP, la CLI abre la UI enseguida y el agente corre dentro del server local. Claude Code
|
|
69
|
+
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. Codex y Gemini siguen por la terminal.
|
|
71
|
+
|
|
72
|
+
- El select de modelo muestra los que ofrece cada agente (se consultan por ACP la primera vez).
|
|
73
|
+
- La web muestra lo que va haciendo (comandos, archivos que lee, plan). La terminal solo avisa fallos duros o JSON válido.
|
|
74
|
+
- Al terminar cada turno, el server valida el JSON; si falta algo se lo pide al agente una vez y, cuando valida,
|
|
75
|
+
la web abre el reporte.
|
|
76
|
+
- Cada pedido de permiso pasa por una política fija: leer dentro del repo, escribir únicamente el JSON del review
|
|
77
|
+
y comandos de solo lectura: `git` (diff, log, show, blame…), `cat`, `head`, `tail`, `grep`, `rg`, `find`, `ls`,
|
|
78
|
+
`wc`, `sed -n 'N,Mp'` y sus equivalentes de PowerShell, encadenados con `|`, `&&`, `||` o `;`, con los paths
|
|
79
|
+
dentro del repo. Se rechazan redirecciones a archivos (salvo `/dev/null`), `$(…)`, red, instalaciones y todo lo
|
|
80
|
+
demás; el agente recibe las reglas en el prompt para no intentarlo.
|
|
81
|
+
- La allowlist propia de cada agente no se usa en estas sesiones (aprobaría por nombre de comando, p. ej.
|
|
82
|
+
`echo x > archivo`): Claude corre sin cargar `~/.claude/settings.json` ni el `.claude/` del repo, y Cursor con
|
|
83
|
+
una copia de su configuración sin `permissions.allow` (en el temp del sistema; el login se conserva).
|
|
84
|
+
- La sesión queda viva: en el reporte, el botón **Agente** abre un chat para preguntarle sobre la review
|
|
85
|
+
(no puede escribir nada) o pedirle cambios al JSON (el reporte se actualiza y el triage de los hallazgos que
|
|
86
|
+
siguen se conserva). Si el server se reinició, la sesión se retoma con `session/load`.
|
|
87
|
+
|
|
88
|
+
También se puede lanzar desde la UI, en "2. Generar review" → **Lanzar agente**. Con `--headless` corre en la
|
|
89
|
+
terminal como el resto de los agentes.
|
|
90
|
+
|
|
66
91
|
Para abrir la UI primero (sin esperar el JSON):
|
|
67
92
|
|
|
68
93
|
```bash
|
|
69
94
|
diff-review --ui
|
|
70
95
|
```
|
|
71
96
|
|
|
72
|
-
Opciones: `--port` (si está ocupado, usa el siguiente libre), `--host`, `--base`, `--branch`, `--agent`, `--no-open`.
|
|
97
|
+
Opciones: `--port` (si está ocupado, usa el siguiente libre), `--host`, `--base`, `--branch`, `--agent`, `--headless`, `--no-open`.
|
|
73
98
|
|
|
74
99
|
Si lo lanzás desde un repositorio git, la UI precarga esa ruta y el branch actual.
|
|
75
100
|
|
|
@@ -85,11 +110,9 @@ npm start # mismo que el bin global
|
|
|
85
110
|
## Flujo (UI)
|
|
86
111
|
|
|
87
112
|
1. Pegá la ruta (o URL) del repo y elegí branch / base — o lanzá `diff-review` / `diff-review --ui` desde el repo.
|
|
88
|
-
2.
|
|
89
|
-
3.
|
|
90
|
-
4.
|
|
91
|
-
|
|
92
|
-
"Ver ejemplo" carga un reporte de muestra sin tocar un repo.
|
|
113
|
+
2. Si hay agentes ACP, el default es **Lanzar agente**; si no, usá **Copiar prompt** y pegalo en tu agente.
|
|
114
|
+
3. El agente escribe `.diff-review/diff-rev_{branchTip}_{baseTip}.json` (en sesiones ACP el server valida solo; si corriste el prompt a mano, `npx --yes @jpkme/diff-review validate <archivo>`).
|
|
115
|
+
4. Con agente integrado o CLI el reporte se abre solo; si no, arrastrá el JSON a la dropzone. La UI hidrata los diffs con git (reusa el companion `.diff` si existe).
|
|
93
116
|
|
|
94
117
|
## Qué hace git (en tu máquina)
|
|
95
118
|
|
package/bin/diff-review.mjs
CHANGED
|
@@ -7,27 +7,40 @@
|
|
|
7
7
|
* diff-review --port 5191
|
|
8
8
|
* diff-review --branch x --base y → saltea selects de Clack
|
|
9
9
|
* diff-review --agent → elegir agente + modelo y lanzarlo con el prompt (sin portapapeles)
|
|
10
|
+
* diff-review --headless → con agente: correrlo en la terminal en vez de por ACP con la web
|
|
10
11
|
* diff-review --no-open
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { spawn, spawnSync } from 'node:child_process';
|
|
15
|
+
import { randomUUID } from 'node:crypto';
|
|
14
16
|
import { existsSync } from 'node:fs';
|
|
15
17
|
import { createServer } from 'node:net';
|
|
16
18
|
import { dirname, join } from 'node:path';
|
|
17
19
|
import { fileURLToPath } from 'node:url';
|
|
18
20
|
import * as p from '@clack/prompts';
|
|
19
|
-
import { AGENTS, detectAgents, modelChoices,
|
|
21
|
+
import { AGENTS, detectAgents, modelChoices, resolvedRunModel, runAgent } from '../lib/agents.mjs';
|
|
22
|
+
import { listAcpModelsCached, supportsAcp } from '../lib/acp.mjs';
|
|
23
|
+
import { buildAcpModelSelectOptions, sortModelSelectOptions } from '../lib/model-label.mjs';
|
|
20
24
|
import { autocompleteEs } from '../lib/autocomplete-es.mjs';
|
|
21
25
|
import { copyTextToClipboard } from '../lib/clipboard.mjs';
|
|
22
26
|
import { branchTips, getRemoteUrl, listBranches, tryResolveGitRoot } from '../lib/git.mjs';
|
|
23
27
|
import { makeOutputFilename } from '../lib/output-name.mjs';
|
|
24
|
-
import { buildPrompt } from '../lib/prompt.mjs';
|
|
28
|
+
import { buildPrompt, outputRelPath } from '../lib/prompt.mjs';
|
|
29
|
+
import { prepareReviewPromptOpts } from '../lib/review-prep.mjs';
|
|
30
|
+
import { ensureReviewDir, writeDiffCache } from '../lib/review-dir.mjs';
|
|
25
31
|
import { existingReviewOutput, waitForReviewOutput } from '../lib/wait-output.mjs';
|
|
32
|
+
import { stampReviewProvenance } from '../lib/review-meta.mjs';
|
|
26
33
|
import { runValidateCli } from '../lib/check-report.mjs';
|
|
27
34
|
|
|
28
35
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
29
36
|
const packageRoot = join(here, '..');
|
|
30
|
-
|
|
37
|
+
// `DIFF_REVIEW_OUT=build-next` cuando `web/build` está bloqueado por un server en marcha (EBUSY en Windows).
|
|
38
|
+
const buildDir = (() => {
|
|
39
|
+
const primary = join(packageRoot, 'web', 'build');
|
|
40
|
+
const overlay = join(packageRoot, 'web', 'build-next');
|
|
41
|
+
if (existsSync(join(overlay, 'index.js'))) return overlay;
|
|
42
|
+
return primary;
|
|
43
|
+
})();
|
|
31
44
|
const entry = join(buildDir, 'index.js');
|
|
32
45
|
|
|
33
46
|
const DEFAULT_HOST = '127.0.0.1';
|
|
@@ -62,6 +75,9 @@ un solo select lista el portapapeles y cada agente detectado.
|
|
|
62
75
|
Si pasás --branch y --base juntos, saltea los selects.
|
|
63
76
|
Con --agent: saltea esa lista; elegís un agente instalado y el modelo,
|
|
64
77
|
y diff-review lo lanza con el prompt.
|
|
78
|
+
Si el agente habla ACP (Claude Code), corre desde la UI web: ves el
|
|
79
|
+
proceso en vivo y después podés preguntarle o pedirle cambios al reporte.
|
|
80
|
+
Con --headless corre en la terminal como antes.
|
|
65
81
|
Con --ui: abre la UI de inmediato (web-first).
|
|
66
82
|
|
|
67
83
|
Options:
|
|
@@ -71,6 +87,7 @@ Options:
|
|
|
71
87
|
--base <b> Base del merge-base (salta el select de base)
|
|
72
88
|
--branch <b> Branch a revisar (salta el select de branch)
|
|
73
89
|
--agent Lanzar un agente instalado (${AGENTS.map((a) => a.bins[0]).join(', ')}) con el prompt
|
|
90
|
+
--headless Con agente: correrlo en la terminal (sin ACP ni chat en la web)
|
|
74
91
|
--no-open No abrir el navegador
|
|
75
92
|
-h, --help Esta ayuda
|
|
76
93
|
|
|
@@ -86,6 +103,7 @@ function parseArgs(argv) {
|
|
|
86
103
|
help: false,
|
|
87
104
|
ui: false,
|
|
88
105
|
agent: false,
|
|
106
|
+
headless: false,
|
|
89
107
|
base: '',
|
|
90
108
|
branch: ''
|
|
91
109
|
};
|
|
@@ -95,6 +113,7 @@ function parseArgs(argv) {
|
|
|
95
113
|
else if (a === '--no-open') opts.open = false;
|
|
96
114
|
else if (a === '--ui') opts.ui = true;
|
|
97
115
|
else if (a === '--agent') opts.agent = true;
|
|
116
|
+
else if (a === '--headless') opts.headless = true;
|
|
98
117
|
else if (a === '--host') opts.host = argv[++i] || opts.host;
|
|
99
118
|
else if (a === '--base') opts.base = argv[++i] || opts.base;
|
|
100
119
|
else if (a === '--branch') opts.branch = argv[++i] || opts.branch;
|
|
@@ -307,28 +326,55 @@ function copyPromptOrWarn(prompt) {
|
|
|
307
326
|
}
|
|
308
327
|
|
|
309
328
|
/**
|
|
310
|
-
*
|
|
329
|
+
* Opciones de modelo: las que ofrece el agente por ACP (si va a correr así) o, si no, la lista fija.
|
|
311
330
|
* @param {import('../lib/agents.mjs').DetectedAgent} agent
|
|
331
|
+
* @param {{ acpCwd?: string }} opts
|
|
312
332
|
*/
|
|
313
|
-
async function
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
333
|
+
async function modelOptions(agent, { acpCwd }) {
|
|
334
|
+
const fixed = modelChoices(agent.def);
|
|
335
|
+
const sortedFixed = {
|
|
336
|
+
...fixed,
|
|
337
|
+
options: sortModelSelectOptions(fixed.options)
|
|
338
|
+
};
|
|
339
|
+
if (!acpCwd) return sortedFixed;
|
|
340
|
+
const spin = p.spinner();
|
|
341
|
+
spin.start(`Consultando los modelos de ${agent.def.label}`);
|
|
342
|
+
try {
|
|
343
|
+
const { models, currentModel, fromCache } = await listAcpModelsCached(agent, { cwd: acpCwd });
|
|
344
|
+
spin.stop(
|
|
345
|
+
fromCache
|
|
346
|
+
? `Modelos de ${agent.def.label} (caché)`
|
|
347
|
+
: `Modelos de ${agent.def.label}`
|
|
348
|
+
);
|
|
349
|
+
if (!models.length) return sortedFixed;
|
|
350
|
+
// Misma fusión/labels/orden que la UI.
|
|
351
|
+
return buildAcpModelSelectOptions({ models, currentModel }, fixed);
|
|
352
|
+
} catch (err) {
|
|
353
|
+
spin.stop(`No pude consultar los modelos (${err instanceof Error ? err.message.split('\n')[0] : err}).`);
|
|
354
|
+
return sortedFixed;
|
|
321
355
|
}
|
|
322
|
-
|
|
356
|
+
}
|
|
323
357
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
358
|
+
/**
|
|
359
|
+
* Modelo del agente: autocomplete con búsqueda; sin TTY, el default (string vacío).
|
|
360
|
+
* @param {import('../lib/agents.mjs').DetectedAgent} agent
|
|
361
|
+
* @param {{ acpCwd?: string }} [opts] con `acpCwd`, la lista sale del agente por ACP
|
|
362
|
+
*/
|
|
363
|
+
async function pickModel(agent, opts = {}) {
|
|
364
|
+
if (!isInteractive()) return '';
|
|
365
|
+
|
|
366
|
+
const { options, initialValue } = await modelOptions(agent, opts);
|
|
367
|
+
return /** @type {string} */ (
|
|
368
|
+
ensureValue(
|
|
369
|
+
await autocompleteEs({
|
|
370
|
+
message: 'Modelo',
|
|
371
|
+
options,
|
|
372
|
+
initialValue,
|
|
373
|
+
placeholder: 'opus, sonnet, gpt…'
|
|
374
|
+
}),
|
|
375
|
+
'modelo'
|
|
376
|
+
)
|
|
330
377
|
);
|
|
331
|
-
return typed.trim();
|
|
332
378
|
}
|
|
333
379
|
|
|
334
380
|
/**
|
|
@@ -336,8 +382,9 @@ async function pickModel(agent) {
|
|
|
336
382
|
* Si `preset` ya viene elegido, solo pregunta el modelo.
|
|
337
383
|
* @param {import('../lib/agents.mjs').DetectedAgent[]} detected
|
|
338
384
|
* @param {import('../lib/agents.mjs').DetectedAgent} [preset]
|
|
385
|
+
* @param {{ repo: string, headless: boolean }} [run] para consultar los modelos por ACP
|
|
339
386
|
*/
|
|
340
|
-
async function pickAgentAndModel(detected, preset) {
|
|
387
|
+
async function pickAgentAndModel(detected, preset, run) {
|
|
341
388
|
if (!isInteractive()) return { agent: preset ?? detected[0], model: '' };
|
|
342
389
|
|
|
343
390
|
let agent = preset;
|
|
@@ -353,11 +400,12 @@ async function pickAgentAndModel(detected, preset) {
|
|
|
353
400
|
agent = detected.find((a) => a.def.id === id) ?? detected[0];
|
|
354
401
|
}
|
|
355
402
|
|
|
356
|
-
|
|
403
|
+
const acpCwd = run && !run.headless && supportsAcp(agent) ? run.repo : undefined;
|
|
404
|
+
return { agent, model: await pickModel(agent, { acpCwd }) };
|
|
357
405
|
}
|
|
358
406
|
|
|
359
407
|
/**
|
|
360
|
-
*
|
|
408
|
+
* Agentes detectados primero (default el primero); portapapeles como opción secundaria.
|
|
361
409
|
* @param {import('../lib/agents.mjs').DetectedAgent[]} detected
|
|
362
410
|
* @returns {Promise<import('../lib/agents.mjs').DetectedAgent | null>} `null` = portapapeles
|
|
363
411
|
*/
|
|
@@ -366,10 +414,10 @@ async function pickDelivery(detected) {
|
|
|
366
414
|
await p.select({
|
|
367
415
|
message: '¿Cómo generás el review?',
|
|
368
416
|
options: [
|
|
369
|
-
{ value:
|
|
370
|
-
|
|
417
|
+
...detected.map((a) => ({ value: a.def.id, label: a.def.label, hint: a.path })),
|
|
418
|
+
{ value: 'clipboard', label: 'Copiar el prompt al portapapeles', hint: 'lo pegás en tu agente' }
|
|
371
419
|
],
|
|
372
|
-
initialValue:
|
|
420
|
+
initialValue: detected[0].def.id
|
|
373
421
|
}),
|
|
374
422
|
'modo'
|
|
375
423
|
);
|
|
@@ -406,7 +454,7 @@ async function reuseExistingReview(existing, outputFilename) {
|
|
|
406
454
|
*/
|
|
407
455
|
async function runAgentAndWait(agent, { repo, prompt, model, outputFilename, useClack }) {
|
|
408
456
|
const what = `${agent.def.label} (${model || 'modelo por defecto'})`;
|
|
409
|
-
const msg = `Lanzando ${what}. Espera ${outputFilename}
|
|
457
|
+
const msg = `Lanzando ${what}. Espera ${outputRelPath(outputFilename)}; Ctrl+C cancela.`;
|
|
410
458
|
if (useClack) p.outro(msg);
|
|
411
459
|
else console.log(msg);
|
|
412
460
|
|
|
@@ -439,14 +487,40 @@ async function runAgentAndWait(agent, { repo, prompt, model, outputFilename, use
|
|
|
439
487
|
process.exit(1);
|
|
440
488
|
}
|
|
441
489
|
if (code !== 0) console.warn(`${agent.def.label} terminó con código ${code}, pero el JSON es válido.`);
|
|
490
|
+
stampReviewProvenance(result.path, {
|
|
491
|
+
agent: agent.def.label,
|
|
492
|
+
model: resolvedRunModel(agent.def, model)
|
|
493
|
+
});
|
|
442
494
|
return /** @type {{ path: string }} */ (result);
|
|
443
495
|
}
|
|
444
496
|
|
|
497
|
+
/**
|
|
498
|
+
* El agente corre dentro del server de la UI (ACP): la web muestra el proceso y abre el reporte; la terminal ve un
|
|
499
|
+
* resumen. La sesión queda viva para preguntas y cambios desde el reporte.
|
|
500
|
+
* @param {import('../lib/agents.mjs').DetectedAgent} agent
|
|
501
|
+
* @param {{ repo: string, branch: string, base: string, model: string, outputFilename: string, useClack: boolean }} run
|
|
502
|
+
* @param {{ host: string, port: number, open: boolean }} opts
|
|
503
|
+
*/
|
|
504
|
+
async function runAgentInUi(agent, { repo, branch, base, model, outputFilename, useClack }, opts) {
|
|
505
|
+
const id = randomUUID();
|
|
506
|
+
const msg = `Lanzando ${agent.def.label} (${model || 'modelo por defecto'}). El progreso se ve en la web; Ctrl+C corta.`;
|
|
507
|
+
if (useClack) p.outro(msg);
|
|
508
|
+
else console.log(msg);
|
|
509
|
+
await startServer({
|
|
510
|
+
host: opts.host,
|
|
511
|
+
port: opts.port,
|
|
512
|
+
open: opts.open,
|
|
513
|
+
launchCwd: repo,
|
|
514
|
+
openPath: `/?agent=${encodeURIComponent(id)}`,
|
|
515
|
+
agentRun: { id, repo, branch, base, outputFilename, agentId: agent.def.id, ...(model ? { model } : {}) }
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
445
519
|
function explainWaitingForAgent(outputFilename, { copied, useClack }) {
|
|
446
520
|
const copyMsg = copied
|
|
447
521
|
? 'Se copió un prompt al portapapeles: pegalo en el chat de tu agente de IA para generar el contenido de la review.'
|
|
448
522
|
: 'Generá el JSON con tu agente de IA usando el prompt de esta sesión (no se pudo copiar solo al portapapeles).';
|
|
449
|
-
const waitMsg = `Aguardando el resultado del agente (${outputFilename}
|
|
523
|
+
const waitMsg = `Aguardando el resultado del agente (${outputRelPath(outputFilename)}). La UI web se abre cuando ese archivo esté listo.`;
|
|
450
524
|
|
|
451
525
|
if (useClack) {
|
|
452
526
|
p.log.success(copyMsg);
|
|
@@ -460,7 +534,8 @@ function explainWaitingForAgent(outputFilename, { copied, useClack }) {
|
|
|
460
534
|
}
|
|
461
535
|
|
|
462
536
|
/**
|
|
463
|
-
* @
|
|
537
|
+
* @typedef {{ id: string, repo: string, branch: string, base: string, outputFilename: string, agentId: string, model?: string }} AgentRun
|
|
538
|
+
* @param {{ host: string, port: number, open: boolean, launchCwd: string, importFile?: string, openPath?: string, agentRun?: AgentRun }} cfg
|
|
464
539
|
*/
|
|
465
540
|
async function startServer(cfg) {
|
|
466
541
|
if (!existsSync(entry)) {
|
|
@@ -490,8 +565,11 @@ O reinstalá el paquete (el tarball de npm incluye el build).`);
|
|
|
490
565
|
HOST: cfg.host,
|
|
491
566
|
PORT: String(port),
|
|
492
567
|
ORIGIN: origin,
|
|
568
|
+
// Reviews con seed/diff o import JSON suelen pasar los 512K por defecto del adapter-node.
|
|
569
|
+
BODY_SIZE_LIMIT: process.env.BODY_SIZE_LIMIT || '16M',
|
|
493
570
|
DIFF_REVIEW_LAUNCH_CWD: cfg.launchCwd,
|
|
494
|
-
...(cfg.importFile ? { DIFF_REVIEW_IMPORT_FILE: cfg.importFile } : {})
|
|
571
|
+
...(cfg.importFile ? { DIFF_REVIEW_IMPORT_FILE: cfg.importFile } : {}),
|
|
572
|
+
...(cfg.agentRun ? { DIFF_REVIEW_AGENT_RUN: JSON.stringify(cfg.agentRun), DIFF_REVIEW_MIRROR: '1' } : {})
|
|
495
573
|
},
|
|
496
574
|
stdio: ['ignore', 'pipe', 'inherit'],
|
|
497
575
|
windowsHide: false
|
|
@@ -509,6 +587,7 @@ O reinstalá el paquete (el tarball de npm incluye el build).`);
|
|
|
509
587
|
ready = true;
|
|
510
588
|
console.log(`diff-review listo en ${terminalLink(origin)} — Ctrl+C para salir.`);
|
|
511
589
|
if (cfg.open) openBrowser(openUrl);
|
|
590
|
+
else if (cfg.agentRun) console.log(`Progreso del agente: ${terminalLink(openUrl)}`);
|
|
512
591
|
}
|
|
513
592
|
continue;
|
|
514
593
|
}
|
|
@@ -543,6 +622,22 @@ async function runUiMode(opts, launch, launchCwd) {
|
|
|
543
622
|
if (launch) {
|
|
544
623
|
console.log(`Repo: ${launch.repo}`);
|
|
545
624
|
console.log(`Branch: ${branch} (base: ${base})`);
|
|
625
|
+
let promptOpts = { outputFilename };
|
|
626
|
+
try {
|
|
627
|
+
promptOpts = prepareReviewPromptOpts({
|
|
628
|
+
repo: launch.repo,
|
|
629
|
+
branch,
|
|
630
|
+
base,
|
|
631
|
+
outputFilename
|
|
632
|
+
});
|
|
633
|
+
} catch (err) {
|
|
634
|
+
console.warn(
|
|
635
|
+
'No pude precomputar el diff:',
|
|
636
|
+
err instanceof Error ? err.message : err
|
|
637
|
+
);
|
|
638
|
+
ensureReviewDir(launch.repo);
|
|
639
|
+
writeDiffCache(launch.repo, outputFilename, { branch, base });
|
|
640
|
+
}
|
|
546
641
|
const prompt = buildPrompt(
|
|
547
642
|
{
|
|
548
643
|
source: 'local',
|
|
@@ -551,7 +646,7 @@ async function runUiMode(opts, launch, launchCwd) {
|
|
|
551
646
|
base,
|
|
552
647
|
remoteUrl: launch.remoteUrl
|
|
553
648
|
},
|
|
554
|
-
|
|
649
|
+
promptOpts
|
|
555
650
|
);
|
|
556
651
|
if (copyTextToClipboard(prompt)) {
|
|
557
652
|
console.log('Prompt copiado al portapapeles.');
|
|
@@ -618,12 +713,28 @@ async function main() {
|
|
|
618
713
|
}
|
|
619
714
|
|
|
620
715
|
const outputFilename = buildOutputFilename(launch.repo, branch, base);
|
|
716
|
+
// El agente escribe en .diff-review/ (que se ignora sola) y lee de ahí el diff ya calculado.
|
|
717
|
+
let promptOpts = { outputFilename };
|
|
718
|
+
try {
|
|
719
|
+
promptOpts = prepareReviewPromptOpts({
|
|
720
|
+
repo: launch.repo,
|
|
721
|
+
branch,
|
|
722
|
+
base,
|
|
723
|
+
outputFilename
|
|
724
|
+
});
|
|
725
|
+
} catch (err) {
|
|
726
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
727
|
+
if (prompted) p.log.warn(`No pude precomputar el diff: ${msg}`);
|
|
728
|
+
else console.warn('No pude precomputar el diff:', msg);
|
|
729
|
+
ensureReviewDir(launch.repo);
|
|
730
|
+
writeDiffCache(launch.repo, outputFilename, { branch, base });
|
|
731
|
+
}
|
|
621
732
|
|
|
622
733
|
if (!prompted) {
|
|
623
734
|
console.log(`Repo: ${launch.repo}`);
|
|
624
735
|
console.log(`Branch: ${branch} (base: ${base})`);
|
|
625
736
|
} else {
|
|
626
|
-
p.note(`${branch}
|
|
737
|
+
p.note(`${branch} → ${base}\n${outputRelPath(outputFilename)}`, 'Review');
|
|
627
738
|
}
|
|
628
739
|
|
|
629
740
|
const prompt = buildPrompt(
|
|
@@ -634,7 +745,7 @@ async function main() {
|
|
|
634
745
|
base,
|
|
635
746
|
remoteUrl: launch.remoteUrl
|
|
636
747
|
},
|
|
637
|
-
|
|
748
|
+
promptOpts
|
|
638
749
|
);
|
|
639
750
|
|
|
640
751
|
// Sin --agent también se detectan (con terminal interactiva) para ofrecer lanzar uno en vez del portapapeles.
|
|
@@ -660,7 +771,18 @@ async function main() {
|
|
|
660
771
|
}
|
|
661
772
|
const useAgent = !result && detected.length > 0 && (opts.agent || chosen != null);
|
|
662
773
|
if (useAgent) {
|
|
663
|
-
const { agent, model } = await pickAgentAndModel(detected, chosen ?? undefined
|
|
774
|
+
const { agent, model } = await pickAgentAndModel(detected, chosen ?? undefined, {
|
|
775
|
+
repo: launch.repo,
|
|
776
|
+
headless: opts.headless
|
|
777
|
+
});
|
|
778
|
+
if (supportsAcp(agent) && !opts.headless) {
|
|
779
|
+
await runAgentInUi(
|
|
780
|
+
agent,
|
|
781
|
+
{ repo: launch.repo, branch, base, model, outputFilename, useClack },
|
|
782
|
+
opts
|
|
783
|
+
);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
664
786
|
result = await runAgentAndWait(agent, {
|
|
665
787
|
repo: launch.repo,
|
|
666
788
|
prompt,
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qué puede hacer el agente cuando corre por ACP. Cada `session/request_permission` pasa por acá: no hay nadie
|
|
3
|
+
* mirando la terminal para aprobar, así que la política es una allowlist (leer, comandos de solo lectura y escribir
|
|
4
|
+
* dentro de `.diff-review/`) y todo lo demás se rechaza.
|
|
5
|
+
*
|
|
6
|
+
* Ojo: el agente solo pide permiso para lo que *su* configuración no aprueba sola (p. ej. Claude Code aplica la
|
|
7
|
+
* allowlist de `~/.claude/settings.json` antes de preguntar). Esto acota lo que pide, no lo que el usuario ya
|
|
8
|
+
* habilitó en su agente.
|
|
9
|
+
*/
|
|
10
|
+
import { isAbsolute, normalize, resolve } from "node:path";
|
|
11
|
+
import { checkShell } from "./acp-shell.mjs";
|
|
12
|
+
import { OUTPUT_DIR } from "./output-name.mjs";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {"review" | "ask" | "edit"} PolicyMode
|
|
16
|
+
* review = generar el JSON; ask = preguntas (no puede escribir nada); edit = modificar el JSON.
|
|
17
|
+
* @typedef {{ repo: string, outputFilename: string, mode: PolicyMode }} PolicyContext
|
|
18
|
+
* @typedef {{
|
|
19
|
+
* kind?: string | null,
|
|
20
|
+
* title?: string | null,
|
|
21
|
+
* rawInput?: unknown,
|
|
22
|
+
* locations?: { path: string }[] | null,
|
|
23
|
+
* content?: { type: string, path?: string }[] | null,
|
|
24
|
+
* }} ToolCallLike
|
|
25
|
+
* @typedef {{ allow: boolean, reason: string }} Decision
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** @param {string} a @param {string} b */
|
|
29
|
+
function samePath(a, b, platform = process.platform) {
|
|
30
|
+
const na = normalize(a);
|
|
31
|
+
const nb = normalize(b);
|
|
32
|
+
return platform === "win32" ? na.toLowerCase() === nb.toLowerCase() : na === nb;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @param {string} child @param {string} root */
|
|
36
|
+
function isInside(child, root, platform = process.platform) {
|
|
37
|
+
const c = normalize(child);
|
|
38
|
+
const r = normalize(root).replace(/[\\/]+$/, "");
|
|
39
|
+
const [cc, rr] = platform === "win32" ? [c.toLowerCase(), r.toLowerCase()] : [c, r];
|
|
40
|
+
return cc === rr || cc.startsWith(`${rr}\\`) || cc.startsWith(`${rr}/`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Paths que toca la tool call (`locations`, diffs de `content` y los campos típicos de `rawInput`). */
|
|
44
|
+
export function toolCallPaths(toolCall, repo) {
|
|
45
|
+
const raw = toolCall.rawInput && typeof toolCall.rawInput === "object" ? /** @type {Record<string, unknown>} */ (toolCall.rawInput) : {};
|
|
46
|
+
const candidates = [
|
|
47
|
+
...(toolCall.locations ?? []).map((l) => l?.path),
|
|
48
|
+
...(toolCall.content ?? []).map((c) => (c?.type === "diff" ? c.path : undefined)),
|
|
49
|
+
raw.file_path,
|
|
50
|
+
raw.path,
|
|
51
|
+
raw.filePath,
|
|
52
|
+
raw.notebook_path,
|
|
53
|
+
];
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const c of candidates) {
|
|
56
|
+
if (typeof c !== "string" || !c.trim()) continue;
|
|
57
|
+
const abs = isAbsolute(c) ? c : resolve(repo, c);
|
|
58
|
+
if (!out.some((p) => samePath(p, abs))) out.push(abs);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @param {unknown} rawInput */
|
|
64
|
+
function commandOf(rawInput) {
|
|
65
|
+
if (!rawInput || typeof rawInput !== "object") return "";
|
|
66
|
+
const cmd = /** @type {Record<string, unknown>} */ (rawInput).command;
|
|
67
|
+
if (Array.isArray(cmd)) return cmd.map(String).join(" ");
|
|
68
|
+
return typeof cmd === "string" ? cmd : "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* ¿El comando de shell solo lee? Ver `checkShell` en `acp-shell.mjs`.
|
|
73
|
+
* @param {string} command
|
|
74
|
+
* @param {{ repo?: string }} [opts]
|
|
75
|
+
*/
|
|
76
|
+
export function isAllowedShell(command, opts) {
|
|
77
|
+
return checkShell(command, opts).ok;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {ToolCallLike} toolCall
|
|
82
|
+
* @param {PolicyContext} ctx
|
|
83
|
+
* @returns {Decision}
|
|
84
|
+
*/
|
|
85
|
+
export function decidePermission(toolCall, { repo, outputFilename, mode }) {
|
|
86
|
+
const kind = toolCall.kind ?? "other";
|
|
87
|
+
const paths = toolCallPaths(toolCall, repo);
|
|
88
|
+
|
|
89
|
+
if (kind === "read" || kind === "search") {
|
|
90
|
+
const outside = paths.find((p) => !isInside(p, repo));
|
|
91
|
+
return outside ? { allow: false, reason: `lectura fuera del repo: ${outside}` } : { allow: true, reason: "lectura" };
|
|
92
|
+
}
|
|
93
|
+
if (kind === "think") return { allow: true, reason: "pensar" };
|
|
94
|
+
|
|
95
|
+
if (kind === "execute") {
|
|
96
|
+
// Cursor no manda `rawInput`: el comando viene solo en el título, entre backticks.
|
|
97
|
+
const title = String(toolCall.title ?? "").trim();
|
|
98
|
+
const cmd = commandOf(toolCall.rawInput) || (/^`([\s\S]+)`$/.exec(title)?.[1] ?? title);
|
|
99
|
+
const check = checkShell(cmd, { repo });
|
|
100
|
+
return check.ok ? { allow: true, reason: "comando de solo lectura" } : { allow: false, reason: check.reason };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (kind === "edit" || kind === "delete") {
|
|
104
|
+
if (mode === "ask") return { allow: false, reason: "modo pregunta: no se edita nada" };
|
|
105
|
+
if (!paths.length) return { allow: false, reason: `${kind === "delete" ? "borrado" : "edición"} sin path` };
|
|
106
|
+
const reviewRoot = resolve(repo, OUTPUT_DIR);
|
|
107
|
+
const outside = paths.find((p) => !isInside(p, reviewRoot) && !samePath(p, reviewRoot));
|
|
108
|
+
return outside
|
|
109
|
+
? {
|
|
110
|
+
allow: false,
|
|
111
|
+
reason: `solo se puede escribir en ${OUTPUT_DIR}/ (no ${outside})`
|
|
112
|
+
}
|
|
113
|
+
: { allow: true, reason: `escritura en ${OUTPUT_DIR}/` };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { allow: false, reason: `${kind} no permitido: ${toolCall.title ?? "?"}` };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Respuesta ACP para una decisión del usuario / política.
|
|
121
|
+
* - allow + once → `allow_once` (la política se vuelve a evaluar en el próximo pedido).
|
|
122
|
+
* - allow + always → `allow_always` si el agente la ofrece; si no, cae a `allow_once`.
|
|
123
|
+
* @param {{ optionId: string, kind: string }[]} options
|
|
124
|
+
* @param {boolean} allow
|
|
125
|
+
* @param {{ always?: boolean }} [opts]
|
|
126
|
+
*/
|
|
127
|
+
export function permissionOutcome(options, allow, opts = {}) {
|
|
128
|
+
const always = Boolean(opts.always);
|
|
129
|
+
const kinds = allow
|
|
130
|
+
? always
|
|
131
|
+
? ["allow_always", "allow_once"]
|
|
132
|
+
: ["allow_once"]
|
|
133
|
+
: ["reject_once", "reject_always"];
|
|
134
|
+
const option = kinds.map((kind) => options.find((o) => o.kind === kind)).find(Boolean);
|
|
135
|
+
return option ? { outcome: { outcome: "selected", optionId: option.optionId } } : { outcome: { outcome: "cancelled" } };
|
|
136
|
+
}
|