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 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 (or any tool supporting Claude Code hooks)
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 hasCommandHook = preToolUse.some(h =>
64
- h.command && h.command.includes('explain-command')
65
- );
66
- const hasChangesHook = postToolUse.some(h =>
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
- if (hasCommandHook && hasChangesHook) {
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 (PreToolUse only)');
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 Claude Code sessions');
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
  }
@@ -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",
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(' 2. (Optional) Start the classifier server for faster ML detection:');
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(' 3. Test classification:');
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-explain-command.py',
22
- 'deliberate-explain-changes.py'
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-explain-command') ||
75
- hook.command.includes('deliberate-explain-changes')
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-explain-command') ||
95
- hook.command.includes('deliberate-explain-changes')
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 the hooks');
244
+ console.log(' Restart Claude Code and OpenCode to unload hooks/plugins');
190
245
  console.log('');
191
246
  }
192
247