@mariozechner/pi-coding-agent 0.45.3 → 0.45.5
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 +28 -0
- package/README.md +2 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +7 -9
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/model-registry.d.ts +4 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +6 -0
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +1 -0
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +7 -5
- 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/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +3 -4
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/light.json +9 -9
- package/dist/utils/image-convert.d.ts.map +1 -1
- package/dist/utils/image-convert.js +11 -4
- package/dist/utils/image-convert.js.map +1 -1
- package/dist/utils/image-resize.d.ts +1 -1
- package/dist/utils/image-resize.d.ts.map +1 -1
- package/dist/utils/image-resize.js +47 -25
- package/dist/utils/image-resize.js.map +1 -1
- package/dist/utils/vips.d.ts +11 -0
- package/dist/utils/vips.d.ts.map +1 -0
- package/dist/utils/vips.js +35 -0
- package/dist/utils/vips.js.map +1 -0
- package/docs/extensions.md +18 -17
- package/docs/sdk.md +21 -48
- package/examples/README.md +5 -2
- package/examples/extensions/README.md +19 -2
- package/examples/extensions/plan-mode/README.md +65 -0
- package/examples/extensions/plan-mode/index.ts +340 -0
- package/examples/extensions/plan-mode/utils.ts +168 -0
- package/examples/extensions/question.ts +211 -13
- package/examples/extensions/questionnaire.ts +427 -0
- package/examples/extensions/summarize.ts +195 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/README.md +3 -4
- package/package.json +5 -5
- package/examples/extensions/plan-mode.ts +0 -548
package/docs/sdk.md
CHANGED
|
@@ -735,12 +735,12 @@ import {
|
|
|
735
735
|
discoverAuthStorage,
|
|
736
736
|
discoverModels,
|
|
737
737
|
discoverSkills,
|
|
738
|
-
|
|
739
|
-
discoverCustomTools,
|
|
738
|
+
discoverExtensions,
|
|
740
739
|
discoverContextFiles,
|
|
741
740
|
discoverPromptTemplates,
|
|
742
741
|
loadSettings,
|
|
743
742
|
buildSystemPrompt,
|
|
743
|
+
createEventBus,
|
|
744
744
|
} from "@mariozechner/pi-coding-agent";
|
|
745
745
|
|
|
746
746
|
// Auth and Models
|
|
@@ -754,19 +754,16 @@ const builtIn = getModel("anthropic", "claude-opus-4-5"); // Built-in only
|
|
|
754
754
|
// Skills
|
|
755
755
|
const { skills, warnings } = discoverSkills(cwd, agentDir, skillsSettings);
|
|
756
756
|
|
|
757
|
-
//
|
|
758
|
-
// Pass eventBus to share pi.events across
|
|
757
|
+
// Extensions (async - loads TypeScript)
|
|
758
|
+
// Pass eventBus to share pi.events across extensions
|
|
759
759
|
const eventBus = createEventBus();
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
// Custom tools (async - loads TypeScript)
|
|
763
|
-
const tools = await discoverCustomTools(eventBus, cwd, agentDir);
|
|
760
|
+
const { extensions, errors } = await discoverExtensions(eventBus, cwd, agentDir);
|
|
764
761
|
|
|
765
762
|
// Context files
|
|
766
763
|
const contextFiles = discoverContextFiles(cwd, agentDir);
|
|
767
764
|
|
|
768
765
|
// Prompt templates
|
|
769
|
-
const
|
|
766
|
+
const templates = discoverPromptTemplates(cwd, agentDir);
|
|
770
767
|
|
|
771
768
|
// Settings (global + project merged)
|
|
772
769
|
const settings = loadSettings(cwd, agentDir);
|
|
@@ -816,8 +813,8 @@ import {
|
|
|
816
813
|
SettingsManager,
|
|
817
814
|
readTool,
|
|
818
815
|
bashTool,
|
|
819
|
-
type
|
|
820
|
-
type
|
|
816
|
+
type ExtensionFactory,
|
|
817
|
+
type ToolDefinition,
|
|
821
818
|
} from "@mariozechner/pi-coding-agent";
|
|
822
819
|
|
|
823
820
|
// Set up auth storage (custom location)
|
|
@@ -831,16 +828,16 @@ if (process.env.MY_KEY) {
|
|
|
831
828
|
// Model registry (no custom models.json)
|
|
832
829
|
const modelRegistry = new ModelRegistry(authStorage);
|
|
833
830
|
|
|
834
|
-
// Inline
|
|
835
|
-
const
|
|
836
|
-
|
|
831
|
+
// Inline extension
|
|
832
|
+
const auditExtension: ExtensionFactory = (pi) => {
|
|
833
|
+
pi.on("tool_call", async (event) => {
|
|
837
834
|
console.log(`[Audit] ${event.toolName}`);
|
|
838
835
|
return undefined;
|
|
839
836
|
});
|
|
840
837
|
};
|
|
841
838
|
|
|
842
839
|
// Inline tool
|
|
843
|
-
const statusTool:
|
|
840
|
+
const statusTool: ToolDefinition = {
|
|
844
841
|
name: "status",
|
|
845
842
|
label: "Status",
|
|
846
843
|
description: "Get system status",
|
|
@@ -872,8 +869,8 @@ const { session } = await createAgentSession({
|
|
|
872
869
|
systemPrompt: "You are a minimal assistant. Be concise.",
|
|
873
870
|
|
|
874
871
|
tools: [readTool, bashTool],
|
|
875
|
-
customTools: [
|
|
876
|
-
|
|
872
|
+
customTools: [statusTool],
|
|
873
|
+
extensions: [auditExtension],
|
|
877
874
|
skills: [],
|
|
878
875
|
contextFiles: [],
|
|
879
876
|
promptTemplates: [],
|
|
@@ -961,7 +958,7 @@ The SDK is preferred when:
|
|
|
961
958
|
- You want type safety
|
|
962
959
|
- You're in the same Node.js process
|
|
963
960
|
- You need direct access to agent state
|
|
964
|
-
- You want to customize tools/
|
|
961
|
+
- You want to customize tools/extensions programmatically
|
|
965
962
|
|
|
966
963
|
RPC mode is preferred when:
|
|
967
964
|
- You're integrating from another language
|
|
@@ -984,12 +981,11 @@ discoverModels
|
|
|
984
981
|
|
|
985
982
|
// Discovery
|
|
986
983
|
discoverSkills
|
|
987
|
-
|
|
988
|
-
discoverCustomTools
|
|
984
|
+
discoverExtensions
|
|
989
985
|
discoverContextFiles
|
|
990
986
|
discoverPromptTemplates
|
|
991
987
|
|
|
992
|
-
// Event Bus (for shared
|
|
988
|
+
// Event Bus (for shared extension communication)
|
|
993
989
|
createEventBus
|
|
994
990
|
|
|
995
991
|
// Helpers
|
|
@@ -1015,8 +1011,9 @@ createGrepTool, createFindTool, createLsTool
|
|
|
1015
1011
|
// Types
|
|
1016
1012
|
type CreateAgentSessionOptions
|
|
1017
1013
|
type CreateAgentSessionResult
|
|
1018
|
-
type
|
|
1019
|
-
type
|
|
1014
|
+
type ExtensionFactory
|
|
1015
|
+
type ExtensionAPI
|
|
1016
|
+
type ToolDefinition
|
|
1020
1017
|
type Skill
|
|
1021
1018
|
type PromptTemplate
|
|
1022
1019
|
type Settings
|
|
@@ -1024,28 +1021,4 @@ type SkillsSettings
|
|
|
1024
1021
|
type Tool
|
|
1025
1022
|
```
|
|
1026
1023
|
|
|
1027
|
-
For
|
|
1028
|
-
|
|
1029
|
-
```typescript
|
|
1030
|
-
import type {
|
|
1031
|
-
HookAPI,
|
|
1032
|
-
HookMessage,
|
|
1033
|
-
HookFactory,
|
|
1034
|
-
HookEventContext,
|
|
1035
|
-
HookCommandContext,
|
|
1036
|
-
ToolCallEvent,
|
|
1037
|
-
ToolResultEvent,
|
|
1038
|
-
} from "@mariozechner/pi-coding-agent/hooks";
|
|
1039
|
-
```
|
|
1040
|
-
|
|
1041
|
-
For message utilities:
|
|
1042
|
-
|
|
1043
|
-
```typescript
|
|
1044
|
-
import { isHookMessage, createHookMessage } from "@mariozechner/pi-coding-agent";
|
|
1045
|
-
```
|
|
1046
|
-
|
|
1047
|
-
For config utilities:
|
|
1048
|
-
|
|
1049
|
-
```typescript
|
|
1050
|
-
import { getAgentDir } from "@mariozechner/pi-coding-agent/config";
|
|
1051
|
-
```
|
|
1024
|
+
For extension types, see [extensions.md](extensions.md) for the full API.
|
package/examples/README.md
CHANGED
|
@@ -10,9 +10,12 @@ Programmatic usage via `createAgentSession()`. Shows how to customize models, pr
|
|
|
10
10
|
### [extensions/](extensions/)
|
|
11
11
|
Example extensions demonstrating:
|
|
12
12
|
- Lifecycle event handlers (tool interception, safety gates, context modifications)
|
|
13
|
-
- Custom tools (todo lists, subagents)
|
|
13
|
+
- Custom tools (todo lists, questions, subagents, output truncation)
|
|
14
14
|
- Commands and keyboard shortcuts
|
|
15
|
-
-
|
|
15
|
+
- Custom UI (footers, headers, editors, overlays)
|
|
16
|
+
- Git integration (checkpoints, auto-commit)
|
|
17
|
+
- System prompt modifications and custom compaction
|
|
18
|
+
- External integrations (SSH, file watchers, system theme sync)
|
|
16
19
|
|
|
17
20
|
## Documentation
|
|
18
21
|
|
|
@@ -30,8 +30,10 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
|
|
30
30
|
|-----------|-------------|
|
|
31
31
|
| `todo.ts` | Todo list tool + `/todos` command with custom rendering and state persistence |
|
|
32
32
|
| `hello.ts` | Minimal custom tool example |
|
|
33
|
-
| `question.ts` | Demonstrates `ctx.ui.select()` for asking the user questions |
|
|
33
|
+
| `question.ts` | Demonstrates `ctx.ui.select()` for asking the user questions with custom UI |
|
|
34
|
+
| `questionnaire.ts` | Multi-question input with tab bar navigation between questions |
|
|
34
35
|
| `tool-override.ts` | Override built-in tools (e.g., add logging/access control to `read`) |
|
|
36
|
+
| `truncated-tool.ts` | Wraps ripgrep with proper output truncation (50KB/2000 lines) |
|
|
35
37
|
| `ssh.ts` | Delegate all tools to a remote machine via SSH using pluggable operations |
|
|
36
38
|
| `subagent/` | Delegate tasks to specialized subagents with isolated context windows |
|
|
37
39
|
|
|
@@ -40,16 +42,24 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
|
|
40
42
|
| Extension | Description |
|
|
41
43
|
|-----------|-------------|
|
|
42
44
|
| `preset.ts` | Named presets for model, thinking level, tools, and instructions via `--preset` flag and `/preset` command |
|
|
43
|
-
| `plan-mode
|
|
45
|
+
| `plan-mode/` | Claude Code-style plan mode for read-only exploration with `/plan` command and step tracking |
|
|
44
46
|
| `tools.ts` | Interactive `/tools` command to enable/disable tools with session persistence |
|
|
45
47
|
| `handoff.ts` | Transfer context to a new focused session via `/handoff <goal>` |
|
|
46
48
|
| `qna.ts` | Extracts questions from last response into editor via `ctx.ui.setEditorText()` |
|
|
47
49
|
| `status-line.ts` | Shows turn progress in footer via `ctx.ui.setStatus()` with themed colors |
|
|
50
|
+
| `model-status.ts` | Shows model changes in status bar via `model_select` hook |
|
|
48
51
|
| `snake.ts` | Snake game with custom UI, keyboard handling, and session persistence |
|
|
49
52
|
| `send-user-message.ts` | Demonstrates `pi.sendUserMessage()` for sending user messages from extensions |
|
|
50
53
|
| `timed-confirm.ts` | Demonstrates AbortSignal for auto-dismissing `ctx.ui.confirm()` and `ctx.ui.select()` dialogs |
|
|
51
54
|
| `modal-editor.ts` | Custom vim-like modal editor via `ctx.ui.setEditorComponent()` |
|
|
55
|
+
| `rainbow-editor.ts` | Animated rainbow text effect via custom editor |
|
|
52
56
|
| `notify.ts` | Desktop notifications via OSC 777 when agent finishes (Ghostty, iTerm2, WezTerm) |
|
|
57
|
+
| `summarize.ts` | Summarize conversation with GPT-5.2 and show in transient UI |
|
|
58
|
+
| `custom-footer.ts` | Custom footer with git branch and token stats via `ctx.ui.setFooter()` |
|
|
59
|
+
| `custom-header.ts` | Custom header via `ctx.ui.setHeader()` |
|
|
60
|
+
| `overlay-test.ts` | Test overlay rendering with inline text inputs |
|
|
61
|
+
| `shutdown-command.ts` | Adds `/quit` command demonstrating `ctx.shutdown()` |
|
|
62
|
+
| `interactive-shell.ts` | Run interactive commands (vim, htop) with full terminal via `user_bash` hook |
|
|
53
63
|
|
|
54
64
|
### Git Integration
|
|
55
65
|
|
|
@@ -63,8 +73,15 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
|
|
63
73
|
| Extension | Description |
|
|
64
74
|
|-----------|-------------|
|
|
65
75
|
| `pirate.ts` | Demonstrates `systemPromptAppend` to dynamically modify system prompt |
|
|
76
|
+
| `claude-rules.ts` | Scans `.claude/rules/` folder and lists rules in system prompt |
|
|
66
77
|
| `custom-compaction.ts` | Custom compaction that summarizes entire conversation |
|
|
67
78
|
|
|
79
|
+
### System Integration
|
|
80
|
+
|
|
81
|
+
| Extension | Description |
|
|
82
|
+
|-----------|-------------|
|
|
83
|
+
| `mac-system-theme.ts` | Syncs pi theme with macOS dark/light mode |
|
|
84
|
+
|
|
68
85
|
### External Dependencies
|
|
69
86
|
|
|
70
87
|
| Extension | Description |
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Plan Mode Extension
|
|
2
|
+
|
|
3
|
+
Read-only exploration mode for safe code analysis.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Read-only tools**: Restricts available tools to read, bash, grep, find, ls, question
|
|
8
|
+
- **Bash allowlist**: Only read-only bash commands are allowed
|
|
9
|
+
- **Plan extraction**: Extracts numbered steps from `Plan:` sections
|
|
10
|
+
- **Progress tracking**: Widget shows completion status during execution
|
|
11
|
+
- **[DONE:n] markers**: Explicit step completion tracking
|
|
12
|
+
- **Session persistence**: State survives session resume
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
- `/plan` - Toggle plan mode
|
|
17
|
+
- `/todos` - Show current plan progress
|
|
18
|
+
- `Shift+P` - Toggle plan mode (shortcut)
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
1. Enable plan mode with `/plan` or `--plan` flag
|
|
23
|
+
2. Ask the agent to analyze code and create a plan
|
|
24
|
+
3. The agent should output a numbered plan under a `Plan:` header:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Plan:
|
|
28
|
+
1. First step description
|
|
29
|
+
2. Second step description
|
|
30
|
+
3. Third step description
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
4. Choose "Execute the plan" when prompted
|
|
34
|
+
5. During execution, the agent marks steps complete with `[DONE:n]` tags
|
|
35
|
+
6. Progress widget shows completion status
|
|
36
|
+
|
|
37
|
+
## How It Works
|
|
38
|
+
|
|
39
|
+
### Plan Mode (Read-Only)
|
|
40
|
+
- Only read-only tools available
|
|
41
|
+
- Bash commands filtered through allowlist
|
|
42
|
+
- Agent creates a plan without making changes
|
|
43
|
+
|
|
44
|
+
### Execution Mode
|
|
45
|
+
- Full tool access restored
|
|
46
|
+
- Agent executes steps in order
|
|
47
|
+
- `[DONE:n]` markers track completion
|
|
48
|
+
- Widget shows progress
|
|
49
|
+
|
|
50
|
+
### Command Allowlist
|
|
51
|
+
|
|
52
|
+
Safe commands (allowed):
|
|
53
|
+
- File inspection: `cat`, `head`, `tail`, `less`, `more`
|
|
54
|
+
- Search: `grep`, `find`, `rg`, `fd`
|
|
55
|
+
- Directory: `ls`, `pwd`, `tree`
|
|
56
|
+
- Git read: `git status`, `git log`, `git diff`, `git branch`
|
|
57
|
+
- Package info: `npm list`, `npm outdated`, `yarn info`
|
|
58
|
+
- System info: `uname`, `whoami`, `date`, `uptime`
|
|
59
|
+
|
|
60
|
+
Blocked commands:
|
|
61
|
+
- File modification: `rm`, `mv`, `cp`, `mkdir`, `touch`
|
|
62
|
+
- Git write: `git add`, `git commit`, `git push`
|
|
63
|
+
- Package install: `npm install`, `yarn add`, `pip install`
|
|
64
|
+
- System: `sudo`, `kill`, `reboot`
|
|
65
|
+
- Editors: `vim`, `nano`, `code`
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Mode Extension
|
|
3
|
+
*
|
|
4
|
+
* Read-only exploration mode for safe code analysis.
|
|
5
|
+
* When enabled, only read-only tools are available.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - /plan command or Shift+P to toggle
|
|
9
|
+
* - Bash restricted to allowlisted read-only commands
|
|
10
|
+
* - Extracts numbered plan steps from "Plan:" sections
|
|
11
|
+
* - [DONE:n] markers to complete steps during execution
|
|
12
|
+
* - Progress tracking widget during execution
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
16
|
+
import type { AssistantMessage, TextContent } from "@mariozechner/pi-ai";
|
|
17
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
18
|
+
import { Key } from "@mariozechner/pi-tui";
|
|
19
|
+
import { extractTodoItems, isSafeCommand, markCompletedSteps, type TodoItem } from "./utils.js";
|
|
20
|
+
|
|
21
|
+
// Tools
|
|
22
|
+
const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire"];
|
|
23
|
+
const NORMAL_MODE_TOOLS = ["read", "bash", "edit", "write"];
|
|
24
|
+
|
|
25
|
+
// Type guard for assistant messages
|
|
26
|
+
function isAssistantMessage(m: AgentMessage): m is AssistantMessage {
|
|
27
|
+
return m.role === "assistant" && Array.isArray(m.content);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Extract text content from an assistant message
|
|
31
|
+
function getTextContent(message: AssistantMessage): string {
|
|
32
|
+
return message.content
|
|
33
|
+
.filter((block): block is TextContent => block.type === "text")
|
|
34
|
+
.map((block) => block.text)
|
|
35
|
+
.join("\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default function planModeExtension(pi: ExtensionAPI): void {
|
|
39
|
+
let planModeEnabled = false;
|
|
40
|
+
let executionMode = false;
|
|
41
|
+
let todoItems: TodoItem[] = [];
|
|
42
|
+
|
|
43
|
+
pi.registerFlag("plan", {
|
|
44
|
+
description: "Start in plan mode (read-only exploration)",
|
|
45
|
+
type: "boolean",
|
|
46
|
+
default: false,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
function updateStatus(ctx: ExtensionContext): void {
|
|
50
|
+
// Footer status
|
|
51
|
+
if (executionMode && todoItems.length > 0) {
|
|
52
|
+
const completed = todoItems.filter((t) => t.completed).length;
|
|
53
|
+
ctx.ui.setStatus("plan-mode", ctx.ui.theme.fg("accent", `📋 ${completed}/${todoItems.length}`));
|
|
54
|
+
} else if (planModeEnabled) {
|
|
55
|
+
ctx.ui.setStatus("plan-mode", ctx.ui.theme.fg("warning", "⏸ plan"));
|
|
56
|
+
} else {
|
|
57
|
+
ctx.ui.setStatus("plan-mode", undefined);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Widget showing todo list
|
|
61
|
+
if (executionMode && todoItems.length > 0) {
|
|
62
|
+
const lines = todoItems.map((item) => {
|
|
63
|
+
if (item.completed) {
|
|
64
|
+
return (
|
|
65
|
+
ctx.ui.theme.fg("success", "☑ ") + ctx.ui.theme.fg("muted", ctx.ui.theme.strikethrough(item.text))
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return `${ctx.ui.theme.fg("muted", "☐ ")}${item.text}`;
|
|
69
|
+
});
|
|
70
|
+
ctx.ui.setWidget("plan-todos", lines);
|
|
71
|
+
} else {
|
|
72
|
+
ctx.ui.setWidget("plan-todos", undefined);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function togglePlanMode(ctx: ExtensionContext): void {
|
|
77
|
+
planModeEnabled = !planModeEnabled;
|
|
78
|
+
executionMode = false;
|
|
79
|
+
todoItems = [];
|
|
80
|
+
|
|
81
|
+
if (planModeEnabled) {
|
|
82
|
+
pi.setActiveTools(PLAN_MODE_TOOLS);
|
|
83
|
+
ctx.ui.notify(`Plan mode enabled. Tools: ${PLAN_MODE_TOOLS.join(", ")}`);
|
|
84
|
+
} else {
|
|
85
|
+
pi.setActiveTools(NORMAL_MODE_TOOLS);
|
|
86
|
+
ctx.ui.notify("Plan mode disabled. Full access restored.");
|
|
87
|
+
}
|
|
88
|
+
updateStatus(ctx);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function persistState(): void {
|
|
92
|
+
pi.appendEntry("plan-mode", {
|
|
93
|
+
enabled: planModeEnabled,
|
|
94
|
+
todos: todoItems,
|
|
95
|
+
executing: executionMode,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
pi.registerCommand("plan", {
|
|
100
|
+
description: "Toggle plan mode (read-only exploration)",
|
|
101
|
+
handler: async (_args, ctx) => togglePlanMode(ctx),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
pi.registerCommand("todos", {
|
|
105
|
+
description: "Show current plan todo list",
|
|
106
|
+
handler: async (_args, ctx) => {
|
|
107
|
+
if (todoItems.length === 0) {
|
|
108
|
+
ctx.ui.notify("No todos. Create a plan first with /plan", "info");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const list = todoItems.map((item, i) => `${i + 1}. ${item.completed ? "✓" : "○"} ${item.text}`).join("\n");
|
|
112
|
+
ctx.ui.notify(`Plan Progress:\n${list}`, "info");
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
pi.registerShortcut(Key.shift("p"), {
|
|
117
|
+
description: "Toggle plan mode",
|
|
118
|
+
handler: async (ctx) => togglePlanMode(ctx),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Block destructive bash commands in plan mode
|
|
122
|
+
pi.on("tool_call", async (event) => {
|
|
123
|
+
if (!planModeEnabled || event.toolName !== "bash") return;
|
|
124
|
+
|
|
125
|
+
const command = event.input.command as string;
|
|
126
|
+
if (!isSafeCommand(command)) {
|
|
127
|
+
return {
|
|
128
|
+
block: true,
|
|
129
|
+
reason: `Plan mode: command blocked (not allowlisted). Use /plan to disable plan mode first.\nCommand: ${command}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Filter out stale plan mode context when not in plan mode
|
|
135
|
+
pi.on("context", async (event) => {
|
|
136
|
+
if (planModeEnabled) return;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
messages: event.messages.filter((m) => {
|
|
140
|
+
const msg = m as AgentMessage & { customType?: string };
|
|
141
|
+
if (msg.customType === "plan-mode-context") return false;
|
|
142
|
+
if (msg.role !== "user") return true;
|
|
143
|
+
|
|
144
|
+
const content = msg.content;
|
|
145
|
+
if (typeof content === "string") {
|
|
146
|
+
return !content.includes("[PLAN MODE ACTIVE]");
|
|
147
|
+
}
|
|
148
|
+
if (Array.isArray(content)) {
|
|
149
|
+
return !content.some(
|
|
150
|
+
(c) => c.type === "text" && (c as TextContent).text?.includes("[PLAN MODE ACTIVE]"),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Inject plan/execution context before agent starts
|
|
159
|
+
pi.on("before_agent_start", async () => {
|
|
160
|
+
if (planModeEnabled) {
|
|
161
|
+
return {
|
|
162
|
+
message: {
|
|
163
|
+
customType: "plan-mode-context",
|
|
164
|
+
content: `[PLAN MODE ACTIVE]
|
|
165
|
+
You are in plan mode - a read-only exploration mode for safe code analysis.
|
|
166
|
+
|
|
167
|
+
Restrictions:
|
|
168
|
+
- You can only use: read, bash, grep, find, ls, questionnaire
|
|
169
|
+
- You CANNOT use: edit, write (file modifications are disabled)
|
|
170
|
+
- Bash is restricted to an allowlist of read-only commands
|
|
171
|
+
|
|
172
|
+
Ask clarifying questions using the questionnaire tool.
|
|
173
|
+
Use brave-search skill via bash for web research.
|
|
174
|
+
|
|
175
|
+
Create a detailed numbered plan under a "Plan:" header:
|
|
176
|
+
|
|
177
|
+
Plan:
|
|
178
|
+
1. First step description
|
|
179
|
+
2. Second step description
|
|
180
|
+
...
|
|
181
|
+
|
|
182
|
+
Do NOT attempt to make changes - just describe what you would do.`,
|
|
183
|
+
display: false,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (executionMode && todoItems.length > 0) {
|
|
189
|
+
const remaining = todoItems.filter((t) => !t.completed);
|
|
190
|
+
const todoList = remaining.map((t) => `${t.step}. ${t.text}`).join("\n");
|
|
191
|
+
return {
|
|
192
|
+
message: {
|
|
193
|
+
customType: "plan-execution-context",
|
|
194
|
+
content: `[EXECUTING PLAN - Full tool access enabled]
|
|
195
|
+
|
|
196
|
+
Remaining steps:
|
|
197
|
+
${todoList}
|
|
198
|
+
|
|
199
|
+
Execute each step in order.
|
|
200
|
+
After completing a step, include a [DONE:n] tag in your response.`,
|
|
201
|
+
display: false,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Track progress after each turn
|
|
208
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
209
|
+
if (!executionMode || todoItems.length === 0) return;
|
|
210
|
+
if (!isAssistantMessage(event.message)) return;
|
|
211
|
+
|
|
212
|
+
const text = getTextContent(event.message);
|
|
213
|
+
if (markCompletedSteps(text, todoItems) > 0) {
|
|
214
|
+
updateStatus(ctx);
|
|
215
|
+
}
|
|
216
|
+
persistState();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Handle plan completion and plan mode UI
|
|
220
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
221
|
+
// Check if execution is complete
|
|
222
|
+
if (executionMode && todoItems.length > 0) {
|
|
223
|
+
if (todoItems.every((t) => t.completed)) {
|
|
224
|
+
const completedList = todoItems.map((t) => `~~${t.text}~~`).join("\n");
|
|
225
|
+
pi.sendMessage(
|
|
226
|
+
{ customType: "plan-complete", content: `**Plan Complete!** ✓\n\n${completedList}`, display: true },
|
|
227
|
+
{ triggerTurn: false },
|
|
228
|
+
);
|
|
229
|
+
executionMode = false;
|
|
230
|
+
todoItems = [];
|
|
231
|
+
pi.setActiveTools(NORMAL_MODE_TOOLS);
|
|
232
|
+
updateStatus(ctx);
|
|
233
|
+
persistState(); // Save cleared state so resume doesn't restore old execution mode
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!planModeEnabled || !ctx.hasUI) return;
|
|
239
|
+
|
|
240
|
+
// Extract todos from last assistant message
|
|
241
|
+
const lastAssistant = [...event.messages].reverse().find(isAssistantMessage);
|
|
242
|
+
if (lastAssistant) {
|
|
243
|
+
const extracted = extractTodoItems(getTextContent(lastAssistant));
|
|
244
|
+
if (extracted.length > 0) {
|
|
245
|
+
todoItems = extracted;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Show plan steps and prompt for next action
|
|
250
|
+
if (todoItems.length > 0) {
|
|
251
|
+
const todoListText = todoItems.map((t, i) => `${i + 1}. ☐ ${t.text}`).join("\n");
|
|
252
|
+
pi.sendMessage(
|
|
253
|
+
{
|
|
254
|
+
customType: "plan-todo-list",
|
|
255
|
+
content: `**Plan Steps (${todoItems.length}):**\n\n${todoListText}`,
|
|
256
|
+
display: true,
|
|
257
|
+
},
|
|
258
|
+
{ triggerTurn: false },
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const choice = await ctx.ui.select("Plan mode - what next?", [
|
|
263
|
+
todoItems.length > 0 ? "Execute the plan (track progress)" : "Execute the plan",
|
|
264
|
+
"Stay in plan mode",
|
|
265
|
+
"Refine the plan",
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
if (choice?.startsWith("Execute")) {
|
|
269
|
+
planModeEnabled = false;
|
|
270
|
+
executionMode = todoItems.length > 0;
|
|
271
|
+
pi.setActiveTools(NORMAL_MODE_TOOLS);
|
|
272
|
+
updateStatus(ctx);
|
|
273
|
+
|
|
274
|
+
const execMessage =
|
|
275
|
+
todoItems.length > 0
|
|
276
|
+
? `Execute the plan. Start with: ${todoItems[0].text}`
|
|
277
|
+
: "Execute the plan you just created.";
|
|
278
|
+
pi.sendMessage(
|
|
279
|
+
{ customType: "plan-mode-execute", content: execMessage, display: true },
|
|
280
|
+
{ triggerTurn: true },
|
|
281
|
+
);
|
|
282
|
+
} else if (choice === "Refine the plan") {
|
|
283
|
+
const refinement = await ctx.ui.editor("Refine the plan:", "");
|
|
284
|
+
if (refinement?.trim()) {
|
|
285
|
+
pi.sendUserMessage(refinement.trim());
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Restore state on session start/resume
|
|
291
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
292
|
+
if (pi.getFlag("plan") === true) {
|
|
293
|
+
planModeEnabled = true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const entries = ctx.sessionManager.getEntries();
|
|
297
|
+
|
|
298
|
+
// Restore persisted state
|
|
299
|
+
const planModeEntry = entries
|
|
300
|
+
.filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "plan-mode")
|
|
301
|
+
.pop() as { data?: { enabled: boolean; todos?: TodoItem[]; executing?: boolean } } | undefined;
|
|
302
|
+
|
|
303
|
+
if (planModeEntry?.data) {
|
|
304
|
+
planModeEnabled = planModeEntry.data.enabled ?? planModeEnabled;
|
|
305
|
+
todoItems = planModeEntry.data.todos ?? todoItems;
|
|
306
|
+
executionMode = planModeEntry.data.executing ?? executionMode;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// On resume: re-scan messages to rebuild completion state
|
|
310
|
+
// Only scan messages AFTER the last "plan-mode-execute" to avoid picking up [DONE:n] from previous plans
|
|
311
|
+
const isResume = planModeEntry !== undefined;
|
|
312
|
+
if (isResume && executionMode && todoItems.length > 0) {
|
|
313
|
+
// Find the index of the last plan-mode-execute entry (marks when current execution started)
|
|
314
|
+
let executeIndex = -1;
|
|
315
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
316
|
+
const entry = entries[i] as { type: string; customType?: string };
|
|
317
|
+
if (entry.customType === "plan-mode-execute") {
|
|
318
|
+
executeIndex = i;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Only scan messages after the execute marker
|
|
324
|
+
const messages: AssistantMessage[] = [];
|
|
325
|
+
for (let i = executeIndex + 1; i < entries.length; i++) {
|
|
326
|
+
const entry = entries[i];
|
|
327
|
+
if (entry.type === "message" && "message" in entry && isAssistantMessage(entry.message as AgentMessage)) {
|
|
328
|
+
messages.push(entry.message as AssistantMessage);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const allText = messages.map(getTextContent).join("\n");
|
|
332
|
+
markCompletedSteps(allText, todoItems);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (planModeEnabled) {
|
|
336
|
+
pi.setActiveTools(PLAN_MODE_TOOLS);
|
|
337
|
+
}
|
|
338
|
+
updateStatus(ctx);
|
|
339
|
+
});
|
|
340
|
+
}
|