@dreki-gg/pi-plan-mode 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/extensions/plan-mode/utils.test.ts +110 -0
- package/extensions/plan-mode/utils.ts +65 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @dreki-gg/pi-plan-mode
|
|
2
2
|
|
|
3
|
+
## 0.5.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`8e9aa09`](https://github.com/dreki-gg/pi-extensions/commit/8e9aa0963fe81286e9c5972f6a9d666645807f1a) Thanks [@jalbarrang](https://github.com/jalbarrang)! - fix(plan-mode): allow safe bash commands that were incorrectly blocked in plan mode
|
|
8
|
+
|
|
9
|
+
Three fixes to `isSafeCommand`:
|
|
10
|
+
|
|
11
|
+
- Allow `mkdir -p .plans/` since the planner needs to create plan directories
|
|
12
|
+
- Fix redirect pattern to not false-positive on stderr redirects like `2>/dev/null`
|
|
13
|
+
- Split piped commands and validate each segment independently, so `curl ... | grep ... | head` works correctly
|
|
14
|
+
|
|
3
15
|
## 0.5.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { isSafeCommand } from './utils.js';
|
|
3
|
+
|
|
4
|
+
describe('isSafeCommand', () => {
|
|
5
|
+
// ── Commands that SHOULD be allowed ──────────────────────────────────────
|
|
6
|
+
describe('allowed commands', () => {
|
|
7
|
+
test('mkdir -p .plans/<name>', () => {
|
|
8
|
+
expect(isSafeCommand('mkdir -p .plans/monorepo-src-migration')).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('mkdir .plans/<name>', () => {
|
|
12
|
+
expect(isSafeCommand('mkdir .plans/my-plan')).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('curl with 2>/dev/null pipe chain', () => {
|
|
16
|
+
expect(
|
|
17
|
+
isSafeCommand(
|
|
18
|
+
`curl -sL "https://effect.website/docs/platform/introduction/" 2>/dev/null | grep -oP '(?<=href=")[^"]*' | grep -iE "(http|server|rpc)" | head -20`,
|
|
19
|
+
),
|
|
20
|
+
).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('curl with 2>/dev/null simple', () => {
|
|
24
|
+
expect(
|
|
25
|
+
isSafeCommand(
|
|
26
|
+
`curl -s "https://raw.githubusercontent.com/Effect-TS/effect/main/packages/platform/README.md" 2>/dev/null | head -100`,
|
|
27
|
+
),
|
|
28
|
+
).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('simple ls', () => {
|
|
32
|
+
expect(isSafeCommand('ls -la')).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('git status', () => {
|
|
36
|
+
expect(isSafeCommand('git status')).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('git log', () => {
|
|
40
|
+
expect(isSafeCommand('git log --oneline -10')).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('find piped to grep', () => {
|
|
44
|
+
expect(isSafeCommand('find . -name "*.ts" | grep -v node_modules')).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('cat piped to head', () => {
|
|
48
|
+
expect(isSafeCommand('cat README.md | head -50')).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('rg (ripgrep)', () => {
|
|
52
|
+
expect(isSafeCommand('rg "pattern" src/')).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('grep with context', () => {
|
|
56
|
+
expect(isSafeCommand('grep -rn "export" src/ | head -20')).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('npm list', () => {
|
|
60
|
+
expect(isSafeCommand('npm list --depth=0')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('curl piped to jq', () => {
|
|
64
|
+
expect(isSafeCommand('curl -s https://api.example.com | jq .name')).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ── Commands that SHOULD be blocked ──────────────────────────────────────
|
|
69
|
+
describe('blocked commands', () => {
|
|
70
|
+
test('rm -rf', () => {
|
|
71
|
+
expect(isSafeCommand('rm -rf node_modules')).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('mkdir outside .plans/', () => {
|
|
75
|
+
expect(isSafeCommand('mkdir -p src/new-dir')).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('stdout redirect to file', () => {
|
|
79
|
+
expect(isSafeCommand('echo "hello" > file.txt')).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('append redirect', () => {
|
|
83
|
+
expect(isSafeCommand('echo "hello" >> file.txt')).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('git commit', () => {
|
|
87
|
+
expect(isSafeCommand('git commit -m "test"')).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('npm install', () => {
|
|
91
|
+
expect(isSafeCommand('npm install lodash')).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('mv file', () => {
|
|
95
|
+
expect(isSafeCommand('mv old.ts new.ts')).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('sudo anything', () => {
|
|
99
|
+
expect(isSafeCommand('sudo ls')).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('cp file', () => {
|
|
103
|
+
expect(isSafeCommand('cp a.ts b.ts')).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('touch file', () => {
|
|
107
|
+
expect(isSafeCommand('touch newfile.ts')).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -24,7 +24,8 @@ const DESTRUCTIVE_PATTERNS = [
|
|
|
24
24
|
/\btruncate\b/i,
|
|
25
25
|
/\bdd\b/i,
|
|
26
26
|
/\bshred\b/i,
|
|
27
|
-
/
|
|
27
|
+
// stdout redirect — but NOT stderr redirects like 2>/dev/null or 2>&1
|
|
28
|
+
/(?<!\d)>(?!>|&)/,
|
|
28
29
|
/>>/,
|
|
29
30
|
/\bnpm\s+(install|uninstall|update|ci|link|publish)/i,
|
|
30
31
|
/\byarn\s+(add|remove|install|publish)/i,
|
|
@@ -117,10 +118,70 @@ const SAFE_PATTERNS = [
|
|
|
117
118
|
/^\s*tasklist\b/,
|
|
118
119
|
];
|
|
119
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Check if a command is safe for plan mode.
|
|
123
|
+
*
|
|
124
|
+
* For simple commands, checks that the command starts with a safe pattern and
|
|
125
|
+
* doesn't contain destructive patterns. For piped commands, each segment is
|
|
126
|
+
* checked individually. Special exceptions allow `mkdir -p` for `.plans/` paths.
|
|
127
|
+
*/
|
|
120
128
|
export function isSafeCommand(command: string): boolean {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
// Special case: allow mkdir for .plans/ directory (planner needs this)
|
|
130
|
+
if (/^\s*mkdir\s+(-p\s+)?\.plans(\/|\\|\s|$)/.test(command)) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Split piped commands and check each segment independently
|
|
135
|
+
const segments = splitPipeSegments(command);
|
|
136
|
+
return segments.every((seg) => {
|
|
137
|
+
const trimmed = seg.trim();
|
|
138
|
+
const isDestructive = DESTRUCTIVE_PATTERNS.some((p) => p.test(trimmed));
|
|
139
|
+
const isSafe = SAFE_PATTERNS.some((p) => p.test(trimmed));
|
|
140
|
+
return !isDestructive && isSafe;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Split a command on unquoted pipe (`|`) characters.
|
|
146
|
+
* Respects single/double quotes and escaped characters.
|
|
147
|
+
*/
|
|
148
|
+
function splitPipeSegments(command: string): string[] {
|
|
149
|
+
const segments: string[] = [];
|
|
150
|
+
let current = '';
|
|
151
|
+
let inSingle = false;
|
|
152
|
+
let inDouble = false;
|
|
153
|
+
let escaped = false;
|
|
154
|
+
|
|
155
|
+
for (const ch of command) {
|
|
156
|
+
if (escaped) {
|
|
157
|
+
current += ch;
|
|
158
|
+
escaped = false;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (ch === '\\') {
|
|
162
|
+
current += ch;
|
|
163
|
+
escaped = true;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (ch === "'" && !inDouble) {
|
|
167
|
+
inSingle = !inSingle;
|
|
168
|
+
current += ch;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (ch === '"' && !inSingle) {
|
|
172
|
+
inDouble = !inDouble;
|
|
173
|
+
current += ch;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (ch === '|' && !inSingle && !inDouble) {
|
|
177
|
+
segments.push(current);
|
|
178
|
+
current = '';
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
current += ch;
|
|
182
|
+
}
|
|
183
|
+
if (current) segments.push(current);
|
|
184
|
+
return segments;
|
|
124
185
|
}
|
|
125
186
|
|
|
126
187
|
// ── Plan extraction ─────────────────────────────────────────────────────────
|
package/package.json
CHANGED