@juanibiapina/pi-powerbar 0.11.1 → 0.12.0

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 (30) hide show
  1. package/node_modules/@juanibiapina/pi-usage/.github/workflows/dependabot-auto-merge.yml +129 -0
  2. package/node_modules/@juanibiapina/pi-usage/.github/workflows/publish.yml +25 -0
  3. package/node_modules/@juanibiapina/pi-usage/.local/share/pi/socket.info.json +8 -0
  4. package/node_modules/@juanibiapina/pi-usage/AGENTS.md +10 -0
  5. package/node_modules/@juanibiapina/pi-usage/CHANGELOG.md +10 -0
  6. package/node_modules/@juanibiapina/pi-usage/README.md +39 -0
  7. package/node_modules/@juanibiapina/pi-usage/biome.json +27 -0
  8. package/node_modules/@juanibiapina/pi-usage/docs/releases.md +40 -0
  9. package/node_modules/@juanibiapina/pi-usage/index.ts +150 -0
  10. package/node_modules/@juanibiapina/pi-usage/package.json +37 -0
  11. package/node_modules/@juanibiapina/pi-usage/src/cache.ts +240 -0
  12. package/node_modules/@juanibiapina/pi-usage/src/dependencies.ts +34 -0
  13. package/node_modules/@juanibiapina/pi-usage/src/detection.ts +61 -0
  14. package/node_modules/@juanibiapina/pi-usage/src/errors.ts +38 -0
  15. package/node_modules/@juanibiapina/pi-usage/src/provider.ts +34 -0
  16. package/node_modules/@juanibiapina/pi-usage/src/providers/anthropic.ts +146 -0
  17. package/node_modules/@juanibiapina/pi-usage/src/providers/antigravity.ts +207 -0
  18. package/node_modules/@juanibiapina/pi-usage/src/providers/codex.ts +137 -0
  19. package/node_modules/@juanibiapina/pi-usage/src/providers/copilot.ts +167 -0
  20. package/node_modules/@juanibiapina/pi-usage/src/providers/gemini.ts +119 -0
  21. package/node_modules/@juanibiapina/pi-usage/src/providers/kiro.ts +89 -0
  22. package/node_modules/@juanibiapina/pi-usage/src/providers/zai.ts +116 -0
  23. package/node_modules/@juanibiapina/pi-usage/src/registry.ts +32 -0
  24. package/node_modules/@juanibiapina/pi-usage/src/types.ts +72 -0
  25. package/node_modules/@juanibiapina/pi-usage/src/utils.ts +70 -0
  26. package/node_modules/@juanibiapina/pi-usage/test/detection.test.ts +76 -0
  27. package/node_modules/@juanibiapina/pi-usage/test/extension.test.ts +138 -0
  28. package/node_modules/@juanibiapina/pi-usage/tsconfig.json +17 -0
  29. package/package.json +8 -4
  30. package/src/powerbar-sub/index.ts +35 -18
@@ -0,0 +1,129 @@
1
+ name: Dependabot auto-merge
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ pull_request_target:
6
+ types:
7
+ - opened
8
+ - reopened
9
+ - synchronize
10
+ - ready_for_review
11
+ check_run:
12
+ types:
13
+ - completed
14
+ check_suite:
15
+ types:
16
+ - completed
17
+ status:
18
+
19
+ permissions:
20
+ contents: write
21
+ pull-requests: write
22
+ checks: read
23
+ statuses: read
24
+
25
+ jobs:
26
+ merge:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - name: Merge Dependabot PRs when checks pass
30
+ uses: actions/github-script@v9
31
+ with:
32
+ github-token: ${{ github.token }}
33
+ script: |
34
+ const owner = context.repo.owner
35
+ const repo = context.repo.repo
36
+ const okConclusions = new Set(['success', 'neutral', 'skipped'])
37
+
38
+ async function prsForSha(ref) {
39
+ const res = await github.request('GET /repos/{owner}/{repo}/commits/{ref}/pulls', {
40
+ owner,
41
+ repo,
42
+ ref,
43
+ mediaType: { previews: ['groot'] },
44
+ })
45
+ return res.data
46
+ }
47
+
48
+ async function listCandidatePRs() {
49
+ if (context.eventName === 'workflow_dispatch') {
50
+ const res = await github.rest.pulls.list({ owner, repo, state: 'open', per_page: 100 })
51
+ return res.data.filter((pr) => pr.user?.login === 'dependabot[bot]')
52
+ }
53
+
54
+ if (context.eventName === 'pull_request_target') {
55
+ const pr = context.payload.pull_request
56
+ if (pr?.state === 'open' && pr.user?.login === 'dependabot[bot]') {
57
+ return [pr]
58
+ }
59
+ return []
60
+ }
61
+
62
+ const ref = context.payload.check_run?.head_sha || context.payload.check_suite?.head_sha || context.payload.sha
63
+ if (!ref) {
64
+ return []
65
+ }
66
+
67
+ const prs = await prsForSha(ref)
68
+ return prs.filter((pr) => pr.state === 'open' && pr.user?.login === 'dependabot[bot]')
69
+ }
70
+
71
+ async function checksPassed(ref) {
72
+ const [statusRes, checkRunsRes] = await Promise.all([
73
+ github.rest.repos.getCombinedStatusForRef({ owner, repo, ref }),
74
+ github.rest.checks.listForRef({ owner, repo, ref, per_page: 100 }),
75
+ ])
76
+
77
+ const statuses = statusRes.data.statuses || []
78
+ const checkRuns = checkRunsRes.data.check_runs || []
79
+
80
+ if (statuses.some((status) => status.state !== 'success')) {
81
+ return false
82
+ }
83
+
84
+ for (const run of checkRuns) {
85
+ if (run.status !== 'completed') {
86
+ return false
87
+ }
88
+ if (!okConclusions.has(run.conclusion)) {
89
+ return false
90
+ }
91
+ }
92
+
93
+ return true
94
+ }
95
+
96
+ const seen = new Set()
97
+ const candidates = await listCandidatePRs()
98
+
99
+ for (const candidate of candidates) {
100
+ if (seen.has(candidate.number)) {
101
+ continue
102
+ }
103
+ seen.add(candidate.number)
104
+
105
+ const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: candidate.number })
106
+
107
+ if (pr.state !== 'open' || pr.user?.login !== 'dependabot[bot]' || pr.draft) {
108
+ core.info(`Skipping #${pr.number}`)
109
+ continue
110
+ }
111
+
112
+ const ready = await checksPassed(pr.head.sha)
113
+ if (!ready) {
114
+ core.info(`Checks not green for #${pr.number}`)
115
+ continue
116
+ }
117
+
118
+ try {
119
+ await github.rest.pulls.merge({
120
+ owner,
121
+ repo,
122
+ pull_number: pr.number,
123
+ merge_method: 'squash',
124
+ })
125
+ core.info(`Merged #${pr.number}`)
126
+ } catch (error) {
127
+ core.warning(`Could not merge #${pr.number}: ${error.message}`)
128
+ }
129
+ }
@@ -0,0 +1,25 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ id-token: write
10
+ contents: read
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v6
17
+
18
+ - uses: actions/setup-node@v6
19
+ with:
20
+ node-version: '24'
21
+ registry-url: 'https://registry.npmjs.org'
22
+
23
+ - run: npm ci
24
+
25
+ - run: npm publish --access public
@@ -0,0 +1,8 @@
1
+ {
2
+ "protocolVersion": 1,
3
+ "cwd": "/Users/juan.ibiapina/workspace/juanibiapina/pi-usage",
4
+ "pid": 25431,
5
+ "socketPath": "/Users/juan.ibiapina/workspace/juanibiapina/pi-usage/.local/share/pi/socket",
6
+ "startedAt": "2026-05-20T16:47:45.523Z",
7
+ "sessionFile": "/Users/juan.ibiapina/.pi/agent/sessions/--Users-juan.ibiapina-workspace-juanibiapina-pi-usage--/2026-05-20T16-47-43-822Z_019e4649-36ce-715f-ab4f-af547f13287f.jsonl"
8
+ }
@@ -0,0 +1,10 @@
1
+ # Project Notes
2
+
3
+ ## Build and Check
4
+
5
+ - Run check with `npm run check` (biome lint + typecheck)
6
+ - Run tests with `npm test`
7
+
8
+ ## Release Process
9
+
10
+ - See [docs/releases.md](docs/releases.md) for the release process
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-05-20
4
+
5
+ ### Added
6
+
7
+ - Initial release: simplified fork of [@marckrenn/pi-sub-core](https://github.com/marckrenn/pi-sub-core)
8
+ - Fetches Anthropic subscription usage data and renders it in the Pi status bar
9
+ - Auto-detects LLM provider from model metadata
10
+ - Configurable refresh interval via `PI_USAGE_REFRESH_MINUTES` environment variable
@@ -0,0 +1,39 @@
1
+ # pi-usage
2
+
3
+ Pi extension that fetches subscription usage for all supported providers (Anthropic, Copilot, Gemini, Antigravity, Codex, Kiro, z.ai).
4
+
5
+ Simplified fork of the excellent [@marckrenn/pi-sub-core](https://github.com/marckrenn/pi-sub). Keeps all providers, applies two bug fixes, drops features we don't need.
6
+
7
+ ## Bug fixes
8
+
9
+ ### Bedrock false positive detection
10
+
11
+ pi-sub-core's `detectProviderFromModel` falls back to matching model tokens (e.g. `"claude"`) when provider tokens don't match. This causes AWS Bedrock models (which run Claude but are billed separately) to be misidentified as Anthropic subscription usage. pi-usage only falls back to model tokens when no explicit provider is set.
12
+
13
+ ### Aggressive refresh causing 429 flicker
14
+
15
+ pi-sub-core uses `force: true` on `turn_end` and `tool_result`, bypassing the cache TTL and hammering the usage APIs. Under load (multiple pi instances), this triggers 429s that cause the usage display to flicker. pi-usage always respects the cache TTL. See [marckrenn/pi-sub#58](https://github.com/marckrenn/pi-sub/issues/58).
16
+
17
+ ## Simplifications vs pi-sub-core
18
+
19
+ - No status page fetching
20
+ - No settings UI or settings persistence
21
+ - No tool registration
22
+ - No `update-all` event (only `update-current`)
23
+ - No provider cycle command
24
+ - Self-contained types — no dependency on `@marckrenn/pi-sub-shared`
25
+
26
+ ## Events
27
+
28
+ | Event | Payload | Description |
29
+ |-------|---------|-------------|
30
+ | `usage-core:ready` | `{ state: UsageCoreState }` | Emitted once on session start |
31
+ | `usage-core:update-current` | `{ state: UsageCoreState }` | Emitted on usage changes |
32
+
33
+ `UsageCoreState` has an optional `provider` name and optional `usage` snapshot with rate windows. When `provider` is undefined, no known subscription provider was detected for the current model.
34
+
35
+ ## Install
36
+
37
+ ```
38
+ pi install npm:@juanibiapina/pi-usage
39
+ ```
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
3
+ "linter": {
4
+ "enabled": true,
5
+ "rules": {
6
+ "recommended": true,
7
+ "style": {
8
+ "noNonNullAssertion": "off",
9
+ "useConst": "error",
10
+ "useNodejsImportProtocol": "off"
11
+ },
12
+ "suspicious": {
13
+ "noExplicitAny": "off"
14
+ }
15
+ }
16
+ },
17
+ "formatter": {
18
+ "enabled": true,
19
+ "formatWithErrors": false,
20
+ "indentStyle": "tab",
21
+ "indentWidth": 3,
22
+ "lineWidth": 120
23
+ },
24
+ "files": {
25
+ "includes": ["index.ts", "src/**/*.ts", "test/**/*.ts", "!**/node_modules/**/*"]
26
+ }
27
+ }
@@ -0,0 +1,40 @@
1
+ # Releases
2
+
3
+ ## Creating a Release
4
+
5
+ Releases are automated via GitHub Actions and triggered by git tags. The workflow uses npm OIDC trusted publishing (no tokens required).
6
+
7
+ 1. Update `CHANGELOG.md`: move `[Unreleased]` entries to new version section with date
8
+ 2. Bump version in `package.json`:
9
+
10
+ ```bash
11
+ npm version patch # or minor, or major
12
+ ```
13
+
14
+ 3. Push with tags:
15
+
16
+ ```bash
17
+ git push origin main --tags
18
+ ```
19
+
20
+ The workflow automatically:
21
+ - Installs dependencies (`npm ci`)
22
+ - Publishes TypeScript source to npm (`npm publish --access public`)
23
+
24
+ After pushing the tag, the coding agent should:
25
+ 1. Wait for the GitHub Actions workflow to complete: `gh run watch`
26
+ 2. Verify the publish: `npm view @juanibiapina/pi-usage`
27
+
28
+ ## Prerequisites
29
+
30
+ OIDC trusted publishing must be configured on [npmjs.com](https://www.npmjs.com):
31
+ - Package settings → Trusted Publishers
32
+ - Repository: `juanibiapina/pi-usage`
33
+ - Workflow: `publish.yml`
34
+
35
+ ## Version Format
36
+
37
+ Follow [semantic versioning](https://semver.org/):
38
+
39
+ - **Production**: `v1.2.3`
40
+ - **Pre-release**: `v1.0.0-beta.1`, `v1.0.0-rc.1`, `v1.0.0-alpha.1`
@@ -0,0 +1,150 @@
1
+ /**
2
+ * pi-usage — Subscription usage extension for all providers.
3
+ *
4
+ * Simplified fork of @marckrenn/pi-sub-core (https://github.com/marckrenn/pi-sub).
5
+ * Supports all providers (anthropic, copilot, gemini, antigravity, codex, kiro, zai)
6
+ * with two bug fixes:
7
+ *
8
+ * 1. Bedrock false positive: detection no longer falls back to model tokens when
9
+ * the provider field is explicitly set and doesn't match any known provider.
10
+ *
11
+ * 2. Aggressive refresh on turn_end/tool_result: always respects cache TTL instead
12
+ * of bypassing it with force:true (see https://github.com/marckrenn/pi-sub/issues/58).
13
+ *
14
+ * Emits:
15
+ * - "usage-core:ready" → { state: UsageCoreState }
16
+ * - "usage-core:update-current" → { state: UsageCoreState }
17
+ */
18
+
19
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
20
+ import { fetchWithCache, getGoodUsage, watchCache } from "./src/cache.js";
21
+ import { createDefaultDependencies } from "./src/dependencies.js";
22
+ import { detectProvider } from "./src/detection.js";
23
+ import { createProvider, hasCredentials } from "./src/registry.js";
24
+ import type { Dependencies, ProviderName, UsageCoreState, UsageSnapshot } from "./src/types.js";
25
+
26
+ const REFRESH_INTERVAL_MS = 60_000;
27
+
28
+ type GlobalGuard = { active: boolean };
29
+ const global = globalThis as typeof globalThis & { __piUsage?: GlobalGuard };
30
+
31
+ export default function createExtension(pi: ExtensionAPI, deps?: Dependencies): void {
32
+ const resolvedDeps = deps ?? createDefaultDependencies();
33
+ // Prevent double-init when bundled alongside other extensions.
34
+ // Skip the guard when deps are explicitly provided (test mode).
35
+ if (!deps && global.__piUsage?.active) return;
36
+ if (!deps) global.__piUsage = { active: true };
37
+
38
+ let lastContext: ExtensionContext | undefined;
39
+ let lastState: UsageCoreState = {};
40
+ let lastSnapshot = "";
41
+ let currentProvider: ProviderName | undefined;
42
+ let stopCacheWatch: (() => void) | undefined;
43
+
44
+ // --- Emit helpers ---
45
+
46
+ function emitState(state: UsageCoreState): void {
47
+ const json = JSON.stringify(state);
48
+ if (json === lastSnapshot) return;
49
+ lastSnapshot = json;
50
+ lastState = state;
51
+ pi.events.emit("usage-core:update-current", { state });
52
+ }
53
+
54
+ function setupCacheWatch(provider: ProviderName): void {
55
+ stopCacheWatch?.();
56
+ stopCacheWatch = watchCache(provider, (usage: UsageSnapshot) => {
57
+ if (currentProvider === provider) {
58
+ emitState({ provider, usage });
59
+ }
60
+ });
61
+ }
62
+
63
+ // --- Refresh ---
64
+
65
+ async function refresh(ctx: ExtensionContext, force = false): Promise<void> {
66
+ lastContext = ctx;
67
+
68
+ const detected = detectProvider(ctx.model);
69
+ if (!detected) {
70
+ currentProvider = undefined;
71
+ emitState({});
72
+ return;
73
+ }
74
+
75
+ // Provider changed — reset.
76
+ if (detected !== currentProvider) {
77
+ currentProvider = detected;
78
+ setupCacheWatch(detected);
79
+ }
80
+
81
+ if (!hasCredentials(detected, resolvedDeps)) {
82
+ emitState({ provider: detected });
83
+ return;
84
+ }
85
+
86
+ // Check cache first (unless forced).
87
+ if (!force) {
88
+ const cached = getGoodUsage(detected, REFRESH_INTERVAL_MS);
89
+ if (cached) {
90
+ emitState({ provider: detected, usage: cached });
91
+ return;
92
+ }
93
+ }
94
+
95
+ const providerInstance = createProvider(detected);
96
+ const usage = await fetchWithCache(detected, REFRESH_INTERVAL_MS, () =>
97
+ providerInstance.fetchUsage(resolvedDeps),
98
+ );
99
+
100
+ // Only emit when we got good data. On errors (429 etc.),
101
+ // fetchWithCache writes a backoff file so all instances wait.
102
+ // UI keeps showing last good state.
103
+ if (usage) {
104
+ emitState({ provider: detected, usage });
105
+ }
106
+ }
107
+
108
+ // --- Periodic refresh ---
109
+
110
+ const refreshTimer = setInterval(() => {
111
+ if (!lastContext || !currentProvider) return;
112
+ void refresh(lastContext);
113
+ }, REFRESH_INTERVAL_MS);
114
+ refreshTimer.unref?.();
115
+
116
+ // --- Lifecycle ---
117
+
118
+ pi.on("session_start", async (_event, ctx) => {
119
+ lastContext = ctx;
120
+ await refresh(ctx);
121
+ pi.events.emit("usage-core:ready", { state: lastState });
122
+ });
123
+
124
+ pi.on("model_select" as any, async (_event: unknown, ctx: ExtensionContext) => {
125
+ // Model changed — force refresh.
126
+ await refresh(ctx, true);
127
+ });
128
+
129
+ pi.on("turn_start", async (_event, ctx) => {
130
+ // Respect TTL — emit cached, don't force.
131
+ lastContext = ctx;
132
+ });
133
+
134
+ pi.on("turn_end", async (_event, ctx) => {
135
+ // Respect TTL — this is the fix for pi-sub#58.
136
+ lastContext = ctx;
137
+ });
138
+
139
+ pi.on("session_switch" as any, async (_event: unknown, ctx: ExtensionContext) => {
140
+ currentProvider = undefined;
141
+ await refresh(ctx, true);
142
+ });
143
+
144
+ pi.on("session_shutdown", async () => {
145
+ clearInterval(refreshTimer);
146
+ stopCacheWatch?.();
147
+ lastContext = undefined;
148
+ if (!deps) global.__piUsage = undefined;
149
+ });
150
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@juanibiapina/pi-usage",
3
+ "version": "0.1.0",
4
+ "description": "Pi extension that fetches Anthropic subscription usage. Simplified fork of @marckrenn/pi-sub-core.",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package"
8
+ ],
9
+ "pi": {
10
+ "extensions": [
11
+ "./index.ts"
12
+ ]
13
+ },
14
+ "scripts": {
15
+ "test": "node --test --import tsx test/*.test.ts",
16
+ "check": "biome check --write --error-on-warnings . && tsc --noEmit"
17
+ },
18
+ "author": "Juan Ibiapina",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/juanibiapina/pi-usage"
23
+ },
24
+ "engines": {
25
+ "node": ">=20.0.0"
26
+ },
27
+ "peerDependencies": {
28
+ "@earendil-works/pi-coding-agent": "*"
29
+ },
30
+ "devDependencies": {
31
+ "@biomejs/biome": "2.4.15",
32
+ "@earendil-works/pi-coding-agent": "^0.75.3",
33
+ "@types/node": "^25.9.1",
34
+ "tsx": "^4.22.3",
35
+ "typescript": "^6.0.3"
36
+ }
37
+ }