newmark-agent 0.6.4 → 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/config.example.json +13 -3
- package/dist/cli-commands.d.ts +1 -1
- package/dist/cli-commands.js +54 -3
- package/dist/cli-discovery.js +3 -0
- package/dist/conversation-utility-host.bundle.cjs +4472 -1767
- package/dist/conversation-utility-host.js +94 -7
- package/dist/core/agent.d.ts +126 -12
- package/dist/core/agent.js +785 -172
- package/dist/core/agentKernelRunner.d.ts +7 -1
- package/dist/core/agentKernelRunner.js +160 -20
- package/dist/core/autoRouter.d.ts +49 -44
- package/dist/core/autoRouter.js +117 -302
- package/dist/core/computerUseSession.js +1 -1
- package/dist/core/config.js +27 -11
- package/dist/core/continuation/contracts.d.ts +1 -1
- package/dist/core/continuation/store.d.ts +91 -1
- package/dist/core/continuation/store.js +291 -61
- package/dist/core/conversationKernel.d.ts +49 -7
- package/dist/core/conversationKernel.js +303 -51
- package/dist/core/conversationStateDocument.d.ts +18 -0
- package/dist/core/conversationStateDocument.js +88 -0
- package/dist/core/conversationVisualBudget.d.ts +74 -0
- package/dist/core/conversationVisualBudget.js +153 -0
- package/dist/core/electronUtilityAgentClient.d.ts +3 -2
- package/dist/core/electronUtilityAgentClient.js +32 -8
- package/dist/core/electronUtilityRuntimePool.d.ts +28 -3
- package/dist/core/electronUtilityRuntimePool.js +93 -24
- package/dist/core/fileWriteObservation.d.ts +41 -0
- package/dist/core/fileWriteObservation.js +454 -0
- package/dist/core/hostRuntimeHooks.d.ts +23 -0
- package/dist/core/hostRuntimeHooks.js +17 -0
- package/dist/core/jevDecision.d.ts +144 -0
- package/dist/core/jevDecision.js +226 -0
- package/dist/core/memoryProbe.d.ts +24 -0
- package/dist/core/memoryProbe.js +104 -0
- package/dist/core/mobilePairing.d.ts +7 -1
- package/dist/core/mobilePairing.js +9 -1
- package/dist/core/performanceDiagnostics.d.ts +1 -1
- package/dist/core/routeDecisionValidator.d.ts +86 -0
- package/dist/core/routeDecisionValidator.js +249 -0
- package/dist/core/routeEligibility.d.ts +52 -0
- package/dist/core/routeEligibility.js +108 -0
- package/dist/core/runtimeMemoryBudget.d.ts +6 -0
- package/dist/core/runtimeMemoryBudget.js +12 -0
- package/dist/core/toolPolicy.d.ts +1 -1
- package/dist/core/toolPolicy.js +1 -1
- package/dist/core/utilityAgentProtocol.d.ts +8 -10
- package/dist/core/utilityHostToolRouter.js +2 -0
- package/dist/core/visualDownscale.d.ts +6 -0
- package/dist/core/visualDownscale.js +137 -0
- package/dist/core/wslAgentClient.d.ts +1 -2
- package/dist/core/wslAgentClient.js +0 -8
- package/dist/core/wslAgentProtocol.d.ts +1 -10
- package/dist/core/wslAgentRuntimePool.d.ts +1 -3
- package/dist/core/wslAgentRuntimePool.js +17 -22
- package/dist/llm/provider.d.ts +3 -1
- package/dist/llm/provider.js +53 -9
- package/dist/main.js +107 -30
- package/dist/preload.js +3 -1
- package/dist/server.js +30 -18
- package/dist/tools/computerUse.d.ts +6 -0
- package/dist/tools/computerUse.js +406 -21
- package/dist/tools/computerUsePowerShellHost.d.ts +6 -1
- package/dist/tools/computerUsePowerShellHost.js +105 -31
- package/dist/tools/index.d.ts +3 -4
- package/dist/tools/index.js +146 -66
- package/dist/tui/src/adapters/core-runtime-adapter.js +13 -1
- package/dist/tui/src/i18n.js +12 -1
- package/dist/tui/src/render.js +9 -2
- package/dist/tui/src/settings-schema.js +19 -0
- package/dist/tui/src/state.js +69 -1
- package/dist/ui/index.html +651 -233
- package/dist/ui/lucide-sprite.svg +0 -8
- package/dist/wsl-agent-host.bundle.cjs +4302 -1767
- package/dist/wsl-agent-host.js +0 -3
- package/package.json +39 -10
|
@@ -35,48 +35,90 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.runPersistentPowerShell = runPersistentPowerShell;
|
|
37
37
|
exports.computerUsePowerShellLaneReady = computerUsePowerShellLaneReady;
|
|
38
|
+
exports.prepareComputerUsePowerShellLane = prepareComputerUsePowerShellLane;
|
|
39
|
+
exports.stopComputerUsePowerShellLane = stopComputerUsePowerShellLane;
|
|
38
40
|
exports.stopComputerUsePowerShellHost = stopComputerUsePowerShellHost;
|
|
39
41
|
const child_process_1 = require("child_process");
|
|
40
42
|
const crypto = __importStar(require("crypto"));
|
|
41
43
|
const readline = __importStar(require("readline"));
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
].
|
|
44
|
+
function createHostScript(includeUiAutomation) {
|
|
45
|
+
return [
|
|
46
|
+
'[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)',
|
|
47
|
+
'[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)',
|
|
48
|
+
'$OutputEncoding = [System.Text.UTF8Encoding]::new($false)',
|
|
49
|
+
'Add-Type -AssemblyName System.Windows.Forms',
|
|
50
|
+
'Add-Type -AssemblyName System.Drawing',
|
|
51
|
+
...(includeUiAutomation ? ['Add-Type -AssemblyName UIAutomationClient', 'Add-Type -AssemblyName UIAutomationTypes'] : []),
|
|
52
|
+
...(includeUiAutomation ? [`if (-not ("NewmarkComputerUseNative" -as [type])) { Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public static class NewmarkComputerUseNative { [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y); [DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, int dwData, UIntPtr dwExtraInfo); [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); }' }`] : []),
|
|
53
|
+
...(!includeUiAutomation ? [`if (-not ("NewmarkSparseCapture" -as [type])) { Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public static class NewmarkSparseCapture { [StructLayout(LayoutKind.Sequential)] struct BitmapInfoHeader { public uint Size; public int Width; public int Height; public ushort Planes; public ushort BitCount; public uint Compression; public uint SizeImage; public int XPelsPerMeter; public int YPelsPerMeter; public uint ColorsUsed; public uint ColorsImportant; } [StructLayout(LayoutKind.Sequential)] struct BitmapInfo { public BitmapInfoHeader Header; public uint Colors; } [DllImport("user32.dll")] static extern IntPtr GetDC(IntPtr hWnd); [DllImport("user32.dll")] static extern int ReleaseDC(IntPtr hWnd, IntPtr hdc); [DllImport("gdi32.dll")] static extern IntPtr CreateCompatibleDC(IntPtr hdc); [DllImport("gdi32.dll")] static extern bool DeleteDC(IntPtr hdc); [DllImport("gdi32.dll")] static extern IntPtr CreateDIBSection(IntPtr hdc, ref BitmapInfo info, uint usage, out IntPtr bits, IntPtr section, uint offset); [DllImport("gdi32.dll")] static extern IntPtr SelectObject(IntPtr hdc, IntPtr obj); [DllImport("gdi32.dll")] static extern bool DeleteObject(IntPtr obj); [DllImport("gdi32.dll")] static extern int SetStretchBltMode(IntPtr hdc, int mode); [DllImport("gdi32.dll")] static extern bool SetBrushOrgEx(IntPtr hdc, int x, int y, IntPtr oldOrigin); [DllImport("gdi32.dll", SetLastError=true)] static extern bool StretchBlt(IntPtr dst, int dx, int dy, int dw, int dh, IntPtr src, int sx, int sy, int sw, int sh, int operation); [DllImport("gdi32.dll")] static extern bool GdiFlush(); public sealed class Capture : IDisposable { private int width; private byte[] pixels; internal Capture(int w, byte[] p) { width=w; pixels=p; } public byte GetLuma(int x, int y) { int offset=(y*width+x)*4; return (byte)Math.Round((pixels[offset+2]*0.299)+(pixels[offset+1]*0.587)+(pixels[offset]*0.114)); } public void Dispose() { pixels=null; } } public static Capture CaptureScaled(int x, int y, int width, int height, int targetWidth, int targetHeight) { IntPtr screen=GetDC(IntPtr.Zero); if (screen == IntPtr.Zero) throw new InvalidOperationException("Could not read desktop DC."); IntPtr memory=IntPtr.Zero; IntPtr bitmap=IntPtr.Zero; IntPtr previous=IntPtr.Zero; IntPtr bits=IntPtr.Zero; try { memory=CreateCompatibleDC(screen); BitmapInfo info=new BitmapInfo(); info.Header.Size=(uint)System.Runtime.InteropServices.Marshal.SizeOf(typeof(BitmapInfoHeader)); info.Header.Width=targetWidth; info.Header.Height=-targetHeight; info.Header.Planes=1; info.Header.BitCount=32; info.Header.Compression=0; info.Header.SizeImage=(uint)(targetWidth*targetHeight*4); bitmap=CreateDIBSection(screen,ref info,0,out bits,IntPtr.Zero,0); if (memory == IntPtr.Zero || bitmap == IntPtr.Zero || bits == IntPtr.Zero) throw new InvalidOperationException("Could not allocate sparse DIB section."); previous=SelectObject(memory,bitmap); if (previous == IntPtr.Zero) throw new InvalidOperationException("Could not select sparse DIB section."); if (SetStretchBltMode(memory,4)==0 || !SetBrushOrgEx(memory,0,0,IntPtr.Zero)) throw new InvalidOperationException("Could not configure sparse capture scaling."); if (!StretchBlt(memory,0,0,targetWidth,targetHeight,screen,x,y,width,height,0x00CC0020)) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); if (!GdiFlush()) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); byte[] pixels=new byte[targetWidth*targetHeight*4]; Marshal.Copy(bits,pixels,0,pixels.Length); return new Capture(targetWidth,pixels); } finally { if (memory != IntPtr.Zero && previous != IntPtr.Zero) SelectObject(memory,previous); if (bitmap != IntPtr.Zero) DeleteObject(bitmap); if (memory != IntPtr.Zero) DeleteDC(memory); ReleaseDC(IntPtr.Zero,screen); } } }' }`] : []),
|
|
54
|
+
'$hostReady = @{ id="__newmark_ready__"; ready=$true }; [Console]::Out.WriteLine(($hostReady | ConvertTo-Json -Compress)); [Console]::Out.Flush()',
|
|
55
|
+
'while (($line = [Console]::In.ReadLine()) -ne $null) {',
|
|
56
|
+
' $id = ""',
|
|
57
|
+
' $timer = [System.Diagnostics.Stopwatch]::StartNew()',
|
|
58
|
+
' try {',
|
|
59
|
+
' $request = $line | ConvertFrom-Json',
|
|
60
|
+
' $id = [string]$request.id',
|
|
61
|
+
' $source = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String([string]$request.script))',
|
|
62
|
+
' $previousPreference = $ErrorActionPreference',
|
|
63
|
+
' $ErrorActionPreference = "Stop"',
|
|
64
|
+
' try { $output = (& ([scriptblock]::Create($source)) 2>&1 | Out-String -Width 1048576).Trim() } finally { $ErrorActionPreference = $previousPreference }',
|
|
65
|
+
' $response = @{ id=$id; ok=$true; output=$output; elapsed_ms=[int]$timer.ElapsedMilliseconds }',
|
|
66
|
+
' } catch {',
|
|
67
|
+
' $response = @{ id=$id; ok=$false; output=[string]$_; elapsed_ms=[int]$timer.ElapsedMilliseconds }',
|
|
68
|
+
' }',
|
|
69
|
+
' [Console]::Out.WriteLine(($response | ConvertTo-Json -Compress -Depth 6))',
|
|
70
|
+
' [Console]::Out.Flush()',
|
|
71
|
+
'}',
|
|
72
|
+
].join('; ');
|
|
73
|
+
}
|
|
74
|
+
const HOST_SCRIPT = createHostScript(true);
|
|
75
|
+
const SPARSE_HOST_SCRIPT = createHostScript(false);
|
|
69
76
|
class PowerShellWorker {
|
|
70
77
|
lane;
|
|
71
78
|
child = null;
|
|
79
|
+
readyChild = null;
|
|
72
80
|
pending = new Map();
|
|
81
|
+
readinessWaiters = new Set();
|
|
73
82
|
restarting = false;
|
|
74
83
|
unavailableUntil = 0;
|
|
75
84
|
constructor(lane) {
|
|
76
85
|
this.lane = lane;
|
|
77
86
|
}
|
|
78
87
|
ready() {
|
|
79
|
-
return Date.now() >= this.unavailableUntil
|
|
88
|
+
return Date.now() >= this.unavailableUntil
|
|
89
|
+
&& !!this.child
|
|
90
|
+
&& !this.child.killed
|
|
91
|
+
&& this.child.exitCode === null
|
|
92
|
+
&& this.readyChild === this.child;
|
|
93
|
+
}
|
|
94
|
+
async prepare(timeoutMs) {
|
|
95
|
+
const startedAt = Date.now();
|
|
96
|
+
if (Date.now() < this.unavailableUntil)
|
|
97
|
+
return { ready: false, elapsedMs: 0 };
|
|
98
|
+
const child = this.ensureChild();
|
|
99
|
+
if (this.readyChild === child)
|
|
100
|
+
return { ready: true, elapsedMs: Date.now() - startedAt };
|
|
101
|
+
const ready = await new Promise(resolve => {
|
|
102
|
+
let settled = false;
|
|
103
|
+
let waiter;
|
|
104
|
+
const finish = (value) => {
|
|
105
|
+
if (settled)
|
|
106
|
+
return;
|
|
107
|
+
settled = true;
|
|
108
|
+
clearTimeout(waiter.timer);
|
|
109
|
+
this.readinessWaiters.delete(waiter);
|
|
110
|
+
resolve(value);
|
|
111
|
+
};
|
|
112
|
+
waiter = {
|
|
113
|
+
child,
|
|
114
|
+
resolve: finish,
|
|
115
|
+
timer: setTimeout(() => finish(false), Math.max(0, Math.floor(timeoutMs))),
|
|
116
|
+
};
|
|
117
|
+
this.readinessWaiters.add(waiter);
|
|
118
|
+
if (this.readyChild === child)
|
|
119
|
+
finish(true);
|
|
120
|
+
});
|
|
121
|
+
return { ready, elapsedMs: Date.now() - startedAt };
|
|
80
122
|
}
|
|
81
123
|
async run(script, timeoutMs) {
|
|
82
124
|
const startedAt = Date.now();
|
|
@@ -114,6 +156,9 @@ class PowerShellWorker {
|
|
|
114
156
|
stop() {
|
|
115
157
|
const child = this.child;
|
|
116
158
|
this.child = null;
|
|
159
|
+
this.readyChild = null;
|
|
160
|
+
if (child)
|
|
161
|
+
this.resolveReadiness(child, false);
|
|
117
162
|
if (child && !child.killed)
|
|
118
163
|
child.kill();
|
|
119
164
|
this.rejectPending(`Computer Use ${this.lane} helper stopped.`);
|
|
@@ -154,14 +199,14 @@ class PowerShellWorker {
|
|
|
154
199
|
'-ExecutionPolicy',
|
|
155
200
|
'Bypass',
|
|
156
201
|
'-Command',
|
|
157
|
-
HOST_SCRIPT,
|
|
202
|
+
this.lane === 'sparse' ? SPARSE_HOST_SCRIPT : HOST_SCRIPT,
|
|
158
203
|
], {
|
|
159
204
|
windowsHide: true,
|
|
160
205
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
161
206
|
});
|
|
162
207
|
child.stdin.setDefaultEncoding('utf8');
|
|
163
208
|
const lines = readline.createInterface({ input: child.stdout });
|
|
164
|
-
lines.on('line', line => this.handleLine(line));
|
|
209
|
+
lines.on('line', line => this.handleLine(line, child));
|
|
165
210
|
let stderr = '';
|
|
166
211
|
child.stderr.on('data', chunk => {
|
|
167
212
|
stderr = `${stderr}${String(chunk)}`.slice(-8192);
|
|
@@ -170,6 +215,8 @@ class PowerShellWorker {
|
|
|
170
215
|
if (this.child !== child)
|
|
171
216
|
return;
|
|
172
217
|
this.child = null;
|
|
218
|
+
this.readyChild = null;
|
|
219
|
+
this.resolveReadiness(child, false);
|
|
173
220
|
this.rejectPending(error.message);
|
|
174
221
|
});
|
|
175
222
|
child.once('exit', (code, signal) => {
|
|
@@ -177,12 +224,14 @@ class PowerShellWorker {
|
|
|
177
224
|
if (this.child !== child)
|
|
178
225
|
return;
|
|
179
226
|
this.child = null;
|
|
227
|
+
this.readyChild = null;
|
|
228
|
+
this.resolveReadiness(child, false);
|
|
180
229
|
this.rejectPending(`Computer Use ${this.lane} helper exited (${code ?? signal ?? 'unknown'}).${stderr ? ` ${stderr.trim()}` : ''}`);
|
|
181
230
|
});
|
|
182
231
|
this.child = child;
|
|
183
232
|
return child;
|
|
184
233
|
}
|
|
185
|
-
handleLine(line) {
|
|
234
|
+
handleLine(line, child) {
|
|
186
235
|
let parsed;
|
|
187
236
|
try {
|
|
188
237
|
parsed = JSON.parse(line);
|
|
@@ -190,6 +239,13 @@ class PowerShellWorker {
|
|
|
190
239
|
catch {
|
|
191
240
|
return;
|
|
192
241
|
}
|
|
242
|
+
if (String(parsed.id || '') === '__newmark_ready__' && parsed.ready === true) {
|
|
243
|
+
if (this.child === child) {
|
|
244
|
+
this.readyChild = child;
|
|
245
|
+
this.resolveReadiness(child, true);
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
193
249
|
const id = String(parsed.id || '');
|
|
194
250
|
const pending = this.pending.get(id);
|
|
195
251
|
if (!pending)
|
|
@@ -209,6 +265,12 @@ class PowerShellWorker {
|
|
|
209
265
|
}
|
|
210
266
|
this.pending.clear();
|
|
211
267
|
}
|
|
268
|
+
resolveReadiness(child, ready) {
|
|
269
|
+
for (const waiter of [...this.readinessWaiters]) {
|
|
270
|
+
if (waiter.child === child)
|
|
271
|
+
waiter.resolve(ready);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
212
274
|
}
|
|
213
275
|
const workers = {
|
|
214
276
|
action: new PowerShellWorker('action'),
|
|
@@ -216,6 +278,7 @@ const workers = {
|
|
|
216
278
|
windows: new PowerShellWorker('windows'),
|
|
217
279
|
uia_advisory: new PowerShellWorker('uia_advisory'),
|
|
218
280
|
windows_advisory: new PowerShellWorker('windows_advisory'),
|
|
281
|
+
sparse: new PowerShellWorker('sparse'),
|
|
219
282
|
};
|
|
220
283
|
let cleanupRegistered = false;
|
|
221
284
|
function registerCleanup() {
|
|
@@ -228,6 +291,7 @@ function registerCleanup() {
|
|
|
228
291
|
workers.windows.stop();
|
|
229
292
|
workers.uia_advisory.stop();
|
|
230
293
|
workers.windows_advisory.stop();
|
|
294
|
+
workers.sparse.stop();
|
|
231
295
|
};
|
|
232
296
|
process.once('exit', stop);
|
|
233
297
|
}
|
|
@@ -241,11 +305,21 @@ async function runPersistentPowerShell(script, timeoutMs = 30000, lane = 'action
|
|
|
241
305
|
function computerUsePowerShellLaneReady(lane) {
|
|
242
306
|
return process.platform === 'win32' && workers[lane].ready();
|
|
243
307
|
}
|
|
308
|
+
async function prepareComputerUsePowerShellLane(lane, timeoutMs) {
|
|
309
|
+
if (process.platform !== 'win32')
|
|
310
|
+
return { ready: false, elapsedMs: 0 };
|
|
311
|
+
registerCleanup();
|
|
312
|
+
return await workers[lane].prepare(timeoutMs);
|
|
313
|
+
}
|
|
314
|
+
function stopComputerUsePowerShellLane(lane) {
|
|
315
|
+
workers[lane].stop();
|
|
316
|
+
}
|
|
244
317
|
function stopComputerUsePowerShellHost() {
|
|
245
318
|
workers.action.stop();
|
|
246
319
|
workers.uia.stop();
|
|
247
320
|
workers.windows.stop();
|
|
248
321
|
workers.uia_advisory.stop();
|
|
249
322
|
workers.windows_advisory.stop();
|
|
323
|
+
workers.sparse.stop();
|
|
250
324
|
}
|
|
251
325
|
//# sourceMappingURL=computerUsePowerShellHost.js.map
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface ToolExecutionContext {
|
|
|
16
16
|
actorId?: string;
|
|
17
17
|
workspaceId?: string;
|
|
18
18
|
runtimeKey?: string;
|
|
19
|
+
runId?: string;
|
|
19
20
|
backend?: string;
|
|
20
21
|
invocation?: 'agent' | 'cli';
|
|
21
22
|
signal?: AbortSignal;
|
|
@@ -42,6 +43,7 @@ export declare class ToolExecutor {
|
|
|
42
43
|
private readonly localOcr;
|
|
43
44
|
private readonly argumentValidators;
|
|
44
45
|
private readonly searchMcpPool;
|
|
46
|
+
private readonly fileWriteObservationGate;
|
|
45
47
|
private hostProfile;
|
|
46
48
|
constructor(root: string, config: ConfigManager, ssh?: SshManager | undefined, workspace?: WorkspaceManager | undefined);
|
|
47
49
|
webSearch(query: string, signal?: AbortSignal): Promise<string>;
|
|
@@ -72,16 +74,13 @@ export declare class ToolExecutor {
|
|
|
72
74
|
private decorateBrowserUseReceipt;
|
|
73
75
|
private sshWorkspace;
|
|
74
76
|
private checkWorkspaceAccess;
|
|
77
|
+
private fileWriteObservationScope;
|
|
75
78
|
private checkBashWorkspaceAccess;
|
|
76
79
|
private extractCommandPathRefs;
|
|
77
80
|
private looksLikePath;
|
|
78
81
|
private isReadonlyShellCommand;
|
|
79
82
|
private resolveBashTimeout;
|
|
80
83
|
private bash;
|
|
81
|
-
private fread;
|
|
82
|
-
private fwrite;
|
|
83
|
-
private fedit;
|
|
84
|
-
private fdelete;
|
|
85
84
|
private glob;
|
|
86
85
|
private grep;
|
|
87
86
|
private createProxyAgent;
|
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,22 +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. 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.', {
|
|
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.' },
|
|
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.' },
|
|
412
416
|
x: { type: 'number' },
|
|
413
417
|
y: { type: 'number' },
|
|
414
|
-
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.' },
|
|
415
419
|
app_target: { type: 'string', description: 'Application title, process name, or process id for app-scoped observation and actions.' },
|
|
416
420
|
window_handle: { type: 'string', description: 'Native window handle from app_list/app_observe for exact app-scoped actions.' },
|
|
417
421
|
max_chars: { type: 'number', minimum: 1000, maximum: 100000, description: 'Maximum returned text characters for observation and application listings.' },
|
|
418
422
|
scroll_x: { type: 'number', description: 'Horizontal scroll delta for action=scroll.' },
|
|
419
423
|
scroll_y: { type: 'number', description: 'Vertical scroll delta for action=scroll. Positive scrolls down.' },
|
|
420
424
|
button: { type: 'string', enum: ['left', 'right'] },
|
|
421
|
-
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.' },
|
|
422
426
|
key: { type: 'string' },
|
|
423
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.' },
|
|
424
429
|
dry_run: { type: 'boolean' },
|
|
425
430
|
include_raw_ui: { type: 'boolean', description: 'Debug only. Include the raw UI Automation element list; normal observation returns compact semantic objects.' },
|
|
426
431
|
steps: {
|
|
@@ -650,14 +655,18 @@ class ToolExecutor {
|
|
|
650
655
|
if (tool.function?.name !== 'computer_use')
|
|
651
656
|
return tool;
|
|
652
657
|
const copy = JSON.parse(JSON.stringify(tool));
|
|
653
|
-
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.';
|
|
654
659
|
copy.function.parameters.properties = {
|
|
655
660
|
action: { type: 'string', enum: [...toolPolicy_1.PLAN_COMPUTER_USE_ACTIONS] },
|
|
656
661
|
app_target: copy.function.parameters.properties.app_target,
|
|
657
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,
|
|
658
665
|
max_chars: copy.function.parameters.properties.max_chars,
|
|
659
666
|
capture_max_width: copy.function.parameters.properties.capture_max_width,
|
|
660
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,
|
|
661
670
|
include_raw_ui: copy.function.parameters.properties.include_raw_ui,
|
|
662
671
|
};
|
|
663
672
|
return copy;
|
|
@@ -787,15 +796,99 @@ class ToolExecutor {
|
|
|
787
796
|
switch (tool) {
|
|
788
797
|
case 'bash': return await this.bash(g('command'), wsPath, args.timeout_ms, context.signal);
|
|
789
798
|
case 'pwd': return `Current directory: ${wsPath}`;
|
|
790
|
-
case 'read':
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
+
}
|
|
794
872
|
case 'glob': return this.glob(g('pattern'), wsPath);
|
|
795
873
|
case 'grep': return this.grep(g('pattern'), resolve(g('path')));
|
|
796
874
|
case 'web_search': return await this.wsearch(g('query'), context.signal);
|
|
797
875
|
case 'web_fetch': return await this.wfetch(g('url'), context.signal);
|
|
798
|
-
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
|
+
}
|
|
799
892
|
case 'browser_open': return await this.browserRun({ action: 'open', url: g('url') }, context.signal, context, wsPath);
|
|
800
893
|
case 'browser_snapshot': return await this.browserRun({ action: 'snapshot', maxChars: Number(args.max_chars || 12000) }, context.signal, context, wsPath);
|
|
801
894
|
case 'browser_click': return await this.browserRun({ action: 'click', selector: g('selector') }, context.signal, context, wsPath);
|
|
@@ -999,6 +1092,14 @@ class ToolExecutor {
|
|
|
999
1092
|
}
|
|
1000
1093
|
case 'computer_use': {
|
|
1001
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
|
+
}
|
|
1002
1103
|
const owner = `${computerUseOwner(context, wsPath)}:${String(context.actorId || 'root')}`;
|
|
1003
1104
|
const lockGuard = action === 'takeover_stop'
|
|
1004
1105
|
? assertComputerUseLockOwner(action, owner, context, wsPath)
|
|
@@ -1063,6 +1164,10 @@ class ToolExecutor {
|
|
|
1063
1164
|
allowEphemeralVisionImage: context.allowEphemeralVisionImage === true,
|
|
1064
1165
|
captureMaxWidth: Number(args.capture_max_width),
|
|
1065
1166
|
captureMaxHeight: Number(args.capture_max_height),
|
|
1167
|
+
sparseWaitMs: Number(args.sparse_wait_ms),
|
|
1168
|
+
includeUiTree: args.include_ui_tree !== false,
|
|
1169
|
+
timeoutMs: Number(args.timeout_ms),
|
|
1170
|
+
signal: context.signal,
|
|
1066
1171
|
gradientColors: Array.isArray(args.gradient_colors) ? args.gradient_colors : undefined,
|
|
1067
1172
|
gradientSpeed: args.gradient_speed !== undefined ? Number(args.gradient_speed) : undefined,
|
|
1068
1173
|
gradientWidth: args.gradient_width !== undefined ? Number(args.gradient_width) : undefined,
|
|
@@ -1299,6 +1404,18 @@ class ToolExecutor {
|
|
|
1299
1404
|
const action = policy === 'ask_user' ? 'User approval required' : 'Denied';
|
|
1300
1405
|
return `[permission] ${action}: ${tool} cannot access outside workspace (${target}).`;
|
|
1301
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
|
+
}
|
|
1302
1419
|
checkBashWorkspaceAccess(command, wsPath) {
|
|
1303
1420
|
const perm = this.config.getStr('workspace', 'access_permission');
|
|
1304
1421
|
if (perm === 'full_access')
|
|
@@ -1386,54 +1503,6 @@ class ToolExecutor {
|
|
|
1386
1503
|
return `[bash] ${e instanceof Error ? e.message : String(e)}`;
|
|
1387
1504
|
}
|
|
1388
1505
|
}
|
|
1389
|
-
fread(p) {
|
|
1390
|
-
try {
|
|
1391
|
-
const c = fs.readFileSync(p, 'utf-8');
|
|
1392
|
-
return c.length > 30000 ? c.slice(0, 30000) + '...\n[truncated]' : c;
|
|
1393
|
-
}
|
|
1394
|
-
catch (e) {
|
|
1395
|
-
return `[read] ${e}`;
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
fwrite(p, content) {
|
|
1399
|
-
try {
|
|
1400
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
1401
|
-
fs.writeFileSync(p, content, 'utf-8');
|
|
1402
|
-
return `[write] OK: ${p}`;
|
|
1403
|
-
}
|
|
1404
|
-
catch (e) {
|
|
1405
|
-
return `[write] ${e}`;
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
fedit(p, oldStr, newStr) {
|
|
1409
|
-
try {
|
|
1410
|
-
const c = fs.readFileSync(p, 'utf-8');
|
|
1411
|
-
if (!c.includes(oldStr))
|
|
1412
|
-
return `[edit] String not found in ${p}.`;
|
|
1413
|
-
const updated = c.replace(oldStr, newStr);
|
|
1414
|
-
fs.writeFileSync(p, updated, 'utf-8');
|
|
1415
|
-
return `[edit] OK: ${p}`;
|
|
1416
|
-
}
|
|
1417
|
-
catch (e) {
|
|
1418
|
-
return `[edit] ${e}`;
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
|
-
fdelete(p) {
|
|
1422
|
-
try {
|
|
1423
|
-
if (/[*?]/.test(p))
|
|
1424
|
-
return '[delete_file] Refused: wildcard paths are not allowed. Delete one file per call.';
|
|
1425
|
-
const resolved = path.resolve(p);
|
|
1426
|
-
const stat = fs.lstatSync(resolved);
|
|
1427
|
-
if (stat.isDirectory()) {
|
|
1428
|
-
return '[delete_file] Refused: deleting a directory is not allowed. Delete files one by one under Agent supervision.';
|
|
1429
|
-
}
|
|
1430
|
-
fs.unlinkSync(resolved);
|
|
1431
|
-
return `[delete_file] OK: ${resolved}`;
|
|
1432
|
-
}
|
|
1433
|
-
catch (e) {
|
|
1434
|
-
return `[delete_file] ${e instanceof Error ? e.message : String(e)}`;
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
1506
|
glob(pattern, ws) {
|
|
1438
1507
|
try {
|
|
1439
1508
|
const results = globSync(pattern, {
|
|
@@ -1885,7 +1954,7 @@ class ToolExecutor {
|
|
|
1885
1954
|
guard.dispose();
|
|
1886
1955
|
}
|
|
1887
1956
|
}
|
|
1888
|
-
async wcatch(args, destination, signal) {
|
|
1957
|
+
async wcatch(args, destination, signal, beforeCommit) {
|
|
1889
1958
|
const action = String(args.action || 'download');
|
|
1890
1959
|
const maxBytes = Math.max(1, Math.min(100 * 1024 * 1024, Number(args.max_bytes || 50 * 1024 * 1024)));
|
|
1891
1960
|
const sourceUrl = this.checkedPublicWebUrl(String(args.url || ''));
|
|
@@ -1929,9 +1998,20 @@ class ToolExecutor {
|
|
|
1929
1998
|
const backup = destinationExists ? `${destination}.newmark-${crypto.randomUUID()}.bak` : '';
|
|
1930
1999
|
try {
|
|
1931
2000
|
fs.writeFileSync(temp, bytes, { flag: 'wx' });
|
|
1932
|
-
|
|
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) {
|
|
1933
2008
|
fs.renameSync(destination, backup);
|
|
1934
|
-
|
|
2009
|
+
fs.renameSync(temp, destination);
|
|
2010
|
+
}
|
|
2011
|
+
else {
|
|
2012
|
+
fs.linkSync(temp, destination);
|
|
2013
|
+
fs.unlinkSync(temp);
|
|
2014
|
+
}
|
|
1935
2015
|
if (backup)
|
|
1936
2016
|
fs.unlinkSync(backup);
|
|
1937
2017
|
}
|