@yusukeshib/pi-stash 0.1.0 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-stash
2
2
 
3
- A personal stash of reusable prompt fragments for the [Pi coding agent](https://pi.dev).
3
+ A per-session stash of reusable prompt fragments for the [Pi coding agent](https://pi.dev).
4
4
 
5
5
  Jot a prompt down whenever you think of it mid-task, then pop it into the editor
6
6
  when you actually need it. Think of it as a stack-like scratchpad for prompts —
@@ -50,7 +50,10 @@ pi install npm:@yusukeshib/pi-stash
50
50
  |---------|--------|
51
51
  | `/stash <text>` | **Push** — save the given text onto the stash. |
52
52
  | `/stash` | **Pop** — pick a saved entry, insert it into the editor, and remove it from the stash. Run repeatedly to stack several fragments together. |
53
- | `/stash-clear` | Delete every stashed entry (with confirm). |
53
+ | `/stash-clear` | Delete every stashed entry in this session (with confirm). |
54
+
55
+ While the stash is non-empty, a red `stash:N` badge is shown in the footer so
56
+ you never forget you have prompts parked.
54
57
 
55
58
  ## How it works
56
59
 
@@ -65,21 +68,13 @@ build up by hand during real work:
65
68
 
66
69
  ## Storage
67
70
 
68
- Entries live in a single global JSON file, **shared across every Pi session on
69
- the machine** (not per-project, not per-session):
70
-
71
- ```
72
- ~/.pi/agent/prompt-stash.json
73
- ```
74
-
75
- Override the location with the `PI_STASH_PATH` environment variable:
76
-
77
- ```bash
78
- export PI_STASH_PATH="$HOME/.config/pi/stash.json"
79
- ```
71
+ Entries are stored **inside the session itself** as custom session entries —
72
+ each Pi session has its own independent stash. It survives restarts and
73
+ `/resume`, and follows branching (`/fork`, `/clone`) correctly: a forked
74
+ session sees the stash as it was at the fork point.
80
75
 
81
- > Note: the store is read-modify-written on each command. If two sessions edit
82
- > the stash at the exact same moment, the last write wins.
76
+ Nothing is written outside the session file, and the stash does not
77
+ participate in the LLM context.
83
78
 
84
79
  ## License
85
80
 
@@ -1,7 +1,7 @@
1
1
  /**
2
- * pi-stash — a personal stash of reusable prompt fragments for the Pi coding
3
- * agent. Jot a prompt down whenever you think of it, then later pull up the
4
- * list and pop one into the editor to compose your next message.
2
+ * pi-stash — a per-session stash of reusable prompt fragments for the Pi
3
+ * coding agent. Jot a prompt down whenever you think of it, then later pull
4
+ * up the list and pop one into the editor to compose your next message.
5
5
  *
6
6
  * Unlike file-based prompt templates (static `/name` commands), this is an
7
7
  * ad-hoc, mutable, stack-like backlog you build up by hand during real work.
@@ -12,43 +12,25 @@
12
12
  * remove it from the stash. Run repeatedly to stack fragments.
13
13
  * /stash-clear Delete every entry (with confirm).
14
14
  *
15
- * Storage: a single global JSON file, shared across every Pi session on the
16
- * machine (not per-project, not per-session). Default location:
17
- * ~/.pi/agent/prompt-stash.json
18
- * Override with the PI_STASH_PATH environment variable.
15
+ * Storage: stash entries are persisted INSIDE the session itself via custom
16
+ * session entries (`pi.appendEntry`). Each session has its own stash, it
17
+ * survives restarts/resume, and it follows branching (fork/clone) correctly.
18
+ *
19
+ * While the stash is non-empty, a red badge with the entry count is shown in
20
+ * the footer so you don't forget about pending prompts.
19
21
  */
20
22
 
21
- import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
22
- import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
23
- import { homedir } from "node:os";
24
- import { dirname, join } from "node:path";
23
+ import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
25
24
 
26
25
  interface StashEntry {
27
26
  text: string;
28
27
  addedAt: number;
29
28
  }
30
29
 
31
- const STORE_PATH = process.env.PI_STASH_PATH || join(homedir(), ".pi", "agent", "prompt-stash.json");
30
+ const CUSTOM_TYPE = "pi-stash-state";
31
+ const STATUS_KEY = "pi-stash";
32
32
  const PREVIEW_LEN = 72;
33
33
 
34
- function load(): StashEntry[] {
35
- try {
36
- const raw = readFileSync(STORE_PATH, "utf8");
37
- const parsed = JSON.parse(raw);
38
- if (Array.isArray(parsed)) {
39
- return parsed.filter((e): e is StashEntry => e && typeof e.text === "string");
40
- }
41
- } catch {
42
- // missing or corrupt → empty stash
43
- }
44
- return [];
45
- }
46
-
47
- function save(entries: StashEntry[]): void {
48
- mkdirSync(dirname(STORE_PATH), { recursive: true });
49
- writeFileSync(STORE_PATH, `${JSON.stringify(entries, null, 2)}\n`, "utf8");
50
- }
51
-
52
34
  /** One-line, length-bounded preview for select menus. */
53
35
  function preview(text: string, index: number): string {
54
36
  const oneLine = text.replace(/\s+/g, " ").trim();
@@ -58,6 +40,39 @@ function preview(text: string, index: number): string {
58
40
  }
59
41
 
60
42
  export default function (pi: ExtensionAPI) {
43
+ // In-memory stash for the current session; persisted as custom entries.
44
+ let entries: StashEntry[] = [];
45
+
46
+ function persist(): void {
47
+ pi.appendEntry(CUSTOM_TYPE, { entries });
48
+ }
49
+
50
+ /** Red, hard-to-miss footer badge while the stash is non-empty. */
51
+ function updateStatus(ctx: ExtensionContext): void {
52
+ if (entries.length > 0) {
53
+ // White on red background (raw ANSI so it stays red in any theme).
54
+ ctx.ui.setStatus(STATUS_KEY, `\x1b[41m\x1b[97m\x1b[1m stash:${entries.length} \x1b[0m`);
55
+ } else {
56
+ ctx.ui.setStatus(STATUS_KEY, undefined);
57
+ }
58
+ }
59
+
60
+ // Restore stash from the session (last persisted state on the active path).
61
+ pi.on("session_start", async (_event, ctx) => {
62
+ entries = [];
63
+ for (const entry of ctx.sessionManager.getEntries()) {
64
+ if (entry.type === "custom" && entry.customType === CUSTOM_TYPE) {
65
+ const data = entry.data as { entries?: unknown } | undefined;
66
+ if (data && Array.isArray(data.entries)) {
67
+ entries = data.entries.filter(
68
+ (e): e is StashEntry => !!e && typeof (e as StashEntry).text === "string",
69
+ );
70
+ }
71
+ }
72
+ }
73
+ updateStatus(ctx);
74
+ });
75
+
61
76
  pi.registerCommand("stash", {
62
77
  description: "Push text (with arg) or pop an entry into the editor (no arg)",
63
78
  handler: async (args, ctx) => {
@@ -65,19 +80,18 @@ export default function (pi: ExtensionAPI) {
65
80
 
66
81
  // PUSH: /stash <text>
67
82
  if (text) {
68
- const entries = load();
69
83
  if (entries.some((e) => e.text === text)) {
70
84
  ctx.ui.notify("Already in stash", "info");
71
85
  return;
72
86
  }
73
87
  entries.push({ text, addedAt: Date.now() });
74
- save(entries);
88
+ persist();
89
+ updateStatus(ctx);
75
90
  ctx.ui.notify(`Stashed (${entries.length} total)`, "info");
76
91
  return;
77
92
  }
78
93
 
79
94
  // POP: /stash → pick, insert into editor, remove from stash
80
- const entries = load();
81
95
  if (entries.length === 0) {
82
96
  ctx.ui.notify("Stash is empty. Add one with /stash <text>", "info");
83
97
  return;
@@ -95,21 +109,23 @@ export default function (pi: ExtensionAPI) {
95
109
 
96
110
  // pop = remove the chosen entry
97
111
  entries.splice(idx, 1);
98
- save(entries);
112
+ persist();
113
+ updateStatus(ctx);
99
114
  },
100
115
  });
101
116
 
102
117
  pi.registerCommand("stash-clear", {
103
- description: "Delete every stashed prompt",
118
+ description: "Delete every stashed prompt in this session",
104
119
  handler: async (_args, ctx) => {
105
- const entries = load();
106
120
  if (entries.length === 0) {
107
121
  ctx.ui.notify("Stash is already empty", "info");
108
122
  return;
109
123
  }
110
124
  const ok = await ctx.ui.confirm("Clear the entire stash?", `${entries.length} entries will be deleted`);
111
125
  if (!ok) return;
112
- save([]);
126
+ entries = [];
127
+ persist();
128
+ updateStatus(ctx);
113
129
  ctx.ui.notify("Stash cleared", "info");
114
130
  },
115
131
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yusukeshib/pi-stash",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A personal stash of reusable prompt fragments for the Pi coding agent. Push prompts you think of mid-task, then pop them into the editor when you need them.",
5
5
  "type": "module",
6
6
  "license": "MIT",