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/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 };
@@ -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 };