newmark-agent 0.6.7 → 0.6.8
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/dist/cli-commands.js +1 -1
- package/dist/conversation-utility-host.bundle.cjs +2060 -1166
- package/dist/core/agent.js +2 -0
- package/dist/core/agentKernelRunner.js +4 -0
- package/dist/core/computerUseSession.js +1 -1
- package/dist/core/fileWriteObservation.d.ts +41 -0
- package/dist/core/fileWriteObservation.js +454 -0
- package/dist/core/toolPolicy.d.ts +1 -1
- package/dist/core/toolPolicy.js +1 -1
- package/dist/tools/computerUse.d.ts +2 -0
- package/dist/tools/computerUse.js +206 -19
- package/dist/tools/index.d.ts +3 -4
- package/dist/tools/index.js +144 -67
- package/dist/wsl-agent-host.bundle.cjs +2060 -1166
- package/package.json +5 -3
package/dist/tools/index.js
CHANGED
|
@@ -64,6 +64,7 @@ const localOcr_1 = require("../core/localOcr");
|
|
|
64
64
|
const visualTextFallback_1 = require("../core/visualTextFallback");
|
|
65
65
|
const computerUseSession_1 = require("../core/computerUseSession");
|
|
66
66
|
const searchMcpPool_1 = require("../core/searchMcpPool");
|
|
67
|
+
const fileWriteObservation_1 = require("../core/fileWriteObservation");
|
|
67
68
|
function normalizeComputerUseAction(action) {
|
|
68
69
|
return String(action || '').trim().toLowerCase();
|
|
69
70
|
}
|
|
@@ -279,6 +280,7 @@ class ToolExecutor {
|
|
|
279
280
|
localOcr;
|
|
280
281
|
argumentValidators = new toolArgumentValidator_1.ToolArgumentValidatorRegistry();
|
|
281
282
|
searchMcpPool;
|
|
283
|
+
fileWriteObservationGate = new fileWriteObservation_1.FileWriteObservationGate();
|
|
282
284
|
hostProfile = {
|
|
283
285
|
kind: 'desktop',
|
|
284
286
|
platform: process.platform,
|
|
@@ -351,15 +353,15 @@ class ToolExecutor {
|
|
|
351
353
|
const tools = [
|
|
352
354
|
t('bash', `${shellDescription} Optional timeout_ms lets the Agent choose this command timeout in milliseconds; 0 requests no limit, but a nonzero terminal.interrupt_timeout_ms setting is the upper cap.`, { command: { type: 'string' }, timeout_ms: { type: 'number', description: 'Per-command timeout in milliseconds. 0 means no requested limit unless capped by settings.' } }, ['command']),
|
|
353
355
|
t('pwd', 'Print working directory (current folder path)', {}, []),
|
|
354
|
-
t('read', 'Read file contents. Use ABSOLUTE paths.
|
|
355
|
-
t('write', 'Write/create a file.
|
|
356
|
-
t('edit', 'Edit file with find-and-replace.
|
|
357
|
-
t('delete_file', 'Delete ONE file
|
|
356
|
+
t('read', 'Read file contents. Use ABSOLUTE paths. A complete read or explicit line range records a short-lived, conversation/workspace/run-scoped observation for write/edit/delete_file. Read an exact target path before changing it. Large files can be read with start_line/end_line; overwrite/delete requires full line coverage, while edit requires the exact old_str lines.', { path: { type: 'string' }, start_line: { type: 'integer', minimum: 1, description: 'Optional inclusive first line for a bounded read. Provide together with end_line.' }, end_line: { type: 'integer', minimum: 1, description: 'Optional inclusive last line for a bounded read. Provide together with start_line.' } }, ['path']),
|
|
357
|
+
t('write', 'Write/create a file only after the exact target path has been read in this conversation, workspace, actor, and Build run. Existing files require a complete read; new files require an exact-path absence observation. Changed targets are refused until read again.', { path: { type: 'string' }, content: { type: 'string' } }, ['path', 'content']),
|
|
358
|
+
t('edit', 'Edit a file with find-and-replace only after reading the exact target lines in this conversation, workspace, actor, and Build run. The old_str must have appeared in a successful read range; stale or ambiguous matches are refused.', { path: { type: 'string' }, old_str: { type: 'string' }, new_str: { type: 'string' } }, ['path', 'old_str', 'new_str']),
|
|
359
|
+
t('delete_file', 'Delete ONE file only after reading its complete contents at the exact target path in this conversation, workspace, actor, and Build run. This tool refuses directory deletion and wildcard paths; delete files one by one. Never use bash rm/del/Remove-Item for batch (recursive/wildcard/loop/pipe/multi-target) deletion — the runtime hard-blocks such commands.', { path: { type: 'string' } }, ['path']),
|
|
358
360
|
t('glob', 'Find files by glob pattern (e.g. **/*.ts, src/**/*.html)', { pattern: { type: 'string' } }, ['pattern']),
|
|
359
361
|
t('grep', 'Search file content with regex', { pattern: { type: 'string' }, path: { type: 'string' } }, ['pattern', 'path']),
|
|
360
362
|
t('web_search', 'Search the web', { query: { type: 'string' } }, ['query']),
|
|
361
363
|
t('web_fetch', 'Fetch and extract URL content', { url: { type: 'string' } }, ['url']),
|
|
362
|
-
t('web_catch', 'Download a public web resource, save a webpage, or capture one page component into an exact workspace destination. Advanced Build/Goal tool; unavailable in Plan and Chat. This writes only the requested local output and never changes the remote page.', {
|
|
364
|
+
t('web_catch', 'Download a public web resource, save a webpage, or capture one page component into an exact workspace destination. Read the exact destination first; existing files require a complete read and new files require an exact-path absence observation. Advanced Build/Goal tool; unavailable in Plan and Chat. This writes only the requested local output and never changes the remote page.', {
|
|
363
365
|
action: { type: 'string', enum: ['download', 'save_page', 'capture_component'], description: 'download saves response bytes; save_page stores the HTML response; capture_component selects one CSS element and saves its src/href asset, or its outer HTML when no asset attribute exists.' },
|
|
364
366
|
url: { type: 'string', description: 'Public http:// or https:// URL.' },
|
|
365
367
|
destination: { type: 'string', description: 'Exact output file path. Relative paths resolve inside the active workspace.' },
|
|
@@ -405,23 +407,25 @@ class ToolExecutor {
|
|
|
405
407
|
capture_max_height: { type: 'number', minimum: 240, maximum: 2048, description: 'Maximum ephemeral screenshot height; defaults to 960. Aspect ratio is preserved and the source is never enlarged.' },
|
|
406
408
|
max_chars: { type: 'number', minimum: 1000, maximum: 100000, description: 'Maximum returned semantic UI text characters.' },
|
|
407
409
|
}, []),
|
|
408
|
-
t('computer_use', 'Native Windows desktop control with persistent observation/action helpers. Use observe/app_observe before acting. For passive observe, sparse_wait_ms can wait up to 5 seconds on a coarse local pixel-change signal without a local perception model; after a signal or timeout it
|
|
409
|
-
action: { type: 'string', enum: ['observe', 'app_list', 'app_observe', 'sequence', 'takeover_start', 'takeover_stop', 'move', 'click', 'scroll', 'type', 'key', 'wait', 'app_activate', 'app_click', 'app_scroll', 'app_type', 'app_key'] },
|
|
410
|
+
t('computer_use', 'Native Windows desktop control with persistent observation/action helpers. Use observe/app_observe before acting. Set include_ui_tree=false for a fast screenshot-only observation; the default includes a bounded UI Automation tree for semantic targets. wait_for polls one visible app window for text, a target_id from the latest observation, or window availability. A match refreshes the semantic target cache; inspect the returned element before acting. For passive observe, sparse_wait_ms can wait up to 5 seconds on a coarse local pixel-change signal without a local perception model; after a signal or timeout it captures a fresh screenshot and the requested UI Automation data. This low-resolution signal can miss small or brief changes and does not prove that no change occurred. sequence may perform up to three stable low-risk move/click/scroll/wait/app_activate steps and stops when focus, window, menu, dialog, scene, or risk changes. Vision screenshots remain one-time inputs and are deleted immediately.', {
|
|
411
|
+
action: { type: 'string', enum: ['observe', 'app_list', 'app_observe', 'wait_for', 'sequence', 'takeover_start', 'takeover_stop', 'move', 'click', 'scroll', 'type', 'key', 'wait', 'app_activate', 'app_click', 'app_scroll', 'app_type', 'app_key'] },
|
|
410
412
|
capture_max_width: { type: 'number', minimum: 320, maximum: 2048, description: 'For observe/app_observe only. Maximum ephemeral screenshot width; defaults to 1280. Aspect ratio is preserved and the source is never enlarged.' },
|
|
411
413
|
capture_max_height: { type: 'number', minimum: 240, maximum: 2048, description: 'For observe/app_observe only. Maximum ephemeral screenshot height; defaults to 960. Aspect ratio is preserved and the source is never enlarged.' },
|
|
412
|
-
|
|
414
|
+
include_ui_tree: { type: 'boolean', description: 'For observe/app_observe only. Defaults to true. Set false to skip UI Automation traversal and return a screenshot-only observation; prior semantic target IDs are invalidated.' },
|
|
415
|
+
sparse_wait_ms: { type: 'integer', minimum: 0, maximum: 5000, description: 'Optional for action=observe only; defaults to 0 (immediate capture). Wait this many milliseconds for a coarse 32×18 grayscale change signal with no local neural model, then capture a fresh screenshot and the UI Automation tree when include_ui_tree=true, whether signaled or timed out. Small/transient changes can be missed.' },
|
|
413
416
|
x: { type: 'number' },
|
|
414
417
|
y: { type: 'number' },
|
|
415
|
-
target_id: { type: 'string', description: 'Stable id from the latest observe perception.objects entry. Preferred over raw coordinates when available.' },
|
|
418
|
+
target_id: { type: 'string', description: 'Stable id from the latest observe perception.objects entry. Preferred over raw coordinates when available; for wait_for it must come from the latest semantic observation.' },
|
|
416
419
|
app_target: { type: 'string', description: 'Application title, process name, or process id for app-scoped observation and actions.' },
|
|
417
420
|
window_handle: { type: 'string', description: 'Native window handle from app_list/app_observe for exact app-scoped actions.' },
|
|
418
421
|
max_chars: { type: 'number', minimum: 1000, maximum: 100000, description: 'Maximum returned text characters for observation and application listings.' },
|
|
419
422
|
scroll_x: { type: 'number', description: 'Horizontal scroll delta for action=scroll.' },
|
|
420
423
|
scroll_y: { type: 'number', description: 'Vertical scroll delta for action=scroll. Positive scrolls down.' },
|
|
421
424
|
button: { type: 'string', enum: ['left', 'right'] },
|
|
422
|
-
text: { type: 'string' },
|
|
425
|
+
text: { type: 'string', description: 'Text to type, or for wait_for a case-insensitive substring of a visible UI Automation Name or AutomationId.' },
|
|
423
426
|
key: { type: 'string' },
|
|
424
427
|
duration_ms: { type: 'number' },
|
|
428
|
+
timeout_ms: { type: 'integer', minimum: 250, maximum: 30000, description: 'For wait_for only. Polling budget in milliseconds; defaults to 5000 and is capped at 30000. A UI Automation call already in flight can add bounded completion latency.' },
|
|
425
429
|
dry_run: { type: 'boolean' },
|
|
426
430
|
include_raw_ui: { type: 'boolean', description: 'Debug only. Include the raw UI Automation element list; normal observation returns compact semantic objects.' },
|
|
427
431
|
steps: {
|
|
@@ -651,14 +655,18 @@ class ToolExecutor {
|
|
|
651
655
|
if (tool.function?.name !== 'computer_use')
|
|
652
656
|
return tool;
|
|
653
657
|
const copy = JSON.parse(JSON.stringify(tool));
|
|
654
|
-
copy.function.description = 'Plan read-only desktop: observe, app_list, app_observe only.';
|
|
658
|
+
copy.function.description = 'Plan read-only desktop: observe, app_list, app_observe, wait_for only.';
|
|
655
659
|
copy.function.parameters.properties = {
|
|
656
660
|
action: { type: 'string', enum: [...toolPolicy_1.PLAN_COMPUTER_USE_ACTIONS] },
|
|
657
661
|
app_target: copy.function.parameters.properties.app_target,
|
|
658
662
|
window_handle: copy.function.parameters.properties.window_handle,
|
|
663
|
+
target_id: copy.function.parameters.properties.target_id,
|
|
664
|
+
text: copy.function.parameters.properties.text,
|
|
659
665
|
max_chars: copy.function.parameters.properties.max_chars,
|
|
660
666
|
capture_max_width: copy.function.parameters.properties.capture_max_width,
|
|
661
667
|
capture_max_height: copy.function.parameters.properties.capture_max_height,
|
|
668
|
+
include_ui_tree: copy.function.parameters.properties.include_ui_tree,
|
|
669
|
+
timeout_ms: copy.function.parameters.properties.timeout_ms,
|
|
662
670
|
include_raw_ui: copy.function.parameters.properties.include_raw_ui,
|
|
663
671
|
};
|
|
664
672
|
return copy;
|
|
@@ -788,15 +796,99 @@ class ToolExecutor {
|
|
|
788
796
|
switch (tool) {
|
|
789
797
|
case 'bash': return await this.bash(g('command'), wsPath, args.timeout_ms, context.signal);
|
|
790
798
|
case 'pwd': return `Current directory: ${wsPath}`;
|
|
791
|
-
case 'read':
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
799
|
+
case 'read':
|
|
800
|
+
return await this.fileWriteObservationGate.read(resolve(g('path')), this.fileWriteObservationScope(context, wsPath), {
|
|
801
|
+
startLine: args.start_line === undefined ? undefined : Number(args.start_line),
|
|
802
|
+
endLine: args.end_line === undefined ? undefined : Number(args.end_line),
|
|
803
|
+
});
|
|
804
|
+
case 'write': {
|
|
805
|
+
const target = resolve(g('path'));
|
|
806
|
+
const mutation = await this.fileWriteObservationGate.mutate(target, this.fileWriteObservationScope(context, wsPath), { kind: 'full' }, gate => {
|
|
807
|
+
const denied = this.checkWorkspaceAccess('write', gate.canonicalPath, context.workspacePath || wsPath);
|
|
808
|
+
if (denied)
|
|
809
|
+
return { value: denied, committed: false };
|
|
810
|
+
if (gate.exists) {
|
|
811
|
+
gate.assertBeforeCommit();
|
|
812
|
+
fs.writeFileSync(target, g('content'), 'utf-8');
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
816
|
+
gate.assertBeforeCommit();
|
|
817
|
+
fs.writeFileSync(target, g('content'), { encoding: 'utf-8', flag: 'wx' });
|
|
818
|
+
}
|
|
819
|
+
return { value: '[write] OK: ' + target, committed: true };
|
|
820
|
+
});
|
|
821
|
+
return mutation.ok ? mutation.value : mutation.error;
|
|
822
|
+
}
|
|
823
|
+
case 'edit': {
|
|
824
|
+
const target = resolve(g('path'));
|
|
825
|
+
const oldText = g('old_str');
|
|
826
|
+
const replacement = g('new_str');
|
|
827
|
+
const mutation = await this.fileWriteObservationGate.mutate(target, this.fileWriteObservationScope(context, wsPath), { kind: 'edit', oldText }, gate => {
|
|
828
|
+
const denied = this.checkWorkspaceAccess('edit', gate.canonicalPath, context.workspacePath || wsPath);
|
|
829
|
+
if (denied)
|
|
830
|
+
return { value: denied, committed: false };
|
|
831
|
+
if (!gate.exists || gate.text === undefined)
|
|
832
|
+
return { value: '[edit] Target file does not exist: ' + target, committed: false };
|
|
833
|
+
const first = gate.text.indexOf(oldText);
|
|
834
|
+
if (first < 0)
|
|
835
|
+
return { value: '[edit] String not found in ' + target + '.', committed: false };
|
|
836
|
+
if (gate.text.indexOf(oldText, first + oldText.length) >= 0) {
|
|
837
|
+
return { value: '[edit] Refused: old_str matches more than one position in ' + target + '; read a more specific context and retry.', committed: false };
|
|
838
|
+
}
|
|
839
|
+
gate.assertBeforeCommit();
|
|
840
|
+
const updated = gate.text.slice(0, first) + replacement + gate.text.slice(first + oldText.length);
|
|
841
|
+
fs.writeFileSync(target, updated, 'utf-8');
|
|
842
|
+
return { value: '[edit] OK: ' + target, committed: true };
|
|
843
|
+
});
|
|
844
|
+
return mutation.ok ? mutation.value : mutation.error;
|
|
845
|
+
}
|
|
846
|
+
case 'delete_file': {
|
|
847
|
+
const target = resolve(g('path'));
|
|
848
|
+
if (/[*?]/.test(target))
|
|
849
|
+
return '[delete_file] Refused: wildcard paths are not allowed. Delete one file per call.';
|
|
850
|
+
try {
|
|
851
|
+
if (fs.lstatSync(target).isDirectory()) {
|
|
852
|
+
return '[delete_file] Refused: deleting a directory is not allowed. Delete files one by one under Agent supervision.';
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
catch (error) {
|
|
856
|
+
if (error?.code !== 'ENOENT') {
|
|
857
|
+
return '[delete_file] ' + (error instanceof Error ? error.message : String(error));
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const mutation = await this.fileWriteObservationGate.mutate(target, this.fileWriteObservationScope(context, wsPath), { kind: 'full' }, gate => {
|
|
861
|
+
const denied = this.checkWorkspaceAccess('delete_file', gate.canonicalPath, context.workspacePath || wsPath);
|
|
862
|
+
if (denied)
|
|
863
|
+
return { value: denied, committed: false };
|
|
864
|
+
if (!gate.exists)
|
|
865
|
+
return { value: '[delete_file] Target does not exist: ' + target, committed: false };
|
|
866
|
+
gate.assertBeforeCommit();
|
|
867
|
+
fs.unlinkSync(target);
|
|
868
|
+
return { value: '[delete_file] OK: ' + target, committed: true };
|
|
869
|
+
});
|
|
870
|
+
return mutation.ok ? mutation.value : mutation.error;
|
|
871
|
+
}
|
|
795
872
|
case 'glob': return this.glob(g('pattern'), wsPath);
|
|
796
873
|
case 'grep': return this.grep(g('pattern'), resolve(g('path')));
|
|
797
874
|
case 'web_search': return await this.wsearch(g('query'), context.signal);
|
|
798
875
|
case 'web_fetch': return await this.wfetch(g('url'), context.signal);
|
|
799
|
-
case 'web_catch':
|
|
876
|
+
case 'web_catch': {
|
|
877
|
+
const destination = resolve(g('destination'));
|
|
878
|
+
const mutation = await this.fileWriteObservationGate.mutate(destination, this.fileWriteObservationScope(context, wsPath), { kind: 'full' }, async (gate) => {
|
|
879
|
+
const denied = this.checkWorkspaceAccess('web_catch', gate.canonicalPath, context.workspacePath || wsPath);
|
|
880
|
+
if (denied)
|
|
881
|
+
return { value: denied, committed: false };
|
|
882
|
+
const value = await this.wcatch(args, destination, context.signal, () => gate.assertBeforeCommit());
|
|
883
|
+
let committed = false;
|
|
884
|
+
try {
|
|
885
|
+
committed = JSON.parse(value).ok === true;
|
|
886
|
+
}
|
|
887
|
+
catch { }
|
|
888
|
+
return { value, committed };
|
|
889
|
+
});
|
|
890
|
+
return mutation.ok ? mutation.value : mutation.error;
|
|
891
|
+
}
|
|
800
892
|
case 'browser_open': return await this.browserRun({ action: 'open', url: g('url') }, context.signal, context, wsPath);
|
|
801
893
|
case 'browser_snapshot': return await this.browserRun({ action: 'snapshot', maxChars: Number(args.max_chars || 12000) }, context.signal, context, wsPath);
|
|
802
894
|
case 'browser_click': return await this.browserRun({ action: 'click', selector: g('selector') }, context.signal, context, wsPath);
|
|
@@ -1000,6 +1092,14 @@ class ToolExecutor {
|
|
|
1000
1092
|
}
|
|
1001
1093
|
case 'computer_use': {
|
|
1002
1094
|
const action = normalizeComputerUseAction(g('action'));
|
|
1095
|
+
if (process.env.NEWMARK_TEST_CU_OBSERVE_ONLY === '1'
|
|
1096
|
+
&& !['observe', 'app_observe', 'app_list', 'wait_for'].includes(action)) {
|
|
1097
|
+
return JSON.stringify({
|
|
1098
|
+
ok: false,
|
|
1099
|
+
action,
|
|
1100
|
+
error: '[computer_use test guard] The live-model smoke only permits read-only desktop observation.',
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1003
1103
|
const owner = `${computerUseOwner(context, wsPath)}:${String(context.actorId || 'root')}`;
|
|
1004
1104
|
const lockGuard = action === 'takeover_stop'
|
|
1005
1105
|
? assertComputerUseLockOwner(action, owner, context, wsPath)
|
|
@@ -1065,6 +1165,8 @@ class ToolExecutor {
|
|
|
1065
1165
|
captureMaxWidth: Number(args.capture_max_width),
|
|
1066
1166
|
captureMaxHeight: Number(args.capture_max_height),
|
|
1067
1167
|
sparseWaitMs: Number(args.sparse_wait_ms),
|
|
1168
|
+
includeUiTree: args.include_ui_tree !== false,
|
|
1169
|
+
timeoutMs: Number(args.timeout_ms),
|
|
1068
1170
|
signal: context.signal,
|
|
1069
1171
|
gradientColors: Array.isArray(args.gradient_colors) ? args.gradient_colors : undefined,
|
|
1070
1172
|
gradientSpeed: args.gradient_speed !== undefined ? Number(args.gradient_speed) : undefined,
|
|
@@ -1302,6 +1404,18 @@ class ToolExecutor {
|
|
|
1302
1404
|
const action = policy === 'ask_user' ? 'User approval required' : 'Denied';
|
|
1303
1405
|
return `[permission] ${action}: ${tool} cannot access outside workspace (${target}).`;
|
|
1304
1406
|
}
|
|
1407
|
+
fileWriteObservationScope(context, wsPath) {
|
|
1408
|
+
const workspacePath = path.resolve(context.workspacePath || wsPath || this.root);
|
|
1409
|
+
return JSON.stringify({
|
|
1410
|
+
runtimeKey: String(context.runtimeKey || process.env.NEWMARK_RUNTIME_KEY || ''),
|
|
1411
|
+
runId: String(context.runId || ''),
|
|
1412
|
+
workspaceId: String(context.workspaceId || workspacePath),
|
|
1413
|
+
workspacePath,
|
|
1414
|
+
conversationId: String(context.conversationId || 'default'),
|
|
1415
|
+
actorId: String(context.actorId || terminalTakeover_1.ROOT_TERMINAL_ACTOR_ID),
|
|
1416
|
+
backend: String(context.backend || (process.env.NEWMARK_WSL_DISTRO ? 'wsl' : process.platform)),
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1305
1419
|
checkBashWorkspaceAccess(command, wsPath) {
|
|
1306
1420
|
const perm = this.config.getStr('workspace', 'access_permission');
|
|
1307
1421
|
if (perm === 'full_access')
|
|
@@ -1389,54 +1503,6 @@ class ToolExecutor {
|
|
|
1389
1503
|
return `[bash] ${e instanceof Error ? e.message : String(e)}`;
|
|
1390
1504
|
}
|
|
1391
1505
|
}
|
|
1392
|
-
fread(p) {
|
|
1393
|
-
try {
|
|
1394
|
-
const c = fs.readFileSync(p, 'utf-8');
|
|
1395
|
-
return c.length > 30000 ? c.slice(0, 30000) + '...\n[truncated]' : c;
|
|
1396
|
-
}
|
|
1397
|
-
catch (e) {
|
|
1398
|
-
return `[read] ${e}`;
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
fwrite(p, content) {
|
|
1402
|
-
try {
|
|
1403
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
1404
|
-
fs.writeFileSync(p, content, 'utf-8');
|
|
1405
|
-
return `[write] OK: ${p}`;
|
|
1406
|
-
}
|
|
1407
|
-
catch (e) {
|
|
1408
|
-
return `[write] ${e}`;
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
fedit(p, oldStr, newStr) {
|
|
1412
|
-
try {
|
|
1413
|
-
const c = fs.readFileSync(p, 'utf-8');
|
|
1414
|
-
if (!c.includes(oldStr))
|
|
1415
|
-
return `[edit] String not found in ${p}.`;
|
|
1416
|
-
const updated = c.replace(oldStr, newStr);
|
|
1417
|
-
fs.writeFileSync(p, updated, 'utf-8');
|
|
1418
|
-
return `[edit] OK: ${p}`;
|
|
1419
|
-
}
|
|
1420
|
-
catch (e) {
|
|
1421
|
-
return `[edit] ${e}`;
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
fdelete(p) {
|
|
1425
|
-
try {
|
|
1426
|
-
if (/[*?]/.test(p))
|
|
1427
|
-
return '[delete_file] Refused: wildcard paths are not allowed. Delete one file per call.';
|
|
1428
|
-
const resolved = path.resolve(p);
|
|
1429
|
-
const stat = fs.lstatSync(resolved);
|
|
1430
|
-
if (stat.isDirectory()) {
|
|
1431
|
-
return '[delete_file] Refused: deleting a directory is not allowed. Delete files one by one under Agent supervision.';
|
|
1432
|
-
}
|
|
1433
|
-
fs.unlinkSync(resolved);
|
|
1434
|
-
return `[delete_file] OK: ${resolved}`;
|
|
1435
|
-
}
|
|
1436
|
-
catch (e) {
|
|
1437
|
-
return `[delete_file] ${e instanceof Error ? e.message : String(e)}`;
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
1506
|
glob(pattern, ws) {
|
|
1441
1507
|
try {
|
|
1442
1508
|
const results = globSync(pattern, {
|
|
@@ -1888,7 +1954,7 @@ class ToolExecutor {
|
|
|
1888
1954
|
guard.dispose();
|
|
1889
1955
|
}
|
|
1890
1956
|
}
|
|
1891
|
-
async wcatch(args, destination, signal) {
|
|
1957
|
+
async wcatch(args, destination, signal, beforeCommit) {
|
|
1892
1958
|
const action = String(args.action || 'download');
|
|
1893
1959
|
const maxBytes = Math.max(1, Math.min(100 * 1024 * 1024, Number(args.max_bytes || 50 * 1024 * 1024)));
|
|
1894
1960
|
const sourceUrl = this.checkedPublicWebUrl(String(args.url || ''));
|
|
@@ -1932,9 +1998,20 @@ class ToolExecutor {
|
|
|
1932
1998
|
const backup = destinationExists ? `${destination}.newmark-${crypto.randomUUID()}.bak` : '';
|
|
1933
1999
|
try {
|
|
1934
2000
|
fs.writeFileSync(temp, bytes, { flag: 'wx' });
|
|
1935
|
-
|
|
2001
|
+
beforeCommit?.();
|
|
2002
|
+
const currentDestinationExists = fs.existsSync(destination);
|
|
2003
|
+
if (currentDestinationExists !== destinationExists)
|
|
2004
|
+
throw new Error('Destination changed during download; read it again and retry.');
|
|
2005
|
+
if (currentDestinationExists && !fs.statSync(destination).isFile())
|
|
2006
|
+
throw new Error('Destination changed to a non-file during download.');
|
|
2007
|
+
if (destinationExists) {
|
|
1936
2008
|
fs.renameSync(destination, backup);
|
|
1937
|
-
|
|
2009
|
+
fs.renameSync(temp, destination);
|
|
2010
|
+
}
|
|
2011
|
+
else {
|
|
2012
|
+
fs.linkSync(temp, destination);
|
|
2013
|
+
fs.unlinkSync(temp);
|
|
2014
|
+
}
|
|
1938
2015
|
if (backup)
|
|
1939
2016
|
fs.unlinkSync(backup);
|
|
1940
2017
|
}
|