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/server.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classifier HTTP Server
|
|
3
|
+
* Provides a REST API for the multi-layer classifier.
|
|
4
|
+
* Hooks call this server instead of running classification inline.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import express from 'express';
|
|
8
|
+
import { classify, quickCheck, getStatus, preloadModel } from './classifier/index.js';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_PORT = 8765;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create and configure the Express app
|
|
14
|
+
* @returns {express.Application}
|
|
15
|
+
*/
|
|
16
|
+
function createApp() {
|
|
17
|
+
const app = express();
|
|
18
|
+
app.use(express.json({ limit: '1mb' }));
|
|
19
|
+
|
|
20
|
+
// Health check
|
|
21
|
+
app.get('/health', (req, res) => {
|
|
22
|
+
const status = getStatus();
|
|
23
|
+
res.json({
|
|
24
|
+
status: 'ok',
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
classifier: status
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Status endpoint
|
|
31
|
+
app.get('/status', (req, res) => {
|
|
32
|
+
res.json(getStatus());
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Quick pattern-only check (synchronous, fast)
|
|
36
|
+
app.post('/quick', (req, res) => {
|
|
37
|
+
try {
|
|
38
|
+
const { input, type = 'command' } = req.body;
|
|
39
|
+
|
|
40
|
+
if (!input) {
|
|
41
|
+
return res.status(400).json({ error: 'Missing required field: input' });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = quickCheck(input, type);
|
|
45
|
+
res.json(result);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('[Server] Quick check error:', error);
|
|
48
|
+
res.status(500).json({ error: error.message });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Full classification (async, uses model)
|
|
53
|
+
app.post('/classify', async (req, res) => {
|
|
54
|
+
try {
|
|
55
|
+
const { input, type = 'command', context = {} } = req.body;
|
|
56
|
+
|
|
57
|
+
if (!input) {
|
|
58
|
+
return res.status(400).json({ error: 'Missing required field: input' });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result = await classify(input, type, context);
|
|
62
|
+
res.json(result);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('[Server] Classification error:', error);
|
|
65
|
+
res.status(500).json({ error: error.message });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Classify a bash command
|
|
70
|
+
app.post('/classify/command', async (req, res) => {
|
|
71
|
+
try {
|
|
72
|
+
const { command } = req.body;
|
|
73
|
+
|
|
74
|
+
if (!command) {
|
|
75
|
+
return res.status(400).json({ error: 'Missing required field: command' });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const result = await classify(command, 'command');
|
|
79
|
+
res.json(result);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('[Server] Command classification error:', error);
|
|
82
|
+
res.status(500).json({ error: error.message });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Classify a file write
|
|
87
|
+
app.post('/classify/write', async (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
const { filePath, content } = req.body;
|
|
90
|
+
|
|
91
|
+
if (!filePath) {
|
|
92
|
+
return res.status(400).json({ error: 'Missing required field: filePath' });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check file path first
|
|
96
|
+
const pathResult = await classify(filePath, 'filepath');
|
|
97
|
+
if (pathResult.risk === 'DANGEROUS') {
|
|
98
|
+
return res.json(pathResult);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// If content provided, check it too
|
|
102
|
+
if (content) {
|
|
103
|
+
const contentResult = await classify(content, 'content', { filePath });
|
|
104
|
+
|
|
105
|
+
// Return the higher risk level
|
|
106
|
+
if (contentResult.risk === 'DANGEROUS' ||
|
|
107
|
+
(contentResult.risk === 'MODERATE' && pathResult.risk === 'SAFE')) {
|
|
108
|
+
return res.json({
|
|
109
|
+
...contentResult,
|
|
110
|
+
pathCheck: pathResult
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
res.json(pathResult);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('[Server] Write classification error:', error);
|
|
118
|
+
res.status(500).json({ error: error.message });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Classify an edit
|
|
123
|
+
app.post('/classify/edit', async (req, res) => {
|
|
124
|
+
try {
|
|
125
|
+
const { filePath, oldString, newString } = req.body;
|
|
126
|
+
|
|
127
|
+
if (!filePath) {
|
|
128
|
+
return res.status(400).json({ error: 'Missing required field: filePath' });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check file path
|
|
132
|
+
const pathResult = await classify(filePath, 'filepath');
|
|
133
|
+
if (pathResult.risk === 'DANGEROUS') {
|
|
134
|
+
return res.json(pathResult);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check the new content being added
|
|
138
|
+
if (newString) {
|
|
139
|
+
const contentResult = await classify(newString, 'content', { filePath });
|
|
140
|
+
|
|
141
|
+
if (contentResult.risk === 'DANGEROUS' ||
|
|
142
|
+
(contentResult.risk === 'MODERATE' && pathResult.risk === 'SAFE')) {
|
|
143
|
+
return res.json({
|
|
144
|
+
...contentResult,
|
|
145
|
+
pathCheck: pathResult,
|
|
146
|
+
editContext: {
|
|
147
|
+
removing: oldString?.length || 0,
|
|
148
|
+
adding: newString?.length || 0
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
res.json({
|
|
155
|
+
...pathResult,
|
|
156
|
+
editContext: {
|
|
157
|
+
removing: oldString?.length || 0,
|
|
158
|
+
adding: newString?.length || 0
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('[Server] Edit classification error:', error);
|
|
163
|
+
res.status(500).json({ error: error.message });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Preload model endpoint
|
|
168
|
+
app.post('/preload', async (req, res) => {
|
|
169
|
+
try {
|
|
170
|
+
console.log('[Server] Preloading model...');
|
|
171
|
+
await preloadModel();
|
|
172
|
+
res.json({ status: 'ok', message: 'Model preloaded successfully' });
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error('[Server] Preload error:', error);
|
|
175
|
+
res.status(500).json({ error: error.message });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Error handler
|
|
180
|
+
app.use((err, req, res, next) => {
|
|
181
|
+
console.error('[Server] Unhandled error:', err);
|
|
182
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return app;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Start the classifier server
|
|
190
|
+
* @param {number} port - Port to listen on
|
|
191
|
+
* @param {Object} options - Server options
|
|
192
|
+
* @param {boolean} options.preloadModel - Preload ML model on startup
|
|
193
|
+
* @returns {Promise<http.Server>}
|
|
194
|
+
*/
|
|
195
|
+
export async function startServer(port = DEFAULT_PORT, options = {}) {
|
|
196
|
+
const app = createApp();
|
|
197
|
+
|
|
198
|
+
// Optionally preload model for faster first request
|
|
199
|
+
if (options.preloadModel) {
|
|
200
|
+
console.log('[Server] Preloading ML model...');
|
|
201
|
+
try {
|
|
202
|
+
await preloadModel();
|
|
203
|
+
console.log('[Server] Model preloaded successfully');
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.warn('[Server] Model preload failed (will load on first request):', error.message);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
const server = app.listen(port, () => {
|
|
211
|
+
console.log(`[Server] Deliberate classifier listening on http://localhost:${port}`);
|
|
212
|
+
console.log('[Server] Endpoints:');
|
|
213
|
+
console.log(' GET /health - Health check');
|
|
214
|
+
console.log(' GET /status - Classifier status');
|
|
215
|
+
console.log(' POST /quick - Quick pattern check');
|
|
216
|
+
console.log(' POST /classify - Full classification');
|
|
217
|
+
console.log(' POST /classify/command - Classify bash command');
|
|
218
|
+
console.log(' POST /classify/write - Classify file write');
|
|
219
|
+
console.log(' POST /classify/edit - Classify file edit');
|
|
220
|
+
console.log(' POST /preload - Preload ML model');
|
|
221
|
+
resolve(server);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
server.on('error', (error) => {
|
|
225
|
+
if (error.code === 'EADDRINUSE') {
|
|
226
|
+
console.error(`[Server] Port ${port} is already in use`);
|
|
227
|
+
}
|
|
228
|
+
reject(error);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Allow running directly
|
|
234
|
+
if (process.argv[1] && process.argv[1].endsWith('server.js')) {
|
|
235
|
+
const port = parseInt(process.env.PORT || DEFAULT_PORT);
|
|
236
|
+
startServer(port, { preloadModel: process.env.PRELOAD_MODEL === 'true' });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export default { startServer, createApp };
|
package/src/uninstall.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uninstaller - Removes Claude Code hooks and optionally removes config
|
|
3
|
+
* Handles:
|
|
4
|
+
* - Removing hooks from ~/.claude/hooks/
|
|
5
|
+
* - Removing hook entries from ~/.claude/settings.json
|
|
6
|
+
* - Optionally removing ~/.deliberate/config.json
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
|
|
13
|
+
const HOME_DIR = os.homedir();
|
|
14
|
+
const CLAUDE_DIR = path.join(HOME_DIR, '.claude');
|
|
15
|
+
const HOOKS_DIR = path.join(CLAUDE_DIR, 'hooks');
|
|
16
|
+
const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
|
|
17
|
+
const CONFIG_FILE = path.join(HOME_DIR, '.deliberate', 'config.json');
|
|
18
|
+
|
|
19
|
+
// Hook files to remove
|
|
20
|
+
const HOOKS_TO_REMOVE = [
|
|
21
|
+
'deliberate-explain-command.py',
|
|
22
|
+
'deliberate-explain-changes.py'
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Remove hook files from ~/.claude/hooks/
|
|
27
|
+
*/
|
|
28
|
+
function removeHooks() {
|
|
29
|
+
console.log('Removing hook files...');
|
|
30
|
+
let removed = 0;
|
|
31
|
+
|
|
32
|
+
for (const hookFile of HOOKS_TO_REMOVE) {
|
|
33
|
+
const hookPath = path.join(HOOKS_DIR, hookFile);
|
|
34
|
+
if (fs.existsSync(hookPath)) {
|
|
35
|
+
fs.unlinkSync(hookPath);
|
|
36
|
+
console.log(` Removed: ${hookPath}`);
|
|
37
|
+
removed++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (removed === 0) {
|
|
42
|
+
console.log(' No hook files found to remove');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return removed;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Remove Deliberate hooks from ~/.claude/settings.json
|
|
50
|
+
*/
|
|
51
|
+
function removeFromSettings() {
|
|
52
|
+
console.log('Updating Claude Code settings...');
|
|
53
|
+
|
|
54
|
+
if (!fs.existsSync(SETTINGS_FILE)) {
|
|
55
|
+
console.log(' Settings file not found, skipping');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const content = fs.readFileSync(SETTINGS_FILE, 'utf-8');
|
|
61
|
+
const settings = JSON.parse(content);
|
|
62
|
+
|
|
63
|
+
// Remove Deliberate hooks from PreToolUse and PostToolUse
|
|
64
|
+
let modified = false;
|
|
65
|
+
|
|
66
|
+
if (settings.hooks) {
|
|
67
|
+
// Filter out Deliberate hooks from PreToolUse
|
|
68
|
+
if (settings.hooks.PreToolUse) {
|
|
69
|
+
const filtered = settings.hooks.PreToolUse.map(matcher => {
|
|
70
|
+
if (!matcher.hooks) return matcher;
|
|
71
|
+
|
|
72
|
+
const filteredHooks = matcher.hooks.filter(hook => {
|
|
73
|
+
const isDeliberate = hook.command && (
|
|
74
|
+
hook.command.includes('deliberate-explain-command') ||
|
|
75
|
+
hook.command.includes('deliberate-explain-changes')
|
|
76
|
+
);
|
|
77
|
+
if (isDeliberate) modified = true;
|
|
78
|
+
return !isDeliberate;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return { ...matcher, hooks: filteredHooks };
|
|
82
|
+
}).filter(matcher => matcher.hooks && matcher.hooks.length > 0);
|
|
83
|
+
|
|
84
|
+
settings.hooks.PreToolUse = filtered;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Filter out Deliberate hooks from PostToolUse
|
|
88
|
+
if (settings.hooks.PostToolUse) {
|
|
89
|
+
const filtered = settings.hooks.PostToolUse.map(matcher => {
|
|
90
|
+
if (!matcher.hooks) return matcher;
|
|
91
|
+
|
|
92
|
+
const filteredHooks = matcher.hooks.filter(hook => {
|
|
93
|
+
const isDeliberate = hook.command && (
|
|
94
|
+
hook.command.includes('deliberate-explain-command') ||
|
|
95
|
+
hook.command.includes('deliberate-explain-changes')
|
|
96
|
+
);
|
|
97
|
+
if (isDeliberate) modified = true;
|
|
98
|
+
return !isDeliberate;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return { ...matcher, hooks: filteredHooks };
|
|
102
|
+
}).filter(matcher => matcher.hooks && matcher.hooks.length > 0);
|
|
103
|
+
|
|
104
|
+
settings.hooks.PostToolUse = filtered;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (modified) {
|
|
109
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
|
110
|
+
console.log(' Settings updated');
|
|
111
|
+
} else {
|
|
112
|
+
console.log(' No Deliberate hooks found in settings');
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error(` Error updating settings: ${error.message}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Prompt for user input (simple version)
|
|
121
|
+
* @param {string} question
|
|
122
|
+
* @returns {Promise<string>}
|
|
123
|
+
*/
|
|
124
|
+
async function prompt(question) {
|
|
125
|
+
const readline = await import('readline');
|
|
126
|
+
const rl = readline.createInterface({
|
|
127
|
+
input: process.stdin,
|
|
128
|
+
output: process.stdout
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
rl.question(question, (answer) => {
|
|
133
|
+
rl.close();
|
|
134
|
+
resolve(answer);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Main uninstall function
|
|
141
|
+
*/
|
|
142
|
+
export async function uninstall() {
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log('===========================================');
|
|
145
|
+
console.log(' Deliberate - Uninstallation');
|
|
146
|
+
console.log('===========================================');
|
|
147
|
+
console.log('');
|
|
148
|
+
|
|
149
|
+
// Remove hooks
|
|
150
|
+
const removed = removeHooks();
|
|
151
|
+
|
|
152
|
+
// Update settings
|
|
153
|
+
console.log('');
|
|
154
|
+
removeFromSettings();
|
|
155
|
+
|
|
156
|
+
// Ask about config
|
|
157
|
+
console.log('');
|
|
158
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
159
|
+
const answer = await prompt('Remove ~/.deliberate/config.json? (y/n): ');
|
|
160
|
+
if (answer.toLowerCase() === 'y') {
|
|
161
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
162
|
+
console.log(' Removed config file');
|
|
163
|
+
|
|
164
|
+
// Try to remove directory if empty
|
|
165
|
+
const configDir = path.dirname(CONFIG_FILE);
|
|
166
|
+
try {
|
|
167
|
+
const files = fs.readdirSync(configDir);
|
|
168
|
+
if (files.length === 0) {
|
|
169
|
+
fs.rmdirSync(configDir);
|
|
170
|
+
console.log(' Removed empty ~/.deliberate directory');
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
// Ignore errors
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
console.log(' Kept config file');
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
console.log('Config file not found, nothing to remove');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Success message
|
|
183
|
+
console.log('');
|
|
184
|
+
console.log('===========================================');
|
|
185
|
+
console.log(' Uninstallation Complete!');
|
|
186
|
+
console.log('===========================================');
|
|
187
|
+
console.log('');
|
|
188
|
+
console.log('Next step:');
|
|
189
|
+
console.log(' Restart Claude Code to unload the hooks');
|
|
190
|
+
console.log('');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Allow running directly
|
|
194
|
+
if (process.argv[1] && process.argv[1].endsWith('uninstall.js')) {
|
|
195
|
+
uninstall();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export default { uninstall };
|