burn-o-meter 0.6.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- burn_o_meter-0.6.5/.claude/RESUME.md +23 -0
- burn_o_meter-0.6.5/.github/ISSUE_TEMPLATE/bug_report.yml +56 -0
- burn_o_meter-0.6.5/.github/ISSUE_TEMPLATE/config.yml +8 -0
- burn_o_meter-0.6.5/.github/ISSUE_TEMPLATE/provider_request.yml +36 -0
- burn_o_meter-0.6.5/.github/dependabot.yml +23 -0
- burn_o_meter-0.6.5/.github/pull_request_template.md +29 -0
- burn_o_meter-0.6.5/.github/workflows/bottles.yml +144 -0
- burn_o_meter-0.6.5/.github/workflows/ci.yml +199 -0
- burn_o_meter-0.6.5/.github/workflows/codeql.yml +37 -0
- burn_o_meter-0.6.5/.github/workflows/pypi.yml +51 -0
- burn_o_meter-0.6.5/.gitignore +29 -0
- burn_o_meter-0.6.5/CHANGELOG.md +519 -0
- burn_o_meter-0.6.5/CODE_OF_CONDUCT.md +50 -0
- burn_o_meter-0.6.5/CONTRIBUTING.md +179 -0
- burn_o_meter-0.6.5/Formula/burn-o-meter.rb +92 -0
- burn_o_meter-0.6.5/LICENSE +21 -0
- burn_o_meter-0.6.5/NOTICE +11 -0
- burn_o_meter-0.6.5/PKG-INFO +85 -0
- burn_o_meter-0.6.5/README.md +323 -0
- burn_o_meter-0.6.5/RELEASING.md +79 -0
- burn_o_meter-0.6.5/ROADMAP.md +197 -0
- burn_o_meter-0.6.5/SECURITY.md +350 -0
- burn_o_meter-0.6.5/docs/adding-an-agent.md +67 -0
- burn_o_meter-0.6.5/docs/configuration.md +54 -0
- burn_o_meter-0.6.5/docs/demo.gif +0 -0
- burn_o_meter-0.6.5/docs/design-budgets-and-alerts.md +100 -0
- burn_o_meter-0.6.5/docs/faq.md +165 -0
- burn_o_meter-0.6.5/docs/install.md +231 -0
- burn_o_meter-0.6.5/docs/org-avatar.png +0 -0
- burn_o_meter-0.6.5/docs/pypi.md +52 -0
- burn_o_meter-0.6.5/docs/social-preview.jpg +0 -0
- burn_o_meter-0.6.5/docs/social-preview.png +0 -0
- burn_o_meter-0.6.5/install.sh +129 -0
- burn_o_meter-0.6.5/macos/burn-o-meter/Package.swift +19 -0
- burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/ContentView.swift +603 -0
- burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/Engine.swift +120 -0
- burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/FreshnessCheck.swift +159 -0
- burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/LayoutCheck.swift +262 -0
- burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/MenuBarIcon.swift +288 -0
- burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/MenuBarTitle.swift +52 -0
- burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/Models.swift +399 -0
- burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/Preferences.swift +150 -0
- burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/SnapshotFile.swift +166 -0
- burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/Theme.swift +547 -0
- burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/Updates.swift +194 -0
- burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/main.swift +759 -0
- burn_o_meter-0.6.5/macos/burn-o-meter.icns +0 -0
- burn_o_meter-0.6.5/macos/make-app.sh +130 -0
- burn_o_meter-0.6.5/macos/make-icon.swift +158 -0
- burn_o_meter-0.6.5/macos/make-social-preview.swift +103 -0
- burn_o_meter-0.6.5/pyproject.toml +65 -0
- burn_o_meter-0.6.5/scripts/setup-repo.sh +98 -0
- burn_o_meter-0.6.5/scripts/smoke-test.sh +129 -0
- burn_o_meter-0.6.5/src/burnometer/__init__.py +3 -0
- burn_o_meter-0.6.5/src/burnometer/__main__.py +6 -0
- burn_o_meter-0.6.5/src/burnometer/adapters/__init__.py +34 -0
- burn_o_meter-0.6.5/src/burnometer/adapters/base.py +182 -0
- burn_o_meter-0.6.5/src/burnometer/adapters/claude_code.py +255 -0
- burn_o_meter-0.6.5/src/burnometer/adapters/claude_desktop.py +242 -0
- burn_o_meter-0.6.5/src/burnometer/adapters/claude_statusline.py +109 -0
- burn_o_meter-0.6.5/src/burnometer/adapters/codex.py +362 -0
- burn_o_meter-0.6.5/src/burnometer/adapters/kimi.py +333 -0
- burn_o_meter-0.6.5/src/burnometer/adapters/opencode.py +275 -0
- burn_o_meter-0.6.5/src/burnometer/agent.py +211 -0
- burn_o_meter-0.6.5/src/burnometer/analytics.py +648 -0
- burn_o_meter-0.6.5/src/burnometer/cli.py +867 -0
- burn_o_meter-0.6.5/src/burnometer/config.py +197 -0
- burn_o_meter-0.6.5/src/burnometer/models.py +251 -0
- burn_o_meter-0.6.5/src/burnometer/pricing/__init__.py +6 -0
- burn_o_meter-0.6.5/src/burnometer/pricing/calculator.py +198 -0
- burn_o_meter-0.6.5/src/burnometer/pricing/catalog.py +574 -0
- burn_o_meter-0.6.5/src/burnometer/pricing/overlay.toml +133 -0
- burn_o_meter-0.6.5/src/burnometer/pricing/snapshot.json +2427 -0
- burn_o_meter-0.6.5/src/burnometer/report.py +221 -0
- burn_o_meter-0.6.5/src/burnometer/safety.py +553 -0
- burn_o_meter-0.6.5/src/burnometer/scan.py +311 -0
- burn_o_meter-0.6.5/src/burnometer/snapshot.py +379 -0
- burn_o_meter-0.6.5/src/burnometer/statusline.py +387 -0
- burn_o_meter-0.6.5/src/burnometer/store/__init__.py +5 -0
- burn_o_meter-0.6.5/src/burnometer/store/db.py +529 -0
- burn_o_meter-0.6.5/src/burnometer/store/schema.sql +99 -0
- burn_o_meter-0.6.5/tests/conftest.py +150 -0
- burn_o_meter-0.6.5/tests/fixtures/claude_code/README.md +14 -0
- burn_o_meter-0.6.5/tests/fixtures/claude_code/basic.jsonl +7 -0
- burn_o_meter-0.6.5/tests/fixtures/claude_code/edge.jsonl +7 -0
- burn_o_meter-0.6.5/tests/fixtures/claude_desktop/plan-usage-history.json +78 -0
- burn_o_meter-0.6.5/tests/fixtures/codex/edge.jsonl +6 -0
- burn_o_meter-0.6.5/tests/fixtures/codex/local.jsonl +8 -0
- burn_o_meter-0.6.5/tests/fixtures/codex/reset.jsonl +6 -0
- burn_o_meter-0.6.5/tests/fixtures/codex/session.jsonl +8 -0
- burn_o_meter-0.6.5/tests/fixtures/kimi/session_index.jsonl +2 -0
- burn_o_meter-0.6.5/tests/fixtures/kimi/sessions/wd_demo_0797854e9d96/session_11111111-1111-4111-8111-111111111111/agents/main/wire.jsonl +13 -0
- burn_o_meter-0.6.5/tests/fixtures/kimi/sessions/wd_work_5559ee09aed8/session_22222222-2222-4222-8222-222222222222/agents/main/wire.jsonl +5 -0
- burn_o_meter-0.6.5/tests/fixtures/opencode/opencode.db +0 -0
- burn_o_meter-0.6.5/tests/test_adapter_claude_code.py +277 -0
- burn_o_meter-0.6.5/tests/test_adapter_claude_desktop.py +231 -0
- burn_o_meter-0.6.5/tests/test_adapter_codex.py +252 -0
- burn_o_meter-0.6.5/tests/test_adapter_kimi.py +225 -0
- burn_o_meter-0.6.5/tests/test_adapter_opencode.py +193 -0
- burn_o_meter-0.6.5/tests/test_agent.py +92 -0
- burn_o_meter-0.6.5/tests/test_analytics.py +277 -0
- burn_o_meter-0.6.5/tests/test_cli.py +569 -0
- burn_o_meter-0.6.5/tests/test_config.py +136 -0
- burn_o_meter-0.6.5/tests/test_pricing.py +929 -0
- burn_o_meter-0.6.5/tests/test_scan.py +316 -0
- burn_o_meter-0.6.5/tests/test_security.py +698 -0
- burn_o_meter-0.6.5/tests/test_series_and_cache.py +232 -0
- burn_o_meter-0.6.5/tests/test_snapshot.py +266 -0
- burn_o_meter-0.6.5/tests/test_statusline.py +364 -0
- burn_o_meter-0.6.5/tests/test_store.py +337 -0
- burn_o_meter-0.6.5/uninstall.sh +89 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Claude Code — resume checkpoint
|
|
2
|
+
|
|
3
|
+
Session: 180ada93-1c90-4b9a-9372-0311fac0c81d
|
|
4
|
+
Written: 2026-08-24T17:57:57.497Z
|
|
5
|
+
Trigger: near-limit
|
|
6
|
+
Ref with your in-progress files: refs/claude/checkpoint-180ada93
|
|
7
|
+
|
|
8
|
+
## To resume
|
|
9
|
+
|
|
10
|
+
claude --resume 180ada93-1c90-4b9a-9372-0311fac0c81d
|
|
11
|
+
|
|
12
|
+
(or open Claude Code in this directory and run /resume)
|
|
13
|
+
|
|
14
|
+
## Plan (from TodoWrite state)
|
|
15
|
+
|
|
16
|
+
No task list was active; see transcript via the resume command above.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
Don't want these changes? Resume this session (above), then run
|
|
21
|
+
`/rewind` to roll back the turn's tool edits (bash-made changes
|
|
22
|
+
excluded). refs/claude/checkpoint-180ada93 holds a full snapshot until this session's
|
|
23
|
+
next checkpoint, or for up to ~2 weeks.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: A number looks wrong, or something does not work
|
|
3
|
+
labels: [bug]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
**Before you paste anything:** `burnometer doctor` and `--json` output are
|
|
9
|
+
designed to be safe to share — they contain aggregates only, no prompt
|
|
10
|
+
text, and no absolute paths under the default privacy setting. If you
|
|
11
|
+
want to be extra careful, set `project_paths = "hash"` in
|
|
12
|
+
`~/.burn-o-meter/config.toml` and re-run; project names become opaque.
|
|
13
|
+
|
|
14
|
+
Never paste a provider log file (`~/.claude/projects/**`,
|
|
15
|
+
`~/.codex/sessions/**`). Those contain your full conversations.
|
|
16
|
+
|
|
17
|
+
- type: textarea
|
|
18
|
+
id: what
|
|
19
|
+
attributes:
|
|
20
|
+
label: What happened
|
|
21
|
+
description: What did you expect, and what did you get instead?
|
|
22
|
+
validations:
|
|
23
|
+
required: true
|
|
24
|
+
|
|
25
|
+
- type: textarea
|
|
26
|
+
id: doctor
|
|
27
|
+
attributes:
|
|
28
|
+
label: Output of `burnometer doctor`
|
|
29
|
+
render: text
|
|
30
|
+
validations:
|
|
31
|
+
required: true
|
|
32
|
+
|
|
33
|
+
- type: input
|
|
34
|
+
id: version
|
|
35
|
+
attributes:
|
|
36
|
+
label: Version
|
|
37
|
+
description: "`burnometer --version`, plus macOS version if it is the app"
|
|
38
|
+
validations:
|
|
39
|
+
required: true
|
|
40
|
+
|
|
41
|
+
- type: dropdown
|
|
42
|
+
id: surface
|
|
43
|
+
attributes:
|
|
44
|
+
label: Where
|
|
45
|
+
options: [CLI, menu bar app, background agent, not sure]
|
|
46
|
+
validations:
|
|
47
|
+
required: true
|
|
48
|
+
|
|
49
|
+
- type: textarea
|
|
50
|
+
id: numbers
|
|
51
|
+
attributes:
|
|
52
|
+
label: If a number looks wrong
|
|
53
|
+
description: |
|
|
54
|
+
What does burn-o-meter say, what do you believe is correct, and where
|
|
55
|
+
does the correct figure come from? Cost figures are labelled `api_billed`
|
|
56
|
+
or `api_equivalent` — please say which you are comparing against.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
blank_issues_enabled: false
|
|
2
|
+
contact_links:
|
|
3
|
+
- name: Security vulnerability
|
|
4
|
+
url: https://github.com/devopsinside/burn-o-meter/security/advisories/new
|
|
5
|
+
about: Please report privately, not as a public issue. See SECURITY.md.
|
|
6
|
+
- name: Question or idea
|
|
7
|
+
url: https://github.com/devopsinside/burn-o-meter/discussions
|
|
8
|
+
about: Ask a question or propose something before building it.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Provider support
|
|
2
|
+
description: Ask for a new AI tool or agent to be supported
|
|
3
|
+
labels: [provider]
|
|
4
|
+
body:
|
|
5
|
+
- type: input
|
|
6
|
+
id: tool
|
|
7
|
+
attributes:
|
|
8
|
+
label: Which tool
|
|
9
|
+
description: Name and link
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
|
|
13
|
+
- type: input
|
|
14
|
+
id: path
|
|
15
|
+
attributes:
|
|
16
|
+
label: Where does it write usage data
|
|
17
|
+
description: "e.g. `~/.foo/sessions/*.jsonl`. Path only — do not paste contents."
|
|
18
|
+
validations:
|
|
19
|
+
required: true
|
|
20
|
+
|
|
21
|
+
- type: textarea
|
|
22
|
+
id: shape
|
|
23
|
+
attributes:
|
|
24
|
+
label: What the usage record looks like
|
|
25
|
+
description: |
|
|
26
|
+
A **redacted** example: field names and numbers, with all message content
|
|
27
|
+
removed. Please strip anything you would not post publicly.
|
|
28
|
+
render: json
|
|
29
|
+
|
|
30
|
+
- type: checkboxes
|
|
31
|
+
id: quota
|
|
32
|
+
attributes:
|
|
33
|
+
label: Does it record rate limits or quota?
|
|
34
|
+
options:
|
|
35
|
+
- label: It writes a percentage or limit figure to disk
|
|
36
|
+
- label: I do not know
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: pip
|
|
4
|
+
directory: /
|
|
5
|
+
schedule:
|
|
6
|
+
interval: weekly
|
|
7
|
+
open-pull-requests-limit: 5
|
|
8
|
+
# Wait a week before proposing a newly published version. Nearly every
|
|
9
|
+
# package-registry compromise is caught within days of the bad version going
|
|
10
|
+
# up, so the cost of a week's delay is small and the thing it avoids is
|
|
11
|
+
# merging a malicious release automatically, hours after it appears.
|
|
12
|
+
cooldown:
|
|
13
|
+
default-days: 7
|
|
14
|
+
|
|
15
|
+
# Actions are pinned to commit SHAs, which a tag cannot be repointed underneath
|
|
16
|
+
# - that is the actual mitigation. Dependabot's job here is to keep those pins
|
|
17
|
+
# from going stale, proposing a new SHA with the version in the commit message.
|
|
18
|
+
- package-ecosystem: github-actions
|
|
19
|
+
directory: /
|
|
20
|
+
schedule:
|
|
21
|
+
interval: weekly
|
|
22
|
+
cooldown:
|
|
23
|
+
default-days: 7
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
## What and why
|
|
2
|
+
|
|
3
|
+
<!-- What changes, and what problem it solves. If a number changes, say which
|
|
4
|
+
and by how much. -->
|
|
5
|
+
|
|
6
|
+
## Checklist
|
|
7
|
+
|
|
8
|
+
- [ ] `ruff check src tests` and `ruff format --check src tests` pass
|
|
9
|
+
- [ ] `pytest` passes
|
|
10
|
+
- [ ] New behaviour has a test
|
|
11
|
+
|
|
12
|
+
If this touches parsing, pricing, or anything that produces a figure:
|
|
13
|
+
|
|
14
|
+
- [ ] No number is invented — unknown stays `NULL`/`—`, never `0.0`
|
|
15
|
+
- [ ] Cost bases are not summed together
|
|
16
|
+
- [ ] Anything derived rather than reported is labelled `est`
|
|
17
|
+
- [ ] Fixtures include a malformed record and a duplicate
|
|
18
|
+
|
|
19
|
+
If this touches file reading or storage:
|
|
20
|
+
|
|
21
|
+
- [ ] No glob widened; no content field added to an allowlist in `safety.py`
|
|
22
|
+
- [ ] No new network call outside `pricing/catalog.py`
|
|
23
|
+
- [ ] Error messages carry `path:line`, never file content
|
|
24
|
+
- [ ] `pytest tests/test_security.py` passes
|
|
25
|
+
|
|
26
|
+
## Verification
|
|
27
|
+
|
|
28
|
+
<!-- What you ran, and what it showed. Reconciling against a second source
|
|
29
|
+
(raw logs, a provider's own totals) is the strongest evidence. -->
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Builds Homebrew bottles - prebuilt tarballs - so `brew install` needs no
|
|
2
|
+
# compiler. burn-o-meter is pure Python and compiles nothing, but Homebrew builds
|
|
3
|
+
# Python formulae from sdists with --no-binary=:all:, so without a bottle it
|
|
4
|
+
# demands Apple's ~1GB Command Line Tools from every user. A bottle removes that.
|
|
5
|
+
name: bottles
|
|
6
|
+
|
|
7
|
+
# Run this BEFORE creating the release, not after. Bottles build from the tag's
|
|
8
|
+
# source archive, which exists as soon as the tag is pushed — and the release must
|
|
9
|
+
# be created with the bottles already attached, because GitHub releases are
|
|
10
|
+
# immutable and assets cannot be added later. A `release: published` trigger was
|
|
11
|
+
# removed for that reason: it fired after the only useful moment had passed, and
|
|
12
|
+
# checked out the tag ref, so any formula fix made after tagging was invisible to
|
|
13
|
+
# it and it failed permanently.
|
|
14
|
+
on:
|
|
15
|
+
workflow_dispatch:
|
|
16
|
+
inputs:
|
|
17
|
+
tag:
|
|
18
|
+
description: "Tag to build bottles for, e.g. v0.3.4 (push the tag first)"
|
|
19
|
+
required: true
|
|
20
|
+
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
|
|
24
|
+
env:
|
|
25
|
+
HOMEBREW_NO_AUTO_UPDATE: 1
|
|
26
|
+
HOMEBREW_NO_ANALYTICS: 1
|
|
27
|
+
# Homebrew 6 refuses untrusted taps. This is our own formula, from this same
|
|
28
|
+
# repository, built by this repository's CI - there is no third party to trust.
|
|
29
|
+
HOMEBREW_NO_REQUIRE_TAP_TRUST: 1
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
build:
|
|
33
|
+
name: ${{ matrix.label }}
|
|
34
|
+
strategy:
|
|
35
|
+
fail-fast: false
|
|
36
|
+
matrix:
|
|
37
|
+
include:
|
|
38
|
+
- runner: macos-26
|
|
39
|
+
label: arm64_tahoe (macOS 26)
|
|
40
|
+
- runner: macos-15
|
|
41
|
+
label: arm64_sequoia (macOS 15)
|
|
42
|
+
- runner: macos-14
|
|
43
|
+
label: arm64_sonoma (macOS 14)
|
|
44
|
+
runs-on: ${{ matrix.runner }}
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
47
|
+
|
|
48
|
+
- name: Resolve the tag being bottled
|
|
49
|
+
id: tag
|
|
50
|
+
# Through env, never spliced into the script. `${{ }}` is substituted
|
|
51
|
+
# before the shell sees the text at all, so a crafted tag name would run
|
|
52
|
+
# as code on a runner holding this repository's token.
|
|
53
|
+
env:
|
|
54
|
+
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
|
55
|
+
INPUT_TAG: ${{ inputs.tag }}
|
|
56
|
+
run: echo "tag=${RELEASE_TAG:-$INPUT_TAG}" >> "$GITHUB_OUTPUT"
|
|
57
|
+
|
|
58
|
+
- name: Tap this checkout
|
|
59
|
+
# Tapping the workspace bottles the formula as it exists in THIS commit,
|
|
60
|
+
# rather than whatever main happens to hold.
|
|
61
|
+
run: brew tap devopsinside/burn-o-meter "$GITHUB_WORKSPACE"
|
|
62
|
+
|
|
63
|
+
- name: Build
|
|
64
|
+
run: brew install --build-bottle devopsinside/burn-o-meter/burn-o-meter
|
|
65
|
+
|
|
66
|
+
- name: Prove it runs before we ship it
|
|
67
|
+
run: |
|
|
68
|
+
brew test devopsinside/burn-o-meter/burn-o-meter
|
|
69
|
+
burn-o-meter --version
|
|
70
|
+
burn-o-meter doctor >/dev/null
|
|
71
|
+
|
|
72
|
+
- name: Bottle
|
|
73
|
+
env:
|
|
74
|
+
REPOSITORY: ${{ github.repository }}
|
|
75
|
+
RESOLVED_TAG: ${{ steps.tag.outputs.tag }}
|
|
76
|
+
run: |
|
|
77
|
+
brew bottle --json --no-rebuild \
|
|
78
|
+
--root-url="https://github.com/${REPOSITORY}/releases/download/${RESOLVED_TAG}" \
|
|
79
|
+
devopsinside/burn-o-meter/burn-o-meter
|
|
80
|
+
# brew writes `name--version.tag.bottle.tar.gz` but serves it as
|
|
81
|
+
# `name-version...`; the JSON carries both names, so rename from that
|
|
82
|
+
# rather than guessing at the string.
|
|
83
|
+
for j in *.json; do
|
|
84
|
+
jq -r '.[].bottle.tags[] | "\(.local_filename)\t\(.filename)"' "$j" |
|
|
85
|
+
while IFS=$'\t' read -r src dst; do
|
|
86
|
+
[ "$src" != "$dst" ] && mv -- "$src" "$dst"
|
|
87
|
+
done
|
|
88
|
+
done
|
|
89
|
+
ls -la *.bottle.tar.gz *.json
|
|
90
|
+
|
|
91
|
+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
92
|
+
with:
|
|
93
|
+
name: bottle-${{ matrix.runner }}
|
|
94
|
+
path: |
|
|
95
|
+
*.bottle.tar.gz
|
|
96
|
+
*.json
|
|
97
|
+
if-no-files-found: error
|
|
98
|
+
|
|
99
|
+
collect:
|
|
100
|
+
name: collect bottles
|
|
101
|
+
needs: build
|
|
102
|
+
runs-on: macos-14
|
|
103
|
+
steps:
|
|
104
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
105
|
+
|
|
106
|
+
# v8 errors on a digest mismatch instead of warning, and no longer unzips a
|
|
107
|
+
# download that is not a zip. The artifacts here are multi-file uploads, which
|
|
108
|
+
# are always zipped, so the second does not apply - verified by running this
|
|
109
|
+
# workflow against v8 before it merged, since pull-request CI never runs it.
|
|
110
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
111
|
+
with:
|
|
112
|
+
pattern: bottle-*
|
|
113
|
+
merge-multiple: true
|
|
114
|
+
|
|
115
|
+
- name: Print the bottle block for the release
|
|
116
|
+
# Deliberately does NOT attach these to the release. GitHub releases are
|
|
117
|
+
# immutable once published: assets can only be added at creation time, and
|
|
118
|
+
# a tag consumed by an immutable release can never be reused - so a
|
|
119
|
+
# workflow that uploads afterwards cannot work, and retrying burns the
|
|
120
|
+
# version number. The bottles are published as workflow artifacts instead,
|
|
121
|
+
# and `scripts/release.sh` attaches them while creating the release.
|
|
122
|
+
env:
|
|
123
|
+
REPOSITORY: ${{ github.repository }}
|
|
124
|
+
RESOLVED_TAG: ${{ steps.tag.outputs.tag }}
|
|
125
|
+
INPUT_TAG: ${{ inputs.tag }}
|
|
126
|
+
run: |
|
|
127
|
+
{
|
|
128
|
+
echo "### Bottles built"
|
|
129
|
+
echo
|
|
130
|
+
echo "Download the \`bottle-*\` artifacts from this run, then create the"
|
|
131
|
+
echo "release with them attached — assets cannot be added afterwards."
|
|
132
|
+
echo
|
|
133
|
+
echo '```ruby'
|
|
134
|
+
echo ' bottle do'
|
|
135
|
+
echo " root_url \"https://github.com/${REPOSITORY}/releases/download/${RESOLVED_TAG:-$INPUT_TAG}\""
|
|
136
|
+
for f in *.bottle.tar.gz; do
|
|
137
|
+
tag=$(echo "$f" | sed -E 's/.*\.(arm64_[a-z]+)\.bottle\.tar\.gz/\1/')
|
|
138
|
+
sum=$(shasum -a 256 "$f" | cut -d' ' -f1)
|
|
139
|
+
printf ' sha256 cellar: :any_skip_relocation, %s: "%s"\n' "$tag" "$sum"
|
|
140
|
+
done
|
|
141
|
+
echo ' end'
|
|
142
|
+
echo '```'
|
|
143
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
144
|
+
cat "$GITHUB_STEP_SUMMARY"
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
engine:
|
|
18
|
+
name: engine (py${{ matrix.python }})
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
strategy:
|
|
21
|
+
fail-fast: false
|
|
22
|
+
matrix:
|
|
23
|
+
python: ["3.11", "3.12", "3.13"]
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
26
|
+
|
|
27
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
28
|
+
with:
|
|
29
|
+
python-version: ${{ matrix.python }}
|
|
30
|
+
|
|
31
|
+
- name: Install
|
|
32
|
+
run: |
|
|
33
|
+
python -m pip install --upgrade pip
|
|
34
|
+
pip install -e ".[dev]"
|
|
35
|
+
|
|
36
|
+
- name: Lint
|
|
37
|
+
run: |
|
|
38
|
+
ruff check src tests
|
|
39
|
+
ruff format --check src tests
|
|
40
|
+
|
|
41
|
+
- name: Test
|
|
42
|
+
run: pytest -q
|
|
43
|
+
|
|
44
|
+
# Again at a different width. The report tables wrap to the terminal, so a
|
|
45
|
+
# test asserting on a sentence can pass at one width and fail at another -
|
|
46
|
+
# a real assertion was green locally at 120 and red here at 80, because 80
|
|
47
|
+
# was the only width anything ever ran at.
|
|
48
|
+
- name: Test (narrow and wide terminals)
|
|
49
|
+
run: |
|
|
50
|
+
COLUMNS=60 pytest -q
|
|
51
|
+
COLUMNS=200 pytest -q
|
|
52
|
+
|
|
53
|
+
scripts:
|
|
54
|
+
name: shell scripts lint
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
58
|
+
- name: shellcheck
|
|
59
|
+
# install.sh and uninstall.sh are the first thing a new user runs. They get
|
|
60
|
+
# the same scrutiny as the Python, because a broken installer is a worse
|
|
61
|
+
# first impression than a broken feature.
|
|
62
|
+
run: shellcheck -S warning install.sh uninstall.sh scripts/*.sh macos/make-app.sh
|
|
63
|
+
|
|
64
|
+
security:
|
|
65
|
+
name: security guarantees
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
69
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
70
|
+
with:
|
|
71
|
+
python-version: "3.12"
|
|
72
|
+
|
|
73
|
+
- name: Install
|
|
74
|
+
run: |
|
|
75
|
+
python -m pip install --upgrade pip
|
|
76
|
+
pip install -e ".[dev]"
|
|
77
|
+
|
|
78
|
+
# The suite runs with an autouse fixture that raises on any outbound
|
|
79
|
+
# socket, so this job also proves the engine makes no network calls.
|
|
80
|
+
- name: Guarantee suite (G1-G7)
|
|
81
|
+
run: pytest tests/test_security.py -v
|
|
82
|
+
|
|
83
|
+
# Belt and braces on top of the in-test canary scan: build a database from
|
|
84
|
+
# the fixtures and grep the artefacts on disk.
|
|
85
|
+
- name: Canary scan of built artefacts
|
|
86
|
+
run: |
|
|
87
|
+
set -euo pipefail
|
|
88
|
+
export BURNOMETER_HOME="$RUNNER_TEMP/bom"
|
|
89
|
+
python - <<'PY'
|
|
90
|
+
import os, pathlib
|
|
91
|
+
from burnometer.adapters.base import LogSource
|
|
92
|
+
from burnometer.adapters.claude_code import ClaudeCodeAdapter
|
|
93
|
+
from burnometer.adapters.codex import CodexAdapter
|
|
94
|
+
from burnometer.scan import scan
|
|
95
|
+
from burnometer.snapshot import write_snapshot
|
|
96
|
+
from burnometer.store import Store
|
|
97
|
+
|
|
98
|
+
root = pathlib.Path("tests/fixtures")
|
|
99
|
+
class C(ClaudeCodeAdapter):
|
|
100
|
+
def sources(self): return [LogSource(root=root / "claude_code", glob="*.jsonl")]
|
|
101
|
+
class X(CodexAdapter):
|
|
102
|
+
def sources(self): return [LogSource(root=root / "codex", glob="*.jsonl")]
|
|
103
|
+
|
|
104
|
+
home = pathlib.Path(os.environ["BURNOMETER_HOME"])
|
|
105
|
+
with Store.open(home / "burn.db") as store:
|
|
106
|
+
scan(store, adapters=[C(), X()])
|
|
107
|
+
assert store.count_events() > 0, "fixtures produced no events"
|
|
108
|
+
write_snapshot(store)
|
|
109
|
+
PY
|
|
110
|
+
for f in "$BURNOMETER_HOME"/*; do
|
|
111
|
+
if grep -qa "CANARY" "$f"; then
|
|
112
|
+
echo "::error::content leaked into $(basename "$f")"; exit 1
|
|
113
|
+
fi
|
|
114
|
+
done
|
|
115
|
+
echo "no prompt content in any produced artefact"
|
|
116
|
+
|
|
117
|
+
- name: Dependency audit
|
|
118
|
+
run: |
|
|
119
|
+
set -euo pipefail
|
|
120
|
+
pip install pip-audit
|
|
121
|
+
# Audit the dependency tree, not the project itself. burn-o-meter is
|
|
122
|
+
# installed from local source and is not on PyPI, so pip-audit cannot
|
|
123
|
+
# look it up and --strict turns that into a failure. Excluding it keeps
|
|
124
|
+
# every real dependency strictly audited.
|
|
125
|
+
pip freeze --exclude-editable | grep -iv '^burn.o.meter' > /tmp/deps.txt
|
|
126
|
+
cat /tmp/deps.txt
|
|
127
|
+
pip-audit --strict --desc -r /tmp/deps.txt
|
|
128
|
+
|
|
129
|
+
package:
|
|
130
|
+
name: wheel installs clean
|
|
131
|
+
runs-on: ubuntu-latest
|
|
132
|
+
steps:
|
|
133
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
134
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
135
|
+
with:
|
|
136
|
+
python-version: "3.12"
|
|
137
|
+
|
|
138
|
+
- name: Build
|
|
139
|
+
run: |
|
|
140
|
+
python -m pip install --upgrade pip build
|
|
141
|
+
python -m build --wheel
|
|
142
|
+
|
|
143
|
+
# A wheel missing the vendored pricing data would silently report every
|
|
144
|
+
# model as unpriced, so its presence is checked rather than assumed.
|
|
145
|
+
- name: Data files are packaged
|
|
146
|
+
run: |
|
|
147
|
+
python - <<'PY'
|
|
148
|
+
import glob, zipfile, sys
|
|
149
|
+
names = zipfile.ZipFile(glob.glob("dist/*.whl")[0]).namelist()
|
|
150
|
+
missing = [f for f in ("snapshot.json", "overlay.toml", "schema.sql")
|
|
151
|
+
if not any(n.endswith(f) for n in names)]
|
|
152
|
+
sys.exit(f"missing from wheel: {missing}") if missing else print("all data files present")
|
|
153
|
+
PY
|
|
154
|
+
|
|
155
|
+
- name: Install into a clean environment and run
|
|
156
|
+
run: |
|
|
157
|
+
python -m venv /tmp/fresh
|
|
158
|
+
/tmp/fresh/bin/pip install --quiet dist/*.whl
|
|
159
|
+
/tmp/fresh/bin/burnometer --version
|
|
160
|
+
BURNOMETER_HOME=/tmp/bom-home /tmp/fresh/bin/burnometer doctor
|
|
161
|
+
|
|
162
|
+
app:
|
|
163
|
+
name: macOS app builds
|
|
164
|
+
runs-on: macos-14
|
|
165
|
+
steps:
|
|
166
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
167
|
+
- name: Build
|
|
168
|
+
run: swift build --package-path macos/burn-o-meter -c release
|
|
169
|
+
- name: Bundle
|
|
170
|
+
run: macos/make-app.sh
|
|
171
|
+
- name: Popover layout fits, and nothing is clipped
|
|
172
|
+
# A view that outgrows the popover compiles cleanly and passes every
|
|
173
|
+
# engine test - AppKit clips it at runtime instead of scrolling. This
|
|
174
|
+
# renders the real view against an oversized snapshot and measures it.
|
|
175
|
+
run: macos/build/burn-o-meter.app/Contents/MacOS/burn-o-meter --check-layout
|
|
176
|
+
|
|
177
|
+
- name: A stale quota reading is never presented as current
|
|
178
|
+
# The menu bar sat at an old percentage indefinitely: a quota's age was
|
|
179
|
+
# read from the payload's own age_seconds, which the writer computes once,
|
|
180
|
+
# so it never advanced and a four-hour-old reading still classified as
|
|
181
|
+
# current. Nothing on the poll timer regenerated the payload either.
|
|
182
|
+
run: macos/build/burn-o-meter.app/Contents/MacOS/burn-o-meter --check-freshness
|
|
183
|
+
|
|
184
|
+
- name: Bundle reports the real version
|
|
185
|
+
# A hardcoded fallback in make-app.sh shipped builds labelled 0.1.0 long
|
|
186
|
+
# after 0.3.0 was out. A version string that lies is worse than a missing
|
|
187
|
+
# one, because nobody thinks to doubt it.
|
|
188
|
+
run: |
|
|
189
|
+
want=$(sed -n 's/^__version__ = "\(.*\)"/\1/p' src/burnometer/__init__.py)
|
|
190
|
+
got=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" \
|
|
191
|
+
macos/build/burn-o-meter.app/Contents/Info.plist)
|
|
192
|
+
[ "$want" = "$got" ] || { echo "bundle says $got, package is $want" >&2; exit 1; }
|
|
193
|
+
echo "bundle version matches package: $got"
|
|
194
|
+
|
|
195
|
+
- name: Bundle is a status-bar app
|
|
196
|
+
run: |
|
|
197
|
+
plist=macos/build/burn-o-meter.app/Contents/Info.plist
|
|
198
|
+
/usr/libexec/PlistBuddy -c "Print :LSUIElement" "$plist" | grep -q true
|
|
199
|
+
echo "LSUIElement set: no Dock icon"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: CodeQL
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
schedule:
|
|
9
|
+
# Weekly, so a newly-published advisory is caught even without a commit.
|
|
10
|
+
- cron: "27 4 * * 1"
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
security-events: write
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
analyze:
|
|
18
|
+
name: analyze python
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
# Code scanning needs GitHub Advanced Security, which private repositories
|
|
21
|
+
# do not get on the free plan — the upload fails with "Resource not
|
|
22
|
+
# accessible by integration". Skipping while private keeps the workflow
|
|
23
|
+
# ready for the moment the repository goes public, without a permanently red
|
|
24
|
+
# check that trains everyone to ignore failures.
|
|
25
|
+
if: github.event.repository.private == false
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
28
|
+
# init and analyze must move together. They are separate pins, so Dependabot
|
|
29
|
+
# bumps them in separate PRs - and each fails alone, because analyze refuses a
|
|
30
|
+
# configuration written by a different version of init ("Loaded a
|
|
31
|
+
# configuration file for version '3.38.0', but running version '4.38.0'").
|
|
32
|
+
# v3 is deprecated in December 2026.
|
|
33
|
+
- uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
|
34
|
+
with:
|
|
35
|
+
languages: python
|
|
36
|
+
queries: security-and-quality
|
|
37
|
+
- uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Publishes a GitHub release's own sdist and wheel to PyPI.
|
|
2
|
+
#
|
|
3
|
+
# Trusted Publishing: PyPI trusts this workflow, in this repository and
|
|
4
|
+
# environment, through a short-lived OIDC token. No API token exists anywhere to
|
|
5
|
+
# be leaked or rotated.
|
|
6
|
+
#
|
|
7
|
+
# The files uploaded are the ones already attached to the release, not a rebuild,
|
|
8
|
+
# so PyPI and the release serve byte-identical artifacts. Run it after the release
|
|
9
|
+
# exists (RELEASING.md step 8). Like a GitHub release, a PyPI version is immutable:
|
|
10
|
+
# a file, once uploaded, can never be replaced.
|
|
11
|
+
name: PyPI
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
inputs:
|
|
16
|
+
tag:
|
|
17
|
+
description: "Release tag to publish, e.g. v0.6.5 (the release must exist)"
|
|
18
|
+
required: true
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
publish:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
environment:
|
|
27
|
+
name: pypi
|
|
28
|
+
url: https://pypi.org/project/burn-o-meter/
|
|
29
|
+
permissions:
|
|
30
|
+
contents: read
|
|
31
|
+
id-token: write # the OIDC token Trusted Publishing exchanges; nothing else
|
|
32
|
+
steps:
|
|
33
|
+
- name: Download the release's sdist and wheel
|
|
34
|
+
env:
|
|
35
|
+
GH_TOKEN: ${{ github.token }}
|
|
36
|
+
# Through the environment, never interpolated into the script: an
|
|
37
|
+
# expression is expanded before the shell sees the text.
|
|
38
|
+
TAG: ${{ inputs.tag }}
|
|
39
|
+
run: |
|
|
40
|
+
[[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "not a release tag: $TAG"; exit 1; }
|
|
41
|
+
version="${TAG#v}"
|
|
42
|
+
mkdir dist
|
|
43
|
+
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir dist \
|
|
44
|
+
--pattern "burn_o_meter-$version.tar.gz" \
|
|
45
|
+
--pattern "burn_o_meter-$version-py3-none-any.whl"
|
|
46
|
+
# Exactly the two files for this version, and nothing else.
|
|
47
|
+
test -f "dist/burn_o_meter-$version.tar.gz"
|
|
48
|
+
test -f "dist/burn_o_meter-$version-py3-none-any.whl"
|
|
49
|
+
test "$(find dist -type f | wc -l)" -eq 2
|
|
50
|
+
|
|
51
|
+
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Local-only working notes — NEVER commit. Contains machine-specific paths
|
|
2
|
+
# and session context, not project documentation.
|
|
3
|
+
PROGRESS.md
|
|
4
|
+
.scratch/
|
|
5
|
+
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
.venv/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
*.egg-info/
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
*.db
|
|
15
|
+
# ...but test fixtures are source, not user data. Without this the OpenCode
|
|
16
|
+
# fixture is silently untracked: tests pass on the machine that generated it and
|
|
17
|
+
# fail on CI, which is the worst possible split.
|
|
18
|
+
!tests/fixtures/**/*.db
|
|
19
|
+
*.db-wal
|
|
20
|
+
*.db-shm
|
|
21
|
+
.DS_Store
|
|
22
|
+
macos/**/xcuserdata/
|
|
23
|
+
macos/**/.build/
|
|
24
|
+
macos/build/
|
|
25
|
+
*.app/
|
|
26
|
+
|
|
27
|
+
# Generated from macos/make-icon.swift — the .icns is committed, the staging
|
|
28
|
+
# directory it is built from is not.
|
|
29
|
+
macos/burn-o-meter.iconset/
|