@fargowealth/fargo-ai-cli 1.4.1 → 1.5.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/src/mcp-proxy.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createInterface } from "node:readline";
2
2
  import { CLI_VERSION, DEFAULT_MCP_URL } from "./constants.mjs";
3
3
  import { currentCredential, refreshCurrentCredential } from "./auth.mjs";
4
+ import { callLocalYidaTool, isLocalYidaTool, localYidaTools } from "./yida-tools.mjs";
4
5
 
5
6
  export async function runMcpProxy(store, options = {}) {
6
7
  const input = options.input ?? process.stdin;
@@ -17,8 +18,21 @@ export async function runMcpProxy(store, options = {}) {
17
18
  try { message = JSON.parse(line); }
18
19
  catch { return; }
19
20
  try {
21
+ if (message.method === "tools/call" && isLocalYidaTool(message.params?.name)) {
22
+ if (message.id === undefined) return;
23
+ let result;
24
+ try { result = await callLocalYidaTool(store, message.params.name, message.params.arguments, options); }
25
+ catch (error) { result = { isError: true, content: [{ type: "text", text: error instanceof Error ? error.message : "Local YiDA tool failed" }] }; }
26
+ output.write(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, result })}\n`);
27
+ return;
28
+ }
20
29
  const responses = await forward(message, false);
21
- for (const response of responses) output.write(`${JSON.stringify(response)}\n`);
30
+ for (const response of responses) {
31
+ if (message.method === "tools/list" && !message.params?.cursor && response.id === message.id && Array.isArray(response.result?.tools)) {
32
+ response.result.tools = [...response.result.tools.filter(tool => !isLocalYidaTool(tool.name)), ...localYidaTools()];
33
+ }
34
+ output.write(`${JSON.stringify(response)}\n`);
35
+ }
22
36
  } catch (error) {
23
37
  if (message.id !== undefined) output.write(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, error: { code: -32001, message: error instanceof Error ? error.message : "Fargo MCP proxy failed" } })}\n`);
24
38
  }
@@ -0,0 +1,422 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { createReadStream } from "node:fs";
4
+ import { chmod, lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
5
+ import { homedir } from "node:os";
6
+ import { basename, extname, join, resolve } from "node:path";
7
+ import { Readable } from "node:stream";
8
+ import { setTimeout as delay } from "node:timers/promises";
9
+ import { callMcpTool } from "./mcp-client.mjs";
10
+ import { mcpToolPayload } from "./mcp-result.mjs";
11
+
12
+ const MAX_UPLOAD_BYTES = 30_000_000_000 - 1;
13
+ const MAX_R2_PUT_BYTES = 5 * 1024 * 1024 * 1024;
14
+ const TUS_CHUNK_BYTES = 50 * 1024 * 1024;
15
+ const MIME_BY_EXTENSION = new Map([
16
+ [".mp4", "video/mp4"],
17
+ [".m4v", "video/mp4"],
18
+ [".webm", "video/webm"],
19
+ [".mov", "video/quicktime"],
20
+ ]);
21
+
22
+ export async function transferUpload(input, options = {}) {
23
+ if (input.resumePath) return resumeUpload(input, options);
24
+
25
+ const originalPath = resolve(input.filePath);
26
+ await regularFileInfo(originalPath);
27
+ let filePath = originalPath;
28
+ let compressedDirectory;
29
+ let checkpointPath;
30
+ let checkpointWritten = false;
31
+ try {
32
+ if (input.compress) {
33
+ const compressed = await compressVideo(originalPath, input.ffmpegBin, options);
34
+ filePath = compressed.filePath;
35
+ compressedDirectory = compressed.directory;
36
+ }
37
+
38
+ const fileInfo = await regularFileInfo(filePath);
39
+ const mimeType = MIME_BY_EXTENSION.get(extname(filePath).toLowerCase()) ?? "application/octet-stream";
40
+ const descriptor = await readUploadDescriptor(input.requestPath);
41
+ const file = {
42
+ fileName: basename(filePath),
43
+ mimeType,
44
+ sizeBytes: fileInfo.size,
45
+ };
46
+ const prepared = await prepareUpload(descriptor, file, options);
47
+ const upload = prepared.upload;
48
+ validateUploadDescriptor(upload, fileInfo.size);
49
+
50
+ let state;
51
+ if (upload.protocol === "tus" || descriptor.complete) {
52
+ const stateDirectory = await ensurePrivateUploadDirectory(options.home ?? homedir());
53
+ checkpointPath = join(stateDirectory, `${randomUUID()}.json`);
54
+ state = {
55
+ version: 1,
56
+ phase: "upload",
57
+ provider: upload.provider,
58
+ upload,
59
+ complete: descriptor.complete,
60
+ prepareResult: prepared.prepareResult,
61
+ file,
62
+ filePath,
63
+ fileSize: fileInfo.size,
64
+ fileMtimeMs: fileInfo.mtimeMs,
65
+ compressedDirectory,
66
+ createdAt: new Date().toISOString(),
67
+ };
68
+ await writeCheckpoint(checkpointPath, state, true);
69
+ checkpointWritten = true;
70
+ if (upload.protocol === "tus" || descriptor.complete) writeLine(options.stderr, `Resume file: ${checkpointPath}`);
71
+ }
72
+
73
+ await transferBytes(upload, filePath, fileInfo.size, file.mimeType, options);
74
+ if (state?.complete) {
75
+ state.phase = "complete";
76
+ await writeCheckpoint(checkpointPath, state);
77
+ await executeAction(state.complete, actionContext(state), options);
78
+ }
79
+
80
+ if (checkpointPath) await rm(checkpointPath, { force: true });
81
+ if (compressedDirectory) await rm(compressedDirectory, { recursive: true, force: true });
82
+ return {
83
+ provider: upload.provider,
84
+ uploadedBytes: fileInfo.size,
85
+ protocol: upload.protocol,
86
+ status: "complete",
87
+ };
88
+ } catch (error) {
89
+ if (compressedDirectory && !checkpointWritten) await rm(compressedDirectory, { recursive: true, force: true }).catch(() => {});
90
+ if (checkpointWritten && checkpointPath && error instanceof Error) {
91
+ error.message = `${error.message}. Resume with: fargo upload --resume "${checkpointPath}"`;
92
+ }
93
+ throw error;
94
+ }
95
+ }
96
+
97
+ async function transferBytes(upload, filePath, fileSize, mimeType, options) {
98
+ if (upload.protocol === "tus") {
99
+ await uploadTus(upload.uploadUrl, filePath, fileSize, options, upload.headers);
100
+ } else if (upload.provider === "media" && upload.protocol === "post") {
101
+ await uploadMultipart(upload.uploadUrl, filePath, basename(filePath), mimeType, fileSize, options, upload.headers);
102
+ } else if (upload.provider === "r2" && upload.protocol === "put") {
103
+ await uploadPut(upload.uploadUrl, filePath, fileSize, options, upload.headers);
104
+ } else {
105
+ throw new Error(`Unsupported upload provider/protocol: ${upload.provider}/${upload.protocol}`);
106
+ }
107
+ }
108
+
109
+ async function resumeUpload(input, options) {
110
+ const checkpointPath = resolve(input.resumePath);
111
+ const details = await lstat(checkpointPath);
112
+ if (!details.isFile() || details.isSymbolicLink()) throw new Error("Resume file must be a regular file");
113
+ const state = JSON.parse(await readFile(checkpointPath, "utf8"));
114
+ if (state?.version !== 1 || !state?.upload || !["media", "r2"].includes(state.provider)
115
+ || !["upload", "complete"].includes(state.phase) || typeof state.filePath !== "string") {
116
+ throw new Error("Resume file is invalid or does not contain a supported upload");
117
+ }
118
+ if (state.phase !== "complete") {
119
+ const filePath = resolve(input.filePath || state.filePath);
120
+ if (filePath !== state.filePath) throw new Error("Resume file belongs to a different local file path");
121
+ const fileInfo = await regularFileInfo(filePath);
122
+ if (fileInfo.size !== state.fileSize || fileInfo.mtimeMs !== state.fileMtimeMs) {
123
+ throw new Error("The local file changed after this upload started; choose the original unchanged file to resume");
124
+ }
125
+ await transferBytes(state.upload, filePath, state.fileSize, state.file.mimeType, options);
126
+ if (state.complete) {
127
+ state.phase = "complete";
128
+ await writeCheckpoint(checkpointPath, state);
129
+ }
130
+ }
131
+ if (state.complete) await executeAction(state.complete, actionContext(state), options);
132
+ await rm(checkpointPath, { force: true });
133
+ if (state.compressedDirectory) await rm(state.compressedDirectory, { recursive: true, force: true });
134
+ return {
135
+ provider: state.provider,
136
+ uploadedBytes: state.fileSize,
137
+ protocol: state.upload.protocol,
138
+ status: "complete",
139
+ };
140
+ }
141
+
142
+ async function uploadTus(uploadUrl, filePath, fileSize, options, extraHeaders = {}) {
143
+ let offset = await getTusOffset(uploadUrl, options.fetchImpl ?? fetch, extraHeaders);
144
+ reportProgress(offset, fileSize, options);
145
+ while (offset < fileSize) {
146
+ const end = Math.min(offset + TUS_CHUNK_BYTES, fileSize);
147
+ let completed = false;
148
+ let lastError;
149
+ for (let attempt = 0; attempt < 6 && !completed; attempt += 1) {
150
+ try {
151
+ const response = await (options.fetchImpl ?? fetch)(uploadUrl, {
152
+ method: "PATCH",
153
+ headers: {
154
+ ...extraHeaders,
155
+ "Tus-Resumable": "1.0.0",
156
+ "Upload-Offset": String(offset),
157
+ "Content-Type": "application/offset+octet-stream",
158
+ "Content-Length": String(end - offset),
159
+ },
160
+ body: createReadStream(filePath, { start: offset, end: end - 1 }),
161
+ duplex: "half",
162
+ });
163
+ if (response.status === 204) {
164
+ const nextOffset = Number(response.headers.get("Upload-Offset"));
165
+ if (!Number.isSafeInteger(nextOffset) || nextOffset <= offset || nextOffset > fileSize) {
166
+ throw new Error("Media TUS server returned an invalid upload offset");
167
+ }
168
+ offset = nextOffset;
169
+ completed = true;
170
+ reportProgress(offset, fileSize, options);
171
+ continue;
172
+ }
173
+ const error = new Error(`Media TUS upload failed (HTTP ${response.status})`);
174
+ if (response.status >= 400 && response.status < 500 && response.status !== 409) throw error;
175
+ lastError = error;
176
+ } catch (error) {
177
+ if (error instanceof Error && /^Media TUS upload failed \(HTTP 4/.test(error.message) && !error.message.includes("HTTP 409)")) throw error;
178
+ lastError = error;
179
+ }
180
+
181
+ if (!completed) {
182
+ if (attempt < 5) {
183
+ await delay([0, 1000, 3000, 5000, 10000, 20000][attempt]);
184
+ offset = await getTusOffset(uploadUrl, options.fetchImpl ?? fetch, extraHeaders);
185
+ if (offset >= end) {
186
+ completed = true;
187
+ reportProgress(offset, fileSize, options);
188
+ }
189
+ }
190
+ }
191
+ }
192
+ if (!completed) throw new Error(`TUS upload paused and can be resumed; ${lastError instanceof Error ? lastError.message : "network request failed"}`);
193
+ }
194
+ }
195
+
196
+ async function getTusOffset(uploadUrl, fetchImpl, extraHeaders = {}) {
197
+ const response = await fetchImpl(uploadUrl, { method: "HEAD", headers: { ...extraHeaders, "Tus-Resumable": "1.0.0" } });
198
+ if (!response.ok) throw new Error(`Unable to resume TUS upload (HTTP ${response.status})`);
199
+ const offset = Number(response.headers.get("Upload-Offset"));
200
+ const length = Number(response.headers.get("Upload-Length"));
201
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length < offset) {
202
+ throw new Error("TUS server returned invalid upload status headers");
203
+ }
204
+ return offset;
205
+ }
206
+
207
+ async function uploadMultipart(uploadUrl, filePath, fileName, mimeType, fileSize, options, extraHeaders = {}) {
208
+ const boundary = `----FargoUpload${randomUUID().replaceAll("-", "")}`;
209
+ const safeName = fileName.replace(/[\r\n"\\]/g, "_");
210
+ const prefix = `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${safeName}"\r\nContent-Type: ${mimeType}\r\n\r\n`;
211
+ const suffix = `\r\n--${boundary}--\r\n`;
212
+ const bodyLength = Buffer.byteLength(prefix) + fileSize + Buffer.byteLength(suffix);
213
+ let response;
214
+ try {
215
+ response = await (options.fetchImpl ?? fetch)(uploadUrl, {
216
+ method: "POST",
217
+ headers: { ...extraHeaders, "Content-Type": `multipart/form-data; boundary=${boundary}`, "Content-Length": String(bodyLength) },
218
+ body: Readable.from((async function* streamBody() {
219
+ yield prefix;
220
+ for await (const chunk of createReadStream(filePath)) yield chunk;
221
+ yield suffix;
222
+ })()),
223
+ duplex: "half",
224
+ });
225
+ } catch {
226
+ throw new Error("Direct upload failed and cannot be resumed; the upload must be restarted.");
227
+ }
228
+ if (!response.ok) throw new Error(`Media direct upload failed (HTTP ${response.status})`);
229
+ reportProgress(fileSize, fileSize, options);
230
+ }
231
+
232
+ async function uploadPut(uploadUrl, filePath, fileSize, options, extraHeaders = {}) {
233
+ let response;
234
+ try {
235
+ response = await (options.fetchImpl ?? fetch)(uploadUrl, {
236
+ method: "PUT",
237
+ headers: { ...extraHeaders, "Content-Length": String(fileSize) },
238
+ body: createReadStream(filePath),
239
+ duplex: "half",
240
+ });
241
+ } catch {
242
+ throw new Error("Signed R2 upload failed; this single PUT cannot be resumed and must be restarted.");
243
+ }
244
+ if (!response.ok) throw new Error(`Signed R2 upload failed (HTTP ${response.status})`);
245
+ reportProgress(fileSize, fileSize, options);
246
+ }
247
+
248
+ async function readUploadDescriptor(requestPath) {
249
+ const requestFile = resolve(requestPath);
250
+ const details = await lstat(requestFile);
251
+ if (!details.isFile() || details.isSymbolicLink()) throw new Error("Upload descriptor path must be a regular file");
252
+ let descriptor;
253
+ try {
254
+ descriptor = JSON.parse(await readFile(requestFile, "utf8"));
255
+ } catch (error) {
256
+ throw new Error(`Could not read upload descriptor JSON: ${error instanceof Error ? error.message : "invalid JSON"}`);
257
+ }
258
+ if (descriptor?.version !== 1 || !["media", "r2"].includes(descriptor.provider)) {
259
+ throw new Error('Upload descriptor must have version 1 and provider "media" or "r2"');
260
+ }
261
+ return descriptor;
262
+ }
263
+
264
+ async function prepareUpload(descriptor, file, options) {
265
+ if (!descriptor.prepare) return { upload: { ...descriptor }, prepareResult: undefined };
266
+ const action = descriptor.prepare.transport === "http"
267
+ ? { ...descriptor.prepare, body: { ...(descriptor.prepare.body ?? {}), ...file } }
268
+ : descriptor.prepare;
269
+ const prepareResult = await executeAction(action, { file }, options);
270
+ const mapped = descriptor.prepare.result
271
+ ? resolveTemplate(descriptor.prepare.result, { file, prepare: prepareResult })
272
+ : prepareResult;
273
+ if (!mapped || typeof mapped !== "object" || Array.isArray(mapped)) throw new Error("Upload preparation did not return an upload descriptor object");
274
+ return {
275
+ upload: { ...mapped, provider: descriptor.provider },
276
+ prepareResult,
277
+ };
278
+ }
279
+
280
+ async function executeAction(action, context, options) {
281
+ if (!action || typeof action !== "object") throw new Error("Upload action must be an object");
282
+ if (action.transport === "mcp") {
283
+ if (typeof action.tool !== "string" || !action.tool) throw new Error("MCP upload action requires a tool name");
284
+ if (!options.store) throw new Error("Sign in with `fargo auth login` before using an MCP upload descriptor");
285
+ const argumentsValue = resolveTemplate(action.arguments ?? {}, context);
286
+ const result = await callMcpTool(options.store, action.tool, argumentsValue, options);
287
+ return mcpToolPayload(result);
288
+ }
289
+ if (action.transport === "http") {
290
+ if (typeof action.url !== "string" || !action.url) throw new Error("HTTP upload action requires a URL");
291
+ const method = action.method ?? "POST";
292
+ if (method !== "POST") throw new Error("HTTP upload action method must be POST");
293
+ const body = resolveTemplate(action.body ?? {}, context);
294
+ const response = await (options.fetchImpl ?? fetch)(action.url, {
295
+ method,
296
+ headers: { ...(action.headers ?? {}), "Content-Type": "application/json" },
297
+ body: JSON.stringify(body),
298
+ });
299
+ if (!response.ok) throw new Error(`Upload action failed (HTTP ${response.status})`);
300
+ try {
301
+ return await response.json();
302
+ } catch {
303
+ throw new Error("Upload endpoint returned invalid JSON");
304
+ }
305
+ }
306
+ throw new Error('Upload action transport must be "mcp" or "http"');
307
+ }
308
+
309
+ function resolveTemplate(value, context) {
310
+ if (Array.isArray(value)) return value.map((item) => resolveTemplate(item, context));
311
+ if (!value || typeof value !== "object") return value;
312
+ const keys = Object.keys(value);
313
+ if (keys.length === 1 && keys[0] === "$file") return readPath(context.file, value.$file);
314
+ if (keys.length === 1 && keys[0] === "$prepare") return readPath(context.prepare, value.$prepare);
315
+ if (keys.length === 1 && keys[0] === "$path") return readPath(context.prepare, value.$path);
316
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, resolveTemplate(item, context)]));
317
+ }
318
+
319
+ function readPath(value, path) {
320
+ if (typeof path !== "string" || !path) throw new Error("Upload descriptor field mapping must be a non-empty path");
321
+ return path.split(".").reduce((current, part) => {
322
+ if (!current || typeof current !== "object" || !(part in current)) throw new Error(`Upload descriptor mapping path was not found: ${path}`);
323
+ return current[part];
324
+ }, value);
325
+ }
326
+
327
+ function actionContext(state) {
328
+ return { file: state.file, prepare: state.prepareResult };
329
+ }
330
+
331
+ async function writeCheckpoint(checkpointPath, state, create = false) {
332
+ await writeFile(checkpointPath, `${JSON.stringify(state)}\n`, {
333
+ flag: create ? "wx" : "w",
334
+ mode: 0o600,
335
+ });
336
+ if (process.platform !== "win32") await chmod(checkpointPath, 0o600);
337
+ }
338
+
339
+ function validateUploadDescriptor(upload, fileSize) {
340
+ if (typeof upload.uploadUrl !== "string" || !upload.uploadUrl) throw new Error("Upload descriptor did not return uploadUrl");
341
+ const allowed = upload.provider === "media" ? ["tus", "post"] : ["put"];
342
+ if (!allowed.includes(upload.protocol)) throw new Error(`Unsupported upload protocol for ${upload.provider}: ${upload.protocol}`);
343
+ if (upload.protocol === "tus" && upload.sizeBytes !== undefined && upload.sizeBytes !== fileSize) {
344
+ throw new Error(`Upload descriptor sizeBytes (${upload.sizeBytes}) does not match local file size (${fileSize})`);
345
+ }
346
+ if (upload.protocol === "tus") assertUploadSize(fileSize);
347
+ if (upload.protocol === "put" && fileSize > MAX_R2_PUT_BYTES) throw new Error("This CLI supports signed R2 PUT uploads up to 5 GiB; larger R2 uploads require a multipart protocol that is not supported yet");
348
+ }
349
+
350
+ async function compressVideo(inputPath, ffmpegBin, options) {
351
+ const checkpointRoot = await ensurePrivateUploadDirectory(options.home ?? homedir());
352
+ const directory = join(checkpointRoot, randomUUID());
353
+ await mkdir(directory, { recursive: true, mode: 0o700 });
354
+ const outputPath = join(directory, "compressed.mp4");
355
+ const command = ffmpegBin || process.env.FFMPEG_PATH || "ffmpeg";
356
+ await new Promise((resolveProcess, rejectProcess) => {
357
+ const child = spawn(command, [
358
+ "-hide_banner", "-nostdin", "-loglevel", "error", "-y",
359
+ "-i", inputPath,
360
+ "-map", "0:v:0", "-map", "0:a?",
361
+ "-vf", "scale='min(1920,iw)':-2",
362
+ "-c:v", "libx264", "-preset", "medium", "-crf", "23", "-pix_fmt", "yuv420p",
363
+ "-c:a", "aac", "-b:a", "128k", "-movflags", "+faststart", outputPath,
364
+ ], { stdio: ["ignore", "ignore", "pipe"] });
365
+ let stderr = "";
366
+ child.stderr.setEncoding("utf8");
367
+ child.stderr.on("data", (chunk) => { stderr = `${stderr}${chunk}`.slice(-3000); });
368
+ child.once("error", (error) => {
369
+ rejectProcess(error.code === "ENOENT"
370
+ ? new Error(`${ffmpegMissingGuide()} Set FFMPEG_PATH or pass --ffmpeg-bin after installation.`)
371
+ : error);
372
+ });
373
+ child.once("close", (code) => {
374
+ if (code === 0) resolveProcess();
375
+ else rejectProcess(new Error(`FFmpeg compression failed${stderr.trim() ? `: ${stderr.trim()}` : ` (exit ${code})`}`));
376
+ });
377
+ }).catch(async (error) => {
378
+ await rm(directory, { recursive: true, force: true });
379
+ throw error;
380
+ });
381
+ return { directory, filePath: outputPath };
382
+ }
383
+
384
+ async function regularFileInfo(filePath) {
385
+ const details = await lstat(filePath);
386
+ if (!details.isFile() || details.isSymbolicLink()) throw new Error(`Video path must be a regular file: ${filePath}`);
387
+ return { size: details.size, mtimeMs: details.mtimeMs };
388
+ }
389
+
390
+ async function ensurePrivateUploadDirectory(home) {
391
+ const directory = join(home, ".fargo", "uploads");
392
+ await mkdir(directory, { recursive: true, mode: 0o700 });
393
+ const details = await lstat(directory);
394
+ if (!details.isDirectory() || details.isSymbolicLink()) throw new Error("Fargo upload checkpoint directory is not a regular directory");
395
+ if (process.platform !== "win32") await chmod(directory, 0o700);
396
+ return directory;
397
+ }
398
+
399
+ function assertUploadSize(size) {
400
+ if (!Number.isSafeInteger(size) || size < 1 || size > MAX_UPLOAD_BYTES) {
401
+ throw new Error("Media TUS uploads must be smaller than 30 GB");
402
+ }
403
+ }
404
+
405
+ function ffmpegMissingGuide() {
406
+ if (process.platform === "darwin") return "FFmpeg was not found. Install it with `brew install ffmpeg` or download it from https://ffmpeg.org/download.html.";
407
+ if (process.platform === "win32") return "FFmpeg was not found. Install it with `winget install Gyan.FFmpeg` or download it from https://ffmpeg.org/download.html.";
408
+ return "FFmpeg was not found. Install it with your package manager (for example, `sudo apt install ffmpeg` on Debian/Ubuntu) or download it from https://ffmpeg.org/download.html.";
409
+ }
410
+
411
+ function reportProgress(bytesUploaded, bytesTotal, options) {
412
+ const percentage = bytesTotal ? Math.min(100, Math.floor((bytesUploaded / bytesTotal) * 100)) : 100;
413
+ writeLine(options.stderr, `Upload progress: ${percentage}% (${formatBytes(bytesUploaded)} / ${formatBytes(bytesTotal)})`);
414
+ }
415
+
416
+ function formatBytes(bytes) {
417
+ return `${(bytes / (1024 ** 3)).toFixed(2)} GiB`;
418
+ }
419
+
420
+ function writeLine(stream, value) {
421
+ stream?.write(`${value}\n`);
422
+ }