deliberate 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/install.js ADDED
@@ -0,0 +1,754 @@
1
+ /**
2
+ * Installer - Sets up Claude Code hooks and configuration
3
+ * Handles:
4
+ * - Symlinking hooks to ~/.claude/hooks/
5
+ * - Updating ~/.claude/settings.json
6
+ * - Configuring Deliberate LLM provider
7
+ * - Optionally starting the classifier server
8
+ */
9
+
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import os from 'os';
13
+ import { fileURLToPath } from 'url';
14
+ import { execSync } from 'child_process';
15
+ import { LLM_PROVIDERS, setLLMProvider, isLLMConfigured, getConfigFile } from './config.js';
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = path.dirname(__filename);
19
+
20
+ // Cross-platform paths
21
+ const HOME_DIR = os.homedir();
22
+ const IS_WINDOWS = process.platform === 'win32';
23
+ const CLAUDE_DIR = path.join(HOME_DIR, '.claude');
24
+ const HOOKS_DIR = path.join(CLAUDE_DIR, 'hooks');
25
+ const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
26
+
27
+ // Python command (python on Windows, python3 on Unix)
28
+ const PYTHON_CMD = IS_WINDOWS ? 'python' : 'python3';
29
+ const PIP_CMD = IS_WINDOWS ? 'pip' : 'pip3';
30
+
31
+ // Required Python packages
32
+ const PYTHON_DEPS = ['sentence-transformers', 'scikit-learn', 'numpy', 'claude-agent-sdk'];
33
+
34
+ // Model download configuration
35
+ const MODELS_URL = 'https://github.com/the-radar/deliberate/releases/download/v1.0.0/deliberate-models.tar.gz';
36
+ const MODELS_DIR = path.join(__dirname, '..', 'models');
37
+
38
+ // Hook files to install
39
+ const HOOKS = [
40
+ // Commands - PreToolUse for analysis and optional blocking
41
+ {
42
+ source: 'deliberate-commands.py',
43
+ dest: 'deliberate-commands.py',
44
+ event: 'PreToolUse',
45
+ matcher: 'Bash',
46
+ timeout: 35
47
+ },
48
+ // Commands - PostToolUse for persistent display of cached analysis
49
+ {
50
+ source: 'deliberate-commands-post.py',
51
+ dest: 'deliberate-commands-post.py',
52
+ event: 'PostToolUse',
53
+ matcher: 'Bash',
54
+ timeout: 5 // Just reads cache, no analysis needed
55
+ },
56
+ // Changes - PostToolUse for informational analysis only (cannot block)
57
+ {
58
+ source: 'deliberate-changes.py',
59
+ dest: 'deliberate-changes.py',
60
+ event: 'PostToolUse',
61
+ matcher: 'Write|Edit|MultiEdit',
62
+ timeout: 35
63
+ }
64
+ ];
65
+
66
+ /**
67
+ * Download and extract ML models from GitHub Release
68
+ * Uses execSync with hardcoded URLs - no user input, shell injection not possible
69
+ * @returns {Promise<boolean>} Success
70
+ */
71
+ async function downloadModels() {
72
+ // Check if models already exist
73
+ const modelCheck = path.join(MODELS_DIR, 'cmdcaliper-base', 'model.safetensors');
74
+ if (fs.existsSync(modelCheck)) {
75
+ console.log(' Models already downloaded');
76
+ return true;
77
+ }
78
+
79
+ console.log(' Downloading models from GitHub Release...');
80
+ const tarPath = path.join(os.tmpdir(), 'deliberate-models.tar.gz');
81
+
82
+ try {
83
+ // Download - URLs are hardcoded constants, not user input
84
+ execSync(`curl -L -o "${tarPath}" "${MODELS_URL}"`, {
85
+ stdio: 'inherit',
86
+ timeout: 300000 // 5 min timeout
87
+ });
88
+
89
+ // Create models directory
90
+ ensureDir(MODELS_DIR);
91
+
92
+ // Extract tarball
93
+ execSync(`tar -xzf "${tarPath}" -C "${path.dirname(MODELS_DIR)}"`, {
94
+ stdio: 'inherit'
95
+ });
96
+
97
+ // Clean up tarball
98
+ fs.unlinkSync(tarPath);
99
+
100
+ console.log(' Models downloaded successfully');
101
+ return true;
102
+ } catch (error) {
103
+ console.error(' Failed to download models:', error.message);
104
+ console.error('');
105
+ console.error(' You can manually download from:');
106
+ console.error(` ${MODELS_URL}`);
107
+ console.error(` And extract to: ${MODELS_DIR}`);
108
+ return false;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Get the command to run a Python hook
114
+ * On Windows, we need to call python explicitly
115
+ * @param {string} hookPath - Path to the hook file
116
+ * @returns {string} Command to run the hook
117
+ */
118
+ function getHookCommand(hookPath) {
119
+ if (IS_WINDOWS) {
120
+ // Windows: call python explicitly
121
+ return `python "${hookPath}"`;
122
+ }
123
+ // Unix: can run directly (shebang)
124
+ return hookPath;
125
+ }
126
+
127
+ /**
128
+ * Ensure a directory exists
129
+ * @param {string} dir - Directory path
130
+ */
131
+ function ensureDir(dir) {
132
+ if (!fs.existsSync(dir)) {
133
+ fs.mkdirSync(dir, { recursive: true });
134
+ console.log(`Created directory: ${dir}`);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Install hook files to ~/.claude/hooks/
140
+ * Uses symlinks on Unix (edits take effect immediately)
141
+ * Uses copies on Windows (symlinks require admin)
142
+ * @returns {string[]} List of installed hook paths
143
+ */
144
+ function installHooks() {
145
+ ensureDir(HOOKS_DIR);
146
+
147
+ const hooksSourceDir = path.join(__dirname, '..', 'hooks');
148
+ const installed = [];
149
+
150
+ for (const hook of HOOKS) {
151
+ const sourcePath = path.join(hooksSourceDir, hook.source);
152
+ const destPath = path.join(HOOKS_DIR, hook.dest);
153
+
154
+ if (!fs.existsSync(sourcePath)) {
155
+ console.warn(`Warning: Hook source not found: ${sourcePath}`);
156
+ continue;
157
+ }
158
+
159
+ // Remove existing file/symlink if present
160
+ try {
161
+ const stat = fs.lstatSync(destPath);
162
+ if (stat.isFile() || stat.isSymbolicLink()) {
163
+ fs.unlinkSync(destPath);
164
+ }
165
+ } catch (err) {
166
+ // File doesn't exist, that's fine
167
+ }
168
+
169
+ if (IS_WINDOWS) {
170
+ // Windows: Copy the file (symlinks require admin/dev mode)
171
+ fs.copyFileSync(sourcePath, destPath);
172
+ console.log(`Installed hook: ${hook.dest} (copied)`);
173
+ } else {
174
+ // Unix: Create symlink for live updates
175
+ fs.symlinkSync(sourcePath, destPath);
176
+ // Ensure source is executable
177
+ fs.chmodSync(sourcePath, 0o755);
178
+ console.log(`Installed hook: ${hook.dest} -> ${sourcePath}`);
179
+ }
180
+
181
+ installed.push(destPath);
182
+ }
183
+
184
+ return installed;
185
+ }
186
+
187
+ /**
188
+ * Update ~/.claude/settings.json with hook configuration
189
+ * Preserves existing settings and hooks
190
+ */
191
+ function updateSettings() {
192
+ let settings = {};
193
+
194
+ // Load existing settings if present
195
+ if (fs.existsSync(SETTINGS_FILE)) {
196
+ try {
197
+ const content = fs.readFileSync(SETTINGS_FILE, 'utf-8');
198
+ settings = JSON.parse(content);
199
+ console.log('Loaded existing settings.json');
200
+ } catch (error) {
201
+ console.warn('Warning: Could not parse existing settings.json, creating backup');
202
+ fs.copyFileSync(SETTINGS_FILE, SETTINGS_FILE + '.backup');
203
+ settings = {};
204
+ }
205
+ }
206
+
207
+ // Ensure hooks structure exists
208
+ if (!settings.hooks) {
209
+ settings.hooks = {};
210
+ }
211
+
212
+ // Add/update our hooks for each event type
213
+ for (const hook of HOOKS) {
214
+ const event = hook.event;
215
+ if (!settings.hooks[event]) {
216
+ settings.hooks[event] = [];
217
+ }
218
+
219
+ // Check if our hook already exists
220
+ const existingIndex = settings.hooks[event].findIndex(h =>
221
+ h.matcher === hook.matcher &&
222
+ h.hooks?.some(hh => hh.command?.includes('deliberate'))
223
+ );
224
+
225
+ const hookPath = path.join(HOOKS_DIR, hook.dest);
226
+ const hookConfig = {
227
+ matcher: hook.matcher,
228
+ hooks: [
229
+ {
230
+ type: 'command',
231
+ command: getHookCommand(hookPath),
232
+ timeout: hook.timeout
233
+ }
234
+ ]
235
+ };
236
+
237
+ if (existingIndex >= 0) {
238
+ // Update existing
239
+ settings.hooks[event][existingIndex] = hookConfig;
240
+ console.log(`Updated ${event} hook for ${hook.matcher}`);
241
+ } else {
242
+ // Add new
243
+ settings.hooks[event].push(hookConfig);
244
+ console.log(`Added ${event} hook for ${hook.matcher}`);
245
+ }
246
+ }
247
+
248
+ // Write settings
249
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
250
+ console.log(`Updated: ${SETTINGS_FILE}`);
251
+ }
252
+
253
+ /**
254
+ * Check if a command exists in PATH
255
+ * @param {string} cmd - Command to check
256
+ * @returns {boolean}
257
+ */
258
+ function commandExists(cmd) {
259
+ try {
260
+ const checkCmd = IS_WINDOWS ? `where ${cmd}` : `which ${cmd}`;
261
+ execSync(checkCmd, { stdio: 'ignore' });
262
+ return true;
263
+ } catch {
264
+ return false;
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Check Python version
270
+ * @returns {{ok: boolean, version: string|null}}
271
+ */
272
+ function checkPython() {
273
+ try {
274
+ const result = execSync(`${PYTHON_CMD} --version`, { encoding: 'utf-8' });
275
+ const match = result.match(/Python (\d+)\.(\d+)/);
276
+ if (match) {
277
+ const major = parseInt(match[1]);
278
+ const minor = parseInt(match[2]);
279
+ if (major >= 3 && minor >= 9) {
280
+ return { ok: true, version: result.trim() };
281
+ }
282
+ return { ok: false, version: result.trim() };
283
+ }
284
+ return { ok: false, version: null };
285
+ } catch {
286
+ return { ok: false, version: null };
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Check if Python packages are installed
292
+ * @returns {{installed: string[], missing: string[]}}
293
+ */
294
+ function checkPythonDeps() {
295
+ const installed = [];
296
+ const missing = [];
297
+
298
+ for (const pkg of PYTHON_DEPS) {
299
+ try {
300
+ // Use pip show to check if package is installed
301
+ execSync(`${PIP_CMD} show ${pkg}`, { stdio: 'ignore' });
302
+ installed.push(pkg);
303
+ } catch {
304
+ missing.push(pkg);
305
+ }
306
+ }
307
+
308
+ return { installed, missing };
309
+ }
310
+
311
+ /**
312
+ * Install missing Python packages
313
+ * @param {string[]} packages - Packages to install
314
+ * @returns {boolean} - Success
315
+ */
316
+ function installPythonDeps(packages) {
317
+ if (packages.length === 0) return true;
318
+
319
+ console.log(`Installing: ${packages.join(', ')}...`);
320
+ try {
321
+ execSync(`${PIP_CMD} install ${packages.join(' ')}`, {
322
+ stdio: 'inherit',
323
+ timeout: 300000 // 5 min timeout for large packages
324
+ });
325
+ return true;
326
+ } catch (error) {
327
+ console.error('Failed to install Python packages:', error.message);
328
+ return false;
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Check if Claude CLI is available
334
+ * @returns {boolean}
335
+ */
336
+ function checkClaudeCLI() {
337
+ return commandExists('claude');
338
+ }
339
+
340
+ /**
341
+ * Check if existing Claude OAuth credentials exist
342
+ * @returns {{exists: boolean, token: string|null}}
343
+ */
344
+ function checkExistingClaudeCredentials() {
345
+ const credentialsFile = path.join(HOME_DIR, '.claude', '.credentials.json');
346
+ try {
347
+ if (fs.existsSync(credentialsFile)) {
348
+ const content = fs.readFileSync(credentialsFile, 'utf-8');
349
+ const creds = JSON.parse(content);
350
+ // Look for OAuth token in credentials
351
+ if (creds.claudeAiOauth?.accessToken) {
352
+ return { exists: true, token: creds.claudeAiOauth.accessToken };
353
+ }
354
+ }
355
+ } catch {
356
+ // Ignore read errors
357
+ }
358
+ return { exists: false, token: null };
359
+ }
360
+
361
+ /**
362
+ * Run claude setup-token and capture the OAuth token
363
+ * @returns {Promise<{success: boolean, token: string|null, error: string|null}>}
364
+ */
365
+ async function captureClaudeOAuthToken() {
366
+ const { spawn } = await import('child_process');
367
+
368
+ return new Promise((resolve) => {
369
+ console.log('');
370
+ console.log('Opening browser for Claude authentication...');
371
+ console.log('(Complete the OAuth flow in your browser)');
372
+ console.log('');
373
+
374
+ const child = spawn('claude', ['setup-token'], {
375
+ stdio: ['inherit', 'pipe', 'inherit'],
376
+ shell: true
377
+ });
378
+
379
+ let output = '';
380
+
381
+ child.stdout.on('data', (data) => {
382
+ const text = data.toString();
383
+ output += text;
384
+ // Print output to user (they need to see the flow)
385
+ process.stdout.write(text);
386
+ });
387
+
388
+ child.on('close', (code) => {
389
+ if (code !== 0) {
390
+ resolve({ success: false, token: null, error: 'setup-token failed' });
391
+ return;
392
+ }
393
+
394
+ // Extract token from output - format: "sk-ant-oat01-..."
395
+ const tokenMatch = output.match(/sk-ant-[a-zA-Z0-9_-]+/);
396
+ if (tokenMatch) {
397
+ resolve({ success: true, token: tokenMatch[0], error: null });
398
+ } else {
399
+ resolve({ success: false, token: null, error: 'Could not find token in output' });
400
+ }
401
+ });
402
+
403
+ child.on('error', (err) => {
404
+ resolve({ success: false, token: null, error: err.message });
405
+ });
406
+ });
407
+ }
408
+
409
+ /**
410
+ * Check if Ollama is running
411
+ * @returns {boolean}
412
+ */
413
+ function isOllamaRunning() {
414
+ try {
415
+ execSync('curl -s http://localhost:11434/api/tags', { stdio: 'ignore', timeout: 2000 });
416
+ return true;
417
+ } catch {
418
+ return false;
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Simple prompt for user input (no external dependencies)
424
+ * @param {string} question - Question to ask
425
+ * @param {boolean} hidden - Hide input (for passwords/keys)
426
+ * @returns {Promise<string>}
427
+ */
428
+ async function prompt(question, hidden = false) {
429
+ const readline = await import('readline');
430
+ const rl = readline.createInterface({
431
+ input: process.stdin,
432
+ output: process.stdout
433
+ });
434
+
435
+ return new Promise((resolve) => {
436
+ if (hidden) {
437
+ process.stdout.write(question);
438
+ let input = '';
439
+ process.stdin.setRawMode(true);
440
+ process.stdin.resume();
441
+ process.stdin.on('data', (char) => {
442
+ char = char.toString();
443
+ if (char === '\n' || char === '\r') {
444
+ process.stdin.setRawMode(false);
445
+ process.stdout.write('\n');
446
+ rl.close();
447
+ resolve(input);
448
+ } else if (char === '\u0003') {
449
+ process.exit();
450
+ } else if (char === '\u007F') {
451
+ if (input.length > 0) {
452
+ input = input.slice(0, -1);
453
+ process.stdout.write('\b \b');
454
+ }
455
+ } else {
456
+ input += char;
457
+ process.stdout.write('*');
458
+ }
459
+ });
460
+ } else {
461
+ rl.question(question, (answer) => {
462
+ rl.close();
463
+ resolve(answer);
464
+ });
465
+ }
466
+ });
467
+ }
468
+
469
+ /**
470
+ * Simple menu selection
471
+ * @param {string} question - Question to ask
472
+ * @param {Array<{value: string, label: string}>} options - Options to choose from
473
+ * @returns {Promise<string>}
474
+ */
475
+ async function select(question, options) {
476
+ console.log(question);
477
+ options.forEach((opt, i) => {
478
+ console.log(` ${i + 1}) ${opt.label}`);
479
+ });
480
+
481
+ while (true) {
482
+ const answer = await prompt('Enter number: ');
483
+ const num = parseInt(answer, 10);
484
+ if (num >= 1 && num <= options.length) {
485
+ return options[num - 1].value;
486
+ }
487
+ console.log('Invalid selection, try again.');
488
+ }
489
+ }
490
+
491
+ /**
492
+ * Configure LLM provider interactively
493
+ * @returns {Promise<void>}
494
+ */
495
+ async function configureLLM() {
496
+ console.log('');
497
+ console.log('Configure Deliberate LLM Explainer');
498
+ console.log('----------------------------------');
499
+ console.log('The LLM provides human-readable explanations for commands.');
500
+ console.log('');
501
+
502
+ // Build options based on what's available
503
+ const options = [];
504
+
505
+ // Check for existing Claude credentials or Claude CLI
506
+ const existingCreds = checkExistingClaudeCredentials();
507
+ if (existingCreds.exists) {
508
+ options.push({
509
+ value: 'claude-subscription-existing',
510
+ label: 'Claude Pro/Max Subscription [credentials found] (recommended)'
511
+ });
512
+ } else if (checkClaudeCLI()) {
513
+ options.push({
514
+ value: 'claude-subscription',
515
+ label: 'Claude Pro/Max Subscription (recommended)'
516
+ });
517
+ }
518
+
519
+ // Always offer direct API
520
+ options.push({
521
+ value: 'anthropic',
522
+ label: 'Anthropic API Key (pay-per-token)'
523
+ });
524
+
525
+ // Check for Ollama
526
+ if (isOllamaRunning()) {
527
+ options.push({
528
+ value: 'ollama',
529
+ label: 'Ollama (local) [running]'
530
+ });
531
+ }
532
+
533
+ options.push({
534
+ value: 'skip',
535
+ label: 'Skip for now (classifier will still work, no explanations)'
536
+ });
537
+
538
+ const provider = await select('How do you want to authenticate?', options);
539
+
540
+ if (provider === 'skip') {
541
+ console.log('');
542
+ console.log('Skipped LLM configuration.');
543
+ console.log('The classifier will still work, but without detailed explanations.');
544
+ console.log('You can configure it later by editing:');
545
+ console.log(` ${getConfigFile()}`);
546
+ return;
547
+ }
548
+
549
+ let apiKey = null;
550
+
551
+ if (provider === 'claude-subscription-existing') {
552
+ // Use existing credentials
553
+ apiKey = existingCreds.token;
554
+ console.log('');
555
+ console.log('Using existing Claude credentials.');
556
+ } else if (provider === 'claude-subscription') {
557
+ // Run claude setup-token to get new credentials
558
+ const result = await captureClaudeOAuthToken();
559
+ if (!result.success) {
560
+ console.log('');
561
+ console.log(`Error: ${result.error}`);
562
+ console.log('');
563
+ console.log('If running inside Claude Code or a non-interactive terminal,');
564
+ console.log('first run this in a separate terminal: claude setup-token');
565
+ console.log('Then re-run: deliberate install');
566
+ return;
567
+ }
568
+ apiKey = result.token;
569
+ } else if (provider === 'anthropic') {
570
+ console.log('');
571
+ console.log('Get your API key from: https://console.anthropic.com/settings/keys');
572
+ apiKey = await prompt('Enter your Anthropic API key: ', true);
573
+
574
+ if (!apiKey || !apiKey.startsWith('sk-ant-')) {
575
+ console.log('');
576
+ console.log('Warning: API key should start with "sk-ant-"');
577
+ const confirm = await prompt('Continue anyway? (y/n): ');
578
+ if (confirm.toLowerCase() !== 'y') {
579
+ console.log('Aborted.');
580
+ return;
581
+ }
582
+ }
583
+ }
584
+
585
+ // Normalize provider name for storage
586
+ const providerToSave = provider.startsWith('claude-subscription') ? 'claude-subscription' : provider;
587
+
588
+ // Save configuration
589
+ try {
590
+ setLLMProvider(providerToSave, { apiKey });
591
+ console.log('');
592
+ console.log(`Configured: ${LLM_PROVIDERS[providerToSave].name}`);
593
+ console.log(`Config saved to: ${getConfigFile()}`);
594
+
595
+ // Set restrictive permissions on config file (contains API key/token)
596
+ if (apiKey && !IS_WINDOWS) {
597
+ try {
598
+ fs.chmodSync(getConfigFile(), 0o600);
599
+ console.log('(File permissions set to 600 for security)');
600
+ } catch (err) {
601
+ // Ignore chmod errors
602
+ }
603
+ } else if (apiKey && IS_WINDOWS) {
604
+ console.log('Note: On Windows, manually restrict access to:');
605
+ console.log(` ${getConfigFile()}`);
606
+ }
607
+ } catch (error) {
608
+ console.error('Error saving configuration:', error.message);
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Check if Deliberate plugin is already loaded
614
+ * @returns {boolean}
615
+ */
616
+ function isPluginInstalled() {
617
+ try {
618
+ const settingsFile = path.join(HOME_DIR, '.claude', 'settings.json');
619
+ if (!fs.existsSync(settingsFile)) return false;
620
+
621
+ const content = fs.readFileSync(settingsFile, 'utf-8');
622
+ const settings = JSON.parse(content);
623
+
624
+ // Check if deliberate plugin is enabled
625
+ if (settings.enabledPlugins && settings.enabledPlugins['deliberate']) {
626
+ return true;
627
+ }
628
+
629
+ return false;
630
+ } catch {
631
+ return false;
632
+ }
633
+ }
634
+
635
+ /**
636
+ * Main installation function
637
+ */
638
+ export async function install() {
639
+ console.log('');
640
+ console.log('===========================================');
641
+ console.log(' Deliberate - Installation');
642
+ console.log('===========================================');
643
+ console.log('');
644
+
645
+ // Check for plugin installation conflict
646
+ if (isPluginInstalled()) {
647
+ console.error('ERROR: Deliberate is already installed as a Claude Code plugin.');
648
+ console.error('');
649
+ console.error('You cannot have both the npm and plugin versions installed.');
650
+ console.error('They will conflict and cause commands to be analyzed twice.');
651
+ console.error('');
652
+ console.error('Options:');
653
+ console.error(' 1. Uninstall the plugin: /plugin uninstall deliberate');
654
+ console.error(' 2. OR: Use the plugin version and skip npm installation');
655
+ console.error('');
656
+ console.error('Recommended: Use the plugin version for better integration.');
657
+ process.exit(1);
658
+ }
659
+
660
+ // Check Python
661
+ console.log('Checking Python...');
662
+ const python = checkPython();
663
+ if (!python.ok) {
664
+ if (python.version) {
665
+ console.error(`Error: ${python.version} found, but Python 3.9+ is required`);
666
+ } else {
667
+ console.error(`Error: Python not found. Install Python 3.9+ first.`);
668
+ }
669
+ process.exit(1);
670
+ }
671
+ console.log(` ${python.version}`);
672
+
673
+ // Check Python dependencies
674
+ console.log('');
675
+ console.log('Checking Python dependencies...');
676
+ const deps = checkPythonDeps();
677
+
678
+ if (deps.missing.length > 0) {
679
+ console.log(` Missing: ${deps.missing.join(', ')}`);
680
+ console.log('');
681
+ console.log('Installing Python dependencies...');
682
+ const success = installPythonDeps(deps.missing);
683
+ if (!success) {
684
+ console.error('');
685
+ console.error('Failed to install dependencies. Try manually:');
686
+ console.error(` ${PIP_CMD} install ${deps.missing.join(' ')}`);
687
+ process.exit(1);
688
+ }
689
+ console.log(' Done!');
690
+ } else {
691
+ console.log(' All dependencies installed');
692
+ }
693
+
694
+ // Download ML models
695
+ console.log('');
696
+ console.log('Checking ML models...');
697
+ const modelsOk = await downloadModels();
698
+ if (!modelsOk) {
699
+ console.warn('Warning: Models not available. Classifier will not work.');
700
+ console.warn('LLM explanations will still work if configured.');
701
+ }
702
+
703
+ // Install hooks
704
+ console.log('');
705
+ console.log('Installing hooks...');
706
+ const installed = installHooks();
707
+
708
+ if (installed.length === 0) {
709
+ console.error('Error: No hooks were installed');
710
+ process.exit(1);
711
+ }
712
+
713
+ // Update settings
714
+ console.log('');
715
+ console.log('Updating Claude Code settings...');
716
+ updateSettings();
717
+
718
+ // Configure LLM if not already configured
719
+ if (!isLLMConfigured()) {
720
+ await configureLLM();
721
+ } else {
722
+ console.log('');
723
+ console.log('LLM already configured. To reconfigure, edit:');
724
+ console.log(` ${getConfigFile()}`);
725
+ }
726
+
727
+ // Success message
728
+ console.log('');
729
+ console.log('===========================================');
730
+ console.log(' Installation Complete!');
731
+ console.log('===========================================');
732
+ console.log('');
733
+ console.log('Installed hooks:');
734
+ for (const hookPath of installed) {
735
+ console.log(` - ${hookPath}`);
736
+ }
737
+ console.log('');
738
+ console.log('Next steps:');
739
+ console.log(' 1. Restart Claude Code to load the new hooks');
740
+ console.log('');
741
+ console.log(' 2. (Optional) Start the classifier server for faster ML detection:');
742
+ console.log(' deliberate serve');
743
+ console.log('');
744
+ console.log(' 3. Test classification:');
745
+ console.log(' deliberate classify "rm -rf /"');
746
+ console.log('');
747
+ }
748
+
749
+ // Allow running directly
750
+ if (process.argv[1] && process.argv[1].endsWith('install.js')) {
751
+ install();
752
+ }
753
+
754
+ export default { install };