@icaruk/zai-peak-hours 0.0.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/LICENSE +21 -0
- package/README.md +116 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +11 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +137 -0
- package/dist/peak-hours.d.ts +7 -0
- package/dist/peak-hours.js +53 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Icaruk
|
|
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,116 @@
|
|
|
1
|
+
# @icaruk/zai-peak-hours
|
|
2
|
+
|
|
3
|
+
OpenCode TUI plugin that displays z.ai peak hours information with automatic timezone detection via toast notifications.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
Add this to your `~/.config/opencode/tui.json`:
|
|
8
|
+
|
|
9
|
+
**After npm publication:**
|
|
10
|
+
```jsonc
|
|
11
|
+
{
|
|
12
|
+
"plugin": ["@icaruk/zai-peak-hours"]
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Local development (before npm publication):**
|
|
17
|
+
```jsonc
|
|
18
|
+
{
|
|
19
|
+
"plugin": ["file://D:/Dev/zai-peak-hours/dist/index.js"]
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- 🌍 Automatic timezone detection (UTC+8 / Asia/Shanghai)
|
|
26
|
+
- ⏰ Real-time peak hours status (14:00-18:00 UTC+8)
|
|
27
|
+
- 📊 Time remaining until next peak/off-peak transition
|
|
28
|
+
- 🔔 Toast notifications on session start and periodic updates
|
|
29
|
+
- ⚙️ Configurable update interval
|
|
30
|
+
- 🛠️ Manual commands for on-demand status checks
|
|
31
|
+
|
|
32
|
+
## TUI vs Regular Plugins
|
|
33
|
+
|
|
34
|
+
This is a **TUI plugin** (runs in the OpenCode client/terminal UI), not a regular server-side plugin.
|
|
35
|
+
|
|
36
|
+
| | TUI Plugin (this) | Regular Plugin |
|
|
37
|
+
| ------------- | ------------------------ | -------------------------- |
|
|
38
|
+
| **Config** | `tui.json` | `opencode.jsonc` |
|
|
39
|
+
| **Dónde corre** | Client/TUI | Server/backend |
|
|
40
|
+
| **Acceso UI** | Sí (toasts, comandos) | No |
|
|
41
|
+
| **Custom tools** | No | Sí |
|
|
42
|
+
| **Hook en tools** | No | Sí |
|
|
43
|
+
|
|
44
|
+
TUI plugins can display toasts, register slash commands, and show UI elements directly in the terminal.
|
|
45
|
+
|
|
46
|
+
## Commands
|
|
47
|
+
|
|
48
|
+
- `/peak_hours` - Display current peak hours status
|
|
49
|
+
- `/peak_hours_status` - Display plugin diagnostics and configuration
|
|
50
|
+
|
|
51
|
+
## Configuration
|
|
52
|
+
|
|
53
|
+
Configuration is optional - the plugin works out of the box.
|
|
54
|
+
|
|
55
|
+
To customize, create `~/.config/opencode/peak-hours.json`:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"enabled": true,
|
|
60
|
+
"updateIntervalMinutes": 15
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Options
|
|
65
|
+
|
|
66
|
+
- `enabled` (boolean, default: `true`) - Enable or disable the plugin
|
|
67
|
+
- `updateIntervalMinutes` (number, default: `15`) - Update interval in minutes
|
|
68
|
+
|
|
69
|
+
## Peak Hours Schedule
|
|
70
|
+
|
|
71
|
+
- **Peak Hours**: 14:00-18:00 UTC+8 (Asia/Shanghai timezone)
|
|
72
|
+
- **Off-Peak Hours**: All other times
|
|
73
|
+
|
|
74
|
+
## Messages
|
|
75
|
+
|
|
76
|
+
The plugin displays toast notifications in the following format:
|
|
77
|
+
|
|
78
|
+
**Peak Hours:**
|
|
79
|
+
```
|
|
80
|
+
Currently in peak hours. X hours Y minutes remaining
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Off-Peak Hours:**
|
|
84
|
+
```
|
|
85
|
+
Currently in off-peak hours. X hours Y minutes remaining
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Development
|
|
89
|
+
|
|
90
|
+
For local development and testing:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Clone the repository
|
|
94
|
+
git clone https://github.com/Icaruk/zai-peak-hours.git
|
|
95
|
+
cd zai-peak-hours
|
|
96
|
+
|
|
97
|
+
# Install dependencies
|
|
98
|
+
npm install
|
|
99
|
+
|
|
100
|
+
# Build the plugin
|
|
101
|
+
npm run build
|
|
102
|
+
|
|
103
|
+
# Run in watch mode for development
|
|
104
|
+
npm run dev
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Then use the local development path in `tui.json`:
|
|
108
|
+
```jsonc
|
|
109
|
+
{
|
|
110
|
+
"plugin": ["file://D:/Dev/zai-peak-hours/dist/index.js"]
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const DEFAULT_CONFIG = {
|
|
2
|
+
enabled: true,
|
|
3
|
+
updateIntervalMinutes: 15 // 15 minutes
|
|
4
|
+
};
|
|
5
|
+
export function getPluginConfig(config) {
|
|
6
|
+
const userConfig = config?.peakHours || {};
|
|
7
|
+
return {
|
|
8
|
+
enabled: userConfig.enabled !== undefined ? userConfig.enabled : DEFAULT_CONFIG.enabled,
|
|
9
|
+
updateIntervalMinutes: userConfig.updateIntervalMinutes !== undefined ? userConfig.updateIntervalMinutes : DEFAULT_CONFIG.updateIntervalMinutes
|
|
10
|
+
};
|
|
11
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TuiPluginApi, TuiPluginMeta } from '../node_modules/@opencode-ai/plugin/dist/tui';
|
|
2
|
+
import type { PluginOptions } from '@opencode-ai/plugin';
|
|
3
|
+
declare function tui(api: TuiPluginApi, options: PluginOptions | undefined, meta: TuiPluginMeta): Promise<void>;
|
|
4
|
+
declare const plugin: {
|
|
5
|
+
id: string;
|
|
6
|
+
tui: typeof tui;
|
|
7
|
+
};
|
|
8
|
+
export default plugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { getPeakHoursStatus, formatPeakHoursMessage } from './peak-hours';
|
|
2
|
+
import { DEFAULT_CONFIG } from './config';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
let timerId = null;
|
|
6
|
+
let pluginLoaded = false;
|
|
7
|
+
let configCache = null;
|
|
8
|
+
function getConfigPath() {
|
|
9
|
+
const configDir = process.env.XDG_CONFIG_HOME
|
|
10
|
+
? path.join(process.env.XDG_CONFIG_HOME, 'opencode')
|
|
11
|
+
: path.join(process.env.HOME || process.env.USERPROFILE || '', '.config', 'opencode');
|
|
12
|
+
return path.join(configDir, 'peak-hours.json');
|
|
13
|
+
}
|
|
14
|
+
function loadConfig() {
|
|
15
|
+
if (configCache) {
|
|
16
|
+
return configCache;
|
|
17
|
+
}
|
|
18
|
+
const configPath = getConfigPath();
|
|
19
|
+
if (fs.existsSync(configPath)) {
|
|
20
|
+
try {
|
|
21
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
22
|
+
const userConfig = JSON.parse(configContent);
|
|
23
|
+
configCache = {
|
|
24
|
+
enabled: userConfig.enabled !== undefined ? userConfig.enabled : DEFAULT_CONFIG.enabled,
|
|
25
|
+
updateIntervalMinutes: userConfig.updateIntervalMinutes !== undefined ? userConfig.updateIntervalMinutes : DEFAULT_CONFIG.updateIntervalMinutes
|
|
26
|
+
};
|
|
27
|
+
return configCache;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.error('Error loading peak-hours config:', error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
configCache = DEFAULT_CONFIG;
|
|
34
|
+
return configCache;
|
|
35
|
+
}
|
|
36
|
+
function showPeakHoursToast(api) {
|
|
37
|
+
const config = loadConfig();
|
|
38
|
+
if (!config.enabled) {
|
|
39
|
+
api.ui.toast({
|
|
40
|
+
message: 'Peak Hours plugin is disabled in config',
|
|
41
|
+
duration: 3000
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const status = getPeakHoursStatus();
|
|
46
|
+
const message = formatPeakHoursMessage(status);
|
|
47
|
+
api.ui.toast({
|
|
48
|
+
message,
|
|
49
|
+
duration: 5000
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function showDiagnostics(api) {
|
|
53
|
+
const config = loadConfig();
|
|
54
|
+
const status = getPeakHoursStatus();
|
|
55
|
+
const currentTime = new Date().toISOString();
|
|
56
|
+
const diagnostics = [
|
|
57
|
+
'=== Peak Hours Plugin Diagnostics ===',
|
|
58
|
+
`Plugin loaded: ${pluginLoaded}`,
|
|
59
|
+
`Plugin enabled: ${config.enabled}`,
|
|
60
|
+
`Update interval: ${config.updateIntervalMinutes} minutes`,
|
|
61
|
+
`Current time: ${currentTime}`,
|
|
62
|
+
`In peak hours: ${status.inPeakHours}`,
|
|
63
|
+
`Time until ${status.transitionType}: ${status.timeUntilTransition}`,
|
|
64
|
+
`====================================`
|
|
65
|
+
].join('\n');
|
|
66
|
+
api.ui.toast({
|
|
67
|
+
message: diagnostics,
|
|
68
|
+
duration: 8000
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async function tui(api, options, meta) {
|
|
72
|
+
pluginLoaded = true;
|
|
73
|
+
const config = loadConfig();
|
|
74
|
+
const updateInterval = config.updateIntervalMinutes * 60 * 1000; // Convert minutes to milliseconds
|
|
75
|
+
// Show plugin loaded message
|
|
76
|
+
api.ui.toast({
|
|
77
|
+
message: 'Peak Hours Plugin loaded successfully',
|
|
78
|
+
duration: 3000
|
|
79
|
+
});
|
|
80
|
+
try {
|
|
81
|
+
const commands = [
|
|
82
|
+
{
|
|
83
|
+
title: 'Show Peak Hours Status',
|
|
84
|
+
value: 'peak_hours',
|
|
85
|
+
description: 'Display current peak hours status and time until next transition',
|
|
86
|
+
category: 'Utilities',
|
|
87
|
+
slash: {
|
|
88
|
+
name: 'peak_hours'
|
|
89
|
+
},
|
|
90
|
+
onSelect: () => {
|
|
91
|
+
showPeakHoursToast(api);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
title: 'Show Peak Hours Diagnostics',
|
|
96
|
+
value: 'peak_hours_status',
|
|
97
|
+
description: 'Display plugin diagnostics and configuration status',
|
|
98
|
+
category: 'Utilities',
|
|
99
|
+
slash: {
|
|
100
|
+
name: 'peak_hours_status'
|
|
101
|
+
},
|
|
102
|
+
onSelect: () => {
|
|
103
|
+
showDiagnostics(api);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
];
|
|
107
|
+
api.command.register(() => commands);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
api.ui.toast({
|
|
111
|
+
message: `Error registering peak_hours commands: ${error}`,
|
|
112
|
+
duration: 5000
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (!config.enabled) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const unsubscribe = api.event.on('session.created', () => {
|
|
119
|
+
showPeakHoursToast(api);
|
|
120
|
+
timerId = setInterval(() => {
|
|
121
|
+
showPeakHoursToast(api);
|
|
122
|
+
}, updateInterval);
|
|
123
|
+
});
|
|
124
|
+
api.lifecycle.onDispose(() => {
|
|
125
|
+
unsubscribe();
|
|
126
|
+
if (timerId) {
|
|
127
|
+
clearInterval(timerId);
|
|
128
|
+
timerId = null;
|
|
129
|
+
}
|
|
130
|
+
pluginLoaded = false;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const plugin = {
|
|
134
|
+
id: "icaruk.peak-hours",
|
|
135
|
+
tui
|
|
136
|
+
};
|
|
137
|
+
export default plugin;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface PeakHoursStatus {
|
|
2
|
+
inPeakHours: boolean;
|
|
3
|
+
timeUntilTransition: string;
|
|
4
|
+
transitionType: 'start' | 'end';
|
|
5
|
+
}
|
|
6
|
+
export declare function getPeakHoursStatus(): PeakHoursStatus;
|
|
7
|
+
export declare function formatPeakHoursMessage(status: PeakHoursStatus): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import utc from 'dayjs/plugin/utc.js';
|
|
3
|
+
import timezone from 'dayjs/plugin/timezone.js';
|
|
4
|
+
dayjs.extend(utc);
|
|
5
|
+
dayjs.extend(timezone);
|
|
6
|
+
export function getPeakHoursStatus() {
|
|
7
|
+
const peakHoursStart = 14;
|
|
8
|
+
const peakHoursEnd = 18;
|
|
9
|
+
const currentTime = dayjs().utc().utcOffset(8);
|
|
10
|
+
const currentHour = currentTime.hour();
|
|
11
|
+
let inPeakHours;
|
|
12
|
+
let transitionTime;
|
|
13
|
+
let transitionType;
|
|
14
|
+
if (currentHour >= peakHoursStart && currentHour < peakHoursEnd) {
|
|
15
|
+
inPeakHours = true;
|
|
16
|
+
transitionType = 'end';
|
|
17
|
+
transitionTime = currentTime.hour(peakHoursEnd).minute(0).second(0);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
inPeakHours = false;
|
|
21
|
+
transitionType = 'start';
|
|
22
|
+
const transitionDay = currentHour >= peakHoursEnd
|
|
23
|
+
? currentTime.add(1, 'day')
|
|
24
|
+
: currentTime;
|
|
25
|
+
transitionTime = transitionDay.hour(peakHoursStart).minute(0).second(0);
|
|
26
|
+
}
|
|
27
|
+
const diff = transitionTime.diff(currentTime);
|
|
28
|
+
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
29
|
+
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
|
30
|
+
let timeUntilTransition;
|
|
31
|
+
if (hours > 0 && minutes > 0) {
|
|
32
|
+
timeUntilTransition = `${hours} hours ${minutes} minutes`;
|
|
33
|
+
}
|
|
34
|
+
else if (hours > 0) {
|
|
35
|
+
timeUntilTransition = `${hours} hours`;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
timeUntilTransition = `${minutes} minutes`;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
inPeakHours,
|
|
42
|
+
timeUntilTransition,
|
|
43
|
+
transitionType
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function formatPeakHoursMessage(status) {
|
|
47
|
+
if (status.inPeakHours) {
|
|
48
|
+
return `Currently in peak hours. ${status.timeUntilTransition} remaining`;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
return `Currently in off-peak hours. ${status.timeUntilTransition} remaining`;
|
|
52
|
+
}
|
|
53
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@icaruk/zai-peak-hours",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "OpenCode plugin to display z.ai peak hours information with automatic timezone detection",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"tui": "dist/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsc --watch",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"opencode",
|
|
22
|
+
"plugin",
|
|
23
|
+
"peak-hours",
|
|
24
|
+
"timezone",
|
|
25
|
+
"cli"
|
|
26
|
+
],
|
|
27
|
+
"author": "Icaruk",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@opencode-ai/plugin": "^1.4.0",
|
|
31
|
+
"dayjs": "^1.11.10"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^20.0.0",
|
|
35
|
+
"typescript": "^5.0.0"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/Icaruk/zai-peak-hours.git"
|
|
45
|
+
}
|
|
46
|
+
}
|