pi-custom-system-prompt 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/extensions/system-prompt.ts +416 -0
- package/package.json +45 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-06-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release.
|
|
13
|
+
- Load a custom system prompt from `~/.pi/agent/system-prompts/<file>.md` and inject it on every agent turn via the `before_agent_start` event.
|
|
14
|
+
- Commands: `/system-prompt-info`, `/system-prompt-toggle`, `/system-prompt-reload`, `/system-prompt-mode`, `/system-prompt-show`, `/system-prompt-select`.
|
|
15
|
+
- Two modes: `append` (default; keep Pi's prompt as the base and add the custom prompt as an extra section) and `replace` (use the custom prompt as the base, with Pi's tools and user customizations appended after it).
|
|
16
|
+
- Multiple `.md` files can coexist in the prompt directory; use `/system-prompt-select` to switch between them.
|
|
17
|
+
- Persistent state (`enabled`, `mode`, `selectedFile`) saved to `~/.pi/agent/state/system-prompt.json` and reloaded on next start.
|
|
18
|
+
- Footer status widget: `system-prompt: <file>` when enabled, `system-prompt: disabled` when not.
|
|
19
|
+
- `PI_SYSTEM_PROMPT_DIR` environment variable to override the default prompt directory.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nocte
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# pi-custom-system-prompt
|
|
2
|
+
|
|
3
|
+
A [pi](https://pi.dev) extension that loads a custom system prompt from a markdown file and injects it into every agent turn. Works with any model or provider — nothing is hardcoded.
|
|
4
|
+
|
|
5
|
+
Multiple `.md` files can live in the prompt directory; pick which one is active with `/system-prompt-select`. All changes take effect on the very next message you send — no `/new` or session restart required.
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
| Command | What it does |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `/system-prompt-info` | Show loaded path, size, mode, enabled state, available files |
|
|
12
|
+
| `/system-prompt-toggle` | Enable or disable the custom prompt |
|
|
13
|
+
| `/system-prompt-reload` | Re-read the selected prompt file from disk |
|
|
14
|
+
| `/system-prompt-mode [replace\|append]` | Toggle or set the merge mode |
|
|
15
|
+
| `/system-prompt-show` | Print the first ~800 chars of the loaded prompt |
|
|
16
|
+
| `/system-prompt-select` | Pick a different prompt file from the directory |
|
|
17
|
+
|
|
18
|
+
## Modes
|
|
19
|
+
|
|
20
|
+
**`append`** (default) — keep Pi's default system prompt as the base, and add the custom prompt as an extra section. Safest for most models, since Pi's tool descriptions stay authoritative.
|
|
21
|
+
|
|
22
|
+
**`replace`** — use the custom prompt as the base system prompt. Pi's tool descriptions, current date, working directory, and any `--append-system-prompt` you passed are appended after it.
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
### 1. Install the extension
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pi install npm:pi-custom-system-prompt
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or try it without installing:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pi -e npm:pi-custom-system-prompt
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Create the prompt directory
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
mkdir -p ~/.pi/agent/system-prompts
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The extension reads from this directory by default. To use a different location, set the `PI_SYSTEM_PROMPT_DIR` environment variable.
|
|
45
|
+
|
|
46
|
+
### 3. Add a markdown file
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Example: a focused prompt for a specific model
|
|
50
|
+
cat > ~/.pi/agent/system-prompts/claude-code.md <<'EOF'
|
|
51
|
+
You are a careful, terse coding assistant.
|
|
52
|
+
- Read files before editing them.
|
|
53
|
+
- Prefer minimal diffs.
|
|
54
|
+
- Explain non-obvious choices in one sentence.
|
|
55
|
+
EOF
|
|
56
|
+
|
|
57
|
+
# Another example: a personality-style prompt
|
|
58
|
+
cat > ~/.pi/agent/system-prompts/pirate.md <<'EOF'
|
|
59
|
+
You are a pirate captain. Speak in pirate vernacular but stay accurate
|
|
60
|
+
about technical content. Never break character.
|
|
61
|
+
EOF
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The first file (alphabetical) is auto-selected on first run. Use `/system-prompt-select` to switch between them.
|
|
65
|
+
|
|
66
|
+
### 4. Verify and toggle
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# inside pi:
|
|
70
|
+
/system-prompt-info # confirm the file loaded
|
|
71
|
+
/system-prompt-toggle off # disable without deleting the file
|
|
72
|
+
/system-prompt-toggle on # re-enable
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The footer shows the active state:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
system-prompt: claude-code.md # enabled, showing the file
|
|
79
|
+
system-prompt: enabled # enabled, no file selected
|
|
80
|
+
system-prompt: disabled # disabled
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## How it works
|
|
84
|
+
|
|
85
|
+
- The extension subscribes to `before_agent_start`, which fires on every user message. It reads the selected `.md` file from the prompt directory, then either appends it to or replaces Pi's resolved system prompt before sending to the model.
|
|
86
|
+
- State (`enabled`, `mode`, `selectedFile`) persists to `~/.pi/agent/state/system-prompt.json` and is reloaded on the next start.
|
|
87
|
+
- All commands are non-destructive: they mutate in-memory state, save to the state file, and take effect on the next message.
|
|
88
|
+
- If the prompt directory is missing, has no `.md` files, or the selected file is empty/unreadable, the extension logs the reason to `/system-prompt-info` and falls back to Pi's default prompt with no error.
|
|
89
|
+
|
|
90
|
+
## Removing the extension
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pi remove npm:pi-custom-system-prompt
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This unloads the extension but does not delete your prompt files in `~/.pi/agent/system-prompts/` or your saved state in `~/.pi/agent/state/system-prompt.json`. Delete those manually if you want a clean uninstall:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
rm -rf ~/.pi/agent/system-prompts
|
|
100
|
+
rm -f ~/.pi/agent/state/system-prompt.json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Environment variables
|
|
104
|
+
|
|
105
|
+
| Variable | Default | Effect |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `PI_SYSTEM_PROMPT_DIR` | `~/.pi/agent/system-prompts` | Override the directory the extension scans for `.md` prompt files |
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
The extension lives in [`extensions/system-prompt.ts`](extensions/system-prompt.ts). To load it from a local checkout during development:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pi -e /absolute/path/to/pi-custom-system-prompt/extensions/system-prompt.ts
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
For a hot-reload loop, symlink it into the global extensions directory:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
ln -s "$(pwd)/extensions/system-prompt.ts" ~/.pi/agent/extensions/system-prompt.ts
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Then `/reload` inside pi after each edit.
|
|
124
|
+
|
|
125
|
+
### Layout
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
pi-custom-system-prompt/
|
|
129
|
+
├── extensions/
|
|
130
|
+
│ └── system-prompt.ts
|
|
131
|
+
├── package.json
|
|
132
|
+
├── tsconfig.json
|
|
133
|
+
├── README.md
|
|
134
|
+
├── CHANGELOG.md
|
|
135
|
+
└── LICENSE
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The `extensions/` directory is a pi convention: any `.ts` file inside it is auto-discovered. The `pi-package` keyword in `package.json` makes the package appear in the [pi package gallery](https://pi.dev/packages).
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom System Prompt Extension for Pi
|
|
3
|
+
*
|
|
4
|
+
* Loads a custom system prompt from ~/.pi/agent/system-prompts/ and injects
|
|
5
|
+
* it into every agent turn. Works with any model or agent — nothing is
|
|
6
|
+
* hardcoded.
|
|
7
|
+
*
|
|
8
|
+
* Multiple .md files can coexist in the prompt directory. Use
|
|
9
|
+
* /system-prompt-select to pick which one is active.
|
|
10
|
+
*
|
|
11
|
+
* Commands
|
|
12
|
+
* --------
|
|
13
|
+
* /system-prompt-info Show loaded path, size, mode, enabled state
|
|
14
|
+
* /system-prompt-toggle Enable or disable the custom prompt
|
|
15
|
+
* /system-prompt-reload Re-read the selected prompt file from disk
|
|
16
|
+
* /system-prompt-mode Toggle between "replace" and "append" mode
|
|
17
|
+
* /system-prompt-show Print the first ~800 chars of the loaded prompt
|
|
18
|
+
* /system-prompt-select Pick a different prompt file from the directory
|
|
19
|
+
*
|
|
20
|
+
* How changes take effect
|
|
21
|
+
* -----------------------
|
|
22
|
+
* The custom prompt is injected via the before_agent_start event, which
|
|
23
|
+
* fires on every user message. This means all changes (toggle, reload,
|
|
24
|
+
* mode, select) take effect on the very next message you send — no /new
|
|
25
|
+
* or session restart required.
|
|
26
|
+
*
|
|
27
|
+
* Modes
|
|
28
|
+
* -----
|
|
29
|
+
* append (default) — Keep Pi's default system prompt as the base and add
|
|
30
|
+
* the custom prompt as an extra section. Safest for
|
|
31
|
+
* most models since Pi's tool descriptions stay
|
|
32
|
+
* authoritative.
|
|
33
|
+
* replace — Use the custom prompt as the base system prompt.
|
|
34
|
+
* Pi's tool descriptions, date, cwd, and user
|
|
35
|
+
* customizations are appended after it.
|
|
36
|
+
*
|
|
37
|
+
* State persistence
|
|
38
|
+
* -----------------
|
|
39
|
+
* enabled, mode, and selectedFile are persisted to:
|
|
40
|
+
* ~/.pi/agent/state/system-prompt.json
|
|
41
|
+
* and survive Pi restarts.
|
|
42
|
+
*
|
|
43
|
+
* Footer indicator
|
|
44
|
+
* ----------------
|
|
45
|
+
* system-prompt: disabled (when disabled)
|
|
46
|
+
* system-prompt: claude-code.md (when enabled, showing filename)
|
|
47
|
+
*
|
|
48
|
+
* Environment variables
|
|
49
|
+
* ---------------------
|
|
50
|
+
* PI_SYSTEM_PROMPT_DIR Override the default prompt directory
|
|
51
|
+
* (default: ~/.pi/agent/system-prompts)
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import * as fs from "node:fs";
|
|
55
|
+
import * as os from "node:os";
|
|
56
|
+
import * as path from "node:path";
|
|
57
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
58
|
+
|
|
59
|
+
const DEFAULT_PROMPT_DIR = path.join(
|
|
60
|
+
os.homedir(),
|
|
61
|
+
".pi",
|
|
62
|
+
"agent",
|
|
63
|
+
"system-prompts",
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const STATE_PATH = path.join(
|
|
67
|
+
os.homedir(),
|
|
68
|
+
".pi",
|
|
69
|
+
"agent",
|
|
70
|
+
"state",
|
|
71
|
+
"system-prompt.json",
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
type Mode = "replace" | "append";
|
|
75
|
+
|
|
76
|
+
interface PersistedState {
|
|
77
|
+
enabled: boolean;
|
|
78
|
+
mode: Mode;
|
|
79
|
+
selectedFile: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default function systemPromptExtension(pi: ExtensionAPI) {
|
|
83
|
+
const promptDir: string =
|
|
84
|
+
process.env.PI_SYSTEM_PROMPT_DIR?.trim() || DEFAULT_PROMPT_DIR;
|
|
85
|
+
|
|
86
|
+
let enabled: boolean;
|
|
87
|
+
let mode: Mode;
|
|
88
|
+
let selectedFile: string;
|
|
89
|
+
let promptContent: string | null = null;
|
|
90
|
+
let loadError: string | null = null;
|
|
91
|
+
let lastLoaded: number | null = null;
|
|
92
|
+
|
|
93
|
+
// --- State persistence ---------------------------------------------------
|
|
94
|
+
|
|
95
|
+
function loadState(): PersistedState {
|
|
96
|
+
try {
|
|
97
|
+
if (!fs.existsSync(STATE_PATH)) {
|
|
98
|
+
return { enabled: true, mode: "append", selectedFile: "" };
|
|
99
|
+
}
|
|
100
|
+
const raw = fs.readFileSync(STATE_PATH, "utf-8");
|
|
101
|
+
const parsed = JSON.parse(raw) as Partial<PersistedState>;
|
|
102
|
+
return {
|
|
103
|
+
enabled: parsed.enabled === false ? false : true,
|
|
104
|
+
mode: parsed.mode === "replace" ? "replace" : "append",
|
|
105
|
+
selectedFile:
|
|
106
|
+
typeof parsed.selectedFile === "string"
|
|
107
|
+
? parsed.selectedFile
|
|
108
|
+
: "",
|
|
109
|
+
};
|
|
110
|
+
} catch {
|
|
111
|
+
return { enabled: true, mode: "append", selectedFile: "" };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function saveState(): void {
|
|
116
|
+
try {
|
|
117
|
+
fs.mkdirSync(path.dirname(STATE_PATH), { recursive: true });
|
|
118
|
+
fs.writeFileSync(
|
|
119
|
+
STATE_PATH,
|
|
120
|
+
JSON.stringify(
|
|
121
|
+
{ enabled, mode, selectedFile } satisfies PersistedState,
|
|
122
|
+
null,
|
|
123
|
+
2,
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
} catch {
|
|
127
|
+
// best-effort — state persistence failure should not break anything
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Initialize from persisted state
|
|
132
|
+
const initial = loadState();
|
|
133
|
+
enabled = initial.enabled;
|
|
134
|
+
mode = initial.mode;
|
|
135
|
+
selectedFile = initial.selectedFile;
|
|
136
|
+
|
|
137
|
+
// --- Prompt file management ----------------------------------------------
|
|
138
|
+
|
|
139
|
+
function listPromptFiles(): string[] {
|
|
140
|
+
try {
|
|
141
|
+
if (!fs.existsSync(promptDir)) return [];
|
|
142
|
+
return fs
|
|
143
|
+
.readdirSync(promptDir, { withFileTypes: true })
|
|
144
|
+
.filter((d) => d.isFile() && d.name.endsWith(".md"))
|
|
145
|
+
.map((d) => d.name)
|
|
146
|
+
.sort();
|
|
147
|
+
} catch {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function ensureSelectedFile(): void {
|
|
153
|
+
const files = listPromptFiles();
|
|
154
|
+
if (files.length === 0) {
|
|
155
|
+
selectedFile = "";
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (!selectedFile || !files.includes(selectedFile)) {
|
|
159
|
+
selectedFile = files[0];
|
|
160
|
+
saveState();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function loadPrompt(): void {
|
|
165
|
+
ensureSelectedFile();
|
|
166
|
+
|
|
167
|
+
if (!selectedFile) {
|
|
168
|
+
promptContent = null;
|
|
169
|
+
loadError = fs.existsSync(promptDir)
|
|
170
|
+
? `no .md files in ${shortPath(promptDir)}`
|
|
171
|
+
: `directory not found: ${shortPath(promptDir)}`;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const filePath = path.join(promptDir, selectedFile);
|
|
176
|
+
try {
|
|
177
|
+
if (!fs.existsSync(filePath)) {
|
|
178
|
+
promptContent = null;
|
|
179
|
+
loadError = `file not found: ${shortPath(filePath)}`;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
183
|
+
if (!content.trim()) {
|
|
184
|
+
promptContent = null;
|
|
185
|
+
loadError = `file is empty: ${shortPath(filePath)}`;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
promptContent = content;
|
|
189
|
+
loadError = null;
|
|
190
|
+
lastLoaded = Date.now();
|
|
191
|
+
} catch (err) {
|
|
192
|
+
promptContent = null;
|
|
193
|
+
loadError = `read failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function shortPath(p: string): string {
|
|
198
|
+
const home = os.homedir();
|
|
199
|
+
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// --- Footer status -------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
function updateStatus(ctx: {
|
|
205
|
+
hasUI: boolean;
|
|
206
|
+
ui: { setStatus: (id: string, value: string | undefined) => void };
|
|
207
|
+
}): void {
|
|
208
|
+
if (!ctx.hasUI) return;
|
|
209
|
+
if (enabled && selectedFile) {
|
|
210
|
+
ctx.ui.setStatus("system-prompt", `system-prompt: ${selectedFile}`);
|
|
211
|
+
} else if (enabled) {
|
|
212
|
+
ctx.ui.setStatus("system-prompt", "system-prompt: enabled");
|
|
213
|
+
} else {
|
|
214
|
+
ctx.ui.setStatus("system-prompt", "system-prompt: disabled");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// --- Initial load --------------------------------------------------------
|
|
219
|
+
loadPrompt();
|
|
220
|
+
|
|
221
|
+
// --- Command handlers ----------------------------------------------------
|
|
222
|
+
|
|
223
|
+
pi.registerCommand("system-prompt-info", {
|
|
224
|
+
description: "Show custom system prompt info (path, size, mode, state)",
|
|
225
|
+
handler: async (_args, ctx) => {
|
|
226
|
+
const files = listPromptFiles();
|
|
227
|
+
const lines = [
|
|
228
|
+
`Directory: ${shortPath(promptDir)}`,
|
|
229
|
+
`File: ${selectedFile || "(none)"}`,
|
|
230
|
+
`Mode: ${mode}`,
|
|
231
|
+
`Enabled: ${enabled}`,
|
|
232
|
+
`Status: ${loadError ?? "loaded"}`,
|
|
233
|
+
`Size: ${promptContent?.length ?? 0} chars`,
|
|
234
|
+
`Loaded: ${lastLoaded ? new Date(lastLoaded).toLocaleTimeString() : "never"}`,
|
|
235
|
+
`Available: ${files.length > 0 ? files.join(", ") : "(none)"}`,
|
|
236
|
+
];
|
|
237
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
pi.registerCommand("system-prompt-toggle", {
|
|
242
|
+
description: "Enable or disable the custom system prompt",
|
|
243
|
+
handler: async (_args, ctx) => {
|
|
244
|
+
enabled = !enabled;
|
|
245
|
+
saveState();
|
|
246
|
+
updateStatus(ctx);
|
|
247
|
+
const state = enabled ? "enabled" : "disabled";
|
|
248
|
+
ctx.ui.notify(
|
|
249
|
+
`System prompt ${state}. Takes effect on next message.`,
|
|
250
|
+
"info",
|
|
251
|
+
);
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
pi.registerCommand("system-prompt-reload", {
|
|
256
|
+
description: "Reload the custom system prompt from disk",
|
|
257
|
+
handler: async (_args, ctx) => {
|
|
258
|
+
loadPrompt();
|
|
259
|
+
updateStatus(ctx);
|
|
260
|
+
if (loadError) {
|
|
261
|
+
ctx.ui.notify(`Reload failed: ${loadError}`, "error");
|
|
262
|
+
} else {
|
|
263
|
+
ctx.ui.notify(
|
|
264
|
+
`Reloaded ${selectedFile} (${promptContent!.length} chars). Takes effect on next message.`,
|
|
265
|
+
"info",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
pi.registerCommand("system-prompt-select", {
|
|
272
|
+
description: "Select which .md file to use as the system prompt",
|
|
273
|
+
handler: async (_args, ctx) => {
|
|
274
|
+
const files = listPromptFiles();
|
|
275
|
+
if (files.length === 0) {
|
|
276
|
+
ctx.ui.notify(
|
|
277
|
+
`No .md files found in ${shortPath(promptDir)}\nCreate prompt files there to use this extension.`,
|
|
278
|
+
"error",
|
|
279
|
+
);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (files.length === 1) {
|
|
283
|
+
if (files[0] === selectedFile) {
|
|
284
|
+
ctx.ui.notify(`Only one prompt available: ${files[0]} (already selected)`, "info");
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
selectedFile = files[0];
|
|
288
|
+
saveState();
|
|
289
|
+
loadPrompt();
|
|
290
|
+
updateStatus(ctx);
|
|
291
|
+
ctx.ui.notify(
|
|
292
|
+
`Selected: ${selectedFile} (${promptContent?.length ?? 0} chars). Takes effect on next message.`,
|
|
293
|
+
"info",
|
|
294
|
+
);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const choice = await ctx.ui.select(
|
|
299
|
+
"Select system prompt",
|
|
300
|
+
files.map((f) => (f === selectedFile ? `${f} ✓` : f)),
|
|
301
|
+
);
|
|
302
|
+
if (!choice) return;
|
|
303
|
+
|
|
304
|
+
// Strip the checkmark suffix if present
|
|
305
|
+
const file = choice.replace(/\s+✓$/, "");
|
|
306
|
+
if (file === selectedFile) return;
|
|
307
|
+
|
|
308
|
+
selectedFile = file;
|
|
309
|
+
saveState();
|
|
310
|
+
loadPrompt();
|
|
311
|
+
updateStatus(ctx);
|
|
312
|
+
ctx.ui.notify(
|
|
313
|
+
`Selected: ${selectedFile} (${promptContent?.length ?? 0} chars). Takes effect on next message.`,
|
|
314
|
+
"info",
|
|
315
|
+
);
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
pi.registerCommand("system-prompt-mode", {
|
|
320
|
+
description: "Toggle mode (replace: use selected file | append: add along with Pi's prompt)",
|
|
321
|
+
handler: async (args, ctx) => {
|
|
322
|
+
const arg = args.trim().toLowerCase();
|
|
323
|
+
if (arg === "replace" || arg === "append") {
|
|
324
|
+
mode = arg;
|
|
325
|
+
} else if (arg === "" || arg === "toggle") {
|
|
326
|
+
mode = mode === "replace" ? "append" : "replace";
|
|
327
|
+
} else {
|
|
328
|
+
ctx.ui.notify(`Usage: /system-prompt-mode [replace|append]`, "error");
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
saveState();
|
|
332
|
+
ctx.ui.notify(
|
|
333
|
+
`Mode: ${mode}. Takes effect on next message.`,
|
|
334
|
+
"info",
|
|
335
|
+
);
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
pi.registerCommand("system-prompt-show", {
|
|
340
|
+
description: "Show first ~800 chars of the loaded custom system prompt",
|
|
341
|
+
handler: async (_args, ctx) => {
|
|
342
|
+
if (!promptContent) {
|
|
343
|
+
ctx.ui.notify(`Not loaded: ${loadError ?? "unknown error"}`, "error");
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const preview = promptContent.slice(0, 800);
|
|
347
|
+
const tail = promptContent.length > 800 ? "\n\n[... truncated]" : "";
|
|
348
|
+
ctx.ui.notify(`${preview}${tail}`, "info");
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// --- Lifecycle -----------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
355
|
+
// Re-read on every start so a hand-edited file gets picked up.
|
|
356
|
+
loadPrompt();
|
|
357
|
+
updateStatus(ctx);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
361
|
+
if (ctx.hasUI) ctx.ui.setStatus("system-prompt", undefined);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// --- Inject the prompt ---------------------------------------------------
|
|
365
|
+
|
|
366
|
+
pi.on("before_agent_start", async (event) => {
|
|
367
|
+
// Graceful no-op when disabled or when the prompt file is missing/unreadable.
|
|
368
|
+
if (!enabled || !promptContent) return;
|
|
369
|
+
|
|
370
|
+
const opts = event.systemPromptOptions;
|
|
371
|
+
const appendSystemPrompt: string = opts?.appendSystemPrompt ?? "";
|
|
372
|
+
const customPrompt: string = opts?.customPrompt ?? "";
|
|
373
|
+
|
|
374
|
+
// Build tool listing preserving tool names
|
|
375
|
+
const snippets = opts?.toolSnippets ?? {};
|
|
376
|
+
const toolLines = Object.entries(snippets)
|
|
377
|
+
.map(([name, desc]) => `- ${name}: ${desc}`)
|
|
378
|
+
.join("\n");
|
|
379
|
+
const toolsSection = toolLines
|
|
380
|
+
? "\n\n## Available tools\n\n" +
|
|
381
|
+
"The following tools are available in this environment. " +
|
|
382
|
+
"Use them as described below instead of the tools mentioned in the system prompt above.\n\n" +
|
|
383
|
+
toolLines
|
|
384
|
+
: "";
|
|
385
|
+
|
|
386
|
+
if (mode === "replace") {
|
|
387
|
+
// Custom prompt is the base. Stack Pi's tools + user customizations after.
|
|
388
|
+
const now = new Date();
|
|
389
|
+
const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
390
|
+
const cwd = opts?.cwd ?? "unknown";
|
|
391
|
+
const customSection = customPrompt ? `\n\n${customPrompt}` : "";
|
|
392
|
+
const appendSection = appendSystemPrompt
|
|
393
|
+
? `\n\n${appendSystemPrompt}`
|
|
394
|
+
: "";
|
|
395
|
+
return {
|
|
396
|
+
systemPrompt:
|
|
397
|
+
promptContent +
|
|
398
|
+
toolsSection +
|
|
399
|
+
customSection +
|
|
400
|
+
appendSection +
|
|
401
|
+
`\nCurrent date: ${dateStr}` +
|
|
402
|
+
`\nCurrent working directory: ${cwd}`,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Append mode: keep Pi's prompt, add custom prompt as an extra section.
|
|
407
|
+
// event.systemPrompt already includes appendSystemPrompt, so no need
|
|
408
|
+
// to re-add it.
|
|
409
|
+
return {
|
|
410
|
+
systemPrompt:
|
|
411
|
+
event.systemPrompt +
|
|
412
|
+
"\n\n---\n\n## Custom system prompt\n\n" +
|
|
413
|
+
promptContent,
|
|
414
|
+
};
|
|
415
|
+
});
|
|
416
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-custom-system-prompt",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi extension that loads a custom system prompt from `~/.pi/agent/system-prompts/*.md` and injects it on every agent turn, with commands to select, toggle, reload, and switch between replace and append modes.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Nocte",
|
|
9
|
+
"email": "nocte19@gmail.com"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/nnocte/pi-custom-system-prompt.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/nnocte/pi-custom-system-prompt/issues"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/nnocte/pi-custom-system-prompt#readme",
|
|
19
|
+
"keywords": [
|
|
20
|
+
"pi-package",
|
|
21
|
+
"pi-extension",
|
|
22
|
+
"pi",
|
|
23
|
+
"extension",
|
|
24
|
+
"system-prompt",
|
|
25
|
+
"custom-prompt",
|
|
26
|
+
"prompt"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"extensions",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"CHANGELOG.md"
|
|
36
|
+
],
|
|
37
|
+
"pi": {
|
|
38
|
+
"extensions": [
|
|
39
|
+
"./extensions"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
44
|
+
}
|
|
45
|
+
}
|