aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-22 00:43:13 +0200
committerAgando <roduccomm@gmail.com>2026-10-22 00:43:24 +0200
commit14c3cb36a3b037111a9fbdf8ff6ea32ee62b3be1 (patch)
tree0802e16ee1fe2538bcf783c7a4c44e3c5f8ed5c1
parent0c45056c7c51daae9b99c9395b94196537c7d32d (diff)
fix(newos4): coreutils flags + loop-guard arg key, both found by the agent's live run (v4.6.49)
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.
-rw-r--r--main.html6
-rw-r--r--modules/core/core-bundle.js120
-rw-r--r--modules/core/module-aiassist.js17
-rw-r--r--modules/core/module-fs.js103
-rw-r--r--modules/core/module-loader.js2
5 files changed, 196 insertions, 52 deletions
diff --git a/main.html b/main.html
index c3b9dfd..636a0a1 100644
--- a/main.html
+++ b/main.html
@@ -1349,13 +1349,13 @@
</script>
<!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix -->
-<script src="modules/core/core-bundle.js?v=v4.6.47"></script>
+<script src="modules/core/core-bundle.js?v=v4.6.49"></script>
<!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules -->
-<script src="modules/core/module-loader.js?v=v4.6.47"></script>
+<script src="modules/core/module-loader.js?v=v4.6.49"></script>
<!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work -->
-<script src="modules/ext/critical-ext-bundle.js?v=v4.6.47"></script>
+<script src="modules/ext/critical-ext-bundle.js?v=v4.6.49"></script>
<script>
// ═══ ONE TRUE VERSION SOURCE ═══
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index c4bf64e..f467f84 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -6469,37 +6469,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 <pattern> [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] <pattern> [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 <filename> [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] <filename> [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;
@@ -6523,7 +6558,11 @@ class FileSystemModule {
if (positional[1] && /^\d+$/.test(positional[1])) n = parseInt(positional[1], 10); // legacy back-compat: tail <file> <count>
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) {
@@ -6577,6 +6616,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++) {
@@ -6650,12 +6699,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 <filename>', '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] <filename>', '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 <filename>', 'warning');
@@ -8219,7 +8284,7 @@ buildSystemPrompt() {
} catch (e) {}
base += '\n\n[File reads, tools, verification]\n' +
'- read_file returns 8000 chars by default (limit up to 16000). If the reply says truncated=true, call read_file again with offset=<next_offset> before concluding anything about the file.\n' +
- '- Prefer targeted reads over whole files: run_command with grep -n "<pattern>" <file>, sed -n \'10,40p\' <file>, head -40 <file>, tail -40 <file>, wc -l <file>.\n' +
+ '- Prefer targeted reads over whole files: run_command with grep "<pattern>" <file> (prints line: text), sed -n \'10,40p\' <file>, head -40 <file>, tail -n 40 <file>, wc -l <file>. Flags are tolerated, but plain `cat <file>` and read_file offset/limit always work.\n' +
'- sed without -i is read-only and allowed; sed -i, rm, mv and writes need user confirmation.\n' +
'- After ANY write or edit: re-read the changed lines (cat or sed -n) and run a check that exists in THIS terminal — `test` for the suite, `grep -n` to confirm the new line, `wc -l` for size. There is no shell, no node and no curl here.\n' +
'- If a tool fails twice, change approach (different tool, smaller scope) instead of retrying the same call.';
@@ -9637,7 +9702,14 @@ buildSystemPrompt() {
const now = Date.now();
const blocked = [];
for (const tc of res.tool_calls) {
- const sig = tc.function.name + ':' + (tc.function.arguments || '');
+ // 2026-10-21: arguments can be an object (already parsed upstream).
+ // `+ object` produced "name:[object Object]", collapsing every call to
+ // the same tool into one key → false "called 3x" loop warnings when the
+ // model legitimately batched distinct commands.
+ const _argKey = (typeof tc.function.arguments === 'string')
+ ? tc.function.arguments
+ : JSON.stringify(tc.function.arguments || {});
+ const sig = tc.function.name + ':' + _argKey;
let entry = _seenCalls.get(sig);
if (!entry || (now - entry.ts) > _loopWindow) {
entry = { count: 0, ts: now, resultHash: null, blocked: false, lastResult: null, epoch: this._toolEpoch };
@@ -9652,7 +9724,7 @@ buildSystemPrompt() {
entry.ts = now;
_seenCalls.set(sig, entry);
- const argPreview = String(tc.function.arguments || '')
+ const argPreview = String(_argKey || '')
.replace(/(token|key|secret|password|bearer)[^,}]*/ig, '$1:[redacted]')
.slice(0, 60);
if (entry.blocked || entry.count >= _blockAt) {
@@ -9765,7 +9837,7 @@ buildSystemPrompt() {
this.terminal.print(`📊 Result (${t1}s): ${obs}`, 'info');
}
try {
- const _sigR = fn.name + ':' + (fn.arguments || '');
+ const _sigR = fn.name + ':' + ((typeof fn.arguments === 'string') ? fn.arguments : JSON.stringify(fn.arguments || {}));
const _eR = _seenCalls.get(_sigR);
if (_eR) {
const _rh = _sigHash(toolResult);
@@ -9840,7 +9912,7 @@ buildSystemPrompt() {
this.terminal.print(`📊 Result (${t1}s): ${dObs}`, 'info');
}
try {
- const _sigD = fn.name + ':' + (fn.arguments || '');
+ const _sigD = fn.name + ':' + ((typeof fn.arguments === 'string') ? fn.arguments : JSON.stringify(fn.arguments || {}));
const _eD = _seenCalls.get(_sigD);
if (_eD) {
const _rhD = _sigHash(toolResult);
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index 8d1fc9d..f69494e 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -122,7 +122,7 @@ buildSystemPrompt() {
} catch (e) {}
base += '\n\n[File reads, tools, verification]\n' +
'- read_file returns 8000 chars by default (limit up to 16000). If the reply says truncated=true, call read_file again with offset=<next_offset> before concluding anything about the file.\n' +
- '- Prefer targeted reads over whole files: run_command with grep -n "<pattern>" <file>, sed -n \'10,40p\' <file>, head -40 <file>, tail -40 <file>, wc -l <file>.\n' +
+ '- Prefer targeted reads over whole files: run_command with grep "<pattern>" <file> (prints line: text), sed -n \'10,40p\' <file>, head -40 <file>, tail -n 40 <file>, wc -l <file>. Flags are tolerated, but plain `cat <file>` and read_file offset/limit always work.\n' +
'- sed without -i is read-only and allowed; sed -i, rm, mv and writes need user confirmation.\n' +
'- After ANY write or edit: re-read the changed lines (cat or sed -n) and run a check that exists in THIS terminal — `test` for the suite, `grep -n` to confirm the new line, `wc -l` for size. There is no shell, no node and no curl here.\n' +
'- If a tool fails twice, change approach (different tool, smaller scope) instead of retrying the same call.';
@@ -1540,7 +1540,14 @@ buildSystemPrompt() {
const now = Date.now();
const blocked = [];
for (const tc of res.tool_calls) {
- const sig = tc.function.name + ':' + (tc.function.arguments || '');
+ // 2026-10-21: arguments can be an object (already parsed upstream).
+ // `+ object` produced "name:[object Object]", collapsing every call to
+ // the same tool into one key → false "called 3x" loop warnings when the
+ // model legitimately batched distinct commands.
+ const _argKey = (typeof tc.function.arguments === 'string')
+ ? tc.function.arguments
+ : JSON.stringify(tc.function.arguments || {});
+ const sig = tc.function.name + ':' + _argKey;
let entry = _seenCalls.get(sig);
if (!entry || (now - entry.ts) > _loopWindow) {
entry = { count: 0, ts: now, resultHash: null, blocked: false, lastResult: null, epoch: this._toolEpoch };
@@ -1555,7 +1562,7 @@ buildSystemPrompt() {
entry.ts = now;
_seenCalls.set(sig, entry);
- const argPreview = String(tc.function.arguments || '')
+ const argPreview = String(_argKey || '')
.replace(/(token|key|secret|password|bearer)[^,}]*/ig, '$1:[redacted]')
.slice(0, 60);
if (entry.blocked || entry.count >= _blockAt) {
@@ -1668,7 +1675,7 @@ buildSystemPrompt() {
this.terminal.print(`📊 Result (${t1}s): ${obs}`, 'info');
}
try {
- const _sigR = fn.name + ':' + (fn.arguments || '');
+ const _sigR = fn.name + ':' + ((typeof fn.arguments === 'string') ? fn.arguments : JSON.stringify(fn.arguments || {}));
const _eR = _seenCalls.get(_sigR);
if (_eR) {
const _rh = _sigHash(toolResult);
@@ -1743,7 +1750,7 @@ buildSystemPrompt() {
this.terminal.print(`📊 Result (${t1}s): ${dObs}`, 'info');
}
try {
- const _sigD = fn.name + ':' + (fn.arguments || '');
+ const _sigD = fn.name + ':' + ((typeof fn.arguments === 'string') ? fn.arguments : JSON.stringify(fn.arguments || {}));
const _eD = _seenCalls.get(_sigD);
if (_eD) {
const _rhD = _sigHash(toolResult);
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 <pattern> [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] <pattern> [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 <filename> [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] <filename> [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 <file> <count>
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 <filename>', '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] <filename>', '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 <filename>', 'warning');
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index ccd92d8..8330eaf 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -4,7 +4,7 @@
// Core module bundle loaded synchronously in main.html.
// This loader only handles version cache-busting, splash, terminal init, and optional ext modules.
- const LATEST_VER = 'v4.6.47';
+ const LATEST_VER = 'v4.6.49';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {