cc-face 0.1.10
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 +102 -0
- package/dist/bin/cli.d.ts +2 -0
- package/dist/bin/cli.js +142 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/config.d.ts +24 -0
- package/dist/src/config.js +32 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/face.d.ts +59 -0
- package/dist/src/face.js +493 -0
- package/dist/src/face.js.map +1 -0
- package/dist/src/frames.d.ts +1 -0
- package/dist/src/frames.js +6 -0
- package/dist/src/frames.js.map +1 -0
- package/dist/src/generator.d.ts +16 -0
- package/dist/src/generator.js +122 -0
- package/dist/src/generator.js.map +1 -0
- package/dist/src/ipc.d.ts +34 -0
- package/dist/src/ipc.js +203 -0
- package/dist/src/ipc.js.map +1 -0
- package/dist/src/loader.d.ts +12 -0
- package/dist/src/loader.js +63 -0
- package/dist/src/loader.js.map +1 -0
- package/dist/src/main.d.ts +2 -0
- package/dist/src/main.js +210 -0
- package/dist/src/main.js.map +1 -0
- package/dist/src/patterns.d.ts +6 -0
- package/dist/src/patterns.js +14 -0
- package/dist/src/patterns.js.map +1 -0
- package/dist/src/pty.d.ts +17 -0
- package/dist/src/pty.js +84 -0
- package/dist/src/pty.js.map +1 -0
- package/dist/src/scroll.d.ts +36 -0
- package/dist/src/scroll.js +56 -0
- package/dist/src/scroll.js.map +1 -0
- package/dist/src/state.d.ts +36 -0
- package/dist/src/state.js +164 -0
- package/dist/src/state.js.map +1 -0
- package/dist/src/theme.d.ts +15 -0
- package/dist/src/theme.js +51 -0
- package/dist/src/theme.js.map +1 -0
- package/dist/src/types.d.ts +60 -0
- package/dist/src/types.js +3 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils.d.ts +8 -0
- package/dist/src/utils.js +13 -0
- package/dist/src/utils.js.map +1 -0
- package/faces/idle/config.json +7 -0
- package/faces/idle/img/1.jpeg +0 -0
- package/faces/idle/img/10.jpeg +0 -0
- package/faces/idle/img/11.jpeg +0 -0
- package/faces/idle/img/12.jpeg +0 -0
- package/faces/idle/img/13.jpeg +0 -0
- package/faces/idle/img/14.jpeg +0 -0
- package/faces/idle/img/15.jpeg +0 -0
- package/faces/idle/img/16.jpeg +0 -0
- package/faces/idle/img/17.jpeg +0 -0
- package/faces/idle/img/18.jpeg +0 -0
- package/faces/idle/img/19.jpeg +0 -0
- package/faces/idle/img/2.jpeg +0 -0
- package/faces/idle/img/20.jpeg +0 -0
- package/faces/idle/img/21.jpeg +0 -0
- package/faces/idle/img/22.jpeg +0 -0
- package/faces/idle/img/23.jpeg +0 -0
- package/faces/idle/img/24.jpeg +0 -0
- package/faces/idle/img/25.jpeg +0 -0
- package/faces/idle/img/26.jpeg +0 -0
- package/faces/idle/img/27.jpeg +0 -0
- package/faces/idle/img/28.jpeg +0 -0
- package/faces/idle/img/29.jpeg +0 -0
- package/faces/idle/img/3.jpeg +0 -0
- package/faces/idle/img/30.jpeg +0 -0
- package/faces/idle/img/4.jpeg +0 -0
- package/faces/idle/img/5.jpeg +0 -0
- package/faces/idle/img/6.jpeg +0 -0
- package/faces/idle/img/7.jpeg +0 -0
- package/faces/idle/img/8.jpeg +0 -0
- package/faces/idle/img/9.jpeg +0 -0
- package/faces/listening/config.json +5 -0
- package/faces/listening/img/1.jpeg +0 -0
- package/faces/listening/img/10.jpeg +0 -0
- package/faces/listening/img/11.jpeg +0 -0
- package/faces/listening/img/12.jpeg +0 -0
- package/faces/listening/img/13.jpeg +0 -0
- package/faces/listening/img/14.jpeg +0 -0
- package/faces/listening/img/15.jpeg +0 -0
- package/faces/listening/img/16.jpeg +0 -0
- package/faces/listening/img/17.jpeg +0 -0
- package/faces/listening/img/18.jpeg +0 -0
- package/faces/listening/img/19.jpeg +0 -0
- package/faces/listening/img/2.jpeg +0 -0
- package/faces/listening/img/20.jpeg +0 -0
- package/faces/listening/img/21.jpeg +0 -0
- package/faces/listening/img/22.jpeg +0 -0
- package/faces/listening/img/23.jpeg +0 -0
- package/faces/listening/img/24.jpeg +0 -0
- package/faces/listening/img/25.jpeg +0 -0
- package/faces/listening/img/26.jpeg +0 -0
- package/faces/listening/img/27.jpeg +0 -0
- package/faces/listening/img/3.jpeg +0 -0
- package/faces/listening/img/4.jpeg +0 -0
- package/faces/listening/img/5.jpeg +0 -0
- package/faces/listening/img/6.jpeg +0 -0
- package/faces/listening/img/7.jpeg +0 -0
- package/faces/listening/img/8.jpeg +0 -0
- package/faces/listening/img/9.jpeg +0 -0
- package/faces/thinking/config.json +5 -0
- package/faces/thinking/img/1.jpeg +0 -0
- package/faces/thinking/img/10.jpeg +0 -0
- package/faces/thinking/img/11.jpeg +0 -0
- package/faces/thinking/img/12.jpeg +0 -0
- package/faces/thinking/img/13.jpeg +0 -0
- package/faces/thinking/img/14.jpeg +0 -0
- package/faces/thinking/img/15.jpeg +0 -0
- package/faces/thinking/img/16.jpeg +0 -0
- package/faces/thinking/img/17.jpeg +0 -0
- package/faces/thinking/img/18.jpeg +0 -0
- package/faces/thinking/img/19.jpeg +0 -0
- package/faces/thinking/img/2.jpeg +0 -0
- package/faces/thinking/img/20.jpeg +0 -0
- package/faces/thinking/img/3.jpeg +0 -0
- package/faces/thinking/img/4.jpeg +0 -0
- package/faces/thinking/img/5.jpeg +0 -0
- package/faces/thinking/img/6.jpeg +0 -0
- package/faces/thinking/img/7.jpeg +0 -0
- package/faces/thinking/img/8.jpeg +0 -0
- package/faces/thinking/img/9.jpeg +0 -0
- package/faces/typing/config.json +5 -0
- package/faces/typing/img/1.jpeg +0 -0
- package/faces/typing/img/10.jpeg +0 -0
- package/faces/typing/img/11.jpeg +0 -0
- package/faces/typing/img/12.jpeg +0 -0
- package/faces/typing/img/13.jpeg +0 -0
- package/faces/typing/img/14.jpeg +0 -0
- package/faces/typing/img/15.jpeg +0 -0
- package/faces/typing/img/16.jpeg +0 -0
- package/faces/typing/img/17.jpeg +0 -0
- package/faces/typing/img/18.jpeg +0 -0
- package/faces/typing/img/19.jpeg +0 -0
- package/faces/typing/img/2.jpeg +0 -0
- package/faces/typing/img/20.jpeg +0 -0
- package/faces/typing/img/21.jpeg +0 -0
- package/faces/typing/img/22.jpeg +0 -0
- package/faces/typing/img/23.jpeg +0 -0
- package/faces/typing/img/24.jpeg +0 -0
- package/faces/typing/img/25.jpeg +0 -0
- package/faces/typing/img/26.jpeg +0 -0
- package/faces/typing/img/27.jpeg +0 -0
- package/faces/typing/img/28.jpeg +0 -0
- package/faces/typing/img/29.jpeg +0 -0
- package/faces/typing/img/3.jpeg +0 -0
- package/faces/typing/img/30.jpeg +0 -0
- package/faces/typing/img/31.jpeg +0 -0
- package/faces/typing/img/32.jpeg +0 -0
- package/faces/typing/img/4.jpeg +0 -0
- package/faces/typing/img/5.jpeg +0 -0
- package/faces/typing/img/6.jpeg +0 -0
- package/faces/typing/img/7.jpeg +0 -0
- package/faces/typing/img/8.jpeg +0 -0
- package/faces/typing/img/9.jpeg +0 -0
- package/package.json +36 -0
package/dist/src/main.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { spawnClaude } from './pty.js';
|
|
2
|
+
import { StateMachine } from './state.js';
|
|
3
|
+
import { FaceRenderer } from './face.js';
|
|
4
|
+
import { getTheme } from './theme.js';
|
|
5
|
+
import { initGenerator, buildFace } from './loader.js';
|
|
6
|
+
import { StateServer, StateClient, getSocketPath, writeDiscovery, readDiscovery, clearDiscovery } from './ipc.js';
|
|
7
|
+
// ── Required expression keys ────────────────────────────────────────
|
|
8
|
+
const REQUIRED_EXPRESSIONS = ['idle', 'typing', 'thinking', 'listening'];
|
|
9
|
+
// ── Face validation ─────────────────────────────────────────────────
|
|
10
|
+
function validateFace(face) {
|
|
11
|
+
if (!face.name || typeof face.name !== 'string') {
|
|
12
|
+
throw new Error('Face definition must have a "name" string property.');
|
|
13
|
+
}
|
|
14
|
+
if (typeof face.width !== 'number' || face.width <= 0) {
|
|
15
|
+
throw new Error('Face definition must have a positive "width" number.');
|
|
16
|
+
}
|
|
17
|
+
if (typeof face.height !== 'number' || face.height <= 0) {
|
|
18
|
+
throw new Error('Face definition must have a positive "height" number.');
|
|
19
|
+
}
|
|
20
|
+
if (!face.expressions || typeof face.expressions !== 'object') {
|
|
21
|
+
throw new Error('Face definition must have an "expressions" object.');
|
|
22
|
+
}
|
|
23
|
+
// Check all required expression keys are present
|
|
24
|
+
for (const key of REQUIRED_EXPRESSIONS) {
|
|
25
|
+
if (!(key in face.expressions)) {
|
|
26
|
+
throw new Error(`Face definition is missing required expression "${key}". ` +
|
|
27
|
+
`Required expressions: ${REQUIRED_EXPRESSIONS.join(', ')}.`);
|
|
28
|
+
}
|
|
29
|
+
const expr = face.expressions[key];
|
|
30
|
+
if (!expr.frames || !Array.isArray(expr.frames) || expr.frames.length === 0) {
|
|
31
|
+
throw new Error(`Expression "${key}" must have a non-empty "frames" array.`);
|
|
32
|
+
}
|
|
33
|
+
// Validate animation config
|
|
34
|
+
const config = expr.config;
|
|
35
|
+
if (!config || typeof config !== 'object') {
|
|
36
|
+
throw new Error(`Expression "${key}" must have a "config" object.`);
|
|
37
|
+
}
|
|
38
|
+
if (typeof config.loopDuration !== 'number' || config.loopDuration <= 0) {
|
|
39
|
+
throw new Error(`Expression "${key}": config.loopDuration must be a positive number.`);
|
|
40
|
+
}
|
|
41
|
+
if (typeof config.gap !== 'number' || config.gap < 0) {
|
|
42
|
+
throw new Error(`Expression "${key}": config.gap must be a non-negative number.`);
|
|
43
|
+
}
|
|
44
|
+
if (typeof config.reverse !== 'boolean') {
|
|
45
|
+
throw new Error(`Expression "${key}": config.reverse must be a boolean.`);
|
|
46
|
+
}
|
|
47
|
+
// Validate each frame
|
|
48
|
+
for (let i = 0; i < expr.frames.length; i++) {
|
|
49
|
+
const frame = expr.frames[i];
|
|
50
|
+
if (!frame.art || !Array.isArray(frame.art)) {
|
|
51
|
+
throw new Error(`Expression "${key}", frame ${i}: must have an "art" array of strings.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// ── Load face definition ────────────────────────────────────────────
|
|
57
|
+
async function loadFace(facePath) {
|
|
58
|
+
if (facePath) {
|
|
59
|
+
try {
|
|
60
|
+
const imported = await import(facePath);
|
|
61
|
+
const face = imported.default ?? imported.face ?? imported;
|
|
62
|
+
validateFace(face);
|
|
63
|
+
return face;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (err instanceof Error) {
|
|
67
|
+
throw new Error(`Failed to load face from "${facePath}": ${err.message}`);
|
|
68
|
+
}
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const cols = process.stdout.columns ?? 80;
|
|
73
|
+
const rows = process.stdout.rows ?? 24;
|
|
74
|
+
return buildFace(cols, rows);
|
|
75
|
+
}
|
|
76
|
+
// ── Main entry point ────────────────────────────────────────────────
|
|
77
|
+
export async function run(options) {
|
|
78
|
+
await initGenerator();
|
|
79
|
+
// ── Face-only mode ──────────────────────────────────────────────
|
|
80
|
+
if (options.displayOnly) {
|
|
81
|
+
return runFaceOnly(options);
|
|
82
|
+
}
|
|
83
|
+
// ── Load face ───────────────────────────────────────────────────
|
|
84
|
+
const face = await loadFace(options.facePath);
|
|
85
|
+
// ── --no-face mode (plain passthrough) ──────────────────────────
|
|
86
|
+
if (options.noFace) {
|
|
87
|
+
const cols = process.stdout.columns ?? 80;
|
|
88
|
+
const rows = process.stdout.rows ?? 24;
|
|
89
|
+
const pty = spawnClaude(cols, rows, []);
|
|
90
|
+
process.stdin.setRawMode(true);
|
|
91
|
+
process.stdin.resume();
|
|
92
|
+
process.stdin.on('data', (data) => pty.write(data.toString()));
|
|
93
|
+
pty.onData((data) => process.stdout.write(data));
|
|
94
|
+
pty.onExit(({ exitCode }) => {
|
|
95
|
+
process.stdin.setRawMode(false);
|
|
96
|
+
process.exit(exitCode);
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// ── Wrapper mode (default) ──────────────────────────────────────
|
|
101
|
+
// Start IPC server
|
|
102
|
+
const socketPath = getSocketPath();
|
|
103
|
+
const server = new StateServer(socketPath);
|
|
104
|
+
writeDiscovery(socketPath);
|
|
105
|
+
process.stderr.write(`Face server listening. In another terminal run: cc-face -display\n`);
|
|
106
|
+
// Spawn Claude PTY at full terminal size
|
|
107
|
+
const cols = process.stdout.columns ?? 80;
|
|
108
|
+
const rows = process.stdout.rows ?? 24;
|
|
109
|
+
const pty = spawnClaude(cols, rows, []);
|
|
110
|
+
// State machine — broadcast state changes over IPC
|
|
111
|
+
const stateMachine = new StateMachine((event) => {
|
|
112
|
+
server.broadcast(event.next);
|
|
113
|
+
});
|
|
114
|
+
// Wire PTY data → stdout + state machine
|
|
115
|
+
pty.onData((data) => {
|
|
116
|
+
process.stdout.write(data);
|
|
117
|
+
stateMachine.feed(data);
|
|
118
|
+
});
|
|
119
|
+
// Wire stdin → PTY + notify state machine of human input
|
|
120
|
+
process.stdin.setRawMode(true);
|
|
121
|
+
process.stdin.resume();
|
|
122
|
+
process.stdin.on('data', (data) => {
|
|
123
|
+
stateMachine.notifyHumanInput();
|
|
124
|
+
pty.write(data.toString());
|
|
125
|
+
});
|
|
126
|
+
// Handle SIGWINCH (terminal resize)
|
|
127
|
+
process.on('SIGWINCH', () => {
|
|
128
|
+
const newCols = process.stdout.columns ?? 80;
|
|
129
|
+
const newRows = process.stdout.rows ?? 24;
|
|
130
|
+
pty.resize(newCols, newRows);
|
|
131
|
+
});
|
|
132
|
+
// Cleanup (guarded against double invocation from concurrent signals)
|
|
133
|
+
let cleaned = false;
|
|
134
|
+
const cleanup = () => {
|
|
135
|
+
if (cleaned)
|
|
136
|
+
return;
|
|
137
|
+
cleaned = true;
|
|
138
|
+
stateMachine.destroy();
|
|
139
|
+
server.broadcastShutdown();
|
|
140
|
+
clearDiscovery(socketPath);
|
|
141
|
+
server.close();
|
|
142
|
+
try {
|
|
143
|
+
process.stdin.setRawMode(false);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// stdin may already be destroyed
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
pty.onExit(({ exitCode }) => {
|
|
150
|
+
cleanup();
|
|
151
|
+
process.exit(exitCode);
|
|
152
|
+
});
|
|
153
|
+
process.on('SIGINT', () => {
|
|
154
|
+
cleanup();
|
|
155
|
+
process.exit(0);
|
|
156
|
+
});
|
|
157
|
+
process.on('SIGTERM', () => {
|
|
158
|
+
cleanup();
|
|
159
|
+
process.exit(0);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// ── Face-only mode ──────────────────────────────────────────────────
|
|
163
|
+
async function runFaceOnly(options) {
|
|
164
|
+
const face = await loadFace(options.facePath);
|
|
165
|
+
const theme = getTheme(options.theme);
|
|
166
|
+
const renderer = new FaceRenderer(face, theme);
|
|
167
|
+
// If --sock given, use it directly. Otherwise, discover via the active file.
|
|
168
|
+
const socketPathOrResolver = options.socketPath
|
|
169
|
+
? options.socketPath
|
|
170
|
+
: () => readDiscovery();
|
|
171
|
+
renderer.showMessage('Waiting for cc-face...');
|
|
172
|
+
// Connect to wrapper's IPC server
|
|
173
|
+
const client = new StateClient(socketPathOrResolver, (state) => {
|
|
174
|
+
renderer.setState(state);
|
|
175
|
+
}, {
|
|
176
|
+
onDisconnect() {
|
|
177
|
+
renderer.showMessage('Disconnected. Waiting for cc-face...');
|
|
178
|
+
},
|
|
179
|
+
onConnect() {
|
|
180
|
+
renderer.setState('idle');
|
|
181
|
+
},
|
|
182
|
+
onShutdown() {
|
|
183
|
+
renderer.destroy();
|
|
184
|
+
client.close();
|
|
185
|
+
process.exit(0);
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
// Handle resize — invalidate pre-rendered frame cache
|
|
189
|
+
process.on('SIGWINCH', () => {
|
|
190
|
+
renderer.invalidateCache();
|
|
191
|
+
});
|
|
192
|
+
// Cleanup (guarded against double invocation from concurrent signals)
|
|
193
|
+
let cleaned = false;
|
|
194
|
+
const cleanup = () => {
|
|
195
|
+
if (cleaned)
|
|
196
|
+
return;
|
|
197
|
+
cleaned = true;
|
|
198
|
+
renderer.destroy();
|
|
199
|
+
client.close();
|
|
200
|
+
};
|
|
201
|
+
process.on('SIGINT', () => {
|
|
202
|
+
cleanup();
|
|
203
|
+
process.exit(0);
|
|
204
|
+
});
|
|
205
|
+
process.on('SIGTERM', () => {
|
|
206
|
+
cleanup();
|
|
207
|
+
process.exit(0);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=main.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/main.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// ── Realtime Patterns ────────────────────────────────────────────────
|
|
2
|
+
// Checked during streaming (each feed() call). Priority = array order.
|
|
3
|
+
export const realtimePatterns = [
|
|
4
|
+
{
|
|
5
|
+
state: 'thinking',
|
|
6
|
+
patterns: [
|
|
7
|
+
/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/, // braille spinners
|
|
8
|
+
/[·✢✳✶✻✽]/, // tweakcc phases
|
|
9
|
+
/Thinking/,
|
|
10
|
+
/Reasoning/,
|
|
11
|
+
],
|
|
12
|
+
},
|
|
13
|
+
];
|
|
14
|
+
//# sourceMappingURL=patterns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patterns.js","sourceRoot":"","sources":["../../src/patterns.ts"],"names":[],"mappings":"AASA,wEAAwE;AACxE,uEAAuE;AAEvE,MAAM,CAAC,MAAM,gBAAgB,GAAmB;IAC9C;QACE,KAAK,EAAE,UAAU;QACjB,QAAQ,EAAE;YACR,cAAc,EAAK,mBAAmB;YACtC,UAAU,EAAe,iBAAiB;YAC1C,UAAU;YACV,WAAW;SACZ;KACF;CACF,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface PtyHandle {
|
|
2
|
+
onData(cb: (data: string) => void): void;
|
|
3
|
+
onExit(cb: (exit: {
|
|
4
|
+
exitCode: number;
|
|
5
|
+
signal?: number;
|
|
6
|
+
}) => void): void;
|
|
7
|
+
write(data: string): void;
|
|
8
|
+
resize(cols: number, rows: number): void;
|
|
9
|
+
kill(): void;
|
|
10
|
+
pid: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function findClaude(): string;
|
|
13
|
+
export declare function resolveSymlink(p: string): string | null;
|
|
14
|
+
/**
|
|
15
|
+
* Spawn `claude` CLI in a pseudo-terminal.
|
|
16
|
+
*/
|
|
17
|
+
export declare function spawnClaude(cols: number, rows: number, args?: string[]): PtyHandle;
|
package/dist/src/pty.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as pty from 'node-pty';
|
|
2
|
+
import { accessSync, constants, readlinkSync } from 'node:fs';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
export function findClaude() {
|
|
5
|
+
// 1. Explicit override via env var
|
|
6
|
+
if (process.env.CLAUDE_PATH)
|
|
7
|
+
return process.env.CLAUDE_PATH;
|
|
8
|
+
if (process.platform === 'win32')
|
|
9
|
+
return 'cmd.exe';
|
|
10
|
+
// 2. Resolve via user's login shell (picks up ~/.local/bin etc.)
|
|
11
|
+
try {
|
|
12
|
+
const resolved = execSync('which claude', { encoding: 'utf-8', env: process.env }).trim();
|
|
13
|
+
if (resolved)
|
|
14
|
+
return resolved;
|
|
15
|
+
}
|
|
16
|
+
catch { /* fall through */ }
|
|
17
|
+
// 3. Check common install locations
|
|
18
|
+
const home = process.env.HOME ?? '';
|
|
19
|
+
const candidates = [
|
|
20
|
+
`${home}/.local/bin/claude`,
|
|
21
|
+
'/usr/local/bin/claude',
|
|
22
|
+
'/opt/homebrew/bin/claude',
|
|
23
|
+
];
|
|
24
|
+
for (const p of candidates) {
|
|
25
|
+
try {
|
|
26
|
+
accessSync(p, constants.X_OK);
|
|
27
|
+
return p;
|
|
28
|
+
}
|
|
29
|
+
catch { /* next */ }
|
|
30
|
+
}
|
|
31
|
+
throw new Error('Could not find "claude" CLI. Ensure Claude Code is installed and on your PATH,\n' +
|
|
32
|
+
'or set CLAUDE_PATH=/path/to/claude.');
|
|
33
|
+
}
|
|
34
|
+
export function resolveSymlink(p) {
|
|
35
|
+
try {
|
|
36
|
+
return readlinkSync(p);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Spawn `claude` CLI in a pseudo-terminal.
|
|
44
|
+
*/
|
|
45
|
+
export function spawnClaude(cols, rows, args = []) {
|
|
46
|
+
const claudePath = findClaude();
|
|
47
|
+
const shellArgs = process.platform === 'win32' ? ['/c', 'claude', ...args] : args;
|
|
48
|
+
let proc;
|
|
49
|
+
try {
|
|
50
|
+
proc = pty.spawn(claudePath, shellArgs, {
|
|
51
|
+
name: 'xterm-256color',
|
|
52
|
+
cols,
|
|
53
|
+
rows,
|
|
54
|
+
cwd: process.cwd(),
|
|
55
|
+
env: process.env,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
60
|
+
throw new Error(`Failed to spawn PTY for "${claudePath}": ${msg}\n` +
|
|
61
|
+
`Tip: verify with: ls -la ${claudePath} && ${claudePath} --version`);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
onData(cb) {
|
|
65
|
+
proc.onData(cb);
|
|
66
|
+
},
|
|
67
|
+
onExit(cb) {
|
|
68
|
+
proc.onExit(cb);
|
|
69
|
+
},
|
|
70
|
+
write(data) {
|
|
71
|
+
proc.write(data);
|
|
72
|
+
},
|
|
73
|
+
resize(c, r) {
|
|
74
|
+
proc.resize(c, r);
|
|
75
|
+
},
|
|
76
|
+
kill() {
|
|
77
|
+
proc.kill();
|
|
78
|
+
},
|
|
79
|
+
get pid() {
|
|
80
|
+
return proc.pid;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=pty.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pty.js","sourceRoot":"","sources":["../../src/pty.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
export interface ScrollDimensions {
|
|
8
|
+
faceRows: number;
|
|
9
|
+
contentRows: number;
|
|
10
|
+
totalRows: number;
|
|
11
|
+
totalCols: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Return current terminal dimensions split by face height.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getScrollDimensions(faceHeight: number): ScrollDimensions;
|
|
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 declare function setupScrollRegion(faceHeight: number): () => void;
|
|
24
|
+
/**
|
|
25
|
+
* Reset scroll region to the full terminal and show cursor.
|
|
26
|
+
*/
|
|
27
|
+
export declare function teardownScrollRegion(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Save cursor position and move to row 0, col 0 (top-left) for face
|
|
30
|
+
* rendering. Uses 1-indexed ANSI coordinates.
|
|
31
|
+
*/
|
|
32
|
+
export declare function moveCursorToFace(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Restore the previously saved cursor position.
|
|
35
|
+
*/
|
|
36
|
+
export declare function restoreCursor(): void;
|
|
@@ -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,36 @@
|
|
|
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
|
+
private hasReceivedHumanInput;
|
|
13
|
+
constructor(onStateChange: (event: StateEvent) => void);
|
|
14
|
+
getState(): FaceState;
|
|
15
|
+
/**
|
|
16
|
+
* Called when the human presses a key. Transitions to 'listening'
|
|
17
|
+
* and resets the listening timeout. When the user stops typing for
|
|
18
|
+
* LISTENING_TIMEOUT_MS, transitions back to idle.
|
|
19
|
+
*/
|
|
20
|
+
notifyHumanInput(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Feed raw PTY data. Instead of processing every chunk synchronously
|
|
23
|
+
* (hundreds of regex ops + timer create/destroy per second during heavy
|
|
24
|
+
* output), we accumulate data and process it at a fixed sample rate.
|
|
25
|
+
* This adds up to 50ms of detection latency, which is imperceptible
|
|
26
|
+
* given the 100ms debounce on output.
|
|
27
|
+
*/
|
|
28
|
+
feed(data: string): void;
|
|
29
|
+
destroy(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Process accumulated data: strip ANSI once for the whole batch,
|
|
32
|
+
* run pattern matching once, check for typing once.
|
|
33
|
+
*/
|
|
34
|
+
private processPendingData;
|
|
35
|
+
private transition;
|
|
36
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
// Suppress typing transitions until the user has pressed a key.
|
|
32
|
+
// Prevents Claude's startup banner from triggering the typing state.
|
|
33
|
+
hasReceivedHumanInput = false;
|
|
34
|
+
constructor(onStateChange) {
|
|
35
|
+
this.onStateChange = onStateChange;
|
|
36
|
+
}
|
|
37
|
+
getState() {
|
|
38
|
+
return this.state;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Called when the human presses a key. Transitions to 'listening'
|
|
42
|
+
* and resets the listening timeout. When the user stops typing for
|
|
43
|
+
* LISTENING_TIMEOUT_MS, transitions back to idle.
|
|
44
|
+
*/
|
|
45
|
+
notifyHumanInput() {
|
|
46
|
+
this.hasReceivedHumanInput = true;
|
|
47
|
+
// Don't override thinking/typing — Claude is actively doing something
|
|
48
|
+
if (this.state === 'thinking' || this.state === 'typing')
|
|
49
|
+
return;
|
|
50
|
+
this.transition('listening');
|
|
51
|
+
// Reset listening timeout
|
|
52
|
+
if (this.listeningTimer !== null)
|
|
53
|
+
clearTimeout(this.listeningTimer);
|
|
54
|
+
this.listeningTimer = setTimeout(() => {
|
|
55
|
+
this.listeningTimer = null;
|
|
56
|
+
if (this.state === 'listening') {
|
|
57
|
+
this.transition('idle');
|
|
58
|
+
}
|
|
59
|
+
}, LISTENING_TIMEOUT_MS);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Feed raw PTY data. Instead of processing every chunk synchronously
|
|
63
|
+
* (hundreds of regex ops + timer create/destroy per second during heavy
|
|
64
|
+
* output), we accumulate data and process it at a fixed sample rate.
|
|
65
|
+
* This adds up to 50ms of detection latency, which is imperceptible
|
|
66
|
+
* given the 100ms debounce on output.
|
|
67
|
+
*/
|
|
68
|
+
feed(data) {
|
|
69
|
+
if (data.length === 0)
|
|
70
|
+
return;
|
|
71
|
+
this.pendingData += data;
|
|
72
|
+
this.hasPendingData = true;
|
|
73
|
+
// Reset idle timer on every chunk (this is cheap — just a clear + set)
|
|
74
|
+
if (this.idleTimer !== null)
|
|
75
|
+
clearTimeout(this.idleTimer);
|
|
76
|
+
this.idleTimer = setTimeout(() => {
|
|
77
|
+
this.idleTimer = null;
|
|
78
|
+
this.transition('idle');
|
|
79
|
+
}, IDLE_TIMEOUT_MS);
|
|
80
|
+
// Schedule processing if not already pending
|
|
81
|
+
if (this.feedTimer === null) {
|
|
82
|
+
this.feedTimer = setTimeout(() => this.processPendingData(), FEED_SAMPLE_INTERVAL_MS);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// ── Cleanup ──────────────────────────────────────────────────────
|
|
86
|
+
destroy() {
|
|
87
|
+
if (this.idleTimer !== null) {
|
|
88
|
+
clearTimeout(this.idleTimer);
|
|
89
|
+
this.idleTimer = null;
|
|
90
|
+
}
|
|
91
|
+
if (this.debounceTimer !== null) {
|
|
92
|
+
clearTimeout(this.debounceTimer);
|
|
93
|
+
this.debounceTimer = null;
|
|
94
|
+
}
|
|
95
|
+
if (this.feedTimer !== null) {
|
|
96
|
+
clearTimeout(this.feedTimer);
|
|
97
|
+
this.feedTimer = null;
|
|
98
|
+
}
|
|
99
|
+
if (this.listeningTimer !== null) {
|
|
100
|
+
clearTimeout(this.listeningTimer);
|
|
101
|
+
this.listeningTimer = null;
|
|
102
|
+
}
|
|
103
|
+
this.pendingData = '';
|
|
104
|
+
this.hasPendingData = false;
|
|
105
|
+
}
|
|
106
|
+
// ── Private helpers ──────────────────────────────────────────────
|
|
107
|
+
/**
|
|
108
|
+
* Process accumulated data: strip ANSI once for the whole batch,
|
|
109
|
+
* run pattern matching once, check for typing once.
|
|
110
|
+
*/
|
|
111
|
+
processPendingData() {
|
|
112
|
+
this.feedTimer = null;
|
|
113
|
+
if (!this.hasPendingData)
|
|
114
|
+
return;
|
|
115
|
+
const raw = this.pendingData;
|
|
116
|
+
this.pendingData = '';
|
|
117
|
+
this.hasPendingData = false;
|
|
118
|
+
const clean = stripAnsi(raw);
|
|
119
|
+
// Realtime pattern matching (priority order)
|
|
120
|
+
const match = matchPatterns(clean, realtimePatterns);
|
|
121
|
+
if (match) {
|
|
122
|
+
if (match.state === 'thinking') {
|
|
123
|
+
this.lastThinkingMatchTime = Date.now();
|
|
124
|
+
}
|
|
125
|
+
// Cancel listening state — Claude is responding
|
|
126
|
+
if (this.listeningTimer !== null) {
|
|
127
|
+
clearTimeout(this.listeningTimer);
|
|
128
|
+
this.listeningTimer = null;
|
|
129
|
+
}
|
|
130
|
+
this.transition(match.state);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// "typing" — non-trivial printable text from Claude (not human echo).
|
|
134
|
+
// If we're in listening state, PTY echo from human keystrokes will appear
|
|
135
|
+
// but it's short (1-2 chars per keystroke). MIN_PRINTABLE_LEN filters that.
|
|
136
|
+
// Suppress if we recently saw a thinking pattern.
|
|
137
|
+
if (this.state !== 'listening') {
|
|
138
|
+
if (!this.hasReceivedHumanInput)
|
|
139
|
+
return;
|
|
140
|
+
const inThinkingCooldown = Date.now() - this.lastThinkingMatchTime < THINKING_COOLDOWN_MS;
|
|
141
|
+
if (inThinkingCooldown)
|
|
142
|
+
return;
|
|
143
|
+
const printable = clean.replace(/[\x00-\x1f\x7f]/g, '').trim();
|
|
144
|
+
if (printable.length >= MIN_PRINTABLE_LEN) {
|
|
145
|
+
this.transition('typing');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
transition(next) {
|
|
150
|
+
if (next === this.state)
|
|
151
|
+
return;
|
|
152
|
+
const prev = this.state;
|
|
153
|
+
this.state = next;
|
|
154
|
+
// Debounce the callback — only fire after state is stable for DEBOUNCE_MS.
|
|
155
|
+
// Internal state updates immediately so getState() is always current.
|
|
156
|
+
if (this.debounceTimer !== null)
|
|
157
|
+
clearTimeout(this.debounceTimer);
|
|
158
|
+
this.debounceTimer = setTimeout(() => {
|
|
159
|
+
this.debounceTimer = null;
|
|
160
|
+
this.onStateChange({ prev, next: this.state, timestamp: Date.now() });
|
|
161
|
+
}, DEBOUNCE_MS);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=state.js.map
|