free-antigravity-cli 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.
Files changed (80) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +142 -0
  3. package/dist/chat.d.ts +2 -0
  4. package/dist/chat.d.ts.map +1 -0
  5. package/dist/chat.js +212 -0
  6. package/dist/chat.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +216 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/config.d.ts +29 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +125 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/crypto.d.ts +5 -0
  16. package/dist/crypto.d.ts.map +1 -0
  17. package/dist/crypto.js +93 -0
  18. package/dist/crypto.js.map +1 -0
  19. package/dist/logger.d.ts +9 -0
  20. package/dist/logger.d.ts.map +1 -0
  21. package/dist/logger.js +10 -0
  22. package/dist/logger.js.map +1 -0
  23. package/dist/proxy/modelUtils.d.ts +31 -0
  24. package/dist/proxy/modelUtils.d.ts.map +1 -0
  25. package/dist/proxy/modelUtils.js +55 -0
  26. package/dist/proxy/modelUtils.js.map +1 -0
  27. package/dist/proxy/registry.d.ts +40 -0
  28. package/dist/proxy/registry.d.ts.map +1 -0
  29. package/dist/proxy/registry.js +176 -0
  30. package/dist/proxy/registry.js.map +1 -0
  31. package/dist/proxy/shared.d.ts +39 -0
  32. package/dist/proxy/shared.d.ts.map +1 -0
  33. package/dist/proxy/shared.js +74 -0
  34. package/dist/proxy/shared.js.map +1 -0
  35. package/dist/proxy/translators/anthropic.d.ts +119 -0
  36. package/dist/proxy/translators/anthropic.d.ts.map +1 -0
  37. package/dist/proxy/translators/anthropic.js +273 -0
  38. package/dist/proxy/translators/anthropic.js.map +1 -0
  39. package/dist/proxy/translators/google.d.ts +86 -0
  40. package/dist/proxy/translators/google.d.ts.map +1 -0
  41. package/dist/proxy/translators/google.js +111 -0
  42. package/dist/proxy/translators/google.js.map +1 -0
  43. package/dist/proxy/translators/ollama.d.ts +27 -0
  44. package/dist/proxy/translators/ollama.d.ts.map +1 -0
  45. package/dist/proxy/translators/ollama.js +82 -0
  46. package/dist/proxy/translators/ollama.js.map +1 -0
  47. package/dist/proxy/translators/openai.d.ts +132 -0
  48. package/dist/proxy/translators/openai.d.ts.map +1 -0
  49. package/dist/proxy/translators/openai.js +396 -0
  50. package/dist/proxy/translators/openai.js.map +1 -0
  51. package/dist/proxy/translators/utils.d.ts +60 -0
  52. package/dist/proxy/translators/utils.d.ts.map +1 -0
  53. package/dist/proxy/translators/utils.js +504 -0
  54. package/dist/proxy/translators/utils.js.map +1 -0
  55. package/dist/proxy.d.ts +22 -0
  56. package/dist/proxy.d.ts.map +1 -0
  57. package/dist/proxy.js +576 -0
  58. package/dist/proxy.js.map +1 -0
  59. package/dist/schemaValidator.d.ts +50 -0
  60. package/dist/schemaValidator.d.ts.map +1 -0
  61. package/dist/schemaValidator.js +208 -0
  62. package/dist/schemaValidator.js.map +1 -0
  63. package/install.cmd +33 -0
  64. package/package.json +46 -0
  65. package/src/chat.ts +184 -0
  66. package/src/cli.ts +184 -0
  67. package/src/config.ts +99 -0
  68. package/src/crypto.ts +49 -0
  69. package/src/logger.ts +8 -0
  70. package/src/proxy/modelUtils.ts +86 -0
  71. package/src/proxy/registry.ts +196 -0
  72. package/src/proxy/shared.ts +102 -0
  73. package/src/proxy/translators/anthropic.ts +420 -0
  74. package/src/proxy/translators/google.ts +162 -0
  75. package/src/proxy/translators/ollama.ts +88 -0
  76. package/src/proxy/translators/openai.ts +556 -0
  77. package/src/proxy/translators/utils.ts +552 -0
  78. package/src/proxy.ts +573 -0
  79. package/src/schemaValidator.ts +215 -0
  80. package/tsconfig.json +19 -0
@@ -0,0 +1,552 @@
1
+ /**
2
+ * Shared translator utility functions.
3
+ * Extracted from proxy.js to avoid duplication across translator modules.
4
+ */
5
+
6
+ import * as path from 'path';
7
+ import { log } from '../../logger';
8
+
9
+ // ─── Types ────────────────────────────────────────────────────────────────
10
+
11
+ export interface GeminiParameterProperties {
12
+ type?: string;
13
+ properties?: GeminiParameterProperties;
14
+ items?: GeminiParameterProperties;
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ export interface ToolCallArgs {
19
+ CommandLine?: string;
20
+ Cwd?: string;
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ export interface TranslatedToolCall {
25
+ name: string;
26
+ args: Record<string, unknown>;
27
+ }
28
+
29
+ export interface TranslatedCallInfo {
30
+ originalName: string;
31
+ translatedName: string;
32
+ cmd: string;
33
+ cwd?: string;
34
+ }
35
+
36
+ export interface MatchResult {
37
+ Filename: string;
38
+ LineNumber: number;
39
+ LineContent: string;
40
+ }
41
+
42
+ export interface DirectoryItem {
43
+ name: string;
44
+ isDir: boolean;
45
+ sizeBytes?: number;
46
+ }
47
+
48
+ export interface FileListResponse {
49
+ files?: DirectoryItem[];
50
+ children?: DirectoryItem[];
51
+ content?: string;
52
+ CodeContent?: string;
53
+ }
54
+
55
+ export type ToolResponse = string | DirectoryItem[] | MatchResult[] | FileListResponse;
56
+
57
+ // ─── Tool Parameter Normalization ──────────────────────────────────────────
58
+
59
+ const TOOL_PARAM_NORMALIZATION: Record<string, { primaryKey: string; aliases: string[] }> = {
60
+ view_file: {
61
+ primaryKey: 'AbsolutePath',
62
+ aliases: [
63
+ 'absolute_path',
64
+ 'absolutePath',
65
+ 'path',
66
+ 'file_path',
67
+ 'filePath',
68
+ 'file',
69
+ 'filename',
70
+ 'FilePath',
71
+ 'FileName',
72
+ 'target',
73
+ 'source',
74
+ 'input',
75
+ 'uri',
76
+ ],
77
+ },
78
+ list_dir: {
79
+ primaryKey: 'DirectoryPath',
80
+ aliases: [
81
+ 'directory_path',
82
+ 'directoryPath',
83
+ 'path',
84
+ 'dir_path',
85
+ 'dirPath',
86
+ 'dir',
87
+ 'directory',
88
+ 'folder',
89
+ 'FolderPath',
90
+ 'folder_path',
91
+ 'target',
92
+ 'root',
93
+ 'base',
94
+ ],
95
+ },
96
+ grep_search: {
97
+ primaryKey: 'Query',
98
+ aliases: [
99
+ 'query',
100
+ 'search',
101
+ 'SearchQuery',
102
+ 'search_query',
103
+ 'searchQuery',
104
+ 'pattern',
105
+ 'Pattern',
106
+ 'regex',
107
+ 'Regex',
108
+ 'term',
109
+ 'keyword',
110
+ 'text',
111
+ 'needle',
112
+ ],
113
+ },
114
+ 'grep_search.SearchPath': {
115
+ primaryKey: 'SearchPath',
116
+ aliases: [
117
+ 'search_path',
118
+ 'searchPath',
119
+ 'path',
120
+ 'directory',
121
+ 'DirectoryPath',
122
+ 'directory_path',
123
+ 'folder',
124
+ 'dir',
125
+ 'root',
126
+ 'base',
127
+ ],
128
+ },
129
+ replace_file_content: {
130
+ primaryKey: 'TargetFile',
131
+ aliases: [
132
+ 'target_file',
133
+ 'targetFile',
134
+ 'file',
135
+ 'AbsolutePath',
136
+ 'absolute_path',
137
+ 'filePath',
138
+ 'file_path',
139
+ 'path',
140
+ 'FilePath',
141
+ 'target',
142
+ 'filename',
143
+ 'source',
144
+ ],
145
+ },
146
+ write_file: {
147
+ primaryKey: 'AbsolutePath',
148
+ aliases: [
149
+ 'absolute_path',
150
+ 'absolutePath',
151
+ 'path',
152
+ 'file_path',
153
+ 'filePath',
154
+ 'file',
155
+ 'filename',
156
+ 'FilePath',
157
+ 'FileName',
158
+ 'target_file',
159
+ 'targetFile',
160
+ 'target',
161
+ 'dest',
162
+ 'destination',
163
+ ],
164
+ },
165
+ run_command: {
166
+ primaryKey: 'CommandLine',
167
+ aliases: [
168
+ 'command_line',
169
+ 'commandLine',
170
+ 'cmd',
171
+ 'command',
172
+ 'Command',
173
+ 'Cmd',
174
+ 'shell_command',
175
+ 'shellCommand',
176
+ 'script',
177
+ 'exec',
178
+ 'execute',
179
+ ],
180
+ },
181
+ 'run_command.Cwd': {
182
+ primaryKey: 'Cwd',
183
+ aliases: ['cwd', 'working_dir', 'workingDirectory', 'working_directory', 'dir', 'directory', 'path', 'folder'],
184
+ },
185
+ read_file: {
186
+ primaryKey: 'AbsolutePath',
187
+ aliases: [
188
+ 'absolute_path',
189
+ 'absolutePath',
190
+ 'path',
191
+ 'file_path',
192
+ 'filePath',
193
+ 'file',
194
+ 'filename',
195
+ 'FilePath',
196
+ 'FileName',
197
+ 'target',
198
+ 'source',
199
+ 'input',
200
+ ],
201
+ },
202
+ search_files: {
203
+ primaryKey: 'SearchPath',
204
+ aliases: [
205
+ 'search_path',
206
+ 'searchPath',
207
+ 'path',
208
+ 'directory',
209
+ 'DirectoryPath',
210
+ 'directory_path',
211
+ 'folder',
212
+ 'dir',
213
+ 'root',
214
+ 'base',
215
+ ],
216
+ },
217
+ create_directory: {
218
+ primaryKey: 'DirectoryPath',
219
+ aliases: ['directory_path', 'directoryPath', 'path', 'dir_path', 'dirPath', 'dir', 'folder', 'target', 'name'],
220
+ },
221
+ delete_file: {
222
+ primaryKey: 'AbsolutePath',
223
+ aliases: [
224
+ 'absolute_path',
225
+ 'absolutePath',
226
+ 'path',
227
+ 'file_path',
228
+ 'filePath',
229
+ 'file',
230
+ 'filename',
231
+ 'FilePath',
232
+ 'target',
233
+ ],
234
+ },
235
+ move_file: {
236
+ primaryKey: 'SourcePath',
237
+ aliases: [
238
+ 'source_path',
239
+ 'sourcePath',
240
+ 'source',
241
+ 'from',
242
+ 'src',
243
+ 'path',
244
+ 'file_path',
245
+ 'filePath',
246
+ 'AbsolutePath',
247
+ 'absolute_path',
248
+ ],
249
+ },
250
+ 'move_file.DestinationPath': {
251
+ primaryKey: 'DestinationPath',
252
+ aliases: ['destination_path', 'destinationPath', 'dest', 'destination', 'to', 'dst', 'target'],
253
+ },
254
+ };
255
+
256
+ /**
257
+ * Normalizes parameter names from external models to match Antigravity's expected PascalCase format.
258
+ */
259
+ export function normalizeToolArgs(
260
+ name: string,
261
+ args: Record<string, unknown> | null | undefined,
262
+ ): Record<string, unknown> {
263
+ if (!args || typeof args !== 'object') return args || {};
264
+
265
+ // Handle array args
266
+ if (Array.isArray(args)) {
267
+ const config = TOOL_PARAM_NORMALIZATION[name];
268
+ if (config && args.length > 0 && typeof args[0] === 'string') {
269
+ return { [config.primaryKey]: args[0] };
270
+ }
271
+ return {};
272
+ }
273
+
274
+ const config = TOOL_PARAM_NORMALIZATION[name];
275
+ if (!config) {
276
+ return applyUniversalPathFallback(args);
277
+ }
278
+
279
+ const normalized: Record<string, unknown> = {};
280
+ const usedKeys = new Set<string>();
281
+
282
+ for (const [key, value] of Object.entries(args)) {
283
+ let matched = false;
284
+
285
+ if (key === config.primaryKey || (config.aliases && config.aliases.includes(key))) {
286
+ normalized[config.primaryKey] = value;
287
+ usedKeys.add(key);
288
+ matched = true;
289
+ }
290
+
291
+ if (!matched) {
292
+ const subConfigKey = name + '.' + key;
293
+ const subConfig = TOOL_PARAM_NORMALIZATION[subConfigKey];
294
+ if (subConfig) {
295
+ normalized[subConfig.primaryKey] = value;
296
+ usedKeys.add(key);
297
+ matched = true;
298
+ }
299
+ }
300
+
301
+ if (!matched) {
302
+ for (const [ck, cv] of Object.entries(TOOL_PARAM_NORMALIZATION)) {
303
+ if (ck.startsWith(name + '.') && cv.aliases && cv.aliases.includes(key)) {
304
+ normalized[cv.primaryKey] = value;
305
+ usedKeys.add(key);
306
+ matched = true;
307
+ break;
308
+ }
309
+ }
310
+ }
311
+
312
+ if (!matched) {
313
+ normalized[key] = value;
314
+ }
315
+ }
316
+
317
+ if (!normalized[config.primaryKey]) {
318
+ const unassigned = Object.entries(args).filter(([k]) => !usedKeys.has(k));
319
+ let found = unassigned.find(
320
+ ([, v]) => typeof v === 'string' && (v.includes('/') || v.includes('\\') || v.includes('.')),
321
+ );
322
+ if (!found) found = unassigned.find(([, v]) => typeof v === 'string' && v.length > 0);
323
+ if (!found) {
324
+ found = Object.entries(args).find(
325
+ ([, v]) => typeof v === 'string' && (v.includes('/') || v.includes('\\') || v.includes('.')),
326
+ );
327
+ if (!found) found = Object.entries(args).find(([, v]) => typeof v === 'string' && v.length > 0);
328
+ }
329
+ if (found) {
330
+ normalized[config.primaryKey] = found[1];
331
+ log.info(
332
+ `[Utils] normalizeToolArgs fallback: "${name}" extracted ${config.primaryKey}=${found[1]} from key "${found[0]}"`,
333
+ );
334
+ } else {
335
+ log.warn(
336
+ `[Utils] normalizeToolArgs: "${name}" could not find value for "${config.primaryKey}". args=${JSON.stringify(args)}`,
337
+ );
338
+ }
339
+ }
340
+
341
+ return normalized;
342
+ }
343
+
344
+ function applyUniversalPathFallback(args: Record<string, unknown>): Record<string, unknown> {
345
+ const result = { ...args };
346
+ const aliasMap: Record<string, string> = {
347
+ path: 'AbsolutePath',
348
+ file_path: 'AbsolutePath',
349
+ filePath: 'AbsolutePath',
350
+ file: 'AbsolutePath',
351
+ filename: 'AbsolutePath',
352
+ target: 'AbsolutePath',
353
+ directory_path: 'DirectoryPath',
354
+ directoryPath: 'DirectoryPath',
355
+ dir: 'DirectoryPath',
356
+ directory: 'DirectoryPath',
357
+ folder: 'DirectoryPath',
358
+ target_file: 'TargetFile',
359
+ targetFile: 'TargetFile',
360
+ source: 'SourcePath',
361
+ sourcePath: 'SourcePath',
362
+ source_path: 'SourcePath',
363
+ dest: 'DestinationPath',
364
+ destination: 'DestinationPath',
365
+ };
366
+
367
+ for (const [key, value] of Object.entries(args)) {
368
+ const mappedKey = aliasMap[key];
369
+ if (mappedKey) {
370
+ result[mappedKey] = value;
371
+ delete result[key];
372
+ return result;
373
+ }
374
+ }
375
+
376
+ for (const [, value] of Object.entries(args)) {
377
+ if (typeof value === 'string' && (value.includes('/') || value.includes('\\') || value.includes('.'))) {
378
+ result['AbsolutePath'] = value;
379
+ return result;
380
+ }
381
+ }
382
+
383
+ return result;
384
+ }
385
+
386
+ // ─── Utility Functions ────────────────────────────────────────────────────
387
+
388
+ /**
389
+ * Recursively converts Gemini parameter types (UPPERCASE) to lowercase format.
390
+ * Gemini uses uppercase (STRING, NUMBER); OpenAI/Anthropic need lowercase.
391
+ */
392
+ export function fixParamTypes(properties: Record<string, unknown> | undefined): void {
393
+ if (!properties) return;
394
+ for (const key of Object.keys(properties)) {
395
+ const val = properties[key];
396
+ if (val && typeof val === 'object') {
397
+ const obj = val as Record<string, unknown>;
398
+ if (typeof obj.type === 'string') {
399
+ obj.type = (obj.type as string).toLowerCase();
400
+ }
401
+ if (obj.properties && typeof obj.properties === 'object') {
402
+ fixParamTypes(obj.properties as Record<string, unknown>);
403
+ }
404
+ if (obj.items && typeof obj.items === 'object') {
405
+ const items = obj.items as Record<string, unknown>;
406
+ if (typeof items.type === 'string') {
407
+ items.type = (items.type as string).toLowerCase();
408
+ }
409
+ if (items.properties && typeof items.properties === 'object') {
410
+ fixParamTypes(items.properties as Record<string, unknown>);
411
+ }
412
+ }
413
+ }
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Translates generic shell/terminal commands (run_command) into native Antigravity file tools.
419
+ */
420
+ export function translateToolCallToNative(name: string, args: ToolCallArgs): TranslatedToolCall {
421
+ if (name !== 'run_command' || !args || !args.CommandLine) {
422
+ return { name, args: args as Record<string, unknown> };
423
+ }
424
+
425
+ const cmd = args.CommandLine.trim();
426
+ const cwd = args.Cwd || process.cwd();
427
+
428
+ // 1. list_dir translation
429
+ const isListDir = /^(ls|dir)(\s+[\w\-\/\.\*]+)*$/i.test(cmd);
430
+ if (isListDir) {
431
+ let dirPath = cwd;
432
+ const tokens = cmd.split(/\s+/).slice(1);
433
+ const pathToken = tokens.find((t) => !t.startsWith('-') && !t.startsWith('/'));
434
+ if (pathToken) {
435
+ dirPath = path.isAbsolute(pathToken) ? pathToken : path.resolve(cwd, pathToken);
436
+ }
437
+ log.info(`[Proxy] Translating run_command "${cmd}" to list_dir on "${dirPath}"`);
438
+ return { name: 'list_dir', args: { DirectoryPath: dirPath } };
439
+ }
440
+
441
+ // 2. view_file translation
442
+ const catMatch = /^(cat|type)\s+(["']?)(.*?)\2$/i.exec(cmd);
443
+ if (catMatch) {
444
+ const filePath = catMatch[3].trim();
445
+ const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
446
+ log.info(`[Proxy] Translating run_command "${cmd}" to view_file on "${absPath}"`);
447
+ return { name: 'view_file', args: { AbsolutePath: absPath } };
448
+ }
449
+
450
+ // 2b. write_file translation (echo redirect)
451
+ const echoRedirectMatch = /^(echo|printf)\s+(.+?)\s*>\s*(.+)$/i.exec(cmd);
452
+ if (echoRedirectMatch) {
453
+ const content = echoRedirectMatch[2].replace(/^["']|["']$/g, '');
454
+ const filePath = echoRedirectMatch[3].trim();
455
+ const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
456
+ log.info(`[Proxy] Translating run_command "${cmd}" to write_file on "${absPath}"`);
457
+ return { name: 'write_file', args: { AbsolutePath: absPath, Content: content, Append: cmd.includes('>>') } };
458
+ }
459
+
460
+ // 3. grep_search translation
461
+ if (cmd.toLowerCase().startsWith('grep') || cmd.toLowerCase().startsWith('findstr')) {
462
+ let query = '';
463
+ let searchPath = cwd;
464
+ const regexQuotes = /"([^"]+)"|'([^']+)'/g;
465
+ const quotesFound = [...cmd.matchAll(regexQuotes)];
466
+ if (quotesFound.length > 0) {
467
+ query = quotesFound[0][1] || quotesFound[0][2];
468
+ } else {
469
+ const tokens = cmd.split(/\s+/);
470
+ query = tokens[tokens.length - 1];
471
+ }
472
+ const tokens = cmd.split(/\s+/);
473
+ const pathToken = tokens.find(
474
+ (t, idx) =>
475
+ idx > 0 && !t.startsWith('-') && !t.startsWith('/') && !t.includes('"') && !t.includes("'") && t !== query,
476
+ );
477
+ if (pathToken) {
478
+ searchPath = path.isAbsolute(pathToken) ? pathToken : path.resolve(cwd, pathToken);
479
+ }
480
+ if (query) {
481
+ log.info(`[Proxy] Translating run_command "${cmd}" to grep_search (Query: "${query}", Path: "${searchPath}")`);
482
+ return {
483
+ name: 'grep_search',
484
+ args: {
485
+ Query: query,
486
+ SearchPath: searchPath,
487
+ CaseInsensitive: cmd.includes('-i') || cmd.toLowerCase().includes('/i'),
488
+ IsRegex: false,
489
+ MatchPerLine: true,
490
+ },
491
+ };
492
+ }
493
+ }
494
+
495
+ return { name, args: args as Record<string, unknown> };
496
+ }
497
+
498
+ /**
499
+ * Formats native file tool outputs (JSON/Array) back into standard textual command-line outputs.
500
+ */
501
+ export function formatTranslatedResponse(translatedInfo: TranslatedCallInfo, responseData: unknown): string {
502
+ const { translatedName, cmd } = translatedInfo;
503
+ log.info(`[Proxy] Formatting native response back to CLI for translated tool "${translatedName}" (Cmd: "${cmd}")`);
504
+
505
+ if (translatedName === 'list_dir') {
506
+ if (Array.isArray(responseData)) {
507
+ return (responseData as DirectoryItem[])
508
+ .map((item) => {
509
+ const typeIndicator = item.isDir ? '<DIR>' : ' ';
510
+ const sizeStr = item.isDir ? '' : ` (${item.sizeBytes || 0} bytes)`;
511
+ return `${typeIndicator} ${item.name}${sizeStr}`;
512
+ })
513
+ .join('\n');
514
+ }
515
+ if (responseData && typeof responseData === 'object') {
516
+ const data = responseData as FileListResponse;
517
+ const items = data.files || data.children || [];
518
+ if (Array.isArray(items)) {
519
+ return items.map((item) => `${item.isDir ? '<DIR>' : ' '} ${item.name}`).join('\n');
520
+ }
521
+ }
522
+ return typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
523
+ }
524
+
525
+ if (translatedName === 'view_file') {
526
+ if (responseData && typeof responseData === 'object') {
527
+ const data = responseData as FileListResponse;
528
+ return data.content || data.CodeContent || JSON.stringify(responseData);
529
+ }
530
+ return typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
531
+ }
532
+
533
+ if (translatedName === 'grep_search') {
534
+ if (Array.isArray(responseData)) {
535
+ return (responseData as MatchResult[])
536
+ .map((match) => `${match.Filename}:${match.LineNumber}:${match.LineContent}`)
537
+ .join('\n');
538
+ }
539
+ return typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
540
+ }
541
+
542
+ if (translatedName === 'write_file') {
543
+ if (responseData && typeof responseData === 'object') {
544
+ const data = responseData as Record<string, unknown>;
545
+ if (data.success) return `File written successfully: ${data.path || 'unknown'}`;
546
+ return `Failed to write file: ${data.error || 'Unknown error'}`;
547
+ }
548
+ return typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
549
+ }
550
+
551
+ return typeof responseData === 'string' ? responseData : JSON.stringify(responseData);
552
+ }