aegiscode 6.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -72,42 +72,60 @@ Every command above that talks to AEGIS names a tool from `mcp/tools.js`, and
72
72
  the test suite asserts both directions: no command points at a tool that does
73
73
  not exist, and no tool is unreachable from the prompt.
74
74
 
75
+ Command names, aliases and categories follow `aegiscodex-dev`'s registry, so the
76
+ vocabulary matches the other terminal host: the AEGIS family is spelled
77
+ `/aegis-ask`, `/aegis-status`, `/aegis-recall`, `/aegis-remember`, `/aegis-import`
78
+ (with `/ask`, `/status`, `/recall`, `/remember` kept as aliases), and the session
79
+ and model controls are its `help`/`?`/`h`, `exit`/`quit`, `clear`/`cls`,
80
+ `theme`/`t`, `version`/`v` and `model`/`m`. Commands that need a local agent loop
81
+ this client deliberately does not have — `/login`, `/doctor`, `/permissions`,
82
+ `/mcp`, `/skills`, `/hooks`, `/agents`, `/resume` and a few more — are listed but
83
+ answer honestly with the reason and the nearest working alternative rather than
84
+ pretending to work.
85
+
75
86
  ## What it looks like
76
87
 
77
88
  ```
78
- ▄▄▄▄▄▄▄
79
- ▟███████▙
80
- ▜███▀███▛
81
- ▜█████▛
82
- ▜███▛
83
- ▜▛
84
-
85
- A E G I S C O D E
86
- Cloud brain in your shell.
87
-
88
- ┏━ aegiscode ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
89
- version v6.0.0 ┃
90
- ┃ model nexus-brain ┃
91
- base https://aegiscloud.org ┃
92
- key aegis_••••4f2a ┃
93
- ┃ render streaming ┃
94
- ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
95
- type /help for commands ∙ /quit to exit
96
-
97
- ┃» you
98
- ┃ summarise what changed in the token accounting
99
- ┃⬢ aegis
100
- ┃ Three things changed, and one of them was costing you money:
101
-
102
- ┃ ∙ the pool merges worker usage instead of overwriting it
103
- cache reads and writes are billed, not ignored
104
- a zero-token call no longer refunds its reservation
105
-
106
- ┃ merge_usage({input_tokens: 1250}, {output_tokens: 312})
107
- ┃ => 1562 total
108
- ┣─ nexus-brain 1,562 tok 1,250/312 ∙ €0.0007 ∙ 4.2s ∙ 4 calls
109
-
110
- ⬢ aegis ∙ nexus-brain ∙ 1,562 tok ∙ €0.0007 ∙ stream ctrl+c quit
89
+ ━────────────────────────────────────────────────────────────────────────────━
90
+
91
+ ✦ █████▓▓░ ▓▓▓▓▓▓▓▓▓ ✦ ▒▒▒▒▒▒▒▒▒▒▒▒
92
+ ✦ ███▓░ ░░ ▓▓▓▓▓▓▓░░░ ▒▒▒▒▒▒▒▒▒▒▒▒▒
93
+ ░░░░ ███▓░ ▓▓▓▓░░░░░░ ██▒▒▒▒▒▒▒▒▒▒▒▒▓▓
94
+ ░░░░░░░░ ███▓░ ░░░░░░ ██▒▒▒▒▒▒▒▒▒▒▒▒▓
95
+ ░░░░░░░░░░░░ ███▓░ ✦ · · ▒▒▒▒▒▒▒▒▒▒▒▒▒▒
96
+ ██▓░░ ▓ ░░░░░░░░░░░░
97
+ ░▓▓███▓▓░
98
+ ▐▛███▜▌
99
+ ▝▜█████▛▘
100
+ ▘▘ ▝▝
101
+
102
+ Welcome to AEGIS Code
103
+ v6.1.0 · Cloud brain in your shell.
104
+
105
+ ───────────────────────────────── aegiscode ──────────────────────────────────
106
+ version v6.1.0
107
+ model nexus-brain
108
+ base https://aegiscloud.org
109
+ key aegis_••••4f2a
110
+ render streaming
111
+
112
+ type /help for commands · /quit to exit
113
+
114
+ summarise what changed in the token accounting
115
+ Three things changed, and one of them was costing you money:
116
+
117
+ - the pool merges worker usage instead of overwriting it
118
+ - cache reads and writes are billed, not ignored
119
+ - a zero-token call no longer refunds its reservation
120
+
121
+ merge_usage({input_tokens: 1250}, {output_tokens: 312})
122
+ => 1562 total
123
+
124
+ Use /balance to see tokens beside € on every row.
125
+ ⎿ nexus-brain · 1,562 tok · 1,250/312 · €0.0007 · 4.2s · 4 calls
126
+
127
+ * Consulting… (2.1s) esc to interrupt
128
+ aegis · nexus-brain · 1,562 tok · €0.0007 · stream ctrl+c quit
111
129
  ```
112
130
 
113
131
  Render it yourself, with no key and no network:
@@ -119,12 +137,15 @@ node cli/scripts/demo.mjs # from a source checkout
119
137
 
120
138
  ## Design notes
121
139
 
122
- **It is not a Claude Code look-alike.** Violet-and-cyan on ink; a hexagonal
123
- shield sigil rather than a mascot; a `»` prompt and a `┃` transcript rail rather
124
- than `❯` and `✻`; heavy box corners rather than rounded ones. This is a
125
- deliberate divergence, not an accident of taste: `test/cli-identity.test.mjs`
126
- fails if any of Claude Code's exact RGB triples, glyphs, spinner frames or art
127
- rows appear in this host.
140
+ **It looks like `aegiscodex-dev` on purpose.** The gold/coral/lavender palette,
141
+ the `✦`-studded welcome mark (mascot, crescent moon and diving whale), the `❯`
142
+ prompt, the `⎿` hook rows and the `✻` spinner line are the `aegiscodex-dev`
143
+ design system, adopted wholesale rather than approximated: the palette, glyphs,
144
+ welcome art, working verbs and command vocabulary all come from it, and
145
+ `test/cli-conformance.test.mjs` pins every RGB triple, glyph, spinner frame, verb
146
+ and art row to that source so the two hosts cannot drift apart. (An earlier
147
+ revision of this CLI was a deliberate divergence — a violet/cyan "Signal" theme
148
+ with a test asserting it. That direction is gone.)
128
149
 
129
150
  **A linear transcript, not a full-screen TUI.** Finished turns are written once
130
151
  to the scrollback; only the one live line (spinner, elapsed time) is redrawn.
@@ -139,12 +160,15 @@ free usage next to a token count.
139
160
  ## Tests
140
161
 
141
162
  ```bash
142
- node ../../test/cli-identity.test.mjs # the divergence guard
143
- node ../../test/cli-render.test.mjs # width safety, accounting, live region
144
- node ../../test/cli-tools.test.mjs # registry parity with the MCP host + dispatch
145
- node ../../test/cli-run.test.mjs # the real binary against a real backend
146
- node ../../test/cli-package.test.mjs # the published layout, isolated from the repo
147
- npm test # all of the above
163
+ node ../../test/cli-conformance.test.mjs # the design guard: palette, glyphs, art, verbs
164
+ node ../../test/cli-render.test.mjs # width safety, accounting, live region
165
+ node ../../test/cli-overlays.test.mjs # the / palette, model + effort pickers, resume list
166
+ node ../../test/cli-fuzzy.test.mjs # palette ranking and match positions
167
+ node ../../test/cli-markdown.test.mjs # span-line markdown, cell widths
168
+ node ../../test/cli-tools.test.mjs # registry parity with the MCP host + dispatch
169
+ node ../../test/cli-run.test.mjs # the real binary against a real backend
170
+ node ../../test/cli-package.test.mjs # the published layout, isolated from the repo
171
+ npm test # all of the above
148
172
  ```
149
173
 
150
174
  `npm run predist` stages `client/`, `mcp/tools.js` and the desktop's `usage.js`
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aegiscode",
3
3
  "productName": "AEGIS Code",
4
- "version": "6.0.0",
4
+ "version": "6.1.0",
5
5
  "description": "aegiscode — the command-line version of AEGIS Desktop. The shared tool surface in your shell, over the same thin transport and tool registry as the MCP plugin and the desktop app. Ships transport + UI only; no brain.",
6
6
  "author": {
7
7
  "name": "AEGIS Code",
@@ -31,7 +31,7 @@
31
31
  "start": "node bin/aegiscode.js",
32
32
  "predist": "node scripts/predist.mjs",
33
33
  "prepublishOnly": "npm run predist",
34
- "check": "node --check bin/aegiscode.js && node --check src/theme.js && node --check src/art.js && node --check src/screen.js && node --check src/format.js && node --check src/render.js && node --check src/commands.js && node --check src/deps.js && node --check src/app.js && node --check scripts/predist.mjs",
34
+ "check": "node --check bin/aegiscode.js && node --check src/theme.js && node --check src/art.js && node --check src/screen.js && node --check src/format.js && node --check src/fuzzy.js && node --check src/markdown.js && node --check src/render.js && node --check src/overlays.js && node --check src/commands.js && node --check src/deps.js && node --check src/app.js && node --check scripts/predist.mjs",
35
35
  "test": "for f in ../../test/cli-*.test.mjs; do node \"$f\" || exit 1; done"
36
36
  },
37
37
  "keywords": [
package/scripts/demo.mjs CHANGED
@@ -87,6 +87,6 @@ push(render.renderStatus(ctx, { model: 'nexus-brain', tokens: 1562, spend: 0.000
87
87
  process.stdout.write((plain ? out.map(stripAnsi) : out).join('\n') + '\n\n');
88
88
  if (!plain) {
89
89
  process.stdout.write(
90
- stripAnsi(`${art.WORDMARK} — ${out.length} lines rendered at ${width} cols${ctx.light ? ' (light)' : ''}\n`)
90
+ stripAnsi(`${art.WELCOME_TITLE} — ${out.length} lines rendered at ${width} cols${ctx.light ? ' (light)' : ''}\n`)
91
91
  );
92
92
  }
package/src/app.js CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  const readline = require('node:readline');
15
15
  const { createTools, createClient, usageTokens } = require('./deps.js');
16
- const { GLYPH, VERBS, themeOf, RESET } = require('./theme.js');
16
+ const { GLYPH, VERBS, themeOf, RESET, BOLD } = require('./theme.js');
17
17
  const { LiveRegion, termWidth, EC, w } = require('./screen.js');
18
18
  const { parseLine, findCommand, COMMANDS } = require('./commands.js');
19
19
  const render = require('./render.js');
@@ -289,21 +289,67 @@ function createApp(options = {}) {
289
289
  emit(render.renderToolResult(ctx(), name, text, width()));
290
290
  }
291
291
 
292
+ // Category order and labels, matching aegiscodex-dev's palette.
293
+ const CATEGORY_ORDER = ['aegis', 'model', 'session', 'data', 'auth', 'support', 'workspace'];
294
+ const CATEGORY_LABEL = {
295
+ aegis: 'Aegis plugin',
296
+ model: 'Model & behavior',
297
+ session: 'Session & context',
298
+ data: 'Data',
299
+ auth: 'Auth',
300
+ support: 'Support',
301
+ workspace: 'Workspace',
302
+ };
303
+
304
+ /**
305
+ * The nearest routable name to a mistyped one: a prefix of it, or a name it
306
+ * is a prefix of. Deliberately simple — `cli/src/fuzzy.js` is a separate
307
+ * workstream and may not exist, so this must not depend on it.
308
+ */
309
+ function nearest(name) {
310
+ const n = String(name || '').toLowerCase();
311
+ if (!n) return null;
312
+ let best = null;
313
+ for (const c of COMMANDS) {
314
+ for (const cand of [c.name, ...(c.aliases || [])]) {
315
+ if (cand === n) continue;
316
+ if (cand.startsWith(n) || n.startsWith(cand)) {
317
+ if (best == null || Math.abs(cand.length - n.length) < Math.abs(best.length - n.length)) {
318
+ best = cand;
319
+ }
320
+ }
321
+ }
322
+ }
323
+ return best;
324
+ }
325
+
292
326
  function printHelp() {
327
+ const t = themeOf(ctx());
293
328
  emit([render.renderHeading(ctx(), 'commands', width())]);
294
- const rows = COMMANDS.map((c) => {
295
- const usage = `/${c.name}${c.args ? ' ' + c.args : ''}`;
296
- return { usage, help: c.help, aliases: (c.aliases || []).map((a) => `/${a}`).join(' ') };
297
- });
329
+ const rows = COMMANDS.filter((c) => !c.unavailable).map((c) => ({
330
+ cat: c.category || 'other',
331
+ usage: `/${c.name}${c.args ? ' ' + c.args : ''}`,
332
+ desc: c.desc,
333
+ aliases: (c.aliases || []).map((a) => `/${a}`).join(' '),
334
+ }));
298
335
  const widest = rows.reduce((m, r) => Math.max(m, r.usage.length), 0);
299
- const t = themeOf(ctx());
300
- for (const r of rows) {
301
- const alias = r.aliases ? render.fg(t, t.dim) + ` (${r.aliases})` + RESET : '';
302
- emit([` ${render.fg(t, t.plasma)}${'/' + r.usage.slice(1)}` +
303
- ' '.repeat(Math.max(1, widest - r.usage.length + 2)) +
304
- render.fg(t, t.text) + r.help + RESET + alias]);
336
+ for (const cat of CATEGORY_ORDER) {
337
+ const group = rows.filter((r) => r.cat === cat);
338
+ if (!group.length) continue;
339
+ emit(['', `${t.dim}${BOLD}${CATEGORY_LABEL[cat] || cat}${RESET}`]);
340
+ for (const r of group) {
341
+ const alias = r.aliases ? `${t.dim} (${r.aliases})${RESET}` : '';
342
+ emit([
343
+ ` ${t.gold}${r.usage}${RESET}${' '.repeat(Math.max(1, widest - r.usage.length + 2))}` +
344
+ `${t.white}${r.desc}${RESET}${alias}`,
345
+ ]);
346
+ }
347
+ }
348
+ const unavail = COMMANDS.filter((c) => c.unavailable).map((c) => `/${c.name}`);
349
+ if (unavail.length) {
350
+ emit(['', `${t.dim}not available in this client: ${unavail.join(' ')}${RESET}`]);
305
351
  }
306
- emit(['', render.renderNotice(ctx(), 'info', `plain text is a prompt ${GLYPH.bullet} /quit exits`)]);
352
+ emit(['', render.renderNotice(ctx(), 'info', `plain text is a prompt ${GLYPH.bullet} /exit exits`)]);
307
353
  }
308
354
 
309
355
  /** Handle one line of input. Returns false when the session should end. */
@@ -316,7 +362,24 @@ function createApp(options = {}) {
316
362
  return true;
317
363
  }
318
364
  if (parsed.kind === 'unknown') {
319
- emit(render.renderNotice(ctx(), 'error', `unknown command: /${parsed.name} — try /help`));
365
+ const alt = nearest(parsed.name);
366
+ emit(
367
+ render.renderNotice(
368
+ ctx(),
369
+ 'error',
370
+ `unknown command: /${parsed.name}${alt ? ` — did you mean /${alt}?` : ` — try /help`}`
371
+ )
372
+ );
373
+ return true;
374
+ }
375
+ if (parsed.kind === 'unavailable') {
376
+ const c = parsed.command;
377
+ emit(render.renderNotice(ctx(), 'warn', `/${c.name} is not available in aegiscode — ${c.why}`));
378
+ if (c.alt) emit(render.renderNotice(ctx(), 'info', `try ${c.alt} instead`));
379
+ else {
380
+ const alt = nearest(c.name);
381
+ if (alt) emit(render.renderNotice(ctx(), 'info', `try /${alt} instead`));
382
+ }
320
383
  return true;
321
384
  }
322
385
 
@@ -325,15 +388,27 @@ function createApp(options = {}) {
325
388
 
326
389
  if (cmd.local) {
327
390
  switch (cmd.local) {
328
- case 'quit':
391
+ case 'exit':
329
392
  return false;
330
393
  case 'clear':
331
394
  out.write(EC.clearScreen);
395
+ // aegiscodex-dev's /clear starts a new session with empty context, so
396
+ // the session tallies reset too (the transcript is not persisted here).
397
+ session.turns = 0;
398
+ session.calls = 0;
399
+ session.tokens = 0;
400
+ session.inputTokens = 0;
401
+ session.outputTokens = 0;
402
+ session.cost = 0;
403
+ session.startedAt = Date.now();
332
404
  emit(bannerLines());
333
405
  return true;
334
406
  case 'help':
335
407
  printHelp();
336
408
  return true;
409
+ case 'version':
410
+ emit(render.renderNotice(ctx(), 'info', `aegiscode v${VERSION}`));
411
+ return true;
337
412
  case 'model':
338
413
  if (!arg) {
339
414
  emit(render.renderNotice(ctx(), 'info', `model: ${opts.model || 'server default'}`));
@@ -353,13 +428,14 @@ function createApp(options = {}) {
353
428
  opts.light = arg ? /^light/i.test(arg) : !opts.light;
354
429
  emit(render.renderNotice(ctx(), 'ok', `theme: ${opts.light ? 'light' : 'dark'}`));
355
430
  return true;
356
- case 'cost': {
431
+ case 'cost':
432
+ case 'tokens': {
357
433
  const t = themeOf(ctx());
358
434
  emit([render.renderHeading(ctx(), 'session', width())]);
359
435
  emit([
360
- ` tokens ${render.fg(t, t.text)}${fmtTokens(session.tokens)}${RESET} ` +
361
- render.fg(t, t.muted) + `(${fmtTokens(session.inputTokens)} in / ${fmtTokens(session.outputTokens)} out)` + RESET,
362
- ` spend ${render.fg(t, t.pulse)}${fmtEur(session.cost)}${RESET}`,
436
+ ` tokens ${t.white}${fmtTokens(session.tokens)}${RESET} ` +
437
+ t.gray + `(${fmtTokens(session.inputTokens)} in / ${fmtTokens(session.outputTokens)} out)` + RESET,
438
+ ` spend ${t.green}${fmtEur(session.cost)}${RESET}`,
363
439
  ` calls ${session.calls}`,
364
440
  ` balance ${session.balance == null ? 'unknown' : fmtEur(session.balance)}`,
365
441
  ` elapsed ${fmtElapsed(Date.now() - session.startedAt)}`,
@@ -397,7 +473,7 @@ function createApp(options = {}) {
397
473
  }
398
474
  args = { ...args, api_key: key };
399
475
  }
400
- if (cmd.name === 'ask') return await runPrompt(args.prompt);
476
+ if (cmd.tool === 'aegis_ask') return await runPrompt(args.prompt);
401
477
  await runTool(cmd.tool, args);
402
478
  } catch (e) {
403
479
  emit(render.renderNotice(ctx(), 'error', e.message));
@@ -412,7 +488,7 @@ function createApp(options = {}) {
412
488
  }
413
489
  return new Promise((resolve) => {
414
490
  const t = themeOf(ctx());
415
- out.write(render.fg(t, t.muted) + promptText + RESET);
491
+ out.write(t.gray + promptText + RESET);
416
492
  const stdin = process.stdin;
417
493
  let buf = '';
418
494
  stdin.setRawMode(true);
@@ -477,9 +553,9 @@ function createApp(options = {}) {
477
553
  if (tokens != null) {
478
554
  const t = themeOf(ctx());
479
555
  out.write(
480
- render.fg(t, t.dim) + GLYPH.spend + ' ' + render.fg(t, t.beam) + (res.model || 'aegis') + RESET +
481
- render.fg(t, t.dim) + ' ' + GLYPH.bullet + ' ' + RESET +
482
- render.fg(t, t.text) + `${fmtTokens(tokens)} tok` + RESET + '\n'
556
+ t.dim + GLYPH.spend + ' ' + t.blue + (res.model || 'aegis') + RESET +
557
+ t.dim + ' ' + GLYPH.bullet + ' ' + RESET +
558
+ t.white + `${fmtTokens(tokens)} tok` + RESET + '\n'
483
559
  );
484
560
  }
485
561
  }
@@ -493,7 +569,7 @@ function createApp(options = {}) {
493
569
  out.write('\n');
494
570
 
495
571
  const rl = options.readline || readline.createInterface({ input: process.stdin, output: out, terminal: true });
496
- const promptStr = render.fg(themeOf(ctx()), themeOf(ctx()).plasma) + GLYPH.prompt + ' ' + RESET;
572
+ const promptStr = themeOf(ctx()).gold + GLYPH.prompt + ' ' + RESET;
497
573
  rl.setPrompt(promptStr);
498
574
  rl.prompt();
499
575
 
package/src/art.js CHANGED
@@ -1,45 +1,235 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * AEGIS brand art.
4
+ * Welcome art — the aegiscodex-dev mark, adopted wholesale.
5
5
  *
6
- * A hexagonal shield with a signal core the mark that replaces the mascot
7
- * art every Claude Code derivative (including this repo's sibling project
8
- * aegiscodex-dev) ships. Same job, different iconography on purpose:
9
- * `test/cli-identity.test.mjs` asserts Claude Code's own art never appears here.
6
+ * The composition is copied from `aegiscodex-dev/src/art.js`, which took the
7
+ * glyph rows from the shipped Claude Code binary: a feather plume (░), a top hat
8
+ * (█ ▓ ▒), the spark face (▐▛███▜▌ / ▝▜█████▛▘ / ▘▘ ▝▝), plus a crescent moon and
9
+ * a diving whale for the second brain. Side-by-side when the terminal is wide
10
+ * enough, stacked when it isn't.
10
11
  *
11
- * Every row is the same display width (9 cells) so it can be centred or stacked
12
- * without drift; `tests` assert that rather than trusting it.
13
- *
14
- * The wordmark spells the whole product name (`aegiscode`) with the same letter
15
- * spacing the shorter `AEGIS` mark used, now that the host and the CLI have been
16
- * folded onto one name. `test/cli-identity.test.mjs` strips the spacing and
17
- * asserts the mark still names the product.
12
+ * Every row of a block is the same display width, so `center()` and
13
+ * `joinSideBySide()` stay exact. `test/cli-conformance.test.mjs` asserts both
14
+ * that property and that these rows still match aegiscodex-dev's source, so the
15
+ * mark can't drift.
18
16
  */
19
17
 
20
- const SIGIL = [
21
- ' ▄▄▄▄▄▄▄ ',
22
- '▟███████▙',
23
- '▜███▀███▛',
24
- ' ▜█████▛ ',
25
- ' ▜███▛ ',
26
- ' ▜▛ ',
18
+ // The mascot face — exact glyphs from the binary (msS template).
19
+ const FACE = [' ▐▛███▜▌', '▝▜█████▛▘', ' ▘▘ ▝▝'];
20
+
21
+ // Top hat + feather plume, composed from the extracted row glyphs.
22
+ const HAT = [
23
+ ' ✦ ░░░░░░░░░░ ✦',
24
+ ' ░░░░░░░░░░░░░░░░',
25
+ ' █████▓▓░',
26
+ ' ███▓░',
27
+ ' ███▓░',
28
+ ' ███▓░',
29
+ ' ░▓▓███▓▓░',
30
+ ' ▒▒░░▒▒ ▒ ▒▒',
31
+ ];
32
+
33
+ // Plume + hat + face: the left half of the welcome mark.
34
+ const MASCOT_ART = [
35
+ ' ✦ █████▓▓░',
36
+ ' ✦ ███▓░ ░░',
37
+ ' ░░░░ ███▓░',
38
+ ' ░░░░░░░░ ███▓░',
39
+ ' ░░░░░░░░░░░░ ███▓░',
40
+ ' ██▓░░ ▓',
41
+ ' ░▓▓███▓▓░',
42
+ ...FACE,
43
+ ];
44
+
45
+ // Crescent moon in a starry night sky — lit rim (▓) with a faint earthshine
46
+ // body (░). Rows are equal code-point length (13).
47
+ const MOON = [
48
+ ' ✦ ▓▓▓ ✦ ',
49
+ ' ▓▓▓▓▓ ✦',
50
+ ' ▓▓▓▓▓▓▓▓▓ ✦',
51
+ ' ▓▓▓▓▓▓▓░░░ ',
52
+ ' ▓▓▓▓░░░░░░ ',
53
+ ' ░░░░░░ ',
54
+ ' ✦ · · ',
27
55
  ];
28
56
 
29
- /** Cell offsets (row, col) of the core, painted in the secondary colour. */
30
- const CORE = [{ row: 2, col: 4 }];
57
+ // The diving whale: spout (░), eye (██) on the rounded head, body (▒) sweeping
58
+ // right to a small forked fluke (▓), water line (░) beneath. Rows are equal
59
+ // code-point length (18).
60
+ const WHALE = [
61
+ ' ✦ ░ ░ ░ ',
62
+ ' ✦ ░░░░░ ',
63
+ ' ▒▒▒▒▒▒▒▒▒▒▒▒ ',
64
+ ' ▒▒▒▒▒▒▒▒▒▒▒▒▒ ',
65
+ ' ██▒▒▒▒▒▒▒▒▒▒▒▒▓▓',
66
+ ' ██▒▒▒▒▒▒▒▒▒▒▒▒▓ ',
67
+ ' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ',
68
+ ' ░░░░░░░░░░░░ ',
69
+ ];
70
+
71
+ // Moon above the whale — the right half of the mark, 2-col gutter. The moon is
72
+ // padded with a trailing blank row so joinSideBySide keeps both blocks aligned.
73
+ const MOON_WHALE = joinSideBySide([...MOON, ''], WHALE, 2);
74
+
75
+ /** Full welcome mark: mascot + (moon + whale) side by side, bottom-aligned. */
76
+ const WELCOME_ART = joinSideBySide(MASCOT_ART, MOON_WHALE, 2);
31
77
 
32
- const WORDMARK = 'A E G I S C O D E';
78
+ /** Narrow-terminal fallback: mascot stacked above (moon + whale). */
79
+ const WELCOME_ART_STACKED = stackVertical(MASCOT_ART, MOON_WHALE, 1);
80
+
81
+ /** Welcome copy, in aegiscode-dev's frame: coral title over the mark. */
82
+ const WELCOME_TITLE = 'Welcome to AEGIS Code';
83
+ const WELCOME_BACK = 'Welcome back!';
33
84
  const TAGLINE = 'Cloud brain in your shell.';
34
85
 
86
+ // ── Terminal portability ─────────────────────────────────────────────────────
87
+ // The block-glyph palette and ✦ stars are single-width on Linux terminals, but
88
+ // not everywhere: Windows fonts often lack the U+259B..U+259F face edges and
89
+ // mangle █▓▒░ entirely; on macOS ✦ renders as a double-width emoji. Each pass
90
+ // swaps offending code points 1:1 (same character count per row) so the width
91
+ // maths stays exact and the mark keeps its shape.
92
+ const ASCII_STENCIL = new Map([
93
+ ['█', '#'],
94
+ ['▓', '@'],
95
+ ['▒', '='],
96
+ ['░', '.'],
97
+ ['▐', '#'],
98
+ ['▛', '#'],
99
+ ['▜', '#'],
100
+ ['▌', '#'],
101
+ ['▝', '#'],
102
+ ['▘', '#'],
103
+ ['✦', '*'],
104
+ ['·', '.'],
105
+ ]);
106
+
107
+ const DARWIN_STENCIL = new Map([
108
+ ['▐', '#'],
109
+ ['▛', '#'],
110
+ ['▜', '#'],
111
+ ['▌', '#'],
112
+ ['▝', '#'],
113
+ ['▘', '#'],
114
+ ['✦', '*'],
115
+ ]);
116
+
117
+ /** One platform pass over an art block. */
118
+ function portability(rows) {
119
+ if (process.platform === 'win32') {
120
+ return rows.map((r) => [...r].map((ch) => ASCII_STENCIL.get(ch) ?? ch).join(''));
121
+ }
122
+ if (process.platform === 'darwin') {
123
+ return rows.map((r) => [...r].map((ch) => DARWIN_STENCIL.get(ch) ?? ch).join(''));
124
+ }
125
+ return rows;
126
+ }
127
+
128
+ /** The mark that fits the current terminal: side-by-side when there's room. */
129
+ function welcomeArtFor(cols) {
130
+ const wide = portability(WELCOME_ART);
131
+ const stacked = portability(WELCOME_ART_STACKED);
132
+ const W = Math.max(...wide.map((r) => [...r].length));
133
+ return cols >= W + 4 ? wide : stacked;
134
+ }
135
+
35
136
  /**
36
- * Left-pad every row of `art` so it sits centred inside `width`.
37
- * @returns {string[]}
137
+ * Split the mark into its two halves so each can carry its own theme ink.
138
+ * @returns {{rows: Array<[string,string]>, width: number, gutter: number}}
38
139
  */
39
- function center(art, width) {
40
- const w = art.reduce((m, r) => Math.max(m, [...r].length), 0);
41
- const pad = Math.max(0, Math.floor((width - w) / 2));
42
- return art.map((r) => ' '.repeat(pad) + r);
140
+ function welcomeArtParts(cols) {
141
+ const mascot = portability(MASCOT_ART);
142
+ const moonWhale = portability(MOON_WHALE);
143
+ const L = Math.max(...mascot.map((r) => [...r].length));
144
+ const R = Math.max(...moonWhale.map((r) => [...r].length));
145
+ const wide = cols >= L + R + 2 + 4;
146
+ const gutter = wide ? 2 : 1;
147
+ if (wide) {
148
+ // The right half carries the moon *and* the whale. Using WHALE alone here —
149
+ // which the reference does, because it only wants the per-mascot ink split —
150
+ // would silently drop the crescent from the welcome screen this CLI renders.
151
+ const n = Math.max(mascot.length, moonWhale.length);
152
+ const rows = [];
153
+ for (let i = 0; i < n; i++) {
154
+ const li = i + (n - mascot.length);
155
+ const ri = i + (n - moonWhale.length);
156
+ rows.push([(mascot[li] ?? '').padEnd(L), (moonWhale[ri] ?? '').padEnd(R)]);
157
+ }
158
+ return { rows, width: L + gutter + R, gutter };
159
+ }
160
+ const w = Math.max(L, R);
161
+ const centerBlock = (rows) =>
162
+ rows.map((r) => ' '.repeat(Math.max(0, Math.floor((w - [...r].length) / 2))) + r);
163
+ return {
164
+ rows: [
165
+ ...centerBlock(mascot).map((c) => [c, '']),
166
+ ['', ''],
167
+ ...centerBlock(moonWhale).map((r) => ['', r]),
168
+ ],
169
+ width: w,
170
+ gutter,
171
+ };
172
+ }
173
+
174
+ /** Stack two art blocks vertically, each centred, separated by `gap` rows. */
175
+ function stackVertical(top, bottom, gap = 1) {
176
+ const w = Math.max(...[...top, ...bottom].map((r) => [...r].length));
177
+ const centerBlock = (rows) =>
178
+ rows.map((r) => {
179
+ const pad = Math.max(0, Math.floor((w - [...r].length) / 2));
180
+ return ' '.repeat(pad) + r;
181
+ });
182
+ return [...centerBlock(top), ...Array(gap).fill(''), ...centerBlock(bottom)];
183
+ }
184
+
185
+ /** Join two art stacks row-aligned (bottom-aligned by default). */
186
+ function joinSideBySide(left, right, gutter = 2, align = 'bottom') {
187
+ const L = Math.max(...left.map((r) => [...r].length));
188
+ const R = Math.max(...right.map((r) => [...r].length));
189
+ const n = Math.max(left.length, right.length);
190
+ return Array.from({ length: n }, (_, i) => {
191
+ const li = align === 'bottom' ? i + (n - left.length) : i;
192
+ const ri = align === 'bottom' ? i + (n - right.length) : i;
193
+ const l = (left[li] ?? '').padEnd(L);
194
+ const r = (right[ri] ?? '').padEnd(R);
195
+ return l + ' '.repeat(gutter) + r;
196
+ });
197
+ }
198
+
199
+ /** Centre every row of an art block inside `width` (left pad + right fill). */
200
+ function center(rows, width) {
201
+ return rows.map((r) => {
202
+ const pad = Math.max(0, Math.floor((width - [...r].length) / 2));
203
+ return ' '.repeat(pad) + r + ' '.repeat(Math.max(0, width - [...r].length - pad));
204
+ });
205
+ }
206
+
207
+ /** Centre each row against the block's own max width (no trailing fill). */
208
+ function padBoth(rows, width) {
209
+ const w = Math.max(...rows.map((r) => [...r].length));
210
+ return rows.map((r) => {
211
+ const pad = Math.floor((width - w) / 2);
212
+ return ' '.repeat(Math.max(0, pad)) + r;
213
+ });
43
214
  }
44
215
 
45
- module.exports = { SIGIL, CORE, WORDMARK, TAGLINE, center, SIGIL_WIDTH: 9 };
216
+ module.exports = {
217
+ FACE,
218
+ HAT,
219
+ MASCOT_ART,
220
+ MOON,
221
+ WHALE,
222
+ MOON_WHALE,
223
+ WELCOME_ART,
224
+ WELCOME_ART_STACKED,
225
+ WELCOME_TITLE,
226
+ WELCOME_BACK,
227
+ TAGLINE,
228
+ portability,
229
+ welcomeArtFor,
230
+ welcomeArtParts,
231
+ stackVertical,
232
+ joinSideBySide,
233
+ center,
234
+ padBoth,
235
+ };