@melihmucuk/leash 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/bin/leash.js +163 -0
- package/bin/lib.js +156 -0
- package/dist/claude-code/leash.js +516 -0
- package/dist/factory/leash.js +517 -0
- package/dist/opencode/leash.js +512 -0
- package/dist/pi/leash.js +517 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Melih Mucuk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# Leash 🔒
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@melihmucuk/leash)
|
|
4
|
+
|
|
5
|
+
**Security guardrails for AI coding agents.** Sandboxes file system access, blocks dangerous commands outside project directory, prevents destructive git operations, catches agent hallucinations before they cause damage.
|
|
6
|
+
|
|
7
|
+
## Why Leash?
|
|
8
|
+
|
|
9
|
+
AI agents can hallucinate dangerous commands. Leash sandboxes them:
|
|
10
|
+
|
|
11
|
+
- Blocks `rm`, `mv`, `cp`, `chmod` outside working directory
|
|
12
|
+
- Protects sensitive files (`.env`, `.git`) even inside project
|
|
13
|
+
- Blocks `git reset --hard`, `push --force`, `clean -f`
|
|
14
|
+
- Resolves symlinks to prevent directory escapes
|
|
15
|
+
- Analyzes command chains (`&&`, `||`, `;`, `|`)
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g @melihmucuk/leash
|
|
23
|
+
leash --setup <platform>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
| Platform | Command |
|
|
27
|
+
|----------|---------|
|
|
28
|
+
| OpenCode | `leash --setup opencode` |
|
|
29
|
+
| Pi Coding Agent | `leash --setup pi` |
|
|
30
|
+
| Claude Code | `leash --setup claude-code` |
|
|
31
|
+
| Factory Droid | `leash --setup factory` |
|
|
32
|
+
|
|
33
|
+
Restart your agent. Done!
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Update anytime
|
|
37
|
+
npm update -g @melihmucuk/leash
|
|
38
|
+
|
|
39
|
+
# Remove from a platform
|
|
40
|
+
leash --remove <platform>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
<details>
|
|
44
|
+
<summary><b>Manual Setup</b></summary>
|
|
45
|
+
|
|
46
|
+
If you prefer manual configuration, use `leash --path <platform>` to get the path and add it to your config file.
|
|
47
|
+
|
|
48
|
+
**Pi Coding Agent** - [docs](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/hooks.md)
|
|
49
|
+
|
|
50
|
+
Add to `~/.pi/agent/settings.json`:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"hooks": ["<path from leash --path pi>"]
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**OpenCode** - [docs](https://opencode.ai/docs/plugins/)
|
|
59
|
+
|
|
60
|
+
Add to `~/.config/opencode/config.json`:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"plugins": ["<path from leash --path opencode>"]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Claude Code** - [docs](https://code.claude.com/docs/en/hooks-guide)
|
|
69
|
+
|
|
70
|
+
Add to `~/.claude/settings.json`:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"hooks": {
|
|
75
|
+
"PreToolUse": [
|
|
76
|
+
{
|
|
77
|
+
"matcher": "Bash|Write|Edit",
|
|
78
|
+
"hooks": [
|
|
79
|
+
{
|
|
80
|
+
"type": "command",
|
|
81
|
+
"command": "node <path from leash --path claude-code>"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Factory Droid** - [docs](https://docs.factory.ai/cli/configuration/hooks-guide)
|
|
91
|
+
|
|
92
|
+
Add to `~/.factory/settings.json`:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"hooks": {
|
|
97
|
+
"PreToolUse": [
|
|
98
|
+
{
|
|
99
|
+
"matcher": "Execute|Write|Edit",
|
|
100
|
+
"hooks": [
|
|
101
|
+
{
|
|
102
|
+
"type": "command",
|
|
103
|
+
"command": "node <path from leash --path factory>"
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
</details>
|
|
113
|
+
|
|
114
|
+
## What Gets Blocked
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Dangerous commands outside working directory
|
|
118
|
+
rm -rf ~/Documents # ❌ Delete outside working dir
|
|
119
|
+
mv ~/.bashrc /tmp/ # ❌ Move from outside
|
|
120
|
+
echo "data" > ~/file.txt # ❌ Redirect to home
|
|
121
|
+
|
|
122
|
+
# Protected files (blocked even inside project)
|
|
123
|
+
rm .env # ❌ Protected file
|
|
124
|
+
echo "SECRET=x" > .env.local # ❌ Protected file
|
|
125
|
+
rm -rf .git # ❌ Protected directory
|
|
126
|
+
|
|
127
|
+
# Dangerous git commands (blocked everywhere)
|
|
128
|
+
git reset --hard # ❌ Destroys uncommitted changes
|
|
129
|
+
git push --force # ❌ Destroys remote history
|
|
130
|
+
git clean -fd # ❌ Removes untracked files
|
|
131
|
+
|
|
132
|
+
# File operations via Write/Edit tools
|
|
133
|
+
~/.bashrc # ❌ Home directory file
|
|
134
|
+
../../../etc/hosts # ❌ Path traversal
|
|
135
|
+
.env # ❌ Protected file
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## What's Allowed
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
rm -rf ./node_modules # ✅ Working directory
|
|
142
|
+
rm -rf /tmp/build-cache # ✅ Temp directory
|
|
143
|
+
rm .env.example # ✅ Example files allowed
|
|
144
|
+
git commit -m "message" # ✅ Safe git commands
|
|
145
|
+
git push origin main # ✅ Normal push (no --force)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
<details>
|
|
149
|
+
|
|
150
|
+
<summary><b>Detailed Examples</b></summary>
|
|
151
|
+
|
|
152
|
+
### Dangerous Commands
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
rm -rf ~/Documents # ❌ Delete outside working dir
|
|
156
|
+
mv ~/.bashrc /tmp/ # ❌ Move from outside
|
|
157
|
+
cp ./secrets ~/leaked # ❌ Copy to outside
|
|
158
|
+
chmod 777 /etc/hosts # ❌ Permission change outside
|
|
159
|
+
chown user ~/file # ❌ Ownership change outside
|
|
160
|
+
ln -s ./file ~/link # ❌ Symlink to outside
|
|
161
|
+
dd if=/dev/zero of=~/file # ❌ Write outside
|
|
162
|
+
truncate -s 0 ~/file # ❌ Truncate outside
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Dangerous Git Commands
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
git checkout -- . # ❌ Discards uncommitted changes
|
|
169
|
+
git restore src/file.ts # ❌ Discards uncommitted changes
|
|
170
|
+
git reset --hard # ❌ Destroys all uncommitted changes
|
|
171
|
+
git reset --hard HEAD~1 # ❌ Destroys commits and changes
|
|
172
|
+
git reset --merge # ❌ Can lose uncommitted changes
|
|
173
|
+
git clean -f # ❌ Removes untracked files permanently
|
|
174
|
+
git clean -fd # ❌ Removes untracked files and directories
|
|
175
|
+
git push --force # ❌ Destroys remote history
|
|
176
|
+
git push -f origin main # ❌ Destroys remote history
|
|
177
|
+
git branch -D feature # ❌ Force-deletes branch without merge check
|
|
178
|
+
git stash drop # ❌ Permanently deletes stashed changes
|
|
179
|
+
git stash clear # ❌ Deletes ALL stashed changes
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Redirects
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
echo "data" > ~/file.txt # ❌ Redirect to home
|
|
186
|
+
echo "log" >> ~/app.log # ❌ Append to home
|
|
187
|
+
cat secrets > "/tmp/../~/x" # ❌ Path traversal in redirect
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Command Chains
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
echo ok && rm ~/file # ❌ Dangerous command after &&
|
|
194
|
+
false || rm -rf ~/ # ❌ Dangerous command after ||
|
|
195
|
+
ls; rm ~/file # ❌ Dangerous command after ;
|
|
196
|
+
cat x | rm ~/file # ❌ Dangerous command in pipe
|
|
197
|
+
cd ~/Downloads && rm file # ❌ cd outside + dangerous command
|
|
198
|
+
cd .. && cd .. && rm target # ❌ cd hops escaping working dir
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Wrapper Commands
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
sudo rm -rf ~/dir # ❌ sudo + dangerous command
|
|
205
|
+
env rm ~/file # ❌ env + dangerous command
|
|
206
|
+
command rm ~/file # ❌ command + dangerous command
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Compound Patterns
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
find ~ -name "*.tmp" -delete # ❌ find -delete outside
|
|
213
|
+
find ~ -exec rm {} \; # ❌ find -exec rm outside
|
|
214
|
+
find ~/logs | xargs rm # ❌ xargs rm outside
|
|
215
|
+
find ~ | xargs -I{} mv {} /tmp # ❌ xargs mv outside
|
|
216
|
+
rsync -av --delete ~/src/ ~/dst/ # ❌ rsync --delete outside
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Protected Files (blocked even inside project)
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
rm .env # ❌ Environment file
|
|
223
|
+
rm .env.local # ❌ Environment file
|
|
224
|
+
rm .env.production # ❌ Environment file
|
|
225
|
+
echo "x" > .env # ❌ Write to env file
|
|
226
|
+
rm -rf .git # ❌ Git directory
|
|
227
|
+
echo "x" > .git/config # ❌ Write to git directory
|
|
228
|
+
find . -name ".env" -delete # ❌ Delete protected via find
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Note: `.env.example` is allowed (template files are safe).
|
|
232
|
+
|
|
233
|
+
### File Operations (Write/Edit tools)
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
/etc/passwd # ❌ System file
|
|
237
|
+
~/.bashrc # ❌ Home directory file
|
|
238
|
+
/home/user/.ssh/id_rsa # ❌ Absolute path outside
|
|
239
|
+
../../../etc/hosts # ❌ Path traversal
|
|
240
|
+
.env # ❌ Protected file
|
|
241
|
+
.git/config # ❌ Protected directory
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### What's Allowed (Full List)
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
# Working directory operations
|
|
248
|
+
rm -rf ./node_modules
|
|
249
|
+
mv ./old.ts ./new.ts
|
|
250
|
+
cp ./src/config.json ./dist/
|
|
251
|
+
find . -name "*.bak" -delete
|
|
252
|
+
find ./logs | xargs rm
|
|
253
|
+
|
|
254
|
+
# Temp directory operations
|
|
255
|
+
rm -rf /tmp/build-cache
|
|
256
|
+
echo "data" > /tmp/output.txt
|
|
257
|
+
rsync -av --delete ./src/ /tmp/backup/
|
|
258
|
+
|
|
259
|
+
# Device paths
|
|
260
|
+
echo "x" > /dev/null
|
|
261
|
+
truncate -s 0 /dev/null
|
|
262
|
+
|
|
263
|
+
# Read from anywhere (safe)
|
|
264
|
+
cp /etc/hosts ./local-hosts
|
|
265
|
+
cat /etc/passwd
|
|
266
|
+
|
|
267
|
+
# Safe git commands
|
|
268
|
+
git status
|
|
269
|
+
git add .
|
|
270
|
+
git commit -m "message"
|
|
271
|
+
git push origin main
|
|
272
|
+
git checkout main
|
|
273
|
+
git checkout -b feature/new
|
|
274
|
+
git branch -d merged-branch # lowercase -d is safe
|
|
275
|
+
git reset --soft HEAD~1 # soft reset is safe
|
|
276
|
+
git restore --staged . # unstaging is safe
|
|
277
|
+
git stash
|
|
278
|
+
git stash pop
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
</details>
|
|
282
|
+
|
|
283
|
+
## Performance
|
|
284
|
+
|
|
285
|
+
Near-zero latency impact on your workflow:
|
|
286
|
+
|
|
287
|
+
| Platform | Latency per tool call | Notes |
|
|
288
|
+
| ----------- | --------------------- | ---------------------------------------- |
|
|
289
|
+
| OpenCode | **~20µs** | In-process plugin, near-zero overhead |
|
|
290
|
+
| Pi | **~20µs** | In-process hook, near-zero overhead |
|
|
291
|
+
| Claude Code | **~31ms** | External process (~30ms Node.js startup) |
|
|
292
|
+
| Factory | **~31ms** | External process (~30ms Node.js startup) |
|
|
293
|
+
|
|
294
|
+
For context: LLM API calls typically take 2-10+ seconds. Even the slower external process hook adds less than 0.3% to total response time.
|
|
295
|
+
|
|
296
|
+
## Limitations
|
|
297
|
+
|
|
298
|
+
Leash is a **defense-in-depth** layer, not a complete sandbox. It cannot protect against:
|
|
299
|
+
|
|
300
|
+
- Kernel exploits or privilege escalation
|
|
301
|
+
- Network-based attacks (downloading and executing scripts)
|
|
302
|
+
- Commands not routed through the intercepted tools
|
|
303
|
+
|
|
304
|
+
For maximum security, combine Leash with container isolation (Docker), user permission restrictions, or read-only filesystem mounts.
|
|
305
|
+
|
|
306
|
+
## Development
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
cd ~/leash
|
|
310
|
+
npm install
|
|
311
|
+
npm run build
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Contributing
|
|
315
|
+
|
|
316
|
+
Contributions are welcome! Areas where help is needed:
|
|
317
|
+
|
|
318
|
+
- [ ] Plugin for AMP Code
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
_Keep your AI agents on a leash._
|
package/bin/leash.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { PLATFORMS, setupPlatform, removePlatform } from "./lib.js";
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
function getDistPath() {
|
|
12
|
+
return join(__dirname, "..", "dist");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getConfigPath(platformKey) {
|
|
16
|
+
const platform = PLATFORMS[platformKey];
|
|
17
|
+
return platform ? join(homedir(), platform.configPath) : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getLeashPath(platformKey) {
|
|
21
|
+
const platform = PLATFORMS[platformKey];
|
|
22
|
+
return platform ? join(getDistPath(), platform.distPath) : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function setup(platformKey) {
|
|
26
|
+
const configPath = getConfigPath(platformKey);
|
|
27
|
+
const leashPath = getLeashPath(platformKey);
|
|
28
|
+
|
|
29
|
+
if (!configPath || !leashPath) {
|
|
30
|
+
console.error(`Unknown platform: ${platformKey}`);
|
|
31
|
+
console.error(`Available: ${Object.keys(PLATFORMS).join(", ")}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!existsSync(leashPath)) {
|
|
36
|
+
console.error(`Leash not found at: ${leashPath}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const result = setupPlatform(platformKey, configPath, leashPath);
|
|
41
|
+
|
|
42
|
+
if (result.error) {
|
|
43
|
+
console.error(result.error);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (result.skipped) {
|
|
48
|
+
console.log(`[ok] Leash already installed for ${result.platform}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(`[ok] Config: ${result.configPath}`);
|
|
53
|
+
console.log(`[ok] Leash installed for ${result.platform}`);
|
|
54
|
+
console.log(`[ok] Restart ${result.platform} to apply changes`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function remove(platformKey) {
|
|
58
|
+
const configPath = getConfigPath(platformKey);
|
|
59
|
+
|
|
60
|
+
if (!configPath) {
|
|
61
|
+
console.error(`Unknown platform: ${platformKey}`);
|
|
62
|
+
console.error(`Available: ${Object.keys(PLATFORMS).join(", ")}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const result = removePlatform(platformKey, configPath);
|
|
67
|
+
|
|
68
|
+
if (result.error) {
|
|
69
|
+
console.error(result.error);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (result.notFound) {
|
|
74
|
+
console.log(`[ok] No config found for ${result.platform}`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (result.notInstalled) {
|
|
79
|
+
console.log(`[ok] Leash not found in ${result.platform} config`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(`[ok] Leash removed from ${result.platform}`);
|
|
84
|
+
console.log(`[ok] Restart ${result.platform} to apply changes`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function showPath(platformKey) {
|
|
88
|
+
const leashPath = getLeashPath(platformKey);
|
|
89
|
+
|
|
90
|
+
if (!leashPath) {
|
|
91
|
+
console.error(`Unknown platform: ${platformKey}`);
|
|
92
|
+
console.error(`Available: ${Object.keys(PLATFORMS).join(", ")}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(leashPath);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function showHelp() {
|
|
100
|
+
console.log(`
|
|
101
|
+
leash - Security guardrails for AI coding agents
|
|
102
|
+
|
|
103
|
+
Usage:
|
|
104
|
+
leash --setup <platform> Install leash for a platform
|
|
105
|
+
leash --remove <platform> Remove leash from a platform
|
|
106
|
+
leash --path <platform> Show leash path for a platform
|
|
107
|
+
leash --help Show this help
|
|
108
|
+
|
|
109
|
+
Platforms:
|
|
110
|
+
opencode OpenCode
|
|
111
|
+
pi Pi Coding Agent
|
|
112
|
+
claude-code Claude Code
|
|
113
|
+
factory Factory Droid
|
|
114
|
+
|
|
115
|
+
Examples:
|
|
116
|
+
leash --setup opencode
|
|
117
|
+
leash --remove claude-code
|
|
118
|
+
leash --path pi
|
|
119
|
+
`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const args = process.argv.slice(2);
|
|
123
|
+
const command = args[0];
|
|
124
|
+
const platform = args[1];
|
|
125
|
+
|
|
126
|
+
switch (command) {
|
|
127
|
+
case "--setup":
|
|
128
|
+
case "-s":
|
|
129
|
+
if (!platform) {
|
|
130
|
+
console.error("Missing platform argument");
|
|
131
|
+
showHelp();
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
setup(platform);
|
|
135
|
+
break;
|
|
136
|
+
case "--remove":
|
|
137
|
+
case "-r":
|
|
138
|
+
if (!platform) {
|
|
139
|
+
console.error("Missing platform argument");
|
|
140
|
+
showHelp();
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
remove(platform);
|
|
144
|
+
break;
|
|
145
|
+
case "--path":
|
|
146
|
+
case "-p":
|
|
147
|
+
if (!platform) {
|
|
148
|
+
console.error("Missing platform argument");
|
|
149
|
+
showHelp();
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
showPath(platform);
|
|
153
|
+
break;
|
|
154
|
+
case "--help":
|
|
155
|
+
case "-h":
|
|
156
|
+
case undefined:
|
|
157
|
+
showHelp();
|
|
158
|
+
break;
|
|
159
|
+
default:
|
|
160
|
+
console.error(`Unknown command: ${command}`);
|
|
161
|
+
showHelp();
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
package/bin/lib.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
|
|
4
|
+
export const PLATFORMS = {
|
|
5
|
+
opencode: {
|
|
6
|
+
name: "OpenCode",
|
|
7
|
+
configPath: ".config/opencode/config.json",
|
|
8
|
+
distPath: "opencode/leash.js",
|
|
9
|
+
setup: (config, leashPath) => {
|
|
10
|
+
config.plugins = config.plugins || [];
|
|
11
|
+
if (config.plugins.some((p) => p.includes("leash"))) {
|
|
12
|
+
return { skipped: true };
|
|
13
|
+
}
|
|
14
|
+
config.plugins.push(leashPath);
|
|
15
|
+
return { skipped: false };
|
|
16
|
+
},
|
|
17
|
+
remove: (config) => {
|
|
18
|
+
if (!config.plugins) return false;
|
|
19
|
+
const before = config.plugins.length;
|
|
20
|
+
config.plugins = config.plugins.filter((p) => !p.includes("leash"));
|
|
21
|
+
return config.plugins.length < before;
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
pi: {
|
|
25
|
+
name: "Pi",
|
|
26
|
+
configPath: ".pi/agent/settings.json",
|
|
27
|
+
distPath: "pi/leash.js",
|
|
28
|
+
setup: (config, leashPath) => {
|
|
29
|
+
config.hooks = config.hooks || [];
|
|
30
|
+
if (config.hooks.some((h) => h.includes("leash"))) {
|
|
31
|
+
return { skipped: true };
|
|
32
|
+
}
|
|
33
|
+
config.hooks.push(leashPath);
|
|
34
|
+
return { skipped: false };
|
|
35
|
+
},
|
|
36
|
+
remove: (config) => {
|
|
37
|
+
if (!config.hooks) return false;
|
|
38
|
+
const before = config.hooks.length;
|
|
39
|
+
config.hooks = config.hooks.filter((h) => !h.includes("leash"));
|
|
40
|
+
return config.hooks.length < before;
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
"claude-code": {
|
|
44
|
+
name: "Claude Code",
|
|
45
|
+
configPath: ".claude/settings.json",
|
|
46
|
+
distPath: "claude-code/leash.js",
|
|
47
|
+
setup: (config, leashPath) => {
|
|
48
|
+
config.hooks = config.hooks || {};
|
|
49
|
+
config.hooks.PreToolUse = config.hooks.PreToolUse || [];
|
|
50
|
+
const exists = config.hooks.PreToolUse.some((entry) =>
|
|
51
|
+
entry.hooks?.some((h) => h.command?.includes("leash"))
|
|
52
|
+
);
|
|
53
|
+
if (exists) {
|
|
54
|
+
return { skipped: true };
|
|
55
|
+
}
|
|
56
|
+
config.hooks.PreToolUse.push({
|
|
57
|
+
matcher: "Bash|Write|Edit",
|
|
58
|
+
hooks: [{ type: "command", command: `node ${leashPath}` }],
|
|
59
|
+
});
|
|
60
|
+
return { skipped: false };
|
|
61
|
+
},
|
|
62
|
+
remove: (config) => {
|
|
63
|
+
if (!config.hooks?.PreToolUse) return false;
|
|
64
|
+
const before = config.hooks.PreToolUse.length;
|
|
65
|
+
config.hooks.PreToolUse = config.hooks.PreToolUse.filter(
|
|
66
|
+
(entry) => !entry.hooks?.some((h) => h.command?.includes("leash"))
|
|
67
|
+
);
|
|
68
|
+
return config.hooks.PreToolUse.length < before;
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
factory: {
|
|
72
|
+
name: "Factory",
|
|
73
|
+
configPath: ".factory/settings.json",
|
|
74
|
+
distPath: "factory/leash.js",
|
|
75
|
+
setup: (config, leashPath) => {
|
|
76
|
+
config.hooks = config.hooks || {};
|
|
77
|
+
config.hooks.PreToolUse = config.hooks.PreToolUse || [];
|
|
78
|
+
const exists = config.hooks.PreToolUse.some((entry) =>
|
|
79
|
+
entry.hooks?.some((h) => h.command?.includes("leash"))
|
|
80
|
+
);
|
|
81
|
+
if (exists) {
|
|
82
|
+
return { skipped: true };
|
|
83
|
+
}
|
|
84
|
+
config.hooks.PreToolUse.push({
|
|
85
|
+
matcher: "Execute|Write|Edit",
|
|
86
|
+
hooks: [{ type: "command", command: `node ${leashPath}` }],
|
|
87
|
+
});
|
|
88
|
+
return { skipped: false };
|
|
89
|
+
},
|
|
90
|
+
remove: (config) => {
|
|
91
|
+
if (!config.hooks?.PreToolUse) return false;
|
|
92
|
+
const before = config.hooks.PreToolUse.length;
|
|
93
|
+
config.hooks.PreToolUse = config.hooks.PreToolUse.filter(
|
|
94
|
+
(entry) => !entry.hooks?.some((h) => h.command?.includes("leash"))
|
|
95
|
+
);
|
|
96
|
+
return config.hooks.PreToolUse.length < before;
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export function readConfig(configPath) {
|
|
102
|
+
if (!existsSync(configPath)) {
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
107
|
+
} catch {
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function writeConfig(configPath, config) {
|
|
113
|
+
const dir = dirname(configPath);
|
|
114
|
+
if (!existsSync(dir)) {
|
|
115
|
+
mkdirSync(dir, { recursive: true });
|
|
116
|
+
}
|
|
117
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function setupPlatform(platformKey, configPath, leashPath) {
|
|
121
|
+
const platform = PLATFORMS[platformKey];
|
|
122
|
+
if (!platform) {
|
|
123
|
+
return { error: `Unknown platform: ${platformKey}` };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const config = readConfig(configPath);
|
|
127
|
+
const result = platform.setup(config, leashPath);
|
|
128
|
+
|
|
129
|
+
if (result.skipped) {
|
|
130
|
+
return { skipped: true, platform: platform.name };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
writeConfig(configPath, config);
|
|
134
|
+
return { success: true, platform: platform.name, configPath };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function removePlatform(platformKey, configPath) {
|
|
138
|
+
const platform = PLATFORMS[platformKey];
|
|
139
|
+
if (!platform) {
|
|
140
|
+
return { error: `Unknown platform: ${platformKey}` };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!existsSync(configPath)) {
|
|
144
|
+
return { notFound: true, platform: platform.name };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const config = readConfig(configPath);
|
|
148
|
+
const removed = platform.remove(config);
|
|
149
|
+
|
|
150
|
+
if (!removed) {
|
|
151
|
+
return { notInstalled: true, platform: platform.name };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
writeConfig(configPath, config);
|
|
155
|
+
return { success: true, platform: platform.name };
|
|
156
|
+
}
|