@nijaru/pi-peak-hours 0.0.7 → 0.0.8

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
@@ -8,7 +8,7 @@ The default covers DeepSeek direct only: pi records it at the published **peak**
8
8
 
9
9
  With a compatible [pi-provider-compaction](https://github.com/nijaru/pi-provider-compaction) companion loaded, this extension scales existing `usage.cost` before request completion reaches stream consumers or cache observers. It covers observed foreground requests, compaction, branch summaries, and cache warming for scheduled providers. Without the companion, it still corrects finalized foreground assistant messages through `message_end`; auxiliary usage stays unchanged. Both paths preserve token counts and existing cost corrections such as `/fast`.
10
10
 
11
- The status line quotes the selected model's scheduled catalog estimate — `· est. $0.30/$1.20/M` during a peak window, `· est. ▼ off-peak $0.15/$0.60/M` off it. `▲ peak` or `▼ off-peak` marks a rate above or below pi's recorded rate. With stock peak-base prices, peak has no indicator and off-peak is marked as a discount. `/peak-hours status` names the schedule.
11
+ The status line quotes the selected model's scheduled catalog estimate — `· $0.30/$1.20/M` during a peak window, `· ▼ off-peak $0.15/$0.60/M` off it. `▲ peak` or `▼ off-peak` marks a rate above or below pi's recorded rate. With stock peak-base prices, peak has no indicator and off-peak is marked as a discount. `/peak-hours status` names the schedule.
12
12
 
13
13
  The footer describes the current hour, not a particular request: a request invoked before a boundary uses its invocation timestamp even after the footer moves to the new hour. Request-specific corrections can also differ from the quoted catalog rate. Stale base prices remain stale; this extension applies a multiplier, not a price lookup. All costs are estimates, not invoices: server arrival times and Chinese public holidays are not modeled.
14
14
 
@@ -102,11 +102,11 @@ Note that a coding plan is billed in credits against a subscription, so pi's cos
102
102
 
103
103
  OpenRouter publishes the same windows machine-readably for models it hosts (`pricing.overrides` with `utc_days` / `utc_start` / `utc_end` on `https://openrouter.ai/api/v1/models`), but this extension reads its schedule from config rather than the network: the correction is a rate difference, not a price lookup, and a fetch would add a failure mode to a handler that must never throw.
104
104
 
105
- A malformed field falls back to its default rather than disabling the extension; a malformed file warns and resolves to defaults.
105
+ A malformed field falls back to its default rather than disabling the extension; a malformed file warns and resolves to defaults. `/peak-hours on|off` refuses to rewrite a malformed, unreadable, or non-object config and reports that the save failed, leaving the existing file untouched.
106
106
 
107
107
  ## Design notes
108
108
 
109
- - **Only the selected model drives the indicator.** Schedules are provider- and model-scoped, so the footer shows no rate for uncovered models. A rate above the recorded one reads `peak`, below it `off-peak`. The `est.` label applies even with request-wide coverage.
109
+ - **Only the selected model drives the indicator.** Schedules are provider- and model-scoped, so the footer shows no rate for uncovered models. A rate above the recorded one reads `peak`, below it `off-peak`.
110
110
  - **The footer follows the window it quotes.** pi has no render tick and fires no event while a session is idle, and a rate that changes at 04:00 would otherwise stay on screen until the next turn. The extension arms a single timer for the next instant the quoted rate changes and re-arms it after each fire, from the same clock reading as the status it is keeping fresh. It arms nothing when there is no status on screen, and the timer is unreferenced and cleared on session shutdown, so it neither holds pi open nor outlives its session.
111
111
  - **Rate by provider invocation.** Pi providers timestamp each assistant message before preparing and sending its HTTP request. That timestamp selects the multiplier, not completion time or the shared payload hook. With the companion, active state and schedules are captured at request dispatch; without it, foreground fallback uses the configuration at `message_end`.
112
112
  - **Scaled once.** Global weak identity guards protect usage and foreground message objects across loaded copies, retries, and returned replacements. Usage observed at dispatch remains handled even when its decision is unchanged (×1 or inactive), so fallback cannot overwrite it after a config change.
@@ -15,8 +15,8 @@
15
15
  *
16
16
  * The footer indicator is scoped to the selected model and quotes the rate that
17
17
  * applies to it right now, in dollars per million tokens: with the default
18
- * schedule, `· est. $0.30/$1.20/M` at peak and
19
- * `· est. ▼ off-peak $0.15/$0.60/M` off it. This is a scheduled catalog estimate;
18
+ * schedule, `· $0.30/$1.20/M` at peak and
19
+ * `· ▼ off-peak $0.15/$0.60/M` off it. This is a scheduled catalog estimate;
20
20
  * request-specific corrections such as /fast can change the actual usage cost.
21
21
  * Uncovered models show no rate; switching models re-evaluates it.
22
22
  *
@@ -415,11 +415,11 @@ export function statusIndicator(
415
415
 
416
416
  switch (rateStateAt(now, schedule)) {
417
417
  case "peak":
418
- return `· est. ${STATUS_PEAK} ${rate}`;
418
+ return `· ${STATUS_PEAK} ${rate}`;
419
419
  case "off-peak":
420
- return `· est. ${STATUS_OFF_PEAK} ${rate}`;
420
+ return `· ${STATUS_OFF_PEAK} ${rate}`;
421
421
  default:
422
- return `· est. ${rate}`;
422
+ return `· ${rate}`;
423
423
  }
424
424
  }
425
425
 
@@ -444,14 +444,16 @@ export function readConfig(path: string): ConfigFile {
444
444
  }
445
445
  }
446
446
 
447
- /** Persist `active` while preserving keys this version does not model. */
448
- export function writeActive(path: string, active: boolean): void {
447
+ /** Refuse to replace unreadable/malformed config; preserve unknown valid keys. */
448
+ export function writeActive(path: string, active: boolean): boolean {
449
449
  try {
450
+ const config: unknown = existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : {};
451
+ if (!isRecord(config)) return false;
450
452
  mkdirSync(dirname(path), { recursive: true });
451
- writeFileSync(path, `${JSON.stringify({ ...readConfig(path), active }, null, 2)}\n`, "utf8");
452
- } catch (error) {
453
- const message = error instanceof Error ? error.message : String(error);
454
- console.warn(`[${STATUS_KEY}] Failed to write ${path}: ${message}`);
453
+ writeFileSync(path, `${JSON.stringify({ ...config, active }, null, 2)}\n`, "utf8");
454
+ return true;
455
+ } catch {
456
+ return false;
455
457
  }
456
458
  }
457
459
 
@@ -686,7 +688,10 @@ export default function peakHours(pi: ExtensionAPI, options: PeakHoursOptions =
686
688
  ctx.ui.notify("Usage: /peak-hours [on|off|status]", "error");
687
689
  return;
688
690
  }
689
- writeActive(configPath, arg === "on");
691
+ if (!writeActive(configPath, arg === "on")) {
692
+ ctx.ui.notify(`Peak hours: failed to save configuration at ${configPath}; keeping the previous setting.`, "error");
693
+ return;
694
+ }
690
695
  refresh();
691
696
  await pricing.reconcile(ctx);
692
697
  if (!pricing.isCurrent(ctx)) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nijaru/pi-peak-hours",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "type": "module",
5
5
  "description": "Correct pi's recorded cost for providers that bill by time of day, with a peak multiplier in the footer status.",
6
6
  "keywords": [