@onesi/genesis-cli 0.1.13 → 0.2.1

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.
Files changed (67) hide show
  1. package/README.md +29 -2
  2. package/dist/beacons.js +68 -0
  3. package/dist/dials.js +7 -4
  4. package/dist/fleets.js +30 -0
  5. package/dist/flocks.d.ts +7 -1
  6. package/dist/flocks.js +281 -4
  7. package/dist/henry.d.ts +5 -1
  8. package/dist/henry.js +60 -1
  9. package/dist/hives-coverage.d.ts +12 -0
  10. package/dist/hives-coverage.js +133 -0
  11. package/dist/hives-node-transport.js +4 -1
  12. package/dist/hives-operations.d.ts +1 -0
  13. package/dist/hives-operations.js +2 -0
  14. package/dist/hives-platform.d.ts +13 -0
  15. package/dist/hives-platform.js +133 -0
  16. package/dist/hives-resources.d.ts +1 -0
  17. package/dist/hives-resources.js +5 -3
  18. package/dist/iam-directory.d.ts +15 -0
  19. package/dist/iam-directory.js +71 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.js +273 -30
  22. package/dist/minds.d.ts +1 -1
  23. package/dist/minds.js +37 -2
  24. package/dist/onboard/art.d.ts +50 -0
  25. package/dist/onboard/art.js +139 -0
  26. package/dist/onboard/brand.d.ts +56 -0
  27. package/dist/onboard/brand.js +134 -0
  28. package/dist/onboard/caps.d.ts +90 -0
  29. package/dist/onboard/caps.js +232 -0
  30. package/dist/onboard/encode.d.ts +23 -0
  31. package/dist/onboard/encode.js +235 -0
  32. package/dist/onboard/eye.d.ts +103 -0
  33. package/dist/onboard/eye.js +147 -0
  34. package/dist/onboard/index.d.ts +61 -0
  35. package/dist/onboard/index.js +562 -0
  36. package/dist/onboard/mcp.d.ts +35 -0
  37. package/dist/onboard/mcp.js +118 -0
  38. package/dist/onboard/raster.d.ts +43 -0
  39. package/dist/onboard/raster.js +253 -0
  40. package/dist/onboard/sound.d.ts +2 -0
  41. package/dist/onboard/sound.js +54 -0
  42. package/dist/onboard/state.d.ts +24 -0
  43. package/dist/onboard/state.js +48 -0
  44. package/dist/onboard/term.d.ts +69 -0
  45. package/dist/onboard/term.js +292 -0
  46. package/dist/onboard/ui.d.ts +97 -0
  47. package/dist/onboard/ui.js +615 -0
  48. package/dist/orgs.d.ts +16 -0
  49. package/dist/orgs.js +89 -0
  50. package/dist/output.d.ts +13 -1
  51. package/dist/output.js +21 -2
  52. package/dist/products.d.ts +6 -2
  53. package/dist/products.js +474 -13
  54. package/dist/relays-commands/certificates.d.ts +3 -0
  55. package/dist/relays-commands/certificates.js +20 -0
  56. package/dist/relays-commands/domains.d.ts +3 -0
  57. package/dist/relays-commands/domains.js +23 -0
  58. package/dist/relays-commands/index.d.ts +4 -0
  59. package/dist/relays-commands/index.js +10 -0
  60. package/dist/relays-commands/monitoring.d.ts +3 -0
  61. package/dist/relays-commands/monitoring.js +41 -0
  62. package/dist/relays-commands/telemetry.d.ts +3 -0
  63. package/dist/relays-commands/telemetry.js +28 -0
  64. package/dist/relays-commands/zones.d.ts +7 -0
  65. package/dist/relays-commands/zones.js +16 -0
  66. package/dist/runner.js +2 -0
  67. package/package.json +2 -2
@@ -0,0 +1,232 @@
1
+ /**
2
+ * What this terminal can do, decided once per setup run. Two passes:
3
+ * 1. detect(): environment only, synchronous, no I/O. Every "no" here is final (NO_COLOR, CI, a pipe, TERM=dumb,
4
+ * --no-animation, GENESIS_REDUCED_MOTION, a screen reader).
5
+ * 2. probe(): asks the terminal itself, in one write, with a single deadline: the background colour (OSC 11),
6
+ * synchronized output (DECRQM 2026), the Kitty graphics protocol, and primary device attributes (DA1, which also
7
+ * reports sixel). DA1 goes last; every terminal answers it, so its reply ends the probe without waiting out the
8
+ * deadline. Nothing is asked unless stdin and stdout are both a TTY.
9
+ * Only `genesis setup` (and a bare `genesis` on a TTY) load this module; normal commands never pay for it.
10
+ */
11
+ import { execFile } from 'node:child_process';
12
+ const truthy = (v) => v !== undefined && v !== '' && v !== '0' && v.toLowerCase() !== 'false';
13
+ function version(v) {
14
+ return (v ?? '').split(/[.\-+]/).map((x) => Number.parseInt(x, 10)).filter((n) => Number.isFinite(n));
15
+ }
16
+ function atLeast(v, min) {
17
+ const have = version(v);
18
+ if (!have.length)
19
+ return false;
20
+ for (let i = 0; i < min.length; i++) {
21
+ const a = have[i] ?? 0;
22
+ if (a !== min[i])
23
+ return a > min[i];
24
+ }
25
+ return true;
26
+ }
27
+ /** Identify the emulator from what it advertises. tmux and screen hide the outer terminal, so they are their own kind. */
28
+ export function terminalKind(env) {
29
+ const p = env.TERM_PROGRAM ?? '';
30
+ if (env.TMUX)
31
+ return 'tmux';
32
+ if (/^screen/.test(env.TERM ?? '') && !env.TERM_PROGRAM)
33
+ return 'screen';
34
+ if (env.KITTY_WINDOW_ID || env.TERM === 'xterm-kitty')
35
+ return 'kitty';
36
+ if (p === 'ghostty' || env.GHOSTTY_RESOURCES_DIR || env.TERM === 'xterm-ghostty')
37
+ return 'ghostty';
38
+ if (p === 'WezTerm' || env.WEZTERM_EXECUTABLE)
39
+ return 'wezterm';
40
+ if (p === 'iTerm.app' || env.LC_TERMINAL === 'iTerm2')
41
+ return 'iterm';
42
+ if (p === 'vscode')
43
+ return 'vscode';
44
+ if (p === 'Apple_Terminal')
45
+ return 'apple';
46
+ if (p === 'Hyper')
47
+ return 'hyper';
48
+ if (p === 'rio')
49
+ return 'rio';
50
+ if (env.WT_SESSION)
51
+ return 'windows-terminal';
52
+ if (env.ConEmuANSI === 'ON')
53
+ return 'conemu';
54
+ if (env.ALACRITTY_WINDOW_ID || env.ALACRITTY_LOG || env.TERM === 'alacritty')
55
+ return 'alacritty';
56
+ if (env.TERM === 'foot' || env.TERM === 'foot-extra')
57
+ return 'foot';
58
+ if (env.KONSOLE_VERSION)
59
+ return 'konsole';
60
+ if (env.VTE_VERSION)
61
+ return 'vte';
62
+ if (env.XTERM_VERSION)
63
+ return 'xterm';
64
+ if (env.MLTERM)
65
+ return 'mlterm';
66
+ return 'unknown';
67
+ }
68
+ export function detect(input) {
69
+ const { env } = input;
70
+ const term = terminalKind(env);
71
+ const dumb = env.TERM === 'dumb';
72
+ const ci = truthy(env.CI) || !!env.GITHUB_ACTIONS || !!env.BUILDKITE || !!env.GITLAB_CI || !!env.TF_BUILD;
73
+ const interactive = input.stdoutTTY && input.stdinTTY && !dumb && !ci;
74
+ // Colour: NO_COLOR wins over everything (no-color.org); FORCE_COLOR can raise a pipe, never past NO_COLOR.
75
+ let color = 0;
76
+ if (env.NO_COLOR === undefined || env.NO_COLOR === '') {
77
+ const force = env.FORCE_COLOR;
78
+ if (force !== undefined && force !== '')
79
+ color = force === '0' || force === 'false' ? 0 : force === '1' ? 4 : force === '2' ? 8 : 24;
80
+ else if (input.stdoutTTY && !dumb) {
81
+ const truecolor = /^(truecolor|24bit)$/i.test(env.COLORTERM ?? '')
82
+ || ['kitty', 'ghostty', 'wezterm', 'iterm', 'vscode', 'windows-terminal', 'alacritty', 'foot', 'konsole', 'rio', 'hyper'].includes(term)
83
+ || (term === 'vte' && Number(env.VTE_VERSION) >= 3600);
84
+ color = truecolor ? 24 : /256/.test(env.TERM ?? '') || term === 'apple' ? 8 : 4;
85
+ }
86
+ }
87
+ const screenReader = !!input.flags?.screenReader || truthy(env.GENESIS_SCREEN_READER) || truthy(env.ACCESSIBILITY);
88
+ const reduced = truthy(env.GENESIS_REDUCED_MOTION) || truthy(env.REDUCED_MOTION);
89
+ let stillBecause;
90
+ if (!input.stdoutTTY)
91
+ stillBecause = 'output is not a terminal';
92
+ else if (dumb)
93
+ stillBecause = 'TERM=dumb';
94
+ else if (ci)
95
+ stillBecause = 'CI';
96
+ else if (input.flags?.animation === false || truthy(env.GENESIS_NO_ANIMATION))
97
+ stillBecause = '--no-animation';
98
+ else if (reduced)
99
+ stillBecause = 'reduced motion';
100
+ else if (screenReader)
101
+ stillBecause = 'screen reader';
102
+ else if (color === 0)
103
+ stillBecause = 'NO_COLOR';
104
+ // The first locale variable that is set wins (POSIX order). Unset everywhere means a modern UTF-8 default; only an
105
+ // explicit non-UTF-8 locale (C, POSIX, ISO-8859-1, …) turns Unicode drawing off.
106
+ const lang = env.LC_ALL || env.LC_CTYPE || env.LANG || '';
107
+ const unicode = (input.platform ?? process.platform) === 'win32' ? term === 'windows-terminal' || term === 'vscode'
108
+ : !lang || /UTF-?8/i.test(lang);
109
+ const hyperlinks = input.stdoutTTY && !dumb && (truthy(env.FORCE_HYPERLINK)
110
+ || ['kitty', 'ghostty', 'wezterm', 'vscode', 'windows-terminal', 'foot', 'konsole', 'rio', 'alacritty'].includes(term)
111
+ || (term === 'iterm' && atLeast(env.TERM_PROGRAM_VERSION, [3, 1]))
112
+ || (term === 'vte' && Number(env.VTE_VERSION) >= 5000));
113
+ // OSC 9 is a notification on iTerm2/WezTerm/Ghostty but progress (9;4) on Windows Terminal/ConEmu; never mix them up.
114
+ const notify = !interactive ? null
115
+ : term === 'kitty' ? 'osc99'
116
+ : term === 'iterm' || term === 'wezterm' ? 'osc9'
117
+ : term === 'ghostty' || term === 'foot' || term === 'vte' || term === 'konsole' ? 'osc777' : null;
118
+ const progress = interactive && (term === 'windows-terminal' || term === 'conemu' || term === 'ghostty'
119
+ || (term === 'iterm' && atLeast(env.TERM_PROGRAM_VERSION, [3, 6])));
120
+ // Graphics are a probe result for Kitty/Ghostty (the reply proves it); iTerm2's protocol is advertised by name.
121
+ // Images are opt-in (GENESIS_SETUP_IMAGES=1) until the hand-off is verified on each emulator; text cells are the default.
122
+ const graphics = !interactive || color < 24 || !truthy(env.GENESIS_SETUP_IMAGES) ? null : term === 'iterm' || term === 'wezterm' ? 'iterm' : null;
123
+ return {
124
+ interactive, color, motion: !stillBecause, stillBecause, screenReader, unicode, hyperlinks, notify, progress, graphics,
125
+ imagesAllowed: truthy(env.GENESIS_SETUP_IMAGES),
126
+ sync: null, background: null, cell: null, dark: !/^(?:.*;)?(?:7|15)$/.test(env.COLORFGBG ?? ''), // COLORFGBG "0;15" means light
127
+ sound: !!input.flags?.sound || truthy(env.GENESIS_SOUND), screencast: !!input.flags?.screencast || truthy(env.GENESIS_SCREENCAST), term,
128
+ columns: input.columns ?? 80, rows: input.rows ?? 24,
129
+ };
130
+ }
131
+ /** Parse every reply the probe can receive. Pure, so the tests can feed it bytes from real terminals. */
132
+ export function parseReplies(data) {
133
+ const out = { da1: false };
134
+ const bg = /\x1b\]11;rgba?:([0-9a-f]{1,4})\/([0-9a-f]{1,4})\/([0-9a-f]{1,4})/i.exec(data);
135
+ if (bg) {
136
+ const ch = (h) => Math.round((Number.parseInt(h, 16) / (16 ** h.length - 1)) * 255);
137
+ out.background = { r: ch(bg[1]), g: ch(bg[2]), b: ch(bg[3]) };
138
+ }
139
+ const mode = /\x1b\[\?2026;(\d)\$y/.exec(data);
140
+ if (mode)
141
+ out.sync = mode[1] === '1' || mode[1] === '2';
142
+ if (/\x1b_Gi=31;OK\x1b\\/.test(data))
143
+ out.kitty = true;
144
+ const cell = /\x1b\[6;(\d+);(\d+)t/.exec(data);
145
+ if (cell && Number(cell[1]) > 0 && Number(cell[2]) > 0)
146
+ out.cell = { height: Number(cell[1]), width: Number(cell[2]) };
147
+ const da = /\x1b\[\?([\d;]+)c/.exec(data);
148
+ if (da) {
149
+ out.da1 = true;
150
+ out.sixel = da[1].split(';').includes('4');
151
+ }
152
+ return out;
153
+ }
154
+ const KITTY_QUERY = '\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\';
155
+ /** The probe. The Kitty query is an APC string, so it goes only to emulators known to swallow APC. */
156
+ export const probeFor = (term) => '\x1b]11;?\x1b\\' + '\x1b[?2026$p'
157
+ + (['kitty', 'ghostty', 'wezterm', 'konsole'].includes(term) ? KITTY_QUERY : '') + '\x1b[16t' + '\x1b[c';
158
+ export const PROBE = probeFor('kitty');
159
+ /** Ask the terminal. Resolves on the DA1 reply or after `timeoutMs`, restoring stdin exactly as it was. */
160
+ export async function probe(caps, io, timeoutMs = 180) {
161
+ if (!caps.interactive || caps.term === 'screen')
162
+ return caps;
163
+ const { stdin, stdout } = io;
164
+ const wasRaw = stdin.isRaw;
165
+ let data = '';
166
+ await new Promise((resolve) => {
167
+ const done = () => { clearTimeout(timer); stdin.off('data', onData); resolve(); };
168
+ const onData = (chunk) => { data += chunk.toString('latin1'); if (parseReplies(data).da1)
169
+ done(); };
170
+ const timer = setTimeout(done, timeoutMs);
171
+ try {
172
+ stdin.setRawMode(true);
173
+ }
174
+ catch {
175
+ done();
176
+ return;
177
+ }
178
+ stdin.on('data', onData);
179
+ stdin.resume();
180
+ stdout.write(probeFor(caps.term));
181
+ });
182
+ try {
183
+ stdin.setRawMode(wasRaw);
184
+ }
185
+ catch { /* already closed */ }
186
+ stdin.pause();
187
+ const r = parseReplies(data);
188
+ const background = r.background ?? null;
189
+ const dark = background ? luminance(background) < 0.4 : caps.dark;
190
+ // Inside tmux, images and graphics replies belong to the outer terminal; keep text only.
191
+ const graphics = caps.term === 'tmux' || caps.color < 24 || !caps.imagesAllowed ? null
192
+ : r.kitty ? 'kitty' : caps.graphics ?? (r.sixel && SIXEL_RENDERS.includes(caps.term) ? 'sixel' : null);
193
+ return { ...caps, background, dark, sync: r.sync ?? null, cell: r.cell ?? null,
194
+ graphics: graphics === 'sixel' && !r.cell ? null : graphics };
195
+ }
196
+ /** DA1 "4" claims sixel, but some emulators (xterm.js without its image addon) claim it and draw nothing. Trust it only here. */
197
+ const SIXEL_RENDERS = ['foot', 'konsole', 'windows-terminal', 'xterm', 'mlterm', 'contour'];
198
+ export function luminance({ r, g, b }) {
199
+ const l = (c) => { const s = c / 255; return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4; };
200
+ return 0.2126 * l(r) + 0.7152 * l(g) + 0.0722 * l(b);
201
+ }
202
+ /**
203
+ * The desktop's own reduce-motion and screen-reader switches (macOS Accessibility, GNOME). Each answer is a short
204
+ * process with a deadline; a missing tool or a timeout just means "not set".
205
+ */
206
+ export async function systemPreferences(platform = process.platform, run = runQuiet) {
207
+ if (platform === 'darwin') {
208
+ const [motion, vo] = await Promise.all([
209
+ run('defaults', ['read', 'com.apple.universalaccess', 'reduceMotion']),
210
+ run('pgrep', ['-xq', 'VoiceOver']).then((r) => (r === null ? null : 'running')),
211
+ ]);
212
+ return { reducedMotion: motion?.trim() === '1', screenReader: vo === 'running' };
213
+ }
214
+ if (platform === 'linux') {
215
+ const [anim, orca] = await Promise.all([
216
+ run('gsettings', ['get', 'org.gnome.desktop.interface', 'enable-animations']),
217
+ run('gsettings', ['get', 'org.gnome.desktop.a11y.applications', 'screen-reader-enabled']),
218
+ ]);
219
+ return { reducedMotion: anim?.trim() === 'false', screenReader: orca?.trim() === 'true' };
220
+ }
221
+ return { reducedMotion: false, screenReader: false };
222
+ }
223
+ function runQuiet(cmd, args, timeoutMs = 250) {
224
+ return new Promise((resolve) => {
225
+ try {
226
+ execFile(cmd, args, { timeout: timeoutMs, windowsHide: true }, (error, stdout) => resolve(error ? null : String(stdout)));
227
+ }
228
+ catch {
229
+ resolve(null);
230
+ }
231
+ });
232
+ }
@@ -0,0 +1,23 @@
1
+ import { Canvas, type Color } from './raster.js';
2
+ import type { ColorDepth } from './caps.js';
3
+ /** Nearest xterm-256 colour: the 6×6×6 cube or the grey ramp, whichever is closer. */
4
+ export declare function ansi256({ r, g, b }: Color): number;
5
+ export declare function ansi16(c: Color): number;
6
+ export declare function fg(c: Color, depth: ColorDepth): string;
7
+ export declare function bg(c: Color, depth: ColorDepth): string;
8
+ export interface CellOptions {
9
+ depth: ColorDepth;
10
+ background: Color | null;
11
+ unicode: boolean;
12
+ }
13
+ /** One string per text row. Runs of identical SGR are merged, so a frame stays small enough to draw at 60 fps. */
14
+ export declare function toCells(cv: Canvas, o: CellOptions): string[];
15
+ /** RGBA PNG of the canvas (straight alpha), for the Kitty and iTerm2 protocols. */
16
+ export declare function png(cv: Canvas): Buffer;
17
+ /** Kitty graphics: transmit and display at the cursor, `cols`×`rows` cells, chunked at 4096 bytes; the cursor does not move. */
18
+ export declare function kittyImage(data: Buffer, cols: number, rows: number, id?: number): string;
19
+ export declare const kittyDelete: (id?: number) => string;
20
+ /** iTerm2 inline image (also WezTerm), sized in cells. */
21
+ export declare function itermImage(data: Buffer, cols: number, rows: number): string;
22
+ /** Sixel with a small palette: the background and up to 63 blends of each ink present. Background pixels are left transparent. */
23
+ export declare function sixel(cv: Canvas, background: Color): string;
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Canvas → terminal. Text cells carry two pixels each (▀ with the top as foreground, the bottom as background), so a
3
+ * row of text is two rows of pixels. Edges are anti-aliased against the terminal's real background when the probe
4
+ * learned it (OSC 11), so a blended edge is the exact colour a browser would paint; when the background is unknown we
5
+ * never paint a cell's background at all and pick ▀, ▄ or █ by coverage instead, so nothing ever shows a box.
6
+ * Images (Kitty, iTerm2, sixel) get the same canvas at device resolution.
7
+ */
8
+ import { deflateSync } from 'node:zlib';
9
+ import { mix } from './raster.js';
10
+ const ESC = '\x1b';
11
+ function cube(v) { return v < 48 ? 0 : v < 115 ? 1 : Math.floor((v - 35) / 40); }
12
+ /** Nearest xterm-256 colour: the 6×6×6 cube or the grey ramp, whichever is closer. */
13
+ export function ansi256({ r, g, b }) {
14
+ const cr = cube(r), cg = cube(g), cb = cube(b);
15
+ const lv = [0, 95, 135, 175, 215, 255];
16
+ const cd = (lv[cr] - r) ** 2 + (lv[cg] - g) ** 2 + (lv[cb] - b) ** 2;
17
+ const grey = Math.max(0, Math.min(23, Math.round(((r + g + b) / 3 - 8) / 10)));
18
+ const gv = 8 + grey * 10, gd = (gv - r) ** 2 + (gv - g) ** 2 + (gv - b) ** 2;
19
+ return gd < cd ? 232 + grey : 16 + 36 * cr + 6 * cg + cb;
20
+ }
21
+ const BASIC = [[30, { r: 0, g: 0, b: 0 }], [31, { r: 205, g: 49, b: 49 }], [32, { r: 13, g: 188, b: 121 }], [33, { r: 229, g: 229, b: 16 }],
22
+ [34, { r: 36, g: 114, b: 200 }], [35, { r: 188, g: 63, b: 188 }], [36, { r: 17, g: 168, b: 205 }], [37, { r: 229, g: 229, b: 229 }],
23
+ [90, { r: 102, g: 102, b: 102 }], [91, { r: 241, g: 76, b: 76 }], [92, { r: 35, g: 209, b: 139 }], [93, { r: 245, g: 245, b: 67 }],
24
+ [94, { r: 59, g: 142, b: 234 }], [95, { r: 214, g: 112, b: 214 }], [96, { r: 41, g: 184, b: 219 }], [97, { r: 255, g: 255, b: 255 }]];
25
+ export function ansi16(c) {
26
+ let best = 37, d = Infinity;
27
+ for (const [code, v] of BASIC) {
28
+ const e = (v.r - c.r) ** 2 + (v.g - c.g) ** 2 + (v.b - c.b) ** 2;
29
+ if (e < d) {
30
+ d = e;
31
+ best = code;
32
+ }
33
+ }
34
+ return best;
35
+ }
36
+ const isInk = (c) => c.r === 1 && c.g === 2 && c.b === 3; // art.DEFAULT_INK: the terminal's own foreground
37
+ export function fg(c, depth) {
38
+ if (isInk(c))
39
+ return `${ESC}[39m`;
40
+ if (depth === 24)
41
+ return `${ESC}[38;2;${c.r};${c.g};${c.b}m`;
42
+ if (depth === 8)
43
+ return `${ESC}[38;5;${ansi256(c)}m`;
44
+ if (depth === 4)
45
+ return `${ESC}[${ansi16(c)}m`;
46
+ return '';
47
+ }
48
+ export function bg(c, depth) {
49
+ if (depth === 24)
50
+ return `${ESC}[48;2;${c.r};${c.g};${c.b}m`;
51
+ if (depth === 8)
52
+ return `${ESC}[48;5;${ansi256(c)}m`;
53
+ if (depth === 4)
54
+ return `${ESC}[${ansi16(c) + 10}m`;
55
+ return '';
56
+ }
57
+ /** One string per text row. Runs of identical SGR are merged, so a frame stays small enough to draw at 60 fps. */
58
+ export function toCells(cv, o) {
59
+ const rows = [];
60
+ const THRESH = 0.5;
61
+ for (let y = 0; y < cv.height; y += 2) {
62
+ let line = '', last = '';
63
+ const put = (sgr, ch) => { if (sgr !== last) {
64
+ line += (last ? `${ESC}[0m` : '') + sgr;
65
+ last = sgr;
66
+ } line += ch; };
67
+ for (let x = 0; x < cv.width; x++) {
68
+ const t = cv.pixel(x, y), b = y + 1 < cv.height ? cv.pixel(x, y + 1) : { c: t.c, a: 0 };
69
+ if (t.a < 0.02 && b.a < 0.02) {
70
+ put('', ' ');
71
+ continue;
72
+ }
73
+ if (!o.unicode || o.depth === 0) {
74
+ // No colour (NO_COLOR) or no Unicode: the shape alone, in the terminal's own ink.
75
+ const on = Math.max(t.a, b.a) >= THRESH;
76
+ put('', on ? (o.unicode ? (t.a >= THRESH && b.a >= THRESH ? '█' : t.a >= THRESH ? '▀' : '▄') : '#') : ' ');
77
+ continue;
78
+ }
79
+ if (o.background) {
80
+ const bgc = o.background;
81
+ const top = mix(bgc, t.c, t.a), bot = mix(bgc, b.c, b.a);
82
+ if (b.a < 0.02)
83
+ put(fg(top, o.depth), '▀');
84
+ else if (t.a < 0.02)
85
+ put(fg(bot, o.depth), '▄');
86
+ else if (t.a > 0.98 && b.a > 0.98 && same(top, bot))
87
+ put(fg(top, o.depth), '█');
88
+ else
89
+ put(fg(top, o.depth) + bg(bot, o.depth), '▀');
90
+ }
91
+ else {
92
+ // Unknown background: never paint it. Partial edges keep their colour at full strength and are drawn only
93
+ // when at least half covered, so the silhouette stays honest.
94
+ const T = t.a >= THRESH, B = b.a >= THRESH;
95
+ if (T && B) {
96
+ if (same(t.c, b.c))
97
+ put(fg(t.c, o.depth), '█');
98
+ else if (isInk(b.c))
99
+ put(fg(b.c, o.depth) + bg(t.c, o.depth), '▄'); // the default ink can only be a foreground
100
+ else
101
+ put(fg(t.c, o.depth) + bg(b.c, o.depth), '▀');
102
+ }
103
+ else if (T)
104
+ put(fg(t.c, o.depth), '▀');
105
+ else if (B)
106
+ put(fg(b.c, o.depth), '▄');
107
+ else
108
+ put('', ' ');
109
+ }
110
+ }
111
+ rows.push((line + (last ? `${ESC}[0m` : '')).replace(/ +$/, ''));
112
+ }
113
+ return rows;
114
+ }
115
+ const same = (a, b) => a.r === b.r && a.g === b.g && a.b === b.b;
116
+ // ── images ─────────────────────────────────────────────────────────────────
117
+ const CRC = (() => { const t = new Uint32Array(256); for (let n = 0; n < 256; n++) {
118
+ let c = n;
119
+ for (let k = 0; k < 8; k++)
120
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
121
+ t[n] = c >>> 0;
122
+ } return t; })();
123
+ function crc32(buf) { let c = 0xffffffff; for (const b of buf)
124
+ c = CRC[(c ^ b) & 255] ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; }
125
+ function chunk(type, data) {
126
+ const len = Buffer.alloc(4);
127
+ len.writeUInt32BE(data.length);
128
+ const td = Buffer.concat([Buffer.from(type, 'ascii'), data]);
129
+ const crc = Buffer.alloc(4);
130
+ crc.writeUInt32BE(crc32(td));
131
+ return Buffer.concat([len, td, crc]);
132
+ }
133
+ /** RGBA PNG of the canvas (straight alpha), for the Kitty and iTerm2 protocols. */
134
+ export function png(cv) {
135
+ const { width: W, height: H } = cv;
136
+ const raw = Buffer.alloc((W * 4 + 1) * H);
137
+ for (let y = 0; y < H; y++) {
138
+ raw[y * (W * 4 + 1)] = 0;
139
+ for (let x = 0; x < W; x++) {
140
+ const { c, a } = cv.pixel(x, y), o = y * (W * 4 + 1) + 1 + x * 4;
141
+ raw[o] = c.r;
142
+ raw[o + 1] = c.g;
143
+ raw[o + 2] = c.b;
144
+ raw[o + 3] = Math.round(a * 255);
145
+ }
146
+ }
147
+ const ihdr = Buffer.alloc(13);
148
+ ihdr.writeUInt32BE(W, 0);
149
+ ihdr.writeUInt32BE(H, 4);
150
+ ihdr[8] = 8;
151
+ ihdr[9] = 6;
152
+ return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk('IHDR', ihdr), chunk('IDAT', deflateSync(raw, { level: 6 })), chunk('IEND', Buffer.alloc(0))]);
153
+ }
154
+ /** Kitty graphics: transmit and display at the cursor, `cols`×`rows` cells, chunked at 4096 bytes; the cursor does not move. */
155
+ export function kittyImage(data, cols, rows, id = 4242) {
156
+ const b64 = data.toString('base64');
157
+ let out = '';
158
+ for (let i = 0; i < b64.length; i += 4096) {
159
+ const part = b64.slice(i, i + 4096), more = i + 4096 < b64.length ? 1 : 0;
160
+ out += i === 0 ? `${ESC}_Ga=T,f=100,q=2,i=${id},c=${cols},r=${rows},C=1,m=${more};${part}${ESC}\\` : `${ESC}_Gm=${more},q=2;${part}${ESC}\\`;
161
+ }
162
+ return out;
163
+ }
164
+ export const kittyDelete = (id = 4242) => `${ESC}_Ga=d,d=I,i=${id},q=2${ESC}\\`;
165
+ /** iTerm2 inline image (also WezTerm), sized in cells. */
166
+ export function itermImage(data, cols, rows) {
167
+ return `${ESC}]1337;File=inline=1;size=${data.length};width=${cols};height=${rows};preserveAspectRatio=1;doNotMoveCursor=1:${data.toString('base64')}\x07`;
168
+ }
169
+ /** Sixel with a small palette: the background and up to 63 blends of each ink present. Background pixels are left transparent. */
170
+ export function sixel(cv, background) {
171
+ const { width: W, height: H } = cv;
172
+ const palette = [];
173
+ const index = new Map();
174
+ const idx = new Int16Array(W * H).fill(-1);
175
+ for (let y = 0; y < H; y++)
176
+ for (let x = 0; x < W; x++) {
177
+ const { c, a } = cv.pixel(x, y);
178
+ if (a < 0.03)
179
+ continue;
180
+ const q = (v) => Math.round(v / 16) * 16;
181
+ const m = mix(background, c, Math.round(a * 8) / 8), key = `${q(m.r)},${q(m.g)},${q(m.b)}`;
182
+ let n = index.get(key);
183
+ if (n === undefined) {
184
+ if (palette.length >= 255)
185
+ n = nearest(palette, m);
186
+ else {
187
+ n = palette.length;
188
+ palette.push(m);
189
+ index.set(key, n);
190
+ }
191
+ }
192
+ idx[y * W + x] = n;
193
+ }
194
+ let out = `${ESC}P0;1;0q"1;1;${W};${H}`;
195
+ palette.forEach((c, n) => { out += `#${n};2;${Math.round(c.r / 2.55)};${Math.round(c.g / 2.55)};${Math.round(c.b / 2.55)}`; });
196
+ for (let band = 0; band < H; band += 6) {
197
+ const used = new Set();
198
+ for (let y = band; y < Math.min(H, band + 6); y++)
199
+ for (let x = 0; x < W; x++)
200
+ if (idx[y * W + x] >= 0)
201
+ used.add(idx[y * W + x]);
202
+ let first = true;
203
+ for (const n of used) {
204
+ out += (first ? '' : '$') + `#${n}`;
205
+ first = false;
206
+ let run = '', prev = '', count = 0;
207
+ const flush = () => { if (!count)
208
+ return; run += count > 3 ? `!${count}${prev}` : prev.repeat(count); };
209
+ for (let x = 0; x < W; x++) {
210
+ let bits = 0;
211
+ for (let k = 0; k < 6; k++) {
212
+ const y = band + k;
213
+ if (y < H && idx[y * W + x] === n)
214
+ bits |= 1 << k;
215
+ }
216
+ const ch = String.fromCharCode(63 + bits);
217
+ if (ch === prev)
218
+ count++;
219
+ else {
220
+ flush();
221
+ prev = ch;
222
+ count = 1;
223
+ }
224
+ }
225
+ flush();
226
+ out += run;
227
+ }
228
+ out += '-';
229
+ }
230
+ return out + `${ESC}\\`;
231
+ }
232
+ function nearest(p, c) { let best = 0, d = Infinity; p.forEach((v, n) => { const e = (v.r - c.r) ** 2 + (v.g - c.g) ** 2 + (v.b - c.b) ** 2; if (e < d) {
233
+ d = e;
234
+ best = n;
235
+ } }); return best; }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * One's eye, from agent-eye.js (the One site's vendor/brand/agent-eye, engine sha256 83cc379a…): its geometry, pose
3
+ * math, easing curves and moments, ported line for line so the terminal eye blinks, glances and smiles on exactly the
4
+ * site's timing. Only the drawing is ours (raster.ts); the numbers are agent-eye's. The bar's painted-on-a-ball bend is
5
+ * left out: at terminal sizes it is under a tenth of a pixel.
6
+ */
7
+ export declare const GEOMETRY: {
8
+ readonly radius: 100;
9
+ readonly barWidth: number;
10
+ readonly barHeight: number;
11
+ readonly cornerRadius: number;
12
+ };
13
+ export declare const MOTION: {
14
+ readonly maxTurn: 55;
15
+ readonly turnRadius: 70;
16
+ readonly foreshorten: 0.8;
17
+ readonly maxLift: 14;
18
+ readonly closedHeight: 7;
19
+ readonly lidSquash: 0.3;
20
+ readonly safeRadius: 89;
21
+ readonly smileLift: 2.5;
22
+ readonly smileSquint: 35;
23
+ readonly squeeze: 1;
24
+ };
25
+ export declare const MOUTH: {
26
+ readonly y: 66;
27
+ readonly halfWidth: 44;
28
+ readonly birthHalfWidth: 20;
29
+ readonly thickness: number;
30
+ readonly born: 3;
31
+ readonly follow: 0.6;
32
+ };
33
+ export interface Pose {
34
+ look: number;
35
+ lift: number;
36
+ open: number;
37
+ smile: number;
38
+ mouth: number;
39
+ }
40
+ export declare const REST: Pose;
41
+ /** The bar for a pose, in the -100…100 head box: centre (x, y), width w, height h. */
42
+ export declare function poseToBar(p: Partial<Pose>): {
43
+ x: number;
44
+ y: number;
45
+ w: number;
46
+ h: number;
47
+ };
48
+ export declare function poseToMouth(p: Partial<Pose>): {
49
+ x: number;
50
+ y: number;
51
+ w: number;
52
+ h: number;
53
+ o: number;
54
+ } | null;
55
+ export declare const EASE: {
56
+ readonly linear: readonly [0, 0, 1, 1];
57
+ readonly shut: readonly [0.6, 0, 0.9, 0.55];
58
+ readonly snap: readonly [0.1, 0.9, 0.25, 1];
59
+ readonly settle: readonly [0.3, 0, 0.3, 1];
60
+ readonly glide: readonly [0.45, 0, 0.25, 1];
61
+ readonly emerge: readonly [0.2, 0.6, 0.35, 1];
62
+ readonly charge: readonly [0.4, 0, 0.2, 1];
63
+ readonly exit: readonly [0.45, 0, 0.7, 0.9];
64
+ readonly enter: readonly [0.16, 1, 0.3, 1];
65
+ readonly leave: readonly [0.7, 0, 0.84, 0];
66
+ };
67
+ export type EaseName = keyof typeof EASE;
68
+ export declare function cubicBezier(x1: number, y1: number, x2: number, y2: number): (x: number) => number;
69
+ export declare const ease: (name: EaseName, x: number) => number;
70
+ /** A curve is [[seconds, value, ease into this key], …]. */
71
+ export type Curve = readonly (readonly [number, number, EaseName?])[];
72
+ export declare function sample(curve: Curve, t: number): number;
73
+ export declare const curveEnd: (c: Curve) => number;
74
+ /** agent-eye BLINK: 80 ms close, 50 ms shut, 170 ms open with a little overshoot. Multiplies openness. */
75
+ export declare const BLINK: Curve;
76
+ export declare const DOUBLE_BLINK: Curve;
77
+ /** agent-eye SMILE, 2.9 s. */
78
+ export declare const SMILE: {
79
+ smile: Curve;
80
+ mouth: Curve;
81
+ };
82
+ /** agent-eye RAINBOW_MOMENT's light, worn here in One's amber: it charges up the bar, holds, and leaves through the top. */
83
+ export declare const LIGHT: Curve;
84
+ /**
85
+ * The idle eye: agent-eye's alive mode in miniature. Blinks every 2.5–6 s (18 % of them double), glances now and then.
86
+ * Deterministic for a seed, so a recorded demo is the same every time.
87
+ */
88
+ export declare class IdleEye {
89
+ private nextBlink;
90
+ private blinkAt;
91
+ private curve;
92
+ private lookFrom;
93
+ private lookTo;
94
+ private lookAt;
95
+ private nextGlance;
96
+ private rnd;
97
+ constructor(seed?: number);
98
+ /** Look toward x (-1…1) now, e.g. at the text it is waiting beside, or straight at you when the window regains focus. */
99
+ look(t: number, x: number): void;
100
+ blink(t: number, double?: boolean): void;
101
+ private lookNow;
102
+ pose(t: number): Pose;
103
+ }