pi-system-prompt-switcher 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/README.md +64 -0
- package/extensions/system-prompt-switcher.ts +241 -0
- package/index.ts +2 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# pi-system-prompt-switcher
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/pi-system-prompt-switcher)
|
|
4
|
+
|
|
5
|
+
Switch Pi's active system prompt from files in `~/.pi/agent/systems/`.
|
|
6
|
+
|
|
7
|
+
This package does one thing: `/system-prompt` picks a prompt file, persists that choice, then reloads Pi so the next turn uses it. There is no file watcher. Editing a prompt file takes effect after a reload or after selecting it again.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pi install npm:pi-system-prompt-switcher
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Prompt files
|
|
16
|
+
|
|
17
|
+
Put prompt files directly in:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
~/.pi/agent/systems/
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
~/.pi/agent/systems/default.md
|
|
27
|
+
~/.pi/agent/systems/reviewer.md
|
|
28
|
+
~/.pi/agent/systems/prototype.md
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Hidden files and subdirectories are ignored. Symlinks are ignored so selections cannot escape the systems directory.
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
Interactive picker:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
/system-prompt
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Direct switch:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
/system-prompt reviewer.md
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Selection is stored in:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
~/.pi/agent/system-prompt-switcher.json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
After a successful switch, the extension calls Pi's reload flow. The selected prompt is appended to Pi's effective system prompt under an `Active system prompt` heading, preserving Pi's generated tool guidance, skills, project context, date, and cwd.
|
|
54
|
+
|
|
55
|
+
## Development
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
bun install
|
|
59
|
+
bun test
|
|
60
|
+
bun run typecheck
|
|
61
|
+
bun run check
|
|
62
|
+
bun pm pack --dry-run
|
|
63
|
+
PI_OFFLINE=1 bunx --bun pi --no-extensions -e . --list-models >/tmp/pi-system-prompt-switcher-pi-load.out
|
|
64
|
+
```
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { lstat, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
4
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_SYSTEMS_DIR = join(homedir(), ".pi", "agent", "systems");
|
|
7
|
+
export const DEFAULT_CONFIG_PATH = join(
|
|
8
|
+
homedir(),
|
|
9
|
+
".pi",
|
|
10
|
+
"agent",
|
|
11
|
+
"system-prompt-switcher.json",
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const NO_SELECTION = "No system prompt selected.";
|
|
15
|
+
|
|
16
|
+
export interface SelectedPrompt {
|
|
17
|
+
ok: true;
|
|
18
|
+
name: string;
|
|
19
|
+
path: string;
|
|
20
|
+
content: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PromptError {
|
|
24
|
+
ok: false;
|
|
25
|
+
error: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface StoredConfig {
|
|
29
|
+
active?: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function promptPath(systemsDir: string, name: string): string | undefined {
|
|
33
|
+
if (!name || name.includes("/") || name.includes("\\")) return undefined;
|
|
34
|
+
const base = resolve(systemsDir);
|
|
35
|
+
const candidate = resolve(base, name);
|
|
36
|
+
if (candidate !== base && candidate.startsWith(`${base}${sep}`))
|
|
37
|
+
return candidate;
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function discoverSystemPrompts(
|
|
42
|
+
systemsDir: string,
|
|
43
|
+
): Promise<string[]> {
|
|
44
|
+
try {
|
|
45
|
+
const entries = await readdir(systemsDir, { withFileTypes: true });
|
|
46
|
+
return entries
|
|
47
|
+
.filter((entry) => entry.isFile() && !entry.name.startsWith("."))
|
|
48
|
+
.map((entry) => entry.name)
|
|
49
|
+
.sort((left, right) => left.localeCompare(right));
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
52
|
+
const code = String(error.code);
|
|
53
|
+
if (code === "ENOENT" || code === "ENOTDIR") return [];
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function saveSelectedPrompt(
|
|
60
|
+
configPath: string,
|
|
61
|
+
name: string,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
64
|
+
await writeFile(
|
|
65
|
+
configPath,
|
|
66
|
+
`${JSON.stringify({ active: name }, null, "\t")}\n`,
|
|
67
|
+
"utf8",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function selectSystemPrompt(options: {
|
|
72
|
+
configPath: string;
|
|
73
|
+
systemsDir: string;
|
|
74
|
+
name: string;
|
|
75
|
+
}): Promise<{ ok: true; name: string; path: string } | PromptError> {
|
|
76
|
+
const name = options.name.trim();
|
|
77
|
+
const path = promptPath(options.systemsDir, name);
|
|
78
|
+
if (!path) return { ok: false, error: `Invalid system prompt name: ${name}` };
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const info = await lstat(path);
|
|
82
|
+
if (!info.isFile()) {
|
|
83
|
+
return { ok: false, error: `System prompt not found: ${name}` };
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
87
|
+
const code = String(error.code);
|
|
88
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
89
|
+
return { ok: false, error: `System prompt not found: ${name}` };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
error: `Could not inspect system prompt ${name}: ${message}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await saveSelectedPrompt(options.configPath, name);
|
|
100
|
+
return { ok: true, name, path };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function loadSelectedPrompt(options: {
|
|
104
|
+
configPath: string;
|
|
105
|
+
systemsDir: string;
|
|
106
|
+
}): Promise<SelectedPrompt | PromptError> {
|
|
107
|
+
let rawConfig: StoredConfig;
|
|
108
|
+
try {
|
|
109
|
+
rawConfig = JSON.parse(
|
|
110
|
+
await readFile(options.configPath, "utf8"),
|
|
111
|
+
) as StoredConfig;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
114
|
+
const code = String(error.code);
|
|
115
|
+
if (code === "ENOENT") return { ok: false, error: NO_SELECTION };
|
|
116
|
+
}
|
|
117
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
error: `Could not read system prompt switcher config: ${message}`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof rawConfig.active !== "string" || !rawConfig.active.trim()) {
|
|
125
|
+
return { ok: false, error: NO_SELECTION };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const name = rawConfig.active.trim();
|
|
129
|
+
const path = promptPath(options.systemsDir, name);
|
|
130
|
+
if (!path) return { ok: false, error: `Invalid system prompt name: ${name}` };
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const systemsInfo = await lstat(options.systemsDir);
|
|
134
|
+
if (!systemsInfo.isDirectory()) return { ok: false, error: NO_SELECTION };
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
137
|
+
const code = String(error.code);
|
|
138
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
139
|
+
return { ok: false, error: NO_SELECTION };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
error: `Could not inspect systems directory ${options.systemsDir}: ${message}`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const info = await lstat(path);
|
|
151
|
+
if (!info.isFile()) {
|
|
152
|
+
return { ok: false, error: `System prompt not found: ${name}` };
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
ok: true,
|
|
156
|
+
name,
|
|
157
|
+
path,
|
|
158
|
+
content: await readFile(path, "utf8"),
|
|
159
|
+
};
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
error: `Could not read selected system prompt ${name}: ${message}`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function appendActiveSystemPrompt(
|
|
170
|
+
basePrompt: string,
|
|
171
|
+
prompt: Pick<SelectedPrompt, "name" | "content">,
|
|
172
|
+
): string {
|
|
173
|
+
return `${basePrompt}\n\n## Active system prompt: ${prompt.name}\n\n${prompt.content.trimEnd()}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface SystemPromptSwitcherOptions {
|
|
177
|
+
configPath?: string;
|
|
178
|
+
systemsDir?: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export default async function systemPromptSwitcherExtension(
|
|
182
|
+
pi: ExtensionAPI,
|
|
183
|
+
options: SystemPromptSwitcherOptions = {},
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
const configPath = options.configPath ?? DEFAULT_CONFIG_PATH;
|
|
186
|
+
const systemsDir = options.systemsDir ?? DEFAULT_SYSTEMS_DIR;
|
|
187
|
+
const activePrompt = await loadSelectedPrompt({ configPath, systemsDir });
|
|
188
|
+
let warningShown = false;
|
|
189
|
+
|
|
190
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
191
|
+
if (!activePrompt.ok) {
|
|
192
|
+
if (activePrompt.error !== NO_SELECTION && !warningShown) {
|
|
193
|
+
warningShown = true;
|
|
194
|
+
ctx.ui.notify(activePrompt.error, "warning");
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
systemPrompt: appendActiveSystemPrompt(event.systemPrompt, activePrompt),
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
pi.registerCommand("system-prompt", {
|
|
204
|
+
description:
|
|
205
|
+
"Switch active system prompt from ~/.pi/agent/systems/ and reload Pi",
|
|
206
|
+
handler: async (args, ctx) => {
|
|
207
|
+
const prompts = await discoverSystemPrompts(systemsDir);
|
|
208
|
+
if (prompts.length === 0) {
|
|
209
|
+
ctx.ui.notify(`No system prompts found in ${systemsDir}.`, "warning");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let name = args.trim();
|
|
214
|
+
if (!name) {
|
|
215
|
+
if (!ctx.hasUI) {
|
|
216
|
+
ctx.ui.notify(`Pass one of: ${prompts.join(", ")}`, "warning");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const choice = await ctx.ui.select(
|
|
220
|
+
"Select active system prompt:",
|
|
221
|
+
prompts,
|
|
222
|
+
);
|
|
223
|
+
if (!choice) return;
|
|
224
|
+
name = choice;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const result = await selectSystemPrompt({ configPath, systemsDir, name });
|
|
228
|
+
if (!result.ok) {
|
|
229
|
+
ctx.ui.notify(result.error, "error");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
ctx.ui.notify(
|
|
234
|
+
`Switched active system prompt to ${result.name}. Reloading Pi...`,
|
|
235
|
+
"info",
|
|
236
|
+
);
|
|
237
|
+
await ctx.reload();
|
|
238
|
+
return;
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
package/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-system-prompt-switcher",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Switch active Pi system prompt from ~/.pi/agent/systems and reload on swap",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": "./index.ts",
|
|
7
|
+
"author": "drsh4dow <daniel.morettiv@gmail.com> (https://codeberg.org/drsh4dow)",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://codeberg.org/drsh4dow/pi-system-prompt-switcher.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://codeberg.org/drsh4dow/pi-system-prompt-switcher/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://codeberg.org/drsh4dow/pi-system-prompt-switcher#readme",
|
|
16
|
+
"scripts": {
|
|
17
|
+
"check": "biome check .",
|
|
18
|
+
"check:fix": "biome check --write .",
|
|
19
|
+
"test": "bun test",
|
|
20
|
+
"typecheck": "bunx tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"pi-package",
|
|
24
|
+
"pi",
|
|
25
|
+
"system-prompt",
|
|
26
|
+
"prompt",
|
|
27
|
+
"reload",
|
|
28
|
+
"extension"
|
|
29
|
+
],
|
|
30
|
+
"files": [
|
|
31
|
+
"extensions",
|
|
32
|
+
"index.ts",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"pi": {
|
|
36
|
+
"extensions": [
|
|
37
|
+
"./extensions/system-prompt-switcher.ts"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@biomejs/biome": "^2.4.14",
|
|
42
|
+
"@mariozechner/pi-ai": "^0.72.1",
|
|
43
|
+
"@mariozechner/pi-coding-agent": "^0.72.1",
|
|
44
|
+
"@mariozechner/pi-tui": "^0.72.1",
|
|
45
|
+
"@types/bun": "^1.3.13",
|
|
46
|
+
"typescript": "^6.0.3"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@mariozechner/pi-ai": "*",
|
|
50
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
51
|
+
"@mariozechner/pi-tui": "*"
|
|
52
|
+
},
|
|
53
|
+
"trustedDependencies": [
|
|
54
|
+
"koffi",
|
|
55
|
+
"protobufjs"
|
|
56
|
+
]
|
|
57
|
+
}
|