From df6c4ec7ef576e188b68ef8e64eb1f94431b5a1c Mon Sep 17 00:00:00 2001 From: Agando Date: Thu, 22 Oct 2026 00:43:13 +0200 Subject: fix(newos4): coreutils flags + loop-guard arg key, both found by the agent's live run (v4.6.49) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent session surfaced two real defects: 1. The A4 prompt told it to use grep -n / sed -n 'N,Mp' / wc -l / head -N — none of those flags parsed: grep treated '-n' as the pattern (6/6 variants failed), wc read '-l' as a filename, head looked for a file named '-2', sed had no range parser. Fixed the tools, not just the text: grep now accepts -n/-i/-c/-v/-l (line numbers were already printed), wc accepts -l/-w/-c, head accepts -N and -n N, sed accepts Np and N,Mp, tail no longer prints a blank line for a trailing newline. Prompt re-ranked to the working forms. 2. Loop guard keyed calls by tool name plus the raw arguments field. When arguments was already an object (parsed upstream), the key became 'run_command:[object Object]', collapsing distinct batched commands into one signature and producing false "called 3x" warnings. Now the key is JSON.stringify(arguments) whenever it is not a string (3 sites: sig, _sigR, _sigD). Verified live v4.6.49: grep -n/-c, wc -l, head -2, sed -n '2,3p', tail -n 1 all correct. --- modules/core/module-fs.js | 103 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 19 deletions(-) (limited to 'modules/core/module-fs.js') diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js index 2b525f0..686a1a6 100644 --- a/modules/core/module-fs.js +++ b/modules/core/module-fs.js @@ -200,37 +200,72 @@ class FileSystemModule { this.terminal.print('Found ' + results.length + ' match(es)', 'info'); }; this.commands['grep'] = (args) => { - if(!args[0]) return this.terminal.print('Usage: grep [filename]', 'warning'); - const pattern = args[0]; const target = args[1]; + // 2026-10-21: accept the common GNU flags instead of treating them as the pattern. + // `grep -n "x" file` used to search for the literal "-n" inside a file named "x" + // (6/6 syntax variants failed for the agent), while plain grep already prints + // "line: text" for every hit. + const opt = { count: false, invert: false, files: false, ignoreCase: true }; + let ai = 0; + while (ai < args.length && /^-[a-zA-Z]+$/.test(args[ai])) { + const f = args[ai].slice(1); + if (f.indexOf('c') !== -1) opt.count = true; + if (f.indexOf('v') !== -1) opt.invert = true; + if (f.indexOf('l') !== -1) opt.files = true; + if (f.indexOf('i') !== -1) opt.ignoreCase = true; + ai++; + } + const pattern = args[ai]; const target = args[ai + 1]; + if (!pattern) return this.terminal.print('Usage: grep [-n] [-i] [-c] [-l] [filename]', 'warning'); + const re = new RegExp(pattern, opt.ignoreCase ? 'i' : ''); + const hits = (line) => (opt.invert ? !re.test(line) : re.test(line)); if (target) { const c = this.terminal.fs.readFile(target); - if(c === null || c === undefined) return this.terminal.print('grep: ' + target + ': No such file', 'error'); - const lines = c.split('\n'); const re = new RegExp(pattern, 'i'); - let count = 0; lines.forEach((line, i) => { if (re.test(line)) { this.terminal.print((i+1) + ': ' + line); count++; } }); + if (c === null || c === undefined) return this.terminal.print('grep: ' + target + ': No such file', 'error'); + const lines = c.split('\n'); + let count = 0; + lines.forEach((line, i) => { + if (!hits(line)) return; + count++; + if (!opt.count && !opt.files) this.terminal.print((i + 1) + ': ' + line); + }); + if (opt.files && count) this.terminal.print(target); if (count === 0) this.terminal.print('(no matches)', 'info'); - else this.terminal.print(count + ' match(es)', 'text-muted'); + else if (opt.count) this.terminal.print(String(count)); + else if (!opt.files) this.terminal.print(count + ' match(es)', 'text-muted'); } else { const dir = this.terminal.fs.getCurrentDir(); if (!dir) return this.terminal.print('grep: cannot search', 'error'); - let total = 0; const re = new RegExp(pattern, 'i'); + let total = 0; Object.keys(dir.content).forEach(name => { const node = dir.content[name]; if (node.type === 'file') { (node.content || '').split('\n').forEach((line, i) => { - if (re.test(line)) { this.terminal.print(name + ':' + (i+1) + ': ' + line); total++; } + if (!hits(line)) return; + total++; + if (!opt.count && !opt.files) this.terminal.print(name + ':' + (i + 1) + ': ' + line); }); } }); + if (opt.files && total) this.terminal.print(dir.name || '.'); if (total === 0) this.terminal.print('(no matches)', 'info'); - else this.terminal.print(total + ' total match(es)', 'text-muted'); + else if (opt.count) this.terminal.print(String(total)); + else if (!opt.files) this.terminal.print(total + ' total match(es)', 'text-muted'); } }; this.commands['head'] = (args) => { - if(!args[0]) return this.terminal.print('Usage: head [lines]', 'warning'); - const c = this.terminal.fs.readFile(args[0]); - if(c === null || c === undefined) return this.terminal.print('head: ' + args[0] + ': No such file', 'error'); - const n = args[1] ? parseInt(args[1]) : 10; - this.terminal.print(c.split('\n').slice(0, n).join('\n')); + // 2026-10-21: accept `head -40 f`, `head -n 40 f` and `head f 40`. + let n = 10, file = null; + for (let i = 0; i < (args || []).length; i++) { + const a = args[i]; + if (/^-\d+$/.test(a)) n = parseInt(a.slice(1), 10); + else if (a === '-n') { const v = parseInt(args[i + 1], 10); if (!isNaN(v)) n = v; i++; } + else if (/^-[a-zA-Z]+$/.test(a)) { /* ignore other flags */ } + else if (!file) file = a; + } + if (!file) return this.terminal.print('Usage: head [-n] [lines]', 'warning'); + const c = this.terminal.fs.readFile(file); + if (c === null || c === undefined) return this.terminal.print('head: ' + file + ': No such file', 'error'); + this.terminal.print(c.split('\n').slice(0, Math.max(1, n)).join('\n')); }; this.commands['tail'] = (args) => { const self = this; @@ -254,7 +289,11 @@ class FileSystemModule { if (positional[1] && /^\d+$/.test(positional[1])) n = parseInt(positional[1], 10); // legacy back-compat: tail const c = this.terminal.fs.readFile(file); if (c === null || c === undefined) return this.terminal.print('tail: ' + file + ': No such file', 'error'); - this.terminal.print(c.split('\n').slice(-n).join('\n')); + // 2026-10-21: a trailing newline produced an empty last element, so `tail -n 1` + // printed a blank line instead of the final line. + const _all = c.split('\n'); + if (_all.length > 1 && _all[_all.length - 1] === '') _all.pop(); + this.terminal.print(_all.slice(-n).join('\n')); if (!follow) return; // AI tool calls cannot press Ctrl+C — auto-downgrade to a bounded read instead of a live timer if (this.terminal.security && this.terminal.security._aiToolDepth > 0) { @@ -308,6 +347,16 @@ class FileSystemModule { return this.terminal.print('sed: missing file operand (no piped input)', 'error'); } let subst = null, delMatch = null, delLine = null; + // 2026-10-21: `sed -n '10,40p' file` and `sed -n '7p' file` had no parser, so the + // most common "show me lines N–M" idiom failed with an unsupported-expression error. + const _range = /^(\d+)(?:,(\d+))?p$/.exec(expr); + if (_range) { + const _a = parseInt(_range[1], 10); + const _b = _range[2] ? parseInt(_range[2], 10) : _a; + const _lines = content.split('\n'); + this.terminal.print(_lines.slice(Math.max(0, _a - 1), Math.max(_a, _b)).join('\n')); + return; + } if (expr.startsWith('s/') && expr.length > 2) { const parts = []; let cur = ''; for (let i = 2; i < expr.length; i++) { @@ -381,12 +430,28 @@ class FileSystemModule { c.split('\n').sort().forEach(l => this.terminal.print(l)); }; this.commands['wc'] = (args) => { - if(!args[0]) return this.terminal.print('Usage: wc ', 'warning'); - const content = this.terminal.fs.readFile(args[0]); - if(content === null || content === undefined) return this.terminal.print('wc: ' + args[0] + ': No such file', 'error'); + // 2026-10-21: accept -l / -w / -c (previously ANY flag was read as a filename, + // so `wc -l file` answered "wc: -l: No such file"). + const want = { l: false, w: false, c: false }; + let file = null; + (args || []).forEach(a => { + if (/^-[lwc]+$/.test(a)) a.slice(1).split('').forEach(k => { want[k] = true; }); + else if (/^-[a-zA-Z]+$/.test(a)) { /* ignore other flags */ } + else if (!file) file = a; + }); + if (!file) return this.terminal.print('Usage: wc [-l] [-w] [-c] ', 'warning'); + const content = this.terminal.fs.readFile(file); + if (content === null || content === undefined) return this.terminal.print('wc: ' + file + ': No such file', 'error'); const lines = content.split('\n').length; const words = content.split(/\s+/).filter(w => w).length; - this.terminal.print(lines + ' ' + words + ' ' + content.length + ' ' + args[0]); + const chars = content.length; + const anyFlag = want.l || want.w || want.c; + const out = []; + if (want.l || !anyFlag) out.push(lines); + if (want.w || !anyFlag) out.push(words); + if (want.c || !anyFlag) out.push(chars); + if (anyFlag && !(want.l && want.w && want.c)) out.push(file); + this.terminal.print(out.join(' ')); }; this.commands['touch'] = (args) => { if(!args[0]) return this.terminal.print('Usage: touch ', 'warning'); -- cgit v1.2.3