@khaosoigai/pi-ds-pricing-regime 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 khaosoigai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # pi-ds-pricing-regime
2
+
3
+ A [pi](https://github.com/earendil-works/pi-coding-agent) extension that shows
4
+ whether DeepSeek is currently in **peak** or **off-peak** billing mode, in the
5
+ bottom-left of the footer. It reads the machine's clock, so all boundary times
6
+ display in the machine's local timezone — nothing is hardcoded.
7
+
8
+ ## What it shows
9
+
10
+ | State | Footer text (bottom-left) |
11
+ | --- | --- |
12
+ | Peak hours | `⚡ DeepSeek PEAK — until 12:00 local` (amber) |
13
+ | Off-peak | `🌙 DeepSeek off-peak — next peak 09:00 local` (green) |
14
+ | Weekend (Beijing time) | `🌙 DeepSeek off-peak (weekend flat rate) — next peak Monday 09:00 local` (green) |
15
+
16
+ When the next boundary is not today (e.g. on a weekend morning), the label
17
+ includes the boundary's local weekday so `09:00` is not mistaken for today's
18
+ clock time.
19
+
20
+ The status refreshes every 30s, so it flips exactly at hour boundaries.
21
+
22
+ The indicator is shown **only while the selected model comes from the DeepSeek
23
+ provider** (`ctx.model.provider === "deepseek"`) — the regime is DeepSeek API
24
+ pricing, so it is meaningless for other providers. Switching models with
25
+ `/model` or `Ctrl+P` clears or restores the status immediately. The provider id
26
+ is a constant (`DEEPSEEK_PROVIDER`) at the top of the extension.
27
+
28
+ ## Pairing with a configurable footer
29
+
30
+ The extension publishes plain pi statuses (`ctx.ui.setStatus`), so it works
31
+ with the **built-in footer** out of the box — no companion extension required.
32
+ It publishes two keys, sorted alphabetically there:
33
+
34
+ | Status key | Shows |
35
+ | --- | --- |
36
+ | `zz-deepseek-regime` | `⚡ DeepSeek PEAK — until 12:00 local` (see table above) |
37
+ | `zz-deepseek-balance` | `💰 ¥12.34` — DeepSeek platform credit balance, warning-coloured under the `LOW_BALANCE` thresholds |
38
+
39
+ For a configurable multi-line footer we recommend
40
+ [pi-footer](https://github.com/wobondar/pi-footer) (`pi install
41
+ npm:pi-footer`). It renders any published status through its `external-status`
42
+ widget, so this extension needs no changes — add widgets keyed to the status
43
+ keys above on their own line, e.g. `Tavily: 73%` (from a usage tracker) →
44
+ regime → balance, ordered exactly as configured and hidden when empty. Hide
45
+ the same keys from pi-footer's automatic extension-status row
46
+ (`extensionStatusRow.hiddenKeys` in `~/.pi/agent/extensions/pi-footer.json`)
47
+ to avoid showing them twice. Removing pi-footer (`pi remove npm:pi-footer`)
48
+ restores the built-in footer with these statuses intact.
49
+
50
+ ## The regime
51
+
52
+ Source: [DeepSeek API pricing](https://api-docs.deepseek.com/quick_start/pricing)
53
+
54
+ - **Peak hours (UTC):** `01:00–04:00` and `06:00–10:00`; all other hours are
55
+ off-peak at half the peak rates.
56
+ - **Live since:** `2026-08-16T16:00:00Z` (before that, billing was flat).
57
+ - **Weekends:** since `2026-08-22T16:00:00Z` (= 2026-08-23 00:00 Beijing), Saturdays and
58
+ Sundays in Beijing calendar time have no peak tiers — every call bills at the uniform
59
+ off-peak rate. The next peak after a live weekend is Monday 01:00 UTC; late UTC Sunday
60
+ evenings are already Monday in Beijing, so weekday rules resume there.
61
+
62
+ All are constants at the top of
63
+ [`extensions/deepseek-peak-offpeak.ts`](extensions/deepseek-peak-offpeak.ts)
64
+ (`PEAK_WINDOWS`, `WEEKEND_OFFPEAK_UTC`, `DEEPSEEK_PROVIDER`) — edit them if
65
+ DeepSeek changes the regime.
66
+
67
+ ## Install
68
+
69
+ Install as a pi package from npm:
70
+
71
+ ```sh
72
+ pi install npm:@khaosoigai/pi-ds-pricing-regime
73
+ ```
74
+
75
+ The package is structured as a pi package: `package.json` declares a
76
+ [`pi` manifest](https://pi.dev/docs/packages) loading `./extensions`, and the
77
+ package is listed with the `pi-package` keyword for the [package gallery](https://pi.dev/packages).
78
+
79
+ ### From git (alternative)
80
+
81
+ The source repo can also be installed directly over SSH (works because the
82
+ repo is private and SSH keys are configured):
83
+
84
+ ```sh
85
+ pi install git:git@github.com:khaosoi/pi-ds-pricing-regime
86
+ ```
87
+
88
+ Pi clones it to `~/.pi/agent/git/github.com/khaosoi/pi-ds-pricing-regime/` and
89
+ runs `npm install` there; restart pi (or `/reload`) to load it. The settings
90
+ entry has no pinned ref, so `pi update --extensions` pulls latest `main`.
91
+ Pin a ref with `pi install git:git@github.com:khaosoi/pi-ds-pricing-regime@<ref>`.
92
+
93
+ > Alternative: copy `extensions/deepseek-peak-offpeak.ts` into `.pi/extensions/`
94
+ > of a project for a project-local install.
95
+
96
+ ## Development
97
+
98
+ ```sh
99
+ just format # format TypeScript, JavaScript, JSON, and config files
100
+ just format-check # verify formatting without changing files
101
+ just lint # run Biome lint rules
102
+ just check # format-check + lint
103
+ just build # check + typecheck + tests — the release gate
104
+ just typecheck # tsc against pi's types (resolved via the peer dep in node_modules)
105
+ just test # node:test — unit, smoke, and timezone matrix tests
106
+ just coverage # run tests with Node's built-in coverage report (100% currently)
107
+ just pack # npm pack --dry-run: inspect the future tarball
108
+ ```
109
+
110
+ Biome formats and lints TypeScript, JavaScript, JSON, and configuration files.
111
+ The Markdown documentation and `justfile` are intentionally outside these
112
+ recipes. Or use the equivalent npm scripts directly (`npm run format`,
113
+ `npm run format:check`, `npm run lint`, `npm run check`, `npm test`, and
114
+ `npm run coverage`).
115
+
116
+ No test framework is needed: Node ≥ 23 runs the TypeScript sources directly
117
+ (type stripping), and tests use the built-in `node:test` runner. Tests never
118
+ modify the operating system clock; clock-dependent tests use only Node's
119
+ process-local virtual timers.
120
+
121
+ - `test/regime.test.ts` — window boundaries, next-boundary math, weekend
122
+ flat-rate rule.
123
+ - `test/smoke.test.ts` — loads the real extension with a mock pi context and
124
+ mocked timers: peak / off-peak / weekend status text, and timer lifecycle.
125
+ - `test/timezone-matrix.test.ts` — runs `scripts/timezone-matrix.mjs` under
126
+ fixed-offset zones (UTC+8, UTC+10, UTC-5) and real DST zones
127
+ (America/New_York, Australia/Sydney), asserting exact local-time labels
128
+ including the midnight wrap to the next day's peak and regime boundaries
129
+ that land on DST transition instants.
130
+
131
+ CI (`.github/workflows/ci.yml`) runs formatting/lint checks, typecheck, and
132
+ 41 tests on Node 24 for every push/PR.
133
+
134
+ ## Publishing
135
+
136
+ The package is published to npm as `@khaosoigai/pi-ds-pricing-regime`. The
137
+ metadata is already in place:
138
+
139
+ - scoped name `@khaosoigai/pi-ds-pricing-regime` (name reserved to your npm account)
140
+ - MIT license (`LICENSE` + `license` field)
141
+ - `repository`/`bugs`/`homepage` point at `github.com/khaosoi/pi-ds-pricing-regime`
142
+ — adjust if the repo gets a different name
143
+ - `publishConfig.access: "public"` so the scoped package publishes publicly
144
+ - `prepublishOnly` gate: runs typecheck + tests before every publish
145
+
146
+ To release a new version:
147
+
148
+ 1. `npm run preview:package` — confirm the tarball contains only `extensions/`,
149
+ `LICENSE`, `README.md`
150
+ 2. Bump `version` in `package.json`
151
+ 3. Merge to `main` and tag the release
152
+ 4. `npm login` (account `khaosoigai`) and `npm publish` — the `prepublishOnly`
153
+ gate re-runs check, typecheck, and tests as the final safety net
154
+
155
+ ## Notes
156
+
157
+ - Labels show the *next regime boundary* in local time, not the full schedule,
158
+ to keep the footer compact (e.g. `until 12:00 local` rather than
159
+ `09:00–12:00, 14:00–18:00`).
160
+ - Boundary labels are rendered from the boundary's *actual instant*
161
+ (`nextBoundaryUtc` → `formatLocalTime`), so they stay correct even when a
162
+ DST transition falls between now and the boundary (covered by the DST
163
+ scenarios in the timezone matrix).
164
+ - A boundary on a later local date is prefixed with its local weekday name
165
+ (`formatLocalDayPrefix`), so Saturday morning reads `next peak Monday 09:00
166
+ local` rather than a bare `09:00` that looks like today.
@@ -0,0 +1,294 @@
1
+ /**
2
+ * DeepSeek Peak / Off-Peak Footer Indicator
3
+ *
4
+ * Shows in the footer (bottom-left) whether DeepSeek is currently in
5
+ * peak or off-peak billing, computed from the machine's local clock.
6
+ * All boundary times in the status are shown in the machine's local
7
+ * timezone — no timezone is hardcoded.
8
+ *
9
+ * A second status shows the DeepSeek platform credit balance (fetched from
10
+ * api.deepseek.com/user/balance with the provider's resolved API key, cached
11
+ * and throttled to BALANCE_REFRESH_MS). It is likewise only shown while a
12
+ * DeepSeek model is selected; switching models clears or restores both.
13
+ *
14
+ * The status is only displayed while the selected model comes from the
15
+ * DeepSeek provider (`ctx.model.provider === "deepseek"`); switching models
16
+ * clears or restores it immediately.
17
+ *
18
+ * DeepSeek's regime (per api-docs.deepseek.com/quick_start/pricing):
19
+ * Peak hours (UTC): 01:00–04:00 and 06:00–10:00
20
+ * All other hours are off-peak (half the peak rates).
21
+ * Peak/off-peak billing has been live since 2026-08-16T16:00:00Z.
22
+ *
23
+ * Since 2026-08-22T16:00:00Z (= 2026-08-23 00:00 Beijing), Saturdays and
24
+ * Sundays (Beijing calendar time) have no peak tiers at all: every call
25
+ * is billed at the uniform off-peak rate.
26
+ */
27
+
28
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
29
+
30
+ /** Peak windows as [start, end) UTC hours. Edit here if DeepSeek changes the regime. */
31
+ export const PEAK_WINDOWS: ReadonlyArray<readonly [number, number]> = [
32
+ [1, 4], // 01:00–04:00 UTC
33
+ [6, 10], // 06:00–10:00 UTC
34
+ ];
35
+
36
+ /**
37
+ * Weekend flat-rate rule: from 2026-08-22T16:00:00Z (2026-08-23 00:00
38
+ * Beijing), Saturdays and Sundays in Beijing time are off-peak all day.
39
+ */
40
+ export const WEEKEND_OFFPEAK_UTC = Date.UTC(2026, 7, 22, 16, 0, 0);
41
+
42
+ /** Provider id whose models the billing indicator is shown for. Edit here if the provider id changes. */
43
+ export const DEEPSEEK_PROVIDER = "deepseek";
44
+
45
+ /** China observes a fixed UTC+8 offset year-round (no DST). */
46
+ const BEIJING_OFFSET_MS = 8 * 3_600_000;
47
+ const DAY_MS = 86_400_000;
48
+
49
+ // Pi's footer sorts extension statuses alphabetically by key, left to right.
50
+ // "zz-" prefix keeps these statuses right of others (e.g. "tavily-usage" stays
51
+ // on the left margin). The keys are never displayed — only the status text.
52
+ export const STATUS_KEY = "zz-deepseek-regime";
53
+ export const BALANCE_KEY = "zz-deepseek-balance";
54
+ const REFRESH_MS = 30_000; // refresh a few times per minute so the local-time label stays current
55
+
56
+ /** DeepSeek platform origin for the balance API (api-docs.deepseek.com/api-create-user-balance). */
57
+ export const BALANCE_ORIGIN = "https://api.deepseek.com";
58
+ /** Minimum interval between balance API calls. The balance only changes when credits are granted or topped up, so a few minutes is ample. */
59
+ export const BALANCE_REFRESH_MS = 5 * 60_000;
60
+ /** Balance colour thresholds in native currency units; currencies not listed fall back to FALLBACK_LOW_BALANCE. */
61
+ export const LOW_BALANCE: Readonly<Record<string, number>> = { CNY: 20, USD: 5 };
62
+ export const FALLBACK_LOW_BALANCE = 10;
63
+ /** Currency code → symbol; currencies not listed render as "12.34 EUR". */
64
+ const CURRENCY_SYMBOLS: Readonly<Record<string, string>> = { CNY: "¥", USD: "$" };
65
+
66
+ /** True when `now` falls on a Saturday or Sunday in Beijing calendar time. */
67
+ export function isWeekendBeijing(now: Date): boolean {
68
+ const day = new Date(now.getTime() + BEIJING_OFFSET_MS).getUTCDay();
69
+ return day === 0 || day === 6;
70
+ }
71
+
72
+ /**
73
+ * True when `now` (UTC) falls inside a peak window.
74
+ * Once the weekend rule is live (WEEKEND_OFFPEAK_UTC), Saturdays and Sundays
75
+ * in Beijing time never peak. Before that instant the legacy tiered schedule
76
+ * applied every day of the week.
77
+ */
78
+ export function inPeak(now: Date): boolean {
79
+ if (now.getTime() >= WEEKEND_OFFPEAK_UTC && isWeekendBeijing(now)) return false;
80
+ const hour = now.getUTCHours();
81
+ return PEAK_WINDOWS.some(([start, end]) => hour >= start && hour < end);
82
+ }
83
+
84
+ /**
85
+ * The next regime boundary after `now`, as a UTC hour (0–23), assuming the
86
+ * weekday tiered schedule (used for the peak → end-of-window computation).
87
+ * - If currently peak: the end of the current peak window.
88
+ * - If currently off-peak: the start of the next peak window (wrapping to tomorrow).
89
+ */
90
+ export function nextBoundaryUtcHour(now: Date): number {
91
+ const minute = now.getUTCHours() * 60 + now.getUTCMinutes();
92
+ const boundaries = inPeak(now)
93
+ ? PEAK_WINDOWS.map(([, end]) => end * 60)
94
+ : PEAK_WINDOWS.map(([start]) => start * 60);
95
+ const next = boundaries.find((b) => b > minute);
96
+ return Math.floor((next ?? boundaries[0]) / 60);
97
+ }
98
+
99
+ /** "HH:MM" wall-clock for an instant, in the machine's local timezone. */
100
+ export function formatLocalTime(at: Date): string {
101
+ const hh = String(at.getHours()).padStart(2, "0");
102
+ const mm = String(at.getMinutes()).padStart(2, "0");
103
+ return `${hh}:${mm}`;
104
+ }
105
+
106
+ const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] as const;
107
+
108
+ /**
109
+ * "" when `at` falls on the same local calendar date as `now`; otherwise the
110
+ * English weekday name of `at` followed by a space (e.g. "Monday "). Used so a
111
+ * boundary that is not today is not mistaken for today's clock time — e.g. on
112
+ * Saturday morning the next peak reads "Monday 09:00 local", not "09:00 local".
113
+ */
114
+ export function formatLocalDayPrefix(now: Date, at: Date): string {
115
+ const sameLocalDay =
116
+ now.getFullYear() === at.getFullYear() && now.getMonth() === at.getMonth() && now.getDate() === at.getDate();
117
+ return sameLocalDay ? "" : `${DAY_NAMES[at.getDay()]} `;
118
+ }
119
+
120
+ /**
121
+ * The next regime boundary after `now`, as an absolute UTC instant.
122
+ * Peak → end of the current window; off-peak → start of the next window,
123
+ * scanning forward over UTC days (up to `maxScanDays`) while skipping
124
+ * candidate instants that fall on a live Beijing weekend (the next peak after
125
+ * Friday is Monday 01:00 UTC). Uses `now`'s own date, so the local-time label
126
+ * stays correct even when a DST transition falls between `now` and the
127
+ * boundary.
128
+ */
129
+ export function nextBoundaryUtc(now: Date, maxScanDays = 9): Date {
130
+ const dayStart = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
131
+ if (inPeak(now)) {
132
+ return new Date(dayStart + nextBoundaryUtcHour(now) * 3_600_000);
133
+ }
134
+ for (let d = 0; d < maxScanDays; d++) {
135
+ const utcDayStart = dayStart + d * DAY_MS;
136
+ for (const [start] of PEAK_WINDOWS) {
137
+ const at = utcDayStart + start * 3_600_000;
138
+ if (at <= now.getTime()) continue;
139
+ if (at >= WEEKEND_OFFPEAK_UTC && isWeekendBeijing(new Date(at))) continue;
140
+ return new Date(at);
141
+ }
142
+ }
143
+ throw new Error("no peak window found within the next 9 days");
144
+ }
145
+
146
+ /**
147
+ * True when the given model (e.g. `ctx.model`) is served by the DeepSeek
148
+ * provider, i.e. when the peak/off-peak regime actually applies to it.
149
+ */
150
+ export function isDeepSeekModel(model: { provider?: string } | undefined | null): boolean {
151
+ return model?.provider === DEEPSEEK_PROVIDER;
152
+ }
153
+
154
+ /**
155
+ * Parse a DeepSeek `/user/balance` response body into the first balance entry.
156
+ * Returns undefined when the shape is not as documented (proxy response, API
157
+ * change, error JSON) so callers can keep showing the previous value.
158
+ */
159
+ export function parseBalance(body: unknown): { total: number; currency: string; isAvailable: boolean } | undefined {
160
+ if (typeof body !== "object" || body === null) return undefined;
161
+ const record = body as Record<string, unknown>;
162
+ const infos = Array.isArray(record.balance_infos) ? record.balance_infos : [];
163
+ const info = infos[0];
164
+ if (typeof info !== "object" || info === null) return undefined;
165
+ const entry = info as Record<string, unknown>;
166
+ const total = Number(entry.total_balance);
167
+ if (!Number.isFinite(total)) return undefined;
168
+ return {
169
+ total,
170
+ currency: typeof entry.currency === "string" ? entry.currency : "",
171
+ isAvailable: record.is_available !== false,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * The rendered balance status: "💰 ¥12.34", low-balances in the theme's
177
+ * warning colour. Precision: integers lose the decimals ("¥12"), anything
178
+ * else keeps two. Unknown currencies render as "12.34 EUR".
179
+ */
180
+ export function formatBalance(balance: { total: number; currency: string }): {
181
+ text: string;
182
+ low: boolean;
183
+ } {
184
+ const { total, currency } = balance;
185
+ const amount = Number.isInteger(total) ? String(total) : total.toFixed(2);
186
+ const symbol = CURRENCY_SYMBOLS[currency];
187
+ const text = symbol ? `💰 ${symbol}${amount}` : `💰 ${amount}${currency ? ` ${currency}` : ""}`;
188
+ const threshold = LOW_BALANCE[currency] ?? FALLBACK_LOW_BALANCE;
189
+ return { text, low: total < threshold };
190
+ }
191
+
192
+ /** The status text for `now`, or undefined to clear the status. */
193
+ export function statusText(now: Date): { text: string; color: "warning" | "success" } {
194
+ const boundary = nextBoundaryUtc(now);
195
+ const label = `${formatLocalDayPrefix(now, boundary)}${formatLocalTime(boundary)}`;
196
+ if (inPeak(now)) {
197
+ return { color: "warning", text: `⚡ DeepSeek PEAK — until ${label} local` };
198
+ }
199
+ if (now.getTime() >= WEEKEND_OFFPEAK_UTC && isWeekendBeijing(now)) {
200
+ return {
201
+ color: "success",
202
+ text: `🌙 DeepSeek off-peak (weekend flat rate) — next peak ${label} local`,
203
+ };
204
+ }
205
+ return {
206
+ color: "success",
207
+ text: `🌙 DeepSeek off-peak — next peak ${label} local`,
208
+ };
209
+ }
210
+
211
+ export default function (pi: ExtensionAPI) {
212
+ let timer: ReturnType<typeof setInterval> | undefined;
213
+ let balance: { text: string; low: boolean } | undefined;
214
+ let lastFetched = 0;
215
+ let inflight: Promise<void> | undefined;
216
+
217
+ const applyBalance = (ctx: ExtensionContext) => {
218
+ if (balance) {
219
+ ctx.ui.setStatus(BALANCE_KEY, ctx.ui.theme.fg(balance.low ? "warning" : "success", balance.text));
220
+ } else {
221
+ ctx.ui.setStatus(BALANCE_KEY, undefined);
222
+ }
223
+ };
224
+
225
+ /**
226
+ * Fetch the credit balance once and cache it. Fire-and-forget: failures
227
+ * (no stored key, network errors, non-2xx, unexpected body) leave the
228
+ * previous value untouched, and lastFetched was advanced at call time so
229
+ * a broken endpoint is retried no more than once per BALANCE_REFRESH_MS.
230
+ */
231
+ const fetchBalance = (ctx: ExtensionContext) => {
232
+ if (inflight) return;
233
+ lastFetched = Date.now();
234
+ inflight = (async () => {
235
+ try {
236
+ const auth = await ctx.modelRegistry?.getProviderAuth?.(DEEPSEEK_PROVIDER);
237
+ const apiKey = auth?.auth?.apiKey;
238
+ if (!apiKey) return;
239
+ // A custom baseUrl (proxy) is respected for the balance endpoint too;
240
+ // proxies that don't implement /user/balance just fail and keep the cache.
241
+ const origin = auth.auth.baseUrl ? new URL(auth.auth.baseUrl).origin : BALANCE_ORIGIN;
242
+ const res = await fetch(`${origin}/user/balance`, {
243
+ headers: { Authorization: `Bearer ${apiKey}` },
244
+ });
245
+ if (!res.ok) return;
246
+ const parsed = parseBalance(await res.json());
247
+ if (parsed) {
248
+ balance = formatBalance(parsed);
249
+ if (!parsed.isAvailable) balance.low = true; // credits exist but can't pay — flag it
250
+ applyBalance(ctx);
251
+ }
252
+ } catch {
253
+ // keep showing the cached value
254
+ } finally {
255
+ inflight = undefined;
256
+ }
257
+ })();
258
+ };
259
+
260
+ const update = async (ctx: ExtensionContext, model: ExtensionContext["model"]) => {
261
+ if (!isDeepSeekModel(model)) {
262
+ ctx.ui.setStatus(STATUS_KEY, undefined);
263
+ ctx.ui.setStatus(BALANCE_KEY, undefined);
264
+ return;
265
+ }
266
+ const { text, color } = statusText(new Date());
267
+ ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg(color, text));
268
+ applyBalance(ctx);
269
+ if (Date.now() - lastFetched >= BALANCE_REFRESH_MS) fetchBalance(ctx);
270
+ };
271
+
272
+ pi.on("session_start", async (_event, ctx) => {
273
+ if (timer) clearInterval(timer);
274
+ await update(ctx, ctx.model);
275
+ timer = setInterval(() => void update(ctx, ctx.model), REFRESH_MS);
276
+ });
277
+
278
+ // Balance can drop mid-session (usage debits, new grants) — re-check on the
279
+ // throttled schedule after each turn as well.
280
+ pi.on("turn_end", async (_event, ctx) => {
281
+ await update(ctx, ctx.model);
282
+ });
283
+
284
+ pi.on("model_select", async (event, ctx) => {
285
+ await update(ctx, ctx.model ?? event.model);
286
+ });
287
+
288
+ pi.on("session_shutdown", async () => {
289
+ if (timer) {
290
+ clearInterval(timer);
291
+ timer = undefined;
292
+ }
293
+ });
294
+ }
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@khaosoigai/pi-ds-pricing-regime",
3
+ "version": "0.1.0",
4
+ "description": "Pi extension: shows DeepSeek peak/off-peak billing mode in the footer, timezone-aware",
5
+ "keywords": [
6
+ "pi",
7
+ "pi-package",
8
+ "pi-extension",
9
+ "deepseek",
10
+ "peak",
11
+ "off-peak",
12
+ "pricing"
13
+ ],
14
+ "license": "MIT",
15
+ "pi": {
16
+ "extensions": [
17
+ "./extensions"
18
+ ]
19
+ },
20
+ "author": {
21
+ "name": "khaosoigai",
22
+ "url": "https://github.com/khaosoi"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/khaosoi/pi-ds-pricing-regime.git"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/khaosoi/pi-ds-pricing-regime/issues"
30
+ },
31
+ "homepage": "https://github.com/khaosoi/pi-ds-pricing-regime#readme",
32
+ "type": "module",
33
+ "files": [
34
+ "extensions",
35
+ "LICENSE",
36
+ "README.md"
37
+ ],
38
+ "engines": {
39
+ "node": ">=23"
40
+ },
41
+ "scripts": {
42
+ "format": "biome format extensions test scripts package.json tsconfig.json biome.json --write",
43
+ "format:check": "biome format extensions test scripts package.json tsconfig.json biome.json",
44
+ "lint": "biome lint extensions test scripts package.json tsconfig.json biome.json",
45
+ "check": "npm run format:check && npm run lint",
46
+ "typecheck": "tsc -p tsconfig.json",
47
+ "test": "node --test",
48
+ "coverage": "node --test --experimental-test-coverage",
49
+ "preview:package": "npm pack --dry-run",
50
+ "prepublishOnly": "npm run check && npm run typecheck && npm test"
51
+ },
52
+ "peerDependencies": {
53
+ "@earendil-works/pi-coding-agent": "*"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "devDependencies": {
59
+ "@biomejs/biome": "2.5.8",
60
+ "@types/node": "^22.10.0",
61
+ "typescript": "^5.7.0"
62
+ }
63
+ }