claude-face 0.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.
Files changed (44) hide show
  1. package/README.md +72 -0
  2. package/dist/bin/cli.d.ts +2 -0
  3. package/dist/bin/cli.js +128 -0
  4. package/dist/bin/cli.js.map +1 -0
  5. package/dist/src/config.d.ts +24 -0
  6. package/dist/src/config.js +32 -0
  7. package/dist/src/config.js.map +1 -0
  8. package/dist/src/face.d.ts +60 -0
  9. package/dist/src/face.js +472 -0
  10. package/dist/src/face.js.map +1 -0
  11. package/dist/src/frames.d.ts +1 -0
  12. package/dist/src/frames.js +3 -0
  13. package/dist/src/frames.js.map +1 -0
  14. package/dist/src/ipc.d.ts +27 -0
  15. package/dist/src/ipc.js +136 -0
  16. package/dist/src/ipc.js.map +1 -0
  17. package/dist/src/loader.d.ts +2 -0
  18. package/dist/src/loader.js +87 -0
  19. package/dist/src/loader.js.map +1 -0
  20. package/dist/src/main.d.ts +2 -0
  21. package/dist/src/main.js +201 -0
  22. package/dist/src/main.js.map +1 -0
  23. package/dist/src/patterns.d.ts +6 -0
  24. package/dist/src/patterns.js +14 -0
  25. package/dist/src/patterns.js.map +1 -0
  26. package/dist/src/pty.d.ts +17 -0
  27. package/dist/src/pty.js +84 -0
  28. package/dist/src/pty.js.map +1 -0
  29. package/dist/src/scroll.d.ts +36 -0
  30. package/dist/src/scroll.js +56 -0
  31. package/dist/src/scroll.js.map +1 -0
  32. package/dist/src/state.d.ts +35 -0
  33. package/dist/src/state.js +158 -0
  34. package/dist/src/state.js.map +1 -0
  35. package/dist/src/theme.d.ts +15 -0
  36. package/dist/src/theme.js +51 -0
  37. package/dist/src/theme.js.map +1 -0
  38. package/dist/src/types.d.ts +58 -0
  39. package/dist/src/types.js +3 -0
  40. package/dist/src/types.js.map +1 -0
  41. package/dist/src/utils.d.ts +14 -0
  42. package/dist/src/utils.js +26 -0
  43. package/dist/src/utils.js.map +1 -0
  44. package/package.json +33 -0
@@ -0,0 +1,56 @@
1
+ /**
2
+ * ANSI scroll-region management.
3
+ *
4
+ * Uses DECSTBM (`ESC [ top ; bottom r`) to split the terminal into a
5
+ * fixed face region at the top and a scrollable content region below.
6
+ */
7
+ /**
8
+ * Return current terminal dimensions split by face height.
9
+ */
10
+ export function getScrollDimensions(faceHeight) {
11
+ const totalRows = process.stdout.rows ?? 24;
12
+ const totalCols = process.stdout.columns ?? 80;
13
+ const faceRows = faceHeight;
14
+ const contentRows = totalRows - faceHeight;
15
+ return { faceRows, contentRows, totalRows, totalCols };
16
+ }
17
+ /**
18
+ * Set the terminal scroll region so that the top `faceHeight` rows are
19
+ * fixed and only the rows below them scroll.
20
+ *
21
+ * Returns a cleanup function that resets the scroll region.
22
+ */
23
+ export function setupScrollRegion(faceHeight) {
24
+ const { totalRows } = getScrollDimensions(faceHeight);
25
+ // DECSTBM: set scroll region from (faceHeight+1) to totalRows (1-indexed)
26
+ process.stdout.write(`\x1b[${faceHeight + 1};${totalRows}r`);
27
+ // Move cursor to the first row of the content area
28
+ process.stdout.write(`\x1b[${faceHeight + 1};1H`);
29
+ return teardownScrollRegion;
30
+ }
31
+ /**
32
+ * Reset scroll region to the full terminal and show cursor.
33
+ */
34
+ export function teardownScrollRegion() {
35
+ // Reset scroll region
36
+ process.stdout.write('\x1b[r');
37
+ // Show cursor
38
+ process.stdout.write('\x1b[?25h');
39
+ }
40
+ /**
41
+ * Save cursor position and move to row 0, col 0 (top-left) for face
42
+ * rendering. Uses 1-indexed ANSI coordinates.
43
+ */
44
+ export function moveCursorToFace() {
45
+ // Save cursor position
46
+ process.stdout.write('\x1b7');
47
+ // Move to row 1, col 1 (top-left, 1-indexed)
48
+ process.stdout.write('\x1b[1;1H');
49
+ }
50
+ /**
51
+ * Restore the previously saved cursor position.
52
+ */
53
+ export function restoreCursor() {
54
+ process.stdout.write('\x1b8');
55
+ }
56
+ //# sourceMappingURL=scroll.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scroll.js","sourceRoot":"","sources":["../../src/scroll.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,UAAkB;IACpD,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;IAC5C,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC;IAC/C,MAAM,QAAQ,GAAG,UAAU,CAAC;IAC5B,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,CAAC;IAE3C,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;AACzD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAAkB;IAClD,MAAM,EAAE,SAAS,EAAE,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;IAEtD,0EAA0E;IAC1E,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,UAAU,GAAG,CAAC,IAAI,SAAS,GAAG,CAAC,CAAC;IAE7D,mDAAmD;IACnD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,UAAU,GAAG,CAAC,KAAK,CAAC,CAAC;IAElD,OAAO,oBAAoB,CAAC;AAC9B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB;IAClC,sBAAsB;IACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC/B,cAAc;IACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;AACpC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB;IAC9B,uBAAuB;IACvB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC9B,6CAA6C;IAC7C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;AACpC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AAChC,CAAC"}
@@ -0,0 +1,35 @@
1
+ import { FaceState, StateEvent } from './types.js';
2
+ export declare class StateMachine {
3
+ private state;
4
+ private idleTimer;
5
+ private debounceTimer;
6
+ private feedTimer;
7
+ private listeningTimer;
8
+ private onStateChange;
9
+ private pendingData;
10
+ private hasPendingData;
11
+ private lastThinkingMatchTime;
12
+ constructor(onStateChange: (event: StateEvent) => void);
13
+ getState(): FaceState;
14
+ /**
15
+ * Called when the human presses a key. Transitions to 'listening'
16
+ * and resets the listening timeout. When the user stops typing for
17
+ * LISTENING_TIMEOUT_MS, transitions back to idle.
18
+ */
19
+ notifyHumanInput(): void;
20
+ /**
21
+ * Feed raw PTY data. Instead of processing every chunk synchronously
22
+ * (hundreds of regex ops + timer create/destroy per second during heavy
23
+ * output), we accumulate data and process it at a fixed sample rate.
24
+ * This adds up to 50ms of detection latency, which is imperceptible
25
+ * given the 100ms debounce on output.
26
+ */
27
+ feed(data: string): void;
28
+ destroy(): void;
29
+ /**
30
+ * Process accumulated data: strip ANSI once for the whole batch,
31
+ * run pattern matching once, check for typing once.
32
+ */
33
+ private processPendingData;
34
+ private transition;
35
+ }
@@ -0,0 +1,158 @@
1
+ import { stripAnsi } from './utils.js';
2
+ import { realtimePatterns } from './patterns.js';
3
+ import { config } from './config.js';
4
+ // ── Timeout constants ────────────────────────────────────────────────
5
+ const { idleTimeoutMs: IDLE_TIMEOUT_MS, debounceMs: DEBOUNCE_MS, feedSampleIntervalMs: FEED_SAMPLE_INTERVAL_MS, minPrintableLen: MIN_PRINTABLE_LEN, thinkingCooldownMs: THINKING_COOLDOWN_MS, listeningTimeoutMs: LISTENING_TIMEOUT_MS, } = config;
6
+ // ── Helper: match against a pattern list (first match wins) ─────────
7
+ function matchPatterns(text, entries) {
8
+ for (const entry of entries) {
9
+ for (const re of entry.patterns) {
10
+ if (re.test(text)) {
11
+ return entry;
12
+ }
13
+ }
14
+ }
15
+ return null;
16
+ }
17
+ // ── State Machine ────────────────────────────────────────────────────
18
+ export class StateMachine {
19
+ state = 'idle';
20
+ idleTimer = null;
21
+ debounceTimer = null;
22
+ feedTimer = null;
23
+ listeningTimer = null;
24
+ onStateChange;
25
+ // Accumulated PTY data between processing ticks
26
+ pendingData = '';
27
+ hasPendingData = false;
28
+ // Timestamp of last thinking pattern match — used to suppress
29
+ // typing transitions during the thinking cooldown window
30
+ lastThinkingMatchTime = 0;
31
+ constructor(onStateChange) {
32
+ this.onStateChange = onStateChange;
33
+ }
34
+ getState() {
35
+ return this.state;
36
+ }
37
+ /**
38
+ * Called when the human presses a key. Transitions to 'listening'
39
+ * and resets the listening timeout. When the user stops typing for
40
+ * LISTENING_TIMEOUT_MS, transitions back to idle.
41
+ */
42
+ notifyHumanInput() {
43
+ // Don't override thinking/typing — Claude is actively doing something
44
+ if (this.state === 'thinking' || this.state === 'typing')
45
+ return;
46
+ this.transition('listening');
47
+ // Reset listening timeout
48
+ if (this.listeningTimer !== null)
49
+ clearTimeout(this.listeningTimer);
50
+ this.listeningTimer = setTimeout(() => {
51
+ this.listeningTimer = null;
52
+ if (this.state === 'listening') {
53
+ this.transition('idle');
54
+ }
55
+ }, LISTENING_TIMEOUT_MS);
56
+ }
57
+ /**
58
+ * Feed raw PTY data. Instead of processing every chunk synchronously
59
+ * (hundreds of regex ops + timer create/destroy per second during heavy
60
+ * output), we accumulate data and process it at a fixed sample rate.
61
+ * This adds up to 50ms of detection latency, which is imperceptible
62
+ * given the 100ms debounce on output.
63
+ */
64
+ feed(data) {
65
+ if (data.length === 0)
66
+ return;
67
+ this.pendingData += data;
68
+ this.hasPendingData = true;
69
+ // Reset idle timer on every chunk (this is cheap — just a clear + set)
70
+ if (this.idleTimer !== null)
71
+ clearTimeout(this.idleTimer);
72
+ this.idleTimer = setTimeout(() => {
73
+ this.idleTimer = null;
74
+ this.transition('idle');
75
+ }, IDLE_TIMEOUT_MS);
76
+ // Schedule processing if not already pending
77
+ if (this.feedTimer === null) {
78
+ this.feedTimer = setTimeout(() => this.processPendingData(), FEED_SAMPLE_INTERVAL_MS);
79
+ }
80
+ }
81
+ // ── Cleanup ──────────────────────────────────────────────────────
82
+ destroy() {
83
+ if (this.idleTimer !== null) {
84
+ clearTimeout(this.idleTimer);
85
+ this.idleTimer = null;
86
+ }
87
+ if (this.debounceTimer !== null) {
88
+ clearTimeout(this.debounceTimer);
89
+ this.debounceTimer = null;
90
+ }
91
+ if (this.feedTimer !== null) {
92
+ clearTimeout(this.feedTimer);
93
+ this.feedTimer = null;
94
+ }
95
+ if (this.listeningTimer !== null) {
96
+ clearTimeout(this.listeningTimer);
97
+ this.listeningTimer = null;
98
+ }
99
+ this.pendingData = '';
100
+ this.hasPendingData = false;
101
+ }
102
+ // ── Private helpers ──────────────────────────────────────────────
103
+ /**
104
+ * Process accumulated data: strip ANSI once for the whole batch,
105
+ * run pattern matching once, check for typing once.
106
+ */
107
+ processPendingData() {
108
+ this.feedTimer = null;
109
+ if (!this.hasPendingData)
110
+ return;
111
+ const raw = this.pendingData;
112
+ this.pendingData = '';
113
+ this.hasPendingData = false;
114
+ const clean = stripAnsi(raw);
115
+ // Realtime pattern matching (priority order)
116
+ const match = matchPatterns(clean, realtimePatterns);
117
+ if (match) {
118
+ if (match.state === 'thinking') {
119
+ this.lastThinkingMatchTime = Date.now();
120
+ }
121
+ // Cancel listening state — Claude is responding
122
+ if (this.listeningTimer !== null) {
123
+ clearTimeout(this.listeningTimer);
124
+ this.listeningTimer = null;
125
+ }
126
+ this.transition(match.state);
127
+ return;
128
+ }
129
+ // "typing" — non-trivial printable text from Claude (not human echo).
130
+ // If we're in listening state, PTY echo from human keystrokes will appear
131
+ // but it's short (1-2 chars per keystroke). MIN_PRINTABLE_LEN filters that.
132
+ // Suppress if we recently saw a thinking pattern.
133
+ if (this.state !== 'listening') {
134
+ const inThinkingCooldown = Date.now() - this.lastThinkingMatchTime < THINKING_COOLDOWN_MS;
135
+ if (inThinkingCooldown)
136
+ return;
137
+ const printable = clean.replace(/[\x00-\x1f\x7f]/g, '').trim();
138
+ if (printable.length >= MIN_PRINTABLE_LEN) {
139
+ this.transition('typing');
140
+ }
141
+ }
142
+ }
143
+ transition(next) {
144
+ if (next === this.state)
145
+ return;
146
+ const prev = this.state;
147
+ this.state = next;
148
+ // Debounce the callback — only fire after state is stable for DEBOUNCE_MS.
149
+ // Internal state updates immediately so getState() is always current.
150
+ if (this.debounceTimer !== null)
151
+ clearTimeout(this.debounceTimer);
152
+ this.debounceTimer = setTimeout(() => {
153
+ this.debounceTimer = null;
154
+ this.onStateChange({ prev, next: this.state, timestamp: Date.now() });
155
+ }, DEBOUNCE_MS);
156
+ }
157
+ }
158
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../src/state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { ColorRole, ThemeMode, ThemeColors } from './types.js';
2
+ /**
3
+ * Detect the terminal background color from the COLORFGBG environment variable.
4
+ * Falls back to 'dark' if not set or unrecognizable.
5
+ */
6
+ export declare function detectTheme(): 'dark' | 'light';
7
+ /**
8
+ * Return the ThemeColors for the given mode.
9
+ * Resolves 'auto' via detectTheme().
10
+ */
11
+ export declare function getTheme(mode: ThemeMode): ThemeColors;
12
+ /**
13
+ * Return the ANSI escape code for the given color role.
14
+ */
15
+ export declare function colorForRole(theme: ThemeColors, role: ColorRole): string;
@@ -0,0 +1,51 @@
1
+ // ── ANSI color codes ────────────────────────────────────────────────
2
+ const DARK_THEME = {
3
+ outline: '\x1b[36m', // cyan
4
+ eye: '\x1b[97m', // bright white
5
+ mouth: '\x1b[33m', // yellow
6
+ highlight: '\x1b[95m', // bright magenta
7
+ cheek: '\x1b[91m', // bright red
8
+ bg: '', // default
9
+ reset: '\x1b[0m',
10
+ };
11
+ const LIGHT_THEME = {
12
+ outline: '\x1b[34m', // blue
13
+ eye: '\x1b[30m', // black
14
+ mouth: '\x1b[33m', // yellow
15
+ highlight: '\x1b[35m', // magenta
16
+ cheek: '\x1b[31m', // red
17
+ bg: '', // default
18
+ reset: '\x1b[0m',
19
+ };
20
+ /**
21
+ * Detect the terminal background color from the COLORFGBG environment variable.
22
+ * Falls back to 'dark' if not set or unrecognizable.
23
+ */
24
+ export function detectTheme() {
25
+ const colorfgbg = process.env.COLORFGBG;
26
+ if (!colorfgbg) {
27
+ return 'dark';
28
+ }
29
+ // COLORFGBG is typically "fg;bg" where bg >= 8 means light background
30
+ const parts = colorfgbg.split(';');
31
+ const bg = parseInt(parts[parts.length - 1], 10);
32
+ if (isNaN(bg)) {
33
+ return 'dark';
34
+ }
35
+ return bg >= 8 ? 'light' : 'dark';
36
+ }
37
+ /**
38
+ * Return the ThemeColors for the given mode.
39
+ * Resolves 'auto' via detectTheme().
40
+ */
41
+ export function getTheme(mode) {
42
+ const resolved = mode === 'auto' ? detectTheme() : mode;
43
+ return resolved === 'light' ? LIGHT_THEME : DARK_THEME;
44
+ }
45
+ /**
46
+ * Return the ANSI escape code for the given color role.
47
+ */
48
+ export function colorForRole(theme, role) {
49
+ return theme[role];
50
+ }
51
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.js","sourceRoot":"","sources":["../../src/theme.ts"],"names":[],"mappings":"AAEA,uEAAuE;AAEvE,MAAM,UAAU,GAAgB;IAC9B,OAAO,EAAE,UAAU,EAAK,OAAO;IAC/B,GAAG,EAAE,UAAU,EAAS,eAAe;IACvC,KAAK,EAAE,UAAU,EAAO,SAAS;IACjC,SAAS,EAAE,UAAU,EAAG,iBAAiB;IACzC,KAAK,EAAE,UAAU,EAAO,aAAa;IACrC,EAAE,EAAE,EAAE,EAAkB,UAAU;IAClC,KAAK,EAAE,SAAS;CACjB,CAAC;AAEF,MAAM,WAAW,GAAgB;IAC/B,OAAO,EAAE,UAAU,EAAK,OAAO;IAC/B,GAAG,EAAE,UAAU,EAAS,QAAQ;IAChC,KAAK,EAAE,UAAU,EAAO,SAAS;IACjC,SAAS,EAAE,UAAU,EAAG,UAAU;IAClC,KAAK,EAAE,UAAU,EAAO,MAAM;IAC9B,EAAE,EAAE,EAAE,EAAkB,UAAU;IAClC,KAAK,EAAE,SAAS;CACjB,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,WAAW;IACzB,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;IACxC,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,sEAAsE;IACtE,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACnC,MAAM,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACjD,IAAI,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;QACd,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;AACpC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAe;IACtC,MAAM,QAAQ,GAAG,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACxD,OAAO,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC;AACzD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,KAAkB,EAAE,IAAe;IAC9D,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC"}
@@ -0,0 +1,58 @@
1
+ export type ColorRole = 'outline' | 'eye' | 'mouth' | 'highlight' | 'cheek' | 'bg';
2
+ export interface ColorSpan {
3
+ row: number;
4
+ col: number;
5
+ len: number;
6
+ role: ColorRole;
7
+ }
8
+ export interface Frame {
9
+ art: string[];
10
+ colors?: ColorSpan[];
11
+ }
12
+ export interface AnimationConfig {
13
+ loopDuration: number;
14
+ reverse: boolean;
15
+ gap: number;
16
+ gapMin?: number;
17
+ gapMax?: number;
18
+ }
19
+ export interface Expression {
20
+ name: string;
21
+ frames: Frame[];
22
+ config: AnimationConfig;
23
+ }
24
+ export interface FaceDefinition {
25
+ name: string;
26
+ width: number;
27
+ height: number;
28
+ expressions: Record<string, Expression>;
29
+ }
30
+ export type FaceState = 'idle' | 'typing' | 'thinking' | 'listening';
31
+ export interface StateEvent {
32
+ prev: FaceState;
33
+ next: FaceState;
34
+ timestamp: number;
35
+ }
36
+ export type ThemeMode = 'dark' | 'light' | 'auto';
37
+ export interface ThemeColors {
38
+ outline: string;
39
+ eye: string;
40
+ mouth: string;
41
+ highlight: string;
42
+ cheek: string;
43
+ bg: string;
44
+ reset: string;
45
+ }
46
+ export interface IpcStateMessage {
47
+ state: FaceState;
48
+ ts: number;
49
+ }
50
+ export interface CliOptions {
51
+ facePath?: string;
52
+ theme: ThemeMode;
53
+ noFace: boolean;
54
+ faceOnly: boolean;
55
+ debug: boolean;
56
+ help: boolean;
57
+ version: boolean;
58
+ }
@@ -0,0 +1,3 @@
1
+ // ── Color & Frame Types ──────────────────────────────────────────────
2
+ export {};
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,wEAAwE"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Strip all ANSI escape sequences from a string.
3
+ *
4
+ * Handles:
5
+ * - CSI sequences `ESC [ ... letter`
6
+ * - OSC sequences `ESC ] ... BEL`
7
+ */
8
+ export declare function stripAnsi(str: string): string;
9
+ /**
10
+ * Center a string within a given width using space padding.
11
+ *
12
+ * If the text is longer than `width` it is returned unchanged.
13
+ */
14
+ export declare function centerText(text: string, width: number): string;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Strip all ANSI escape sequences from a string.
3
+ *
4
+ * Handles:
5
+ * - CSI sequences `ESC [ ... letter`
6
+ * - OSC sequences `ESC ] ... BEL`
7
+ */
8
+ export function stripAnsi(str) {
9
+ return str
10
+ .replace(/\x1b\[[0-9;]*[A-Za-z]/g, '')
11
+ .replace(/\x1b\][^\x07]*\x07/g, '');
12
+ }
13
+ /**
14
+ * Center a string within a given width using space padding.
15
+ *
16
+ * If the text is longer than `width` it is returned unchanged.
17
+ */
18
+ export function centerText(text, width) {
19
+ if (text.length >= width)
20
+ return text;
21
+ const totalPad = width - text.length;
22
+ const leftPad = Math.floor(totalPad / 2);
23
+ const rightPad = totalPad - leftPad;
24
+ return ' '.repeat(leftPad) + text + ' '.repeat(rightPad);
25
+ }
26
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,OAAO,GAAG;SACP,OAAO,CAAC,wBAAwB,EAAE,EAAE,CAAC;SACrC,OAAO,CAAC,qBAAqB,EAAE,EAAE,CAAC,CAAC;AACxC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,KAAa;IACpD,IAAI,IAAI,CAAC,MAAM,IAAI,KAAK;QAAE,OAAO,IAAI,CAAC;IAEtC,MAAM,QAAQ,GAAG,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC;IACrC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC;IACzC,MAAM,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;IAEpC,OAAO,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC3D,CAAC"}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "claude-face",
3
+ "version": "0.1.0",
4
+ "description": "Animated ASCII face for Claude Code — reacts to AI state in real-time",
5
+ "type": "module",
6
+ "bin": {
7
+ "claude-face": "./dist/bin/cli.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "postinstall": "chmod +x node_modules/node-pty/prebuilds/darwin-*/spawn-helper 2>/dev/null || true",
14
+ "prepublishOnly": "npm run build",
15
+ "build": "tsc",
16
+ "dev": "tsx bin/cli.ts",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest"
19
+ },
20
+ "dependencies": {
21
+ "node-pty": "^1.0.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.0.0",
25
+ "tsx": "^4.19.0",
26
+ "typescript": "^5.7.0",
27
+ "vitest": "^3.0.0"
28
+ },
29
+ "license": "MIT",
30
+ "engines": {
31
+ "node": ">=18"
32
+ }
33
+ }