aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-09-29 13:11:48 +0200
committerAgando <roduccomm@gmail.com>2026-09-29 13:11:48 +0200
commit1b35dfa9ae70edbac0d474c79578661f973877e6 (patch)
treefb5600e17bc82a377bac302b7dc8c430ec0f9e91
parentd28f14aa11dad81c94d6487446f1af3fc4d943c6 (diff)
newOS4 v4.6.12: rebuilt bundles with tail -f and sed
-rw-r--r--main.html6
-rw-r--r--modules/core/core-bundle.js171
2 files changed, 162 insertions, 15 deletions
diff --git a/main.html b/main.html
index b4259ed..1c84e8a 100644
--- a/main.html
+++ b/main.html
@@ -1345,13 +1345,13 @@
</script>
<!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix -->
-<script src="modules/core/core-bundle.js?v=v4.6.11"></script>
+<script src="modules/core/core-bundle.js?v=v4.6.12"></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.11"></script>
+<script src="modules/core/module-loader.js?v=v4.6.12"></script>
<!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work -->
-<script src="modules/ext/critical-ext-bundle.js?v=v4.6.11"></script>
+<script src="modules/ext/critical-ext-bundle.js?v=v4.6.12"></script>
<script>
// ═══ ONE TRUE VERSION SOURCE ═══
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index 387afdf..cfdfe11 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -2436,7 +2436,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
this._baseDestructive = ['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot'];
this._baseSystem = ['reboot','shutdown','format','kill','halt','poweroff'];
this._baseSuspiciousFlags = ['--dump','--execute','--eval','--exec','--shell','--script'];
- this._baseWhitelist = ['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','reset','factory-reset','reset-vfs','js','js-run','node','debug','debug-ai','debug-stats','debug-history','debug-analyze','debug-trends','debug-fix','debug-health','session-new','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','dashboard','context-auto'];
+ this._baseWhitelist = ['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','sed','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','reset','factory-reset','reset-vfs','js','js-run','node','debug','debug-ai','debug-stats','debug-history','debug-analyze','debug-trends','debug-fix','debug-health','session-new','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','dashboard','context-auto'];
this.destructiveCmds = new Set(this._baseDestructive);
this.systemCmds = new Set(this._baseSystem);
this.suspiciousFlags = new Set(this._baseSuspiciousFlags);
@@ -2719,6 +2719,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
this.registerConfigItem('terminal-sysmsg', true, (v) => { this._showSystemMessages = !!v; });
this._pipeInput = null;
this._pipeBufferSize = 5000;
+ this._activeFollow = null;
this._historyBuffer = [];
this._historyMaxLines = 50;
this._historySavePending = false;
@@ -2776,6 +2777,15 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
_invalidateCommandCache() {
if (this.security) this.security.invalidateCommandCache();
}
+ stopFollow(msg) {
+ if (!this._activeFollow) return false;
+ clearInterval(this._activeFollow.interval);
+ const f = this._activeFollow.file;
+ this._activeFollow = null;
+ if (msg) this.print(msg, 'warning');
+ else this.print('^C — stopped following ' + f, 'warning');
+ return true;
+ }
initEventListeners() {
this.inputField.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
@@ -2838,13 +2848,16 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
// Ctrl+L — clear output (standard terminal)
e.preventDefault();
this.clearOutput();
- } else if (e.ctrlKey && (e.key === 'c' || e.key === 'C') && this.inputField.value) {
- // Ctrl+C — cancel current input line (standard terminal)
+ } else if (e.ctrlKey && (e.key === 'c' || e.key === 'C')) {
+ // Ctrl+C — stop tail -f if active, else cancel current input line (standard terminal)
e.preventDefault();
- this.print('^C', 'warning');
- this.inputField.value = '';
- this.inputField.style.height = 'auto';
- this.hideAutocomplete();
+ if (this.stopFollow()) return;
+ if (this.inputField.value) {
+ this.print('^C', 'warning');
+ this.inputField.value = '';
+ this.inputField.style.height = 'auto';
+ this.hideAutocomplete();
+ }
return;
} else if (e.ctrlKey && (e.key === 'u' || e.key === 'U')) {
// Ctrl+U — clear line from cursor to start (standard terminal)
@@ -2854,6 +2867,8 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
} else if (e.key === 'Escape') {
if (this._aiBusy) {
this.executeCommand('ai-stop');
+ } else if (this.stopFollow()) {
+ // tail -f stopped
} else {
this.hideAutocomplete();
}
@@ -6154,7 +6169,8 @@ class FileSystemModule {
'find': 'find <pattern> — Search files by name pattern',
'grep': 'grep <pattern> [file] — Search file content for pattern',
'head': 'head <file> [lines] — Show first N lines (default: 10)',
- 'tail': 'tail <file> [lines] — Show last N lines (default: 10)',
+ 'tail': 'tail [-f] [-n N] <file> — Show last N lines; -f follows live changes (Ctrl+C/Esc to stop)',
+ 'sed': 'sed <expr> [file] — Stream editor; s/pat/rep/[g][i][p], /pat/d, Nd, $d; -i write in place; -n quiet mode',
'sort': 'sort <file> — Sort lines of a file alphabetically',
'wc': 'wc <file> — Count lines, words, and bytes',
'touch': 'touch <file> — Create empty file or update timestamp',
@@ -6363,11 +6379,142 @@ class FileSystemModule {
this.terminal.print(c.split('\n').slice(0, n).join('\n'));
};
this.commands['tail'] = (args) => {
- if(!args[0]) return this.terminal.print('Usage: tail <filename> [lines]', 'warning');
- const c = this.terminal.fs.readFile(args[0]);
- if(c === null || c === undefined) return this.terminal.print('tail: ' + args[0] + ': No such file', 'error');
- const n = args[1] ? parseInt(args[1]) : 10;
+ const self = this;
+ let follow = false, n = 10;
+ const positional = [];
+ for (let i = 0; i < (args || []).length; i++) {
+ const a = args[i];
+ if (a === '-f' || a === '--follow') follow = true;
+ else if (a === '-n') {
+ const v = args[++i];
+ if (v === undefined || !/^\d+$/.test(v)) return this.terminal.print('tail: -n requires a number', 'error');
+ n = parseInt(v, 10);
+ }
+ else if (/^-\d+$/.test(a)) n = parseInt(a.slice(1), 10);
+ else if (a === '-c' || a === '-q' || a === '-v' || a === '--pid') return this.terminal.print('tail: option ' + a + ' not supported', 'error');
+ 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');
this.terminal.print(c.split('\n').slice(-n).join('\n'));
+ if (!follow) return;
+ // stop any previous follow before starting a new one
+ if (this.terminal._activeFollow && this.terminal._activeFollow.interval) clearInterval(this.terminal._activeFollow.interval);
+ this.terminal._activeFollow = null;
+ let offset = c.length;
+ const interval = setInterval(() => {
+ const cur = self.terminal.fs.readFile(file);
+ if (cur === null || cur === undefined) {
+ self.terminal.stopFollow('tail: ' + file + ': file removed — follow stopped');
+ return;
+ }
+ if (cur.length < offset) {
+ self.terminal.print('tail: ' + file + ': file truncated — following from start', 'warning');
+ self.terminal.print(cur);
+ offset = cur.length;
+ return;
+ }
+ if (cur.length > offset) {
+ self.terminal.print(cur.slice(offset));
+ offset = cur.length;
+ }
+ }, 1500);
+ this.terminal._activeFollow = { interval, file };
+ this.terminal.print('tail: following ' + file + ' (Ctrl+C or Esc to stop)', 'text-muted');
+ };
+ this.commands['sed'] = (args) => {
+ if (!args || args.length === 0) {
+ return this.terminal.print('Usage: sed <expr> [file] — expressions: s/pat/rep/[g][i][p], /pat/d, Nd, $d; flags: -i in place, -n quiet', 'warning');
+ }
+ let inPlace = false, quiet = false, expr = null, file = null;
+ for (let i = 0; i < args.length; i++) {
+ const a = args[i];
+ if (a === '-i') inPlace = true;
+ else if (a === '-n') quiet = true;
+ else if (expr === null) expr = a;
+ else if (file === null) file = a;
+ else return this.terminal.print('sed: too many arguments: ' + a, 'error');
+ }
+ if (!expr) return this.terminal.print('sed: missing expression', 'error');
+ let content;
+ if (file) {
+ content = this.terminal.fs.readFile(file);
+ if (content === null || content === undefined) return this.terminal.print('sed: ' + file + ': No such file', 'error');
+ } else if (this.terminal._pipeInput !== null && this.terminal._pipeInput !== undefined) {
+ content = this.terminal._pipeInput; // piped input: cat x | sed s/a/b/
+ } else {
+ return this.terminal.print('sed: missing file operand (no piped input)', 'error');
+ }
+ let subst = null, delMatch = null, delLine = null;
+ if (expr.startsWith('s/') && expr.length > 2) {
+ const parts = []; let cur = '';
+ for (let i = 2; i < expr.length; i++) {
+ const ch = expr[i];
+ if (ch === '\\' && expr[i + 1] === '/') { cur += '/'; i++; }
+ else if (ch === '/') { parts.push(cur); cur = ''; }
+ else cur += ch;
+ }
+ parts.push(cur);
+ if (parts.length < 2 || parts.length > 3) return this.terminal.print('sed: malformed expression: ' + expr, 'error');
+ const flags = parts[2] || '';
+ if ([...flags].some(c => !'gip'.includes(c))) return this.terminal.print('sed: unsupported flags: ' + flags, 'error');
+ subst = { pat: parts[0], rep: parts[1], flags };
+ } else if (/^\/(.+)\/d$/.test(expr)) {
+ delMatch = expr.slice(1, -2);
+ } else if (/^(\d+|\$)d$/.test(expr)) {
+ delLine = expr.slice(0, -1);
+ } else {
+ return this.terminal.print('sed: unsupported expression: ' + expr, 'error');
+ }
+ let lines = content.split('\n');
+ let trailingNL = false;
+ if (lines.length && lines[lines.length - 1] === '') { trailingNL = true; lines.pop(); }
+ const outLines = [];
+ let changed = 0;
+ if (subst) {
+ let re;
+ try {
+ re = new RegExp(subst.pat, (subst.flags.includes('i') ? 'i' : '') + (subst.flags.includes('g') ? 'g' : ''));
+ } catch (e) {
+ return this.terminal.print('sed: invalid regex: ' + e.message, 'error');
+ }
+ for (const line of lines) {
+ const after = line.replace(re, subst.rep);
+ if (after !== line) changed++;
+ if (quiet) {
+ if (subst.flags.includes('p') && after !== line) this.terminal.print(after);
+ } else {
+ outLines.push(after);
+ }
+ }
+ } else if (delMatch !== null) {
+ let re;
+ try { re = new RegExp(delMatch); } catch (e) { return this.terminal.print('sed: invalid regex: ' + e.message, 'error'); }
+ for (const line of lines) {
+ if (re.test(line)) changed++;
+ else outLines.push(line);
+ }
+ } else {
+ const idx = delLine === '$' ? lines.length - 1 : parseInt(delLine, 10) - 1;
+ if (idx < 0 || idx >= lines.length) return this.terminal.print('sed: line number out of range: ' + delLine, 'error');
+ for (let i = 0; i < lines.length; i++) {
+ if (i === idx) changed++;
+ else outLines.push(lines[i]);
+ }
+ }
+ if (inPlace) {
+ if (!file) return this.terminal.print('sed: -i requires a file operand', 'error');
+ this.terminal.fs.writeFile(file, outLines.join('\n') + (trailingNL ? '\n' : ''));
+ this.terminal.print('sed: updated ' + file + ' (' + changed + ' line' + (changed === 1 ? '' : 's') + ' changed)', 'success');
+ } else if (quiet) {
+ if (changed === 0) this.terminal.print('(no changes)', 'text-muted');
+ } else {
+ this.terminal.print(outLines.join('\n'));
+ }
};
this.commands['sort'] = (args) => {
if(!args[0]) return this.terminal.print('Usage: sort <filename>', 'warning');