@dreki-gg/pi-plan-mode 0.6.4 → 0.10.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.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared types for plan mode.
3
+ */
4
+
5
+ export interface PlanStep {
6
+ description: string;
7
+ details: string;
8
+ status: 'pending' | 'done' | 'skipped' | 'blocked';
9
+ notes?: string;
10
+ }
11
+
12
+ export interface PlanData {
13
+ title: string;
14
+ context: string;
15
+ steps: PlanStep[];
16
+ risks: string;
17
+ }
18
+
19
+ export type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
20
+
21
+ export interface ExecPendingConfig {
22
+ model: { provider: string; id: string };
23
+ thinking: string;
24
+ }
25
+
26
+ export interface PersistedState {
27
+ planEnabled: boolean;
28
+ executing: boolean;
29
+ planDir: string | undefined;
30
+ plan: PlanData | undefined;
31
+ executionStartIdx: number | undefined;
32
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Plan mode UI — status bar and step widget rendering.
3
+ */
4
+
5
+ import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
6
+ import type { PlanModeState } from './state.js';
7
+
8
+ export function updateUI(state: PlanModeState, ctx: ExtensionContext): void {
9
+ const { theme } = ctx.ui;
10
+
11
+ if (state.executing && state.plan) {
12
+ const done = state.plan.steps.filter((s) => s.status === 'done').length;
13
+ const total = state.plan.steps.length;
14
+ ctx.ui.setStatus('plan-mode', theme.fg('accent', `📋 exec ${done}/${total}`));
15
+ } else if (state.planEnabled) {
16
+ ctx.ui.setStatus('plan-mode', theme.fg('warning', '📝 plan'));
17
+ } else {
18
+ ctx.ui.setStatus('plan-mode', undefined);
19
+ }
20
+
21
+ if (state.executing && state.plan) {
22
+ const lines = state.plan.steps.map((step, i) => {
23
+ const num = `${i + 1}. `;
24
+ switch (step.status) {
25
+ case 'done':
26
+ return theme.fg('success', '✓ ') + theme.fg('muted', theme.strikethrough(num + step.description));
27
+ case 'skipped':
28
+ return theme.fg('warning', '⊘ ') + theme.fg('muted', theme.strikethrough(num + step.description));
29
+ case 'blocked':
30
+ return theme.fg('error', '✗ ') + theme.fg('error', num + step.description);
31
+ default:
32
+ return theme.fg('muted', '☐ ') + (num + step.description);
33
+ }
34
+ });
35
+ ctx.ui.setWidget('plan-todos', lines);
36
+ } else {
37
+ ctx.ui.setWidget('plan-todos', undefined);
38
+ }
39
+ }
@@ -6,12 +6,6 @@
6
6
 
7
7
  import { isSafeCommand as baseSafeCommand } from '@dreki-gg/pi-command-sandbox';
8
8
 
9
- export interface TodoItem {
10
- step: number;
11
- text: string;
12
- completed: boolean;
13
- }
14
-
15
9
  /**
16
10
  * Check if a command is safe for plan mode.
17
11
  *
@@ -20,7 +14,7 @@ export interface TodoItem {
20
14
  */
21
15
  export function isSafeCommand(command: string): boolean {
22
16
  return baseSafeCommand(command, {
23
- allowCommand: (cmd) => isMkdirPlans(cmd) || isCurlWithStderrRedirect(cmd),
17
+ allowCommand: (cmd) => isMkdirPlans(cmd),
24
18
  });
25
19
  }
26
20
 
@@ -29,81 +23,6 @@ function isMkdirPlans(command: string): boolean {
29
23
  return /^\s*mkdir\s+(-p\s+)?\.plans(\/|\\|\s|$)/.test(command);
30
24
  }
31
25
 
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
- );
43
- }
44
-
45
- // ── Plan extraction ─────────────────────────────────────────────────────────
46
-
47
- export function cleanStepText(text: string): string {
48
- let cleaned = text
49
- .replace(/\*{1,2}([^*]+)\*{1,2}/g, '$1')
50
- .replace(/`([^`]+)`/g, '$1')
51
- .replace(/\s+/g, ' ')
52
- .trim();
53
-
54
- if (cleaned.length > 0) {
55
- cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
56
- }
57
- if (cleaned.length > 60) {
58
- cleaned = `${cleaned.slice(0, 57)}...`;
59
- }
60
- return cleaned;
61
- }
62
-
63
- export function extractTodoItems(message: string): TodoItem[] {
64
- const items: TodoItem[] = [];
65
- const headerMatch = message.match(/\*{0,2}Plan:\*{0,2}\s*\n/i);
66
- if (!headerMatch) return items;
67
-
68
- const planSection = message.slice(message.indexOf(headerMatch[0]) + headerMatch[0].length);
69
- const numberedPattern = /^\s*(\d+)[.)]\s+\*{0,2}([^*\n]+)/gm;
70
-
71
- for (const match of planSection.matchAll(numberedPattern)) {
72
- const text = match[2]
73
- .trim()
74
- .replace(/\*{1,2}$/, '')
75
- .trim();
76
-
77
- if (text.length <= 5) continue;
78
- if (text.startsWith('`') || text.startsWith('/') || text.startsWith('-')) continue;
79
-
80
- const cleaned = cleanStepText(text);
81
- if (cleaned.length <= 3) continue;
82
-
83
- items.push({ step: items.length + 1, text: cleaned, completed: false });
84
- }
85
-
86
- return items;
87
- }
88
-
89
- export function extractDoneSteps(message: string): number[] {
90
- const steps: number[] = [];
91
- for (const match of message.matchAll(/\[DONE:(\d+)\]/gi)) {
92
- const step = Number(match[1]);
93
- if (Number.isFinite(step)) steps.push(step);
94
- }
95
- return steps;
96
- }
97
-
98
- export function markCompletedSteps(text: string, items: TodoItem[]): number {
99
- const doneSteps = extractDoneSteps(text);
100
- for (const step of doneSteps) {
101
- const item = items.find((t) => t.step === step);
102
- if (item) item.completed = true;
103
- }
104
- return doneSteps.length;
105
- }
106
-
107
26
  // ── Plan name utilities ─────────────────────────────────────────────────────
108
27
 
109
28
  export function toKebabCase(name: string): string {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreki-gg/pi-plan-mode",
3
- "version": "0.6.4",
3
+ "version": "0.10.1",
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,23 +24,19 @@
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",
29
27
  "typecheck": "tsc --noEmit",
30
28
  "lint": "oxlint extensions bin",
31
29
  "format": "oxfmt --write extensions bin",
32
- "format:check": "oxfmt --check extensions bin"
30
+ "format:check": "oxfmt --check extensions bin",
31
+ "test": "bun test extensions/plan-mode/__tests__"
33
32
  },
34
33
  "pi": {
35
34
  "extensions": [
36
35
  "./extensions/plan-mode"
37
36
  ]
38
37
  },
39
- "bundledDependencies": [
40
- "@dreki-gg/pi-command-sandbox"
41
- ],
42
38
  "dependencies": {
43
- "@dreki-gg/pi-command-sandbox": "0.1.0"
39
+ "@dreki-gg/pi-command-sandbox": "^0.2.0"
44
40
  },
45
41
  "devDependencies": {
46
42
  "@types/node": "24",
@@ -1,104 +0,0 @@
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 };