pi-extmgr 0.1.6 → 0.1.7

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
@@ -22,6 +22,7 @@ Then reload Pi with `/reload`.
22
22
  - **Package actions**: Update or remove installed packages with `A`
23
23
  - **Browse community**: Search and install from npm (`R` to browse)
24
24
  - **History tracking**: See what you've changed with `/extensions history`
25
+ - **Smart reload prompt**: After applying extension state changes, extmgr can trigger reload directly when Pi exposes a reload API (falls back to `/reload` command text otherwise)
25
26
 
26
27
  ## Usage
27
28
 
@@ -38,7 +39,13 @@ Open the manager:
38
39
  | `↑↓` | Navigate |
39
40
  | `Space/Enter` | Toggle local extension on/off |
40
41
  | `S` | Save changes |
41
- | `A` | Actions on selected package (update/remove/view) |
42
+ | `Enter` / `A` | Actions on selected package (update/remove/view) |
43
+ | `u` | Update selected package directly |
44
+ | `X` | Remove selected package directly |
45
+ | `i` | Quick install by source |
46
+ | `f` | Quick search |
47
+ | `U` | Update all packages |
48
+ | `t` | Auto-update wizard |
42
49
  | `R` | Browse remote packages |
43
50
  | `?` / `H` | Help |
44
51
  | `Esc` | Exit |
@@ -50,6 +57,8 @@ Open the manager:
50
57
  /extensions search <query> # Search npm
51
58
  /extensions install <source> # Install package
52
59
  /extensions remove [source] # Remove package
60
+ /extensions update [source] # Update one package (or all when omitted)
61
+ /extensions auto-update [every] # No arg opens wizard; accepts 1d, 1w, never, etc.
53
62
  /extensions history # View change history
54
63
  /extensions stats # View statistics
55
64
  /extensions clear-cache # Clear metadata cache
@@ -72,6 +81,8 @@ Open the manager:
72
81
  - **Two install modes**:
73
82
  - **Managed** (npm): Auto-updates with `pi update`, stored in pi's package cache
74
83
  - **Local** (standalone): Copies to `~/.pi/agent/extensions/{package}/`, supports multi-file extensions
84
+ - **Auto-update schedule is persistent**: `/extensions auto-update 1d` stays active across future Pi sessions.
85
+ - **Reload is API-aware**: When extmgr asks to reload, it will call Pi's reload API when available, otherwise it pre-fills `/reload` for you.
75
86
  - **Remove requires restart**: After removing a package, you need to fully restart Pi (not just `/reload`) for it to be completely unloaded.
76
87
 
77
88
  ## Keyboard shortcut
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extmgr",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Enhanced UX for managing local Pi extensions and community packages",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/index.ts CHANGED
@@ -14,19 +14,26 @@ import { showInteractive, showListOnly, showInstalledPackagesLegacy } from "./ui
14
14
  import { showRemote } from "./ui/remote.js";
15
15
  import { showHelp } from "./ui/help.js";
16
16
  import { installPackage } from "./packages/install.js";
17
- import { removePackage, promptRemove, showInstalledPackagesList } from "./packages/management.js";
17
+ import {
18
+ removePackage,
19
+ promptRemove,
20
+ showInstalledPackagesList,
21
+ updatePackage,
22
+ updatePackages,
23
+ } from "./packages/management.js";
18
24
  import { getInstalledPackages } from "./packages/discovery.js";
19
25
  import { getRecentChanges, formatChangeEntry, getChangeStats } from "./utils/history.js";
20
26
  import { clearCache } from "./utils/cache.js";
21
27
  import { notify } from "./utils/notify.js";
22
28
  import { formatListOutput } from "./utils/ui-helpers.js";
23
- import { parseDuration } from "./utils/settings.js";
29
+ import { parseDuration, getAutoUpdateConfig } from "./utils/settings.js";
24
30
  import {
25
31
  startAutoUpdateTimer,
26
32
  stopAutoUpdateTimer,
27
33
  enableAutoUpdate,
28
34
  disableAutoUpdate,
29
35
  getAutoUpdateStatus,
36
+ promptAutoUpdateWizard,
30
37
  } from "./utils/auto-update.js";
31
38
 
32
39
  export default function extensionsManager(pi: ExtensionAPI) {
@@ -43,6 +50,7 @@ export default function extensionsManager(pi: ExtensionAPI) {
43
50
  { value: "install", description: "Install a package" },
44
51
  { value: "remove", description: "Remove an installed package" },
45
52
  { value: "uninstall", description: "Remove an installed package (alias)" },
53
+ { value: "update", description: "Update one package or all packages" },
46
54
  { value: "history", description: "View extension change history" },
47
55
  { value: "stats", description: "View extension manager statistics" },
48
56
  { value: "clear-cache", description: "Clear metadata cache" },
@@ -84,6 +92,8 @@ export default function extensionsManager(pi: ExtensionAPI) {
84
92
  rest.length > 0 ? removePackage(rest.join(" "), ctx, pi) : promptRemove(ctx, pi),
85
93
  uninstall: () =>
86
94
  rest.length > 0 ? removePackage(rest.join(" "), ctx, pi) : promptRemove(ctx, pi),
95
+ update: () =>
96
+ rest.length > 0 ? updatePackage(rest.join(" "), ctx, pi) : updatePackages(ctx, pi),
87
97
  "auto-update": () => handleAutoUpdateCommand(rest.join(" "), ctx),
88
98
  history: () => showHistory(ctx, pi),
89
99
  stats: () => showStats(ctx, pi),
@@ -101,7 +111,7 @@ export default function extensionsManager(pi: ExtensionAPI) {
101
111
  } else {
102
112
  notify(
103
113
  ctx,
104
- `Unknown command: ${subcommand ?? "(empty)"}. Try: local, remote, installed, search, install, remove`,
114
+ `Unknown command: ${subcommand ?? "(empty)"}. Try: local, remote, installed, search, install, remove, update`,
105
115
  "warning"
106
116
  );
107
117
  }
@@ -125,6 +135,11 @@ export default function extensionsManager(pi: ExtensionAPI) {
125
135
  statusParts.push(autoUpdateStatus);
126
136
  }
127
137
 
138
+ const knownUpdates = getAutoUpdateConfig(ctx).updatesAvailable ?? [];
139
+ if (knownUpdates.length > 0) {
140
+ statusParts.push(`${knownUpdates.length} update${knownUpdates.length === 1 ? "" : "s"}`);
141
+ }
142
+
128
143
  if (statusParts.length > 0) {
129
144
  ctx.ui.setStatus("extmgr", ctx.ui.theme.fg("dim", statusParts.join(" • ")));
130
145
  } else {
@@ -160,11 +175,26 @@ export default function extensionsManager(pi: ExtensionAPI) {
160
175
  });
161
176
 
162
177
  // Handle auto-update command
163
- function handleAutoUpdateCommand(
178
+ async function handleAutoUpdateCommand(
164
179
  args: string,
165
180
  ctx: ExtensionCommandContext | ExtensionContext
166
- ): void {
167
- const duration = parseDuration(args);
181
+ ): Promise<void> {
182
+ const trimmed = args.trim();
183
+
184
+ // Interactive wizard when no arguments are provided
185
+ if (!trimmed && ctx.hasUI) {
186
+ await promptAutoUpdateWizard(pi, ctx, (packages) => {
187
+ notify(
188
+ ctx,
189
+ `Updates available for ${packages.length} package(s): ${packages.join(", ")}`,
190
+ "info"
191
+ );
192
+ });
193
+ void updateStatusBar(ctx);
194
+ return;
195
+ }
196
+
197
+ const duration = parseDuration(trimmed);
168
198
 
169
199
  if (!duration) {
170
200
  // Show current status
@@ -226,6 +256,7 @@ async function handleNonInteractive(
226
256
  console.log(" /extensions installed - List installed packages");
227
257
  console.log(" /extensions install <source> - Install a package");
228
258
  console.log(" /extensions remove <source> - Remove a package");
259
+ console.log(" /extensions update [source] - Update one package or all packages");
229
260
  };
230
261
 
231
262
  const nonInteractiveHandlers: Record<string, () => Promise<void> | void> = {
@@ -246,6 +277,8 @@ async function handleNonInteractive(
246
277
  rest.length > 0
247
278
  ? removePackage(rest.join(" "), ctx, pi)
248
279
  : console.log("Usage: /extensions remove <npm:package|git:url|path>"),
280
+ update: () =>
281
+ rest.length > 0 ? updatePackage(rest.join(" "), ctx, pi) : updatePackages(ctx, pi),
249
282
  "auto-update": () => {
250
283
  console.log("Auto-update requires interactive mode.");
251
284
  console.log("Usage: /extensions auto-update <duration>");
@@ -49,6 +49,7 @@ export interface UnifiedItem {
49
49
  version?: string | undefined;
50
50
  description?: string | undefined;
51
51
  size?: number | undefined; // Package size in bytes
52
+ updateAvailable?: boolean | undefined;
52
53
  }
53
54
 
54
55
  export interface SearchCache {
@@ -64,7 +65,8 @@ export type UnifiedAction =
64
65
  | { type: "remote" }
65
66
  | { type: "help" }
66
67
  | { type: "menu" }
67
- | { type: "action"; itemId: string };
68
+ | { type: "quick"; action: "install" | "search" | "update-all" | "auto-update" }
69
+ | { type: "action"; itemId: string; action?: "menu" | "update" | "remove" | "details" };
68
70
 
69
71
  export type BrowseAction =
70
72
  | { type: "package"; name: string }
package/src/ui/help.ts CHANGED
@@ -16,7 +16,13 @@ export function showHelp(ctx: ExtensionCommandContext): void {
16
16
  " ↑↓ Navigate list",
17
17
  " Space/Enter Toggle local extension enabled/disabled",
18
18
  " S Save changes to local extensions",
19
- " A Actions on selected package (update/remove)",
19
+ " Enter/A Open actions for selected package",
20
+ " u Update selected package",
21
+ " X Remove selected package",
22
+ " i Quick install by source",
23
+ " f Quick search",
24
+ " U Update all packages",
25
+ " t Auto-update wizard",
20
26
  " R Browse remote packages",
21
27
  " ?/H Show this help",
22
28
  " Esc Cancel",
@@ -35,6 +41,8 @@ export function showHelp(ctx: ExtensionCommandContext): void {
35
41
  " /extensions search <q> Search for packages",
36
42
  " /extensions install <s> Install package (npm:, git:, or path)",
37
43
  " /extensions remove <s> Remove installed package",
44
+ " /extensions update [s] Update package (or all packages)",
45
+ " /extensions auto-update Show or change update schedule",
38
46
  ];
39
47
 
40
48
  const output = lines.join("\n");
package/src/ui/remote.ts CHANGED
@@ -174,7 +174,7 @@ export async function browseRemotePackages(
174
174
  await showRemoteMenu(ctx, pi);
175
175
  return;
176
176
  case "package":
177
- await showPackageDetails(result.name, ctx, pi);
177
+ await showPackageDetails(result.name, ctx, pi, query, offset);
178
178
  return;
179
179
  }
180
180
  }
@@ -182,7 +182,9 @@ export async function browseRemotePackages(
182
182
  async function showPackageDetails(
183
183
  packageName: string,
184
184
  ctx: ExtensionCommandContext,
185
- pi: ExtensionAPI
185
+ pi: ExtensionAPI,
186
+ previousQuery: string,
187
+ previousOffset: number
186
188
  ): Promise<void> {
187
189
  if (!ctx.hasUI) {
188
190
  console.log(`Package: ${packageName}`);
@@ -230,9 +232,9 @@ async function showPackageDetails(
230
232
  ctx.ui.notify(`Package: ${packageName}\n${infoRes.stdout.slice(0, 500)}`, "info");
231
233
  }
232
234
  }
233
- await showPackageDetails(packageName, ctx, pi);
235
+ await showPackageDetails(packageName, ctx, pi, previousQuery, previousOffset);
234
236
  } else if (choice.includes("Back")) {
235
- await browseRemotePackages(ctx, "keywords:pi-package", pi);
237
+ await browseRemotePackages(ctx, previousQuery, pi, previousOffset);
236
238
  }
237
239
  }
238
240
 
package/src/ui/unified.ts CHANGED
@@ -17,11 +17,16 @@ import {
17
17
  import type { UnifiedItem, State, UnifiedAction, InstalledPackage } from "../types/index.js";
18
18
  import { discoverExtensions, setExtensionState } from "../extensions/discovery.js";
19
19
  import { getInstalledPackages } from "../packages/discovery.js";
20
- import { showPackageActions } from "../packages/management.js";
20
+ import {
21
+ showPackageActions,
22
+ updatePackage,
23
+ removePackage,
24
+ updatePackages,
25
+ } from "../packages/management.js";
21
26
  import { showRemote } from "./remote.js";
22
27
  import { showHelp } from "./help.js";
23
28
  import { discoverExtensions as discoverExt } from "../extensions/discovery.js";
24
- import { formatEntry as formatExtEntry, dynamicTruncate } from "../utils/format.js";
29
+ import { formatEntry as formatExtEntry, dynamicTruncate, formatBytes } from "../utils/format.js";
25
30
  import {
26
31
  getStatusIcon,
27
32
  getPackageIcon,
@@ -30,6 +35,7 @@ import {
30
35
  formatSize,
31
36
  } from "./theme.js";
32
37
  import { logExtensionToggle } from "../utils/history.js";
38
+ import { getKnownUpdates, promptAutoUpdateWizard } from "../utils/auto-update.js";
33
39
 
34
40
  export async function showInteractive(
35
41
  ctx: ExtensionCommandContext,
@@ -53,7 +59,8 @@ async function showInteractiveOnce(
53
59
  ]);
54
60
 
55
61
  // Build unified items list
56
- const items = buildUnifiedItems(localEntries, installedPackages);
62
+ const knownUpdates = getKnownUpdates(ctx);
63
+ const items = buildUnifiedItems(localEntries, installedPackages, knownUpdates);
57
64
 
58
65
  // If nothing found, show quick actions
59
66
  if (items.length === 0) {
@@ -85,12 +92,15 @@ async function showInteractiveOnce(
85
92
  new Text(
86
93
  theme.fg(
87
94
  "muted",
88
- `${items.length} item${items.length === 1 ? "" : "s"} • Space/Enter to toggle local extensions, A for actions on packages`
95
+ `${items.length} item${items.length === 1 ? "" : "s"} • Space/Enter toggle locals Enter/A actions u update pkg • x remove pkg`
89
96
  ),
90
97
  2,
91
98
  0
92
99
  )
93
100
  );
101
+ container.addChild(
102
+ new Text(theme.fg("dim", "Quick: i Install | f Search | U Update all | t Auto-update"), 2, 0)
103
+ );
94
104
  container.addChild(new Spacer(1));
95
105
 
96
106
  // Build settings items
@@ -134,19 +144,66 @@ async function showInteractiveOnce(
134
144
  container.invalidate();
135
145
  },
136
146
  handleInput(data: string) {
147
+ const getSelectedId = (): string | undefined => {
148
+ const selIdx = (settingsList as unknown as { selectedIndex: number }).selectedIndex ?? 0;
149
+ return settingsItems[selIdx]?.id ?? settingsItems[0]?.id;
150
+ };
151
+
152
+ const selectedId = getSelectedId();
153
+ const selectedItem = selectedId ? byId.get(selectedId) : undefined;
154
+
137
155
  if (matchesKey(data, Key.ctrl("s")) || data === "s" || data === "S") {
138
156
  done({ type: "apply" });
139
157
  return;
140
158
  }
159
+
160
+ // Enter on a package opens its action menu (fewer clicks)
161
+ if ((data === "\r" || data === "\n") && selectedId && selectedItem?.type === "package") {
162
+ done({ type: "action", itemId: selectedId, action: "menu" });
163
+ return;
164
+ }
165
+
141
166
  if (data === "a" || data === "A") {
142
- // Get currently selected item and show actions
143
- const selIdx = (settingsList as unknown as { selectedIndex: number }).selectedIndex ?? 0;
144
- const selectedId = settingsItems[selIdx]?.id ?? settingsItems[0]?.id;
145
167
  if (selectedId) {
146
- done({ type: "action", itemId: selectedId });
168
+ done({ type: "action", itemId: selectedId, action: "menu" });
147
169
  }
148
170
  return;
149
171
  }
172
+
173
+ // Quick actions (global)
174
+ if (data === "i") {
175
+ done({ type: "quick", action: "install" });
176
+ return;
177
+ }
178
+ if (data === "f") {
179
+ done({ type: "quick", action: "search" });
180
+ return;
181
+ }
182
+ if (data === "U") {
183
+ done({ type: "quick", action: "update-all" });
184
+ return;
185
+ }
186
+ if (data === "t" || data === "T") {
187
+ done({ type: "quick", action: "auto-update" });
188
+ return;
189
+ }
190
+
191
+ // Fast package actions
192
+ if (selectedId && selectedItem?.type === "package") {
193
+ if (data === "u") {
194
+ done({ type: "action", itemId: selectedId, action: "update" });
195
+ return;
196
+ }
197
+ if (data === "x" || data === "X") {
198
+ done({ type: "action", itemId: selectedId, action: "remove" });
199
+ return;
200
+ }
201
+ if (data === "v" || data === "V") {
202
+ done({ type: "action", itemId: selectedId, action: "details" });
203
+ return;
204
+ }
205
+ }
206
+
150
207
  if (data === "r" || data === "R") {
151
208
  done({ type: "remote" });
152
209
  return;
@@ -170,7 +227,8 @@ async function showInteractiveOnce(
170
227
 
171
228
  function buildUnifiedItems(
172
229
  localEntries: Awaited<ReturnType<typeof discoverExtensions>>,
173
- installedPackages: InstalledPackage[]
230
+ installedPackages: InstalledPackage[],
231
+ knownUpdates: Set<string>
174
232
  ): UnifiedItem[] {
175
233
  const items: UnifiedItem[] = [];
176
234
 
@@ -207,6 +265,7 @@ function buildUnifiedItems(
207
265
  version: pkg.version,
208
266
  description: pkg.description,
209
267
  size: pkg.size,
268
+ updateAvailable: knownUpdates.has(pkg.name),
210
269
  });
211
270
  }
212
271
 
@@ -260,7 +319,13 @@ function buildFooter(
260
319
  footerParts.push("↑↓ Navigate");
261
320
  if (hasLocals) footerParts.push("Space/Enter Toggle");
262
321
  if (hasLocals) footerParts.push(hasChanges ? "S Save*" : "S Save");
263
- if (hasPackages) footerParts.push("A Actions");
322
+ if (hasPackages) footerParts.push("Enter/A Actions");
323
+ if (hasPackages) footerParts.push("u Update");
324
+ if (hasPackages) footerParts.push("X Remove");
325
+ footerParts.push("i Install");
326
+ footerParts.push("f Search");
327
+ footerParts.push("U Update all");
328
+ footerParts.push("t Auto-update");
264
329
  footerParts.push("R Browse");
265
330
  footerParts.push("? Help");
266
331
  footerParts.push("Esc Cancel");
@@ -286,6 +351,7 @@ function formatUnifiedItemLabel(
286
351
  const scopeIcon = getScopeIcon(theme, item.scope as "global" | "project");
287
352
  const name = theme.bold(item.displayName);
288
353
  const version = item.version ? theme.fg("dim", `@${item.version}`) : "";
354
+ const updateBadge = item.updateAvailable ? ` ${theme.fg("warning", "[update]")}` : "";
289
355
 
290
356
  // Build info parts
291
357
  const infoParts: string[] = [];
@@ -308,7 +374,7 @@ function formatUnifiedItemLabel(
308
374
  }
309
375
 
310
376
  const summary = theme.fg("dim", infoParts.join(" • "));
311
- return `${pkgIcon} [${scopeIcon}] ${name}${version} - ${summary}`;
377
+ return `${pkgIcon} [${scopeIcon}] ${name}${version}${updateBadge} - ${summary}`;
312
378
  }
313
379
  }
314
380
 
@@ -342,6 +408,28 @@ async function handleUnifiedAction(
342
408
  return false;
343
409
  }
344
410
 
411
+ if (result.type === "quick") {
412
+ switch (result.action) {
413
+ case "install":
414
+ await showRemote("install", ctx, pi);
415
+ return false;
416
+ case "search":
417
+ await showRemote("search", ctx, pi);
418
+ return false;
419
+ case "update-all":
420
+ await updatePackages(ctx, pi);
421
+ return false;
422
+ case "auto-update":
423
+ await promptAutoUpdateWizard(pi, ctx, (packages) => {
424
+ ctx.ui.notify(
425
+ `Updates available for ${packages.length} package(s): ${packages.join(", ")}`,
426
+ "info"
427
+ );
428
+ });
429
+ return false;
430
+ }
431
+ }
432
+
345
433
  if (result.type === "action") {
346
434
  const item = byId.get(result.itemId);
347
435
  if (item?.type === "package") {
@@ -350,9 +438,31 @@ async function handleUnifiedAction(
350
438
  name: item.displayName,
351
439
  ...(item.version ? { version: item.version } : {}),
352
440
  scope: item.scope as "global" | "project",
441
+ ...(item.description ? { description: item.description } : {}),
442
+ ...(item.size !== undefined ? { size: item.size } : {}),
353
443
  };
354
- const exitManager = await showPackageActions(pkg, ctx, pi);
355
- return exitManager;
444
+
445
+ switch (result.action) {
446
+ case "update":
447
+ await updatePackage(pkg.source, ctx, pi);
448
+ return false;
449
+ case "remove":
450
+ await removePackage(pkg.source, ctx, pi);
451
+ return false;
452
+ case "details": {
453
+ const sizeStr = pkg.size !== undefined ? `\nSize: ${formatBytes(pkg.size)}` : "";
454
+ ctx.ui.notify(
455
+ `Name: ${pkg.name}\nVersion: ${pkg.version || "unknown"}\nSource: ${pkg.source}\nScope: ${pkg.scope}${sizeStr}${pkg.description ? `\nDescription: ${pkg.description}` : ""}`,
456
+ "info"
457
+ );
458
+ return false;
459
+ }
460
+ case "menu":
461
+ default: {
462
+ const exitManager = await showPackageActions(pkg, ctx, pi);
463
+ return exitManager;
464
+ }
465
+ }
356
466
  }
357
467
  return false;
358
468
  }
@@ -390,7 +500,12 @@ async function handleUnifiedAction(
390
500
  );
391
501
 
392
502
  if (shouldReload) {
393
- ctx.ui.setEditorText("/reload");
503
+ const reload = (ctx as ExtensionCommandContext & { reload?: () => Promise<void> }).reload;
504
+ if (typeof reload === "function") {
505
+ await reload.call(ctx);
506
+ } else {
507
+ ctx.ui.setEditorText("/reload");
508
+ }
394
509
  return true;
395
510
  }
396
511
  }
@@ -14,6 +14,7 @@ import {
14
14
  saveAutoUpdateConfig,
15
15
  getScheduleInterval,
16
16
  calculateNextCheck,
17
+ parseDuration,
17
18
  type AutoUpdateConfig,
18
19
  } from "./settings.js";
19
20
 
@@ -98,6 +99,7 @@ export async function checkForUpdates(
98
99
  ...config,
99
100
  lastCheck: Date.now(),
100
101
  nextCheck: calculateNextCheck(config.intervalMs),
102
+ updatesAvailable,
101
103
  });
102
104
 
103
105
  if (updatesAvailable.length > 0 && onUpdateAvailable) {
@@ -152,6 +154,76 @@ export function getAutoUpdateStatus(ctx: ExtensionCommandContext | ExtensionCont
152
154
  return `${indicator} ${config.displayText}`;
153
155
  }
154
156
 
157
+ /**
158
+ * Return package names currently known to have updates available
159
+ * (from the latest background check).
160
+ */
161
+ export function getKnownUpdates(ctx: ExtensionCommandContext | ExtensionContext): Set<string> {
162
+ const config = getAutoUpdateConfig(ctx);
163
+ return new Set(config.updatesAvailable ?? []);
164
+ }
165
+
166
+ /**
167
+ * Interactive wizard to configure auto-update frequency.
168
+ */
169
+ export async function promptAutoUpdateWizard(
170
+ pi: ExtensionAPI,
171
+ ctx: ExtensionCommandContext | ExtensionContext,
172
+ onUpdateAvailable?: (packages: string[]) => void
173
+ ): Promise<void> {
174
+ if (!ctx.hasUI) {
175
+ notify(ctx, "Auto-update wizard requires interactive mode.", "warning");
176
+ return;
177
+ }
178
+
179
+ const current = getAutoUpdateConfig(ctx);
180
+ const choice = await ctx.ui.select(`Auto-update (${current.displayText})`, [
181
+ "Off",
182
+ "Every hour",
183
+ "Daily",
184
+ "Weekly",
185
+ "Custom...",
186
+ "Cancel",
187
+ ]);
188
+
189
+ if (!choice || choice === "Cancel") return;
190
+
191
+ if (choice === "Off") {
192
+ disableAutoUpdate(pi, ctx);
193
+ return;
194
+ }
195
+
196
+ if (choice === "Every hour") {
197
+ enableAutoUpdate(pi, ctx, 60 * 60 * 1000, "1 hour", onUpdateAvailable);
198
+ return;
199
+ }
200
+
201
+ if (choice === "Daily") {
202
+ enableAutoUpdate(pi, ctx, 24 * 60 * 60 * 1000, "daily", onUpdateAvailable);
203
+ return;
204
+ }
205
+
206
+ if (choice === "Weekly") {
207
+ enableAutoUpdate(pi, ctx, 7 * 24 * 60 * 60 * 1000, "weekly", onUpdateAvailable);
208
+ return;
209
+ }
210
+
211
+ const input = await ctx.ui.input("Auto-update interval", current.displayText || "1d");
212
+ if (!input?.trim()) return;
213
+
214
+ const parsed = parseDuration(input.trim());
215
+ if (!parsed) {
216
+ notify(ctx, "Invalid duration. Examples: 1h, 1d, 1w, 1m, never", "warning");
217
+ return;
218
+ }
219
+
220
+ if (parsed.ms === 0) {
221
+ disableAutoUpdate(pi, ctx);
222
+ } else {
223
+ enableAutoUpdate(pi, ctx, parsed.ms, parsed.display, onUpdateAvailable);
224
+ }
225
+ }
226
+
155
227
  /**
156
228
  * Enable auto-update with specified interval
157
229
  */
@@ -168,6 +240,7 @@ export function enableAutoUpdate(
168
240
  displayText,
169
241
  lastCheck: Date.now(),
170
242
  nextCheck: calculateNextCheck(intervalMs),
243
+ updatesAvailable: [],
171
244
  };
172
245
 
173
246
  saveAutoUpdateConfig(pi, config);
@@ -189,6 +262,7 @@ export function disableAutoUpdate(
189
262
  intervalMs: 0,
190
263
  enabled: false,
191
264
  displayText: "off",
265
+ updatesAvailable: [],
192
266
  });
193
267
 
194
268
  notify(ctx, "Auto-update disabled", "info");
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * Auto-update settings storage
3
- * Uses extension state via pi.appendEntry() for persistence
3
+ * Persists to disk so config survives across pi sessions.
4
4
  */
5
5
  import type {
6
6
  ExtensionAPI,
7
7
  ExtensionCommandContext,
8
8
  ExtensionContext,
9
9
  } from "@mariozechner/pi-coding-agent";
10
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
10
13
 
11
14
  export interface AutoUpdateConfig {
12
15
  intervalMs: number;
@@ -14,6 +17,7 @@ export interface AutoUpdateConfig {
14
17
  nextCheck?: number;
15
18
  enabled: boolean;
16
19
  displayText: string; // Human-readable description
20
+ updatesAvailable?: string[];
17
21
  }
18
22
 
19
23
  const DEFAULT_CONFIG: AutoUpdateConfig = {
@@ -23,16 +27,43 @@ const DEFAULT_CONFIG: AutoUpdateConfig = {
23
27
  };
24
28
 
25
29
  const SETTINGS_KEY = "extmgr-auto-update";
30
+ const SETTINGS_DIR = process.env.PI_EXTMGR_CACHE_DIR
31
+ ? process.env.PI_EXTMGR_CACHE_DIR
32
+ : join(homedir(), ".pi", "agent", ".extmgr-cache");
33
+ const SETTINGS_FILE = join(SETTINGS_DIR, "auto-update.json");
34
+
35
+ function readConfigFromDisk(): AutoUpdateConfig | undefined {
36
+ try {
37
+ const raw = readFileSync(SETTINGS_FILE, "utf8");
38
+ const parsed = JSON.parse(raw) as Partial<AutoUpdateConfig>;
39
+ return { ...DEFAULT_CONFIG, ...parsed };
40
+ } catch {
41
+ return undefined;
42
+ }
43
+ }
44
+
45
+ function writeConfigToDisk(config: AutoUpdateConfig): void {
46
+ try {
47
+ mkdirSync(SETTINGS_DIR, { recursive: true });
48
+ writeFileSync(SETTINGS_FILE, JSON.stringify(config, null, 2), "utf8");
49
+ } catch {
50
+ // Best effort; session state still works even if disk write fails
51
+ }
52
+ }
26
53
 
27
54
  /**
28
- * Get auto-update config from session
55
+ * Get auto-update config.
56
+ * Priority:
57
+ * 1) latest value in current session entries
58
+ * 2) persisted value on disk
59
+ * 3) defaults
29
60
  */
30
61
  export function getAutoUpdateConfig(
31
62
  ctx: ExtensionCommandContext | ExtensionContext
32
63
  ): AutoUpdateConfig {
33
64
  const entries = ctx.sessionManager.getEntries();
34
65
 
35
- // Find most recent config entry
66
+ // Find most recent config entry in current session
36
67
  for (let i = entries.length - 1; i >= 0; i--) {
37
68
  const entry = entries[i];
38
69
  if (entry?.type === "custom" && entry.customType === SETTINGS_KEY && entry.data) {
@@ -40,19 +71,26 @@ export function getAutoUpdateConfig(
40
71
  }
41
72
  }
42
73
 
74
+ const persisted = readConfigFromDisk();
75
+ if (persisted) {
76
+ return persisted;
77
+ }
78
+
43
79
  return { ...DEFAULT_CONFIG };
44
80
  }
45
81
 
46
82
  /**
47
- * Save auto-update config to session
83
+ * Save auto-update config to session + disk.
48
84
  */
49
85
  export function saveAutoUpdateConfig(pi: ExtensionAPI, config: Partial<AutoUpdateConfig>): void {
50
86
  const fullConfig: AutoUpdateConfig = {
51
87
  ...DEFAULT_CONFIG,
88
+ ...(readConfigFromDisk() ?? {}),
52
89
  ...config,
53
90
  };
54
91
 
55
92
  pi.appendEntry(SETTINGS_KEY, fullConfig);
93
+ writeConfigToDisk(fullConfig);
56
94
  }
57
95
 
58
96
  /**
@@ -20,7 +20,12 @@ export async function confirmReload(
20
20
  const confirmed = await ctx.ui.confirm("Reload Required", `${reason}\nReload pi now?`);
21
21
 
22
22
  if (confirmed) {
23
- ctx.ui.setEditorText("/reload");
23
+ const reload = (ctx as ExtensionCommandContext & { reload?: () => Promise<void> }).reload;
24
+ if (typeof reload === "function") {
25
+ await reload.call(ctx);
26
+ } else {
27
+ ctx.ui.setEditorText("/reload");
28
+ }
24
29
  return true;
25
30
  }
26
31