@dreki-gg/pi-plan-mode 0.5.0 → 0.6.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/CHANGELOG.md +26 -0
- package/extensions/plan-mode/utils.test.ts +110 -0
- package/extensions/plan-mode/utils.ts +31 -112
- package/node_modules/@dreki-gg/pi-command-sandbox/dist/index.d.mts +104 -0
- package/node_modules/@dreki-gg/pi-command-sandbox/dist/index.mjs +396 -0
- package/node_modules/@dreki-gg/pi-command-sandbox/package.json +42 -0
- package/package.json +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @dreki-gg/pi-plan-mode
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`2a08c1d`](https://github.com/dreki-gg/pi-extensions/commit/2a08c1d0b10a1ca74dfab74f93dd200570537e0f) Thanks [@jalbarrang](https://github.com/jalbarrang)! - feat(ask-mode, plan-mode): support concatenated shell commands in sandbox validation
|
|
8
|
+
|
|
9
|
+
Commands using `&&`, `||`, and `;` operators are now parsed and validated per-segment instead of being blocked outright. Uses `shell-quote` for proper shell tokenization that respects quoted strings, subshells, and redirects.
|
|
10
|
+
|
|
11
|
+
Previously, safe commands like `cd src && ls -la` or `git status && git log` were incorrectly blocked because the sandbox only split on pipes (`|`). Now each segment is validated independently against the safe/destructive pattern lists.
|
|
12
|
+
|
|
13
|
+
Also adds `cd`, `basename`, `dirname`, `realpath`, `readlink`, and `bun pm ls` to the safe commands list, and blocks command substitution (`$(...)` and backticks) by default.
|
|
14
|
+
|
|
15
|
+
Shared sandbox logic extracted to private `@dreki-gg/pi-command-sandbox` package (bundled into published tarballs via `bundledDependencies`).
|
|
16
|
+
|
|
17
|
+
## 0.5.1
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- [`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
|
|
22
|
+
|
|
23
|
+
Three fixes to `isSafeCommand`:
|
|
24
|
+
|
|
25
|
+
- Allow `mkdir -p .plans/` since the planner needs to create plan directories
|
|
26
|
+
- Fix redirect pattern to not false-positive on stderr redirects like `2>/dev/null`
|
|
27
|
+
- Split piped commands and validate each segment independently, so `curl ... | grep ... | head` works correctly
|
|
28
|
+
|
|
3
29
|
## 0.5.0
|
|
4
30
|
|
|
5
31
|
### 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
|
+
});
|
|
@@ -1,126 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure utility functions for plan mode.
|
|
3
|
+
*
|
|
4
|
+
* Command sandboxing is delegated to @dreki-gg/pi-command-sandbox.
|
|
3
5
|
*/
|
|
4
6
|
|
|
7
|
+
import { isSafeCommand as baseSafeCommand } from '@dreki-gg/pi-command-sandbox';
|
|
8
|
+
|
|
5
9
|
export interface TodoItem {
|
|
6
10
|
step: number;
|
|
7
11
|
text: string;
|
|
8
12
|
completed: boolean;
|
|
9
13
|
}
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
/\bmkdir\b/i,
|
|
18
|
-
/\btouch\b/i,
|
|
19
|
-
/\bchmod\b/i,
|
|
20
|
-
/\bchown\b/i,
|
|
21
|
-
/\bchgrp\b/i,
|
|
22
|
-
/\bln\b/i,
|
|
23
|
-
/\btee\b/i,
|
|
24
|
-
/\btruncate\b/i,
|
|
25
|
-
/\bdd\b/i,
|
|
26
|
-
/\bshred\b/i,
|
|
27
|
-
/(^|[^<])>(?!>)/,
|
|
28
|
-
/>>/,
|
|
29
|
-
/\bnpm\s+(install|uninstall|update|ci|link|publish)/i,
|
|
30
|
-
/\byarn\s+(add|remove|install|publish)/i,
|
|
31
|
-
/\bpnpm\s+(add|remove|install|publish)/i,
|
|
32
|
-
/\bpip\s+(install|uninstall)/i,
|
|
33
|
-
/\bapt(-get)?\s+(install|remove|purge|update|upgrade)/i,
|
|
34
|
-
/\bbrew\s+(install|uninstall|upgrade)/i,
|
|
35
|
-
/\bgit\s+(add|commit|push|pull|merge|rebase|reset|checkout|branch\s+-[dD]|stash|cherry-pick|revert|tag|init|clone)/i,
|
|
36
|
-
/\bsudo\b/i,
|
|
37
|
-
/\bsu\b/i,
|
|
38
|
-
/\bkill\b/i,
|
|
39
|
-
/\bpkill\b/i,
|
|
40
|
-
/\bkillall\b/i,
|
|
41
|
-
/\breboot\b/i,
|
|
42
|
-
/\bshutdown\b/i,
|
|
43
|
-
/\bsystemctl\s+(start|stop|restart|enable|disable)/i,
|
|
44
|
-
/\bservice\s+\S+\s+(start|stop|restart)/i,
|
|
45
|
-
/\b(vim?|nano|emacs|code|subl)\b/i,
|
|
46
|
-
// Windows equivalents
|
|
47
|
-
/\bdel\b/i,
|
|
48
|
-
/\brd\b/i,
|
|
49
|
-
/\bcopy\b/i,
|
|
50
|
-
/\bmove\b/i,
|
|
51
|
-
/\bren\b/i,
|
|
52
|
-
/\brename\b/i,
|
|
53
|
-
/\bicacls\b/i,
|
|
54
|
-
/\battrib\b/i,
|
|
55
|
-
/\bpowershell\b/i,
|
|
56
|
-
/\bpwsh\b/i,
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
// ── Safe read-only bash patterns (allowed in plan mode) ─────────────────────
|
|
60
|
-
const SAFE_PATTERNS = [
|
|
61
|
-
/^\s*cat\b/,
|
|
62
|
-
/^\s*head\b/,
|
|
63
|
-
/^\s*tail\b/,
|
|
64
|
-
/^\s*less\b/,
|
|
65
|
-
/^\s*more\b/,
|
|
66
|
-
/^\s*grep\b/,
|
|
67
|
-
/^\s*find\b/,
|
|
68
|
-
/^\s*ls\b/,
|
|
69
|
-
/^\s*pwd\b/,
|
|
70
|
-
/^\s*echo\b/,
|
|
71
|
-
/^\s*printf\b/,
|
|
72
|
-
/^\s*wc\b/,
|
|
73
|
-
/^\s*sort\b/,
|
|
74
|
-
/^\s*uniq\b/,
|
|
75
|
-
/^\s*diff\b/,
|
|
76
|
-
/^\s*file\b/,
|
|
77
|
-
/^\s*stat\b/,
|
|
78
|
-
/^\s*du\b/,
|
|
79
|
-
/^\s*df\b/,
|
|
80
|
-
/^\s*tree\b/,
|
|
81
|
-
/^\s*which\b/,
|
|
82
|
-
/^\s*whereis\b/,
|
|
83
|
-
/^\s*type\b/,
|
|
84
|
-
/^\s*env\b/,
|
|
85
|
-
/^\s*printenv\b/,
|
|
86
|
-
/^\s*uname\b/,
|
|
87
|
-
/^\s*whoami\b/,
|
|
88
|
-
/^\s*id\b/,
|
|
89
|
-
/^\s*date\b/,
|
|
90
|
-
/^\s*cal\b/,
|
|
91
|
-
/^\s*uptime\b/,
|
|
92
|
-
/^\s*ps\b/,
|
|
93
|
-
/^\s*top\b/,
|
|
94
|
-
/^\s*htop\b/,
|
|
95
|
-
/^\s*free\b/,
|
|
96
|
-
/^\s*git\s+(status|log|diff|show|branch|remote|config\s+--get)/i,
|
|
97
|
-
/^\s*git\s+ls-/i,
|
|
98
|
-
/^\s*npm\s+(list|ls|view|info|search|outdated|audit)/i,
|
|
99
|
-
/^\s*yarn\s+(list|info|why|audit)/i,
|
|
100
|
-
/^\s*pnpm\s+(list|ls|why|audit|outdated)/i,
|
|
101
|
-
/^\s*node\s+--version/i,
|
|
102
|
-
/^\s*python\s+--version/i,
|
|
103
|
-
/^\s*curl\s/i,
|
|
104
|
-
/^\s*wget\s+-O\s*-/i,
|
|
105
|
-
/^\s*jq\b/,
|
|
106
|
-
/^\s*sed\s+-n/i,
|
|
107
|
-
/^\s*awk\b/,
|
|
108
|
-
/^\s*rg\b/,
|
|
109
|
-
/^\s*fd\b/,
|
|
110
|
-
/^\s*bat\b/,
|
|
111
|
-
/^\s*eza\b/,
|
|
112
|
-
// Windows equivalents
|
|
113
|
-
/^\s*dir\b/,
|
|
114
|
-
/^\s*where\b/,
|
|
115
|
-
/^\s*set\b/,
|
|
116
|
-
/^\s*systeminfo\b/,
|
|
117
|
-
/^\s*tasklist\b/,
|
|
118
|
-
];
|
|
119
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Check if a command is safe for plan mode.
|
|
17
|
+
*
|
|
18
|
+
* Delegates to the shared command sandbox with a custom allow rule
|
|
19
|
+
* for `mkdir -p .plans/` (planner needs to create plan directories).
|
|
20
|
+
*/
|
|
120
21
|
export function isSafeCommand(command: string): boolean {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
22
|
+
return baseSafeCommand(command, {
|
|
23
|
+
allowCommand: (cmd) => isMkdirPlans(cmd) || isCurlWithStderrRedirect(cmd),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Allow mkdir only for .plans/ directory paths. */
|
|
28
|
+
function isMkdirPlans(command: string): boolean {
|
|
29
|
+
return /^\s*mkdir\s+(-p\s+)?\.plans(\/|\\|\s|$)/.test(command);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Allow curl commands that only redirect stderr to /dev/null.
|
|
34
|
+
* shell-quote parses `2>/dev/null` as a stdout redirect, but it's
|
|
35
|
+
* actually a stderr redirect which is safe for read-only mode.
|
|
36
|
+
*/
|
|
37
|
+
function isCurlWithStderrRedirect(command: string): boolean {
|
|
38
|
+
return (
|
|
39
|
+
/^\s*curl\b/.test(command) &&
|
|
40
|
+
/2>\/dev\/null/.test(command) &&
|
|
41
|
+
!/>(?!\/dev\/null)/.test(command.replace(/2>\/dev\/null/g, ''))
|
|
42
|
+
);
|
|
124
43
|
}
|
|
125
44
|
|
|
126
45
|
// ── Plan extraction ─────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
//#region src/sandbox.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Core sandbox logic — determines whether a shell command is safe to execute.
|
|
4
|
+
*/
|
|
5
|
+
interface SandboxOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Extra patterns to allow beyond the built-in SAFE_PATTERNS.
|
|
8
|
+
* Checked against the reconstructed command string for each segment.
|
|
9
|
+
*/
|
|
10
|
+
extraSafe?: RegExp[];
|
|
11
|
+
/**
|
|
12
|
+
* Extra patterns to block beyond the built-in DESTRUCTIVE_PATTERNS.
|
|
13
|
+
* Checked against the reconstructed command string for each segment.
|
|
14
|
+
*/
|
|
15
|
+
extraDestructive?: RegExp[];
|
|
16
|
+
/**
|
|
17
|
+
* Custom predicate to allow specific full commands before normal checks.
|
|
18
|
+
* Return `true` to allow the command, `false` to continue with normal checks.
|
|
19
|
+
*/
|
|
20
|
+
allowCommand?: (command: string) => boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Whether to allow stdout redirects (`>`, `>>`).
|
|
23
|
+
* Default: false (blocked).
|
|
24
|
+
*/
|
|
25
|
+
allowRedirects?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Whether to allow command substitution ($(...) and backticks).
|
|
28
|
+
* Default: false (blocked).
|
|
29
|
+
*/
|
|
30
|
+
allowCommandSubstitution?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Check if a shell command is safe to execute in a sandboxed mode.
|
|
34
|
+
*
|
|
35
|
+
* The command is parsed into segments using shell-quote, and each segment
|
|
36
|
+
* is checked independently against the destructive and safe pattern lists.
|
|
37
|
+
* A command is safe only if ALL segments pass.
|
|
38
|
+
*
|
|
39
|
+
* For a segment to pass:
|
|
40
|
+
* 1. It must NOT match any destructive pattern
|
|
41
|
+
* 2. It MUST match at least one safe pattern
|
|
42
|
+
* 3. It must NOT contain redirects (unless explicitly allowed)
|
|
43
|
+
*
|
|
44
|
+
* Additionally, command substitution ($() and backticks) is blocked by default.
|
|
45
|
+
*/
|
|
46
|
+
declare function isSafeCommand(command: string, options?: SandboxOptions): boolean;
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/parser.d.ts
|
|
49
|
+
/**
|
|
50
|
+
* Shell command parser using shell-quote.
|
|
51
|
+
*
|
|
52
|
+
* Splits a full shell command string into individual command segments,
|
|
53
|
+
* properly handling &&, ||, ;, |, pipes, quotes, and subshells.
|
|
54
|
+
*/
|
|
55
|
+
interface ParsedSegment {
|
|
56
|
+
/** The reconstructed command string for this segment. */
|
|
57
|
+
command: string;
|
|
58
|
+
/** Whether this segment contains a redirect operator. */
|
|
59
|
+
hasRedirect: boolean;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Parse a shell command into individual command segments.
|
|
63
|
+
*
|
|
64
|
+
* Uses shell-quote to properly tokenize the input, then splits on
|
|
65
|
+
* command separators (&&, ||, ;, |). Each segment is returned as a
|
|
66
|
+
* reconstructed command string.
|
|
67
|
+
*
|
|
68
|
+
* Subshell grouping operators `(` and `)` are stripped — the inner
|
|
69
|
+
* commands are still validated individually.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* parseCommandSegments('cd foo && ls -la')
|
|
74
|
+
* // → [{ command: 'cd foo', hasRedirect: false },
|
|
75
|
+
* // { command: 'ls -la', hasRedirect: false }]
|
|
76
|
+
*
|
|
77
|
+
* parseCommandSegments('echo "hello && world"')
|
|
78
|
+
* // → [{ command: 'echo "hello && world"', hasRedirect: false }]
|
|
79
|
+
*
|
|
80
|
+
* parseCommandSegments('curl -s url 2>/dev/null | head')
|
|
81
|
+
* // → [{ command: 'curl -s url 2>/dev/null', hasRedirect: false },
|
|
82
|
+
* // { command: 'head', hasRedirect: false }]
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
declare function parseCommandSegments(input: string): ParsedSegment[];
|
|
86
|
+
/**
|
|
87
|
+
* Check if the parsed tokens contain command substitution patterns.
|
|
88
|
+
*
|
|
89
|
+
* Command substitution ($(...) or `...`) can hide arbitrary commands
|
|
90
|
+
* and should be blocked in sandboxed modes.
|
|
91
|
+
*/
|
|
92
|
+
declare function hasCommandSubstitution(input: string): boolean;
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/patterns.d.ts
|
|
95
|
+
/**
|
|
96
|
+
* Destructive and safe command patterns for shell sandboxing.
|
|
97
|
+
*
|
|
98
|
+
* DESTRUCTIVE_PATTERNS — if any segment matches, the command is blocked.
|
|
99
|
+
* SAFE_PATTERNS — a segment must match at least one to be allowed.
|
|
100
|
+
*/
|
|
101
|
+
declare const DESTRUCTIVE_PATTERNS: RegExp[];
|
|
102
|
+
declare const SAFE_PATTERNS: RegExp[];
|
|
103
|
+
//#endregion
|
|
104
|
+
export { DESTRUCTIVE_PATTERNS, type ParsedSegment, SAFE_PATTERNS, type SandboxOptions, hasCommandSubstitution, isSafeCommand, parseCommandSegments };
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
3
|
+
//#endregion
|
|
4
|
+
//#region ../../node_modules/shell-quote/quote.js
|
|
5
|
+
var require_quote = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
6
|
+
module.exports = function quote(xs) {
|
|
7
|
+
return xs.map(function(s) {
|
|
8
|
+
if (s === "") return "''";
|
|
9
|
+
if (s && typeof s === "object") return s.op.replace(/(.)/g, "\\$1");
|
|
10
|
+
if (/["\s\\]/.test(s) && !/'/.test(s)) return "'" + s.replace(/(['])/g, "\\$1") + "'";
|
|
11
|
+
if (/["'\s]/.test(s)) return "\"" + s.replace(/(["\\$`!])/g, "\\$1") + "\"";
|
|
12
|
+
return String(s).replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}])/g, "$1\\$2");
|
|
13
|
+
}).join(" ");
|
|
14
|
+
};
|
|
15
|
+
}));
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region ../../node_modules/shell-quote/parse.js
|
|
18
|
+
var require_parse = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
19
|
+
var CONTROL = "(?:" + [
|
|
20
|
+
"\\|\\|",
|
|
21
|
+
"\\&\\&",
|
|
22
|
+
";;",
|
|
23
|
+
"\\|\\&",
|
|
24
|
+
"\\<\\(",
|
|
25
|
+
"\\<\\<\\<",
|
|
26
|
+
">>",
|
|
27
|
+
">\\&",
|
|
28
|
+
"<\\&",
|
|
29
|
+
"[&;()|<>]"
|
|
30
|
+
].join("|") + ")";
|
|
31
|
+
var controlRE = new RegExp("^" + CONTROL + "$");
|
|
32
|
+
var META = "|&;()<> \\t";
|
|
33
|
+
var SINGLE_QUOTE = "\"((\\\\\"|[^\"])*?)\"";
|
|
34
|
+
var DOUBLE_QUOTE = "'((\\\\'|[^'])*?)'";
|
|
35
|
+
var hash = /^#$/;
|
|
36
|
+
var SQ = "'";
|
|
37
|
+
var DQ = "\"";
|
|
38
|
+
var DS = "$";
|
|
39
|
+
var TOKEN = "";
|
|
40
|
+
var mult = 4294967296;
|
|
41
|
+
for (var i = 0; i < 4; i++) TOKEN += (mult * Math.random()).toString(16);
|
|
42
|
+
var startsWithToken = new RegExp("^" + TOKEN);
|
|
43
|
+
function matchAll(s, r) {
|
|
44
|
+
var origIndex = r.lastIndex;
|
|
45
|
+
var matches = [];
|
|
46
|
+
var matchObj;
|
|
47
|
+
while (matchObj = r.exec(s)) {
|
|
48
|
+
matches.push(matchObj);
|
|
49
|
+
if (r.lastIndex === matchObj.index) r.lastIndex += 1;
|
|
50
|
+
}
|
|
51
|
+
r.lastIndex = origIndex;
|
|
52
|
+
return matches;
|
|
53
|
+
}
|
|
54
|
+
function getVar(env, pre, key) {
|
|
55
|
+
var r = typeof env === "function" ? env(key) : env[key];
|
|
56
|
+
if (typeof r === "undefined" && key != "") r = "";
|
|
57
|
+
else if (typeof r === "undefined") r = "$";
|
|
58
|
+
if (typeof r === "object") return pre + TOKEN + JSON.stringify(r) + TOKEN;
|
|
59
|
+
return pre + r;
|
|
60
|
+
}
|
|
61
|
+
function parseInternal(string, env, opts) {
|
|
62
|
+
if (!opts) opts = {};
|
|
63
|
+
var BS = opts.escape || "\\";
|
|
64
|
+
var BAREWORD = "(\\" + BS + "['\"" + META + "]|[^\\s'\"" + META + "])+";
|
|
65
|
+
var matches = matchAll(string, new RegExp(["(" + CONTROL + ")", "(" + BAREWORD + "|" + SINGLE_QUOTE + "|" + DOUBLE_QUOTE + ")+"].join("|"), "g"));
|
|
66
|
+
if (matches.length === 0) return [];
|
|
67
|
+
if (!env) env = {};
|
|
68
|
+
var commented = false;
|
|
69
|
+
return matches.map(function(match) {
|
|
70
|
+
var s = match[0];
|
|
71
|
+
if (!s || commented) return;
|
|
72
|
+
if (controlRE.test(s)) return { op: s };
|
|
73
|
+
var quote = false;
|
|
74
|
+
var esc = false;
|
|
75
|
+
var out = "";
|
|
76
|
+
var isGlob = false;
|
|
77
|
+
var i;
|
|
78
|
+
function parseEnvVar() {
|
|
79
|
+
i += 1;
|
|
80
|
+
var varend;
|
|
81
|
+
var varname;
|
|
82
|
+
var char = s.charAt(i);
|
|
83
|
+
if (char === "{") {
|
|
84
|
+
i += 1;
|
|
85
|
+
if (s.charAt(i) === "}") throw new Error("Bad substitution: " + s.slice(i - 2, i + 1));
|
|
86
|
+
varend = s.indexOf("}", i);
|
|
87
|
+
if (varend < 0) throw new Error("Bad substitution: " + s.slice(i));
|
|
88
|
+
varname = s.slice(i, varend);
|
|
89
|
+
i = varend;
|
|
90
|
+
} else if (/[*@#?$!_-]/.test(char)) {
|
|
91
|
+
varname = char;
|
|
92
|
+
i += 1;
|
|
93
|
+
} else {
|
|
94
|
+
var slicedFromI = s.slice(i);
|
|
95
|
+
varend = slicedFromI.match(/[^\w\d_]/);
|
|
96
|
+
if (!varend) {
|
|
97
|
+
varname = slicedFromI;
|
|
98
|
+
i = s.length;
|
|
99
|
+
} else {
|
|
100
|
+
varname = slicedFromI.slice(0, varend.index);
|
|
101
|
+
i += varend.index - 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return getVar(env, "", varname);
|
|
105
|
+
}
|
|
106
|
+
for (i = 0; i < s.length; i++) {
|
|
107
|
+
var c = s.charAt(i);
|
|
108
|
+
isGlob = isGlob || !quote && (c === "*" || c === "?");
|
|
109
|
+
if (esc) {
|
|
110
|
+
out += c;
|
|
111
|
+
esc = false;
|
|
112
|
+
} else if (quote) if (c === quote) quote = false;
|
|
113
|
+
else if (quote == SQ) out += c;
|
|
114
|
+
else if (c === BS) {
|
|
115
|
+
i += 1;
|
|
116
|
+
c = s.charAt(i);
|
|
117
|
+
if (c === DQ || c === BS || c === DS) out += c;
|
|
118
|
+
else out += BS + c;
|
|
119
|
+
} else if (c === DS) out += parseEnvVar();
|
|
120
|
+
else out += c;
|
|
121
|
+
else if (c === DQ || c === SQ) quote = c;
|
|
122
|
+
else if (controlRE.test(c)) return { op: s };
|
|
123
|
+
else if (hash.test(c)) {
|
|
124
|
+
commented = true;
|
|
125
|
+
var commentObj = { comment: string.slice(match.index + i + 1) };
|
|
126
|
+
if (out.length) return [out, commentObj];
|
|
127
|
+
return [commentObj];
|
|
128
|
+
} else if (c === BS) esc = true;
|
|
129
|
+
else if (c === DS) out += parseEnvVar();
|
|
130
|
+
else out += c;
|
|
131
|
+
}
|
|
132
|
+
if (isGlob) return {
|
|
133
|
+
op: "glob",
|
|
134
|
+
pattern: out
|
|
135
|
+
};
|
|
136
|
+
return out;
|
|
137
|
+
}).reduce(function(prev, arg) {
|
|
138
|
+
return typeof arg === "undefined" ? prev : prev.concat(arg);
|
|
139
|
+
}, []);
|
|
140
|
+
}
|
|
141
|
+
module.exports = function parse(s, env, opts) {
|
|
142
|
+
var mapped = parseInternal(s, env, opts);
|
|
143
|
+
if (typeof env !== "function") return mapped;
|
|
144
|
+
return mapped.reduce(function(acc, s) {
|
|
145
|
+
if (typeof s === "object") return acc.concat(s);
|
|
146
|
+
var xs = s.split(RegExp("(" + TOKEN + ".*?" + TOKEN + ")", "g"));
|
|
147
|
+
if (xs.length === 1) return acc.concat(xs[0]);
|
|
148
|
+
return acc.concat(xs.filter(Boolean).map(function(x) {
|
|
149
|
+
if (startsWithToken.test(x)) return JSON.parse(x.split(TOKEN)[1]);
|
|
150
|
+
return x;
|
|
151
|
+
}));
|
|
152
|
+
}, []);
|
|
153
|
+
};
|
|
154
|
+
}));
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/parser.ts
|
|
157
|
+
var import_shell_quote = (/* @__PURE__ */ __commonJSMin(((exports) => {
|
|
158
|
+
exports.quote = require_quote();
|
|
159
|
+
exports.parse = require_parse();
|
|
160
|
+
})))();
|
|
161
|
+
/** Operators that separate independent commands. */
|
|
162
|
+
const COMMAND_SEPARATORS = new Set([
|
|
163
|
+
"&&",
|
|
164
|
+
"||",
|
|
165
|
+
";",
|
|
166
|
+
"|"
|
|
167
|
+
]);
|
|
168
|
+
/** Operators that indicate potentially dangerous constructs. */
|
|
169
|
+
const DANGEROUS_OPS = new Set([">", ">>"]);
|
|
170
|
+
/** Grouping operators (subshells) — we strip them and validate inner commands. */
|
|
171
|
+
const GROUPING_OPS = new Set(["(", ")"]);
|
|
172
|
+
/**
|
|
173
|
+
* Parse a shell command into individual command segments.
|
|
174
|
+
*
|
|
175
|
+
* Uses shell-quote to properly tokenize the input, then splits on
|
|
176
|
+
* command separators (&&, ||, ;, |). Each segment is returned as a
|
|
177
|
+
* reconstructed command string.
|
|
178
|
+
*
|
|
179
|
+
* Subshell grouping operators `(` and `)` are stripped — the inner
|
|
180
|
+
* commands are still validated individually.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* parseCommandSegments('cd foo && ls -la')
|
|
185
|
+
* // → [{ command: 'cd foo', hasRedirect: false },
|
|
186
|
+
* // { command: 'ls -la', hasRedirect: false }]
|
|
187
|
+
*
|
|
188
|
+
* parseCommandSegments('echo "hello && world"')
|
|
189
|
+
* // → [{ command: 'echo "hello && world"', hasRedirect: false }]
|
|
190
|
+
*
|
|
191
|
+
* parseCommandSegments('curl -s url 2>/dev/null | head')
|
|
192
|
+
* // → [{ command: 'curl -s url 2>/dev/null', hasRedirect: false },
|
|
193
|
+
* // { command: 'head', hasRedirect: false }]
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
function parseCommandSegments(input) {
|
|
197
|
+
const tokens = (0, import_shell_quote.parse)(input);
|
|
198
|
+
const segments = [];
|
|
199
|
+
let currentTokens = [];
|
|
200
|
+
let hasRedirect = false;
|
|
201
|
+
function flushSegment() {
|
|
202
|
+
if (currentTokens.length > 0) {
|
|
203
|
+
segments.push({
|
|
204
|
+
command: currentTokens.join(" "),
|
|
205
|
+
hasRedirect
|
|
206
|
+
});
|
|
207
|
+
currentTokens = [];
|
|
208
|
+
hasRedirect = false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
for (const token of tokens) {
|
|
212
|
+
if (typeof token === "string") {
|
|
213
|
+
currentTokens.push(token);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!("op" in token)) {
|
|
217
|
+
currentTokens.push(String(token));
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const { op } = token;
|
|
221
|
+
if (COMMAND_SEPARATORS.has(op)) {
|
|
222
|
+
flushSegment();
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (DANGEROUS_OPS.has(op)) {
|
|
226
|
+
hasRedirect = true;
|
|
227
|
+
currentTokens.push(op);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (GROUPING_OPS.has(op)) continue;
|
|
231
|
+
currentTokens.push(op);
|
|
232
|
+
}
|
|
233
|
+
flushSegment();
|
|
234
|
+
return segments;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Check if the parsed tokens contain command substitution patterns.
|
|
238
|
+
*
|
|
239
|
+
* Command substitution ($(...) or `...`) can hide arbitrary commands
|
|
240
|
+
* and should be blocked in sandboxed modes.
|
|
241
|
+
*/
|
|
242
|
+
function hasCommandSubstitution(input) {
|
|
243
|
+
return /\$\(/.test(input) || /`[^`]+`/.test(input);
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/patterns.ts
|
|
247
|
+
/**
|
|
248
|
+
* Destructive and safe command patterns for shell sandboxing.
|
|
249
|
+
*
|
|
250
|
+
* DESTRUCTIVE_PATTERNS — if any segment matches, the command is blocked.
|
|
251
|
+
* SAFE_PATTERNS — a segment must match at least one to be allowed.
|
|
252
|
+
*/
|
|
253
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
254
|
+
/\brm\b/i,
|
|
255
|
+
/\brmdir\b/i,
|
|
256
|
+
/\bmv\b/i,
|
|
257
|
+
/\bcp\b/i,
|
|
258
|
+
/\bmkdir\b/i,
|
|
259
|
+
/\btouch\b/i,
|
|
260
|
+
/\bchmod\b/i,
|
|
261
|
+
/\bchown\b/i,
|
|
262
|
+
/\bchgrp\b/i,
|
|
263
|
+
/\bln\b/i,
|
|
264
|
+
/\btee\b/i,
|
|
265
|
+
/\btruncate\b/i,
|
|
266
|
+
/\bdd\b/i,
|
|
267
|
+
/\bshred\b/i,
|
|
268
|
+
/\bnpm\s+(install|uninstall|update|ci|link|publish)/i,
|
|
269
|
+
/\byarn\s+(add|remove|install|publish)/i,
|
|
270
|
+
/\bpnpm\s+(add|remove|install|publish)/i,
|
|
271
|
+
/\bpip\s+(install|uninstall)/i,
|
|
272
|
+
/\bapt(-get)?\s+(install|remove|purge|update|upgrade)/i,
|
|
273
|
+
/\bbrew\s+(install|uninstall|upgrade)/i,
|
|
274
|
+
/\bgit\s+(add|commit|push|pull|merge|rebase|reset|checkout|branch\s+-[dD]|stash|cherry-pick|revert|tag|init|clone)/i,
|
|
275
|
+
/\bsudo\b/i,
|
|
276
|
+
/\bsu\b/i,
|
|
277
|
+
/\bkill\b/i,
|
|
278
|
+
/\bpkill\b/i,
|
|
279
|
+
/\bkillall\b/i,
|
|
280
|
+
/\breboot\b/i,
|
|
281
|
+
/\bshutdown\b/i,
|
|
282
|
+
/\bsystemctl\s+(start|stop|restart|enable|disable)/i,
|
|
283
|
+
/\bservice\s+\S+\s+(start|stop|restart)/i,
|
|
284
|
+
/\b(vim?|nano|emacs|code|subl)\b/i,
|
|
285
|
+
/\bdel\b/i,
|
|
286
|
+
/\brd\b/i,
|
|
287
|
+
/\bcopy\b/i,
|
|
288
|
+
/\bmove\b/i,
|
|
289
|
+
/\bren\b/i,
|
|
290
|
+
/\brename\b/i,
|
|
291
|
+
/\bicacls\b/i,
|
|
292
|
+
/\battrib\b/i,
|
|
293
|
+
/\bpowershell\b/i,
|
|
294
|
+
/\bpwsh\b/i
|
|
295
|
+
];
|
|
296
|
+
const SAFE_PATTERNS = [
|
|
297
|
+
/^\s*cat\b/,
|
|
298
|
+
/^\s*head\b/,
|
|
299
|
+
/^\s*tail\b/,
|
|
300
|
+
/^\s*less\b/,
|
|
301
|
+
/^\s*more\b/,
|
|
302
|
+
/^\s*grep\b/,
|
|
303
|
+
/^\s*find\b/,
|
|
304
|
+
/^\s*ls\b/,
|
|
305
|
+
/^\s*pwd\b/,
|
|
306
|
+
/^\s*echo\b/,
|
|
307
|
+
/^\s*printf\b/,
|
|
308
|
+
/^\s*wc\b/,
|
|
309
|
+
/^\s*sort\b/,
|
|
310
|
+
/^\s*uniq\b/,
|
|
311
|
+
/^\s*diff\b/,
|
|
312
|
+
/^\s*file\b/,
|
|
313
|
+
/^\s*stat\b/,
|
|
314
|
+
/^\s*du\b/,
|
|
315
|
+
/^\s*df\b/,
|
|
316
|
+
/^\s*tree\b/,
|
|
317
|
+
/^\s*which\b/,
|
|
318
|
+
/^\s*whereis\b/,
|
|
319
|
+
/^\s*type\b/,
|
|
320
|
+
/^\s*env\b/,
|
|
321
|
+
/^\s*printenv\b/,
|
|
322
|
+
/^\s*uname\b/,
|
|
323
|
+
/^\s*whoami\b/,
|
|
324
|
+
/^\s*id\b/,
|
|
325
|
+
/^\s*date\b/,
|
|
326
|
+
/^\s*cal\b/,
|
|
327
|
+
/^\s*uptime\b/,
|
|
328
|
+
/^\s*ps\b/,
|
|
329
|
+
/^\s*top\b/,
|
|
330
|
+
/^\s*htop\b/,
|
|
331
|
+
/^\s*free\b/,
|
|
332
|
+
/^\s*cd\b/,
|
|
333
|
+
/^\s*git\s+(status|log|diff|show|branch|remote|config\s+--get)/i,
|
|
334
|
+
/^\s*git\s+ls-/i,
|
|
335
|
+
/^\s*npm\s+(list|ls|view|info|search|outdated|audit)/i,
|
|
336
|
+
/^\s*yarn\s+(list|info|why|audit)/i,
|
|
337
|
+
/^\s*pnpm\s+(list|ls|why|audit|outdated)/i,
|
|
338
|
+
/^\s*bun\s+(pm\s+ls)/i,
|
|
339
|
+
/^\s*node\s+--version/i,
|
|
340
|
+
/^\s*python\s+--version/i,
|
|
341
|
+
/^\s*curl\s/i,
|
|
342
|
+
/^\s*wget\s+-O\s*-/i,
|
|
343
|
+
/^\s*jq\b/,
|
|
344
|
+
/^\s*sed\s+-n/i,
|
|
345
|
+
/^\s*awk\b/,
|
|
346
|
+
/^\s*rg\b/,
|
|
347
|
+
/^\s*fd\b/,
|
|
348
|
+
/^\s*bat\b/,
|
|
349
|
+
/^\s*eza\b/,
|
|
350
|
+
/^\s*basename\b/,
|
|
351
|
+
/^\s*dirname\b/,
|
|
352
|
+
/^\s*realpath\b/,
|
|
353
|
+
/^\s*readlink\b/,
|
|
354
|
+
/^\s*dir\b/,
|
|
355
|
+
/^\s*where\b/,
|
|
356
|
+
/^\s*set\b/,
|
|
357
|
+
/^\s*systeminfo\b/,
|
|
358
|
+
/^\s*tasklist\b/
|
|
359
|
+
];
|
|
360
|
+
//#endregion
|
|
361
|
+
//#region src/sandbox.ts
|
|
362
|
+
/**
|
|
363
|
+
* Core sandbox logic — determines whether a shell command is safe to execute.
|
|
364
|
+
*/
|
|
365
|
+
/**
|
|
366
|
+
* Check if a shell command is safe to execute in a sandboxed mode.
|
|
367
|
+
*
|
|
368
|
+
* The command is parsed into segments using shell-quote, and each segment
|
|
369
|
+
* is checked independently against the destructive and safe pattern lists.
|
|
370
|
+
* A command is safe only if ALL segments pass.
|
|
371
|
+
*
|
|
372
|
+
* For a segment to pass:
|
|
373
|
+
* 1. It must NOT match any destructive pattern
|
|
374
|
+
* 2. It MUST match at least one safe pattern
|
|
375
|
+
* 3. It must NOT contain redirects (unless explicitly allowed)
|
|
376
|
+
*
|
|
377
|
+
* Additionally, command substitution ($() and backticks) is blocked by default.
|
|
378
|
+
*/
|
|
379
|
+
function isSafeCommand(command, options = {}) {
|
|
380
|
+
const { extraSafe = [], extraDestructive = [], allowCommand, allowRedirects = false, allowCommandSubstitution = false } = options;
|
|
381
|
+
if (allowCommand?.(command)) return true;
|
|
382
|
+
if (!allowCommandSubstitution && hasCommandSubstitution(command)) return false;
|
|
383
|
+
const allDestructive = [...DESTRUCTIVE_PATTERNS, ...extraDestructive];
|
|
384
|
+
const allSafe = [...SAFE_PATTERNS, ...extraSafe];
|
|
385
|
+
const segments = parseCommandSegments(command);
|
|
386
|
+
if (segments.length === 0) return false;
|
|
387
|
+
return segments.every((segment) => {
|
|
388
|
+
if (!allowRedirects && segment.hasRedirect) return false;
|
|
389
|
+
const cmd = segment.command;
|
|
390
|
+
const isDestructive = allDestructive.some((p) => p.test(cmd));
|
|
391
|
+
const isSafe = allSafe.some((p) => p.test(cmd));
|
|
392
|
+
return !isDestructive && isSafe;
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
//#endregion
|
|
396
|
+
export { DESTRUCTIVE_PATTERNS, SAFE_PATTERNS, hasCommandSubstitution, isSafeCommand, parseCommandSegments };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dreki-gg/pi-command-sandbox",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Shared command sandboxing utilities for pi extensions — validates shell commands against safe/destructive pattern lists using proper shell parsing",
|
|
6
|
+
"author": "Juan Albarran <jalbarrandev@gmail.com>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/dreki-gg/pi-extensions",
|
|
11
|
+
"directory": "packages/command-sandbox"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.mjs",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": "./dist/index.mjs",
|
|
18
|
+
"types": "./dist/index.d.mts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": ["dist"],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsdown",
|
|
24
|
+
"test": "bun test",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"lint": "oxlint src",
|
|
27
|
+
"format": "oxfmt --write src",
|
|
28
|
+
"format:check": "oxfmt --check src"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"shell-quote": "^1.8.3"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "24",
|
|
35
|
+
"@types/shell-quote": "^1.7.5",
|
|
36
|
+
"bun-types": "latest",
|
|
37
|
+
"oxfmt": "^0.43.0",
|
|
38
|
+
"oxlint": "^1.58.0",
|
|
39
|
+
"tsdown": "^0.22.0",
|
|
40
|
+
"typescript": "^6.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dreki-gg/pi-plan-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Two-phase planning workflow for pi — plan with claude-opus-4-6:medium, execute with gpt-5.5:low, with .plans/ file-based handoff",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
"package.json"
|
|
25
25
|
],
|
|
26
26
|
"scripts": {
|
|
27
|
+
"prepack": "node --experimental-strip-types scripts/prepack.js",
|
|
28
|
+
"postpack": "rm -rf node_modules/@dreki-gg",
|
|
27
29
|
"typecheck": "tsc --noEmit",
|
|
28
30
|
"lint": "oxlint extensions bin",
|
|
29
31
|
"format": "oxfmt --write extensions bin",
|
|
@@ -34,6 +36,12 @@
|
|
|
34
36
|
"./extensions/plan-mode"
|
|
35
37
|
]
|
|
36
38
|
},
|
|
39
|
+
"bundledDependencies": [
|
|
40
|
+
"@dreki-gg/pi-command-sandbox"
|
|
41
|
+
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@dreki-gg/pi-command-sandbox": "workspace:*"
|
|
44
|
+
},
|
|
37
45
|
"devDependencies": {
|
|
38
46
|
"@types/node": "24",
|
|
39
47
|
"bun-types": "latest",
|