mono-pilot 0.1.0 → 0.2.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 (38) hide show
  1. package/README.md +107 -42
  2. package/dist/src/cli.js +15 -1
  3. package/dist/src/extensions/mode-runtime.js +124 -0
  4. package/dist/src/extensions/mono-pilot.js +24 -0
  5. package/dist/src/extensions/system-prompt.js +301 -0
  6. package/dist/src/extensions/user-message.js +263 -0
  7. package/dist/src/utils/mcp-client.js +263 -0
  8. package/dist/tools/README.md +36 -19
  9. package/dist/tools/apply-patch-description.md +92 -0
  10. package/dist/tools/apply-patch.js +218 -60
  11. package/dist/tools/apply-patch.test.js +89 -0
  12. package/dist/tools/ask-question.js +281 -0
  13. package/dist/tools/call-mcp-tool.js +367 -0
  14. package/dist/tools/delete.js +3 -36
  15. package/dist/tools/fetch-mcp-resource.js +308 -0
  16. package/dist/tools/glob.js +17 -38
  17. package/dist/tools/list-mcp-resources.js +192 -0
  18. package/dist/tools/list-mcp-tools.js +230 -0
  19. package/dist/tools/plan-mode-reminder.md +62 -0
  20. package/dist/tools/read-file.js +97 -49
  21. package/dist/tools/rg.js +225 -76
  22. package/dist/tools/rg.test.js +78 -0
  23. package/dist/tools/semantic-search-description.md +94 -0
  24. package/dist/tools/semantic-search.js +358 -0
  25. package/dist/tools/shell-description.md +142 -0
  26. package/dist/tools/shell.js +63 -46
  27. package/dist/tools/subagent-description.md +64 -0
  28. package/dist/tools/subagent.js +1043 -0
  29. package/dist/tools/switch-mode.js +336 -0
  30. package/dist/tools/web-fetch.js +360 -0
  31. package/dist/tools/web-search.js +253 -0
  32. package/package.json +16 -6
  33. package/dist/tools/apply-patch.md +0 -93
  34. package/dist/tools/delete.md +0 -5
  35. package/dist/tools/glob.md +0 -18
  36. package/dist/tools/read-file.md +0 -23
  37. package/dist/tools/rg.md +0 -35
  38. package/dist/tools/shell.md +0 -152
package/README.md CHANGED
@@ -1,39 +1,19 @@
1
1
  # MonoPilot
2
2
 
3
- > Cursor-compatible coding agent profile powered by pi-mono.
3
+ > Cursor-compatible coding agent profile powered by [pi](https://github.com/badlogic/pi-mono).
4
4
 
5
- **⚠️ Disclaimer:** This project is an independent compatibility implementation and is **not affiliated with Cursor** or Anysphere Inc.
5
+ MonoPilot is a lightweight, highly customizable Cursor-compatible coding agent built on top of the [pi](https://github.com/badlogic/pi-mono) framework. It is designed for developers who want full control over how their coding agent behaves, prefer not to pay a middleman, and want to explore the limits of a lean agent architecture.
6
6
 
7
- ## MVP Scope (Current)
7
+ ## Why MonoPilot
8
8
 
9
- This repository is intentionally in a **minimal MVP** stage:
9
+ - Transparent prompt/runtime envelope with inspection tooling.
10
+ - Cursor-styled tool layer replaces default pi tools so launch-time capability and behavior are defined by MonoPilot.
11
+ - Extensible tool layer with MCP support for custom tools and resources.
10
12
 
11
- - ✅ Core toolset is wired through `mono-pilot` extensions (`read`, `rg`, `glob`, `shell`, `delete`, `apply_patch`).
12
- - ✅ Built-in `edit` / `write` are removed from default exposed tools.
13
- - ✅ File mutation is funneled through `apply_patch` (which internally delegates to robust pi-mono primitives).
14
- - ✅ Additional tools can be migrated incrementally without changing the launcher architecture.
15
-
16
- ## What ships now
17
-
18
- - `src/cli.ts` – `mono-pilot` launcher that wraps `pi`
19
- - `src/extensions/mono-pilot.ts` – extension entrypoint (wires the current toolset)
20
- - `tools/*.ts` – tool implementations (`read`, `rg`, `glob`, `shell`, `delete`, `apply_patch`)
21
- - `tools/*.md` – tool prompt specs injected at runtime
22
-
23
- ## Local development setup
24
-
25
- ```bash
26
- git clone https://github.com/qianwan/mono-pilot.git
27
- cd mono-pilot
28
- npm install
29
- npm run build
30
- ```
31
-
32
- ## Use in any repository
13
+ ## Quickstart
33
14
 
34
15
  ```bash
35
16
  # Run directly without global install
36
- cd /path/to/your/project
37
17
  npx mono-pilot
38
18
 
39
19
  # Or install globally
@@ -58,30 +38,115 @@ By default, `mono-pilot` launches pi with:
58
38
 
59
39
  - `--no-extensions`
60
40
  - `--extension <mono-pilot extension>`
61
- - `--tools read,bash,grep,find,ls`
41
+ - `--tools ls` (only when you do not pass `--tools` or `--no-tools`)
42
+
43
+ If you pass `--tools`, MonoPilot removes built-in `edit`, `write`, `read`, `grep`, `glob`, and `bash` so the extension-provided Cursor-styled tools are used instead. If the list becomes empty, it falls back to `ls`. The write path is provided by the `ApplyPatch` tool from the extension.
44
+
45
+ ## What ships now
46
+
47
+ - `src/cli.ts` – launcher that wraps `pi`
48
+ - `src/extensions/mono-pilot.ts` – extension entrypoint (tool wiring)
49
+ - `src/extensions/system-prompt.ts` – provider-agnostic prompt stack
50
+ - `src/extensions/user-message.ts` – user message envelope assembly
51
+ - `tools/` – tool implementations and descriptions (see `tools/README.md`)
52
+
53
+ ## Cursor-styled tools
54
+
55
+ MonoPilot exposes a Cursor-style tool set to highlight capability at launch:
56
+
57
+ These replace pi defaults such as `edit`, `write`, `read`, `grep`, `glob`, and `bash`.
58
+
59
+ Default-to-MonoPilot mapping:
60
+
61
+ - `edit` / `write` → `ApplyPatch`
62
+ - `read` → `ReadFile`
63
+ - `grep` → `rg`
64
+ - `glob` → `Glob`
65
+ - `bash` → `Shell`
66
+
67
+ The full Cursor-styled tool list exposed by the extension:
68
+
69
+ - `Shell` – execute shell commands in the workspace
70
+ - `Glob` – find paths by glob pattern
71
+ - `rg` – search file content with ripgrep
72
+ - `ReadFile` – read file content with pagination
73
+ - `Delete` – delete files or directories
74
+ - `SemanticSearch` – semantic search by intent
75
+ - `WebSearch` – search the web with snippets
76
+ - `WebFetch` – fetch and render web content
77
+ - `AskQuestion` – collect structured multiple-choice answers
78
+ - `Subagent` – launch delegated subprocesses
79
+ - `ListMcpResources` – list MCP resources from config
80
+ - `FetchMcpResource` – fetch a specific MCP resource
81
+ - `ListMcpTools` – discover MCP tools and schemas
82
+ - `CallMcpTool` – invoke MCP tools by name
83
+ - `SwitchMode` – switch interaction mode (`/plan`)
84
+ - `ApplyPatch` – apply single-file patches
62
85
 
63
- So the exposed write path is `apply_patch`, not built-in `edit` / `write`.
64
- If you pass `--tools` manually and include `edit`/`write`, MonoPilot strips them automatically.
86
+ ## User rules
65
87
 
66
- ## Publishing
88
+ MonoPilot can inject workspace user rules into the runtime envelope on each input (handled by `src/extensions/user-message.ts`).
89
+
90
+ - Rules live in `.pi/rules/*.rule.txt` under the workspace root
91
+ - Each file becomes one `<user_rule>` block wrapped by a `<rules>` envelope
92
+ - Files are read in filename order; empty files are ignored
93
+ - If no rules are present, the `<rules>` section is omitted
94
+
95
+ ## MCP
96
+
97
+ - The user message envelope issues a lightweight MCP server `initialize` request to collect server instructions.
98
+ - MCP tools then progressively load and surface resources, schemas, and execution only when needed.
99
+
100
+ ## Local development
67
101
 
68
102
  ```bash
69
- # 1) Validate and build
70
- npm run check
103
+ git clone https://github.com/qianwan/mono-pilot.git
104
+ cd mono-pilot
105
+ npm install
71
106
  npm run build
107
+ ```
108
+
109
+ Source-mode development (no build needed on each change):
110
+
111
+ ```bash
112
+ # Run from TypeScript sources directly
113
+ npm run dev
114
+
115
+ # Optional: auto-restart on file changes
116
+ npm run dev:watch
72
117
 
73
- # 2) Verify package contents
74
- npm pack --dry-run
118
+ # Continue the latest session from source mode
119
+ npm run dev:continue
75
120
 
76
- # 3) Publish
77
- npm publish
121
+ # Auto-restart + continue latest session
122
+ npm run dev:watch:continue
78
123
  ```
79
124
 
80
- ## Roadmap
125
+ ## Prompt inspection
126
+
127
+ ```bash
128
+ # Build first (ensures dist extension exists)
129
+ npm run build
130
+
131
+ # Print injected system prompt + runtime envelope (snippet)
132
+ npm run inspect:injection
133
+
134
+ # Print full prompt and envelope to stdout
135
+ npm run inspect:injection:full
81
136
 
82
- - Gradually migrate other Cursor-style tools from `tools/`
83
- - Keep compatibility behavior focused and testable, one tool at a time
137
+ # Provide a custom user query to render
138
+ node scripts/inspect-injection.mjs --query="Summarize this repo"
139
+ ```
140
+
141
+ The report shows:
142
+ - the final system prompt after tool injection
143
+ - the runtime envelope built from `<rules>`, `<mcp_instructions>`, `<system_reminder>`, and `<user_query>`
144
+
145
+
146
+ ## Roadmap
84
147
 
85
- ---
148
+ Project status: MVP (core capabilities are in place and actively evolving).
86
149
 
87
- *Not affiliated with Cursor or Anysphere Inc. Cursor is a trademark of Anysphere Inc.*
150
+ - Gradually migrate additional Cursor-style tools from `tools/`.
151
+ - Keep compatibility behavior focused and testable, one tool at a time.
152
+ - Expand docs and examples for customization.
package/dist/src/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
+ import { existsSync } from "node:fs";
3
4
  import { dirname, resolve } from "node:path";
4
5
  import process from "node:process";
5
6
  import { fileURLToPath } from "node:url";
@@ -47,9 +48,22 @@ function sanitizeToolsArgs(args) {
47
48
  }
48
49
  return sanitized;
49
50
  }
51
+ function resolveExtensionPath(here) {
52
+ const candidates = [
53
+ resolve(here, "extensions", "mono-pilot.js"),
54
+ resolve(here, "extensions", "mono-pilot.ts"),
55
+ ];
56
+ for (const candidate of candidates) {
57
+ if (existsSync(candidate)) {
58
+ return candidate;
59
+ }
60
+ }
61
+ // Fallback keeps previous behavior even if file is unexpectedly missing.
62
+ return candidates[0];
63
+ }
50
64
  function buildPiArgs(userArgs) {
51
65
  const here = dirname(fileURLToPath(import.meta.url));
52
- const extensionPath = resolve(here, "extensions", "mono-pilot.js");
66
+ const extensionPath = resolveExtensionPath(here);
53
67
  const sanitizedUserArgs = sanitizeToolsArgs(userArgs);
54
68
  const args = ["--no-extensions", "--extension", extensionPath];
55
69
  if (!hasFlag(sanitizedUserArgs, ["--tools", "--no-tools"])) {
@@ -0,0 +1,124 @@
1
+ export const MODE_STATE_ENTRY_TYPE = "switch-mode-state";
2
+ export const PLAN_MODE_STILL_ACTIVE_REMINDER = `<system_reminder>
3
+ Plan mode is still active. Continue with the task in the current mode.
4
+ </system_reminder>`;
5
+ export const AGENT_MODE_SWITCH_REMINDER = `<system_reminder>
6
+ You are now in Agent mode. Continue with the task in the new mode.
7
+ </system_reminder>`;
8
+ export function parseModeStateEntry(entry) {
9
+ if (typeof entry !== "object" || entry === null)
10
+ return undefined;
11
+ const record = entry;
12
+ if (record.type !== "custom")
13
+ return undefined;
14
+ if (record.customType !== MODE_STATE_ENTRY_TYPE)
15
+ return undefined;
16
+ const data = record.data;
17
+ if (typeof data !== "object" || data === null)
18
+ return undefined;
19
+ const state = data;
20
+ if (state.activeMode === "plan" || state.activeMode === "agent") {
21
+ return {
22
+ activeMode: state.activeMode,
23
+ pendingReminder: state.pendingReminder === "plan-entry" || state.pendingReminder === "agent-entry"
24
+ ? state.pendingReminder
25
+ : undefined,
26
+ };
27
+ }
28
+ if (typeof state.planModeActive === "boolean") {
29
+ return {
30
+ activeMode: state.planModeActive ? "plan" : "agent",
31
+ pendingReminder: undefined,
32
+ };
33
+ }
34
+ return undefined;
35
+ }
36
+ export function deriveInitialModeState(fromPlanFlag) {
37
+ if (fromPlanFlag) {
38
+ return { activeMode: "plan", pendingReminder: "plan-entry" };
39
+ }
40
+ return { activeMode: "agent" };
41
+ }
42
+ export function createModeStateData(snapshot) {
43
+ return {
44
+ activeMode: snapshot.activeMode,
45
+ pendingReminder: snapshot.pendingReminder,
46
+ planModeActive: snapshot.activeMode === "plan",
47
+ };
48
+ }
49
+ export function hasMessageEntries(entries) {
50
+ for (const entry of entries) {
51
+ if (typeof entry !== "object" || entry === null)
52
+ continue;
53
+ const record = entry;
54
+ if (record.type === "message")
55
+ return true;
56
+ }
57
+ return false;
58
+ }
59
+ export function buildRuntimeEnvelope(userQuery, reminder, mcpInstructions, rulesEnvelope) {
60
+ const sections = [];
61
+ if (rulesEnvelope) {
62
+ sections.push(rulesEnvelope.trim());
63
+ }
64
+ if (mcpInstructions) {
65
+ sections.push(mcpInstructions.trim());
66
+ }
67
+ if (reminder) {
68
+ sections.push(reminder.trim());
69
+ }
70
+ sections.push(`<user_query>\n${userQuery}\n</user_query>`);
71
+ return sections.join("\n\n");
72
+ }
73
+ class ModeRuntimeStore {
74
+ state = { activeMode: "agent" };
75
+ initialize(snapshot) {
76
+ this.state = { ...snapshot };
77
+ }
78
+ getSnapshot() {
79
+ return { ...this.state };
80
+ }
81
+ setMode(nextMode) {
82
+ if (this.state.activeMode === nextMode) {
83
+ return { changed: false, snapshot: this.getSnapshot() };
84
+ }
85
+ this.state.activeMode = nextMode;
86
+ this.state.pendingReminder = nextMode === "plan" ? "plan-entry" : "agent-entry";
87
+ return { changed: true, snapshot: this.getSnapshot() };
88
+ }
89
+ toggleMode() {
90
+ const nextMode = this.state.activeMode === "plan" ? "agent" : "plan";
91
+ return this.setMode(nextMode);
92
+ }
93
+ consumeReminder(planEntryReminder) {
94
+ if (this.state.activeMode === "plan") {
95
+ if (this.state.pendingReminder === "plan-entry") {
96
+ this.state.pendingReminder = undefined;
97
+ return {
98
+ reminder: planEntryReminder,
99
+ changed: true,
100
+ snapshot: this.getSnapshot(),
101
+ };
102
+ }
103
+ return {
104
+ reminder: PLAN_MODE_STILL_ACTIVE_REMINDER,
105
+ changed: false,
106
+ snapshot: this.getSnapshot(),
107
+ };
108
+ }
109
+ if (this.state.pendingReminder === "agent-entry") {
110
+ this.state.pendingReminder = undefined;
111
+ return {
112
+ reminder: AGENT_MODE_SWITCH_REMINDER,
113
+ changed: true,
114
+ snapshot: this.getSnapshot(),
115
+ };
116
+ }
117
+ return {
118
+ reminder: undefined,
119
+ changed: false,
120
+ snapshot: this.getSnapshot(),
121
+ };
122
+ }
123
+ }
124
+ export const modeRuntimeStore = new ModeRuntimeStore();
@@ -3,14 +3,38 @@ import globExtension from "../../tools/glob.js";
3
3
  import rgExtension from "../../tools/rg.js";
4
4
  import readFileExtension from "../../tools/read-file.js";
5
5
  import deleteExtension from "../../tools/delete.js";
6
+ import semanticSearchExtension from "../../tools/semantic-search.js";
7
+ import webSearchExtension from "../../tools/web-search.js";
8
+ import webFetchExtension from "../../tools/web-fetch.js";
9
+ import askQuestionExtension from "../../tools/ask-question.js";
10
+ import subagentExtension from "../../tools/subagent.js";
11
+ import listMcpResourcesExtension from "../../tools/list-mcp-resources.js";
12
+ import fetchMcpResourceExtension from "../../tools/fetch-mcp-resource.js";
13
+ import listMcpToolsExtension from "../../tools/list-mcp-tools.js";
14
+ import callMcpToolExtension from "../../tools/call-mcp-tool.js";
15
+ import switchModeExtension from "../../tools/switch-mode.js";
6
16
  import applyPatchExtension from "../../tools/apply-patch.js";
17
+ import userMessageExtension from "./user-message.js";
18
+ import systemPromptExtension from "./system-prompt.js";
7
19
  const toolExtensions = [
8
20
  shellExtension,
9
21
  globExtension,
10
22
  rgExtension,
11
23
  readFileExtension,
12
24
  deleteExtension,
25
+ semanticSearchExtension,
26
+ webSearchExtension,
27
+ webFetchExtension,
28
+ askQuestionExtension,
29
+ subagentExtension,
30
+ listMcpResourcesExtension,
31
+ fetchMcpResourceExtension,
32
+ listMcpToolsExtension,
33
+ callMcpToolExtension,
34
+ switchModeExtension,
13
35
  applyPatchExtension,
36
+ userMessageExtension,
37
+ systemPromptExtension,
14
38
  ];
15
39
  export default function monoPilotExtension(pi) {
16
40
  for (const register of toolExtensions) {
@@ -0,0 +1,301 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import process from "node:process";
4
+ import { fileURLToPath } from "node:url";
5
+ const NO_DESCRIPTION_PLACEHOLDER = "No short description provided.";
6
+ const PROJECT_CONTEXT_HEADER = "# Project Context";
7
+ const CURRENT_DATETIME_PREFIX = "Current date and time:";
8
+ const CURRENT_WORKING_DIRECTORY_PREFIX = "Current working directory:";
9
+ const AVAILABLE_TOOLS_TOKEN = "{{AVAILABLE_TOOLS_BULLETS}}";
10
+ const PROJECT_CONTEXT_TOKEN = "{{PROJECT_CONTEXT_BLOCK}}";
11
+ const CURRENT_DATETIME_TOKEN = "{{CURRENT_DATETIME}}";
12
+ const CURRENT_WORKING_DIRECTORY_TOKEN = "{{CURRENT_WORKING_DIRECTORY}}";
13
+ const PI_README_PATH_TOKEN = "{{PI_README_PATH}}";
14
+ const PI_DOCS_PATH_TOKEN = "{{PI_DOCS_PATH}}";
15
+ const PI_EXAMPLES_PATH_TOKEN = "{{PI_EXAMPLES_PATH}}";
16
+ const UNIFIED_SYSTEM_PROMPT_TEMPLATE = `You are an expert coding assistant operating inside MonoPilot (a pi-coding-agent compatibility harness) on a user's computer.
17
+ You help users by reading files, executing commands, editing code, and writing new files.
18
+
19
+ <oververbosity>
20
+ ## Desired oververbosity for the final answer (not analysis): 2
21
+ An oververbosity of 1 means the model should respond using only the minimal content necessary to satisfy the request, using concise phrasing and avoiding extra detail or explanation."
22
+ An oververbosity of 10 means the model should provide maximally detailed, thorough responses with context, explanations, and possibly multiple examples."
23
+ The desired oververbosity should be treated only as a *default*. Defer to any user or developer requirements regarding response length, if present.
24
+ </oververbosity>
25
+
26
+ <general>
27
+ - Each time the user sends a message, we may automatically attach some information about their current state, such as what files they have open, where their cursor is, recently viewed files, edit history in their session so far, linter errors, and more. This information may or may not be relevant to the coding task, it is up for you to decide.
28
+ - When using the Shell tool, your terminal session is persisted across tool calls. On the first call, you should cd to the appropriate directory and do necessary setup. On subsequent calls, you will have the same environment.
29
+ - If a tool exists for an action, prefer to use the tool instead of shell commands (e.g ReadFile over cat).
30
+ - Code chunks that you receive (via tool calls or from user) may include inline line numbers in the form "Lxxx:LINE_CONTENT", e.g. "L123:LINE_CONTENT". Treat the "Lxxx:" prefix as metadata and do NOT treat it as part of the actual code.
31
+ </general>
32
+
33
+ <system-communication>
34
+ Users can reference context like files and folders using the @ symbol, e.g. @src/components/ is a reference to the src/components/ folder.
35
+ </system-communication>
36
+
37
+ <tools>
38
+ Available tools:
39
+ ${AVAILABLE_TOOLS_TOKEN}
40
+ In addition to the tools above, custom extension tools may also be available.
41
+ </tools>
42
+
43
+ <persistence>
44
+ ## Autonomy and persistence
45
+
46
+ Persist until the task is fully handled end-to-end within the current turn whenever feasible: do not stop at analysis or partial fixes; carry changes through implementation, verification, and a clear explanation of outcomes unless the user explicitly pauses or redirects you.
47
+
48
+ Unless the user explicitly asks for a plan, asks a question about the code, is brainstorming potential solutions, or some other intent that makes it clear that code should not be written, assume the user wants you to make code changes or run tools to solve the user's problem. In these cases, it's bad to output your proposed solution in a message, you should go ahead and actually implement the change. If you encounter challenges or blockers, you should attempt to resolve them yourself.
49
+ </persistence>
50
+
51
+ <editing_constraints>
52
+ - Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them.
53
+ - Add succinct code comments that explain what is going on if code is not self-explanatory. You should not add comments like "Assigns the value to the variable", but a brief comment might be useful ahead of a complex code block that the user would otherwise have to spend time parsing out. Usage of these comments should be rare.
54
+ - Try to use \`ApplyPatch\` for single file edits, but it is fine to explore other options to make the edit if it does not work well. Do not use \`ApplyPatch\` for changes that are auto-generated (i.e. generating package.json or running a lint or format command like gofmt) or when scripting is more efficient (such as search and replacing a string across a codebase).
55
+ - You may be in a dirty git worktree.
56
+ - NEVER revert existing changes you did not make unless explicitly requested, since these changes were made by the user.
57
+ - If asked to make a commit or code edits and there are unrelated changes to your work or changes that you didn't make in those files, don't revert those changes.
58
+ - If the changes are in files you've touched recently, you should read carefully and understand how you can work with the changes rather than reverting them.
59
+ - If the changes are in unrelated files, just ignore them and don't revert them.
60
+ - Do not amend a commit unless explicitly requested to do so.
61
+ - While you are working, you might notice unexpected changes that you didn't make. If this happens, STOP IMMEDIATELY and ask the user how they would like to proceed.
62
+ - **NEVER** use destructive commands like \`git reset --hard\` or \`git checkout --\` unless specifically requested or approved by the user.
63
+ </editing_constraints>
64
+
65
+ <special_user_requests>
66
+ - If the user makes a simple request (such as asking for the time) which you can fulfill by running a terminal command (such as \`date\`), you should do so.
67
+ - If the user asks for a "review", default to a code review mindset: prioritise identifying bugs, risks, behavioural regressions, and missing tests. Findings must be the primary focus of the response - keep summaries or overviews brief and only after enumerating the issues. Present findings first (ordered by severity with file/codeblock references), follow with open questions or assumptions, and offer a change-summary only as a secondary detail. If no findings are discovered, state that explicitly and mention explicitly and mention any residual risks or testing gaps.
68
+ </special_user_requests>
69
+
70
+ <mode_selection>
71
+ Choose the best interaction mode for the user's current goal before proceeding. Reassess when the goal changes or you're stuck. If another mode would work better, call \`SwitchMode\` now and include a brief explanation.
72
+
73
+ - **Plan**: user asks for a plan, or the task is large/ambiguous or has meaningful trade-offs
74
+
75
+ Consult the \`SwitchMode\` tool description for detailed guidance on each mode and when to use it. Be proactive about switching to the optimal mode—this significantly improves your ability to help the user.
76
+ </mode_selection>
77
+
78
+ <linter_errors>
79
+ After substantive edits, use the ReadLints tool to check recently edited files for linter errors. If you've introduced any, fix them if you can easily figure out how.
80
+ </linter_errors>
81
+
82
+ <pi_docs_policy>
83
+ - Only when user asks about pi/pi-mono itself (its SDK, extensions, themes, skills, or TUI), consult:
84
+ - ${PI_README_PATH_TOKEN}
85
+ - ${PI_DOCS_PATH_TOKEN}
86
+ - ${PI_EXAMPLES_PATH_TOKEN}
87
+ - When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md)
88
+ - When working on pi topics, read the docs and examples, and follow .md cross-references before implementing
89
+ - Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)
90
+ </pi_docs_policy>
91
+
92
+ <project_context>
93
+ ${PROJECT_CONTEXT_TOKEN}
94
+ </project_context>
95
+
96
+ <working_with_the_user>
97
+ ## Working with the user
98
+
99
+ You are producing plain text that will later be styled by Cursor. Follow these rules exactly. Formatting should make results easy to scan, but not feel mechanical. Use judgment to decide how much structure adds value.
100
+
101
+ - Default: be very concise; friendly teammate tone.
102
+ - Do not begin responses with conversational interjections. Avoid openers such as acknowledgements ("Done —", "Got it", "Great question, ") or framing phrases.
103
+ - Ask only when needed; suggest ideas; mirror the user's style.
104
+ - For substantial work, summarize clearly; follow final-answer formatting.
105
+ - Skip heavy formatting for simple confirmations.
106
+ - Don't dump large files you've written; reference paths only.
107
+ - No "save/copy this file", user is on the same machine.
108
+ - Offer logical next steps (tests, commits, build) briefly; add verify steps if you couldn't do something.
109
+ - For code changes:
110
+ - Lead with a quick explanation of the change, and then give more details on the context covering where and why a change was made. Do not start this explanation with "summary", just jump right in.
111
+ - The user does not see command execution outputs. When asked to show the output of a command (e.g. \`git show\`), relay the important details in your answer or summarize the key lines so the user understands the result.
112
+
113
+ ## Final answer structure and style guidelines
114
+
115
+ - Use Markdown formatting.
116
+ - Plain text: Cursor handles styling; use structure only when it helps scanability or when response is several paragraphs.
117
+ - Headers: optional; short Title Case (1-5 words) starting with ## or ###; add only if they truly help.
118
+ - Bullets: use - ; merge related points; keep to one line when possible; 4-6 per list ordered by importance; keep phrasing consistent.
119
+ - Monospace: backticks for commands/paths/env vars/code ids and inline examples; use for literal keyword bullets; never combine with **.
120
+ - Structure: group related bullets; order sections general → specific → supporting; for subsections, start with a bolded keyword bullet, then items; match complexity to the task.
121
+ - Tone: collaborative, concise, factual; present tense, active voice; self-contained; no "above/below"; parallel wording.
122
+ - Don'ts: no nested bullets/hierarchies; no ANSI codes; don't cram unrelated keywords; keep keyword lists short—wrap/reformat if long; avoid naming formatting styles in answers.
123
+ - Adaptation: code explanations → precise, structured with code refs; simple tasks → lead with outcome; big changes → logical walkthrough + rationale + next actions; casual one-offs → plain sentences, no headers/bullets.
124
+ - Path and Symbol References: When referencing a file, directory or symbol, always surround it with backticks. Ex: \`getSha256()\`, \`src/app.ts\`. NEVER include line numbers or other info.
125
+
126
+ ## Citing Code Blocks
127
+
128
+ - Cite code when it illustrates better than words
129
+ - Don't overuse or cite large blocks; don't use codeblocks to show the final code since can already review them in UI
130
+ - Citing code that is in the codebase:\`\`\`startLine:endLine:filepath
131
+ // ... existing code ...
132
+ \`\`\`
133
+ - Do not add anything besides the startLine:endLine:filepath (no language tag, line numbers)
134
+ - Example:\`\`\`12:14:app/components/Todo.tsx
135
+ // ... existing code ...
136
+ \`\`\`
137
+ - Code blocks should contain the code content from the file
138
+ - You can truncate the code, add your own edits, or add comments for readability
139
+ - If you do truncate the code, include a comment to indicate that there is more code that is not shown
140
+ - YOU MUST SHOW AT LEAST 1 LINE OF CODE IN THE CODE BLOCK OR ELSE THE BLOCK WILL NOT RENDER PROPERLY IN THE EDITOR.
141
+ - Proposing new code that is not in the codebase
142
+ - Use fenced blocks with language tags; nothing else
143
+ - Prefer updating files directly, unless the user clearly wants you to propose code without editing files
144
+ - For both methods of citing code blocks:
145
+ - Always put a newline before the code fences (\n\`\`\`); no indentation between \n and \`\`\`; no newline between \`\`\` and startLine:endLine:filepath
146
+ - Remember that line numbers must NOT be included for non-codeblock citations (e.g. citing a filepath)
147
+
148
+ <intermediary_updates>
149
+ ## Intermediary updates
150
+ - User updates are short updates while you are working, they are NOT final answers.
151
+ - You use 1-2 sentence user updates to communicate progress and new information to the user as you are doing work.
152
+ - Do not begin responses with conversational interjections. Avoid openers such as acknowledgements ("Done —", "Got it", "Great question, ") or framing phrases.
153
+ - You provide user updates frequently, every 20s.
154
+ - Before exploring or doing substantial work, you start with a user update acknowledging the request and explaining your first step. You should include your understanding of the user request and explain what you will do.
155
+ - When exploring, e.g. searching, reading files you provide user updates as you go, every 20s, explaining what context you are gathering and what you've learned. Vary your sentence structure when providing these updates to avoid sounding repetitive - in particular, don't start each sentence the same way.
156
+ - After you have sufficient context, and the work is substantial you provide a longer plan (this is the only user update that may be longer than 2 sentences and can contain formatting).
157
+ - Before performing file edits of any kind, you provide updates explaining what edits you are making.
158
+ - As you are thinking, you very frequently provide updates even if not taking any actions, informing the user of your progress. You interrupt your thinking and send multiple updates in a row if thinking for more than 100 words.
159
+ </intermediary_updates>
160
+ </working_with_the_user>
161
+
162
+ <main_goal>
163
+ Your main goal is to follow the USER's instructions at each message, denoted by the <user_query> tag.
164
+ </main_goal>
165
+
166
+ <runtime_context>
167
+ Current date and time: ${CURRENT_DATETIME_TOKEN}
168
+ Current working directory: ${CURRENT_WORKING_DIRECTORY_TOKEN}
169
+ </runtime_context>`;
170
+ function getFirstDescriptionLine(description) {
171
+ if (!description)
172
+ return NO_DESCRIPTION_PLACEHOLDER;
173
+ for (const line of description.split(/\r?\n/)) {
174
+ const trimmed = line.trim();
175
+ if (trimmed.length > 0)
176
+ return trimmed;
177
+ }
178
+ return NO_DESCRIPTION_PLACEHOLDER;
179
+ }
180
+ function buildActiveToolBullets(activeToolNames, allTools) {
181
+ const activeSet = new Set(activeToolNames);
182
+ const lines = allTools
183
+ .filter((tool) => activeSet.has(tool.name))
184
+ .map((tool) => `- ${tool.name}: ${getFirstDescriptionLine(tool.description)}`);
185
+ return lines.length > 0 ? lines.join("\n") : "- (none)";
186
+ }
187
+ function extractProjectContextBlock(systemPrompt) {
188
+ const lines = systemPrompt.split(/\r?\n/);
189
+ const headerIndex = lines.findIndex((line) => line.trim() === PROJECT_CONTEXT_HEADER);
190
+ if (headerIndex === -1)
191
+ return undefined;
192
+ let endIndex = lines.length;
193
+ for (let i = headerIndex + 1; i < lines.length; i++) {
194
+ const trimmed = lines[i].trim();
195
+ if (trimmed.startsWith(CURRENT_DATETIME_PREFIX) || trimmed.startsWith(CURRENT_WORKING_DIRECTORY_PREFIX)) {
196
+ endIndex = i;
197
+ break;
198
+ }
199
+ }
200
+ const block = lines.slice(headerIndex + 1, endIndex).join("\n").trim();
201
+ if (block.length > 0)
202
+ return block;
203
+ return "Project-specific instructions were not provided.";
204
+ }
205
+ function extractValueAfterPrefix(systemPrompt, prefix) {
206
+ for (const line of systemPrompt.split(/\r?\n/)) {
207
+ const trimmed = line.trim();
208
+ if (trimmed.startsWith(prefix)) {
209
+ const value = trimmed.slice(prefix.length).trim();
210
+ if (value.length > 0)
211
+ return value;
212
+ }
213
+ }
214
+ return undefined;
215
+ }
216
+ function renderTemplate(template, values) {
217
+ return template
218
+ .split(AVAILABLE_TOOLS_TOKEN)
219
+ .join(values.tools)
220
+ .split(PROJECT_CONTEXT_TOKEN)
221
+ .join(values.projectContext)
222
+ .split(CURRENT_DATETIME_TOKEN)
223
+ .join(values.currentDateTime)
224
+ .split(CURRENT_WORKING_DIRECTORY_TOKEN)
225
+ .join(values.cwd)
226
+ .split(PI_README_PATH_TOKEN)
227
+ .join(values.piReadmePath)
228
+ .split(PI_DOCS_PATH_TOKEN)
229
+ .join(values.piDocsPath)
230
+ .split(PI_EXAMPLES_PATH_TOKEN)
231
+ .join(values.piExamplesPath);
232
+ }
233
+ function getFallbackDateTimeText() {
234
+ return new Date().toString();
235
+ }
236
+ function resolvePiPackageRoot() {
237
+ try {
238
+ const entryUrl = import.meta.resolve("@mariozechner/pi-coding-agent");
239
+ const entryPath = fileURLToPath(entryUrl);
240
+ let currentDir = dirname(entryPath);
241
+ while (true) {
242
+ const packageJsonPath = join(currentDir, "package.json");
243
+ if (existsSync(packageJsonPath)) {
244
+ try {
245
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
246
+ if (pkg.name === "@mariozechner/pi-coding-agent") {
247
+ return currentDir;
248
+ }
249
+ }
250
+ catch {
251
+ // Continue walking upward.
252
+ }
253
+ }
254
+ const parent = dirname(currentDir);
255
+ if (parent === currentDir)
256
+ break;
257
+ currentDir = parent;
258
+ }
259
+ }
260
+ catch {
261
+ // Fall through to undefined.
262
+ }
263
+ return undefined;
264
+ }
265
+ function resolvePiDocsPaths() {
266
+ const packageRoot = resolvePiPackageRoot();
267
+ if (!packageRoot) {
268
+ return {
269
+ readmePath: "@mariozechner/pi-coding-agent/README.md",
270
+ docsPath: "@mariozechner/pi-coding-agent/docs",
271
+ examplesPath: "@mariozechner/pi-coding-agent/examples",
272
+ };
273
+ }
274
+ return {
275
+ readmePath: resolve(join(packageRoot, "README.md")),
276
+ docsPath: resolve(join(packageRoot, "docs")),
277
+ examplesPath: resolve(join(packageRoot, "examples")),
278
+ };
279
+ }
280
+ export default function systemPromptExtension(pi) {
281
+ pi.on("before_agent_start", (event) => {
282
+ const tools = buildActiveToolBullets(pi.getActiveTools(), pi.getAllTools());
283
+ const piDocsPaths = resolvePiDocsPaths();
284
+ const projectContext = extractProjectContextBlock(event.systemPrompt) ?? "Project-specific instructions were not provided.";
285
+ const currentDateTime = extractValueAfterPrefix(event.systemPrompt, CURRENT_DATETIME_PREFIX) ?? getFallbackDateTimeText();
286
+ const cwd = extractValueAfterPrefix(event.systemPrompt, CURRENT_WORKING_DIRECTORY_PREFIX) ?? process.cwd();
287
+ const unifiedPrompt = renderTemplate(UNIFIED_SYSTEM_PROMPT_TEMPLATE, {
288
+ tools,
289
+ projectContext,
290
+ currentDateTime,
291
+ cwd,
292
+ piReadmePath: piDocsPaths.readmePath,
293
+ piDocsPath: piDocsPaths.docsPath,
294
+ piExamplesPath: piDocsPaths.examplesPath,
295
+ });
296
+ if (unifiedPrompt === event.systemPrompt) {
297
+ return undefined;
298
+ }
299
+ return { systemPrompt: unifiedPrompt };
300
+ });
301
+ }