feat(chat-cli): add SSH, adventure, figlet, fortunes, achievements, leaderboard, boss key
This commit is contained in:
parent
df53d85d01
commit
d91c0bed11
4 changed files with 681 additions and 5 deletions
|
|
@ -1,6 +1,28 @@
|
||||||
{
|
{
|
||||||
"site": "jaeswift.xyz",
|
"site": "jaeswift.xyz",
|
||||||
"entries": [
|
"entries": [
|
||||||
|
{
|
||||||
|
"version": "1.38.1",
|
||||||
|
"date": "20/04/2026",
|
||||||
|
"category": "FEATURE",
|
||||||
|
"title": "CLI Extensions: SSH, Adventure, Fortunes, Figlet, Achievements, Leaderboard, Boss Key",
|
||||||
|
"changes": [
|
||||||
|
"JAE-AI terminal (/) now has 7 new commands and 3 meta-features — type /help to see the categorised menu (SYSTEM / FUN / MODES / STATS)",
|
||||||
|
"/ssh <user>@<host> — fake SSH session with ED25519 handshake, MOTD, nested shell (ls/cat/cd/pwd/whoami/hostname/uname/date); exit via /exit-ssh or 'exit'",
|
||||||
|
"/adventure — short cyberpunk text adventure (DARK ALLEY → NEON ARCADE → HACKER DEN → LOW ROOFTOP → NEON ROOFTOP boss fight) with inventory, drone HP, /exit-adventure",
|
||||||
|
"/ttyper — typing-speed test with random programming quote, timer, WPM + accuracy calc, all-time best persisted in localStorage (jae-cli-ttyper-best-v1)",
|
||||||
|
"/cowsay <msg> — classic ASCII cow with auto-sized speech bubble",
|
||||||
|
"/figlet <text> — block-font ASCII banner (5-row UNICODE █ glyphs, full A-Z/0-9/punctuation, truncated to 20 chars)",
|
||||||
|
"/fortune -o — offensive mode with 30 crude operator-themed quips; /fortune alone returns 20 tame SITREP entries",
|
||||||
|
"/cmatrix — enhanced matrix rain: dual-colour green+cyan, denser (14px), speed ramps over 30s cycles, optional square-wave AudioContext ticks for typing-sound effect",
|
||||||
|
"/achievements — progress tracker showing X/24 commands discovered with ASCII progress bar [███░░░░░░░], list of unlocked commands, persists via localStorage (jae-cli-achievements-v1); toast pops for each new unlock",
|
||||||
|
"/leaderboard — session + all-time stats for this browser: command counts, unlocks, most-used command, top-5 session & all-time, military rank (recruit → general based on 20/50/100/200/500/1000/2000 thresholds)",
|
||||||
|
"Boss key — press Escape (while anything is 'hot') or Ctrl+Shift+B to instantly hide matrix rains, dev-mode badge, toasts, and clear chat input; shows 'NORMAL MODE' tooltip for ~2s",
|
||||||
|
"Nested mode routing — when inside /ssh, /adventure, /ttyper, plain (non-slash) chat input is routed to the nested handler instead of JAE-AI; chat.js updated with isInMode() check",
|
||||||
|
"Stats tracked in localStorage (jae-cli-stats-v1) and in-memory session counter; commands object grown from 17 → 24",
|
||||||
|
"Files: js/chat-cli.js (+632 lines → 1142 total), css/chat-cli.css (+21 lines), js/chat.js (+1 line — mode-aware CLI interception)"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "1.38.0",
|
"version": "1.38.0",
|
||||||
"date": "20/04/2026",
|
"date": "20/04/2026",
|
||||||
|
|
|
||||||
|
|
@ -40,3 +40,24 @@
|
||||||
.chat-msg-cli .chat-msg-body { font-size: 11px; }
|
.chat-msg-cli .chat-msg-body { font-size: 11px; }
|
||||||
#devModeBadge { font-size: 9px !important; top: 62px !important; padding: 4px 9px !important; }
|
#devModeBadge { font-size: 9px !important; top: 62px !important; padding: 4px 9px !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Extensions v1.38.1: toasts, mode accents ─── */
|
||||||
|
.jae-cli-toast {
|
||||||
|
border-radius: 2px;
|
||||||
|
animation: jaeCliToastIn 0.28s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes jaeCliToastIn {
|
||||||
|
from { transform: translateY(14px); opacity: 0; }
|
||||||
|
to { transform: translateY(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SSH & adventure nested mode user bubble tint */
|
||||||
|
.chat-msg-user-cli.chat-msg-user-cli-mode .chat-msg-body::before {
|
||||||
|
content: '> ';
|
||||||
|
color: #33ddff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Figlet / cowsay bodies: ensure wide output doesn't wrap mid-block */
|
||||||
|
.chat-msg-cli .chat-msg-body {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
|
||||||
640
js/chat-cli.js
640
js/chat-cli.js
|
|
@ -11,6 +11,45 @@
|
||||||
let cmdHistory = [];
|
let cmdHistory = [];
|
||||||
let devMode = false;
|
let devMode = false;
|
||||||
let matrixActive = false;
|
let matrixActive = false;
|
||||||
|
let sessionCmdCount = 0;
|
||||||
|
let sessionPerCmd = {};
|
||||||
|
let mode = null; // null | 'ssh' | 'adventure' | 'ttyper'
|
||||||
|
let modeState = {};
|
||||||
|
const ACH_KEY = 'jae-cli-achievements-v1';
|
||||||
|
const STATS_KEY = 'jae-cli-stats-v1';
|
||||||
|
const TTYPER_KEY = 'jae-cli-ttyper-best-v1';
|
||||||
|
let achievements = new Set();
|
||||||
|
let allTimeStats = { total: 0, perCmd: {} };
|
||||||
|
try {
|
||||||
|
const ar = localStorage.getItem(ACH_KEY);
|
||||||
|
if (ar) { const a = JSON.parse(ar); if (Array.isArray(a)) achievements = new Set(a); }
|
||||||
|
const sr = localStorage.getItem(STATS_KEY);
|
||||||
|
if (sr) allTimeStats = Object.assign({ total: 0, perCmd: {} }, JSON.parse(sr));
|
||||||
|
} catch (e) {}
|
||||||
|
function saveAch() { try { localStorage.setItem(ACH_KEY, JSON.stringify([...achievements])); } catch(e){} }
|
||||||
|
function saveStats() { try { localStorage.setItem(STATS_KEY, JSON.stringify(allTimeStats)); } catch(e){} }
|
||||||
|
function unlockAchievement(cmd) {
|
||||||
|
if (achievements.has(cmd)) return false;
|
||||||
|
achievements.add(cmd);
|
||||||
|
saveAch();
|
||||||
|
try {
|
||||||
|
const t = document.createElement('div');
|
||||||
|
t.className = 'jae-cli-toast';
|
||||||
|
t.textContent = '🏆 NEW: Discovered /' + cmd + ' command';
|
||||||
|
Object.assign(t.style, {
|
||||||
|
position: 'fixed', bottom: '24px', right: '24px', zIndex: '9999',
|
||||||
|
background: 'var(--bg-panel, #0d1810)', border: '1px solid var(--warning, #c9a227)',
|
||||||
|
color: 'var(--warning, #c9a227)', padding: '10px 16px',
|
||||||
|
fontFamily: 'var(--font-mono, monospace)', fontSize: '12px', letterSpacing: '1px',
|
||||||
|
boxShadow: '0 0 18px rgba(201,162,39,0.45)', transition: 'opacity 0.4s',
|
||||||
|
borderRadius: '2px', pointerEvents: 'none',
|
||||||
|
});
|
||||||
|
document.body.appendChild(t);
|
||||||
|
setTimeout(() => { t.style.opacity = '0'; }, 3400);
|
||||||
|
setTimeout(() => { t.remove(); }, 4000);
|
||||||
|
} catch (e) {}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Load history
|
// Load history
|
||||||
try {
|
try {
|
||||||
|
|
@ -122,6 +161,7 @@
|
||||||
const base = [
|
const base = [
|
||||||
'JAE-AI TERMINAL — available commands:',
|
'JAE-AI TERMINAL — available commands:',
|
||||||
'',
|
'',
|
||||||
|
'— SYSTEM —',
|
||||||
' /help show this help',
|
' /help show this help',
|
||||||
' /ls list virtual files',
|
' /ls list virtual files',
|
||||||
' /cat <file> print file contents',
|
' /cat <file> print file contents',
|
||||||
|
|
@ -132,13 +172,27 @@
|
||||||
' /ping <host> fake ping sweep',
|
' /ping <host> fake ping sweep',
|
||||||
' /sudo <cmd> attempt privilege escalation',
|
' /sudo <cmd> attempt privilege escalation',
|
||||||
' /rm -rf / don\'t do it',
|
' /rm -rf / don\'t do it',
|
||||||
' /hack <target> hollywood hacking simulator',
|
|
||||||
' /matrix toggle matrix rain',
|
|
||||||
' /clear clear the chat',
|
' /clear clear the chat',
|
||||||
' /exit try to leave',
|
' /exit try to leave',
|
||||||
' /fortune random sitrep quip',
|
|
||||||
' /history show last commands',
|
' /history show last commands',
|
||||||
' /neofetch system summary ASCII',
|
' /neofetch system summary ASCII',
|
||||||
|
'',
|
||||||
|
'— FUN —',
|
||||||
|
' /hack <target> hollywood hacking simulator',
|
||||||
|
' /matrix toggle matrix rain',
|
||||||
|
' /cmatrix enhanced matrix (sound + ramp)',
|
||||||
|
' /fortune [-o] random quip (-o for offensive)',
|
||||||
|
' /cowsay <msg> a cow says something',
|
||||||
|
' /figlet <text> big ASCII banner (<=20 chars)',
|
||||||
|
'',
|
||||||
|
'— MODES —',
|
||||||
|
' /ssh <user>@<host> enter fake SSH session',
|
||||||
|
' /adventure cyberpunk text adventure',
|
||||||
|
' /ttyper typing speed test',
|
||||||
|
'',
|
||||||
|
'— STATS —',
|
||||||
|
' /achievements your unlocked commands',
|
||||||
|
' /leaderboard session & all-time stats',
|
||||||
];
|
];
|
||||||
if (devMode) {
|
if (devMode) {
|
||||||
base.push('');
|
base.push('');
|
||||||
|
|
@ -148,6 +202,7 @@
|
||||||
base.push(' /geo show GEO INTEL from telemetry');
|
base.push(' /geo show GEO INTEL from telemetry');
|
||||||
}
|
}
|
||||||
base.push('');
|
base.push('');
|
||||||
|
base.push('Tip: Press Escape (or Ctrl+Shift+B) as a boss key to hide everything.');
|
||||||
base.push('Anything not prefixed with / goes to JAE-AI.');
|
base.push('Anything not prefixed with / goes to JAE-AI.');
|
||||||
return fmt(base);
|
return fmt(base);
|
||||||
};
|
};
|
||||||
|
|
@ -401,6 +456,514 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── /cowsay ─────────────────────────────────────
|
||||||
|
commands.cowsay = function (args) {
|
||||||
|
const msg = args.join(' ') || 'moo.';
|
||||||
|
const text = msg.slice(0, 200);
|
||||||
|
const top = ' ' + '_'.repeat(text.length + 2);
|
||||||
|
const bot = ' ' + '-'.repeat(text.length + 2);
|
||||||
|
return fmt([
|
||||||
|
top,
|
||||||
|
'< ' + text + ' >',
|
||||||
|
bot,
|
||||||
|
' \\ ^__^',
|
||||||
|
' \\ (oo)\\_______',
|
||||||
|
' (__)\\ )\\/\\',
|
||||||
|
' ||----w |',
|
||||||
|
' || ||'
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── /fortune (tame + offensive) ─────────────────
|
||||||
|
const FORTUNES_TAME = [
|
||||||
|
'SITREP: the compiler whispers. listen.',
|
||||||
|
'SITREP: your branch is ahead by one regret.',
|
||||||
|
'SITREP: staging is production if staging is where the users are.',
|
||||||
|
'SITREP: tests pass locally is not a deployment strategy.',
|
||||||
|
'SITREP: cache invalidation is a personality trait.',
|
||||||
|
'SITREP: the database is fine. the database is never fine.',
|
||||||
|
'SITREP: write it down. future-you is stupider than you think.',
|
||||||
|
'SITREP: uptime is measured in coffee.',
|
||||||
|
'SITREP: legacy code is just working code you hate.',
|
||||||
|
'SITREP: every bug is someone else\'s feature.',
|
||||||
|
'SITREP: the stack trace knows. trust the stack trace.',
|
||||||
|
'SITREP: container orchestration is a synonym for chaos.',
|
||||||
|
'SITREP: there are two kinds of backup: tested and imaginary.',
|
||||||
|
'SITREP: TODO comments age like milk.',
|
||||||
|
'SITREP: ship small, ship often, ship sober.',
|
||||||
|
'SITREP: rm is a lifestyle. ls is religion.',
|
||||||
|
'SITREP: root access is a personality disorder.',
|
||||||
|
'SITREP: the terminal is the only honest relationship.',
|
||||||
|
'SITREP: `it compiles` is not a vibe.',
|
||||||
|
'SITREP: operator status: caffeinated. proceed.'
|
||||||
|
];
|
||||||
|
const FORTUNES_OFFENSIVE = [
|
||||||
|
'SITREP: your code smells like a fucking landfill. grep deeper.',
|
||||||
|
'SITREP: whoever wrote this legacy shit owes the team a pint and a resignation letter.',
|
||||||
|
'SITREP: you can\'t centralise a garbage fire. you can only photograph it for post-mortem.',
|
||||||
|
'SITREP: touch grass, operator. the leaves are greener than your terminal.',
|
||||||
|
'SITREP: the sysadmin is tired. the sysadmin is always fucking tired.',
|
||||||
|
'SITREP: if it works, don\'t touch it. if it doesn\'t, blame the intern.',
|
||||||
|
'SITREP: your git history is a crime scene. get a lawyer.',
|
||||||
|
'SITREP: nginx config is 80% vibes and 20% stack overflow.',
|
||||||
|
'SITREP: the only thing more fragile than your ego is this regex.',
|
||||||
|
'SITREP: deploy on friday. embrace the chaos. live deliciously.',
|
||||||
|
'SITREP: your tests are flaky because YOU are flaky.',
|
||||||
|
'SITREP: merge conflicts are the universe\'s way of saying grow up.',
|
||||||
|
'SITREP: chmod 777 is a cry for help.',
|
||||||
|
'SITREP: whoever invented JavaScript owes humanity a bloody apology.',
|
||||||
|
'SITREP: your Docker image is 4GB because you don\'t know what you\'re doing. fix it.',
|
||||||
|
'SITREP: if you ssh into prod to "just check something", you are the incident.',
|
||||||
|
'SITREP: that variable name is an act of violence against future maintainers.',
|
||||||
|
'SITREP: you didn\'t break the build. the build broke you.',
|
||||||
|
'SITREP: 3am deploys are for masochists and idiots. which one are you?',
|
||||||
|
'SITREP: the documentation is a lie. it has always been a lie.',
|
||||||
|
'SITREP: `works on my machine` is a confession, not a defence.',
|
||||||
|
'SITREP: your monorepo is a cry for therapy.',
|
||||||
|
'SITREP: stop copy-pasting from stack overflow you absolute muppet.',
|
||||||
|
'SITREP: curl | bash is a religious experience and a mistake.',
|
||||||
|
'SITREP: kubernetes is just containers with extra steps and extra crying.',
|
||||||
|
'SITREP: the only good password is the one you forgot.',
|
||||||
|
'SITREP: your side project is a liability, not a portfolio.',
|
||||||
|
'SITREP: linters are for people who can\'t spell. spell better.',
|
||||||
|
'SITREP: shit deploys fast. good shit deploys slower. great shit doesn\'t deploy on fridays.',
|
||||||
|
'SITREP: if the server is smoking, that\'s your problem now.'
|
||||||
|
];
|
||||||
|
commands.fortune = function (args) {
|
||||||
|
const offensive = args && (args[0] === '-o' || args[0] === '--offensive');
|
||||||
|
const pool = offensive ? FORTUNES_OFFENSIVE : FORTUNES_TAME;
|
||||||
|
return pool[Math.floor(Math.random() * pool.length)];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── /figlet — block-font ASCII banner ─<><E29480><EFBFBD>─────────
|
||||||
|
const FIGLET_FONT = {
|
||||||
|
'A': [' ██ ','█ █ ','████ ','█ █ ','█ █ '],
|
||||||
|
'B': ['███ ','█ █ ','███ ','█ █ ','███ '],
|
||||||
|
'C': [' ███ ','█ ','█ ','█ ',' ███ '],
|
||||||
|
'D': ['███ ','█ █ ','█ █ ','█ █ ','███ '],
|
||||||
|
'E': ['████ ','█ ','███ ','█ ','████ '],
|
||||||
|
'F': ['████ ','█ ','███ ','█ ','█ '],
|
||||||
|
'G': [' ███ ','█ ','█ ██ ','█ █ ',' ███ '],
|
||||||
|
'H': ['█ █ ','█ █ ','████ ','█ █ ','█ █ '],
|
||||||
|
'I': ['███ ',' █ ',' █ ',' █ ','███ '],
|
||||||
|
'J': [' ██ ',' █ ',' █ ','█ █ ',' ██ '],
|
||||||
|
'K': ['█ █ ','█ █ ','██ ','█ █ ','█ █ '],
|
||||||
|
'L': ['█ ','█ ','█ ','█ ','████ '],
|
||||||
|
'M': ['█ █','██ ██','█ █ █','█ █','█ █'],
|
||||||
|
'N': ['█ █ ','██ █ ','█ ██ ','█ █ ','█ █ '],
|
||||||
|
'O': [' ██ ','█ █ ','█ █ ','█ █ ',' ██ '],
|
||||||
|
'P': ['███ ','█ █ ','███ ','█ ','█ '],
|
||||||
|
'Q': [' ██ ','█ █ ','█ █ ','█ █ ',' █ █ '],
|
||||||
|
'R': ['███ ','█ █ ','███ ','█ █ ','█ █ '],
|
||||||
|
'S': [' ███ ','█ ',' ██ ',' █ ','███ '],
|
||||||
|
'T': ['█████',' █ ',' █ ',' █ ',' █ '],
|
||||||
|
'U': ['█ █ ','█ █ ','█ █ ','█ █ ',' ██ '],
|
||||||
|
'V': ['█ █ ','█ █ ','█ █ ',' ██ ',' █ '],
|
||||||
|
'W': ['█ █','█ █','█ █ █','██ ██','█ █'],
|
||||||
|
'X': ['█ █ ',' ██ ',' █ ',' ██ ','█ █ '],
|
||||||
|
'Y': ['█ █ ',' ██ ',' █ ',' █ ',' █ '],
|
||||||
|
'Z': ['████ ',' █ ',' █ ',' █ ','████ '],
|
||||||
|
'0': [' ██ ','█ █ ','█ █ ','█ █ ',' ██ '],
|
||||||
|
'1': [' █ ','██ ',' █ ',' █ ','███ '],
|
||||||
|
'2': [' ██ ','█ █ ',' █ ',' █ ','████ '],
|
||||||
|
'3': ['███ ',' █ ',' ██ ',' █ ','███ '],
|
||||||
|
'4': ['█ █ ','█ █ ','████ ',' █ ',' █ '],
|
||||||
|
'5': ['████ ','█ ','███ ',' █ ','███ '],
|
||||||
|
'6': [' ██ ','█ ','███ ','█ █ ',' ██ '],
|
||||||
|
'7': ['████ ',' █ ',' █ ',' █ ','█ '],
|
||||||
|
'8': [' ██ ','█ █ ',' ██ ','█ █ ',' ██ '],
|
||||||
|
'9': [' ██ ','█ █ ',' ███ ',' █ ',' ██ '],
|
||||||
|
' ': [' ',' ',' ',' ',' '],
|
||||||
|
'!': [' █ ',' █ ',' █ ',' ',' █ '],
|
||||||
|
'?': [' ██ ','█ █ ',' █ ',' ',' █ '],
|
||||||
|
'.': [' ',' ',' ',' ',' █ '],
|
||||||
|
'-': [' ',' ','████ ',' ',' '],
|
||||||
|
'_': [' ',' ',' ',' ','████ '],
|
||||||
|
'/': [' █ ',' █ ',' █ ','█ ',' '],
|
||||||
|
':': [' ',' █ ',' ',' █ ',' ']
|
||||||
|
};
|
||||||
|
commands.figlet = function (args) {
|
||||||
|
const text = (args.join(' ') || 'JAE').toUpperCase().slice(0, 20);
|
||||||
|
const rows = ['', '', '', '', ''];
|
||||||
|
for (const ch of text) {
|
||||||
|
const glyph = FIGLET_FONT[ch] || FIGLET_FONT['?'];
|
||||||
|
for (let i = 0; i < 5; i++) rows[i] += glyph[i] + ' ';
|
||||||
|
}
|
||||||
|
return rows.join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── /ssh fake session ───────────────────────────
|
||||||
|
const SSH_FS = {
|
||||||
|
'/': ['home', 'etc', 'var', 'opt', 'tmp'],
|
||||||
|
'/home': ['operator', 'guest'],
|
||||||
|
'/etc': ['hostname', 'motd', 'shadow'],
|
||||||
|
'/var': ['log'],
|
||||||
|
'/var/log': ['auth.log', 'syslog'],
|
||||||
|
};
|
||||||
|
const SSH_FILES = {
|
||||||
|
'/etc/hostname': 'jaeswift-prod-01',
|
||||||
|
'/etc/motd': '\n WELCOME TO JAESWIFT PROD — authorised operators only.\n All activity logged. Press Ctrl+C to get off my lawn.\n',
|
||||||
|
'/etc/shadow': 'nice try. permission denied.',
|
||||||
|
'/home/operator/notes.txt': 'remember: the password is NOT hunter2.',
|
||||||
|
'/var/log/auth.log': '[' + new Date().toISOString() + '] sshd: Accepted publickey for operator from 127.0.0.1',
|
||||||
|
};
|
||||||
|
commands.ssh = function (args) {
|
||||||
|
const target = args[0] || '';
|
||||||
|
const m = target.match(/^([a-zA-Z0-9_-]+)@([a-zA-Z0-9.\-_]+)$/);
|
||||||
|
if (!m) return 'usage: /ssh <user>@<host> e.g. /ssh operator@jaeswift.xyz';
|
||||||
|
const user = m[1], host = m[2];
|
||||||
|
mode = 'ssh';
|
||||||
|
modeState = { user, host, cwd: '/home/' + user };
|
||||||
|
return fmt([
|
||||||
|
`Connecting to ${host} on port 22...`,
|
||||||
|
`The authenticity of host '${host}' can\'t be established.`,
|
||||||
|
`ED25519 key fingerprint is SHA256:J4Esw1FtX${Math.random().toString(36).slice(2,14)}.`,
|
||||||
|
`Warning: Permanently added '${host}' (ED25519) to the list of known hosts.`,
|
||||||
|
`${user}@${host}'s password: ****`,
|
||||||
|
`Authenticating...`,
|
||||||
|
`Last login: ${new Date().toUTCString()} from 10.0.0.1`,
|
||||||
|
``,
|
||||||
|
`Welcome to ${host} — type 'help' for commands, '/exit-ssh' to disconnect.`
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
function sshHandle(line) {
|
||||||
|
const raw = line.trim();
|
||||||
|
if (!raw) return '';
|
||||||
|
if (raw === '/exit-ssh' || raw === 'exit' || raw === 'logout') {
|
||||||
|
const h = modeState.host;
|
||||||
|
mode = null; modeState = {};
|
||||||
|
return `Connection to ${h} closed.`;
|
||||||
|
}
|
||||||
|
const parts = raw.split(/\s+/);
|
||||||
|
const cmd = parts[0].toLowerCase();
|
||||||
|
const arg = parts.slice(1).join(' ');
|
||||||
|
switch (cmd) {
|
||||||
|
case 'help':
|
||||||
|
return fmt(['available: ls, cat <f>, pwd, cd <d>, whoami, hostname, uname, date, exit']);
|
||||||
|
case 'whoami': return modeState.user;
|
||||||
|
case 'hostname': return modeState.host;
|
||||||
|
case 'pwd': return modeState.cwd;
|
||||||
|
case 'uname': return 'Linux';
|
||||||
|
case 'date': return new Date().toString();
|
||||||
|
case 'ls': {
|
||||||
|
const dir = SSH_FS[modeState.cwd];
|
||||||
|
if (dir) return dir.join(' ');
|
||||||
|
if (modeState.cwd === '/home/' + modeState.user) return 'notes.txt .bashrc .ssh';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
case 'cd': {
|
||||||
|
if (!arg || arg === '~') { modeState.cwd = '/home/' + modeState.user; return ''; }
|
||||||
|
if (arg.startsWith('/')) modeState.cwd = arg;
|
||||||
|
else modeState.cwd = (modeState.cwd + '/' + arg).replace(/\/+/g, '/');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
case 'cat': {
|
||||||
|
if (!arg) return 'cat: missing operand';
|
||||||
|
const full = arg.startsWith('/') ? arg : (modeState.cwd + '/' + arg).replace(/\/+/g, '/');
|
||||||
|
if (SSH_FILES[full] != null) return SSH_FILES[full];
|
||||||
|
return `cat: ${arg}: No such file or directory`;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return `${cmd}: command not found`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── /adventure — cyberpunk text adventure ───────
|
||||||
|
const ADV_ROOMS = {
|
||||||
|
'alley': { name: 'DARK ALLEY', desc: 'Rain slicks the neon puddles. A fire-escape ladder climbs north to a rooftop. East is a buzzing arcade door.', exits: { n: 'rooftop_low', e: 'arcade' }, items: ['rusty knife'] },
|
||||||
|
'arcade': { name: 'NEON ARCADE', desc: 'Machines scream colour. A bouncer eyes you. A back door leads south to a hacker den. West back to the alley.', exits: { w: 'alley', s: 'hacker_den' }, items: ['token'] },
|
||||||
|
'hacker_den': { name: 'HACKER DEN', desc: 'Ten monitors, one cat. A hacker types without looking. They might talk. North back to arcade.', exits: { n: 'arcade' }, items: [], npc: 'hacker' },
|
||||||
|
'rooftop_low': { name: 'LOW ROOFTOP', desc: 'Gravel, pigeons, a distant drone. A ladder climbs east to the high neon rooftop.', exits: { s: 'alley', e: 'rooftop_high' }, items: [] },
|
||||||
|
'rooftop_high': { name: 'NEON ROOFTOP', desc: 'The city spreads below. A corporate drone blocks the datalink pylon. BOSS FIGHT.', exits: { w: 'rooftop_low' }, items: [], boss: 'drone' }
|
||||||
|
};
|
||||||
|
commands.adventure = function () {
|
||||||
|
mode = 'adventure';
|
||||||
|
modeState = { room: 'alley', inv: [], hp: 100, bossHp: 60, talked: false, victory: false };
|
||||||
|
return fmt([
|
||||||
|
'== NEON/NULL — a very short cyberpunk — ==',
|
||||||
|
'',
|
||||||
|
'You are an operator looking to upload the encryption key to the city.',
|
||||||
|
'Commands: look, n/s/e/w (or north/south/east/west), take <item>,',
|
||||||
|
' use <item>, inventory (i), attack, talk, /exit-adventure',
|
||||||
|
'',
|
||||||
|
advDescribe()
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
function advDescribe() {
|
||||||
|
const r = ADV_ROOMS[modeState.room];
|
||||||
|
const out = [`[${r.name}]`, r.desc];
|
||||||
|
if (r.items && r.items.length) out.push('You see: ' + r.items.join(', '));
|
||||||
|
if (r.boss && modeState.bossHp > 0) out.push('A CORPORATE DRONE is here. HP: ' + modeState.bossHp);
|
||||||
|
const exits = Object.keys(r.exits).map(k => ({n:'north',s:'south',e:'east',w:'west'})[k]).join(', ');
|
||||||
|
out.push('Exits: ' + exits);
|
||||||
|
return out.join('\n');
|
||||||
|
}
|
||||||
|
function adventureHandle(line) {
|
||||||
|
const raw = line.trim().toLowerCase();
|
||||||
|
if (!raw) return '';
|
||||||
|
if (raw === '/exit-adventure' || raw === 'exit-adventure' || raw === 'quit') {
|
||||||
|
mode = null; modeState = {};
|
||||||
|
return 'You jack out. Back to JAE-AI.';
|
||||||
|
}
|
||||||
|
const dirMap = { n: 'n', north: 'n', s: 's', south: 's', e: 'e', east: 'e', w: 'w', west: 'w' };
|
||||||
|
if (dirMap[raw]) {
|
||||||
|
const r = ADV_ROOMS[modeState.room];
|
||||||
|
const d = dirMap[raw];
|
||||||
|
if (r.boss && modeState.bossHp > 0) return 'The drone blocks your path. ATTACK it first.';
|
||||||
|
if (!r.exits[d]) return 'You can\'t go that way.';
|
||||||
|
modeState.room = r.exits[d];
|
||||||
|
return advDescribe();
|
||||||
|
}
|
||||||
|
if (raw === 'look' || raw === 'l') return advDescribe();
|
||||||
|
if (raw === 'inventory' || raw === 'i' || raw === 'inv') {
|
||||||
|
return modeState.inv.length ? 'You carry: ' + modeState.inv.join(', ') : 'Your pockets are empty.';
|
||||||
|
}
|
||||||
|
if (raw.startsWith('take ')) {
|
||||||
|
const item = raw.slice(5).trim();
|
||||||
|
const r = ADV_ROOMS[modeState.room];
|
||||||
|
const idx = r.items.indexOf(item);
|
||||||
|
if (idx === -1) return `There is no ${item} here.`;
|
||||||
|
r.items.splice(idx, 1);
|
||||||
|
modeState.inv.push(item);
|
||||||
|
return `You take the ${item}.`;
|
||||||
|
}
|
||||||
|
if (raw.startsWith('use ')) {
|
||||||
|
const item = raw.slice(4).trim();
|
||||||
|
if (!modeState.inv.includes(item)) return `You don't have a ${item}.`;
|
||||||
|
if (item === 'token' && modeState.room === 'arcade') {
|
||||||
|
modeState.inv.push('encryption key');
|
||||||
|
return 'The arcade machine clunks. A secret panel opens — you palm an ENCRYPTION KEY.';
|
||||||
|
}
|
||||||
|
if (item === 'encryption key' && modeState.room === 'rooftop_high' && modeState.bossHp <= 0) {
|
||||||
|
modeState.victory = true;
|
||||||
|
mode = null; modeState = {};
|
||||||
|
return fmt([
|
||||||
|
'You jack the ENCRYPTION KEY into the pylon.',
|
||||||
|
'The city\'s surveillance grid goes dark for 11 seconds.',
|
||||||
|
'That\'s all you needed.',
|
||||||
|
'',
|
||||||
|
'>> YOU WIN. Mission complete. Exiting adventure. <<'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return 'Nothing happens.';
|
||||||
|
}
|
||||||
|
if (raw === 'talk') {
|
||||||
|
const r = ADV_ROOMS[modeState.room];
|
||||||
|
if (r.npc === 'hacker' && !modeState.talked) {
|
||||||
|
modeState.talked = true;
|
||||||
|
if (!modeState.inv.includes('token')) return 'The hacker: "Arcade machine. Back one. Use the token. Trust me."';
|
||||||
|
if (!modeState.inv.includes('encryption key')) return 'The hacker: "Use what you have. The arcade remembers."';
|
||||||
|
return 'The hacker: "Rooftop. Pylon. Key. Go."';
|
||||||
|
}
|
||||||
|
return 'No one to talk to.';
|
||||||
|
}
|
||||||
|
if (raw === 'attack') {
|
||||||
|
const r = ADV_ROOMS[modeState.room];
|
||||||
|
if (!r.boss || modeState.bossHp <= 0) return 'Nothing to attack.';
|
||||||
|
const weapon = modeState.inv.includes('rusty knife') ? 'knife' : 'fist';
|
||||||
|
const dmg = weapon === 'knife' ? (8 + Math.floor(Math.random()*8)) : (3 + Math.floor(Math.random()*4));
|
||||||
|
modeState.bossHp -= dmg;
|
||||||
|
modeState.hp -= (3 + Math.floor(Math.random()*6));
|
||||||
|
let out = `You attack with your ${weapon} for ${dmg} damage. Drone HP: ${Math.max(0,modeState.bossHp)}.`;
|
||||||
|
if (modeState.hp <= 0) {
|
||||||
|
mode = null; modeState = {};
|
||||||
|
return out + '\nThe drone flatlines you. GAME OVER.';
|
||||||
|
}
|
||||||
|
if (modeState.bossHp <= 0) out += '\nThe drone crashes to the gravel. The pylon is exposed.';
|
||||||
|
out += ` Your HP: ${modeState.hp}.`;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return `Unknown command: '${raw}'. Try look, directions, take, use, attack, talk.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── /ttyper — typing speed test ─────────────────
|
||||||
|
const TTYPER_QUOTES = [
|
||||||
|
'the quick brown fox jumps over the lazy dog while the sysadmin sips cold coffee and watches nginx logs scroll endlessly',
|
||||||
|
'code is read many more times than it is written so leave comments that future you will not curse at',
|
||||||
|
'in the terminal no one can hear you scream but the bash history remembers every single thing you have ever typed',
|
||||||
|
'deploy on friday and spend the weekend rolling back changes like a very unfortunate software archaeologist',
|
||||||
|
'the best debugger is still a print statement followed by staring at the output for twenty minutes in silence'
|
||||||
|
];
|
||||||
|
commands.ttyper = function () {
|
||||||
|
const q = TTYPER_QUOTES[Math.floor(Math.random() * TTYPER_QUOTES.length)];
|
||||||
|
mode = 'ttyper';
|
||||||
|
modeState = { quote: q, start: 0 };
|
||||||
|
return fmt([
|
||||||
|
'=== TTYPER — typing speed test ===',
|
||||||
|
'Type the quote below exactly as shown, then press Enter.',
|
||||||
|
'Type "/exit-ttyper" to cancel.',
|
||||||
|
'',
|
||||||
|
'> ' + q,
|
||||||
|
'',
|
||||||
|
'(timer starts on your first submission)'
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
function ttyperHandle(line) {
|
||||||
|
const raw = line.trim();
|
||||||
|
if (raw === '/exit-ttyper' || raw === 'exit-ttyper' || raw === 'quit') {
|
||||||
|
mode = null; modeState = {};
|
||||||
|
return 'TTYPER cancelled.';
|
||||||
|
}
|
||||||
|
if (!modeState.start) modeState.start = Date.now();
|
||||||
|
const elapsed = (Date.now() - modeState.start) / 1000;
|
||||||
|
const target = modeState.quote;
|
||||||
|
const a = raw, b = target;
|
||||||
|
const maxLen = Math.max(a.length, b.length);
|
||||||
|
let correct = 0;
|
||||||
|
for (let i = 0; i < Math.min(a.length, b.length); i++) if (a[i] === b[i]) correct++;
|
||||||
|
const accuracy = maxLen === 0 ? 100 : Math.round((correct / maxLen) * 100);
|
||||||
|
const words = target.split(/\s+/).length;
|
||||||
|
const wpm = elapsed > 0 ? Math.round((words / elapsed) * 60) : 0;
|
||||||
|
let best = null;
|
||||||
|
try {
|
||||||
|
const raw2 = localStorage.getItem(TTYPER_KEY);
|
||||||
|
if (raw2) best = JSON.parse(raw2);
|
||||||
|
} catch (e) {}
|
||||||
|
let bestNote = '';
|
||||||
|
if (!best || (wpm > (best.wpm || 0) && accuracy >= 90)) {
|
||||||
|
const rec = { wpm, accuracy, ts: Date.now() };
|
||||||
|
try { localStorage.setItem(TTYPER_KEY, JSON.stringify(rec)); } catch(e){}
|
||||||
|
bestNote = '\n*** NEW PERSONAL BEST ***';
|
||||||
|
best = rec;
|
||||||
|
}
|
||||||
|
mode = null; modeState = {};
|
||||||
|
return fmt([
|
||||||
|
'=== TTYPER RESULT ===',
|
||||||
|
`time: ${elapsed.toFixed(2)}s`,
|
||||||
|
`WPM: ${wpm}`,
|
||||||
|
`accuracy: ${accuracy}%`,
|
||||||
|
best ? `all-time best: ${best.wpm} WPM @ ${best.accuracy}%` : '',
|
||||||
|
bestNote
|
||||||
|
].filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── /cmatrix — enhanced matrix rain ─────────────
|
||||||
|
let cmatrixCanvas = null, cmatrixRAF = null, cmatrixAudio = null, cmatrixStart = 0;
|
||||||
|
function toggleCMatrix(on) {
|
||||||
|
if (on) {
|
||||||
|
cmatrixCanvas = document.createElement('canvas');
|
||||||
|
cmatrixCanvas.id = 'cmatrixRain';
|
||||||
|
Object.assign(cmatrixCanvas.style, {
|
||||||
|
position: 'fixed', inset: '0', width: '100%', height: '100%',
|
||||||
|
pointerEvents: 'none', zIndex: '8', opacity: '0.32',
|
||||||
|
});
|
||||||
|
document.body.appendChild(cmatrixCanvas);
|
||||||
|
const ctx = cmatrixCanvas.getContext('2d');
|
||||||
|
function resize() { cmatrixCanvas.width = innerWidth; cmatrixCanvas.height = innerHeight; }
|
||||||
|
resize();
|
||||||
|
window.addEventListener('resize', resize);
|
||||||
|
const chars = 'アイウエオカキクケコサシスセソタチツテトナニヌネノ01JAESWIFT░▒▓█'.split('');
|
||||||
|
const size = 14;
|
||||||
|
const cols = Math.floor(cmatrixCanvas.width / size);
|
||||||
|
const drops = new Array(cols).fill(0).map(() => Math.random() * 40);
|
||||||
|
cmatrixStart = Date.now();
|
||||||
|
try {
|
||||||
|
const AC = window.AudioContext || window.webkitAudioContext;
|
||||||
|
if (AC) cmatrixAudio = new AC();
|
||||||
|
} catch (e) {}
|
||||||
|
let tick = 0;
|
||||||
|
function draw() {
|
||||||
|
const t = (Date.now() - cmatrixStart) / 1000;
|
||||||
|
const phase = (t % 30) / 30;
|
||||||
|
const speed = 1 + phase * 2.5;
|
||||||
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.08)';
|
||||||
|
ctx.fillRect(0, 0, cmatrixCanvas.width, cmatrixCanvas.height);
|
||||||
|
ctx.font = size + 'px JetBrains Mono, monospace';
|
||||||
|
for (let i = 0; i < drops.length; i++) {
|
||||||
|
const ch = chars[Math.floor(Math.random() * chars.length)];
|
||||||
|
ctx.fillStyle = (i % 2 === 0) ? '#00ff66' : '#33ddff';
|
||||||
|
ctx.fillText(ch, i * size, drops[i] * size);
|
||||||
|
if (drops[i] * size > cmatrixCanvas.height && Math.random() > 0.97) drops[i] = 0;
|
||||||
|
drops[i] += speed;
|
||||||
|
}
|
||||||
|
tick++;
|
||||||
|
if (cmatrixAudio && tick % 4 === 0) {
|
||||||
|
try {
|
||||||
|
const osc = cmatrixAudio.createOscillator();
|
||||||
|
const g = cmatrixAudio.createGain();
|
||||||
|
osc.type = 'square';
|
||||||
|
osc.frequency.value = 1600 + Math.random() * 800;
|
||||||
|
g.gain.value = 0.0035;
|
||||||
|
osc.connect(g); g.connect(cmatrixAudio.destination);
|
||||||
|
osc.start();
|
||||||
|
osc.stop(cmatrixAudio.currentTime + 0.02);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
cmatrixRAF = requestAnimationFrame(draw);
|
||||||
|
}
|
||||||
|
draw();
|
||||||
|
} else {
|
||||||
|
if (cmatrixRAF) cancelAnimationFrame(cmatrixRAF);
|
||||||
|
if (cmatrixCanvas) cmatrixCanvas.remove();
|
||||||
|
if (cmatrixAudio) { try { cmatrixAudio.close(); } catch(e){} }
|
||||||
|
cmatrixCanvas = null; cmatrixAudio = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let cmatrixActive = false;
|
||||||
|
commands.cmatrix = function () {
|
||||||
|
cmatrixActive = !cmatrixActive;
|
||||||
|
toggleCMatrix(cmatrixActive);
|
||||||
|
return cmatrixActive
|
||||||
|
? 'CMATRIX engaged. Dual-colour rain with audio. Toggle off with /cmatrix.'
|
||||||
|
: 'CMATRIX disengaged.';
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── /achievements ───────────────────────────────
|
||||||
|
const TOTAL_COMMANDS = 24;
|
||||||
|
commands.achievements = function () {
|
||||||
|
const unlocked = [...achievements].sort();
|
||||||
|
const n = unlocked.length;
|
||||||
|
const pct = Math.round((n / TOTAL_COMMANDS) * 100);
|
||||||
|
const filled = Math.min(10, Math.round((n / TOTAL_COMMANDS) * 10));
|
||||||
|
const bar = '[' + '█'.repeat(filled) + '░'.repeat(10 - filled) + ']';
|
||||||
|
const out = [
|
||||||
|
'=== ACHIEVEMENTS ===',
|
||||||
|
'',
|
||||||
|
`Progress: ${bar} ${n}/${TOTAL_COMMANDS} (${pct}%)`,
|
||||||
|
''
|
||||||
|
];
|
||||||
|
if (n === 0) out.push('No commands discovered yet. Try /help to start.');
|
||||||
|
else {
|
||||||
|
out.push('Unlocked:');
|
||||||
|
unlocked.forEach(c => out.push(' ✓ /' + c));
|
||||||
|
}
|
||||||
|
out.push('');
|
||||||
|
out.push('Keep exploring, operator.');
|
||||||
|
return fmt(out);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── /leaderboard ────────────────────────────────
|
||||||
|
function rankFor(total) {
|
||||||
|
if (total >= 2000) return 'general';
|
||||||
|
if (total >= 1000) return 'colonel';
|
||||||
|
if (total >= 500) return 'major';
|
||||||
|
if (total >= 200) return 'captain';
|
||||||
|
if (total >= 100) return 'lieutenant';
|
||||||
|
if (total >= 50) return 'sergeant';
|
||||||
|
if (total >= 20) return 'corporal';
|
||||||
|
return 'recruit';
|
||||||
|
}
|
||||||
|
commands.leaderboard = function () {
|
||||||
|
const entries = Object.entries(allTimeStats.perCmd || {}).sort((a,b) => b[1] - a[1]);
|
||||||
|
const mostUsed = entries.length ? entries[0][0] : '—';
|
||||||
|
const sessionEntries = Object.entries(sessionPerCmd).sort((a,b) => b[1] - a[1]);
|
||||||
|
return fmt([
|
||||||
|
'=== LEADERBOARD (this browser) ===',
|
||||||
|
'',
|
||||||
|
`Session commands: ${sessionCmdCount}`,
|
||||||
|
`All-time commands: ${allTimeStats.total || 0}`,
|
||||||
|
`Commands unlocked: ${achievements.size}/${TOTAL_COMMANDS}`,
|
||||||
|
`Most-used (all-time): /${mostUsed}`,
|
||||||
|
`Rank: ${rankFor(allTimeStats.total || 0).toUpperCase()}`,
|
||||||
|
'',
|
||||||
|
'Session top 5:',
|
||||||
|
...sessionEntries.slice(0, 5).map(([c, n]) => ` /${c} ×${n}`),
|
||||||
|
'',
|
||||||
|
'All-time top 5:',
|
||||||
|
...entries.slice(0, 5).map(([c, n]) => ` /${c} ×${n}`)
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
// ─── Matrix rain overlay ─────────────────────────
|
// ─── Matrix rain overlay ─────────────────────────
|
||||||
let matrixCanvas = null;
|
let matrixCanvas = null;
|
||||||
let matrixRAF = null;
|
let matrixRAF = null;
|
||||||
|
|
@ -476,7 +1039,24 @@
|
||||||
|
|
||||||
// ─── Entry point — handle a /-command ───────────
|
// ─── Entry point — handle a /-command ───────────
|
||||||
async function handle(raw) {
|
async function handle(raw) {
|
||||||
const trimmed = raw.trim();
|
const trimmed = (raw || '').trim();
|
||||||
|
if (!trimmed) return { handled: false };
|
||||||
|
|
||||||
|
// If we are inside a nested mode, route input there regardless of '/' prefix,
|
||||||
|
// except for an explicit /exit-<mode> escape (handled inside each).
|
||||||
|
if (mode === 'ssh') {
|
||||||
|
try { const out = sshHandle(trimmed); return { handled: true, output: out == null ? '' : String(out) }; }
|
||||||
|
catch (e) { return { handled: true, output: 'ssh error: ' + e.message }; }
|
||||||
|
}
|
||||||
|
if (mode === 'adventure') {
|
||||||
|
try { const out = adventureHandle(trimmed); return { handled: true, output: out == null ? '' : String(out) }; }
|
||||||
|
catch (e) { return { handled: true, output: 'adventure error: ' + e.message }; }
|
||||||
|
}
|
||||||
|
if (mode === 'ttyper') {
|
||||||
|
try { const out = ttyperHandle(trimmed); return { handled: true, output: out == null ? '' : String(out) }; }
|
||||||
|
catch (e) { return { handled: true, output: 'ttyper error: ' + e.message }; }
|
||||||
|
}
|
||||||
|
|
||||||
if (!trimmed.startsWith('/')) return { handled: false };
|
if (!trimmed.startsWith('/')) return { handled: false };
|
||||||
|
|
||||||
const parts = trimmed.slice(1).split(/\s+/);
|
const parts = trimmed.slice(1).split(/\s+/);
|
||||||
|
|
@ -494,6 +1074,17 @@
|
||||||
return { handled: true, output: `command not found: /${cmd}. type /help for available commands.` };
|
return { handled: true, output: `command not found: /${cmd}. type /help for available commands.` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track stats + unlock achievements for known commands
|
||||||
|
try {
|
||||||
|
sessionCmdCount++;
|
||||||
|
sessionPerCmd[cmd] = (sessionPerCmd[cmd] || 0) + 1;
|
||||||
|
allTimeStats.total = (allTimeStats.total || 0) + 1;
|
||||||
|
allTimeStats.perCmd = allTimeStats.perCmd || {};
|
||||||
|
allTimeStats.perCmd[cmd] = (allTimeStats.perCmd[cmd] || 0) + 1;
|
||||||
|
saveStats();
|
||||||
|
unlockAchievement(cmd);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await fn(args);
|
const result = await fn(args);
|
||||||
return { handled: true, output: (result == null ? '' : String(result)) };
|
return { handled: true, output: (result == null ? '' : String(result)) };
|
||||||
|
|
@ -502,9 +1093,50 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Boss key — Escape / Ctrl+Shift+B ────────────
|
||||||
|
function bossKeyActivate() {
|
||||||
|
try {
|
||||||
|
if (matrixActive) { matrixActive = false; toggleMatrix(false); }
|
||||||
|
if (cmatrixActive) { cmatrixActive = false; toggleCMatrix(false); }
|
||||||
|
const badge = document.getElementById('devModeBadge');
|
||||||
|
if (badge) badge.style.display = 'none';
|
||||||
|
const toasts = document.querySelectorAll('.jae-cli-toast');
|
||||||
|
toasts.forEach(t => t.remove());
|
||||||
|
const ci = document.getElementById('chatInput');
|
||||||
|
if (ci) { ci.value = ''; try { ci.blur(); } catch(e){} }
|
||||||
|
const tip = document.createElement('div');
|
||||||
|
tip.textContent = 'NORMAL MODE';
|
||||||
|
Object.assign(tip.style, {
|
||||||
|
position: 'fixed', top: '12px', left: '50%', transform: 'translateX(-50%)',
|
||||||
|
background: '#111', color: '#9aa', border: '1px solid #333',
|
||||||
|
padding: '4px 10px', fontFamily: 'monospace', fontSize: '10px',
|
||||||
|
letterSpacing: '2px', zIndex: '99999', opacity: '0.85',
|
||||||
|
borderRadius: '2px', pointerEvents: 'none',
|
||||||
|
});
|
||||||
|
document.body.appendChild(tip);
|
||||||
|
setTimeout(() => { tip.style.transition = 'opacity 0.5s'; tip.style.opacity = '0'; }, 1500);
|
||||||
|
setTimeout(() => tip.remove(), 2200);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
const isEsc = (e.key === 'Escape');
|
||||||
|
const isCtrlShiftB = (e.ctrlKey && e.shiftKey && (e.key === 'B' || e.key === 'b'));
|
||||||
|
if (isEsc || isCtrlShiftB) {
|
||||||
|
// Don't nuke on Escape while user is typing in chat unless something is actually "hot"
|
||||||
|
const hot = matrixActive || cmatrixActive || document.getElementById('devModeBadge') || document.querySelector('.jae-cli-toast');
|
||||||
|
if (isCtrlShiftB || (isEsc && hot)) {
|
||||||
|
bossKeyActivate();
|
||||||
|
if (isCtrlShiftB) e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
window.__jaeCLI = {
|
window.__jaeCLI = {
|
||||||
handle: handle,
|
handle: handle,
|
||||||
commands: commands,
|
commands: commands,
|
||||||
isDev: function () { return devMode; },
|
isDev: function () { return devMode; },
|
||||||
|
isInMode: function () { return mode !== null; },
|
||||||
|
currentMode: function () { return mode; },
|
||||||
|
bossKey: bossKeyActivate,
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,8 @@
|
||||||
if (!text || isWaiting) return;
|
if (!text || isWaiting) return;
|
||||||
|
|
||||||
// ─── CLI interception ───
|
// ─── CLI interception ───
|
||||||
if (text.startsWith('/') && window.__jaeCLI && typeof window.__jaeCLI.handle === 'function') {
|
const inCliMode = window.__jaeCLI && typeof window.__jaeCLI.isInMode === 'function' && window.__jaeCLI.isInMode();
|
||||||
|
if ((text.startsWith('/') || inCliMode) && window.__jaeCLI && typeof window.__jaeCLI.handle === 'function') {
|
||||||
isWaiting = true;
|
isWaiting = true;
|
||||||
chatInput.value = '';
|
chatInput.value = '';
|
||||||
const userBubble = addMessage('user', text);
|
const userBubble = addMessage('user', text);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue