@robota-sdk/agent-tools 3.0.0-beta.6 → 3.0.0-beta.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,16 +46,20 @@ const agent = new Robota({
46
46
 
47
47
  ## Built-in Tools (8)
48
48
 
49
- | Export | Tool Name | Description |
50
- | --------------- | --------- | ------------------------------------ |
51
- | `bashTool` | Bash | Execute shell commands |
52
- | `readTool` | Read | Read file contents with line numbers |
53
- | `writeTool` | Write | Write content to a file |
54
- | `editTool` | Edit | Replace a specific string in a file |
55
- | `globTool` | Glob | Find files matching a glob pattern |
56
- | `grepTool` | Grep | Search file contents with regex |
57
- | `webFetchTool` | WebFetch | Fetch URL content (HTML-to-text) |
58
- | `webSearchTool` | WebSearch | Web search via Brave Search API |
49
+ | Export | Tool Name | Description |
50
+ | --------------- | --------- | ------------------------------------------------ |
51
+ | `bashTool` | Bash | Execute shell commands via `child_process.spawn` |
52
+ | `readTool` | Read | Read file contents with line numbers (cat -n) |
53
+ | `writeTool` | Write | Write content to a file (creates parent dirs) |
54
+ | `editTool` | Edit | Replace a specific string in a file |
55
+ | `globTool` | Glob | Find files matching a glob pattern (fast-glob) |
56
+ | `grepTool` | Grep | Search file contents with regex patterns |
57
+ | `webFetchTool` | WebFetch | Fetch URL content (HTML-to-text conversion) |
58
+ | `webSearchTool` | WebSearch | Web search via Brave Search API |
59
+
60
+ ## Edit and Write Safety
61
+
62
+ Recent file tool updates keep write/edit behavior atomic and make Edit tool results easier for higher layers to display. Atomic replacements preserve existing target mode bits, so executable scripts remain executable after Write or Edit updates. The Edit tool returns line metadata for changed regions, allowing the CLI to render concise context hunks instead of dumping full files or opaque summaries.
59
63
 
60
64
  ## Tool Infrastructure
61
65
 
@@ -66,7 +70,31 @@ const agent = new Robota({
66
70
  | `createFunctionTool` | Factory for creating function tools |
67
71
  | `createZodFunctionTool` | Factory with Zod validation and JSON Schema conversion |
68
72
  | `OpenAPITool` | Tool generated from OpenAPI specification |
73
+ | `createOpenAPITool` | Factory for creating OpenAPI tools |
69
74
  | `zodToJsonSchema` | Converts Zod schemas to JSON Schema format |
75
+ | `TToolResult` | Result type for built-in CLI tool invocations |
76
+
77
+ ## TToolResult Shape
78
+
79
+ ```typescript
80
+ interface TToolResult {
81
+ success: boolean;
82
+ output: string;
83
+ error?: string;
84
+ exitCode?: number;
85
+ startLine?: number; // Start line number of the edit in the original file (Edit tool only)
86
+ }
87
+ ```
88
+
89
+ `TToolResult` is the inner result type used by built-in tools. It is serialized to JSON and placed inside the `IToolResult.data` field before being returned to the Robota execution loop.
90
+
91
+ ## Dependencies
92
+
93
+ | Dependency | Kind | Purpose |
94
+ | ------------------------ | ---- | ------------------------------------------------------ |
95
+ | `@robota-sdk/agent-core` | Peer | Abstract tool base class, tool interfaces, event types |
96
+ | `fast-glob` | Prod | High-performance glob matching for the Glob tool |
97
+ | `zod` | Prod | Schema validation for function tool parameters |
70
98
 
71
99
  ## License
72
100
 
@@ -215,7 +215,9 @@ function zodToJsonSchema(schema, options = {}) {
215
215
  type: "object",
216
216
  properties,
217
217
  required,
218
- ...options.allowAdditionalProperties && { additionalProperties: true }
218
+ ...(options.allowAdditionalProperties || schemaDef.unknownKeys === "passthrough") && {
219
+ additionalProperties: true
220
+ }
219
221
  };
220
222
  }
221
223
  function convertZodTypeToProperty(typeObj) {
@@ -294,6 +296,100 @@ function isRequiredField(typeObj) {
294
296
  return typeDef.typeName !== "ZodOptional" && typeDef.typeName !== "ZodNullable" && typeDef.typeName !== "ZodDefault";
295
297
  }
296
298
 
299
+ // src/implementations/function-tool/parameter-validator.ts
300
+ function validateParameterType(key, value, schema) {
301
+ const expectedType = schema["type"];
302
+ switch (expectedType) {
303
+ case "string":
304
+ if (typeof value !== "string") {
305
+ return `Parameter "${key}" must be a string, got ${typeof value}`;
306
+ }
307
+ break;
308
+ case "number":
309
+ if (typeof value !== "number" || isNaN(value)) {
310
+ return `Parameter "${key}" must be a number, got ${typeof value}`;
311
+ }
312
+ break;
313
+ case "boolean":
314
+ if (typeof value !== "boolean") {
315
+ return `Parameter "${key}" must be a boolean, got ${typeof value}`;
316
+ }
317
+ break;
318
+ case "array":
319
+ if (!Array.isArray(value)) {
320
+ return `Parameter "${key}" must be an array, got ${typeof value}`;
321
+ }
322
+ if (schema.items) {
323
+ for (let i = 0; i < value.length; i++) {
324
+ const itemError = validateParameterType(`${key}[${i}]`, value[i], schema.items);
325
+ if (itemError) {
326
+ return itemError;
327
+ }
328
+ }
329
+ }
330
+ break;
331
+ case "object":
332
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
333
+ return `Parameter "${key}" must be an object, got ${typeof value}`;
334
+ }
335
+ break;
336
+ }
337
+ if (schema.enum && schema.enum.length > 0) {
338
+ const enumValues = schema.enum;
339
+ let isValidEnum = false;
340
+ for (const enumValue of enumValues) {
341
+ if (value === enumValue) {
342
+ isValidEnum = true;
343
+ break;
344
+ }
345
+ }
346
+ if (!isValidEnum) {
347
+ return `Parameter "${key}" must be one of: ${enumValues.join(", ")}, got ${value}`;
348
+ }
349
+ }
350
+ return void 0;
351
+ }
352
+ function getValidationErrors(parameters, schemaRequired, schemaProperties, additionalProperties) {
353
+ const errors = [];
354
+ for (const field of schemaRequired) {
355
+ if (!(field in parameters)) {
356
+ errors.push(`Missing required parameter: ${field}`);
357
+ }
358
+ }
359
+ for (const [key, value] of Object.entries(parameters)) {
360
+ const paramSchema = schemaProperties[key];
361
+ if (!paramSchema) {
362
+ if (additionalProperties === true) {
363
+ continue;
364
+ }
365
+ if (additionalProperties && typeof additionalProperties === "object") {
366
+ const additionalTypeError = validateParameterType(key, value, additionalProperties);
367
+ if (additionalTypeError) errors.push(additionalTypeError);
368
+ continue;
369
+ }
370
+ errors.push(`Unknown parameter: ${key}`);
371
+ continue;
372
+ }
373
+ const typeError = validateParameterType(key, value, paramSchema);
374
+ if (typeError) {
375
+ errors.push(typeError);
376
+ }
377
+ }
378
+ return errors;
379
+ }
380
+ function validateToolParameters(parameters, schemaRequired, schemaProperties, additionalProperties) {
381
+ const errors = getValidationErrors(
382
+ parameters,
383
+ schemaRequired,
384
+ schemaProperties,
385
+ additionalProperties
386
+ );
387
+ return {
388
+ isValid: errors.length === 0,
389
+ errors
390
+ };
391
+ }
392
+
297
393
  // src/implementations/function-tool.ts
298
394
  var FunctionTool = class {
299
395
  schema;
@@ -324,7 +420,12 @@ var FunctionTool = class {
324
420
  async execute(parameters, context) {
325
421
  const toolName = this.schema.name;
326
422
  if (!this.validate(parameters)) {
327
- const errors = this.getValidationErrors(parameters);
423
+ const errors = getValidationErrors(
424
+ parameters,
425
+ this.schema.parameters.required || [],
426
+ this.schema.parameters.properties || {},
427
+ this.schema.parameters.additionalProperties
428
+ );
328
429
  throw new import_agent_core3.ValidationError(`Invalid parameters for tool "${toolName}": ${errors.join(", ")}`);
329
430
  }
330
431
  const startTime = Date.now();
@@ -360,17 +461,23 @@ var FunctionTool = class {
360
461
  * Validate parameters (simple boolean result)
361
462
  */
362
463
  validate(parameters) {
363
- return this.getValidationErrors(parameters).length === 0;
464
+ return getValidationErrors(
465
+ parameters,
466
+ this.schema.parameters.required || [],
467
+ this.schema.parameters.properties || {},
468
+ this.schema.parameters.additionalProperties
469
+ ).length === 0;
364
470
  }
365
471
  /**
366
472
  * Validate tool parameters with detailed result
367
473
  */
368
474
  validateParameters(parameters) {
369
- const errors = this.getValidationErrors(parameters);
370
- return {
371
- isValid: errors.length === 0,
372
- errors
373
- };
475
+ return validateToolParameters(
476
+ parameters,
477
+ this.schema.parameters.required || [],
478
+ this.schema.parameters.properties || {},
479
+ this.schema.parameters.additionalProperties
480
+ );
374
481
  }
375
482
  /**
376
483
  * Get tool description
@@ -378,86 +485,6 @@ var FunctionTool = class {
378
485
  getDescription() {
379
486
  return this.schema.description;
380
487
  }
381
- /**
382
- * Get detailed validation errors
383
- */
384
- getValidationErrors(parameters) {
385
- const errors = [];
386
- const required = this.schema.parameters.required || [];
387
- const properties = this.schema.parameters.properties || {};
388
- for (const field of required) {
389
- if (!(field in parameters)) {
390
- errors.push(`Missing required parameter: ${field}`);
391
- }
392
- }
393
- for (const [key, value] of Object.entries(parameters)) {
394
- const paramSchema = properties[key];
395
- if (!paramSchema) {
396
- errors.push(`Unknown parameter: ${key}`);
397
- continue;
398
- }
399
- const typeError = this.validateParameterType(key, value, paramSchema);
400
- if (typeError) {
401
- errors.push(typeError);
402
- }
403
- }
404
- return errors;
405
- }
406
- /**
407
- * Validate individual parameter type
408
- */
409
- validateParameterType(key, value, schema) {
410
- const expectedType = schema["type"];
411
- switch (expectedType) {
412
- case "string":
413
- if (typeof value !== "string") {
414
- return `Parameter "${key}" must be a string, got ${typeof value}`;
415
- }
416
- break;
417
- case "number":
418
- if (typeof value !== "number" || isNaN(value)) {
419
- return `Parameter "${key}" must be a number, got ${typeof value}`;
420
- }
421
- break;
422
- case "boolean":
423
- if (typeof value !== "boolean") {
424
- return `Parameter "${key}" must be a boolean, got ${typeof value}`;
425
- }
426
- break;
427
- case "array":
428
- if (!Array.isArray(value)) {
429
- return `Parameter "${key}" must be an array, got ${typeof value}`;
430
- }
431
- if (schema.items) {
432
- for (let i = 0; i < value.length; i++) {
433
- const itemError = this.validateParameterType(`${key}[${i}]`, value[i], schema.items);
434
- if (itemError) {
435
- return itemError;
436
- }
437
- }
438
- }
439
- break;
440
- case "object":
441
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
442
- return `Parameter "${key}" must be an object, got ${typeof value}`;
443
- }
444
- break;
445
- }
446
- if (schema.enum && schema.enum.length > 0) {
447
- const enumValues = schema.enum;
448
- let isValidEnum = false;
449
- for (const enumValue of enumValues) {
450
- if (value === enumValue) {
451
- isValidEnum = true;
452
- break;
453
- }
454
- }
455
- if (!isValidEnum) {
456
- return `Parameter "${key}" must be one of: ${enumValues.join(", ")}, got ${value}`;
457
- }
458
- }
459
- return void 0;
460
- }
461
488
  /**
462
489
  * Validate constructor inputs
463
490
  */
@@ -501,6 +528,132 @@ function createZodFunctionTool(name, description, zodSchema, fn) {
501
528
 
502
529
  // src/implementations/openapi-tool.ts
503
530
  var import_agent_core4 = require("@robota-sdk/agent-core");
531
+
532
+ // src/implementations/openapi-schema-converter.ts
533
+ var HTTP_METHODS = [
534
+ "get",
535
+ "post",
536
+ "put",
537
+ "delete",
538
+ "patch",
539
+ "head",
540
+ "options"
541
+ ];
542
+ function findOperation(apiSpec, operationId) {
543
+ for (const [path, pathItem] of Object.entries(apiSpec.paths || {})) {
544
+ if (!pathItem) continue;
545
+ for (const method of HTTP_METHODS) {
546
+ const operation = pathItem[method];
547
+ if (operation?.operationId === operationId) {
548
+ return { method, path, operation };
549
+ }
550
+ }
551
+ }
552
+ return void 0;
553
+ }
554
+ function mapOpenAPIType(type) {
555
+ switch (type) {
556
+ case "string":
557
+ return "string";
558
+ case "number":
559
+ return "number";
560
+ case "integer":
561
+ return "integer";
562
+ case "boolean":
563
+ return "boolean";
564
+ case "array":
565
+ return "array";
566
+ case "object":
567
+ return "object";
568
+ default:
569
+ return "string";
570
+ }
571
+ }
572
+ function convertOpenAPISchemaToParameterSchema(schema) {
573
+ if ("$ref" in schema) {
574
+ return { type: "object" };
575
+ }
576
+ const result = {
577
+ type: mapOpenAPIType(schema.type)
578
+ };
579
+ if (schema.description) {
580
+ result.description = schema.description;
581
+ }
582
+ if (schema.enum) {
583
+ result.enum = schema.enum;
584
+ }
585
+ if (schema.minimum !== void 0) {
586
+ result.minimum = schema.minimum;
587
+ }
588
+ if (schema.maximum !== void 0) {
589
+ result.maximum = schema.maximum;
590
+ }
591
+ if (schema.pattern) {
592
+ result.pattern = schema.pattern;
593
+ }
594
+ if (schema.format) {
595
+ result.format = schema.format;
596
+ }
597
+ if (schema.default !== void 0) {
598
+ result.default = schema.default;
599
+ }
600
+ if (schema.type === "array" && schema.items) {
601
+ result.items = convertOpenAPISchemaToParameterSchema(schema.items);
602
+ }
603
+ if (schema.type === "object" && schema.properties) {
604
+ result.properties = {};
605
+ for (const [propName, propSchema] of Object.entries(schema.properties)) {
606
+ result.properties[propName] = convertOpenAPISchemaToParameterSchema(propSchema);
607
+ }
608
+ if (schema.required && schema.required.length > 0) {
609
+ result.required = schema.required;
610
+ }
611
+ }
612
+ return result;
613
+ }
614
+ function convertOpenAPIParamToSchema(param) {
615
+ const schema = param.schema;
616
+ return convertOpenAPISchemaToParameterSchema(schema);
617
+ }
618
+ function createSchemaFromOperation(operationId, opSpec) {
619
+ const properties = {};
620
+ const required = [];
621
+ const params = opSpec.parameters || [];
622
+ for (const param of params) {
623
+ properties[param.name] = convertOpenAPIParamToSchema(param);
624
+ if (param.required) {
625
+ required.push(param.name);
626
+ }
627
+ }
628
+ if (opSpec.requestBody) {
629
+ const requestBody = opSpec.requestBody;
630
+ const jsonContent = requestBody.content?.["application/json"];
631
+ if (jsonContent?.schema) {
632
+ const bodySchema = convertOpenAPISchemaToParameterSchema(jsonContent.schema);
633
+ if (bodySchema.type === "object" && bodySchema.properties) {
634
+ Object.assign(properties, bodySchema.properties);
635
+ const schemaWithRequired = bodySchema;
636
+ if (schemaWithRequired.required) {
637
+ required.push(...schemaWithRequired.required);
638
+ }
639
+ }
640
+ }
641
+ }
642
+ const schemaParams = {
643
+ type: "object",
644
+ properties
645
+ };
646
+ if (required.length > 0) {
647
+ schemaParams.required = required;
648
+ }
649
+ return {
650
+ name: operationId,
651
+ description: opSpec.summary || opSpec.description || `OpenAPI operation: ${operationId}`,
652
+ parameters: schemaParams
653
+ };
654
+ }
655
+
656
+ // src/implementations/openapi-tool.ts
504
657
  var OpenAPITool = class {
505
658
  schema;
506
659
  apiSpec;
@@ -607,36 +760,13 @@ var OpenAPITool = class {
607
760
  * @private
608
761
  */
609
762
  async executeAPICall(parameters, _context) {
610
- const operation = this.findOperation();
763
+ const operation = findOperation(this.apiSpec, this.operationId);
611
764
  if (!operation) {
612
765
  throw new Error(`Operation ${this.operationId} not found in OpenAPI spec`);
613
766
  }
614
- const requestConfig = this.buildRequestConfig(operation, parameters);
767
+ this.buildRequestConfig(operation, parameters);
615
768
  throw new Error("Not implemented: actual API execution is not yet available");
616
769
  }
617
- /**
618
- * Find the operation in the OpenAPI specification
619
- */
620
- findOperation() {
621
- for (const [path, pathItem] of Object.entries(this.apiSpec.paths || {})) {
622
- if (!pathItem) continue;
623
- for (const method of [
624
- "get",
625
- "post",
626
- "put",
627
- "delete",
628
- "patch",
629
- "head",
630
- "options"
631
- ]) {
632
- const operation = pathItem[method];
633
- if (operation?.operationId === this.operationId) {
634
- return { method, path, operation };
635
- }
636
- }
637
- }
638
- return void 0;
639
- }
640
770
  /**
641
771
  * Build HTTP request configuration from OpenAPI operation and parameters
642
772
  */
@@ -708,121 +838,13 @@ var OpenAPITool = class {
708
838
  * Create tool schema from OpenAPI operation specification
709
839
  */
710
840
  createSchemaFromOpenAPI() {
711
- const operation = this.findOperation();
841
+ const operation = findOperation(this.apiSpec, this.operationId);
712
842
  if (!operation) {
713
843
  throw new Error(
714
844
  `[STRICT-POLICY][EMITTER-CONTRACT] OpenAPI operation not found: ${this.operationId}. Emitter contract must provide a valid operationId present in the OpenAPI document.`
715
845
  );
716
846
  }
717
- const { operation: opSpec } = operation;
718
- const properties = {};
719
- const required = [];
720
- const params = opSpec.parameters || [];
721
- for (const param of params) {
722
- properties[param.name] = this.convertOpenAPIParamToSchema(param);
723
- if (param.required) {
724
- required.push(param.name);
725
- }
726
- }
727
- if (opSpec.requestBody) {
728
- const requestBody = opSpec.requestBody;
729
- const jsonContent = requestBody.content?.["application/json"];
730
- if (jsonContent?.schema) {
731
- const bodySchema = this.convertOpenAPISchemaToParameterSchema(jsonContent.schema);
732
- if (bodySchema.type === "object" && bodySchema.properties) {
733
- Object.assign(properties, bodySchema.properties);
734
- const schemaWithRequired = bodySchema;
735
- if (schemaWithRequired.required) {
736
- required.push(...schemaWithRequired.required);
737
- }
738
- }
739
- }
740
- }
741
- const schemaParams = {
742
- type: "object",
743
- properties
744
- };
745
- if (required.length > 0) {
746
- schemaParams.required = required;
747
- }
748
- return {
749
- name: this.operationId,
750
- description: opSpec.summary || opSpec.description || `OpenAPI operation: ${this.operationId}`,
751
- parameters: schemaParams
752
- };
753
- }
754
- /**
755
- * Convert OpenAPI parameter to tool parameter schema
756
- */
757
- convertOpenAPIParamToSchema(param) {
758
- const schema = param.schema;
759
- return this.convertOpenAPISchemaToParameterSchema(schema);
760
- }
761
- /**
762
- * Convert OpenAPI schema to parameter schema
763
- */
764
- convertOpenAPISchemaToParameterSchema(schema) {
765
- if ("$ref" in schema) {
766
- return { type: "object" };
767
- }
768
- const result = {
769
- type: this.mapOpenAPIType(schema.type)
770
- };
771
- if (schema.description) {
772
- result.description = schema.description;
773
- }
774
- if (schema.enum) {
775
- result.enum = schema.enum;
776
- }
777
- if (schema.minimum !== void 0) {
778
- result.minimum = schema.minimum;
779
- }
780
- if (schema.maximum !== void 0) {
781
- result.maximum = schema.maximum;
782
- }
783
- if (schema.pattern) {
784
- result.pattern = schema.pattern;
785
- }
786
- if (schema.format) {
787
- result.format = schema.format;
788
- }
789
- if (schema.default !== void 0) {
790
- result.default = schema.default;
791
- }
792
- if (schema.type === "array" && schema.items) {
793
- result.items = this.convertOpenAPISchemaToParameterSchema(schema.items);
794
- }
795
- if (schema.type === "object" && schema.properties) {
796
- result.properties = {};
797
- for (const [propName, propSchema] of Object.entries(schema.properties)) {
798
- result.properties[propName] = this.convertOpenAPISchemaToParameterSchema(propSchema);
799
- }
800
- if (schema.required && schema.required.length > 0) {
801
- result.required = schema.required;
802
- }
803
- }
804
- return result;
805
- }
806
- /**
807
- * Map OpenAPI type to JSON schema type
808
- */
809
- mapOpenAPIType(type) {
810
- switch (type) {
811
- case "string":
812
- return "string";
813
- case "number":
814
- return "number";
815
- case "integer":
816
- return "integer";
817
- case "boolean":
818
- return "boolean";
819
- case "array":
820
- return "array";
821
- case "object":
822
- return "object";
823
- default:
824
- return "string";
825
- }
847
+ return createSchemaFromOperation(this.operationId, operation.operation);
826
848
  }
827
849
  };
828
850
  function createOpenAPITool(config) {
@@ -859,6 +881,11 @@ async function runBash(args) {
859
881
  const timer = setTimeout(() => {
860
882
  timedOut = true;
861
883
  child.kill("SIGTERM");
884
+ settle({
885
+ success: false,
886
+ output: Buffer.concat(stdoutChunks).toString("utf8"),
887
+ error: `Command timed out after ${timeout}ms`
888
+ });
862
889
  }, timeout);
863
890
  function settle(result) {
864
891
  if (settled) return;
@@ -1004,9 +1031,51 @@ var readTool = createZodFunctionTool(
1004
1031
  );
1005
1032
 
1006
1033
  // src/builtins/write-tool.ts
1034
+ var import_zod3 = require("zod");
1035
+
1036
+ // src/builtins/atomic-file-write.ts
1037
+ var import_node_crypto = require("crypto");
1007
1038
  var import_promises2 = require("fs/promises");
1008
1039
  var import_node_path = require("path");
1009
- var import_zod3 = require("zod");
1040
+ var TEMP_RANDOM_BYTES = 6;
1041
+ var PRESERVED_MODE_BITS = 4095;
1042
+ var MISSING_FILE_ERROR_CODE = "ENOENT";
1043
+ function createTempFilePath(filePath) {
1044
+ const dir = (0, import_node_path.dirname)(filePath);
1045
+ const name = (0, import_node_path.basename)(filePath);
1046
+ const suffix = (0, import_node_crypto.randomBytes)(TEMP_RANDOM_BYTES).toString("hex");
1047
+ return (0, import_node_path.join)(dir, `.${name}.robota-tmp-${process.pid}-${Date.now()}-${suffix}`);
1048
+ }
1049
+ async function readExistingMode(filePath) {
1050
+ try {
1051
+ const fileStats = await (0, import_promises2.stat)(filePath);
1052
+ return fileStats.mode & PRESERVED_MODE_BITS;
1053
+ } catch (error) {
1054
+ if (error instanceof Error && hasErrorCode(error, MISSING_FILE_ERROR_CODE)) return void 0;
1055
+ throw error;
1056
+ }
1057
+ }
1058
+ function hasErrorCode(error, code) {
1059
+ return "code" in error && error.code === code;
1060
+ }
1061
+ async function atomicWriteUtf8File(filePath, content) {
1062
+ const dir = (0, import_node_path.dirname)(filePath);
1063
+ await (0, import_promises2.mkdir)(dir, { recursive: true });
1064
+ const existingMode = await readExistingMode(filePath);
1065
+ const tempFilePath = createTempFilePath(filePath);
1066
+ try {
1067
+ await (0, import_promises2.writeFile)(tempFilePath, content, "utf8");
1068
+ if (existingMode !== void 0) {
1069
+ await (0, import_promises2.chmod)(tempFilePath, existingMode);
1070
+ }
1071
+ await (0, import_promises2.rename)(tempFilePath, filePath);
1072
+ } catch (error) {
1073
+ await (0, import_promises2.rm)(tempFilePath, { force: true }).catch(() => void 0);
1074
+ throw error;
1075
+ }
1076
+ }
1077
+
1078
+ // src/builtins/write-tool.ts
1010
1079
  var WriteSchema = import_zod3.z.object({
1011
1080
  filePath: import_zod3.z.string().describe("The absolute path to the file to write"),
1012
1081
  content: import_zod3.z.string().describe("The content to write to the file")
@@ -1014,8 +1083,7 @@ var WriteSchema = import_zod3.z.object({
1014
1083
  async function writeFileTool(args) {
1015
1084
  const { filePath, content } = args;
1016
1085
  try {
1017
- await (0, import_promises2.mkdir)((0, import_node_path.dirname)(filePath), { recursive: true });
1018
- await (0, import_promises2.writeFile)(filePath, content, "utf8");
1086
+ await atomicWriteUtf8File(filePath, content);
1019
1087
  const result = {
1020
1088
  success: true,
1021
1089
  output: `Written ${Buffer.byteLength(content, "utf8")} bytes to ${filePath}`
@@ -1086,7 +1154,7 @@ async function editFileTool(args) {
1086
1154
  }
1087
1155
  const updated = replaceAll ? content.split(oldString).join(newString) : content.slice(0, content.indexOf(oldString)) + newString + content.slice(content.indexOf(oldString) + oldString.length);
1088
1156
  try {
1089
- await (0, import_promises3.writeFile)(filePath, updated, "utf8");
1157
+ await atomicWriteUtf8File(filePath, updated);
1090
1158
  } catch (err) {
1091
1159
  const result2 = {
1092
1160
  success: false,
@@ -1096,9 +1164,12 @@ async function editFileTool(args) {
1096
1164
  return JSON.stringify(result2);
1097
1165
  }
1098
1166
  const count = replaceAll ? content.split(oldString).length - 1 : 1;
1167
+ const matchIdx = content.indexOf(oldString);
1168
+ const startLine = matchIdx >= 0 ? content.substring(0, matchIdx).split("\n").length : 1;
1099
1169
  const result = {
1100
1170
  success: true,
1101
- output: `Replaced ${count} occurrence(s) in ${filePath}`
1171
+ output: `Replaced ${count} occurrence(s) in ${filePath}`,
1172
+ startLine
1102
1173
  };
1103
1174
  return JSON.stringify(result);
1104
1175
  }