deliberate 1.0.3 → 1.0.4
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 +12 -3
- package/bin/cli.js +50 -16
- package/hooks/__pycache__/deliberate-changes.cpython-312.pyc +0 -0
- package/hooks/__pycache__/deliberate-commands.cpython-312.pyc +0 -0
- package/hooks/deliberate-commands.py +4 -0
- package/opencode/deliberate-changes-plugin.js +170 -0
- package/opencode/deliberate-plugin.js +174 -0
- package/package.json +2 -1
- package/src/install.js +134 -2
- package/src/uninstall.js +62 -7
package/README.md
CHANGED
|
@@ -111,7 +111,7 @@ npm install -g deliberate
|
|
|
111
111
|
deliberate install
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
The installer walks you through LLM provider setup (Claude, Anthropic API, or Ollama).
|
|
114
|
+
The installer sets up Claude Code hooks and the OpenCode plugin, then walks you through LLM provider setup (Claude, Anthropic API, or Ollama). If OpenCode is installed, it registers two `file://` plugins (commands + changes) in `~/.config/opencode/opencode.json`.
|
|
115
115
|
|
|
116
116
|
### Dependencies
|
|
117
117
|
|
|
@@ -126,7 +126,7 @@ The CmdCaliper embedding model (~419MB) downloads on first use.
|
|
|
126
126
|
## CLI
|
|
127
127
|
|
|
128
128
|
```bash
|
|
129
|
-
deliberate install # Install hooks, configure LLM
|
|
129
|
+
deliberate install # Install Claude Code hooks + OpenCode plugin, configure LLM
|
|
130
130
|
deliberate status # Check installation
|
|
131
131
|
deliberate classify "rm -rf /" # Test classification → DANGEROUS
|
|
132
132
|
deliberate serve # Start classifier server (faster)
|
|
@@ -159,10 +159,19 @@ python training/build_classifier.py --model base # Retrain
|
|
|
159
159
|
|
|
160
160
|
- Node.js 18+
|
|
161
161
|
- Python 3.9+
|
|
162
|
-
- Claude Code
|
|
162
|
+
- Claude Code or OpenCode 1.0+
|
|
163
163
|
|
|
164
164
|
Works on macOS, Linux, and Windows.
|
|
165
165
|
|
|
166
|
+
## OpenCode
|
|
167
|
+
|
|
168
|
+
OpenCode support is installed by `deliberate install`. It registers two plugins in `~/.config/opencode/opencode.json`:
|
|
169
|
+
|
|
170
|
+
- `file://~/.config/opencode/plugins/deliberate.js` (command safety)
|
|
171
|
+
- `file://~/.config/opencode/plugins/deliberate-changes.js` (edit/change summaries)
|
|
172
|
+
|
|
173
|
+
After install, restart OpenCode to load the plugins. For edit/change summaries, OpenCode must be configured to allow edit tools (write/edit/patch/multiedit) so the plugin can read tool metadata. The plugins call the same Deliberate hook scripts, so LLM explanations behave the same as Claude Code.
|
|
174
|
+
|
|
166
175
|
## Uninstall
|
|
167
176
|
|
|
168
177
|
```bash
|
package/bin/cli.js
CHANGED
|
@@ -21,7 +21,7 @@ program
|
|
|
21
21
|
|
|
22
22
|
program
|
|
23
23
|
.command('install')
|
|
24
|
-
.description('Install hooks and configure Claude Code integration')
|
|
24
|
+
.description('Install hooks and configure Claude Code/OpenCode integration')
|
|
25
25
|
.action(async () => {
|
|
26
26
|
await install();
|
|
27
27
|
});
|
|
@@ -49,7 +49,7 @@ program
|
|
|
49
49
|
.action(async () => {
|
|
50
50
|
console.log('Deliberate Status\n');
|
|
51
51
|
|
|
52
|
-
// Check hooks installation
|
|
52
|
+
// Check Claude Code hooks installation
|
|
53
53
|
const claudeSettingsPath = join(homedir(), '.claude', 'settings.json');
|
|
54
54
|
let hooksInstalled = false;
|
|
55
55
|
|
|
@@ -60,21 +60,20 @@ program
|
|
|
60
60
|
const preToolUse = hooks.PreToolUse || [];
|
|
61
61
|
const postToolUse = hooks.PostToolUse || [];
|
|
62
62
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
h.command && h.command.includes('explain-changes')
|
|
68
|
-
);
|
|
63
|
+
const hasHookCommand = (entries, needle) =>
|
|
64
|
+
entries.some(entry => Array.isArray(entry.hooks) && entry.hooks.some(hook =>
|
|
65
|
+
hook.command && hook.command.includes(needle)
|
|
66
|
+
));
|
|
69
67
|
|
|
70
|
-
|
|
68
|
+
const hasCommandHook = hasHookCommand(preToolUse, 'deliberate-commands');
|
|
69
|
+
const hasChangesHook = hasHookCommand(postToolUse, 'deliberate-changes');
|
|
70
|
+
const hasCommandPostHook = hasHookCommand(postToolUse, 'deliberate-commands-post');
|
|
71
|
+
|
|
72
|
+
if (hasCommandHook && hasChangesHook && hasCommandPostHook) {
|
|
71
73
|
console.log('Hooks: ✅ Installed (PreToolUse + PostToolUse)');
|
|
72
74
|
hooksInstalled = true;
|
|
73
|
-
} else if (hasCommandHook) {
|
|
74
|
-
console.log('Hooks: ⚠️ Partial (
|
|
75
|
-
hooksInstalled = true;
|
|
76
|
-
} else if (hasChangesHook) {
|
|
77
|
-
console.log('Hooks: ⚠️ Partial (PostToolUse only)');
|
|
75
|
+
} else if (hasCommandHook || hasChangesHook || hasCommandPostHook) {
|
|
76
|
+
console.log('Hooks: ⚠️ Partial (missing some hooks)');
|
|
78
77
|
hooksInstalled = true;
|
|
79
78
|
} else {
|
|
80
79
|
console.log('Hooks: ❌ Not installed');
|
|
@@ -86,6 +85,41 @@ program
|
|
|
86
85
|
console.log('Hooks: ❌ Claude settings not found');
|
|
87
86
|
}
|
|
88
87
|
|
|
88
|
+
// Check OpenCode plugin installation
|
|
89
|
+
const openCodeConfigDir = join(homedir(), '.config', 'opencode');
|
|
90
|
+
const openCodeConfigPaths = [join(openCodeConfigDir, 'opencode.json'), join(openCodeConfigDir, 'opencode.jsonc')];
|
|
91
|
+
const openCodePluginPaths = [
|
|
92
|
+
join(openCodeConfigDir, 'plugins', 'deliberate.js'),
|
|
93
|
+
join(openCodeConfigDir, 'plugins', 'deliberate-changes.js')
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
let openCodeInstalled = false;
|
|
97
|
+
const configPath = openCodeConfigPaths.find(path => existsSync(path));
|
|
98
|
+
|
|
99
|
+
if (configPath) {
|
|
100
|
+
try {
|
|
101
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
102
|
+
const plugins = Array.isArray(config.plugin) ? config.plugin.map(String) : [];
|
|
103
|
+
const commandPlugin = plugins.some(entry => entry.includes('deliberate.js'));
|
|
104
|
+
const changesPlugin = plugins.some(entry => entry.includes('deliberate-changes.js'));
|
|
105
|
+
const filesPresent = openCodePluginPaths.every(path => existsSync(path));
|
|
106
|
+
|
|
107
|
+
if (commandPlugin && changesPlugin && filesPresent) {
|
|
108
|
+
console.log('OpenCode: ✅ Installed (commands + changes)');
|
|
109
|
+
openCodeInstalled = true;
|
|
110
|
+
} else if (commandPlugin || changesPlugin || filesPresent) {
|
|
111
|
+
console.log('OpenCode: ⚠️ Partial install');
|
|
112
|
+
openCodeInstalled = true;
|
|
113
|
+
} else {
|
|
114
|
+
console.log('OpenCode: ❌ Not installed');
|
|
115
|
+
}
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.log('OpenCode: ❌ Error reading opencode.json');
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
console.log('OpenCode: ❌ Config not found');
|
|
121
|
+
}
|
|
122
|
+
|
|
89
123
|
// Check classifier status
|
|
90
124
|
const classifierStatus = getStatus();
|
|
91
125
|
|
|
@@ -103,8 +137,8 @@ program
|
|
|
103
137
|
|
|
104
138
|
// Overall status
|
|
105
139
|
console.log('');
|
|
106
|
-
if (hooksInstalled) {
|
|
107
|
-
console.log('Status: Ready to protect your
|
|
140
|
+
if (hooksInstalled || openCodeInstalled) {
|
|
141
|
+
console.log('Status: Ready to protect your agent sessions');
|
|
108
142
|
} else {
|
|
109
143
|
console.log('Status: Run "deliberate install" to set up hooks');
|
|
110
144
|
}
|
|
Binary file
|
|
Binary file
|
|
@@ -27,6 +27,7 @@ from pathlib import Path
|
|
|
27
27
|
|
|
28
28
|
# Configuration
|
|
29
29
|
CLASSIFIER_URL = "http://localhost:8765/classify/command"
|
|
30
|
+
LLM_MODE = os.environ.get("DELIBERATE_LLM_MODE")
|
|
30
31
|
|
|
31
32
|
# Support both plugin mode (CLAUDE_PLUGIN_ROOT) and npm install mode (~/.deliberate/)
|
|
32
33
|
# Plugin mode: config in plugin directory
|
|
@@ -1057,6 +1058,9 @@ def get_token_from_keychain():
|
|
|
1057
1058
|
|
|
1058
1059
|
def load_llm_config() -> dict | None:
|
|
1059
1060
|
"""Load LLM configuration from config file or keychain."""
|
|
1061
|
+
if LLM_MODE == "manual":
|
|
1062
|
+
return None
|
|
1063
|
+
|
|
1060
1064
|
llm = _load_config().get("llm", {})
|
|
1061
1065
|
provider = llm.get("provider")
|
|
1062
1066
|
if not provider:
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
const PYTHON_CMD = process.platform === "win32" ? "python" : "python3";
|
|
12
|
+
const HOME_DIR = os.homedir();
|
|
13
|
+
const HOOK_SCRIPT_LOCAL = path.join(__dirname, "..", "hooks", "deliberate-changes.py");
|
|
14
|
+
const HOOK_SCRIPT_GLOBAL = path.join(HOME_DIR, ".claude", "hooks", "deliberate-changes.py");
|
|
15
|
+
const HOOK_SCRIPT = fs.existsSync(HOOK_SCRIPT_LOCAL) ? HOOK_SCRIPT_LOCAL : HOOK_SCRIPT_GLOBAL;
|
|
16
|
+
const TIMEOUT_MS = 30000;
|
|
17
|
+
|
|
18
|
+
function stripAnsi(input) {
|
|
19
|
+
return input.replace(/\u001b\[[0-9;]*m/g, "");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function runChangeHook({ toolName, toolInput, sessionID }) {
|
|
23
|
+
if (!fs.existsSync(HOOK_SCRIPT)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const payload = {
|
|
28
|
+
tool_name: toolName,
|
|
29
|
+
tool_input: toolInput,
|
|
30
|
+
session_id: sessionID
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const child = spawn(PYTHON_CMD, [HOOK_SCRIPT], {
|
|
35
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
36
|
+
env: {
|
|
37
|
+
...process.env
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
let stdout = "";
|
|
42
|
+
let stderr = "";
|
|
43
|
+
let finished = false;
|
|
44
|
+
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
if (!finished) {
|
|
47
|
+
finished = true;
|
|
48
|
+
child.kill("SIGKILL");
|
|
49
|
+
resolve({ code: 124, stdout: "", stderr: "Deliberate hook timed out" });
|
|
50
|
+
}
|
|
51
|
+
}, TIMEOUT_MS);
|
|
52
|
+
|
|
53
|
+
child.stdout.on("data", (chunk) => {
|
|
54
|
+
stdout += chunk.toString();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
child.stderr.on("data", (chunk) => {
|
|
58
|
+
stderr += chunk.toString();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
child.on("close", (code) => {
|
|
62
|
+
if (finished) return;
|
|
63
|
+
finished = true;
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
resolve({ code: code ?? 0, stdout, stderr });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
child.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
69
|
+
child.stdin.end();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseHookOutput(stdout) {
|
|
74
|
+
if (!stdout) return null;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(stdout.trim());
|
|
78
|
+
const hookOutput = parsed?.hookSpecificOutput || {};
|
|
79
|
+
const context = hookOutput.additionalContext || "";
|
|
80
|
+
const systemMessage = parsed?.systemMessage || "";
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
context: stripAnsi(context).trim(),
|
|
84
|
+
message: stripAnsi(systemMessage).trim()
|
|
85
|
+
};
|
|
86
|
+
} catch (err) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const DeliberateOpenCodeChangesPlugin = async ({ client }) => {
|
|
92
|
+
if (!client) {
|
|
93
|
+
throw new Error("Deliberate OpenCode plugin requires SDK client access");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
"tool.execute.after": async (input, output) => {
|
|
98
|
+
const tool = input.tool;
|
|
99
|
+
if (!tool || !["write", "edit", "multiedit", "patch"].includes(tool)) return;
|
|
100
|
+
if (!output) return;
|
|
101
|
+
|
|
102
|
+
const toolArgs = output?.metadata?.args || output?.args || {};
|
|
103
|
+
const filePath = toolArgs.filePath || toolArgs.file_path || output?.metadata?.filepath || output?.metadata?.filePath;
|
|
104
|
+
|
|
105
|
+
if (!filePath) return;
|
|
106
|
+
|
|
107
|
+
const edits = Array.isArray(toolArgs.edits)
|
|
108
|
+
? toolArgs.edits.map((edit) => ({
|
|
109
|
+
old_string: edit.oldString || edit.old_string || "",
|
|
110
|
+
new_string: edit.newString || edit.new_string || ""
|
|
111
|
+
}))
|
|
112
|
+
: undefined;
|
|
113
|
+
|
|
114
|
+
const hookToolName = tool === "multiedit" ? "MultiEdit" : tool === "write" ? "Write" : "Edit";
|
|
115
|
+
|
|
116
|
+
const toolInput = {
|
|
117
|
+
file_path: filePath,
|
|
118
|
+
content: toolArgs.content || "",
|
|
119
|
+
old_string: toolArgs.oldString || toolArgs.old_string || "",
|
|
120
|
+
new_string: toolArgs.newString || toolArgs.new_string || "",
|
|
121
|
+
edits
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (hookToolName === "Edit" && output?.metadata?.filediff?.before && output?.metadata?.filediff?.after) {
|
|
125
|
+
toolInput.old_string = output.metadata.filediff.before;
|
|
126
|
+
toolInput.new_string = output.metadata.filediff.after;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (hookToolName === "Edit" && !toolInput.old_string && output?.metadata?.diff) {
|
|
130
|
+
const diff = output.metadata.diff;
|
|
131
|
+
const lines = diff.split("\n");
|
|
132
|
+
let oldBlock = [];
|
|
133
|
+
let newBlock = [];
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
if (line.startsWith("---") || line.startsWith("+++")) continue;
|
|
136
|
+
if (line.startsWith("-")) {
|
|
137
|
+
oldBlock.push(line.slice(1));
|
|
138
|
+
} else if (line.startsWith("+")) {
|
|
139
|
+
newBlock.push(line.slice(1));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (oldBlock.length) toolInput.old_string = oldBlock.join("\n");
|
|
143
|
+
if (newBlock.length) toolInput.new_string = newBlock.join("\n");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const result = await runChangeHook({
|
|
147
|
+
toolName: hookToolName,
|
|
148
|
+
toolInput,
|
|
149
|
+
sessionID: input.sessionID
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!result) return;
|
|
153
|
+
|
|
154
|
+
const parsed = parseHookOutput(result.stdout);
|
|
155
|
+
const message = parsed?.message || parsed?.context;
|
|
156
|
+
|
|
157
|
+
if (message) {
|
|
158
|
+
await client.session.prompt({
|
|
159
|
+
path: { id: input.sessionID },
|
|
160
|
+
body: {
|
|
161
|
+
noReply: true,
|
|
162
|
+
parts: [{ type: "text", text: message }]
|
|
163
|
+
}
|
|
164
|
+
}).catch(() => {});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export default DeliberateOpenCodeChangesPlugin;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
const PYTHON_CMD = process.platform === "win32" ? "python" : "python3";
|
|
12
|
+
const HOME_DIR = os.homedir();
|
|
13
|
+
const HOOK_SCRIPT_LOCAL = path.join(__dirname, "..", "hooks", "deliberate-commands.py");
|
|
14
|
+
const HOOK_SCRIPT_GLOBAL = path.join(HOME_DIR, ".claude", "hooks", "deliberate-commands.py");
|
|
15
|
+
const HOOK_SCRIPT = fs.existsSync(HOOK_SCRIPT_LOCAL) ? HOOK_SCRIPT_LOCAL : HOOK_SCRIPT_GLOBAL;
|
|
16
|
+
const TIMEOUT_MS = 30000;
|
|
17
|
+
const DANGEROUS_PREFIXES = [
|
|
18
|
+
"rm ",
|
|
19
|
+
"rm-",
|
|
20
|
+
"git ",
|
|
21
|
+
"sudo ",
|
|
22
|
+
"bash ",
|
|
23
|
+
"sh ",
|
|
24
|
+
"python ",
|
|
25
|
+
"python3 ",
|
|
26
|
+
"node ",
|
|
27
|
+
"perl ",
|
|
28
|
+
"ruby ",
|
|
29
|
+
"docker ",
|
|
30
|
+
"kubectl ",
|
|
31
|
+
"aws ",
|
|
32
|
+
"terraform ",
|
|
33
|
+
"gcloud ",
|
|
34
|
+
"az ",
|
|
35
|
+
"scp ",
|
|
36
|
+
"rsync ",
|
|
37
|
+
"dd ",
|
|
38
|
+
"mkfs ",
|
|
39
|
+
"chmod ",
|
|
40
|
+
"chown ",
|
|
41
|
+
"find ",
|
|
42
|
+
"xargs ",
|
|
43
|
+
"parallel ",
|
|
44
|
+
"base64 ",
|
|
45
|
+
"xxd "
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function shouldAnalyze(command) {
|
|
49
|
+
const trimmed = command.trim();
|
|
50
|
+
if (!trimmed) return false;
|
|
51
|
+
if (trimmed.length < 4) return false;
|
|
52
|
+
const lower = trimmed.toLowerCase();
|
|
53
|
+
if (lower.includes("rm -rf") || lower.includes("git reset") || lower.includes("git clean")) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
return DANGEROUS_PREFIXES.some((prefix) => lower.startsWith(prefix));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stripAnsi(input) {
|
|
60
|
+
return input.replace(/\u001b\[[0-9;]*m/g, "");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function runCommandHook({ command, sessionID, cwd }) {
|
|
64
|
+
if (!fs.existsSync(HOOK_SCRIPT)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const payload = {
|
|
69
|
+
tool_name: "Bash",
|
|
70
|
+
tool_input: { command },
|
|
71
|
+
session_id: sessionID,
|
|
72
|
+
cwd: cwd || process.cwd()
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
const child = spawn(PYTHON_CMD, [HOOK_SCRIPT], {
|
|
77
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
78
|
+
env: {
|
|
79
|
+
...process.env
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
let stdout = "";
|
|
84
|
+
let stderr = "";
|
|
85
|
+
let finished = false;
|
|
86
|
+
|
|
87
|
+
const timer = setTimeout(() => {
|
|
88
|
+
if (!finished) {
|
|
89
|
+
finished = true;
|
|
90
|
+
child.kill("SIGKILL");
|
|
91
|
+
resolve({ code: 124, stdout: "", stderr: "Deliberate hook timed out" });
|
|
92
|
+
}
|
|
93
|
+
}, TIMEOUT_MS);
|
|
94
|
+
|
|
95
|
+
child.stdout.on("data", (chunk) => {
|
|
96
|
+
stdout += chunk.toString();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
child.stderr.on("data", (chunk) => {
|
|
100
|
+
stderr += chunk.toString();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
child.on("close", (code) => {
|
|
104
|
+
if (finished) return;
|
|
105
|
+
finished = true;
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
resolve({ code: code ?? 0, stdout, stderr });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
child.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
111
|
+
child.stdin.end();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseHookOutput(stdout) {
|
|
116
|
+
if (!stdout) return null;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(stdout.trim());
|
|
120
|
+
const hookOutput = parsed?.hookSpecificOutput || {};
|
|
121
|
+
const reason = hookOutput.permissionDecisionReason || "";
|
|
122
|
+
const context = hookOutput.additionalContext || "";
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
reason: stripAnsi(reason).trim(),
|
|
126
|
+
context: stripAnsi(context).trim()
|
|
127
|
+
};
|
|
128
|
+
} catch (err) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const DeliberateOpenCodePlugin = async ({ client, directory }) => {
|
|
134
|
+
if (!client) {
|
|
135
|
+
throw new Error("Deliberate OpenCode plugin requires SDK client access");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"tool.execute.before": async (input, output) => {
|
|
140
|
+
if (input.tool !== "bash") return;
|
|
141
|
+
|
|
142
|
+
const command = output?.args?.command;
|
|
143
|
+
if (!command) return;
|
|
144
|
+
|
|
145
|
+
if (!shouldAnalyze(command)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const cwd = output?.args?.workdir || directory;
|
|
150
|
+
const result = await runCommandHook({ command, sessionID: input.sessionID, cwd });
|
|
151
|
+
if (!result) return;
|
|
152
|
+
|
|
153
|
+
const parsed = parseHookOutput(result.stdout);
|
|
154
|
+
const message = parsed?.context || parsed?.reason || "";
|
|
155
|
+
|
|
156
|
+
if (message) {
|
|
157
|
+
await client.session.prompt({
|
|
158
|
+
path: { id: input.sessionID },
|
|
159
|
+
body: {
|
|
160
|
+
noReply: true,
|
|
161
|
+
parts: [{ type: "text", text: message }]
|
|
162
|
+
}
|
|
163
|
+
}).catch(() => {});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (result.code === 2 || (result.stderr && result.stderr.includes("BLOCKED by Deliberate"))) {
|
|
167
|
+
const blockedMessage = stripAnsi(result.stderr || message || "Blocked by Deliberate");
|
|
168
|
+
throw new Error(blockedMessage.trim() || "Blocked by Deliberate");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export default DeliberateOpenCodePlugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deliberate",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Safety layer for agentic coding tools - classifies shell commands before execution",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"bin/",
|
|
13
13
|
"src/",
|
|
14
14
|
"hooks/",
|
|
15
|
+
"opencode/",
|
|
15
16
|
"training/build_classifier.py",
|
|
16
17
|
"training/expanded-command-safety.jsonl",
|
|
17
18
|
"models/classifier_base.pkl",
|
package/src/install.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Handles:
|
|
4
4
|
* - Symlinking hooks to ~/.claude/hooks/
|
|
5
5
|
* - Updating ~/.claude/settings.json
|
|
6
|
+
* - Installing OpenCode plugin (if available)
|
|
6
7
|
* - Configuring Deliberate LLM provider
|
|
7
8
|
* - Optionally starting the classifier server
|
|
8
9
|
*/
|
|
@@ -23,6 +24,8 @@ const IS_WINDOWS = process.platform === 'win32';
|
|
|
23
24
|
const CLAUDE_DIR = path.join(HOME_DIR, '.claude');
|
|
24
25
|
const HOOKS_DIR = path.join(CLAUDE_DIR, 'hooks');
|
|
25
26
|
const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
|
|
27
|
+
const OPENCODE_DIR = path.join(HOME_DIR, '.config', 'opencode');
|
|
28
|
+
const OPENCODE_PLUGIN_DIR = path.join(OPENCODE_DIR, 'plugins');
|
|
26
29
|
|
|
27
30
|
// Python command (python on Windows, python3 on Unix)
|
|
28
31
|
const PYTHON_CMD = IS_WINDOWS ? 'python' : 'python3';
|
|
@@ -31,6 +34,8 @@ const PIP_CMD = IS_WINDOWS ? 'pip' : 'pip3';
|
|
|
31
34
|
// Required Python packages
|
|
32
35
|
const PYTHON_DEPS = ['sentence-transformers', 'scikit-learn', 'numpy', 'claude-agent-sdk'];
|
|
33
36
|
|
|
37
|
+
const OPENCODE_CONFIG_FILES = ['opencode.json', 'opencode.jsonc'];
|
|
38
|
+
|
|
34
39
|
// Model download configuration
|
|
35
40
|
const MODELS_URL = 'https://github.com/the-radar/deliberate/releases/download/v1.0.0/deliberate-models.tar.gz';
|
|
36
41
|
const MODELS_DIR = path.join(__dirname, '..', 'models');
|
|
@@ -184,6 +189,118 @@ function installHooks() {
|
|
|
184
189
|
return installed;
|
|
185
190
|
}
|
|
186
191
|
|
|
192
|
+
function loadOpenCodeConfig() {
|
|
193
|
+
for (const filename of OPENCODE_CONFIG_FILES) {
|
|
194
|
+
const candidate = path.join(OPENCODE_DIR, filename);
|
|
195
|
+
if (fs.existsSync(candidate)) {
|
|
196
|
+
try {
|
|
197
|
+
const content = fs.readFileSync(candidate, 'utf-8');
|
|
198
|
+
return { path: candidate, config: JSON.parse(content) };
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return { path: candidate, config: null, error: error.message };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { path: path.join(OPENCODE_DIR, OPENCODE_CONFIG_FILES[0]), config: null };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function saveOpenCodeConfig(configPath, config) {
|
|
209
|
+
ensureDir(OPENCODE_DIR);
|
|
210
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function ensureOpenCodePluginReference() {
|
|
214
|
+
const pluginPaths = [
|
|
215
|
+
path.join(OPENCODE_PLUGIN_DIR, 'deliberate.js'),
|
|
216
|
+
path.join(OPENCODE_PLUGIN_DIR, 'deliberate-changes.js')
|
|
217
|
+
];
|
|
218
|
+
const targets = pluginPaths.map((pluginPath) => {
|
|
219
|
+
const normalized = pluginPath.split(path.sep).join('/');
|
|
220
|
+
return normalized.startsWith('/') ? `file://${normalized}` : `file:///${normalized}`;
|
|
221
|
+
});
|
|
222
|
+
const { path: configPath, config, error } = loadOpenCodeConfig();
|
|
223
|
+
|
|
224
|
+
if (error) {
|
|
225
|
+
console.warn(`Warning: Could not parse ${configPath}: ${error}`);
|
|
226
|
+
return { updated: false, configPath };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const nextConfig = config || { $schema: 'https://opencode.ai/config.json' };
|
|
230
|
+
const plugins = Array.isArray(nextConfig.plugin) ? nextConfig.plugin : [];
|
|
231
|
+
let updated = false;
|
|
232
|
+
|
|
233
|
+
for (const target of targets) {
|
|
234
|
+
if (!plugins.includes(target)) {
|
|
235
|
+
plugins.push(target);
|
|
236
|
+
updated = true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (updated) {
|
|
241
|
+
nextConfig.plugin = plugins;
|
|
242
|
+
saveOpenCodeConfig(configPath, nextConfig);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { updated, configPath };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Install OpenCode plugin to ~/.config/opencode/plugins
|
|
250
|
+
* Uses symlink on Unix, copy on Windows
|
|
251
|
+
* @returns {string|null} Installed plugin path or null
|
|
252
|
+
*/
|
|
253
|
+
function installOpenCodePlugin() {
|
|
254
|
+
const pluginSource = path.join(__dirname, '..', 'opencode');
|
|
255
|
+
|
|
256
|
+
const primaryPluginSource = path.join(pluginSource, 'deliberate-plugin.js');
|
|
257
|
+
const changesPluginSource = path.join(pluginSource, 'deliberate-changes-plugin.js');
|
|
258
|
+
|
|
259
|
+
if (!fs.existsSync(primaryPluginSource) || !fs.existsSync(changesPluginSource)) {
|
|
260
|
+
console.warn('Warning: OpenCode plugin source not found, skipping');
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
ensureDir(OPENCODE_PLUGIN_DIR);
|
|
265
|
+
|
|
266
|
+
const primaryDest = path.join(OPENCODE_PLUGIN_DIR, 'deliberate.js');
|
|
267
|
+
const changesDest = path.join(OPENCODE_PLUGIN_DIR, 'deliberate-changes.js');
|
|
268
|
+
|
|
269
|
+
for (const dest of [primaryDest, changesDest]) {
|
|
270
|
+
try {
|
|
271
|
+
const stat = fs.lstatSync(dest);
|
|
272
|
+
if (stat.isFile() || stat.isSymbolicLink()) {
|
|
273
|
+
fs.unlinkSync(dest);
|
|
274
|
+
}
|
|
275
|
+
} catch (err) {
|
|
276
|
+
// File doesn't exist, that's fine
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (IS_WINDOWS) {
|
|
281
|
+
fs.copyFileSync(primaryPluginSource, primaryDest);
|
|
282
|
+
fs.copyFileSync(changesPluginSource, changesDest);
|
|
283
|
+
console.log(`Installed OpenCode plugin: ${primaryDest} (copied)`);
|
|
284
|
+
console.log(`Installed OpenCode plugin: ${changesDest} (copied)`);
|
|
285
|
+
} else {
|
|
286
|
+
fs.symlinkSync(primaryPluginSource, primaryDest);
|
|
287
|
+
fs.symlinkSync(changesPluginSource, changesDest);
|
|
288
|
+
fs.chmodSync(primaryPluginSource, 0o755);
|
|
289
|
+
fs.chmodSync(changesPluginSource, 0o755);
|
|
290
|
+
console.log(`Installed OpenCode plugin: ${primaryDest} -> ${primaryPluginSource}`);
|
|
291
|
+
console.log(`Installed OpenCode plugin: ${changesDest} -> ${changesPluginSource}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const configResult = ensureOpenCodePluginReference();
|
|
295
|
+
if (configResult.updated) {
|
|
296
|
+
console.log(`Updated OpenCode config: ${configResult.configPath}`);
|
|
297
|
+
} else {
|
|
298
|
+
console.log(`OpenCode config already references plugin: ${configResult.configPath}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return primaryDest;
|
|
302
|
+
}
|
|
303
|
+
|
|
187
304
|
/**
|
|
188
305
|
* Update ~/.claude/settings.json with hook configuration
|
|
189
306
|
* Preserves existing settings and hooks
|
|
@@ -715,6 +832,16 @@ export async function install() {
|
|
|
715
832
|
console.log('Updating Claude Code settings...');
|
|
716
833
|
updateSettings();
|
|
717
834
|
|
|
835
|
+
// Install OpenCode plugin
|
|
836
|
+
console.log('');
|
|
837
|
+
console.log('Installing OpenCode plugin...');
|
|
838
|
+
const opencodePlugin = installOpenCodePlugin();
|
|
839
|
+
if (!opencodePlugin) {
|
|
840
|
+
console.log('OpenCode plugin: ⚠️ Not installed');
|
|
841
|
+
} else {
|
|
842
|
+
console.log('OpenCode plugin: ✅ Installed (commands + changes)');
|
|
843
|
+
}
|
|
844
|
+
|
|
718
845
|
// Configure LLM if not already configured
|
|
719
846
|
if (!isLLMConfigured()) {
|
|
720
847
|
await configureLLM();
|
|
@@ -734,14 +861,19 @@ export async function install() {
|
|
|
734
861
|
for (const hookPath of installed) {
|
|
735
862
|
console.log(` - ${hookPath}`);
|
|
736
863
|
}
|
|
864
|
+
if (opencodePlugin) {
|
|
865
|
+
console.log(` - ${opencodePlugin} (OpenCode plugin: commands)`);
|
|
866
|
+
console.log(` - ${path.join(OPENCODE_PLUGIN_DIR, 'deliberate-changes.js')} (OpenCode plugin: changes)`);
|
|
867
|
+
}
|
|
737
868
|
console.log('');
|
|
738
869
|
console.log('Next steps:');
|
|
739
870
|
console.log(' 1. Restart Claude Code to load the new hooks');
|
|
871
|
+
console.log(' 2. Restart OpenCode to load the new plugin');
|
|
740
872
|
console.log('');
|
|
741
|
-
console.log('
|
|
873
|
+
console.log(' 3. (Optional) Start the classifier server for faster ML detection:');
|
|
742
874
|
console.log(' deliberate serve');
|
|
743
875
|
console.log('');
|
|
744
|
-
console.log('
|
|
876
|
+
console.log(' 4. Test classification:');
|
|
745
877
|
console.log(' deliberate classify "rm -rf /"');
|
|
746
878
|
console.log('');
|
|
747
879
|
}
|
package/src/uninstall.js
CHANGED
|
@@ -15,11 +15,15 @@ const CLAUDE_DIR = path.join(HOME_DIR, '.claude');
|
|
|
15
15
|
const HOOKS_DIR = path.join(CLAUDE_DIR, 'hooks');
|
|
16
16
|
const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
|
|
17
17
|
const CONFIG_FILE = path.join(HOME_DIR, '.deliberate', 'config.json');
|
|
18
|
+
const OPENCODE_DIR = path.join(HOME_DIR, '.config', 'opencode');
|
|
19
|
+
const OPENCODE_PLUGIN_DIR = path.join(OPENCODE_DIR, 'plugins');
|
|
20
|
+
const OPENCODE_CONFIG_FILES = ['opencode.json', 'opencode.jsonc'];
|
|
18
21
|
|
|
19
22
|
// Hook files to remove
|
|
20
23
|
const HOOKS_TO_REMOVE = [
|
|
21
|
-
'deliberate-
|
|
22
|
-
'deliberate-
|
|
24
|
+
'deliberate-commands.py',
|
|
25
|
+
'deliberate-commands-post.py',
|
|
26
|
+
'deliberate-changes.py'
|
|
23
27
|
];
|
|
24
28
|
|
|
25
29
|
/**
|
|
@@ -45,6 +49,53 @@ function removeHooks() {
|
|
|
45
49
|
return removed;
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Remove OpenCode plugin from config and plugins dir
|
|
54
|
+
*/
|
|
55
|
+
function removeOpenCodePlugin() {
|
|
56
|
+
let removed = false;
|
|
57
|
+
|
|
58
|
+
const pluginPaths = [
|
|
59
|
+
path.join(OPENCODE_PLUGIN_DIR, 'deliberate.js'),
|
|
60
|
+
path.join(OPENCODE_PLUGIN_DIR, 'deliberate-changes.js')
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
for (const pluginPath of pluginPaths) {
|
|
64
|
+
if (fs.existsSync(pluginPath)) {
|
|
65
|
+
fs.unlinkSync(pluginPath);
|
|
66
|
+
console.log(` Removed OpenCode plugin: ${pluginPath}`);
|
|
67
|
+
removed = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const filename of OPENCODE_CONFIG_FILES) {
|
|
72
|
+
const configPath = path.join(OPENCODE_DIR, filename);
|
|
73
|
+
if (!fs.existsSync(configPath)) continue;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
77
|
+
const config = JSON.parse(content);
|
|
78
|
+
if (Array.isArray(config.plugin)) {
|
|
79
|
+
const nextPlugins = config.plugin.filter((entry) => !String(entry).includes('deliberate'));
|
|
80
|
+
if (nextPlugins.length !== config.plugin.length) {
|
|
81
|
+
config.plugin = nextPlugins;
|
|
82
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
83
|
+
console.log(` Updated OpenCode config: ${configPath}`);
|
|
84
|
+
removed = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.warn(` Warning: Could not parse ${configPath}: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!removed) {
|
|
93
|
+
console.log(' No OpenCode plugin config found');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return removed;
|
|
97
|
+
}
|
|
98
|
+
|
|
48
99
|
/**
|
|
49
100
|
* Remove Deliberate hooks from ~/.claude/settings.json
|
|
50
101
|
*/
|
|
@@ -71,8 +122,8 @@ function removeFromSettings() {
|
|
|
71
122
|
|
|
72
123
|
const filteredHooks = matcher.hooks.filter(hook => {
|
|
73
124
|
const isDeliberate = hook.command && (
|
|
74
|
-
hook.command.includes('deliberate-
|
|
75
|
-
hook.command.includes('deliberate-
|
|
125
|
+
hook.command.includes('deliberate-commands') ||
|
|
126
|
+
hook.command.includes('deliberate-changes')
|
|
76
127
|
);
|
|
77
128
|
if (isDeliberate) modified = true;
|
|
78
129
|
return !isDeliberate;
|
|
@@ -91,8 +142,8 @@ function removeFromSettings() {
|
|
|
91
142
|
|
|
92
143
|
const filteredHooks = matcher.hooks.filter(hook => {
|
|
93
144
|
const isDeliberate = hook.command && (
|
|
94
|
-
hook.command.includes('deliberate-
|
|
95
|
-
hook.command.includes('deliberate-
|
|
145
|
+
hook.command.includes('deliberate-commands') ||
|
|
146
|
+
hook.command.includes('deliberate-changes')
|
|
96
147
|
);
|
|
97
148
|
if (isDeliberate) modified = true;
|
|
98
149
|
return !isDeliberate;
|
|
@@ -153,6 +204,10 @@ export async function uninstall() {
|
|
|
153
204
|
console.log('');
|
|
154
205
|
removeFromSettings();
|
|
155
206
|
|
|
207
|
+
// Remove OpenCode plugin
|
|
208
|
+
console.log('');
|
|
209
|
+
removeOpenCodePlugin();
|
|
210
|
+
|
|
156
211
|
// Ask about config
|
|
157
212
|
console.log('');
|
|
158
213
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
@@ -186,7 +241,7 @@ export async function uninstall() {
|
|
|
186
241
|
console.log('===========================================');
|
|
187
242
|
console.log('');
|
|
188
243
|
console.log('Next step:');
|
|
189
|
-
console.log(' Restart Claude Code to unload
|
|
244
|
+
console.log(' Restart Claude Code and OpenCode to unload hooks/plugins');
|
|
190
245
|
console.log('');
|
|
191
246
|
}
|
|
192
247
|
|