deliberate 1.0.2 → 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 +171 -217
- 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
|
@@ -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
|
|