chocolatito-code 1.0.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.
- package/.mcp.json.ejemplo +19 -0
- package/COMPUTER-USE.md +484 -0
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/agent/context.d.ts +9 -0
- package/dist/agent/context.js +123 -0
- package/dist/agent/fileTracker.d.ts +16 -0
- package/dist/agent/fileTracker.js +53 -0
- package/dist/agent/loop.d.ts +25 -0
- package/dist/agent/loop.js +524 -0
- package/dist/agent/mentions.d.ts +11 -0
- package/dist/agent/mentions.js +43 -0
- package/dist/agent/retry.d.ts +17 -0
- package/dist/agent/retry.js +57 -0
- package/dist/agent/subagent.d.ts +2 -0
- package/dist/agent/subagent.js +116 -0
- package/dist/agent/syntaxValidator.d.ts +5 -0
- package/dist/agent/syntaxValidator.js +71 -0
- package/dist/agent/tracker.d.ts +19 -0
- package/dist/agent/tracker.js +38 -0
- package/dist/agent/undoManager.d.ts +37 -0
- package/dist/agent/undoManager.js +95 -0
- package/dist/config/constants.d.ts +77 -0
- package/dist/config/constants.js +146 -0
- package/dist/config/engine.d.ts +49 -0
- package/dist/config/engine.js +86 -0
- package/dist/config/env.d.ts +13 -0
- package/dist/config/env.js +103 -0
- package/dist/config/license.d.ts +39 -0
- package/dist/config/license.js +306 -0
- package/dist/config/permissions.d.ts +38 -0
- package/dist/config/permissions.js +239 -0
- package/dist/config/quota.d.ts +58 -0
- package/dist/config/quota.js +156 -0
- package/dist/hooks/manager.d.ts +60 -0
- package/dist/hooks/manager.js +193 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +470 -0
- package/dist/mcp/client.d.ts +55 -0
- package/dist/mcp/client.js +282 -0
- package/dist/mcp/manager.d.ts +26 -0
- package/dist/mcp/manager.js +167 -0
- package/dist/memory/manager.d.ts +9 -0
- package/dist/memory/manager.js +59 -0
- package/dist/prompts/systemPrompt.d.ts +5 -0
- package/dist/prompts/systemPrompt.js +177 -0
- package/dist/sessions/manager.d.ts +21 -0
- package/dist/sessions/manager.js +54 -0
- package/dist/skills/manager.d.ts +14 -0
- package/dist/skills/manager.js +86 -0
- package/dist/tools/browserCdp.d.ts +26 -0
- package/dist/tools/browserCdp.js +610 -0
- package/dist/tools/browserExtension.d.ts +58 -0
- package/dist/tools/browserExtension.js +479 -0
- package/dist/tools/browserLive.d.ts +12 -0
- package/dist/tools/browserLive.js +265 -0
- package/dist/tools/changeDir.d.ts +5 -0
- package/dist/tools/changeDir.js +63 -0
- package/dist/tools/computerUse.d.ts +58 -0
- package/dist/tools/computerUse.js +567 -0
- package/dist/tools/createImage.d.ts +1 -0
- package/dist/tools/createImage.js +34 -0
- package/dist/tools/definitions.d.ts +8 -0
- package/dist/tools/definitions.js +401 -0
- package/dist/tools/deleteFile.d.ts +1 -0
- package/dist/tools/deleteFile.js +17 -0
- package/dist/tools/editFile.d.ts +5 -0
- package/dist/tools/editFile.js +88 -0
- package/dist/tools/findFiles.d.ts +1 -0
- package/dist/tools/findFiles.js +33 -0
- package/dist/tools/getSystemInfo.d.ts +1 -0
- package/dist/tools/getSystemInfo.js +23 -0
- package/dist/tools/gitTools.d.ts +3 -0
- package/dist/tools/gitTools.js +47 -0
- package/dist/tools/grepSearch.d.ts +1 -0
- package/dist/tools/grepSearch.js +37 -0
- package/dist/tools/listDir.d.ts +1 -0
- package/dist/tools/listDir.js +33 -0
- package/dist/tools/moveCopyFile.d.ts +1 -0
- package/dist/tools/moveCopyFile.js +21 -0
- package/dist/tools/runCommand.d.ts +1 -0
- package/dist/tools/runCommand.js +61 -0
- package/dist/tools/runner.d.ts +5 -0
- package/dist/tools/runner.js +177 -0
- package/dist/tools/safety.d.ts +17 -0
- package/dist/tools/safety.js +33 -0
- package/dist/tools/todoTool.d.ts +8 -0
- package/dist/tools/todoTool.js +27 -0
- package/dist/tools/toolDefsComputer.d.ts +13 -0
- package/dist/tools/toolDefsComputer.js +205 -0
- package/dist/tools/truncator.d.ts +11 -0
- package/dist/tools/truncator.js +24 -0
- package/dist/tools/viewFile.d.ts +1 -0
- package/dist/tools/viewFile.js +45 -0
- package/dist/tools/visionBridge.d.ts +37 -0
- package/dist/tools/visionBridge.js +107 -0
- package/dist/tools/webFetch.d.ts +1 -0
- package/dist/tools/webFetch.js +25 -0
- package/dist/tools/webSearch.d.ts +1 -0
- package/dist/tools/webSearch.js +35 -0
- package/dist/tools/win/hostClient.d.ts +31 -0
- package/dist/tools/win/hostClient.js +158 -0
- package/dist/tools/win/hostScript.d.ts +1 -0
- package/dist/tools/win/hostScript.js +925 -0
- package/dist/tools/writeFile.d.ts +1 -0
- package/dist/tools/writeFile.js +13 -0
- package/dist/ui/banner.d.ts +10 -0
- package/dist/ui/banner.js +165 -0
- package/dist/ui/diffViewer.d.ts +1 -0
- package/dist/ui/diffViewer.js +60 -0
- package/dist/ui/interrupt.d.ts +17 -0
- package/dist/ui/interrupt.js +156 -0
- package/dist/ui/markdownStream.d.ts +23 -0
- package/dist/ui/markdownStream.js +0 -0
- package/dist/ui/modes.d.ts +28 -0
- package/dist/ui/modes.js +57 -0
- package/dist/ui/permissionPrompt.d.ts +35 -0
- package/dist/ui/permissionPrompt.js +271 -0
- package/dist/ui/prompt.d.ts +7 -0
- package/dist/ui/prompt.js +466 -0
- package/dist/ui/reasoningStream.d.ts +6 -0
- package/dist/ui/reasoningStream.js +38 -0
- package/dist/ui/renderer.d.ts +10 -0
- package/dist/ui/renderer.js +142 -0
- package/dist/ui/spinner.d.ts +24 -0
- package/dist/ui/spinner.js +111 -0
- package/dist/ui/theme.d.ts +36 -0
- package/dist/ui/theme.js +37 -0
- package/dist/ui/typeAhead.d.ts +31 -0
- package/dist/ui/typeAhead.js +87 -0
- package/extension/README.md +74 -0
- package/extension/background.js +575 -0
- package/extension/content.js +446 -0
- package/extension/manifest.json +36 -0
- package/extension/popup.html +22 -0
- package/extension/popup.js +12 -0
- package/package.json +66 -0
- package/skills/complex-refactor/SKILL.md +13 -0
- package/skills/deep-debugger/SKILL.md +14 -0
- package/skills/software-architect/SKILL.md +20 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { cycleMode, describeMode, getMode, modeAnnouncement } from "./modes.js";
|
|
5
|
+
/**
|
|
6
|
+
* Aprobacion automatica: se activa con --yes / -y o con la variable de entorno
|
|
7
|
+
* CHOCOLATITO_AUTO_APPROVE. Pensada para modo no interactivo y CI.
|
|
8
|
+
*/
|
|
9
|
+
let autoApprove = process.env.CHOCOLATITO_AUTO_APPROVE === "1";
|
|
10
|
+
export function setAutoApprove(value) {
|
|
11
|
+
autoApprove = value;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* DIALOGO DE PERMISO
|
|
15
|
+
*
|
|
16
|
+
* La version anterior tenia dos fallos visibles en cuanto se usaba:
|
|
17
|
+
*
|
|
18
|
+
* 1. Al mover la seleccion subia el cursor 12 lineas fijas, pero la caja mide
|
|
19
|
+
* entre 10 y 14 segun cuantos detalles tenga. Cada flecha dejaba media caja
|
|
20
|
+
* huerfana en pantalla, y por eso salian marcos vacios apilados.
|
|
21
|
+
*
|
|
22
|
+
* 2. El titulo llevaba un simbolo de anchura ambigua. El terminal lo pinta
|
|
23
|
+
* ocupando dos columnas y el calculo del ancho contaba una, asi que el borde
|
|
24
|
+
* derecho quedaba descuadrado justo en esa linea.
|
|
25
|
+
*
|
|
26
|
+
* Aqui se dibuja la caja a mano, con anchura conocida, y se repinta subiendo
|
|
27
|
+
* exactamente las lineas que se escribieron.
|
|
28
|
+
*/
|
|
29
|
+
export async function askToolPermission(toolName, args, cwd = process.cwd()) {
|
|
30
|
+
if (autoApprove) {
|
|
31
|
+
return { approved: true };
|
|
32
|
+
}
|
|
33
|
+
// Sin terminal interactiva no hay nadie que pueda contestar. Antes el agente
|
|
34
|
+
// se quedaba colgado para siempre esperando una tecla que nunca llegaba, y en
|
|
35
|
+
// modo `chocolatito "tarea"` o en CI eso es un bloqueo silencioso. Ante la
|
|
36
|
+
// duda se deniega, que es lo seguro, y se explica como autorizar.
|
|
37
|
+
if (!process.stdin.isTTY) {
|
|
38
|
+
return {
|
|
39
|
+
approved: false,
|
|
40
|
+
userFeedback: `No hay terminal interactiva para pedir permiso de "${toolName}". ` +
|
|
41
|
+
`Vuelve a lanzarlo con --yes (o CHOCOLATITO_AUTO_APPROVE=1) si quieres aprobar automaticamente, ` +
|
|
42
|
+
`o ejecuta el comando tu mismo.`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
let selectedIndex = 0;
|
|
47
|
+
const options = PERMISSION_OPTIONS;
|
|
48
|
+
const kind = classify(toolName, args);
|
|
49
|
+
const width = boxWidth();
|
|
50
|
+
const wasRaw = process.stdin.isRaw;
|
|
51
|
+
if (process.stdin.setRawMode)
|
|
52
|
+
process.stdin.setRawMode(true);
|
|
53
|
+
process.stdin.resume();
|
|
54
|
+
/** Lineas que ocupa el ultimo dibujo, para poder repintarlo sin dejar restos. */
|
|
55
|
+
let renderedRows = 0;
|
|
56
|
+
function draw() {
|
|
57
|
+
if (renderedRows > 0) {
|
|
58
|
+
// Se sube justo lo que se escribio: ni una linea mas, ni una menos.
|
|
59
|
+
process.stdout.write(`\x1b[${renderedRows}A`);
|
|
60
|
+
}
|
|
61
|
+
const lines = buildPermissionBox(toolName, args, cwd, selectedIndex, width);
|
|
62
|
+
// Cada linea se limpia antes de escribirse: si el dibujo anterior era mas
|
|
63
|
+
// ancho, no queda cola a la derecha.
|
|
64
|
+
process.stdout.write(lines.map((l) => `\r\x1b[2K${l}`).join("\n") + "\n");
|
|
65
|
+
renderedRows = lines.length;
|
|
66
|
+
}
|
|
67
|
+
draw();
|
|
68
|
+
function cleanup() {
|
|
69
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
70
|
+
if (process.stdin.setRawMode)
|
|
71
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
72
|
+
}
|
|
73
|
+
async function promptForFeedback() {
|
|
74
|
+
cleanup();
|
|
75
|
+
return new Promise((resFeedback) => {
|
|
76
|
+
const rl = readline.createInterface({
|
|
77
|
+
input: process.stdin,
|
|
78
|
+
output: process.stdout,
|
|
79
|
+
});
|
|
80
|
+
rl.question(chalk.hex("#A855F7")("\n¿Qué prefieres que haga? ❯ "), (answer) => {
|
|
81
|
+
rl.close();
|
|
82
|
+
resFeedback(answer.trim() || "El usuario canceló la acción sin dar detalles adicionales.");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async function onKeypress(str, key) {
|
|
87
|
+
// Shift+Tab desde el propio dialogo: cambia de modo instantáneamente
|
|
88
|
+
const isShiftTab = Boolean(key && key.name === "tab" && key.shift) ||
|
|
89
|
+
str === "\x1b[Z" ||
|
|
90
|
+
Boolean(key && key.sequence === "\x1b[Z");
|
|
91
|
+
if (isShiftTab) {
|
|
92
|
+
const nuevo = cycleMode();
|
|
93
|
+
if (nuevo === "bypass") {
|
|
94
|
+
cleanup();
|
|
95
|
+
process.stdout.write(`\r\x1b[2K${modeAnnouncement(nuevo)}\n`);
|
|
96
|
+
resolve({ approved: true });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
process.stdout.write(`\r\x1b[2K${modeAnnouncement(nuevo)}\n`);
|
|
100
|
+
renderedRows = 0;
|
|
101
|
+
draw();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (key && (key.name === "return" || key.name === "enter")) {
|
|
105
|
+
if (selectedIndex === 0) {
|
|
106
|
+
cleanup();
|
|
107
|
+
resolve({ approved: true });
|
|
108
|
+
}
|
|
109
|
+
else if (selectedIndex === 1) {
|
|
110
|
+
cleanup();
|
|
111
|
+
resolve({ approved: true, rememberPattern: true });
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const feedback = await promptForFeedback();
|
|
115
|
+
resolve({ approved: false, userFeedback: feedback });
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (key && key.name === "escape") {
|
|
120
|
+
cleanup();
|
|
121
|
+
resolve({ approved: false, userFeedback: "Cancelado por el usuario con la tecla Esc." });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (str === "1") {
|
|
125
|
+
cleanup();
|
|
126
|
+
resolve({ approved: true });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (str === "2") {
|
|
130
|
+
cleanup();
|
|
131
|
+
resolve({ approved: true, rememberPattern: true });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (str === "3") {
|
|
135
|
+
const feedback = await promptForFeedback();
|
|
136
|
+
resolve({ approved: false, userFeedback: feedback });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (key && (key.name === "up" || key.name === "down")) {
|
|
140
|
+
const delta = key.name === "up" ? -1 : 1;
|
|
141
|
+
selectedIndex = (selectedIndex + delta + options.length) % options.length;
|
|
142
|
+
draw();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (key && key.ctrl && key.name === "c") {
|
|
146
|
+
cleanup();
|
|
147
|
+
process.stdout.write("\n");
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
process.stdin.on("keypress", onKeypress);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Dibujo de la caja.
|
|
156
|
+
//
|
|
157
|
+
// Se hace a mano en vez de con una libreria porque hay que saber con exactitud
|
|
158
|
+
// cuantas lineas ocupa y cuanto mide el texto sin los codigos de color. Los
|
|
159
|
+
// simbolos usados son de anchura fija: nada de emoji, que es lo que descuadraba
|
|
160
|
+
// el borde derecho.
|
|
161
|
+
const H = "─";
|
|
162
|
+
export const PERMISSION_OPTIONS = [
|
|
163
|
+
{ label: "Sí, adelante", hint: "solo esta vez" },
|
|
164
|
+
{ label: "Sí, y no preguntar más por esto", hint: "lo recuerda en este proyecto" },
|
|
165
|
+
{ label: "No, prefiero decirte otra cosa", hint: "cancela y te lo explico" },
|
|
166
|
+
];
|
|
167
|
+
/**
|
|
168
|
+
* Construye el dialogo entero como lista de lineas.
|
|
169
|
+
*
|
|
170
|
+
* Es una funcion pura a proposito: asi se puede comprobar en una prueba que
|
|
171
|
+
* todas las lineas del marco miden exactamente lo mismo. El descuadre del borde
|
|
172
|
+
* derecho no se ve leyendo el codigo, solo ejecutandolo.
|
|
173
|
+
*/
|
|
174
|
+
export function buildPermissionBox(toolName, args, cwd, selectedIndex, width = boxWidth()) {
|
|
175
|
+
const kind = classify(toolName, args);
|
|
176
|
+
const lines = [];
|
|
177
|
+
lines.push(top(width, kind.color));
|
|
178
|
+
lines.push(row(chalk.bold.hex(kind.color)(kind.title), width, kind.color, kind.title.length));
|
|
179
|
+
for (const d of details(toolName, args, cwd, width)) {
|
|
180
|
+
lines.push(row(d.text, width, kind.color, d.length));
|
|
181
|
+
}
|
|
182
|
+
PERMISSION_OPTIONS.forEach((opt, idx) => {
|
|
183
|
+
const sel = idx === selectedIndex;
|
|
184
|
+
const marker = sel ? "❯" : " ";
|
|
185
|
+
const num = `${idx + 1}.`;
|
|
186
|
+
const cabeza = ` ${marker} ${num} ${opt.label}`;
|
|
187
|
+
// La pista es lo primero que se cae si no cabe: perder el marco por una
|
|
188
|
+
// aclaración opcional es peor que quedarse sin la aclaración.
|
|
189
|
+
const conPista = `${cabeza} ${opt.hint}`;
|
|
190
|
+
const cabe = conPista.length <= width - 3;
|
|
191
|
+
const plain = cabe ? conPista : cabeza;
|
|
192
|
+
const etiqueta = sel ? chalk.bold.white(opt.label) : chalk.gray(opt.label);
|
|
193
|
+
const flecha = sel ? chalk.hex(kind.color)(marker) : marker;
|
|
194
|
+
const painted = ` ${flecha} ${chalk.gray(num)} ${etiqueta}${cabe ? ` ${chalk.gray(opt.hint)}` : ""}`;
|
|
195
|
+
lines.push(row(painted, width, kind.color, plain.length));
|
|
196
|
+
});
|
|
197
|
+
lines.push(bottom(width, kind.color));
|
|
198
|
+
const modo = describeMode(getMode());
|
|
199
|
+
lines.push(chalk.gray(" ↑↓ o 1-3 · enter confirma · esc cancela · ") +
|
|
200
|
+
chalk.hex(modo.color)("shift+tab") +
|
|
201
|
+
chalk.gray(" cambia a modo automático"));
|
|
202
|
+
return lines;
|
|
203
|
+
}
|
|
204
|
+
function boxWidth() {
|
|
205
|
+
const cols = process.stdout.columns || 80;
|
|
206
|
+
return Math.max(48, Math.min(96, cols - 4));
|
|
207
|
+
}
|
|
208
|
+
function top(width, color) {
|
|
209
|
+
return chalk.hex(color)(`╭${H.repeat(width - 2)}╮`);
|
|
210
|
+
}
|
|
211
|
+
function bottom(width, color) {
|
|
212
|
+
return chalk.hex(color)(`╰${H.repeat(width - 2)}╯`);
|
|
213
|
+
}
|
|
214
|
+
/** Una fila del marco. `plainLength` es el ancho real del texto ya pintado. */
|
|
215
|
+
function row(painted, width, color, plainLength) {
|
|
216
|
+
const inner = width - 2;
|
|
217
|
+
const relleno = Math.max(0, inner - plainLength);
|
|
218
|
+
const bar = chalk.hex(color)("│");
|
|
219
|
+
return `${bar}${painted}${" ".repeat(relleno)}${bar}`;
|
|
220
|
+
}
|
|
221
|
+
function classify(toolName, args) {
|
|
222
|
+
if (toolName === "delete_file")
|
|
223
|
+
return { title: " Borrar un archivo", color: "#EF4444" };
|
|
224
|
+
if (toolName === "run_command")
|
|
225
|
+
return { title: " Ejecutar un comando", color: "#F59E0B" };
|
|
226
|
+
if (toolName === "edit_file" || toolName === "write_file") {
|
|
227
|
+
return { title: " Escribir en un archivo", color: "#D97757" };
|
|
228
|
+
}
|
|
229
|
+
if (toolName === "move_copy_file")
|
|
230
|
+
return { title: " Mover o copiar un archivo", color: "#D97757" };
|
|
231
|
+
if (toolName === "chrome" || toolName === "browser" || toolName === "browser_live") {
|
|
232
|
+
return { title: " Controlar tu navegador", color: "#A855F7" };
|
|
233
|
+
}
|
|
234
|
+
if (toolName === "computer_use")
|
|
235
|
+
return { title: " Controlar tu ordenador", color: "#A855F7" };
|
|
236
|
+
return { title: " Permiso de ejecución", color: "#D97757" };
|
|
237
|
+
}
|
|
238
|
+
/** Las lineas de detalle, ya recortadas para que quepan dentro del marco. */
|
|
239
|
+
function details(toolName, args, cwd, width) {
|
|
240
|
+
const max = width - 8;
|
|
241
|
+
const out = [];
|
|
242
|
+
const add = (plain, painted) => {
|
|
243
|
+
const corto = plain.length > max ? `${plain.slice(0, max - 1)}…` : plain;
|
|
244
|
+
const texto = painted && plain.length <= max ? painted : chalk.white(corto);
|
|
245
|
+
out.push({ text: ` ${texto}`, length: corto.length + 2 });
|
|
246
|
+
};
|
|
247
|
+
out.push({ text: "", length: 0 });
|
|
248
|
+
if (typeof args.command === "string") {
|
|
249
|
+
add(args.command, chalk.bold.white(args.command));
|
|
250
|
+
add(`en ${cwd}`, chalk.gray(`en ${cwd}`));
|
|
251
|
+
}
|
|
252
|
+
else if (typeof args.path === "string") {
|
|
253
|
+
add(args.path, chalk.bold.white(args.path));
|
|
254
|
+
if (typeof args.oldText === "string" && typeof args.newText === "string") {
|
|
255
|
+
const mas = args.newText.split("\n").length;
|
|
256
|
+
const menos = args.oldText.split("\n").length;
|
|
257
|
+
const plain = `+${mas} líneas -${menos} líneas`;
|
|
258
|
+
add(plain, `${chalk.green(`+${mas} líneas`)} ${chalk.red(`-${menos} líneas`)}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else if (typeof args.action === "string") {
|
|
262
|
+
const extra = args.url || args.appName || args.window || args.text || "";
|
|
263
|
+
const plain = extra ? `${args.action} ${String(extra)}` : String(args.action);
|
|
264
|
+
add(plain, extra ? `${chalk.bold.white(args.action)} ${chalk.gray(String(extra))}` : chalk.bold.white(args.action));
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
add(toolName, chalk.bold.white(toolName));
|
|
268
|
+
}
|
|
269
|
+
out.push({ text: "", length: 0 });
|
|
270
|
+
return out;
|
|
271
|
+
}
|