@oh-my-pi/pi-coding-agent 3.15.0 → 3.20.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.
Files changed (193) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/docs/extensions.md +1055 -0
  3. package/docs/rpc.md +69 -13
  4. package/docs/session-tree-plan.md +1 -1
  5. package/examples/extensions/README.md +141 -0
  6. package/examples/extensions/api-demo.ts +87 -0
  7. package/examples/extensions/chalk-logger.ts +26 -0
  8. package/examples/extensions/hello.ts +33 -0
  9. package/examples/extensions/pirate.ts +44 -0
  10. package/examples/extensions/plan-mode.ts +551 -0
  11. package/examples/extensions/subagent/agents/reviewer.md +35 -0
  12. package/examples/extensions/todo.ts +299 -0
  13. package/examples/extensions/tools.ts +145 -0
  14. package/examples/extensions/with-deps/index.ts +36 -0
  15. package/examples/extensions/with-deps/package-lock.json +31 -0
  16. package/examples/extensions/with-deps/package.json +16 -0
  17. package/examples/sdk/02-custom-model.ts +3 -3
  18. package/examples/sdk/05-tools.ts +7 -3
  19. package/examples/sdk/06-extensions.ts +81 -0
  20. package/examples/sdk/06-hooks.ts +14 -13
  21. package/examples/sdk/08-prompt-templates.ts +42 -0
  22. package/examples/sdk/08-slash-commands.ts +17 -12
  23. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  24. package/examples/sdk/12-full-control.ts +6 -6
  25. package/package.json +11 -7
  26. package/src/capability/extension-module.ts +34 -0
  27. package/src/cli/args.ts +22 -7
  28. package/src/cli/file-processor.ts +38 -67
  29. package/src/cli/list-models.ts +1 -1
  30. package/src/config.ts +25 -14
  31. package/src/core/agent-session.ts +505 -242
  32. package/src/core/auth-storage.ts +33 -21
  33. package/src/core/compaction/branch-summarization.ts +4 -4
  34. package/src/core/compaction/compaction.ts +3 -3
  35. package/src/core/custom-commands/bundled/wt/index.ts +430 -0
  36. package/src/core/custom-commands/loader.ts +9 -0
  37. package/src/core/custom-tools/wrapper.ts +5 -0
  38. package/src/core/event-bus.ts +59 -0
  39. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  40. package/src/core/export-html/vendor/marked.min.js +6 -0
  41. package/src/core/extensions/index.ts +100 -0
  42. package/src/core/extensions/loader.ts +501 -0
  43. package/src/core/extensions/runner.ts +477 -0
  44. package/src/core/extensions/types.ts +712 -0
  45. package/src/core/extensions/wrapper.ts +147 -0
  46. package/src/core/hooks/types.ts +2 -2
  47. package/src/core/index.ts +10 -21
  48. package/src/core/keybindings.ts +199 -0
  49. package/src/core/messages.ts +26 -7
  50. package/src/core/model-registry.ts +123 -46
  51. package/src/core/model-resolver.ts +7 -5
  52. package/src/core/prompt-templates.ts +242 -0
  53. package/src/core/sdk.ts +378 -295
  54. package/src/core/session-manager.ts +72 -58
  55. package/src/core/settings-manager.ts +118 -22
  56. package/src/core/system-prompt.ts +24 -1
  57. package/src/core/terminal-notify.ts +37 -0
  58. package/src/core/tools/context.ts +4 -4
  59. package/src/core/tools/exa/mcp-client.ts +5 -4
  60. package/src/core/tools/exa/render.ts +176 -131
  61. package/src/core/tools/gemini-image.ts +361 -0
  62. package/src/core/tools/git.ts +216 -0
  63. package/src/core/tools/index.ts +28 -15
  64. package/src/core/tools/lsp/config.ts +5 -4
  65. package/src/core/tools/lsp/index.ts +17 -12
  66. package/src/core/tools/lsp/render.ts +39 -47
  67. package/src/core/tools/read.ts +66 -29
  68. package/src/core/tools/render-utils.ts +268 -0
  69. package/src/core/tools/renderers.ts +243 -225
  70. package/src/core/tools/task/discovery.ts +2 -2
  71. package/src/core/tools/task/executor.ts +66 -58
  72. package/src/core/tools/task/index.ts +29 -10
  73. package/src/core/tools/task/model-resolver.ts +8 -13
  74. package/src/core/tools/task/omp-command.ts +24 -0
  75. package/src/core/tools/task/render.ts +35 -60
  76. package/src/core/tools/task/types.ts +3 -0
  77. package/src/core/tools/web-fetch.ts +29 -28
  78. package/src/core/tools/web-search/index.ts +6 -5
  79. package/src/core/tools/web-search/providers/exa.ts +6 -5
  80. package/src/core/tools/web-search/render.ts +66 -111
  81. package/src/core/voice-controller.ts +135 -0
  82. package/src/core/voice-supervisor.ts +1003 -0
  83. package/src/core/voice.ts +308 -0
  84. package/src/discovery/builtin.ts +75 -1
  85. package/src/discovery/claude.ts +47 -1
  86. package/src/discovery/codex.ts +54 -2
  87. package/src/discovery/gemini.ts +55 -2
  88. package/src/discovery/helpers.ts +100 -1
  89. package/src/discovery/index.ts +2 -0
  90. package/src/index.ts +14 -9
  91. package/src/lib/worktree/collapse.ts +179 -0
  92. package/src/lib/worktree/constants.ts +14 -0
  93. package/src/lib/worktree/errors.ts +23 -0
  94. package/src/lib/worktree/git.ts +110 -0
  95. package/src/lib/worktree/index.ts +23 -0
  96. package/src/lib/worktree/operations.ts +216 -0
  97. package/src/lib/worktree/session.ts +114 -0
  98. package/src/lib/worktree/stats.ts +67 -0
  99. package/src/main.ts +61 -37
  100. package/src/migrations.ts +37 -7
  101. package/src/modes/interactive/components/bash-execution.ts +6 -4
  102. package/src/modes/interactive/components/custom-editor.ts +55 -0
  103. package/src/modes/interactive/components/custom-message.ts +95 -0
  104. package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
  105. package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
  106. package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
  107. package/src/modes/interactive/components/extensions/types.ts +1 -0
  108. package/src/modes/interactive/components/footer.ts +324 -0
  109. package/src/modes/interactive/components/hook-editor.ts +1 -0
  110. package/src/modes/interactive/components/hook-selector.ts +3 -3
  111. package/src/modes/interactive/components/model-selector.ts +7 -6
  112. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  113. package/src/modes/interactive/components/settings-defs.ts +55 -6
  114. package/src/modes/interactive/components/status-line/separators.ts +4 -4
  115. package/src/modes/interactive/components/status-line.ts +45 -35
  116. package/src/modes/interactive/components/tool-execution.ts +95 -23
  117. package/src/modes/interactive/interactive-mode.ts +644 -113
  118. package/src/modes/interactive/theme/defaults/alabaster.json +99 -0
  119. package/src/modes/interactive/theme/defaults/amethyst.json +103 -0
  120. package/src/modes/interactive/theme/defaults/anthracite.json +100 -0
  121. package/src/modes/interactive/theme/defaults/basalt.json +90 -0
  122. package/src/modes/interactive/theme/defaults/birch.json +101 -0
  123. package/src/modes/interactive/theme/defaults/dark-abyss.json +97 -0
  124. package/src/modes/interactive/theme/defaults/dark-aurora.json +94 -0
  125. package/src/modes/interactive/theme/defaults/dark-cavern.json +97 -0
  126. package/src/modes/interactive/theme/defaults/dark-copper.json +94 -0
  127. package/src/modes/interactive/theme/defaults/dark-cosmos.json +96 -0
  128. package/src/modes/interactive/theme/defaults/dark-eclipse.json +97 -0
  129. package/src/modes/interactive/theme/defaults/dark-ember.json +94 -0
  130. package/src/modes/interactive/theme/defaults/dark-equinox.json +96 -0
  131. package/src/modes/interactive/theme/defaults/dark-lavender.json +94 -0
  132. package/src/modes/interactive/theme/defaults/dark-lunar.json +95 -0
  133. package/src/modes/interactive/theme/defaults/dark-midnight.json +94 -0
  134. package/src/modes/interactive/theme/defaults/dark-nebula.json +96 -0
  135. package/src/modes/interactive/theme/defaults/dark-rainforest.json +97 -0
  136. package/src/modes/interactive/theme/defaults/dark-reef.json +97 -0
  137. package/src/modes/interactive/theme/defaults/dark-sakura.json +94 -0
  138. package/src/modes/interactive/theme/defaults/dark-slate.json +94 -0
  139. package/src/modes/interactive/theme/defaults/dark-solstice.json +96 -0
  140. package/src/modes/interactive/theme/defaults/dark-starfall.json +97 -0
  141. package/src/modes/interactive/theme/defaults/dark-swamp.json +96 -0
  142. package/src/modes/interactive/theme/defaults/dark-taiga.json +97 -0
  143. package/src/modes/interactive/theme/defaults/dark-terminal.json +94 -0
  144. package/src/modes/interactive/theme/defaults/dark-tundra.json +97 -0
  145. package/src/modes/interactive/theme/defaults/dark-twilight.json +97 -0
  146. package/src/modes/interactive/theme/defaults/dark-volcanic.json +97 -0
  147. package/src/modes/interactive/theme/defaults/graphite.json +99 -0
  148. package/src/modes/interactive/theme/defaults/index.ts +128 -0
  149. package/src/modes/interactive/theme/defaults/light-aurora-day.json +97 -0
  150. package/src/modes/interactive/theme/defaults/light-canyon.json +97 -0
  151. package/src/modes/interactive/theme/defaults/light-cirrus.json +96 -0
  152. package/src/modes/interactive/theme/defaults/light-coral.json +94 -0
  153. package/src/modes/interactive/theme/defaults/light-dawn.json +96 -0
  154. package/src/modes/interactive/theme/defaults/light-dunes.json +97 -0
  155. package/src/modes/interactive/theme/defaults/light-eucalyptus.json +94 -0
  156. package/src/modes/interactive/theme/defaults/light-frost.json +94 -0
  157. package/src/modes/interactive/theme/defaults/light-glacier.json +97 -0
  158. package/src/modes/interactive/theme/defaults/light-haze.json +96 -0
  159. package/src/modes/interactive/theme/defaults/light-honeycomb.json +94 -0
  160. package/src/modes/interactive/theme/defaults/light-lagoon.json +97 -0
  161. package/src/modes/interactive/theme/defaults/light-lavender.json +94 -0
  162. package/src/modes/interactive/theme/defaults/light-meadow.json +97 -0
  163. package/src/modes/interactive/theme/defaults/light-mint.json +94 -0
  164. package/src/modes/interactive/theme/defaults/light-opal.json +97 -0
  165. package/src/modes/interactive/theme/defaults/light-orchard.json +97 -0
  166. package/src/modes/interactive/theme/defaults/light-paper.json +94 -0
  167. package/src/modes/interactive/theme/defaults/light-prism.json +96 -0
  168. package/src/modes/interactive/theme/defaults/light-sand.json +94 -0
  169. package/src/modes/interactive/theme/defaults/light-savanna.json +97 -0
  170. package/src/modes/interactive/theme/defaults/light-soleil.json +96 -0
  171. package/src/modes/interactive/theme/defaults/light-wetland.json +97 -0
  172. package/src/modes/interactive/theme/defaults/light-zenith.json +95 -0
  173. package/src/modes/interactive/theme/defaults/limestone.json +100 -0
  174. package/src/modes/interactive/theme/defaults/mahogany.json +104 -0
  175. package/src/modes/interactive/theme/defaults/marble.json +99 -0
  176. package/src/modes/interactive/theme/defaults/obsidian.json +90 -0
  177. package/src/modes/interactive/theme/defaults/onyx.json +90 -0
  178. package/src/modes/interactive/theme/defaults/pearl.json +99 -0
  179. package/src/modes/interactive/theme/defaults/porcelain.json +90 -0
  180. package/src/modes/interactive/theme/defaults/quartz.json +102 -0
  181. package/src/modes/interactive/theme/defaults/sandstone.json +101 -0
  182. package/src/modes/interactive/theme/defaults/titanium.json +89 -0
  183. package/src/modes/print-mode.ts +14 -72
  184. package/src/modes/rpc/rpc-client.ts +23 -9
  185. package/src/modes/rpc/rpc-mode.ts +137 -125
  186. package/src/modes/rpc/rpc-types.ts +46 -24
  187. package/src/prompts/task.md +1 -0
  188. package/src/prompts/tools/gemini-image.md +4 -0
  189. package/src/prompts/tools/git.md +9 -0
  190. package/src/prompts/voice-summary.md +12 -0
  191. package/src/utils/image-convert.ts +26 -0
  192. package/src/utils/image-resize.ts +215 -0
  193. package/src/utils/shell-snapshot.ts +22 -20
@@ -0,0 +1,551 @@
1
+ /**
2
+ * Plan Mode Extension
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 ~/.omp/agent/extensions/ (legacy: ~/.pi/agent/extensions/) or your project's .omp/extensions/
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 { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
23
+ import { Key } from "@oh-my-pi/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 planModeExtension(pi: ExtensionAPI) {
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: ExtensionContext) {
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(ctx.ui.theme.fg("success", "☑ ") + ctx.ui.theme.fg("dim", item.text));
240
+ } else {
241
+ lines.push(ctx.ui.theme.fg("muted", "☐ ") + item.text);
242
+ }
243
+ }
244
+ ctx.ui.setWidget("plan-todos", lines);
245
+ } else {
246
+ ctx.ui.setWidget("plan-todos", undefined);
247
+ }
248
+ }
249
+
250
+ function togglePlanMode(ctx: ExtensionContext) {
251
+ planModeEnabled = !planModeEnabled;
252
+ executionMode = false;
253
+ todoItems = [];
254
+
255
+ if (planModeEnabled) {
256
+ pi.setActiveTools(PLAN_MODE_TOOLS);
257
+ ctx.ui.notify(`Plan mode enabled. Tools: ${PLAN_MODE_TOOLS.join(", ")}`);
258
+ } else {
259
+ pi.setActiveTools(NORMAL_MODE_TOOLS);
260
+ ctx.ui.notify("Plan mode disabled. Full access restored.");
261
+ }
262
+ updateStatus(ctx);
263
+ }
264
+
265
+ // Register /plan command
266
+ pi.registerCommand("plan", {
267
+ description: "Toggle plan mode (read-only exploration)",
268
+ handler: async (_args, ctx) => {
269
+ togglePlanMode(ctx);
270
+ },
271
+ });
272
+
273
+ // Register /todos command
274
+ pi.registerCommand("todos", {
275
+ description: "Show current plan todo list",
276
+ handler: async (_args, ctx) => {
277
+ if (todoItems.length === 0) {
278
+ ctx.ui.notify("No todos. Create a plan first with /plan", "info");
279
+ return;
280
+ }
281
+
282
+ const todoList = todoItems
283
+ .map((item, i) => {
284
+ const checkbox = item.completed ? "✓" : "○";
285
+ return `${i + 1}. ${checkbox} ${item.text}`;
286
+ })
287
+ .join("\n");
288
+
289
+ ctx.ui.notify(`Plan Progress:\n${todoList}`, "info");
290
+ },
291
+ });
292
+
293
+ // Register Shift+P shortcut
294
+ pi.registerShortcut(Key.shift("p"), {
295
+ description: "Toggle plan mode",
296
+ handler: async (ctx) => {
297
+ togglePlanMode(ctx);
298
+ },
299
+ });
300
+
301
+ // Block destructive bash in plan mode
302
+ pi.on("tool_call", async (event) => {
303
+ if (!planModeEnabled) return;
304
+ if (event.toolName !== "bash") return;
305
+
306
+ const command = event.input.command as string;
307
+ if (!isSafeCommand(command)) {
308
+ return {
309
+ block: true,
310
+ reason: `Plan mode: destructive command blocked. Use /plan to disable plan mode first.\nCommand: ${command}`,
311
+ };
312
+ }
313
+ });
314
+
315
+ // Track step completion based on tool results
316
+ pi.on("tool_result", async (_event, ctx) => {
317
+ toolsCalledThisTurn = true;
318
+
319
+ if (!executionMode || todoItems.length === 0) return;
320
+
321
+ // Mark the first uncompleted step as done when any tool succeeds
322
+ const nextStep = todoItems.find((t) => !t.completed);
323
+ if (nextStep) {
324
+ nextStep.completed = true;
325
+ updateStatus(ctx);
326
+ }
327
+ });
328
+
329
+ // Filter out stale plan mode context messages from LLM context
330
+ // This ensures the agent only sees the CURRENT state (plan mode on/off)
331
+ pi.on("context", async (event) => {
332
+ // Only filter when NOT in plan mode (i.e., when executing)
333
+ if (planModeEnabled) {
334
+ return;
335
+ }
336
+
337
+ // Remove any previous plan-mode-context messages
338
+ const _beforeCount = event.messages.length;
339
+ const filtered = event.messages.filter((m) => {
340
+ if (m.role === "user" && Array.isArray(m.content)) {
341
+ const hasOldContext = m.content.some(
342
+ (c: { type: string; text?: string }) => c.type === "text" && c.text?.includes("[PLAN MODE ACTIVE]"),
343
+ );
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(
435
+ (block: { type: string; text?: string }): block is { type: "text"; text: string } =>
436
+ block.type === "text",
437
+ )
438
+ .map((block: { type: "text"; text: string }) => block.text)
439
+ .join("\n");
440
+
441
+ if (textContent) {
442
+ const extracted = extractTodoItems(textContent);
443
+ if (extracted.length > 0) {
444
+ todoItems = extracted;
445
+ }
446
+ }
447
+ }
448
+
449
+ const hasTodos = todoItems.length > 0;
450
+
451
+ // Show todo list in chat (no IDs shown to user, just numbered)
452
+ if (hasTodos) {
453
+ const todoListText = todoItems.map((t, i) => `${i + 1}. ☐ ${t.text}`).join("\n");
454
+ pi.sendMessage(
455
+ {
456
+ customType: "plan-todo-list",
457
+ content: `**Plan Steps (${todoItems.length}):**\n\n${todoListText}`,
458
+ display: true,
459
+ },
460
+ { triggerTurn: false },
461
+ );
462
+ }
463
+
464
+ const choice = await ctx.ui.select("Plan mode - what next?", [
465
+ hasTodos ? "Execute the plan (track progress)" : "Execute the plan",
466
+ "Stay in plan mode",
467
+ "Refine the plan",
468
+ ]);
469
+
470
+ if (choice?.startsWith("Execute")) {
471
+ planModeEnabled = false;
472
+ executionMode = hasTodos;
473
+ pi.setActiveTools(NORMAL_MODE_TOOLS);
474
+ updateStatus(ctx);
475
+
476
+ // Simple execution message - context event filters old plan mode messages
477
+ // and before_agent_start injects fresh execution context with IDs
478
+ const execMessage = hasTodos
479
+ ? `Execute the plan. Start with: ${todoItems[0].text}`
480
+ : "Execute the plan you just created.";
481
+
482
+ pi.sendMessage(
483
+ {
484
+ customType: "plan-mode-execute",
485
+ content: execMessage,
486
+ display: true,
487
+ },
488
+ { triggerTurn: true },
489
+ );
490
+ } else if (choice === "Refine the plan") {
491
+ const refinement = await ctx.ui.input("What should be refined?");
492
+ if (refinement) {
493
+ ctx.ui.setEditorText(refinement);
494
+ }
495
+ }
496
+ });
497
+
498
+ // Initialize state on session start
499
+ pi.on("session_start", async (_event, ctx) => {
500
+ if (pi.getFlag("plan") === true) {
501
+ planModeEnabled = true;
502
+ }
503
+
504
+ const entries = ctx.sessionManager.getEntries();
505
+ const planModeEntry = entries
506
+ .filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "plan-mode")
507
+ .pop() as { data?: { enabled: boolean; todos?: TodoItem[]; executing?: boolean } } | undefined;
508
+
509
+ if (planModeEntry?.data) {
510
+ if (planModeEntry.data.enabled !== undefined) {
511
+ planModeEnabled = planModeEntry.data.enabled;
512
+ }
513
+ if (planModeEntry.data.todos) {
514
+ todoItems = planModeEntry.data.todos;
515
+ }
516
+ if (planModeEntry.data.executing) {
517
+ executionMode = planModeEntry.data.executing;
518
+ }
519
+ }
520
+
521
+ if (planModeEnabled) {
522
+ pi.setActiveTools(PLAN_MODE_TOOLS);
523
+ }
524
+ updateStatus(ctx);
525
+ });
526
+
527
+ // Reset tool tracking at start of each turn and persist state
528
+ pi.on("turn_start", async () => {
529
+ toolsCalledThisTurn = false;
530
+ pi.appendEntry("plan-mode", {
531
+ enabled: planModeEnabled,
532
+ todos: todoItems,
533
+ executing: executionMode,
534
+ });
535
+ });
536
+
537
+ // Handle non-tool turns (e.g., analysis, explanation steps)
538
+ pi.on("turn_end", async (_event, ctx) => {
539
+ if (!executionMode || todoItems.length === 0) return;
540
+
541
+ // If no tools were called this turn, the agent was doing analysis/explanation
542
+ // Mark the next uncompleted step as done
543
+ if (!toolsCalledThisTurn) {
544
+ const nextStep = todoItems.find((t) => !t.completed);
545
+ if (nextStep) {
546
+ nextStep.completed = true;
547
+ updateStatus(ctx);
548
+ }
549
+ }
550
+ });
551
+ }
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: reviewer
3
+ description: Code review specialist for quality and security analysis
4
+ tools: read, grep, find, ls, bash
5
+ model: claude-sonnet-4-5
6
+ ---
7
+
8
+ You are a senior code reviewer. Analyze code for quality, security, and maintainability.
9
+
10
+ Bash is for read-only commands only: `git diff`, `git log`, `git show`. Do NOT modify files or run builds.
11
+ Assume tool permissions are not perfectly enforceable; keep all bash usage strictly read-only.
12
+
13
+ Strategy:
14
+ 1. Run `git diff` to see recent changes (if applicable)
15
+ 2. Read the modified files
16
+ 3. Check for bugs, security issues, code smells
17
+
18
+ Output format:
19
+
20
+ ## Files Reviewed
21
+ - `path/to/file.ts` (lines X-Y)
22
+
23
+ ## Critical (must fix)
24
+ - `file.ts:42` - Issue description
25
+
26
+ ## Warnings (should fix)
27
+ - `file.ts:100` - Issue description
28
+
29
+ ## Suggestions (consider)
30
+ - `file.ts:150` - Improvement idea
31
+
32
+ ## Summary
33
+ Overall assessment in 2-3 sentences.
34
+
35
+ Be specific with file paths and line numbers.