@suheugene/deepseek-peak-indicator 0.0.0-stage → 1.0.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
@@ -1,3 +1,40 @@
1
- # Temporary Holding Version
1
+ # @suheugene/deepseek-peak-indicator
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An [OpenCode](https://opencode.ai) TUI plugin that shows a DeepSeek off-peak
4
+ badge in the sidebar:
5
+
6
+ ```
7
+ Deepseek Peak: in 02h 41m ← off-peak, next peak window in 2h 41m (green)
8
+ Deepseek Peak: 22m left ← peak window ends in 22 min (red)
9
+ ```
10
+
11
+ `Deepseek Peak:` is blue; `in HHh MMm` is green; `MMm left` is red. Gaps
12
+ longer than a day show as `Xd Hh` (e.g. Friday evening shows the gap to
13
+ Monday 01:00 UTC).
14
+
15
+ ## When is peak?
16
+
17
+ Per the [official DeepSeek pricing](https://api-docs.deepseek.com/quick_start/pricing):
18
+ peak hours are **01:00–04:00 and 06:00–10:00 UTC, Monday through Friday,
19
+ excluding Chinese public holidays**. All other hours are off-peak (half price),
20
+ including weekends and holidays in full.
21
+
22
+ Holidays are hardcoded in `src/peak.ts`: 2026 is official (State Council
23
+ notice, Nov 2025); 2027 is projected until the State Council publishes its
24
+ notice — update the table when it lands.
25
+
26
+ ## Install
27
+
28
+ ```jsonc
29
+ // ~/.config/opencode/opencode.json(c)
30
+ {
31
+ "plugin": ["/path/to/this/directory"]
32
+ }
33
+ ```
34
+
35
+ ## Develop
36
+
37
+ ```bash
38
+ bun install
39
+ bunx tsc --noEmit
40
+ ```
package/package.json CHANGED
@@ -1,6 +1,28 @@
1
1
  {
2
2
  "name": "@suheugene/deepseek-peak-indicator",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "OpenCode TUI plugin that shows a DeepSeek off-peak badge in the sidebar",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/SuhEugene/deepseek-peak-indicator.git"
10
+ },
11
+ "files": ["src"],
12
+ "exports": {
13
+ ".": "./src/index.ts",
14
+ "./tui": "./src/tui.tsx"
15
+ },
16
+ "dependencies": {
17
+ "@opencode/plugin": "latest"
18
+ },
19
+ "peerDependencies": {
20
+ "@opencode/theme": "2.2.2",
21
+ "@opentui/core": "0.7.0",
22
+ "@opentui/solid": "0.7.0",
23
+ "solid-js": ">=1.9.0"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ }
28
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { Plugin } from "@opencode/plugin"
2
+
3
+ export default Plugin.define({
4
+ id: "deepseek-peak-indicator",
5
+ setup() {},
6
+ })
package/src/peak.ts ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * DeepSeek off-peak logic.
3
+ *
4
+ * Official (api-docs.deepseek.com): peak hours are 01:00–04:00 and 06:00–10:00
5
+ * UTC, Monday through Friday, excluding Chinese public holidays. All other
6
+ * hours are off-peak, including weekends and holidays in full.
7
+ *
8
+ * China is UTC+8 with no DST, and both peak windows (+8 = 09:00–12:00 and
9
+ * 14:00–18:00) fall on the same China calendar date as their UTC date — so the
10
+ * UTC date can be matched directly against the China holiday list.
11
+ */
12
+
13
+ // Rest-day ranges (mmdd, inclusive) per year.
14
+ // 2026: official — 国办发明电〔2025〕7号 (gov.cn, 2025-11-04).
15
+ // 2027: projected — the State Council notice is not published yet (due
16
+ // Nov 2026); statutory lunar dates plus the codified bridging pattern.
17
+ // shortcut: 2027 dates are estimates, update when the official notice lands.
18
+ export const HOLIDAYS: Record<number, Array<[number, number]>> = {
19
+ 2026: [
20
+ [101, 103],
21
+ [215, 223],
22
+ [404, 406],
23
+ [501, 505],
24
+ [619, 621],
25
+ [925, 927],
26
+ [1001, 1007],
27
+ ],
28
+ 2027: [
29
+ [101, 101],
30
+ [205, 214],
31
+ [403, 405],
32
+ [501, 505],
33
+ [609, 609],
34
+ [915, 915],
35
+ [1001, 1007],
36
+ ],
37
+ }
38
+
39
+ // UTC hours [start, end)
40
+ const PEAK_WINDOWS: Array<[number, number]> = [
41
+ [1, 4],
42
+ [6, 10],
43
+ ]
44
+
45
+ const md = (d: Date) => (d.getUTCMonth() + 1) * 100 + d.getUTCDate()
46
+
47
+ export function isChinaHoliday(d: Date): boolean {
48
+ const year = d.getUTCFullYear()
49
+ const day = md(d)
50
+ return (HOLIDAYS[year] ?? []).some(([a, b]) => day >= a && day <= b)
51
+ }
52
+
53
+ function peakWindows(d: Date): Array<[number, number]> {
54
+ const wd = d.getUTCDay()
55
+ if (wd === 0 || wd === 6 || isChinaHoliday(d)) return []
56
+ return PEAK_WINDOWS
57
+ }
58
+
59
+ /** Milliseconds since that date's 00:00 UTC. */
60
+ function dayMs(d: Date): number {
61
+ return d.getUTCHours() * 3_600_000 + d.getUTCMinutes() * 60_000 + d.getUTCSeconds() * 1000 + d.getUTCMilliseconds()
62
+ }
63
+
64
+ export type PeakStatus = { peak: boolean; ms: number }
65
+
66
+ /**
67
+ * Current state and time until it flips: `ms` counts down to the end of the
68
+ * current peak window, or up to the start of the next one.
69
+ */
70
+ export function peakStatus(now: Date): PeakStatus {
71
+ let day = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()))
72
+ const t = dayMs(now)
73
+
74
+ for (const [start, end] of peakWindows(day)) {
75
+ if (t >= start * 3_600_000 && t < end * 3_600_000) {
76
+ return { peak: true, ms: end * 3_600_000 - t }
77
+ }
78
+ }
79
+
80
+ // Off-peak: find the next window start, at most ~3 weeks out (longest gap:
81
+ // a long Spring Festival break, ~12 days).
82
+ for (let i = 0; i < 21; i++) {
83
+ const windows = peakWindows(day)
84
+ for (const [start] of windows) {
85
+ const s = start * 3_600_000
86
+ if (i > 0 || s > t) return { peak: false, ms: s - t + i * 86_400_000 }
87
+ }
88
+ day = new Date(day.getTime() + 86_400_000)
89
+ }
90
+ return { peak: false, ms: 0 }
91
+ }
92
+
93
+ /** "HHh MMm" (or "Xd Hh" over a day, "Mm" under an hour). */
94
+ export function fmt(ms: number): string {
95
+ const total = Math.max(0, Math.floor(ms / 60_000)) + 1
96
+ const d = Math.floor(total / 60 / 24)
97
+ const h = Math.floor(total / 60) % 24
98
+ const m = total % 60
99
+
100
+ if (d > 0) return `${d}d ${h}h`
101
+
102
+ if (h == 0) return `${m}m`
103
+
104
+ return `${String(h).padStart(2, "0")}h ${String(m).padStart(2, "0")}m`
105
+ }
package/src/tui.tsx ADDED
@@ -0,0 +1,34 @@
1
+ import { Plugin } from "@opencode/plugin/tui"
2
+ import type { Context } from "@opencode/plugin/tui/plugin"
3
+ import { onCleanup, createSignal } from "solid-js"
4
+ import type { ResolvedTheme } from "@opencode/theme/tui"
5
+ import { fmt, peakStatus } from "./peak"
6
+
7
+ function Badge(props: { theme: ResolvedTheme }) {
8
+ const [now, setNow] = createSignal(new Date())
9
+ const tick = setInterval(() => setNow(new Date()), 1000)
10
+ onCleanup(() => clearInterval(tick))
11
+
12
+ const status = () => peakStatus(now())
13
+ return (
14
+ <box flexDirection="row">
15
+ {/* theme.hue.blue exists at runtime, just missing from @opencode/theme's SemanticHue type */}
16
+ {/* @ts-expect-error blue is a real theme hue */}
17
+ <text fg={props.theme.hue.blue[100]}>Deepseek Peak: </text>
18
+ <text fg={status().peak ? props.theme.text.feedback.error.base : props.theme.text.base}>
19
+ {status().peak ? `${fmt(status().ms)} left` : `in ${fmt(status().ms)}`}
20
+ </text>
21
+ </box>
22
+ )
23
+ }
24
+
25
+ export default Plugin.define({
26
+ id: "deepseek-peak-indicator",
27
+ setup(context: Context) {
28
+ const theme = context.theme
29
+ context.ui.slot({
30
+ append: "sidebar.content",
31
+ render: () => <Badge theme={theme} />,
32
+ })
33
+ },
34
+ })