Skip to content
Closed
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
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,12 @@ NO-LOOPS.md
.handoff/
build/
.automation/

# TypeScript / Electron candidate output
node_modules/
.vite-plus/
release/
coverage/

# Generated from the existing macOS SF Symbol stand-ins.
Resources/*-symbol.png
59 changes: 59 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,67 @@ follows [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added

- Add a macOS TypeScript/Electron/React migration candidate and headless
schema-1 JSON CLI, with synthetic provider, account, history, pacing and IPC
tests. Use pnpm, Vite+, Tailwind, Vitest and electron-builder. Retain Swift as
the behavior oracle until native acceptance passes. No Electron release or
installed-app replacement is included.

### Changed

- Upgrade the Electron candidate's TypeScript checker from 6.0.3 to the native
7.0.2 compiler. Keep the existing typecheck, test and build commands.
- Record synthetic Claude and Codex source-scanner timings alongside the Swift
baseline. Counts and cache transitions pass; packaged-app performance, idle
RSS and native GUI acceptance remain open.
- Verify the current Apple silicon Electron candidate with 128 macOS fixtures,
TypeScript checking, production bundles, extracted ZIP signature checks and
the native schema-1 demo CLI. The candidate is ad-hoc signed and unpublished;
native GUI acceptance and cutover remain open.
- Replace the earlier React Native/Tauri Windows plan with the selected macOS
Electron direction. Windows implementation, installers, CI, testing and
release are paused until a test machine is available.
- Correct privacy documentation for Codex token/model ledger reads, transient
project paths, existing Grok/OpenCode keys, managed accounts and hourly
update checks. Provider files and credentials stay in the main process.

### Fixed

- Rescan corrupt Claude cache entries with non-finite costs or unsupported
dates. Ignore unsupported optional provider timestamps before JSON output.
- Use canonical temporary paths in candidate fixtures and demo commands.
Check the packaged executable's exact directory-entry spelling on both
case-sensitive and case-insensitive filesystems. Preserve symlink rejection.
- Remove unknown daily-history fields before saving or publishing renderer
state, while preserving larger historic totals and account separation.
- Keep readable sessions when a sibling disappears or cannot be opened.
Preserve Codex metadata/ledger fallbacks and propagate scan cancellation.
- Scope the reset-button setting to side-notch details and context menus;
retain flyout redemption with the same availability and confirmation checks.
- Restore 30-day rolling cost/token share bars and the existing adaptive
Claude, Antigravity, OpenCode Go and OpenRouter identity colours.
- Validate optional archive metadata and restore only known fields. Reject
reset redemption unless the credit is reported available and unexpired,
including after the confirmation dialog.
- Restore the primary Claude companion-file order and file modification time
fallback. Keep valid required quota when optional OpenRouter credits stall
or additional Codex model limits are malformed.
- Show today's usage counts and reading age, restore textual tray outage/reset
summaries, and adapt the existing Grok mark tint to light and dark appearance.
- Keep candidate service health visible when provider usage is hidden, with
buttons for the existing public status pages. Restore notch ambient ETA
from raw window pace and hide it when pacing is off.
- Invalidate live readings and reset confirmations when a candidate account
source changes. Reject late results and reset completion from the old source
while keeping dated slot archives and durable history.
- Measure shared cards at full height. Reject captures above 8,192 pixels with
an explicit error instead of exporting a cropped image.
- Hide candidate accounts whose configured directory disappears. Bind sharing
to the displayed card, hide forecasts when pacing is off, restore active-window
burn detail and bound tall notch cards to the display work area.

- `Scripts/make-app.sh` now downloads and verifies the prebuilt app by default,
so installation from a Git checkout does not need Swift. Source builds require
`--build-from-source`. Failed downloads or verification never trigger compilation.
Expand Down
68 changes: 64 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ pull request.

## Before you start

meterusage is a macOS 13+ Swift menu-bar app. The package uses Swift tools
version 5.9 and has no third-party package dependencies. Install Xcode Command
Line Tools before building:
The published app is a macOS 13+ Swift menu-bar app. A TypeScript/Electron
candidate is under development; see the candidate commands below. The package uses Swift tools
version 5.9 and has no third-party package dependencies. Swift source builds need Xcode Command Line Tools:

```sh
xcode-select --install
Expand Down Expand Up @@ -63,7 +63,8 @@ git fetch upstream
git rebase upstream/main
```

Run the test commands again after a rebase.
After a rebase, rerun checks affected by changed inputs and refresh independent
review. Reuse matching checks for unchanged artifacts and inputs.

## Commit messages

Expand Down Expand Up @@ -116,6 +117,65 @@ not move, and include the captures in the pull request. See

See [`docs/DEMO.md`](docs/DEMO.md) for the data and privacy rules.

## TypeScript Electron candidate

Use the Node and pnpm versions declared in `package.json`. Provider accounts
are optional; synthetic tests and demo mode require no login. Shared checks
can run on Linux. Packaging, system symbol export and native acceptance require
macOS with the existing Swift and signing tools.

```sh
pnpm install --frozen-lockfile
pnpm typecheck
pnpm test
pnpm build
meterusage_demo_root="$(node -p 'require("node:fs").realpathSync(require("node:os").tmpdir())')"
node dist/main/cli.cjs json --force --demo --candidate-profile "$meterusage_demo_root/meterusage-cli-demo"
```

The candidate profile must be an empty test directory on first use. It may be
reused for relaunch checks after the candidate creates its marker. All demo
preferences, caches, history and Electron state stay in that profile.
Use the canonical temporary root above. On macOS, `/tmp` and `/var` can be
symlink aliases; candidate mode rejects paths that follow a symlink.

On macOS, build and launch a separate candidate without installing it:

```sh
pnpm package:mac
./release/mac-arm64/MeterUsage.app/Contents/MacOS/meterusage --demo --candidate-profile /private/tmp/meterusage-native-demo
```

`package:mac` exports the existing SF Symbol stand-ins, builds the app and
packages an Apple silicon ZIP with publication disabled. It does not install
or replace an app. Do not upload this candidate as the current public release.
Windows packaging, CI, native testing and release remain paused.

electron-builder resolves `pnpm` from PATH when collecting production
dependencies. If an existing launcher incorrectly loads a standalone pnpm
executable as JavaScript, verify the executable directly, then put a `pnpm`
symlink to it in a separate candidate-local directory at the start of PATH.
Keep host launchers and installed dependencies unchanged. This PATH correction
still requires a new native packaging check.

Changes live in `src/domain/` (shared calculations), `src/main/` (provider
readers, persistence, coordination and native shell), `src/shared/` (IPC DTOs),
`src/renderer/` (React UI) and `tests/` (synthetic fixtures). Provider paths,
credentials and raw responses must stay out of renderer imports. Settings may
show a reduced directory label chosen by the user.

TypeScript tests and builds do not prove native parity. Before cutover, record
candidate identity/digest/signature, headless JSON, launch and relaunch,
settings/account isolation, offline failures, notification/login behavior,
sharing and updater paths. Capture two different-height provider cards on
macOS and confirm the top edge and strip stay fixed. Use only synthetic reset
credits. Keep the published app and its data untouched during candidate QA.
Retained Swift sources are the behavior oracle and rollback path. Run affected
Swift checks when those inputs change; reuse matching receipts otherwise.

See [ADR 0010](docs/adr/0010-macos-typescript-electron.md) and
[the migration plan](docs/plans/macos-electron-migration-plan.md).

## Where to make changes

- `Sources/MeterUsage/Services/` contains provider readers, pricing, and
Expand Down
37 changes: 29 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<p align="center">
<img src="https://img.shields.io/badge/platform-macOS%2013%2B-blue?style=flat-square" alt="Platform: macOS 13+">
<img src="https://img.shields.io/badge/privacy-100%25%20local%20%7C%20zero%20telemetry-brightgreen?style=flat-square" alt="Privacy: 100% local">
<img src="https://img.shields.io/badge/privacy-no%20telemetry-brightgreen?style=flat-square" alt="Privacy: no telemetry">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License: MIT">
</p>

Expand All @@ -25,7 +25,7 @@
* **Ambient Time-to-Empty**: Live depletion velocity against reset deadlines (`~47m left at current pace` / `Paced to last until reset`) directly in the menu bar and side notch.
* **Burn Attribution & Context Waste**: Token breakdown by project, model, and turns over the last 7 days across every token-bearing provider, plus cache-hit efficiency % and long-chat flags.
* **Durable Daily History**: Local summary store surviving CLI transcript purges and session cleanup.
* **100% Private & Zero Setup**: No accounts to connect, no passwords entered. Reads already-authenticated local CLI sessions and local SQLite/JSON logs on your machine.
* **Existing provider access**: No accounts to connect or passwords to enter. Uses existing provider CLIs, local SQLite/JSON stores and aggregate usage endpoints.
* **Agent Budget API**: Machine-readable JSON CLI (`meterusage json`) exporting burn rates, pacing, and time-to-empty for autonomous AI agents.

[Download MeterUsage 0.2.41 for Apple silicon](https://github.com/pekth/meterusage/releases/download/v0.2.41/MeterUsage-0.2.41.zip),
Expand All @@ -36,6 +36,27 @@ See [installation](#download-and-installation) for first-launch steps.

---

## macOS Electron migration candidate

The macOS app and schema-1 JSON CLI are being ported to TypeScript, Electron
and React. The candidate uses pnpm, Vite+, Tailwind, Vitest and electron-builder.
The downloadable 0.2.41 app above remains the Swift release. The Electron
candidate has not passed native macOS acceptance and is not a published release.

Provider readers and credentials stay in the main process. The sandboxed
renderer receives validated snapshots through a small IPC bridge. Existing
history/archive JSON, preference keys, managed account IDs and provider marks
remain compatibility targets. Windows implementation, installers, testing and
release are paused until a test machine is available.

See [the migration decision](docs/adr/0010-macos-typescript-electron.md),
[contributor commands](CONTRIBUTING.md#typescript-electron-candidate), and
[isolated demo testing](docs/DEMO.md#electron-candidate). Native tray, settings,
notifications, login items, sharing, updater and two different-height notch
captures are required before cutover.

---

## 📸 Showcase

### Ambient Time-To-Empty & Side Notch HUD
Expand Down Expand Up @@ -87,9 +108,9 @@ Toggle providers on or off, choose refresh cadence, and switch themes and the ac
| **OpenRouter** | ✅ | ✅ | ✅ | — | Public account API & `/api/v1/activity` telemetry |
| **Grok** | ✅ | ✅ | ✅ | — | CLI auth bearer token & session summaries |
| **OpenCode Go** | ✅ | ✅ | ✅ | — | Local `opencode db` read-only queries |
| **Cursor** | ✅ | ✅ | ✅ | — | Local sqlite state & token usage cache |
| **Copilot CLI** | ✅ | ✅ | ✅ | — | Local GitHub CLI auth token & token telemetry |
| **Gemini CLI** | ✅ | ✅ | ✅ | — | Local Gemini CLI session state |
| **Cursor** | Local snapshot | — | Snapshot-dependent | — | Existing local quota/usage JSON or presence check |
| **Copilot CLI** | Local snapshot | — | Snapshot-dependent | — | Existing local quota/usage JSON or presence check |
| **Gemini CLI** | Local snapshot | — | Snapshot-dependent | — | Existing local quota/usage JSON or presence check |

<small>*Claude publishes no public quota API; meterusage reads an on-disk JSON snapshot if a local companion writes one (see [docs/COMPANION.md](docs/COMPANION.md) and [`Scripts/claude-companion.sh`](Scripts/claude-companion.sh)).</small>

Expand Down Expand Up @@ -126,7 +147,7 @@ appear once the directory exists. Removing a row stops metering that account;
nothing in the directory is deleted.

Slots are named by your own label ("Codex · Work", a digit beside the mark in
the tray and notch); no account identifier is ever read or displayed. Each
the tray and notch); no provider account identifier is read or displayed. Each
account stays under its own keys in the Agent Budget API, which reports the
label in an additive `account` field.

Expand All @@ -136,9 +157,9 @@ label in an additive `account` field.

meterusage is built from the ground up to respect developer privacy:
* **No Network Man-in-the-Middle**: Reuses the authenticated CLI sessions already on your Mac.
* **No Prompts or Code Read**: Reads only numeric session metadata, token tallies, and timestamps. Project identifiers are strictly directory basenames. Never opens prompt contents, tool payloads, or file diffs.
* **Selective local reads**: Selects numeric session metadata, token tallies, model names and timestamps. Project directories become basenames. Message content, tool payloads and file diffs are skipped.
* **No Telemetry / Analytics**: Zero outgoing telemetry calls.
* **Sandboxed & Inspectable**: Full privacy architecture documented in [docs/PRIVACY.md](docs/PRIVACY.md).
* **Inspectable data boundary**: Full privacy architecture documented in [docs/PRIVACY.md](docs/PRIVACY.md).

---

Expand Down
15 changes: 15 additions & 0 deletions Scripts/electron/after-pack.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const { renameSync } = require("node:fs");
const { join } = require("node:path");
const { execFileSync } = require("node:child_process");

// executableName also changes electron-builder's bundle filename. Keep the
// product bundle name, then rename only its main executable before signing.
function renameExecutable(bundle, command = execFileSync) {
renameSync(join(bundle, "Contents/MacOS/MeterUsage"), join(bundle, "Contents/MacOS/meterusage"));
command("/usr/libexec/PlistBuddy", ["-c", "Set :CFBundleExecutable meterusage", join(bundle, "Contents/Info.plist")]);
}
module.exports = async context => {
if (context.electronPlatformName !== "darwin") throw new Error("Only macOS candidate packaging is enabled");
renameExecutable(join(context.appOutDir, "MeterUsage.app"));
};
module.exports.renameExecutable = renameExecutable;
23 changes: 23 additions & 0 deletions Scripts/electron/export-symbols.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import AppKit
import Foundation

// Lossless rasterization of the same SF Symbol stand-ins used by ProviderMark.
// This is build-time asset tooling. The TypeScript app never invokes Swift.
let output = URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true)
try FileManager.default.createDirectory(at: output, withIntermediateDirectories: true)
for (provider, symbol) in [("openRouter", "arrow.triangle.branch"), ("cursor", "cursorarrow.rays"), ("copilot", "terminal"), ("gemini", "diamond")] {
guard let image = NSImage(systemSymbolName: symbol, accessibilityDescription: nil),
let configured = image.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 24, weight: .medium)),
let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: 32, pixelsHigh: 32,
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true,
isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0),
let context = NSGraphicsContext(bitmapImageRep: rep) else {
fatalError("Existing provider symbol unavailable: \(provider)")
}
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = context
configured.draw(in: NSRect(x: 0, y: 0, width: 32, height: 32))
NSGraphicsContext.restoreGraphicsState()
guard let png = rep.representation(using: .png, properties: [:]) else { fatalError("Cannot export symbol") }
try png.write(to: output.appendingPathComponent("\(provider)-symbol.png"))
}
29 changes: 28 additions & 1 deletion docs/DEMO.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,31 @@ Percentages span calm green and amber warning bands so screenshots demonstrate c
- **Strict Activation**: The environment variable must be exactly `1`. Values like `true`, `yes`, or `0` remain off.
- **Whole-App Integrity**: Evaluated once at composition root — the dashboard is never partially real and partially fake.
- **Prominent Badge**: A **Demo** badge is displayed in the header to prevent any confusion with live telemetry.
- **Zero Disk & Network Access**: Demo sources perform no network requests, touch no external disk files, and invoke no CLI commands.
- **Synthetic sources**: Demo provider sources perform no network requests and invoke no provider CLI commands. The Electron candidate writes its own isolated preferences, history and cache files.

## Electron candidate

After `pnpm build`, run the candidate JSON path without a tray or window:

```sh
meterusage_demo_root="$(node -p 'require("node:fs").realpathSync(require("node:os").tmpdir())')"
node dist/main/cli.cjs json --demo --candidate-profile "$meterusage_demo_root/meterusage-cli-demo"
```

On macOS, launch the separate packaged candidate directly:

```sh
./release/mac-arm64/MeterUsage.app/Contents/MacOS/meterusage --demo --candidate-profile /private/tmp/meterusage-native-demo
```

The explicit profile must be empty on first use and use a canonical path.
Candidate mode rejects symlink aliases, including macOS `/tmp` and `/var`.
Demo mode selects synthetic
providers before composition and isolates all preferences, caches, history and
Electron state. Relaunch with the same profile to test persistence. Live
provider discovery, login-item changes and update installation are disabled.
Demo reset confirmation consumes only synthetic fixture credits. The original
logo PNGs are reused; SF Symbol stand-ins are exported on macOS during packaging.

Native candidate UI and two different-height notch captures remain required
acceptance evidence. Swift screenshots above do not prove Electron rendering.
Loading
Loading