pi-custom-system-prompt 0.1.0 → 0.1.1
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 +12 -0
- package/README.md +4 -1
- package/extensions/system-prompt.ts +57 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.1] - 2026-06-27
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Replace mode no longer drops project context files.** Previously, switching to `replace` mode reconstructed the system prompt from the custom file but omitted the `<project_context>` block Pi builds from `AGENTS.md`, `.pi/rules`, and other configured context files. The model never saw project-specific instructions in `replace` mode. The replace branch now reads `systemPromptOptions.contextFiles` and emits the same `<project_context>` block Pi's own prompt builder produces, in the same assembly order.
|
|
13
|
+
- **Replace mode no longer drops skills.** The `<available_skills>` block that tells the model which `SKILL.md` files it can read (frontend-design, librarian, context7-docs, pi-subagents, and any custom skills) was also missing in `replace` mode, so the model would not invoke skills even when a task matched one. The replace branch now reads `systemPromptOptions.skills` and emits the block.
|
|
14
|
+
- The available-skills block is produced by an inlined helper (`formatSkillsBlock`) that mirrors Pi's `formatSkillsForPrompt` exactly — same XML escaping, same `disableModelInvocation` filtering — so the extension takes no runtime dependency on the host package and stays resolvable regardless of `node_modules` topology.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Documented the full `replace`-mode assembly order in the README (tools → append → project context → skills → date → cwd).
|
|
19
|
+
|
|
8
20
|
## [0.1.0] - 2026-06-21
|
|
9
21
|
|
|
10
22
|
### Added
|
package/README.md
CHANGED
|
@@ -19,7 +19,9 @@ Multiple `.md` files can live in the prompt directory; pick which one is active
|
|
|
19
19
|
|
|
20
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
21
|
|
|
22
|
-
**`replace`** — use the custom prompt as the base system prompt. Pi
|
|
22
|
+
**`replace`** — use the custom prompt as the base system prompt. Pi appends the following after it, so nothing Pi would normally load is lost: the available-tools listing (with a note to use those tools instead of any fictional ones mentioned in the custom prompt), any `--append-system-prompt` text, project context files (e.g. `AGENTS.md`, `.pi/rules`) wrapped in `<project_context>`, the `<available_skills>` block listing loadable `SKILL.md` files, the current date, and the working directory.
|
|
23
|
+
|
|
24
|
+
The project-context and skills blocks are mirrored from Pi's own prompt assembly, so in `replace` mode the model sees the same project instructions and skill list it would in `append` mode — the custom prompt just becomes the base instead of an add-on.
|
|
23
25
|
|
|
24
26
|
## Setup
|
|
25
27
|
|
|
@@ -83,6 +85,7 @@ system-prompt: disabled # disabled
|
|
|
83
85
|
## How it works
|
|
84
86
|
|
|
85
87
|
- 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.
|
|
88
|
+
- In `replace` mode, the extension reconstructs the prompt as `customPrompt + tools + appendSystemPrompt + <project_context> + <available_skills> + date + cwd`, matching Pi's own assembly order so project context files and skills survive the swap. The available-skills block is emitted by a small inlined helper that mirrors Pi's `formatSkillsForPrompt` exactly (same XML escaping, same `disableModelInvocation` filtering); it is inlined rather than imported so the extension takes no runtime dependency on the host package and stays resolvable regardless of `node_modules` topology.
|
|
86
89
|
- State (`enabled`, `mode`, `selectedFile`) persists to `~/.pi/agent/state/system-prompt.json` and is reloaded on the next start.
|
|
87
90
|
- All commands are non-destructive: they mutate in-memory state, save to the state file, and take effect on the next message.
|
|
88
91
|
- 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.
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
import * as fs from "node:fs";
|
|
55
55
|
import * as os from "node:os";
|
|
56
56
|
import * as path from "node:path";
|
|
57
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
57
|
+
import type { ExtensionAPI, Skill } from "@earendil-works/pi-coding-agent";
|
|
58
58
|
|
|
59
59
|
const DEFAULT_PROMPT_DIR = path.join(
|
|
60
60
|
os.homedir(),
|
|
@@ -79,6 +79,40 @@ interface PersistedState {
|
|
|
79
79
|
selectedFile: string;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
function escapeXml(str: string): string {
|
|
83
|
+
return str
|
|
84
|
+
.replace(/&/g, "&")
|
|
85
|
+
.replace(/</g, "<")
|
|
86
|
+
.replace(/>/g, ">")
|
|
87
|
+
.replace(/"/g, """)
|
|
88
|
+
.replace(/'/g, "'");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Mirror pi's formatSkillsForPrompt exactly so the <available_skills> block the
|
|
92
|
+
// model sees is identical to what pi's own default prompt would emit. Inlined
|
|
93
|
+
// (rather than imported) so the extension takes no runtime dependency on the
|
|
94
|
+
// host package and stays resolvable regardless of node_modules topology.
|
|
95
|
+
function formatSkillsBlock(skills: Skill[]): string {
|
|
96
|
+
const visible = skills.filter((s) => !s.disableModelInvocation);
|
|
97
|
+
if (visible.length === 0) return "";
|
|
98
|
+
const lines = [
|
|
99
|
+
"\n\nThe following skills provide specialized instructions for specific tasks.",
|
|
100
|
+
"Use the read tool to load a skill's file when the task matches its description.",
|
|
101
|
+
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
|
|
102
|
+
"",
|
|
103
|
+
"<available_skills>",
|
|
104
|
+
];
|
|
105
|
+
for (const skill of visible) {
|
|
106
|
+
lines.push(" <skill>");
|
|
107
|
+
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
108
|
+
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
109
|
+
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
|
|
110
|
+
lines.push(" </skill>");
|
|
111
|
+
}
|
|
112
|
+
lines.push("</available_skills>");
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
82
116
|
export default function systemPromptExtension(pi: ExtensionAPI) {
|
|
83
117
|
const promptDir: string =
|
|
84
118
|
process.env.PI_SYSTEM_PROMPT_DIR?.trim() || DEFAULT_PROMPT_DIR;
|
|
@@ -385,6 +419,10 @@ export default function systemPromptExtension(pi: ExtensionAPI) {
|
|
|
385
419
|
|
|
386
420
|
if (mode === "replace") {
|
|
387
421
|
// Custom prompt is the base. Stack Pi's tools + user customizations after.
|
|
422
|
+
// Mirror pi's own assembly order (system-prompt.js): append → context →
|
|
423
|
+
// skills → date → cwd, so we don't silently drop project context files
|
|
424
|
+
// or the <available_skills> block that tells the model which SKILL.md
|
|
425
|
+
// files it can read.
|
|
388
426
|
const now = new Date();
|
|
389
427
|
const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
390
428
|
const cwd = opts?.cwd ?? "unknown";
|
|
@@ -392,12 +430,30 @@ export default function systemPromptExtension(pi: ExtensionAPI) {
|
|
|
392
430
|
const appendSection = appendSystemPrompt
|
|
393
431
|
? `\n\n${appendSystemPrompt}`
|
|
394
432
|
: "";
|
|
433
|
+
const contextFiles = opts?.contextFiles ?? [];
|
|
434
|
+
const contextSection =
|
|
435
|
+
contextFiles.length > 0
|
|
436
|
+
? "\n\n<project_context>\n\nProject-specific instructions and guidelines:\n\n" +
|
|
437
|
+
contextFiles
|
|
438
|
+
.map(
|
|
439
|
+
(f) =>
|
|
440
|
+
`<project_instructions path="${f.path}">\n${f.content}\n</project_instructions>\n\n`,
|
|
441
|
+
)
|
|
442
|
+
.join("") +
|
|
443
|
+
"</project_context>\n"
|
|
444
|
+
: "";
|
|
445
|
+
// Emit the <available_skills> block so the model knows which SKILL.md
|
|
446
|
+
// files it can read. Skills with disableModelInvocation=true are hidden.
|
|
447
|
+
const skills: Skill[] = opts?.skills ?? [];
|
|
448
|
+
const skillsSection = skills.length > 0 ? formatSkillsBlock(skills) : "";
|
|
395
449
|
return {
|
|
396
450
|
systemPrompt:
|
|
397
451
|
promptContent +
|
|
398
452
|
toolsSection +
|
|
399
453
|
customSection +
|
|
400
454
|
appendSection +
|
|
455
|
+
contextSection +
|
|
456
|
+
skillsSection +
|
|
401
457
|
`\nCurrent date: ${dateStr}` +
|
|
402
458
|
`\nCurrent working directory: ${cwd}`,
|
|
403
459
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-custom-system-prompt",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|