aniclaude 1.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/dist/index.js ADDED
@@ -0,0 +1,332 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * AniClaude CLI
5
+ * Voice-enabled Claude Code - talk to Claude in your terminal
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ const config_1 = require("./config");
42
+ const server_1 = require("./server");
43
+ const pty_1 = require("./pty");
44
+ const child_process_1 = require("child_process");
45
+ const os = __importStar(require("os"));
46
+ const fs = __importStar(require("fs"));
47
+ const path = __importStar(require("path"));
48
+ // Hook script content - reads transcript and sends ALL new assistant text to TTS
49
+ const TTS_HOOK_SCRIPT = `#!/usr/bin/env python3
50
+ import sys
51
+ import json
52
+ import os
53
+ import urllib.request
54
+
55
+ try:
56
+ input_data = json.load(sys.stdin)
57
+ transcript_path = input_data.get('transcript_path', '')
58
+
59
+ if not transcript_path:
60
+ sys.exit(0)
61
+
62
+ # State file to track last processed line
63
+ state_file = transcript_path + '.aniclaude_state'
64
+ last_line = 0
65
+ if os.path.exists(state_file):
66
+ try:
67
+ with open(state_file, 'r') as f:
68
+ last_line = int(f.read().strip())
69
+ except:
70
+ last_line = 0
71
+
72
+ # Read transcript (JSONL format)
73
+ with open(transcript_path, 'r') as f:
74
+ lines = f.readlines()
75
+
76
+ # Collect all NEW assistant text (since last processed)
77
+ all_text = []
78
+ for i, line in enumerate(lines):
79
+ if i < last_line:
80
+ continue
81
+ try:
82
+ entry = json.loads(line)
83
+ message = entry.get('message', entry)
84
+ if message.get('role') == 'assistant':
85
+ content = message.get('content', [])
86
+ for block in content:
87
+ if isinstance(block, dict) and block.get('type') == 'text':
88
+ text = block.get('text', '')
89
+ if text and len(text) > 5:
90
+ all_text.append(text)
91
+ except:
92
+ continue
93
+
94
+ # Save state
95
+ with open(state_file, 'w') as f:
96
+ f.write(str(len(lines)))
97
+
98
+ if not all_text:
99
+ sys.exit(0)
100
+
101
+ # Join all text and send to TTS
102
+ combined = ' '.join(all_text)
103
+ if len(combined) < 10:
104
+ sys.exit(0)
105
+
106
+ data = json.dumps({'text': combined}).encode('utf-8')
107
+ req = urllib.request.Request(
108
+ 'http://localhost:${config_1.HTTP_PORT}/relay',
109
+ data=data,
110
+ headers={'Content-Type': 'application/json'}
111
+ )
112
+ urllib.request.urlopen(req, timeout=5)
113
+ except Exception as e:
114
+ # Uncomment for debugging:
115
+ # print(f"Hook error: {e}", file=sys.stderr)
116
+ pass
117
+
118
+ sys.exit(0)
119
+ `;
120
+ /**
121
+ * Set up the TTS hook in Claude settings
122
+ */
123
+ function setupTTSHook() {
124
+ const homeDir = os.homedir();
125
+ const claudeDir = path.join(homeDir, '.claude');
126
+ const hooksDir = path.join(claudeDir, 'hooks');
127
+ const settingsPath = path.join(claudeDir, 'settings.json');
128
+ const hookScriptPath = path.join(hooksDir, 'aniclaude-tts.py');
129
+ // Create hooks directory if needed
130
+ if (!fs.existsSync(hooksDir)) {
131
+ fs.mkdirSync(hooksDir, { recursive: true });
132
+ }
133
+ // Write hook script
134
+ fs.writeFileSync(hookScriptPath, TTS_HOOK_SCRIPT.replace('${HTTP_PORT}', String(config_1.HTTP_PORT)));
135
+ fs.chmodSync(hookScriptPath, '755');
136
+ // Update settings.json
137
+ let settings = {};
138
+ if (fs.existsSync(settingsPath)) {
139
+ try {
140
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
141
+ }
142
+ catch {
143
+ settings = {};
144
+ }
145
+ }
146
+ // Add our hook to Stop hooks
147
+ const hooks = settings.hooks || {};
148
+ const stopHooks = hooks.Stop || [];
149
+ // Check if our hook already exists
150
+ const hookExists = stopHooks.some((h) => {
151
+ const hook = h;
152
+ const innerHooks = hook.hooks;
153
+ return innerHooks?.some((ih) => typeof ih.command === 'string' && ih.command.includes('aniclaude-tts.py'));
154
+ });
155
+ if (!hookExists) {
156
+ stopHooks.push({
157
+ matcher: '',
158
+ hooks: [
159
+ {
160
+ type: 'command',
161
+ command: hookScriptPath
162
+ }
163
+ ]
164
+ });
165
+ hooks.Stop = stopHooks;
166
+ settings.hooks = hooks;
167
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
168
+ console.log('[AniClaude] TTS hook configured');
169
+ }
170
+ }
171
+ // Parse command line arguments
172
+ function parseArgs() {
173
+ const args = process.argv.slice(2);
174
+ let skipPermissions = true; // Default to skipping permissions
175
+ let noBrowser = false;
176
+ const claudeArgs = [];
177
+ for (let i = 0; i < args.length; i++) {
178
+ const arg = args[i];
179
+ if (arg === '--help' || arg === '-h') {
180
+ showHelp();
181
+ process.exit(0);
182
+ }
183
+ else if (arg === '--with-permissions') {
184
+ skipPermissions = false;
185
+ }
186
+ else if (arg === '--no-browser') {
187
+ noBrowser = true;
188
+ }
189
+ else if (arg === '--version' || arg === '-v') {
190
+ console.log('aniclaude v1.0.0');
191
+ process.exit(0);
192
+ }
193
+ else {
194
+ claudeArgs.push(arg);
195
+ }
196
+ }
197
+ return { skipPermissions, noBrowser, claudeArgs };
198
+ }
199
+ function showHelp() {
200
+ console.log(`
201
+ \x1b[36mAniClaude\x1b[0m - Voice-enabled Claude Code
202
+
203
+ \x1b[33mUsage:\x1b[0m
204
+ npx aniclaude [options]
205
+
206
+ \x1b[33mOptions:\x1b[0m
207
+ --with-permissions Run Claude with permission prompts (default: skipped)
208
+ --no-browser Don't open the avatar window
209
+ -h, --help Show this help
210
+ -v, --version Show version
211
+
212
+ \x1b[33mExamples:\x1b[0m
213
+ npx aniclaude Start with defaults (no permission prompts)
214
+ npx aniclaude --no-browser CLI only, no avatar window
215
+ `);
216
+ }
217
+ /**
218
+ * Open the avatar window in a small popup
219
+ */
220
+ function openAvatarWindow(url) {
221
+ const platform = os.platform();
222
+ if (platform === 'darwin') {
223
+ // macOS: Use AppleScript to open Chrome with specific size
224
+ const chromeScript = `
225
+ tell application "Google Chrome"
226
+ set newWindow to make new window
227
+ set bounds of newWindow to {100, 100, ${100 + config_1.WINDOW_WIDTH}, ${100 + config_1.WINDOW_HEIGHT}}
228
+ set URL of active tab of newWindow to "${url}"
229
+ end tell
230
+ `;
231
+ try {
232
+ (0, child_process_1.execSync)(`osascript -e '${chromeScript}'`, { stdio: 'ignore' });
233
+ return;
234
+ }
235
+ catch {
236
+ // Chrome not available, try Safari
237
+ }
238
+ const safariScript = `
239
+ tell application "Safari"
240
+ make new document with properties {URL:"${url}"}
241
+ set bounds of front window to {100, 100, ${100 + config_1.WINDOW_WIDTH}, ${100 + config_1.WINDOW_HEIGHT}}
242
+ end tell
243
+ `;
244
+ try {
245
+ (0, child_process_1.execSync)(`osascript -e '${safariScript}'`, { stdio: 'ignore' });
246
+ return;
247
+ }
248
+ catch {
249
+ // Safari not available
250
+ }
251
+ }
252
+ // Fallback: use open module
253
+ Promise.resolve().then(() => __importStar(require('open'))).then(({ default: open }) => {
254
+ open(url);
255
+ }).catch(() => {
256
+ console.log(`[AniClaude] Open this URL in your browser: ${url}`);
257
+ });
258
+ }
259
+ /**
260
+ * Main entry point
261
+ */
262
+ async function main() {
263
+ const { skipPermissions, noBrowser, claudeArgs } = parseArgs();
264
+ // Show banner
265
+ console.log(config_1.BANNER);
266
+ console.log(`\x1b[33mTTS Server:\x1b[0m ${config_1.TTS_SERVER_URL}`);
267
+ console.log(`\x1b[33mLocal HTTP:\x1b[0m http://localhost:${config_1.HTTP_PORT}`);
268
+ console.log(`\x1b[33mWebSocket:\x1b[0m ws://localhost:${config_1.WS_PORT}`);
269
+ console.log();
270
+ // Set up TTS hook in Claude settings
271
+ setupTTSHook();
272
+ // Start local servers
273
+ (0, server_1.startHTTPServer)();
274
+ // Create PTY manager
275
+ const claudePty = new pty_1.ClaudePTY({
276
+ skipPermissions,
277
+ claudeArgs,
278
+ });
279
+ // Start WebSocket server with voice input handler
280
+ (0, server_1.startWebSocketServer)((text) => {
281
+ claudePty.writeText(text);
282
+ });
283
+ // Open avatar window
284
+ if (!noBrowser) {
285
+ const url = (0, server_1.getAvatarWindowURL)();
286
+ console.log('[AniClaude] Opening avatar window...');
287
+ openAvatarWindow(url);
288
+ }
289
+ // Small delay for servers to start
290
+ await new Promise(resolve => setTimeout(resolve, 500));
291
+ // Spawn Claude
292
+ console.log('[AniClaude] Starting Claude Code...');
293
+ console.log();
294
+ // Suppress logging to avoid TUI glitches
295
+ (0, server_1.suppressLogging)();
296
+ const ptyProcess = claudePty.spawn();
297
+ // Pipe PTY output to stdout
298
+ ptyProcess.onData((data) => {
299
+ process.stdout.write(data);
300
+ });
301
+ // Pipe stdin to PTY
302
+ if (process.stdin.isTTY) {
303
+ process.stdin.setRawMode(true);
304
+ }
305
+ process.stdin.resume();
306
+ process.stdin.on('data', (data) => {
307
+ claudePty.write(data.toString());
308
+ });
309
+ // Handle terminal resize
310
+ process.stdout.on('resize', () => {
311
+ claudePty.resize(process.stdout.columns || 80, process.stdout.rows || 24);
312
+ });
313
+ // Handle PTY exit
314
+ ptyProcess.onExit(({ exitCode }) => {
315
+ console.log(`\n[AniClaude] Session ended (exit code: ${exitCode})`);
316
+ process.exit(exitCode);
317
+ });
318
+ // Handle process signals
319
+ process.on('SIGINT', () => {
320
+ // Send Ctrl+C to Claude
321
+ claudePty.write('\x03');
322
+ });
323
+ process.on('SIGTERM', () => {
324
+ claudePty.kill();
325
+ process.exit(0);
326
+ });
327
+ }
328
+ // Run
329
+ main().catch((error) => {
330
+ console.error('\x1b[31m[AniClaude] Error:\x1b[0m', error.message);
331
+ process.exit(1);
332
+ });
package/dist/pty.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * AniClaude PTY Manager
3
+ * Handles spawning Claude Code with proper TTY support
4
+ */
5
+ import { IPty } from 'node-pty';
6
+ export declare class ClaudePTY {
7
+ private pty;
8
+ private skipPermissions;
9
+ private claudeArgs;
10
+ constructor(options?: {
11
+ skipPermissions?: boolean;
12
+ claudeArgs?: string[];
13
+ });
14
+ /**
15
+ * Find the claude binary path
16
+ */
17
+ private findClaudePath;
18
+ /**
19
+ * Spawn Claude Code process
20
+ */
21
+ spawn(): IPty;
22
+ /**
23
+ * Write text to Claude (with bracketed paste mode)
24
+ */
25
+ writeText(text: string): void;
26
+ /**
27
+ * Write raw data to Claude
28
+ */
29
+ write(data: string): void;
30
+ /**
31
+ * Resize the PTY
32
+ */
33
+ resize(cols: number, rows: number): void;
34
+ /**
35
+ * Kill the PTY process
36
+ */
37
+ kill(): void;
38
+ }
package/dist/pty.js ADDED
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ /**
3
+ * AniClaude PTY Manager
4
+ * Handles spawning Claude Code with proper TTY support
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.ClaudePTY = void 0;
41
+ const pty = __importStar(require("node-pty"));
42
+ const child_process_1 = require("child_process");
43
+ class ClaudePTY {
44
+ pty = null;
45
+ skipPermissions;
46
+ claudeArgs;
47
+ constructor(options = {}) {
48
+ this.skipPermissions = options.skipPermissions || false;
49
+ this.claudeArgs = options.claudeArgs || [];
50
+ }
51
+ /**
52
+ * Find the claude binary path
53
+ */
54
+ findClaudePath() {
55
+ try {
56
+ // Try to find claude in PATH using shell
57
+ const claudePath = (0, child_process_1.execSync)('which claude', { encoding: 'utf-8' }).trim();
58
+ if (claudePath)
59
+ return claudePath;
60
+ }
61
+ catch {
62
+ // Ignore
63
+ }
64
+ // Common locations
65
+ const paths = [
66
+ `${process.env.HOME}/.nvm/versions/node/v20.19.0/bin/claude`,
67
+ `${process.env.HOME}/.local/bin/claude`,
68
+ '/usr/local/bin/claude',
69
+ '/opt/homebrew/bin/claude',
70
+ ];
71
+ for (const p of paths) {
72
+ try {
73
+ (0, child_process_1.execSync)(`test -x "${p}"`, { stdio: 'ignore' });
74
+ return p;
75
+ }
76
+ catch {
77
+ // Not found, continue
78
+ }
79
+ }
80
+ return 'claude'; // Fall back to PATH
81
+ }
82
+ /**
83
+ * Spawn Claude Code process
84
+ */
85
+ spawn() {
86
+ const args = [];
87
+ if (this.skipPermissions) {
88
+ args.push('--dangerously-skip-permissions');
89
+ }
90
+ args.push(...this.claudeArgs);
91
+ // Get terminal size
92
+ const cols = process.stdout.columns || 80;
93
+ const rows = process.stdout.rows || 24;
94
+ // Find claude binary
95
+ const claudePath = this.findClaudePath();
96
+ // Spawn through shell since claude is a Node script
97
+ const shell = process.env.SHELL || '/bin/bash';
98
+ const shellArgs = ['-c', `"${claudePath}" ${args.map(a => `"${a}"`).join(' ')}`];
99
+ this.pty = pty.spawn(shell, shellArgs, {
100
+ name: 'xterm-256color',
101
+ cols,
102
+ rows,
103
+ cwd: process.cwd(),
104
+ env: {
105
+ ...process.env,
106
+ TERM: process.env.TERM || 'xterm-256color',
107
+ },
108
+ });
109
+ return this.pty;
110
+ }
111
+ /**
112
+ * Write text to Claude (with bracketed paste mode)
113
+ */
114
+ writeText(text) {
115
+ if (!this.pty)
116
+ return;
117
+ // Use bracketed paste mode to prevent TUI glitches
118
+ const pasteStart = '\x1b[200~';
119
+ const pasteEnd = '\x1b[201~';
120
+ this.pty.write(pasteStart + text + pasteEnd);
121
+ // Small delay then send Enter
122
+ setTimeout(() => {
123
+ this.pty?.write('\r');
124
+ }, 50);
125
+ }
126
+ /**
127
+ * Write raw data to Claude
128
+ */
129
+ write(data) {
130
+ this.pty?.write(data);
131
+ }
132
+ /**
133
+ * Resize the PTY
134
+ */
135
+ resize(cols, rows) {
136
+ this.pty?.resize(cols, rows);
137
+ }
138
+ /**
139
+ * Kill the PTY process
140
+ */
141
+ kill() {
142
+ this.pty?.kill();
143
+ this.pty = null;
144
+ }
145
+ }
146
+ exports.ClaudePTY = ClaudePTY;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * AniClaude Local Servers
3
+ * - HTTP server: serves the avatar window HTML and assets
4
+ * - WebSocket server: relays messages between browser and Claude PTY
5
+ */
6
+ import http from 'http';
7
+ import { WebSocketServer } from 'ws';
8
+ /**
9
+ * Suppress logging (call after Claude starts to avoid TUI glitches)
10
+ */
11
+ export declare function suppressLogging(): void;
12
+ /**
13
+ * Start HTTP server for serving static files
14
+ */
15
+ export declare function startHTTPServer(): http.Server;
16
+ /**
17
+ * Start WebSocket server for browser relay
18
+ */
19
+ export declare function startWebSocketServer(onVoiceInput: (text: string) => void): WebSocketServer;
20
+ /**
21
+ * Get the URL for the avatar window
22
+ */
23
+ export declare function getAvatarWindowURL(): string;