pi-deepseek-tariff 0.1.0 → 0.3.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/README.md CHANGED
@@ -9,6 +9,8 @@ A [pi](https://pi.dev) extension that shows the current **DeepSeek API tariff wi
9
9
  DeepSeek charges double during peak hours, so knowing the current window is worth real money.
10
10
 
11
11
  ```
12
+ ● DeepSeek Off-peak for 11h 59m ← footer line, always visible
13
+
12
14
  Now OFF-PEAK
13
15
  Computer: 14:43:52 26-10-2026(MSK) | UTC: 2026-10-26 11:43
14
16
  Next window: 04:00 - 07:00 27-10-2026(MSK) in 13h 16m
@@ -25,18 +27,18 @@ Peak windows (MSK): 04:00-07:00 and 09:00-13:00, Mon-Fri
25
27
  Off-peak = night, weekends and Chinese holidays
26
28
  Price source: api-docs.deepseek.com, updated 11:20 26-10 (next in 5h 59m)
27
29
  ────────────────────────────────────────────────────────────
28
- Commands: /deepseek-peak on|off|refresh|en|ru
30
+ Commands: /dsp on|off|refresh|en|ru · /dsp footer on|off
29
31
  ```
30
32
 
31
33
  ## Install
32
34
 
33
35
  ```bash
34
- # from git (tags are pinned)
35
- pi install git:github.com/valderan/pi-deepseek-tariff@v0.1.0
36
-
37
- # from npm
36
+ # from npm (always the `latest` dist-tag)
38
37
  pi install npm:pi-deepseek-tariff
39
38
 
39
+ # from git: tags mirror the npm versions, so pin whichever you want
40
+ pi install git:github.com/valderan/pi-deepseek-tariff@v0.3.0
41
+
40
42
  # local checkout, for development
41
43
  git clone git@github.com:valderan/pi-deepseek-tariff.git
42
44
  pi install ./pi-deepseek-tariff
@@ -45,20 +47,27 @@ pi install ./pi-deepseek-tariff
45
47
  pi -e ./pi-deepseek-tariff
46
48
  ```
47
49
 
48
- Then run `/deepseek-peak`. Afterwards `pi list`, `pi update --extensions`, `pi remove …` manage it like any other package.
50
+ Then run `/dsp`. Afterwards `pi list`, `pi update --extensions`, `pi remove …` manage it like any other package.
49
51
 
50
52
  ## Commands
51
53
 
54
+ `/dsp` is the short form of the command. The full name `/deepseek-peak` works too, and `/deepseek-peek` is accepted for muscle memory.
55
+
52
56
  | Command | Effect |
53
57
  |---|---|
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) |
58
+ | `/dsp` | toggle the panel |
59
+ | `/dsp on` / `off` | show or hide the panel (the footer line stays while a DeepSeek model is active) |
60
+ | `/dsp refresh` | re-parse prices from the docs page now |
61
+ | `/dsp en` / `ru` | switch the panel language (persisted) |
62
+ | `/dsp footer on` / `off` (or `/dsp footeron` / `footeroff`) | show or hide the footer line (persisted) |
58
63
 
59
64
  ## What it does
60
65
 
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.
66
+ - **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.
67
+
68
+ Russian looks like `● DeepSeek Обычный ещё 11ч 59м` / `● DeepSeek Пиковый ещё 1ч 40м`.
69
+
70
+ Pi renders every extension status on a single footer line, joined alphabetically, so the segment is kept short.
62
71
  - **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
72
  - **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
73
  - **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 +83,7 @@ Then run `/deepseek-peak`. Afterwards `pi list`, `pi update --extensions`, `pi r
74
83
  "intervalMs": 30000,
75
84
  "pricingTtlHours": 6,
76
85
  "alwaysShow": false,
86
+ "footer": true,
77
87
  "models": ["deepseek"],
78
88
  "providerPrefix": "deepseek:",
79
89
  "holidays": ["2026-10-01", "2026-10-02"]
@@ -86,6 +96,7 @@ Then run `/deepseek-peak`. Afterwards `pi list`, `pi update --extensions`, `pi r
86
96
  | `intervalMs` | `30000` | minimum refresh cadence (clamped to ≥ 5000) |
87
97
  | `pricingTtlHours` | `6` | how long parsed prices stay valid |
88
98
  | `alwaysShow` | `false` | show the footer line for non-DeepSeek models too |
99
+ | `footer` | `true` | show the footer status line at all; `/dsp footer off` writes this |
89
100
  | `models` | `["deepseek"]` | id substrings that count as DeepSeek |
90
101
  | `providerPrefix` | `"deepseek:"` | prefix used when printing model ids |
91
102
  | `holidays` | `[]` | Chinese public holidays as Beijing dates (`YYYY-MM-DD`), off-peak all day |
@@ -117,12 +128,36 @@ The rules and parsing modules are pure and take injectable inputs, so tests neve
117
128
 
118
129
  ## Release
119
130
 
131
+ Git tags and npm versions are kept in lockstep: the tag `vX.Y.Z` always points at
132
+ the commit whose `package.json` says `X.Y.Z`, and npm publishes that same version.
133
+
120
134
  ```bash
121
- git tag -a v0.1.0 -m "v0.1.0"
135
+ # 1. bump package.json + README, commit
136
+ npm version 0.3.1 --no-git-tag-version # or edit by hand
137
+ git commit -am "chore: release 0.3.1"
138
+
139
+ # 2. tag the release commit and push
140
+ git tag -a v0.3.1 -m v0.3.1
122
141
  git push origin main --tags
142
+
143
+ # 3. publish the tag to npm (2FA prompt, or --otp=123456)
144
+ npm publish --access public
123
145
  ```
124
146
 
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).
147
+ `npm publish` sets the `latest` dist-tag automatically for a plain semver version.
148
+ Check the result:
149
+
150
+ ```bash
151
+ npm view pi-deepseek-tariff dist-tags # { latest: '0.3.1' }
152
+ npm view pi-deepseek-tariff versions
153
+ ```
154
+
155
+ `.github/workflows/ci.yml` can publish on a `v*` tag instead of step 3 when the
156
+ `NPM_TOKEN` secret is configured; with trusted publishing enabled on npmjs.com the
157
+ token is not needed at all and the build is signed with provenance.
158
+
159
+ Tags are pinnable with `pi install git:…@v0.3.1`. Published versions show up in the
160
+ [pi package gallery](https://pi.dev/packages) through the `pi-package` keyword.
126
161
 
127
162
  ## License
128
163
 
@@ -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.0",
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",
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
  }