@henryqw/pi-notes 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/README.md CHANGED
@@ -15,11 +15,10 @@ pi install npm:@henryqw/pi-notes
15
15
  | `/note <text>` | command | Add a note for current Git worktree (max 4). |
16
16
  | `/note-rm` | command | Pick a note from current worktree to remove. |
17
17
  | `/note-clear` | command | Clear current worktree's notes. |
18
- | `/note-prune` | command | Delete notes for repositories and worktrees that no longer exist. |
19
18
 
20
- Notes are isolated per Git worktree, render as a numbered widget above editor, and persist across sessions under `~/.pi/agent/config/pi-notes/`. Startup only reads config. `/note-prune` is explicit stale-data cleanup.
19
+ Notes are isolated per Git worktree, render as a numbered widget above editor, and persist across sessions under `~/.pi/agent/config/pi-notes/`. Empty worktrees show no widget. Stale files for removed repositories and worktrees are deleted silently when a session starts or notes change.
21
20
 
22
- Each worktree file is validated as untrusted data. Malformed files are preserved and block mutation for affected worktree until fixed or reset with `/note-clear`; `/note-prune` reports but never deletes malformed files.
21
+ Each worktree file is validated as untrusted data. Malformed files are preserved and block mutation for affected worktree until fixed or reset with `/note-clear`.
23
22
 
24
23
  ## Remove
25
24
 
@@ -69,7 +69,11 @@ export function parseNotes(raw: string): NotesRecord {
69
69
  }
70
70
 
71
71
  export function renderNotes(notes: string[]): string[] {
72
- return notes.length ? notes.map((note, i) => `${i + 1}. ${note}`) : ["no notes"];
72
+ return notes.map((note, i) => `${i + 1}. ${note}`);
73
+ }
74
+
75
+ function setNotesWidget(ctx: ExtensionContext, notes: string[]): void {
76
+ ctx.ui.setWidget(WIDGET_KEY, notes.length ? renderNotes(notes) : undefined);
73
77
  }
74
78
 
75
79
  async function resolveWorktree(pi: ExtensionAPI, cwd: string): Promise<WorktreeIdentity> {
@@ -151,22 +155,22 @@ async function refresh(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
151
155
  try {
152
156
  const identity = await resolveWorktree(pi, ctx.cwd);
153
157
  const loaded = await loadNotes(identity);
154
- ctx.ui.setWidget(WIDGET_KEY, issueMessage(loaded.issue) ? [issueMessage(loaded.issue)!] : renderNotes(loaded.notes));
158
+ const message = issueMessage(loaded.issue);
159
+ if (message) ctx.ui.setWidget(WIDGET_KEY, [message]);
160
+ else setNotesWidget(ctx, loaded.notes);
155
161
  } catch {
156
162
  ctx.ui.setWidget(WIDGET_KEY, undefined);
157
163
  }
158
164
  }
159
165
 
160
- async function pruneStale(pi: ExtensionAPI): Promise<{ removed: number; skipped: number }> {
166
+ async function pruneStale(pi: ExtensionAPI): Promise<void> {
161
167
  let entries;
162
168
  try {
163
169
  entries = await readdir(configDir(), { withFileTypes: true });
164
170
  } catch (error) {
165
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return { removed: 0, skipped: 0 };
171
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
166
172
  throw error;
167
173
  }
168
- let removed = 0;
169
- let skipped = 0;
170
174
  for (const entry of entries) {
171
175
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
172
176
  const path = join(configDir(), entry.name);
@@ -174,13 +178,9 @@ async function pruneStale(pi: ExtensionAPI): Promise<{ removed: number; skipped:
174
178
  try {
175
179
  record = parseNotes(await readFile(path, "utf8"));
176
180
  } catch {
177
- skipped++;
178
- continue;
179
- }
180
- if (path !== notesPath(record.worktree)) {
181
- skipped++;
182
181
  continue;
183
182
  }
183
+ if (path !== notesPath(record.worktree)) continue;
184
184
  let stale = false;
185
185
  try {
186
186
  const [worktree, repository, gitDir] = await Promise.all([
@@ -194,31 +194,34 @@ async function pruneStale(pi: ExtensionAPI): Promise<{ removed: number; skipped:
194
194
  || await worktreeGeneration(gitDir) !== record.generation;
195
195
  } catch (error) {
196
196
  if (isMissing(error)) stale = true;
197
- else {
198
- skipped++;
199
- continue;
200
- }
197
+ else continue;
201
198
  }
202
199
  if (!stale) {
203
200
  let current: WorktreeIdentity;
204
201
  try {
205
202
  current = await resolveWorktree(pi, record.worktree);
206
203
  } catch {
207
- skipped++;
208
204
  continue;
209
205
  }
210
206
  stale = !isDeepStrictEqual(current, recordIdentity(record));
211
207
  }
212
- if (stale) {
213
- await rm(path, { force: true });
214
- removed++;
215
- }
208
+ if (stale) await rm(path, { force: true });
209
+ }
210
+ }
211
+
212
+ async function pruneStaleNotes(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
213
+ try {
214
+ await pruneStale(pi);
215
+ } catch (error) {
216
+ ctx.ui.notify(`Failed to prune stale notes: ${error instanceof Error ? error.message : String(error)}`, "error");
216
217
  }
217
- return { removed, skipped };
218
218
  }
219
219
 
220
220
  export default function notesExtension(pi: ExtensionAPI): void {
221
- pi.on("session_start", (_event, ctx) => refresh(pi, ctx));
221
+ pi.on("session_start", async (_event, ctx) => {
222
+ await pruneStaleNotes(pi, ctx);
223
+ await refresh(pi, ctx);
224
+ });
222
225
 
223
226
  pi.registerCommand("note", {
224
227
  description: `Add a note to this Git worktree (max ${MAX_NOTES})`,
@@ -240,7 +243,8 @@ export default function notesExtension(pi: ExtensionAPI): void {
240
243
  }
241
244
  current.notes.push(text);
242
245
  await persist(current.identity, current.notes);
243
- ctx.ui.setWidget(WIDGET_KEY, renderNotes(current.notes));
246
+ setNotesWidget(ctx, current.notes);
247
+ await pruneStaleNotes(pi, ctx);
244
248
  },
245
249
  });
246
250
 
@@ -268,7 +272,8 @@ export default function notesExtension(pi: ExtensionAPI): void {
268
272
  }
269
273
  current.notes.splice(index, 1);
270
274
  await persist(current.identity, current.notes);
271
- ctx.ui.setWidget(WIDGET_KEY, renderNotes(current.notes));
275
+ setNotesWidget(ctx, current.notes);
276
+ await pruneStaleNotes(pi, ctx);
272
277
  },
273
278
  });
274
279
 
@@ -282,23 +287,11 @@ export default function notesExtension(pi: ExtensionAPI): void {
282
287
  try {
283
288
  const identity = await resolveWorktree(pi, ctx.cwd);
284
289
  await rm(notesPath(identity.worktree), { force: true });
285
- ctx.ui.setWidget(WIDGET_KEY, renderNotes([]));
290
+ setNotesWidget(ctx, []);
291
+ await pruneStaleNotes(pi, ctx);
286
292
  } catch (error) {
287
293
  ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
288
294
  }
289
295
  },
290
296
  });
291
-
292
- pi.registerCommand("note-prune", {
293
- description: "Delete notes for removed repositories and worktrees",
294
- handler: async (args, ctx) => {
295
- if (args.trim()) {
296
- ctx.ui.notify("Usage: /note-prune", "warning");
297
- return;
298
- }
299
- const { removed, skipped } = await pruneStale(pi);
300
- ctx.ui.notify(`Removed ${removed} stale worktree note file${removed === 1 ? "" : "s"}; preserved ${skipped} unchecked or invalid file${skipped === 1 ? "" : "s"}.`, "info");
301
- await refresh(pi, ctx);
302
- },
303
- });
304
297
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-notes",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Persistent notes in a Pi widget, managed with /note commands.",
5
5
  "keywords": [
6
6
  "pi-package",