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.
Files changed (111) hide show
  1. burn_o_meter-0.6.5/.claude/RESUME.md +23 -0
  2. burn_o_meter-0.6.5/.github/ISSUE_TEMPLATE/bug_report.yml +56 -0
  3. burn_o_meter-0.6.5/.github/ISSUE_TEMPLATE/config.yml +8 -0
  4. burn_o_meter-0.6.5/.github/ISSUE_TEMPLATE/provider_request.yml +36 -0
  5. burn_o_meter-0.6.5/.github/dependabot.yml +23 -0
  6. burn_o_meter-0.6.5/.github/pull_request_template.md +29 -0
  7. burn_o_meter-0.6.5/.github/workflows/bottles.yml +144 -0
  8. burn_o_meter-0.6.5/.github/workflows/ci.yml +199 -0
  9. burn_o_meter-0.6.5/.github/workflows/codeql.yml +37 -0
  10. burn_o_meter-0.6.5/.github/workflows/pypi.yml +51 -0
  11. burn_o_meter-0.6.5/.gitignore +29 -0
  12. burn_o_meter-0.6.5/CHANGELOG.md +519 -0
  13. burn_o_meter-0.6.5/CODE_OF_CONDUCT.md +50 -0
  14. burn_o_meter-0.6.5/CONTRIBUTING.md +179 -0
  15. burn_o_meter-0.6.5/Formula/burn-o-meter.rb +92 -0
  16. burn_o_meter-0.6.5/LICENSE +21 -0
  17. burn_o_meter-0.6.5/NOTICE +11 -0
  18. burn_o_meter-0.6.5/PKG-INFO +85 -0
  19. burn_o_meter-0.6.5/README.md +323 -0
  20. burn_o_meter-0.6.5/RELEASING.md +79 -0
  21. burn_o_meter-0.6.5/ROADMAP.md +197 -0
  22. burn_o_meter-0.6.5/SECURITY.md +350 -0
  23. burn_o_meter-0.6.5/docs/adding-an-agent.md +67 -0
  24. burn_o_meter-0.6.5/docs/configuration.md +54 -0
  25. burn_o_meter-0.6.5/docs/demo.gif +0 -0
  26. burn_o_meter-0.6.5/docs/design-budgets-and-alerts.md +100 -0
  27. burn_o_meter-0.6.5/docs/faq.md +165 -0
  28. burn_o_meter-0.6.5/docs/install.md +231 -0
  29. burn_o_meter-0.6.5/docs/org-avatar.png +0 -0
  30. burn_o_meter-0.6.5/docs/pypi.md +52 -0
  31. burn_o_meter-0.6.5/docs/social-preview.jpg +0 -0
  32. burn_o_meter-0.6.5/docs/social-preview.png +0 -0
  33. burn_o_meter-0.6.5/install.sh +129 -0
  34. burn_o_meter-0.6.5/macos/burn-o-meter/Package.swift +19 -0
  35. burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/ContentView.swift +603 -0
  36. burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/Engine.swift +120 -0
  37. burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/FreshnessCheck.swift +159 -0
  38. burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/LayoutCheck.swift +262 -0
  39. burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/MenuBarIcon.swift +288 -0
  40. burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/MenuBarTitle.swift +52 -0
  41. burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/Models.swift +399 -0
  42. burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/Preferences.swift +150 -0
  43. burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/SnapshotFile.swift +166 -0
  44. burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/Theme.swift +547 -0
  45. burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/Updates.swift +194 -0
  46. burn_o_meter-0.6.5/macos/burn-o-meter/Sources/burn-o-meter/main.swift +759 -0
  47. burn_o_meter-0.6.5/macos/burn-o-meter.icns +0 -0
  48. burn_o_meter-0.6.5/macos/make-app.sh +130 -0
  49. burn_o_meter-0.6.5/macos/make-icon.swift +158 -0
  50. burn_o_meter-0.6.5/macos/make-social-preview.swift +103 -0
  51. burn_o_meter-0.6.5/pyproject.toml +65 -0
  52. burn_o_meter-0.6.5/scripts/setup-repo.sh +98 -0
  53. burn_o_meter-0.6.5/scripts/smoke-test.sh +129 -0
  54. burn_o_meter-0.6.5/src/burnometer/__init__.py +3 -0
  55. burn_o_meter-0.6.5/src/burnometer/__main__.py +6 -0
  56. burn_o_meter-0.6.5/src/burnometer/adapters/__init__.py +34 -0
  57. burn_o_meter-0.6.5/src/burnometer/adapters/base.py +182 -0
  58. burn_o_meter-0.6.5/src/burnometer/adapters/claude_code.py +255 -0
  59. burn_o_meter-0.6.5/src/burnometer/adapters/claude_desktop.py +242 -0
  60. burn_o_meter-0.6.5/src/burnometer/adapters/claude_statusline.py +109 -0
  61. burn_o_meter-0.6.5/src/burnometer/adapters/codex.py +362 -0
  62. burn_o_meter-0.6.5/src/burnometer/adapters/kimi.py +333 -0
  63. burn_o_meter-0.6.5/src/burnometer/adapters/opencode.py +275 -0
  64. burn_o_meter-0.6.5/src/burnometer/agent.py +211 -0
  65. burn_o_meter-0.6.5/src/burnometer/analytics.py +648 -0
  66. burn_o_meter-0.6.5/src/burnometer/cli.py +867 -0
  67. burn_o_meter-0.6.5/src/burnometer/config.py +197 -0
  68. burn_o_meter-0.6.5/src/burnometer/models.py +251 -0
  69. burn_o_meter-0.6.5/src/burnometer/pricing/__init__.py +6 -0
  70. burn_o_meter-0.6.5/src/burnometer/pricing/calculator.py +198 -0
  71. burn_o_meter-0.6.5/src/burnometer/pricing/catalog.py +574 -0
  72. burn_o_meter-0.6.5/src/burnometer/pricing/overlay.toml +133 -0
  73. burn_o_meter-0.6.5/src/burnometer/pricing/snapshot.json +2427 -0
  74. burn_o_meter-0.6.5/src/burnometer/report.py +221 -0
  75. burn_o_meter-0.6.5/src/burnometer/safety.py +553 -0
  76. burn_o_meter-0.6.5/src/burnometer/scan.py +311 -0
  77. burn_o_meter-0.6.5/src/burnometer/snapshot.py +379 -0
  78. burn_o_meter-0.6.5/src/burnometer/statusline.py +387 -0
  79. burn_o_meter-0.6.5/src/burnometer/store/__init__.py +5 -0
  80. burn_o_meter-0.6.5/src/burnometer/store/db.py +529 -0
  81. burn_o_meter-0.6.5/src/burnometer/store/schema.sql +99 -0
  82. burn_o_meter-0.6.5/tests/conftest.py +150 -0
  83. burn_o_meter-0.6.5/tests/fixtures/claude_code/README.md +14 -0
  84. burn_o_meter-0.6.5/tests/fixtures/claude_code/basic.jsonl +7 -0
  85. burn_o_meter-0.6.5/tests/fixtures/claude_code/edge.jsonl +7 -0
  86. burn_o_meter-0.6.5/tests/fixtures/claude_desktop/plan-usage-history.json +78 -0
  87. burn_o_meter-0.6.5/tests/fixtures/codex/edge.jsonl +6 -0
  88. burn_o_meter-0.6.5/tests/fixtures/codex/local.jsonl +8 -0
  89. burn_o_meter-0.6.5/tests/fixtures/codex/reset.jsonl +6 -0
  90. burn_o_meter-0.6.5/tests/fixtures/codex/session.jsonl +8 -0
  91. burn_o_meter-0.6.5/tests/fixtures/kimi/session_index.jsonl +2 -0
  92. 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
  93. 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
  94. burn_o_meter-0.6.5/tests/fixtures/opencode/opencode.db +0 -0
  95. burn_o_meter-0.6.5/tests/test_adapter_claude_code.py +277 -0
  96. burn_o_meter-0.6.5/tests/test_adapter_claude_desktop.py +231 -0
  97. burn_o_meter-0.6.5/tests/test_adapter_codex.py +252 -0
  98. burn_o_meter-0.6.5/tests/test_adapter_kimi.py +225 -0
  99. burn_o_meter-0.6.5/tests/test_adapter_opencode.py +193 -0
  100. burn_o_meter-0.6.5/tests/test_agent.py +92 -0
  101. burn_o_meter-0.6.5/tests/test_analytics.py +277 -0
  102. burn_o_meter-0.6.5/tests/test_cli.py +569 -0
  103. burn_o_meter-0.6.5/tests/test_config.py +136 -0
  104. burn_o_meter-0.6.5/tests/test_pricing.py +929 -0
  105. burn_o_meter-0.6.5/tests/test_scan.py +316 -0
  106. burn_o_meter-0.6.5/tests/test_security.py +698 -0
  107. burn_o_meter-0.6.5/tests/test_series_and_cache.py +232 -0
  108. burn_o_meter-0.6.5/tests/test_snapshot.py +266 -0
  109. burn_o_meter-0.6.5/tests/test_statusline.py +364 -0
  110. burn_o_meter-0.6.5/tests/test_store.py +337 -0
  111. 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/