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/README.md +40 -0
- package/assets/cli-window.html +602 -0
- package/assets/idle.mp4 +0 -0
- package/assets/speak.mp4 +0 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +22 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +332 -0
- package/dist/pty.d.ts +38 -0
- package/dist/pty.js +146 -0
- package/dist/server.d.ts +23 -0
- package/dist/server.js +175 -0
- package/package.json +48 -0
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;
|
package/dist/server.d.ts
ADDED
|
@@ -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;
|