@visorcraft/idlehands 1.1.7 → 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/dist/agent/formatting.js +273 -0
- package/dist/agent/formatting.js.map +1 -0
- package/dist/agent/review-artifact.js +147 -0
- package/dist/agent/review-artifact.js.map +1 -0
- package/dist/agent/tool-calls.js +411 -0
- package/dist/agent/tool-calls.js.map +1 -0
- package/dist/agent.js +285 -684
- package/dist/agent.js.map +1 -1
- package/dist/anton/controller.js +1 -1
- package/dist/anton/controller.js.map +1 -1
- package/dist/anton/lock.js +0 -3
- package/dist/anton/lock.js.map +1 -1
- package/dist/anton/parser.js +6 -6
- package/dist/anton/parser.js.map +1 -1
- package/dist/anton/reporter.js +1 -1
- package/dist/anton/reporter.js.map +1 -1
- package/dist/bot/commands.js +3 -2
- package/dist/bot/commands.js.map +1 -1
- package/dist/bot/confirm-telegram.js +2 -1
- package/dist/bot/confirm-telegram.js.map +1 -1
- package/dist/bot/discord-routing.js +186 -0
- package/dist/bot/discord-routing.js.map +1 -0
- package/dist/bot/discord-streaming.js +107 -0
- package/dist/bot/discord-streaming.js.map +1 -0
- package/dist/bot/discord.js +49 -237
- package/dist/bot/discord.js.map +1 -1
- package/dist/bot/format.js +2 -25
- package/dist/bot/format.js.map +1 -1
- package/dist/bot/session-manager.js +22 -11
- package/dist/bot/session-manager.js.map +1 -1
- package/dist/bot/telegram.js +83 -94
- package/dist/bot/telegram.js.map +1 -1
- package/dist/cli/build-repl-context.js.map +1 -1
- package/dist/cli/command-registry.js +2 -1
- package/dist/cli/command-registry.js.map +1 -1
- package/dist/cli/command-utils.js +27 -0
- package/dist/cli/command-utils.js.map +1 -0
- package/dist/cli/commands/anton.js +3 -2
- package/dist/cli/commands/anton.js.map +1 -1
- package/dist/cli/commands/model.js +8 -7
- package/dist/cli/commands/model.js.map +1 -1
- package/dist/cli/commands/project.js +5 -4
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/session.js +9 -8
- package/dist/cli/commands/session.js.map +1 -1
- package/dist/cli/commands/tools.js +4 -3
- package/dist/cli/commands/tools.js.map +1 -1
- package/dist/cli/input.js +2 -1
- package/dist/cli/input.js.map +1 -1
- package/dist/cli/repl-dispatch.js +85 -0
- package/dist/cli/repl-dispatch.js.map +1 -0
- package/dist/cli/runtime-cmds.js +148 -20
- package/dist/cli/runtime-cmds.js.map +1 -1
- package/dist/cli/service.js +0 -14
- package/dist/cli/service.js.map +1 -1
- package/dist/cli/setup.js +3 -3
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/watch.js +2 -1
- package/dist/cli/watch.js.map +1 -1
- package/dist/client.js +24 -7
- package/dist/client.js.map +1 -1
- package/dist/context.js +101 -10
- package/dist/context.js.map +1 -1
- package/dist/harnesses.js +1 -1
- package/dist/harnesses.js.map +1 -1
- package/dist/hooks/manager.js +5 -0
- package/dist/hooks/manager.js.map +1 -1
- package/dist/index.js +13 -64
- package/dist/index.js.map +1 -1
- package/dist/progress/agent-hooks.js +37 -0
- package/dist/progress/agent-hooks.js.map +1 -0
- package/dist/progress/ir.js +10 -0
- package/dist/progress/ir.js.map +1 -0
- package/dist/progress/message-edit-scheduler.js +97 -0
- package/dist/progress/message-edit-scheduler.js.map +1 -0
- package/dist/progress/progress-message-renderer.js +120 -0
- package/dist/progress/progress-message-renderer.js.map +1 -0
- package/dist/progress/progress-presenter.js +137 -0
- package/dist/progress/progress-presenter.js.map +1 -0
- package/dist/progress/serialize-discord.js +72 -0
- package/dist/progress/serialize-discord.js.map +1 -0
- package/dist/progress/serialize-telegram.js +67 -0
- package/dist/progress/serialize-telegram.js.map +1 -0
- package/dist/progress/serialize-tui.js +52 -0
- package/dist/progress/serialize-tui.js.map +1 -0
- package/dist/progress/tool-summary.js +58 -0
- package/dist/progress/tool-summary.js.map +1 -0
- package/dist/progress/tool-tail.js +48 -0
- package/dist/progress/tool-tail.js.map +1 -0
- package/dist/progress/turn-progress.js +215 -0
- package/dist/progress/turn-progress.js.map +1 -0
- package/dist/replay.js +2 -5
- package/dist/replay.js.map +1 -1
- package/dist/runtime/executor.js +58 -10
- package/dist/runtime/executor.js.map +1 -1
- package/dist/runtime/planner.js +19 -6
- package/dist/runtime/planner.js.map +1 -1
- package/dist/runtime/store.js +2 -1
- package/dist/runtime/store.js.map +1 -1
- package/dist/safety.js +0 -1
- package/dist/safety.js.map +1 -1
- package/dist/spinner.js +8 -0
- package/dist/spinner.js.map +1 -1
- package/dist/tools/tool-error.js +97 -0
- package/dist/tools/tool-error.js.map +1 -0
- package/dist/tools.js +471 -41
- package/dist/tools.js.map +1 -1
- package/dist/tui/branch-picker.js.map +1 -1
- package/dist/tui/command-handler.js.map +1 -1
- package/dist/tui/controller.js +91 -28
- package/dist/tui/controller.js.map +1 -1
- package/dist/tui/render.js +15 -2
- package/dist/tui/render.js.map +1 -1
- package/dist/tui/state.js +13 -0
- package/dist/tui/state.js.map +1 -1
- package/dist/upgrade.js.map +1 -1
- package/dist/utils.js +17 -0
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import { spawn, spawnSync } from 'node:child_process';
|
|
5
|
+
import { ToolError } from './tools/tool-error.js';
|
|
5
6
|
import { checkExecSafety, checkPathSafety, isProtectedDeleteTarget } from './safety.js';
|
|
6
7
|
import { sys_context as sysContextTool } from './sys/context.js';
|
|
7
8
|
import { stateDir, shellEscape, BASH_PATH } from './utils.js';
|
|
@@ -292,14 +293,24 @@ export async function undo_path(ctx, args) {
|
|
|
292
293
|
}
|
|
293
294
|
export async function read_file(ctx, args) {
|
|
294
295
|
const p = resolvePath(ctx, args?.path);
|
|
295
|
-
const offset = args?.offset ? Number(args.offset) : undefined;
|
|
296
|
+
const offset = args?.offset != null ? Number(args.offset) : undefined;
|
|
296
297
|
const rawLimit = args?.limit != null ? Number(args.limit) : undefined;
|
|
297
298
|
const limit = Number.isFinite(rawLimit) && rawLimit > 0
|
|
298
299
|
? Math.max(1, Math.floor(rawLimit))
|
|
299
|
-
:
|
|
300
|
+
: 200;
|
|
300
301
|
const search = typeof args?.search === 'string' ? args.search : undefined;
|
|
301
|
-
const
|
|
302
|
-
const
|
|
302
|
+
const rawContext = args?.context != null ? Number(args.context) : undefined;
|
|
303
|
+
const context = Number.isFinite(rawContext) && rawContext >= 0
|
|
304
|
+
? Math.max(0, Math.min(200, Math.floor(rawContext)))
|
|
305
|
+
: 10;
|
|
306
|
+
const formatRaw = typeof args?.format === 'string' ? args.format.trim().toLowerCase() : 'numbered';
|
|
307
|
+
const format = (formatRaw === 'plain' || formatRaw === 'numbered' || formatRaw === 'sparse')
|
|
308
|
+
? formatRaw
|
|
309
|
+
: 'numbered';
|
|
310
|
+
const rawMaxBytes = args?.max_bytes != null ? Number(args.max_bytes) : undefined;
|
|
311
|
+
const maxBytes = Number.isFinite(rawMaxBytes) && rawMaxBytes > 0
|
|
312
|
+
? Math.min(256 * 1024, Math.max(256, Math.floor(rawMaxBytes)))
|
|
313
|
+
: 20 * 1024;
|
|
303
314
|
if (!p)
|
|
304
315
|
throw new Error('read_file: missing path');
|
|
305
316
|
// Detect directories early with a helpful message instead of cryptic EISDIR
|
|
@@ -315,13 +326,6 @@ export async function read_file(ctx, args) {
|
|
|
315
326
|
const buf = await fs.readFile(p).catch((e) => {
|
|
316
327
|
throw new Error(`read_file: cannot read ${p}: ${e?.message ?? String(e)}`);
|
|
317
328
|
});
|
|
318
|
-
if (buf.length > maxBytes) {
|
|
319
|
-
// Truncate gracefully instead of throwing
|
|
320
|
-
const truncText = buf.subarray(0, maxBytes).toString('utf8');
|
|
321
|
-
const truncLines = truncText.split(/\r?\n/);
|
|
322
|
-
const numbered = truncLines.map((l, i) => `${String(i + 1).padStart(4)}| ${l}`).join('\n');
|
|
323
|
-
return `# ${p} [TRUNCATED: ${buf.length} bytes, showing first ${maxBytes}]\n${numbered}`;
|
|
324
|
-
}
|
|
325
329
|
// Binary detection: NUL byte in first 512 bytes (§8)
|
|
326
330
|
for (let i = 0; i < Math.min(buf.length, 512); i++) {
|
|
327
331
|
if (buf[i] === 0) {
|
|
@@ -332,51 +336,82 @@ export async function read_file(ctx, args) {
|
|
|
332
336
|
const text = buf.toString('utf8');
|
|
333
337
|
const lines = text.split(/\r?\n/);
|
|
334
338
|
let start = 1;
|
|
335
|
-
let end =
|
|
339
|
+
let end = Math.min(lines.length, limit);
|
|
340
|
+
let matchLines = [];
|
|
336
341
|
if (search) {
|
|
337
|
-
|
|
342
|
+
matchLines = [];
|
|
338
343
|
for (let i = 0; i < lines.length; i++) {
|
|
339
344
|
if (lines[i].includes(search))
|
|
340
345
|
matchLines.push(i + 1);
|
|
341
346
|
}
|
|
342
347
|
if (!matchLines.length) {
|
|
343
|
-
return `# ${p}\n# search not found: ${JSON.stringify(search)}\n# file has ${lines.length} lines
|
|
348
|
+
return truncateBytes(`# ${p}\n# search not found: ${JSON.stringify(search)}\n# file has ${lines.length} lines`, maxBytes).text;
|
|
344
349
|
}
|
|
345
350
|
const firstIdx = matchLines[0];
|
|
351
|
+
// Window around the first match, but never return more than `limit` lines.
|
|
346
352
|
start = Math.max(1, firstIdx - context);
|
|
347
353
|
end = Math.min(lines.length, firstIdx + context);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
out.push(`${String(ln).padStart(6, ' ')}| ${lines[ln - 1] ?? ''}`);
|
|
354
|
+
if (end - start + 1 > limit) {
|
|
355
|
+
const half = Math.floor(limit / 2);
|
|
356
|
+
start = Math.max(1, firstIdx - half);
|
|
357
|
+
end = Math.min(lines.length, start + limit - 1);
|
|
353
358
|
}
|
|
354
|
-
if (end < lines.length)
|
|
355
|
-
out.push(`# ... (${lines.length - end} more lines)`);
|
|
356
|
-
return out.join('\n');
|
|
357
359
|
}
|
|
358
360
|
else if (offset && offset >= 1) {
|
|
359
|
-
start = offset;
|
|
360
|
-
end =
|
|
361
|
+
start = Math.max(1, Math.floor(offset));
|
|
362
|
+
end = Math.min(lines.length, start + limit - 1);
|
|
361
363
|
}
|
|
364
|
+
const matchSet = new Set(matchLines);
|
|
362
365
|
const out = [];
|
|
363
|
-
out.push(`# ${p}`);
|
|
366
|
+
out.push(`# ${p} (lines ${start}-${end} of ${lines.length})`);
|
|
367
|
+
if (search) {
|
|
368
|
+
const shown = matchLines.slice(0, 20);
|
|
369
|
+
out.push(`# matches at lines: ${shown.join(', ')}${matchLines.length > shown.length ? ' …' : ''}`);
|
|
370
|
+
}
|
|
371
|
+
const renderNumbered = (ln, body) => `${ln}| ${body}`;
|
|
364
372
|
for (let ln = start; ln <= end; ln++) {
|
|
365
|
-
|
|
373
|
+
const body = lines[ln - 1] ?? '';
|
|
374
|
+
if (format === 'plain') {
|
|
375
|
+
out.push(body);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (format === 'numbered') {
|
|
379
|
+
out.push(renderNumbered(ln, body));
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
// sparse: number anchor lines + matches; otherwise raw text.
|
|
383
|
+
const isAnchor = ln === start || ln === end || (ln - start) % 10 === 0;
|
|
384
|
+
if (isAnchor || matchSet.has(ln))
|
|
385
|
+
out.push(renderNumbered(ln, body));
|
|
386
|
+
else
|
|
387
|
+
out.push(body);
|
|
366
388
|
}
|
|
367
389
|
if (end < lines.length)
|
|
368
390
|
out.push(`# ... (${lines.length - end} more lines)`);
|
|
369
|
-
return out.join('\n');
|
|
391
|
+
return truncateBytes(out.join('\n'), maxBytes).text;
|
|
370
392
|
}
|
|
371
393
|
export async function read_files(ctx, args) {
|
|
372
394
|
const reqs = Array.isArray(args?.requests) ? args.requests : [];
|
|
373
395
|
if (!reqs.length)
|
|
374
|
-
throw new
|
|
396
|
+
throw new ToolError('invalid_args', 'read_files: missing requests[]', false, 'Provide requests as an array of {path, limit,...} objects.');
|
|
375
397
|
const parts = [];
|
|
376
|
-
|
|
377
|
-
|
|
398
|
+
let failures = 0;
|
|
399
|
+
for (let i = 0; i < reqs.length; i++) {
|
|
400
|
+
const r = reqs[i];
|
|
401
|
+
const p = typeof r?.path === 'string' ? r.path : `request[${i}]`;
|
|
402
|
+
try {
|
|
403
|
+
parts.push(await read_file(ctx, r));
|
|
404
|
+
}
|
|
405
|
+
catch (e) {
|
|
406
|
+
failures++;
|
|
407
|
+
const te = ToolError.fromError(e, 'internal');
|
|
408
|
+
parts.push(`[file:${p}] ERROR: code=${te.code} msg=${te.message}`);
|
|
409
|
+
}
|
|
378
410
|
parts.push('');
|
|
379
411
|
}
|
|
412
|
+
if (failures > 0) {
|
|
413
|
+
parts.push(`# read_files completed with partial failures: ${failures}/${reqs.length}`);
|
|
414
|
+
}
|
|
380
415
|
return parts.join('\n');
|
|
381
416
|
}
|
|
382
417
|
export async function write_file(ctx, args) {
|
|
@@ -602,6 +637,292 @@ export async function edit_file(ctx, args) {
|
|
|
602
637
|
const cwdWarning = checkCwdWarning('edit_file', p, ctx);
|
|
603
638
|
return `edited ${p} (replace_all=${replaceAll})${replayNote}${cwdWarning}`;
|
|
604
639
|
}
|
|
640
|
+
function normalizePatchPath(p) {
|
|
641
|
+
let s = String(p ?? '').trim();
|
|
642
|
+
if (!s || s === '/dev/null')
|
|
643
|
+
return '';
|
|
644
|
+
// Strip quotes some generators add
|
|
645
|
+
s = s.replace(/^"|"$/g, '');
|
|
646
|
+
// Drop common diff prefixes
|
|
647
|
+
s = s.replace(/^[ab]\//, '').replace(/^\.\/+/, '');
|
|
648
|
+
// Normalize to posix separators for diffs
|
|
649
|
+
s = s.replace(/\\/g, '/');
|
|
650
|
+
const norm = path.posix.normalize(s);
|
|
651
|
+
if (norm.startsWith('../') || norm === '..' || norm.startsWith('/')) {
|
|
652
|
+
throw new Error(`apply_patch: unsafe path in patch: ${JSON.stringify(s)}`);
|
|
653
|
+
}
|
|
654
|
+
return norm;
|
|
655
|
+
}
|
|
656
|
+
function extractTouchedFilesFromPatch(patchText) {
|
|
657
|
+
const paths = [];
|
|
658
|
+
const created = new Set();
|
|
659
|
+
const deleted = new Set();
|
|
660
|
+
let pendingOld = null;
|
|
661
|
+
let pendingNew = null;
|
|
662
|
+
const seen = new Set();
|
|
663
|
+
const lines = String(patchText ?? '').split(/\r?\n/);
|
|
664
|
+
for (const line of lines) {
|
|
665
|
+
// Primary: git-style header
|
|
666
|
+
if (line.startsWith('diff --git ')) {
|
|
667
|
+
const m = /^diff --git\s+a\/(.+?)\s+b\/(.+?)\s*$/.exec(line);
|
|
668
|
+
if (m) {
|
|
669
|
+
const aPath = normalizePatchPath(m[1]);
|
|
670
|
+
const bPath = normalizePatchPath(m[2]);
|
|
671
|
+
const use = bPath || aPath;
|
|
672
|
+
if (use && !seen.has(use)) {
|
|
673
|
+
seen.add(use);
|
|
674
|
+
paths.push(use);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
pendingOld = null;
|
|
678
|
+
pendingNew = null;
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
// Fallback: unified diff headers
|
|
682
|
+
if (line.startsWith('--- ')) {
|
|
683
|
+
pendingOld = line.slice(4).trim();
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
if (line.startsWith('+++ ')) {
|
|
687
|
+
pendingNew = line.slice(4).trim();
|
|
688
|
+
const oldP = pendingOld ? pendingOld.replace(/^a\//, '').trim() : '';
|
|
689
|
+
const newP = pendingNew ? pendingNew.replace(/^b\//, '').trim() : '';
|
|
690
|
+
const oldIsDevNull = oldP === '/dev/null';
|
|
691
|
+
const newIsDevNull = newP === '/dev/null';
|
|
692
|
+
if (!newIsDevNull) {
|
|
693
|
+
const rel = normalizePatchPath(newP);
|
|
694
|
+
if (rel && !seen.has(rel)) {
|
|
695
|
+
seen.add(rel);
|
|
696
|
+
paths.push(rel);
|
|
697
|
+
}
|
|
698
|
+
if (oldIsDevNull)
|
|
699
|
+
created.add(rel);
|
|
700
|
+
}
|
|
701
|
+
if (!oldIsDevNull && newIsDevNull) {
|
|
702
|
+
const rel = normalizePatchPath(oldP);
|
|
703
|
+
if (rel && !seen.has(rel)) {
|
|
704
|
+
seen.add(rel);
|
|
705
|
+
paths.push(rel);
|
|
706
|
+
}
|
|
707
|
+
deleted.add(rel);
|
|
708
|
+
}
|
|
709
|
+
pendingOld = null;
|
|
710
|
+
pendingNew = null;
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return { paths, created, deleted };
|
|
715
|
+
}
|
|
716
|
+
async function runCommandWithStdin(cmd, cmdArgs, stdinText, cwd, maxOutBytes) {
|
|
717
|
+
return await new Promise((resolve, reject) => {
|
|
718
|
+
const child = spawn(cmd, cmdArgs, { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
719
|
+
const outChunks = [];
|
|
720
|
+
const errChunks = [];
|
|
721
|
+
let outSeen = 0;
|
|
722
|
+
let errSeen = 0;
|
|
723
|
+
let outCaptured = 0;
|
|
724
|
+
let errCaptured = 0;
|
|
725
|
+
const pushCapped = (chunks, buf, kind) => {
|
|
726
|
+
const n = buf.length;
|
|
727
|
+
if (kind === 'out')
|
|
728
|
+
outSeen += n;
|
|
729
|
+
else
|
|
730
|
+
errSeen += n;
|
|
731
|
+
const captured = kind === 'out' ? outCaptured : errCaptured;
|
|
732
|
+
const remaining = maxOutBytes - captured;
|
|
733
|
+
if (remaining <= 0)
|
|
734
|
+
return;
|
|
735
|
+
const take = n <= remaining ? buf : buf.subarray(0, remaining);
|
|
736
|
+
chunks.push(Buffer.from(take));
|
|
737
|
+
if (kind === 'out')
|
|
738
|
+
outCaptured += take.length;
|
|
739
|
+
else
|
|
740
|
+
errCaptured += take.length;
|
|
741
|
+
};
|
|
742
|
+
child.stdout.on('data', (d) => pushCapped(outChunks, Buffer.from(d), 'out'));
|
|
743
|
+
child.stderr.on('data', (d) => pushCapped(errChunks, Buffer.from(d), 'err'));
|
|
744
|
+
child.on('error', (e) => reject(new Error(`${cmd}: ${e?.message ?? String(e)}`)));
|
|
745
|
+
child.on('close', (code) => {
|
|
746
|
+
const outRaw = stripAnsi(Buffer.concat(outChunks).toString('utf8'));
|
|
747
|
+
const errRaw = stripAnsi(Buffer.concat(errChunks).toString('utf8'));
|
|
748
|
+
const outT = truncateBytes(outRaw, maxOutBytes, outSeen);
|
|
749
|
+
const errT = truncateBytes(errRaw, maxOutBytes, errSeen);
|
|
750
|
+
resolve({ rc: code ?? 0, out: outT.text, err: errT.text });
|
|
751
|
+
});
|
|
752
|
+
child.stdin.write(String(stdinText ?? ''), 'utf8');
|
|
753
|
+
child.stdin.end();
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
export async function edit_range(ctx, args) {
|
|
757
|
+
const p = resolvePath(ctx, args?.path);
|
|
758
|
+
const startLine = Number(args?.start_line);
|
|
759
|
+
const endLine = Number(args?.end_line);
|
|
760
|
+
const rawReplacement = args?.replacement;
|
|
761
|
+
const replacement = typeof rawReplacement === 'string' ? rawReplacement
|
|
762
|
+
: (rawReplacement != null && typeof rawReplacement === 'object' ? JSON.stringify(rawReplacement, null, 2) : undefined);
|
|
763
|
+
if (!p)
|
|
764
|
+
throw new Error('edit_range: missing path');
|
|
765
|
+
if (!Number.isFinite(startLine) || startLine < 1)
|
|
766
|
+
throw new Error('edit_range: missing/invalid start_line');
|
|
767
|
+
if (!Number.isFinite(endLine) || endLine < startLine)
|
|
768
|
+
throw new Error('edit_range: missing/invalid end_line');
|
|
769
|
+
if (replacement == null)
|
|
770
|
+
throw new Error('edit_range: missing replacement (got ' + typeof rawReplacement + ')');
|
|
771
|
+
// Path safety check (Phase 9)
|
|
772
|
+
const pathVerdict = checkPathSafety(p);
|
|
773
|
+
if (pathVerdict.tier === 'forbidden') {
|
|
774
|
+
throw new Error(`edit_range: ${pathVerdict.reason}`);
|
|
775
|
+
}
|
|
776
|
+
if (pathVerdict.tier === 'cautious' && !ctx.noConfirm) {
|
|
777
|
+
if (ctx.confirm) {
|
|
778
|
+
const ok = await ctx.confirm(pathVerdict.prompt || `Edit range in ${p}?`, { tool: 'edit_range', args: { path: p, start_line: startLine, end_line: endLine } });
|
|
779
|
+
if (!ok)
|
|
780
|
+
throw new Error(`edit_range: cancelled by user (${pathVerdict.reason})`);
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
throw new Error(`edit_range: blocked (${pathVerdict.reason}) without --no-confirm/--yolo`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (ctx.dryRun)
|
|
787
|
+
return `dry-run: would edit_range ${p} lines ${startLine}-${endLine} (${Buffer.byteLength(replacement, 'utf8')} bytes)`;
|
|
788
|
+
// Phase 9d: snapshot /etc/ files before editing
|
|
789
|
+
if (ctx.mode === 'sys' && ctx.vault) {
|
|
790
|
+
await snapshotBeforeEdit(ctx.vault, p).catch(() => { });
|
|
791
|
+
}
|
|
792
|
+
const beforeText = await fs.readFile(p, 'utf8').catch((e) => {
|
|
793
|
+
throw new Error(`edit_range: cannot read ${p}: ${e?.message ?? String(e)}`);
|
|
794
|
+
});
|
|
795
|
+
const eol = beforeText.includes('\r\n') ? '\r\n' : '\n';
|
|
796
|
+
const lines = beforeText.split(/\r?\n/);
|
|
797
|
+
if (startLine > lines.length) {
|
|
798
|
+
throw new Error(`edit_range: start_line ${startLine} out of range (file has ${lines.length} lines)`);
|
|
799
|
+
}
|
|
800
|
+
if (endLine > lines.length) {
|
|
801
|
+
throw new Error(`edit_range: end_line ${endLine} out of range (file has ${lines.length} lines)`);
|
|
802
|
+
}
|
|
803
|
+
const startIdx = startLine - 1;
|
|
804
|
+
const deleteCount = endLine - startLine + 1;
|
|
805
|
+
// For deletion, allow empty replacement to remove the range without leaving a blank line.
|
|
806
|
+
const replacementLines = replacement === '' ? [] : replacement.split(/\r?\n/);
|
|
807
|
+
lines.splice(startIdx, deleteCount, ...replacementLines);
|
|
808
|
+
const out = lines.join(eol);
|
|
809
|
+
await backupFile(p, ctx);
|
|
810
|
+
await atomicWrite(p, out);
|
|
811
|
+
ctx.onMutation?.(p);
|
|
812
|
+
const replayNote = await checkpointReplay(ctx, {
|
|
813
|
+
op: 'edit_range',
|
|
814
|
+
filePath: p,
|
|
815
|
+
before: Buffer.from(beforeText, 'utf8'),
|
|
816
|
+
after: Buffer.from(out, 'utf8')
|
|
817
|
+
});
|
|
818
|
+
const cwdWarning = checkCwdWarning('edit_range', p, ctx);
|
|
819
|
+
return `edited ${p} lines ${startLine}-${endLine}${replayNote}${cwdWarning}`;
|
|
820
|
+
}
|
|
821
|
+
export async function apply_patch(ctx, args) {
|
|
822
|
+
const rawPatch = args?.patch;
|
|
823
|
+
const patchText = typeof rawPatch === 'string' ? rawPatch
|
|
824
|
+
: (rawPatch != null && typeof rawPatch === 'object' ? JSON.stringify(rawPatch, null, 2) : undefined);
|
|
825
|
+
const rawFiles = Array.isArray(args?.files) ? args.files : [];
|
|
826
|
+
const files = rawFiles
|
|
827
|
+
.map((f) => (typeof f === 'string' ? f.trim() : ''))
|
|
828
|
+
.filter(Boolean);
|
|
829
|
+
const stripRaw = Number(args?.strip);
|
|
830
|
+
const strip = Number.isFinite(stripRaw) ? Math.max(0, Math.min(5, Math.floor(stripRaw))) : 0;
|
|
831
|
+
if (!patchText)
|
|
832
|
+
throw new Error('apply_patch: missing patch');
|
|
833
|
+
if (!files.length)
|
|
834
|
+
throw new Error('apply_patch: missing files[]');
|
|
835
|
+
const touched = extractTouchedFilesFromPatch(patchText);
|
|
836
|
+
if (!touched.paths.length) {
|
|
837
|
+
throw new Error('apply_patch: patch contains no recognizable file headers');
|
|
838
|
+
}
|
|
839
|
+
const declared = new Set(files.map(normalizePatchPath));
|
|
840
|
+
const unknown = touched.paths.filter((p) => !declared.has(p));
|
|
841
|
+
if (unknown.length) {
|
|
842
|
+
throw new Error(`apply_patch: patch touches undeclared file(s): ${unknown.join(', ')}`);
|
|
843
|
+
}
|
|
844
|
+
const absPaths = touched.paths.map((rel) => resolvePath(ctx, rel));
|
|
845
|
+
// Path safety check (Phase 9)
|
|
846
|
+
const verdicts = absPaths.map((p) => ({ p, v: checkPathSafety(p) }));
|
|
847
|
+
const forbidden = verdicts.filter(({ v }) => v.tier === 'forbidden');
|
|
848
|
+
if (forbidden.length) {
|
|
849
|
+
throw new Error(`apply_patch: ${forbidden[0].v.reason} (${forbidden[0].p})`);
|
|
850
|
+
}
|
|
851
|
+
const cautious = verdicts.filter(({ v }) => v.tier === 'cautious');
|
|
852
|
+
if (cautious.length && !ctx.noConfirm) {
|
|
853
|
+
if (ctx.confirm) {
|
|
854
|
+
const preview = patchText.length > 4000 ? patchText.slice(0, 4000) + '\n[truncated]' : patchText;
|
|
855
|
+
const ok = await ctx.confirm(`Apply patch touching ${touched.paths.length} file(s)?\n- ${touched.paths.join('\n- ')}\n\nProceed? (y/N) `, { tool: 'apply_patch', args: { files: touched.paths, strip }, diff: preview });
|
|
856
|
+
if (!ok)
|
|
857
|
+
throw new Error('apply_patch: cancelled by user');
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
860
|
+
throw new Error('apply_patch: blocked (cautious paths) without --no-confirm/--yolo');
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const maxToolBytes = ctx.maxExecBytes ?? DEFAULT_MAX_EXEC_BYTES;
|
|
864
|
+
const stripArg = `-p${strip}`;
|
|
865
|
+
// Dry-run: validate the patch applies cleanly, but do not mutate files.
|
|
866
|
+
if (ctx.dryRun) {
|
|
867
|
+
const haveGit = !spawnSync('git', ['--version'], { stdio: 'ignore' }).error;
|
|
868
|
+
if (haveGit) {
|
|
869
|
+
const chk = await runCommandWithStdin('git', ['apply', stripArg, '--check', '--whitespace=nowarn'], patchText, ctx.cwd, maxToolBytes);
|
|
870
|
+
if (chk.rc !== 0)
|
|
871
|
+
throw new Error(`apply_patch: git apply --check failed:\n${chk.err || chk.out}`);
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
const chk = await runCommandWithStdin('patch', [stripArg, '--dry-run', '--batch'], patchText, ctx.cwd, maxToolBytes);
|
|
875
|
+
if (chk.rc !== 0)
|
|
876
|
+
throw new Error(`apply_patch: patch --dry-run failed:\n${chk.err || chk.out}`);
|
|
877
|
+
}
|
|
878
|
+
return `dry-run: patch would apply cleanly (${touched.paths.length} files): ${touched.paths.join(', ')}`;
|
|
879
|
+
}
|
|
880
|
+
// Snapshot + backup before applying
|
|
881
|
+
const beforeMap = new Map();
|
|
882
|
+
for (const abs of absPaths) {
|
|
883
|
+
// Phase 9d: snapshot /etc/ files before editing
|
|
884
|
+
if (ctx.mode === 'sys' && ctx.vault) {
|
|
885
|
+
await snapshotBeforeEdit(ctx.vault, abs).catch(() => { });
|
|
886
|
+
}
|
|
887
|
+
const before = await fs.readFile(abs).catch(() => Buffer.from(''));
|
|
888
|
+
beforeMap.set(abs, before);
|
|
889
|
+
await backupFile(abs, ctx);
|
|
890
|
+
}
|
|
891
|
+
// Apply with git apply if available; fallback to patch.
|
|
892
|
+
const haveGit = !spawnSync('git', ['--version'], { stdio: 'ignore' }).error;
|
|
893
|
+
if (haveGit) {
|
|
894
|
+
const chk = await runCommandWithStdin('git', ['apply', stripArg, '--check', '--whitespace=nowarn'], patchText, ctx.cwd, maxToolBytes);
|
|
895
|
+
if (chk.rc !== 0)
|
|
896
|
+
throw new Error(`apply_patch: git apply --check failed:\n${chk.err || chk.out}`);
|
|
897
|
+
const app = await runCommandWithStdin('git', ['apply', stripArg, '--whitespace=nowarn'], patchText, ctx.cwd, maxToolBytes);
|
|
898
|
+
if (app.rc !== 0)
|
|
899
|
+
throw new Error(`apply_patch: git apply failed:\n${app.err || app.out}`);
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
const chk = await runCommandWithStdin('patch', [stripArg, '--dry-run', '--batch'], patchText, ctx.cwd, maxToolBytes);
|
|
903
|
+
if (chk.rc !== 0)
|
|
904
|
+
throw new Error(`apply_patch: patch --dry-run failed:\n${chk.err || chk.out}`);
|
|
905
|
+
const app = await runCommandWithStdin('patch', [stripArg, '--batch'], patchText, ctx.cwd, maxToolBytes);
|
|
906
|
+
if (app.rc !== 0)
|
|
907
|
+
throw new Error(`apply_patch: patch failed:\n${app.err || app.out}`);
|
|
908
|
+
}
|
|
909
|
+
// Replay checkpoints + mutation hooks
|
|
910
|
+
let replayNotes = '';
|
|
911
|
+
let cwdWarnings = '';
|
|
912
|
+
for (const abs of absPaths) {
|
|
913
|
+
const after = await fs.readFile(abs).catch(() => Buffer.from(''));
|
|
914
|
+
ctx.onMutation?.(abs);
|
|
915
|
+
const replayNote = await checkpointReplay(ctx, {
|
|
916
|
+
op: 'apply_patch',
|
|
917
|
+
filePath: abs,
|
|
918
|
+
before: beforeMap.get(abs) ?? Buffer.from(''),
|
|
919
|
+
after
|
|
920
|
+
});
|
|
921
|
+
replayNotes += replayNote;
|
|
922
|
+
cwdWarnings += checkCwdWarning('apply_patch', abs, ctx);
|
|
923
|
+
}
|
|
924
|
+
return `applied patch (${touched.paths.length} files): ${touched.paths.join(', ')}${replayNotes}${cwdWarnings}`;
|
|
925
|
+
}
|
|
605
926
|
export async function list_dir(ctx, args) {
|
|
606
927
|
const p = resolvePath(ctx, args?.path ?? '.');
|
|
607
928
|
const recursive = Boolean(args?.recursive);
|
|
@@ -673,7 +994,13 @@ export async function search_files(ctx, args) {
|
|
|
673
994
|
}
|
|
674
995
|
}
|
|
675
996
|
// Slow fallback
|
|
676
|
-
|
|
997
|
+
let re;
|
|
998
|
+
try {
|
|
999
|
+
re = new RegExp(pattern);
|
|
1000
|
+
}
|
|
1001
|
+
catch (e) {
|
|
1002
|
+
throw new ToolError('invalid_args', `search_files: invalid regex pattern: ${e?.message ?? String(e)}`, false, 'Escape regex metacharacters (\\, [, ], (, ), +, *, ?). If you intended literal text, use an escaped/literal pattern.');
|
|
1003
|
+
}
|
|
677
1004
|
const out = [];
|
|
678
1005
|
async function walk(dir, depth) {
|
|
679
1006
|
if (out.length >= maxResults)
|
|
@@ -739,6 +1066,84 @@ function hasBackgroundExecIntent(command) {
|
|
|
739
1066
|
// like >&2, <&, and &>.
|
|
740
1067
|
return /(^|[;\s])&(?![&><\d])(?=($|[;\s]))/.test(stripped);
|
|
741
1068
|
}
|
|
1069
|
+
function safeFireAndForget(fn) {
|
|
1070
|
+
if (!fn)
|
|
1071
|
+
return;
|
|
1072
|
+
try {
|
|
1073
|
+
const r = fn();
|
|
1074
|
+
if (r && typeof r.catch === 'function')
|
|
1075
|
+
r.catch(() => { });
|
|
1076
|
+
}
|
|
1077
|
+
catch {
|
|
1078
|
+
// best effort only
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
function makeExecStreamer(ctx) {
|
|
1082
|
+
const cb = ctx.onToolStream;
|
|
1083
|
+
if (!cb)
|
|
1084
|
+
return null;
|
|
1085
|
+
const id = ctx.toolCallId ?? '';
|
|
1086
|
+
const name = ctx.toolName ?? 'exec';
|
|
1087
|
+
const intervalMs = Math.max(50, Math.floor(ctx.toolStreamIntervalMs ?? 750));
|
|
1088
|
+
const maxChunkChars = Math.max(80, Math.floor(ctx.toolStreamMaxChunkChars ?? 900));
|
|
1089
|
+
const maxBufferChars = Math.max(maxChunkChars, Math.floor(ctx.toolStreamMaxBufferChars ?? 12_000));
|
|
1090
|
+
let outBuf = '';
|
|
1091
|
+
let errBuf = '';
|
|
1092
|
+
let lastEmit = 0;
|
|
1093
|
+
let timer = null;
|
|
1094
|
+
const emit = (stream, chunk) => {
|
|
1095
|
+
const trimmed = chunk.length > maxChunkChars ? chunk.slice(-maxChunkChars) : chunk;
|
|
1096
|
+
const ev = { id, name, stream, chunk: trimmed };
|
|
1097
|
+
safeFireAndForget(() => cb(ev));
|
|
1098
|
+
};
|
|
1099
|
+
const schedule = () => {
|
|
1100
|
+
if (timer)
|
|
1101
|
+
return;
|
|
1102
|
+
const delay = Math.max(0, intervalMs - (Date.now() - lastEmit));
|
|
1103
|
+
timer = setTimeout(() => {
|
|
1104
|
+
timer = null;
|
|
1105
|
+
flush(false);
|
|
1106
|
+
}, delay);
|
|
1107
|
+
};
|
|
1108
|
+
const flush = (force = false) => {
|
|
1109
|
+
const now = Date.now();
|
|
1110
|
+
if (!force && now - lastEmit < intervalMs) {
|
|
1111
|
+
schedule();
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
lastEmit = now;
|
|
1115
|
+
if (outBuf) {
|
|
1116
|
+
emit('stdout', outBuf);
|
|
1117
|
+
outBuf = '';
|
|
1118
|
+
}
|
|
1119
|
+
if (errBuf) {
|
|
1120
|
+
emit('stderr', errBuf);
|
|
1121
|
+
errBuf = '';
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
const push = (stream, textRaw) => {
|
|
1125
|
+
const text = stripAnsi(textRaw).replace(/\r/g, '\n');
|
|
1126
|
+
if (!text)
|
|
1127
|
+
return;
|
|
1128
|
+
if (stream === 'stdout')
|
|
1129
|
+
outBuf += text;
|
|
1130
|
+
else
|
|
1131
|
+
errBuf += text;
|
|
1132
|
+
if (outBuf.length > maxBufferChars)
|
|
1133
|
+
outBuf = outBuf.slice(-maxBufferChars);
|
|
1134
|
+
if (errBuf.length > maxBufferChars)
|
|
1135
|
+
errBuf = errBuf.slice(-maxBufferChars);
|
|
1136
|
+
schedule();
|
|
1137
|
+
};
|
|
1138
|
+
const done = () => {
|
|
1139
|
+
if (timer) {
|
|
1140
|
+
clearTimeout(timer);
|
|
1141
|
+
timer = null;
|
|
1142
|
+
}
|
|
1143
|
+
flush(true);
|
|
1144
|
+
};
|
|
1145
|
+
return { push, done };
|
|
1146
|
+
}
|
|
742
1147
|
export async function exec(ctx, args) {
|
|
743
1148
|
const command = typeof args?.command === 'string' ? args.command : undefined;
|
|
744
1149
|
const cwd = args?.cwd ? resolvePath(ctx, args.cwd) : ctx.cwd;
|
|
@@ -751,7 +1156,7 @@ export async function exec(ctx, args) {
|
|
|
751
1156
|
let execCwdWarning = '';
|
|
752
1157
|
if (args?.cwd) {
|
|
753
1158
|
const absExecCwd = path.resolve(cwd);
|
|
754
|
-
if (!
|
|
1159
|
+
if (!isWithinDir(absExecCwd, absCwd)) {
|
|
755
1160
|
throw new Error(`exec: BLOCKED — cwd "${absExecCwd}" is outside the working directory "${absCwd}". Use relative paths and work within the project directory.`);
|
|
756
1161
|
}
|
|
757
1162
|
}
|
|
@@ -761,7 +1166,7 @@ export async function exec(ctx, args) {
|
|
|
761
1166
|
let cdMatch;
|
|
762
1167
|
while ((cdMatch = cdPattern.exec(command)) !== null) {
|
|
763
1168
|
const cdTarget = path.resolve(cdMatch[2]);
|
|
764
|
-
if (!
|
|
1169
|
+
if (!isWithinDir(cdTarget, absCwd)) {
|
|
765
1170
|
throw new Error(`exec: BLOCKED — command navigates to "${cdTarget}" which is outside the working directory "${absCwd}". Use relative paths and work within the project directory.`);
|
|
766
1171
|
}
|
|
767
1172
|
}
|
|
@@ -771,7 +1176,7 @@ export async function exec(ctx, args) {
|
|
|
771
1176
|
let apMatch;
|
|
772
1177
|
while ((apMatch = absPathPattern.exec(command)) !== null) {
|
|
773
1178
|
const absTarget = path.resolve(apMatch[2]);
|
|
774
|
-
if (!
|
|
1179
|
+
if (!isWithinDir(absTarget, absCwd)) {
|
|
775
1180
|
throw new Error(`exec: BLOCKED — command targets "${absTarget}" which is outside the working directory "${absCwd}". Use relative paths to work within the project directory.`);
|
|
776
1181
|
}
|
|
777
1182
|
}
|
|
@@ -844,7 +1249,8 @@ export async function exec(ctx, args) {
|
|
|
844
1249
|
maxBytes,
|
|
845
1250
|
captureLimit,
|
|
846
1251
|
signal: ctx.signal,
|
|
847
|
-
|
|
1252
|
+
execCwdWarning,
|
|
1253
|
+
});
|
|
848
1254
|
}
|
|
849
1255
|
// Use spawn with shell:true — lets Node.js resolve the shell internally,
|
|
850
1256
|
// avoiding ENOENT issues with explicit bash paths in certain environments.
|
|
@@ -900,8 +1306,15 @@ export async function exec(ctx, args) {
|
|
|
900
1306
|
else
|
|
901
1307
|
errCaptured += take.length;
|
|
902
1308
|
};
|
|
903
|
-
|
|
904
|
-
child.
|
|
1309
|
+
const streamer = makeExecStreamer(ctx);
|
|
1310
|
+
child.stdout.on('data', (d) => {
|
|
1311
|
+
pushCapped(outChunks, d, 'out');
|
|
1312
|
+
streamer?.push('stdout', d.toString('utf8'));
|
|
1313
|
+
});
|
|
1314
|
+
child.stderr.on('data', (d) => {
|
|
1315
|
+
pushCapped(errChunks, d, 'err');
|
|
1316
|
+
streamer?.push('stderr', d.toString('utf8'));
|
|
1317
|
+
});
|
|
905
1318
|
const rc = await new Promise((resolve, reject) => {
|
|
906
1319
|
child.on('error', (err) => {
|
|
907
1320
|
clearTimeout(killTimer);
|
|
@@ -912,6 +1325,7 @@ export async function exec(ctx, args) {
|
|
|
912
1325
|
});
|
|
913
1326
|
clearTimeout(killTimer);
|
|
914
1327
|
ctx.signal?.removeEventListener('abort', onAbort);
|
|
1328
|
+
streamer?.done();
|
|
915
1329
|
const outRaw = stripAnsi(Buffer.concat(outChunks).toString('utf8'));
|
|
916
1330
|
const errRaw = stripAnsi(Buffer.concat(errChunks).toString('utf8'));
|
|
917
1331
|
const outLines = collapseStackTraces(dedupeRepeats(outRaw.split(/\r?\n/))).join('\n').trimEnd();
|
|
@@ -933,15 +1347,21 @@ export async function exec(ctx, args) {
|
|
|
933
1347
|
if (killed) {
|
|
934
1348
|
errText = (errText ? errText + '\n' : '') + `[killed after ${timeout}s timeout]`;
|
|
935
1349
|
}
|
|
936
|
-
const result = {
|
|
1350
|
+
const result = {
|
|
1351
|
+
rc,
|
|
1352
|
+
out: outText,
|
|
1353
|
+
err: errText,
|
|
1354
|
+
truncated: outT.truncated || errT.truncated || capOut || capErr,
|
|
1355
|
+
...(execCwdWarning && { warnings: [execCwdWarning.trim()] })
|
|
1356
|
+
};
|
|
937
1357
|
// Phase 9d: auto-note system changes in sys mode
|
|
938
1358
|
if (ctx.mode === 'sys' && ctx.vault && rc === 0) {
|
|
939
1359
|
autoNoteSysChange(ctx.vault, command, outText).catch(() => { });
|
|
940
1360
|
}
|
|
941
|
-
return JSON.stringify(result)
|
|
1361
|
+
return JSON.stringify(result);
|
|
942
1362
|
}
|
|
943
1363
|
async function execWithPty(args) {
|
|
944
|
-
const { pty, command, cwd, timeout, maxBytes, captureLimit, signal } = args;
|
|
1364
|
+
const { pty, command, cwd, timeout, maxBytes, captureLimit, signal, execCwdWarning } = args;
|
|
945
1365
|
const proc = pty.spawn(BASH_PATH, ['-lc', command], {
|
|
946
1366
|
name: 'xterm-color',
|
|
947
1367
|
cwd,
|
|
@@ -1009,6 +1429,7 @@ async function execWithPty(args) {
|
|
|
1009
1429
|
out: outText,
|
|
1010
1430
|
err: errText,
|
|
1011
1431
|
truncated: outT.truncated || cap || killed,
|
|
1432
|
+
...(execCwdWarning && { warnings: [execCwdWarning.trim()] })
|
|
1012
1433
|
};
|
|
1013
1434
|
return JSON.stringify(result);
|
|
1014
1435
|
}
|
|
@@ -1056,13 +1477,22 @@ function resolvePath(ctx, p) {
|
|
|
1056
1477
|
throw new Error('missing path');
|
|
1057
1478
|
return path.resolve(ctx.cwd, p);
|
|
1058
1479
|
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Check if a target path is within a directory.
|
|
1482
|
+
* Handles the classic root directory edge case: when dir is `/`, every absolute path is valid.
|
|
1483
|
+
*/
|
|
1484
|
+
function isWithinDir(target, dir) {
|
|
1485
|
+
if (dir === '/')
|
|
1486
|
+
return target.startsWith('/');
|
|
1487
|
+
return target === dir || target.startsWith(dir + path.sep);
|
|
1488
|
+
}
|
|
1059
1489
|
/**
|
|
1060
1490
|
* Check if a resolved path is outside the working directory.
|
|
1061
1491
|
* Returns a model-visible warning string if so, empty string otherwise.
|
|
1062
1492
|
*/
|
|
1063
1493
|
function checkCwdWarning(tool, resolvedPath, ctx) {
|
|
1064
1494
|
const absCwd = path.resolve(ctx.cwd);
|
|
1065
|
-
if (
|
|
1495
|
+
if (isWithinDir(resolvedPath, absCwd))
|
|
1066
1496
|
return '';
|
|
1067
1497
|
const warning = `\n[WARNING] Path "${resolvedPath}" is OUTSIDE the working directory "${absCwd}". You MUST use relative paths and work within the project directory. Do NOT create or edit files outside the cwd.`;
|
|
1068
1498
|
console.warn(`[warning] ${tool}: path "${resolvedPath}" is outside the working directory "${absCwd}".`);
|