pi-deepseek-tariff 0.1.0 → 0.3.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
@@ -3,12 +3,15 @@
3
3
  [![npm](https://img.shields.io/npm/v/pi-deepseek-tariff?color=blue)](https://www.npmjs.com/package/pi-deepseek-tariff)
4
4
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
5
5
  [![pi-package](https://img.shields.io/badge/pi-package-8a2be2)](https://pi.dev/packages)
6
+ [![changelog](https://img.shields.io/badge/changelog-Keep%20a%20Changelog-orange)](CHANGELOG.md)
6
7
 
7
8
  A [pi](https://pi.dev) extension that shows the current **DeepSeek API tariff window** — peak or off-peak — with a countdown, and a panel with live prices parsed from the official DeepSeek pricing page.
8
9
 
9
10
  DeepSeek charges double during peak hours, so knowing the current window is worth real money.
10
11
 
11
12
  ```
13
+ ● DeepSeek Off-peak for 11h 59m ← footer line, always visible
14
+
12
15
  Now OFF-PEAK
13
16
  Computer: 14:43:52 26-10-2026(MSK) | UTC: 2026-10-26 11:43
14
17
  Next window: 04:00 - 07:00 27-10-2026(MSK) in 13h 16m
@@ -25,18 +28,18 @@ Peak windows (MSK): 04:00-07:00 and 09:00-13:00, Mon-Fri
25
28
  Off-peak = night, weekends and Chinese holidays
26
29
  Price source: api-docs.deepseek.com, updated 11:20 26-10 (next in 5h 59m)
27
30
  ────────────────────────────────────────────────────────────
28
- Commands: /deepseek-peak on|off|refresh|en|ru
31
+ Commands: /dsp on|off|refresh|en|ru · /dsp footer on|off
29
32
  ```
30
33
 
31
34
  ## Install
32
35
 
33
36
  ```bash
34
- # from git (tags are pinned)
35
- pi install git:github.com/valderan/pi-deepseek-tariff@v0.1.0
36
-
37
- # from npm
37
+ # from npm (always the `latest` dist-tag)
38
38
  pi install npm:pi-deepseek-tariff
39
39
 
40
+ # from git: tags mirror the npm versions, so pin whichever you want
41
+ pi install git:github.com/valderan/pi-deepseek-tariff@v0.3.0
42
+
40
43
  # local checkout, for development
41
44
  git clone git@github.com:valderan/pi-deepseek-tariff.git
42
45
  pi install ./pi-deepseek-tariff
@@ -45,20 +48,27 @@ pi install ./pi-deepseek-tariff
45
48
  pi -e ./pi-deepseek-tariff
46
49
  ```
47
50
 
48
- Then run `/deepseek-peak`. Afterwards `pi list`, `pi update --extensions`, `pi remove …` manage it like any other package.
51
+ Then run `/dsp`. Afterwards `pi list`, `pi update --extensions`, `pi remove …` manage it like any other package.
49
52
 
50
53
  ## Commands
51
54
 
55
+ `/dsp` is the short form of the command. The full name `/deepseek-peak` works too, and `/deepseek-peek` is accepted for muscle memory.
56
+
52
57
  | Command | Effect |
53
58
  |---|---|
54
- | `/deepseek-peak` | toggle the panel |
55
- | `/deepseek-peak on` / `off` | show or hide it |
56
- | `/deepseek-peak refresh` | re-parse prices from the docs page now |
57
- | `/deepseek-peak en` / `ru` | switch the panel language (persisted) |
59
+ | `/dsp` | toggle the panel |
60
+ | `/dsp on` / `off` | show or hide the panel (the footer line stays while a DeepSeek model is active) |
61
+ | `/dsp refresh` | re-parse prices from the docs page now |
62
+ | `/dsp en` / `ru` | switch the panel language (persisted) |
63
+ | `/dsp footer on` / `off` (or `/dsp footeron` / `footeroff`) | show or hide the footer line (persisted) |
58
64
 
59
65
  ## What it does
60
66
 
61
- - **Footer line** — `deepseek: PEAK → off-peak 13:00 (1h 40m)`. Shown only while a DeepSeek model is active; red for peak, green for off-peak.
67
+ - **Footer line** — one compact status line: `● DeepSeek Off-peak for 11h 59m`, or `● DeepSeek Peak for 1h 40m` during peak. The text is always yellow and only the dot changes colour — green for off-peak, red for peak. Shown only while a DeepSeek model is active; `/dsp footer off` hides it for good.
68
+
69
+ Russian looks like `● DeepSeek Обычный ещё 11ч 59м` / `● DeepSeek Пиковый ещё 1ч 40м`.
70
+
71
+ Pi renders every extension status on a single footer line, joined alphabetically, so the segment is kept short.
62
72
  - **Peak rules** — DeepSeek defines peak as `01:00-04:00` and `06:00-10:00` **UTC, Monday to Friday**; weekends and Chinese public holidays are off-peak all day.
63
73
  - **Local time everywhere** — the UTC clock comes from the machine clock, so peak windows, weekday labels and countdowns are rendered in *your* zone (`(MSK)`, `(UTC-04:00)`, …). Weekday labels shift with the zone: at UTC−4 the windows land on Sun–Thu.
64
74
  - **Prices** — parsed from `api-docs.deepseek.com/quick_start/pricing/`: cache hit, cache miss and output per model, plus the Search & Fetch request rates. The tariff in effect is highlighted.
@@ -74,6 +84,7 @@ Then run `/deepseek-peak`. Afterwards `pi list`, `pi update --extensions`, `pi r
74
84
  "intervalMs": 30000,
75
85
  "pricingTtlHours": 6,
76
86
  "alwaysShow": false,
87
+ "footer": true,
77
88
  "models": ["deepseek"],
78
89
  "providerPrefix": "deepseek:",
79
90
  "holidays": ["2026-10-01", "2026-10-02"]
@@ -86,6 +97,7 @@ Then run `/deepseek-peak`. Afterwards `pi list`, `pi update --extensions`, `pi r
86
97
  | `intervalMs` | `30000` | minimum refresh cadence (clamped to ≥ 5000) |
87
98
  | `pricingTtlHours` | `6` | how long parsed prices stay valid |
88
99
  | `alwaysShow` | `false` | show the footer line for non-DeepSeek models too |
100
+ | `footer` | `true` | show the footer status line at all; `/dsp footer off` writes this |
89
101
  | `models` | `["deepseek"]` | id substrings that count as DeepSeek |
90
102
  | `providerPrefix` | `"deepseek:"` | prefix used when printing model ids |
91
103
  | `holidays` | `[]` | Chinese public holidays as Beijing dates (`YYYY-MM-DD`), off-peak all day |
@@ -117,12 +129,41 @@ The rules and parsing modules are pure and take injectable inputs, so tests neve
117
129
 
118
130
  ## Release
119
131
 
132
+ Git tags and npm versions are kept in lockstep: the tag `vX.Y.Z` always points at
133
+ the commit whose `package.json` says `X.Y.Z`, and npm publishes that same version.
134
+
120
135
  ```bash
121
- git tag -a v0.1.0 -m "v0.1.0"
136
+ # 1. bump package.json, add a CHANGELOG entry, commit
137
+ npm version 0.3.2 --no-git-tag-version # or edit by hand
138
+ git commit -am "chore: release 0.3.2"
139
+
140
+ # 2. tag the release commit and push
141
+ git tag -a v0.3.2 -m v0.3.2
122
142
  git push origin main --tags
143
+
144
+ # 3. publish the tag to npm (2FA prompt, or --otp=123456)
145
+ npm publish --access public
146
+ ```
147
+
148
+ `npm publish` sets the `latest` dist-tag automatically for a plain semver version.
149
+ Check the result:
150
+
151
+ ```bash
152
+ npm view pi-deepseek-tariff dist-tags # { latest: '0.3.2' }
153
+ npm view pi-deepseek-tariff versions
123
154
  ```
124
155
 
125
- Tags are pinnable with `pi install git:…@v0.1.0`. Publishing to npm (`.github/workflows/ci.yml` runs it on a `v*` tag) puts the package into the [pi package gallery](https://pi.dev/packages).
156
+ `.github/workflows/ci.yml` can publish on a `v*` tag instead of step 3 when the
157
+ `NPM_TOKEN` secret is configured; with trusted publishing enabled on npmjs.com the
158
+ token is not needed at all and the build is signed with provenance.
159
+
160
+ Tags are pinnable with `pi install git:…@v0.3.2`. Published versions show up in the
161
+ [pi package gallery](https://pi.dev/packages) through the `pi-package` keyword.
162
+
163
+ ## Changelog
164
+
165
+ See [CHANGELOG.md](CHANGELOG.md). Git tags and npm versions are kept in lockstep, so
166
+ `vX.Y.Z` always matches the published `X.Y.Z`.
126
167
 
127
168
  ## License
128
169
 
@@ -40,7 +40,7 @@ export default function deepseekPeak(pi: ExtensionAPI) {
40
40
  };
41
41
 
42
42
  const renderFooter = (ctx: ExtensionContext | ExtensionCommandContext, current: Pricing = pricing.get()) => {
43
- if (!config.alwaysShow && !isDeepSeekModel(ctx)) {
43
+ if (!config.footer || (!config.alwaysShow && !isDeepSeekModel(ctx))) {
44
44
  ctx.ui.setStatus(STATUS_KEY, undefined);
45
45
  return;
46
46
  }
@@ -117,41 +117,61 @@ export default function deepseekPeak(pi: ExtensionAPI) {
117
117
  pi.on("agent_start", async (_event, ctx) => render(ctx));
118
118
  pi.on("turn_start", async (_event, ctx) => render(ctx));
119
119
 
120
- pi.registerCommand("deepseek-peak", {
121
- description: "DeepSeek peak/off-peak panel · Панель DeepSeek peak/off-peak",
122
- handler: async (args, ctx) => {
123
- config = loadConfig();
124
- lang = config.language ?? "en";
125
- pricing.setTtl(ttlMs());
126
- const arg = args.trim().toLowerCase();
127
-
128
- if (arg === "ru" || arg === "en") {
129
- lang = arg;
130
- config.language = arg;
131
- saveConfig({ language: arg });
132
- panelVisible = true;
133
- render(ctx);
134
- return;
135
- }
120
+ const commandHandler = async (args: string, ctx: ExtensionCommandContext) => {
121
+ config = loadConfig();
122
+ lang = config.language ?? "en";
123
+ pricing.setTtl(ttlMs());
124
+ const parts = args.trim().toLowerCase().split(/\s+/).filter(Boolean);
125
+ const arg = parts[0] ?? "";
126
+ const option = parts[1] ?? "";
127
+
128
+ // Footer line: /dsp footer on|off, or the one-word /dsp footeron|footeroff.
129
+ if (arg === "footer" || arg === "footeron" || arg === "footeroff") {
130
+ const next =
131
+ arg === "footeron" ? true : arg === "footeroff" ? false : option === "on" ? true : option === "off" ? false : !config.footer;
132
+ config.footer = next;
133
+ saveConfig({ footer: next });
134
+ render(ctx);
135
+ ctx.ui.notify(
136
+ next ? "DeepSeek footer line: on · Строка футера: вкл" : "DeepSeek footer line: off · Строка футера: выкл",
137
+ "info",
138
+ );
139
+ return;
140
+ }
136
141
 
137
- if (arg === "off" || (arg === "" && panelVisible)) {
138
- panelVisible = false;
139
- ctx.ui.setWidget(WIDGET_KEY, undefined);
140
- renderFooter(ctx);
141
- return;
142
- }
142
+ if (arg === "ru" || arg === "en") {
143
+ lang = arg;
144
+ config.language = arg;
145
+ saveConfig({ language: arg });
146
+ panelVisible = true;
147
+ render(ctx);
148
+ return;
149
+ }
143
150
 
144
- if (arg === "refresh") {
145
- const value = await pricing.refresh(true);
146
- panelVisible = true;
147
- render(ctx, value);
148
- return;
149
- }
151
+ if (arg === "off" || (arg === "" && panelVisible)) {
152
+ panelVisible = false;
153
+ ctx.ui.setWidget(WIDGET_KEY, undefined);
154
+ renderFooter(ctx);
155
+ return;
156
+ }
150
157
 
158
+ if (arg === "refresh") {
159
+ const value = await pricing.refresh(true);
151
160
  panelVisible = true;
152
- render(ctx);
153
- },
154
- });
161
+ render(ctx, value);
162
+ return;
163
+ }
164
+
165
+ panelVisible = true;
166
+ render(ctx);
167
+ };
168
+
169
+ const description = "DeepSeek peak/off-peak panel · Панель DeepSeek peak/off-peak";
170
+ // `/dsp` is the short form; `deepseek-peek` accepts the spelling people reach
171
+ // for when they type "peak" wrong, so the command still resolves.
172
+ for (const name of ["deepseek-peak", "dsp", "deepseek-peek"]) {
173
+ pi.registerCommand(name, { description, handler: commandHandler });
174
+ }
155
175
  }
156
176
 
157
177
  export { STRINGS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-deepseek-tariff",
3
- "version": "0.1.0",
3
+ "version": "0.3.1",
4
4
  "description": "DeepSeek peak/off-peak tariff panel for pi: local clock, next window, live per-model prices and Search & Fetch rates",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -30,7 +30,8 @@
30
30
  "pi": {
31
31
  "extensions": [
32
32
  "./extensions/deepseek-peak.ts"
33
- ]
33
+ ],
34
+ "image": "https://raw.githubusercontent.com/valderan/pi-deepseek-tariff/main/assets/banner.png"
34
35
  },
35
36
  "peerDependencies": {
36
37
  "@earendil-works/pi-coding-agent": "*",
package/src/config.ts CHANGED
@@ -21,6 +21,7 @@ export type Config = {
21
21
  intervalMs: number;
22
22
  pricingTtlHours: number;
23
23
  alwaysShow: boolean;
24
+ footer: boolean;
24
25
  models: string[];
25
26
  providerPrefix: string;
26
27
  holidays: Set<string>;
@@ -41,6 +42,7 @@ export function loadConfig(): Config {
41
42
  intervalMs: 30_000,
42
43
  pricingTtlHours: DEFAULT_TTL_HOURS,
43
44
  alwaysShow: false,
45
+ footer: true,
44
46
  models: ["deepseek"],
45
47
  providerPrefix: "deepseek:",
46
48
  holidays: new Set<string>(),
@@ -54,6 +56,7 @@ export function loadConfig(): Config {
54
56
  config.pricingTtlHours = raw.pricingTtlHours;
55
57
  }
56
58
  if (typeof raw.alwaysShow === "boolean") config.alwaysShow = raw.alwaysShow;
59
+ if (typeof raw.footer === "boolean") config.footer = raw.footer;
57
60
  if (typeof raw.providerPrefix === "string" && raw.providerPrefix.length > 0) {
58
61
  config.providerPrefix = raw.providerPrefix;
59
62
  }
package/src/i18n.ts CHANGED
@@ -34,6 +34,10 @@ type Strings = {
34
34
  sourceBuiltin: string;
35
35
  requestPricing: string;
36
36
  commands: string;
37
+ statusBrand: string;
38
+ statusPeak: string;
39
+ statusOffPeak: string;
40
+ statusFor: (duration: string) => string;
37
41
  locale: string;
38
42
  hourUnit: string;
39
43
  minuteUnit: string;
@@ -60,6 +64,10 @@ export const STRINGS: Record<Lang, Strings> = {
60
64
  sourceBuiltin: "built-in values (page unavailable)",
61
65
  requestPricing: "Search & Fetch:",
62
66
  commands: "Commands:",
67
+ statusBrand: "DeepSeek",
68
+ statusPeak: "Peak",
69
+ statusOffPeak: "Off-peak",
70
+ statusFor: (duration) => `for ${duration}`,
63
71
  locale: "en-GB",
64
72
  hourUnit: "h",
65
73
  minuteUnit: "m",
@@ -84,6 +92,10 @@ export const STRINGS: Record<Lang, Strings> = {
84
92
  sourceBuiltin: "встроенные значения (страница недоступна)",
85
93
  requestPricing: "Search & Fetch:",
86
94
  commands: "Команды:",
95
+ statusBrand: "DeepSeek",
96
+ statusPeak: "Пиковый",
97
+ statusOffPeak: "Обычный",
98
+ statusFor: (duration) => `ещё ${duration}`,
87
99
  locale: "ru-RU",
88
100
  hourUnit: "ч",
89
101
  minuteUnit: "м",
package/src/view.ts CHANGED
@@ -10,7 +10,6 @@ import type { Config } from "./config.ts";
10
10
  import { localizeUnit, STRINGS, type Lang } from "./i18n.ts";
11
11
  import type { Pricing, RequestPricing } from "./pricing.ts";
12
12
  import {
13
- clockTime,
14
13
  dayRangeLabel,
15
14
  humanize,
16
15
  isHoliday,
@@ -54,16 +53,21 @@ export function requestPricingText(entries: RequestPricing[], lang: Lang): strin
54
53
  return `${parts.join(" · ")}${sharedUnit ? ` ${localized(sharedUnit)}` : ""}`.trim();
55
54
  }
56
55
 
56
+ /**
57
+ * Compact footer line: a coloured dot plus always-yellow text, for example
58
+ * "● DeepSeek Off-peak for 11h 59m" or "● DeepSeek Peak for 1h 40m".
59
+ * Only the dot changes colour, so the line reads calmly in both states.
60
+ */
57
61
  export function statusText(theme: Theme, config: Config, lang: Lang, pricing: Pricing): string {
58
62
  void pricing;
63
+ const text = STRINGS[lang];
59
64
  const now = new Date();
60
65
  const peak = isPeak(now, config.holidays);
61
66
  const next = nextTransition(now, config.holidays);
62
- const label = peak ? "PEAK" : "off-peak";
63
- const color = theme.fg(peak ? "error" : "success", label);
64
- const hint = peak ? "→ off-peak" : "→ peak";
65
- const when = `${clockTime(next, lang)} (${humanize(next.getTime() - now.getTime(), lang)})`;
66
- return `${theme.fg("dim", "deepseek:")} ${color} ${theme.fg("dim", `${hint} ${when}`)}`;
67
+ const dot = theme.fg(peak ? "error" : "success", "●");
68
+ const label = peak ? text.statusPeak : text.statusOffPeak;
69
+ const duration = humanize(next.getTime() - now.getTime(), lang);
70
+ return `${dot} ${theme.fg("warning", `${text.statusBrand} ${label} ${text.statusFor(duration)}`)}`;
67
71
  }
68
72
 
69
73
  export function panelLines(
@@ -153,7 +157,7 @@ export function panelLines(
153
157
  lines.push(SEPARATOR);
154
158
 
155
159
  // 8. Command hints.
156
- lines.push(`${theme.fg("dim", text.commands)} ${white("/deepseek-peak on|off|refresh|en|ru")}`);
160
+ lines.push(`${theme.fg("dim", text.commands)} ${white("/dsp on|off|refresh|en|ru · /dsp footer on|off")}`);
157
161
 
158
162
  return lines;
159
163
  }