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 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's tool descriptions, current date, working directory, and any `--append-system-prompt` you passed are appended after it.
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, "&amp;")
85
+ .replace(/</g, "&lt;")
86
+ .replace(/>/g, "&gt;")
87
+ .replace(/"/g, "&quot;")
88
+ .replace(/'/g, "&apos;");
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.0",
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",