@jpkme/diff-review 0.1.15 → 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-policy.mjs +10 -2
- package/lib/acp-policy.test.mjs +27 -0
- 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/output-name.mjs +7 -0
- package/lib/prompt.mjs +14 -11
- package/lib/prompt.test.mjs +49 -0
- package/lib/review-dir.mjs +14 -1
- package/lib/review-meta.mjs +11 -3
- package/lib/review-meta.test.mjs +19 -2
- package/lib/review-prep.mjs +63 -63
- package/lib/review.mjs +36 -4
- package/lib/review.test.mjs +35 -0
- package/lib/validate.mjs +11 -1
- package/lib/validate.test.mjs +47 -0
- package/lib/wait-output.test.mjs +1 -1
- package/package.json +2 -2
- package/schema.json +16 -1
- 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/B4SiKXUO.js +2 -0
- 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/{D8WOD_eS.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/{BpVtXA-T.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-DUStDOsr.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-DUj63QTT.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-C7UD6rei.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-4RcoUd0l.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-C_hlZJKN.js → agent-sessions-BArYLTW6.js} +1103 -146
- package/web/build/server/chunks/agent-sessions-BArYLTW6.js.map +1 -0
- package/web/build/server/chunks/{git-review-Bh1w0gak.js → git-review-BJ_JrFNP.js} +87 -26
- package/web/build/server/chunks/git-review-BJ_JrFNP.js.map +1 -0
- package/web/build/server/chunks/{handler-e2lEvjVT.js → handler-Df_bJiCq.js} +3 -3
- package/web/build/server/chunks/{handler-e2lEvjVT.js.map → handler-Df_bJiCq.js.map} +1 -1
- package/web/build/server/chunks/{hooks.server-Um1Sknmz.js → hooks.server-BEl7rVad.js} +4 -3
- package/web/build/server/chunks/{hooks.server-Um1Sknmz.js.map → hooks.server-BEl7rVad.js.map} +1 -1
- package/web/build/server/chunks/{index.js-C3-YX8SA.js → index.js-Bb0CZLZ_.js} +884 -881
- 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-DMiPy1mT.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-RlIVOUe-.js → remote-wh6t6j-B-AHrGaN.js} +58 -12
- package/web/build/server/chunks/remote-wh6t6j-B-AHrGaN.js.map +1 -0
- package/web/build/server/chunks/{validate-CAQ31C7k.js → validate-CXjTRGnR.js} +67 -21
- 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.Cw1J6YTM.css +0 -1
- package/web/build/client/_app/immutable/assets/3.Cw1J6YTM.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.Cw1J6YTM.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/storage.TlsiS-AP.css +0 -1
- package/web/build/client/_app/immutable/assets/storage.TlsiS-AP.css.br +0 -0
- package/web/build/client/_app/immutable/assets/storage.TlsiS-AP.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/BpVtXA-T.js.br +0 -2
- package/web/build/client/_app/immutable/chunks/BpVtXA-T.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/CUhrLa7d.js +0 -210
- package/web/build/client/_app/immutable/chunks/CUhrLa7d.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/CUhrLa7d.js.gz +0 -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/QvO6K-HK.js +0 -2
- package/web/build/client/_app/immutable/chunks/QvO6K-HK.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/QvO6K-HK.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js +0 -1
- 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.BjJsHui4.js +0 -2
- package/web/build/client/_app/immutable/entry/app.BjJsHui4.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.BjJsHui4.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.D4unRHC4.js +0 -1
- package/web/build/client/_app/immutable/entry/start.D4unRHC4.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.D4unRHC4.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js +0 -1
- package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.C8JJYlWG.js +0 -1
- package/web/build/client/_app/immutable/nodes/1.C8JJYlWG.js.br +0 -1
- package/web/build/client/_app/immutable/nodes/1.C8JJYlWG.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.CDI0Ipi4.js +0 -10
- package/web/build/client/_app/immutable/nodes/2.CDI0Ipi4.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.CDI0Ipi4.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.DVC22CDg.js +0 -171
- package/web/build/client/_app/immutable/nodes/3.DVC22CDg.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.DVC22CDg.js.gz +0 -0
- package/web/build/server/chunks/0-DUStDOsr.js +0 -11
- package/web/build/server/chunks/1-DUj63QTT.js +0 -7
- package/web/build/server/chunks/2-B4PSKe5P.js +0 -7
- package/web/build/server/chunks/2-B4PSKe5P.js.map +0 -1
- package/web/build/server/chunks/3-C7UD6rei.js +0 -7
- package/web/build/server/chunks/_server.ts-4RcoUd0l.js.map +0 -1
- package/web/build/server/chunks/agent-sessions-C_hlZJKN.js.map +0 -1
- package/web/build/server/chunks/git-review-Bh1w0gak.js.map +0 -1
- package/web/build/server/chunks/index.js-C3-YX8SA.js.map +0 -1
- package/web/build/server/chunks/manifest.js-CmbRPv0j.js +0 -58
- package/web/build/server/chunks/manifest.js-CmbRPv0j.js.map +0 -1
- package/web/build/server/chunks/remote-1g4ayag-DMiPy1mT.js.map +0 -1
- package/web/build/server/chunks/remote-wh6t6j-RlIVOUe-.js.map +0 -1
- package/web/build/server/chunks/validate-CAQ31C7k.js.map +0 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync,
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
1
|
+
import { p as getRemoteUrl, e as ensureReviewDir, w as writeDiffCache, q as readDiffCache, u as extractBranchDiff, v as loadReviewConfig, x as outputRelPath, m as makeOutputFilename, O as OUTPUT_DIR, y as writeSessionRef, c as companionPaths, d as decodePayload, t as tryResolveGitRoot, f as loadBranchDiff, z as validateAgainst, A as reviewIssues, R as REVIEW_SCHEMA, C as globMatch, D as writeDiffText, E as writeHandoffChat } from './validate-CXjTRGnR.js';
|
|
2
|
+
import { existsSync, statSync, accessSync, constants, readFileSync, readdirSync, mkdirSync, writeFileSync, copyFileSync, rmSync, realpathSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { spawnSync, spawn } from 'node:child_process';
|
|
4
4
|
import { homedir, tmpdir } from 'node:os';
|
|
5
5
|
import { join, delimiter, isAbsolute, resolve, basename, dirname, normalize } from 'node:path';
|
|
6
6
|
import { Writable, Readable } from 'node:stream';
|
|
7
7
|
import * as acp from '@agentclientprotocol/sdk';
|
|
8
|
+
import { randomBytes } from 'node:crypto';
|
|
8
9
|
|
|
9
10
|
//#region ../lib/agents.mjs
|
|
10
11
|
/**
|
|
@@ -40,15 +41,6 @@ function readJson(file) {
|
|
|
40
41
|
return null;
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
|
-
/** Primer `model = "..."` de nivel raíz de un TOML (antes de cualquier `[sección]`). */
|
|
44
|
-
function topLevelTomlModel(text) {
|
|
45
|
-
for (const line of String(text).split(/\r?\n/)) {
|
|
46
|
-
if (/^\s*\[/.test(line)) break;
|
|
47
|
-
const m = line.match(/^\s*model\s*=\s*["']([^"']+)["']/);
|
|
48
|
-
if (m) return m[1];
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
44
|
/** Versiones tipo `2.1.286` de mayor a menor (las que no parsean, al final). */
|
|
53
45
|
function byVersionDesc(a, b) {
|
|
54
46
|
const pa = a.split(".").map(Number);
|
|
@@ -182,30 +174,6 @@ var AGENTS = [
|
|
|
182
174
|
};
|
|
183
175
|
}
|
|
184
176
|
},
|
|
185
|
-
{
|
|
186
|
-
id: "codex",
|
|
187
|
-
label: "Codex",
|
|
188
|
-
bins: ["codex"],
|
|
189
|
-
models: [],
|
|
190
|
-
configuredModel: ({ home }) => {
|
|
191
|
-
try {
|
|
192
|
-
const model = topLevelTomlModel(readFileSync(join(home, ".codex", "config.toml"), "utf8"));
|
|
193
|
-
return model ? {
|
|
194
|
-
model,
|
|
195
|
-
source: "~/.codex/config.toml"
|
|
196
|
-
} : null;
|
|
197
|
-
} catch {
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
},
|
|
201
|
-
args: ({ model }) => [
|
|
202
|
-
"exec",
|
|
203
|
-
"--sandbox",
|
|
204
|
-
"workspace-write",
|
|
205
|
-
...model ? ["-m", model] : [],
|
|
206
|
-
"-"
|
|
207
|
-
]
|
|
208
|
-
},
|
|
209
177
|
{
|
|
210
178
|
id: "cursor",
|
|
211
179
|
label: "Cursor CLI",
|
|
@@ -266,21 +234,6 @@ var AGENTS = [
|
|
|
266
234
|
file: path,
|
|
267
235
|
args: ["--acp"]
|
|
268
236
|
})
|
|
269
|
-
},
|
|
270
|
-
{
|
|
271
|
-
id: "gemini",
|
|
272
|
-
label: "Gemini CLI",
|
|
273
|
-
bins: ["gemini"],
|
|
274
|
-
models: [],
|
|
275
|
-
configuredModel: ({ home }) => {
|
|
276
|
-
const model = readJson(join(home, ".gemini", "settings.json"))?.model;
|
|
277
|
-
const name = typeof model === "string" ? model : model?.name;
|
|
278
|
-
return typeof name === "string" && name ? {
|
|
279
|
-
model: name,
|
|
280
|
-
source: "~/.gemini/settings.json"
|
|
281
|
-
} : null;
|
|
282
|
-
},
|
|
283
|
-
args: ({ model }) => [...model ? ["-m", model] : [], "--yolo"]
|
|
284
237
|
}
|
|
285
238
|
];
|
|
286
239
|
/** @param {string} file @param {NodeJS.Platform} platform */
|
|
@@ -856,8 +809,9 @@ function checkShell(command, { repo, platform = process.platform } = {}) {
|
|
|
856
809
|
* habilitó en su agente.
|
|
857
810
|
*/
|
|
858
811
|
/**
|
|
859
|
-
* @typedef {"review" | "ask" | "edit"} PolicyMode
|
|
860
|
-
* review = generar el JSON; ask = preguntas (no puede escribir nada); edit = modificar el JSON
|
|
812
|
+
* @typedef {"review" | "ask" | "edit" | "fix"} PolicyMode
|
|
813
|
+
* review = generar el JSON; ask = preguntas (no puede escribir nada); edit = modificar el JSON;
|
|
814
|
+
* fix = editar código dentro del repo (no solo el JSON).
|
|
861
815
|
* @typedef {{ repo: string, outputFilename: string, mode: PolicyMode }} PolicyContext
|
|
862
816
|
* @typedef {{
|
|
863
817
|
* kind?: string | null,
|
|
@@ -1021,6 +975,16 @@ function decidePermission(toolCall, { repo, outputFilename, mode }) {
|
|
|
1021
975
|
allow: false,
|
|
1022
976
|
reason: `${kind === "delete" ? "borrado" : "edición"} sin path`
|
|
1023
977
|
};
|
|
978
|
+
if (mode === "fix") {
|
|
979
|
+
const outside = paths.find((p) => !isInside(p, repo));
|
|
980
|
+
return outside ? {
|
|
981
|
+
allow: false,
|
|
982
|
+
reason: `path fuera del repo: ${outside}`
|
|
983
|
+
} : {
|
|
984
|
+
allow: true,
|
|
985
|
+
reason: "escritura en el repo"
|
|
986
|
+
};
|
|
987
|
+
}
|
|
1024
988
|
const target = resolve(repo, outputRelPath(outputFilename));
|
|
1025
989
|
const outside = paths.find((p) => !samePath(canonicalPath(p), canonicalPath(target)));
|
|
1026
990
|
return outside ? {
|
|
@@ -1055,48 +1019,139 @@ function permissionOutcome(options, allow, opts = {}) {
|
|
|
1055
1019
|
//#endregion
|
|
1056
1020
|
//#region ../lib/agent-login.mjs
|
|
1057
1021
|
/**
|
|
1058
|
-
* Login de agentes ACP: mensajes claros
|
|
1022
|
+
* Login de agentes ACP: clasificación de authMethods, mensajes claros y lanzamiento del CLI de auth.
|
|
1023
|
+
*
|
|
1024
|
+
* Preferencia de launch:
|
|
1025
|
+
* 1. Misma TTY (stdio inherit) si stdin/stdout son TTY — CLI interactiva, exit code real.
|
|
1026
|
+
* 2. Proceso hijo oculto (sin ventana de terminal) si no hay TTY — p. ej. web; OAuth abre el browser.
|
|
1027
|
+
* 3. Terminal externa solo con `external: true` (raro: login que exige TTY interactiva).
|
|
1028
|
+
*
|
|
1029
|
+
* En Windows, `cmd /c start /wait` **no** propaga el exit code del proceso iniciado (siempre 0
|
|
1030
|
+
* hacia Node). El fallback externo usa un archivo sentinel, igual que macOS/Linux.
|
|
1059
1031
|
*/
|
|
1060
1032
|
/**
|
|
1061
|
-
* @typedef {{ bin: string, args: string[], display: string }} LoginCommand
|
|
1033
|
+
* @typedef {{ bin: string, args: string[], display: string, env?: Record<string, string> }} LoginCommand
|
|
1034
|
+
* @typedef {'agent' | 'terminal' | 'terminal-meta' | 'unknown'} AuthMethodKind
|
|
1035
|
+
* @typedef {'inherit' | 'hidden' | 'external'} LoginLaunchMode
|
|
1062
1036
|
*/
|
|
1063
1037
|
/**
|
|
1064
|
-
*
|
|
1038
|
+
* Hostname de Copilot (Enterprise). Vacío / `github.com` = cuenta en github.com.
|
|
1039
|
+
* @param {string | null | undefined} host
|
|
1040
|
+
* @returns {string}
|
|
1041
|
+
*/
|
|
1042
|
+
function normalizeCopilotLoginHost(host) {
|
|
1043
|
+
const h = String(host || "").trim().replace(/^https?:\/\//i, "").replace(/\/.*$/, "");
|
|
1044
|
+
if (!h || /^github\.com$/i.test(h)) return "";
|
|
1045
|
+
return h;
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Agrega `--host` a un comando `copilot login` cuando el usuario eligió Enterprise.
|
|
1049
|
+
* No fuerza nada por env: el host lo elige quien llama (CLI/UI).
|
|
1050
|
+
* @param {LoginCommand} cmd
|
|
1051
|
+
* @param {string | null | undefined} host
|
|
1052
|
+
* @returns {LoginCommand}
|
|
1053
|
+
*/
|
|
1054
|
+
function withCopilotLoginHost(cmd, host) {
|
|
1055
|
+
if (!cmd || cmd.bin == null) return cmd;
|
|
1056
|
+
const h = normalizeCopilotLoginHost(host);
|
|
1057
|
+
if (!h) return cmd;
|
|
1058
|
+
const args = Array.isArray(cmd.args) ? [...cmd.args] : [];
|
|
1059
|
+
const hostIdx = args.findIndex((a) => a === "--host");
|
|
1060
|
+
if (hostIdx >= 0) args[hostIdx + 1] = h;
|
|
1061
|
+
else args.push("--host", h);
|
|
1062
|
+
const withoutHost = String(cmd.display || "").replace(/\s+--host\s+\S+/g, "").trim();
|
|
1063
|
+
return {
|
|
1064
|
+
...cmd,
|
|
1065
|
+
args,
|
|
1066
|
+
display: `${withoutHost} --host ${h}`.trim()
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Comando de login interactivo por agente (fallback si no hay authMethod usable).
|
|
1065
1071
|
* @param {string} agentId
|
|
1072
|
+
* @param {{ host?: string | null }} [opts] solo Copilot: host Enterprise opcional
|
|
1066
1073
|
* @returns {LoginCommand | null}
|
|
1067
1074
|
*/
|
|
1068
|
-
function loginCommand(agentId) {
|
|
1075
|
+
function loginCommand(agentId, opts = {}) {
|
|
1069
1076
|
switch (agentId) {
|
|
1070
1077
|
case "claude": return {
|
|
1071
1078
|
bin: "claude",
|
|
1072
|
-
args: ["/login"],
|
|
1073
|
-
display: "claude /login"
|
|
1074
|
-
};
|
|
1075
|
-
case "copilot": return {
|
|
1076
|
-
bin: "gh",
|
|
1077
1079
|
args: ["auth", "login"],
|
|
1078
|
-
display: "
|
|
1080
|
+
display: "claude auth login"
|
|
1079
1081
|
};
|
|
1080
|
-
case "
|
|
1081
|
-
bin: "
|
|
1082
|
+
case "copilot": return withCopilotLoginHost({
|
|
1083
|
+
bin: "copilot",
|
|
1082
1084
|
args: ["login"],
|
|
1083
|
-
display: "
|
|
1084
|
-
};
|
|
1085
|
-
case "
|
|
1086
|
-
bin: "
|
|
1087
|
-
args: [],
|
|
1088
|
-
display: "
|
|
1085
|
+
display: "copilot login"
|
|
1086
|
+
}, opts.host);
|
|
1087
|
+
case "cursor": return {
|
|
1088
|
+
bin: "cursor-agent",
|
|
1089
|
+
args: ["login"],
|
|
1090
|
+
display: "cursor-agent login"
|
|
1089
1091
|
};
|
|
1090
1092
|
default: return null;
|
|
1091
1093
|
}
|
|
1092
1094
|
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Args de `claude auth login …` a partir de un authMethod ACP.
|
|
1097
|
+
* El adaptador anuncia `["--cli", "auth", "login", "--claudeai"]` (o lo mismo tras el path del
|
|
1098
|
+
* adapter en `_meta.terminal-auth.args`); `--cli` solo entiende el wrapper ACP, no `claude`.
|
|
1099
|
+
* @param {any} [method]
|
|
1100
|
+
* @returns {string[]}
|
|
1101
|
+
*/
|
|
1102
|
+
function claudeCliLoginArgs(method) {
|
|
1103
|
+
const meta = method?._meta?.["terminal-auth"];
|
|
1104
|
+
const raw = Array.isArray(meta?.args) ? meta.args.map(String) : Array.isArray(method?.args) ? method.args.map(String) : [];
|
|
1105
|
+
const cliIdx = raw.indexOf("--cli");
|
|
1106
|
+
const after = cliIdx >= 0 ? raw.slice(cliIdx + 1) : raw;
|
|
1107
|
+
if (after[0] === "auth") return after;
|
|
1108
|
+
return [
|
|
1109
|
+
"auth",
|
|
1110
|
+
"login",
|
|
1111
|
+
...after.filter((a) => a.startsWith("--"))
|
|
1112
|
+
];
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Clasifica un authMethod de `initialize` según ACP v1 + extensión Copilot `_meta.terminal-auth`.
|
|
1116
|
+
* @param {any} method
|
|
1117
|
+
* @returns {AuthMethodKind}
|
|
1118
|
+
*/
|
|
1119
|
+
function classifyAuthMethod(method) {
|
|
1120
|
+
if (!method || typeof method.id !== "string" || !method.id) return "unknown";
|
|
1121
|
+
if (method._meta != null && method._meta["terminal-auth"] != null) return "terminal-meta";
|
|
1122
|
+
if (method.type === "terminal") return "terminal";
|
|
1123
|
+
if (!method.type || method.type === "agent") return "agent";
|
|
1124
|
+
return "unknown";
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* @param {any[]} authMethods
|
|
1128
|
+
* @returns {{ agent: any[], terminal: any[], terminalMeta: any[], unknown: any[] }}
|
|
1129
|
+
*/
|
|
1130
|
+
function groupAuthMethods(authMethods) {
|
|
1131
|
+
/** @type {{ agent: any[], terminal: any[], terminalMeta: any[], unknown: any[] }} */
|
|
1132
|
+
const groups = {
|
|
1133
|
+
agent: [],
|
|
1134
|
+
terminal: [],
|
|
1135
|
+
terminalMeta: [],
|
|
1136
|
+
unknown: []
|
|
1137
|
+
};
|
|
1138
|
+
for (const method of authMethods || []) {
|
|
1139
|
+
const kind = classifyAuthMethod(method);
|
|
1140
|
+
if (kind === "agent") groups.agent.push(method);
|
|
1141
|
+
else if (kind === "terminal") groups.terminal.push(method);
|
|
1142
|
+
else if (kind === "terminal-meta") groups.terminalMeta.push(method);
|
|
1143
|
+
else groups.unknown.push(method);
|
|
1144
|
+
}
|
|
1145
|
+
return groups;
|
|
1146
|
+
}
|
|
1093
1147
|
/** @param {string} text */
|
|
1094
1148
|
function looksLikeAuthFailure(text) {
|
|
1095
1149
|
const s = String(text || "").toLowerCase();
|
|
1096
|
-
return /(not\s+)?auth|login|unauthor|not logged|sign[\s-]?in|credentials|token.*(invalid|expired|missing)|please\s+run/.test(s);
|
|
1150
|
+
return /(not\s+)?auth|login|unauthor|not logged|sign[\s-]?in|credentials|token.*(invalid|expired|missing)|please\s+run|auth_required/.test(s);
|
|
1097
1151
|
}
|
|
1098
1152
|
/**
|
|
1099
1153
|
* Mensaje en español si el agente murió / falló por falta de login.
|
|
1154
|
+
* Sin comandos CLI: el CTA abre el login por el usuario.
|
|
1100
1155
|
* @param {string} agentId
|
|
1101
1156
|
* @param {string} agentLabel
|
|
1102
1157
|
* @param {string} detail
|
|
@@ -1104,38 +1159,280 @@ function looksLikeAuthFailure(text) {
|
|
|
1104
1159
|
*/
|
|
1105
1160
|
function authFailureMessage$1(agentId, agentLabel, detail) {
|
|
1106
1161
|
if (!looksLikeAuthFailure(detail)) return null;
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1162
|
+
return `Necesitás iniciar sesión en ${String(agentLabel || "").trim() || "el agente"}.`;
|
|
1163
|
+
}
|
|
1164
|
+
/** Fallo del proceso de login (exit ≠ 0 o cancelado a medias). */
|
|
1165
|
+
function loginIncompleteMessage() {
|
|
1166
|
+
return "No se pudo completar el inicio de sesión. Probá de nuevo.";
|
|
1167
|
+
}
|
|
1168
|
+
/** Estado mientras corre el login (TTY, proceso oculto o terminal externa). */
|
|
1169
|
+
function loginWaitingMessage() {
|
|
1170
|
+
return "Esperando que completes el inicio de sesión…";
|
|
1111
1171
|
}
|
|
1112
1172
|
/**
|
|
1113
|
-
*
|
|
1114
|
-
*
|
|
1173
|
+
* Método interactivo a abrir: `_meta.terminal-auth` (Copilot) o `type: terminal` (Claude).
|
|
1174
|
+
* Si solo hay métodos `agent` (Cursor), devolvemos null → `resolveLoginCommand` usa el fallback CLI.
|
|
1175
|
+
* @param {any[]} [methods]
|
|
1176
|
+
* @returns {any | null}
|
|
1177
|
+
*/
|
|
1178
|
+
function pickLoginAuthMethod(methods) {
|
|
1179
|
+
const grouped = groupAuthMethods(methods || []);
|
|
1180
|
+
return grouped.terminalMeta[0] ?? grouped.terminal[0] ?? null;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* ¿Podemos correr el login en la misma terminal (stdio inherit)?
|
|
1184
|
+
* La web / procesos sin TTY usan proceso oculto (o `external: true` → ventana).
|
|
1185
|
+
* @param {{ external?: boolean }} [opts]
|
|
1186
|
+
*/
|
|
1187
|
+
function canInheritLoginTty(opts = {}) {
|
|
1188
|
+
if (opts.external) return false;
|
|
1189
|
+
return Boolean(process.stdin?.isTTY && process.stdout?.isTTY);
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Modo de launch: inherit (CLI con TTY) → hidden (web / OAuth en browser) → external (forzado).
|
|
1193
|
+
* @param {{ wait?: boolean, external?: boolean }} [opts]
|
|
1194
|
+
* @returns {LoginLaunchMode}
|
|
1195
|
+
*/
|
|
1196
|
+
function resolveLoginLaunchMode(opts = {}) {
|
|
1197
|
+
if (Boolean(opts.wait) && canInheritLoginTty(opts)) return "inherit";
|
|
1198
|
+
if (opts.external) return "external";
|
|
1199
|
+
return "hidden";
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Arma el comando de login a partir de un authMethod (o fallback por agentId).
|
|
1203
|
+
* @param {{ def: { id: string, acp?: Function }, path: string }} agent
|
|
1204
|
+
* @param {any} [method]
|
|
1205
|
+
* @returns {LoginCommand}
|
|
1206
|
+
*/
|
|
1207
|
+
function resolveLoginCommand(agent, method) {
|
|
1208
|
+
const kind = method ? classifyAuthMethod(method) : "unknown";
|
|
1209
|
+
const meta = method?._meta?.["terminal-auth"];
|
|
1210
|
+
if (agent?.def?.id === "claude" && method && (kind === "terminal" || kind === "terminal-meta")) {
|
|
1211
|
+
const args = claudeCliLoginArgs(method);
|
|
1212
|
+
return {
|
|
1213
|
+
bin: typeof agent.path === "string" && agent.path.trim() ? agent.path : "claude",
|
|
1214
|
+
args,
|
|
1215
|
+
display: `claude ${args.join(" ")}`.trim(),
|
|
1216
|
+
...method?.env && typeof method.env === "object" ? { env: method.env } : {}
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
if ((kind === "terminal-meta" || kind === "terminal") && meta && typeof meta === "object") {
|
|
1220
|
+
const command = typeof meta.command === "string" ? meta.command.trim() : "";
|
|
1221
|
+
if (command) {
|
|
1222
|
+
const args = Array.isArray(meta.args) ? meta.args.map(String) : [];
|
|
1223
|
+
return {
|
|
1224
|
+
bin: command,
|
|
1225
|
+
args,
|
|
1226
|
+
display: (typeof meta.label === "string" && meta.label.trim() ? meta.label.trim() : "") || `${command}${args.length ? ` ${args.join(" ")}` : ""}`.trim(),
|
|
1227
|
+
...method?.env && typeof method.env === "object" ? { env: method.env } : {}
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
if (kind === "terminal" && method && typeof agent?.def?.acp === "function") {
|
|
1232
|
+
const launch = agent.def.acp({ path: agent.path });
|
|
1233
|
+
const extra = Array.isArray(method.args) ? method.args.map(String) : [];
|
|
1234
|
+
const env = {
|
|
1235
|
+
...launch.env && typeof launch.env === "object" ? launch.env : {},
|
|
1236
|
+
...method.env && typeof method.env === "object" ? method.env : {}
|
|
1237
|
+
};
|
|
1238
|
+
const args = [...launch.args || [], ...extra];
|
|
1239
|
+
return {
|
|
1240
|
+
bin: launch.file,
|
|
1241
|
+
args,
|
|
1242
|
+
display: `${launch.file}${args.length ? ` ${args.join(" ")}` : ""}`.trim(),
|
|
1243
|
+
...Object.keys(env).length ? { env } : {}
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
const fallback = loginCommand(agent?.def?.id);
|
|
1247
|
+
if (fallback) {
|
|
1248
|
+
if (agent?.def?.id === "claude" && typeof agent.path === "string" && agent.path.trim()) return {
|
|
1249
|
+
...fallback,
|
|
1250
|
+
bin: agent.path
|
|
1251
|
+
};
|
|
1252
|
+
return fallback;
|
|
1253
|
+
}
|
|
1254
|
+
throw new Error("Este agente no tiene un inicio de sesión conocido.");
|
|
1255
|
+
}
|
|
1256
|
+
/** @param {import('node:child_process').ChildProcess} child */
|
|
1257
|
+
function waitExit(child) {
|
|
1258
|
+
return new Promise((resolve, reject) => {
|
|
1259
|
+
child.once("error", reject);
|
|
1260
|
+
child.once("exit", (code, signal) => resolve(signal ? 1 : code ?? 1));
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Lanza el login en background (proceso oculto; no espera). Compat con UI/CTA simples (web sin sesión).
|
|
1265
|
+
* @param {string | LoginCommand} agentIdOrCmd
|
|
1115
1266
|
* @returns {{ display: string }}
|
|
1116
1267
|
*/
|
|
1117
|
-
function openLoginTerminal(
|
|
1118
|
-
const cmd = loginCommand(
|
|
1119
|
-
if (!cmd) throw new Error(
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
"
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1268
|
+
function openLoginTerminal(agentIdOrCmd) {
|
|
1269
|
+
const cmd = typeof agentIdOrCmd === "string" ? loginCommand(agentIdOrCmd) : agentIdOrCmd;
|
|
1270
|
+
if (!cmd) throw new Error((typeof agentIdOrCmd === "string" ? agentIdOrCmd : "") === "cursor" ? "Necesitás iniciar sesión en Cursor." : "Este agente no tiene un inicio de sesión conocido.");
|
|
1271
|
+
launchLoginProcess(cmd, {
|
|
1272
|
+
wait: false,
|
|
1273
|
+
mode: "hidden"
|
|
1274
|
+
});
|
|
1275
|
+
return { display: cmd.display };
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Lanza el login. Con TTY y `wait: true`, corre en esta terminal (inherit); si no, proceso oculto
|
|
1279
|
+
* (OAuth en browser). Solo con `external: true` abre una ventana de terminal del OS.
|
|
1280
|
+
* @param {LoginCommand} cmd
|
|
1281
|
+
* @param {{ wait?: boolean, cwd?: string, external?: boolean }} [opts]
|
|
1282
|
+
* @returns {Promise<{ display: string, code: number | null, mode: LoginLaunchMode }>}
|
|
1283
|
+
*/
|
|
1284
|
+
async function runLoginTerminal(cmd, opts = {}) {
|
|
1285
|
+
const wait = Boolean(opts.wait);
|
|
1286
|
+
const mode = resolveLoginLaunchMode(opts);
|
|
1287
|
+
const code = await launchLoginProcess(cmd, {
|
|
1288
|
+
wait,
|
|
1289
|
+
cwd: opts.cwd,
|
|
1290
|
+
mode
|
|
1291
|
+
});
|
|
1292
|
+
return {
|
|
1293
|
+
display: cmd.display,
|
|
1294
|
+
code: wait ? code : null,
|
|
1295
|
+
mode
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
/** @param {string} arg */
|
|
1299
|
+
function quoteBatArg(arg) {
|
|
1300
|
+
const s = String(arg);
|
|
1301
|
+
if (/^[\w./:\\@=-]+$/.test(s)) return s;
|
|
1302
|
+
return `"${s.replace(/"/g, "\"\"")}"`;
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Espera a que el sentinel tenga un exit code (macOS/Linux/Windows externo).
|
|
1306
|
+
* @param {string} sentinel
|
|
1307
|
+
* @param {string} display
|
|
1308
|
+
* @returns {Promise<number>}
|
|
1309
|
+
*/
|
|
1310
|
+
async function waitForExitSentinel(sentinel, display) {
|
|
1311
|
+
const deadline = Date.now() + 1800 * 1e3;
|
|
1312
|
+
while (Date.now() < deadline) {
|
|
1313
|
+
try {
|
|
1314
|
+
const raw = readFileSync(sentinel, "utf8").trim();
|
|
1315
|
+
if (raw !== "") {
|
|
1316
|
+
const code = Number.parseInt(raw, 10);
|
|
1317
|
+
try {
|
|
1318
|
+
unlinkSync(sentinel);
|
|
1319
|
+
} catch {}
|
|
1320
|
+
return Number.isFinite(code) ? code : 1;
|
|
1321
|
+
}
|
|
1322
|
+
} catch {}
|
|
1323
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
1324
|
+
}
|
|
1325
|
+
try {
|
|
1326
|
+
unlinkSync(sentinel);
|
|
1327
|
+
} catch {}
|
|
1328
|
+
throw new Error(`Timeout esperando el login (${display}).`);
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Proceso hijo sin ventana de terminal (web / OAuth). Exit code real vía waitExit.
|
|
1332
|
+
* @param {LoginCommand} cmd
|
|
1333
|
+
* @param {{ wait: boolean, cwd?: string, env: NodeJS.ProcessEnv }} opts
|
|
1334
|
+
* @returns {Promise<number>}
|
|
1335
|
+
*/
|
|
1336
|
+
async function launchHiddenLogin(cmd, { wait, cwd, env }) {
|
|
1337
|
+
const child = spawnCommand(cmd.bin, cmd.args, {
|
|
1338
|
+
cwd,
|
|
1339
|
+
env,
|
|
1131
1340
|
stdio: "ignore",
|
|
1132
|
-
windowsHide:
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1341
|
+
windowsHide: true,
|
|
1342
|
+
...wait ? {} : { detached: true }
|
|
1343
|
+
});
|
|
1344
|
+
if (!wait) {
|
|
1345
|
+
child.unref();
|
|
1346
|
+
return 0;
|
|
1347
|
+
}
|
|
1348
|
+
return waitExit(child);
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* @param {LoginCommand} cmd
|
|
1352
|
+
* @param {{ wait: boolean, cwd?: string, mode: LoginLaunchMode }} opts
|
|
1353
|
+
* @returns {Promise<number>}
|
|
1354
|
+
*/
|
|
1355
|
+
async function launchLoginProcess(cmd, { wait, cwd, mode }) {
|
|
1356
|
+
const env = {
|
|
1357
|
+
...process.env,
|
|
1358
|
+
...cmd.env || {}
|
|
1359
|
+
};
|
|
1360
|
+
if (mode === "inherit") return waitExit(spawnCommand(cmd.bin, cmd.args, {
|
|
1361
|
+
cwd,
|
|
1362
|
+
env,
|
|
1363
|
+
stdio: "inherit"
|
|
1364
|
+
}));
|
|
1365
|
+
if (mode === "hidden") return launchHiddenLogin(cmd, {
|
|
1366
|
+
wait,
|
|
1367
|
+
cwd,
|
|
1368
|
+
env
|
|
1369
|
+
});
|
|
1370
|
+
const platform = process.platform;
|
|
1371
|
+
if (platform === "win32") {
|
|
1372
|
+
if (!wait) {
|
|
1373
|
+
spawn("cmd.exe", [
|
|
1374
|
+
"/c",
|
|
1375
|
+
"start",
|
|
1376
|
+
"diff-review login",
|
|
1377
|
+
"cmd.exe",
|
|
1378
|
+
"/k",
|
|
1379
|
+
cmd.bin,
|
|
1380
|
+
...cmd.args
|
|
1381
|
+
], {
|
|
1382
|
+
cwd,
|
|
1383
|
+
env,
|
|
1384
|
+
detached: true,
|
|
1385
|
+
stdio: "ignore",
|
|
1386
|
+
windowsHide: false
|
|
1387
|
+
}).unref();
|
|
1388
|
+
return 0;
|
|
1389
|
+
}
|
|
1390
|
+
const id = randomBytes(8).toString("hex");
|
|
1391
|
+
const sentinel = join(tmpdir(), `diff-review-login-${id}.exit`);
|
|
1392
|
+
const bat = join(tmpdir(), `diff-review-login-${id}.bat`);
|
|
1393
|
+
const line = [quoteBatArg(cmd.bin), ...cmd.args.map(quoteBatArg)].join(" ");
|
|
1394
|
+
const sentPath = sentinel.replace(/"/g, "\"\"");
|
|
1395
|
+
writeFileSync(bat, [
|
|
1396
|
+
`@echo off`,
|
|
1397
|
+
`call ${line}`,
|
|
1398
|
+
`> "${sentPath}" echo %ERRORLEVEL%`,
|
|
1399
|
+
``
|
|
1400
|
+
].join("\r\n"));
|
|
1401
|
+
try {
|
|
1402
|
+
writeFileSync(sentinel, "");
|
|
1403
|
+
} catch {}
|
|
1404
|
+
const child = spawn("cmd.exe", [
|
|
1405
|
+
"/c",
|
|
1406
|
+
"start",
|
|
1407
|
+
"/wait",
|
|
1408
|
+
"diff-review login",
|
|
1409
|
+
"cmd.exe",
|
|
1410
|
+
"/c",
|
|
1411
|
+
bat
|
|
1412
|
+
], {
|
|
1413
|
+
cwd,
|
|
1414
|
+
env,
|
|
1415
|
+
stdio: "ignore",
|
|
1416
|
+
windowsHide: false
|
|
1417
|
+
});
|
|
1418
|
+
try {
|
|
1419
|
+
await waitExit(child);
|
|
1420
|
+
return await waitForExitSentinel(sentinel, cmd.display);
|
|
1421
|
+
} finally {
|
|
1422
|
+
try {
|
|
1423
|
+
unlinkSync(bat);
|
|
1424
|
+
} catch {}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
if (!wait) {
|
|
1428
|
+
if (platform === "darwin") {
|
|
1429
|
+
spawn("osascript", ["-e", `tell application "Terminal" to do script ${JSON.stringify(`${cmd.bin} ${cmd.args.join(" ")}`.trim())}`], {
|
|
1430
|
+
detached: true,
|
|
1431
|
+
stdio: "ignore",
|
|
1432
|
+
env
|
|
1433
|
+
}).unref();
|
|
1434
|
+
return 0;
|
|
1435
|
+
}
|
|
1139
1436
|
const line = `${cmd.bin} ${cmd.args.join(" ")}`.trim();
|
|
1140
1437
|
const tried = [
|
|
1141
1438
|
["x-terminal-emulator", [
|
|
@@ -1163,18 +1460,375 @@ function openLoginTerminal(agentId) {
|
|
|
1163
1460
|
`${line}; exec bash`
|
|
1164
1461
|
]]
|
|
1165
1462
|
];
|
|
1166
|
-
let ok = false;
|
|
1167
1463
|
for (const [bin, args] of tried) try {
|
|
1168
1464
|
spawn(bin, args, {
|
|
1169
1465
|
detached: true,
|
|
1170
|
-
stdio: "ignore"
|
|
1466
|
+
stdio: "ignore",
|
|
1467
|
+
env,
|
|
1468
|
+
cwd
|
|
1171
1469
|
}).unref();
|
|
1470
|
+
return 0;
|
|
1471
|
+
} catch {}
|
|
1472
|
+
throw new Error(`No pude abrir una terminal. Corré a mano: ${cmd.display}`);
|
|
1473
|
+
}
|
|
1474
|
+
const sentinel = join(tmpdir(), `diff-review-login-${randomBytes(8).toString("hex")}.exit`);
|
|
1475
|
+
try {
|
|
1476
|
+
writeFileSync(sentinel, "");
|
|
1477
|
+
} catch {}
|
|
1478
|
+
const line = `${[cmd.bin, ...cmd.args].map((a) => JSON.stringify(a)).join(" ")}; echo $? > ${JSON.stringify(sentinel)}`;
|
|
1479
|
+
if (platform === "darwin") spawn("osascript", ["-e", `tell application "Terminal" to do script ${JSON.stringify(line)}`], {
|
|
1480
|
+
stdio: "ignore",
|
|
1481
|
+
env,
|
|
1482
|
+
cwd
|
|
1483
|
+
});
|
|
1484
|
+
else {
|
|
1485
|
+
const tried = [
|
|
1486
|
+
["gnome-terminal", [
|
|
1487
|
+
"--wait",
|
|
1488
|
+
"--",
|
|
1489
|
+
"bash",
|
|
1490
|
+
"-lc",
|
|
1491
|
+
line
|
|
1492
|
+
]],
|
|
1493
|
+
["x-terminal-emulator", [
|
|
1494
|
+
"-e",
|
|
1495
|
+
"bash",
|
|
1496
|
+
"-lc",
|
|
1497
|
+
line
|
|
1498
|
+
]],
|
|
1499
|
+
["konsole", [
|
|
1500
|
+
"-e",
|
|
1501
|
+
"bash",
|
|
1502
|
+
"-lc",
|
|
1503
|
+
line
|
|
1504
|
+
]],
|
|
1505
|
+
["xterm", [
|
|
1506
|
+
"-e",
|
|
1507
|
+
"bash",
|
|
1508
|
+
"-lc",
|
|
1509
|
+
line
|
|
1510
|
+
]]
|
|
1511
|
+
];
|
|
1512
|
+
let ok = false;
|
|
1513
|
+
for (const [bin, args] of tried) try {
|
|
1514
|
+
await waitExit(spawn(bin, args, {
|
|
1515
|
+
stdio: "ignore",
|
|
1516
|
+
env,
|
|
1517
|
+
cwd
|
|
1518
|
+
}));
|
|
1172
1519
|
ok = true;
|
|
1173
1520
|
break;
|
|
1174
1521
|
} catch {}
|
|
1175
1522
|
if (!ok) throw new Error(`No pude abrir una terminal. Corré a mano: ${cmd.display}`);
|
|
1176
1523
|
}
|
|
1177
|
-
return
|
|
1524
|
+
return waitForExitSentinel(sentinel, cmd.display);
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Cuenta activa del agente (para el header de la web).
|
|
1528
|
+
* @param {string} agentId
|
|
1529
|
+
* @param {{ env?: NodeJS.ProcessEnv, run?: typeof spawnSync }} [opts]
|
|
1530
|
+
* @returns {{
|
|
1531
|
+
* agentId: string,
|
|
1532
|
+
* loggedIn: boolean,
|
|
1533
|
+
* accountLabel: string | null,
|
|
1534
|
+
* detail: string | null,
|
|
1535
|
+
* source: 'gh' | 'claude' | 'env' | 'unknown' | null,
|
|
1536
|
+
* warnings: string[],
|
|
1537
|
+
* }}
|
|
1538
|
+
*/
|
|
1539
|
+
function agentAccountStatus(agentId, opts = {}) {
|
|
1540
|
+
const id = String(agentId || "").trim();
|
|
1541
|
+
const env = opts.env || process.env;
|
|
1542
|
+
const run = opts.run || spawnSync;
|
|
1543
|
+
/** @type {string[]} */
|
|
1544
|
+
const warnings = [];
|
|
1545
|
+
if (id === "copilot") {
|
|
1546
|
+
const envHits = copilotTokenEnvOverrides(env);
|
|
1547
|
+
if (envHits.length) {
|
|
1548
|
+
warnings.push(`${envHits.join(", ")} en el entorno (pisa otras cuentas).`);
|
|
1549
|
+
return {
|
|
1550
|
+
agentId: id,
|
|
1551
|
+
loggedIn: true,
|
|
1552
|
+
accountLabel: "Token de entorno",
|
|
1553
|
+
detail: envHits.join(", "),
|
|
1554
|
+
source: "env",
|
|
1555
|
+
warnings
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
const accounts = listGhAuthAccounts({
|
|
1559
|
+
env,
|
|
1560
|
+
run
|
|
1561
|
+
});
|
|
1562
|
+
const active = accounts.find((a) => a.active) || accounts[0] || null;
|
|
1563
|
+
if (active) return {
|
|
1564
|
+
agentId: id,
|
|
1565
|
+
loggedIn: true,
|
|
1566
|
+
accountLabel: active.user,
|
|
1567
|
+
detail: active.host,
|
|
1568
|
+
source: "gh",
|
|
1569
|
+
warnings
|
|
1570
|
+
};
|
|
1571
|
+
return {
|
|
1572
|
+
agentId: id,
|
|
1573
|
+
loggedIn: false,
|
|
1574
|
+
accountLabel: null,
|
|
1575
|
+
detail: null,
|
|
1576
|
+
source: null,
|
|
1577
|
+
warnings
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
if (id === "claude") {
|
|
1581
|
+
const r = run("claude", [
|
|
1582
|
+
"auth",
|
|
1583
|
+
"status",
|
|
1584
|
+
"--json"
|
|
1585
|
+
], {
|
|
1586
|
+
encoding: "utf8",
|
|
1587
|
+
env,
|
|
1588
|
+
windowsHide: true
|
|
1589
|
+
});
|
|
1590
|
+
if (r.error) return {
|
|
1591
|
+
agentId: id,
|
|
1592
|
+
loggedIn: false,
|
|
1593
|
+
accountLabel: null,
|
|
1594
|
+
detail: null,
|
|
1595
|
+
source: null,
|
|
1596
|
+
warnings: [r.error.message]
|
|
1597
|
+
};
|
|
1598
|
+
try {
|
|
1599
|
+
const parsed = JSON.parse(String(r.stdout || "{}"));
|
|
1600
|
+
const loggedIn = Boolean(parsed?.loggedIn);
|
|
1601
|
+
const email = String(parsed?.email || "").trim() || null;
|
|
1602
|
+
const org = String(parsed?.orgName || "").trim() || null;
|
|
1603
|
+
return {
|
|
1604
|
+
agentId: id,
|
|
1605
|
+
loggedIn,
|
|
1606
|
+
accountLabel: loggedIn ? email || "Claude" : null,
|
|
1607
|
+
detail: loggedIn ? org || String(parsed?.authMethod || "") || null : null,
|
|
1608
|
+
source: loggedIn ? "claude" : null,
|
|
1609
|
+
warnings
|
|
1610
|
+
};
|
|
1611
|
+
} catch {
|
|
1612
|
+
return {
|
|
1613
|
+
agentId: id,
|
|
1614
|
+
loggedIn: false,
|
|
1615
|
+
accountLabel: null,
|
|
1616
|
+
detail: null,
|
|
1617
|
+
source: null,
|
|
1618
|
+
warnings: ["No pude leer el estado de Claude."]
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
if (id === "cursor") return {
|
|
1623
|
+
agentId: id,
|
|
1624
|
+
loggedIn: true,
|
|
1625
|
+
accountLabel: "Cursor",
|
|
1626
|
+
detail: "Sesión del CLI / app",
|
|
1627
|
+
source: "unknown",
|
|
1628
|
+
warnings
|
|
1629
|
+
};
|
|
1630
|
+
return {
|
|
1631
|
+
agentId: id,
|
|
1632
|
+
loggedIn: false,
|
|
1633
|
+
accountLabel: null,
|
|
1634
|
+
detail: null,
|
|
1635
|
+
source: null,
|
|
1636
|
+
warnings
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Env vars que Copilot usa antes que OAuth / gh (impiden un cambio de cuenta real).
|
|
1641
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
1642
|
+
* @returns {string[]}
|
|
1643
|
+
*/
|
|
1644
|
+
function copilotTokenEnvOverrides(env = process.env) {
|
|
1645
|
+
/** @type {string[]} */
|
|
1646
|
+
const hit = [];
|
|
1647
|
+
for (const key of [
|
|
1648
|
+
"COPILOT_GITHUB_TOKEN",
|
|
1649
|
+
"GH_TOKEN",
|
|
1650
|
+
"GITHUB_TOKEN"
|
|
1651
|
+
]) if (String(env[key] || "").trim()) hit.push(key);
|
|
1652
|
+
return hit;
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Cuentas conocidas por `gh auth status --json hosts`.
|
|
1656
|
+
* @param {{ env?: NodeJS.ProcessEnv, run?: typeof spawnSync }} [opts]
|
|
1657
|
+
* @returns {{ host: string, user: string, active: boolean }[]}
|
|
1658
|
+
*/
|
|
1659
|
+
function listGhAuthAccounts(opts = {}) {
|
|
1660
|
+
const env = opts.env || process.env;
|
|
1661
|
+
const r = (opts.run || spawnSync)("gh", [
|
|
1662
|
+
"auth",
|
|
1663
|
+
"status",
|
|
1664
|
+
"--json",
|
|
1665
|
+
"hosts"
|
|
1666
|
+
], {
|
|
1667
|
+
encoding: "utf8",
|
|
1668
|
+
env,
|
|
1669
|
+
windowsHide: true
|
|
1670
|
+
});
|
|
1671
|
+
if (r.error) return [];
|
|
1672
|
+
try {
|
|
1673
|
+
const parsed = JSON.parse(String(r.stdout || "{}"));
|
|
1674
|
+
const hosts = parsed?.hosts && typeof parsed.hosts === "object" ? parsed.hosts : {};
|
|
1675
|
+
/** @type {{ host: string, user: string, active: boolean }[]} */
|
|
1676
|
+
const out = [];
|
|
1677
|
+
for (const [host, accounts] of Object.entries(hosts)) {
|
|
1678
|
+
const list = Array.isArray(accounts) ? accounts : [];
|
|
1679
|
+
for (const acc of list) {
|
|
1680
|
+
const user = String(acc?.login || acc?.user || "").trim();
|
|
1681
|
+
if (!user) continue;
|
|
1682
|
+
out.push({
|
|
1683
|
+
host: String(host),
|
|
1684
|
+
user,
|
|
1685
|
+
active: Boolean(acc?.active)
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
return out;
|
|
1690
|
+
} catch {
|
|
1691
|
+
return [];
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* Cierra sesiones de `gh` (Copilot las reusa si no hay OAuth propio).
|
|
1696
|
+
* @param {{ env?: NodeJS.ProcessEnv, run?: typeof spawnSync, host?: string | null }} [opts]
|
|
1697
|
+
* @returns {{ ok: boolean, loggedOut: { host: string, user: string }[], warnings: string[] }}
|
|
1698
|
+
*/
|
|
1699
|
+
function logoutGhAuthAccounts(opts = {}) {
|
|
1700
|
+
const env = opts.env || process.env;
|
|
1701
|
+
const run = opts.run || spawnSync;
|
|
1702
|
+
const onlyHost = normalizeCopilotLoginHost(opts.host) || null;
|
|
1703
|
+
const accounts = listGhAuthAccounts({
|
|
1704
|
+
env,
|
|
1705
|
+
run
|
|
1706
|
+
}).filter((a) => onlyHost ? a.host === onlyHost || a.host === `https://${onlyHost}` : true);
|
|
1707
|
+
/** @type {{ host: string, user: string }[]} */
|
|
1708
|
+
const loggedOut = [];
|
|
1709
|
+
/** @type {string[]} */
|
|
1710
|
+
const warnings = [];
|
|
1711
|
+
for (const acc of accounts) {
|
|
1712
|
+
const r = run("gh", [
|
|
1713
|
+
"auth",
|
|
1714
|
+
"logout",
|
|
1715
|
+
"--hostname",
|
|
1716
|
+
acc.host,
|
|
1717
|
+
"--user",
|
|
1718
|
+
acc.user
|
|
1719
|
+
], {
|
|
1720
|
+
encoding: "utf8",
|
|
1721
|
+
env,
|
|
1722
|
+
windowsHide: true
|
|
1723
|
+
});
|
|
1724
|
+
if (r.error) {
|
|
1725
|
+
warnings.push(`No pude cerrar ${acc.user}@${acc.host}: ${r.error.message}`);
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
if (r.status !== 0) {
|
|
1729
|
+
const err = String(r.stderr || r.stdout || "").trim() || `exit ${r.status}`;
|
|
1730
|
+
warnings.push(`No pude cerrar ${acc.user}@${acc.host}: ${err}`);
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1733
|
+
loggedOut.push({
|
|
1734
|
+
host: acc.host,
|
|
1735
|
+
user: acc.user
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
const envHits = copilotTokenEnvOverrides(env);
|
|
1739
|
+
if (envHits.length) warnings.push(`Hay ${envHits.join(", ")} en el entorno: Copilot seguirá usando ese token hasta que lo saques.`);
|
|
1740
|
+
return {
|
|
1741
|
+
ok: warnings.length === 0 || loggedOut.length > 0,
|
|
1742
|
+
loggedOut,
|
|
1743
|
+
warnings
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Comando de logout CLI por agente (sin depender de gh).
|
|
1748
|
+
* @param {string} agentId
|
|
1749
|
+
* @returns {LoginCommand | null}
|
|
1750
|
+
*/
|
|
1751
|
+
function logoutCommand(agentId) {
|
|
1752
|
+
switch (agentId) {
|
|
1753
|
+
case "claude": return {
|
|
1754
|
+
bin: "claude",
|
|
1755
|
+
args: ["auth", "logout"],
|
|
1756
|
+
display: "claude auth logout"
|
|
1757
|
+
};
|
|
1758
|
+
default: return null;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Cierra la sesión del agente para poder elegir otra cuenta.
|
|
1763
|
+
* Copilot: también desloguea cuentas de `gh` (si solo hacés logout OAuth, gh sigue ganando).
|
|
1764
|
+
*
|
|
1765
|
+
* @param {string} agentId
|
|
1766
|
+
* @param {{
|
|
1767
|
+
* host?: string | null,
|
|
1768
|
+
* env?: NodeJS.ProcessEnv,
|
|
1769
|
+
* run?: typeof spawnSync,
|
|
1770
|
+
* runTerminal?: typeof runLoginTerminal,
|
|
1771
|
+
* cwd?: string,
|
|
1772
|
+
* }} [opts]
|
|
1773
|
+
* @returns {Promise<{ ok: boolean, steps: string[], warnings: string[] }>}
|
|
1774
|
+
*/
|
|
1775
|
+
async function runAgentLogout(agentId, opts = {}) {
|
|
1776
|
+
const id = String(agentId || "").trim();
|
|
1777
|
+
/** @type {string[]} */
|
|
1778
|
+
const steps = [];
|
|
1779
|
+
/** @type {string[]} */
|
|
1780
|
+
const warnings = [];
|
|
1781
|
+
const env = opts.env || process.env;
|
|
1782
|
+
const run = opts.run || spawnSync;
|
|
1783
|
+
const runTerm = opts.runTerminal || runLoginTerminal;
|
|
1784
|
+
if (id === "copilot") {
|
|
1785
|
+
const gh = logoutGhAuthAccounts({
|
|
1786
|
+
env,
|
|
1787
|
+
run,
|
|
1788
|
+
host: opts.host
|
|
1789
|
+
});
|
|
1790
|
+
for (const a of gh.loggedOut) steps.push(`Cerré ${a.user} en ${a.host} (GitHub CLI)`);
|
|
1791
|
+
warnings.push(...gh.warnings);
|
|
1792
|
+
if (!gh.loggedOut.length && !gh.warnings.length) steps.push("No había cuentas de GitHub CLI activas.");
|
|
1793
|
+
return {
|
|
1794
|
+
ok: true,
|
|
1795
|
+
steps,
|
|
1796
|
+
warnings
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
const cmd = logoutCommand(id);
|
|
1800
|
+
if (!cmd) return {
|
|
1801
|
+
ok: false,
|
|
1802
|
+
steps,
|
|
1803
|
+
warnings: ["Este agente no tiene cierre de sesión desde la app."]
|
|
1804
|
+
};
|
|
1805
|
+
try {
|
|
1806
|
+
const result = await runTerm(cmd, {
|
|
1807
|
+
wait: true,
|
|
1808
|
+
cwd: opts.cwd
|
|
1809
|
+
});
|
|
1810
|
+
if (result.code !== 0) {
|
|
1811
|
+
warnings.push(`El logout terminó con código ${result.code ?? "?"}.`);
|
|
1812
|
+
return {
|
|
1813
|
+
ok: false,
|
|
1814
|
+
steps,
|
|
1815
|
+
warnings
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
steps.push(`Cerré la sesión (${cmd.display}).`);
|
|
1819
|
+
return {
|
|
1820
|
+
ok: true,
|
|
1821
|
+
steps,
|
|
1822
|
+
warnings
|
|
1823
|
+
};
|
|
1824
|
+
} catch (err) {
|
|
1825
|
+
warnings.push(err instanceof Error ? err.message : String(err));
|
|
1826
|
+
return {
|
|
1827
|
+
ok: false,
|
|
1828
|
+
steps,
|
|
1829
|
+
warnings
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1178
1832
|
}
|
|
1179
1833
|
/** @returns {string} */
|
|
1180
1834
|
function modelsCachePath(home = homedir()) {
|
|
@@ -1250,6 +1904,23 @@ function writeModelsCache(agentId, data, opts = {}) {
|
|
|
1250
1904
|
* review y después para las preguntas y los cambios que se le pidan desde la web. Cada pedido de permiso pasa por
|
|
1251
1905
|
* `decide` (ver `acp-policy.mjs`): no hay nadie mirando una terminal para aprobar.
|
|
1252
1906
|
*/
|
|
1907
|
+
/**
|
|
1908
|
+
* session/new (u otra op) requiere login interactivo (terminal / terminal-meta).
|
|
1909
|
+
* No es un fallo genérico: la UI debe abrir el login y reintentar.
|
|
1910
|
+
*/
|
|
1911
|
+
var AcpAuthRequiredError = class extends Error {
|
|
1912
|
+
/**
|
|
1913
|
+
* @param {string} message
|
|
1914
|
+
* @param {{ methods?: any[], agentId?: string, agentLabel?: string }} [info]
|
|
1915
|
+
*/
|
|
1916
|
+
constructor(message, info = {}) {
|
|
1917
|
+
super(message);
|
|
1918
|
+
this.name = "AcpAuthRequiredError";
|
|
1919
|
+
this.methods = Array.isArray(info.methods) ? info.methods : [];
|
|
1920
|
+
this.agentId = info.agentId || "";
|
|
1921
|
+
this.agentLabel = info.agentLabel || "";
|
|
1922
|
+
}
|
|
1923
|
+
};
|
|
1253
1924
|
/** El agente tiene un modo ACP declarado (no todos los agentes lo traen). */
|
|
1254
1925
|
function supportsAcp(agent) {
|
|
1255
1926
|
return typeof agent?.def?.acp === "function";
|
|
@@ -1280,7 +1951,7 @@ var ACP_START_TIMEOUT_MS = 9e4;
|
|
|
1280
1951
|
* @returns {Promise<AcpSession>}
|
|
1281
1952
|
*/
|
|
1282
1953
|
async function startAcpSession(agent, opts) {
|
|
1283
|
-
if (!supportsAcp(agent)) throw new Error(`${agent.def.label} no
|
|
1954
|
+
if (!supportsAcp(agent)) throw new Error(`${agent.def.label} no se puede usar desde la web.`);
|
|
1284
1955
|
const { file, args, env, sessionMeta } = agent.def.acp({ path: agent.path });
|
|
1285
1956
|
const child = spawnCommand(file, args, {
|
|
1286
1957
|
cwd: opts.cwd,
|
|
@@ -1358,16 +2029,21 @@ async function startAcpSession(agent, opts) {
|
|
|
1358
2029
|
readTextFile: false,
|
|
1359
2030
|
writeTextFile: false
|
|
1360
2031
|
},
|
|
1361
|
-
terminal: false
|
|
2032
|
+
terminal: false,
|
|
2033
|
+
auth: { terminal: true },
|
|
2034
|
+
_meta: { "terminal-auth": true }
|
|
1362
2035
|
},
|
|
1363
2036
|
clientInfo: {
|
|
1364
2037
|
name: "diff-review",
|
|
1365
2038
|
version: "0"
|
|
1366
2039
|
}
|
|
1367
2040
|
});
|
|
1368
|
-
|
|
1369
|
-
const
|
|
1370
|
-
/**
|
|
2041
|
+
const authMethods = Array.isArray(init.authMethods) ? init.authMethods : [];
|
|
2042
|
+
const grouped = groupAuthMethods(authMethods);
|
|
2043
|
+
/** Solo type agent (p. ej. Cursor `cursor_login`). Nunca terminal ni Copilot `_meta.terminal-auth`. */
|
|
2044
|
+
const agentAuthMethods = grouped.agent;
|
|
2045
|
+
const interactiveAuthMethods = [...grouped.terminal, ...grouped.terminalMeta];
|
|
2046
|
+
/** Si session/new falla por auth, pedimos `authenticate` una vez (métodos agent) y reintentamos. */
|
|
1371
2047
|
const tryAuthenticate = async () => {
|
|
1372
2048
|
for (const method of agentAuthMethods) try {
|
|
1373
2049
|
await conn.agent.request(acp.methods.agent.authenticate, { methodId: method.id });
|
|
@@ -1375,6 +2051,14 @@ async function startAcpSession(agent, opts) {
|
|
|
1375
2051
|
} catch {}
|
|
1376
2052
|
return false;
|
|
1377
2053
|
};
|
|
2054
|
+
const authRequired = (msg) => {
|
|
2055
|
+
const label = agent.def.label;
|
|
2056
|
+
return new AcpAuthRequiredError(authFailureMessage$1(agent.def.id, label, msg) || `Necesitás iniciar sesión en ${label}.`, {
|
|
2057
|
+
methods: interactiveAuthMethods.length ? interactiveAuthMethods : authMethods,
|
|
2058
|
+
agentId: agent.def.id,
|
|
2059
|
+
agentLabel: label
|
|
2060
|
+
});
|
|
2061
|
+
};
|
|
1378
2062
|
const openNewSession = async () => {
|
|
1379
2063
|
try {
|
|
1380
2064
|
return await conn.agent.request(acp.methods.agent.session.new, {
|
|
@@ -1384,11 +2068,13 @@ async function startAcpSession(agent, opts) {
|
|
|
1384
2068
|
});
|
|
1385
2069
|
} catch (err) {
|
|
1386
2070
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1387
|
-
if (
|
|
2071
|
+
if (!looksLikeAuthFailure(msg)) throw err;
|
|
2072
|
+
if (agentAuthMethods.length && await tryAuthenticate()) return await conn.agent.request(acp.methods.agent.session.new, {
|
|
1388
2073
|
cwd: opts.cwd,
|
|
1389
2074
|
mcpServers: [],
|
|
1390
2075
|
...sessionMeta ? { _meta: sessionMeta } : {}
|
|
1391
2076
|
});
|
|
2077
|
+
if (interactiveAuthMethods.length || agentAuthMethods.length || looksLikeAuthFailure(msg)) throw authRequired(msg);
|
|
1392
2078
|
throw err;
|
|
1393
2079
|
}
|
|
1394
2080
|
};
|
|
@@ -1420,7 +2106,7 @@ async function startAcpSession(agent, opts) {
|
|
|
1420
2106
|
if (!wanted || wanted === currentModel) return { currentModel };
|
|
1421
2107
|
if (!models) return {
|
|
1422
2108
|
currentModel,
|
|
1423
|
-
warning: `El agente no deja elegir el modelo
|
|
2109
|
+
warning: `El agente no deja elegir el modelo; usa el suyo.`
|
|
1424
2110
|
};
|
|
1425
2111
|
if (!models.options.some((o) => o.value === wanted)) return {
|
|
1426
2112
|
currentModel,
|
|
@@ -1440,6 +2126,7 @@ async function startAcpSession(agent, opts) {
|
|
|
1440
2126
|
const applied = await setModel(initialWanted);
|
|
1441
2127
|
if (applied.warning) modelWarning = applied.warning;
|
|
1442
2128
|
}
|
|
2129
|
+
const supportsLogout = Boolean(init.agentCapabilities?.auth?.logout);
|
|
1443
2130
|
clearTimeout(timer);
|
|
1444
2131
|
return {
|
|
1445
2132
|
sessionId,
|
|
@@ -1448,6 +2135,7 @@ async function startAcpSession(agent, opts) {
|
|
|
1448
2135
|
get currentModel() {
|
|
1449
2136
|
return currentModel;
|
|
1450
2137
|
},
|
|
2138
|
+
supportsLogout,
|
|
1451
2139
|
...modelWarning ? { modelWarning } : {},
|
|
1452
2140
|
setModel,
|
|
1453
2141
|
prompt: async (text) => {
|
|
@@ -1461,6 +2149,11 @@ async function startAcpSession(agent, opts) {
|
|
|
1461
2149
|
return { stopReason: String(res.stopReason) };
|
|
1462
2150
|
},
|
|
1463
2151
|
cancel: () => conn.agent.notify(acp.methods.agent.session.cancel, { sessionId }),
|
|
2152
|
+
/** ACP `logout` si el agente lo anunció; no garantiza nada sobre credenciales de `gh`. */
|
|
2153
|
+
logout: async () => {
|
|
2154
|
+
if (!supportsLogout) throw new Error(`${agent.def.label} no admite cerrar sesión desde acá.`);
|
|
2155
|
+
await conn.agent.request(acp.methods.agent.logout, {});
|
|
2156
|
+
},
|
|
1464
2157
|
close,
|
|
1465
2158
|
closed: conn.closed,
|
|
1466
2159
|
stderrTail: () => stderr
|
|
@@ -1468,13 +2161,24 @@ async function startAcpSession(agent, opts) {
|
|
|
1468
2161
|
} catch (err) {
|
|
1469
2162
|
clearTimeout(timer);
|
|
1470
2163
|
close();
|
|
2164
|
+
if (err instanceof AcpAuthRequiredError) throw err;
|
|
1471
2165
|
const tail = stderr.trim().split(/\r?\n/).slice(-8).join("\n");
|
|
1472
2166
|
if (timedOut) {
|
|
1473
2167
|
const msg = `Timeout (${Math.round(timeoutMs / 1e3)}s) al lanzar ${agent.def.label}. ¿Está autenticado el CLI?`;
|
|
1474
|
-
|
|
2168
|
+
const full = tail ? `${msg}\n${tail}` : msg;
|
|
2169
|
+
if (looksLikeAuthFailure(full)) throw new AcpAuthRequiredError(authFailureMessage$1(agent.def.id, agent.def.label, full) || full, {
|
|
2170
|
+
agentId: agent.def.id,
|
|
2171
|
+
agentLabel: agent.def.label
|
|
2172
|
+
});
|
|
2173
|
+
throw new Error(full);
|
|
1475
2174
|
}
|
|
1476
2175
|
const reason = err instanceof Error ? err.message : String(err);
|
|
1477
|
-
|
|
2176
|
+
const full = tail ? `${reason}\n${tail}` : reason;
|
|
2177
|
+
if (looksLikeAuthFailure(full)) throw new AcpAuthRequiredError(authFailureMessage$1(agent.def.id, agent.def.label, full) || full, {
|
|
2178
|
+
agentId: agent.def.id,
|
|
2179
|
+
agentLabel: agent.def.label
|
|
2180
|
+
});
|
|
2181
|
+
throw new Error(full);
|
|
1478
2182
|
}
|
|
1479
2183
|
}
|
|
1480
2184
|
/**
|
|
@@ -1690,12 +2394,15 @@ function formatCheckReport(file, command, result) {
|
|
|
1690
2394
|
/**
|
|
1691
2395
|
* `agentSession` = la sesión ACP que generó la review, para seguir conversando con el agente desde la web.
|
|
1692
2396
|
* @param {string} filePath
|
|
1693
|
-
*
|
|
2397
|
+
* `modelLabel` = nombre legible que el agente da al modelo ("Haiku 5.5"): `model` puede ser un alias
|
|
2398
|
+
* (`haiku`) que más adelante apunte a otra versión.
|
|
2399
|
+
* @param {{ agent?: string, model?: string, modelLabel?: string, agentSession?: { agentId: string, sessionId: string, outputFilename: string } }} provenance
|
|
1694
2400
|
* @returns {boolean}
|
|
1695
2401
|
*/
|
|
1696
|
-
function stampReviewProvenance(filePath, { agent, model, agentSession } = {}) {
|
|
2402
|
+
function stampReviewProvenance(filePath, { agent, model, modelLabel, agentSession } = {}) {
|
|
1697
2403
|
const agentName = String(agent || "").trim();
|
|
1698
2404
|
const modelId = String(model || "").trim();
|
|
2405
|
+
const label = String(modelLabel || "").trim();
|
|
1699
2406
|
if (!agentName && !modelId && !agentSession) return false;
|
|
1700
2407
|
let payload;
|
|
1701
2408
|
try {
|
|
@@ -1707,7 +2414,11 @@ function stampReviewProvenance(filePath, { agent, model, agentSession } = {}) {
|
|
|
1707
2414
|
const current = payload.meta;
|
|
1708
2415
|
const meta = current && typeof current === "object" && !Array.isArray(current) ? { ...current } : {};
|
|
1709
2416
|
if (agentName) meta.agent = agentName;
|
|
1710
|
-
if (modelId)
|
|
2417
|
+
if (modelId) {
|
|
2418
|
+
meta.model = modelId;
|
|
2419
|
+
if (label && label !== modelId) meta.modelLabel = label;
|
|
2420
|
+
else delete meta.modelLabel;
|
|
2421
|
+
}
|
|
1711
2422
|
if (agentSession) meta.agentSession = { ...agentSession };
|
|
1712
2423
|
payload.meta = meta;
|
|
1713
2424
|
writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
@@ -1736,7 +2447,7 @@ var OUTPUT_SCHEMA_BLOCK = `{
|
|
|
1736
2447
|
"id": "t1",
|
|
1737
2448
|
"kind": "feat|fix|refactor|perf|test|chore|docs|infra",
|
|
1738
2449
|
"title": "string, aim for ≤72 chars, imperative, no kind prefix",
|
|
1739
|
-
"intent": "string"
|
|
2450
|
+
"intent": "string: why this theme exists + what it changes for the reader (not a restatement of the title)"
|
|
1740
2451
|
}
|
|
1741
2452
|
],
|
|
1742
2453
|
"blocks": [
|
|
@@ -1749,8 +2460,8 @@ var OUTPUT_SCHEMA_BLOCK = `{
|
|
|
1749
2460
|
"start": 40,
|
|
1750
2461
|
"end": 58,
|
|
1751
2462
|
"op": "add|mod|del|rename",
|
|
1752
|
-
"what": "string
|
|
1753
|
-
"why": "string
|
|
2463
|
+
"what": "string: concrete change + effect (aim ≤120 chars; longer OK if needed for clarity)",
|
|
2464
|
+
"why": "string: motivation / why this change belongs in the branch (aim ≤120 chars)",
|
|
1754
2465
|
"source": "code|commit|pr|issue|inferred"
|
|
1755
2466
|
}
|
|
1756
2467
|
],
|
|
@@ -1764,8 +2475,8 @@ var OUTPUT_SCHEMA_BLOCK = `{
|
|
|
1764
2475
|
"file": "relative/path.ext",
|
|
1765
2476
|
"line": 87,
|
|
1766
2477
|
"block": "b1",
|
|
1767
|
-
"what": "string
|
|
1768
|
-
"fix": "string
|
|
2478
|
+
"what": "string: cause + why it is a problem + observable consequence (aim ≤120 chars; prefer clarity over brevity)",
|
|
2479
|
+
"fix": "string: suggested change + why that change resolves the problem / what it avoids (aim ≤120 chars)"
|
|
1769
2480
|
}
|
|
1770
2481
|
],
|
|
1771
2482
|
"skipped": [
|
|
@@ -1779,13 +2490,16 @@ var RULES = `- Ids: themes use \`t1\`, \`t2\`, …; findings use \`f1\`, \`f2\`,
|
|
|
1779
2490
|
- Line anchors: "lines"/"start"/"end"/"line" MUST match the chosen "side" ("new" = post-image / right side of @@; "old" = pre-image, typically deletions). One block per coherent change span, not one per line. Point findings at that block; leave "block" empty only if cross-cutting.
|
|
1780
2491
|
- Do NOT report: pre-existing code outside the diff; linter/formatter noise; naming/style prefs with no real consequence; intentional branch behavior; silenced lint rules; "missing docs/coverage" without a concrete broken scenario.
|
|
1781
2492
|
- Quality findings must name a concrete cost, not a vague feeling.
|
|
1782
|
-
-
|
|
1783
|
-
-
|
|
2493
|
+
- Theme "intent": explain why the theme exists and what it changes for the reader — not a paraphrase of "title". One short sentence is too little; aim for 1–3 sentences with cause and scope.
|
|
2494
|
+
- Top-level "intent": 2–4 sentences on what the branch solves and why it matters.
|
|
2495
|
+
- Finding "what": cover (1) the concrete cause in the diff, (2) why it is a problem (failure mode, risk, or cost), and (3) the observable consequence. Do not stop at a symptom label.
|
|
2496
|
+
- Finding "fix": cover (1) the suggested change and (2) why that change resolves the problem / what the reader gains or avoids. Vague advice ("mejorar el manejo") is not enough.
|
|
1784
2497
|
- Signal over volume: prefer fewer high-signal findings. Order findings by severity (high, then nit). Use "nit" sparingly.
|
|
1785
2498
|
- "blocking": true ONLY if the branch should not merge as-is. Use only with class "risk".
|
|
1786
|
-
-
|
|
2499
|
+
- Parallelize: when several read-only lookups are independent (Read, Grep/search, Glob, etc.), issue them in the same turn if your tools allow parallel calls. Do not serialize independent work — finish faster by batching.
|
|
1787
2500
|
- Write human-readable strings (intent, title, what, why, fix, notes) in Spanish. Keep enums/ids/paths/JSON keys exactly as in the schema.
|
|
1788
|
-
- Length is a soft target, not a check: aim for title ≤72 characters and what/why/fix ≤120 characters. Prefer clarity over shortening. Longer text is still valid. Do not rewrite the file only to shorten it
|
|
2501
|
+
- Length is a soft target, not a check: aim for title ≤72 characters and what/why/fix ≤120 characters when possible. Prefer clarity over shortening — thin one-liners that omit cause, impact, or why-the-fix-works are worse than a slightly longer explanation. Longer text is still valid. Do not rewrite the file only to shorten it.
|
|
2502
|
+
- If the output JSON already exists and findings have \`resolution\` ({ kind: ignored|fix|corrected, detail?: string }), preserve those fields on rewrite (coverage / edit / regenerate). Keep existing finding ids when possible; do not drop or reshuffle resolutions.`;
|
|
1789
2503
|
/** @param {{ hasDiff?: boolean }} [ctx] */
|
|
1790
2504
|
function workflowLocal(base, branch, ctx = {}) {
|
|
1791
2505
|
if (ctx.hasDiff) return `Workflow:
|
|
@@ -1934,9 +2648,9 @@ Write the result to \`${outputPath}\` (relative to the repo root; use forward sl
|
|
|
1934
2648
|
|
|
1935
2649
|
${outputSchemaBlock(meta)}
|
|
1936
2650
|
|
|
1937
|
-
Include "meta" with the source/repo/branch/base above (and origin "remoteUrl" when local). "agent" and "model" are required: set "agent" to the product you are (Claude Code, Cursor,
|
|
2651
|
+
Include "meta" with the source/repo/branch/base above (and origin "remoteUrl" when local). "agent" and "model" are required: set "agent" to the product you are (Claude Code, Cursor, Copilot, …) and "model" to the exact model id you are running. Do not leave placeholders or omit them; validation fails without both. "generatedAt" is optional (ISO-8601 now).
|
|
1938
2652
|
|
|
1939
|
-
Do NOT include diffs or a "files" array. The tool loads the companion \`.diff\` (or computes with git if missing) when opening the report. Do not invent hunks or paste patches into the JSON.
|
|
2653
|
+
Do NOT include diffs or a "files" array. The tool loads the companion \`.diff\` (or computes with git if missing) when opening the report. Do not invent hunks or paste patches into the JSON. If you are rewriting an existing review JSON, copy through any per-finding \`resolution\` already present.
|
|
1940
2654
|
|
|
1941
2655
|
${validateSection}`;
|
|
1942
2656
|
}
|
|
@@ -2102,7 +2816,7 @@ function agentModels(agentId, cwd, opts) {
|
|
|
2102
2816
|
function findAgent(agentId) {
|
|
2103
2817
|
const agent = detectAgents().find((a) => a.def.id === agentId);
|
|
2104
2818
|
if (!agent) throw new Error(`No encontré el agente "${agentId}" instalado.`);
|
|
2105
|
-
if (!supportsAcp(agent)) throw new Error(`${agent.def.label} no
|
|
2819
|
+
if (!supportsAcp(agent)) throw new Error(`${agent.def.label} no se puede usar desde la web.`);
|
|
2106
2820
|
return agent;
|
|
2107
2821
|
}
|
|
2108
2822
|
function info(entry) {
|
|
@@ -2126,9 +2840,137 @@ function info(entry) {
|
|
|
2126
2840
|
function authFailureMessage(agentId, agentLabel, detail) {
|
|
2127
2841
|
return authFailureMessage$1(agentId, agentLabel, detail);
|
|
2128
2842
|
}
|
|
2129
|
-
/**
|
|
2130
|
-
|
|
2131
|
-
|
|
2843
|
+
/**
|
|
2844
|
+
* Abre el login del agente.
|
|
2845
|
+
* - Con `sessionId`: comando según authMethods ACP (proceso oculto sin TTY), espera exit 0, reconecta y retoma.
|
|
2846
|
+
* - Solo `agentId`: lanza el comando fallback en background (p. ej. fallo al listar modelos).
|
|
2847
|
+
*/
|
|
2848
|
+
async function openAgentLogin(input) {
|
|
2849
|
+
const sessionId = String(input?.sessionId || "").trim();
|
|
2850
|
+
const agentId = String(input?.agentId || "").trim();
|
|
2851
|
+
const host = input?.host != null ? String(input.host) : void 0;
|
|
2852
|
+
if (sessionId) {
|
|
2853
|
+
const entry = sessions.get(sessionId);
|
|
2854
|
+
if (!entry) throw new Error("La sesión del agente ya no existe. Recargá la página.");
|
|
2855
|
+
const method = pickLoginAuthMethod(entry.pendingAuthMethods || []);
|
|
2856
|
+
let cmd;
|
|
2857
|
+
try {
|
|
2858
|
+
cmd = resolveLoginCommand(entry.agent, method);
|
|
2859
|
+
} catch {
|
|
2860
|
+
cmd = loginCommand(entry.agentId, { host });
|
|
2861
|
+
if (!cmd) throw new Error("Este agente no tiene un inicio de sesión conocido.");
|
|
2862
|
+
}
|
|
2863
|
+
if (entry.agentId === "copilot" || agentId === "copilot") cmd = withCopilotLoginHost(cmd, host);
|
|
2864
|
+
setStatus(entry, "starting", loginWaitingMessage());
|
|
2865
|
+
entry.busy = true;
|
|
2866
|
+
try {
|
|
2867
|
+
const result = await runLoginTerminal(cmd, {
|
|
2868
|
+
wait: true,
|
|
2869
|
+
cwd: entry.repo
|
|
2870
|
+
});
|
|
2871
|
+
if (result.code !== 0) {
|
|
2872
|
+
entry.busy = false;
|
|
2873
|
+
const msg = loginIncompleteMessage();
|
|
2874
|
+
setStatus(entry, "needs_auth", msg);
|
|
2875
|
+
throw new Error(msg);
|
|
2876
|
+
}
|
|
2877
|
+
try {
|
|
2878
|
+
entry.acp?.close();
|
|
2879
|
+
} catch {}
|
|
2880
|
+
entry.acp = null;
|
|
2881
|
+
entry.pendingAuthMethods = null;
|
|
2882
|
+
const resume = entry.authResume || "chat";
|
|
2883
|
+
entry.authResume = null;
|
|
2884
|
+
entry.busy = false;
|
|
2885
|
+
if (resume === "review") {
|
|
2886
|
+
setStatus(entry, "starting", "Login ok. Reintentando la review…");
|
|
2887
|
+
continueReviewAfterDiff(entry, {
|
|
2888
|
+
repo: entry.repo,
|
|
2889
|
+
branch: entry.branch,
|
|
2890
|
+
base: entry.base,
|
|
2891
|
+
outputFilename: entry.outputFilename,
|
|
2892
|
+
model: entry.model || void 0
|
|
2893
|
+
});
|
|
2894
|
+
} else {
|
|
2895
|
+
setStatus(entry, "starting", "Login ok. Reconectando con el agente…");
|
|
2896
|
+
try {
|
|
2897
|
+
await ensureAcp(entry, entry.model || void 0);
|
|
2898
|
+
setStatus(entry, "idle");
|
|
2899
|
+
} catch (err) {
|
|
2900
|
+
entry.busy = false;
|
|
2901
|
+
if (isAuthError(err)) markNeedsAuth(entry, err, "chat");
|
|
2902
|
+
else {
|
|
2903
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
2904
|
+
setStatus(entry, "error", authFailureMessage(entry.agentId, entry.agentLabel, raw) || raw);
|
|
2905
|
+
}
|
|
2906
|
+
throw err;
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
return {
|
|
2910
|
+
display: result.display,
|
|
2911
|
+
resumed: true,
|
|
2912
|
+
code: result.code
|
|
2913
|
+
};
|
|
2914
|
+
} catch (err) {
|
|
2915
|
+
entry.busy = false;
|
|
2916
|
+
if (entry.status !== "needs_auth" && entry.status !== "error" && entry.status !== "starting") setStatus(entry, "needs_auth", err instanceof Error ? err.message : String(err));
|
|
2917
|
+
throw err;
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
if (!agentId) throw new Error("Falta agentId o sessionId.");
|
|
2921
|
+
const cmd = loginCommand(agentId, { host });
|
|
2922
|
+
if (!cmd) throw new Error("Este agente no tiene un inicio de sesión conocido.");
|
|
2923
|
+
return openLoginTerminal(cmd);
|
|
2924
|
+
}
|
|
2925
|
+
/**
|
|
2926
|
+
* Cierra la sesión del agente (y cuentas `gh` si es Copilot) para poder elegir otra cuenta.
|
|
2927
|
+
* Deja la entry en `needs_auth` lista para un nuevo login.
|
|
2928
|
+
*/
|
|
2929
|
+
async function openAgentLogout(input) {
|
|
2930
|
+
const sessionId = String(input?.sessionId || "").trim();
|
|
2931
|
+
const host = input?.host != null ? String(input.host) : void 0;
|
|
2932
|
+
let agentId = String(input?.agentId || "").trim();
|
|
2933
|
+
/** @type {Entry | undefined} */
|
|
2934
|
+
let entry;
|
|
2935
|
+
if (sessionId) {
|
|
2936
|
+
entry = sessions.get(sessionId);
|
|
2937
|
+
if (!entry) throw new Error("La sesión del agente ya no existe. Recargá la página.");
|
|
2938
|
+
agentId = entry.agentId || agentId;
|
|
2939
|
+
}
|
|
2940
|
+
if (!agentId) throw new Error("Falta agentId o sessionId.");
|
|
2941
|
+
/** @type {string[]} */
|
|
2942
|
+
const steps = [];
|
|
2943
|
+
/** @type {string[]} */
|
|
2944
|
+
const warnings = [];
|
|
2945
|
+
if (entry?.acp?.supportsLogout) try {
|
|
2946
|
+
await entry.acp.logout();
|
|
2947
|
+
steps.push("Cerré la sesión del agente.");
|
|
2948
|
+
} catch (err) {
|
|
2949
|
+
warnings.push(err instanceof Error ? err.message : String(err));
|
|
2950
|
+
}
|
|
2951
|
+
try {
|
|
2952
|
+
entry?.acp?.close();
|
|
2953
|
+
} catch {}
|
|
2954
|
+
if (entry) {
|
|
2955
|
+
entry.acp = null;
|
|
2956
|
+
entry.starting = null;
|
|
2957
|
+
entry.busy = false;
|
|
2958
|
+
entry.pendingAuthMethods = null;
|
|
2959
|
+
entry.authResume = entry.authResume || (entry.seed || entry.diffText ? "review" : "chat");
|
|
2960
|
+
}
|
|
2961
|
+
const cli = await runAgentLogout(agentId, {
|
|
2962
|
+
host,
|
|
2963
|
+
cwd: entry?.repo
|
|
2964
|
+
});
|
|
2965
|
+
steps.push(...cli.steps);
|
|
2966
|
+
warnings.push(...cli.warnings);
|
|
2967
|
+
const msg = warnings.length && !cli.ok ? warnings[0] : "Sesión cerrada. Elegí con qué cuenta iniciar de nuevo.";
|
|
2968
|
+
if (entry) setStatus(entry, "needs_auth", msg);
|
|
2969
|
+
return {
|
|
2970
|
+
ok: cli.ok || steps.length > 0,
|
|
2971
|
+
steps,
|
|
2972
|
+
warnings
|
|
2973
|
+
};
|
|
2132
2974
|
}
|
|
2133
2975
|
/** Escribe seed (JSON + .diff) en `.diff-review/` para que el agente los lea. */
|
|
2134
2976
|
function materializeSeed(entry) {
|
|
@@ -2145,15 +2987,25 @@ function materializeSeed(entry) {
|
|
|
2145
2987
|
});
|
|
2146
2988
|
} catch {}
|
|
2147
2989
|
}
|
|
2990
|
+
/** Escribe (pisa) el companion `.chat.md` de handoff si hay markdown pendiente. */
|
|
2991
|
+
function materializeHandoffChat(entry) {
|
|
2992
|
+
const text = entry.handoffChatMarkdown;
|
|
2993
|
+
if (text == null) return;
|
|
2994
|
+
try {
|
|
2995
|
+
writeHandoffChat(entry.repo, entry.outputFilename, text);
|
|
2996
|
+
} catch {}
|
|
2997
|
+
entry.handoffChatMarkdown = null;
|
|
2998
|
+
}
|
|
2148
2999
|
/** Prefijo del primer mensaje cuando la sesión ACP es nueva. */
|
|
2149
|
-
function contextPreamble(entry) {
|
|
3000
|
+
function contextPreamble(entry, handoffNote) {
|
|
2150
3001
|
materializeSeed(entry);
|
|
3002
|
+
materializeHandoffChat(entry);
|
|
2151
3003
|
const paths = companionPaths(entry.repo, entry.outputFilename);
|
|
2152
3004
|
const jsonRel = outputRelPath(entry.outputFilename);
|
|
2153
3005
|
const diffRel = `${outputRelPath(entry.outputFilename).replace(/\.json$/i, "")}.diff`;
|
|
2154
3006
|
const hasJson = existsSync(paths.json);
|
|
2155
3007
|
const hasDiff = existsSync(paths.diff);
|
|
2156
|
-
|
|
3008
|
+
const lines = [
|
|
2157
3009
|
"Context for this chat (new agent session — read these files before answering):",
|
|
2158
3010
|
`- Repo: ${entry.repo}`,
|
|
2159
3011
|
`- Branch under review: ${entry.branch}`,
|
|
@@ -2161,8 +3013,14 @@ function contextPreamble(entry) {
|
|
|
2161
3013
|
hasJson ? `- Review JSON (relative to repo root): \`${jsonRel}\` — read this file.` : `- Review JSON: not available on disk (no seed/payload was provided). Say so if you need it.`,
|
|
2162
3014
|
hasDiff ? `- Diff (relative to repo root): \`${diffRel}\` — read this file.` : `- Diff file: not available on disk (no seed/diff and git cache failed). Say so if you need it.`,
|
|
2163
3015
|
"",
|
|
2164
|
-
"
|
|
2165
|
-
|
|
3016
|
+
"The user question or fix request is the priority. The review JSON is reference context — do not re-litigate or re-review findings unless the user asks.",
|
|
3017
|
+
"",
|
|
3018
|
+
"Findings may include optional `resolution` ({ kind: ignored|fix|corrected, detail?: string }) written by the UI. Preserve those fields when editing the JSON. Corrected findings and their detail are prior work — use them as context; do not undo them unless the user asks."
|
|
3019
|
+
];
|
|
3020
|
+
const note = String(handoffNote || "").trim();
|
|
3021
|
+
if (note) lines.push("", note);
|
|
3022
|
+
lines.push("", "User message:");
|
|
3023
|
+
return lines.join("\n");
|
|
2166
3024
|
}
|
|
2167
3025
|
function permissionReasonLabel(allow, scope, deniedReason) {
|
|
2168
3026
|
if (!allow) return deniedReason;
|
|
@@ -2263,14 +3121,36 @@ function emit(entry, event) {
|
|
|
2263
3121
|
}
|
|
2264
3122
|
function setStatus(entry, status, message) {
|
|
2265
3123
|
entry.status = status;
|
|
2266
|
-
if (status === "error") entry.statusMessage = message || entry.statusMessage || "Error del agente.";
|
|
3124
|
+
if (status === "error" || status === "needs_auth") entry.statusMessage = message || entry.statusMessage || "Error del agente.";
|
|
2267
3125
|
else if (message !== void 0) delete entry.statusMessage;
|
|
2268
3126
|
emit(entry, {
|
|
2269
3127
|
type: "status",
|
|
2270
3128
|
status,
|
|
2271
|
-
...message || status === "error" ? { message: message || entry.statusMessage } : {}
|
|
3129
|
+
...message || status === "error" || status === "needs_auth" ? { message: message || entry.statusMessage } : {}
|
|
2272
3130
|
});
|
|
2273
3131
|
}
|
|
3132
|
+
function markNeedsAuth(entry, err, resume) {
|
|
3133
|
+
const authErr = err instanceof AcpAuthRequiredError ? err : null;
|
|
3134
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
3135
|
+
const msg = authErr?.message || authFailureMessage(entry.agentId, entry.agentLabel, raw) || raw;
|
|
3136
|
+
entry.pendingAuthMethods = authErr?.methods?.length ? authErr.methods : entry.pendingAuthMethods;
|
|
3137
|
+
entry.authResume = resume;
|
|
3138
|
+
entry.acp = null;
|
|
3139
|
+
setStatus(entry, "needs_auth", msg);
|
|
3140
|
+
}
|
|
3141
|
+
function isAuthError(err) {
|
|
3142
|
+
if (err instanceof AcpAuthRequiredError) return true;
|
|
3143
|
+
return looksLikeAuthFailure(err instanceof Error ? err.message : String(err));
|
|
3144
|
+
}
|
|
3145
|
+
/** Sin sesión CLI y con login desde la app (Claude / Copilot / Cursor). */
|
|
3146
|
+
function needsInteractiveLogin(agentId) {
|
|
3147
|
+
if (!loginCommand(agentId)) return false;
|
|
3148
|
+
try {
|
|
3149
|
+
return !agentAccountStatus(agentId).loggedIn;
|
|
3150
|
+
} catch {
|
|
3151
|
+
return false;
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
2274
3154
|
/** SessionUpdate de ACP → evento de la web (lo que no se muestra se descarta). */
|
|
2275
3155
|
function toEvent(update) {
|
|
2276
3156
|
switch (update.sessionUpdate) {
|
|
@@ -2354,13 +3234,13 @@ async function ensureAcp(entry, model) {
|
|
|
2354
3234
|
entry.busy = false;
|
|
2355
3235
|
const auth = authFailureMessage(entry.agentId, entry.agentLabel, stderr);
|
|
2356
3236
|
if (auth) {
|
|
2357
|
-
|
|
3237
|
+
markNeedsAuth(entry, new Error(auth), entry.mode === "review" ? "review" : "chat");
|
|
2358
3238
|
return;
|
|
2359
3239
|
}
|
|
2360
3240
|
const raw = stderr.trim() || `código ${code ?? "?"}`;
|
|
2361
3241
|
const authFromCode = authFailureMessage(entry.agentId, entry.agentLabel, raw);
|
|
2362
3242
|
if (authFromCode) {
|
|
2363
|
-
|
|
3243
|
+
markNeedsAuth(entry, new Error(authFromCode), entry.mode === "review" ? "review" : "chat");
|
|
2364
3244
|
return;
|
|
2365
3245
|
}
|
|
2366
3246
|
const tail = stderr.trim().split(/\r?\n/).slice(-3).join(" · ");
|
|
@@ -2428,7 +3308,7 @@ function checkEntryReport(entry) {
|
|
|
2428
3308
|
function invalidJsonFixPrompt(entry, path, result) {
|
|
2429
3309
|
return `${formatCheckReport(path, validateCliCommand(entry.outputFilename), result)}
|
|
2430
3310
|
|
|
2431
|
-
Critical:
|
|
3311
|
+
Critical: fix ONLY the listed schema/validation errors above. Do NOT regenerate or reshuffle findings unnecessarily. Preserve existing finding ids and any \`resolution\` fields when possible. Do NOT only bump meta.generatedAt or rewrite notes. Edit \`${outputRelPath(entry.outputFilename).replace(/\\/g, "/")}\` so every problem above is fixed (add blocks or skipped for each missing path). Do not claim success until coverage is complete.
|
|
2432
3312
|
|
|
2433
3313
|
${TOOL_RULES}
|
|
2434
3314
|
|
|
@@ -2478,6 +3358,7 @@ async function checkOutput(entry, since, retriesLeft) {
|
|
|
2478
3358
|
stampReviewProvenance(path, {
|
|
2479
3359
|
agent: entry.agentLabel,
|
|
2480
3360
|
model: entry.model,
|
|
3361
|
+
modelLabel: entry.acp?.models.find((o) => o.value === entry.model)?.name,
|
|
2481
3362
|
agentSession: {
|
|
2482
3363
|
agentId: entry.agentId,
|
|
2483
3364
|
sessionId: entry.acpSessionId,
|
|
@@ -2545,6 +3426,11 @@ async function runTurn(entry, text, mode, opts) {
|
|
|
2545
3426
|
mode: mode === "review" ? "review" : mode
|
|
2546
3427
|
});
|
|
2547
3428
|
try {
|
|
3429
|
+
if (!entry.acp && needsInteractiveLogin(entry.agentId)) {
|
|
3430
|
+
entry.busy = false;
|
|
3431
|
+
markNeedsAuth(entry, /* @__PURE__ */ new Error("not logged in"), mode === "review" ? "review" : "chat");
|
|
3432
|
+
return;
|
|
3433
|
+
}
|
|
2548
3434
|
if (!entry.acp) setStatus(entry, "starting", `Lanzando ${entry.agentLabel}…`);
|
|
2549
3435
|
const acp = await ensureAcp(entry, opts.model);
|
|
2550
3436
|
if (entry.cancelled) {
|
|
@@ -2566,7 +3452,7 @@ async function runTurn(entry, text, mode, opts) {
|
|
|
2566
3452
|
setStatus(entry, "idle", "Cancelado.");
|
|
2567
3453
|
return;
|
|
2568
3454
|
}
|
|
2569
|
-
if (mode !== "ask") await checkOutput(entry, since, OUTPUT_AUTO_RETRIES);
|
|
3455
|
+
if (mode !== "ask" && mode !== "fix") await checkOutput(entry, since, OUTPUT_AUTO_RETRIES);
|
|
2570
3456
|
entry.busy = false;
|
|
2571
3457
|
if (entry.cancelled) setStatus(entry, "idle", "Cancelado.");
|
|
2572
3458
|
else setStatus(entry, "idle");
|
|
@@ -2576,6 +3462,10 @@ async function runTurn(entry, text, mode, opts) {
|
|
|
2576
3462
|
setStatus(entry, "idle", "Cancelado.");
|
|
2577
3463
|
return;
|
|
2578
3464
|
}
|
|
3465
|
+
if (isAuthError(err)) {
|
|
3466
|
+
markNeedsAuth(entry, err, mode === "review" ? "review" : "chat");
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
2579
3469
|
const raw = err instanceof Error ? err.message : String(err);
|
|
2580
3470
|
setStatus(entry, "error", authFailureMessage(entry.agentId, entry.agentLabel, raw) || raw);
|
|
2581
3471
|
} finally {
|
|
@@ -2608,10 +3498,14 @@ function newEntry(agent, fields) {
|
|
|
2608
3498
|
rejections: [],
|
|
2609
3499
|
needsContext: false,
|
|
2610
3500
|
seed: null,
|
|
3501
|
+
handoffNote: null,
|
|
3502
|
+
handoffChatMarkdown: null,
|
|
2611
3503
|
diffPaths: null,
|
|
2612
3504
|
pendingPermissions: /* @__PURE__ */ new Map(),
|
|
2613
3505
|
sessionAllowDenied: false,
|
|
2614
|
-
cancelled: false
|
|
3506
|
+
cancelled: false,
|
|
3507
|
+
pendingAuthMethods: null,
|
|
3508
|
+
authResume: null
|
|
2615
3509
|
};
|
|
2616
3510
|
sessions.set(entry.id, entry);
|
|
2617
3511
|
return entry;
|
|
@@ -2697,9 +3591,13 @@ async function continueReviewAfterDiff(entry, input) {
|
|
|
2697
3591
|
*/
|
|
2698
3592
|
async function openChat(input) {
|
|
2699
3593
|
const sessionId = String(input.sessionId || "").trim();
|
|
3594
|
+
const handoffNote = String(input.handoffNote || "").trim() || null;
|
|
3595
|
+
const handoffChat = input.handoffChatMarkdown != null ? String(input.handoffChatMarkdown) : null;
|
|
2700
3596
|
if (sessionId) {
|
|
2701
3597
|
for (const entry of sessions.values()) if (entry.acpSessionId === sessionId && entry.repo === input.repo) {
|
|
2702
3598
|
if (input.seed) entry.seed = input.seed;
|
|
3599
|
+
if (handoffNote) entry.handoffNote = handoffNote;
|
|
3600
|
+
if (handoffChat != null) entry.handoffChatMarkdown = handoffChat;
|
|
2703
3601
|
return info(entry);
|
|
2704
3602
|
}
|
|
2705
3603
|
}
|
|
@@ -2709,14 +3607,23 @@ async function openChat(input) {
|
|
|
2709
3607
|
acpSessionId: sessionId
|
|
2710
3608
|
});
|
|
2711
3609
|
if (input.seed) entry.seed = input.seed;
|
|
3610
|
+
if (handoffNote) entry.handoffNote = handoffNote;
|
|
3611
|
+
if (handoffChat != null) entry.handoffChatMarkdown = handoffChat;
|
|
2712
3612
|
if (!sessionId) entry.needsContext = true;
|
|
3613
|
+
if (needsInteractiveLogin(entry.agentId)) {
|
|
3614
|
+
markNeedsAuth(entry, /* @__PURE__ */ new Error("not logged in"), "chat");
|
|
3615
|
+
return info(entry);
|
|
3616
|
+
}
|
|
2713
3617
|
try {
|
|
2714
3618
|
if (!entry.acp) setStatus(entry, "starting", `Lanzando ${entry.agentLabel}…`);
|
|
2715
3619
|
await ensureAcp(entry, input.model);
|
|
2716
3620
|
if (entry.status === "starting") setStatus(entry, "idle");
|
|
2717
3621
|
} catch (err) {
|
|
2718
|
-
|
|
2719
|
-
|
|
3622
|
+
if (isAuthError(err)) markNeedsAuth(entry, err, "chat");
|
|
3623
|
+
else {
|
|
3624
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
3625
|
+
setStatus(entry, "error", authFailureMessage(entry.agentId, entry.agentLabel, raw) || raw);
|
|
3626
|
+
}
|
|
2720
3627
|
}
|
|
2721
3628
|
return info(entry);
|
|
2722
3629
|
}
|
|
@@ -2726,13 +3633,17 @@ function sendMessage(id, text, mode, opts) {
|
|
|
2726
3633
|
const body = text.trim();
|
|
2727
3634
|
if (!body) throw new Error("El mensaje está vacío.");
|
|
2728
3635
|
if (entry.busy) throw new Error("El agente todavía está trabajando en el pedido anterior.");
|
|
3636
|
+
const sendHandoff = String(opts?.handoffNote || "").trim();
|
|
3637
|
+
if (sendHandoff) entry.handoffNote = sendHandoff;
|
|
3638
|
+
if (opts?.handoffChatMarkdown != null) entry.handoffChatMarkdown = String(opts.handoffChatMarkdown);
|
|
2729
3639
|
let agentText = body;
|
|
2730
3640
|
if (entry.needsContext) {
|
|
2731
|
-
agentText = `${contextPreamble(entry)}\n${body}`;
|
|
3641
|
+
agentText = `${contextPreamble(entry, entry.handoffNote)}\n${body}`;
|
|
2732
3642
|
entry.needsContext = false;
|
|
3643
|
+
entry.handoffNote = null;
|
|
2733
3644
|
entry.seed = null;
|
|
2734
3645
|
}
|
|
2735
|
-
runTurn(entry, mode === "edit" ? `${agentText}\n\nApply this by editing ${entry.outputFilename} in place (keep everything else as is). Reply briefly in Spanish with what you changed.\n\n${SERVER_VALIDATES}` : `${agentText}\n\nAnswer in Spanish. This is a question about the review: do not modify any file.`, mode, {
|
|
3646
|
+
runTurn(entry, mode === "edit" ? `${agentText}\n\nApply this by editing ${entry.outputFilename} in place (keep everything else as is; preserve finding ids and any \`resolution\` fields). Reply briefly in Spanish with what you changed.\n\n${SERVER_VALIDATES}` : mode === "fix" ? `${agentText}\n\nApply ONLY the listed code fixes by editing files inside this repo. Do not rewrite the review JSON. Touch nothing else. Reply briefly in Spanish with a short per-finding detail suitable for resolution.detail.` : `${agentText}\n\nAnswer in Spanish. This is a question about the review: do not modify any file.`, mode, {
|
|
2736
3647
|
shown: body,
|
|
2737
3648
|
model: String(opts?.model || "").trim() || void 0
|
|
2738
3649
|
});
|
|
@@ -2747,6 +3658,23 @@ function retryReview(id) {
|
|
|
2747
3658
|
const entry = sessions.get(String(id || ""));
|
|
2748
3659
|
if (!entry) throw new Error("La sesión del agente ya no existe. Recargá la página.");
|
|
2749
3660
|
if (entry.busy) throw new Error("El agente todavía está trabajando en el pedido anterior.");
|
|
3661
|
+
if (entry.status === "needs_auth" || entry.status === "error" && looksLikeAuthFailure(entry.statusMessage || "")) {
|
|
3662
|
+
entry.pendingAuthMethods = null;
|
|
3663
|
+
entry.authResume = null;
|
|
3664
|
+
try {
|
|
3665
|
+
entry.acp?.close();
|
|
3666
|
+
} catch {}
|
|
3667
|
+
entry.acp = null;
|
|
3668
|
+
setStatus(entry, "starting", "Reintentando la review…");
|
|
3669
|
+
continueReviewAfterDiff(entry, {
|
|
3670
|
+
repo: entry.repo,
|
|
3671
|
+
branch: entry.branch,
|
|
3672
|
+
base: entry.base,
|
|
3673
|
+
outputFilename: entry.outputFilename,
|
|
3674
|
+
model: entry.model || void 0
|
|
3675
|
+
});
|
|
3676
|
+
return info(entry);
|
|
3677
|
+
}
|
|
2750
3678
|
if (entry.status === "error") throw new Error(entry.statusMessage || "La sesión está en error; iniciá una nueva review.");
|
|
2751
3679
|
const posixRel = outputRelPath(entry.outputFilename).replace(/\\/g, "/");
|
|
2752
3680
|
const existing = checkEntryReport(entry);
|
|
@@ -2853,6 +3781,35 @@ async function cancelTurn(id) {
|
|
|
2853
3781
|
entry.busy = false;
|
|
2854
3782
|
setStatus(entry, "idle", "Cancelado.");
|
|
2855
3783
|
}
|
|
3784
|
+
/**
|
|
3785
|
+
* Cierra la sesión de chat: ACP, Map y permisos pendientes.
|
|
3786
|
+
* La UI lo usa al cambiar de agente (el reporte / triage se mantienen).
|
|
3787
|
+
*/
|
|
3788
|
+
async function closeChat(id) {
|
|
3789
|
+
const entry = sessions.get(String(id || ""));
|
|
3790
|
+
if (!entry) return;
|
|
3791
|
+
entry.cancelled = true;
|
|
3792
|
+
for (const [requestId, pending] of [...entry.pendingPermissions]) {
|
|
3793
|
+
clearTimeout(pending.timer);
|
|
3794
|
+
entry.pendingPermissions.delete(requestId);
|
|
3795
|
+
try {
|
|
3796
|
+
pending.resolve(false, { quiet: true });
|
|
3797
|
+
} catch {}
|
|
3798
|
+
}
|
|
3799
|
+
if (entry.idleTimer) {
|
|
3800
|
+
clearTimeout(entry.idleTimer);
|
|
3801
|
+
entry.idleTimer = null;
|
|
3802
|
+
}
|
|
3803
|
+
try {
|
|
3804
|
+
if (entry.acp && entry.busy) try {
|
|
3805
|
+
await entry.acp.cancel();
|
|
3806
|
+
} catch {}
|
|
3807
|
+
entry.acp?.close();
|
|
3808
|
+
} catch {}
|
|
3809
|
+
entry.acp = null;
|
|
3810
|
+
entry.listeners.clear();
|
|
3811
|
+
sessions.delete(entry.id);
|
|
3812
|
+
}
|
|
2856
3813
|
function subscribe(id, listener) {
|
|
2857
3814
|
const entry = sessions.get(id);
|
|
2858
3815
|
if (!entry) return null;
|
|
@@ -2876,5 +3833,5 @@ function startPendingAgentRun() {
|
|
|
2876
3833
|
}
|
|
2877
3834
|
}
|
|
2878
3835
|
|
|
2879
|
-
export { agentModels as a, startReview as b, sendMessage as c, setPermissionTrust as d, cancelTurn as e,
|
|
2880
|
-
//# sourceMappingURL=agent-sessions-
|
|
3836
|
+
export { agentModels as a, startReview as b, sendMessage as c, setPermissionTrust as d, cancelTurn as e, closeChat as f, retryReview as g, agentAccountStatus as h, openAgentLogin as i, openAgentLogout as j, subscribe as k, listAcpAgents as l, startPendingAgentRun as m, openChat as o, respondPermission as r, sessionInfo as s };
|
|
3837
|
+
//# sourceMappingURL=agent-sessions-BArYLTW6.js.map
|