aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--help.html3
-rw-r--r--main.html6
-rw-r--r--modules/core/core-bundle.js103
-rw-r--r--modules/core/module-fs.js103
-rw-r--r--modules/core/module-loader.js2
5 files changed, 148 insertions, 69 deletions
diff --git a/help.html b/help.html
index 00eff18..4de3a9c 100644
--- a/help.html
+++ b/help.html
@@ -1689,13 +1689,14 @@ listmods</pre>
<h2>๐Ÿ“‹ Changelog</h2>
<div class="version-item">
- <div class="v-header"><span class="v-num">v4.6.57โ€“64 โ€” October 2026</span><span class="v-date">2026-10-23</span></div>
+ <div class="v-header"><span class="v-num">v4.6.57โ€“66 โ€” October 2026</span><span class="v-date">2026-10-23</span></div>
<ul>
<li><strong>No shell โ€” and it now says so</strong> โ€” <code>;</code>, <code>&amp;&amp;</code> and <code>||</code> are refused with an explicit message instead of being folded into the command's argv (where <code>wc -l f &amp;&amp; grep -c x f</code> returned a plausible wrong number). A <code>|</code> arriving from the AI <code>run_command</code> tool is refused too, because that path bypasses the pipe splitter and degraded the pattern to empty. Documented at the top of the command reference.</li>
<li><strong>Multiple paths in <code>wc</code></strong> โ€” <code>wc -c a b</code> printed only <code>a</code> and exited clean. It now prints one line per file plus a <code>total</code>, and reports each missing file separately.</li>
<li><strong><code>ls</code> prints text</strong> โ€” it rendered only clickable chips (DOM-only), so any capture โ€” the AI <code>run_command</code> tool, the diagnostics sweep โ€” saw an empty result and reported "directory empty".</li>
<li><strong><code>view_module</code> shows what is deployed</strong> โ€” it returned the <code>_mods/&lt;Class&gt;.js</code> edit cache first, unversioned, so after a rebuild it showed pre-fix source while the bundle was new. It now fetches the deployed file and prefixes the version; the cache is a labelled fallback.</li>
<li><strong><code>load_skills</code> searches tokens, not one phrase</strong> โ€” the query used to be tested whole against name+description+tags, so <code>"telemetry dashboard"</code> returned 0 while <code>"telemetry"</code> returned 1, and a sentence could never match. Skill <em>bodies</em> were never searched. It now tokenises (stopwords dropped), scores name &gt; description/tags &gt; body, weights by how many tokens matched, keeps a phrase bonus so word order cannot zero a real hit, and reports <code>matchedTokens: 2/3</code> with each result.</li>
+ <li><strong><code>grep</code>, <code>head</code> and <code>tail</code> take several paths</strong> โ€” they read only the first path argument and exited clean on the rest, the same silent drop <code>wc</code> had. Multiple files now print <code>file:line: text</code> (grep) or <code>==&gt; file &lt;==</code> (head/tail), and <code>grep -c</code> prints one count per file. <code>tail -f</code> stays single-file and says so.</li>
<li><strong>An invalid <code>grep</code> pattern is an answer, not a crash</strong> โ€” it used to throw out of the command handler into the error ledger, which is the ledger that has to stay trustworthy. It now prints <code>grep: invalid pattern: โ€ฆ</code> and returns.</li>
<li><strong>Agent status says what it means</strong> โ€” four agents showed <code>sleeping</code> with no way to tell "waiting for work" from "never handed a task". They now read <code>parked โ€” NEVER SCHEDULED (0 tasks since โ€ฆ)</code> or <code>waiting for work (3 tasks, last โ€ฆ)</code>, plus a dispatch line and a warning when spawning happened but nothing was ever dispatched.</li>
<li><strong>A guide you can actually find</strong> โ€” <code>guides</code> lists <code>/guides/</code> newest-first; the help page has a Guides section with the command, the preview/cat hints and links to published guides.</li>
diff --git a/main.html b/main.html
index 57017c5..8a6ee5d 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.65"></script>
+<script src="modules/core/core-bundle.js?v=v4.6.67"></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.65"></script>
+<script src="modules/core/module-loader.js?v=v4.6.67"></script>
<!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work -->
-<script src="modules/ext/critical-ext-bundle.js?v=v4.6.65"></script>
+<script src="modules/ext/critical-ext-bundle.js?v=v4.6.67"></script>
<script>
// โ•โ•โ• ONE TRUE VERSION SOURCE โ•โ•โ•
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index a5e9a79..e4cd832 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -6641,7 +6641,7 @@ class FileSystemModule {
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');
+ if (!pattern) return this.terminal.print('Usage: grep [-n] [-i] [-c] [-l] <pattern> [filename ...]', 'warning');
// v4.6.64: an invalid pattern is user input, not a system fault. It used to throw
// out of the command handler into the error ledger, polluting the counter that
// packet ยง2 says nobody can trust (packet ยง8).
@@ -6653,20 +6653,36 @@ class FileSystemModule {
return;
}
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');
- let count = 0;
- lines.forEach((line, i) => {
- if (!hits(line)) return;
- count++;
- if (!opt.count && !opt.files) this.terminal.print((i + 1) + ': ' + line);
+ // v4.6.66: every path argument is searched. `grep x a b` used to read only `a` and
+ // exit clean โ€” a silent false negative (packet ยง1).
+ const grepFiles = args.slice(ai + 1).filter(Boolean);
+ if (grepFiles.length) {
+ const multi = grepFiles.length > 1;
+ let grand = 0;
+ let readFiles = 0;
+ grepFiles.forEach(file => {
+ const c = this.terminal.fs.readFile(file);
+ if (c === null || c === undefined) {
+ this.terminal.print('grep: ' + file + ': No such file', 'error');
+ return;
+ }
+ readFiles++;
+ let count = 0;
+ c.split('\n').forEach((line, i) => {
+ if (!hits(line)) return;
+ count++;
+ if (opt.count || opt.files) return;
+ this.terminal.print(multi ? (file + ':' + (i + 1) + ': ' + line) : ((i + 1) + ': ' + line));
+ });
+ grand += count;
+ if (opt.files) { if (count) this.terminal.print(file); }
+ else if (opt.count) this.terminal.print(multi ? (file + ':' + count) : String(count));
+ else if (!count && !multi) this.terminal.print('(no matches)', 'info');
});
- if (opt.files && count) this.terminal.print(target);
- if (count === 0) this.terminal.print('(no matches)', 'info');
- else if (opt.count) this.terminal.print(String(count));
- else if (!opt.files) this.terminal.print(count + ' match(es)', 'text-muted');
+ if (!opt.count && !opt.files) {
+ if (grand === 0) this.terminal.print('(no matches)', 'info');
+ else this.terminal.print(grand + ' match(es)' + (multi ? ' in ' + readFiles + ' file(s)' : ''), 'text-muted');
+ }
} else {
const dir = this.terminal.fs.getCurrentDir();
if (!dir) return this.terminal.print('grep: cannot search', 'error');
@@ -6689,18 +6705,25 @@ class FileSystemModule {
};
this.commands['head'] = (args) => {
// 2026-10-21: accept `head -40 f`, `head -n 40 f` and `head f 40`.
- let n = 10, file = null;
+ // v4.6.66: every path argument, not just the first (packet ยง1).
+ let n = 10;
+ const headFiles = [];
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 === '-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'));
+ else if (headFiles.length && /^\d+$/.test(a)) n = parseInt(a, 10); // head f 40
+ else headFiles.push(a);
+ }
+ if (!headFiles.length) return this.terminal.print('Usage: head [-n] <filename> [...] [lines]', 'warning');
+ const headMulti = headFiles.length > 1;
+ headFiles.forEach(file => {
+ const c = this.terminal.fs.readFile(file);
+ if (c === null || c === undefined) return this.terminal.print('head: ' + file + ': No such file', 'error');
+ if (headMulti) this.terminal.print('==> ' + file + ' <==', 'text-muted');
+ this.terminal.print(c.split('\n').slice(0, Math.max(1, n)).join('\n'));
+ });
};
this.commands['tail'] = (args) => {
const self = this;
@@ -6719,17 +6742,33 @@ class FileSystemModule {
else if (a.startsWith('-')) return this.terminal.print('tail: invalid option: ' + a, 'error');
else positional.push(a);
}
- const file = positional[0];
- if (!file) return this.terminal.print('Usage: tail [-f] [-n N] <file> โ€” last N lines; -f follows changes (Ctrl+C or Esc to stop)', 'warning');
- 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');
- // 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'));
+ // v4.6.66: every path argument (packet ยง1). A numeric second positional is still
+ // the legacy `tail <file> <count>`, not a filename.
+ const tailFiles = positional.filter((p, i) => !(i > 0 && /^\d+$/.test(p)));
+ if (!tailFiles.length) return this.terminal.print('Usage: tail [-f] [-n N] <file> [...] โ€” last N lines; -f follows changes (Ctrl+C or Esc to stop)', 'warning');
+ if (positional[1] && /^\d+$/.test(positional[1])) n = parseInt(positional[1], 10); // legacy back-compat
+ const tailMulti = tailFiles.length > 1;
+ let shownFiles = tailFiles;
+ if (follow && tailMulti) {
+ this.terminal.print('tail: -f needs a single file โ€” following ' + tailFiles[0] + ' only', 'warning');
+ shownFiles = [tailFiles[0]];
+ }
+ let firstContent = null;
+ shownFiles.forEach(f2 => {
+ const cc = this.terminal.fs.readFile(f2);
+ if (cc === null || cc === undefined) return this.terminal.print('tail: ' + f2 + ': No such file', 'error');
+ if (shownFiles.length > 1) this.terminal.print('==> ' + f2 + ' <==', 'text-muted');
+ // 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 = cc.split('\n');
+ if (_all.length > 1 && _all[_all.length - 1] === '') _all.pop();
+ this.terminal.print(_all.slice(-n).join('\n'));
+ if (firstContent === null) firstContent = cc;
+ });
+ const file = tailFiles[0];
+ const c = firstContent === null ? '' : firstContent;
if (!follow) return;
+ if (c === '') return; // v4.6.66: the only file was unreadable โ€” nothing to follow
// 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) {
return this.terminal.print('tail: follow mode (-f) is interactive-only; showing last lines instead (use tail -n ' + Math.max(n, 50) + ' for more)', 'warning');
diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js
index 415bd92..f2b215a 100644
--- a/modules/core/module-fs.js
+++ b/modules/core/module-fs.js
@@ -237,7 +237,7 @@ class FileSystemModule {
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');
+ if (!pattern) return this.terminal.print('Usage: grep [-n] [-i] [-c] [-l] <pattern> [filename ...]', 'warning');
// v4.6.64: an invalid pattern is user input, not a system fault. It used to throw
// out of the command handler into the error ledger, polluting the counter that
// packet ยง2 says nobody can trust (packet ยง8).
@@ -249,20 +249,36 @@ class FileSystemModule {
return;
}
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');
- let count = 0;
- lines.forEach((line, i) => {
- if (!hits(line)) return;
- count++;
- if (!opt.count && !opt.files) this.terminal.print((i + 1) + ': ' + line);
+ // v4.6.66: every path argument is searched. `grep x a b` used to read only `a` and
+ // exit clean โ€” a silent false negative (packet ยง1).
+ const grepFiles = args.slice(ai + 1).filter(Boolean);
+ if (grepFiles.length) {
+ const multi = grepFiles.length > 1;
+ let grand = 0;
+ let readFiles = 0;
+ grepFiles.forEach(file => {
+ const c = this.terminal.fs.readFile(file);
+ if (c === null || c === undefined) {
+ this.terminal.print('grep: ' + file + ': No such file', 'error');
+ return;
+ }
+ readFiles++;
+ let count = 0;
+ c.split('\n').forEach((line, i) => {
+ if (!hits(line)) return;
+ count++;
+ if (opt.count || opt.files) return;
+ this.terminal.print(multi ? (file + ':' + (i + 1) + ': ' + line) : ((i + 1) + ': ' + line));
+ });
+ grand += count;
+ if (opt.files) { if (count) this.terminal.print(file); }
+ else if (opt.count) this.terminal.print(multi ? (file + ':' + count) : String(count));
+ else if (!count && !multi) this.terminal.print('(no matches)', 'info');
});
- if (opt.files && count) this.terminal.print(target);
- if (count === 0) this.terminal.print('(no matches)', 'info');
- else if (opt.count) this.terminal.print(String(count));
- else if (!opt.files) this.terminal.print(count + ' match(es)', 'text-muted');
+ if (!opt.count && !opt.files) {
+ if (grand === 0) this.terminal.print('(no matches)', 'info');
+ else this.terminal.print(grand + ' match(es)' + (multi ? ' in ' + readFiles + ' file(s)' : ''), 'text-muted');
+ }
} else {
const dir = this.terminal.fs.getCurrentDir();
if (!dir) return this.terminal.print('grep: cannot search', 'error');
@@ -285,18 +301,25 @@ class FileSystemModule {
};
this.commands['head'] = (args) => {
// 2026-10-21: accept `head -40 f`, `head -n 40 f` and `head f 40`.
- let n = 10, file = null;
+ // v4.6.66: every path argument, not just the first (packet ยง1).
+ let n = 10;
+ const headFiles = [];
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 === '-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'));
+ else if (headFiles.length && /^\d+$/.test(a)) n = parseInt(a, 10); // head f 40
+ else headFiles.push(a);
+ }
+ if (!headFiles.length) return this.terminal.print('Usage: head [-n] <filename> [...] [lines]', 'warning');
+ const headMulti = headFiles.length > 1;
+ headFiles.forEach(file => {
+ const c = this.terminal.fs.readFile(file);
+ if (c === null || c === undefined) return this.terminal.print('head: ' + file + ': No such file', 'error');
+ if (headMulti) this.terminal.print('==> ' + file + ' <==', 'text-muted');
+ this.terminal.print(c.split('\n').slice(0, Math.max(1, n)).join('\n'));
+ });
};
this.commands['tail'] = (args) => {
const self = this;
@@ -315,17 +338,33 @@ class FileSystemModule {
else if (a.startsWith('-')) return this.terminal.print('tail: invalid option: ' + a, 'error');
else positional.push(a);
}
- const file = positional[0];
- if (!file) return this.terminal.print('Usage: tail [-f] [-n N] <file> โ€” last N lines; -f follows changes (Ctrl+C or Esc to stop)', 'warning');
- 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');
- // 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'));
+ // v4.6.66: every path argument (packet ยง1). A numeric second positional is still
+ // the legacy `tail <file> <count>`, not a filename.
+ const tailFiles = positional.filter((p, i) => !(i > 0 && /^\d+$/.test(p)));
+ if (!tailFiles.length) return this.terminal.print('Usage: tail [-f] [-n N] <file> [...] โ€” last N lines; -f follows changes (Ctrl+C or Esc to stop)', 'warning');
+ if (positional[1] && /^\d+$/.test(positional[1])) n = parseInt(positional[1], 10); // legacy back-compat
+ const tailMulti = tailFiles.length > 1;
+ let shownFiles = tailFiles;
+ if (follow && tailMulti) {
+ this.terminal.print('tail: -f needs a single file โ€” following ' + tailFiles[0] + ' only', 'warning');
+ shownFiles = [tailFiles[0]];
+ }
+ let firstContent = null;
+ shownFiles.forEach(f2 => {
+ const cc = this.terminal.fs.readFile(f2);
+ if (cc === null || cc === undefined) return this.terminal.print('tail: ' + f2 + ': No such file', 'error');
+ if (shownFiles.length > 1) this.terminal.print('==> ' + f2 + ' <==', 'text-muted');
+ // 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 = cc.split('\n');
+ if (_all.length > 1 && _all[_all.length - 1] === '') _all.pop();
+ this.terminal.print(_all.slice(-n).join('\n'));
+ if (firstContent === null) firstContent = cc;
+ });
+ const file = tailFiles[0];
+ const c = firstContent === null ? '' : firstContent;
if (!follow) return;
+ if (c === '') return; // v4.6.66: the only file was unreadable โ€” nothing to follow
// 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) {
return this.terminal.print('tail: follow mode (-f) is interactive-only; showing last lines instead (use tail -n ' + Math.max(n, 50) + ' for more)', 'warning');
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index e3905ee..badaeed 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.65';
+ const LATEST_VER = 'v4.6.67';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {