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/LICENSE +11 -0
- package/README.md +180 -0
- package/bin/cli.js +113 -0
- package/hooks/__pycache__/deliberate-commands.cpython-312.pyc +0 -0
- package/hooks/deliberate-changes.py +606 -0
- package/hooks/deliberate-commands-post.py +126 -0
- package/hooks/deliberate-commands.py +1742 -0
- package/hooks/hooks.json +29 -0
- package/hooks/setup-check.py +67 -0
- package/hooks/test_skip_commands.py +293 -0
- package/package.json +51 -0
- package/src/classifier/classify_command.py +346 -0
- package/src/classifier/embed_command.py +56 -0
- package/src/classifier/index.js +324 -0
- package/src/classifier/model-classifier.js +531 -0
- package/src/classifier/pattern-matcher.js +230 -0
- package/src/config.js +207 -0
- package/src/index.js +23 -0
- package/src/install.js +754 -0
- package/src/server.js +239 -0
- package/src/uninstall.js +198 -0
- package/training/build_classifier.py +325 -0
- package/training/expanded-command-safety.jsonl +712 -0
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 };
|