Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,23 @@ async function getCredentials(): Promise<Credentials | null> { }

### Development Principles

**CRITICAL:** Implementation MUST follow these principles to avoid over-engineering and maintain simplicity.
**CRITICAL:** Implementation MUST follow these principles to avoid over-engineering and maintain simplicity. Violating any of them is a blocking issue — stop and simplify before proceeding.

**No Over-Thinking (Primary Rule):**
- Do NOT deliberate extensively on simple tasks — act decisively
- Do NOT speculate about future needs — solve the problem in front of you
- Do NOT create design documents for single-function changes
- Do NOT weigh alternatives for more than a few minutes — pick the simplest viable option and implement
- If a senior engineer would say "just do it", then just do it
- Over-thinking wastes time; over-engineering wastes code. Both are blockers.

**KISS (Keep It Simple, Stupid):**
- Simplest correct solution wins. Always.
- Prefer plain functions over classes, factories, or patterns
- Prefer constants over config objects
- Prefer inline code over abstractions for single-use cases
- If code needs a 5-line comment to explain WHY it exists, it's too complex
- Fewer lines of code = fewer bugs = easier review

**YAGNI (You Aren't Gonna Need It):**
- Build only what's needed NOW, not what you might need LATER
Expand Down Expand Up @@ -338,6 +354,28 @@ function isApiResponse(data: unknown): data is ApiResponse {
- Simple code is easier to maintain
- Flexible without requirements = complex for no reason

**TDD (Test-Driven Development) — MANDATORY for all code changes:**

Workflow (Red → Green → Refactor):
1. **RED**: Write a failing test that describes the desired behavior BEFORE any implementation
2. **GREEN**: Write the minimum code to make the test pass — nothing more
3. **REFACTOR**: Improve code quality while keeping tests green

Rules:
- ❌ NEVER write implementation before tests
- ❌ NEVER add code without a corresponding test
- ✅ One test at a time — write, run, repeat
- ✅ Tests define the spec; implementation satisfies it
- ✅ If a bug is found, write a test that reproduces it FIRST, then fix
- ✅ Commit only when all tests pass

```bash
# TDD cycle commands
npm run test -- path/to/test.test.ts # Run specific test (RED check)
npm run test # Run all tests (GREEN verification)
npm run build # Ensure compiles
```

**Reference:**
- Implementation plan: `docs/implementation-plan.md` - Follows TDD with vertical slicing
- ALL context needed for OpenAgents: `/home/eddy/distrobox/box-go-debian-home/.config/opencode/context/` - find the needed context here if you didnt found it in the project folder
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).

## [Unreleased]

## [1.8.0] - 2026-06-15

### Added
- **Reset timestamp display in local timezone** - The "Resets In" column now shows the actual clock time alongside the countdown (Issue #34):
- Short durations: `4h 36m (01:34)` — countdown + local HH:MM
- Long durations (≥24h): `6d 16h (Sat 13:48)` — countdown + weekday + local HH:MM
- Uses native `Date` local time methods (`getHours()`, `getMinutes()`, `getDay()`) — no external dependencies, no unreliable timezone abbreviations

### Changed
- `formatResetCell()` in `src/index.ts` now appends local reset time after the countdown portion

### Technical
- Integration tests updated to use regex assertions (`assert.match`) for timezone-dependent output instead of exact string matching
- 117 tests passing (0 new tests, 2 updated assertions)
- Reported by @eagleii-dev (Pacific/Auckland, UTC+12)

## [1.7.0] - 2026-04-02

### Changed
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ OpenCode plugin to query Z.ai GLM Coding Plan usage statistics with real-time qu
- 🌍 Supports both Global (api.z.ai) and CN (open.bigmodel.cn) platforms
- 🔐 Automatic credential discovery via OpenCode authentication context
- 📈 Visual progress bars for quota percentages rendered in Markdown code spans
- ⏰ Reset countdown display - Shows when quota resets (hours/days)
- ⏰ Reset countdown display - Shows when quota resets with local timezone clock time (hours/days + HH:MM)
- 🏷️ Account plan display - Shows your subscription tier (Lite, Pro, etc.)
- ⚡ Fail-fast error handling (no retry logic - user controls when to retry)

Expand Down Expand Up @@ -137,8 +137,8 @@ After authentication, simply run:

| Window | Usage | Progress | Resets In |
|--------|------:|----------|-----------|
| ⏱️ 5h Token | 40.5% | `█████░░░░░░░` | 3h 42m |
| 📅 Weekly | 52.0% | `██████░░░░░░` | 4d 12h |
| ⏱️ 5h Token | 40.5% | `█████░░░░░░░` | 3h 42m (01:34) |
| 📅 Weekly | 52.0% | `██████░░░░░░` | 4d 12h (Sat 13:48) |
| 🔌 MCP (1 Month) | 12.3% | `█░░░░░░░░░░░` | — |

##### 📊 Quota Usage
Expand Down
167 changes: 167 additions & 0 deletions docs/implementation-plan-v1.8.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Implementation Plan: v1.8.0 Reset Timestamp Display (Local Timezone)

**Version:** 1.0
**Date:** June 15, 2026
**Branch:** `fix/reset-timer-timezone-34`
**Target Version:** 1.8.0
**Issue:** [#34 — Display actual reset timestamp in local timezone](https://github.com/guyinwonder168/opencode-glm-quota/issues/34)
**PRD Reference:** `docs/opencode-glm-quota-prd-final.md` Section 1.12

---

## Goal

Show the actual reset clock time in the user's local timezone alongside the existing countdown, so users in far-from-UTC zones can plan around resets without guessing when they occur.

## Problem

**Before:** `| ⏱️ 5h Token | 87.0% | ████████████████░░░░░ | 4h 36m |`
**After:** `| ⏱️ 5h Token | 87.0% | ████████████████░░░░░ | 4h 36m (01:34) |`

For long durations (≥24h), also show the weekday:
`| 📅 Weekly | 17.0% | ████░░░░░░░░░░░░░░░░ | 6d 16h (Sat 13:48) |`

## Design Decisions

| Decision | Choice | Rationale |
|----------|--------|-----------|
| Timezone display format | `HH:MM` (local clock time) | Deterministic, user knows their own timezone |
| Timezone abbreviation | **Not included** | Node.js `toLocaleTimeString` TZ abbreviations are unreliable across platforms (e.g., "GMT+13" vs "NZST") |
| Long duration format | `Day HH:MM` (e.g., `Sat 13:48`) | Matches issue's proposed format for weekly resets |
| API for local time | `getHours()`, `getMinutes()`, `getDay()` | Follows existing `date-formatter.ts` pattern, no dependencies |
| Testing approach | Regex assertions in integration tests | Timezone-dependent output made deterministic via pattern matching |

## Impact Analysis

### Files MODIFIED

| File | Changes |
|------|---------|
| `src/index.ts` | `formatResetCell()` — add local time display after countdown |
| `tests/integration/reset-time-display.test.ts` | Update assertions: `includes` → `match` with regex for time pattern |
| `package.json` | Version: `1.7.0` → `1.8.0` |
| `sonar-project.properties` | `sonar.projectVersion`: `1.7.0` → `1.8.0` |
| `CHANGELOG.md` | Add `[1.8.0]` entry |
| `README.md` | Update feature bullet + output example |
| `docs/opencode-glm-quota-prd-final.md` | Add Section 1.12 |

### Files UNCHANGED

| File | Reason |
|------|--------|
| `src/utils/reset-timer.ts` | Long-form `formatTimeUntilReset()` not used in Markdown table, separate concern |
| All other `src/` files | No output or logic changes needed |

---

## Implementation Slices

### Slice 1: TDD RED — Update integration test assertions

**Goal:** Write failing tests that describe the desired new format.

**Changes to `tests/integration/reset-time-display.test.ts`:**
- Short duration test: `includes('| ... | 4h 42m |')` → `match(/\| ... \| 4h 42m \(\d{2}:\d{2}\) \|/)`
- Long duration test: `includes('| ... | 4d 12h |')` → `match(/\| ... \| 4d 12h \([A-Z][a-z]{2} \d{2}:\d{2}\) \|/)`
- MCP test: Unchanged (no reset time → still `—`)

**Verify:** `npm run test -- tests/integration/reset-time-display.test.ts` → 2 failures (RED)

---

### Slice 2: TDD GREEN — Modify `formatResetCell()`

**Goal:** Minimum code to make tests pass.

**Changes to `src/index.ts` `formatResetCell()` (was lines 283-301):**

```typescript
function formatResetCell(resetTime?: number): string {
const resetAt = asNumber(resetTime);
if (resetAt === null) return '—';

const diffMs = resetAt - Date.now();
if (diffMs <= 0) return '—';

const totalMinutes = Math.floor(diffMs / (1000 * 60));

// Format countdown portion
let countdown: string;
if (totalMinutes >= 24 * 60) {
const totalHours = Math.floor(totalMinutes / 60);
countdown = `${Math.floor(totalHours / 24)}d ${totalHours % 24}h`;
} else {
countdown = `${Math.floor(totalMinutes / 60)}h ${totalMinutes % 60}m`;
}

// Format local reset time portion (Issue #34)
const resetDate = new Date(resetAt);
const hh = String(resetDate.getHours()).padStart(2, '0');
const mm = String(resetDate.getMinutes()).padStart(2, '0');

if (totalMinutes >= 24 * 60) {
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return `${countdown} (${dayNames[resetDate.getDay()]} ${hh}:${mm})`;
}

return `${countdown} (${hh}:${mm})`;
}
```

**Verify:** `npm run test -- tests/integration/reset-time-display.test.ts` → all pass (GREEN)

---

### Slice 3: Full test suite verification

**Goal:** Ensure no regressions.

**Verify:** `npm test` → 117 tests pass, 0 failures

---

### Slice 4: Documentation + version bump

**Tasks:**
1. `package.json`: `1.7.0` → `1.8.0`
2. `sonar-project.properties`: `sonar.projectVersion` `1.7.0` → `1.8.0`
3. `CHANGELOG.md`: Add `[1.8.0]` entry under `[Unreleased]`
4. `README.md`: Update feature bullet + output example
5. `docs/opencode-glm-quota-prd-final.md`: Add Section 1.12

**Verify:** `npm run build && npm run lint && npm test` all pass

---

## Execution Order

```
Slice 1 (RED tests)
Slice 2 (GREEN implementation)
Slice 3 (full suite verification)
Slice 4 (docs + version bump)
```

All slices are sequential — small scope, no parallelism needed.

## Risk Assessment

| Risk | Mitigation |
|------|------------|
| Timezone-dependent test output | Regex assertions (`\d{2}:\d{2}`) instead of hardcoded times |
| Cell width increase | "Resets In" column is last, Markdown tables auto-size — no alignment issues |
| DST transitions | `getHours()`/`getMinutes()` always return local time per system TZ — correct by design |
| Node.js version differences | Using standard `Date` methods available since ES1 — universally supported |

## Estimated Effort

| Slice | Files | Time |
|-------|-------|------|
| Slice 1 | 1 | 10 min |
| Slice 2 | 1 | 10 min |
| Slice 3 | 0 | 5 min |
| Slice 4 | 5 | 20 min |
| **Total** | **7 files** | **~45 min** |
41 changes: 41 additions & 0 deletions docs/opencode-glm-quota-prd-final.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,47 @@ Your token was rejected by the API.

---

### 1.12 Reset Timestamp Display in Local Timezone (COMPLETED - v1.8.0 ✅)

**Problem Statement:**

The reset timer only shows a **countdown** (`4h 36m`), but not the **actual reset timestamp**. For users in timezones far from UTC (e.g., UTC+12 Pacific/Auckland), the 5-hour rolling window reset often happens at an inconvenient local time (e.g., 1–2 AM), and it's not obvious *when* exactly the quota will reset (Issue #34).

**Solution:**

Show the actual reset time in the user's local timezone alongside the countdown:

```
| ⏱️ 5h Token | 87.0% | ████████████████░░░░░ | 4h 36m (01:34) |
| 📅 Weekly | 17.0% | ████░░░░░░░░░░░░░░░░ | 6d 16h (Sat 13:48) |
```

**Design Decisions:**

| Decision | Choice | Rationale |
|----------|--------|-----------|
| Timezone display format | `HH:MM` local clock time | Deterministic, user knows their own timezone |
| Timezone abbreviation | Not included | Node.js `toLocaleTimeString` TZ abbreviations unreliable across platforms |
| Long duration format | `Day HH:MM` | Matches issue's proposed format for weekly resets |
| API | `Date.getHours()`, `getMinutes()`, `getDay()` | Native methods, no dependencies, follows existing `date-formatter.ts` pattern |

**Implementation:**

- `formatResetCell()` in `src/index.ts` computes local time from `nextResetTime` Unix timestamp
- Short durations (`<24h`): Returns `${countdown} (${HH}:${MM})`
- Long durations (`≥24h`): Returns `${countdown} (${DayName} ${HH}:${MM})`
- MCP rows without `nextResetTime`: Unchanged (`—`)

**Testing:**

- Integration tests use regex assertions (`assert.match`) for timezone-dependent output
- Pattern: `/\d{2}:\d{2}/` for short durations, `/[A-Z][a-z]{2} \d{2}:\d{2}/` for long durations
- Works deterministically across CI environments (GitHub Actions defaults to UTC)

**Status:** ✅ COMPLETED (2026-06-15) - v1.8.0, resolves Issue #34

---

## 2. OpenCode Plugin Architecture

### 2.1 OpenCode Plugin System
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "opencode-glm-quota",
"version": "1.7.0",
"version": "1.8.0",
"type": "module",
"description": "OpenCode plugin to query Z.ai GLM Coding Plan usage statistics including quota limits, model usage, and MCP tool usage",
"main": "dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ sonar.host.url=https://sonarcloud.io

# Project metadata
sonar.projectName=OpenCode GLM Quota Plugin
sonar.projectVersion=1.7.0
sonar.projectVersion=1.8.0

# Source code configuration
sonar.sources=src
Expand Down
19 changes: 17 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,12 +292,27 @@ function formatResetCell(resetTime?: number): string {
}

const totalMinutes = Math.floor(diffMs / (1000 * 60));

// Format countdown portion
let countdown: string;
if (totalMinutes >= 24 * 60) {
const totalHours = Math.floor(totalMinutes / 60);
return `${Math.floor(totalHours / 24)}d ${totalHours % 24}h`;
countdown = `${Math.floor(totalHours / 24)}d ${totalHours % 24}h`;
} else {
countdown = `${Math.floor(totalMinutes / 60)}h ${totalMinutes % 60}m`;
}

// Format local reset time portion (Issue #34: show actual clock time)
const resetDate = new Date(resetAt);
const hh = String(resetDate.getHours()).padStart(2, '0');
const mm = String(resetDate.getMinutes()).padStart(2, '0');

if (totalMinutes >= 24 * 60) {
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return `${countdown} (${dayNames[resetDate.getDay()]} ${hh}:${mm})`;
}

return `${Math.floor(totalMinutes / 60)}h ${totalMinutes % 60}m`;
return `${countdown} (${hh}:${mm})`;
}

function formatMarkdownTable(headers: string[], separator: string[], rows: string[][]): string {
Expand Down
10 changes: 6 additions & 4 deletions tests/integration/reset-time-display.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ describe('Reset Time Display Integration', () => {
delete process.env.ZAI_API_KEY
})

test('renders short reset windows as h and m in the Markdown quota table', async () => {
test('renders short reset windows as countdown with local time in the Markdown quota table', async () => {
https.request = createMockRequest({
'/quota/limit': {
statusCode: 200,
Expand Down Expand Up @@ -103,10 +103,11 @@ describe('Reset Time Display Integration', () => {
const plugin = await GlmQuotaPlugin({} as unknown as PluginContext)
const result = await (plugin.tool!.glm_quota as unknown as ToolExecutor).execute()

assert.ok(result.includes('| ⏱️ 5h Token | 45.0% | `█████░░░░░░░` | 4h 42m |'))
// Should show countdown + local time: "4h 42m (HH:MM)"
assert.match(result, /\| ⏱️ 5h Token \| 45\.0% \| `█████░░░░░░░` \| 4h 42m \(\d{2}:\d{2}\) \|/)
})

test('renders long reset windows as days and hours in the Markdown quota table', async () => {
test('renders long reset windows as countdown with day and local time in the Markdown quota table', async () => {
https.request = createMockRequest({
'/quota/limit': {
statusCode: 200,
Expand Down Expand Up @@ -139,7 +140,8 @@ describe('Reset Time Display Integration', () => {
const plugin = await GlmQuotaPlugin({} as unknown as PluginContext)
const result = await (plugin.tool!.glm_quota as unknown as ToolExecutor).execute()

assert.ok(result.includes('| 📅 Weekly | 52.0% | `██████░░░░░░` | 4d 12h |'))
// Should show countdown + day name + local time: "4d 12h (Day HH:MM)"
assert.match(result, /\| 📅 Weekly \| 52\.0% \| `██████░░░░░░` \| 4d 12h \([A-Z][a-z]{2} \d{2}:\d{2}\) \|/)
})

test('renders MCP rows without a reset countdown in the Markdown quota table', async () => {
Expand Down
Loading