@myagentroam/agent 0.10.17 → 0.10.19

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.
@@ -27,10 +27,11 @@ export const codeModeToolDefinition = {
27
27
  - Single-tool cells are valid. Keep loops and retries bounded, and do not blindly replay operations with side effects.
28
28
  - Evaluates source in a fresh restricted ECMAScript runtime with top-level await and Promise.all. It is not Node.js or a browser. JavaScript globals do not survive between cells; bounded JSON state is available through state.
29
29
  - Accepts JavaScript source text. When exposed as a freeform tool, send the source directly; when exposed with a JSON input schema, put the source in its code field. Do not add another JSON wrapper, quoted string, or markdown code fence inside the source.
30
+ - Keep embedded commands, patches and source in JSON-escaped double-quoted strings when they contain backticks or literal \${...}; JavaScript template literals would interpolate them before the tool runs. Do not paste Python literals such as None into JavaScript expressions.
30
31
  - Use only standard ECMAScript built-ins and the explicitly documented tools, observe, state, catalog and cell namespaces. Do not use Node.js, browser or Web APIs. Module imports, filesystem and network APIs are unavailable. Obtain external capabilities only through available tools. Use tools.exec to start managed processes when permitted; use tools.exec_wait afterward to wait, write stdin or cancel.
31
32
 
32
33
  Tool calls:
33
- - tools.NAME(input) returns a Promise. Use the existing tool input schema; freeform tools such as apply_patch accept a raw string, not a JSON wrapper.
34
+ - tools.NAME(input) returns a Promise. Use the existing tool input schema; freeform tools such as apply_patch accept a raw string, not a JSON wrapper. For parameterless tools prefer tools.NAME({}); tools.NAME() is accepted only for explicitly closed empty-object schemas.
34
35
  - Results use the MAR envelope: { content?: string, data?: unknown, images?: Array<{mimeType:string,dataBase64:string}>, truncated?: boolean }. Redundant JSON content is omitted when it exactly renders data; independent text remains in content. Prefer data for programmatic results: result.data.results[0].text for read source text and result.data.exitCode/result.data.stdout for exec. content, when present, is rendered text, not raw file bytes. Use only fields documented by that tool; do not invent success or ok flags.
35
36
  - Tool errors reject with code/message and may be caught. A failed cell preserves already emitted output alongside its status and error; completed tool effects are not rolled back. Inspect it before retrying completed work. Await every call; unawaited calls fail the cell. Use Promise.all for independent calls and sequential await for dependencies. Runtime preserves safe/serial barriers and Host checks.
36
37
  - skill, tool_search, question, code_exec, code_exec_wait and direct-only tools cannot be called here. Load Skills directly and read their instructions before the next tool call. Use direct tool_search before a later code cell for deferred tools; the current cell's tools do not change.
@@ -224,10 +224,13 @@ export async function createCodeModeEngine(options) {
224
224
  return guestError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Code Mode state is too large.');
225
225
  };
226
226
  const tools = vm.newObject();
227
+ const parameterlessTools = new Set(input.catalog.filter(isParameterlessTool).map((tool) => tool.name));
227
228
  let toolCallSequence = 0;
228
229
  for (const name of input.tools) {
229
230
  method(tools, name, (args = vm.undefined) => {
230
- const parsed = jsonValue(args, true);
231
+ const parsed = vm.typeof(args) === 'undefined' && parameterlessTools.has(name)
232
+ ? { value: {} }
233
+ : jsonValue(args, true);
231
234
  if (parsed.error)
232
235
  return parsed;
233
236
  const operationId = ++toolCallSequence;
@@ -704,19 +707,64 @@ function compilerSourceLocation(error, source) {
704
707
  const lines = source.split('\n');
705
708
  const line = error.lineNumber - 1;
706
709
  if (line > lines.length)
707
- return ` (code-mode.js:${source.split(/\r\n|[\r\n\u2028\u2029]/u).length}, end of input)`;
710
+ return compilerCaret(source, source, true);
708
711
  const column = Number(typeof error.stack === 'string'
709
712
  ? error.stack.match(/^\s*at code-mode\.js:\d+:(\d+)(?:\n|$)/u)?.[1]
710
713
  : undefined);
711
- if (!Number.isSafeInteger(column) || column < 1)
712
- return '';
714
+ if (!Number.isSafeInteger(column) ||
715
+ column < 1 ||
716
+ column > Array.from(lines[line - 1]).length + 1) {
717
+ // Without a valid column, intra-LF-line separators make the original source line ambiguous.
718
+ if (/[\r\u2028\u2029]/u.test(lines[line - 1].replace(/\r$/u, '')))
719
+ return '';
720
+ const preceding = line > 1 ? lines.slice(0, line - 1).join('\n') + '\n' : '';
721
+ return ` (code-mode.js:${preceding.split(/\r\n|[\r\n\u2028\u2029]/u).length})`;
722
+ }
713
723
  const prefix = [
714
724
  ...lines.slice(0, line - 1),
715
725
  Array.from(lines[line - 1])
716
726
  .slice(0, column - 1)
717
727
  .join('')
718
728
  ].join('\n');
719
- return ` (code-mode.js:${prefix.split(/\r\n|[\r\n\u2028\u2029]/u).length})`;
729
+ return compilerCaret(source, prefix);
730
+ }
731
+ function compilerCaret(source, prefix, endOfInput = false) {
732
+ const segments = prefix.split(/\r\n|[\r\n\u2028\u2029]/u);
733
+ const line = segments.length;
734
+ const column = Array.from(segments.at(-1)).length + 1;
735
+ const character = source.slice(prefix.length)[Symbol.iterator]().next().value;
736
+ // Source may contain credentials, commands or private code. Show only a punctuation token.
737
+ const token = endOfInput
738
+ ? '<end of input>'
739
+ : character === undefined || /[\r\n\u2028\u2029]/u.test(character)
740
+ ? '<end of line>'
741
+ : /^[{}[\]():;,.=+*/?!<>|&%\-]$/u.test(character)
742
+ ? character
743
+ : '<character omitted>';
744
+ const preview = '<source omitted>';
745
+ return ` (code-mode.js:${line}${endOfInput ? ', end of input' : ''})\nSyntax location: line ${line}, column ${column}\n${line} | ${preview}${token}\n${' '.repeat(String(line).length)} | ${' '.repeat(preview.length)}^`;
746
+ }
747
+ function isParameterlessTool(tool) {
748
+ const schema = tool.inputSchema;
749
+ return (tool.inputMode === 'json' &&
750
+ schema?.type === 'object' &&
751
+ schema.additionalProperties === false &&
752
+ schema.properties !== null &&
753
+ typeof schema.properties === 'object' &&
754
+ !Array.isArray(schema.properties) &&
755
+ Object.keys(schema.properties).length === 0 &&
756
+ (schema.required === undefined ||
757
+ (Array.isArray(schema.required) && schema.required.length === 0)) &&
758
+ Object.keys(schema).every((key) => [
759
+ 'type',
760
+ 'properties',
761
+ 'additionalProperties',
762
+ 'required',
763
+ 'title',
764
+ 'description',
765
+ '$schema',
766
+ '$comment'
767
+ ].includes(key)));
720
768
  }
721
769
  function diagnostic(code, message) {
722
770
  return Object.assign(new Error(message), { code });
@@ -0,0 +1,25 @@
1
+ export type PatchHunk = {
2
+ changeContext?: string;
3
+ oldLines: string[];
4
+ patchLines: Array<{
5
+ kind: 'context' | 'addition' | 'deletion';
6
+ value: string;
7
+ }>;
8
+ addedLines: number;
9
+ deletedLines: number;
10
+ anchor: boolean;
11
+ };
12
+ export type PatchChange = {
13
+ kind: 'add';
14
+ path: string;
15
+ lines: string[];
16
+ } | {
17
+ kind: 'delete';
18
+ path: string;
19
+ } | {
20
+ kind: 'update';
21
+ path: string;
22
+ targetPath?: string;
23
+ hunks: PatchHunk[];
24
+ };
25
+ export declare function parsePatch(patch: string): PatchChange[];
@@ -0,0 +1,228 @@
1
+ import { isAbsolute, resolve } from 'node:path';
2
+ import { MarAgentError } from '../../error.js';
3
+ import { TOOL_EXECUTION_LIMITS } from '../execution-limits.js';
4
+ const beginMarker = '*** Begin Patch';
5
+ const endMarker = '*** End Patch';
6
+ const addHeader = '*** Add File: ';
7
+ const updateHeader = '*** Update File: ';
8
+ const deleteHeader = '*** Delete File: ';
9
+ const moveHeader = '*** Move to: ';
10
+ const syntaxTokens = [
11
+ `${beginMarker} ***`,
12
+ `${endMarker} ***`,
13
+ addHeader,
14
+ updateHeader,
15
+ deleteHeader,
16
+ moveHeader,
17
+ '@@ ',
18
+ '+',
19
+ '-',
20
+ ' '
21
+ ];
22
+ export function parsePatch(patch) {
23
+ if (typeof patch !== 'string')
24
+ throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Patch must be a string.');
25
+ if (Buffer.byteLength(patch) > TOOL_EXECUTION_LIMITS.patchMaxBytes)
26
+ throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Patch exceeds the input byte limit.');
27
+ return new FreeformPatchParser(patch).parse();
28
+ }
29
+ class FreeformPatchParser {
30
+ #lines;
31
+ #lineOffsets = [];
32
+ #claimed = new Set();
33
+ #index = 1;
34
+ constructor(patch) {
35
+ const sourceLines = patch.split('\n');
36
+ let offset = 0;
37
+ this.#lines = sourceLines.map((line, index) => {
38
+ this.#lineOffsets.push(offset);
39
+ offset += line.length + 1;
40
+ return index < sourceLines.length - 1 && line.endsWith('\r') ? line.slice(0, -1) : line;
41
+ });
42
+ if (patch.endsWith('\n'))
43
+ this.#lines.pop();
44
+ }
45
+ parse() {
46
+ for (const [index, line] of this.#lines.entries()) {
47
+ const column = line.indexOf('\r');
48
+ if (column !== -1)
49
+ this.#fail('Patch contains unsupported bare carriage returns.', index, column);
50
+ }
51
+ this.#marker(0, beginMarker, 'first');
52
+ this.#marker(this.#lines.length - 1, endMarker, 'last');
53
+ if (this.#lines.length < 3)
54
+ this.#fail('Patch must contain at least one operation.', 1);
55
+ const changes = [];
56
+ while (this.#index < this.#lines.length - 1) {
57
+ const headerIndex = this.#index++;
58
+ const header = this.#lines[headerIndex];
59
+ if (header.startsWith(addHeader)) {
60
+ const path = this.#path(headerIndex, addHeader);
61
+ const lines = [];
62
+ while (this.#hasBodyLine()) {
63
+ const index = this.#index++;
64
+ const line = this.#lines[index];
65
+ if (!line.startsWith('+'))
66
+ this.#fail('Added file lines must start with +.', index);
67
+ lines.push(line.slice(1));
68
+ }
69
+ changes.push({ kind: 'add', path, lines });
70
+ }
71
+ else if (header.startsWith(deleteHeader)) {
72
+ changes.push({ kind: 'delete', path: this.#path(headerIndex, deleteHeader) });
73
+ }
74
+ else if (header.startsWith(updateHeader)) {
75
+ const path = this.#path(headerIndex, updateHeader);
76
+ let targetPath;
77
+ if (this.#lines[this.#index]?.startsWith(moveHeader)) {
78
+ targetPath = this.#path(this.#index++, moveHeader);
79
+ }
80
+ const hunks = [];
81
+ while (this.#index < this.#lines.length - 1 && this.#lines[this.#index].startsWith('@@'))
82
+ hunks.push(this.#hunk());
83
+ if (!hunks.length && !targetPath)
84
+ this.#fail('Update operations must contain hunks or move a file.', headerIndex);
85
+ changes.push({ kind: 'update', path, ...(targetPath ? { targetPath } : {}), hunks });
86
+ }
87
+ else {
88
+ this.#fail("Expected an operation header: '*** Add File: <path>', '*** Update File: <path>', or '*** Delete File: <path>'.", headerIndex, Math.max(...[addHeader, updateHeader, deleteHeader].map((prefix) => commonPrefixLength(header, prefix))));
89
+ }
90
+ if (changes.length > TOOL_EXECUTION_LIMITS.patchMaxFiles)
91
+ throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Patch exceeds the file limit.');
92
+ }
93
+ if (!changes.some((change) => change.kind !== 'update' || change.targetPath || change.hunks.some((hunk) => !hunk.anchor)))
94
+ this.#fail('Patch must change or move at least one file. No additions, deletions, or moves were supplied; context-only hunks only validate existing lines. Use read to inspect a file, or supply -old/+new lines to edit it.', 1);
95
+ return changes;
96
+ }
97
+ #marker(index, marker, position) {
98
+ const line = this.#lines[index];
99
+ const trimmed = line.trim();
100
+ if (trimmed === marker || trimmed === `${marker} ***`)
101
+ return;
102
+ const leading = line.length - line.trimStart().length;
103
+ const expected = trimmed.startsWith(`${marker} ***`) ? `${marker} ***` : marker;
104
+ this.#fail(`Expected ${position} line to be '${marker}' (optional trailing ' ***').`, index, leading + commonPrefixLength(trimmed, expected));
105
+ }
106
+ #path(index, prefix) {
107
+ const path = this.#lines[index].slice(prefix.length);
108
+ if (!path || path.trim() !== path) {
109
+ const column = path.trimStart() !== path ? prefix.length : prefix.length + path.trimEnd().length;
110
+ this.#fail('Patch paths must be non-empty and unpadded.', index, column);
111
+ }
112
+ const key = isAbsolute(path) ? resolve(path) : resolve('/', path);
113
+ if (this.#claimed.has(key))
114
+ this.#fail('Patch contains duplicate or overlapping paths.', index, prefix.length);
115
+ this.#claimed.add(key);
116
+ return path;
117
+ }
118
+ #hasBodyLine() {
119
+ return this.#index < this.#lines.length - 1 && !this.#lines[this.#index].startsWith('*** ');
120
+ }
121
+ #hunk() {
122
+ const index = this.#index++;
123
+ const header = this.#lines[index];
124
+ let changeContext;
125
+ if (header !== '@@') {
126
+ if (!header.startsWith('@@ '))
127
+ this.#fail('Patch change context headers must use @@ or @@ <context>.', index, 2);
128
+ changeContext = header.slice(3);
129
+ if (!changeContext.trim())
130
+ this.#fail('Patch change context must be non-empty.', index, 3);
131
+ }
132
+ const oldLines = [];
133
+ const patchLines = [];
134
+ let addedLines = 0;
135
+ let deletedLines = 0;
136
+ while (this.#hasBodyLine() && !this.#lines[this.#index].startsWith('@@')) {
137
+ const lineIndex = this.#index++;
138
+ const line = this.#lines[lineIndex];
139
+ const value = line.slice(1);
140
+ if (line.startsWith(' ')) {
141
+ oldLines.push(value);
142
+ patchLines.push({ kind: 'context', value });
143
+ }
144
+ else if (line.startsWith('+')) {
145
+ patchLines.push({ kind: 'addition', value });
146
+ addedLines++;
147
+ }
148
+ else if (line.startsWith('-')) {
149
+ oldLines.push(value);
150
+ patchLines.push({ kind: 'deletion', value });
151
+ deletedLines++;
152
+ }
153
+ else
154
+ this.#fail('Patch hunk lines must start with space, +, or -.', lineIndex);
155
+ }
156
+ if (!oldLines.length)
157
+ this.#fail('Insertion hunks require context.', index);
158
+ return {
159
+ ...(changeContext === undefined ? {} : { changeContext }),
160
+ oldLines,
161
+ patchLines,
162
+ addedLines,
163
+ deletedLines,
164
+ anchor: !addedLines && !deletedLines
165
+ };
166
+ }
167
+ #fail(message, index, characterOffset = 0) {
168
+ const source = this.#lines[index];
169
+ const line = index + 1;
170
+ const column = characterCount(source.slice(0, characterOffset)) + 1;
171
+ const offset = this.#lineOffsets[index] + characterOffset;
172
+ const preview = syntaxPreview(source, characterOffset);
173
+ const gutter = String(line);
174
+ throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', `Patch syntax error at line ${line}, column ${column}: ${message}\n${gutter} | ${preview.text}\n${' '.repeat(gutter.length)} | ${' '.repeat(preview.caret)}^`, { details: { line, column, offset } });
175
+ }
176
+ }
177
+ function commonPrefixLength(actual, expected) {
178
+ let index = 0;
179
+ while (index < actual.length && index < expected.length && actual[index] === expected[index])
180
+ index++;
181
+ return index;
182
+ }
183
+ function characterCount(value) {
184
+ let count = 0;
185
+ for (const _character of value)
186
+ count++;
187
+ return count;
188
+ }
189
+ function syntaxPreview(source, characterOffset) {
190
+ // Only grammar tokens and one offending character are public; paths and file bodies are not.
191
+ const trimmed = source.trimStart();
192
+ const leading = source.length - trimmed.length;
193
+ const tokenLength = Math.max(...syntaxTokens.map((token) => commonPrefixLength(trimmed, token)));
194
+ const token = trimmed.slice(0, tokenLength);
195
+ if (characterOffset < leading) {
196
+ const before = characterOffset > 40 ? '… ' : displaySyntax(source.slice(0, characterOffset));
197
+ return {
198
+ text: `${before}${displayCharacter(source[characterOffset])}<content omitted>`,
199
+ caret: before.length
200
+ };
201
+ }
202
+ const prefix = leading > 40 ? '… ' : displaySyntax(source.slice(0, leading));
203
+ const syntaxEnd = leading + tokenLength;
204
+ if (characterOffset < syntaxEnd) {
205
+ return {
206
+ text: `${prefix}${token}${source.length > syntaxEnd ? '<content omitted>' : ''}`,
207
+ caret: prefix.length + characterOffset - leading
208
+ };
209
+ }
210
+ const gap = characterOffset > syntaxEnd ? '<content omitted>' : '';
211
+ const character = source.slice(characterOffset)[Symbol.iterator]().next().value;
212
+ const before = `${prefix}${token}${gap}`;
213
+ return {
214
+ text: `${before}${character === undefined ? '<end of line>' : displayCharacter(character)}${character !== undefined && source.length > characterOffset + character.length ? '<content omitted>' : ''}`,
215
+ caret: before.length
216
+ };
217
+ }
218
+ function displayCharacter(character) {
219
+ const point = character.codePointAt(0);
220
+ if (point >= 0x7f)
221
+ return `\\u{${point.toString(16)}}`;
222
+ return JSON.stringify(character).slice(1, -1);
223
+ }
224
+ function displaySyntax(value) {
225
+ return [...value]
226
+ .map((character) => (character === '\t' ? ' ' : displayCharacter(character)))
227
+ .join('');
228
+ }
@@ -1,15 +1,16 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { chmod, lstat, mkdir, readFile, rename, rm, rmdir, writeFile } from 'node:fs/promises';
3
- import { dirname, isAbsolute, parse, resolve } from 'node:path';
3
+ import { dirname, parse, resolve } from 'node:path';
4
4
  import { TextDecoder } from 'node:util';
5
5
  import { MarAgentError } from '../../error.js';
6
6
  import { TOOL_EXECUTION_LIMITS } from '../execution-limits.js';
7
7
  import { captureFileChange, reserveFileChangePaths } from '../result-extensions.js';
8
+ import { parsePatch } from './apply-patch-parser.js';
8
9
  export const applyPatchToolDefinition = {
9
10
  name: 'apply_patch',
10
11
  allowedCallers: ['direct', 'programmatic'],
11
12
  parallelSafety: 'serial',
12
- description: `Preferred for precise, reviewable local edits. Do not batch all task files merely to reduce tool calls. Group only a few tightly coupled edits whose combined patch is quick to generate and review; split broad or independent work into successive bounded patches before composing them. Submit a raw *** Begin Patch / *** End Patch patch. Supports Add File, Update File, Delete File, and Move to. Add File may replace an existing regular file, and Move to may replace an existing regular destination; duplicate operations on the same resolved path remain invalid. Update hunks accept @@ or @@ <change context>. Context-only hunks, including trailing hunks, and unchanged file sections are validated in order but are not written or reported as edits. The patch must include a change or move. Context is searched forward with Codex-style exact, whitespace-tolerant, then common Unicode punctuation-tolerant matching. Syntax and resolved paths are validated before writing, then file units are committed in order. Each file unit is atomic, and a Move keeps its source and destination atomic. On MAR_AGENT_PATCH_PARTIAL, earlier files remain applied; the error lists applied, failed, and pending paths, so retry only failed and pending paths. Existing file permissions, line endings, and final-newline state are preserved. Returns changed file paths and operations with per-file and total line counts when all units succeed. In Code Mode, the Promise resolves on success and rejects with code/message on failure; the returned envelope has data.files and data.totals, not an ok or success flag. Oversized patches are rejected without advertising defensive limits as recommended patch sizes. A first-unit MAR_AGENT_PATCH_CONFLICT applies nothing; hunk match conflicts identify the affected path and hunk so you can reread only that file before retrying.`,
13
+ description: `Preferred for precise, reviewable local edits. Do not batch all task files merely to reduce tool calls. Group only a few tightly coupled edits whose combined patch is quick to generate and review; split broad or independent work into successive bounded patches before composing them. Submit a raw *** Begin Patch / *** End Patch patch, each marker on its own line. Surrounding marker whitespace and an optional trailing *** are accepted; incomplete markers or extra text are rejected. Add File content lines, including blank lines, must start with +. Syntax errors include line/column and a bounded caret diagnostic with file content omitted. Supports Add File, Update File, Delete File, and Move to. Add File may replace an existing regular file, and Move to may replace an existing regular destination; duplicate operations on the same resolved path remain invalid. Update hunks accept @@ or @@ <change context>. Context-only hunks, including trailing hunks, and unchanged file sections are validated in order but are not written or reported as edits. The patch must include a change or move. Context is searched forward with Codex-style exact, whitespace-tolerant, then common Unicode punctuation-tolerant matching. Syntax and resolved paths are validated before writing, then file units are committed in order. Each file unit is atomic, and a Move keeps its source and destination atomic. On MAR_AGENT_PATCH_PARTIAL, earlier files remain applied; the error lists applied, failed, and pending paths, so retry only failed and pending paths. Existing file permissions, line endings, and final-newline state are preserved. Returns changed file paths and operations with per-file and total line counts when all units succeed. In Code Mode, the Promise resolves on success and rejects with code/message on failure; the returned envelope has data.files and data.totals, not an ok or success flag. Oversized patches are rejected without advertising defensive limits as recommended patch sizes. A first-unit MAR_AGENT_PATCH_CONFLICT applies nothing; hunk match conflicts identify the affected path and hunk so you can reread only that file before retrying.`,
13
14
  inputMode: 'freeform',
14
15
  outputSchema: {
15
16
  type: 'object',
@@ -306,121 +307,6 @@ export class ApplyPatchTool {
306
307
  await captureFileChange(mutation.path, mutation.original, mutation.content);
307
308
  }
308
309
  }
309
- function parsePatch(patch) {
310
- if (typeof patch !== 'string')
311
- invalid('Patch must be a string.');
312
- if (Buffer.byteLength(patch) > TOOL_EXECUTION_LIMITS.patchMaxBytes)
313
- throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Patch exceeds the input byte limit.');
314
- const normalized = patch.replaceAll('\r\n', '\n');
315
- if (normalized.includes('\r'))
316
- invalid('Patch contains unsupported bare carriage returns.');
317
- const value = normalized.endsWith('\n') ? normalized.slice(0, -1) : normalized;
318
- if (!value.startsWith('*** Begin Patch\n') || !value.endsWith('\n*** End Patch'))
319
- invalid('Patch envelope is invalid.');
320
- const body = value.slice('*** Begin Patch\n'.length, -'\n*** End Patch'.length);
321
- if (!body)
322
- invalid('Patch must contain at least one operation.');
323
- const lines = body.split('\n');
324
- const changes = [];
325
- const claimed = new Set();
326
- let index = 0;
327
- const claim = (path) => {
328
- if (!path || path.trim() !== path)
329
- invalid('Patch paths must be non-empty and unpadded.');
330
- const key = isAbsolute(path) ? resolve(path) : resolve('/', path);
331
- if (claimed.has(key))
332
- invalid('Patch contains duplicate or overlapping paths.');
333
- claimed.add(key);
334
- };
335
- while (index < lines.length) {
336
- const header = lines[index++];
337
- if (header.startsWith('*** Add File: ')) {
338
- const path = header.slice('*** Add File: '.length);
339
- claim(path);
340
- const content = [];
341
- while (index < lines.length && !lines[index].startsWith('*** ')) {
342
- const line = lines[index++];
343
- if (!line.startsWith('+'))
344
- invalid('Added file lines must start with +.');
345
- content.push(line.slice(1));
346
- }
347
- changes.push({ kind: 'add', path, lines: content });
348
- }
349
- else if (header.startsWith('*** Delete File: ')) {
350
- const path = header.slice('*** Delete File: '.length);
351
- claim(path);
352
- changes.push({ kind: 'delete', path });
353
- }
354
- else if (header.startsWith('*** Update File: ')) {
355
- const path = header.slice('*** Update File: '.length);
356
- claim(path);
357
- let targetPath;
358
- if (lines[index]?.startsWith('*** Move to: ')) {
359
- targetPath = lines[index++].slice('*** Move to: '.length);
360
- claim(targetPath);
361
- }
362
- const hunks = [];
363
- while (index < lines.length && lines[index].startsWith('@@')) {
364
- const hunkHeader = lines[index++];
365
- let changeContext;
366
- if (hunkHeader !== '@@') {
367
- if (!hunkHeader.startsWith('@@ '))
368
- invalid('Patch change context headers must use @@ or @@ <context>.');
369
- changeContext = hunkHeader.slice(3);
370
- if (!changeContext.trim())
371
- invalid('Patch change context must be non-empty.');
372
- }
373
- const oldLines = [];
374
- const patchLines = [];
375
- let addedLines = 0;
376
- let deletedLines = 0;
377
- while (index < lines.length &&
378
- !lines[index].startsWith('@@') &&
379
- !lines[index].startsWith('*** ')) {
380
- const line = lines[index++];
381
- if (line.startsWith(' ')) {
382
- const value = line.slice(1);
383
- oldLines.push(value);
384
- patchLines.push({ kind: 'context', value });
385
- }
386
- else if (line.startsWith('+')) {
387
- const value = line.slice(1);
388
- patchLines.push({ kind: 'addition', value });
389
- addedLines++;
390
- }
391
- else if (line.startsWith('-')) {
392
- const value = line.slice(1);
393
- oldLines.push(value);
394
- patchLines.push({ kind: 'deletion', value });
395
- deletedLines++;
396
- }
397
- else
398
- invalid('Patch hunk lines must start with space, +, or -.');
399
- }
400
- if (!oldLines.length)
401
- invalid('Insertion hunks require context.');
402
- hunks.push({
403
- ...(changeContext === undefined ? {} : { changeContext }),
404
- oldLines,
405
- patchLines,
406
- addedLines,
407
- deletedLines,
408
- anchor: !addedLines && !deletedLines
409
- });
410
- }
411
- if (!hunks.length && !targetPath)
412
- invalid('Update operations must contain hunks or move a file.');
413
- changes.push({ kind: 'update', path, ...(targetPath ? { targetPath } : {}), hunks });
414
- }
415
- else
416
- invalid('Patch operation is unsupported.');
417
- if (changes.length > TOOL_EXECUTION_LIMITS.patchMaxFiles)
418
- throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Patch exceeds the file limit.');
419
- }
420
- if (!changes.some((change) => change.kind !== 'update' || change.targetPath || change.hunks.some((hunk) => !hunk.anchor)))
421
- invalid('Patch must change or move at least one file.');
422
- return changes;
423
- }
424
310
  function applyHunks(original, hunks, path) {
425
311
  let text;
426
312
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.10.17",
3
+ "version": "0.10.19",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",