@sreetej510/pi-prompt-manager 0.1.1 → 0.1.4

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/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import{matchesKey as u,truncateToWidth as f}from"@earendil-works/pi-tui";var g=class{constructor(t,e,n,a){this.prompts=t;this.theme=n;this.done=a;this.selectedIndex=t.length>0?Math.max(0,Math.min(e,t.length-1)):0}selectedIndex;cache;handleInput(t){let e=this.prompts.length;if(u(t,"up")||t==="k"){this.selectedIndex>0&&(this.selectedIndex--,this.invalidate());return}if(u(t,"down")||t==="j"){this.selectedIndex<e-1&&(this.selectedIndex++,this.invalidate());return}if(u(t,"enter")&&e>0){this.done({action:"paste",index:this.selectedIndex});return}if((t==="e"||t==="E")&&e>0){this.done({action:"edit",index:this.selectedIndex});return}if((t==="d"||t==="D")&&e>0){this.done({action:"delete",index:this.selectedIndex});return}if(t==="a"||t==="A"){this.done({action:"add"});return}if(u(t,"escape")||u(t,"ctrl+c")){this.done({action:"close"});return}}render(t){if(this.cache?.w===t)return this.cache.lines;let e=this.theme,n=[],a=" Prompt Manager ",d=Math.max(0,t-a.length-3);if(n.push(f(e.fg("borderMuted","\u2500".repeat(3))+e.fg("accent",a)+e.fg("borderMuted","\u2500".repeat(d)),t)),n.push(""),this.prompts.length===0)n.push(f(" "+e.fg("dim","No prompts saved yet. Press [a] to add one."),t));else{let r=Math.floor(5),o=Math.max(0,Math.min(this.selectedIndex-r,this.prompts.length-10)),m=Math.min(this.prompts.length,o+10);o>0&&n.push(f(" "+e.fg("dim",`\u2191 ${o} more above`),t));for(let l=o;l<m;l++){let p=this.prompts[l],v=l===this.selectedIndex,S=v?e.fg("accent","\u276F "):" ",b=v?e.fg("text",e.bold(p.name)):e.fg("muted",p.name),I=S+b;if(v&&p.content){let h=p.content.split(`
2
+ `)[0]?.trim()??"";if(h){let y=t-2-p.name.length-4;if(y>8){let k=h.length>y?h.slice(0,y-1)+"\u2026":h;I+=" "+e.fg("dim",k)}}}n.push(f(I,t))}m<this.prompts.length&&n.push(f(" "+e.fg("dim",`\u2193 ${this.prompts.length-m} more below`),t))}n.push("");let i=this.prompts.length>0?"[\u2191\u2193/jk] navigate [Enter] paste [e] edit [d] delete [a] add [Esc] close":"[a] add a new prompt [Esc] close";return n.push(f(" "+e.fg("dim",i),t)),n.push(""),this.cache={w:t,lines:n},n}invalidate(){this.cache=void 0}};import{mkdir as C,readFile as L,writeFile as D}from"node:fs/promises";import{dirname as $,join as j}from"node:path";import{getAgentDir as T}from"@earendil-works/pi-coding-agent";function P(){return j(T(),"prompt-manager.json")}async function x(){try{let s=await L(P(),"utf8"),t=JSON.parse(s);return Array.isArray(t.prompts)?t.prompts:[]}catch{return[]}}async function w(s){let t=P();await C($(t),{recursive:!0}),await D(t,JSON.stringify({prompts:s},null,2),"utf8")}function A(){return Date.now().toString(36)+Math.random().toString(36).slice(2,7)}async function E(s,t){let e=t?.trim();if(!e){let i=await s.input("Prompt name:","e.g. Code Review Template");if(!i?.trim()){s.notify("Cancelled \u2014 no name given.","info");return}e=i.trim()}let n=await s.editor(`New prompt: ${e}`,"");if(!n?.trim()){s.notify("Cancelled \u2014 empty content not saved.","info");return}let a=await x(),d=a.findIndex(i=>i.name.toLowerCase()===e.toLowerCase());if(d>=0){if(!await s.confirm("Name already exists",`A prompt named "${e}" already exists. Overwrite it?`))return;a[d]={...a[d],content:n.trim(),updatedAt:Date.now()}}else{let i=Date.now();a.push({id:A(),name:e,content:n.trim(),createdAt:i,updatedAt:i})}await w(a),s.notify(`Prompt "${e}" saved.`,"info")}function M(s){s.registerCommand("prompt",{description:"Manage saved prompts \u2014 /prompt | /prompt add [name]",getArgumentCompletions:t=>{let n=[{value:"add",label:"add \u2014 create a new prompt"}].filter(a=>a.value.startsWith(t));return n.length>0?n:null},handler:async(t,e)=>{if(e.mode!=="tui"){e.ui.notify("/prompt requires interactive mode","error");return}let n=e.ui,a=t.trim().split(/\s+/).filter(Boolean),d=a[0]??"";if(d==="add"){let r=a.slice(1).join(" ")||void 0;await E(n,r);return}if(d!==""){n.notify("Usage: /prompt | /prompt add [name]","warning");return}let i=await x(),c=0;for(;;){let r=await n.custom((o,m,l,p)=>new g(i,c,m,p));if(!r||r.action==="close")break;if(r.action==="paste"){let o=i[r.index];o&&n.pasteToEditor(o.content);break}if(r.action==="add"){await E(n),i=await x();continue}if(r.action==="edit"){c=r.index;let o=i[r.index];if(!o)continue;let m=await n.editor(`Edit: ${o.name}`,o.content);m?.trim()&&(i[r.index]={...o,content:m.trim(),updatedAt:Date.now()},await w(i),n.notify(`"${o.name}" updated.`,"info"));continue}if(r.action==="delete"){c=r.index;let o=i[r.index];if(!o)continue;await n.confirm("Delete prompt",`Delete "${o.name}"? This cannot be undone.`)&&(i=i.filter((l,p)=>p!==r.index),c=Math.max(0,c-1),await w(i),n.notify(`"${o.name}" deleted.`,"info"))}}}})}function _(s){M(s)}export{_ as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sreetej510/pi-prompt-manager",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "Pi extension to quickly save, manage, and paste reusable prompts without retyping.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,16 +13,18 @@
13
13
  "snippets"
14
14
  ],
15
15
  "files": [
16
- "src",
16
+ "dist",
17
17
  "README.md",
18
18
  "LICENSE"
19
19
  ],
20
20
  "pi": {
21
21
  "extensions": [
22
- "./src/index.ts"
22
+ "./dist/index.js"
23
23
  ]
24
24
  },
25
25
  "scripts": {
26
+ "build": "node ../../scripts/build-extension.mjs",
27
+ "prepublishOnly": "npm run build",
26
28
  "check": "biome check . && npm run typecheck",
27
29
  "format": "biome check --write .",
28
30
  "typecheck": "tsc --noEmit"
package/src/command.ts DELETED
@@ -1,137 +0,0 @@
1
- import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
- import { PromptManagerComponent } from "./component.js";
3
- import { loadPrompts, newId, savePrompts } from "./storage.js";
4
- import type { ListResult } from "./types.js";
5
-
6
- type PromptUi = ExtensionCommandContext["ui"];
7
-
8
- async function addPrompt(ui: PromptUi, nameHint?: string): Promise<void> {
9
- let name = nameHint?.trim();
10
-
11
- if (!name) {
12
- const entered = await ui.input("Prompt name:", "e.g. Code Review Template");
13
- if (!entered?.trim()) {
14
- ui.notify("Cancelled — no name given.", "info");
15
- return;
16
- }
17
- name = entered.trim();
18
- }
19
-
20
- const content = await ui.editor(`New prompt: ${name}`, "");
21
- if (!content?.trim()) {
22
- ui.notify("Cancelled — empty content not saved.", "info");
23
- return;
24
- }
25
-
26
- const all = await loadPrompts();
27
- const existIdx = all.findIndex((p) => p.name.toLowerCase() === name!.toLowerCase());
28
-
29
- if (existIdx >= 0) {
30
- const ok = await ui.confirm("Name already exists", `A prompt named "${name}" already exists. Overwrite it?`);
31
- if (!ok) return;
32
- all[existIdx] = {
33
- ...all[existIdx]!,
34
- content: content.trim(),
35
- updatedAt: Date.now(),
36
- };
37
- } else {
38
- const now = Date.now();
39
- all.push({
40
- id: newId(),
41
- name,
42
- content: content.trim(),
43
- createdAt: now,
44
- updatedAt: now,
45
- });
46
- }
47
-
48
- await savePrompts(all);
49
- ui.notify(`Prompt "${name}" saved.`, "info");
50
- }
51
-
52
- export function registerPromptCommand(pi: ExtensionAPI): void {
53
- pi.registerCommand("prompt", {
54
- description: "Manage saved prompts — /prompt | /prompt add [name]",
55
-
56
- getArgumentCompletions: (prefix) => {
57
- const opts = [{ value: "add", label: "add — create a new prompt" }];
58
- const filtered = opts.filter((o) => o.value.startsWith(prefix));
59
- return filtered.length > 0 ? filtered : null;
60
- },
61
-
62
- handler: async (args, ctx) => {
63
- if (ctx.mode !== "tui") {
64
- ctx.ui.notify("/prompt requires interactive mode", "error");
65
- return;
66
- }
67
-
68
- const ui = ctx.ui;
69
- const parts = args.trim().split(/\s+/).filter(Boolean);
70
- const sub = parts[0] ?? "";
71
-
72
- if (sub === "add") {
73
- const nameArg = parts.slice(1).join(" ") || undefined;
74
- await addPrompt(ui, nameArg);
75
- return;
76
- }
77
-
78
- if (sub !== "") {
79
- ui.notify("Usage: /prompt | /prompt add [name]", "warning");
80
- return;
81
- }
82
-
83
- let all = await loadPrompts();
84
- let sel = 0;
85
-
86
- for (;;) {
87
- const r = await ui.custom<ListResult>(
88
- (_tui, theme, _kb, done) => new PromptManagerComponent(all, sel, theme, done),
89
- );
90
-
91
- if (!r || r.action === "close") break;
92
-
93
- if (r.action === "paste") {
94
- const p = all[r.index];
95
- if (p) ui.pasteToEditor(p.content);
96
- break;
97
- }
98
-
99
- if (r.action === "add") {
100
- await addPrompt(ui);
101
- all = await loadPrompts();
102
- continue;
103
- }
104
-
105
- if (r.action === "edit") {
106
- sel = r.index;
107
- const p = all[r.index];
108
- if (!p) continue;
109
- const txt = await ui.editor(`Edit: ${p.name}`, p.content);
110
- if (txt?.trim()) {
111
- all[r.index] = {
112
- ...p,
113
- content: txt.trim(),
114
- updatedAt: Date.now(),
115
- };
116
- await savePrompts(all);
117
- ui.notify(`"${p.name}" updated.`, "info");
118
- }
119
- continue;
120
- }
121
-
122
- if (r.action === "delete") {
123
- sel = r.index;
124
- const p = all[r.index];
125
- if (!p) continue;
126
- const ok = await ui.confirm("Delete prompt", `Delete "${p.name}"? This cannot be undone.`);
127
- if (ok) {
128
- all = all.filter((_, i) => i !== r.index);
129
- sel = Math.max(0, sel - 1);
130
- await savePrompts(all);
131
- ui.notify(`"${p.name}" deleted.`, "info");
132
- }
133
- }
134
- }
135
- },
136
- });
137
- }
package/src/component.ts DELETED
@@ -1,130 +0,0 @@
1
- import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
3
- import type { ListResult, SavedPrompt } from "./types.js";
4
-
5
- export class PromptManagerComponent {
6
- private selectedIndex: number;
7
- private cache?: { w: number; lines: string[] };
8
-
9
- constructor(
10
- private prompts: SavedPrompt[],
11
- initialIndex: number,
12
- private theme: Theme,
13
- private done: (result: ListResult) => void,
14
- ) {
15
- this.selectedIndex = prompts.length > 0 ? Math.max(0, Math.min(initialIndex, prompts.length - 1)) : 0;
16
- }
17
-
18
- handleInput(data: string): void {
19
- const n = this.prompts.length;
20
-
21
- if (matchesKey(data, "up") || data === "k") {
22
- if (this.selectedIndex > 0) {
23
- this.selectedIndex--;
24
- this.invalidate();
25
- }
26
- return;
27
- }
28
- if (matchesKey(data, "down") || data === "j") {
29
- if (this.selectedIndex < n - 1) {
30
- this.selectedIndex++;
31
- this.invalidate();
32
- }
33
- return;
34
- }
35
-
36
- if (matchesKey(data, "enter") && n > 0) {
37
- this.done({ action: "paste", index: this.selectedIndex });
38
- return;
39
- }
40
- if ((data === "e" || data === "E") && n > 0) {
41
- this.done({ action: "edit", index: this.selectedIndex });
42
- return;
43
- }
44
- if ((data === "d" || data === "D") && n > 0) {
45
- this.done({ action: "delete", index: this.selectedIndex });
46
- return;
47
- }
48
- if (data === "a" || data === "A") {
49
- this.done({ action: "add" });
50
- return;
51
- }
52
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
53
- this.done({ action: "close" });
54
- return;
55
- }
56
- }
57
-
58
- render(width: number): string[] {
59
- if (this.cache?.w === width) return this.cache.lines;
60
-
61
- const th = this.theme;
62
- const lines: string[] = [];
63
-
64
- const TITLE = " Prompt Manager ";
65
- const rightDashes = Math.max(0, width - TITLE.length - 3);
66
- lines.push(
67
- truncateToWidth(
68
- th.fg("borderMuted", "─".repeat(3)) + th.fg("accent", TITLE) + th.fg("borderMuted", "─".repeat(rightDashes)),
69
- width,
70
- ),
71
- );
72
- lines.push("");
73
-
74
- if (this.prompts.length === 0) {
75
- lines.push(truncateToWidth(" " + th.fg("dim", "No prompts saved yet. Press [a] to add one."), width));
76
- } else {
77
- const MAX_VISIBLE = 10;
78
- const half = Math.floor(MAX_VISIBLE / 2);
79
- const start = Math.max(0, Math.min(this.selectedIndex - half, this.prompts.length - MAX_VISIBLE));
80
- const end = Math.min(this.prompts.length, start + MAX_VISIBLE);
81
-
82
- if (start > 0) {
83
- lines.push(truncateToWidth(" " + th.fg("dim", `↑ ${start} more above`), width));
84
- }
85
-
86
- for (let i = start; i < end; i++) {
87
- const p = this.prompts[i]!;
88
- const isSel = i === this.selectedIndex;
89
-
90
- const arrow = isSel ? th.fg("accent", "❯ ") : " ";
91
- const nameStyled = isSel ? th.fg("text", th.bold(p.name)) : th.fg("muted", p.name);
92
-
93
- let row = arrow + nameStyled;
94
-
95
- if (isSel && p.content) {
96
- const firstLine = p.content.split("\n")[0]?.trim() ?? "";
97
- if (firstLine) {
98
- const avail = width - 2 - p.name.length - 4;
99
- if (avail > 8) {
100
- const preview = firstLine.length > avail ? firstLine.slice(0, avail - 1) + "…" : firstLine;
101
- row += " " + th.fg("dim", preview);
102
- }
103
- }
104
- }
105
-
106
- lines.push(truncateToWidth(row, width));
107
- }
108
-
109
- if (end < this.prompts.length) {
110
- lines.push(truncateToWidth(" " + th.fg("dim", `↓ ${this.prompts.length - end} more below`), width));
111
- }
112
- }
113
-
114
- lines.push("");
115
-
116
- const hints =
117
- this.prompts.length > 0
118
- ? "[↑↓/jk] navigate [Enter] paste [e] edit [d] delete [a] add [Esc] close"
119
- : "[a] add a new prompt [Esc] close";
120
- lines.push(truncateToWidth(" " + th.fg("dim", hints), width));
121
- lines.push("");
122
-
123
- this.cache = { w: width, lines };
124
- return lines;
125
- }
126
-
127
- invalidate(): void {
128
- this.cache = undefined;
129
- }
130
- }
package/src/index.ts DELETED
@@ -1,25 +0,0 @@
1
- /**
2
- * Prompt Manager Extension
3
- *
4
- * Quickly save, manage, and paste reusable prompts without retyping.
5
- *
6
- * Commands
7
- * ────────
8
- * /prompt Open the prompt manager (select · edit · delete · add)
9
- * /prompt add [name] Create a new saved prompt directly (opens multi-line editor)
10
- *
11
- * Storage: <pi-agent-dir>/prompt-manager.json
12
- *
13
- * File layout:
14
- * index.ts extension entry point (this file)
15
- * command.ts /prompt command handler
16
- * component.ts list-manager TUI component
17
- * storage.ts prompt-manager.json load/save helpers
18
- * types.ts shared TypeScript types
19
- */
20
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
21
- import { registerPromptCommand } from "./command.js";
22
-
23
- export default function promptManagerExtension(pi: ExtensionAPI) {
24
- registerPromptCommand(pi);
25
- }
package/src/storage.ts DELETED
@@ -1,28 +0,0 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
3
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
- import type { SavedPrompt } from "./types.js";
5
-
6
- function dataFilePath(): string {
7
- return join(getAgentDir(), "prompt-manager.json");
8
- }
9
-
10
- export async function loadPrompts(): Promise<SavedPrompt[]> {
11
- try {
12
- const raw = await readFile(dataFilePath(), "utf8");
13
- const parsed = JSON.parse(raw) as { prompts?: unknown };
14
- return Array.isArray(parsed.prompts) ? (parsed.prompts as SavedPrompt[]) : [];
15
- } catch {
16
- return [];
17
- }
18
- }
19
-
20
- export async function savePrompts(prompts: SavedPrompt[]): Promise<void> {
21
- const path = dataFilePath();
22
- await mkdir(dirname(path), { recursive: true });
23
- await writeFile(path, JSON.stringify({ prompts }, null, 2), "utf8");
24
- }
25
-
26
- export function newId(): string {
27
- return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
28
- }
package/src/types.ts DELETED
@@ -1,14 +0,0 @@
1
- export interface SavedPrompt {
2
- id: string;
3
- name: string;
4
- content: string;
5
- createdAt: number;
6
- updatedAt: number;
7
- }
8
-
9
- export type ListResult =
10
- | { action: "close" }
11
- | { action: "paste"; index: number }
12
- | { action: "edit"; index: number }
13
- | { action: "delete"; index: number }
14
- | { action: "add" };