aegiscode 5.2.32 → 6.0.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/src/screen.js ADDED
@@ -0,0 +1,177 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Terminal plumbing: cell-accurate width maths, wrapping, and the ephemeral
5
+ * "live" region the CLI redraws while a call is in flight.
6
+ *
7
+ * Design note — this CLI is a LINEAR transcript, not a full-screen alt-buffer
8
+ * TUI (which is what Claude Code and aegiscodex-dev are). Finished turns are
9
+ * written once to scrollback, so output stays selectable, pipeable and
10
+ * scrollback-searchable; only the one live status/spinner line is redrawn. That
11
+ * is a deliberate product difference, and it is also why there is no alt-screen
12
+ * or mouse handling here.
13
+ */
14
+
15
+ const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
16
+
17
+ /** Combining marks and variation selectors occupy no cell. */
18
+ const RE_ZERO = /[\u0300-\u036f\ufe00-\ufe0f\u200b-\u200f]/;
19
+ /** Wide (CJK/fullwidth/emoji) ranges occupy two cells. */
20
+ const RE_WIDE =
21
+ /[\u1100-\u115f\u2e80-\u303e\u3041-\u33ff\u3400-\u4dbf\u4e00-\u9fff\ua000-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe30-\ufe6f\uff00-\uff60\uffe0-\uffe6]|[\u{1f300}-\u{1faff}]|[\u{1f000}-\u{1f2ff}]/u;
22
+
23
+ function stripAnsi(s) {
24
+ return String(s).replace(ANSI_RE, '');
25
+ }
26
+
27
+ /** Display width of a string in terminal cells. */
28
+ function w(s) {
29
+ let n = 0;
30
+ for (const ch of stripAnsi(s)) {
31
+ if (RE_ZERO.test(ch)) continue;
32
+ n += RE_WIDE.test(ch) ? 2 : 1;
33
+ }
34
+ return n;
35
+ }
36
+
37
+ /** Truncate to `width` cells without cutting a wide/combining codepoint. */
38
+ function clip(s, width) {
39
+ if (w(s) <= width) return s;
40
+ let out = '';
41
+ let used = 0;
42
+ for (const ch of s) {
43
+ const cw = RE_ZERO.test(ch) ? 0 : RE_WIDE.test(ch) ? 2 : 1;
44
+ if (used + cw > width) break;
45
+ out += ch;
46
+ used += cw;
47
+ }
48
+ return out;
49
+ }
50
+
51
+ /** Pad (or clip) to exactly `width` cells. */
52
+ function pad(s, width) {
53
+ const t = clip(s, width);
54
+ return t + ' '.repeat(Math.max(0, width - w(t)));
55
+ }
56
+
57
+ /** Right-align inside `width`. */
58
+ function padStart(s, width) {
59
+ const t = clip(s, width);
60
+ return ' '.repeat(Math.max(0, width - w(t))) + t;
61
+ }
62
+
63
+ /**
64
+ * Word-wrap one logical line to `width` cells, preserving words and never
65
+ * breaking mid-word unless the word alone exceeds the width.
66
+ */
67
+ function wrapLine(line, width, indent = '') {
68
+ const limit = Math.max(8, width);
69
+ if (w(line) <= limit) return [line];
70
+ const words = line.split(' ');
71
+ const out = [];
72
+ let cur = '';
73
+ for (const word of words) {
74
+ const piece = cur ? `${cur} ${word}` : word;
75
+ if (w(piece) <= limit) {
76
+ cur = piece;
77
+ continue;
78
+ }
79
+ if (cur) out.push(cur);
80
+ if (w(word) <= limit) {
81
+ cur = word;
82
+ continue;
83
+ }
84
+ // A single token longer than the line: hard-split on cells.
85
+ let rest = word;
86
+ while (w(rest) > limit) {
87
+ const head = clip(rest, limit);
88
+ out.push(head);
89
+ rest = rest.slice(head.length);
90
+ }
91
+ cur = rest;
92
+ }
93
+ if (cur || !out.length) out.push(cur);
94
+ return out;
95
+ }
96
+
97
+ /** Wrap a block of text (honours existing newlines), applying `indent` to
98
+ * continuation lines only when `hang` is true. */
99
+ function wrapBlock(text, width, indent = '', hang = false) {
100
+ const lines = [];
101
+ for (const raw of String(text).split('\n')) {
102
+ if (raw === '') {
103
+ lines.push('');
104
+ continue;
105
+ }
106
+ const parts = wrapLine(raw, width - w(indent));
107
+ parts.forEach((p, i) => lines.push(i === 0 || !hang ? indent + p : indent + p));
108
+ }
109
+ return lines;
110
+ }
111
+
112
+ // --- cursor / screen control ------------------------------------------------
113
+
114
+ const ESC = {
115
+ hideCursor: '\x1b[?25l',
116
+ showCursor: '\x1b[?25h',
117
+ clearToEnd: '\x1b[0K',
118
+ clearLine: '\x1b[2K',
119
+ clearScreen: '\x1b[2J\x1b[H',
120
+ up: (n) => (n > 0 ? `\x1b[${n}A` : ''),
121
+ down: (n) => (n > 0 ? `\x1b[${n}B` : ''),
122
+ col: (n) => `\x1b[${n}G`,
123
+ };
124
+
125
+ function termWidth(fallback = 80) {
126
+ return Math.max(20, (process.stdout && process.stdout.columns) || fallback);
127
+ }
128
+
129
+ /**
130
+ * A block of lines at the bottom of the transcript that can be redrawn in
131
+ * place. Every write goes through here, so the escape arithmetic lives in one
132
+ * place: `update()` erases the previous block, prints the new one, and keeps
133
+ * the cursor below it.
134
+ */
135
+ class LiveRegion {
136
+ constructor(out = process.stdout) {
137
+ this.out = out;
138
+ this.rows = 0;
139
+ this.active = false;
140
+ }
141
+
142
+ update(lines) {
143
+ const body = lines.map((l) => l + ESC.clearToEnd).join('\n');
144
+ if (!this.active) {
145
+ this.out.write(body);
146
+ this.active = true;
147
+ } else {
148
+ // Back to the first row of the old block, rewrite, clear the tail.
149
+ this.out.write(ESC.col(1) + ESC.up(this.rows) + body + '\n' + ESC.col(1) + ESC.up(1));
150
+ }
151
+ this.rows = lines.length;
152
+ }
153
+
154
+ /** Erase the block and return the cursor to its start, so the caller can
155
+ * print the durable version of the same content. */
156
+ clear() {
157
+ if (!this.active) return;
158
+ this.out.write(ESC.col(1) + ESC.clearLine);
159
+ for (let i = 1; i < this.rows; i++) this.out.write(`\n${ESC.clearLine}`);
160
+ this.out.write(ESC.up(this.rows - 1) + ESC.col(1));
161
+ this.rows = 0;
162
+ this.active = false;
163
+ }
164
+ }
165
+
166
+ module.exports = {
167
+ w,
168
+ clip,
169
+ pad,
170
+ padStart,
171
+ stripAnsi,
172
+ wrapLine,
173
+ wrapBlock,
174
+ EC: ESC,
175
+ LiveRegion,
176
+ termWidth,
177
+ };
package/src/theme.js ADDED
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * AEGIS terminal theme — "Signal".
5
+ *
6
+ * This CLI is deliberately NOT a Claude Code look-alike. The palette below is
7
+ * the whole point of the file: violet/cyan on ink, not the warm gold/coral
8
+ * scheme every Claude Code derivative ships. `test/cli-identity.test.mjs` pins
9
+ * that divergence — it fails if any of Claude Code's exact RGB triples, or its
10
+ * prompt/spinner glyphs, reappear here.
11
+ *
12
+ * Zero dependencies: SGR escapes are hand-built, like the rest of this repo.
13
+ */
14
+
15
+ const RESET = '\x1b[0m';
16
+ const BOLD = '\x1b[1m';
17
+ const DIM = '\x1b[2m';
18
+ const ITALIC = '\x1b[3m';
19
+ const UNDER = '\x1b[4m';
20
+ const BOLD_OFF = '\x1b[22m';
21
+ const ITALIC_OFF = '\x1b[23m';
22
+ const UNDER_OFF = '\x1b[24m';
23
+ const RESET_FG = '\x1b[39m';
24
+ const RESET_BG = '\x1b[49m';
25
+
26
+ const RGB = (r, g, b) => `\x1b[38;2;${r};${g};${b}m`;
27
+ const BG = (r, g, b) => `\x1b[48;2;${r};${g};${b}m`;
28
+
29
+ /**
30
+ * Dark theme (default). Every value is a truecolor triple; the palette leans
31
+ * cool, and `ink`/`panel` are blue-black rather than neutral grey so the
32
+ * transcript reads as a different product at a glance.
33
+ */
34
+ const SIGNAL = {
35
+ plasma: [124, 92, 255], // brand accent — sigil, prompts, active states
36
+ beam: [34, 211, 238], // secondary — structure, model ids, code
37
+ pulse: [52, 211, 153], // success
38
+ alert: [251, 113, 133], // warning
39
+ fault: [255, 99, 99], // error
40
+ muted: [148, 163, 184], // secondary text
41
+ dim: [71, 85, 105], // rails, rules, disabled
42
+ text: [226, 232, 240], // body
43
+ ink: [10, 12, 18], // deepest background (status bar, inverse)
44
+ panel: [22, 25, 34], // raised surface (banner box)
45
+ };
46
+
47
+ /** Light theme — same roles, daylight values. Never a Claude Code palette. */
48
+ const SIGNAL_LIGHT = {
49
+ plasma: [79, 55, 200],
50
+ beam: [8, 122, 145],
51
+ pulse: [5, 122, 85],
52
+ alert: [190, 55, 90],
53
+ fault: [185, 28, 28],
54
+ muted: [71, 85, 105],
55
+ dim: [148, 163, 184],
56
+ text: [15, 23, 42],
57
+ ink: [248, 250, 252],
58
+ panel: [241, 245, 249],
59
+ };
60
+
61
+ /**
62
+ * Glyphs. Chosen to share no character with Claude Code's prompt (`❯`),
63
+ * spinner (`✢ · ✻ * ✽ ✶`), block cursor (`●`), hook (`⎿`) or quote bar (`▎`).
64
+ */
65
+ const GLYPH = {
66
+ prompt: '»', // input prompt
67
+ sigil: '⬢', // AEGIS mark / assistant turns
68
+ rail: '┃', // heavy vertical gutter, both transcript roles
69
+ railEnd: '┣', // gutter corner where a turn's meta line attaches
70
+ spend: '∅', // money readout
71
+ ok: '✓',
72
+ err: '✗',
73
+ warn: '!',
74
+ bullet: '∙', // U+2219, not Claude's U+00B7
75
+ pointer: '▶', // selected row
76
+ divider: '─',
77
+ rule: '━',
78
+ spin: ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█', '▇', '▆', '▅', '▄', '▃', '▂'],
79
+ box: { tl: '┏', tr: '┓', bl: '┗', br: '┛', h: '━', v: '┃' }, // heavy, not rounded
80
+ };
81
+
82
+ /** Verbs for the working line — plain, no Claude Code whimsy. */
83
+ const VERBS = [
84
+ 'Consulting',
85
+ 'Routing',
86
+ 'Pooling',
87
+ 'Reasoning',
88
+ 'Drafting',
89
+ 'Checking',
90
+ 'Settling',
91
+ ];
92
+
93
+ const THEMES = { dark: SIGNAL, light: SIGNAL_LIGHT };
94
+
95
+ /** The palette object for a context, per the rule that colours are always
96
+ * derived from one of the two theme objects — never typed inline. */
97
+ function themeOf(ctx) {
98
+ return ctx && ctx.light ? SIGNAL_LIGHT : SIGNAL;
99
+ }
100
+
101
+ module.exports = {
102
+ RGB,
103
+ BG,
104
+ RESET,
105
+ BOLD,
106
+ DIM,
107
+ ITALIC,
108
+ UNDER,
109
+ BOLD_OFF,
110
+ ITALIC_OFF,
111
+ UNDER_OFF,
112
+ RESET_FG,
113
+ RESET_BG,
114
+ SIGNAL,
115
+ SIGNAL_LIGHT,
116
+ THEMES,
117
+ GLYPH,
118
+ VERBS,
119
+ themeOf,
120
+ };