@agnishc/edb-append-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 ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ### Added
6
+ - Initial release: `/sys-prompt` command with compose and list overlay modes
7
+ - Confirm dialog before adding snippets
8
+ - Status bar indicator showing active snippet count
9
+ - Session persistence via session storage entries
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Agnish Chakraborty
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,32 @@
1
+ # @agnishc/edb-append-system-prompt
2
+
3
+ A Pi CLI extension that lets you build up a list of system-prompt snippets that are appended to every agent turn in the current session.
4
+
5
+ ## Features
6
+
7
+ - **Always appends** — snippets accumulate as a list, never replace the base prompt
8
+ - **Confirm before adding** — shows the exact text in a confirm dialog before saving
9
+ - **Status bar indicator** — `⊕ N snippets` shown when active snippets exist
10
+ - **Delete individual snippets** — select and delete from the list view
11
+ - **Persists across `/reload`** — stored in session history, survives extension reloads
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pi install npm:@agnishc/edb-append-system-prompt
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```
22
+ /sys-prompt
23
+ ```
24
+
25
+ Opens an overlay with two modes:
26
+
27
+ - **Compose mode** — write a snippet, press Enter to add (with confirm dialog)
28
+ - **List mode** — browse existing snippets, press `d` to delete selected
29
+
30
+ ## License
31
+
32
+ [MIT](LICENSE) © Agnish Chakraborty
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@agnishc/edb-append-system-prompt",
3
+ "version": "0.1.0",
4
+ "description": "Pi extension: manage per-session system prompt snippets with add/delete UI",
5
+ "keywords": ["pi-package", "pi-extension", "edb"],
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "author": "Agnish Chakraborty",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/agnishcc/pi-extention-monorepo.git",
12
+ "directory": "packages/edb-append-system-prompt"
13
+ },
14
+ "homepage": "https://github.com/agnishcc/pi-extention-monorepo/tree/main/packages/edb-append-system-prompt#readme",
15
+ "bugs": { "url": "https://github.com/agnishcc/pi-extention-monorepo/issues" },
16
+ "publishConfig": { "access": "public" },
17
+ "scripts": { "test": "vitest run" },
18
+ "files": ["src", "README.md", "LICENSE", "CHANGELOG.md"],
19
+ "pi": {
20
+ "extensions": ["./src/index.ts"]
21
+ },
22
+ "peerDependencies": {
23
+ "@mariozechner/pi-coding-agent": "*",
24
+ "@mariozechner/pi-tui": "*"
25
+ }
26
+ }
@@ -0,0 +1,194 @@
1
+ import {
2
+ Editor,
3
+ type EditorTheme,
4
+ Key,
5
+ matchesKey,
6
+ type SelectItem,
7
+ SelectList,
8
+ truncateToWidth,
9
+ visibleWidth,
10
+ } from "@mariozechner/pi-tui";
11
+ import { snippets } from "./state";
12
+ import type { OverlayAction } from "./types";
13
+ import { formatAge, wordCount } from "./utils";
14
+
15
+ // ── Overlay launcher ───────────────────────────────────────────────────────────
16
+
17
+ export function openOverlay(ctx: any, prefillText?: string): Promise<OverlayAction | undefined> {
18
+ return (ctx.ui as any).custom(
19
+ (tui: any, theme: any, _kb: any, done: (result?: OverlayAction) => void) =>
20
+ createComponent(tui, theme, done, prefillText),
21
+ {
22
+ overlay: true,
23
+ overlayOptions: {
24
+ anchor: "center" as const,
25
+ width: "65%" as const,
26
+ maxHeight: "80%" as const,
27
+ },
28
+ },
29
+ );
30
+ }
31
+
32
+ // ── Component ──────────────────────────────────────────────────────────────────
33
+
34
+ type Mode = "list" | "composing";
35
+
36
+ export function createComponent(tui: any, theme: any, done: (result?: OverlayAction) => void, prefillText?: string) {
37
+ const dim = (s: string) => theme.fg("dim", s);
38
+ const accent = (s: string) => theme.fg("accent", s);
39
+ const muted = (s: string) => theme.fg("muted", s);
40
+
41
+ let mode: Mode = snippets.length === 0 ? "composing" : "list";
42
+
43
+ // ── Editor (composing mode) ────────────────────────────────────────────
44
+ const editorTheme: EditorTheme = {
45
+ borderColor: (s) => theme.fg("accent", s),
46
+ selectList: {
47
+ selectedPrefix: (t) => theme.fg("accent", t),
48
+ selectedText: (t) => theme.fg("accent", t),
49
+ description: (t) => theme.fg("muted", t),
50
+ scrollInfo: (t) => theme.fg("dim", t),
51
+ noMatch: (t) => theme.fg("warning", t),
52
+ },
53
+ };
54
+ const editor = new Editor(tui, editorTheme);
55
+ editor.focused = true;
56
+ if (prefillText) editor.setText(prefillText);
57
+
58
+ editor.onSubmit = (text) => {
59
+ const trimmed = text.trim();
60
+ if (trimmed) {
61
+ done({ type: "add", text: trimmed });
62
+ } else if (snippets.length > 0) {
63
+ mode = "list";
64
+ tui.requestRender();
65
+ } else {
66
+ done();
67
+ }
68
+ };
69
+
70
+ // ── SelectList (list mode) ─────────────────────────────────────────────
71
+ const selectTheme = {
72
+ selectedPrefix: (t: string) => theme.fg("accent", t),
73
+ selectedText: (t: string) => theme.fg("accent", t),
74
+ description: (t: string) => theme.fg("dim", t),
75
+ scrollInfo: (t: string) => theme.fg("dim", t),
76
+ noMatch: (t: string) => theme.fg("warning", t),
77
+ };
78
+
79
+ function buildItems(): SelectItem[] {
80
+ const items: SelectItem[] = [{ value: "__add__", label: accent("+ Add new snippet"), description: "" }];
81
+ for (const s of snippets) {
82
+ const preview = s.text.replace(/\n/g, " ");
83
+ items.push({
84
+ value: s.id,
85
+ label: truncateToWidth(preview, 55),
86
+ description: `${formatAge(s.createdAt)} · ${wordCount(s.text)} words`,
87
+ });
88
+ }
89
+ return items;
90
+ }
91
+
92
+ const list = new SelectList(buildItems(), 12, selectTheme);
93
+
94
+ // ── Rendering ──────────────────────────────────────────────────────────
95
+
96
+ function renderHeader(width: number): string[] {
97
+ const title = theme.bold(accent(" ✦ System Prompt Snippets"));
98
+ const count = snippets.length === 0 ? muted("none active") : accent(`${snippets.length} active`);
99
+ const gap = Math.max(2, width - visibleWidth(" ✦ System Prompt Snippets") - visibleWidth(count) - 1);
100
+ return [title + " ".repeat(gap) + count];
101
+ }
102
+
103
+ function renderBody(width: number): string[] {
104
+ if (mode === "composing") {
105
+ const lines: string[] = [];
106
+ lines.push(dim(" Write your system prompt addition:"));
107
+ lines.push("");
108
+ for (const line of editor.render(width - 2)) {
109
+ lines.push(` ${line}`);
110
+ }
111
+ return lines;
112
+ }
113
+ return list.render(width);
114
+ }
115
+
116
+ function renderFooter(width: number): string[] {
117
+ const divider = dim("─".repeat(width));
118
+ if (mode === "composing") {
119
+ return [divider, dim(` Enter submit · Esc ${snippets.length > 0 ? "back to list" : "close"}`)];
120
+ }
121
+ return [divider, dim(" ↑↓ navigate · Enter select · d delete selected · Esc close")];
122
+ }
123
+
124
+ // ── Input handling ─────────────────────────────────────────────────────
125
+
126
+ return {
127
+ render(width: number): string[] {
128
+ return [...renderHeader(width), dim("─".repeat(width)), ...renderBody(width), ...renderFooter(width)];
129
+ },
130
+
131
+ handleInput(data: string): void {
132
+ if (mode === "composing") {
133
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
134
+ if (snippets.length > 0) {
135
+ mode = "list";
136
+ tui.requestRender();
137
+ } else {
138
+ done();
139
+ }
140
+ return;
141
+ }
142
+ editor.handleInput(data);
143
+ tui.requestRender();
144
+ return;
145
+ }
146
+
147
+ // list mode
148
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
149
+ done();
150
+ return;
151
+ }
152
+
153
+ if (data === "d") {
154
+ const sel = list.getSelectedItem();
155
+ if (sel && sel.value !== "__add__") {
156
+ const snippet = snippets.find((s) => s.id === sel.value);
157
+ if (snippet) done({ type: "delete", id: snippet.id, text: snippet.text });
158
+ }
159
+ return;
160
+ }
161
+
162
+ if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
163
+ const sel = list.getSelectedItem();
164
+ if (!sel) return;
165
+ if (sel.value === "__add__") {
166
+ mode = "composing";
167
+ editor.setText("");
168
+ editor.focused = true;
169
+ tui.requestRender();
170
+ } else {
171
+ const snippet = snippets.find((s) => s.id === sel.value);
172
+ if (snippet) done({ type: "delete", id: snippet.id, text: snippet.text });
173
+ }
174
+ return;
175
+ }
176
+
177
+ if (
178
+ matchesKey(data, Key.up) ||
179
+ matchesKey(data, Key.down) ||
180
+ matchesKey(data, Key.pageUp) ||
181
+ matchesKey(data, Key.pageDown)
182
+ ) {
183
+ list.handleInput(data);
184
+ tui.requestRender();
185
+ return;
186
+ }
187
+ },
188
+
189
+ invalidate(): void {
190
+ editor.invalidate?.();
191
+ list.invalidate();
192
+ },
193
+ };
194
+ }
package/src/index.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * pi-append-system-prompt
3
+ *
4
+ * Manages a list of system-prompt snippets that are appended to every agent
5
+ * turn in the current session.
6
+ *
7
+ * Features:
8
+ * - Always appends (never replaces) — snippets accumulate as a list
9
+ * - Confirm dialog before any snippet is added, showing the exact text
10
+ * - Status bar indicator when snippets are active
11
+ * - Delete individual snippets
12
+ * - Persists across /reload via session storage — scoped to this session only
13
+ *
14
+ * Command: /sys-prompt
15
+ */
16
+
17
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
18
+ import { openOverlay } from "./component";
19
+ import {
20
+ addSnippet,
21
+ loadFromSession,
22
+ persistSnippets,
23
+ removeSnippet,
24
+ setSnippets,
25
+ snippets,
26
+ updateStatusBar,
27
+ } from "./state";
28
+
29
+ // ── Extension ──────────────────────────────────────────────────────────────────
30
+
31
+ export default function sysPromptExtension(pi: ExtensionAPI): void {
32
+ // Restore state on session start / reload
33
+ pi.on("session_start", async (_e, ctx) => {
34
+ setSnippets(loadFromSession(ctx));
35
+ updateStatusBar(ctx);
36
+ });
37
+
38
+ // Append all snippets to the system prompt before each turn
39
+ pi.on("before_agent_start", async (event) => {
40
+ if (snippets.length === 0) return;
41
+ const addition = snippets.map((s) => s.text).join("\n\n");
42
+ return { systemPrompt: `${event.systemPrompt}\n\n${addition}` };
43
+ });
44
+
45
+ pi.registerCommand("sys-prompt", {
46
+ description: "Manage system prompt snippets — add, view, and delete",
47
+ handler: async (_args, ctx: ExtensionCommandContext) => {
48
+ if (!ctx.hasUI) return;
49
+ await ctx.waitForIdle();
50
+
51
+ let pendingText: string | undefined;
52
+ let shouldReopen = false;
53
+
54
+ do {
55
+ shouldReopen = false;
56
+ const action = await openOverlay(ctx, pendingText);
57
+ pendingText = undefined;
58
+
59
+ if (!action) break;
60
+
61
+ if (action.type === "add") {
62
+ const preview = action.text.length > 500 ? `${action.text.slice(0, 497)}…` : action.text;
63
+ const confirmed = await ctx.ui.confirm("Add this to your system prompt?", preview);
64
+ if (confirmed) {
65
+ addSnippet(action.text);
66
+ persistSnippets(pi);
67
+ updateStatusBar(ctx);
68
+ } else {
69
+ // Preserve typed text so user doesn't lose their work
70
+ pendingText = action.text;
71
+ }
72
+ shouldReopen = true;
73
+ }
74
+
75
+ if (action.type === "delete") {
76
+ const preview = action.text.length > 300 ? `${action.text.slice(0, 297)}…` : action.text;
77
+ const confirmed = await ctx.ui.confirm("Remove this snippet from your system prompt?", preview);
78
+ if (confirmed) {
79
+ removeSnippet(action.id);
80
+ persistSnippets(pi);
81
+ updateStatusBar(ctx);
82
+ }
83
+ shouldReopen = true;
84
+ }
85
+ } while (shouldReopen);
86
+ },
87
+ });
88
+ }
package/src/state.ts ADDED
@@ -0,0 +1,52 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
+ import type { Snippet } from "./types";
4
+
5
+ // ── Module state ───────────────────────────────────────────────────────────────
6
+
7
+ export let snippets: Snippet[] = [];
8
+
9
+ export const STATUS_KEY = "sys-prompt";
10
+ export const ENTRY_TYPE = "sys-prompt-snippets";
11
+
12
+ // ── State helpers ──────────────────────────────────────────────────────────────
13
+
14
+ export function addSnippet(text: string): void {
15
+ snippets.push({ id: randomUUID(), text: text.trim(), createdAt: Date.now() });
16
+ }
17
+
18
+ export function removeSnippet(id: string): void {
19
+ snippets = snippets.filter((s) => s.id !== id);
20
+ }
21
+
22
+ export function setSnippets(next: Snippet[]): void {
23
+ snippets = next;
24
+ }
25
+
26
+ // ── Session persistence ────────────────────────────────────────────────────────
27
+
28
+ export function loadFromSession(ctx: any): Snippet[] {
29
+ const entries: any[] = ctx.sessionManager.getEntries();
30
+ for (let i = entries.length - 1; i >= 0; i--) {
31
+ const e = entries[i];
32
+ if (e.type === "custom" && e.customType === ENTRY_TYPE) {
33
+ return (e.data?.snippets as Snippet[]) ?? [];
34
+ }
35
+ }
36
+ return [];
37
+ }
38
+
39
+ export function persistSnippets(pi: ExtensionAPI): void {
40
+ pi.appendEntry(ENTRY_TYPE, { snippets });
41
+ }
42
+
43
+ // ── Status bar ─────────────────────────────────────────────────────────────────
44
+
45
+ export function updateStatusBar(ctx: any): void {
46
+ if (snippets.length === 0) {
47
+ ctx.ui.setStatus(STATUS_KEY, undefined);
48
+ return;
49
+ }
50
+ const label = snippets.length === 1 ? "1 snippet" : `${snippets.length} snippets`;
51
+ ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("accent", `⊕ ${label}`));
52
+ }
package/src/types.ts ADDED
@@ -0,0 +1,9 @@
1
+ // ── Types ──────────────────────────────────────────────────────────────────────
2
+
3
+ export interface Snippet {
4
+ id: string;
5
+ text: string;
6
+ createdAt: number;
7
+ }
8
+
9
+ export type OverlayAction = { type: "add"; text: string } | { type: "delete"; id: string; text: string };
package/src/utils.ts ADDED
@@ -0,0 +1,13 @@
1
+ // ── Utils ──────────────────────────────────────────────────────────────────────
2
+
3
+ export function formatAge(ts: number): string {
4
+ const s = Math.floor((Date.now() - ts) / 1000);
5
+ if (s < 60) return `${s}s ago`;
6
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
7
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
8
+ return `${Math.floor(s / 86400)}d ago`;
9
+ }
10
+
11
+ export function wordCount(text: string): number {
12
+ return text.trim().split(/\s+/).filter(Boolean).length;
13
+ }