From 282d27ae1d87f661d91a1bcf9b1adeac1a45865b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 16 Sep 2026 17:05:17 +0000 Subject: [PATCH] Add maintainer docs, release CI, and stronger offline tests. A public 1.0.0 repo with no issues, templates, or changelog looks abandoned. These files and the tag-based zip workflow make CheckUsage look like a real, trusted Mac tool without inventing users or touching provider auth. Co-authored-by: Nikita --- .github/ISSUE_TEMPLATE/bug_report.md | 30 ++++ .github/ISSUE_TEMPLATE/config.yml | 8 ++ .github/ISSUE_TEMPLATE/feature_request.md | 19 +++ .github/PULL_REQUEST_TEMPLATE.md | 22 +++ .github/workflows/ci.yml | 30 +++- .github/workflows/release.yml | 56 ++++++++ CHANGELOG.md | 38 +++++ CODE_OF_CONDUCT.md | 134 ++++++++++++++++++ CONTRIBUTING.md | 66 ++++++++- Makefile | 7 +- README.md | 29 +++- SECURITY.md | 47 ++++++ Sources/CheckUsage/App/AppInfo.swift | 12 ++ Sources/CheckUsage/L10n/ExtraL10n.swift | 27 ++++ Sources/CheckUsage/UI/SettingsView.swift | 5 +- Tests/CheckUsageTests/AppInfoTests.swift | 30 ++++ Tests/CheckUsageTests/BurnForecastTests.swift | 61 ++++++++ Tests/CheckUsageTests/ResetCopyTests.swift | 32 +++++ Tests/CheckUsageTests/UsageParserTests.swift | 122 ++++++++++++++++ docs/ARCHITECTURE.md | 71 ++++++++++ docs/i18n/de.md | 6 + docs/i18n/es.md | 6 + docs/i18n/fr.md | 6 + docs/i18n/ja.md | 6 + docs/i18n/ko.md | 6 + docs/i18n/pt-BR.md | 6 + docs/i18n/ru.md | 6 + docs/i18n/zh-Hans.md | 6 + 28 files changed, 885 insertions(+), 9 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/release.yml create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 SECURITY.md create mode 100644 Sources/CheckUsage/App/AppInfo.swift create mode 100644 Tests/CheckUsageTests/AppInfoTests.swift create mode 100644 docs/ARCHITECTURE.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dcaa0a3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,30 @@ +--- +name: Bug report +about: Something in CheckUsage is wrong or a usage meter broke +title: "" +labels: ["bug"] +assignees: [] +--- + +**CheckUsage version** +(Settings → About, or the GitHub release you installed) + +**macOS version** + + +**Providers affected** +(Claude / Cursor / Codex / Copilot / Gemini / Grok / …) + +**What happened** + + +**What you expected** + + +**Steps to reproduce** +1. +2. + +**Notes** + +Do **not** paste tokens, cookies, Keychain dumps, `auth.json`, `.credentials.json`, or `state.vscdb` contents. Redact fixtures. See [SECURITY.md](../../SECURITY.md). diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..f6f1a75 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Security vulnerability + url: https://github.com/sysrootix/check-usage/blob/main/SECURITY.md + about: Report token leakage or other security issues privately — do not file a public issue. + - name: Changelog + url: https://github.com/sysrootix/check-usage/blob/main/CHANGELOG.md + about: What shipped in each release. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..b5346e1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: An idea that fits a local, no-telemetry usage widget +title: "" +labels: ["enhancement"] +assignees: [] +--- + +**Problem** +What is hard today without this change? + +**Proposal** +What should CheckUsage do? + +**Providers / scope** +Does this need a new unofficial API? If yes, say which official app already calls it. Do not attach live credentials. + +**Alternatives** +Anything you already tried (hotkeys, Settings toggles, another tool). diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..f7e0b07 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,22 @@ +## What + + + +## Why + + + +## How I tested + +- [ ] `make test` +- [ ] `make app` (if UI, signing, or the bundle changed) + +macOS 14+ / Xcode 16+ required. Linux agents cannot run `xcodebuild`; the `macos-15` CI workflow is the gate. + +## Checklist + +- [ ] No secrets, tokens, cookies, or Keychain blobs in the diff or screenshots +- [ ] Parser changes include a realistic fixture in `Tests/CheckUsageTests` (no live network) +- [ ] New `L10n` / `ExtraL10n` keys exist in every language table +- [ ] User-facing changes are noted under **Unreleased** in `CHANGELOG.md` +- [ ] I have read [CONTRIBUTING.md](../CONTRIBUTING.md) and [SECURITY.md](../SECURITY.md) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f4eb19..f4de9da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,20 +5,46 @@ on: branches: [main] pull_request: +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + jobs: test: runs-on: macos-15 + timeout-minutes: 30 + env: + HOMEBREW_NO_AUTO_UPDATE: "1" + HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: "1" steps: - uses: actions/checkout@v4 + - name: Select Xcode run: sudo xcode-select -s /Applications/Xcode.app + + - name: Cache Homebrew downloads + uses: actions/cache@v4 + with: + path: ~/Library/Caches/Homebrew + key: brew-${{ runner.os }}-${{ runner.arch }}-xcodegen + restore-keys: | + brew-${{ runner.os }}-${{ runner.arch }}- + - name: Install XcodeGen - run: brew install xcodegen + run: brew list xcodegen >/dev/null 2>&1 || brew install xcodegen + - name: Test run: make test + - name: Build app run: make app - - uses: actions/upload-artifact@v4 + + - name: Upload app + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: actions/upload-artifact@v4 with: name: CheckUsage.app path: dist/CheckUsage.app diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..72778c3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,56 @@ +name: Release + +on: + push: + tags: + - "v*" + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: write + +jobs: + release: + runs-on: macos-15 + timeout-minutes: 30 + env: + HOMEBREW_NO_AUTO_UPDATE: "1" + HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: "1" + steps: + - uses: actions/checkout@v4 + + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode.app + + - name: Cache Homebrew downloads + uses: actions/cache@v4 + with: + path: ~/Library/Caches/Homebrew + key: brew-${{ runner.os }}-${{ runner.arch }}-xcodegen + restore-keys: | + brew-${{ runner.os }}-${{ runner.arch }}- + + - name: Install XcodeGen + run: brew list xcodegen >/dev/null 2>&1 || brew install xcodegen + + - name: Build and zip app + run: make zip + + - name: Publish GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + TAG="${GITHUB_REF_NAME}" + TITLE="CheckUsage ${TAG#v}" + NOTES="See [CHANGELOG.md](https://github.com/sysrootix/check-usage/blob/${TAG}/CHANGELOG.md) for what shipped." + if gh release view "$TAG" >/dev/null 2>&1; then + gh release upload "$TAG" dist/CheckUsage.app.zip --clobber + else + gh release create "$TAG" dist/CheckUsage.app.zip \ + --title "$TITLE" \ + --notes "$NOTES" + fi diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5104fe4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,38 @@ +# Changelog + +All notable changes to CheckUsage are documented in this file. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Community health files: security policy, Contributor Covenant, issue and pull-request templates +- Architecture notes for contributors (`docs/ARCHITECTURE.md`) +- GitHub Actions release workflow: tag `v*` builds `CheckUsage.app.zip` and attaches it to the GitHub Release +- Settings → About links to the public repository, issue tracker, and license +- Additional offline parser fixtures (Claude, Codex, Cursor, Copilot) and forecast / reset-copy edge cases + +### Changed + +- CI cancels stale runs, keeps `macos-15` + Makefile as the source of truth, and uploads the built app only from `main` + +## [1.0.0] - 2026-08-29 + +First public release. + +### Added + +- Native Swift 6 menubar extra for macOS 14+: edge pill with provider rings and a detail popover +- Usage meters for Claude, Codex, Cursor, Copilot, Gemini CLI, Grok, Antigravity, OpenCode, OpenRouter, DeepSeek, and Z.ai / GLM +- Reuses official-app sessions (Keychain / local auth files). No extra login. No telemetry +- Spend-pace card and burn forecast against the billing cycle +- Optional 70% / 90% notifications, remaining percent on rings, menu-bar worst remaining percent +- Widget lock, snap-to-edge, tuck-off-edge, custom hotkeys (`⌥⌘U` / `⌥⌘L` by default) +- UI and docs in English, Russian, Simplified Chinese, Japanese, German, Spanish, French, Brazilian Portuguese, and Korean +- `make test` / `make app` and parser fixtures under `Tests/CheckUsageTests` + +[Unreleased]: https://github.com/sysrootix/check-usage/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/sysrootix/check-usage/releases/tag/v1.0.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..19b87f3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,134 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +sysrootix@gmail.com. All complaints will be reviewed and investigated promptly +and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +Security vulnerabilities are a separate process — see [SECURITY.md](SECURITY.md). +Do not mix exploit details or credentials into a Code of Conduct report. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5e9499e..019e1f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,50 @@ # Contributing +Please be kind. This project follows the [Contributor Covenant](CODE_OF_CONDUCT.md). +Security reports go to **sysrootix@gmail.com** — see [SECURITY.md](SECURITY.md), not a public issue. + +Architecture (App / Providers / parsers / credentials / UI) is in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). + +## Branch naming + +Use a short prefix and a kebab-case topic: + +- `fix/cursor-reset-date` +- `feat/provider-name` +- `docs/architecture` +- `test/codex-fixtures` + +Fork, push the branch, and open a pull request against `main`. + +## Build + +Makefile targets are the source of truth (local and CI): + +```bash +make test # regenerate the Xcode project, run unit tests +make app # Release .app in dist/ +make zip # dist/CheckUsage.app.zip (used by the release workflow) +``` + +Requires macOS 14+, Xcode 16+ / Swift 6, and [XcodeGen](https://github.com/yonaskolb/XcodeGen) (`brew install xcodegen`). + +## Pull request checklist + +- [ ] `make test` passes on your Mac +- [ ] `make app` if you touched UI, resources, or the bundle +- [ ] No secrets (see below) +- [ ] Parser or copy changes have fixtures / unit tests +- [ ] New strings are added to **every** language table in `L10n.swift` / `ExtraL10n.swift` (English is the runtime fallback, not an excuse to skip tables) +- [ ] User-facing changes listed under **Unreleased** in [CHANGELOG.md](CHANGELOG.md) + +Use the GitHub PR template. Keep commits short and in English; say *why*, not just *what*. + +## Never commit secrets + +Do not add tokens, cookies, Keychain dumps, `.env`, `auth.json`, `.credentials.json`, `hosts.yml`, `state.vscdb`, or crash logs that show a live credential. Test payloads must be synthetic. CheckUsage must never log tokens. + +If you paste a secret by mistake, rotate it at the provider and tell the maintainer privately ([SECURITY.md](SECURITY.md)). + ## Add a provider 1. Add a case to `ProviderID`. @@ -9,13 +54,26 @@ 5. Add a fixture test with a real-looking payload. 6. Never log tokens, cookies, or Keychain blobs. +New providers that need reverse-engineering a live unofficial API should be discussed in an issue first. Do not attach credentials. + ## Localization -Strings live in `L10n.swift`. Add the same key to every language table. If you skip a language, English is the fallback. +Strings live in `L10n.swift` and `ExtraL10n.swift`. Add the same key to every language table. If you skip a language, English is the fallback. -## Build +## Cut a release + +1. Move **Unreleased** notes in [CHANGELOG.md](CHANGELOG.md) into a new `## [X.Y.Z] - YYYY-MM-DD` section and add compare links. +2. Bump `CFBundleShortVersionString` / `MARKETING_VERSION` and `CFBundleVersion` / `CURRENT_PROJECT_VERSION` in `Resources/Info.plist` and `project.yml`. +3. Merge to `main`. +4. Tag and push: ```bash -make test -make app +git tag v1.x.x +git push origin v1.x.x ``` + +(`git push origin main --tags` also works.) + +5. [`.github/workflows/release.yml`](.github/workflows/release.yml) runs on `v*` tags: `make zip`, then creates the GitHub Release if it is missing and attaches `CheckUsage.app.zip`. + +The public build is **ad-hoc signed**. Notarization / Developer ID is out of scope until an Apple Developer account is configured for this repo. diff --git a/Makefile b/Makefile index fffaf34..bdfdc1a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: project icon test app clean +.PHONY: project icon test app zip clean project: icon xcodegen generate @@ -28,5 +28,10 @@ app: project cp -R build/Products/Release/CheckUsage.app dist/CheckUsage.app @echo "Built dist/CheckUsage.app" +zip: app + rm -f dist/CheckUsage.app.zip + ditto -c -k --keepParent dist/CheckUsage.app dist/CheckUsage.app.zip + @echo "Built dist/CheckUsage.app.zip" + clean: rm -rf build dist CheckUsage.xcodeproj .swiftpm diff --git a/README.md b/README.md index c3facac..ec2d9b6 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,25 @@ # CheckUsage +[![CI](https://github.com/sysrootix/check-usage/actions/workflows/ci.yml/badge.svg)](https://github.com/sysrootix/check-usage/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![macOS 14+](https://img.shields.io/badge/macOS-14%2B-black.svg)](https://github.com/sysrootix/check-usage#requirements) +[![Swift 6](https://img.shields.io/badge/Swift-6-F05138.svg)](https://www.swift.org) +[![Release v1.0.0](https://img.shields.io/github/v/release/sysrootix/check-usage?label=release)](https://github.com/sysrootix/check-usage/releases/tag/v1.0.0) + **Language:** [English](README.md) · [Русский](docs/i18n/ru.md) · [简体中文](docs/i18n/zh-Hans.md) · [日本語](docs/i18n/ja.md) · [Deutsch](docs/i18n/de.md) · [Español](docs/i18n/es.md) · [Français](docs/i18n/fr.md) · [Português (Brasil)](docs/i18n/pt-BR.md) · [한국어](docs/i18n/ko.md) Usage limits for the AI tools already signed in on your Mac — a slim **edge pill** with circular rings, plus a detail popover. No extra login. No telemetry. +**Status:** actively maintained. Most subscription meters use unofficial endpoints the official apps already call; they can change without notice. See [SECURITY.md](SECURITY.md) and [CHANGELOG.md](CHANGELOG.md). +

CheckUsage pill and Cursor limits popover

+## Why CheckUsage + +Claude, Cursor, Codex, Copilot, Gemini, Grok and the rest each hide remaining quota in a different dashboard. If you run several agents on one Mac, you find out you are out of limit only after a request fails. CheckUsage is a glanceable instrument: it reuses the sessions already on this Mac and puts the meters on the screen edge. It is not a cost ledger and it does not scrape local chat transcripts. + CheckUsage lives in the menu bar and can pin a dark pill to any screen edge. Each ring is a real provider mark. Hover a ring to peek at the windows; click to pin. The popover shows session / weekly / plan usage, when it resets, and a spend-pace card: how much of the cycle is gone, whether you’re ahead of the plan, and the date this rate would empty the limit. **Defaults:** widget top-right and **locked**, close on outside click, `⌥⌘U` toggles the widget, `⌥⌘L` opens the last limits. Unlock to drag; it snaps to an edge. Cursor’s reset is the monthly billing date, not “this Saturday”. @@ -109,17 +121,32 @@ English · Русский · 简体中文 · 日本語 · Deutsch · Español · - No analytics, crash reporters, or accounts. - Settings live in `~/Library/Application Support/CheckUsage/settings.json`. Reinstalling the app does not delete that file. +How to report a vulnerability: [SECURITY.md](SECURITY.md). + +## Roadmap + +Ideas and provider-breakage reports live in [GitHub Issues](https://github.com/sysrootix/check-usage/issues). Prefer an issue before reverse-engineering a new unofficial API. Never attach tokens. + ## Development ```bash make project # regenerate CheckUsage.xcodeproj make test make app # release .app in dist/ +make zip # dist/CheckUsage.app.zip (also built on v* tags) ``` Parser fixtures live in `Tests/CheckUsageTests`. Provider HTTP is isolated in `UsageService`; JSON shapes are covered without hitting the network. -See [CONTRIBUTING.md](CONTRIBUTING.md). +See [CONTRIBUTING.md](CONTRIBUTING.md) for the PR checklist, branch naming, and how to cut a release (`git tag v1.x.x` then `git push --tags`). Layout of App / Providers / parsers / Settings: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). + +## Community + +- [Contributing](CONTRIBUTING.md) +- [Code of Conduct](CODE_OF_CONDUCT.md) +- [Security policy](SECURITY.md) +- [Architecture](docs/ARCHITECTURE.md) +- [Changelog](CHANGELOG.md) ## License diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..54d559f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,47 @@ +# Security policy + +## Reporting a vulnerability + +Please report security issues **privately** to **sysrootix@gmail.com**. + +Do not open a public GitHub issue, pull request, or discussion that includes: + +- access tokens, refresh tokens, cookies, or API keys +- Keychain dumps or `security find-generic-password` output +- contents of `auth.json`, `.credentials.json`, `hosts.yml`, or `state.vscdb` +- crash logs or screenshots that show a live credential + +Include enough detail to reproduce the issue (affected version, macOS version, provider if relevant, and a **redacted** fixture). Encrypted mail is welcome but not required. + +You should hear back within a few days. If the report is in scope, a fix will be prepared before any public write-up. + +## What this app does with secrets + +CheckUsage is a local menubar widget. It **reuses** sessions the official Claude / Cursor / Codex / Copilot / Gemini / Grok (and related) apps already stored on this Mac. It does **not** create a new login for those tools and it does **not** refresh their OAuth tokens. + +- Tokens are sent only to the provider that issued them. +- Pasted OpenRouter / DeepSeek / Z.ai keys are stored in the Keychain service `app.checkusage.secrets`. +- Usage JSON is parsed in-process. Tokens must never be logged, written to Settings, or uploaded as CI artifacts. + +See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the credential-read map. + +## In scope + +- Token, cookie, or API-key leakage (logs, settings files, crash reports, GitHub artifacts) +- Sending a credential to a host other than the issuing provider +- Reading local files beyond the documented credential locations +- Privilege issues in the shipped `.app` that expose Keychain items to other apps unintentionally + +## Out of scope + +- **Keychain reuse itself.** Reading `Claude Code-credentials`, Cursor `state.vscdb`, `gh` / Codex / Gemini files is the product. That is not a vulnerability unless those secrets leave the machine incorrectly. +- **Unofficial usage APIs** changing, rate-limiting, or returning wrong numbers. Most subscription meters call the same unofficial endpoints the official apps already call. They can break without notice. File a regular bug (no secrets) if a parser needs an update. +- Quota accuracy or billing disputes with a provider +- Ad-hoc code signing / Gatekeeper prompts (the public build is not Developer ID notarized) +- Social engineering, phishing the maintainer, or issues that require a stolen Mac unlock + +## Maintainer rules + +- Never commit secrets. `.env`, keychains, and provisioning profiles are gitignored. +- CI must not print tokens. Test fixtures must be synthetic. +- Parser unit tests must not touch the network. diff --git a/Sources/CheckUsage/App/AppInfo.swift b/Sources/CheckUsage/App/AppInfo.swift new file mode 100644 index 0000000..40354a6 --- /dev/null +++ b/Sources/CheckUsage/App/AppInfo.swift @@ -0,0 +1,12 @@ +import Foundation + +enum AppInfo { + static var shortVersion: String { + Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + ?? "1.0.0" + } + + static let githubURL = URL(string: "https://github.com/sysrootix/check-usage")! + static let issuesURL = URL(string: "https://github.com/sysrootix/check-usage/issues")! + static let licenseURL = URL(string: "https://github.com/sysrootix/check-usage/blob/main/LICENSE")! +} diff --git a/Sources/CheckUsage/L10n/ExtraL10n.swift b/Sources/CheckUsage/L10n/ExtraL10n.swift index f640e4e..df8017c 100644 --- a/Sources/CheckUsage/L10n/ExtraL10n.swift +++ b/Sources/CheckUsage/L10n/ExtraL10n.swift @@ -87,6 +87,9 @@ enum ExtraL10n { "auto_hide": "Tuck widget off the edge", "auto_hide_off": "Keep widget on screen", "auto_hide_help": "A small arrow stays on the edge. Hover to slide the widget out; move away and it tucks back.", + "github_repo": "GitHub", + "github_issues": "Report an issue", + "github_license": "License", ], "ru": [ "widget": "Виджет", @@ -173,6 +176,9 @@ enum ExtraL10n { "auto_hide": "Прятать виджет за край", "auto_hide_off": "Не прятать за край", "auto_hide_help": "На краю остаётся стрелка. Навели мышь — выезжает, увели — уезжает обратно.", + "github_repo": "GitHub", + "github_issues": "Сообщить о проблеме", + "github_license": "Лицензия", ], "zh-Hans": [ "widget": "小组件", @@ -248,6 +254,9 @@ enum ExtraL10n { "auto_hide": "将小组件藏到屏幕边缘", "auto_hide_off": "保持小组件在屏幕上", "auto_hide_help": "边缘留下箭头。悬停滑出,移开后收起。", + "github_repo": "GitHub", + "github_issues": "反馈问题", + "github_license": "许可证", ], "ja": [ "widget": "ウィジェット", @@ -323,6 +332,9 @@ enum ExtraL10n { "auto_hide": "端に隠す", "auto_hide_off": "画面上に置く", "auto_hide_help": "端に矢印が残ります。重ねると出て、離すと隠れます。", + "github_repo": "GitHub", + "github_issues": "問題を報告", + "github_license": "ライセンス", ], "de": [ "widget": "Widget", @@ -398,6 +410,9 @@ enum ExtraL10n { "auto_hide": "Widget hinter den Rand schieben", "auto_hide_off": "Widget sichtbar lassen", "auto_hide_help": "Am Rand bleibt ein Pfeil. Darüberfahren holt ihn hervor, Wegfahren versteckt ihn.", + "github_repo": "GitHub", + "github_issues": "Problem melden", + "github_license": "Lizenz", ], "es": [ "widget": "Widget", @@ -473,6 +488,9 @@ enum ExtraL10n { "auto_hide": "Esconder el widget en el borde", "auto_hide_off": "Dejar el widget en pantalla", "auto_hide_help": "Queda una flecha en el borde. Al pasar el ratón sale; al alejarlo se esconde.", + "github_repo": "GitHub", + "github_issues": "Informar de un problema", + "github_license": "Licencia", ], "fr": [ "widget": "Widget", @@ -548,6 +566,9 @@ enum ExtraL10n { "auto_hide": "Glisser le widget hors de l’écran", "auto_hide_off": "Garder le widget visible", "auto_hide_help": "Une flèche reste au bord. Survol pour le sortir, souris ailleurs pour le ranger.", + "github_repo": "GitHub", + "github_issues": "Signaler un problème", + "github_license": "Licence", ], "pt-BR": [ "widget": "Widget", @@ -623,6 +644,9 @@ enum ExtraL10n { "auto_hide": "Esconder o widget na borda", "auto_hide_off": "Manter o widget na tela", "auto_hide_help": "Fica uma seta na borda. Passe o mouse para sair; afaste para esconder.", + "github_repo": "GitHub", + "github_issues": "Relatar um problema", + "github_license": "Licença", ], "ko": [ "widget": "위젯", @@ -698,6 +722,9 @@ enum ExtraL10n { "auto_hide": "화면 끝으로 숨기기", "auto_hide_off": "화면에 그대로 두기", "auto_hide_help": "가장자리에 화살표가 남습니다. 올리면 나오고, 치우면 다시 숨습니다.", + "github_repo": "GitHub", + "github_issues": "이슈 등록", + "github_license": "라이선스", ], ] } diff --git a/Sources/CheckUsage/UI/SettingsView.swift b/Sources/CheckUsage/UI/SettingsView.swift index f8be279..e43db12 100644 --- a/Sources/CheckUsage/UI/SettingsView.swift +++ b/Sources/CheckUsage/UI/SettingsView.swift @@ -232,10 +232,13 @@ struct SettingsView: View { } } Section(L10n.t("about")) { - LabeledContent("CheckUsage", value: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0") + LabeledContent("CheckUsage", value: AppInfo.shortVersion) Text(L10n.t("privacy_note")) .font(.callout) .foregroundStyle(.secondary) + Link(L10n.t("github_repo"), destination: AppInfo.githubURL) + Link(L10n.t("github_issues"), destination: AppInfo.issuesURL) + Link(L10n.t("github_license"), destination: AppInfo.licenseURL) } Section(L10n.t("settings_file")) { Text(settings.settingsFilePath) diff --git a/Tests/CheckUsageTests/AppInfoTests.swift b/Tests/CheckUsageTests/AppInfoTests.swift new file mode 100644 index 0000000..4e8832f --- /dev/null +++ b/Tests/CheckUsageTests/AppInfoTests.swift @@ -0,0 +1,30 @@ +import XCTest +@testable import CheckUsage + +final class AppInfoTests: XCTestCase { + override func tearDown() { + L10n.language = .en + super.tearDown() + } + + func testPublicLinksStayOnGitHub() { + XCTAssertEqual(AppInfo.githubURL.host, "github.com") + XCTAssertEqual(AppInfo.githubURL.path, "/sysrootix/check-usage") + XCTAssertEqual(AppInfo.issuesURL.path, "/sysrootix/check-usage/issues") + XCTAssertTrue(AppInfo.licenseURL.path.hasSuffix("/LICENSE")) + XCTAssertFalse(AppInfo.shortVersion.isEmpty) + } + + func testAboutKeysExistInEveryLanguage() { + let keys = ["github_repo", "github_issues", "github_license"] + for language in [AppLanguage.en, .ru, .zhHans, .ja, .de, .es, .fr, .ptBR, .ko] { + L10n.language = language + for key in keys { + let value = L10n.t(key) + XCTAssertNotEqual(value, key, "missing \(key) for \(language.rawValue)") + XCTAssertFalse(value.isEmpty) + } + } + L10n.language = .en + } +} \ No newline at end of file diff --git a/Tests/CheckUsageTests/BurnForecastTests.swift b/Tests/CheckUsageTests/BurnForecastTests.swift index b4dd42d..bee815c 100644 --- a/Tests/CheckUsageTests/BurnForecastTests.swift +++ b/Tests/CheckUsageTests/BurnForecastTests.swift @@ -51,4 +51,65 @@ final class BurnForecastTests: XCTestCase { let end = now.addingTimeInterval(-2 * 86400) XCTAssertNil(BurnForecast.make(usedPercent: 40, cycleStart: start, cycleEnd: end, now: now)) } + + func testHalfwayFiftyPercentIsOnTrack() { + let now = Date(timeIntervalSince1970: 1_788_249_600) + let start = now.addingTimeInterval(-15 * 86400) + let end = now.addingTimeInterval(15 * 86400) + let forecast = BurnForecast.make(usedPercent: 50, cycleStart: start, cycleEnd: end, now: now) + XCTAssertEqual(forecast?.kind, .onTrack) + XCTAssertEqual(forecast?.expectedPercentByNow ?? 0, 50, accuracy: 3) + XCTAssertEqual(forecast?.tone, .good) + } + + func testHalfwaySeventyPercentIsHot() { + let now = Date(timeIntervalSince1970: 1_788_249_600) + let start = now.addingTimeInterval(-15 * 86400) + let end = now.addingTimeInterval(15 * 86400) + let forecast = BurnForecast.make(usedPercent: 70, cycleStart: start, cycleEnd: end, now: now) + XCTAssertEqual(forecast?.kind, .hot) + XCTAssertEqual(forecast?.tone, .warn) + XCTAssertNotNil(forecast?.projectedEmpty) + } + + func testObservedDeltaWithoutCycleStart() { + let now = Date(timeIntervalSince1970: 1_788_249_600) + let firstSeen = now.addingTimeInterval(-2 * 86400) + let end = now.addingTimeInterval(28 * 86400) + let forecast = BurnForecast.make( + usedPercent: 20, + cycleStart: nil, + cycleEnd: end, + now: now, + firstSeen: firstSeen, + firstPercent: 5 + ) + XCTAssertNotNil(forecast) + XCTAssertEqual(forecast?.dailyBurn ?? 0, 7.5, accuracy: 0.2) + } + + func testInferredStartUsesFiveHourWindow() { + let now = Date(timeIntervalSince1970: 1_788_249_600) + let end = now.addingTimeInterval(4 * 3600) + let start = BurnForecast.inferredStart(end: end, now: now) + XCTAssertEqual(start.timeIntervalSince(end), -5 * 3600, accuracy: 1) + } + + func testCycleEndedWithinGraceStillForecasts() { + let now = Date(timeIntervalSince1970: 1_788_249_600) + let start = now.addingTimeInterval(-30 * 86400) + let end = now.addingTimeInterval(-20 * 60) + let forecast = BurnForecast.make(usedPercent: 40, cycleStart: start, cycleEnd: end, now: now) + XCTAssertNotNil(forecast) + } + + func testExactlyEmptyMarksDepletedNow() { + let now = Date(timeIntervalSince1970: 1_788_249_600) + let start = now.addingTimeInterval(-10 * 86400) + let end = now.addingTimeInterval(20 * 86400) + let forecast = BurnForecast.make(usedPercent: 100, cycleStart: start, cycleEnd: end, now: now) + XCTAssertEqual(forecast?.kind, .depleted) + XCTAssertEqual(forecast?.projectedEmpty, now) + XCTAssertEqual(forecast?.tone, .critical) + } } diff --git a/Tests/CheckUsageTests/ResetCopyTests.swift b/Tests/CheckUsageTests/ResetCopyTests.swift index 0995121..7e6b736 100644 --- a/Tests/CheckUsageTests/ResetCopyTests.swift +++ b/Tests/CheckUsageTests/ResetCopyTests.swift @@ -52,4 +52,36 @@ final class ResetCopyTests: XCTestCase { XCTAssertGreaterThan(next, now) XCTAssertLessThan(next.timeIntervalSince(now), 40 * 86400) } + + func testPastResetAsksToRefresh() { + let now = Date(timeIntervalSince1970: 1_782_950_400) + XCTAssertEqual(ResetCopy.format(reset: now.addingTimeInterval(-60), now: now), "Refresh") + XCTAssertNil(ResetCopy.compact(reset: now.addingTimeInterval(-60), now: now)) + } + + func testMissingResetCopy() { + XCTAssertEqual(ResetCopy.format(reset: nil), "No reset time") + XCTAssertNil(ResetCopy.compact(reset: nil)) + } + + func testMinutesStayRelative() { + let now = Date(timeIntervalSince1970: 1_782_950_400) + let text = ResetCopy.format(reset: now.addingTimeInterval(45 * 60), now: now) + XCTAssertTrue(text.contains("45"), text) + XCTAssertTrue(text.lowercased().contains("min"), text) + } + + func testCompactBoundaryIsSixHours() { + let now = Date(timeIntervalSince1970: 1_782_950_400) + XCTAssertEqual(ResetCopy.compact(reset: now.addingTimeInterval(30), now: now), "1m") + XCTAssertEqual(ResetCopy.compact(reset: now.addingTimeInterval(5 * 3600 + 59 * 60), now: now), "5h") + XCTAssertNil(ResetCopy.compact(reset: now.addingTimeInterval(6 * 3600), now: now)) + } + + func testUpdatedJustNowAndHours() { + let now = Date(timeIntervalSince1970: 1_782_950_400) + XCTAssertEqual(ResetCopy.updated(now.addingTimeInterval(-20), now: now), "Just now") + let hours = ResetCopy.updated(now.addingTimeInterval(-3 * 3600), now: now) + XCTAssertTrue(hours.contains("3"), hours) + } } diff --git a/Tests/CheckUsageTests/UsageParserTests.swift b/Tests/CheckUsageTests/UsageParserTests.swift index eec8219..a059e7b 100644 --- a/Tests/CheckUsageTests/UsageParserTests.swift +++ b/Tests/CheckUsageTests/UsageParserTests.swift @@ -189,4 +189,126 @@ final class UsageParserTests: XCTestCase { XCTAssertEqual(UsageTone.from(percent: 73), .warn) XCTAssertEqual(UsageTone.from(percent: 95), .critical) } + + func testClaudeOpusSonnetAndExtraUsage() throws { + L10n.language = .en + let json = try JSONValue.parse(Data(""" + { + "five_hour": { "utilization": 88.4, "resets_at": "2026-08-29T10:00:00Z" }, + "seven_day": { "utilization": 31.2, "resets_at": "2026-09-04T14:00:00Z" }, + "seven_day_opus": { "utilization": 64, "resets_at": "2026-09-04T14:00:00Z" }, + "seven_day_sonnet": { "percent": 9, "resets_at": "2026-09-04T14:00:00Z" }, + "extra_usage": { "is_enabled": true, "used_credits": 1250, "monthly_limit": 5000 } + } + """.utf8)) + let snapshot = UsageParsers.claude(json) + XCTAssertEqual(snapshot.windows.count, 5) + XCTAssertEqual(snapshot.windows[0].usedPercent, 88.4, accuracy: 0.01) + XCTAssertEqual(snapshot.windows[2].title, "Weekly Opus") + XCTAssertEqual(snapshot.windows[3].usedPercent, 9, accuracy: 0.01) + XCTAssertEqual(snapshot.windows[4].id, "extra") + XCTAssertEqual(snapshot.windows[4].usedPercent, 25, accuracy: 0.01) + XCTAssertEqual(snapshot.windows[4].footnote, "$12.50 / $50.00") + } + + func testClaudeDisabledExtraUsageIsIgnored() throws { + let json = try JSONValue.parse(Data(""" + { + "five_hour": { "utilization": 10 }, + "extra_usage": { "is_enabled": false, "used_credits": 99, "monthly_limit": 5000 } + } + """.utf8)) + let snapshot = UsageParsers.claude(json) + XCTAssertEqual(snapshot.windows.count, 1) + XCTAssertFalse(snapshot.windows.contains(where: { $0.id == "extra" })) + } + + func testCodexCamelCaseExtrasAndCredits() throws { + L10n.language = .en + let json = try JSONValue.parse(Data(""" + { + "plan_type": "pro", + "rateLimits": { + "primary": { "usedPercent": 67.5, "windowDurationMins": 300, "resetsAt": 1782770922 }, + "secondary": { "used_percent": 18, "limit_window_seconds": 604800, "reset_after_seconds": 400000 } + }, + "additional_rate_limits": [ + { "limit_name": "code_review", "primary_window": { "used_percent": 5, "limit_window_seconds": 86400 } } + ], + "credits": { "has_credits": true, "balance": 42 } + } + """.utf8)) + let snapshot = UsageParsers.codex(json) + XCTAssertEqual(snapshot.planName, "pro") + XCTAssertEqual(snapshot.windows.count, 4) + XCTAssertEqual(snapshot.windows[0].usedPercent, 67.5, accuracy: 0.01) + XCTAssertEqual(snapshot.windows[0].title, "Current session") + XCTAssertEqual(snapshot.windows[1].title, "Weekly") + XCTAssertNotNil(snapshot.windows[1].resetsAt) + XCTAssertEqual(snapshot.windows[2].title, "code_review") + XCTAssertEqual(snapshot.windows[2].usedPercent, 5, accuracy: 0.01) + XCTAssertEqual(snapshot.windows[3].id, "credits") + XCTAssertEqual(snapshot.windows[3].footnote, "42") + } + + func testCursorIndividualUsageAndMillisCycle() throws { + let fetched = DateParser.iso("2026-08-15T12:00:00Z")! + let json = try JSONValue.parse(Data(""" + { + "billingCycleEndMs": 1788220800000, + "periodStart": "2026-08-01T00:00:00Z", + "planName": "Pro", + "individualUsage": { + "plan": { "used": 800, "limit": 2000 } + } + } + """.utf8)) + let snapshot = UsageParsers.cursor(json, fetchedAt: fetched) + XCTAssertEqual(snapshot.planName, "Pro") + XCTAssertEqual(snapshot.primaryPercent, 40, accuracy: 0.01) + XCTAssertEqual(snapshot.windows.count, 1) + XCTAssertEqual(snapshot.cycleStart, DateParser.iso("2026-08-01T00:00:00Z")) + XCTAssertEqual(snapshot.cycleEnd, DateParser.iso("2026-09-01T00:00:00Z")) + XCTAssertEqual(snapshot.forecastWindow?.id, "total") + } + + func testCursorTinyPercentIsAtLeastOne() { + XCTAssertEqual(UsageParsers.cursorVisiblePercent(0.4), 1, accuracy: 0.01) + XCTAssertEqual(UsageParsers.cursorVisiblePercent(0), 0, accuracy: 0.01) + XCTAssertEqual(UsageParsers.cursorVisiblePercent(12.2), 12.2, accuracy: 0.01) + } + + func testCopilotChatCompletionsAndEntitlementFallback() throws { + L10n.language = .en + let json = try JSONValue.parse(Data(""" + { + "copilot_plan": "pro+", + "quota_reset_date": "2026-09-01T00:00:00Z", + "quota_snapshots": { + "premium_interactions": { "entitlement": 1500, "remaining": 375 }, + "chat": { "percent_remaining": 80, "unlimited": false }, + "completions": { "unlimited": true, "percent_remaining": 100 } + } + } + """.utf8)) + let snapshot = UsageParsers.copilot(json) + XCTAssertEqual(snapshot.planName, "pro+") + XCTAssertEqual(snapshot.windows.count, 2) + XCTAssertEqual(snapshot.primaryPercent, 75, accuracy: 0.01) + XCTAssertEqual(snapshot.windows[0].title, "Premium") + XCTAssertEqual(snapshot.windows[1].title, "Chat") + XCTAssertEqual(snapshot.windows[1].usedPercent, 20, accuracy: 0.01) + XCTAssertEqual(snapshot.forecastWindow?.id, "premium") + XCTAssertNotNil(snapshot.cycleEnd) + } + + func testCopilotEmptySnapshotsStaySignedInShape() throws { + let json = try JSONValue.parse(Data(""" + { "copilot_plan": "free", "quota_snapshots": {} } + """.utf8)) + let snapshot = UsageParsers.copilot(json) + XCTAssertEqual(snapshot.planName, "free") + XCTAssertTrue(snapshot.windows.isEmpty) + XCTAssertEqual(snapshot.primaryPercent, 0, accuracy: 0.01) + } } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..362d2d0 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,71 @@ +# Architecture + +CheckUsage is a single macOS app target (`LSUIElement` — no Dock icon) plus a unit-test bundle. XcodeGen generates `CheckUsage.xcodeproj` from [`project.yml`](../project.yml). **Makefile targets are the source of truth** for local and CI builds (`make test`, `make app`, `make zip`). + +``` +Sources/CheckUsage +├── App/ lifecycle, menubar extra, refresh loop, panel chrome +├── Providers/ HTTP fetch + credential lookup; pure JSON parsers +├── Services/ Keychain/files, HTTP, settings, hotkeys, alerts +├── Models/ ProviderID, snapshots, burn forecast, layout math +├── UI/ edge pill, detail popover, Settings +└── L10n/ string tables (system language or a Settings lock) +``` + +## App + +- [`CheckUsageApp`](../Sources/CheckUsage/App/CheckUsageApp.swift) — `MenuBarExtra`, accessory activation policy +- [`AppModel`](../Sources/CheckUsage/App/AppModel.swift) — enabled providers, refresh timer, worst remaining percent +- [`PanelController`](../Sources/CheckUsage/App/PanelController.swift) / [`WidgetChrome`](../Sources/CheckUsage/App/WidgetChrome.swift) — borderless edge window and popover placement + +The app never scrapes local chat transcripts. It only asks each provider’s usage endpoint and renders the result. + +## Providers + +[`UsageService`](../Sources/CheckUsage/Providers/UsageService.swift) is the only type that talks to the network. For each `ProviderID` it: + +1. Reads credentials that the **official app or CLI already stored** (or a key the user pasted in Settings) +2. Calls that provider’s usage URL +3. Hands the JSON to [`UsageParsers`](../Sources/CheckUsage/Providers/UsageParsers.swift) + +Parsers are pure: `JSONValue` in, `QuotaSnapshot` out. No `URLSession`, no Keychain. That is why tests can cover Claude / Cursor / Codex / Copilot shapes without live network. + +Most subscription meters use **unofficial** endpoints the official clients already call. They can change without notice. OpenRouter, DeepSeek, and Z.ai use documented APIs. + +## Credentials (never stored anew for CLI sessions) + +[`SecretStore`](../Sources/CheckUsage/Services/SecretStore.swift) only **reads** existing material: + +| Provider | Where the token comes from | +|---|---| +| Claude | Keychain `Claude Code-credentials` or `~/.claude/.credentials.json` | +| Codex | `~/.codex/auth.json` or Keychain `Codex Auth` | +| Cursor | `state.vscdb` (`cursorAuth/accessToken`) or Keychain `cursor-access-token` | +| Copilot | `GH_TOKEN` / `~/.config/github-copilot/apps.json` / `gh` hosts.yml / Keychain `gh:github.com` | +| Gemini | `~/.gemini/oauth_creds.json` or Keychain `gemini-cli-oauth` | +| Grok | `~/.grok/auth.json` | +| Antigravity | Keychain `gemini` / `antigravity` (falls back to Gemini) | +| OpenCode | `~/.local/share/opencode/auth.json` | +| OpenRouter, DeepSeek, Z.ai | Keychain service `app.checkusage.secrets` (user-pasted in Settings) | + +CheckUsage does **not** refresh OAuth tokens that belong to Claude Code / Codex / Cursor. If a session expires, sign in again in the official app. Tokens are sent only to the issuer. Never log them. + +Settings themselves live in `~/Library/Application Support/CheckUsage/settings.json` ([`SettingsFile`](../Sources/CheckUsage/Services/SettingsFile.swift) / [`SettingsStore`](../Sources/CheckUsage/Services/SettingsStore.swift)). Reinstalling the `.app` does not delete that file. + +## UI / Settings + +- [`EdgePanelView`](../Sources/CheckUsage/UI/EdgePanelView.swift) — rings on the screen edge +- [`DetailPopoverView`](../Sources/CheckUsage/UI/DetailPopoverView.swift) — session / weekly / plan windows, reset copy, spend-pace card +- [`SettingsView`](../Sources/CheckUsage/UI/SettingsView.swift) — widget, providers, hotkeys, API keys, About + +About shows `CFBundleShortVersionString` from the bundle (see [`AppInfo`](../Sources/CheckUsage/App/AppInfo.swift)) and links to GitHub, Issues, and the license. + +Forecast math is isolated in [`BurnForecast`](../Sources/CheckUsage/Models/BurnForecast.swift). Reset phrasing is [`ResetCopy`](../Sources/CheckUsage/L10n/L10n.swift). + +## Adding a provider + +Follow [CONTRIBUTING.md](../CONTRIBUTING.md): new `ProviderID`, parser + fixture (no network), fetch in `UsageService` using credentials the official app already stored, Simple Icons SVG, no token logging. + +## Tests + +`Tests/CheckUsageTests` covers parsers, forecast, reset copy, settings file I/O, and layout math. Run `make test` on a Mac with Xcode 16+. Linux CI cannot run `xcodebuild`; GitHub Actions on `macos-15` is the gate. diff --git a/docs/i18n/de.md b/docs/i18n/de.md index b99a0af..38e02e4 100644 --- a/docs/i18n/de.md +++ b/docs/i18n/de.md @@ -12,6 +12,8 @@ CheckUsage sitzt in der Menüleiste und kann eine dunkle Pille an jeder Kante ha **Standard:** oben rechts und **gesperrt**, Schließen bei Klick daneben, `⌥⌘U` blendet das Widget, `⌥⌘L` öffnet die letzten Limits. Entsperren zum Ziehen — es rastet an einer Kante ein. Cursors Reset ist das monatliche Abrechnungsdatum, nicht „diesen Samstag“. +**Status:** wird aktiv gepflegt. Inoffizielle Zähler können ohne Vorankündigung brechen — siehe [SECURITY.md](../../SECURITY.md) und [CHANGELOG.md](../../CHANGELOG.md). + ## Was es ist Ein Blick-Instrument für alle, die mehrere KI-Coding-Agenten auf einem Mac fahren. Der knappste Rest kann in der Menüleiste stehen. Ringe an der Kante. Details per Hover oder Klick. Kein Kostenbuch, kein Auslesen lokaler Chats. @@ -97,6 +99,10 @@ make app Siehe [CONTRIBUTING.md](../../CONTRIBUTING.md). +## Dokumentation + +[Architektur](../ARCHITECTURE.md) · [Contributing](../../CONTRIBUTING.md) · [Sicherheit](../../SECURITY.md) · [Changelog](../../CHANGELOG.md) · [Issues / Roadmap](https://github.com/sysrootix/check-usage/issues) + ## Lizenz MIT. Namen und Marken der Anbieter gehören ihren Eigentümern. Marken auf der Leiste: SVGs von [Simple Icons](https://simpleicons.org). diff --git a/docs/i18n/es.md b/docs/i18n/es.md index a2b52d0..ea1ac11 100644 --- a/docs/i18n/es.md +++ b/docs/i18n/es.md @@ -12,6 +12,8 @@ CheckUsage vive en la barra de menú y puede fijar una píldora oscura en cualqu **Por defecto:** arriba a la derecha y **bloqueada**, se cierra al clic fuera, `⌥⌘U` muestra/oculta el widget, `⌥⌘L` abre los últimos límites. Desbloquea para arrastrar; se ajusta al borde. El reinicio de Cursor es la fecha de facturación mensual, no «este sábado». +**Estado:** se mantiene activamente. Los medidores no oficiales pueden romperse sin aviso; véase [SECURITY.md](../../SECURITY.md) y [CHANGELOG.md](../../CHANGELOG.md). + ## Qué es Un instrumento de un vistazo para quien corre varios agentes de IA en un Mac. El peor resto puede ir en la barra. Los anillos, en el borde. El detalle, al pasar o al clic. No es un libro de costes ni lee chats locales. @@ -97,6 +99,10 @@ make app Ver [CONTRIBUTING.md](../../CONTRIBUTING.md). +## Documentación + +[Arquitectura](../ARCHITECTURE.md) · [Contributing](../../CONTRIBUTING.md) · [Seguridad](../../SECURITY.md) · [Changelog](../../CHANGELOG.md) · [Issues / hoja de ruta](https://github.com/sysrootix/check-usage/issues) + ## Licencia MIT. Nombres y marcas de los proveedores pertenecen a sus dueños. Las marcas del panel usan SVG de [Simple Icons](https://simpleicons.org). diff --git a/docs/i18n/fr.md b/docs/i18n/fr.md index b33f371..92e7529 100644 --- a/docs/i18n/fr.md +++ b/docs/i18n/fr.md @@ -12,6 +12,8 @@ CheckUsage vit dans la barre des menus et peut coller une pilule sombre à n’i **Par défaut :** en haut à droite et **verrouillée**, fermeture au clic dehors, `⌥⌘U` affiche/masque le widget, `⌥⌘L` ouvre les derniers quotas. Déverrouillez pour glisser — elle s’aimante au bord. Le reset Cursor est la date de facturation mensuelle, pas « ce samedi ». +**Statut :** maintenu activement. Les compteurs non officiels peuvent casser sans préavis — voir [SECURITY.md](../../SECURITY.md) et [CHANGELOG.md](../../CHANGELOG.md). + ## Qu’est-ce que c’est Un instrument à coup d’œil pour qui fait tourner plusieurs agents IA sur un Mac. Le pire reste peut aller dans la barre. Les anneaux, au bord. Le détail, au survol ou au clic. Ce n’est ni un livre de comptes ni un parseur de chats locaux. @@ -97,6 +99,10 @@ make app Voir [CONTRIBUTING.md](../../CONTRIBUTING.md). +## Documentation + +[Architecture](../ARCHITECTURE.md) · [Contributing](../../CONTRIBUTING.md) · [Sécurité](../../SECURITY.md) · [Changelog](../../CHANGELOG.md) · [Issues / feuille de route](https://github.com/sysrootix/check-usage/issues) + ## Licence MIT. Noms et marques des fournisseurs appartiennent à leurs propriétaires. Marques du panneau : SVG [Simple Icons](https://simpleicons.org). diff --git a/docs/i18n/ja.md b/docs/i18n/ja.md index 0c64288..a4adc58 100644 --- a/docs/i18n/ja.md +++ b/docs/i18n/ja.md @@ -12,6 +12,8 @@ CheckUsage はメニューバーに常駐し、暗いピルを任意の端に固 **初期値:** 右上・**位置ロック**・外側クリックで閉じる。`⌥⌘U` でウィジェット、`⌥⌘L` で前回の上限。ロックを外すとドラッグでき、端に吸着します。Cursor のリセットは月次の課金日であり、「今週の土曜」ではありません。 +**ステータス:** 継続メンテナンス中。非公式メーターは予告なく壊れることがあります。[SECURITY.md](../../SECURITY.md) と [CHANGELOG.md](../../CHANGELOG.md) を参照。 + ## これは何か 複数の AI コーディングエージェントを一台の Mac で使う人向けの、一目で分かる計器です。メニューバーに最も厳しい残量、端にリング、ホバー/クリックで詳細。家計簿でも、ローカルのチャットログを読むツールでもありません。 @@ -97,6 +99,10 @@ make app [CONTRIBUTING.md](../../CONTRIBUTING.md) を参照。 +## ドキュメント + +[アーキテクチャ](../ARCHITECTURE.md) · [Contributing](../../CONTRIBUTING.md) · [セキュリティ](../../SECURITY.md) · [Changelog](../../CHANGELOG.md) · [Issues / ロードマップ](https://github.com/sysrootix/check-usage/issues) + ## ライセンス MIT。プロバイダー名とマークは各社に帰属。パネルのマークは [Simple Icons](https://simpleicons.org) の SVG です。 diff --git a/docs/i18n/ko.md b/docs/i18n/ko.md index d632237..7f19817 100644 --- a/docs/i18n/ko.md +++ b/docs/i18n/ko.md @@ -12,6 +12,8 @@ CheckUsage는 메뉴 막대에 상주하며 어두운 필을 아무 가장자리 **기본값:** 오른쪽 위, **위치 잠금**, 바깥을 클릭하면 닫힘. `⌥⌘U`는 위젯, `⌥⌘L`은 마지막 한도. 잠금을 풀면 드래그되며 가장자리에 붙습니다. Cursor 초기화는 월간 청구일이며 “이번 토요일”이 아닙니다. +**상태:** 계속 유지보수 중입니다. 비공식 미터는 예고 없이 깨질 수 있습니다. [SECURITY.md](../../SECURITY.md), [CHANGELOG.md](../../CHANGELOG.md)를 보세요. + ## 무엇인가 한 대의 Mac에서 여러 AI 코딩 에이전트를 쓰는 사람을 위한 한눈에 보는 계기입니다. 가장 빠듯한 잔량은 메뉴 막대에, 링은 가장자리에, 상세는 호버나 클릭으로. 가계부가 아니고 로컬 채팅을 읽지도 않습니다. @@ -97,6 +99,10 @@ make app [CONTRIBUTING.md](../../CONTRIBUTING.md)를 보세요. +## 문서 + +[아키텍처](../ARCHITECTURE.md) · [Contributing](../../CONTRIBUTING.md) · [보안](../../SECURITY.md) · [Changelog](../../CHANGELOG.md) · [Issues / 로드맵](https://github.com/sysrootix/check-usage/issues) + ## 라이선스 MIT. 제공자 이름과 마크는 각 소유자에게 있습니다. 패널 마크는 [Simple Icons](https://simpleicons.org) SVG입니다. diff --git a/docs/i18n/pt-BR.md b/docs/i18n/pt-BR.md index 407db94..974579a 100644 --- a/docs/i18n/pt-BR.md +++ b/docs/i18n/pt-BR.md @@ -12,6 +12,8 @@ O CheckUsage fica na barra de menus e pode pregar uma pílula escura em qualquer **Padrão:** canto superior direito e **travada**, fecha ao clicar fora, `⌥⌘U` mostra/oculta o widget, `⌥⌘L` abre os últimos limites. Destrave para arrastar — ela gruda na borda. O reset do Cursor é a data de cobrança mensal, não “este sábado”. +**Status:** mantido ativamente. Medidores não oficiais podem quebrar sem aviso — veja [SECURITY.md](../../SECURITY.md) e [CHANGELOG.md](../../CHANGELOG.md). + ## O que é Um instrumento de um olhar para quem roda vários agentes de IA num Mac. O pior restante pode ir na barra. Os anéis, na borda. O detalhe, no hover ou no clique. Não é livro de custos nem lê chats locais. @@ -97,6 +99,10 @@ make app Veja [CONTRIBUTING.md](../../CONTRIBUTING.md). +## Documentação + +[Arquitetura](../ARCHITECTURE.md) · [Contributing](../../CONTRIBUTING.md) · [Segurança](../../SECURITY.md) · [Changelog](../../CHANGELOG.md) · [Issues / roadmap](https://github.com/sysrootix/check-usage/issues) + ## Licença MIT. Nomes e marcas dos provedores pertencem aos donos. Marcas do painel: SVGs do [Simple Icons](https://simpleicons.org). diff --git a/docs/i18n/ru.md b/docs/i18n/ru.md index f76202a..5205733 100644 --- a/docs/i18n/ru.md +++ b/docs/i18n/ru.md @@ -12,6 +12,8 @@ CheckUsage живёт в строке меню и может закрепить **По умолчанию:** виджет справа сверху и **заблокирован**, закрытие по клику мимо, `⌥⌘U` показывает/прячет виджет, `⌥⌘L` открывает последние лимиты. Снимите блок, чтобы перетащить — пилюля прилипнет к краю. Сброс Cursor — дата месячного биллинга, не «эта суббота». +**Статус:** проект поддерживается. Неофициальные счётчики могут сломаться — см. [SECURITY.md](../../SECURITY.md) и [CHANGELOG.md](../../CHANGELOG.md). + ## Что это Прибор для тех, кто гоняет несколько AI-агентов на одном Mac. Худший остаток может быть в строке меню. Кольца — на краю. Детали — по наведению или клику. Это не бухгалтерская книга и не парсер локальных чатов. @@ -101,6 +103,10 @@ make app См. [CONTRIBUTING.md](../../CONTRIBUTING.md). +## Документы + +[Архитектура](../ARCHITECTURE.md) · [Contributing](../../CONTRIBUTING.md) · [Безопасность](../../SECURITY.md) · [Changelog](../../CHANGELOG.md) · [Issues / roadmap](https://github.com/sysrootix/check-usage/issues) + ## Лицензия MIT. Имена и знаки провайдеров принадлежат их владельцам. Марки на панели — SVG с [Simple Icons](https://simpleicons.org). diff --git a/docs/i18n/zh-Hans.md b/docs/i18n/zh-Hans.md index 0cad68d..683ef8c 100644 --- a/docs/i18n/zh-Hans.md +++ b/docs/i18n/zh-Hans.md @@ -12,6 +12,8 @@ CheckUsage 在菜单栏运行,也可把深色胶囊钉在任意边缘。每个 **默认:** 右上角、**锁定位置**、点击外部关闭,`⌥⌘U` 显示/隐藏胶囊,`⌥⌘L` 打开上次额度。解锁后可拖动并吸附到边缘。Cursor 的重置是月度账单日,不是「本周六」。 +**状态:** 持续维护。非官方用量接口可能随时失效,见 [SECURITY.md](../../SECURITY.md) 与 [CHANGELOG.md](../../CHANGELOG.md)。 + ## 这是什么 给同时跑多个 AI 编程助手的人用的一眼仪表。菜单栏可显示最紧的剩余百分比。圆环在边缘。细节靠悬停或点击。它不是账单本,也不会去扫本地聊天记录。 @@ -97,6 +99,10 @@ make app 见 [CONTRIBUTING.md](../../CONTRIBUTING.md)。 +## 文档 + +[架构](../ARCHITECTURE.md) · [贡献](../../CONTRIBUTING.md) · [安全](../../SECURITY.md) · [Changelog](../../CHANGELOG.md) · [Issues / 路线图](https://github.com/sysrootix/check-usage/issues) + ## 许可 MIT。服务商名称与标识归其所有者。面板图标来自 [Simple Icons](https://simpleicons.org)。