chocolatito-code 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +89 -0
  2. package/dist/agent/compaction.d.ts +65 -0
  3. package/dist/agent/compaction.js +134 -0
  4. package/dist/agent/context.d.ts +3 -0
  5. package/dist/agent/context.js +35 -0
  6. package/dist/agent/loop.d.ts +91 -2
  7. package/dist/agent/loop.js +536 -78
  8. package/dist/agent/loopDetector.d.ts +57 -0
  9. package/dist/agent/loopDetector.js +0 -0
  10. package/dist/agent/mentions.d.ts +13 -0
  11. package/dist/agent/mentions.js +49 -0
  12. package/dist/agent/repoMap.d.ts +117 -0
  13. package/dist/agent/repoMap.js +560 -0
  14. package/dist/agent/result.d.ts +64 -0
  15. package/dist/agent/result.js +48 -0
  16. package/dist/agent/retry.d.ts +35 -0
  17. package/dist/agent/retry.js +123 -4
  18. package/dist/agent/subagent.js +62 -15
  19. package/dist/agent/syntaxValidator.js +288 -52
  20. package/dist/agent/toolGate.d.ts +58 -0
  21. package/dist/agent/toolGate.js +111 -0
  22. package/dist/agent/tracker.js +4 -7
  23. package/dist/agent/undoManager.d.ts +44 -11
  24. package/dist/agent/undoManager.js +95 -24
  25. package/dist/agent/usageMeter.d.ts +56 -0
  26. package/dist/agent/usageMeter.js +51 -0
  27. package/dist/agent/verifier.d.ts +49 -0
  28. package/dist/agent/verifier.js +158 -0
  29. package/dist/config/constants.js +14 -0
  30. package/dist/config/engine.d.ts +19 -0
  31. package/dist/config/engine.js +35 -3
  32. package/dist/config/license.d.ts +5 -0
  33. package/dist/config/license.js +107 -9
  34. package/dist/config/limits.d.ts +28 -0
  35. package/dist/config/limits.js +51 -0
  36. package/dist/config/permissions.d.ts +76 -1
  37. package/dist/config/permissions.js +379 -23
  38. package/dist/config/quota.js +24 -1
  39. package/dist/config/updater.d.ts +68 -0
  40. package/dist/config/updater.js +215 -0
  41. package/dist/hooks/manager.js +159 -14
  42. package/dist/index.js +222 -34
  43. package/dist/mcp/client.d.ts +22 -0
  44. package/dist/mcp/client.js +93 -11
  45. package/dist/prompts/systemPrompt.js +12 -1
  46. package/dist/sessions/manager.d.ts +12 -0
  47. package/dist/sessions/manager.js +33 -5
  48. package/dist/sessions/resume.d.ts +36 -0
  49. package/dist/sessions/resume.js +43 -0
  50. package/dist/skills/manager.d.ts +1 -0
  51. package/dist/skills/manager.js +9 -0
  52. package/dist/tools/binary.d.ts +46 -0
  53. package/dist/tools/binary.js +100 -0
  54. package/dist/tools/browserCdp.js +13 -1
  55. package/dist/tools/browserExtension.d.ts +5 -0
  56. package/dist/tools/browserExtension.js +202 -18
  57. package/dist/tools/browserSession.d.ts +23 -0
  58. package/dist/tools/browserSession.js +28 -0
  59. package/dist/tools/computerUse.js +33 -1
  60. package/dist/tools/createImage.d.ts +11 -0
  61. package/dist/tools/createImage.js +13 -1
  62. package/dist/tools/definitions.js +9 -3
  63. package/dist/tools/editDiagnosis.d.ts +54 -0
  64. package/dist/tools/editDiagnosis.js +142 -0
  65. package/dist/tools/editFile.d.ts +1 -0
  66. package/dist/tools/editFile.js +118 -62
  67. package/dist/tools/findFiles.d.ts +21 -0
  68. package/dist/tools/findFiles.js +75 -6
  69. package/dist/tools/getSystemInfo.js +15 -7
  70. package/dist/tools/gitTools.d.ts +24 -0
  71. package/dist/tools/gitTools.js +87 -26
  72. package/dist/tools/moveCopyFile.d.ts +18 -1
  73. package/dist/tools/moveCopyFile.js +74 -4
  74. package/dist/tools/patchCascade.d.ts +57 -0
  75. package/dist/tools/patchCascade.js +338 -0
  76. package/dist/tools/runCommand.js +213 -25
  77. package/dist/tools/runner.js +57 -4
  78. package/dist/tools/toolDefsComputer.js +12 -2
  79. package/dist/tools/truncator.d.ts +27 -1
  80. package/dist/tools/truncator.js +117 -11
  81. package/dist/tools/viewFile.js +15 -11
  82. package/dist/tools/visionBridge.js +7 -0
  83. package/dist/tools/webSearch.d.ts +21 -0
  84. package/dist/tools/webSearch.js +125 -14
  85. package/dist/tools/writeFile.js +18 -1
  86. package/dist/ui/interrupt.d.ts +4 -1
  87. package/dist/ui/interrupt.js +56 -5
  88. package/dist/ui/keyboardGuard.d.ts +55 -0
  89. package/dist/ui/keyboardGuard.js +65 -0
  90. package/dist/ui/loopPrompt.d.ts +17 -0
  91. package/dist/ui/loopPrompt.js +59 -0
  92. package/dist/ui/permissionPrompt.js +84 -2
  93. package/dist/ui/prompt.d.ts +35 -0
  94. package/dist/ui/prompt.js +345 -125
  95. package/dist/ui/reasoningStream.d.ts +8 -0
  96. package/dist/ui/reasoningStream.js +99 -7
  97. package/extension/README.md +95 -74
  98. package/extension/background.js +598 -178
  99. package/extension/content.js +92 -9
  100. package/extension/manifest.json +2 -1
  101. package/extension/popup.js +28 -12
  102. package/package.json +1 -1
@@ -1,6 +1,52 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import fg from "fast-glob";
4
+ /**
5
+ * Tope de coincidencias que se recogen del disco. Antes el freno era
6
+ * `deep: 6`, que recortaba por profundidad y en silencio. Ahora no hay limite
7
+ * de profundidad, asi que hace falta un tope de resultados para que una
8
+ * busqueda desde la raiz de un disco no se quede recorriendo millones de
9
+ * entradas. Este si se avisa en la respuesta.
10
+ */
11
+ const MAX_COINCIDENCIAS = 1000;
12
+ /** Cuantas se le enseñan al modelo. El resto solo se cuentan. */
13
+ const MAX_MOSTRADAS = 40;
14
+ /** Caracteres que convierten un texto en glob. Si no hay ninguno, es un nombre. */
15
+ const META_GLOB = /[*?[\]{}]/;
16
+ /**
17
+ * Traduce lo que escribe el modelo al glob que fast-glob necesita.
18
+ *
19
+ * Dos trampas, y las dos daban cero resultados sin decir nada:
20
+ *
21
+ * 1. En glob, `*.ts` significa ".ts en el nivel raiz", no "en todo el arbol".
22
+ * La descripcion de la herramienta pone `'*.tsx'` como ejemplo, asi que el
23
+ * modelo lo escribe tal cual, recibe cero y concluye que el proyecto no
24
+ * tiene TypeScript. El codigo viejo hacia justo lo contrario de lo que hace
25
+ * falta: solo volvia recursivos los patrones SIN comodin.
26
+ *
27
+ * 2. fast-glob trata `\` como caracter de escape, no como separador. Un
28
+ * patron con rutas de Windows (`src\**\*.ts`) se convertia en `src**.ts` y
29
+ * no casaba con nada.
30
+ *
31
+ * Devuelve una lista porque para un patron con ruta se prueban dos: la ruta tal
32
+ * cual (relativa a la carpeta de busqueda) y la misma anclada a cualquier nivel,
33
+ * que es lo que suele querer decir quien escribe `src/*.ts` sin saber desde
34
+ * donde se esta buscando. fast-glob deduplica.
35
+ */
36
+ export function normalizarPatron(pattern) {
37
+ const limpio = pattern.trim().replace(/\\/g, "/").replace(/^\.\//, "");
38
+ if (limpio === "")
39
+ return ["**/*"];
40
+ // Nombre suelto ("package.json", "claude"): subcadena, en cualquier nivel.
41
+ if (!META_GLOB.test(limpio))
42
+ return [`**/*${limpio}*`];
43
+ // Glob sin ruta ("*.ts", "test-*"): lo que el modelo quiere decir es
44
+ // "en todo el arbol", no "solo en la raiz".
45
+ if (!limpio.includes("/"))
46
+ return [`**/${limpio}`];
47
+ // Glob con ruta: se respeta, y ademas se busca en cualquier nivel.
48
+ return limpio.startsWith("**/") ? [limpio] : [limpio, `**/${limpio}`];
49
+ }
4
50
  export async function findFiles(pattern, searchDirectory = process.cwd(), cwd = process.cwd()) {
5
51
  const resolvedDir = path.isAbsolute(searchDirectory)
6
52
  ? searchDirectory
@@ -8,17 +54,36 @@ export async function findFiles(pattern, searchDirectory = process.cwd(), cwd =
8
54
  if (!fs.existsSync(resolvedDir)) {
9
55
  return `Error: La carpeta "${searchDirectory}" no existe.`;
10
56
  }
11
- const matches = await fg([pattern.includes("*") ? pattern : `**/*${pattern}*`], {
57
+ const patrones = normalizarPatron(pattern);
58
+ const flujo = fg.stream(patrones, {
12
59
  cwd: resolvedDir,
13
60
  dot: true,
14
61
  onlyFiles: false,
15
- deep: 6,
62
+ // Sin `deep`: la profundidad la limitaba a 6 niveles y perdia ficheros sin
63
+ // avisar. El freno ahora es MAX_COINCIDENCIAS.
64
+ followSymbolicLinks: false, // sin tope de profundidad, un symlink circular no termina
65
+ suppressErrors: true, // una carpeta sin permisos no debe tumbar la busqueda entera
16
66
  ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/AppData/**", "**/__pycache__/**"],
17
67
  });
68
+ const matches = [];
69
+ let truncado = false;
70
+ // `break` en un for-await destruye el stream, asi que el recorrido del disco
71
+ // se para aqui de verdad.
72
+ for await (const entrada of flujo) {
73
+ matches.push(String(entrada));
74
+ if (matches.length >= MAX_COINCIDENCIAS) {
75
+ truncado = true;
76
+ break;
77
+ }
78
+ }
79
+ matches.sort();
18
80
  if (matches.length === 0) {
19
- return `No se encontraron archivos ni carpetas con el patrón "${pattern}" en "${resolvedDir}".`;
81
+ // Se dice con que patron se busco de verdad: un vacio legitimo y un patron
82
+ // mal escrito daban antes el mismo mensaje.
83
+ const usados = patrones.map((p) => `"${p}"`).join(" y ");
84
+ return `No se encontraron archivos ni carpetas con el patrón "${pattern}" (buscado como ${usados}) en "${resolvedDir}".`;
20
85
  }
21
- const formatted = matches.slice(0, 40).map((relPath) => {
86
+ const formatted = matches.slice(0, MAX_MOSTRADAS).map((relPath) => {
22
87
  const full = path.join(resolvedDir, relPath);
23
88
  try {
24
89
  const isDir = fs.statSync(full).isDirectory();
@@ -28,6 +93,10 @@ export async function findFiles(pattern, searchDirectory = process.cwd(), cwd =
28
93
  return `[ITEM] ${relPath}`;
29
94
  }
30
95
  });
31
- const totalStr = matches.length > 40 ? ` (mostrando primeros 40 de ${matches.length})` : "";
32
- return `Resultados encontrados para "${pattern}" en "${resolvedDir}"${totalStr}:\n${formatted.join("\n")}`;
96
+ const total = truncado ? `${MAX_COINCIDENCIAS}+` : `${matches.length}`;
97
+ const totalStr = matches.length > MAX_MOSTRADAS ? ` (mostrando primeros ${MAX_MOSTRADAS} de ${total})` : "";
98
+ const aviso = truncado
99
+ ? `\n(Búsqueda cortada en ${MAX_COINCIDENCIAS} coincidencias: afina el patrón o la carpeta.)`
100
+ : "";
101
+ return `Resultados encontrados para "${pattern}" en "${resolvedDir}"${totalStr}:\n${formatted.join("\n")}${aviso}`;
33
102
  }
@@ -1,17 +1,25 @@
1
1
  import os from "node:os";
2
- import { execa } from "execa";
2
+ import { ejecutarGit } from "./gitTools.js";
3
3
  export async function getSystemInfo(cwd = process.cwd()) {
4
4
  const nodeVersion = process.version;
5
5
  const platform = `${os.type()} ${os.release()} (${os.arch()})`;
6
6
  const memoryFree = `${(os.freemem() / (1024 * 1024 * 1024)).toFixed(1)} GB`;
7
7
  const memoryTotal = `${(os.totalmem() / (1024 * 1024 * 1024)).toFixed(1)} GB`;
8
- let gitInfo = "Git no disponible o fuera de repositorio.";
9
- try {
10
- const { stdout: status } = await execa("git", ["status", "--short"], { cwd, reject: false });
11
- const { stdout: branch } = await execa("git", ["branch", "--show-current"], { cwd, reject: false });
12
- gitInfo = `Rama: ${branch.trim() || "HEAD"}\nCambios:\n${status.trim() || "Directorio limpio (sin cambios pendientes)."}`;
8
+ // Antes esto era `let gitInfo = "Git no disponible..."` dentro de un try/catch
9
+ // con `reject: false`: el catch no podia saltar nunca, el fallback era codigo
10
+ // muerto y la linea de asignacion se ejecutaba SIEMPRE, incluso con git
11
+ // saliendo por 128. Resultado: "Rama: HEAD / Directorio limpio" fuera de
12
+ // cualquier repositorio. Ahora el fallo se ve porque ejecutarGit lo devuelve.
13
+ const estado = await ejecutarGit(["status", "--short"], cwd);
14
+ let gitInfo;
15
+ if (!estado.ok) {
16
+ gitInfo = `No disponible: ${estado.error}.`;
17
+ }
18
+ else {
19
+ const rama = await ejecutarGit(["branch", "--show-current"], cwd);
20
+ const nombreRama = rama.ok ? rama.stdout.trim() || "HEAD (sin rama)" : `desconocida (${rama.error})`;
21
+ gitInfo = `Rama: ${nombreRama}\nCambios:\n${estado.stdout.trim() || "Directorio limpio (sin cambios pendientes)."}`;
13
22
  }
14
- catch { }
15
23
  return [
16
24
  `--- INFORMACIÓN DEL SISTEMA ---`,
17
25
  `Plataforma: ${platform}`,
@@ -1,3 +1,27 @@
1
+ /** O la salida de git, o el motivo por el que no hay salida. Nunca las dos cosas. */
2
+ export type SalidaGit = {
3
+ ok: true;
4
+ stdout: string;
5
+ } | {
6
+ ok: false;
7
+ error: string;
8
+ };
9
+ /**
10
+ * Ejecuta git distinguiendo "no hay nada que contar" de "git ha fallado".
11
+ *
12
+ * Por que existe: las llamadas de este archivo (y las de getSystemInfo) usaban
13
+ * `{ reject: false }` y despues miraban SOLO `stdout`. Con reject:false execa no
14
+ * lanza nada: devuelve un resultado con `exitCode: 128` y `stdout: ""`. Asi que
15
+ * fuera de un repositorio -o con dubious ownership, o con el index.lock ocupado,
16
+ * o sin git en el PATH- el `|| "Repositorio limpio"` se disparaba y la
17
+ * herramienta afirmaba que el arbol estaba limpio y sin commits. El try/catch
18
+ * que parecia cubrir eso era codigo muerto por el mismo motivo: reject:false
19
+ * significa que no hay excepcion que atrapar.
20
+ *
21
+ * Mentir "limpio" es peor que no contestar: el agente decide sobre esa frase si
22
+ * puede seguir escribiendo sin miedo.
23
+ */
24
+ export declare function ejecutarGit(args: string[], cwd: string): Promise<SalidaGit>;
1
25
  export declare function gitStatus(cwd?: string): Promise<string>;
2
26
  export declare function gitDiff(staged?: boolean, cwd?: string): Promise<string>;
3
27
  export declare function gitCommit(message: string, addAll?: boolean, cwd?: string): Promise<string>;
@@ -1,43 +1,104 @@
1
1
  import { execa } from "execa";
2
- export async function gitStatus(cwd = process.cwd()) {
3
- try {
4
- const { stdout: status } = await execa("git", ["status", "--short"], { cwd, reject: false });
5
- const { stdout: branch } = await execa("git", ["branch", "--show-current"], { cwd, reject: false });
6
- const { stdout: lastLog } = await execa("git", ["log", "-n", "3", "--oneline"], { cwd, reject: false });
7
- return [
8
- `--- ESTADO DE GIT ---`,
9
- `Rama actual: ${branch.trim() || "HEAD (sin rama)"}`,
10
- `Archivos modificados/nuevos:\n${status.trim() || "Repositorio limpio (sin cambios pendientes)."}\n`,
11
- `Últimos commits:\n${lastLog.trim() || "Sin historial de commits."}`,
12
- ].join("\n");
2
+ /**
3
+ * Traduce un fallo de git a una frase que el agente pueda creerse.
4
+ *
5
+ * No intenta clasificarlo todo: los tres casos con nombre son los que el
6
+ * usuario se encuentra de verdad (fuera de repo, repo ajeno, git ausente) y el
7
+ * resto cae al primer renglon de stderr, que ya es informacion util.
8
+ */
9
+ function explicarFalloGit(stderrCrudo, respaldo) {
10
+ const stderr = typeof stderrCrudo === "string" ? stderrCrudo.trim() : "";
11
+ const texto = stderr || (typeof respaldo === "string" ? respaldo : "");
12
+ if (/not a git repository|no es un repositorio/i.test(texto)) {
13
+ return "este directorio no es un repositorio git";
13
14
  }
14
- catch (err) {
15
- return `Error consultando estado de Git: ${err?.message || String(err)}`;
15
+ if (/dubious ownership/i.test(texto)) {
16
+ return "git se niega a leer el repositorio por 'dubious ownership' (pertenece a otro usuario)";
16
17
  }
18
+ if (/ENOENT|not recognized|no se reconoce|command not found/i.test(texto)) {
19
+ return "git no esta instalado o no esta en el PATH";
20
+ }
21
+ const primeraLinea = texto.split("\n").find((l) => l.trim())?.trim();
22
+ return primeraLinea || "git fallo sin dar ningun mensaje";
17
23
  }
18
- export async function gitDiff(staged = false, cwd = process.cwd()) {
24
+ /**
25
+ * Ejecuta git distinguiendo "no hay nada que contar" de "git ha fallado".
26
+ *
27
+ * Por que existe: las llamadas de este archivo (y las de getSystemInfo) usaban
28
+ * `{ reject: false }` y despues miraban SOLO `stdout`. Con reject:false execa no
29
+ * lanza nada: devuelve un resultado con `exitCode: 128` y `stdout: ""`. Asi que
30
+ * fuera de un repositorio -o con dubious ownership, o con el index.lock ocupado,
31
+ * o sin git en el PATH- el `|| "Repositorio limpio"` se disparaba y la
32
+ * herramienta afirmaba que el arbol estaba limpio y sin commits. El try/catch
33
+ * que parecia cubrir eso era codigo muerto por el mismo motivo: reject:false
34
+ * significa que no hay excepcion que atrapar.
35
+ *
36
+ * Mentir "limpio" es peor que no contestar: el agente decide sobre esa frase si
37
+ * puede seguir escribiendo sin miedo.
38
+ */
39
+ export async function ejecutarGit(args, cwd) {
19
40
  try {
20
- const args = ["diff"];
21
- if (staged)
22
- args.push("--staged");
23
- const { stdout } = await execa("git", args, { cwd, reject: false });
24
- if (!stdout.trim()) {
25
- return staged ? "No hay cambios preparados (staged)." : "No hay cambios sin preparar (unstaged).";
41
+ const res = await execa("git", args, { cwd, reject: false });
42
+ // `failed` cubre tambien el caso en que el binario ni siquiera arranca, donde
43
+ // exitCode puede llegar como undefined.
44
+ if (res.failed || res.exitCode !== 0) {
45
+ return { ok: false, error: explicarFalloGit(res.stderr, res?.shortMessage) };
26
46
  }
27
- return stdout.slice(0, 12_000);
47
+ return { ok: true, stdout: typeof res.stdout === "string" ? res.stdout : "" };
28
48
  }
29
49
  catch (err) {
30
- return `Error obteniendo git diff: ${err?.message || String(err)}`;
50
+ // Solo alcanzable si execa revienta antes de lanzar el proceso (opciones
51
+ // invalidas, cwd inexistente). Raro, pero no puede acabar en "limpio".
52
+ return { ok: false, error: explicarFalloGit(err?.stderr, err?.shortMessage || err?.message || String(err)) };
53
+ }
54
+ }
55
+ export async function gitStatus(cwd = process.cwd()) {
56
+ const status = await ejecutarGit(["status", "--short"], cwd);
57
+ if (!status.ok) {
58
+ return `--- ESTADO DE GIT ---\nNo se pudo leer el estado: ${status.error}.`;
59
+ }
60
+ const branch = await ejecutarGit(["branch", "--show-current"], cwd);
61
+ const log = await ejecutarGit(["log", "-n", "3", "--oneline"], cwd);
62
+ // `git log` sale con 128 en un repo recien creado: ahi "sin historial" si es
63
+ // la verdad, no un fallo que ocultar.
64
+ const historial = log.ok
65
+ ? log.stdout.trim() || "Sin historial de commits."
66
+ : /does not have any commits|bad default revision|unknown revision/i.test(log.error)
67
+ ? "Sin historial de commits."
68
+ : `No se pudo leer el historial: ${log.error}.`;
69
+ return [
70
+ `--- ESTADO DE GIT ---`,
71
+ `Rama actual: ${branch.ok ? branch.stdout.trim() || "HEAD (sin rama)" : `desconocida (${branch.error})`}`,
72
+ `Archivos modificados/nuevos:\n${status.stdout.trim() || "Repositorio limpio (sin cambios pendientes)."}\n`,
73
+ `Últimos commits:\n${historial}`,
74
+ ].join("\n");
75
+ }
76
+ export async function gitDiff(staged = false, cwd = process.cwd()) {
77
+ const args = ["diff"];
78
+ if (staged)
79
+ args.push("--staged");
80
+ const res = await ejecutarGit(args, cwd);
81
+ if (!res.ok) {
82
+ return `No se pudo obtener el diff: ${res.error}.`;
83
+ }
84
+ if (!res.stdout.trim()) {
85
+ return staged ? "No hay cambios preparados (staged)." : "No hay cambios sin preparar (unstaged).";
31
86
  }
87
+ return res.stdout.slice(0, 12_000);
32
88
  }
33
89
  export async function gitCommit(message, addAll = true, cwd = process.cwd()) {
34
90
  try {
35
91
  if (addAll) {
36
- await execa("git", ["add", "-A"], { cwd, reject: false });
92
+ // Si el `add` falla (index.lock, permisos) y aun asi hubiera algo ya
93
+ // preparado, el commit saldria bien y el usuario creeria que se ha
94
+ // guardado todo. Se corta aqui.
95
+ const add = await ejecutarGit(["add", "-A"], cwd);
96
+ if (!add.ok)
97
+ return `Fallo al preparar los cambios (git add -A): ${add.error}.`;
37
98
  }
38
- const { stdout, stderr, exitCode } = await execa("git", ["commit", "-m", message], { cwd, reject: false });
39
- if (exitCode !== 0) {
40
- return `Fallo al hacer commit:\n${stderr || stdout}`;
99
+ const { stdout, stderr, exitCode, failed } = await execa("git", ["commit", "-m", message], { cwd, reject: false });
100
+ if (failed || exitCode !== 0) {
101
+ return `Fallo al hacer commit:\n${stderr || stdout || `git salio con codigo ${exitCode}`}`;
41
102
  }
42
103
  return `✔ Commit creado exitosamente: "${message}"\n${stdout}`;
43
104
  }
@@ -1 +1,18 @@
1
- export declare function moveCopyFile(sourcePath: string, destinationPath: string, operation?: "move" | "copy", cwd?: string): Promise<string>;
1
+ /**
2
+ * Mover o copiar sobre una ruta que ya existe borra lo que hubiera alli, y esto
3
+ * lo hacia en silencio: renameSync reemplaza el destino tanto en Windows como en
4
+ * POSIX, y cpSync trae force:true de serie. "Mueve nuevo.txt a importante.txt"
5
+ * contestaba el mismo "✔ Movido/Renombrado ... exitosamente" tanto si el destino
6
+ * estaba vacio como si acababa de tragarse tres dias de trabajo. Y move_copy_file
7
+ * no abria dialogo en modo default, cuando delete_file si lo abre y el efecto
8
+ * sobre ese archivo es identico: desaparece.
9
+ *
10
+ * Por eso pisar el destino pasa a ser explicito. Sin `overwrite` la llamada se
11
+ * corta antes de tocar el disco y el modelo recibe por que; con `overwrite` la
12
+ * tabla de permisos lo trata como un borrado y pregunta (permissions.ts), y el
13
+ * mensaje de vuelta ya no se confunde con el de un destino vacio.
14
+ *
15
+ * El guardia vive aqui, en la herramienta, y no solo en la tabla de permisos:
16
+ * asi tambien lo tienen los ejecutores que no pasan por el bucle principal.
17
+ */
18
+ export declare function moveCopyFile(sourcePath: string, destinationPath: string, operation?: "move" | "copy", cwd?: string, overwrite?: boolean): Promise<string>;
@@ -1,21 +1,91 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- export async function moveCopyFile(sourcePath, destinationPath, operation = "move", cwd = process.cwd()) {
3
+ /**
4
+ * Mover o copiar sobre una ruta que ya existe borra lo que hubiera alli, y esto
5
+ * lo hacia en silencio: renameSync reemplaza el destino tanto en Windows como en
6
+ * POSIX, y cpSync trae force:true de serie. "Mueve nuevo.txt a importante.txt"
7
+ * contestaba el mismo "✔ Movido/Renombrado ... exitosamente" tanto si el destino
8
+ * estaba vacio como si acababa de tragarse tres dias de trabajo. Y move_copy_file
9
+ * no abria dialogo en modo default, cuando delete_file si lo abre y el efecto
10
+ * sobre ese archivo es identico: desaparece.
11
+ *
12
+ * Por eso pisar el destino pasa a ser explicito. Sin `overwrite` la llamada se
13
+ * corta antes de tocar el disco y el modelo recibe por que; con `overwrite` la
14
+ * tabla de permisos lo trata como un borrado y pregunta (permissions.ts), y el
15
+ * mensaje de vuelta ya no se confunde con el de un destino vacio.
16
+ *
17
+ * El guardia vive aqui, en la herramienta, y no solo en la tabla de permisos:
18
+ * asi tambien lo tienen los ejecutores que no pasan por el bucle principal.
19
+ */
20
+ export async function moveCopyFile(sourcePath, destinationPath, operation = "move", cwd = process.cwd(), overwrite = false) {
4
21
  const resolvedSource = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(cwd, sourcePath);
5
22
  const resolvedDest = path.isAbsolute(destinationPath) ? destinationPath : path.resolve(cwd, destinationPath);
6
23
  if (!fs.existsSync(resolvedSource)) {
7
24
  return `Error: El origen "${sourcePath}" no existe.`;
8
25
  }
26
+ // Un destino que es el propio origen no es nada que pisar: renombrar
27
+ // "notas.txt" a "Notas.txt" es legitimo y existsSync dice que si en cualquier
28
+ // disco que no distinga mayusculas.
29
+ const esElMismo = mismaEntradaEnDisco(resolvedSource, resolvedDest);
30
+ const destinoExiste = !esElMismo && fs.existsSync(resolvedDest);
31
+ const destinoEsDirectorio = destinoExiste && esDirectorio(resolvedDest);
32
+ if (destinoExiste && !overwrite) {
33
+ const que = destinoEsDirectorio ? "un directorio" : "un archivo";
34
+ const verbo = operation === "copy" ? "Copiar" : "Mover";
35
+ return (`Error: El destino "${destinationPath}" ya existe (${que}). ` +
36
+ `${verbo} el origen encima reemplazaria su contenido y no se hace sin permiso. ` +
37
+ `Elige otra ruta, o repite la llamada con overwrite=true si de verdad quieres ` +
38
+ `reemplazarlo (el usuario tendra que confirmarlo).`);
39
+ }
9
40
  const destDir = path.dirname(resolvedDest);
10
41
  if (!fs.existsSync(destDir)) {
11
42
  fs.mkdirSync(destDir, { recursive: true });
12
43
  }
13
44
  if (operation === "copy") {
14
45
  fs.cpSync(resolvedSource, resolvedDest, { recursive: true });
15
- return `✔ Copiado "${sourcePath}" -> "${destinationPath}" exitosamente.`;
46
+ if (!destinoExiste) {
47
+ return `✔ Copiado "${sourcePath}" -> "${destinationPath}" exitosamente.`;
48
+ }
49
+ // Directorio contra directorio, cpSync fusiona en vez de sustituir, y el
50
+ // snapshot de /undo de una carpeta vale null (readFileSync de un directorio
51
+ // da EISDIR), asi que esa perdida no se recupera. Decirlo, que el usuario
52
+ // acaba de aprobar el dialogo contando con la red.
53
+ if (destinoEsDirectorio) {
54
+ return (`⚠ Copiado "${sourcePath}" -> "${destinationPath}": el destino ya existia y los archivos ` +
55
+ `que coincidian se han reemplazado. /undo no restaura directorios.`);
56
+ }
57
+ return `⚠ Copiado "${sourcePath}" -> "${destinationPath}": el destino ya existia y su contenido anterior se ha reemplazado (/undo lo recupera).`;
16
58
  }
17
- else {
18
- fs.renameSync(resolvedSource, resolvedDest);
59
+ fs.renameSync(resolvedSource, resolvedDest);
60
+ if (!destinoExiste) {
19
61
  return `✔ Movido/Renombrado "${sourcePath}" -> "${destinationPath}" exitosamente.`;
20
62
  }
63
+ return `⚠ Movido/Renombrado "${sourcePath}" -> "${destinationPath}": el destino ya existia y su contenido anterior se ha reemplazado (/undo lo recupera).`;
64
+ }
65
+ /**
66
+ * Si las dos rutas son la misma entrada del disco. Se le pregunta al sistema de
67
+ * ficheros en vez de deducirlo de la plataforma: hay volumenes de macOS que si
68
+ * distinguen mayusculas, y ahi "Notas.txt" seria otro archivo al que si hay que
69
+ * proteger.
70
+ */
71
+ function mismaEntradaEnDisco(a, b) {
72
+ if (a === b)
73
+ return true;
74
+ try {
75
+ // .native pregunta al sistema operativo, que devuelve la caja real con la
76
+ // que esta escrita la entrada; realpathSync a secas respeta la que le pasen.
77
+ return fs.realpathSync.native(a) === fs.realpathSync.native(b);
78
+ }
79
+ catch {
80
+ // Si el destino no existe, realpathSync lanza: no son la misma entrada.
81
+ return false;
82
+ }
83
+ }
84
+ function esDirectorio(ruta) {
85
+ try {
86
+ return fs.statSync(ruta).isDirectory();
87
+ }
88
+ catch {
89
+ return false;
90
+ }
21
91
  }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Cascada de estrategias para localizar el fragmento que el modelo quiere
3
+ * reemplazar.
4
+ *
5
+ * El problema real no es que el modelo invente el codigo: es que copia bien las
6
+ * lineas y se come la sangria, o se deja una linea en blanco delante, o resume
7
+ * el interior de un bloque largo. Con una unica busqueda exacta, todos esos
8
+ * casos son un fallo y una iteracion perdida; Aider y opencode los resuelven
9
+ * probando estrategias cada vez mas flexibles y parando en la primera que casa.
10
+ *
11
+ * Reglas que se mantienen en TODAS las estrategias, porque son lo que separa
12
+ * "tolerante" de "escribe donde le parece":
13
+ *
14
+ * - Si una estrategia encuentra mas de un sitio posible, no elige: falla. La
15
+ * ambiguedad se resuelve pidiendo mas contexto, no adivinando.
16
+ * - Una coincidencia desproporcionada (mucho mas grande que lo buscado) se
17
+ * rechaza aunque sea unica. Es el fallo mas caro: un anclaje laxo que se
18
+ * come medio archivo y responde "editado exitosamente".
19
+ */
20
+ export interface Coincidencia {
21
+ /** Indice de caracter donde empieza el tramo, en el texto normalizado a LF. */
22
+ inicio: number;
23
+ /** Indice de caracter donde acaba (exclusivo). */
24
+ fin: number;
25
+ /** Texto de reemplazo ya adaptado (p. ej. re-sangrado) al sitio encontrado. */
26
+ textoNuevo: string;
27
+ /** Nombre de la estrategia que acerto. Se informa para poder medirlo. */
28
+ estrategia: string;
29
+ }
30
+ /** Hay candidatos, pero mas de uno: no se puede elegir sin adivinar. */
31
+ export interface Rechazo {
32
+ ambiguo: true;
33
+ estrategia: string;
34
+ ocurrencias: number;
35
+ }
36
+ export type ResultadoCascada = Coincidencia | Rechazo | null;
37
+ export declare function esRechazo(r: ResultadoCascada): r is Rechazo;
38
+ export declare function normalizarPuntuacion(texto: string): string;
39
+ /** true si el texto tiene algo que normalizar; evita recorrer el archivo en balde. */
40
+ export declare function tienePuntuacionRara(texto: string): boolean;
41
+ /**
42
+ * Rechaza un tramo que abarque mucho mas de lo buscado.
43
+ *
44
+ * Los umbrales son los de opencode: mas del doble de lineas (o tres de mas), o
45
+ * cuatro veces los caracteres (o 500 de mas).
46
+ */
47
+ export declare function esDesproporcionada(buscado: string, encontrado: string): boolean;
48
+ /**
49
+ * Orden de la cascada, de mas estricta a mas flexible. Se para en la primera
50
+ * que devuelve algo: una estrategia mas laxa no puede mejorar lo que ya acerto
51
+ * una mas estricta.
52
+ *
53
+ * Un rechazo por ambiguedad NO deja pasar a la siguiente estrategia. Si el
54
+ * fragmento aparece dos veces exactas, encontrar un tercer sitio "parecido" no
55
+ * arregla nada: lo que hay que hacer es pedir mas contexto.
56
+ */
57
+ export declare function localizarFragmento(contenidoNormalizado: string, viejoNormalizado: string, nuevoNormalizado: string): ResultadoCascada;