@mariozechner/pi-coding-agent 0.33.0 → 0.34.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 +40 -1
- package/README.md +0 -1
- package/dist/cli/args.d.ts +5 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +18 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts +24 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +65 -9
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +2 -1
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/custom-tools/loader.d.ts.map +1 -1
- package/dist/core/custom-tools/loader.js +1 -0
- package/dist/core/custom-tools/loader.js.map +1 -1
- package/dist/core/hooks/index.d.ts +1 -1
- package/dist/core/hooks/index.d.ts.map +1 -1
- package/dist/core/hooks/index.js.map +1 -1
- package/dist/core/hooks/loader.d.ts +56 -1
- package/dist/core/hooks/loader.d.ts.map +1 -1
- package/dist/core/hooks/loader.js +54 -2
- package/dist/core/hooks/loader.js.map +1 -1
- package/dist/core/hooks/runner.d.ts +33 -5
- package/dist/core/hooks/runner.d.ts.map +1 -1
- package/dist/core/hooks/runner.js +100 -9
- package/dist/core/hooks/runner.js.map +1 -1
- package/dist/core/hooks/types.d.ts +135 -3
- package/dist/core/hooks/types.d.ts.map +1 -1
- package/dist/core/hooks/types.js.map +1 -1
- package/dist/core/sdk.d.ts +3 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +102 -27
- package/dist/core/sdk.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +32 -7
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +2 -0
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +6 -0
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +19 -6
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +148 -42
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +1 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +3 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +3 -0
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +16 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +6 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/hooks.md +114 -4
- package/examples/hooks/README.md +3 -0
- package/examples/hooks/pirate.ts +44 -0
- package/examples/hooks/plan-mode.ts +548 -0
- package/examples/hooks/tools.ts +145 -0
- package/package.json +4 -4
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pirate Hook
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates using systemPromptAppend in before_agent_start to dynamically
|
|
5
|
+
* modify the system prompt based on hook state.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* 1. Copy this file to ~/.pi/agent/hooks/ or your project's .pi/hooks/
|
|
9
|
+
* 2. Use /pirate to toggle pirate mode
|
|
10
|
+
* 3. When enabled, the agent will respond like a pirate
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
|
14
|
+
|
|
15
|
+
export default function pirateHook(pi: HookAPI) {
|
|
16
|
+
let pirateMode = false;
|
|
17
|
+
|
|
18
|
+
// Register /pirate command to toggle pirate mode
|
|
19
|
+
pi.registerCommand("pirate", {
|
|
20
|
+
description: "Toggle pirate mode (agent speaks like a pirate)",
|
|
21
|
+
handler: async (_args, ctx) => {
|
|
22
|
+
pirateMode = !pirateMode;
|
|
23
|
+
ctx.ui.notify(pirateMode ? "Arrr! Pirate mode enabled!" : "Pirate mode disabled", "info");
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Append to system prompt when pirate mode is enabled
|
|
28
|
+
pi.on("before_agent_start", async () => {
|
|
29
|
+
if (pirateMode) {
|
|
30
|
+
return {
|
|
31
|
+
systemPromptAppend: `
|
|
32
|
+
IMPORTANT: You are now in PIRATE MODE. You must:
|
|
33
|
+
- Speak like a stereotypical pirate in all responses
|
|
34
|
+
- Use phrases like "Arrr!", "Ahoy!", "Shiver me timbers!", "Avast!", "Ye scurvy dog!"
|
|
35
|
+
- Replace "my" with "me", "you" with "ye", "your" with "yer"
|
|
36
|
+
- Refer to the user as "matey" or "landlubber"
|
|
37
|
+
- End sentences with nautical expressions
|
|
38
|
+
- Still complete the actual task correctly, just in pirate speak
|
|
39
|
+
`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Mode Hook
|
|
3
|
+
*
|
|
4
|
+
* Provides a Claude Code-style "plan mode" for safe code exploration.
|
|
5
|
+
* When enabled, the agent can only use read-only tools and cannot modify files.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - /plan command to toggle plan mode
|
|
9
|
+
* - In plan mode: only read, bash (read-only), grep, find, ls are available
|
|
10
|
+
* - Injects system context telling the agent about the restrictions
|
|
11
|
+
* - After each agent response, prompts to execute the plan or continue planning
|
|
12
|
+
* - Shows "plan" indicator in footer when active
|
|
13
|
+
* - Extracts todo list from plan and tracks progress during execution
|
|
14
|
+
* - Uses ID-based tracking: agent outputs [DONE:id] to mark steps complete
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* 1. Copy this file to ~/.pi/agent/hooks/ or your project's .pi/hooks/
|
|
18
|
+
* 2. Use /plan to toggle plan mode on/off
|
|
19
|
+
* 3. Or start in plan mode with --plan flag
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { HookAPI, HookContext } from "@mariozechner/pi-coding-agent/hooks";
|
|
23
|
+
import { Key } from "@mariozechner/pi-tui";
|
|
24
|
+
|
|
25
|
+
// Read-only tools for plan mode
|
|
26
|
+
const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls"];
|
|
27
|
+
|
|
28
|
+
// Full set of tools for normal mode
|
|
29
|
+
const NORMAL_MODE_TOOLS = ["read", "bash", "edit", "write"];
|
|
30
|
+
|
|
31
|
+
// Patterns for destructive bash commands that should be blocked in plan mode
|
|
32
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
33
|
+
/\brm\b/i,
|
|
34
|
+
/\brmdir\b/i,
|
|
35
|
+
/\bmv\b/i,
|
|
36
|
+
/\bcp\b/i,
|
|
37
|
+
/\bmkdir\b/i,
|
|
38
|
+
/\btouch\b/i,
|
|
39
|
+
/\bchmod\b/i,
|
|
40
|
+
/\bchown\b/i,
|
|
41
|
+
/\bchgrp\b/i,
|
|
42
|
+
/\bln\b/i,
|
|
43
|
+
/\btee\b/i,
|
|
44
|
+
/\btruncate\b/i,
|
|
45
|
+
/\bdd\b/i,
|
|
46
|
+
/\bshred\b/i,
|
|
47
|
+
/[^<]>(?!>)/,
|
|
48
|
+
/>>/,
|
|
49
|
+
/\bnpm\s+(install|uninstall|update|ci|link|publish)/i,
|
|
50
|
+
/\byarn\s+(add|remove|install|publish)/i,
|
|
51
|
+
/\bpnpm\s+(add|remove|install|publish)/i,
|
|
52
|
+
/\bpip\s+(install|uninstall)/i,
|
|
53
|
+
/\bapt(-get)?\s+(install|remove|purge|update|upgrade)/i,
|
|
54
|
+
/\bbrew\s+(install|uninstall|upgrade)/i,
|
|
55
|
+
/\bgit\s+(add|commit|push|pull|merge|rebase|reset|checkout\s+-b|branch\s+-[dD]|stash|cherry-pick|revert|tag|init|clone)/i,
|
|
56
|
+
/\bsudo\b/i,
|
|
57
|
+
/\bsu\b/i,
|
|
58
|
+
/\bkill\b/i,
|
|
59
|
+
/\bpkill\b/i,
|
|
60
|
+
/\bkillall\b/i,
|
|
61
|
+
/\breboot\b/i,
|
|
62
|
+
/\bshutdown\b/i,
|
|
63
|
+
/\bsystemctl\s+(start|stop|restart|enable|disable)/i,
|
|
64
|
+
/\bservice\s+\S+\s+(start|stop|restart)/i,
|
|
65
|
+
/\b(vim?|nano|emacs|code|subl)\b/i,
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// Read-only commands that are always safe
|
|
69
|
+
const SAFE_COMMANDS = [
|
|
70
|
+
/^\s*cat\b/,
|
|
71
|
+
/^\s*head\b/,
|
|
72
|
+
/^\s*tail\b/,
|
|
73
|
+
/^\s*less\b/,
|
|
74
|
+
/^\s*more\b/,
|
|
75
|
+
/^\s*grep\b/,
|
|
76
|
+
/^\s*find\b/,
|
|
77
|
+
/^\s*ls\b/,
|
|
78
|
+
/^\s*pwd\b/,
|
|
79
|
+
/^\s*echo\b/,
|
|
80
|
+
/^\s*printf\b/,
|
|
81
|
+
/^\s*wc\b/,
|
|
82
|
+
/^\s*sort\b/,
|
|
83
|
+
/^\s*uniq\b/,
|
|
84
|
+
/^\s*diff\b/,
|
|
85
|
+
/^\s*file\b/,
|
|
86
|
+
/^\s*stat\b/,
|
|
87
|
+
/^\s*du\b/,
|
|
88
|
+
/^\s*df\b/,
|
|
89
|
+
/^\s*tree\b/,
|
|
90
|
+
/^\s*which\b/,
|
|
91
|
+
/^\s*whereis\b/,
|
|
92
|
+
/^\s*type\b/,
|
|
93
|
+
/^\s*env\b/,
|
|
94
|
+
/^\s*printenv\b/,
|
|
95
|
+
/^\s*uname\b/,
|
|
96
|
+
/^\s*whoami\b/,
|
|
97
|
+
/^\s*id\b/,
|
|
98
|
+
/^\s*date\b/,
|
|
99
|
+
/^\s*cal\b/,
|
|
100
|
+
/^\s*uptime\b/,
|
|
101
|
+
/^\s*ps\b/,
|
|
102
|
+
/^\s*top\b/,
|
|
103
|
+
/^\s*htop\b/,
|
|
104
|
+
/^\s*free\b/,
|
|
105
|
+
/^\s*git\s+(status|log|diff|show|branch|remote|config\s+--get)/i,
|
|
106
|
+
/^\s*git\s+ls-/i,
|
|
107
|
+
/^\s*npm\s+(list|ls|view|info|search|outdated|audit)/i,
|
|
108
|
+
/^\s*yarn\s+(list|info|why|audit)/i,
|
|
109
|
+
/^\s*node\s+--version/i,
|
|
110
|
+
/^\s*python\s+--version/i,
|
|
111
|
+
/^\s*curl\s/i,
|
|
112
|
+
/^\s*wget\s+-O\s*-/i,
|
|
113
|
+
/^\s*jq\b/,
|
|
114
|
+
/^\s*sed\s+-n/i,
|
|
115
|
+
/^\s*awk\b/,
|
|
116
|
+
/^\s*rg\b/,
|
|
117
|
+
/^\s*fd\b/,
|
|
118
|
+
/^\s*bat\b/,
|
|
119
|
+
/^\s*exa\b/,
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
function isSafeCommand(command: string): boolean {
|
|
123
|
+
if (SAFE_COMMANDS.some((pattern) => pattern.test(command))) {
|
|
124
|
+
if (!DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(command))) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(command))) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Todo item with step number
|
|
135
|
+
interface TodoItem {
|
|
136
|
+
step: number;
|
|
137
|
+
text: string;
|
|
138
|
+
completed: boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Clean up extracted step text for display.
|
|
143
|
+
*/
|
|
144
|
+
function cleanStepText(text: string): string {
|
|
145
|
+
let cleaned = text
|
|
146
|
+
// Remove markdown bold/italic
|
|
147
|
+
.replace(/\*{1,2}([^*]+)\*{1,2}/g, "$1")
|
|
148
|
+
// Remove markdown code
|
|
149
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
150
|
+
// Remove leading action words that are redundant
|
|
151
|
+
.replace(
|
|
152
|
+
/^(Use|Run|Execute|Create|Write|Read|Check|Verify|Update|Modify|Add|Remove|Delete|Install)\s+(the\s+)?/i,
|
|
153
|
+
"",
|
|
154
|
+
)
|
|
155
|
+
// Clean up extra whitespace
|
|
156
|
+
.replace(/\s+/g, " ")
|
|
157
|
+
.trim();
|
|
158
|
+
|
|
159
|
+
// Capitalize first letter
|
|
160
|
+
if (cleaned.length > 0) {
|
|
161
|
+
cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Truncate if too long
|
|
165
|
+
if (cleaned.length > 50) {
|
|
166
|
+
cleaned = `${cleaned.slice(0, 47)}...`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return cleaned;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Extract todo items from assistant message.
|
|
174
|
+
*/
|
|
175
|
+
function extractTodoItems(message: string): TodoItem[] {
|
|
176
|
+
const items: TodoItem[] = [];
|
|
177
|
+
|
|
178
|
+
// Match numbered lists: "1. Task" or "1) Task" - also handle **bold** prefixes
|
|
179
|
+
const numberedPattern = /^\s*(\d+)[.)]\s+\*{0,2}([^*\n]+)/gm;
|
|
180
|
+
for (const match of message.matchAll(numberedPattern)) {
|
|
181
|
+
let text = match[2].trim();
|
|
182
|
+
text = text.replace(/\*{1,2}$/, "").trim();
|
|
183
|
+
// Skip if too short or looks like code/command
|
|
184
|
+
if (text.length > 5 && !text.startsWith("`") && !text.startsWith("/") && !text.startsWith("-")) {
|
|
185
|
+
const cleaned = cleanStepText(text);
|
|
186
|
+
if (cleaned.length > 3) {
|
|
187
|
+
items.push({ step: items.length + 1, text: cleaned, completed: false });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// If no numbered items, try bullet points
|
|
193
|
+
if (items.length === 0) {
|
|
194
|
+
const stepPattern = /^\s*[-*]\s*(?:Step\s*\d+[:.])?\s*\*{0,2}([^*\n]+)/gim;
|
|
195
|
+
for (const match of message.matchAll(stepPattern)) {
|
|
196
|
+
let text = match[1].trim();
|
|
197
|
+
text = text.replace(/\*{1,2}$/, "").trim();
|
|
198
|
+
if (text.length > 10 && !text.startsWith("`")) {
|
|
199
|
+
const cleaned = cleanStepText(text);
|
|
200
|
+
if (cleaned.length > 3) {
|
|
201
|
+
items.push({ step: items.length + 1, text: cleaned, completed: false });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return items;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export default function planModeHook(pi: HookAPI) {
|
|
211
|
+
let planModeEnabled = false;
|
|
212
|
+
let toolsCalledThisTurn = false;
|
|
213
|
+
let executionMode = false;
|
|
214
|
+
let todoItems: TodoItem[] = [];
|
|
215
|
+
|
|
216
|
+
// Register --plan CLI flag
|
|
217
|
+
pi.registerFlag("plan", {
|
|
218
|
+
description: "Start in plan mode (read-only exploration)",
|
|
219
|
+
type: "boolean",
|
|
220
|
+
default: false,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Helper to update status displays
|
|
224
|
+
function updateStatus(ctx: HookContext) {
|
|
225
|
+
if (executionMode && todoItems.length > 0) {
|
|
226
|
+
const completed = todoItems.filter((t) => t.completed).length;
|
|
227
|
+
ctx.ui.setStatus("plan-mode", ctx.ui.theme.fg("accent", `📋 ${completed}/${todoItems.length}`));
|
|
228
|
+
} else if (planModeEnabled) {
|
|
229
|
+
ctx.ui.setStatus("plan-mode", ctx.ui.theme.fg("warning", "⏸ plan"));
|
|
230
|
+
} else {
|
|
231
|
+
ctx.ui.setStatus("plan-mode", undefined);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Show widget during execution (no IDs shown to user)
|
|
235
|
+
if (executionMode && todoItems.length > 0) {
|
|
236
|
+
const lines: string[] = [];
|
|
237
|
+
for (const item of todoItems) {
|
|
238
|
+
if (item.completed) {
|
|
239
|
+
lines.push(
|
|
240
|
+
ctx.ui.theme.fg("success", "☑ ") + ctx.ui.theme.fg("muted", ctx.ui.theme.strikethrough(item.text)),
|
|
241
|
+
);
|
|
242
|
+
} else {
|
|
243
|
+
lines.push(ctx.ui.theme.fg("muted", "☐ ") + item.text);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
ctx.ui.setWidget("plan-todos", lines);
|
|
247
|
+
} else {
|
|
248
|
+
ctx.ui.setWidget("plan-todos", undefined);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function togglePlanMode(ctx: HookContext) {
|
|
253
|
+
planModeEnabled = !planModeEnabled;
|
|
254
|
+
executionMode = false;
|
|
255
|
+
todoItems = [];
|
|
256
|
+
|
|
257
|
+
if (planModeEnabled) {
|
|
258
|
+
pi.setActiveTools(PLAN_MODE_TOOLS);
|
|
259
|
+
ctx.ui.notify(`Plan mode enabled. Tools: ${PLAN_MODE_TOOLS.join(", ")}`);
|
|
260
|
+
} else {
|
|
261
|
+
pi.setActiveTools(NORMAL_MODE_TOOLS);
|
|
262
|
+
ctx.ui.notify("Plan mode disabled. Full access restored.");
|
|
263
|
+
}
|
|
264
|
+
updateStatus(ctx);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Register /plan command
|
|
268
|
+
pi.registerCommand("plan", {
|
|
269
|
+
description: "Toggle plan mode (read-only exploration)",
|
|
270
|
+
handler: async (_args, ctx) => {
|
|
271
|
+
togglePlanMode(ctx);
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Register /todos command
|
|
276
|
+
pi.registerCommand("todos", {
|
|
277
|
+
description: "Show current plan todo list",
|
|
278
|
+
handler: async (_args, ctx) => {
|
|
279
|
+
if (todoItems.length === 0) {
|
|
280
|
+
ctx.ui.notify("No todos. Create a plan first with /plan", "info");
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const todoList = todoItems
|
|
285
|
+
.map((item, i) => {
|
|
286
|
+
const checkbox = item.completed ? "✓" : "○";
|
|
287
|
+
return `${i + 1}. ${checkbox} ${item.text}`;
|
|
288
|
+
})
|
|
289
|
+
.join("\n");
|
|
290
|
+
|
|
291
|
+
ctx.ui.notify(`Plan Progress:\n${todoList}`, "info");
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Register Shift+P shortcut
|
|
296
|
+
pi.registerShortcut(Key.shift("p"), {
|
|
297
|
+
description: "Toggle plan mode",
|
|
298
|
+
handler: async (ctx) => {
|
|
299
|
+
togglePlanMode(ctx);
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Block destructive bash in plan mode
|
|
304
|
+
pi.on("tool_call", async (event) => {
|
|
305
|
+
if (!planModeEnabled) return;
|
|
306
|
+
if (event.toolName !== "bash") return;
|
|
307
|
+
|
|
308
|
+
const command = event.input.command as string;
|
|
309
|
+
if (!isSafeCommand(command)) {
|
|
310
|
+
return {
|
|
311
|
+
block: true,
|
|
312
|
+
reason: `Plan mode: destructive command blocked. Use /plan to disable plan mode first.\nCommand: ${command}`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Track step completion based on tool results
|
|
318
|
+
pi.on("tool_result", async (_event, ctx) => {
|
|
319
|
+
toolsCalledThisTurn = true;
|
|
320
|
+
|
|
321
|
+
if (!executionMode || todoItems.length === 0) return;
|
|
322
|
+
|
|
323
|
+
// Mark the first uncompleted step as done when any tool succeeds
|
|
324
|
+
const nextStep = todoItems.find((t) => !t.completed);
|
|
325
|
+
if (nextStep) {
|
|
326
|
+
nextStep.completed = true;
|
|
327
|
+
updateStatus(ctx);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Filter out stale plan mode context messages from LLM context
|
|
332
|
+
// This ensures the agent only sees the CURRENT state (plan mode on/off)
|
|
333
|
+
pi.on("context", async (event) => {
|
|
334
|
+
// Only filter when NOT in plan mode (i.e., when executing)
|
|
335
|
+
if (planModeEnabled) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Remove any previous plan-mode-context messages
|
|
340
|
+
const _beforeCount = event.messages.length;
|
|
341
|
+
const filtered = event.messages.filter((m) => {
|
|
342
|
+
if (m.role === "user" && Array.isArray(m.content)) {
|
|
343
|
+
const hasOldContext = m.content.some((c) => c.type === "text" && c.text.includes("[PLAN MODE ACTIVE]"));
|
|
344
|
+
if (hasOldContext) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return true;
|
|
349
|
+
});
|
|
350
|
+
return { messages: filtered };
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Inject plan mode context
|
|
354
|
+
pi.on("before_agent_start", async () => {
|
|
355
|
+
if (!planModeEnabled && !executionMode) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (planModeEnabled) {
|
|
360
|
+
return {
|
|
361
|
+
message: {
|
|
362
|
+
customType: "plan-mode-context",
|
|
363
|
+
content: `[PLAN MODE ACTIVE]
|
|
364
|
+
You are in plan mode - a read-only exploration mode for safe code analysis.
|
|
365
|
+
|
|
366
|
+
Restrictions:
|
|
367
|
+
- You can only use: read, bash, grep, find, ls
|
|
368
|
+
- You CANNOT use: edit, write (file modifications are disabled)
|
|
369
|
+
- Bash is restricted to READ-ONLY commands
|
|
370
|
+
- Focus on analysis, planning, and understanding the codebase
|
|
371
|
+
|
|
372
|
+
Create a detailed numbered plan:
|
|
373
|
+
1. First step description
|
|
374
|
+
2. Second step description
|
|
375
|
+
...
|
|
376
|
+
|
|
377
|
+
Do NOT attempt to make changes - just describe what you would do.`,
|
|
378
|
+
display: false,
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (executionMode && todoItems.length > 0) {
|
|
384
|
+
const remaining = todoItems.filter((t) => !t.completed);
|
|
385
|
+
const todoList = remaining.map((t) => `${t.step}. ${t.text}`).join("\n");
|
|
386
|
+
return {
|
|
387
|
+
message: {
|
|
388
|
+
customType: "plan-execution-context",
|
|
389
|
+
content: `[EXECUTING PLAN - Full tool access enabled]
|
|
390
|
+
|
|
391
|
+
Remaining steps:
|
|
392
|
+
${todoList}
|
|
393
|
+
|
|
394
|
+
Execute each step in order.`,
|
|
395
|
+
display: false,
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// After agent finishes
|
|
402
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
403
|
+
// In execution mode, check if all steps complete
|
|
404
|
+
if (executionMode && todoItems.length > 0) {
|
|
405
|
+
const allComplete = todoItems.every((t) => t.completed);
|
|
406
|
+
if (allComplete) {
|
|
407
|
+
// Show final completed list in chat
|
|
408
|
+
const completedList = todoItems.map((t) => `~~${t.text}~~`).join("\n");
|
|
409
|
+
pi.sendMessage(
|
|
410
|
+
{
|
|
411
|
+
customType: "plan-complete",
|
|
412
|
+
content: `**Plan Complete!** ✓\n\n${completedList}`,
|
|
413
|
+
display: true,
|
|
414
|
+
},
|
|
415
|
+
{ triggerTurn: false },
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
executionMode = false;
|
|
419
|
+
todoItems = [];
|
|
420
|
+
pi.setActiveTools(NORMAL_MODE_TOOLS);
|
|
421
|
+
updateStatus(ctx);
|
|
422
|
+
}
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!planModeEnabled) return;
|
|
427
|
+
if (!ctx.hasUI) return;
|
|
428
|
+
|
|
429
|
+
// Extract todos from last message
|
|
430
|
+
const messages = event.messages;
|
|
431
|
+
const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
|
|
432
|
+
if (lastAssistant && Array.isArray(lastAssistant.content)) {
|
|
433
|
+
const textContent = lastAssistant.content
|
|
434
|
+
.filter((block): block is { type: "text"; text: string } => block.type === "text")
|
|
435
|
+
.map((block) => block.text)
|
|
436
|
+
.join("\n");
|
|
437
|
+
|
|
438
|
+
if (textContent) {
|
|
439
|
+
const extracted = extractTodoItems(textContent);
|
|
440
|
+
if (extracted.length > 0) {
|
|
441
|
+
todoItems = extracted;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const hasTodos = todoItems.length > 0;
|
|
447
|
+
|
|
448
|
+
// Show todo list in chat (no IDs shown to user, just numbered)
|
|
449
|
+
if (hasTodos) {
|
|
450
|
+
const todoListText = todoItems.map((t, i) => `${i + 1}. ☐ ${t.text}`).join("\n");
|
|
451
|
+
pi.sendMessage(
|
|
452
|
+
{
|
|
453
|
+
customType: "plan-todo-list",
|
|
454
|
+
content: `**Plan Steps (${todoItems.length}):**\n\n${todoListText}`,
|
|
455
|
+
display: true,
|
|
456
|
+
},
|
|
457
|
+
{ triggerTurn: false },
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const choice = await ctx.ui.select("Plan mode - what next?", [
|
|
462
|
+
hasTodos ? "Execute the plan (track progress)" : "Execute the plan",
|
|
463
|
+
"Stay in plan mode",
|
|
464
|
+
"Refine the plan",
|
|
465
|
+
]);
|
|
466
|
+
|
|
467
|
+
if (choice?.startsWith("Execute")) {
|
|
468
|
+
planModeEnabled = false;
|
|
469
|
+
executionMode = hasTodos;
|
|
470
|
+
pi.setActiveTools(NORMAL_MODE_TOOLS);
|
|
471
|
+
updateStatus(ctx);
|
|
472
|
+
|
|
473
|
+
// Simple execution message - context event filters old plan mode messages
|
|
474
|
+
// and before_agent_start injects fresh execution context with IDs
|
|
475
|
+
const execMessage = hasTodos
|
|
476
|
+
? `Execute the plan. Start with: ${todoItems[0].text}`
|
|
477
|
+
: "Execute the plan you just created.";
|
|
478
|
+
|
|
479
|
+
pi.sendMessage(
|
|
480
|
+
{
|
|
481
|
+
customType: "plan-mode-execute",
|
|
482
|
+
content: execMessage,
|
|
483
|
+
display: true,
|
|
484
|
+
},
|
|
485
|
+
{ triggerTurn: true },
|
|
486
|
+
);
|
|
487
|
+
} else if (choice === "Refine the plan") {
|
|
488
|
+
const refinement = await ctx.ui.input("What should be refined?");
|
|
489
|
+
if (refinement) {
|
|
490
|
+
ctx.ui.setEditorText(refinement);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// Initialize state on session start
|
|
496
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
497
|
+
if (pi.getFlag("plan") === true) {
|
|
498
|
+
planModeEnabled = true;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const entries = ctx.sessionManager.getEntries();
|
|
502
|
+
const planModeEntry = entries
|
|
503
|
+
.filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "plan-mode")
|
|
504
|
+
.pop() as { data?: { enabled: boolean; todos?: TodoItem[]; executing?: boolean } } | undefined;
|
|
505
|
+
|
|
506
|
+
if (planModeEntry?.data) {
|
|
507
|
+
if (planModeEntry.data.enabled !== undefined) {
|
|
508
|
+
planModeEnabled = planModeEntry.data.enabled;
|
|
509
|
+
}
|
|
510
|
+
if (planModeEntry.data.todos) {
|
|
511
|
+
todoItems = planModeEntry.data.todos;
|
|
512
|
+
}
|
|
513
|
+
if (planModeEntry.data.executing) {
|
|
514
|
+
executionMode = planModeEntry.data.executing;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (planModeEnabled) {
|
|
519
|
+
pi.setActiveTools(PLAN_MODE_TOOLS);
|
|
520
|
+
}
|
|
521
|
+
updateStatus(ctx);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Reset tool tracking at start of each turn and persist state
|
|
525
|
+
pi.on("turn_start", async () => {
|
|
526
|
+
toolsCalledThisTurn = false;
|
|
527
|
+
pi.appendEntry("plan-mode", {
|
|
528
|
+
enabled: planModeEnabled,
|
|
529
|
+
todos: todoItems,
|
|
530
|
+
executing: executionMode,
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// Handle non-tool turns (e.g., analysis, explanation steps)
|
|
535
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
536
|
+
if (!executionMode || todoItems.length === 0) return;
|
|
537
|
+
|
|
538
|
+
// If no tools were called this turn, the agent was doing analysis/explanation
|
|
539
|
+
// Mark the next uncompleted step as done
|
|
540
|
+
if (!toolsCalledThisTurn) {
|
|
541
|
+
const nextStep = todoItems.find((t) => !t.completed);
|
|
542
|
+
if (nextStep) {
|
|
543
|
+
nextStep.completed = true;
|
|
544
|
+
updateStatus(ctx);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|