From cab41010b56fa68a7724712e733619abf1244587 Mon Sep 17 00:00:00 2001 From: thrr87 <193831865+thrr87@users.noreply.github.com> Date: Sun, 19 Jul 2026 22:43:24 +0200 Subject: [PATCH] Clean public repository content --- AGENTS.md | 30 ------ CONTEXT.md | 65 ------------ README.md | 26 ++--- Sources/CodexLimits/ForecastEngine.swift | 2 +- Sources/CodexLimits/MenuContentView.swift | 2 +- .../CodexLimitsTests/UsageHistoryTests.swift | 98 +++++++++---------- docs/usage-history-sync.md | 6 -- 7 files changed, 57 insertions(+), 172 deletions(-) delete mode 100644 AGENTS.md delete mode 100644 CONTEXT.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 61d0c4b..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,30 +0,0 @@ -# Project instructions - -## User-facing copy - -- Write every visible UI string in natural English. -- Prefer short, concrete wording that tells the user what the number means or what they can do next. -- Avoid AI terminology, analytics jargon, statistical labels, hype, canned coaching, and anthropomorphic language. -- Do not expose internal reasoning, implementation notes, TODOs, test data, or debug text in the interface. -- Review all visible text before completing any UI change. - -## Visual design - -- Use native SwiftUI controls, system typography, semantic colors, and macOS materials. -- Build the burn-down visualization with Apple Swift Charts; do not add a third-party chart dependency. -- Show the remaining percentage only once inside the popover. The separate menu-bar value remains the compact always-visible reading. -- Show a percentage axis and a time axis with restrained grid lines so every trajectory has a clear scale. -- Differentiate forecast series with both color and line style; never rely on color alone. -- Draw the observed usage curve, a straight target path from 100% to the safety buffer, the current projection, the historical projection, and a clear `Now` marker. -- Keep the safety forecast in the calculation layer; communicate its conclusion through the pace status instead of another chart series or band. - -## Product boundaries - -- Keep the app entirely passive. -- Do not request notification permission or send system notifications. -- Keep the app purely analytical: observe actual usage and explain whether the current pace is sustainable. -- Do not add manual usage modes, planned breaks, schedules, calendars, or intensity profiles. -- Enable launch at login by default and expose a single `Launch at login` toggle in settings. -- Refresh usage on launch, wake, panel open, and every 10 minutes in the background. -- Provide a manual `Refresh` action in the panel. -- Keep the last successful value when refresh fails and show its age in plain English. diff --git a/CONTEXT.md b/CONTEXT.md deleted file mode 100644 index 059cbd6..0000000 --- a/CONTEXT.md +++ /dev/null @@ -1,65 +0,0 @@ -# Codex Usage Pacing - -This context describes how to pace Codex usage safely between limit resets while making deliberate use of the available budget. - -## Language - -**Usage window**: -An independently resetting Codex allowance defined by its remaining budget and next automatic reset. A window may be weekly or shorter. -_Avoid_: Billing cycle, weekly limit as the name of the whole mechanism - -**Main limit**: -The `codex` allowance that drives the menu-bar percentage, target pace, forecasts, and pace status. -_Avoid_: Total of all limits, active model - -**Other limit**: -An independent model-specific allowance shown separately for reference. It is never added to the main limit and does not affect its forecast. -_Avoid_: Extra main budget, secondary usage window - -**Remaining budget**: -The unused percentage of a usage window. -_Avoid_: Balance, credits - -**Window reset**: -The automatic time at which a usage window renews. -_Avoid_: Manual reset, subscription renewal - -**Emergency reset**: -An unguaranteed manual reset kept only as a last resort. It is never included in the remaining budget, target pace, or forecast; its availability may be shown for reference. -_Avoid_: Extra budget, backup allowance - -**Target pace**: -The planned rate of use that brings the remaining budget to the safety buffer at the window reset. The interface expresses it as a percentage per day, switching to a percentage per hour during the final 24 hours. -_Avoid_: Daily limit, fixed allowance - -**Safety buffer**: -The part of the remaining budget deliberately preserved at the window reset to reduce the risk of running out early. It defaults to 3% and can be changed by the user. -_Avoid_: Unused allowance, emergency reset - -**Pace status**: -A plain-language comparison between the safety forecast and the target pace, expressed as `Slow down`, `On track`, or `Room to use more`. -_Avoid_: User score, productivity rating - -**Expected forecast**: -The best estimate of the remaining budget at reset, weighted toward recent use and informed by earlier variation. -_Avoid_: Guaranteed outcome, long-term average - -**Safety forecast**: -A deliberately conservative estimate used for the pace status. It does not assume that a future break from Codex will occur. -_Avoid_: Worst case, guaranteed minimum - -**Burn-down chart**: -A chart for the current usage window with time and remaining-budget axes. It shows the observed usage curve, a straight path from 100% to the safety buffer, the current projection, the historical projection, and the current point; earlier windows inform the historical projection but are not drawn individually. -_Avoid_: Activity chart, task history - -**Usage sample**: -A timestamped observation of the main limit's remaining budget and window reset. Its observation time is recorded locally; its remaining budget and window reset come from Codex. -_Avoid_: Token bucket, activity event - -**Shared usage history**: -The combined usage samples recorded by Macs connected through the same optional sync folder. It excludes preferences, launch settings, credentials, and raw Codex responses. -_Avoid_: Shared app state, cloud backup - -**Sync folder**: -An optional user-selected folder that connects the shared usage history of Macs signed in to the same Codex account. One sync folder represents one Codex account; the app does not identify or verify that account. -_Avoid_: Account store, settings sync diff --git a/README.md b/README.md index a64024d..415f329 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ The menu bar shows the current remaining percentage. The popover adds the reset - Records local samples to show actual usage during the current window. - Compares actual usage with a straight target path ending at a configurable safety buffer. - Projects the current pace and compares it with recent historical usage. +- Keeps up to 90 days of usage history in versioned daily JSON files. +- Can optionally replicate usage history through a private folder selected by the user. - Refreshes on launch, wake, popover open, every ten minutes, or when requested manually. - Runs as a native SwiftUI menu-bar app with no third-party runtime dependencies. @@ -24,7 +26,9 @@ Codex Limits is local-first: - It does not copy or store Codex credentials. - It starts the user-managed Codex CLI and reads usage through its local app-server interface. -- It stores usage samples only in macOS `UserDefaults` for the app. +- It stores main-limit usage samples as versioned daily JSON files in the app's Application Support directory. +- If history sync is enabled, it replicates only those samples to the selected folder. Preferences, credentials, and raw Codex responses remain local. +- Synced history is readable JSON and contains observation times, remaining percentages, and reset times. Choose a private folder that is not shared with other people. - It has no telemetry, analytics, notifications, or direct network client. - The Codex CLI may contact the Codex service as part of its normal operation. @@ -33,7 +37,7 @@ Do not attach raw CLI output or screenshots containing account usage to public i ## Requirements - macOS 14 or later -- Xcode 15.3 or later +- Xcode 16.4 or later - a signed-in, Homebrew-managed Codex CLI at either `/opt/homebrew/bin/codex` or `/usr/local/bin/codex` Codex Limits deliberately does not use a Codex binary bundled inside another application. Install and update the standalone CLI yourself. @@ -64,22 +68,6 @@ swift test The test suite intentionally uses synthetic usage data. Do not commit exported account data or local app state as fixtures. -## Signed commits on another Mac - -Use a different SSH key pair on each computer. Never copy a private key between machines. - -After adding that Mac's public key to GitHub for authentication, add the same public key as a signing key and configure this clone: - -```sh -gh auth refresh -h github.com -s admin:ssh_signing_key -gh ssh-key add ~/.ssh/id_ed25519.pub --type signing --title "Mac signing key" -git config gpg.format ssh -git config user.signingkey ~/.ssh/id_ed25519.pub -git config commit.gpgsign true -``` - -GitHub should show locally created commits as `Verified`. - ## Current limitations - No prebuilt or notarized release is provided. @@ -90,8 +78,6 @@ GitHub should show locally created commits as `Verified`. Please report vulnerabilities privately. See [SECURITY.md](.github/SECURITY.md) for instructions. -Commits reaching `main` must be signed. Use a separate signing key on each development machine; never copy a private key between computers. - ## License MIT. See [LICENSE](LICENSE). diff --git a/Sources/CodexLimits/ForecastEngine.swift b/Sources/CodexLimits/ForecastEngine.swift index 69ac8ff..d014851 100644 --- a/Sources/CodexLimits/ForecastEngine.swift +++ b/Sources/CodexLimits/ForecastEngine.swift @@ -107,7 +107,7 @@ enum ForecastEngine { let historicalAverage = Double(historyTokens) / Double(historyCount) guard currentAverage > 0, historicalAverage > 0 else { return nil } - // ponytail: Daily token buckets are a coarse bootstrap; local percentage windows replace it. + // Daily token buckets are a coarse bootstrap; percentage-based windows replace them. let relativePace = min(max(historicalAverage / currentAverage, 0.25), 4) return windowRate * relativePace } diff --git a/Sources/CodexLimits/MenuContentView.swift b/Sources/CodexLimits/MenuContentView.swift index 8635129..936c1ca 100644 --- a/Sources/CodexLimits/MenuContentView.swift +++ b/Sources/CodexLimits/MenuContentView.swift @@ -260,7 +260,7 @@ private struct BurnDownChart: View { } } - // ponytail: Daily Codex buckets seed the curve; local percentage samples replace them over time. + // Daily token buckets seed the curve until percentage samples cover the window. return deduplicated( [BurnPoint(date: window.startsAt, remaining: 100)] + bootstrapped + local + [current] ) diff --git a/Tests/CodexLimitsTests/UsageHistoryTests.swift b/Tests/CodexLimitsTests/UsageHistoryTests.swift index fc2dd5d..2e68a39 100644 --- a/Tests/CodexLimitsTests/UsageHistoryTests.swift +++ b/Tests/CodexLimitsTests/UsageHistoryTests.swift @@ -17,7 +17,7 @@ final class UsageHistoryTests: XCTestCase { ) let history = UsageHistory( localDirectory: root.appendingPathComponent("local", isDirectory: true), - installationID: "macbook", + installationID: "writer-a", now: { now } ) _ = await history.load() @@ -55,12 +55,12 @@ final class UsageHistoryTests: XCTestCase { ) let history = UsageHistory( localDirectory: root, - installationID: "macbook", + installationID: "writer-a", now: { now } ) _ = await history.load() _ = await history.record(sample) - let file = try XCTUnwrap(jsonFiles(for: "macbook", in: root).first) + let file = try XCTUnwrap(jsonFiles(for: "writer-a", in: root).first) try Data("broken".utf8).write(to: file) let state = await history.synchronize() @@ -81,12 +81,12 @@ final class UsageHistoryTests: XCTestCase { ) let history = UsageHistory( localDirectory: root, - installationID: "macbook", + installationID: "writer-a", now: { now } ) _ = await history.load() _ = await history.record(sample) - let file = try XCTUnwrap(jsonFiles(for: "macbook", in: root).first) + let file = try XCTUnwrap(jsonFiles(for: "writer-a", in: root).first) let original = try XCTUnwrap(String(data: Data(contentsOf: file), encoding: .utf8)) let oversized = original.replacingOccurrences( of: "{", @@ -97,7 +97,7 @@ final class UsageHistoryTests: XCTestCase { let reloaded = UsageHistory( localDirectory: root, - installationID: "macbook", + installationID: "writer-a", now: { now } ) let state = await reloaded.load() @@ -120,7 +120,7 @@ final class UsageHistoryTests: XCTestCase { ) let history = UsageHistory( localDirectory: root.appendingPathComponent("local", isDirectory: true), - installationID: "macbook", + installationID: "writer-a", now: { now } ) @@ -145,7 +145,7 @@ final class UsageHistoryTests: XCTestCase { try unsupportedMarker.write(to: marker) let history = UsageHistory( localDirectory: root.appendingPathComponent("local", isDirectory: true), - installationID: "macbook" + installationID: "writer-a" ) _ = await history.load() @@ -181,7 +181,7 @@ final class UsageHistoryTests: XCTestCase { ) let history = UsageHistory( localDirectory: root.appendingPathComponent("local", isDirectory: true), - installationID: "macbook", + installationID: "writer-a", now: { now } ) _ = await history.load() @@ -211,7 +211,7 @@ final class UsageHistoryTests: XCTestCase { let missing = root.appendingPathComponent("missing", isDirectory: true) let history = UsageHistory( localDirectory: root.appendingPathComponent("local", isDirectory: true), - installationID: "macbook" + installationID: "writer-a" ) _ = await history.load() @@ -273,38 +273,38 @@ final class UsageHistoryTests: XCTestCase { let currentDate = Date(timeIntervalSince1970: 10_000_000) let oldDate = currentDate.addingTimeInterval(-91 * 86_400) let oldReset = oldDate.addingTimeInterval(7 * 86_400) - let macBook = UsageHistory( + let firstWriter = UsageHistory( localDirectory: root, - installationID: "macbook", + installationID: "writer-a", now: { oldDate } ) - let macStudio = UsageHistory( + let secondWriter = UsageHistory( localDirectory: root, - installationID: "mac-studio", + installationID: "writer-b", now: { oldDate } ) - _ = await macBook.load() - _ = await macBook.record(UsageSample( + _ = await firstWriter.load() + _ = await firstWriter.record(UsageSample( observedAt: oldDate, remainingPercent: 80, resetsAt: oldReset )) - _ = await macStudio.record(UsageSample( + _ = await secondWriter.record(UsageSample( observedAt: oldDate, remainingPercent: 79, resetsAt: oldReset )) - let currentMacBook = UsageHistory( + let currentFirstWriter = UsageHistory( localDirectory: root, - installationID: "macbook", + installationID: "writer-a", now: { currentDate } ) - _ = await currentMacBook.load() + _ = await currentFirstWriter.load() - XCTAssertTrue(jsonFiles(for: "macbook", in: root).isEmpty) - XCTAssertEqual(jsonFiles(for: "mac-studio", in: root).count, 1) + XCTAssertTrue(jsonFiles(for: "writer-a", in: root).isEmpty) + XCTAssertEqual(jsonFiles(for: "writer-b", in: root).count, 1) } func testMalformedFileKeepsValidHistoryAndReportsWarning() async throws { @@ -319,21 +319,21 @@ final class UsageHistoryTests: XCTestCase { ) let history = UsageHistory( localDirectory: root, - installationID: "macbook", + installationID: "writer-a", now: { now } ) _ = await history.load() _ = await history.record(sample) let writerDirectory = root .appendingPathComponent("installations", isDirectory: true) - .appendingPathComponent("macbook", isDirectory: true) + .appendingPathComponent("writer-a", isDirectory: true) try Data("broken".utf8).write( to: writerDirectory.appendingPathComponent("broken.json") ) let reloaded = UsageHistory( localDirectory: root, - installationID: "macbook", + installationID: "writer-a", now: { now } ) let state = await reloaded.load() @@ -357,7 +357,7 @@ final class UsageHistoryTests: XCTestCase { ) let history = UsageHistory( localDirectory: unusableDirectory, - installationID: "macbook", + installationID: "writer-a", now: { now } ) @@ -375,48 +375,48 @@ final class UsageHistoryTests: XCTestCase { let shared = root.appendingPathComponent("shared", isDirectory: true) try FileManager.default.createDirectory(at: shared, withIntermediateDirectories: true) let now = Date(timeIntervalSince1970: 1_900_060) - let macBook = UsageHistory( - localDirectory: root.appendingPathComponent("macbook", isDirectory: true), - installationID: "macbook", + let firstWriter = UsageHistory( + localDirectory: root.appendingPathComponent("writer-a", isDirectory: true), + installationID: "writer-a", now: { now } ) - let macStudio = UsageHistory( - localDirectory: root.appendingPathComponent("mac-studio", isDirectory: true), - installationID: "mac-studio", + let secondWriter = UsageHistory( + localDirectory: root.appendingPathComponent("writer-b", isDirectory: true), + installationID: "writer-b", now: { now } ) let reset = Date(timeIntervalSince1970: 2_000_000) - let macBookSample = UsageSample( + let firstSample = UsageSample( observedAt: Date(timeIntervalSince1970: 1_900_000), remainingPercent: 82, resetsAt: reset ) - let macStudioSample = UsageSample( + let secondSample = UsageSample( observedAt: Date(timeIntervalSince1970: 1_900_060), remainingPercent: 81, resetsAt: reset ) - _ = await macBook.load() - _ = await macStudio.load() - _ = await macBook.connect(to: shared) - _ = await macStudio.connect(to: shared) + _ = await firstWriter.load() + _ = await secondWriter.load() + _ = await firstWriter.connect(to: shared) + _ = await secondWriter.connect(to: shared) - async let macBookWrite = macBook.record(macBookSample) - async let macStudioWrite = macStudio.record(macStudioSample) - let writeStates = await (macBookWrite, macStudioWrite) + async let firstWrite = firstWriter.record(firstSample) + async let secondWrite = secondWriter.record(secondSample) + let writeStates = await (firstWrite, secondWrite) XCTAssertNil(writeStates.0.errorMessage) XCTAssertNil(writeStates.1.errorMessage) - XCTAssertEqual(jsonFiles(for: "macbook", in: shared).count, 1) - XCTAssertEqual(jsonFiles(for: "mac-studio", in: shared).count, 1) + XCTAssertEqual(jsonFiles(for: "writer-a", in: shared).count, 1) + XCTAssertEqual(jsonFiles(for: "writer-b", in: shared).count, 1) - let macBookState = await macBook.synchronize() - let macStudioState = await macStudio.synchronize() + let firstState = await firstWriter.synchronize() + let secondState = await secondWriter.synchronize() - XCTAssertEqual(macBookState.samples, [macBookSample, macStudioSample]) - XCTAssertEqual(macStudioState.samples, [macBookSample, macStudioSample]) - XCTAssertNil(macBookState.errorMessage) - XCTAssertNil(macStudioState.errorMessage) + XCTAssertEqual(firstState.samples, [firstSample, secondSample]) + XCTAssertEqual(secondState.samples, [firstSample, secondSample]) + XCTAssertNil(firstState.errorMessage) + XCTAssertNil(secondState.errorMessage) } private func jsonFiles(for installationID: String, in root: URL) -> [URL] { diff --git a/docs/usage-history-sync.md b/docs/usage-history-sync.md index fc0e48c..8d8ac98 100644 --- a/docs/usage-history-sync.md +++ b/docs/usage-history-sync.md @@ -43,9 +43,3 @@ The account invariant is presented as: Folder privacy is presented as: > Choose a private folder that isn’t shared with other people. - -## TDD seam - -Critical behavior is tested through the concrete `UsageHistory` module interface, using real temporary directories instead of filesystem mocks. The interface covers loading local history, recording a usage sample, connecting or disconnecting a sync folder, merging the replica during refresh, and returning history with nonblocking sync status. - -Tests cover lossless legacy migration, simultaneous writes by two installations, initial merge, idempotent repeated merge, malformed files, unsupported folder versions, nondestructive disconnect, and writer-owned retention. Tests do not target private functions, call order, or JSON implementation details.